luci-base: dispatcher.lua: introduce dispatch tree JSON conversion
authorJo-Philipp Wich <jo@mein.io>
Sun, 1 Dec 2019 19:06:43 +0000 (20:06 +0100)
committerJo-Philipp Wich <jo@mein.io>
Thu, 7 May 2020 17:40:49 +0000 (19:40 +0200)
Introduce a new method menu_json() which converts the current dispatch
tree into JSON structure.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
(cherry picked from commit 852d24061d236048a8c2c886787eede2586b10b4)

modules/luci-base/luasrc/dispatcher.lua

index 17228ac32fee8236c3f05db86d86541f4a8bebd3..d7b722a07c97853dd5cd157f8c722cfc433bee9e 100644 (file)
@@ -21,6 +21,324 @@ local index = nil
 local fi
 
 
+local function check_fs_depends(fs)
+       local fs = require "nixio.fs"
+
+       for path, kind in pairs(fs) 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_depends(spec)
+       if type(spec.depends) ~= "table" then
+               return true
+       end
+
+       if type(spec.depends.fs) == "table" and not check_fs_depends(spec.depends.fs) 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
+               }
+       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 (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 = { "param: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
+
+                       for _, v in pairs(spec) do
+                               if v ~= nil then
+                                       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)
+                                       break
+                               end
+                       end
+               end
+       end
+
+       return json
+end
+
 function build_url(...)
        local path = {...}
        local url = { http.getenv("SCRIPT_NAME") or "" }
@@ -306,6 +624,16 @@ local function session_setup(user, pass, allowed_users)
        return nil, nil
 end
 
+function menu_json()
+       local tree = context.tree or createtree()
+       return tree_to_json(tree, {
+               action = {
+                       ["type"] = "firstchild",
+                       ["recurse"] = true
+               }
+       })
+end
+
 function dispatch(request)
        --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
        local ctx = context
@@ -848,38 +1176,43 @@ function firstnode()
        return { type = "firstnode", target = _firstnode }
 end
 
-function alias(...)
-       local req = {...}
-       return function(...)
-               for _, r in ipairs({...}) do
-                       req[#req+1] = r
-               end
+function _alias(self, ...)
+       local req = { unpack(self.req) }
 
-               dispatch(req)
+       for _, r in ipairs({...}) do
+               req[#req+1] = r
        end
+
+       dispatch(req)
 end
 
-function rewrite(n, ...)
-       local req = {...}
-       return function(...)
-               local dispatched = util.clone(context.dispatched)
+function alias(...)
+       return { type = "alias", target = _alias, req = { ... } }
+end
 
-               for i=1,n do
-                       table.remove(dispatched, 1)
-               end
+function _rewrite(self, ...)
+       local n = self.n
+       local req = { unpack(self.req) }
+       local dispatched = util.clone(context.dispatched)
 
-               for i, r in ipairs(req) do
-                       table.insert(dispatched, i, r)
-               end
+       for i=1,n do
+               table.remove(dispatched, 1)
+       end
 
-               for _, r in ipairs({...}) do
-                       dispatched[#dispatched+1] = r
-               end
+       for i, r in ipairs(req) do
+               table.insert(dispatched, i, r)
+       end
 
-               dispatch(dispatched)
+       for _, r in ipairs({...}) do
+               dispatched[#dispatched+1] = r
        end
+
+       dispatch(dispatched)
 end
 
+function rewrite(n, ...)
+       return { type = "rewrite", target = _rewrite, n = n, req = { ... } }
+end
 
 local function _call(self, ...)
        local func = getfenv()[self.name]
@@ -1092,7 +1425,7 @@ end
 
 function form(model)
        return {
-               type = "cbi",
+               type = "form",
                post = { ["cbi.submit"] = true },
                model = model,
                target = _form