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_depends(spec)
138 if type(spec.depends) ~= "table" then
142 if type(spec.depends.fs) == "table" then
143 local satisfied = false
144 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
145 for _, alternative in ipairs(alternatives) do
146 if check_fs_depends(alternative) then
151 if not satisfied then
156 if type(spec.depends.uci) == "table" then
157 local satisfied = false
158 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
159 for _, alternative in ipairs(alternatives) do
160 if check_uci_depends(alternative) then
165 if not satisfied then
173 local function target_to_json(target, module)
176 if target.type == "call" then
180 ["function"] = target.name,
181 ["parameters"] = target.argv
183 elseif target.type == "view" then
186 ["path"] = target.view
188 elseif target.type == "template" then
190 ["type"] = "template",
191 ["path"] = target.view
193 elseif target.type == "cbi" then
196 ["path"] = target.model,
197 ["config"] = target.config
199 elseif target.type == "form" then
202 ["path"] = target.model
204 elseif target.type == "firstchild" then
206 ["type"] = "firstchild"
208 elseif target.type == "firstnode" then
210 ["type"] = "firstchild",
213 elseif target.type == "arcombine" then
214 if type(target.targets) == "table" then
216 ["type"] = "arcombine",
218 target_to_json(target.targets[1], module),
219 target_to_json(target.targets[2], module)
223 elseif target.type == "alias" then
226 ["path"] = table.concat(target.req, "/")
228 elseif target.type == "rewrite" then
230 ["type"] = "rewrite",
231 ["path"] = table.concat(target.req, "/"),
232 ["remove"] = target.n
236 if target.post and action then
237 action.post = target.post
243 local function tree_to_json(node, json)
244 local fs = require "nixio.fs"
245 local util = require "luci.util"
247 if type(node.nodes) == "table" then
248 for subname, subnode in pairs(node.nodes) do
250 title = util.striptags(subnode.title),
251 order = subnode.order
262 if subnode.setuser then
263 spec.setuser = subnode.setuser
266 if subnode.setgroup then
267 spec.setgroup = subnode.setgroup
270 if type(subnode.target) == "table" then
271 spec.action = target_to_json(subnode.target, subnode.module)
274 if type(subnode.file_depends) == "table" then
275 for _, v in ipairs(subnode.file_depends) do
276 spec.depends = spec.depends or {}
277 spec.depends.fs = spec.depends.fs or {}
279 local ft = fs.stat(v, "type")
281 spec.depends.fs[v] = "directory"
282 elseif v:match("/s?bin/") then
283 spec.depends.fs[v] = "executable"
285 spec.depends.fs[v] = "file"
290 if type(subnode.uci_depends) == "table" then
291 for k, v in pairs(subnode.uci_depends) do
292 spec.depends = spec.depends or {}
293 spec.depends.uci = spec.depends.uci or {}
294 spec.depends.uci[k] = v
298 if (subnode.sysauth_authenticator ~= nil) or
299 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
301 if subnode.sysauth_authenticator == "htmlauth" then
304 methods = { "cookie:sysauth" }
306 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
309 methods = { "query:auth", "cookie:sysauth" }
311 elseif subnode.module == "luci.controller.admin.uci" then
314 methods = { "param:sid" }
317 elseif subnode.sysauth == false then
321 if not spec.action then
325 spec.satisfied = check_depends(spec)
326 json.children = json.children or {}
327 json.children[subname] = tree_to_json(subnode, spec)
334 function build_url(...)
336 local url = { http.getenv("SCRIPT_NAME") or "" }
339 for _, p in ipairs(path) do
340 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
350 return table.concat(url, "")
354 function error404(message)
355 http.status(404, "Not Found")
356 message = message or "Not Found"
358 local function render()
359 local template = require "luci.template"
360 template.render("error404")
363 if not util.copcall(render) then
364 http.prepare_content("text/plain")
371 function error500(message)
373 if not context.template_header_sent then
374 http.status(500, "Internal Server Error")
375 http.prepare_content("text/plain")
378 require("luci.template")
379 if not util.copcall(luci.template.render, "error500", {message=message}) then
380 http.prepare_content("text/plain")
387 local function determine_request_language()
388 local conf = require "luci.config"
389 assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
391 local lang = conf.main.lang or "auto"
392 if lang == "auto" then
393 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
394 for aclang in aclang:gmatch("[%w_-]+") do
395 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
396 if country and culture then
397 local cc = "%s_%s" %{ country, culture:lower() }
398 if conf.languages[cc] then
401 elseif conf.languages[country] then
405 elseif conf.languages[aclang] then
412 if lang == "auto" then
416 i18n.setlanguage(lang)
419 function httpdispatch(request, prefix)
420 http.context.request = request
425 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
428 for _, node in ipairs(prefix) do
434 for node in pathinfo:gmatch("[^/%z]+") do
438 determine_request_language()
440 local stat, err = util.coxpcall(function()
441 dispatch(context.request)
446 --context._disable_memtrace()
449 local function require_post_security(target, args)
450 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
451 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
454 if type(target) == "table" then
455 if type(target.post) == "table" then
456 local param_name, required_val, request_val
458 for param_name, required_val in pairs(target.post) do
459 request_val = http.formvalue(param_name)
461 if (type(required_val) == "string" and
462 request_val ~= required_val) or
463 (required_val == true and request_val == nil)
472 return (target.post == true)
478 function test_post_security()
479 if http.getenv("REQUEST_METHOD") ~= "POST" then
480 http.status(405, "Method Not Allowed")
481 http.header("Allow", "POST")
485 if http.formvalue("token") ~= context.authtoken then
486 http.status(403, "Forbidden")
487 luci.template.render("csrftoken")
494 local function session_retrieve(sid, allowed_users)
495 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
497 if type(sdat) == "table" and
498 type(sdat.values) == "table" and
499 type(sdat.values.token) == "string" and
500 (not allowed_users or
501 util.contains(allowed_users, sdat.values.username))
503 uci:set_session_id(sid)
504 return sid, sdat.values
510 local function session_setup(user, pass, allowed_users)
511 if util.contains(allowed_users, user) then
512 local login = util.ubus("session", "login", {
515 timeout = tonumber(luci.config.sauth.sessiontime)
518 local rp = context.requestpath
519 and table.concat(context.requestpath, "/") or ""
521 if type(login) == "table" and
522 type(login.ubus_rpc_session) == "string"
524 util.ubus("session", "set", {
525 ubus_rpc_session = login.ubus_rpc_session,
526 values = { token = sys.uniqueid(16) }
529 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
530 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
532 return session_retrieve(login.ubus_rpc_session)
535 io.stderr:write("luci: failed login on /%s for %s from %s\n"
536 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
542 local function check_authentication(method)
543 local auth_type, auth_param = method:match("^(%w+):(.+)$")
546 if auth_type == "cookie" then
547 sid = http.getcookie(auth_param)
548 elseif auth_type == "param" then
549 sid = http.formvalue(auth_param)
550 elseif auth_type == "query" then
551 sid = http.formvalue(auth_param, true)
554 return session_retrieve(sid)
557 local function get_children(node)
560 if not node.wildcard and type(node.children) == "table" then
561 for name, child in pairs(node.children) do
562 children[#children+1] = {
565 order = child.order or 1000
569 table.sort(children, function(a, b)
570 if a.order == b.order then
571 return a.name < b.name
573 return a.order < b.order
581 local function find_subnode(root, prefix, recurse, descended)
582 local children = get_children(root)
584 if #children > 0 and (not descended or recurse) then
585 local sub_path = { unpack(prefix) }
587 if recurse == false then
591 for _, child in ipairs(children) do
592 sub_path[#prefix+1] = child.name
594 local res_path = find_subnode(child.node, sub_path, recurse, true)
604 root.action.type == "cbi" or
605 root.action.type == "form" or
606 root.action.type == "view" or
607 root.action.type == "template" or
608 root.action.type == "arcombine"
615 local function merge_trees(node_a, node_b)
616 for k, v in pairs(node_b) do
617 if k == "children" then
618 node_a.children = node_a.children or {}
620 for name, spec in pairs(v) do
621 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
628 if type(node_a.action) == "table" and
629 node_a.action.type == "firstchild" and
630 node_a.children == nil
632 node_a.satisfied = false
639 local tree = context.tree or createtree()
640 local lua_tree = tree_to_json(tree, {
642 ["type"] = "firstchild",
647 local json_tree = createtree_json()
648 return merge_trees(lua_tree, json_tree)
651 local function init_template_engine(ctx)
652 local tpl = require "luci.template"
653 local media = luci.config.main.mediaurlbase
655 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
657 for name, theme in pairs(luci.config.themes) do
658 if name:sub(1,1) ~= "." and pcall(tpl.Template,
659 "themes/%s/header" % fs.basename(theme)) then
663 assert(media, "No valid theme found")
666 local function _ifattr(cond, key, val, noescape)
668 local env = getfenv(3)
669 local scope = (type(env.self) == "table") and env.self
670 if type(val) == "table" then
671 if not next(val) then
674 val = util.serialize_json(val)
678 val = tostring(val or
679 (type(env[key]) ~= "function" and env[key]) or
680 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
682 if noescape ~= true then
683 val = util.pcdata(val)
686 return string.format(' %s="%s"', tostring(key), val)
692 tpl.context.viewns = setmetatable({
694 include = function(name) tpl.Template(name):render(getfenv(2)) end;
695 translate = i18n.translate;
696 translatef = i18n.translatef;
697 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
698 striptags = util.striptags;
699 pcdata = util.pcdata;
701 theme = fs.basename(media);
702 resource = luci.config.main.resourcebase;
703 ifattr = function(...) return _ifattr(...) end;
704 attr = function(...) return _ifattr(true, ...) end;
706 }, {__index=function(tbl, key)
707 if key == "controller" then
709 elseif key == "REQUEST_URI" then
710 return build_url(unpack(ctx.requestpath))
711 elseif key == "FULL_REQUEST_URI" then
712 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
713 local query = http.getenv("QUERY_STRING")
714 if query and #query > 0 then
718 return table.concat(url, "")
719 elseif key == "token" then
722 return rawget(tbl, key) or _G[key]
729 function dispatch(request)
730 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
733 local auth, cors, suid, sgid
734 local menu = menu_json()
737 local requested_path_full = {}
738 local requested_path_node = {}
739 local requested_path_args = {}
741 for i, s in ipairs(request) do
742 if type(page.children) ~= "table" or not page.children[s] then
747 if not page.children[s].satisfied then
752 page = page.children[s]
753 auth = page.auth or auth
754 cors = page.cors or cors
755 suid = page.setuser or suid
756 sgid = page.setgroup or sgid
758 requested_path_full[i] = s
759 requested_path_node[i] = s
761 if page.wildcard then
762 for j = i + 1, #request do
763 requested_path_args[j - i] = request[j]
764 requested_path_full[j] = request[j]
770 local tpl = init_template_engine(ctx)
772 ctx.args = requested_path_args
773 ctx.path = requested_path_node
774 ctx.dispatched = page
776 ctx.requestpath = ctx.requestpath or requested_path_full
777 ctx.requestargs = ctx.requestargs or requested_path_args
778 ctx.requested = ctx.requested or page
780 if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
782 for _, method in ipairs(auth.methods) do
783 sid, sdat = check_authentication(method)
790 if not (sid and sdat) and auth.login then
791 local user = http.getenv("HTTP_AUTH_USER")
792 local pass = http.getenv("HTTP_AUTH_PASS")
794 if user == nil and pass == nil then
795 user = http.formvalue("luci_username")
796 pass = http.formvalue("luci_password")
799 sid, sdat = session_setup(user, pass, { "root" })
804 http.status(403, "Forbidden")
805 http.header("X-LuCI-Login-Required", "yes")
807 return tpl.render("sysauth", { duser = "root", fuser = user })
810 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
811 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
814 http.redirect(build_url(unpack(ctx.requestpath)))
818 if not sid or not sdat then
819 http.status(403, "Forbidden")
820 http.header("X-LuCI-Login-Required", "yes")
824 ctx.authsession = sid
825 ctx.authtoken = sdat.token
826 ctx.authuser = sdat.username
829 local action = (page and type(page.action) == "table") and page.action or {}
831 if action.type == "arcombine" then
832 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
835 if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
836 luci.http.status(200, "OK")
837 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
838 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
842 if require_post_security(action) then
843 if not test_post_security() then
849 sys.process.setgroup(sgid)
853 sys.process.setuser(suid)
856 if action.type == "view" then
857 tpl.render("view", { view = action.path })
859 elseif action.type == "call" then
860 local ok, mod = util.copcall(require, action.module)
866 local func = mod[action["function"]]
869 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
871 assert(type(func) == "function",
872 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
873 'of type "' .. type(func) .. '".')
875 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
876 for _, s in ipairs(requested_path_args) do
880 local ok, err = util.copcall(func, unpack(argv))
885 elseif action.type == "firstchild" then
886 local sub_request = find_subnode(page, requested_path_full, action.recurse)
888 dispatch(sub_request)
890 tpl.render("empty_node_placeholder", getfenv(1))
893 elseif action.type == "alias" then
894 local sub_request = {}
895 for name in action.path:gmatch("[^/]+") do
896 sub_request[#sub_request + 1] = name
899 for _, s in ipairs(requested_path_args) do
900 sub_request[#sub_request + 1] = s
903 dispatch(sub_request)
905 elseif action.type == "rewrite" then
906 local sub_request = { unpack(request) }
907 for i = 1, action.remove do
908 table.remove(sub_request, 1)
912 for s in action.path:gmatch("[^/]+") do
913 table.insert(sub_request, n, s)
917 for _, s in ipairs(requested_path_args) do
918 sub_request[#sub_request + 1] = s
921 dispatch(sub_request)
923 elseif action.type == "template" then
924 tpl.render(action.path, getfenv(1))
926 elseif action.type == "cbi" then
927 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
929 elseif action.type == "form" then
930 _form({ model = action.path }, unpack(requested_path_args))
933 local root = find_subnode(menu, {}, true)
935 error404("No root node was registered, this usually happens if no module was installed.\n" ..
936 "Install luci-mod-admin-full and retry. " ..
937 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
939 error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
940 "If this url belongs to an extension, make sure it is properly installed.\n" ..
941 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
946 function createindex()
947 local controllers = { }
948 local base = "%s/controller/" % util.libpath()
951 for path in (fs.glob("%s*.lua" % base) or function() end) do
952 controllers[#controllers+1] = path
955 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
956 controllers[#controllers+1] = path
960 local cachedate = fs.stat(indexcache, "mtime")
963 for _, obj in ipairs(controllers) do
964 local omtime = fs.stat(obj, "mtime")
965 realdate = (omtime and omtime > realdate) and omtime or realdate
968 if cachedate > realdate and sys.process.info("uid") == 0 then
970 sys.process.info("uid") == fs.stat(indexcache, "uid")
971 and fs.stat(indexcache, "modestr") == "rw-------",
972 "Fatal: Indexcache is not sane!"
975 index = loadfile(indexcache)()
983 for _, path in ipairs(controllers) do
984 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
985 local mod = require(modname)
987 "Invalid controller file found\n" ..
988 "The file '" .. path .. "' contains an invalid module line.\n" ..
989 "Please verify whether the module name is set to '" .. modname ..
990 "' - It must correspond to the file path!")
992 local idx = mod.index
993 if type(idx) == "function" then
999 local f = nixio.open(indexcache, "w", 600)
1000 f:writeall(util.get_bytecode(index))
1005 function createtree_json()
1006 local json = require "luci.jsonc"
1015 setgroup = "string",
1018 wildcard = "boolean"
1025 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1026 files[#files+1] = file
1029 local st = fs.stat(file)
1031 fprint[#fprint+1] = '%x' % st.ino
1032 fprint[#fprint+1] = '%x' % st.mtime
1033 fprint[#fprint+1] = '%x' % st.size
1039 cachefile = "%s.%s.json" %{
1041 nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1044 local res = json.parse(fs.readfile(cachefile) or "")
1049 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1054 for _, file in ipairs(files) do
1055 local data = json.parse(fs.readfile(file) or "")
1056 if type(data) == "table" then
1057 for path, spec in pairs(data) do
1058 if type(spec) == "table" then
1061 for s in path:gmatch("[^/]+") do
1063 node.wildcard = true
1067 node.children = node.children or {}
1068 node.children[s] = node.children[s] or {}
1069 node = node.children[s]
1072 if node ~= tree then
1073 for k, t in pairs(schema) do
1074 if type(spec[k]) == t then
1079 node.satisfied = check_depends(spec)
1087 fs.writefile(cachefile, json.stringify(tree))
1093 -- Build the index before if it does not exist yet.
1094 function createtree()
1100 local tree = {nodes={}, inreq=true}
1102 ctx.treecache = setmetatable({}, {__mode="v"})
1105 local scope = setmetatable({}, {__index = luci.dispatcher})
1107 for k, v in pairs(index) do
1116 function assign(path, clone, title, order)
1117 local obj = node(unpack(path))
1124 setmetatable(obj, {__index = _create_node(clone)})
1129 function entry(path, target, title, order)
1130 local c = node(unpack(path))
1135 c.module = getfenv(2)._NAME
1140 -- enabling the node.
1142 return _create_node({...})
1146 local c = _create_node({...})
1148 c.module = getfenv(2)._NAME
1154 function lookup(...)
1155 local i, path = nil, {}
1156 for i = 1, select('#', ...) do
1157 local name, arg = nil, tostring(select(i, ...))
1158 for name in arg:gmatch("[^/]+") do
1159 path[#path+1] = name
1163 for i = #path, 1, -1 do
1164 local node = context.treecache[table.concat(path, ".", 1, i)]
1165 if node and (i == #path or node.leaf) then
1166 return node, build_url(unpack(path))
1171 function _create_node(path)
1176 local name = table.concat(path, ".")
1177 local c = context.treecache[name]
1180 local last = table.remove(path)
1181 local parent = _create_node(path)
1183 c = {nodes={}, auto=true, inreq=true}
1185 parent.nodes[last] = c
1186 context.treecache[name] = c
1192 -- Subdispatchers --
1194 function firstchild()
1195 return { type = "firstchild" }
1198 function firstnode()
1199 return { type = "firstnode" }
1203 return { type = "alias", req = { ... } }
1206 function rewrite(n, ...)
1207 return { type = "rewrite", n = n, req = { ... } }
1210 function call(name, ...)
1211 return { type = "call", argv = {...}, name = name }
1214 function post_on(params, name, ...)
1224 return post_on(true, ...)
1228 function template(name)
1229 return { type = "template", view = name }
1233 return { type = "view", view = name }
1237 function _cbi(self, ...)
1238 local cbi = require "luci.cbi"
1239 local tpl = require "luci.template"
1240 local http = require "luci.http"
1242 local config = self.config or {}
1243 local maps = cbi.load(self.model, ...)
1248 for i, res in ipairs(maps) do
1249 if util.instanceof(res, cbi.SimpleForm) then
1250 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1253 io.stderr:write("please change %s to use the form() action instead.\n"
1254 % table.concat(context.request, "/"))
1258 local cstate = res:parse()
1259 if cstate and (not state or cstate < state) then
1264 local function _resolve_path(path)
1265 return type(path) == "table" and build_url(unpack(path)) or path
1268 if config.on_valid_to and state and state > 0 and state < 2 then
1269 http.redirect(_resolve_path(config.on_valid_to))
1273 if config.on_changed_to and state and state > 1 then
1274 http.redirect(_resolve_path(config.on_changed_to))
1278 if config.on_success_to and state and state > 0 then
1279 http.redirect(_resolve_path(config.on_success_to))
1283 if config.state_handler then
1284 if not config.state_handler(state, maps) then
1289 http.header("X-CBI-State", state or 0)
1291 if not config.noheader then
1292 tpl.render("cbi/header", {state = state})
1297 local applymap = false
1298 local pageaction = true
1299 local parsechain = { }
1301 for i, res in ipairs(maps) do
1302 if res.apply_needed and res.parsechain then
1304 for _, c in ipairs(res.parsechain) do
1305 parsechain[#parsechain+1] = c
1310 if res.redirect then
1311 redirect = redirect or res.redirect
1314 if res.pageaction == false then
1319 messages = messages or { }
1320 messages[#messages+1] = res.message
1324 for i, res in ipairs(maps) do
1326 firstmap = (i == 1),
1327 redirect = redirect,
1328 messages = messages,
1329 pageaction = pageaction,
1330 parsechain = parsechain
1334 if not config.nofooter then
1335 tpl.render("cbi/footer", {
1337 pageaction = pageaction,
1338 redirect = redirect,
1340 autoapply = config.autoapply,
1341 trigger_apply = applymap
1346 function cbi(model, config)
1349 post = { ["cbi.submit"] = true },
1356 function arcombine(trg1, trg2)
1360 targets = {trg1, trg2}
1365 function _form(self, ...)
1366 local cbi = require "luci.cbi"
1367 local tpl = require "luci.template"
1368 local http = require "luci.http"
1370 local maps = luci.cbi.load(self.model, ...)
1374 for i, res in ipairs(maps) do
1375 local cstate = res:parse()
1376 if cstate and (not state or cstate < state) then
1381 http.header("X-CBI-State", state or 0)
1382 tpl.render("header")
1383 for i, res in ipairs(maps) do
1386 tpl.render("footer")
1389 function form(model)
1392 post = { ["cbi.submit"] = true },
1397 translate = i18n.translate
1399 -- This function does not actually translate the given argument but
1400 -- is used by build/i18n-scan.pl to find translatable entries.