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 (subnode.sysauth_authenticator ~= nil) or
328 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
330 if subnode.sysauth_authenticator == "htmlauth" then
333 methods = { "cookie:sysauth" }
335 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
338 methods = { "query:auth", "cookie:sysauth" }
340 elseif subnode.module == "luci.controller.admin.uci" then
343 methods = { "param:sid" }
346 elseif subnode.sysauth == false then
350 if not spec.action then
354 spec.satisfied = check_depends(spec)
355 json.children = json.children or {}
356 json.children[subname] = tree_to_json(subnode, spec)
363 function build_url(...)
365 local url = { http.getenv("SCRIPT_NAME") or "" }
368 for _, p in ipairs(path) do
369 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
379 return table.concat(url, "")
383 function error404(message)
384 http.status(404, "Not Found")
385 message = message or "Not Found"
387 local function render()
388 local template = require "luci.template"
389 template.render("error404")
392 if not util.copcall(render) then
393 http.prepare_content("text/plain")
400 function error500(message)
402 if not context.template_header_sent then
403 http.status(500, "Internal Server Error")
404 http.prepare_content("text/plain")
407 require("luci.template")
408 if not util.copcall(luci.template.render, "error500", {message=message}) then
409 http.prepare_content("text/plain")
416 local function determine_request_language()
417 local conf = require "luci.config"
418 assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
420 local lang = conf.main.lang or "auto"
421 if lang == "auto" then
422 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
423 for aclang in aclang:gmatch("[%w_-]+") do
424 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
425 if country and culture then
426 local cc = "%s_%s" %{ country, culture:lower() }
427 if conf.languages[cc] then
430 elseif conf.languages[country] then
434 elseif conf.languages[aclang] then
441 if lang == "auto" then
445 i18n.setlanguage(lang)
448 function httpdispatch(request, prefix)
449 http.context.request = request
454 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
457 for _, node in ipairs(prefix) do
463 for node in pathinfo:gmatch("[^/%z]+") do
467 determine_request_language()
469 local stat, err = util.coxpcall(function()
470 dispatch(context.request)
475 --context._disable_memtrace()
478 local function require_post_security(target, args)
479 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
480 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
483 if type(target) == "table" then
484 if type(target.post) == "table" then
485 local param_name, required_val, request_val
487 for param_name, required_val in pairs(target.post) do
488 request_val = http.formvalue(param_name)
490 if (type(required_val) == "string" and
491 request_val ~= required_val) or
492 (required_val == true and request_val == nil)
501 return (target.post == true)
507 function test_post_security()
508 if http.getenv("REQUEST_METHOD") ~= "POST" then
509 http.status(405, "Method Not Allowed")
510 http.header("Allow", "POST")
514 if http.formvalue("token") ~= context.authtoken then
515 http.status(403, "Forbidden")
516 luci.template.render("csrftoken")
523 local function session_retrieve(sid, allowed_users)
524 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
525 local sacl = util.ubus("session", "access", { ubus_rpc_session = sid })
527 if type(sdat) == "table" and
528 type(sdat.values) == "table" and
529 type(sdat.values.token) == "string" and
530 (not allowed_users or
531 util.contains(allowed_users, sdat.values.username))
533 uci:set_session_id(sid)
534 return sid, sdat.values, type(sacl) == "table" and sacl or {}
540 local function session_setup(user, pass)
541 local login = util.ubus("session", "login", {
544 timeout = tonumber(luci.config.sauth.sessiontime)
547 local rp = context.requestpath
548 and table.concat(context.requestpath, "/") or ""
550 if type(login) == "table" and
551 type(login.ubus_rpc_session) == "string"
553 util.ubus("session", "set", {
554 ubus_rpc_session = login.ubus_rpc_session,
555 values = { token = sys.uniqueid(16) }
558 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
559 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })
561 return session_retrieve(login.ubus_rpc_session)
564 io.stderr:write("luci: failed login on /%s for %s from %s\n"
565 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })
568 local function check_authentication(method)
569 local auth_type, auth_param = method:match("^(%w+):(.+)$")
572 if auth_type == "cookie" then
573 sid = http.getcookie(auth_param)
574 elseif auth_type == "param" then
575 sid = http.formvalue(auth_param)
576 elseif auth_type == "query" then
577 sid = http.formvalue(auth_param, true)
580 return session_retrieve(sid)
583 local function get_children(node)
586 if not node.wildcard and type(node.children) == "table" then
587 for name, child in pairs(node.children) do
588 children[#children+1] = {
591 order = child.order or 1000
595 table.sort(children, function(a, b)
596 if a.order == b.order then
597 return a.name < b.name
599 return a.order < b.order
607 local function find_subnode(root, prefix, recurse, descended)
608 local children = get_children(root)
610 if #children > 0 and (not descended or recurse) then
611 local sub_path = { unpack(prefix) }
613 if recurse == false then
617 for _, child in ipairs(children) do
618 sub_path[#prefix+1] = child.name
620 local res_path = find_subnode(child.node, sub_path, recurse, true)
630 root.action.type == "cbi" or
631 root.action.type == "form" or
632 root.action.type == "view" or
633 root.action.type == "template" or
634 root.action.type == "arcombine"
641 local function merge_trees(node_a, node_b)
642 for k, v in pairs(node_b) do
643 if k == "children" then
644 node_a.children = node_a.children or {}
646 for name, spec in pairs(v) do
647 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
654 if type(node_a.action) == "table" and
655 node_a.action.type == "firstchild" and
656 node_a.children == nil
658 node_a.satisfied = false
664 local function apply_tree_acls(node, acl)
665 if type(node.children) == "table" then
666 for _, child in pairs(node.children) do
667 apply_tree_acls(child, acl)
672 if type(node.depends) == "table" then
673 perm = check_acl_depends(node.depends.acl, acl["access-group"])
679 node.satisfied = false
680 elseif perm == false then
685 function menu_json(acl)
686 local tree = context.tree or createtree()
687 local lua_tree = tree_to_json(tree, {
689 ["type"] = "firstchild",
694 local json_tree = createtree_json()
695 local menu_tree = merge_trees(lua_tree, json_tree)
698 apply_tree_acls(menu_tree, acl)
704 local function init_template_engine(ctx)
705 local tpl = require "luci.template"
706 local media = luci.config.main.mediaurlbase
708 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
710 for name, theme in pairs(luci.config.themes) do
711 if name:sub(1,1) ~= "." and pcall(tpl.Template,
712 "themes/%s/header" % fs.basename(theme)) then
716 assert(media, "No valid theme found")
719 local function _ifattr(cond, key, val, noescape)
721 local env = getfenv(3)
722 local scope = (type(env.self) == "table") and env.self
723 if type(val) == "table" then
724 if not next(val) then
727 val = util.serialize_json(val)
731 val = tostring(val or
732 (type(env[key]) ~= "function" and env[key]) or
733 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
735 if noescape ~= true then
736 val = util.pcdata(val)
739 return string.format(' %s="%s"', tostring(key), val)
745 tpl.context.viewns = setmetatable({
747 include = function(name) tpl.Template(name):render(getfenv(2)) end;
748 translate = i18n.translate;
749 translatef = i18n.translatef;
750 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
751 striptags = util.striptags;
752 pcdata = util.pcdata;
754 theme = fs.basename(media);
755 resource = luci.config.main.resourcebase;
756 ifattr = function(...) return _ifattr(...) end;
757 attr = function(...) return _ifattr(true, ...) end;
759 }, {__index=function(tbl, key)
760 if key == "controller" then
762 elseif key == "REQUEST_URI" then
763 return build_url(unpack(ctx.requestpath))
764 elseif key == "FULL_REQUEST_URI" then
765 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
766 local query = http.getenv("QUERY_STRING")
767 if query and #query > 0 then
771 return table.concat(url, "")
772 elseif key == "token" then
775 return rawget(tbl, key) or _G[key]
782 function dispatch(request)
783 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
786 local auth, cors, suid, sgid
787 local menu = menu_json()
790 local requested_path_full = {}
791 local requested_path_node = {}
792 local requested_path_args = {}
794 local required_path_acls = {}
796 for i, s in ipairs(request) do
797 if type(page.children) ~= "table" or not page.children[s] then
802 if not page.children[s].satisfied then
807 page = page.children[s]
808 auth = page.auth or auth
809 cors = page.cors or cors
810 suid = page.setuser or suid
811 sgid = page.setgroup or sgid
813 if type(page.depends) == "table" and type(page.depends.acl) == "table" then
814 for _, group in ipairs(page.depends.acl) do
816 for _, item in ipairs(required_path_acls) do
817 if item == group then
823 required_path_acls[#required_path_acls + 1] = group
828 requested_path_full[i] = s
829 requested_path_node[i] = s
831 if page.wildcard then
832 for j = i + 1, #request do
833 requested_path_args[j - i] = request[j]
834 requested_path_full[j] = request[j]
840 local tpl = init_template_engine(ctx)
842 ctx.args = requested_path_args
843 ctx.path = requested_path_node
844 ctx.dispatched = page
846 ctx.requestpath = ctx.requestpath or requested_path_full
847 ctx.requestargs = ctx.requestargs or requested_path_args
848 ctx.requested = ctx.requested or page
850 if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
851 local sid, sdat, sacl
852 for _, method in ipairs(auth.methods) do
853 sid, sdat, sacl = check_authentication(method)
855 if sid and sdat and sacl then
860 if not (sid and sdat and sacl) and auth.login then
861 local user = http.getenv("HTTP_AUTH_USER")
862 local pass = http.getenv("HTTP_AUTH_PASS")
864 if user == nil and pass == nil then
865 user = http.formvalue("luci_username")
866 pass = http.formvalue("luci_password")
869 if user and pass then
870 sid, sdat, sacl = session_setup(user, pass)
876 http.status(403, "Forbidden")
877 http.header("X-LuCI-Login-Required", "yes")
879 return tpl.render("sysauth", { duser = "root", fuser = user })
882 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
883 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
886 http.redirect(build_url(unpack(ctx.requestpath)))
890 if not sid or not sdat or not sacl then
891 http.status(403, "Forbidden")
892 http.header("X-LuCI-Login-Required", "yes")
896 ctx.authsession = sid
897 ctx.authtoken = sdat.token
898 ctx.authuser = sdat.username
902 if #required_path_acls > 0 then
903 local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"])
905 http.status(403, "Forbidden")
909 page.readonly = not perm
912 local action = (page and type(page.action) == "table") and page.action or {}
914 if action.type == "arcombine" then
915 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
918 if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
919 luci.http.status(200, "OK")
920 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
921 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
925 if require_post_security(action) then
926 if not test_post_security() then
932 sys.process.setgroup(sgid)
936 sys.process.setuser(suid)
939 if action.type == "view" then
940 tpl.render("view", { view = action.path })
942 elseif action.type == "call" then
943 local ok, mod = util.copcall(require, action.module)
949 local func = mod[action["function"]]
952 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
954 assert(type(func) == "function",
955 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
956 'of type "' .. type(func) .. '".')
958 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
959 for _, s in ipairs(requested_path_args) do
963 local ok, err = util.copcall(func, unpack(argv))
968 elseif action.type == "firstchild" then
969 local sub_request = find_subnode(page, requested_path_full, action.recurse)
971 dispatch(sub_request)
973 tpl.render("empty_node_placeholder", getfenv(1))
976 elseif action.type == "alias" then
977 local sub_request = {}
978 for name in action.path:gmatch("[^/]+") do
979 sub_request[#sub_request + 1] = name
982 for _, s in ipairs(requested_path_args) do
983 sub_request[#sub_request + 1] = s
986 dispatch(sub_request)
988 elseif action.type == "rewrite" then
989 local sub_request = { unpack(request) }
990 for i = 1, action.remove do
991 table.remove(sub_request, 1)
995 for s in action.path:gmatch("[^/]+") do
996 table.insert(sub_request, n, s)
1000 for _, s in ipairs(requested_path_args) do
1001 sub_request[#sub_request + 1] = s
1004 dispatch(sub_request)
1006 elseif action.type == "template" then
1007 tpl.render(action.path, getfenv(1))
1009 elseif action.type == "cbi" then
1010 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
1012 elseif action.type == "form" then
1013 _form({ model = action.path }, unpack(requested_path_args))
1016 local root = find_subnode(menu, {}, true)
1018 error404("No root node was registered, this usually happens if no module was installed.\n" ..
1019 "Install luci-mod-admin-full and retry. " ..
1020 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
1022 error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
1023 "If this url belongs to an extension, make sure it is properly installed.\n" ..
1024 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
1029 function createindex()
1030 local controllers = { }
1031 local base = "%s/controller/" % util.libpath()
1034 for path in (fs.glob("%s*.lua" % base) or function() end) do
1035 controllers[#controllers+1] = path
1038 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1039 controllers[#controllers+1] = path
1043 local cachedate = fs.stat(indexcache, "mtime")
1046 for _, obj in ipairs(controllers) do
1047 local omtime = fs.stat(obj, "mtime")
1048 realdate = (omtime and omtime > realdate) and omtime or realdate
1051 if cachedate > realdate and sys.process.info("uid") == 0 then
1053 sys.process.info("uid") == fs.stat(indexcache, "uid")
1054 and fs.stat(indexcache, "modestr") == "rw-------",
1055 "Fatal: Indexcache is not sane!"
1058 index = loadfile(indexcache)()
1066 for _, path in ipairs(controllers) do
1067 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1068 local mod = require(modname)
1070 "Invalid controller file found\n" ..
1071 "The file '" .. path .. "' contains an invalid module line.\n" ..
1072 "Please verify whether the module name is set to '" .. modname ..
1073 "' - It must correspond to the file path!")
1075 local idx = mod.index
1076 if type(idx) == "function" then
1077 index[modname] = idx
1082 local f = nixio.open(indexcache, "w", 600)
1083 f:writeall(util.get_bytecode(index))
1088 function createtree_json()
1089 local json = require "luci.jsonc"
1098 setgroup = "string",
1101 wildcard = "boolean"
1108 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1109 files[#files+1] = file
1112 local st = fs.stat(file)
1114 fprint[#fprint+1] = '%x' % st.ino
1115 fprint[#fprint+1] = '%x' % st.mtime
1116 fprint[#fprint+1] = '%x' % st.size
1122 cachefile = "%s.%s.json" %{
1124 nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1127 local res = json.parse(fs.readfile(cachefile) or "")
1132 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1137 for _, file in ipairs(files) do
1138 local data = json.parse(fs.readfile(file) or "")
1139 if type(data) == "table" then
1140 for path, spec in pairs(data) do
1141 if type(spec) == "table" then
1144 for s in path:gmatch("[^/]+") do
1146 node.wildcard = true
1150 node.children = node.children or {}
1151 node.children[s] = node.children[s] or {}
1152 node = node.children[s]
1155 if node ~= tree then
1156 for k, t in pairs(schema) do
1157 if type(spec[k]) == t then
1162 node.satisfied = check_depends(spec)
1170 fs.writefile(cachefile, json.stringify(tree))
1176 -- Build the index before if it does not exist yet.
1177 function createtree()
1183 local tree = {nodes={}, inreq=true}
1185 ctx.treecache = setmetatable({}, {__mode="v"})
1188 local scope = setmetatable({}, {__index = luci.dispatcher})
1190 for k, v in pairs(index) do
1199 function assign(path, clone, title, order)
1200 local obj = node(unpack(path))
1207 setmetatable(obj, {__index = _create_node(clone)})
1212 function entry(path, target, title, order)
1213 local c = node(unpack(path))
1218 c.module = getfenv(2)._NAME
1223 -- enabling the node.
1225 return _create_node({...})
1229 local c = _create_node({...})
1231 c.module = getfenv(2)._NAME
1237 function lookup(...)
1238 local i, path = nil, {}
1239 for i = 1, select('#', ...) do
1240 local name, arg = nil, tostring(select(i, ...))
1241 for name in arg:gmatch("[^/]+") do
1242 path[#path+1] = name
1246 for i = #path, 1, -1 do
1247 local node = context.treecache[table.concat(path, ".", 1, i)]
1248 if node and (i == #path or node.leaf) then
1249 return node, build_url(unpack(path))
1254 function _create_node(path)
1259 local name = table.concat(path, ".")
1260 local c = context.treecache[name]
1263 local last = table.remove(path)
1264 local parent = _create_node(path)
1266 c = {nodes={}, auto=true, inreq=true}
1268 parent.nodes[last] = c
1269 context.treecache[name] = c
1275 -- Subdispatchers --
1277 function firstchild()
1278 return { type = "firstchild" }
1281 function firstnode()
1282 return { type = "firstnode" }
1286 return { type = "alias", req = { ... } }
1289 function rewrite(n, ...)
1290 return { type = "rewrite", n = n, req = { ... } }
1293 function call(name, ...)
1294 return { type = "call", argv = {...}, name = name }
1297 function post_on(params, name, ...)
1307 return post_on(true, ...)
1311 function template(name)
1312 return { type = "template", view = name }
1316 return { type = "view", view = name }
1320 function _cbi(self, ...)
1321 local cbi = require "luci.cbi"
1322 local tpl = require "luci.template"
1323 local http = require "luci.http"
1325 local config = self.config or {}
1326 local maps = cbi.load(self.model, ...)
1331 for i, res in ipairs(maps) do
1332 if util.instanceof(res, cbi.SimpleForm) then
1333 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1336 io.stderr:write("please change %s to use the form() action instead.\n"
1337 % table.concat(context.request, "/"))
1341 local cstate = res:parse()
1342 if cstate and (not state or cstate < state) then
1347 local function _resolve_path(path)
1348 return type(path) == "table" and build_url(unpack(path)) or path
1351 if config.on_valid_to and state and state > 0 and state < 2 then
1352 http.redirect(_resolve_path(config.on_valid_to))
1356 if config.on_changed_to and state and state > 1 then
1357 http.redirect(_resolve_path(config.on_changed_to))
1361 if config.on_success_to and state and state > 0 then
1362 http.redirect(_resolve_path(config.on_success_to))
1366 if config.state_handler then
1367 if not config.state_handler(state, maps) then
1372 http.header("X-CBI-State", state or 0)
1374 if not config.noheader then
1375 tpl.render("cbi/header", {state = state})
1380 local applymap = false
1381 local pageaction = true
1382 local parsechain = { }
1384 for i, res in ipairs(maps) do
1385 if res.apply_needed and res.parsechain then
1387 for _, c in ipairs(res.parsechain) do
1388 parsechain[#parsechain+1] = c
1393 if res.redirect then
1394 redirect = redirect or res.redirect
1397 if res.pageaction == false then
1402 messages = messages or { }
1403 messages[#messages+1] = res.message
1407 for i, res in ipairs(maps) do
1409 firstmap = (i == 1),
1410 redirect = redirect,
1411 messages = messages,
1412 pageaction = pageaction,
1413 parsechain = parsechain
1417 if not config.nofooter then
1418 tpl.render("cbi/footer", {
1420 pageaction = pageaction,
1421 redirect = redirect,
1423 autoapply = config.autoapply,
1424 trigger_apply = applymap
1429 function cbi(model, config)
1432 post = { ["cbi.submit"] = true },
1439 function arcombine(trg1, trg2)
1443 targets = {trg1, trg2}
1448 function _form(self, ...)
1449 local cbi = require "luci.cbi"
1450 local tpl = require "luci.template"
1451 local http = require "luci.http"
1453 local maps = luci.cbi.load(self.model, ...)
1457 for i, res in ipairs(maps) do
1458 local cstate = res:parse()
1459 if cstate and (not state or cstate < state) then
1464 http.header("X-CBI-State", state or 0)
1465 tpl.render("header")
1466 for i, res in ipairs(maps) do
1469 tpl.render("footer")
1472 function form(model)
1475 post = { ["cbi.submit"] = true },
1480 translate = i18n.translate
1482 -- This function does not actually translate the given argument but
1483 -- is used by build/i18n-scan.pl to find translatable entries.