luci-base: dispatcher.lua: pass permission state to legacy CBI templates
[project/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 function createindex()
1038 local controllers = { }
1039 local base = "%s/controller/" % util.libpath()
1040 local _, path
1041
1042 for path in (fs.glob("%s*.lua" % base) or function() end) do
1043 controllers[#controllers+1] = path
1044 end
1045
1046 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1047 controllers[#controllers+1] = path
1048 end
1049
1050 if indexcache then
1051 local cachedate = fs.stat(indexcache, "mtime")
1052 if cachedate then
1053 local realdate = 0
1054 for _, obj in ipairs(controllers) do
1055 local omtime = fs.stat(obj, "mtime")
1056 realdate = (omtime and omtime > realdate) and omtime or realdate
1057 end
1058
1059 if cachedate > realdate and sys.process.info("uid") == 0 then
1060 assert(
1061 sys.process.info("uid") == fs.stat(indexcache, "uid")
1062 and fs.stat(indexcache, "modestr") == "rw-------",
1063 "Fatal: Indexcache is not sane!"
1064 )
1065
1066 index = loadfile(indexcache)()
1067 return index
1068 end
1069 end
1070 end
1071
1072 index = {}
1073
1074 for _, path in ipairs(controllers) do
1075 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1076 local mod = require(modname)
1077 assert(mod ~= true,
1078 "Invalid controller file found\n" ..
1079 "The file '" .. path .. "' contains an invalid module line.\n" ..
1080 "Please verify whether the module name is set to '" .. modname ..
1081 "' - It must correspond to the file path!")
1082
1083 local idx = mod.index
1084 if type(idx) == "function" then
1085 index[modname] = idx
1086 end
1087 end
1088
1089 if indexcache then
1090 local f = nixio.open(indexcache, "w", 600)
1091 f:writeall(util.get_bytecode(index))
1092 f:close()
1093 end
1094 end
1095
1096 function createtree_json()
1097 local json = require "luci.jsonc"
1098 local tree = {}
1099
1100 local schema = {
1101 action = "table",
1102 auth = "table",
1103 cors = "boolean",
1104 depends = "table",
1105 order = "number",
1106 setgroup = "string",
1107 setuser = "string",
1108 title = "string",
1109 wildcard = "boolean"
1110 }
1111
1112 local files = {}
1113 local fprint = {}
1114 local cachefile
1115
1116 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1117 files[#files+1] = file
1118
1119 if indexcache then
1120 local st = fs.stat(file)
1121 if st then
1122 fprint[#fprint+1] = '%x' % st.ino
1123 fprint[#fprint+1] = '%x' % st.mtime
1124 fprint[#fprint+1] = '%x' % st.size
1125 end
1126 end
1127 end
1128
1129 if indexcache then
1130 cachefile = "%s.%s.json" %{
1131 indexcache,
1132 nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1133 }
1134
1135 local res = json.parse(fs.readfile(cachefile) or "")
1136 if res then
1137 return res
1138 end
1139
1140 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1141 fs.unlink(file)
1142 end
1143 end
1144
1145 for _, file in ipairs(files) do
1146 local data = json.parse(fs.readfile(file) or "")
1147 if type(data) == "table" then
1148 for path, spec in pairs(data) do
1149 if type(spec) == "table" then
1150 local node = tree
1151
1152 for s in path:gmatch("[^/]+") do
1153 if s == "*" then
1154 node.wildcard = true
1155 break
1156 end
1157
1158 node.children = node.children or {}
1159 node.children[s] = node.children[s] or {}
1160 node = node.children[s]
1161 end
1162
1163 if node ~= tree then
1164 for k, t in pairs(schema) do
1165 if type(spec[k]) == t then
1166 node[k] = spec[k]
1167 end
1168 end
1169
1170 node.satisfied = check_depends(spec)
1171 end
1172 end
1173 end
1174 end
1175 end
1176
1177 if cachefile then
1178 fs.writefile(cachefile, json.stringify(tree))
1179 end
1180
1181 return tree
1182 end
1183
1184 -- Build the index before if it does not exist yet.
1185 function createtree()
1186 if not index then
1187 createindex()
1188 end
1189
1190 local ctx = context
1191 local tree = {nodes={}, inreq=true}
1192
1193 ctx.treecache = setmetatable({}, {__mode="v"})
1194 ctx.tree = tree
1195
1196 local scope = setmetatable({}, {__index = luci.dispatcher})
1197
1198 for k, v in pairs(index) do
1199 scope._NAME = k
1200 setfenv(v, scope)
1201 v()
1202 end
1203
1204 return tree
1205 end
1206
1207 function assign(path, clone, title, order)
1208 local obj = node(unpack(path))
1209 obj.nodes = nil
1210 obj.module = nil
1211
1212 obj.title = title
1213 obj.order = order
1214
1215 setmetatable(obj, {__index = _create_node(clone)})
1216
1217 return obj
1218 end
1219
1220 function entry(path, target, title, order)
1221 local c = node(unpack(path))
1222
1223 c.target = target
1224 c.title = title
1225 c.order = order
1226 c.module = getfenv(2)._NAME
1227
1228 return c
1229 end
1230
1231 -- enabling the node.
1232 function get(...)
1233 return _create_node({...})
1234 end
1235
1236 function node(...)
1237 local c = _create_node({...})
1238
1239 c.module = getfenv(2)._NAME
1240 c.auto = nil
1241
1242 return c
1243 end
1244
1245 function lookup(...)
1246 local i, path = nil, {}
1247 for i = 1, select('#', ...) do
1248 local name, arg = nil, tostring(select(i, ...))
1249 for name in arg:gmatch("[^/]+") do
1250 path[#path+1] = name
1251 end
1252 end
1253
1254 for i = #path, 1, -1 do
1255 local node = context.treecache[table.concat(path, ".", 1, i)]
1256 if node and (i == #path or node.leaf) then
1257 return node, build_url(unpack(path))
1258 end
1259 end
1260 end
1261
1262 function _create_node(path)
1263 if #path == 0 then
1264 return context.tree
1265 end
1266
1267 local name = table.concat(path, ".")
1268 local c = context.treecache[name]
1269
1270 if not c then
1271 local last = table.remove(path)
1272 local parent = _create_node(path)
1273
1274 c = {nodes={}, auto=true, inreq=true}
1275
1276 parent.nodes[last] = c
1277 context.treecache[name] = c
1278 end
1279
1280 return c
1281 end
1282
1283 -- Subdispatchers --
1284
1285 function firstchild()
1286 return { type = "firstchild" }
1287 end
1288
1289 function firstnode()
1290 return { type = "firstnode" }
1291 end
1292
1293 function alias(...)
1294 return { type = "alias", req = { ... } }
1295 end
1296
1297 function rewrite(n, ...)
1298 return { type = "rewrite", n = n, req = { ... } }
1299 end
1300
1301 function call(name, ...)
1302 return { type = "call", argv = {...}, name = name }
1303 end
1304
1305 function post_on(params, name, ...)
1306 return {
1307 type = "call",
1308 post = params,
1309 argv = { ... },
1310 name = name
1311 }
1312 end
1313
1314 function post(...)
1315 return post_on(true, ...)
1316 end
1317
1318
1319 function template(name)
1320 return { type = "template", view = name }
1321 end
1322
1323 function view(name)
1324 return { type = "view", view = name }
1325 end
1326
1327
1328 function _cbi(self, ...)
1329 local cbi = require "luci.cbi"
1330 local tpl = require "luci.template"
1331 local http = require "luci.http"
1332 local util = require "luci.util"
1333
1334 local config = self.config or {}
1335 local maps = cbi.load(self.model, ...)
1336
1337 local state = nil
1338
1339 local function has_uci_access(config, level)
1340 local rv = util.ubus("session", "access", {
1341 ubus_rpc_session = context.authsession,
1342 scope = "uci", object = config,
1343 ["function"] = level
1344 })
1345
1346 return (type(rv) == "table" and rv.access == true) or false
1347 end
1348
1349 local i, res
1350 for i, res in ipairs(maps) do
1351 if util.instanceof(res, cbi.SimpleForm) then
1352 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1353 % self.model)
1354
1355 io.stderr:write("please change %s to use the form() action instead.\n"
1356 % table.concat(context.request, "/"))
1357 end
1358
1359 res.flow = config
1360 local cstate = res:parse()
1361 if cstate and (not state or cstate < state) then
1362 state = cstate
1363 end
1364 end
1365
1366 local function _resolve_path(path)
1367 return type(path) == "table" and build_url(unpack(path)) or path
1368 end
1369
1370 if config.on_valid_to and state and state > 0 and state < 2 then
1371 http.redirect(_resolve_path(config.on_valid_to))
1372 return
1373 end
1374
1375 if config.on_changed_to and state and state > 1 then
1376 http.redirect(_resolve_path(config.on_changed_to))
1377 return
1378 end
1379
1380 if config.on_success_to and state and state > 0 then
1381 http.redirect(_resolve_path(config.on_success_to))
1382 return
1383 end
1384
1385 if config.state_handler then
1386 if not config.state_handler(state, maps) then
1387 return
1388 end
1389 end
1390
1391 http.header("X-CBI-State", state or 0)
1392
1393 if not config.noheader then
1394 tpl.render("cbi/header", {state = state})
1395 end
1396
1397 local redirect
1398 local messages
1399 local applymap = false
1400 local pageaction = true
1401 local parsechain = { }
1402 local writable = false
1403
1404 for i, res in ipairs(maps) do
1405 if res.apply_needed and res.parsechain then
1406 local c
1407 for _, c in ipairs(res.parsechain) do
1408 parsechain[#parsechain+1] = c
1409 end
1410 applymap = true
1411 end
1412
1413 if res.redirect then
1414 redirect = redirect or res.redirect
1415 end
1416
1417 if res.pageaction == false then
1418 pageaction = false
1419 end
1420
1421 if res.message then
1422 messages = messages or { }
1423 messages[#messages+1] = res.message
1424 end
1425 end
1426
1427 for i, res in ipairs(maps) do
1428 local is_readable_map = has_uci_access(res.config, "read")
1429 local is_writable_map = has_uci_access(res.config, "write")
1430
1431 writable = writable or is_writable_map
1432
1433 res:render({
1434 firstmap = (i == 1),
1435 redirect = redirect,
1436 messages = messages,
1437 pageaction = pageaction,
1438 parsechain = parsechain,
1439 readable = is_readable_map,
1440 writable = is_writable_map
1441 })
1442 end
1443
1444 if not config.nofooter then
1445 tpl.render("cbi/footer", {
1446 flow = config,
1447 pageaction = pageaction,
1448 redirect = redirect,
1449 state = state,
1450 autoapply = config.autoapply,
1451 trigger_apply = applymap,
1452 writable = writable
1453 })
1454 end
1455 end
1456
1457 function cbi(model, config)
1458 return {
1459 type = "cbi",
1460 post = { ["cbi.submit"] = true },
1461 config = config,
1462 model = model
1463 }
1464 end
1465
1466
1467 function arcombine(trg1, trg2)
1468 return {
1469 type = "arcombine",
1470 env = getfenv(),
1471 targets = {trg1, trg2}
1472 }
1473 end
1474
1475
1476 function _form(self, ...)
1477 local cbi = require "luci.cbi"
1478 local tpl = require "luci.template"
1479 local http = require "luci.http"
1480
1481 local maps = luci.cbi.load(self.model, ...)
1482 local state = nil
1483
1484 local i, res
1485 for i, res in ipairs(maps) do
1486 local cstate = res:parse()
1487 if cstate and (not state or cstate < state) then
1488 state = cstate
1489 end
1490 end
1491
1492 http.header("X-CBI-State", state or 0)
1493 tpl.render("header")
1494 for i, res in ipairs(maps) do
1495 res:render()
1496 end
1497 tpl.render("footer")
1498 end
1499
1500 function form(model)
1501 return {
1502 type = "form",
1503 post = { ["cbi.submit"] = true },
1504 model = model
1505 }
1506 end
1507
1508 translate = i18n.translate
1509
1510 -- This function does not actually translate the given argument but
1511 -- is used by build/i18n-scan.pl to find translatable entries.
1512 function _(text)
1513 return text
1514 end