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