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