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 local scope = { duser = "root", fuser = user }
888 local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope)
892 return tpl.render("sysauth", scope)
895 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
896 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
899 http.redirect(build_url(unpack(ctx.requestpath)))
903 if not sid or not sdat or not sacl then
904 http.status(403, "Forbidden")
905 http.header("X-LuCI-Login-Required", "yes")
909 ctx.authsession = sid
910 ctx.authtoken = sdat.token
911 ctx.authuser = sdat.username
915 if #required_path_acls > 0 then
916 local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"])
918 http.status(403, "Forbidden")
922 page.readonly = not perm
925 local action = (page and type(page.action) == "table") and page.action or {}
927 if action.type == "arcombine" then
928 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
931 if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
932 luci.http.status(200, "OK")
933 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
934 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
938 if require_post_security(action) then
939 if not test_post_security() then
945 sys.process.setgroup(sgid)
949 sys.process.setuser(suid)
952 if action.type == "view" then
953 tpl.render("view", { view = action.path })
955 elseif action.type == "call" then
956 local ok, mod = util.copcall(require, action.module)
962 local func = mod[action["function"]]
965 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
967 assert(type(func) == "function",
968 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
969 'of type "' .. type(func) .. '".')
971 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
972 for _, s in ipairs(requested_path_args) do
976 local ok, err = util.copcall(func, unpack(argv))
981 elseif action.type == "firstchild" then
982 local sub_request = find_subnode(page, requested_path_full, action.recurse)
984 dispatch(sub_request)
986 tpl.render("empty_node_placeholder", getfenv(1))
989 elseif action.type == "alias" then
990 local sub_request = {}
991 for name in action.path:gmatch("[^/]+") do
992 sub_request[#sub_request + 1] = name
995 for _, s in ipairs(requested_path_args) do
996 sub_request[#sub_request + 1] = s
999 dispatch(sub_request)
1001 elseif action.type == "rewrite" then
1002 local sub_request = { unpack(request) }
1003 for i = 1, action.remove do
1004 table.remove(sub_request, 1)
1008 for s in action.path:gmatch("[^/]+") do
1009 table.insert(sub_request, n, s)
1013 for _, s in ipairs(requested_path_args) do
1014 sub_request[#sub_request + 1] = s
1017 dispatch(sub_request)
1019 elseif action.type == "template" then
1020 tpl.render(action.path, getfenv(1))
1022 elseif action.type == "cbi" then
1023 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
1025 elseif action.type == "form" then
1026 _form({ model = action.path }, unpack(requested_path_args))
1029 local root = find_subnode(menu, {}, true)
1031 error404("No root node was registered, this usually happens if no module was installed.\n" ..
1032 "Install luci-mod-admin-full and retry. " ..
1033 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
1035 error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
1036 "If this url belongs to an extension, make sure it is properly installed.\n" ..
1037 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
1042 local function hash_filelist(files)
1046 for i, file in ipairs(files) do
1047 local st = fs.stat(file)
1049 fprint[n + 1] = '%x' % st.ino
1050 fprint[n + 2] = '%x' % st.mtime
1051 fprint[n + 3] = '%x' % st.size
1056 return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1059 local function read_cachefile(file, reader)
1060 local euid = sys.process.info("uid")
1061 local fuid = fs.stat(file, "uid")
1062 local mode = fs.stat(file, "modestr")
1064 if euid ~= fuid or mode ~= "rw-------" then
1071 function createindex()
1072 local controllers = { }
1073 local base = "%s/controller/" % util.libpath()
1076 for path in (fs.glob("%s*.lua" % base) or function() end) do
1077 controllers[#controllers+1] = path
1080 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1081 controllers[#controllers+1] = path
1087 cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) }
1089 local res = read_cachefile(cachefile, function(path) return loadfile(path)() end)
1095 for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do
1102 for _, path in ipairs(controllers) do
1103 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1104 local mod = require(modname)
1106 "Invalid controller file found\n" ..
1107 "The file '" .. path .. "' contains an invalid module line.\n" ..
1108 "Please verify whether the module name is set to '" .. modname ..
1109 "' - It must correspond to the file path!")
1111 local idx = mod.index
1112 if type(idx) == "function" then
1113 index[modname] = idx
1118 local f = nixio.open(cachefile, "w", 600)
1119 f:writeall(util.get_bytecode(index))
1124 function createtree_json()
1125 local json = require "luci.jsonc"
1134 setgroup = "string",
1137 wildcard = "boolean"
1143 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1144 files[#files+1] = file
1148 cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) }
1150 local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end)
1155 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1160 for _, file in ipairs(files) do
1161 local data = json.parse(fs.readfile(file) or "")
1162 if type(data) == "table" then
1163 for path, spec in pairs(data) do
1164 if type(spec) == "table" then
1167 for s in path:gmatch("[^/]+") do
1169 node.wildcard = true
1173 node.children = node.children or {}
1174 node.children[s] = node.children[s] or {}
1175 node = node.children[s]
1178 if node ~= tree then
1179 for k, t in pairs(schema) do
1180 if type(spec[k]) == t then
1185 node.satisfied = check_depends(spec)
1193 local f = nixio.open(cachefile, "w", 600)
1194 f:writeall(json.stringify(tree))
1201 -- Build the index before if it does not exist yet.
1202 function createtree()
1208 local tree = {nodes={}, inreq=true}
1210 ctx.treecache = setmetatable({}, {__mode="v"})
1213 local scope = setmetatable({}, {__index = luci.dispatcher})
1215 for k, v in pairs(index) do
1224 function assign(path, clone, title, order)
1225 local obj = node(unpack(path))
1232 setmetatable(obj, {__index = _create_node(clone)})
1237 function entry(path, target, title, order)
1238 local c = node(unpack(path))
1243 c.module = getfenv(2)._NAME
1248 -- enabling the node.
1250 return _create_node({...})
1254 local c = _create_node({...})
1256 c.module = getfenv(2)._NAME
1262 function lookup(...)
1263 local i, path = nil, {}
1264 for i = 1, select('#', ...) do
1265 local name, arg = nil, tostring(select(i, ...))
1266 for name in arg:gmatch("[^/]+") do
1267 path[#path+1] = name
1271 for i = #path, 1, -1 do
1272 local node = context.treecache[table.concat(path, ".", 1, i)]
1273 if node and (i == #path or node.leaf) then
1274 return node, build_url(unpack(path))
1279 function _create_node(path)
1284 local name = table.concat(path, ".")
1285 local c = context.treecache[name]
1288 local last = table.remove(path)
1289 local parent = _create_node(path)
1291 c = {nodes={}, auto=true, inreq=true}
1293 parent.nodes[last] = c
1294 context.treecache[name] = c
1300 -- Subdispatchers --
1302 function firstchild()
1303 return { type = "firstchild" }
1306 function firstnode()
1307 return { type = "firstnode" }
1311 return { type = "alias", req = { ... } }
1314 function rewrite(n, ...)
1315 return { type = "rewrite", n = n, req = { ... } }
1318 function call(name, ...)
1319 return { type = "call", argv = {...}, name = name }
1322 function post_on(params, name, ...)
1332 return post_on(true, ...)
1336 function template(name)
1337 return { type = "template", view = name }
1341 return { type = "view", view = name }
1345 function _cbi(self, ...)
1346 local cbi = require "luci.cbi"
1347 local tpl = require "luci.template"
1348 local http = require "luci.http"
1349 local util = require "luci.util"
1351 local config = self.config or {}
1352 local maps = cbi.load(self.model, ...)
1356 local function has_uci_access(config, level)
1357 local rv = util.ubus("session", "access", {
1358 ubus_rpc_session = context.authsession,
1359 scope = "uci", object = config,
1360 ["function"] = level
1363 return (type(rv) == "table" and rv.access == true) or false
1367 for i, res in ipairs(maps) do
1368 if util.instanceof(res, cbi.SimpleForm) then
1369 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1372 io.stderr:write("please change %s to use the form() action instead.\n"
1373 % table.concat(context.request, "/"))
1377 local cstate = res:parse()
1378 if cstate and (not state or cstate < state) then
1383 local function _resolve_path(path)
1384 return type(path) == "table" and build_url(unpack(path)) or path
1387 if config.on_valid_to and state and state > 0 and state < 2 then
1388 http.redirect(_resolve_path(config.on_valid_to))
1392 if config.on_changed_to and state and state > 1 then
1393 http.redirect(_resolve_path(config.on_changed_to))
1397 if config.on_success_to and state and state > 0 then
1398 http.redirect(_resolve_path(config.on_success_to))
1402 if config.state_handler then
1403 if not config.state_handler(state, maps) then
1408 http.header("X-CBI-State", state or 0)
1410 if not config.noheader then
1411 tpl.render("cbi/header", {state = state})
1416 local applymap = false
1417 local pageaction = true
1418 local parsechain = { }
1419 local writable = false
1421 for i, res in ipairs(maps) do
1422 if res.apply_needed and res.parsechain then
1424 for _, c in ipairs(res.parsechain) do
1425 parsechain[#parsechain+1] = c
1430 if res.redirect then
1431 redirect = redirect or res.redirect
1434 if res.pageaction == false then
1439 messages = messages or { }
1440 messages[#messages+1] = res.message
1444 for i, res in ipairs(maps) do
1445 local is_readable_map = has_uci_access(res.config, "read")
1446 local is_writable_map = has_uci_access(res.config, "write")
1448 writable = writable or is_writable_map
1451 firstmap = (i == 1),
1452 redirect = redirect,
1453 messages = messages,
1454 pageaction = pageaction,
1455 parsechain = parsechain,
1456 readable = is_readable_map,
1457 writable = is_writable_map
1461 if not config.nofooter then
1462 tpl.render("cbi/footer", {
1464 pageaction = pageaction,
1465 redirect = redirect,
1467 autoapply = config.autoapply,
1468 trigger_apply = applymap,
1474 function cbi(model, config)
1477 post = { ["cbi.submit"] = true },
1484 function arcombine(trg1, trg2)
1488 targets = {trg1, trg2}
1493 function _form(self, ...)
1494 local cbi = require "luci.cbi"
1495 local tpl = require "luci.template"
1496 local http = require "luci.http"
1498 local maps = luci.cbi.load(self.model, ...)
1502 for i, res in ipairs(maps) do
1503 local cstate = res:parse()
1504 if cstate and (not state or cstate < state) then
1509 http.header("X-CBI-State", state or 0)
1510 tpl.render("header")
1511 for i, res in ipairs(maps) do
1514 tpl.render("footer")
1517 function form(model)
1520 post = { ["cbi.submit"] = true },
1525 translate = i18n.translate
1527 -- This function does not actually translate the given argument but
1528 -- is used by build/i18n-scan.pl to find translatable entries.