luci-base: make rpc webserver path configurable
[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
627         if type(node_a.action) == "table" and
628            node_a.action.type == "firstchild" and
629            node_a.children == nil
630         then
631                 node_a.satisfied = false
632         end
633
634         return node_a
635 end
636
637 function menu_json()
638         local tree = context.tree or createtree()
639         local lua_tree = tree_to_json(tree, {
640                 action = {
641                         ["type"] = "firstchild",
642                         ["recurse"] = true
643                 }
644         })
645
646         local json_tree = createtree_json()
647         return merge_trees(lua_tree, json_tree)
648 end
649
650 local function init_template_engine(ctx)
651         local tpl = require "luci.template"
652         local media = luci.config.main.mediaurlbase
653
654         if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
655                 media = nil
656                 for name, theme in pairs(luci.config.themes) do
657                         if name:sub(1,1) ~= "." and pcall(tpl.Template,
658                          "themes/%s/header" % fs.basename(theme)) then
659                                 media = theme
660                         end
661                 end
662                 assert(media, "No valid theme found")
663         end
664
665         local function _ifattr(cond, key, val, noescape)
666                 if cond then
667                         local env = getfenv(3)
668                         local scope = (type(env.self) == "table") and env.self
669                         if type(val) == "table" then
670                                 if not next(val) then
671                                         return ''
672                                 else
673                                         val = util.serialize_json(val)
674                                 end
675                         end
676
677                         val = tostring(val or
678                                 (type(env[key]) ~= "function" and env[key]) or
679                                 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
680
681                         if noescape ~= true then
682                                 val = util.pcdata(val)
683                         end
684
685                         return string.format(' %s="%s"', tostring(key), val)
686                 else
687                         return ''
688                 end
689         end
690
691         tpl.context.viewns = setmetatable({
692                 write       = http.write;
693                 include     = function(name) tpl.Template(name):render(getfenv(2)) end;
694                 translate   = i18n.translate;
695                 translatef  = i18n.translatef;
696                 export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
697                 striptags   = util.striptags;
698                 pcdata      = util.pcdata;
699                 media       = media;
700                 theme       = fs.basename(media);
701                 resource    = luci.config.main.resourcebase;
702                 ifattr      = function(...) return _ifattr(...) end;
703                 attr        = function(...) return _ifattr(true, ...) end;
704                 url         = build_url;
705         }, {__index=function(tbl, key)
706                 if key == "controller" then
707                         return build_url()
708                 elseif key == "REQUEST_URI" then
709                         return build_url(unpack(ctx.requestpath))
710                 elseif key == "FULL_REQUEST_URI" then
711                         local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
712                         local query = http.getenv("QUERY_STRING")
713                         if query and #query > 0 then
714                                 url[#url+1] = "?"
715                                 url[#url+1] = query
716                         end
717                         return table.concat(url, "")
718                 elseif key == "token" then
719                         return ctx.authtoken
720                 else
721                         return rawget(tbl, key) or _G[key]
722                 end
723         end})
724
725         return tpl
726 end
727
728 function dispatch(request)
729         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
730         local ctx = context
731
732         local auth, cors, suid, sgid
733         local menu = menu_json()
734         local page = menu
735
736         local requested_path_full = {}
737         local requested_path_node = {}
738         local requested_path_args = {}
739
740         for i, s in ipairs(request) do
741                 if type(page.children) ~= "table" or not page.children[s] then
742                         page = nil
743                         break
744                 end
745
746                 if not page.children[s].satisfied then
747                         page = nil
748                         break
749                 end
750
751                 page = page.children[s]
752                 auth = page.auth or auth
753                 cors = page.cors or cors
754                 suid = page.setuser or suid
755                 sgid = page.setgroup or sgid
756
757                 requested_path_full[i] = s
758                 requested_path_node[i] = s
759
760                 if page.wildcard then
761                         for j = i + 1, #request do
762                                 requested_path_args[j - i] = request[j]
763                                 requested_path_full[j] = request[j]
764                         end
765                         break
766                 end
767         end
768
769         local tpl = init_template_engine(ctx)
770
771         ctx.args = requested_path_args
772         ctx.path = requested_path_node
773         ctx.dispatched = page
774
775         ctx.requestpath = ctx.requestpath or requested_path_full
776         ctx.requestargs = ctx.requestargs or requested_path_args
777         ctx.requested = ctx.requested or page
778
779         if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
780                 local sid, sdat
781                 for _, method in ipairs(auth.methods) do
782                         sid, sdat = check_authentication(method)
783
784                         if sid and sdat then
785                                 break
786                         end
787                 end
788
789                 if not (sid and sdat) and auth.login then
790                         local user = http.getenv("HTTP_AUTH_USER")
791                         local pass = http.getenv("HTTP_AUTH_PASS")
792
793                         if user == nil and pass == nil then
794                                 user = http.formvalue("luci_username")
795                                 pass = http.formvalue("luci_password")
796                         end
797
798                         sid, sdat = session_setup(user, pass, { "root" })
799
800                         if not sid then
801                                 context.path = {}
802
803                                 http.status(403, "Forbidden")
804                                 http.header("X-LuCI-Login-Required", "yes")
805
806                                 return tpl.render("sysauth", { duser = "root", fuser = user })
807                         end
808
809                         http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
810                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
811                         })
812
813                         http.redirect(build_url(unpack(ctx.requestpath)))
814                         return
815                 end
816
817                 if not sid or not sdat then
818                         http.status(403, "Forbidden")
819                         http.header("X-LuCI-Login-Required", "yes")
820                         return
821                 end
822
823                 ctx.authsession = sid
824                 ctx.authtoken = sdat.token
825                 ctx.authuser = sdat.username
826         end
827
828         local action = (page and type(page.action) == "table") and page.action or {}
829
830         if action.type == "arcombine" then
831                 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
832         end
833
834         if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
835                 luci.http.status(200, "OK")
836                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
837                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
838                 return
839         end
840
841         if require_post_security(action) then
842                 if not test_post_security() then
843                         return
844                 end
845         end
846
847         if sgid then
848                 sys.process.setgroup(sgid)
849         end
850
851         if suid then
852                 sys.process.setuser(suid)
853         end
854
855         if action.type == "view" then
856                 tpl.render("view", { view = action.path })
857
858         elseif action.type == "call" then
859                 local ok, mod = util.copcall(require, action.module)
860                 if not ok then
861                         error500(mod)
862                         return
863                 end
864
865                 local func = mod[action["function"]]
866
867                 assert(func ~= nil,
868                        'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
869
870                 assert(type(func) == "function",
871                        'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
872                        'of type "' .. type(func) .. '".')
873
874                 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
875                 for _, s in ipairs(requested_path_args) do
876                         argv[#argv + 1] = s
877                 end
878
879                 local ok, err = util.copcall(func, unpack(argv))
880                 if not ok then
881                         error500(err)
882                 end
883
884         elseif action.type == "firstchild" then
885                 local sub_request = find_subnode(page, requested_path_full, action.recurse)
886                 if sub_request then
887                         dispatch(sub_request)
888                 else
889                         tpl.render("empty_node_placeholder", getfenv(1))
890                 end
891
892         elseif action.type == "alias" then
893                 local sub_request = {}
894                 for name in action.path:gmatch("[^/]+") do
895                         sub_request[#sub_request + 1] = name
896                 end
897
898                 for _, s in ipairs(requested_path_args) do
899                         sub_request[#sub_request + 1] = s
900                 end
901
902                 dispatch(sub_request)
903
904         elseif action.type == "rewrite" then
905                 local sub_request = { unpack(request) }
906                 for i = 1, action.remove do
907                         table.remove(sub_request, 1)
908                 end
909
910                 local n = 1
911                 for s in action.path:gmatch("[^/]+") do
912                         table.insert(sub_request, n, s)
913                         n = n + 1
914                 end
915
916                 for _, s in ipairs(requested_path_args) do
917                         sub_request[#sub_request + 1] = s
918                 end
919
920                 dispatch(sub_request)
921
922         elseif action.type == "template" then
923                 tpl.render(action.path, getfenv(1))
924
925         elseif action.type == "cbi" then
926                 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
927
928         elseif action.type == "form" then
929                 _form({ model = action.path }, unpack(requested_path_args))
930
931         else
932                 local root = find_subnode(menu, {}, true)
933                 if not root then
934                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
935                                  "Install luci-mod-admin-full and retry. " ..
936                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
937                 else
938                         error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
939                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
940                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
941                 end
942         end
943 end
944
945 function createindex()
946         local controllers = { }
947         local base = "%s/controller/" % util.libpath()
948         local _, path
949
950         for path in (fs.glob("%s*.lua" % base) or function() end) do
951                 controllers[#controllers+1] = path
952         end
953
954         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
955                 controllers[#controllers+1] = path
956         end
957
958         if indexcache then
959                 local cachedate = fs.stat(indexcache, "mtime")
960                 if cachedate then
961                         local realdate = 0
962                         for _, obj in ipairs(controllers) do
963                                 local omtime = fs.stat(obj, "mtime")
964                                 realdate = (omtime and omtime > realdate) and omtime or realdate
965                         end
966
967                         if cachedate > realdate and sys.process.info("uid") == 0 then
968                                 assert(
969                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
970                                         and fs.stat(indexcache, "modestr") == "rw-------",
971                                         "Fatal: Indexcache is not sane!"
972                                 )
973
974                                 index = loadfile(indexcache)()
975                                 return index
976                         end
977                 end
978         end
979
980         index = {}
981
982         for _, path in ipairs(controllers) do
983                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
984                 local mod = require(modname)
985                 assert(mod ~= true,
986                        "Invalid controller file found\n" ..
987                        "The file '" .. path .. "' contains an invalid module line.\n" ..
988                        "Please verify whether the module name is set to '" .. modname ..
989                        "' - It must correspond to the file path!")
990
991                 local idx = mod.index
992                 if type(idx) == "function" then
993                         index[modname] = idx
994                 end
995         end
996
997         if indexcache then
998                 local f = nixio.open(indexcache, "w", 600)
999                 f:writeall(util.get_bytecode(index))
1000                 f:close()
1001         end
1002 end
1003
1004 function createtree_json()
1005         local json = require "luci.jsonc"
1006         local tree = {}
1007
1008         local schema = {
1009                 action = "table",
1010                 auth = "table",
1011                 cors = "boolean",
1012                 depends = "table",
1013                 order = "number",
1014                 setgroup = "string",
1015                 setuser = "string",
1016                 title = "string",
1017                 wildcard = "boolean"
1018         }
1019
1020         local files = {}
1021         local fprint = {}
1022         local cachefile
1023
1024         for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1025                 files[#files+1] = file
1026
1027                 if indexcache then
1028                         local st = fs.stat(file)
1029                         if st then
1030                                 fprint[#fprint+1] = '%x' % st.ino
1031                                 fprint[#fprint+1] = '%x' % st.mtime
1032                                 fprint[#fprint+1] = '%x' % st.size
1033                         end
1034                 end
1035         end
1036
1037         if indexcache then
1038                 cachefile = "%s.%s.json" %{
1039                         indexcache,
1040                         nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1041                 }
1042
1043                 local res = json.parse(fs.readfile(cachefile) or "")
1044                 if res then
1045                         return res
1046                 end
1047
1048                 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1049                         fs.unlink(file)
1050                 end
1051         end
1052
1053         for _, file in ipairs(files) do
1054                 local data = json.parse(fs.readfile(file) or "")
1055                 if type(data) == "table" then
1056                         for path, spec in pairs(data) do
1057                                 if type(spec) == "table" then
1058                                         local node = tree
1059
1060                                         for s in path:gmatch("[^/]+") do
1061                                                 if s == "*" then
1062                                                         node.wildcard = true
1063                                                         break
1064                                                 end
1065
1066                                                 node.children = node.children or {}
1067                                                 node.children[s] = node.children[s] or {}
1068                                                 node = node.children[s]
1069                                         end
1070
1071                                         if node ~= tree then
1072                                                 for k, t in pairs(schema) do
1073                                                         if type(spec[k]) == t then
1074                                                                 node[k] = spec[k]
1075                                                         end
1076                                                 end
1077
1078                                                 node.satisfied = check_depends(spec)
1079                                         end
1080                                 end
1081                         end
1082                 end
1083         end
1084
1085         if cachefile then
1086                 fs.writefile(cachefile, json.stringify(tree))
1087         end
1088
1089         return tree
1090 end
1091
1092 -- Build the index before if it does not exist yet.
1093 function createtree()
1094         if not index then
1095                 createindex()
1096         end
1097
1098         local ctx  = context
1099         local tree = {nodes={}, inreq=true}
1100
1101         ctx.treecache = setmetatable({}, {__mode="v"})
1102         ctx.tree = tree
1103
1104         local scope = setmetatable({}, {__index = luci.dispatcher})
1105
1106         for k, v in pairs(index) do
1107                 scope._NAME = k
1108                 setfenv(v, scope)
1109                 v()
1110         end
1111
1112         return tree
1113 end
1114
1115 function assign(path, clone, title, order)
1116         local obj  = node(unpack(path))
1117         obj.nodes  = nil
1118         obj.module = nil
1119
1120         obj.title = title
1121         obj.order = order
1122
1123         setmetatable(obj, {__index = _create_node(clone)})
1124
1125         return obj
1126 end
1127
1128 function entry(path, target, title, order)
1129         local c = node(unpack(path))
1130
1131         c.target = target
1132         c.title  = title
1133         c.order  = order
1134         c.module = getfenv(2)._NAME
1135
1136         return c
1137 end
1138
1139 -- enabling the node.
1140 function get(...)
1141         return _create_node({...})
1142 end
1143
1144 function node(...)
1145         local c = _create_node({...})
1146
1147         c.module = getfenv(2)._NAME
1148         c.auto = nil
1149
1150         return c
1151 end
1152
1153 function lookup(...)
1154         local i, path = nil, {}
1155         for i = 1, select('#', ...) do
1156                 local name, arg = nil, tostring(select(i, ...))
1157                 for name in arg:gmatch("[^/]+") do
1158                         path[#path+1] = name
1159                 end
1160         end
1161
1162         for i = #path, 1, -1 do
1163                 local node = context.treecache[table.concat(path, ".", 1, i)]
1164                 if node and (i == #path or node.leaf) then
1165                         return node, build_url(unpack(path))
1166                 end
1167         end
1168 end
1169
1170 function _create_node(path)
1171         if #path == 0 then
1172                 return context.tree
1173         end
1174
1175         local name = table.concat(path, ".")
1176         local c = context.treecache[name]
1177
1178         if not c then
1179                 local last = table.remove(path)
1180                 local parent = _create_node(path)
1181
1182                 c = {nodes={}, auto=true, inreq=true}
1183
1184                 parent.nodes[last] = c
1185                 context.treecache[name] = c
1186         end
1187
1188         return c
1189 end
1190
1191 -- Subdispatchers --
1192
1193 function firstchild()
1194         return { type = "firstchild" }
1195 end
1196
1197 function firstnode()
1198         return { type = "firstnode" }
1199 end
1200
1201 function alias(...)
1202         return { type = "alias", req = { ... } }
1203 end
1204
1205 function rewrite(n, ...)
1206         return { type = "rewrite", n = n, req = { ... } }
1207 end
1208
1209 function call(name, ...)
1210         return { type = "call", argv = {...}, name = name }
1211 end
1212
1213 function post_on(params, name, ...)
1214         return {
1215                 type = "call",
1216                 post = params,
1217                 argv = { ... },
1218                 name = name
1219         }
1220 end
1221
1222 function post(...)
1223         return post_on(true, ...)
1224 end
1225
1226
1227 function template(name)
1228         return { type = "template", view = name }
1229 end
1230
1231 function view(name)
1232         return { type = "view", view = name }
1233 end
1234
1235
1236 function _cbi(self, ...)
1237         local cbi = require "luci.cbi"
1238         local tpl = require "luci.template"
1239         local http = require "luci.http"
1240
1241         local config = self.config or {}
1242         local maps = cbi.load(self.model, ...)
1243
1244         local state = nil
1245
1246         local i, res
1247         for i, res in ipairs(maps) do
1248                 if util.instanceof(res, cbi.SimpleForm) then
1249                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1250                                 % self.model)
1251
1252                         io.stderr:write("please change %s to use the form() action instead.\n"
1253                                 % table.concat(context.request, "/"))
1254                 end
1255
1256                 res.flow = config
1257                 local cstate = res:parse()
1258                 if cstate and (not state or cstate < state) then
1259                         state = cstate
1260                 end
1261         end
1262
1263         local function _resolve_path(path)
1264                 return type(path) == "table" and build_url(unpack(path)) or path
1265         end
1266
1267         if config.on_valid_to and state and state > 0 and state < 2 then
1268                 http.redirect(_resolve_path(config.on_valid_to))
1269                 return
1270         end
1271
1272         if config.on_changed_to and state and state > 1 then
1273                 http.redirect(_resolve_path(config.on_changed_to))
1274                 return
1275         end
1276
1277         if config.on_success_to and state and state > 0 then
1278                 http.redirect(_resolve_path(config.on_success_to))
1279                 return
1280         end
1281
1282         if config.state_handler then
1283                 if not config.state_handler(state, maps) then
1284                         return
1285                 end
1286         end
1287
1288         http.header("X-CBI-State", state or 0)
1289
1290         if not config.noheader then
1291                 tpl.render("cbi/header", {state = state})
1292         end
1293
1294         local redirect
1295         local messages
1296         local applymap   = false
1297         local pageaction = true
1298         local parsechain = { }
1299
1300         for i, res in ipairs(maps) do
1301                 if res.apply_needed and res.parsechain then
1302                         local c
1303                         for _, c in ipairs(res.parsechain) do
1304                                 parsechain[#parsechain+1] = c
1305                         end
1306                         applymap = true
1307                 end
1308
1309                 if res.redirect then
1310                         redirect = redirect or res.redirect
1311                 end
1312
1313                 if res.pageaction == false then
1314                         pageaction = false
1315                 end
1316
1317                 if res.message then
1318                         messages = messages or { }
1319                         messages[#messages+1] = res.message
1320                 end
1321         end
1322
1323         for i, res in ipairs(maps) do
1324                 res:render({
1325                         firstmap   = (i == 1),
1326                         redirect   = redirect,
1327                         messages   = messages,
1328                         pageaction = pageaction,
1329                         parsechain = parsechain
1330                 })
1331         end
1332
1333         if not config.nofooter then
1334                 tpl.render("cbi/footer", {
1335                         flow          = config,
1336                         pageaction    = pageaction,
1337                         redirect      = redirect,
1338                         state         = state,
1339                         autoapply     = config.autoapply,
1340                         trigger_apply = applymap
1341                 })
1342         end
1343 end
1344
1345 function cbi(model, config)
1346         return {
1347                 type = "cbi",
1348                 post = { ["cbi.submit"] = true },
1349                 config = config,
1350                 model = model
1351         }
1352 end
1353
1354
1355 function arcombine(trg1, trg2)
1356         return {
1357                 type = "arcombine",
1358                 env = getfenv(),
1359                 targets = {trg1, trg2}
1360         }
1361 end
1362
1363
1364 function _form(self, ...)
1365         local cbi = require "luci.cbi"
1366         local tpl = require "luci.template"
1367         local http = require "luci.http"
1368
1369         local maps = luci.cbi.load(self.model, ...)
1370         local state = nil
1371
1372         local i, res
1373         for i, res in ipairs(maps) do
1374                 local cstate = res:parse()
1375                 if cstate and (not state or cstate < state) then
1376                         state = cstate
1377                 end
1378         end
1379
1380         http.header("X-CBI-State", state or 0)
1381         tpl.render("header")
1382         for i, res in ipairs(maps) do
1383                 res:render()
1384         end
1385         tpl.render("footer")
1386 end
1387
1388 function form(model)
1389         return {
1390                 type = "form",
1391                 post = { ["cbi.submit"] = true },
1392                 model = model
1393         }
1394 end
1395
1396 translate = i18n.translate
1397
1398 -- This function does not actually translate the given argument but
1399 -- is used by build/i18n-scan.pl to find translatable entries.
1400 function _(text)
1401         return text
1402 end