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.
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local xml = require "luci.xml"
9 local http = require "luci.http"
10 local nixio = require "nixio", require "nixio.util"
12 module("luci.dispatcher", package.seeall)
13 context = util.threadlocal()
14 uci = require "luci.model.uci"
15 i18n = require "luci.i18n"
21 local function check_fs_depends(spec)
22 local fs = require "nixio.fs"
24 for path, kind in pairs(spec) do
25 if kind == "directory" then
27 for entry in (fs.dir(path) or function() end) do
34 elseif kind == "executable" then
35 if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then
38 elseif kind == "file" then
39 if fs.stat(path, "type") ~= "reg" then
42 elseif kind == "absent" then
43 if fs.stat(path, "type") then
52 local function check_uci_depends_options(conf, s, opts)
53 local uci = require "luci.model.uci"
55 if type(opts) == "string" then
56 return (s[".type"] == opts)
57 elseif opts == true then
58 for option, value in pairs(s) do
59 if option:byte(1) ~= 46 then
63 elseif type(opts) == "table" then
64 for option, value in pairs(opts) do
65 local sval = s[option]
66 if type(sval) == "table" then
68 for _, v in ipairs(sval) do
77 elseif value == true then
92 local function check_uci_depends_section(conf, sect)
93 local uci = require "luci.model.uci"
95 for section, options in pairs(sect) do
96 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
99 uci:foreach(conf, stype, function(s)
100 if check_uci_depends_options(conf, s, options) then
109 local s = uci:get_all(conf, section)
110 if not s or not check_uci_depends_options(conf, s, options) then
119 local function check_uci_depends(conf)
120 local uci = require "luci.model.uci"
122 for config, values in pairs(conf) do
123 if values == true then
125 uci:foreach(config, nil, function(s)
132 elseif type(values) == "table" then
133 if not check_uci_depends_section(config, values) then
142 local function check_acl_depends(require_groups, groups)
143 if type(require_groups) == "table" and #require_groups > 0 then
144 local writable = false
146 for _, group in ipairs(require_groups) do
149 if type(groups) == "table" and type(groups[group]) == "table" then
150 for _, perm in ipairs(groups[group]) do
151 if perm == "read" then
153 elseif perm == "write" then
158 if not read and not write then
171 local function check_depends(spec)
172 if type(spec.depends) ~= "table" then
176 if type(spec.depends.fs) == "table" then
177 local satisfied = false
178 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
179 for _, alternative in ipairs(alternatives) do
180 if check_fs_depends(alternative) then
185 if not satisfied then
190 if type(spec.depends.uci) == "table" then
191 local satisfied = false
192 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
193 for _, alternative in ipairs(alternatives) do
194 if check_uci_depends(alternative) then
199 if not satisfied then
207 local function target_to_json(target, module)
210 if target.type == "call" then
214 ["function"] = target.name,
215 ["parameters"] = target.argv
217 elseif target.type == "view" then
220 ["path"] = target.view
222 elseif target.type == "template" then
224 ["type"] = "template",
225 ["path"] = target.view
227 elseif target.type == "cbi" then
230 ["path"] = target.model,
231 ["config"] = target.config
233 elseif target.type == "form" then
236 ["path"] = target.model
238 elseif target.type == "firstchild" then
240 ["type"] = "firstchild"
242 elseif target.type == "firstnode" then
244 ["type"] = "firstchild",
247 elseif target.type == "arcombine" then
248 if type(target.targets) == "table" then
250 ["type"] = "arcombine",
252 target_to_json(target.targets[1], module),
253 target_to_json(target.targets[2], module)
257 elseif target.type == "alias" then
260 ["path"] = table.concat(target.req, "/")
262 elseif target.type == "rewrite" then
264 ["type"] = "rewrite",
265 ["path"] = table.concat(target.req, "/"),
266 ["remove"] = target.n
270 if target.post and action then
271 action.post = target.post
277 local function tree_to_json(node, json)
278 local fs = require "nixio.fs"
279 local util = require "luci.util"
281 if type(node.nodes) == "table" then
282 for subname, subnode in pairs(node.nodes) do
284 title = xml.striptags(subnode.title),
285 order = subnode.order
296 if subnode.setuser then
297 spec.setuser = subnode.setuser
300 if subnode.setgroup then
301 spec.setgroup = subnode.setgroup
304 if type(subnode.target) == "table" then
305 spec.action = target_to_json(subnode.target, subnode.module)
308 if type(subnode.file_depends) == "table" then
309 for _, v in ipairs(subnode.file_depends) do
310 spec.depends = spec.depends or {}
311 spec.depends.fs = spec.depends.fs or {}
313 local ft = fs.stat(v, "type")
315 spec.depends.fs[v] = "directory"
316 elseif v:match("/s?bin/") then
317 spec.depends.fs[v] = "executable"
319 spec.depends.fs[v] = "file"
324 if type(subnode.uci_depends) == "table" then
325 for k, v in pairs(subnode.uci_depends) do
326 spec.depends = spec.depends or {}
327 spec.depends.uci = spec.depends.uci or {}
328 spec.depends.uci[k] = v
332 if type(subnode.acl_depends) == "table" then
333 for _, acl in ipairs(subnode.acl_depends) do
334 spec.depends = spec.depends or {}
335 spec.depends.acl = spec.depends.acl or {}
336 spec.depends.acl[#spec.depends.acl + 1] = acl
340 if (subnode.sysauth_authenticator ~= nil) or
341 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
343 if subnode.sysauth_authenticator == "htmlauth" then
346 methods = { "cookie:sysauth" }
348 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
351 methods = { "query:auth", "cookie:sysauth" }
353 elseif subnode.module == "luci.controller.admin.uci" then
356 methods = { "param:sid" }
359 elseif subnode.sysauth == false then
363 if not spec.action then
367 spec.satisfied = check_depends(spec)
368 json.children = json.children or {}
369 json.children[subname] = tree_to_json(subnode, spec)
376 function build_url(...)
378 local url = { http.getenv("SCRIPT_NAME") or "" }
381 for _, p in ipairs(path) do
382 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
392 return table.concat(url, "")
396 function error404(message)
397 http.status(404, "Not Found")
398 message = message or "Not Found"
400 local function render()
401 local template = require "luci.template"
402 template.render("error404", {message=message})
405 if not util.copcall(render) then
406 http.prepare_content("text/plain")
413 function error500(message)
415 if not context.template_header_sent then
416 http.status(500, "Internal Server Error")
417 http.prepare_content("text/plain")
420 require("luci.template")
421 if not util.copcall(luci.template.render, "error500", {message=message}) then
422 http.prepare_content("text/plain")
429 local function determine_request_language()
430 local conf = require "luci.config"
431 assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
433 local lang = conf.main.lang or "auto"
434 if lang == "auto" then
435 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
436 for aclang in aclang:gmatch("[%w_-]+") do
437 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
438 if country and culture then
439 local cc = "%s_%s" %{ country, culture:lower() }
440 if conf.languages[cc] then
443 elseif conf.languages[country] then
447 elseif conf.languages[aclang] then
454 if lang == "auto" then
458 i18n.setlanguage(lang)
461 function httpdispatch(request, prefix)
462 http.context.request = request
467 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
470 for _, node in ipairs(prefix) do
476 for node in pathinfo:gmatch("[^/%z]+") do
480 determine_request_language()
482 local stat, err = util.coxpcall(function()
483 dispatch(context.request)
488 --context._disable_memtrace()
491 local function require_post_security(target, args)
492 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
493 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
496 if type(target) == "table" then
497 if type(target.post) == "table" then
498 local param_name, required_val, request_val
500 for param_name, required_val in pairs(target.post) do
501 request_val = http.formvalue(param_name)
503 if (type(required_val) == "string" and
504 request_val ~= required_val) or
505 (required_val == true and request_val == nil)
514 return (target.post == true)
520 function test_post_security()
521 if http.getenv("REQUEST_METHOD") ~= "POST" then
522 http.status(405, "Method Not Allowed")
523 http.header("Allow", "POST")
527 if http.formvalue("token") ~= context.authtoken then
528 http.status(403, "Forbidden")
529 luci.template.render("csrftoken")
536 local function session_retrieve(sid, allowed_users)
537 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
538 local sacl = util.ubus("session", "access", { ubus_rpc_session = sid })
540 if type(sdat) == "table" and
541 type(sdat.values) == "table" and
542 type(sdat.values.token) == "string" and
543 (not allowed_users or
544 util.contains(allowed_users, sdat.values.username))
546 uci:set_session_id(sid)
547 return sid, sdat.values, type(sacl) == "table" and sacl or {}
553 local function session_setup(user, pass)
554 local login = util.ubus("session", "login", {
557 timeout = tonumber(luci.config.sauth.sessiontime)
560 local rp = context.requestpath
561 and table.concat(context.requestpath, "/") or ""
563 if type(login) == "table" and
564 type(login.ubus_rpc_session) == "string"
566 util.ubus("session", "set", {
567 ubus_rpc_session = login.ubus_rpc_session,
568 values = { token = sys.uniqueid(16) }
570 nixio.syslog("info", tostring("luci: accepted login on /%s for %s from %s\n"
571 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" }))
573 return session_retrieve(login.ubus_rpc_session)
575 nixio.syslog("info", tostring("luci: failed login on /%s for %s from %s\n"
576 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" }))
579 local function check_authentication(method)
580 local auth_type, auth_param = method:match("^(%w+):(.+)$")
583 if auth_type == "cookie" then
584 sid = http.getcookie(auth_param)
585 elseif auth_type == "param" then
586 sid = http.formvalue(auth_param)
587 elseif auth_type == "query" then
588 sid = http.formvalue(auth_param, true)
591 return session_retrieve(sid)
594 local function merge_trees(node_a, node_b)
595 for k, v in pairs(node_b) do
596 if k == "children" then
597 node_a.children = node_a.children or {}
599 for name, spec in pairs(v) do
600 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
607 if type(node_a.action) == "table" and
608 node_a.action.type == "firstchild" and
609 node_a.children == nil
611 node_a.satisfied = false
617 local function apply_tree_acls(node, acl)
618 if type(node.children) == "table" then
619 for _, child in pairs(node.children) do
620 apply_tree_acls(child, acl)
625 if type(node.depends) == "table" then
626 perm = check_acl_depends(node.depends.acl, acl["access-group"])
632 node.satisfied = false
633 elseif perm == false then
638 function menu_json(acl)
639 local tree = context.tree or createtree()
640 local lua_tree = tree_to_json(tree, {
642 ["type"] = "firstchild",
647 local json_tree = createtree_json()
648 local menu_tree = merge_trees(lua_tree, json_tree)
651 apply_tree_acls(menu_tree, acl)
657 local function init_template_engine(ctx)
658 local tpl = require "luci.template"
659 local media = luci.config.main.mediaurlbase
661 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
663 for name, theme in pairs(luci.config.themes) do
664 if name:sub(1,1) ~= "." and pcall(tpl.Template,
665 "themes/%s/header" % fs.basename(theme)) then
669 assert(media, "No valid theme found")
672 local function _ifattr(cond, key, val, noescape)
674 local env = getfenv(3)
675 local scope = (type(env.self) == "table") and env.self
676 if type(val) == "table" then
677 if not next(val) then
680 val = util.serialize_json(val)
684 val = tostring(val or
685 (type(env[key]) ~= "function" and env[key]) or
686 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
688 if noescape ~= true then
689 val = xml.pcdata(val)
692 return string.format(' %s="%s"', tostring(key), val)
698 tpl.context.viewns = setmetatable({
700 include = function(name) tpl.Template(name):render(getfenv(2)) end;
701 translate = i18n.translate;
702 translatef = i18n.translatef;
703 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
704 striptags = xml.striptags;
707 theme = fs.basename(media);
708 resource = luci.config.main.resourcebase;
709 ifattr = function(...) return _ifattr(...) end;
710 attr = function(...) return _ifattr(true, ...) end;
712 }, {__index=function(tbl, key)
713 if key == "controller" then
715 elseif key == "REQUEST_URI" then
716 return build_url(unpack(ctx.requestpath))
717 elseif key == "FULL_REQUEST_URI" then
718 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
719 local query = http.getenv("QUERY_STRING")
720 if query and #query > 0 then
724 return table.concat(url, "")
725 elseif key == "token" then
728 return rawget(tbl, key) or _G[key]
735 local function is_authenticated(auth)
736 if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
737 local sid, sdat, sacl
738 for _, method in ipairs(auth.methods) do
739 sid, sdat, sacl = check_authentication(method)
741 if sid and sdat and sacl then
742 return sid, sdat, sacl
748 local function ctx_append(ctx, name, node)
749 ctx.path = ctx.path or {}
750 ctx.path[#ctx.path + 1] = name
752 ctx.acls = ctx.acls or {}
754 local acls = (type(node.depends) == "table" and type(node.depends.acl) == "table") and node.depends.acl or {}
755 for _, acl in ipairs(acls) do
759 ctx.auth = node.auth or ctx.auth
760 ctx.cors = node.cors or ctx.cors
761 ctx.suid = node.setuser or ctx.suid
762 ctx.sgid = node.setgroup or ctx.sgid
767 local function node_weight(node)
768 local weight = node.order or 9999
770 if weight > 9999 then
774 if type(node.auth) == "table" and node.auth.login then
775 weight = weight + 10000
781 local function resolve_firstchild(node, sacl, login_allowed, ctx)
782 local candidate = nil
783 local candidate_ctx = nil
785 for name, child in pairs(node.children) do
786 if child.satisfied then
789 _, _, sacl = is_authenticated(node.auth)
792 local cacl = (type(child.depends) == "table") and child.depends.acl or nil
793 local login = login_allowed or (type(child.auth) == "table" and child.auth.login)
794 if login or check_acl_depends(cacl, sacl and sacl["access-group"]) ~= nil then
795 if child.title and type(child.action) == "table" then
796 local child_ctx = ctx_append(util.clone(ctx, true), name, child)
797 if child.action.type == "firstchild" then
798 if not candidate or node_weight(candidate) > node_weight(child) then
799 local have_grandchild = resolve_firstchild(child, sacl, login, child_ctx)
800 if have_grandchild then
802 candidate_ctx = child_ctx
805 elseif not child.firstchild_ineligible then
806 if not candidate or node_weight(candidate) > node_weight(child) then
808 candidate_ctx = child_ctx
817 for k, v in pairs(candidate_ctx) do
827 local function resolve_page(tree, request_path)
833 for i, s in ipairs(request_path) do
834 node = node.children and node.children[s]
836 if not node or not node.satisfied then
840 ctx_append(ctx, s, node)
844 _, _, sacl = is_authenticated(node.auth)
847 if not login and type(node.auth) == "table" and node.auth.login then
851 if node.wildcard then
852 ctx.request_args = {}
853 ctx.request_path = util.clone(ctx.path, true)
855 for j = i + 1, #request_path do
856 ctx.request_path[j] = request_path[j]
857 ctx.request_args[j - i] = request_path[j]
864 if node and type(node.action) == "table" and node.action.type == "firstchild" then
865 resolve_firstchild(node, sacl, login, ctx)
868 ctx.acls = ctx.acls or {}
869 ctx.path = ctx.path or {}
870 ctx.request_args = ctx.request_args or {}
871 ctx.request_path = ctx.request_path or util.clone(request_path, true)
875 for _, s in ipairs(ctx.path or {}) do
876 node = node.children[s]
877 assert(node, "Internal node resolve error")
883 function dispatch(request)
884 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
887 local auth, cors, suid, sgid
888 local menu = menu_json()
889 local page, lookup_ctx = resolve_page(menu, request)
890 local action = (page and type(page.action) == "table") and page.action or {}
892 local tpl = init_template_engine(ctx)
894 ctx.args = lookup_ctx.request_args
895 ctx.path = lookup_ctx.path
896 ctx.dispatched = page
898 ctx.requestpath = ctx.requestpath or lookup_ctx.request_path
899 ctx.requestargs = ctx.requestargs or lookup_ctx.request_args
900 ctx.requested = ctx.requested or page
902 if type(lookup_ctx.auth) == "table" and next(lookup_ctx.auth) then
903 local sid, sdat, sacl = is_authenticated(lookup_ctx.auth)
905 if not (sid and sdat and sacl) and lookup_ctx.auth.login then
906 local user = http.getenv("HTTP_AUTH_USER")
907 local pass = http.getenv("HTTP_AUTH_PASS")
909 if user == nil and pass == nil then
910 user = http.formvalue("luci_username")
911 pass = http.formvalue("luci_password")
914 if user and pass then
915 sid, sdat, sacl = session_setup(user, pass)
921 http.status(403, "Forbidden")
922 http.header("X-LuCI-Login-Required", "yes")
924 local scope = { duser = "root", fuser = user }
925 local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope)
929 return tpl.render("sysauth", scope)
932 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
933 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
936 http.redirect(build_url(unpack(ctx.requestpath)))
940 if not sid or not sdat or not sacl then
941 http.status(403, "Forbidden")
942 http.header("X-LuCI-Login-Required", "yes")
946 ctx.authsession = sid
947 ctx.authtoken = sdat.token
948 ctx.authuser = sdat.username
952 if #lookup_ctx.acls > 0 then
953 local perm = check_acl_depends(lookup_ctx.acls, ctx.authacl and ctx.authacl["access-group"])
955 http.status(403, "Forbidden")
960 page.readonly = not perm
964 if action.type == "arcombine" then
965 action = (#lookup_ctx.request_args > 0) and action.targets[2] or action.targets[1]
968 if lookup_ctx.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
969 luci.http.status(200, "OK")
970 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
971 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
975 if require_post_security(action) then
976 if not test_post_security() then
981 if lookup_ctx.sgid then
982 sys.process.setgroup(lookup_ctx.sgid)
985 if lookup_ctx.suid then
986 sys.process.setuser(lookup_ctx.suid)
989 if action.type == "view" then
990 tpl.render("view", { view = action.path })
992 elseif action.type == "call" then
993 local ok, mod = util.copcall(require, action.module)
999 local func = mod[action["function"]]
1002 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
1004 assert(type(func) == "function",
1005 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
1006 'of type "' .. type(func) .. '".')
1008 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
1009 for _, s in ipairs(lookup_ctx.request_args) do
1013 local ok, err = util.copcall(func, unpack(argv))
1018 --elseif action.type == "firstchild" then
1019 -- tpl.render("empty_node_placeholder", getfenv(1))
1021 elseif action.type == "alias" then
1022 local sub_request = {}
1023 for name in action.path:gmatch("[^/]+") do
1024 sub_request[#sub_request + 1] = name
1027 for _, s in ipairs(lookup_ctx.request_args) do
1028 sub_request[#sub_request + 1] = s
1031 dispatch(sub_request)
1033 elseif action.type == "rewrite" then
1034 local sub_request = { unpack(request) }
1035 for i = 1, action.remove do
1036 table.remove(sub_request, 1)
1040 for s in action.path:gmatch("[^/]+") do
1041 table.insert(sub_request, n, s)
1045 for _, s in ipairs(lookup_ctx.request_args) do
1046 sub_request[#sub_request + 1] = s
1049 dispatch(sub_request)
1051 elseif action.type == "template" then
1052 tpl.render(action.path, getfenv(1))
1054 elseif action.type == "cbi" then
1055 _cbi({ config = action.config, model = action.path }, unpack(lookup_ctx.request_args))
1057 elseif action.type == "form" then
1058 _form({ model = action.path }, unpack(lookup_ctx.request_args))
1061 if not menu.children then
1062 error404("No root node was registered, this usually happens if no module was installed.\n" ..
1063 "Install luci-mod-admin-full and retry. " ..
1064 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
1066 error404("No page is registered at '/" .. table.concat(lookup_ctx.request_path, "/") .. "'.\n" ..
1067 "If this url belongs to an extension, make sure it is properly installed.\n" ..
1068 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
1073 local function hash_filelist(files)
1077 for i, file in ipairs(files) do
1078 local st = fs.stat(file)
1080 fprint[n + 1] = '%x' % st.ino
1081 fprint[n + 2] = '%x' % st.mtime
1082 fprint[n + 3] = '%x' % st.size
1087 return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1090 local function read_cachefile(file, reader)
1091 local euid = sys.process.info("uid")
1092 local fuid = fs.stat(file, "uid")
1093 local mode = fs.stat(file, "modestr")
1095 if euid ~= fuid or mode ~= "rw-------" then
1102 function createindex()
1103 local controllers = { }
1104 local base = "%s/controller/" % util.libpath()
1107 for path in (fs.glob("%s*.lua" % base) or function() end) do
1108 controllers[#controllers+1] = path
1111 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1112 controllers[#controllers+1] = path
1118 cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) }
1120 local res = read_cachefile(cachefile, function(path) return loadfile(path)() end)
1126 for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do
1133 for _, path in ipairs(controllers) do
1134 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1135 local mod = require(modname)
1137 "Invalid controller file found\n" ..
1138 "The file '" .. path .. "' contains an invalid module line.\n" ..
1139 "Please verify whether the module name is set to '" .. modname ..
1140 "' - It must correspond to the file path!")
1142 local idx = mod.index
1143 if type(idx) == "function" then
1144 index[modname] = idx
1149 local f = nixio.open(cachefile, "w", 600)
1150 f:writeall(util.get_bytecode(index))
1155 function createtree_json()
1156 local json = require "luci.jsonc"
1165 setgroup = "string",
1168 wildcard = "boolean",
1169 firstchild_ineligible = "boolean"
1175 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1176 files[#files+1] = file
1180 cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) }
1182 local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end)
1187 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1192 for _, file in ipairs(files) do
1193 local data = json.parse(fs.readfile(file) or "")
1194 if type(data) == "table" then
1195 for path, spec in pairs(data) do
1196 if type(spec) == "table" then
1199 for s in path:gmatch("[^/]+") do
1201 node.wildcard = true
1205 node.children = node.children or {}
1206 node.children[s] = node.children[s] or {}
1207 node = node.children[s]
1210 if node ~= tree then
1211 for k, t in pairs(schema) do
1212 if type(spec[k]) == t then
1217 node.satisfied = check_depends(spec)
1225 local f = nixio.open(cachefile, "w", 600)
1226 f:writeall(json.stringify(tree))
1233 -- Build the index before if it does not exist yet.
1234 function createtree()
1240 local tree = {nodes={}, inreq=true}
1242 ctx.treecache = setmetatable({}, {__mode="v"})
1245 local scope = setmetatable({}, {__index = luci.dispatcher})
1247 for k, v in pairs(index) do
1256 function assign(path, clone, title, order)
1257 local obj = node(unpack(path))
1264 setmetatable(obj, {__index = _create_node(clone)})
1269 function entry(path, target, title, order)
1270 local c = node(unpack(path))
1275 c.module = getfenv(2)._NAME
1280 -- enabling the node.
1282 return _create_node({...})
1286 local c = _create_node({...})
1288 c.module = getfenv(2)._NAME
1294 function lookup(...)
1295 local i, path = nil, {}
1296 for i = 1, select('#', ...) do
1297 local name, arg = nil, tostring(select(i, ...))
1298 for name in arg:gmatch("[^/]+") do
1299 path[#path+1] = name
1303 for i = #path, 1, -1 do
1304 local node = context.treecache[table.concat(path, ".", 1, i)]
1305 if node and (i == #path or node.leaf) then
1306 return node, build_url(unpack(path))
1311 function _create_node(path)
1316 local name = table.concat(path, ".")
1317 local c = context.treecache[name]
1320 local last = table.remove(path)
1321 local parent = _create_node(path)
1323 c = {nodes={}, auto=true, inreq=true}
1325 parent.nodes[last] = c
1326 context.treecache[name] = c
1332 -- Subdispatchers --
1334 function firstchild()
1335 return { type = "firstchild" }
1338 function firstnode()
1339 return { type = "firstnode" }
1343 return { type = "alias", req = { ... } }
1346 function rewrite(n, ...)
1347 return { type = "rewrite", n = n, req = { ... } }
1350 function call(name, ...)
1351 return { type = "call", argv = {...}, name = name }
1354 function post_on(params, name, ...)
1364 return post_on(true, ...)
1368 function template(name)
1369 return { type = "template", view = name }
1373 return { type = "view", view = name }
1377 function _cbi(self, ...)
1378 local cbi = require "luci.cbi"
1379 local tpl = require "luci.template"
1380 local http = require "luci.http"
1381 local util = require "luci.util"
1383 local config = self.config or {}
1384 local maps = cbi.load(self.model, ...)
1388 local function has_uci_access(config, level)
1389 local rv = util.ubus("session", "access", {
1390 ubus_rpc_session = context.authsession,
1391 scope = "uci", object = config,
1392 ["function"] = level
1395 return (type(rv) == "table" and rv.access == true) or false
1399 for i, res in ipairs(maps) do
1400 if util.instanceof(res, cbi.SimpleForm) then
1401 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1404 io.stderr:write("please change %s to use the form() action instead.\n"
1405 % table.concat(context.request, "/"))
1409 local cstate = res:parse()
1410 if cstate and (not state or cstate < state) then
1415 local function _resolve_path(path)
1416 return type(path) == "table" and build_url(unpack(path)) or path
1419 if config.on_valid_to and state and state > 0 and state < 2 then
1420 http.redirect(_resolve_path(config.on_valid_to))
1424 if config.on_changed_to and state and state > 1 then
1425 http.redirect(_resolve_path(config.on_changed_to))
1429 if config.on_success_to and state and state > 0 then
1430 http.redirect(_resolve_path(config.on_success_to))
1434 if config.state_handler then
1435 if not config.state_handler(state, maps) then
1440 http.header("X-CBI-State", state or 0)
1442 if not config.noheader then
1443 tpl.render("cbi/header", {state = state})
1448 local applymap = false
1449 local pageaction = true
1450 local parsechain = { }
1451 local writable = false
1453 for i, res in ipairs(maps) do
1454 if res.apply_needed and res.parsechain then
1456 for _, c in ipairs(res.parsechain) do
1457 parsechain[#parsechain+1] = c
1462 if res.redirect then
1463 redirect = redirect or res.redirect
1466 if res.pageaction == false then
1471 messages = messages or { }
1472 messages[#messages+1] = res.message
1476 for i, res in ipairs(maps) do
1477 local is_readable_map = has_uci_access(res.config, "read")
1478 local is_writable_map = has_uci_access(res.config, "write")
1480 writable = writable or is_writable_map
1483 firstmap = (i == 1),
1484 redirect = redirect,
1485 messages = messages,
1486 pageaction = pageaction,
1487 parsechain = parsechain,
1488 readable = is_readable_map,
1489 writable = is_writable_map
1493 if not config.nofooter then
1494 tpl.render("cbi/footer", {
1496 pageaction = pageaction,
1497 redirect = redirect,
1499 autoapply = config.autoapply,
1500 trigger_apply = applymap,
1506 function cbi(model, config)
1509 post = { ["cbi.submit"] = true },
1516 function arcombine(trg1, trg2)
1520 targets = {trg1, trg2}
1525 function _form(self, ...)
1526 local cbi = require "luci.cbi"
1527 local tpl = require "luci.template"
1528 local http = require "luci.http"
1530 local maps = luci.cbi.load(self.model, ...)
1534 for i, res in ipairs(maps) do
1535 local cstate = res:parse()
1536 if cstate and (not state or cstate < state) then
1541 http.header("X-CBI-State", state or 0)
1542 tpl.render("header")
1543 for i, res in ipairs(maps) do
1546 tpl.render("footer")
1549 function form(model)
1552 post = { ["cbi.submit"] = true },
1557 translate = i18n.translate
1559 -- This function does not actually translate the given argument but
1560 -- is used by build/i18n-scan.pl to find translatable entries.