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