Translated using Weblate (Chinese (Simplified))
[oweals/luci.git] / modules / luci-base / luasrc / dispatcher.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
4
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
10
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
15 _M.fs = fs
16
17 -- Index table
18 local index = nil
19
20 local function check_fs_depends(spec)
21         local fs = require "nixio.fs"
22
23         for path, kind in pairs(spec) do
24                 if kind == "directory" then
25                         local empty = true
26                         for entry in (fs.dir(path) or function() end) do
27                                 empty = false
28                                 break
29                         end
30                         if empty then
31                                 return false
32                         end
33                 elseif kind == "executable" then
34                         if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then
35                                 return false
36                         end
37                 elseif kind == "file" then
38                         if fs.stat(path, "type") ~= "reg" then
39                                 return false
40                         end
41                 end
42         end
43
44         return true
45 end
46
47 local function check_uci_depends_options(conf, s, opts)
48         local uci = require "luci.model.uci"
49
50         if type(opts) == "string" then
51                 return (s[".type"] == opts)
52         elseif opts == true then
53                 for option, value in pairs(s) do
54                         if option:byte(1) ~= 46 then
55                                 return true
56                         end
57                 end
58         elseif type(opts) == "table" then
59                 for option, value in pairs(opts) do
60                         local sval = s[option]
61                         if type(sval) == "table" then
62                                 local found = false
63                                 for _, v in ipairs(sval) do
64                                         if v == value then
65                                                 found = true
66                                                 break
67                                         end
68                                 end
69                                 if not found then
70                                         return false
71                                 end
72                         elseif value == true then
73                                 if sval == nil then
74                                         return false
75                                 end
76                         else
77                                 if sval ~= value then
78                                         return false
79                                 end
80                         end
81                 end
82         end
83
84         return true
85 end
86
87 local function check_uci_depends_section(conf, sect)
88         local uci = require "luci.model.uci"
89
90         for section, options in pairs(sect) do
91                 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
92                 if stype then
93                         local found = false
94                         uci:foreach(conf, stype, function(s)
95                                 if check_uci_depends_options(conf, s, options) then
96                                         found = true
97                                         return false
98                                 end
99                         end)
100                         if not found then
101                                 return false
102                         end
103                 else
104                         local s = uci:get_all(conf, section)
105                         if not s or not check_uci_depends_options(conf, s, options) then
106                                 return false
107                         end
108                 end
109         end
110
111         return true
112 end
113
114 local function check_uci_depends(conf)
115         local uci = require "luci.model.uci"
116
117         for config, values in pairs(conf) do
118                 if values == true then
119                         local found = false
120                         uci:foreach(config, nil, function(s)
121                                 found = true
122                                 return false
123                         end)
124                         if not found then
125                                 return false
126                         end
127                 elseif type(values) == "table" then
128                         if not check_uci_depends_section(config, values) then
129                                 return false
130                         end
131                 end
132         end
133
134         return true
135 end
136
137 local function check_depends(spec)
138         if type(spec.depends) ~= "table" then
139                 return true
140         end
141
142         if type(spec.depends.fs) == "table" then
143                 local satisfied = false
144                 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
145                 for _, alternative in ipairs(alternatives) do
146                         if check_fs_depends(alternative) then
147                                 satisfied = true
148                                 break
149                         end
150                 end
151                 if not satisfied then
152                         return false
153                 end
154         end
155
156         if type(spec.depends.uci) == "table" then
157                 local satisfied = false
158                 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
159                 for _, alternative in ipairs(alternatives) do
160                         if check_uci_depends(alternative) then
161                                 satisfied = true
162                                 break
163                         end
164                 end
165                 if not satisfied then
166                         return false
167                 end
168         end
169
170         return true
171 end
172
173 local function target_to_json(target, module)
174         local action
175
176         if target.type == "call" then
177                 action = {
178                         ["type"] = "call",
179                         ["module"] = module,
180                         ["function"] = target.name,
181                         ["parameters"] = target.argv
182                 }
183         elseif target.type == "view" then
184                 action = {
185                         ["type"] = "view",
186                         ["path"] = target.view
187                 }
188         elseif target.type == "template" then
189                 action = {
190                         ["type"] = "template",
191                         ["path"] = target.view
192                 }
193         elseif target.type == "cbi" then
194                 action = {
195                         ["type"] = "cbi",
196                         ["path"] = target.model,
197                         ["config"] = target.config
198                 }
199         elseif target.type == "form" then
200                 action = {
201                         ["type"] = "form",
202                         ["path"] = target.model
203                 }
204         elseif target.type == "firstchild" then
205                 action = {
206                         ["type"] = "firstchild"
207                 }
208         elseif target.type == "firstnode" then
209                 action = {
210                         ["type"] = "firstchild",
211                         ["recurse"] = true
212                 }
213         elseif target.type == "arcombine" then
214                 if type(target.targets) == "table" then
215                         action = {
216                                 ["type"] = "arcombine",
217                                 ["targets"] = {
218                                         target_to_json(target.targets[1], module),
219                                         target_to_json(target.targets[2], module)
220                                 }
221                         }
222                 end
223         elseif target.type == "alias" then
224                 action = {
225                         ["type"] = "alias",
226                         ["path"] = table.concat(target.req, "/")
227                 }
228         elseif target.type == "rewrite" then
229                 action = {
230                         ["type"] = "rewrite",
231                         ["path"] = table.concat(target.req, "/"),
232                         ["remove"] = target.n
233                 }
234         end
235
236         if target.post and action then
237                 action.post = target.post
238         end
239
240         return action
241 end
242
243 local function tree_to_json(node, json)
244         local fs = require "nixio.fs"
245         local util = require "luci.util"
246
247         if type(node.nodes) == "table" then
248                 for subname, subnode in pairs(node.nodes) do
249                         local spec = {
250                                 title = util.striptags(subnode.title),
251                                 order = subnode.order
252                         }
253
254                         if subnode.leaf then
255                                 spec.wildcard = true
256                         end
257
258                         if subnode.cors then
259                                 spec.cors = true
260                         end
261
262                         if subnode.setuser then
263                                 spec.setuser = subnode.setuser
264                         end
265
266                         if subnode.setgroup then
267                                 spec.setgroup = subnode.setgroup
268                         end
269
270                         if type(subnode.target) == "table" then
271                                 spec.action = target_to_json(subnode.target, subnode.module)
272                         end
273
274                         if type(subnode.file_depends) == "table" then
275                                 for _, v in ipairs(subnode.file_depends) do
276                                         spec.depends = spec.depends or {}
277                                         spec.depends.fs = spec.depends.fs or {}
278
279                                         local ft = fs.stat(v, "type")
280                                         if ft == "dir" then
281                                                 spec.depends.fs[v] = "directory"
282                                         elseif v:match("/s?bin/") then
283                                                 spec.depends.fs[v] = "executable"
284                                         else
285                                                 spec.depends.fs[v] = "file"
286                                         end
287                                 end
288                         end
289
290                         if type(subnode.uci_depends) == "table" then
291                                 for k, v in pairs(subnode.uci_depends) do
292                                         spec.depends = spec.depends or {}
293                                         spec.depends.uci = spec.depends.uci or {}
294                                         spec.depends.uci[k] = v
295                                 end
296                         end
297
298                         if (subnode.sysauth_authenticator ~= nil) or
299                            (subnode.sysauth ~= nil and subnode.sysauth ~= false)
300                         then
301                                 if subnode.sysauth_authenticator == "htmlauth" then
302                                         spec.auth = {
303                                                 login = true,
304                                                 methods = { "cookie:sysauth" }
305                                         }
306                                 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
307                                         spec.auth = {
308                                                 login = false,
309                                                 methods = { "query:auth", "cookie:sysauth" }
310                                         }
311                                 elseif subnode.module == "luci.controller.admin.uci" then
312                                         spec.auth = {
313                                                 login = false,
314                                                 methods = { "param:sid" }
315                                         }
316                                 end
317                         elseif subnode.sysauth == false then
318                                 spec.auth = {}
319                         end
320
321                         if not spec.action then
322                                 spec.title = nil
323                         end
324
325                         spec.satisfied = check_depends(spec)
326                         json.children = json.children or {}
327                         json.children[subname] = tree_to_json(subnode, spec)
328                 end
329         end
330
331         return json
332 end
333
334 function build_url(...)
335         local path = {...}
336         local url = { http.getenv("SCRIPT_NAME") or "" }
337
338         local p
339         for _, p in ipairs(path) do
340                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
341                         url[#url+1] = "/"
342                         url[#url+1] = p
343                 end
344         end
345
346         if #path == 0 then
347                 url[#url+1] = "/"
348         end
349
350         return table.concat(url, "")
351 end
352
353
354 function error404(message)
355         http.status(404, "Not Found")
356         message = message or "Not Found"
357
358         local function render()
359                 local template = require "luci.template"
360                 template.render("error404")
361         end
362
363         if not util.copcall(render) then
364                 http.prepare_content("text/plain")
365                 http.write(message)
366         end
367
368         return false
369 end
370
371 function error500(message)
372         util.perror(message)
373         if not context.template_header_sent then
374                 http.status(500, "Internal Server Error")
375                 http.prepare_content("text/plain")
376                 http.write(message)
377         else
378                 require("luci.template")
379                 if not util.copcall(luci.template.render, "error500", {message=message}) then
380                         http.prepare_content("text/plain")
381                         http.write(message)
382                 end
383         end
384         return false
385 end
386
387 local function determine_request_language()
388         local conf = require "luci.config"
389         assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
390
391         local lang = conf.main.lang or "auto"
392         if lang == "auto" then
393                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
394                 for aclang in aclang:gmatch("[%w_-]+") do
395                         local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
396                         if country and culture then
397                                 local cc = "%s_%s" %{ country, culture:lower() }
398                                 if conf.languages[cc] then
399                                         lang = cc
400                                         break
401                                 elseif conf.languages[country] then
402                                         lang = country
403                                         break
404                                 end
405                         elseif conf.languages[aclang] then
406                                 lang = aclang
407                                 break
408                         end
409                 end
410         end
411
412         if lang == "auto" then
413                 lang = i18n.default
414         end
415
416         i18n.setlanguage(lang)
417 end
418
419 function httpdispatch(request, prefix)
420         http.context.request = request
421
422         local r = {}
423         context.request = r
424
425         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
426
427         if prefix then
428                 for _, node in ipairs(prefix) do
429                         r[#r+1] = node
430                 end
431         end
432
433         local node
434         for node in pathinfo:gmatch("[^/%z]+") do
435                 r[#r+1] = node
436         end
437
438         determine_request_language()
439
440         local stat, err = util.coxpcall(function()
441                 dispatch(context.request)
442         end, error500)
443
444         http.close()
445
446         --context._disable_memtrace()
447 end
448
449 local function require_post_security(target, args)
450         if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
451                 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
452         end
453
454         if type(target) == "table" then
455                 if type(target.post) == "table" then
456                         local param_name, required_val, request_val
457
458                         for param_name, required_val in pairs(target.post) do
459                                 request_val = http.formvalue(param_name)
460
461                                 if (type(required_val) == "string" and
462                                     request_val ~= required_val) or
463                                    (required_val == true and request_val == nil)
464                                 then
465                                         return false
466                                 end
467                         end
468
469                         return true
470                 end
471
472                 return (target.post == true)
473         end
474
475         return false
476 end
477
478 function test_post_security()
479         if http.getenv("REQUEST_METHOD") ~= "POST" then
480                 http.status(405, "Method Not Allowed")
481                 http.header("Allow", "POST")
482                 return false
483         end
484
485         if http.formvalue("token") ~= context.authtoken then
486                 http.status(403, "Forbidden")
487                 luci.template.render("csrftoken")
488                 return false
489         end
490
491         return true
492 end
493
494 local function session_retrieve(sid, allowed_users)
495         local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
496
497         if type(sdat) == "table" and
498            type(sdat.values) == "table" and
499            type(sdat.values.token) == "string" and
500            (not allowed_users or
501             util.contains(allowed_users, sdat.values.username))
502         then
503                 uci:set_session_id(sid)
504                 return sid, sdat.values
505         end
506
507         return nil, nil
508 end
509
510 local function session_setup(user, pass, allowed_users)
511         if util.contains(allowed_users, user) then
512                 local login = util.ubus("session", "login", {
513                         username = user,
514                         password = pass,
515                         timeout  = tonumber(luci.config.sauth.sessiontime)
516                 })
517
518                 local rp = context.requestpath
519                         and table.concat(context.requestpath, "/") or ""
520
521                 if type(login) == "table" and
522                    type(login.ubus_rpc_session) == "string"
523                 then
524                         util.ubus("session", "set", {
525                                 ubus_rpc_session = login.ubus_rpc_session,
526                                 values = { token = sys.uniqueid(16) }
527                         })
528
529                         io.stderr:write("luci: accepted login on /%s for %s from %s\n"
530                                 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
531
532                         return session_retrieve(login.ubus_rpc_session)
533                 end
534
535                 io.stderr:write("luci: failed login on /%s for %s from %s\n"
536                         %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
537         end
538
539         return nil, nil
540 end
541
542 local function check_authentication(method)
543         local auth_type, auth_param = method:match("^(%w+):(.+)$")
544         local sid, sdat
545
546         if auth_type == "cookie" then
547                 sid = http.getcookie(auth_param)
548         elseif auth_type == "param" then
549                 sid = http.formvalue(auth_param)
550         elseif auth_type == "query" then
551                 sid = http.formvalue(auth_param, true)
552         end
553
554         return session_retrieve(sid)
555 end
556
557 local function get_children(node)
558         local children = {}
559
560         if not node.wildcard and type(node.children) == "table" then
561                 for name, child in pairs(node.children) do
562                         children[#children+1] = {
563                                 name  = name,
564                                 node  = child,
565                                 order = child.order or 1000
566                         }
567                 end
568
569                 table.sort(children, function(a, b)
570                         if a.order == b.order then
571                                 return a.name < b.name
572                         else
573                                 return a.order < b.order
574                         end
575                 end)
576         end
577
578         return children
579 end
580
581 local function find_subnode(root, prefix, recurse, descended)
582         local children = get_children(root)
583
584         if #children > 0 and (not descended or recurse) then
585                 local sub_path = { unpack(prefix) }
586
587                 if recurse == false then
588                         recurse = nil
589                 end
590
591                 for _, child in ipairs(children) do
592                         sub_path[#prefix+1] = child.name
593
594                         local res_path = find_subnode(child.node, sub_path, recurse, true)
595
596                         if res_path then
597                                 return res_path
598                         end
599                 end
600         end
601
602         if descended then
603                 if not recurse or
604                    root.action.type == "cbi" or
605                    root.action.type == "form" or
606                    root.action.type == "view" or
607                    root.action.type == "template" or
608                    root.action.type == "arcombine"
609                 then
610                         return prefix
611                 end
612         end
613 end
614
615 local function merge_trees(node_a, node_b)
616         for k, v in pairs(node_b) do
617                 if k == "children" then
618                         node_a.children = node_a.children or {}
619
620                         for name, spec in pairs(v) do
621                                 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
622                         end
623                 else
624                         node_a[k] = v
625                 end
626         end
627
628         if type(node_a.action) == "table" and
629            node_a.action.type == "firstchild" and
630            node_a.children == nil
631         then
632                 node_a.satisfied = false
633         end
634
635         return node_a
636 end
637
638 function menu_json()
639         local tree = context.tree or createtree()
640         local lua_tree = tree_to_json(tree, {
641                 action = {
642                         ["type"] = "firstchild",
643                         ["recurse"] = true
644                 }
645         })
646
647         local json_tree = createtree_json()
648         return merge_trees(lua_tree, json_tree)
649 end
650
651 local function init_template_engine(ctx)
652         local tpl = require "luci.template"
653         local media = luci.config.main.mediaurlbase
654
655         if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
656                 media = nil
657                 for name, theme in pairs(luci.config.themes) do
658                         if name:sub(1,1) ~= "." and pcall(tpl.Template,
659                          "themes/%s/header" % fs.basename(theme)) then
660                                 media = theme
661                         end
662                 end
663                 assert(media, "No valid theme found")
664         end
665
666         local function _ifattr(cond, key, val, noescape)
667                 if cond then
668                         local env = getfenv(3)
669                         local scope = (type(env.self) == "table") and env.self
670                         if type(val) == "table" then
671                                 if not next(val) then
672                                         return ''
673                                 else
674                                         val = util.serialize_json(val)
675                                 end
676                         end
677
678                         val = tostring(val or
679                                 (type(env[key]) ~= "function" and env[key]) or
680                                 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
681
682                         if noescape ~= true then
683                                 val = util.pcdata(val)
684                         end
685
686                         return string.format(' %s="%s"', tostring(key), val)
687                 else
688                         return ''
689                 end
690         end
691
692         tpl.context.viewns = setmetatable({
693                 write       = http.write;
694                 include     = function(name) tpl.Template(name):render(getfenv(2)) end;
695                 translate   = i18n.translate;
696                 translatef  = i18n.translatef;
697                 export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
698                 striptags   = util.striptags;
699                 pcdata      = util.pcdata;
700                 media       = media;
701                 theme       = fs.basename(media);
702                 resource    = luci.config.main.resourcebase;
703                 ifattr      = function(...) return _ifattr(...) end;
704                 attr        = function(...) return _ifattr(true, ...) end;
705                 url         = build_url;
706         }, {__index=function(tbl, key)
707                 if key == "controller" then
708                         return build_url()
709                 elseif key == "REQUEST_URI" then
710                         return build_url(unpack(ctx.requestpath))
711                 elseif key == "FULL_REQUEST_URI" then
712                         local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
713                         local query = http.getenv("QUERY_STRING")
714                         if query and #query > 0 then
715                                 url[#url+1] = "?"
716                                 url[#url+1] = query
717                         end
718                         return table.concat(url, "")
719                 elseif key == "token" then
720                         return ctx.authtoken
721                 else
722                         return rawget(tbl, key) or _G[key]
723                 end
724         end})
725
726         return tpl
727 end
728
729 function dispatch(request)
730         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
731         local ctx = context
732
733         local auth, cors, suid, sgid
734         local menu = menu_json()
735         local page = menu
736
737         local requested_path_full = {}
738         local requested_path_node = {}
739         local requested_path_args = {}
740
741         for i, s in ipairs(request) do
742                 if type(page.children) ~= "table" or not page.children[s] then
743                         page = nil
744                         break
745                 end
746
747                 if not page.children[s].satisfied then
748                         page = nil
749                         break
750                 end
751
752                 page = page.children[s]
753                 auth = page.auth or auth
754                 cors = page.cors or cors
755                 suid = page.setuser or suid
756                 sgid = page.setgroup or sgid
757
758                 requested_path_full[i] = s
759                 requested_path_node[i] = s
760
761                 if page.wildcard then
762                         for j = i + 1, #request do
763                                 requested_path_args[j - i] = request[j]
764                                 requested_path_full[j] = request[j]
765                         end
766                         break
767                 end
768         end
769
770         local tpl = init_template_engine(ctx)
771
772         ctx.args = requested_path_args
773         ctx.path = requested_path_node
774         ctx.dispatched = page
775
776         ctx.requestpath = ctx.requestpath or requested_path_full
777         ctx.requestargs = ctx.requestargs or requested_path_args
778         ctx.requested = ctx.requested or page
779
780         if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
781                 local sid, sdat
782                 for _, method in ipairs(auth.methods) do
783                         sid, sdat = check_authentication(method)
784
785                         if sid and sdat then
786                                 break
787                         end
788                 end
789
790                 if not (sid and sdat) and auth.login then
791                         local user = http.getenv("HTTP_AUTH_USER")
792                         local pass = http.getenv("HTTP_AUTH_PASS")
793
794                         if user == nil and pass == nil then
795                                 user = http.formvalue("luci_username")
796                                 pass = http.formvalue("luci_password")
797                         end
798
799                         sid, sdat = session_setup(user, pass, { "root" })
800
801                         if not sid then
802                                 context.path = {}
803
804                                 http.status(403, "Forbidden")
805                                 http.header("X-LuCI-Login-Required", "yes")
806
807                                 return tpl.render("sysauth", { duser = "root", fuser = user })
808                         end
809
810                         http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
811                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
812                         })
813
814                         http.redirect(build_url(unpack(ctx.requestpath)))
815                         return
816                 end
817
818                 if not sid or not sdat then
819                         http.status(403, "Forbidden")
820                         http.header("X-LuCI-Login-Required", "yes")
821                         return
822                 end
823
824                 ctx.authsession = sid
825                 ctx.authtoken = sdat.token
826                 ctx.authuser = sdat.username
827         end
828
829         local action = (page and type(page.action) == "table") and page.action or {}
830
831         if action.type == "arcombine" then
832                 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
833         end
834
835         if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
836                 luci.http.status(200, "OK")
837                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
838                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
839                 return
840         end
841
842         if require_post_security(action) then
843                 if not test_post_security() then
844                         return
845                 end
846         end
847
848         if sgid then
849                 sys.process.setgroup(sgid)
850         end
851
852         if suid then
853                 sys.process.setuser(suid)
854         end
855
856         if action.type == "view" then
857                 tpl.render("view", { view = action.path })
858
859         elseif action.type == "call" then
860                 local ok, mod = util.copcall(require, action.module)
861                 if not ok then
862                         error500(mod)
863                         return
864                 end
865
866                 local func = mod[action["function"]]
867
868                 assert(func ~= nil,
869                        'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
870
871                 assert(type(func) == "function",
872                        'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
873                        'of type "' .. type(func) .. '".')
874
875                 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
876                 for _, s in ipairs(requested_path_args) do
877                         argv[#argv + 1] = s
878                 end
879
880                 local ok, err = util.copcall(func, unpack(argv))
881                 if not ok then
882                         error500(err)
883                 end
884
885         elseif action.type == "firstchild" then
886                 local sub_request = find_subnode(page, requested_path_full, action.recurse)
887                 if sub_request then
888                         dispatch(sub_request)
889                 else
890                         tpl.render("empty_node_placeholder", getfenv(1))
891                 end
892
893         elseif action.type == "alias" then
894                 local sub_request = {}
895                 for name in action.path:gmatch("[^/]+") do
896                         sub_request[#sub_request + 1] = name
897                 end
898
899                 for _, s in ipairs(requested_path_args) do
900                         sub_request[#sub_request + 1] = s
901                 end
902
903                 dispatch(sub_request)
904
905         elseif action.type == "rewrite" then
906                 local sub_request = { unpack(request) }
907                 for i = 1, action.remove do
908                         table.remove(sub_request, 1)
909                 end
910
911                 local n = 1
912                 for s in action.path:gmatch("[^/]+") do
913                         table.insert(sub_request, n, s)
914                         n = n + 1
915                 end
916
917                 for _, s in ipairs(requested_path_args) do
918                         sub_request[#sub_request + 1] = s
919                 end
920
921                 dispatch(sub_request)
922
923         elseif action.type == "template" then
924                 tpl.render(action.path, getfenv(1))
925
926         elseif action.type == "cbi" then
927                 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
928
929         elseif action.type == "form" then
930                 _form({ model = action.path }, unpack(requested_path_args))
931
932         else
933                 local root = find_subnode(menu, {}, true)
934                 if not root then
935                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
936                                  "Install luci-mod-admin-full and retry. " ..
937                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
938                 else
939                         error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
940                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
941                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
942                 end
943         end
944 end
945
946 function createindex()
947         local controllers = { }
948         local base = "%s/controller/" % util.libpath()
949         local _, path
950
951         for path in (fs.glob("%s*.lua" % base) or function() end) do
952                 controllers[#controllers+1] = path
953         end
954
955         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
956                 controllers[#controllers+1] = path
957         end
958
959         if indexcache then
960                 local cachedate = fs.stat(indexcache, "mtime")
961                 if cachedate then
962                         local realdate = 0
963                         for _, obj in ipairs(controllers) do
964                                 local omtime = fs.stat(obj, "mtime")
965                                 realdate = (omtime and omtime > realdate) and omtime or realdate
966                         end
967
968                         if cachedate > realdate and sys.process.info("uid") == 0 then
969                                 assert(
970                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
971                                         and fs.stat(indexcache, "modestr") == "rw-------",
972                                         "Fatal: Indexcache is not sane!"
973                                 )
974
975                                 index = loadfile(indexcache)()
976                                 return index
977                         end
978                 end
979         end
980
981         index = {}
982
983         for _, path in ipairs(controllers) do
984                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
985                 local mod = require(modname)
986                 assert(mod ~= true,
987                        "Invalid controller file found\n" ..
988                        "The file '" .. path .. "' contains an invalid module line.\n" ..
989                        "Please verify whether the module name is set to '" .. modname ..
990                        "' - It must correspond to the file path!")
991
992                 local idx = mod.index
993                 if type(idx) == "function" then
994                         index[modname] = idx
995                 end
996         end
997
998         if indexcache then
999                 local f = nixio.open(indexcache, "w", 600)
1000                 f:writeall(util.get_bytecode(index))
1001                 f:close()
1002         end
1003 end
1004
1005 function createtree_json()
1006         local json = require "luci.jsonc"
1007         local tree = {}
1008
1009         local schema = {
1010                 action = "table",
1011                 auth = "table",
1012                 cors = "boolean",
1013                 depends = "table",
1014                 order = "number",
1015                 setgroup = "string",
1016                 setuser = "string",
1017                 title = "string",
1018                 wildcard = "boolean"
1019         }
1020
1021         local files = {}
1022         local fprint = {}
1023         local cachefile
1024
1025         for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1026                 files[#files+1] = file
1027
1028                 if indexcache then
1029                         local st = fs.stat(file)
1030                         if st then
1031                                 fprint[#fprint+1] = '%x' % st.ino
1032                                 fprint[#fprint+1] = '%x' % st.mtime
1033                                 fprint[#fprint+1] = '%x' % st.size
1034                         end
1035                 end
1036         end
1037
1038         if indexcache then
1039                 cachefile = "%s.%s.json" %{
1040                         indexcache,
1041                         nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1042                 }
1043
1044                 local res = json.parse(fs.readfile(cachefile) or "")
1045                 if res then
1046                         return res
1047                 end
1048
1049                 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1050                         fs.unlink(file)
1051                 end
1052         end
1053
1054         for _, file in ipairs(files) do
1055                 local data = json.parse(fs.readfile(file) or "")
1056                 if type(data) == "table" then
1057                         for path, spec in pairs(data) do
1058                                 if type(spec) == "table" then
1059                                         local node = tree
1060
1061                                         for s in path:gmatch("[^/]+") do
1062                                                 if s == "*" then
1063                                                         node.wildcard = true
1064                                                         break
1065                                                 end
1066
1067                                                 node.children = node.children or {}
1068                                                 node.children[s] = node.children[s] or {}
1069                                                 node = node.children[s]
1070                                         end
1071
1072                                         if node ~= tree then
1073                                                 for k, t in pairs(schema) do
1074                                                         if type(spec[k]) == t then
1075                                                                 node[k] = spec[k]
1076                                                         end
1077                                                 end
1078
1079                                                 node.satisfied = check_depends(spec)
1080                                         end
1081                                 end
1082                         end
1083                 end
1084         end
1085
1086         if cachefile then
1087                 fs.writefile(cachefile, json.stringify(tree))
1088         end
1089
1090         return tree
1091 end
1092
1093 -- Build the index before if it does not exist yet.
1094 function createtree()
1095         if not index then
1096                 createindex()
1097         end
1098
1099         local ctx  = context
1100         local tree = {nodes={}, inreq=true}
1101
1102         ctx.treecache = setmetatable({}, {__mode="v"})
1103         ctx.tree = tree
1104
1105         local scope = setmetatable({}, {__index = luci.dispatcher})
1106
1107         for k, v in pairs(index) do
1108                 scope._NAME = k
1109                 setfenv(v, scope)
1110                 v()
1111         end
1112
1113         return tree
1114 end
1115
1116 function assign(path, clone, title, order)
1117         local obj  = node(unpack(path))
1118         obj.nodes  = nil
1119         obj.module = nil
1120
1121         obj.title = title
1122         obj.order = order
1123
1124         setmetatable(obj, {__index = _create_node(clone)})
1125
1126         return obj
1127 end
1128
1129 function entry(path, target, title, order)
1130         local c = node(unpack(path))
1131
1132         c.target = target
1133         c.title  = title
1134         c.order  = order
1135         c.module = getfenv(2)._NAME
1136
1137         return c
1138 end
1139
1140 -- enabling the node.
1141 function get(...)
1142         return _create_node({...})
1143 end
1144
1145 function node(...)
1146         local c = _create_node({...})
1147
1148         c.module = getfenv(2)._NAME
1149         c.auto = nil
1150
1151         return c
1152 end
1153
1154 function lookup(...)
1155         local i, path = nil, {}
1156         for i = 1, select('#', ...) do
1157                 local name, arg = nil, tostring(select(i, ...))
1158                 for name in arg:gmatch("[^/]+") do
1159                         path[#path+1] = name
1160                 end
1161         end
1162
1163         for i = #path, 1, -1 do
1164                 local node = context.treecache[table.concat(path, ".", 1, i)]
1165                 if node and (i == #path or node.leaf) then
1166                         return node, build_url(unpack(path))
1167                 end
1168         end
1169 end
1170
1171 function _create_node(path)
1172         if #path == 0 then
1173                 return context.tree
1174         end
1175
1176         local name = table.concat(path, ".")
1177         local c = context.treecache[name]
1178
1179         if not c then
1180                 local last = table.remove(path)
1181                 local parent = _create_node(path)
1182
1183                 c = {nodes={}, auto=true, inreq=true}
1184
1185                 parent.nodes[last] = c
1186                 context.treecache[name] = c
1187         end
1188
1189         return c
1190 end
1191
1192 -- Subdispatchers --
1193
1194 function firstchild()
1195         return { type = "firstchild" }
1196 end
1197
1198 function firstnode()
1199         return { type = "firstnode" }
1200 end
1201
1202 function alias(...)
1203         return { type = "alias", req = { ... } }
1204 end
1205
1206 function rewrite(n, ...)
1207         return { type = "rewrite", n = n, req = { ... } }
1208 end
1209
1210 function call(name, ...)
1211         return { type = "call", argv = {...}, name = name }
1212 end
1213
1214 function post_on(params, name, ...)
1215         return {
1216                 type = "call",
1217                 post = params,
1218                 argv = { ... },
1219                 name = name
1220         }
1221 end
1222
1223 function post(...)
1224         return post_on(true, ...)
1225 end
1226
1227
1228 function template(name)
1229         return { type = "template", view = name }
1230 end
1231
1232 function view(name)
1233         return { type = "view", view = name }
1234 end
1235
1236
1237 function _cbi(self, ...)
1238         local cbi = require "luci.cbi"
1239         local tpl = require "luci.template"
1240         local http = require "luci.http"
1241
1242         local config = self.config or {}
1243         local maps = cbi.load(self.model, ...)
1244
1245         local state = nil
1246
1247         local i, res
1248         for i, res in ipairs(maps) do
1249                 if util.instanceof(res, cbi.SimpleForm) then
1250                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1251                                 % self.model)
1252
1253                         io.stderr:write("please change %s to use the form() action instead.\n"
1254                                 % table.concat(context.request, "/"))
1255                 end
1256
1257                 res.flow = config
1258                 local cstate = res:parse()
1259                 if cstate and (not state or cstate < state) then
1260                         state = cstate
1261                 end
1262         end
1263
1264         local function _resolve_path(path)
1265                 return type(path) == "table" and build_url(unpack(path)) or path
1266         end
1267
1268         if config.on_valid_to and state and state > 0 and state < 2 then
1269                 http.redirect(_resolve_path(config.on_valid_to))
1270                 return
1271         end
1272
1273         if config.on_changed_to and state and state > 1 then
1274                 http.redirect(_resolve_path(config.on_changed_to))
1275                 return
1276         end
1277
1278         if config.on_success_to and state and state > 0 then
1279                 http.redirect(_resolve_path(config.on_success_to))
1280                 return
1281         end
1282
1283         if config.state_handler then
1284                 if not config.state_handler(state, maps) then
1285                         return
1286                 end
1287         end
1288
1289         http.header("X-CBI-State", state or 0)
1290
1291         if not config.noheader then
1292                 tpl.render("cbi/header", {state = state})
1293         end
1294
1295         local redirect
1296         local messages
1297         local applymap   = false
1298         local pageaction = true
1299         local parsechain = { }
1300
1301         for i, res in ipairs(maps) do
1302                 if res.apply_needed and res.parsechain then
1303                         local c
1304                         for _, c in ipairs(res.parsechain) do
1305                                 parsechain[#parsechain+1] = c
1306                         end
1307                         applymap = true
1308                 end
1309
1310                 if res.redirect then
1311                         redirect = redirect or res.redirect
1312                 end
1313
1314                 if res.pageaction == false then
1315                         pageaction = false
1316                 end
1317
1318                 if res.message then
1319                         messages = messages or { }
1320                         messages[#messages+1] = res.message
1321                 end
1322         end
1323
1324         for i, res in ipairs(maps) do
1325                 res:render({
1326                         firstmap   = (i == 1),
1327                         redirect   = redirect,
1328                         messages   = messages,
1329                         pageaction = pageaction,
1330                         parsechain = parsechain
1331                 })
1332         end
1333
1334         if not config.nofooter then
1335                 tpl.render("cbi/footer", {
1336                         flow          = config,
1337                         pageaction    = pageaction,
1338                         redirect      = redirect,
1339                         state         = state,
1340                         autoapply     = config.autoapply,
1341                         trigger_apply = applymap
1342                 })
1343         end
1344 end
1345
1346 function cbi(model, config)
1347         return {
1348                 type = "cbi",
1349                 post = { ["cbi.submit"] = true },
1350                 config = config,
1351                 model = model
1352         }
1353 end
1354
1355
1356 function arcombine(trg1, trg2)
1357         return {
1358                 type = "arcombine",
1359                 env = getfenv(),
1360                 targets = {trg1, trg2}
1361         }
1362 end
1363
1364
1365 function _form(self, ...)
1366         local cbi = require "luci.cbi"
1367         local tpl = require "luci.template"
1368         local http = require "luci.http"
1369
1370         local maps = luci.cbi.load(self.model, ...)
1371         local state = nil
1372
1373         local i, res
1374         for i, res in ipairs(maps) do
1375                 local cstate = res:parse()
1376                 if cstate and (not state or cstate < state) then
1377                         state = cstate
1378                 end
1379         end
1380
1381         http.header("X-CBI-State", state or 0)
1382         tpl.render("header")
1383         for i, res in ipairs(maps) do
1384                 res:render()
1385         end
1386         tpl.render("footer")
1387 end
1388
1389 function form(model)
1390         return {
1391                 type = "form",
1392                 post = { ["cbi.submit"] = true },
1393                 model = model
1394         }
1395 end
1396
1397 translate = i18n.translate
1398
1399 -- This function does not actually translate the given argument but
1400 -- is used by build/i18n-scan.pl to find translatable entries.
1401 function _(text)
1402         return text
1403 end