luci-base: dispatcher.lua: pass permission state to legacy CBI templates
[project/luci.git] / modules / luci-base / luasrc / dispatcher.lua
index 6850d7e3a946ae55f58f7f2066221dee6be132a3..d14a866737a15e438249e0cb4d71f30363fb6d53 100644 (file)
@@ -17,9 +17,356 @@ _M.fs = fs
 -- Index table
 local index = nil
 
--- Fastindex
-local fi
+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
+               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 = util.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" }
+                                       }
+                               elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
+                                       spec.auth = {
+                                               login = false,
+                                               methods = { "query:auth", "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 = {...}
@@ -40,36 +387,6 @@ function build_url(...)
        return table.concat(url, "")
 end
 
-function node_visible(node)
-   if node then
-         return not (
-                (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 k, v
-               for k, v in util.spairs(node.nodes,
-                       function(a, b)
-                               return (node.nodes[a].order or 100)
-                                    < (node.nodes[b].order or 100)
-                       end)
-               do
-                       if node_visible(v) then
-                               rv[#rv+1] = k
-                       end
-               end
-       end
-       return rv
-end
-
 
 function error404(message)
        http.status(404, "Not Found")
@@ -104,6 +421,38 @@ function error500(message)
        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
 
@@ -123,6 +472,8 @@ function httpdispatch(request, prefix)
                r[#r+1] = node
        end
 
+       determine_request_language()
+
        local stat, err = util.coxpcall(function()
                dispatch(context.request)
        end, error500)
@@ -132,7 +483,11 @@ function httpdispatch(request, prefix)
        --context._disable_memtrace()
 end
 
-local function require_post_security(target)
+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
@@ -175,6 +530,7 @@ 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
@@ -183,228 +539,333 @@ local function session_retrieve(sid, allowed_users)
            util.contains(allowed_users, sdat.values.username))
        then
                uci:set_session_id(sid)
-               return sid, sdat.values
+               return sid, sdat.values, type(sacl) == "table" and sacl or {}
        end
 
-       return nil, nil
+       return nil, nil, nil
 end
 
-local function session_setup(user, pass, allowed_users)
-       if util.contains(allowed_users, user) then
-               local login = util.ubus("session", "login", {
-                       username = user,
-                       password = pass,
-                       timeout  = tonumber(luci.config.sauth.sessiontime)
+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) }
                })
 
-               local rp = context.requestpath
-                       and table.concat(context.requestpath, "/") or ""
+               io.stderr:write("luci: accepted login on /%s for %s from %s\n"
+                       %{ rp, user or "?", http.getenv("REMOTE_ADDR") 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) }
-                       })
+               return session_retrieve(login.ubus_rpc_session)
+       end
+
+       io.stderr:write("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
 
-                       io.stderr:write("luci: accepted login on /%s for %s from %s\n"
-                               %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
+       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
 
-                       return session_retrieve(login.ubus_rpc_session)
+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
 
-               io.stderr:write("luci: failed login on /%s for %s from %s\n"
-                       %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
+               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 nil, nil
+       return children
 end
 
-function dispatch(request)
-       --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
-       local ctx = context
-       ctx.path = request
+local function find_subnode(root, prefix, recurse, descended)
+       local children = get_children(root)
 
-       local conf = require "luci.config"
-       assert(conf.main,
-               "/etc/config/luci seems to be corrupt, unable to find section 'main'")
+       if #children > 0 and (not descended or recurse) then
+               local sub_path = { unpack(prefix) }
 
-       local i18n = require "luci.i18n"
-       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
+               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 lang == "auto" then
-               lang = i18n.default
-       end
-       i18n.setlanguage(lang)
 
-       local c = ctx.tree
-       local stat
-       if not c then
-               c = createtree()
+       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
 
-       local track = {}
-       local args = {}
-       ctx.args = args
-       ctx.requestargs = ctx.requestargs or args
-       local n
-       local preq = {}
-       local freq = {}
+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 i, s in ipairs(request) do
-               preq[#preq+1] = s
-               freq[#freq+1] = s
-               c = c.nodes[s]
-               n = i
-               if not c then
-                       break
+                       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
 
-               util.update(track, c)
+       if type(node_a.action) == "table" and
+          node_a.action.type == "firstchild" and
+          node_a.children == nil
+       then
+               node_a.satisfied = false
+       end
 
-               if c.leaf then
-                       break
+       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
 
-       if c and c.leaf then
-               for j=n+1, #request do
-                       args[#args+1] = request[j]
-                       freq[#freq+1] = request[j]
-               end
+       local perm
+       if type(node.depends) == "table" then
+               perm = check_acl_depends(node.depends.acl, acl["access-group"])
+       else
+               perm = true
        end
 
-       ctx.requestpath = ctx.requestpath or freq
-       ctx.path = preq
+       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
+               }
+       })
 
-       if track.i18n then
-               i18n.loadc(track.i18n)
+       local json_tree = createtree_json()
+       local menu_tree = merge_trees(lua_tree, json_tree)
+
+       if acl then
+               apply_tree_acls(menu_tree, acl)
        end
 
-       -- Init template engine
-       if (c and c.index) or not track.notemplate then
-               local tpl = require("luci.template")
-               local media = track.mediaurlbase or 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
+       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
-                       assert(media, "No valid theme found")
                end
+               assert(media, "No valid theme found")
+       end
 
-               local function _ifattr(cond, key, val)
-                       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
-                               return string.format(
-                                       ' %s="%s"', tostring(key),
-                                       util.pcdata(tostring( val
-                                        or (type(env[key]) ~= "function" and env[key])
-                                        or (scope and type(scope[key]) ~= "function" and scope[key])
-                                        or "" ))
-                               )
-                       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   = util.striptags;
-                  pcdata      = util.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
+       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
-                               return table.concat(url, "")
-                       elseif key == "token" then
-                               return ctx.authtoken
-                       else
-                               return rawget(tbl, key) or _G[key]
                        end
-               end})
-       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"
-       )
+                       val = tostring(val or
+                               (type(env[key]) ~= "function" and env[key]) or
+                               (scope and type(scope[key]) ~= "function" and scope[key]) or "")
 
-       if track.sysauth and not ctx.authsession then
-               local authen = track.sysauth_authenticator
-               local _, sid, sdat, default_user, allowed_users
+                       if noescape ~= true then
+                               val = util.pcdata(val)
+                       end
 
-               if type(authen) == "string" and authen ~= "htmlauth" then
-                       error500("Unsupported authenticator %q configured" % authen)
-                       return
+                       return string.format(' %s="%s"', tostring(key), val)
+               else
+                       return ''
                end
+       end
 
-               if type(track.sysauth) == "table" then
-                       default_user, allowed_users = nil, track.sysauth
+       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   = util.striptags;
+               pcdata      = util.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
-                       default_user, allowed_users = track.sysauth, { track.sysauth }
+                       return rawget(tbl, key) or _G[key]
                end
+       end})
 
-               if type(authen) == "function" then
-                       _, sid = authen(sys.user.checkpasswd, allowed_users)
-               else
-                       sid = http.getcookie("sysauth")
+       return tpl
+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 = menu
+
+       local requested_path_full = {}
+       local requested_path_node = {}
+       local requested_path_args = {}
+
+       local required_path_acls = {}
+
+       for i, s in ipairs(request) do
+               if type(page.children) ~= "table" or not page.children[s] then
+                       page = nil
+                       break
                end
 
-               sid, sdat = session_retrieve(sid, allowed_users)
+               if not page.children[s].satisfied then
+                       page = nil
+                       break
+               end
+
+               page = page.children[s]
+               auth = page.auth or auth
+               cors = page.cors or cors
+               suid = page.setuser or suid
+               sgid = page.setgroup or sgid
+
+               if type(page.depends) == "table" and type(page.depends.acl) == "table" then
+                       for _, group in ipairs(page.depends.acl) do
+                               local found = false
+                               for _, item in ipairs(required_path_acls) do
+                                       if item == group then
+                                               found = true
+                                               break
+                                       end
+                               end
+                               if not found then
+                                       required_path_acls[#required_path_acls + 1] = group
+                               end
+                       end
+               end
 
-               if not (sid and sdat) and authen == "htmlauth" then
+               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
+               end
+       end
+
+       local tpl = init_template_engine(ctx)
+
+       ctx.args = requested_path_args
+       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, sacl
+               for _, method in ipairs(auth.methods) do
+                       sid, sdat, sacl = check_authentication(method)
+
+                       if sid and sdat and sacl then
+                               break
+                       end
+               end
+
+               if not (sid and sdat and sacl) and auth.login then
                        local user = http.getenv("HTTP_AUTH_USER")
                        local pass = http.getenv("HTTP_AUTH_PASS")
 
@@ -413,113 +874,160 @@ function dispatch(request)
                                pass = http.formvalue("luci_password")
                        end
 
-                       sid, sdat = session_setup(user, pass, allowed_users)
+                       if user and pass then
+                               sid, sdat, sacl = session_setup(user, pass)
+                       end
 
                        if not sid then
-                               local tmpl = require "luci.template"
-
                                context.path = {}
 
                                http.status(403, "Forbidden")
-                               tmpl.render(track.sysauth_template or "sysauth", {
-                                       duser = default_user,
-                                       fuser = user
-                               })
+                               http.header("X-LuCI-Login-Required", "yes")
 
-                               return
+                               return tpl.render("sysauth", { duser = "root", fuser = user })
                        end
 
-                       http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
+                       http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
                                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 then
+               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 #required_path_acls > 0 then
+               local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"])
+               if perm == nil then
+                       http.status(403, "Forbidden")
+                       return
+               end
+
+               page.readonly = not perm
        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.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
                luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
                return
        end
 
-       if c and require_post_security(c.target) then
-               if not test_post_security(c) then
+       if require_post_security(action) then
+               if not test_post_security() then
                        return
                end
        end
 
-       if track.setgroup then
-               sys.process.setgroup(track.setgroup)
+       if sgid then
+               sys.process.setgroup(sgid)
        end
 
-       if track.setuser then
-               sys.process.setuser(track.setuser)
+       if suid then
+               sys.process.setuser(suid)
        end
 
-       local target = nil
-       if c then
-               if type(c.target) == "function" then
-                       target = c.target
-               elseif type(c.target) == "table" then
-                       target = c.target.target
+       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
-       end
 
-       if c and (c.index or type(target) == "function") then
-               ctx.dispatched = c
-               ctx.requested = ctx.requested or ctx.dispatched
-       end
+               local func = mod[action["function"]]
 
-       if c and c.index then
-               local tpl = require "luci.template"
+               assert(func ~= nil,
+                      'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
 
-               if util.copcall(tpl.render, "indexer", {}) then
-                       return true
+               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
-       end
 
-       if type(target) == "function" then
-               util.copcall(function()
-                       local oldenv = getfenv(target)
-                       local module = require(c.module)
-                       local env = setmetatable({}, {__index=
+       elseif action.type == "firstchild" then
+               local sub_request = find_subnode(page, requested_path_full, action.recurse)
+               if sub_request then
+                       dispatch(sub_request)
+               else
+                       tpl.render("empty_node_placeholder", getfenv(1))
+               end
 
-                       function(tbl, key)
-                               return rawget(tbl, key) or module[key] or oldenv[key]
-                       end})
+       elseif action.type == "alias" then
+               local sub_request = {}
+               for name in action.path:gmatch("[^/]+") do
+                       sub_request[#sub_request + 1] = name
+               end
 
-                       setfenv(target, env)
-               end)
+               for _, s in ipairs(requested_path_args) do
+                       sub_request[#sub_request + 1] = s
+               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))
+               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
-               if not ok then
-                       error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
-                                " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
-                                "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
+
+               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 = node()
-               if not root or not root.target then
+               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" ..
                                 "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 '/" .. 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 the extension was recently installed, try removing the /tmp/luci-indexcache file.")
                end
@@ -573,13 +1081,9 @@ function createindex()
                       "' - It must correspond to the file path!")
 
                local idx = mod.index
-               assert(type(idx) == "function",
-                      "Invalid controller file found\n" ..
-                      "The file '" .. path .. "' contains no index() function.\n" ..
-                      "Please make sure that the controller contains a valid " ..
-                      "index function and verify the spelling!")
-
-               index[modname] = idx
+               if type(idx) == "function" then
+                       index[modname] = idx
+               end
        end
 
        if indexcache then
@@ -589,6 +1093,94 @@ function createindex()
        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"
+       }
+
+       local files = {}
+       local fprint = {}
+       local cachefile
+
+       for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
+               files[#files+1] = file
+
+               if indexcache then
+                       local st = fs.stat(file)
+                       if st then
+                               fprint[#fprint+1] = '%x' % st.ino
+                               fprint[#fprint+1] = '%x' % st.mtime
+                               fprint[#fprint+1] = '%x' % st.size
+                       end
+               end
+       end
+
+       if indexcache then
+               cachefile = "%s.%s.json" %{
+                       indexcache,
+                       nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
+               }
+
+               local res = json.parse(fs.readfile(cachefile) or "")
+               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
+               fs.writefile(cachefile, json.stringify(tree))
+       end
+
+       return tree
+end
+
 -- Build the index before if it does not exist yet.
 function createtree()
        if not index then
@@ -597,14 +1189,9 @@ function createtree()
 
        local ctx  = context
        local tree = {nodes={}, inreq=true}
-       local modi = {}
 
        ctx.treecache = setmetatable({}, {__mode="v"})
        ctx.tree = tree
-       ctx.modifiers = modi
-
-       -- Load default translation
-       require "luci.i18n".loadc("base")
 
        local scope = setmetatable({}, {__index = luci.dispatcher})
 
@@ -614,28 +1201,9 @@ function createtree()
                v()
        end
 
-       local function modisort(a,b)
-               return modi[a].order < modi[b].order
-       end
-
-       for _, v in util.spairs(modi, modisort) do
-               scope._NAME = v.module
-               setfenv(v.func, scope)
-               v.func()
-       end
-
        return tree
 end
 
-function modifier(func, order)
-       context.modifiers[#context.modifiers+1] = {
-               func = func,
-               order = order or 0,
-               module
-                       = getfenv(2)._NAME
-       }
-end
-
 function assign(path, clone, title, order)
        local obj  = node(unpack(path))
        obj.nodes  = nil
@@ -703,99 +1271,35 @@ function _create_node(path)
                local last = table.remove(path)
                local parent = _create_node(path)
 
-               c = {nodes={}, auto=true}
-               -- the node is "in request" if the request path matches
-               -- at least up to the length of the node path
-               if parent.inreq and context.path[#path+1] == last then
-                 c.inreq = true
-               end
+               c = {nodes={}, auto=true, inreq=true}
+
                parent.nodes[last] = c
                context.treecache[name] = c
        end
+
        return c
 end
 
 -- Subdispatchers --
 
-function _firstchild()
-   local path = { unpack(context.path) }
-   local name = table.concat(path, ".")
-   local node = context.treecache[name]
-
-   local lowest
-   if node and node.nodes and next(node.nodes) then
-         local k, v
-         for k, v in pairs(node.nodes) do
-                if not lowest or
-                       (v.order or 100) < (node.nodes[lowest].order or 100)
-                then
-                       lowest = k
-                end
-         end
-   end
-
-   assert(lowest ~= nil,
-                 "The requested node contains no childs, unable to redispatch")
-
-   path[#path+1] = lowest
-   dispatch(path)
+function firstchild()
+       return { type = "firstchild" }
 end
 
-function firstchild()
-   return { type = "firstchild", target = _firstchild }
+function firstnode()
+       return { type = "firstnode" }
 end
 
 function alias(...)
-       local req = {...}
-       return function(...)
-               for _, r in ipairs({...}) do
-                       req[#req+1] = r
-               end
-
-               dispatch(req)
-       end
+       return { type = "alias", req = { ... } }
 end
 
 function rewrite(n, ...)
-       local req = {...}
-       return function(...)
-               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
-
-
-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
+       return { type = "rewrite", n = n, req = { ... } }
 end
 
 function call(name, ...)
-       return {type = "call", argv = {...}, name = name, target = _call}
+       return { type = "call", argv = {...}, name = name }
 end
 
 function post_on(params, name, ...)
@@ -803,8 +1307,7 @@ function post_on(params, name, ...)
                type = "call",
                post = params,
                argv = { ... },
-               name = name,
-               target = _call
+               name = name
        }
 end
 
@@ -813,25 +1316,36 @@ function post(...)
 end
 
 
-local _template = function(self, ...)
-       require "luci.template".render(self.view)
+function template(name)
+       return { type = "template", view = name }
 end
 
-function template(name)
-       return {type = "template", view = name, target = _template}
+function view(name)
+       return { type = "view", view = name }
 end
 
 
-local function _cbi(self, ...)
+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
@@ -885,8 +1399,7 @@ local function _cbi(self, ...)
        local applymap   = false
        local pageaction = true
        local parsechain = { }
-
-       local is_rollback, time_remaining = uci:rollback_pending()
+       local writable   = false
 
        for i, res in ipairs(maps) do
                if res.apply_needed and res.parsechain then
@@ -912,24 +1425,31 @@ local function _cbi(self, ...)
        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),
-                       applymap   = applymap,
-                       confirmmap = (is_rollback and time_remaining or nil),
                        redirect   = redirect,
                        messages   = messages,
                        pageaction = pageaction,
-                       parsechain = parsechain
+                       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
+                       flow          = config,
+                       pageaction    = pageaction,
+                       redirect      = redirect,
+                       state         = state,
+                       autoapply     = config.autoapply,
+                       trigger_apply = applymap,
+                       writable      = writable
                })
        end
 end
@@ -939,25 +1459,21 @@ function cbi(model, config)
                type = "cbi",
                post = { ["cbi.submit"] = true },
                config = config,
-               model = model,
-               target = _cbi
+               model = model
        }
 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)
-       return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
+       return {
+               type = "arcombine",
+               env = getfenv(),
+               targets = {trg1, trg2}
+       }
 end
 
 
-local function _form(self, ...)
+function _form(self, ...)
        local cbi = require "luci.cbi"
        local tpl = require "luci.template"
        local http = require "luci.http"
@@ -983,10 +1499,9 @@ end
 
 function form(model)
        return {
-               type = "cbi",
+               type = "form",
                post = { ["cbi.submit"] = true },
-               model = model,
-               target = _form
+               model = model
        }
 end