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