You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1565 lines
35 KiB
Lua

1 year ago
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
-- 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