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 http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
20 local function check_fs_depends(spec)
21 local fs = require "nixio.fs"
23 for path, kind in pairs(spec) do
24 if kind == "directory" then
26 for entry in (fs.dir(path) or function() end) do
33 elseif kind == "executable" then
34 if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then
37 elseif kind == "file" then
38 if fs.stat(path, "type") ~= "reg" then
47 local function check_uci_depends_options(conf, s, opts)
48 local uci = require "luci.model.uci"
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
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
63 for _, v in ipairs(sval) do
72 elseif value == true then
87 local function check_uci_depends_section(conf, sect)
88 local uci = require "luci.model.uci"
90 for section, options in pairs(sect) do
91 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
94 uci:foreach(conf, stype, function(s)
95 if check_uci_depends_options(conf, s, options) then
104 local s = uci:get_all(conf, section)
105 if not s or not check_uci_depends_options(conf, s, options) then
114 local function check_uci_depends(conf)
115 local uci = require "luci.model.uci"
117 for config, values in pairs(conf) do
118 if values == true then
120 uci:foreach(config, nil, function(s)
127 elseif type(values) == "table" then
128 if not check_uci_depends_section(config, values) then
137 local function check_acl_depends(require_groups, groups)
138 if type(require_groups) == "table" and #require_groups > 0 then
139 local writable = false
141 for _, group in ipairs(require_groups) do
144 if type(groups) == "table" and type(groups[group]) == "table" then
145 for _, perm in ipairs(groups[group]) do
146 if perm == "read" then
148 elseif perm == "write" then
153 if not read and not write then
166 local function check_depends(spec)
167 if type(spec.depends) ~= "table" then
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
180 if not satisfied then
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
194 if not satisfied then
202 local function target_to_json(target, module)
205 if target.type == "call" then
209 ["function"] = target.name,
210 ["parameters"] = target.argv
212 elseif target.type == "view" then
215 ["path"] = target.view
217 elseif target.type == "template" then
219 ["type"] = "template",
220 ["path"] = target.view
222 elseif target.type == "cbi" then
225 ["path"] = target.model,
226 ["config"] = target.config
228 elseif target.type == "form" then
231 ["path"] = target.model
233 elseif target.type == "firstchild" then
235 ["type"] = "firstchild"
237 elseif target.type == "firstnode" then
239 ["type"] = "firstchild",
242 elseif target.type == "arcombine" then
243 if type(target.targets) == "table" then
245 ["type"] = "arcombine",
247 target_to_json(target.targets[1], module),
248 target_to_json(target.targets[2], module)
252 elseif target.type == "alias" then
255 ["path"] = table.concat(target.req, "/")
257 elseif target.type == "rewrite" then
259 ["type"] = "rewrite",
260 ["path"] = table.concat(target.req, "/"),
261 ["remove"] = target.n
265 if target.post and action then
266 action.post = target.post
272 local function tree_to_json(node, json)
273 local fs = require "nixio.fs"
274 local util = require "luci.util"
276 if type(node.nodes) == "table" then
277 for subname, subnode in pairs(node.nodes) do
279 title = util.striptags(subnode.title),
280 order = subnode.order
291 if subnode.setuser then
292 spec.setuser = subnode.setuser
295 if subnode.setgroup then
296 spec.setgroup = subnode.setgroup
299 if type(subnode.target) == "table" then
300 spec.action = target_to_json(subnode.target, subnode.module)
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 {}
308 local ft = fs.stat(v, "type")
310 spec.depends.fs[v] = "directory"
311 elseif v:match("/s?bin/") then
312 spec.depends.fs[v] = "executable"
314 spec.depends.fs[v] = "file"
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
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
335 if (subnode.sysauth_authenticator ~= nil) or
336 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
338 if subnode.sysauth_authenticator == "htmlauth" then
341 methods = { "cookie:sysauth" }
343 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
346 methods = { "query:auth", "cookie:sysauth" }
348 elseif subnode.module == "luci.controller.admin.uci" then
351 methods = { "param:sid" }
354 elseif subnode.sysauth == false then
358 if not spec.action then
362 spec.satisfied = check_depends(spec)
363 json.children = json.children or {}
364 json.children[subname] = tree_to_json(subnode, spec)
371 function build_url(...)
373 local url = { http.getenv("SCRIPT_NAME") or "" }
376 for _, p in ipairs(path) do
377 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
387 return table.concat(url, "")
391 function error404(message)
392 http.status(404, "Not Found")
393 message = message or "Not Found"
395 local function render()
396 local template = require "luci.template"
397 template.render("error404")
400 if not util.copcall(render) then
401 http.prepare_content("text/plain")
408 function error500(message)
410 if not context.template_header_sent then
411 http.status(500, "Internal Server Error")
412 http.prepare_content("text/plain")
415 require("luci.template")
416 if not util.copcall(luci.template.render, "error500", {message=message}) then
417 http.prepare_content("text/plain")
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'")
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
438 elseif conf.languages[country] then
442 elseif conf.languages[aclang] then
449 if lang == "auto" then
453 i18n.setlanguage(lang)
456 function httpdispatch(request, prefix)
457 http.context.request = request
462 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
465 for _, node in ipairs(prefix) do
471 for node in pathinfo:gmatch("[^/%z]+") do
475 determine_request_language()
477 local stat, err = util.coxpcall(function()
478 dispatch(context.request)
483 --context._disable_memtrace()
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)
491 if type(target) == "table" then
492 if type(target.post) == "table" then
493 local param_name, required_val, request_val
495 for param_name, required_val in pairs(target.post) do
496 request_val = http.formvalue(param_name)
498 if (type(required_val) == "string" and
499 request_val ~= required_val) or
500 (required_val == true and request_val == nil)
509 return (target.post == true)
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")
522 if http.formvalue("token") ~= context.authtoken then
523 http.status(403, "Forbidden")
524 luci.template.render("csrftoken")
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 })
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))
541 uci:set_session_id(sid)
542 return sid, sdat.values, type(sacl) == "table" and sacl or {}
548 local function session_setup(user, pass)
549 local login = util.ubus("session", "login", {
552 timeout = tonumber(luci.config.sauth.sessiontime)
555 local rp = context.requestpath
556 and table.concat(context.requestpath, "/") or ""
558 if type(login) == "table" and
559 type(login.ubus_rpc_session) == "string"
561 util.ubus("session", "set", {
562 ubus_rpc_session = login.ubus_rpc_session,
563 values = { token = sys.uniqueid(16) }
566 io.stderr:write("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)
572 io.stderr:write("luci: failed login on /%s for %s from %s\n"
573 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })
576 local function check_authentication(method)
577 local auth_type, auth_param = method:match("^(%w+):(.+)$")
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)
588 return session_retrieve(sid)
591 local function get_children(node)
594 if not node.wildcard and type(node.children) == "table" then
595 for name, child in pairs(node.children) do
596 children[#children+1] = {
599 order = child.order or 1000
603 table.sort(children, function(a, b)
604 if a.order == b.order then
605 return a.name < b.name
607 return a.order < b.order
615 local function find_subnode(root, prefix, recurse, descended)
616 local children = get_children(root)
618 if #children > 0 and (not descended or recurse) then
619 local sub_path = { unpack(prefix) }
621 if recurse == false then
625 for _, child in ipairs(children) do
626 sub_path[#prefix+1] = child.name
628 local res_path = find_subnode(child.node, sub_path, recurse, true)
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"
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 {}
654 for name, spec in pairs(v) do
655 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
662 if type(node_a.action) == "table" and
663 node_a.action.type == "firstchild" and
664 node_a.children == nil
666 node_a.satisfied = false
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)
680 if type(node.depends) == "table" then
681 perm = check_acl_depends(node.depends.acl, acl["access-group"])
687 node.satisfied = false
688 elseif perm == false then
693 function menu_json(acl)
694 local tree = context.tree or createtree()
695 local lua_tree = tree_to_json(tree, {
697 ["type"] = "firstchild",
702 local json_tree = createtree_json()
703 local menu_tree = merge_trees(lua_tree, json_tree)
706 apply_tree_acls(menu_tree, acl)
712 local function init_template_engine(ctx)
713 local tpl = require "luci.template"
714 local media = luci.config.main.mediaurlbase
716 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
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
724 assert(media, "No valid theme found")
727 local function _ifattr(cond, key, val, noescape)
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
735 val = util.serialize_json(val)
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 "")
743 if noescape ~= true then
744 val = util.pcdata(val)
747 return string.format(' %s="%s"', tostring(key), val)
753 tpl.context.viewns = setmetatable({
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;
762 theme = fs.basename(media);
763 resource = luci.config.main.resourcebase;
764 ifattr = function(...) return _ifattr(...) end;
765 attr = function(...) return _ifattr(true, ...) end;
767 }, {__index=function(tbl, key)
768 if key == "controller" then
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
779 return table.concat(url, "")
780 elseif key == "token" then
783 return rawget(tbl, key) or _G[key]
790 function dispatch(request)
791 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
794 local auth, cors, suid, sgid
795 local menu = menu_json()
798 local requested_path_full = {}
799 local requested_path_node = {}
800 local requested_path_args = {}
802 local required_path_acls = {}
804 for i, s in ipairs(request) do
805 if type(page.children) ~= "table" or not page.children[s] then
810 if not page.children[s].satisfied then
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
821 if type(page.depends) == "table" and type(page.depends.acl) == "table" then
822 for _, group in ipairs(page.depends.acl) do
824 for _, item in ipairs(required_path_acls) do
825 if item == group then
831 required_path_acls[#required_path_acls + 1] = group
836 requested_path_full[i] = s
837 requested_path_node[i] = s
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]
848 local tpl = init_template_engine(ctx)
850 ctx.args = requested_path_args
851 ctx.path = requested_path_node
852 ctx.dispatched = page
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
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)
863 if sid and sdat and sacl then
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")
872 if user == nil and pass == nil then
873 user = http.formvalue("luci_username")
874 pass = http.formvalue("luci_password")
877 if user and pass then
878 sid, sdat, sacl = session_setup(user, pass)
884 http.status(403, "Forbidden")
885 http.header("X-LuCI-Login-Required", "yes")
887 return tpl.render("sysauth", { duser = "root", fuser = user })
890 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
891 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
894 http.redirect(build_url(unpack(ctx.requestpath)))
898 if not sid or not sdat or not sacl then
899 http.status(403, "Forbidden")
900 http.header("X-LuCI-Login-Required", "yes")
904 ctx.authsession = sid
905 ctx.authtoken = sdat.token
906 ctx.authuser = sdat.username
910 if #required_path_acls > 0 then
911 local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"])
913 http.status(403, "Forbidden")
917 page.readonly = not perm
920 local action = (page and type(page.action) == "table") and page.action or {}
922 if action.type == "arcombine" then
923 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
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")
933 if require_post_security(action) then
934 if not test_post_security() then
940 sys.process.setgroup(sgid)
944 sys.process.setuser(suid)
947 if action.type == "view" then
948 tpl.render("view", { view = action.path })
950 elseif action.type == "call" then
951 local ok, mod = util.copcall(require, action.module)
957 local func = mod[action["function"]]
960 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
962 assert(type(func) == "function",
963 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
964 'of type "' .. type(func) .. '".')
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
971 local ok, err = util.copcall(func, unpack(argv))
976 elseif action.type == "firstchild" then
977 local sub_request = find_subnode(page, requested_path_full, action.recurse)
979 dispatch(sub_request)
981 tpl.render("empty_node_placeholder", getfenv(1))
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
990 for _, s in ipairs(requested_path_args) do
991 sub_request[#sub_request + 1] = s
994 dispatch(sub_request)
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)
1003 for s in action.path:gmatch("[^/]+") do
1004 table.insert(sub_request, n, s)
1008 for _, s in ipairs(requested_path_args) do
1009 sub_request[#sub_request + 1] = s
1012 dispatch(sub_request)
1014 elseif action.type == "template" then
1015 tpl.render(action.path, getfenv(1))
1017 elseif action.type == "cbi" then
1018 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
1020 elseif action.type == "form" then
1021 _form({ model = action.path }, unpack(requested_path_args))
1024 local root = find_subnode(menu, {}, true)
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.")
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.")
1037 local function hash_filelist(files)
1041 for i, file in ipairs(files) do
1042 local st = fs.stat(file)
1044 fprint[n + 1] = '%x' % st.ino
1045 fprint[n + 2] = '%x' % st.mtime
1046 fprint[n + 3] = '%x' % st.size
1051 return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1054 local function read_cachefile(file, reader)
1055 local euid = sys.process.info("uid")
1056 local fuid = fs.stat(file, "uid")
1057 local mode = fs.stat(file, "modestr")
1059 if euid ~= fuid or mode ~= "rw-------" then
1066 function createindex()
1067 local controllers = { }
1068 local base = "%s/controller/" % util.libpath()
1071 for path in (fs.glob("%s*.lua" % base) or function() end) do
1072 controllers[#controllers+1] = path
1075 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1076 controllers[#controllers+1] = path
1082 cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) }
1084 local res = read_cachefile(cachefile, function(path) return loadfile(path)() end)
1090 for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do
1097 for _, path in ipairs(controllers) do
1098 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1099 local mod = require(modname)
1101 "Invalid controller file found\n" ..
1102 "The file '" .. path .. "' contains an invalid module line.\n" ..
1103 "Please verify whether the module name is set to '" .. modname ..
1104 "' - It must correspond to the file path!")
1106 local idx = mod.index
1107 if type(idx) == "function" then
1108 index[modname] = idx
1113 local f = nixio.open(cachefile, "w", 600)
1114 f:writeall(util.get_bytecode(index))
1119 function createtree_json()
1120 local json = require "luci.jsonc"
1129 setgroup = "string",
1132 wildcard = "boolean"
1138 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1139 files[#files+1] = file
1143 cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) }
1145 local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end)
1150 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1155 for _, file in ipairs(files) do
1156 local data = json.parse(fs.readfile(file) or "")
1157 if type(data) == "table" then
1158 for path, spec in pairs(data) do
1159 if type(spec) == "table" then
1162 for s in path:gmatch("[^/]+") do
1164 node.wildcard = true
1168 node.children = node.children or {}
1169 node.children[s] = node.children[s] or {}
1170 node = node.children[s]
1173 if node ~= tree then
1174 for k, t in pairs(schema) do
1175 if type(spec[k]) == t then
1180 node.satisfied = check_depends(spec)
1188 local f = nixio.open(cachefile, "w", 600)
1189 f:writeall(json.stringify(tree))
1196 -- Build the index before if it does not exist yet.
1197 function createtree()
1203 local tree = {nodes={}, inreq=true}
1205 ctx.treecache = setmetatable({}, {__mode="v"})
1208 local scope = setmetatable({}, {__index = luci.dispatcher})
1210 for k, v in pairs(index) do
1219 function assign(path, clone, title, order)
1220 local obj = node(unpack(path))
1227 setmetatable(obj, {__index = _create_node(clone)})
1232 function entry(path, target, title, order)
1233 local c = node(unpack(path))
1238 c.module = getfenv(2)._NAME
1243 -- enabling the node.
1245 return _create_node({...})
1249 local c = _create_node({...})
1251 c.module = getfenv(2)._NAME
1257 function lookup(...)
1258 local i, path = nil, {}
1259 for i = 1, select('#', ...) do
1260 local name, arg = nil, tostring(select(i, ...))
1261 for name in arg:gmatch("[^/]+") do
1262 path[#path+1] = name
1266 for i = #path, 1, -1 do
1267 local node = context.treecache[table.concat(path, ".", 1, i)]
1268 if node and (i == #path or node.leaf) then
1269 return node, build_url(unpack(path))
1274 function _create_node(path)
1279 local name = table.concat(path, ".")
1280 local c = context.treecache[name]
1283 local last = table.remove(path)
1284 local parent = _create_node(path)
1286 c = {nodes={}, auto=true, inreq=true}
1288 parent.nodes[last] = c
1289 context.treecache[name] = c
1295 -- Subdispatchers --
1297 function firstchild()
1298 return { type = "firstchild" }
1301 function firstnode()
1302 return { type = "firstnode" }
1306 return { type = "alias", req = { ... } }
1309 function rewrite(n, ...)
1310 return { type = "rewrite", n = n, req = { ... } }
1313 function call(name, ...)
1314 return { type = "call", argv = {...}, name = name }
1317 function post_on(params, name, ...)
1327 return post_on(true, ...)
1331 function template(name)
1332 return { type = "template", view = name }
1336 return { type = "view", view = name }
1340 function _cbi(self, ...)
1341 local cbi = require "luci.cbi"
1342 local tpl = require "luci.template"
1343 local http = require "luci.http"
1344 local util = require "luci.util"
1346 local config = self.config or {}
1347 local maps = cbi.load(self.model, ...)
1351 local function has_uci_access(config, level)
1352 local rv = util.ubus("session", "access", {
1353 ubus_rpc_session = context.authsession,
1354 scope = "uci", object = config,
1355 ["function"] = level
1358 return (type(rv) == "table" and rv.access == true) or false
1362 for i, res in ipairs(maps) do
1363 if util.instanceof(res, cbi.SimpleForm) then
1364 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1367 io.stderr:write("please change %s to use the form() action instead.\n"
1368 % table.concat(context.request, "/"))
1372 local cstate = res:parse()
1373 if cstate and (not state or cstate < state) then
1378 local function _resolve_path(path)
1379 return type(path) == "table" and build_url(unpack(path)) or path
1382 if config.on_valid_to and state and state > 0 and state < 2 then
1383 http.redirect(_resolve_path(config.on_valid_to))
1387 if config.on_changed_to and state and state > 1 then
1388 http.redirect(_resolve_path(config.on_changed_to))
1392 if config.on_success_to and state and state > 0 then
1393 http.redirect(_resolve_path(config.on_success_to))
1397 if config.state_handler then
1398 if not config.state_handler(state, maps) then
1403 http.header("X-CBI-State", state or 0)
1405 if not config.noheader then
1406 tpl.render("cbi/header", {state = state})
1411 local applymap = false
1412 local pageaction = true
1413 local parsechain = { }
1414 local writable = false
1416 for i, res in ipairs(maps) do
1417 if res.apply_needed and res.parsechain then
1419 for _, c in ipairs(res.parsechain) do
1420 parsechain[#parsechain+1] = c
1425 if res.redirect then
1426 redirect = redirect or res.redirect
1429 if res.pageaction == false then
1434 messages = messages or { }
1435 messages[#messages+1] = res.message
1439 for i, res in ipairs(maps) do
1440 local is_readable_map = has_uci_access(res.config, "read")
1441 local is_writable_map = has_uci_access(res.config, "write")
1443 writable = writable or is_writable_map
1446 firstmap = (i == 1),
1447 redirect = redirect,
1448 messages = messages,
1449 pageaction = pageaction,
1450 parsechain = parsechain,
1451 readable = is_readable_map,
1452 writable = is_writable_map
1456 if not config.nofooter then
1457 tpl.render("cbi/footer", {
1459 pageaction = pageaction,
1460 redirect = redirect,
1462 autoapply = config.autoapply,
1463 trigger_apply = applymap,
1469 function cbi(model, config)
1472 post = { ["cbi.submit"] = true },
1479 function arcombine(trg1, trg2)
1483 targets = {trg1, trg2}
1488 function _form(self, ...)
1489 local cbi = require "luci.cbi"
1490 local tpl = require "luci.template"
1491 local http = require "luci.http"
1493 local maps = luci.cbi.load(self.model, ...)
1497 for i, res in ipairs(maps) do
1498 local cstate = res:parse()
1499 if cstate and (not state or cstate < state) then
1504 http.header("X-CBI-State", state or 0)
1505 tpl.render("header")
1506 for i, res in ipairs(maps) do
1509 tpl.render("footer")
1512 function form(model)
1515 post = { ["cbi.submit"] = true },
1520 translate = i18n.translate
1522 -- This function does not actually translate the given argument but
1523 -- is used by build/i18n-scan.pl to find translatable entries.