luci-base: dispatcher.lua: refactor dispatch logic

Refactor the dispatch logic to operate on the internal JSON representation
of the menu tree.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich 2019-12-04 19:19:45 +01:00
parent 1ec6e72106
commit 184ea62300

View file

@ -17,10 +17,6 @@ _M.fs = fs
-- Index table -- Index table
local index = nil local index = nil
-- Fastindex
local fi
local function check_fs_depends(fs) local function check_fs_depends(fs)
local fs = require "nixio.fs" local fs = require "nixio.fs"
@ -321,8 +317,6 @@ local function tree_to_json(node, json)
spec.auth = {} spec.auth = {}
end end
for _, v in pairs(spec) do
if v ~= nil then
if not spec.action then if not spec.action then
spec.title = nil spec.title = nil
end end
@ -330,9 +324,6 @@ local function tree_to_json(node, json)
spec.satisfied = check_depends(spec) spec.satisfied = check_depends(spec)
json.children = json.children or {} json.children = json.children or {}
json.children[subname] = tree_to_json(subnode, spec) json.children[subname] = tree_to_json(subnode, spec)
break
end
end
end end
end end
@ -358,117 +349,6 @@ function build_url(...)
return table.concat(url, "") return table.concat(url, "")
end end
function _ordered_children(node)
local name, child, children = nil, nil, {}
for name, child in pairs(node.nodes) do
children[#children+1] = {
name = name,
node = child,
order = child.order or 100
}
end
table.sort(children, function(a, b)
if a.order == b.order then
return a.name < b.name
else
return a.order < b.order
end
end)
return children
end
local function dependencies_satisfied(node)
if type(node.file_depends) == "table" then
for _, file in ipairs(node.file_depends) do
local ftype = fs.stat(file, "type")
if ftype == "dir" then
local empty = true
for e in (fs.dir(file) or function() end) do
empty = false
end
if empty then
return false
end
elseif ftype == nil then
return false
end
end
end
if type(node.uci_depends) == "table" then
for config, expect_sections in pairs(node.uci_depends) do
if type(expect_sections) == "table" then
for section, expect_options in pairs(expect_sections) do
if type(expect_options) == "table" then
for option, expect_value in pairs(expect_options) do
local val = uci:get(config, section, option)
if expect_value == true and val == nil then
return false
elseif type(expect_value) == "string" then
if type(val) == "table" then
local found = false
for _, subval in ipairs(val) do
if subval == expect_value then
found = true
end
end
if not found then
return false
end
elseif val ~= expect_value then
return false
end
end
end
else
local val = uci:get(config, section)
if expect_options == true and val == nil then
return false
elseif type(expect_options) == "string" and val ~= expect_options then
return false
end
end
end
elseif expect_sections == true then
if not uci:get_first(config) then
return false
end
end
end
end
return true
end
function node_visible(node)
if node then
return not (
(not dependencies_satisfied(node)) or
(not node.title or #node.title == 0) or
(not node.target or node.hidden == true) or
(type(node.target) == "table" and node.target.type == "firstchild" and
(type(node.nodes) ~= "table" or not next(node.nodes)))
)
end
return false
end
function node_childs(node)
local rv = { }
if node then
local _, child
for _, child in ipairs(_ordered_children(node)) do
if node_visible(child.node) then
rv[#rv+1] = child.name
end
end
end
return rv
end
function error404(message) function error404(message)
http.status(404, "Not Found") http.status(404, "Not Found")
@ -658,6 +538,77 @@ local function session_setup(user, pass, allowed_users)
return nil, nil return nil, nil
end 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)
end
return session_retrieve(sid)
end
local function get_children(node)
local children = {}
if not node.wildcard and type(node.children) == "table" then
for name, child in pairs(node.children) do
children[#children+1] = {
name = name,
node = child,
order = child.order or 1000
}
end
table.sort(children, function(a, b)
if a.order == b.order then
return a.name < b.name
else
return a.order < b.order
end
end)
end
return children
end
local function find_subnode(root, prefix, recurse, descended)
local children = get_children(root)
if #children > 0 and (not descended or recurse) then
local sub_path = { unpack(prefix) }
if recurse == false then
recurse = nil
end
for _, child in ipairs(children) do
sub_path[#prefix+1] = child.name
local res_path = find_subnode(child.node, sub_path, recurse, true)
if res_path then
return res_path
end
end
end
if descended then
if not recurse or
root.action.type == "cbi" or
root.action.type == "form" or
root.action.type == "view" or
root.action.type == "template" or
root.action.type == "arcombine"
then
return prefix
end
end
end
function menu_json() function menu_json()
local tree = context.tree or createtree() local tree = context.tree or createtree()
return tree_to_json(tree, { return tree_to_json(tree, {
@ -749,85 +700,65 @@ end
function dispatch(request) function dispatch(request)
--context._disable_memtrace = require "luci.debug".trap_memtrace("l") --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
local ctx = context local ctx = context
ctx.path = request
local c = ctx.tree local auth, cors, suid, sgid
local stat local menu = menu_json()
if not c then local page = menu
c = createtree()
end
local track = {} local requested_path_full = {}
local args = {} local requested_path_node = {}
ctx.args = args local requested_path_args = {}
ctx.requestargs = ctx.requestargs or args
local n
local preq = {}
local freq = {}
for i, s in ipairs(request) do for i, s in ipairs(request) do
preq[#preq+1] = s if type(page.children) ~= "table" or not page.children[s] then
freq[#freq+1] = s page = nil
c = c.nodes[s]
n = i
if not c then
break break
end end
util.update(track, c) if not page.children[s].satisfied then
page = nil
break
end
if c.leaf then page = page.children[s]
auth = page.auth or auth
cors = page.cors or cors
suid = page.setuser or suid
sgid = page.setgroup or sgid
requested_path_full[i] = s
requested_path_node[i] = s
if page.wildcard then
for j = i + 1, #request do
requested_path_args[j - i] = request[j]
requested_path_full[j] = request[j]
end
break break
end end
end end
if c and c.leaf then local tpl = init_template_engine(ctx)
for j=n+1, #request do
args[#args+1] = request[j] ctx.args = requested_path_args
freq[#freq+1] = request[j] ctx.path = requested_path_node
ctx.dispatched = page
ctx.requestpath = ctx.requestpath or requested_path_full
ctx.requestargs = ctx.requestargs or requested_path_args
ctx.requested = ctx.requested or page
if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
local sid, sdat
for _, method in ipairs(auth.methods) do
sid, sdat = check_authentication(method)
if sid and sdat then
break
end end
end end
ctx.requestpath = ctx.requestpath or freq if not (sid and sdat) and auth.login then
ctx.path = preq
-- Init template engine
if (c and c.index) or not track.notemplate then
init_template_engine(ctx)
end
track.dependent = (track.dependent ~= false)
assert(not track.dependent or not track.auto,
"Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
"has no parent node so the access to this location has been denied.\n" ..
"This is a software bug, please report this message at " ..
"https://github.com/openwrt/luci/issues"
)
if track.sysauth and not ctx.authsession then
local authen = track.sysauth_authenticator
local _, sid, sdat, default_user, allowed_users
if type(authen) == "string" and authen ~= "htmlauth" then
error500("Unsupported authenticator %q configured" % authen)
return
end
if type(track.sysauth) == "table" then
default_user, allowed_users = nil, track.sysauth
else
default_user, allowed_users = track.sysauth, { track.sysauth }
end
if type(authen) == "function" then
_, sid = authen(sys.user.checkpasswd, allowed_users)
else
sid = http.getcookie("sysauth")
end
sid, sdat = session_retrieve(sid, allowed_users)
if not (sid and sdat) and authen == "htmlauth" then
local user = http.getenv("HTTP_AUTH_USER") local user = http.getenv("HTTP_AUTH_USER")
local pass = http.getenv("HTTP_AUTH_PASS") local pass = http.getenv("HTTP_AUTH_PASS")
@ -836,27 +767,23 @@ function dispatch(request)
pass = http.formvalue("luci_password") pass = http.formvalue("luci_password")
end end
sid, sdat = session_setup(user, pass, allowed_users) sid, sdat = session_setup(user, pass, { "root" })
if not sid then if not sid then
local tmpl = require "luci.template"
context.path = {} context.path = {}
http.status(403, "Forbidden") http.status(403, "Forbidden")
http.header("X-LuCI-Login-Required", "yes") http.header("X-LuCI-Login-Required", "yes")
tmpl.render(track.sysauth_template or "sysauth", {
duser = default_user,
fuser = user
})
return return tpl.render("sysauth", { duser = "root", fuser = user })
end end
http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{ http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or "" sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
}) })
http.redirect(build_url(unpack(ctx.requestpath))) http.redirect(build_url(unpack(ctx.requestpath)))
return
end end
if not sid or not sdat then if not sid or not sdat then
@ -870,81 +797,117 @@ function dispatch(request)
ctx.authuser = sdat.username ctx.authuser = sdat.username
end end
if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then local action = (page and type(page.action) == "table") and page.action or {}
if action.type == "arcombine" then
action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
end
if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
luci.http.status(200, "OK") luci.http.status(200, "OK")
luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*") luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
return return
end end
if c and require_post_security(c.target, args) then if require_post_security(action) then
if not test_post_security(c) then if not test_post_security() then
return return
end end
end end
if track.setgroup then if sgid then
sys.process.setgroup(track.setgroup) sys.process.setgroup(sgid)
end end
if track.setuser then if suid then
sys.process.setuser(track.setuser) sys.process.setuser(suid)
end end
local target = nil if action.type == "view" then
if c then tpl.render("view", { view = action.path })
if type(c.target) == "function" then
target = c.target
elseif type(c.target) == "table" then
target = c.target.target
end
end
if c and (c.index or type(target) == "function") then elseif action.type == "call" then
ctx.dispatched = c local ok, mod = util.copcall(require, action.module)
ctx.requested = ctx.requested or ctx.dispatched
end
if c and c.index then
local tpl = require "luci.template"
if util.copcall(tpl.render, "indexer", {}) then
return true
end
end
if type(target) == "function" then
util.copcall(function()
local oldenv = getfenv(target)
local module = require(c.module)
local env = setmetatable({}, {__index=
function(tbl, key)
return rawget(tbl, key) or module[key] or oldenv[key]
end})
setfenv(target, env)
end)
local ok, err
if type(c.target) == "table" then
ok, err = util.copcall(target, c.target, unpack(args))
else
ok, err = util.copcall(target, unpack(args))
end
if not ok then if not ok then
error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") .. error500(mod)
" dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" .. return
"The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
end 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(requested_path_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
local sub_request = find_subnode(page, requested_path_full, action.recurse)
if sub_request then
dispatch(sub_request)
else else
local root = node() tpl.render("empty_node_placeholder", getfenv(1))
if not root or not root.target then end
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(requested_path_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(requested_path_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(requested_path_args))
elseif action.type == "form" then
_form({ model = action.path }, unpack(requested_path_args))
else
local root = find_subnode(menu, {}, true)
if not root then
error404("No root node was registered, this usually happens if no module was installed.\n" .. error404("No root node was registered, this usually happens if no module was installed.\n" ..
"Install luci-mod-admin-full and retry. " .. "Install luci-mod-admin-full and retry. " ..
"If the module is already installed, try removing the /tmp/luci-indexcache file.") "If the module is already installed, try removing the /tmp/luci-indexcache file.")
else else
error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" .. error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
"If this url belongs to an extension, make sure it is properly installed.\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.") "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
end end
@ -1106,16 +1069,6 @@ function _create_node(path)
c = {nodes={}, auto=true, inreq=true} c = {nodes={}, auto=true, inreq=true}
local _, n
for _, n in ipairs(path) do
if context.path[_] ~= n then
c.inreq = false
break
end
end
c.inreq = c.inreq and (context.path[#path + 1] == last)
parent.nodes[last] = c parent.nodes[last] = c
context.treecache[name] = c context.treecache[name] = c
end end
@ -1125,124 +1078,24 @@ end
-- Subdispatchers -- -- Subdispatchers --
function _find_eligible_node(root, prefix, deep, types, descend)
local children = _ordered_children(root)
if not root.leaf and deep ~= nil then
local sub_path = { unpack(prefix) }
if deep == false then
deep = nil
end
local _, child
for _, child in ipairs(children) do
sub_path[#prefix+1] = child.name
local res_path = _find_eligible_node(child.node, sub_path,
deep, types, true)
if res_path then
return res_path
end
end
end
if descend and
(not types or
(type(root.target) == "table" and
util.contains(types, root.target.type)))
then
return prefix
end
end
function _find_node(recurse, types)
local path = { unpack(context.path) }
local name = table.concat(path, ".")
local node = context.treecache[name]
path = _find_eligible_node(node, path, recurse, types)
if path then
dispatch(path)
else
require "luci.template".render("empty_node_placeholder")
end
end
function _firstchild()
return _find_node(false, nil)
end
function firstchild() function firstchild()
return { type = "firstchild", target = _firstchild } return { type = "firstchild" }
end
function _firstnode()
return _find_node(true, { "cbi", "form", "template", "arcombine" })
end end
function firstnode() function firstnode()
return { type = "firstnode", target = _firstnode } return { type = "firstnode" }
end
function _alias(self, ...)
local req = { unpack(self.req) }
for _, r in ipairs({...}) do
req[#req+1] = r
end
dispatch(req)
end end
function alias(...) function alias(...)
return { type = "alias", target = _alias, req = { ... } } return { type = "alias", req = { ... } }
end
function _rewrite(self, ...)
local n = self.n
local req = { unpack(self.req) }
local dispatched = util.clone(context.dispatched)
for i=1,n do
table.remove(dispatched, 1)
end
for i, r in ipairs(req) do
table.insert(dispatched, i, r)
end
for _, r in ipairs({...}) do
dispatched[#dispatched+1] = r
end
dispatch(dispatched)
end end
function rewrite(n, ...) function rewrite(n, ...)
return { type = "rewrite", target = _rewrite, n = n, req = { ... } } return { type = "rewrite", n = n, req = { ... } }
end
local function _call(self, ...)
local func = getfenv()[self.name]
assert(func ~= nil,
'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
assert(type(func) == "function",
'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
'of type "' .. type(func) .. '".')
if #self.argv > 0 then
return func(unpack(self.argv), ...)
else
return func(...)
end
end end
function call(name, ...) function call(name, ...)
return {type = "call", argv = {...}, name = name, target = _call} return { type = "call", argv = {...}, name = name }
end end
function post_on(params, name, ...) function post_on(params, name, ...)
@ -1250,8 +1103,7 @@ function post_on(params, name, ...)
type = "call", type = "call",
post = params, post = params,
argv = { ... }, argv = { ... },
name = name, name = name
target = _call
} }
end end
@ -1260,25 +1112,16 @@ function post(...)
end end
local _template = function(self, ...)
require "luci.template".render(self.view)
end
function template(name) function template(name)
return {type = "template", view = name, target = _template} return { type = "template", view = name }
end
local _view = function(self, ...)
require "luci.template".render("view", { view = self.view })
end end
function view(name) function view(name)
return {type = "view", view = name, target = _view} return { type = "view", view = name }
end end
local function _cbi(self, ...) function _cbi(self, ...)
local cbi = require "luci.cbi" local cbi = require "luci.cbi"
local tpl = require "luci.template" local tpl = require "luci.template"
local http = require "luci.http" local http = require "luci.http"
@ -1392,25 +1235,21 @@ function cbi(model, config)
type = "cbi", type = "cbi",
post = { ["cbi.submit"] = true }, post = { ["cbi.submit"] = true },
config = config, config = config,
model = model, model = model
target = _cbi
} }
end end
local function _arcombine(self, ...)
local argv = {...}
local target = #argv > 0 and self.targets[2] or self.targets[1]
setfenv(target.target, self.env)
target:target(unpack(argv))
end
function arcombine(trg1, trg2) function arcombine(trg1, trg2)
return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}} return {
type = "arcombine",
env = getfenv(),
targets = {trg1, trg2}
}
end end
local function _form(self, ...) function _form(self, ...)
local cbi = require "luci.cbi" local cbi = require "luci.cbi"
local tpl = require "luci.template" local tpl = require "luci.template"
local http = require "luci.http" local http = require "luci.http"
@ -1438,8 +1277,7 @@ function form(model)
return { return {
type = "form", type = "form",
post = { ["cbi.submit"] = true }, post = { ["cbi.submit"] = true },
model = model, model = model
target = _form
} }
end end