libs/web: restore protected call to Template(), was accidentally removed in r4889
[project/luci.git] / libs / web / luasrc / dispatcher.lua
1 --[[
2 LuCI - Dispatcher
3
4 Description:
5 The request dispatcher and module dispatcher generators
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17         http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27 --- LuCI web dispatcher.
28 local fs = require "nixio.fs"
29 local sys = require "luci.sys"
30 local init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33 local nixio = require "nixio", require "nixio.util"
34
35 module("luci.dispatcher", package.seeall)
36 context = util.threadlocal()
37
38 authenticator = {}
39
40 -- Index table
41 local index = nil
42
43 -- Fastindex
44 local fi
45
46
47 --- Build the URL relative to the server webroot from given virtual path.
48 -- @param ...   Virtual path
49 -- @return              Relative URL
50 function build_url(...)
51         local path = {...}
52         local sn = http.getenv("SCRIPT_NAME") or ""
53         for k, v in pairs(context.urltoken) do
54                 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
55         end
56         return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
57 end
58
59 --- Send a 404 error code and render the "error404" template if available.
60 -- @param message       Custom error message (optional)
61 -- @return                      false
62 function error404(message)
63         luci.http.status(404, "Not Found")
64         message = message or "Not Found"
65
66         require("luci.template")
67         if not luci.util.copcall(luci.template.render, "error404") then
68                 luci.http.prepare_content("text/plain")
69                 luci.http.write(message)
70         end
71         return false
72 end
73
74 --- Send a 500 error code and render the "error500" template if available.
75 -- @param message       Custom error message (optional)#
76 -- @return                      false
77 function error500(message)
78         luci.util.perror(message)
79         if not context.template_header_sent then
80                 luci.http.status(500, "Internal Server Error")
81                 luci.http.prepare_content("text/plain")
82                 luci.http.write(message)
83         else
84                 require("luci.template")
85                 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
86                         luci.http.prepare_content("text/plain")
87                         luci.http.write(message)
88                 end
89         end
90         return false
91 end
92
93 function authenticator.htmlauth(validator, accs, default)
94         local user = luci.http.formvalue("username")
95         local pass = luci.http.formvalue("password")
96
97         if user and validator(user, pass) then
98                 return user
99         end
100
101         require("luci.i18n")
102         require("luci.template")
103         context.path = {}
104         luci.template.render("sysauth", {duser=default, fuser=user})
105         return false
106
107 end
108
109 --- Dispatch an HTTP request.
110 -- @param request       LuCI HTTP Request object
111 function httpdispatch(request, prefix)
112         luci.http.context.request = request
113
114         local r = {}
115         context.request = r
116         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
117
118         if prefix then
119                 for _, node in ipairs(prefix) do
120                         r[#r+1] = node
121                 end
122         end
123
124         for node in pathinfo:gmatch("[^/]+") do
125                 r[#r+1] = node
126         end
127
128         local stat, err = util.coxpcall(function()
129                 dispatch(context.request)
130         end, error500)
131
132         luci.http.close()
133
134         --context._disable_memtrace()
135 end
136
137 --- Dispatches a LuCI virtual path.
138 -- @param request       Virtual path
139 function dispatch(request)
140         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
141         local ctx = context
142         ctx.path = request
143         ctx.urltoken   = ctx.urltoken or {}
144
145         local conf = require "luci.config"
146         assert(conf.main,
147                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
148
149         local lang = conf.main.lang
150         if lang == "auto" then
151                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
152                 for lpat in aclang:gmatch("[%w-]+") do
153                         lpat = lpat and lpat:gsub("-", "_")
154                         if conf.languages[lpat] then
155                                 lang = lpat
156                                 break
157                         end
158                 end
159         end
160         require "luci.i18n".setlanguage(lang)
161
162         local c = ctx.tree
163         local stat
164         if not c then
165                 c = createtree()
166         end
167
168         local track = {}
169         local args = {}
170         ctx.args = args
171         ctx.requestargs = ctx.requestargs or args
172         local n
173         local t = true
174         local token = ctx.urltoken
175         local preq = {}
176         local freq = {}
177
178         for i, s in ipairs(request) do
179                 local tkey, tval
180                 if t then
181                         tkey, tval = s:match(";(%w+)=(.*)")
182                 end
183
184                 if tkey then
185                         token[tkey] = tval
186                 else
187                         t = false
188                         preq[#preq+1] = s
189                         freq[#freq+1] = s
190                         c = c.nodes[s]
191                         n = i
192                         if not c then
193                                 break
194                         end
195
196                         util.update(track, c)
197
198                         if c.leaf then
199                                 break
200                         end
201                 end
202         end
203
204         if c and c.leaf then
205                 for j=n+1, #request do
206                         args[#args+1] = request[j]
207                         freq[#freq+1] = request[j]
208                 end
209         end
210
211         ctx.requestpath = freq
212         ctx.path = preq
213
214         if track.i18n then
215                 require("luci.i18n").loadc(track.i18n)
216         end
217
218         -- Init template engine
219         if (c and c.index) or not track.notemplate then
220                 local tpl = require("luci.template")
221                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
222                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
223                         media = nil
224                         for name, theme in pairs(luci.config.themes) do
225                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
226                                  "themes/%s/header" % fs.basename(theme)) then
227                                         media = theme
228                                 end
229                         end
230                         assert(media, "No valid theme found")
231                 end
232
233                 tpl.context.viewns = setmetatable({
234                    write       = luci.http.write;
235                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
236                    translate   = function(...) return require("luci.i18n").translate(...) end;
237                    striptags   = util.striptags;
238                    media       = media;
239                    theme       = fs.basename(media);
240                    resource    = luci.config.main.resourcebase
241                 }, {__index=function(table, key)
242                         if key == "controller" then
243                                 return build_url()
244                         elseif key == "REQUEST_URI" then
245                                 return build_url(unpack(ctx.requestpath))
246                         else
247                                 return rawget(table, key) or _G[key]
248                         end
249                 end})
250         end
251
252         track.dependent = (track.dependent ~= false)
253         assert(not track.dependent or not track.auto, "Access Violation")
254
255         if track.sysauth then
256                 local sauth = require "luci.sauth"
257
258                 local authen = type(track.sysauth_authenticator) == "function"
259                  and track.sysauth_authenticator
260                  or authenticator[track.sysauth_authenticator]
261
262                 local def  = (type(track.sysauth) == "string") and track.sysauth
263                 local accs = def and {track.sysauth} or track.sysauth
264                 local sess = ctx.authsession
265                 local verifytoken = false
266                 if not sess then
267                         sess = luci.http.getcookie("sysauth")
268                         sess = sess and sess:match("^[a-f0-9]*$")
269                         verifytoken = true
270                 end
271
272                 local sdat = sauth.read(sess)
273                 local user
274
275                 if sdat then
276                         sdat = loadstring(sdat)
277                         setfenv(sdat, {})
278                         sdat = sdat()
279                         if not verifytoken or ctx.urltoken.stok == sdat.token then
280                                 user = sdat.user
281                         end
282                 else
283                         local eu = http.getenv("HTTP_AUTH_USER")
284                         local ep = http.getenv("HTTP_AUTH_PASS")
285                         if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
286                                 authen = function() return eu end
287                         end
288                 end
289
290                 if not util.contains(accs, user) then
291                         if authen then
292                                 ctx.urltoken.stok = nil
293                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
294                                 if not user or not util.contains(accs, user) then
295                                         return
296                                 else
297                                         local sid = sess or luci.sys.uniqueid(16)
298                                         if not sess then
299                                                 local token = luci.sys.uniqueid(16)
300                                                 sauth.write(sid, util.get_bytecode({
301                                                         user=user,
302                                                         token=token,
303                                                         secret=luci.sys.uniqueid(16)
304                                                 }))
305                                                 ctx.urltoken.stok = token
306                                         end
307                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
308                                         ctx.authsession = sid
309                                 end
310                         else
311                                 luci.http.status(403, "Forbidden")
312                                 return
313                         end
314                 else
315                         ctx.authsession = sess
316                 end
317         end
318
319         if track.setgroup then
320                 luci.sys.process.setgroup(track.setgroup)
321         end
322
323         if track.setuser then
324                 luci.sys.process.setuser(track.setuser)
325         end
326
327         local target = nil
328         if c then
329                 if type(c.target) == "function" then
330                         target = c.target
331                 elseif type(c.target) == "table" then
332                         target = c.target.target
333                 end
334         end
335
336         if c and (c.index or type(target) == "function") then
337                 ctx.dispatched = c
338                 ctx.requested = ctx.requested or ctx.dispatched
339         end
340
341         if c and c.index then
342                 local tpl = require "luci.template"
343
344                 if util.copcall(tpl.render, "indexer", {}) then
345                         return true
346                 end
347         end
348
349         if type(target) == "function" then
350                 util.copcall(function()
351                         local oldenv = getfenv(target)
352                         local module = require(c.module)
353                         local env = setmetatable({}, {__index=
354
355                         function(tbl, key)
356                                 return rawget(tbl, key) or module[key] or oldenv[key]
357                         end})
358
359                         setfenv(target, env)
360                 end)
361
362                 if type(c.target) == "table" then
363                         target(c.target, unpack(args))
364                 else
365                         target(unpack(args))
366                 end
367         else
368                 error404()
369         end
370 end
371
372 --- Generate the dispatching index using the best possible strategy.
373 function createindex()
374         local path = luci.util.libpath() .. "/controller/"
375         local suff = { ".lua", ".lua.gz" }
376
377         if luci.util.copcall(require, "luci.fastindex") then
378                 createindex_fastindex(path, suff)
379         else
380                 createindex_plain(path, suff)
381         end
382 end
383
384 --- Generate the dispatching index using the fastindex C-indexer.
385 -- @param path          Controller base directory
386 -- @param suffixes      Controller file suffixes
387 function createindex_fastindex(path, suffixes)
388         index = {}
389
390         if not fi then
391                 fi = luci.fastindex.new("index")
392                 for _, suffix in ipairs(suffixes) do
393                         fi.add(path .. "*" .. suffix)
394                         fi.add(path .. "*/*" .. suffix)
395                 end
396         end
397         fi.scan()
398
399         for k, v in pairs(fi.indexes) do
400                 index[v[2]] = v[1]
401         end
402 end
403
404 --- Generate the dispatching index using the native file-cache based strategy.
405 -- @param path          Controller base directory
406 -- @param suffixes      Controller file suffixes
407 function createindex_plain(path, suffixes)
408         local controllers = { }
409         for _, suffix in ipairs(suffixes) do
410                 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
411                 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
412         end
413
414         if indexcache then
415                 local cachedate = fs.stat(indexcache, "mtime")
416                 if cachedate then
417                         local realdate = 0
418                         for _, obj in ipairs(controllers) do
419                                 local omtime = fs.stat(path .. "/" .. obj, "mtime")
420                                 realdate = (omtime and omtime > realdate) and omtime or realdate
421                         end
422
423                         if cachedate > realdate then
424                                 assert(
425                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
426                                         and fs.stat(indexcache, "modestr") == "rw-------",
427                                         "Fatal: Indexcache is not sane!"
428                                 )
429
430                                 index = loadfile(indexcache)()
431                                 return index
432                         end
433                 end
434         end
435
436         index = {}
437
438         for i,c in ipairs(controllers) do
439                 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
440                 for _, suffix in ipairs(suffixes) do
441                         module = module:gsub(suffix.."$", "")
442                 end
443
444                 local mod = require(module)
445                 local idx = mod.index
446
447                 if type(idx) == "function" then
448                         index[module] = idx
449                 end
450         end
451
452         if indexcache then
453                 local f = nixio.open(indexcache, "w", 600)
454                 f:writeall(util.get_bytecode(index))
455                 f:close()
456         end
457 end
458
459 --- Create the dispatching tree from the index.
460 -- Build the index before if it does not exist yet.
461 function createtree()
462         if not index then
463                 createindex()
464         end
465
466         local ctx  = context
467         local tree = {nodes={}}
468         local modi = {}
469
470         ctx.treecache = setmetatable({}, {__mode="v"})
471         ctx.tree = tree
472         ctx.modifiers = modi
473
474         -- Load default translation
475         require "luci.i18n".loadc("default")
476
477         local scope = setmetatable({}, {__index = luci.dispatcher})
478
479         for k, v in pairs(index) do
480                 scope._NAME = k
481                 setfenv(v, scope)
482                 v()
483         end
484
485         local function modisort(a,b)
486                 return modi[a].order < modi[b].order
487         end
488
489         for _, v in util.spairs(modi, modisort) do
490                 scope._NAME = v.module
491                 setfenv(v.func, scope)
492                 v.func()
493         end
494
495         return tree
496 end
497
498 --- Register a tree modifier.
499 -- @param       func    Modifier function
500 -- @param       order   Modifier order value (optional)
501 function modifier(func, order)
502         context.modifiers[#context.modifiers+1] = {
503                 func = func,
504                 order = order or 0,
505                 module
506                         = getfenv(2)._NAME
507         }
508 end
509
510 --- Clone a node of the dispatching tree to another position.
511 -- @param       path    Virtual path destination
512 -- @param       clone   Virtual path source
513 -- @param       title   Destination node title (optional)
514 -- @param       order   Destination node order value (optional)
515 -- @return                      Dispatching tree node
516 function assign(path, clone, title, order)
517         local obj  = node(unpack(path))
518         obj.nodes  = nil
519         obj.module = nil
520
521         obj.title = title
522         obj.order = order
523
524         setmetatable(obj, {__index = _create_node(clone)})
525
526         return obj
527 end
528
529 --- Create a new dispatching node and define common parameters.
530 -- @param       path    Virtual path
531 -- @param       target  Target function to call when dispatched.
532 -- @param       title   Destination node title
533 -- @param       order   Destination node order value (optional)
534 -- @return                      Dispatching tree node
535 function entry(path, target, title, order)
536         local c = node(unpack(path))
537
538         c.target = target
539         c.title  = title
540         c.order  = order
541         c.module = getfenv(2)._NAME
542
543         return c
544 end
545
546 --- Fetch or create a dispatching node without setting the target module or
547 -- enabling the node.
548 -- @param       ...             Virtual path
549 -- @return                      Dispatching tree node
550 function get(...)
551         return _create_node({...})
552 end
553
554 --- Fetch or create a new dispatching node.
555 -- @param       ...             Virtual path
556 -- @return                      Dispatching tree node
557 function node(...)
558         local c = _create_node({...})
559
560         c.module = getfenv(2)._NAME
561         c.auto = nil
562
563         return c
564 end
565
566 function _create_node(path, cache)
567         if #path == 0 then
568                 return context.tree
569         end
570
571         cache = cache or context.treecache
572         local name = table.concat(path, ".")
573         local c = cache[name]
574
575         if not c then
576                 local new = {nodes={}, auto=true, path=util.clone(path)}
577                 local last = table.remove(path)
578
579                 c = _create_node(path, cache)
580
581                 c.nodes[last] = new
582                 cache[name] = new
583
584                 return new
585         else
586                 return c
587         end
588 end
589
590 -- Subdispatchers --
591
592 --- Create a redirect to another dispatching node.
593 -- @param       ...             Virtual path destination
594 function alias(...)
595         local req = {...}
596         return function(...)
597                 for _, r in ipairs({...}) do
598                         req[#req+1] = r
599                 end
600
601                 dispatch(req)
602         end
603 end
604
605 --- Rewrite the first x path values of the request.
606 -- @param       n               Number of path values to replace
607 -- @param       ...             Virtual path to replace removed path values with
608 function rewrite(n, ...)
609         local req = {...}
610         return function(...)
611                 local dispatched = util.clone(context.dispatched)
612
613                 for i=1,n do
614                         table.remove(dispatched, 1)
615                 end
616
617                 for i, r in ipairs(req) do
618                         table.insert(dispatched, i, r)
619                 end
620
621                 for _, r in ipairs({...}) do
622                         dispatched[#dispatched+1] = r
623                 end
624
625                 dispatch(dispatched)
626         end
627 end
628
629
630 local function _call(self, ...)
631         if #self.argv > 0 then
632                 return getfenv()[self.name](unpack(self.argv), ...)
633         else
634                 return getfenv()[self.name](...)
635         end
636 end
637
638 --- Create a function-call dispatching target.
639 -- @param       name    Target function of local controller
640 -- @param       ...             Additional parameters passed to the function
641 function call(name, ...)
642         return {type = "call", argv = {...}, name = name, target = _call}
643 end
644
645
646 local _template = function(self, ...)
647         require "luci.template".render(self.view)
648 end
649
650 --- Create a template render dispatching target.
651 -- @param       name    Template to be rendered
652 function template(name)
653         return {type = "template", view = name, target = _template}
654 end
655
656
657 local function _cbi(self, ...)
658         local cbi = require "luci.cbi"
659         local tpl = require "luci.template"
660         local http = require "luci.http"
661
662         local config = self.config or {}
663         local maps = cbi.load(self.model, ...)
664
665         local state = nil
666
667         for i, res in ipairs(maps) do
668                 res.flow = config
669                 local cstate = res:parse()
670                 if cstate and (not state or cstate < state) then
671                         state = cstate
672                 end
673         end
674
675         local function _resolve_path(path)
676                 return type(path) == "table" and build_url(unpack(path)) or path
677         end
678
679         if config.on_valid_to and state and state > 0 and state < 2 then
680                 http.redirect(_resolve_path(config.on_valid_to))
681                 return
682         end
683
684         if config.on_changed_to and state and state > 1 then
685                 http.redirect(_resolve_path(config.on_changed_to))
686                 return
687         end
688
689         if config.on_success_to and state and state > 0 then
690                 http.redirect(_resolve_path(config.on_success_to))
691                 return
692         end
693
694         if config.state_handler then
695                 if not config.state_handler(state, maps) then
696                         return
697                 end
698         end
699
700         local pageaction = true
701         http.header("X-CBI-State", state or 0)
702         if not config.noheader then
703                 tpl.render("cbi/header", {state = state})
704         end
705         for i, res in ipairs(maps) do
706                 res:render()
707                 if res.pageaction == false then
708                         pageaction = false
709                 end
710         end
711         if not config.nofooter then
712                 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
713         end
714 end
715
716 --- Create a CBI model dispatching target.
717 -- @param       model   CBI model to be rendered
718 function cbi(model, config)
719         return {type = "cbi", config = config, model = model, target = _cbi}
720 end
721
722
723 local function _arcombine(self, ...)
724         local argv = {...}
725         local target = #argv > 0 and self.targets[2] or self.targets[1]
726         setfenv(target.target, self.env)
727         target:target(unpack(argv))
728 end
729
730 --- Create a combined dispatching target for non argv and argv requests.
731 -- @param trg1  Overview Target
732 -- @param trg2  Detail Target
733 function arcombine(trg1, trg2)
734         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
735 end
736
737
738 local function _form(self, ...)
739         local cbi = require "luci.cbi"
740         local tpl = require "luci.template"
741         local http = require "luci.http"
742
743         local maps = luci.cbi.load(self.model, ...)
744         local state = nil
745
746         for i, res in ipairs(maps) do
747                 local cstate = res:parse()
748                 if cstate and (not state or cstate < state) then
749                         state = cstate
750                 end
751         end
752
753         http.header("X-CBI-State", state or 0)
754         tpl.render("header")
755         for i, res in ipairs(maps) do
756                 res:render()
757         end
758         tpl.render("footer")
759 end
760
761 --- Create a CBI form model dispatching target.
762 -- @param       model   CBI form model tpo be rendered
763 function form(model)
764         return {type = "cbi", model = model, target = _form}
765 end