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