-- Copyright 2008 Steven Barth -- Copyright 2008-2015 Jo-Philipp Wich -- Licensed to the public under the Apache License 2.0. local fs = require "nixio.fs" local sys = require "luci.sys" local util = require "luci.util" local xml = require "luci.xml" local http = require "luci.http" local nixio = require "nixio", require "nixio.util" module("luci.dispatcher", package.seeall) context = util.threadlocal() uci = require "luci.model.uci" i18n = require "luci.i18n" _M.fs = fs -- Index table local index = nil local function check_fs_depends(spec) local fs = require "nixio.fs" for path, kind in pairs(spec) do if kind == "directory" then local empty = true for entry in (fs.dir(path) or function() end) do empty = false break end if empty then return false end elseif kind == "executable" then if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then return false end elseif kind == "file" then if fs.stat(path, "type") ~= "reg" then return false end elseif kind == "absent" then if fs.stat(path, "type") then return false end end end return true end local function check_uci_depends_options(conf, s, opts) local uci = require "luci.model.uci" if type(opts) == "string" then return (s[".type"] == opts) elseif opts == true then for option, value in pairs(s) do if option:byte(1) ~= 46 then return true end end elseif type(opts) == "table" then for option, value in pairs(opts) do local sval = s[option] if type(sval) == "table" then local found = false for _, v in ipairs(sval) do if v == value then found = true break end end if not found then return false end elseif value == true then if sval == nil then return false end else if sval ~= value then return false end end end end return true end local function check_uci_depends_section(conf, sect) local uci = require "luci.model.uci" for section, options in pairs(sect) do local stype = section:match("^@([A-Za-z0-9_%-]+)$") if stype then local found = false uci:foreach(conf, stype, function(s) if check_uci_depends_options(conf, s, options) then found = true return false end end) if not found then return false end else local s = uci:get_all(conf, section) if not s or not check_uci_depends_options(conf, s, options) then return false end end end return true end local function check_uci_depends(conf) local uci = require "luci.model.uci" for config, values in pairs(conf) do if values == true then local found = false uci:foreach(config, nil, function(s) found = true return false end) if not found then return false end elseif type(values) == "table" then if not check_uci_depends_section(config, values) then return false end end end return true end local function check_acl_depends(require_groups, groups) if type(require_groups) == "table" and #require_groups > 0 then local writable = false for _, group in ipairs(require_groups) do local read = false local write = false if type(groups) == "table" and type(groups[group]) == "table" then for _, perm in ipairs(groups[group]) do if perm == "read" then read = true elseif perm == "write" then write = true end end end if not read and not write then return nil elseif write then writable = true end end return writable end return true end local function check_depends(spec) if type(spec.depends) ~= "table" then return true end if type(spec.depends.fs) == "table" then local satisfied = false local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs } for _, alternative in ipairs(alternatives) do if check_fs_depends(alternative) then satisfied = true break end end if not satisfied then return false end end if type(spec.depends.uci) == "table" then local satisfied = false local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci } for _, alternative in ipairs(alternatives) do if check_uci_depends(alternative) then satisfied = true break end end if not satisfied then return false end end return true end local function target_to_json(target, module) local action if target.type == "call" then action = { ["type"] = "call", ["module"] = module, ["function"] = target.name, ["parameters"] = target.argv } elseif target.type == "view" then action = { ["type"] = "view", ["path"] = target.view } elseif target.type == "template" then action = { ["type"] = "template", ["path"] = target.view } elseif target.type == "cbi" then action = { ["type"] = "cbi", ["path"] = target.model, ["config"] = target.config } elseif target.type == "form" then action = { ["type"] = "form", ["path"] = target.model } elseif target.type == "firstchild" then action = { ["type"] = "firstchild" } elseif target.type == "firstnode" then action = { ["type"] = "firstchild", ["recurse"] = true } elseif target.type == "arcombine" then if type(target.targets) == "table" then action = { ["type"] = "arcombine", ["targets"] = { target_to_json(target.targets[1], module), target_to_json(target.targets[2], module) } } end elseif target.type == "alias" then action = { ["type"] = "alias", ["path"] = table.concat(target.req, "/") } elseif target.type == "rewrite" then action = { ["type"] = "rewrite", ["path"] = table.concat(target.req, "/"), ["remove"] = target.n } end if target.post and action then action.post = target.post end return action end local function tree_to_json(node, json) local fs = require "nixio.fs" local util = require "luci.util" if type(node.nodes) == "table" then for subname, subnode in pairs(node.nodes) do local spec = { title = xml.striptags(subnode.title), order = subnode.order } if subnode.leaf then spec.wildcard = true end if subnode.cors then spec.cors = true end if subnode.setuser then spec.setuser = subnode.setuser end if subnode.setgroup then spec.setgroup = subnode.setgroup end if type(subnode.target) == "table" then spec.action = target_to_json(subnode.target, subnode.module) end if type(subnode.file_depends) == "table" then for _, v in ipairs(subnode.file_depends) do spec.depends = spec.depends or {} spec.depends.fs = spec.depends.fs or {} local ft = fs.stat(v, "type") if ft == "dir" then spec.depends.fs[v] = "directory" elseif v:match("/s?bin/") then spec.depends.fs[v] = "executable" else spec.depends.fs[v] = "file" end end end if type(subnode.uci_depends) == "table" then for k, v in pairs(subnode.uci_depends) do spec.depends = spec.depends or {} spec.depends.uci = spec.depends.uci or {} spec.depends.uci[k] = v end end if type(subnode.acl_depends) == "table" then for _, acl in ipairs(subnode.acl_depends) do spec.depends = spec.depends or {} spec.depends.acl = spec.depends.acl or {} spec.depends.acl[#spec.depends.acl + 1] = acl end end if (subnode.sysauth_authenticator ~= nil) or (subnode.sysauth ~= nil and subnode.sysauth ~= false) then if subnode.sysauth_authenticator == "htmlauth" then spec.auth = { login = true, methods = { "cookie:sysauth_https", "cookie:sysauth_http" } } elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then spec.auth = { login = false, methods = { "query:auth", "cookie:sysauth_https", "cookie:sysauth_http", "cookie:sysauth" } } elseif subnode.module == "luci.controller.admin.uci" then spec.auth = { login = false, methods = { "param:sid" } } end elseif subnode.sysauth == false then spec.auth = {} end if not spec.action then spec.title = nil end spec.satisfied = check_depends(spec) json.children = json.children or {} json.children[subname] = tree_to_json(subnode, spec) end end return json end function build_url(...) local path = {...} local url = { http.getenv("SCRIPT_NAME") or "" } local p for _, p in ipairs(path) do if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then url[#url+1] = "/" url[#url+1] = p end end if #path == 0 then url[#url+1] = "/" end return table.concat(url, "") end function error404(message) http.status(404, "Not Found") message = message or "Not Found" local function render() local template = require "luci.template" template.render("error404", {message=message}) end if not util.copcall(render) then http.prepare_content("text/plain") http.write(message) end return false end function error500(message) util.perror(message) if not context.template_header_sent then http.status(500, "Internal Server Error") http.prepare_content("text/plain") http.write(message) else require("luci.template") if not util.copcall(luci.template.render, "error500", {message=message}) then http.prepare_content("text/plain") http.write(message) end end return false end local function determine_request_language() local conf = require "luci.config" assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'") local lang = conf.main.lang or "auto" if lang == "auto" then local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" for aclang in aclang:gmatch("[%w_-]+") do local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$") if country and culture then local cc = "%s_%s" %{ country, culture:lower() } if conf.languages[cc] then lang = cc break elseif conf.languages[country] then lang = country break end elseif conf.languages[aclang] then lang = aclang break end end end if lang == "auto" then lang = i18n.default end i18n.setlanguage(lang) end function httpdispatch(request, prefix) http.context.request = request local r = {} context.request = r local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true) if prefix then for _, node in ipairs(prefix) do r[#r+1] = node end end local node for node in pathinfo:gmatch("[^/%z]+") do r[#r+1] = node end determine_request_language() local stat, err = util.coxpcall(function() dispatch(context.request) end, error500) http.close() --context._disable_memtrace() end local function require_post_security(target, args) if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args) end if type(target) == "table" then if type(target.post) == "table" then local param_name, required_val, request_val for param_name, required_val in pairs(target.post) do request_val = http.formvalue(param_name) if (type(required_val) == "string" and request_val ~= required_val) or (required_val == true and request_val == nil) then return false end end return true end return (target.post == true) end return false end function test_post_security() if http.getenv("REQUEST_METHOD") ~= "POST" then http.status(405, "Method Not Allowed") http.header("Allow", "POST") return false end if http.formvalue("token") ~= context.authtoken then http.status(403, "Forbidden") luci.template.render("csrftoken") return false end return true end local function session_retrieve(sid, allowed_users) local sdat = util.ubus("session", "get", { ubus_rpc_session = sid }) local sacl = util.ubus("session", "access", { ubus_rpc_session = sid }) if type(sdat) == "table" and type(sdat.values) == "table" and type(sdat.values.token) == "string" and (not allowed_users or util.contains(allowed_users, sdat.values.username)) then uci:set_session_id(sid) return sid, sdat.values, type(sacl) == "table" and sacl or {} end return nil, nil, nil end local function session_setup(user, pass) local login = util.ubus("session", "login", { username = user, password = pass, timeout = tonumber(luci.config.sauth.sessiontime) }) local rp = context.requestpath and table.concat(context.requestpath, "/") or "" if type(login) == "table" and type(login.ubus_rpc_session) == "string" then util.ubus("session", "set", { ubus_rpc_session = login.ubus_rpc_session, values = { token = sys.uniqueid(16) } }) nixio.syslog("info", tostring("luci: accepted login on /%s for %s from %s\n" %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })) return session_retrieve(login.ubus_rpc_session) end nixio.syslog("info", tostring("luci: failed login on /%s for %s from %s\n" %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })) end local function check_authentication(method) local auth_type, auth_param = method:match("^(%w+):(.+)$") local sid, sdat if auth_type == "cookie" then sid = http.getcookie(auth_param) elseif auth_type == "param" then sid = http.formvalue(auth_param) elseif auth_type == "query" then sid = http.formvalue(auth_param, true) end return session_retrieve(sid) end local function merge_trees(node_a, node_b) for k, v in pairs(node_b) do if k == "children" then node_a.children = node_a.children or {} for name, spec in pairs(v) do node_a.children[name] = merge_trees(node_a.children[name] or {}, spec) end else node_a[k] = v end end if type(node_a.action) == "table" and node_a.action.type == "firstchild" and node_a.children == nil then node_a.satisfied = false end return node_a end local function apply_tree_acls(node, acl) if type(node.children) == "table" then for _, child in pairs(node.children) do apply_tree_acls(child, acl) end end local perm if type(node.depends) == "table" then perm = check_acl_depends(node.depends.acl, acl["access-group"]) else perm = true end if perm == nil then node.satisfied = false elseif perm == false then node.readonly = true end end function menu_json(acl) local tree = context.tree or createtree() local lua_tree = tree_to_json(tree, { action = { ["type"] = "firstchild", ["recurse"] = true } }) local json_tree = createtree_json() local menu_tree = merge_trees(lua_tree, json_tree) if acl then apply_tree_acls(menu_tree, acl) end return menu_tree end local function init_template_engine(ctx) local tpl = require "luci.template" local media = luci.config.main.mediaurlbase if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then media = nil for name, theme in pairs(luci.config.themes) do if name:sub(1,1) ~= "." and pcall(tpl.Template, "themes/%s/header" % fs.basename(theme)) then media = theme end end assert(media, "No valid theme found") end local function _ifattr(cond, key, val, noescape) if cond then local env = getfenv(3) local scope = (type(env.self) == "table") and env.self if type(val) == "table" then if not next(val) then return '' else val = util.serialize_json(val) end end val = tostring(val or (type(env[key]) ~= "function" and env[key]) or (scope and type(scope[key]) ~= "function" and scope[key]) or "") if noescape ~= true then val = xml.pcdata(val) end return string.format(' %s="%s"', tostring(key), val) else return '' end end tpl.context.viewns = setmetatable({ write = http.write; include = function(name) tpl.Template(name):render(getfenv(2)) end; translate = i18n.translate; translatef = i18n.translatef; export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end; striptags = xml.striptags; pcdata = xml.pcdata; media = media; theme = fs.basename(media); resource = luci.config.main.resourcebase; ifattr = function(...) return _ifattr(...) end; attr = function(...) return _ifattr(true, ...) end; url = build_url; }, {__index=function(tbl, key) if key == "controller" then return build_url() elseif key == "REQUEST_URI" then return build_url(unpack(ctx.requestpath)) elseif key == "FULL_REQUEST_URI" then local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") } local query = http.getenv("QUERY_STRING") if query and #query > 0 then url[#url+1] = "?" url[#url+1] = query end return table.concat(url, "") elseif key == "token" then return ctx.authtoken else return rawget(tbl, key) or _G[key] end end}) return tpl end function is_authenticated(auth) if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then local sid, sdat, sacl for _, method in ipairs(auth.methods) do sid, sdat, sacl = check_authentication(method) if sid and sdat and sacl then return sid, sdat, sacl end end end end local function ctx_append(ctx, name, node) ctx.path = ctx.path or {} ctx.path[#ctx.path + 1] = name ctx.acls = ctx.acls or {} local acls = (type(node.depends) == "table" and type(node.depends.acl) == "table") and node.depends.acl or {} for _, acl in ipairs(acls) do ctx.acls[_] = acl end ctx.auth = node.auth or ctx.auth ctx.cors = node.cors or ctx.cors ctx.suid = node.setuser or ctx.suid ctx.sgid = node.setgroup or ctx.sgid return ctx end local function node_weight(node) local weight = node.order or 9999 if weight > 9999 then weight = 9999 end if type(node.auth) == "table" and node.auth.login then weight = weight + 10000 end return weight end local function resolve_firstchild(node, sacl, login_allowed, ctx) local candidate = nil local candidate_ctx = nil for name, child in pairs(node.children) do if child.satisfied then if not sacl then local _ _, _, sacl = is_authenticated(node.auth) end local cacl = (type(child.depends) == "table") and child.depends.acl or nil local login = login_allowed or (type(child.auth) == "table" and child.auth.login) if login or check_acl_depends(cacl, sacl and sacl["access-group"]) ~= nil then if child.title and type(child.action) == "table" then local child_ctx = ctx_append(util.clone(ctx, true), name, child) if child.action.type == "firstchild" then if not candidate or node_weight(candidate) > node_weight(child) then local have_grandchild = resolve_firstchild(child, sacl, login, child_ctx) if have_grandchild then candidate = child candidate_ctx = child_ctx end end elseif not child.firstchild_ineligible then if not candidate or node_weight(candidate) > node_weight(child) then candidate = child candidate_ctx = child_ctx end end end end end end if candidate then for k, v in pairs(candidate_ctx) do ctx[k] = v end return true end return false end local function resolve_page(tree, request_path) local node = tree local sacl = nil local login = false local ctx = {} for i, s in ipairs(request_path) do node = node.children and node.children[s] if not node or not node.satisfied then break end ctx_append(ctx, s, node) if not sacl then local _ _, _, sacl = is_authenticated(node.auth) end if not login and type(node.auth) == "table" and node.auth.login then login = true end if node.wildcard then ctx.request_args = {} ctx.request_path = util.clone(ctx.path, true) for j = i + 1, #request_path do ctx.request_path[j] = request_path[j] ctx.request_args[j - i] = request_path[j] end break end end if node and type(node.action) == "table" and node.action.type == "firstchild" then resolve_firstchild(node, sacl, login, ctx) end ctx.acls = ctx.acls or {} ctx.path = ctx.path or {} ctx.request_args = ctx.request_args or {} ctx.request_path = ctx.request_path or util.clone(request_path, true) node = tree for _, s in ipairs(ctx.path or {}) do node = node.children[s] assert(node, "Internal node resolve error") end return node, ctx end function dispatch(request) --context._disable_memtrace = require "luci.debug".trap_memtrace("l") local ctx = context local auth, cors, suid, sgid local menu = menu_json() local page, lookup_ctx = resolve_page(menu, request) local action = (page and type(page.action) == "table") and page.action or {} local tpl = init_template_engine(ctx) ctx.args = lookup_ctx.request_args ctx.path = lookup_ctx.path ctx.dispatched = page ctx.requestpath = ctx.requestpath or lookup_ctx.request_path ctx.requestargs = ctx.requestargs or lookup_ctx.request_args ctx.requested = ctx.requested or page if type(lookup_ctx.auth) == "table" and next(lookup_ctx.auth) then local sid, sdat, sacl = is_authenticated(lookup_ctx.auth) if not (sid and sdat and sacl) and lookup_ctx.auth.login then local user = http.getenv("HTTP_AUTH_USER") local pass = http.getenv("HTTP_AUTH_PASS") if user == nil and pass == nil then user = http.formvalue("luci_username") pass = http.formvalue("luci_password") end if user and pass then sid, sdat, sacl = session_setup(user, pass) end if not sid then context.path = {} http.status(403, "Forbidden") http.header("X-LuCI-Login-Required", "yes") local scope = { duser = "root", fuser = user } local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope) if ok then return res end return tpl.render("sysauth", scope) end http.header("Set-Cookie", 'sysauth_%s=%s; path=%s; SameSite=Strict; HttpOnly%s' %{ http.getenv("HTTPS") == "on" and "https" or "http", sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or "" }) http.redirect(build_url(unpack(ctx.requestpath))) return end if not sid or not sdat or not sacl then http.status(403, "Forbidden") http.header("X-LuCI-Login-Required", "yes") return end ctx.authsession = sid ctx.authtoken = sdat.token ctx.authuser = sdat.username ctx.authacl = sacl end if #lookup_ctx.acls > 0 then local perm = check_acl_depends(lookup_ctx.acls, ctx.authacl and ctx.authacl["access-group"]) if perm == nil then http.status(403, "Forbidden") return end if page then page.readonly = not perm end end if action.type == "arcombine" then action = (#lookup_ctx.request_args > 0) and action.targets[2] or action.targets[1] end if lookup_ctx.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then luci.http.status(200, "OK") luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*") luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") return end if require_post_security(action) then if not test_post_security() then return end end if lookup_ctx.sgid then sys.process.setgroup(lookup_ctx.sgid) end if lookup_ctx.suid then sys.process.setuser(lookup_ctx.suid) end if action.type == "view" then tpl.render("view", { view = action.path }) elseif action.type == "call" then local ok, mod = util.copcall(require, action.module) if not ok then error500(mod) return end local func = mod[action["function"]] assert(func ~= nil, 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?') assert(type(func) == "function", 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' .. 'of type "' .. type(func) .. '".') local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {} for _, s in ipairs(lookup_ctx.request_args) do argv[#argv + 1] = s end local ok, err = util.copcall(func, unpack(argv)) if not ok then error500(err) end --elseif action.type == "firstchild" then -- tpl.render("empty_node_placeholder", getfenv(1)) elseif action.type == "alias" then local sub_request = {} for name in action.path:gmatch("[^/]+") do sub_request[#sub_request + 1] = name end for _, s in ipairs(lookup_ctx.request_args) do sub_request[#sub_request + 1] = s end dispatch(sub_request) elseif action.type == "rewrite" then local sub_request = { unpack(request) } for i = 1, action.remove do table.remove(sub_request, 1) end local n = 1 for s in action.path:gmatch("[^/]+") do table.insert(sub_request, n, s) n = n + 1 end for _, s in ipairs(lookup_ctx.request_args) do sub_request[#sub_request + 1] = s end dispatch(sub_request) elseif action.type == "template" then tpl.render(action.path, getfenv(1)) elseif action.type == "cbi" then _cbi({ config = action.config, model = action.path }, unpack(lookup_ctx.request_args)) elseif action.type == "form" then _form({ model = action.path }, unpack(lookup_ctx.request_args)) else if not menu.children then error404("No root node was registered, this usually happens if no module was installed.\n" .. "Install luci-mod-admin-full and retry. " .. "If the module is already installed, try removing the /tmp/luci-indexcache file.") else error404("No page is registered at '/" .. xml.pcdata(table.concat(lookup_ctx.request_path, "/")) .. "'.\n" .. "If this url belongs to an extension, make sure it is properly installed.\n" .. "If the extension was recently installed, try removing the /tmp/luci-indexcache file.") end end end local function hash_filelist(files) local fprint = {} local n = 0 for i, file in ipairs(files) do local st = fs.stat(file) if st then fprint[n + 1] = '%x' % st.ino fprint[n + 2] = '%x' % st.mtime fprint[n + 3] = '%x' % st.size n = n + 3 end end return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".") end local function read_cachefile(file, reader) local euid = sys.process.info("uid") local fuid = fs.stat(file, "uid") local mode = fs.stat(file, "modestr") if euid ~= fuid or mode ~= "rw-------" then return nil end return reader(file) end function createindex() local controllers = { } local base = "%s/controller/" % util.libpath() local _, path for path in (fs.glob("%s*.lua" % base) or function() end) do controllers[#controllers+1] = path end for path in (fs.glob("%s*/*.lua" % base) or function() end) do controllers[#controllers+1] = path end local cachefile if indexcache then cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) } local res = read_cachefile(cachefile, function(path) return loadfile(path)() end) if res then index = res return res end for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do fs.unlink(file) end end index = {} for _, path in ipairs(controllers) do local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".") local mod = require(modname) assert(mod ~= true, "Invalid controller file found\n" .. "The file '" .. path .. "' contains an invalid module line.\n" .. "Please verify whether the module name is set to '" .. modname .. "' - It must correspond to the file path!") local idx = mod.index if type(idx) == "function" then index[modname] = idx end end if cachefile then local f = nixio.open(cachefile, "w", 600) f:writeall(util.get_bytecode(index)) f:close() end end function createtree_json() local json = require "luci.jsonc" local tree = {} local schema = { action = "table", auth = "table", cors = "boolean", depends = "table", order = "number", setgroup = "string", setuser = "string", title = "string", wildcard = "boolean", firstchild_ineligible = "boolean" } local files = {} local cachefile for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do files[#files+1] = file end if indexcache then cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) } local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end) if res then return res end for file in (fs.glob("%s.*.json" % indexcache) or function() end) do fs.unlink(file) end end for _, file in ipairs(files) do local data = json.parse(fs.readfile(file) or "") if type(data) == "table" then for path, spec in pairs(data) do if type(spec) == "table" then local node = tree for s in path:gmatch("[^/]+") do if s == "*" then node.wildcard = true break end node.children = node.children or {} node.children[s] = node.children[s] or {} node = node.children[s] end if node ~= tree then for k, t in pairs(schema) do if type(spec[k]) == t then node[k] = spec[k] end end node.satisfied = check_depends(spec) end end end end end if cachefile then local f = nixio.open(cachefile, "w", 600) f:writeall(json.stringify(tree)) f:close() end return tree end -- Build the index before if it does not exist yet. function createtree() if not index then createindex() end local ctx = context local tree = {nodes={}, inreq=true} ctx.treecache = setmetatable({}, {__mode="v"}) ctx.tree = tree local scope = setmetatable({}, {__index = luci.dispatcher}) for k, v in pairs(index) do scope._NAME = k setfenv(v, scope) v() end return tree end function assign(path, clone, title, order) local obj = node(unpack(path)) obj.nodes = nil obj.module = nil obj.title = title obj.order = order setmetatable(obj, {__index = _create_node(clone)}) return obj end function entry(path, target, title, order) local c = node(unpack(path)) c.target = target c.title = title c.order = order c.module = getfenv(2)._NAME return c end -- enabling the node. function get(...) return _create_node({...}) end function node(...) local c = _create_node({...}) c.module = getfenv(2)._NAME c.auto = nil return c end function lookup(...) local i, path = nil, {} for i = 1, select('#', ...) do local name, arg = nil, tostring(select(i, ...)) for name in arg:gmatch("[^/]+") do path[#path+1] = name end end for i = #path, 1, -1 do local node = context.treecache[table.concat(path, ".", 1, i)] if node and (i == #path or node.leaf) then return node, build_url(unpack(path)) end end end function _create_node(path) if #path == 0 then return context.tree end local name = table.concat(path, ".") local c = context.treecache[name] if not c then local last = table.remove(path) local parent = _create_node(path) c = {nodes={}, auto=true, inreq=true} parent.nodes[last] = c context.treecache[name] = c end return c end -- Subdispatchers -- function firstchild() return { type = "firstchild" } end function firstnode() return { type = "firstnode" } end function alias(...) return { type = "alias", req = { ... } } end function rewrite(n, ...) return { type = "rewrite", n = n, req = { ... } } end function call(name, ...) return { type = "call", argv = {...}, name = name } end function post_on(params, name, ...) return { type = "call", post = params, argv = { ... }, name = name } end function post(...) return post_on(true, ...) end function template(name) return { type = "template", view = name } end function view(name) return { type = "view", view = name } end function _cbi(self, ...) local cbi = require "luci.cbi" local tpl = require "luci.template" local http = require "luci.http" local util = require "luci.util" local config = self.config or {} local maps = cbi.load(self.model, ...) local state = nil local function has_uci_access(config, level) local rv = util.ubus("session", "access", { ubus_rpc_session = context.authsession, scope = "uci", object = config, ["function"] = level }) return (type(rv) == "table" and rv.access == true) or false end local i, res for i, res in ipairs(maps) do if util.instanceof(res, cbi.SimpleForm) then io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n" % self.model) io.stderr:write("please change %s to use the form() action instead.\n" % table.concat(context.request, "/")) end res.flow = config local cstate = res:parse() if cstate and (not state or cstate < state) then state = cstate end end local function _resolve_path(path) return type(path) == "table" and build_url(unpack(path)) or path end if config.on_valid_to and state and state > 0 and state < 2 then http.redirect(_resolve_path(config.on_valid_to)) return end if config.on_changed_to and state and state > 1 then http.redirect(_resolve_path(config.on_changed_to)) return end if config.on_success_to and state and state > 0 then http.redirect(_resolve_path(config.on_success_to)) return end if config.state_handler then if not config.state_handler(state, maps) then return end end http.header("X-CBI-State", state or 0) if not config.noheader then tpl.render("cbi/header", {state = state}) end local redirect local messages local applymap = false local pageaction = true local parsechain = { } local writable = false for i, res in ipairs(maps) do if res.apply_needed and res.parsechain then local c for _, c in ipairs(res.parsechain) do parsechain[#parsechain+1] = c end applymap = true end if res.redirect then redirect = redirect or res.redirect end if res.pageaction == false then pageaction = false end if res.message then messages = messages or { } messages[#messages+1] = res.message end end for i, res in ipairs(maps) do local is_readable_map = has_uci_access(res.config, "read") local is_writable_map = has_uci_access(res.config, "write") writable = writable or is_writable_map res:render({ firstmap = (i == 1), redirect = redirect, messages = messages, pageaction = pageaction, parsechain = parsechain, readable = is_readable_map, writable = is_writable_map }) end if not config.nofooter then tpl.render("cbi/footer", { flow = config, pageaction = pageaction, redirect = redirect, state = state, autoapply = config.autoapply, trigger_apply = applymap, writable = writable }) end end function cbi(model, config) return { type = "cbi", post = { ["cbi.submit"] = true }, config = config, model = model } end function arcombine(trg1, trg2) return { type = "arcombine", env = getfenv(), targets = {trg1, trg2} } end function _form(self, ...) local cbi = require "luci.cbi" local tpl = require "luci.template" local http = require "luci.http" local maps = luci.cbi.load(self.model, ...) local state = nil local i, res for i, res in ipairs(maps) do local cstate = res:parse() if cstate and (not state or cstate < state) then state = cstate end end http.header("X-CBI-State", state or 0) tpl.render("header") for i, res in ipairs(maps) do res:render() end tpl.render("footer") end function form(model) return { type = "form", post = { ["cbi.submit"] = true }, model = model } end translate = i18n.translate -- This function does not actually translate the given argument but -- is used by build/i18n-scan.pl to find translatable entries. function _(text) return text end