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