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