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 function createindex()
1038 local controllers = { }
1039 local base = "%s/controller/" % util.libpath()
1042 for path in (fs.glob("%s*.lua" % base) or function() end) do
1043 controllers[#controllers+1] = path
1046 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1047 controllers[#controllers+1] = path
1051 local cachedate = fs.stat(indexcache, "mtime")
1054 for _, obj in ipairs(controllers) do
1055 local omtime = fs.stat(obj, "mtime")
1056 realdate = (omtime and omtime > realdate) and omtime or realdate
1059 if cachedate > realdate and sys.process.info("uid") == 0 then
1061 sys.process.info("uid") == fs.stat(indexcache, "uid")
1062 and fs.stat(indexcache, "modestr") == "rw-------",
1063 "Fatal: Indexcache is not sane!"
1066 index = loadfile(indexcache)()
1074 for _, path in ipairs(controllers) do
1075 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1076 local mod = require(modname)
1078 "Invalid controller file found\n" ..
1079 "The file '" .. path .. "' contains an invalid module line.\n" ..
1080 "Please verify whether the module name is set to '" .. modname ..
1081 "' - It must correspond to the file path!")
1083 local idx = mod.index
1084 if type(idx) == "function" then
1085 index[modname] = idx
1090 local f = nixio.open(indexcache, "w", 600)
1091 f:writeall(util.get_bytecode(index))
1096 function createtree_json()
1097 local json = require "luci.jsonc"
1106 setgroup = "string",
1109 wildcard = "boolean"
1116 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1117 files[#files+1] = file
1120 local st = fs.stat(file)
1122 fprint[#fprint+1] = '%x' % st.ino
1123 fprint[#fprint+1] = '%x' % st.mtime
1124 fprint[#fprint+1] = '%x' % st.size
1130 cachefile = "%s.%s.json" %{
1132 nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1135 local res = json.parse(fs.readfile(cachefile) or "")
1140 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1145 for _, file in ipairs(files) do
1146 local data = json.parse(fs.readfile(file) or "")
1147 if type(data) == "table" then
1148 for path, spec in pairs(data) do
1149 if type(spec) == "table" then
1152 for s in path:gmatch("[^/]+") do
1154 node.wildcard = true
1158 node.children = node.children or {}
1159 node.children[s] = node.children[s] or {}
1160 node = node.children[s]
1163 if node ~= tree then
1164 for k, t in pairs(schema) do
1165 if type(spec[k]) == t then
1170 node.satisfied = check_depends(spec)
1178 fs.writefile(cachefile, json.stringify(tree))
1184 -- Build the index before if it does not exist yet.
1185 function createtree()
1191 local tree = {nodes={}, inreq=true}
1193 ctx.treecache = setmetatable({}, {__mode="v"})
1196 local scope = setmetatable({}, {__index = luci.dispatcher})
1198 for k, v in pairs(index) do
1207 function assign(path, clone, title, order)
1208 local obj = node(unpack(path))
1215 setmetatable(obj, {__index = _create_node(clone)})
1220 function entry(path, target, title, order)
1221 local c = node(unpack(path))
1226 c.module = getfenv(2)._NAME
1231 -- enabling the node.
1233 return _create_node({...})
1237 local c = _create_node({...})
1239 c.module = getfenv(2)._NAME
1245 function lookup(...)
1246 local i, path = nil, {}
1247 for i = 1, select('#', ...) do
1248 local name, arg = nil, tostring(select(i, ...))
1249 for name in arg:gmatch("[^/]+") do
1250 path[#path+1] = name
1254 for i = #path, 1, -1 do
1255 local node = context.treecache[table.concat(path, ".", 1, i)]
1256 if node and (i == #path or node.leaf) then
1257 return node, build_url(unpack(path))
1262 function _create_node(path)
1267 local name = table.concat(path, ".")
1268 local c = context.treecache[name]
1271 local last = table.remove(path)
1272 local parent = _create_node(path)
1274 c = {nodes={}, auto=true, inreq=true}
1276 parent.nodes[last] = c
1277 context.treecache[name] = c
1283 -- Subdispatchers --
1285 function firstchild()
1286 return { type = "firstchild" }
1289 function firstnode()
1290 return { type = "firstnode" }
1294 return { type = "alias", req = { ... } }
1297 function rewrite(n, ...)
1298 return { type = "rewrite", n = n, req = { ... } }
1301 function call(name, ...)
1302 return { type = "call", argv = {...}, name = name }
1305 function post_on(params, name, ...)
1315 return post_on(true, ...)
1319 function template(name)
1320 return { type = "template", view = name }
1324 return { type = "view", view = name }
1328 function _cbi(self, ...)
1329 local cbi = require "luci.cbi"
1330 local tpl = require "luci.template"
1331 local http = require "luci.http"
1333 local config = self.config or {}
1334 local maps = cbi.load(self.model, ...)
1339 for i, res in ipairs(maps) do
1340 if util.instanceof(res, cbi.SimpleForm) then
1341 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1344 io.stderr:write("please change %s to use the form() action instead.\n"
1345 % table.concat(context.request, "/"))
1349 local cstate = res:parse()
1350 if cstate and (not state or cstate < state) then
1355 local function _resolve_path(path)
1356 return type(path) == "table" and build_url(unpack(path)) or path
1359 if config.on_valid_to and state and state > 0 and state < 2 then
1360 http.redirect(_resolve_path(config.on_valid_to))
1364 if config.on_changed_to and state and state > 1 then
1365 http.redirect(_resolve_path(config.on_changed_to))
1369 if config.on_success_to and state and state > 0 then
1370 http.redirect(_resolve_path(config.on_success_to))
1374 if config.state_handler then
1375 if not config.state_handler(state, maps) then
1380 http.header("X-CBI-State", state or 0)
1382 if not config.noheader then
1383 tpl.render("cbi/header", {state = state})
1388 local applymap = false
1389 local pageaction = true
1390 local parsechain = { }
1392 for i, res in ipairs(maps) do
1393 if res.apply_needed and res.parsechain then
1395 for _, c in ipairs(res.parsechain) do
1396 parsechain[#parsechain+1] = c
1401 if res.redirect then
1402 redirect = redirect or res.redirect
1405 if res.pageaction == false then
1410 messages = messages or { }
1411 messages[#messages+1] = res.message
1415 for i, res in ipairs(maps) do
1417 firstmap = (i == 1),
1418 redirect = redirect,
1419 messages = messages,
1420 pageaction = pageaction,
1421 parsechain = parsechain
1425 if not config.nofooter then
1426 tpl.render("cbi/footer", {
1428 pageaction = pageaction,
1429 redirect = redirect,
1431 autoapply = config.autoapply,
1432 trigger_apply = applymap
1437 function cbi(model, config)
1440 post = { ["cbi.submit"] = true },
1447 function arcombine(trg1, trg2)
1451 targets = {trg1, trg2}
1456 function _form(self, ...)
1457 local cbi = require "luci.cbi"
1458 local tpl = require "luci.template"
1459 local http = require "luci.http"
1461 local maps = luci.cbi.load(self.model, ...)
1465 for i, res in ipairs(maps) do
1466 local cstate = res:parse()
1467 if cstate and (not state or cstate < state) then
1472 http.header("X-CBI-State", state or 0)
1473 tpl.render("header")
1474 for i, res in ipairs(maps) do
1477 tpl.render("footer")
1480 function form(model)
1483 post = { ["cbi.submit"] = true },
1488 translate = i18n.translate
1490 -- This function does not actually translate the given argument but
1491 -- is used by build/i18n-scan.pl to find translatable entries.