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