luci-base: dispatcher: use consistent ordering
[project/luci.git] / modules / luci-base / luasrc / dispatcher.lua
index 2c58b0ab3d9220d76f7b0a1e7196b0da622949f6..d85cb58243082e007a3144ca2ee03bbc50e2aca0 100644 (file)
@@ -40,6 +40,28 @@ function build_url(...)
        return table.concat(url, "")
 end
 
+function _ordered_children(node)
+       local name, child, children = nil, nil, {}
+
+       for name, child in pairs(node.nodes) do
+               children[#children+1] = {
+                       name  = name,
+                       node  = child,
+                       order = child.order or 100
+               }
+       end
+
+       table.sort(children, function(a, b)
+               if a.order == b.order then
+                       return a.name < b.name
+               else
+                       return a.order < b.order
+               end
+       end)
+
+       return children
+end
+
 function node_visible(node)
    if node then
          return not (
@@ -55,15 +77,10 @@ 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
+               local _, child
+               for _, child in ipairs(_ordered_children(node)) do
+                       if node_visible(child.node) then
+                               rv[#rv+1] = child.name
                        end
                end
        end
@@ -296,10 +313,6 @@ function dispatch(request)
        ctx.requestpath = ctx.requestpath or freq
        ctx.path = preq
 
-       if track.i18n then
-               i18n.loadc(track.i18n)
-       end
-
        -- Init template engine
        if (c and c.index) or not track.notemplate then
                local tpl = require("luci.template")
@@ -358,7 +371,7 @@ function dispatch(request)
                        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 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] = "?"
@@ -421,6 +434,7 @@ function dispatch(request)
                                context.path = {}
 
                                http.status(403, "Forbidden")
+                               http.header("X-LuCI-Login-Required", "yes")
                                tmpl.render(track.sysauth_template or "sysauth", {
                                        duser = default_user,
                                        fuser = user
@@ -437,6 +451,7 @@ function dispatch(request)
 
                if not sid or not sdat then
                        http.status(403, "Forbidden")
+                       http.header("X-LuCI-Login-Required", "yes")
                        return
                end
 
@@ -507,10 +522,11 @@ function dispatch(request)
                else
                        ok, err = util.copcall(target, unpack(args))
                end
-               assert(ok,
-                      "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)"))
+               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)"))
+               end
        else
                local root = node()
                if not root or not root.target then
@@ -596,14 +612,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})
 
@@ -613,28 +624,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
@@ -702,46 +694,87 @@ 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
+               c = {nodes={}, auto=true, inreq=true}
+
+               local _, n
+               for _, n in ipairs(path) do
+                       if context.path[_] ~= n then
+                               c.inreq = false
+                               break
+                       end
                end
+
+               c.inreq = c.inreq and (context.path[#path + 1] == last)
+
                parent.nodes[last] = c
                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
+function _find_eligible_node(root, prefix, deep, types, descend)
+       local children = _ordered_children(root)
 
-   assert(lowest ~= nil,
-                 "The requested node contains no childs, unable to redispatch")
+       if not root.leaf and deep ~= nil then
+               local sub_path = { unpack(prefix) }
 
-   path[#path+1] = lowest
-   dispatch(path)
+               if deep == false then
+                       deep = nil
+               end
+
+               local _, child
+               for _, child in ipairs(children) do
+                       sub_path[#prefix+1] = child.name
+
+                       local res_path = _find_eligible_node(child.node, sub_path,
+                                                            deep, types, true)
+
+                       if res_path then
+                               return res_path
+                       end
+               end
+       end
+
+       if descend and
+          (not types or
+           (type(root.target) == "table" and
+            util.contains(types, root.target.type)))
+       then
+               return prefix
+       end
+end
+
+function _find_node(recurse, types)
+       local path = { unpack(context.path) }
+       local name = table.concat(path, ".")
+       local node = context.treecache[name]
+
+       path = _find_eligible_node(node, path, recurse, types)
+
+       if path then
+               dispatch(path)
+       else
+               require "luci.template".render("empty_node_placeholder")
+       end
+end
+
+function _firstchild()
+       return _find_node(false, nil)
 end
 
 function firstchild()
-   return { type = "firstchild", target = _firstchild }
+       return { type = "firstchild", target = _firstchild }
+end
+
+function _firstnode()
+       return _find_node(true, { "cbi", "form", "template", "arcombine" })
+end
+
+function firstnode()
+       return { type = "firstnode", target = _firstnode }
 end
 
 function alias(...)
@@ -885,8 +918,6 @@ local function _cbi(self, ...)
        local pageaction = true
        local parsechain = { }
 
-       local is_rollback, time_remaining = uci:rollback_pending()
-
        for i, res in ipairs(maps) do
                if res.apply_needed and res.parsechain then
                        local c
@@ -913,8 +944,6 @@ local function _cbi(self, ...)
        for i, res in ipairs(maps) do
                res:render({
                        firstmap   = (i == 1),
-                       applymap   = applymap,
-                       confirmmap = (is_rollback and time_remaining or nil),
                        redirect   = redirect,
                        messages   = messages,
                        pageaction = pageaction,
@@ -924,11 +953,12 @@ local function _cbi(self, ...)
 
        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
                })
        end
 end