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
48 local function check_uci_depends_options(conf, s, opts)
49 local uci = require "luci.model.uci"
51 if type(opts) == "string" then
52 return (s[".type"] == opts)
53 elseif opts == true then
54 for option, value in pairs(s) do
55 if option:byte(1) ~= 46 then
59 elseif type(opts) == "table" then
60 for option, value in pairs(opts) do
61 local sval = s[option]
62 if type(sval) == "table" then
64 for _, v in ipairs(sval) do
73 elseif value == true then
88 local function check_uci_depends_section(conf, sect)
89 local uci = require "luci.model.uci"
91 for section, options in pairs(sect) do
92 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
95 uci:foreach(conf, stype, function(s)
96 if check_uci_depends_options(conf, s, options) then
105 local s = uci:get_all(conf, section)
106 if not s or not check_uci_depends_options(conf, s, options) then
115 local function check_uci_depends(conf)
116 local uci = require "luci.model.uci"
118 for config, values in pairs(conf) do
119 if values == true then
121 uci:foreach(config, nil, function(s)
128 elseif type(values) == "table" then
129 if not check_uci_depends_section(config, values) then
138 local function check_acl_depends(require_groups, groups)
139 if type(require_groups) == "table" and #require_groups > 0 then
140 local writable = false
142 for _, group in ipairs(require_groups) do
145 if type(groups) == "table" and type(groups[group]) == "table" then
146 for _, perm in ipairs(groups[group]) do
147 if perm == "read" then
149 elseif perm == "write" then
154 if not read and not write then
167 local function check_depends(spec)
168 if type(spec.depends) ~= "table" then
172 if type(spec.depends.fs) == "table" then
173 local satisfied = false
174 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
175 for _, alternative in ipairs(alternatives) do
176 if check_fs_depends(alternative) then
181 if not satisfied then
186 if type(spec.depends.uci) == "table" then
187 local satisfied = false
188 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
189 for _, alternative in ipairs(alternatives) do
190 if check_uci_depends(alternative) then
195 if not satisfied then
203 local function target_to_json(target, module)
206 if target.type == "call" then
210 ["function"] = target.name,
211 ["parameters"] = target.argv
213 elseif target.type == "view" then
216 ["path"] = target.view
218 elseif target.type == "template" then
220 ["type"] = "template",
221 ["path"] = target.view
223 elseif target.type == "cbi" then
226 ["path"] = target.model,
227 ["config"] = target.config
229 elseif target.type == "form" then
232 ["path"] = target.model
234 elseif target.type == "firstchild" then
236 ["type"] = "firstchild"
238 elseif target.type == "firstnode" then
240 ["type"] = "firstchild",
243 elseif target.type == "arcombine" then
244 if type(target.targets) == "table" then
246 ["type"] = "arcombine",
248 target_to_json(target.targets[1], module),
249 target_to_json(target.targets[2], module)
253 elseif target.type == "alias" then
256 ["path"] = table.concat(target.req, "/")
258 elseif target.type == "rewrite" then
260 ["type"] = "rewrite",
261 ["path"] = table.concat(target.req, "/"),
262 ["remove"] = target.n
266 if target.post and action then
267 action.post = target.post
273 local function tree_to_json(node, json)
274 local fs = require "nixio.fs"
275 local util = require "luci.util"
277 if type(node.nodes) == "table" then
278 for subname, subnode in pairs(node.nodes) do
280 title = xml.striptags(subnode.title),
281 order = subnode.order
292 if subnode.setuser then
293 spec.setuser = subnode.setuser
296 if subnode.setgroup then
297 spec.setgroup = subnode.setgroup
300 if type(subnode.target) == "table" then
301 spec.action = target_to_json(subnode.target, subnode.module)
304 if type(subnode.file_depends) == "table" then
305 for _, v in ipairs(subnode.file_depends) do
306 spec.depends = spec.depends or {}
307 spec.depends.fs = spec.depends.fs or {}
309 local ft = fs.stat(v, "type")
311 spec.depends.fs[v] = "directory"
312 elseif v:match("/s?bin/") then
313 spec.depends.fs[v] = "executable"
315 spec.depends.fs[v] = "file"
320 if type(subnode.uci_depends) == "table" then
321 for k, v in pairs(subnode.uci_depends) do
322 spec.depends = spec.depends or {}
323 spec.depends.uci = spec.depends.uci or {}
324 spec.depends.uci[k] = v
328 if type(subnode.acl_depends) == "table" then
329 for _, acl in ipairs(subnode.acl_depends) do
330 spec.depends = spec.depends or {}
331 spec.depends.acl = spec.depends.acl or {}
332 spec.depends.acl[#spec.depends.acl + 1] = acl
336 if (subnode.sysauth_authenticator ~= nil) or
337 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
339 if subnode.sysauth_authenticator == "htmlauth" then
342 methods = { "cookie:sysauth" }
344 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
347 methods = { "query:auth", "cookie:sysauth" }
349 elseif subnode.module == "luci.controller.admin.uci" then
352 methods = { "param:sid" }
355 elseif subnode.sysauth == false then
359 if not spec.action then
363 spec.satisfied = check_depends(spec)
364 json.children = json.children or {}
365 json.children[subname] = tree_to_json(subnode, spec)
372 function build_url(...)
374 local url = { http.getenv("SCRIPT_NAME") or "" }
377 for _, p in ipairs(path) do
378 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
388 return table.concat(url, "")
392 function error404(message)
393 http.status(404, "Not Found")
394 message = message or "Not Found"
396 local function render()
397 local template = require "luci.template"
398 template.render("error404", {message=message})
401 if not util.copcall(render) then
402 http.prepare_content("text/plain")
409 function error500(message)
411 if not context.template_header_sent then
412 http.status(500, "Internal Server Error")
413 http.prepare_content("text/plain")
416 require("luci.template")
417 if not util.copcall(luci.template.render, "error500", {message=message}) then
418 http.prepare_content("text/plain")
425 local function determine_request_language()
426 local conf = require "luci.config"
427 assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
429 local lang = conf.main.lang or "auto"
430 if lang == "auto" then
431 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
432 for aclang in aclang:gmatch("[%w_-]+") do
433 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
434 if country and culture then
435 local cc = "%s_%s" %{ country, culture:lower() }
436 if conf.languages[cc] then
439 elseif conf.languages[country] then
443 elseif conf.languages[aclang] then
450 if lang == "auto" then
454 i18n.setlanguage(lang)
457 function httpdispatch(request, prefix)
458 http.context.request = request
463 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
466 for _, node in ipairs(prefix) do
472 for node in pathinfo:gmatch("[^/%z]+") do
476 determine_request_language()
478 local stat, err = util.coxpcall(function()
479 dispatch(context.request)
484 --context._disable_memtrace()
487 local function require_post_security(target, args)
488 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
489 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
492 if type(target) == "table" then
493 if type(target.post) == "table" then
494 local param_name, required_val, request_val
496 for param_name, required_val in pairs(target.post) do
497 request_val = http.formvalue(param_name)
499 if (type(required_val) == "string" and
500 request_val ~= required_val) or
501 (required_val == true and request_val == nil)
510 return (target.post == true)
516 function test_post_security()
517 if http.getenv("REQUEST_METHOD") ~= "POST" then
518 http.status(405, "Method Not Allowed")
519 http.header("Allow", "POST")
523 if http.formvalue("token") ~= context.authtoken then
524 http.status(403, "Forbidden")
525 luci.template.render("csrftoken")
532 local function session_retrieve(sid, allowed_users)
533 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
534 local sacl = util.ubus("session", "access", { ubus_rpc_session = sid })
536 if type(sdat) == "table" and
537 type(sdat.values) == "table" and
538 type(sdat.values.token) == "string" and
539 (not allowed_users or
540 util.contains(allowed_users, sdat.values.username))
542 uci:set_session_id(sid)
543 return sid, sdat.values, type(sacl) == "table" and sacl or {}
549 local function session_setup(user, pass)
550 local login = util.ubus("session", "login", {
553 timeout = tonumber(luci.config.sauth.sessiontime)
556 local rp = context.requestpath
557 and table.concat(context.requestpath, "/") or ""
559 if type(login) == "table" and
560 type(login.ubus_rpc_session) == "string"
562 util.ubus("session", "set", {
563 ubus_rpc_session = login.ubus_rpc_session,
564 values = { token = sys.uniqueid(16) }
566 nixio.syslog("info", tostring("luci: accepted login on /%s for %s from %s\n"
567 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" }))
569 return session_retrieve(login.ubus_rpc_session)
571 nixio.syslog("info", tostring("luci: failed login on /%s for %s from %s\n"
572 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" }))
575 local function check_authentication(method)
576 local auth_type, auth_param = method:match("^(%w+):(.+)$")
579 if auth_type == "cookie" then
580 sid = http.getcookie(auth_param)
581 elseif auth_type == "param" then
582 sid = http.formvalue(auth_param)
583 elseif auth_type == "query" then
584 sid = http.formvalue(auth_param, true)
587 return session_retrieve(sid)
590 local function merge_trees(node_a, node_b)
591 for k, v in pairs(node_b) do
592 if k == "children" then
593 node_a.children = node_a.children or {}
595 for name, spec in pairs(v) do
596 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
603 if type(node_a.action) == "table" and
604 node_a.action.type == "firstchild" and
605 node_a.children == nil
607 node_a.satisfied = false
613 local function apply_tree_acls(node, acl)
614 if type(node.children) == "table" then
615 for _, child in pairs(node.children) do
616 apply_tree_acls(child, acl)
621 if type(node.depends) == "table" then
622 perm = check_acl_depends(node.depends.acl, acl["access-group"])
628 node.satisfied = false
629 elseif perm == false then
634 function menu_json(acl)
635 local tree = context.tree or createtree()
636 local lua_tree = tree_to_json(tree, {
638 ["type"] = "firstchild",
643 local json_tree = createtree_json()
644 local menu_tree = merge_trees(lua_tree, json_tree)
647 apply_tree_acls(menu_tree, acl)
653 local function init_template_engine(ctx)
654 local tpl = require "luci.template"
655 local media = luci.config.main.mediaurlbase
657 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
659 for name, theme in pairs(luci.config.themes) do
660 if name:sub(1,1) ~= "." and pcall(tpl.Template,
661 "themes/%s/header" % fs.basename(theme)) then
665 assert(media, "No valid theme found")
668 local function _ifattr(cond, key, val, noescape)
670 local env = getfenv(3)
671 local scope = (type(env.self) == "table") and env.self
672 if type(val) == "table" then
673 if not next(val) then
676 val = util.serialize_json(val)
680 val = tostring(val or
681 (type(env[key]) ~= "function" and env[key]) or
682 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
684 if noescape ~= true then
685 val = xml.pcdata(val)
688 return string.format(' %s="%s"', tostring(key), val)
694 tpl.context.viewns = setmetatable({
696 include = function(name) tpl.Template(name):render(getfenv(2)) end;
697 translate = i18n.translate;
698 translatef = i18n.translatef;
699 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
700 striptags = xml.striptags;
703 theme = fs.basename(media);
704 resource = luci.config.main.resourcebase;
705 ifattr = function(...) return _ifattr(...) end;
706 attr = function(...) return _ifattr(true, ...) end;
708 }, {__index=function(tbl, key)
709 if key == "controller" then
711 elseif key == "REQUEST_URI" then
712 return build_url(unpack(ctx.requestpath))
713 elseif key == "FULL_REQUEST_URI" then
714 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
715 local query = http.getenv("QUERY_STRING")
716 if query and #query > 0 then
720 return table.concat(url, "")
721 elseif key == "token" then
724 return rawget(tbl, key) or _G[key]
731 local function is_authenticated(auth)
732 if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
733 local sid, sdat, sacl
734 for _, method in ipairs(auth.methods) do
735 sid, sdat, sacl = check_authentication(method)
737 if sid and sdat and sacl then
738 return sid, sdat, sacl
744 local function ctx_append(ctx, name, node)
745 ctx.path = ctx.path or {}
746 ctx.path[#ctx.path + 1] = name
748 ctx.acls = ctx.acls or {}
750 local acls = (type(node.depends) == "table" and type(node.depends.acl) == "table") and node.depends.acl or {}
751 for _, acl in ipairs(acls) do
755 ctx.auth = node.auth or ctx.auth
756 ctx.cors = node.cors or ctx.cors
757 ctx.suid = node.setuser or ctx.suid
758 ctx.sgid = node.setgroup or ctx.sgid
763 local function node_weight(node)
764 local weight = node.order or 9999
766 if weight > 9999 then
770 if type(node.auth) == "table" and node.auth.login then
771 weight = weight + 10000
777 local function resolve_firstchild(node, sacl, login_allowed, ctx)
778 local candidate = nil
779 local candidate_ctx = nil
781 for name, child in pairs(node.children) do
782 if child.satisfied then
785 _, _, sacl = is_authenticated(node.auth)
788 local cacl = (type(child.depends) == "table") and child.depends.acl or nil
789 local login = login_allowed or (type(child.auth) == "table" and child.auth.login)
790 if login or check_acl_depends(cacl, sacl and sacl["access-group"]) ~= nil then
791 if child.title and type(child.action) == "table" then
792 local child_ctx = ctx_append(util.clone(ctx, true), name, child)
793 if child.action.type == "firstchild" then
794 if not candidate or node_weight(candidate) > node_weight(child) then
795 local have_grandchild = resolve_firstchild(child, sacl, login, child_ctx)
796 if have_grandchild then
798 candidate_ctx = child_ctx
801 elseif not child.firstchild_ineligible then
802 if not candidate or node_weight(candidate) > node_weight(child) then
804 candidate_ctx = child_ctx
813 for k, v in pairs(candidate_ctx) do
823 local function resolve_page(tree, request_path)
829 for i, s in ipairs(request_path) do
830 node = node.children and node.children[s]
832 if not node or not node.satisfied then
836 ctx_append(ctx, s, node)
840 _, _, sacl = is_authenticated(node.auth)
843 if not login and type(node.auth) == "table" and node.auth.login then
847 if node.wildcard then
848 ctx.request_args = {}
849 ctx.request_path = util.clone(ctx.path, true)
851 for j = i + 1, #request_path do
852 ctx.request_path[j] = request_path[j]
853 ctx.request_args[j - i] = request_path[j]
860 if node and type(node.action) == "table" and node.action.type == "firstchild" then
861 resolve_firstchild(node, sacl, login, ctx)
864 ctx.acls = ctx.acls or {}
865 ctx.path = ctx.path or {}
866 ctx.request_args = ctx.request_args or {}
867 ctx.request_path = ctx.request_path or util.clone(request_path, true)
871 for _, s in ipairs(ctx.path or {}) do
872 node = node.children[s]
873 assert(node, "Internal node resolve error")
879 function dispatch(request)
880 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
883 local auth, cors, suid, sgid
884 local menu = menu_json()
885 local page, lookup_ctx = resolve_page(menu, request)
886 local action = (page and type(page.action) == "table") and page.action or {}
888 local tpl = init_template_engine(ctx)
890 ctx.args = lookup_ctx.request_args
891 ctx.path = lookup_ctx.path
892 ctx.dispatched = page
894 ctx.requestpath = ctx.requestpath or lookup_ctx.request_path
895 ctx.requestargs = ctx.requestargs or lookup_ctx.request_args
896 ctx.requested = ctx.requested or page
898 if type(lookup_ctx.auth) == "table" and next(lookup_ctx.auth) then
899 local sid, sdat, sacl = is_authenticated(lookup_ctx.auth)
901 if not (sid and sdat and sacl) and lookup_ctx.auth.login then
902 local user = http.getenv("HTTP_AUTH_USER")
903 local pass = http.getenv("HTTP_AUTH_PASS")
905 if user == nil and pass == nil then
906 user = http.formvalue("luci_username")
907 pass = http.formvalue("luci_password")
910 if user and pass then
911 sid, sdat, sacl = session_setup(user, pass)
917 http.status(403, "Forbidden")
918 http.header("X-LuCI-Login-Required", "yes")
920 local scope = { duser = "root", fuser = user }
921 local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope)
925 return tpl.render("sysauth", scope)
928 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
929 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
932 http.redirect(build_url(unpack(ctx.requestpath)))
936 if not sid or not sdat or not sacl then
937 http.status(403, "Forbidden")
938 http.header("X-LuCI-Login-Required", "yes")
942 ctx.authsession = sid
943 ctx.authtoken = sdat.token
944 ctx.authuser = sdat.username
948 if #lookup_ctx.acls > 0 then
949 local perm = check_acl_depends(lookup_ctx.acls, ctx.authacl and ctx.authacl["access-group"])
951 http.status(403, "Forbidden")
956 page.readonly = not perm
960 if action.type == "arcombine" then
961 action = (#lookup_ctx.request_args > 0) and action.targets[2] or action.targets[1]
964 if lookup_ctx.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
965 luci.http.status(200, "OK")
966 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
967 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
971 if require_post_security(action) then
972 if not test_post_security() then
977 if lookup_ctx.sgid then
978 sys.process.setgroup(lookup_ctx.sgid)
981 if lookup_ctx.suid then
982 sys.process.setuser(lookup_ctx.suid)
985 if action.type == "view" then
986 tpl.render("view", { view = action.path })
988 elseif action.type == "call" then
989 local ok, mod = util.copcall(require, action.module)
995 local func = mod[action["function"]]
998 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
1000 assert(type(func) == "function",
1001 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
1002 'of type "' .. type(func) .. '".')
1004 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
1005 for _, s in ipairs(lookup_ctx.request_args) do
1009 local ok, err = util.copcall(func, unpack(argv))
1014 --elseif action.type == "firstchild" then
1015 -- tpl.render("empty_node_placeholder", getfenv(1))
1017 elseif action.type == "alias" then
1018 local sub_request = {}
1019 for name in action.path:gmatch("[^/]+") do
1020 sub_request[#sub_request + 1] = name
1023 for _, s in ipairs(lookup_ctx.request_args) do
1024 sub_request[#sub_request + 1] = s
1027 dispatch(sub_request)
1029 elseif action.type == "rewrite" then
1030 local sub_request = { unpack(request) }
1031 for i = 1, action.remove do
1032 table.remove(sub_request, 1)
1036 for s in action.path:gmatch("[^/]+") do
1037 table.insert(sub_request, n, s)
1041 for _, s in ipairs(lookup_ctx.request_args) do
1042 sub_request[#sub_request + 1] = s
1045 dispatch(sub_request)
1047 elseif action.type == "template" then
1048 tpl.render(action.path, getfenv(1))
1050 elseif action.type == "cbi" then
1051 _cbi({ config = action.config, model = action.path }, unpack(lookup_ctx.request_args))
1053 elseif action.type == "form" then
1054 _form({ model = action.path }, unpack(lookup_ctx.request_args))
1057 if not menu.children then
1058 error404("No root node was registered, this usually happens if no module was installed.\n" ..
1059 "Install luci-mod-admin-full and retry. " ..
1060 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
1062 error404("No page is registered at '/" .. table.concat(lookup_ctx.request_path, "/") .. "'.\n" ..
1063 "If this url belongs to an extension, make sure it is properly installed.\n" ..
1064 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
1069 local function hash_filelist(files)
1073 for i, file in ipairs(files) do
1074 local st = fs.stat(file)
1076 fprint[n + 1] = '%x' % st.ino
1077 fprint[n + 2] = '%x' % st.mtime
1078 fprint[n + 3] = '%x' % st.size
1083 return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1086 local function read_cachefile(file, reader)
1087 local euid = sys.process.info("uid")
1088 local fuid = fs.stat(file, "uid")
1089 local mode = fs.stat(file, "modestr")
1091 if euid ~= fuid or mode ~= "rw-------" then
1098 function createindex()
1099 local controllers = { }
1100 local base = "%s/controller/" % util.libpath()
1103 for path in (fs.glob("%s*.lua" % base) or function() end) do
1104 controllers[#controllers+1] = path
1107 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1108 controllers[#controllers+1] = path
1114 cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) }
1116 local res = read_cachefile(cachefile, function(path) return loadfile(path)() end)
1122 for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do
1129 for _, path in ipairs(controllers) do
1130 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1131 local mod = require(modname)
1133 "Invalid controller file found\n" ..
1134 "The file '" .. path .. "' contains an invalid module line.\n" ..
1135 "Please verify whether the module name is set to '" .. modname ..
1136 "' - It must correspond to the file path!")
1138 local idx = mod.index
1139 if type(idx) == "function" then
1140 index[modname] = idx
1145 local f = nixio.open(cachefile, "w", 600)
1146 f:writeall(util.get_bytecode(index))
1151 function createtree_json()
1152 local json = require "luci.jsonc"
1161 setgroup = "string",
1164 wildcard = "boolean",
1165 firstchild_ineligible = "boolean"
1171 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1172 files[#files+1] = file
1176 cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) }
1178 local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end)
1183 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1188 for _, file in ipairs(files) do
1189 local data = json.parse(fs.readfile(file) or "")
1190 if type(data) == "table" then
1191 for path, spec in pairs(data) do
1192 if type(spec) == "table" then
1195 for s in path:gmatch("[^/]+") do
1197 node.wildcard = true
1201 node.children = node.children or {}
1202 node.children[s] = node.children[s] or {}
1203 node = node.children[s]
1206 if node ~= tree then
1207 for k, t in pairs(schema) do
1208 if type(spec[k]) == t then
1213 node.satisfied = check_depends(spec)
1221 local f = nixio.open(cachefile, "w", 600)
1222 f:writeall(json.stringify(tree))
1229 -- Build the index before if it does not exist yet.
1230 function createtree()
1236 local tree = {nodes={}, inreq=true}
1238 ctx.treecache = setmetatable({}, {__mode="v"})
1241 local scope = setmetatable({}, {__index = luci.dispatcher})
1243 for k, v in pairs(index) do
1252 function assign(path, clone, title, order)
1253 local obj = node(unpack(path))
1260 setmetatable(obj, {__index = _create_node(clone)})
1265 function entry(path, target, title, order)
1266 local c = node(unpack(path))
1271 c.module = getfenv(2)._NAME
1276 -- enabling the node.
1278 return _create_node({...})
1282 local c = _create_node({...})
1284 c.module = getfenv(2)._NAME
1290 function lookup(...)
1291 local i, path = nil, {}
1292 for i = 1, select('#', ...) do
1293 local name, arg = nil, tostring(select(i, ...))
1294 for name in arg:gmatch("[^/]+") do
1295 path[#path+1] = name
1299 for i = #path, 1, -1 do
1300 local node = context.treecache[table.concat(path, ".", 1, i)]
1301 if node and (i == #path or node.leaf) then
1302 return node, build_url(unpack(path))
1307 function _create_node(path)
1312 local name = table.concat(path, ".")
1313 local c = context.treecache[name]
1316 local last = table.remove(path)
1317 local parent = _create_node(path)
1319 c = {nodes={}, auto=true, inreq=true}
1321 parent.nodes[last] = c
1322 context.treecache[name] = c
1328 -- Subdispatchers --
1330 function firstchild()
1331 return { type = "firstchild" }
1334 function firstnode()
1335 return { type = "firstnode" }
1339 return { type = "alias", req = { ... } }
1342 function rewrite(n, ...)
1343 return { type = "rewrite", n = n, req = { ... } }
1346 function call(name, ...)
1347 return { type = "call", argv = {...}, name = name }
1350 function post_on(params, name, ...)
1360 return post_on(true, ...)
1364 function template(name)
1365 return { type = "template", view = name }
1369 return { type = "view", view = name }
1373 function _cbi(self, ...)
1374 local cbi = require "luci.cbi"
1375 local tpl = require "luci.template"
1376 local http = require "luci.http"
1377 local util = require "luci.util"
1379 local config = self.config or {}
1380 local maps = cbi.load(self.model, ...)
1384 local function has_uci_access(config, level)
1385 local rv = util.ubus("session", "access", {
1386 ubus_rpc_session = context.authsession,
1387 scope = "uci", object = config,
1388 ["function"] = level
1391 return (type(rv) == "table" and rv.access == true) or false
1395 for i, res in ipairs(maps) do
1396 if util.instanceof(res, cbi.SimpleForm) then
1397 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1400 io.stderr:write("please change %s to use the form() action instead.\n"
1401 % table.concat(context.request, "/"))
1405 local cstate = res:parse()
1406 if cstate and (not state or cstate < state) then
1411 local function _resolve_path(path)
1412 return type(path) == "table" and build_url(unpack(path)) or path
1415 if config.on_valid_to and state and state > 0 and state < 2 then
1416 http.redirect(_resolve_path(config.on_valid_to))
1420 if config.on_changed_to and state and state > 1 then
1421 http.redirect(_resolve_path(config.on_changed_to))
1425 if config.on_success_to and state and state > 0 then
1426 http.redirect(_resolve_path(config.on_success_to))
1430 if config.state_handler then
1431 if not config.state_handler(state, maps) then
1436 http.header("X-CBI-State", state or 0)
1438 if not config.noheader then
1439 tpl.render("cbi/header", {state = state})
1444 local applymap = false
1445 local pageaction = true
1446 local parsechain = { }
1447 local writable = false
1449 for i, res in ipairs(maps) do
1450 if res.apply_needed and res.parsechain then
1452 for _, c in ipairs(res.parsechain) do
1453 parsechain[#parsechain+1] = c
1458 if res.redirect then
1459 redirect = redirect or res.redirect
1462 if res.pageaction == false then
1467 messages = messages or { }
1468 messages[#messages+1] = res.message
1472 for i, res in ipairs(maps) do
1473 local is_readable_map = has_uci_access(res.config, "read")
1474 local is_writable_map = has_uci_access(res.config, "write")
1476 writable = writable or is_writable_map
1479 firstmap = (i == 1),
1480 redirect = redirect,
1481 messages = messages,
1482 pageaction = pageaction,
1483 parsechain = parsechain,
1484 readable = is_readable_map,
1485 writable = is_writable_map
1489 if not config.nofooter then
1490 tpl.render("cbi/footer", {
1492 pageaction = pageaction,
1493 redirect = redirect,
1495 autoapply = config.autoapply,
1496 trigger_apply = applymap,
1502 function cbi(model, config)
1505 post = { ["cbi.submit"] = true },
1512 function arcombine(trg1, trg2)
1516 targets = {trg1, trg2}
1521 function _form(self, ...)
1522 local cbi = require "luci.cbi"
1523 local tpl = require "luci.template"
1524 local http = require "luci.http"
1526 local maps = luci.cbi.load(self.model, ...)
1530 for i, res in ipairs(maps) do
1531 local cstate = res:parse()
1532 if cstate and (not state or cstate < state) then
1537 http.header("X-CBI-State", state or 0)
1538 tpl.render("header")
1539 for i, res in ipairs(maps) do
1542 tpl.render("footer")
1545 function form(model)
1548 post = { ["cbi.submit"] = true },
1553 translate = i18n.translate
1555 -- This function does not actually translate the given argument but
1556 -- is used by build/i18n-scan.pl to find translatable entries.