luci-base: break circular luci.config <> luci.model.uci dependency
[project/luci.git] / modules / luci-base / luasrc / dispatcher.lua
index 4bbd58f..45e1e30 100644 (file)
@@ -1,33 +1,9 @@
---[[
-LuCI - Dispatcher
+-- 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.
 
-Description:
-The request dispatcher and module dispatcher generators
-
-FileId:
-$Id$
-
-License:
-Copyright 2008 Steven Barth <steven@midlink.org>
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-]]--
-
---- LuCI web dispatcher.
 local fs = require "nixio.fs"
 local sys = require "luci.sys"
-local init = require "luci.init"
 local util = require "luci.util"
 local http = require "luci.http"
 local nixio = require "nixio", require "nixio.util"
@@ -38,8 +14,6 @@ uci = require "luci.model.uci"
 i18n = require "luci.i18n"
 _M.fs = fs
 
-authenticator = {}
-
 -- Index table
 local index = nil
 
@@ -47,21 +21,10 @@ local index = nil
 local fi
 
 
---- Build the URL relative to the server webroot from given virtual path.
--- @param ...  Virtual path
--- @return             Relative URL
 function build_url(...)
        local path = {...}
        local url = { http.getenv("SCRIPT_NAME") or "" }
 
-       local k, v
-       for k, v in pairs(context.urltoken) do
-               url[#url+1] = "/;"
-               url[#url+1] = http.urlencode(k)
-               url[#url+1] = "="
-               url[#url+1] = http.urlencode(v)
-       end
-
        local p
        for _, p in ipairs(path) do
                if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
@@ -70,12 +33,13 @@ function build_url(...)
                end
        end
 
+       if #path == 0 then
+               url[#url+1] = "/"
+       end
+
        return table.concat(url, "")
 end
 
---- Check whether a dispatch node shall be visible
--- @param node Dispatch node
--- @return             Boolean indicating whether the node should be visible
 function node_visible(node)
    if node then
          return not (
@@ -88,9 +52,6 @@ function node_visible(node)
    return false
 end
 
---- Return a sorted table of visible childs within a given node
--- @param node Dispatch node
--- @return             Ordered table of child node names
 function node_childs(node)
        local rv = { }
        if node then
@@ -110,64 +71,44 @@ function node_childs(node)
 end
 
 
---- Send a 404 error code and render the "error404" template if available.
--- @param message      Custom error message (optional)
--- @return                     false
 function error404(message)
-       luci.http.status(404, "Not Found")
+       http.status(404, "Not Found")
        message = message or "Not Found"
 
-       require("luci.template")
-       if not luci.util.copcall(luci.template.render, "error404") then
-               luci.http.prepare_content("text/plain")
-               luci.http.write(message)
+       local function render()
+               local template = require "luci.template"
+               template.render("error404")
        end
+
+       if not util.copcall(render) then
+               http.prepare_content("text/plain")
+               http.write(message)
+       end
+
        return false
 end
 
---- Send a 500 error code and render the "error500" template if available.
--- @param message      Custom error message (optional)#
--- @return                     false
 function error500(message)
-       luci.util.perror(message)
+       util.perror(message)
        if not context.template_header_sent then
-               luci.http.status(500, "Internal Server Error")
-               luci.http.prepare_content("text/plain")
-               luci.http.write(message)
+               http.status(500, "Internal Server Error")
+               http.prepare_content("text/plain")
+               http.write(message)
        else
                require("luci.template")
-               if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
-                       luci.http.prepare_content("text/plain")
-                       luci.http.write(message)
+               if not util.copcall(luci.template.render, "error500", {message=message}) then
+                       http.prepare_content("text/plain")
+                       http.write(message)
                end
        end
        return false
 end
 
-function authenticator.htmlauth(validator, accs, default)
-       local user = luci.http.formvalue("luci_username")
-       local pass = luci.http.formvalue("luci_password")
-
-       if user and validator(user, pass) then
-               return user
-       end
-
-       require("luci.i18n")
-       require("luci.template")
-       context.path = {}
-       luci.template.render("sysauth", {duser=default, fuser=user})
-       return false
-
-end
-
---- Dispatch an HTTP request.
--- @param request      LuCI HTTP Request object
 function httpdispatch(request, prefix)
-       luci.http.context.request = request
+       http.context.request = request
 
        local r = {}
        context.request = r
-       context.urltoken = {}
 
        local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
 
@@ -177,31 +118,109 @@ function httpdispatch(request, prefix)
                end
        end
 
-       local tokensok = true
-       for node in pathinfo:gmatch("[^/]+") do
-               local tkey, tval
-               if tokensok then
-                       tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
-               end
-               if tkey then
-                       context.urltoken[tkey] = tval
-               else
-                       tokensok = false
-                       r[#r+1] = node
-               end
+       local node
+       for node in pathinfo:gmatch("[^/%z]+") do
+               r[#r+1] = node
        end
 
        local stat, err = util.coxpcall(function()
                dispatch(context.request)
        end, error500)
 
-       luci.http.close()
+       http.close()
 
        --context._disable_memtrace()
 end
 
---- Dispatches a LuCI virtual path.
--- @param request      Virtual path
+local function require_post_security(target)
+       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 })
+
+       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
+       end
+
+       return 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 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) }
+                       })
+
+                       io.stderr:write("luci: accepted login on /%s for %s from %s\n"
+                               %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
+
+                       return session_retrieve(login.ubus_rpc_session)
+               end
+
+               io.stderr:write("luci: failed login on /%s for %s from %s\n"
+                       %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
+       end
+
+       return nil, nil
+end
+
 function dispatch(request)
        --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
        local ctx = context
@@ -211,18 +230,31 @@ function dispatch(request)
        assert(conf.main,
                "/etc/config/luci seems to be corrupt, unable to find section 'main'")
 
+       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 lpat in aclang:gmatch("[%w-]+") do
-                       lpat = lpat and lpat:gsub("-", "_")
-                       if conf.languages[lpat] then
-                               lang = lpat
+               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
-       require "luci.i18n".setlanguage(lang)
+       if lang == "auto" then
+               lang = i18n.default
+       end
+       i18n.setlanguage(lang)
 
        local c = ctx.tree
        local stat
@@ -235,7 +267,6 @@ function dispatch(request)
        ctx.args = args
        ctx.requestargs = ctx.requestargs or args
        local n
-       local token = ctx.urltoken
        local preq = {}
        local freq = {}
 
@@ -288,9 +319,16 @@ function dispatch(request)
                        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),
-                                       luci.util.pcdata(tostring( val
+                                       util.pcdata(tostring( val
                                         or (type(env[key]) ~= "function" and env[key])
                                         or (scope and type(scope[key]) ~= "function" and scope[key])
                                         or "" ))
@@ -301,7 +339,7 @@ function dispatch(request)
                end
 
                tpl.context.viewns = setmetatable({
-                  write       = luci.http.write;
+                  write       = http.write;
                   include     = function(name) tpl.Template(name):render(getfenv(2)) end;
                   translate   = i18n.translate;
                   translatef  = i18n.translatef;
@@ -313,13 +351,24 @@ function dispatch(request)
                   resource    = luci.config.main.resourcebase;
                   ifattr      = function(...) return _ifattr(...) end;
                   attr        = function(...) return _ifattr(true, ...) end;
-               }, {__index=function(table, key)
+                  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"), 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(table, key) or _G[key]
+                               return rawget(tbl, key) or _G[key]
                        end
                end})
        end
@@ -329,85 +378,92 @@ function dispatch(request)
                "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 " ..
-               "http://luci.subsignal.org/trac/newticket"
+               "https://github.com/openwrt/luci/issues"
        )
 
-       if track.sysauth then
-               local authen = type(track.sysauth_authenticator) == "function"
-                and track.sysauth_authenticator
-                or authenticator[track.sysauth_authenticator]
+       if track.sysauth and not ctx.authsession then
+               local authen = track.sysauth_authenticator
+               local _, sid, sdat, default_user, allowed_users
 
-               local def  = (type(track.sysauth) == "string") and track.sysauth
-               local accs = def and {track.sysauth} or track.sysauth
-               local sess = ctx.authsession
-               local verifytoken = false
-               if not sess then
-                       sess = luci.http.getcookie("sysauth")
-                       sess = sess and sess:match("^[a-f0-9]*$")
-                       verifytoken = true
+               if type(authen) == "string" and authen ~= "htmlauth" then
+                       error500("Unsupported authenticator %q configured" % authen)
+                       return
                end
 
-               local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
-               local user
+               if type(track.sysauth) == "table" then
+                       default_user, allowed_users = nil, track.sysauth
+               else
+                       default_user, allowed_users = track.sysauth, { track.sysauth }
+               end
 
-               if sdat then
-                       if not verifytoken or ctx.urltoken.stok == sdat.token then
-                               user = sdat.user
-                       end
+               if type(authen) == "function" then
+                       _, sid = authen(sys.user.checkpasswd, allowed_users)
                else
-                       local eu = http.getenv("HTTP_AUTH_USER")
-                       local ep = http.getenv("HTTP_AUTH_PASS")
-                       if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
-                               authen = function() return eu end
-                       end
+                       sid = http.getcookie("sysauth")
                end
 
-               if not util.contains(accs, user) then
-                       if authen then
-                               ctx.urltoken.stok = nil
-                               local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
-                               if not user or not util.contains(accs, user) then
-                                       return
-                               else
-                                       if not sess then
-                                               local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
-                                               if sdat then
-                                                       local token = luci.sys.uniqueid(16)
-                                                       util.ubus("session", "set", {
-                                                               ubus_rpc_session = sdat.ubus_rpc_session,
-                                                               values = {
-                                                                       user = user,
-                                                                       token = token,
-                                                                       section = luci.sys.uniqueid(16)
-                                                               }
-                                                       })
-                                                       sess = sdat.ubus_rpc_session
-                                                       ctx.urltoken.stok = token
-                                               end
-                                       end
+               sid, sdat = session_retrieve(sid, allowed_users)
+
+               if not (sid and sdat) and authen == "htmlauth" then
+                       local user = http.getenv("HTTP_AUTH_USER")
+                       local pass = http.getenv("HTTP_AUTH_PASS")
+
+                       if user == nil and pass == nil then
+                               user = http.formvalue("luci_username")
+                               pass = http.formvalue("luci_password")
+                       end
+
+                       sid, sdat = session_setup(user, pass, allowed_users)
+
+                       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
+                               })
 
-                                       if sess then
-                                               luci.http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
-                                               ctx.authsession = sess
-                                               ctx.authuser = user
-                                       end
-                               end
-                       else
-                               luci.http.status(403, "Forbidden")
                                return
                        end
-               else
-                       ctx.authsession = sess
-                       ctx.authuser = user
+
+                       http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
+                               sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
+                       })
+                       http.redirect(build_url(unpack(ctx.requestpath)))
+               end
+
+               if not sid or not sdat then
+                       http.status(403, "Forbidden")
+                       return
+               end
+
+               ctx.authsession = sid
+               ctx.authtoken = sdat.token
+               ctx.authuser = sdat.username
+       end
+
+       if track.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
+                       return
                end
        end
 
        if track.setgroup then
-               luci.sys.process.setgroup(track.setgroup)
+               sys.process.setgroup(track.setgroup)
        end
 
        if track.setuser then
-               luci.sys.process.setuser(track.setuser)
+               sys.process.setuser(track.setuser)
        end
 
        local target = nil
@@ -469,46 +525,17 @@ function dispatch(request)
        end
 end
 
---- Generate the dispatching index using the best possible strategy.
 function createindex()
-       local path = luci.util.libpath() .. "/controller/"
-       local suff = { ".lua", ".lua.gz" }
-
-       if luci.util.copcall(require, "luci.fastindex") then
-               createindex_fastindex(path, suff)
-       else
-               createindex_plain(path, suff)
-       end
-end
-
---- Generate the dispatching index using the fastindex C-indexer.
--- @param path         Controller base directory
--- @param suffixes     Controller file suffixes
-function createindex_fastindex(path, suffixes)
-       index = {}
-
-       if not fi then
-               fi = luci.fastindex.new("index")
-               for _, suffix in ipairs(suffixes) do
-                       fi.add(path .. "*" .. suffix)
-                       fi.add(path .. "*/*" .. suffix)
-               end
-       end
-       fi.scan()
+       local controllers = { }
+       local base = "%s/controller/" % util.libpath()
+       local _, path
 
-       for k, v in pairs(fi.indexes) do
-               index[v[2]] = v[1]
+       for path in (fs.glob("%s*.lua" % base) or function() end) do
+               controllers[#controllers+1] = path
        end
-end
 
---- Generate the dispatching index using the native file-cache based strategy.
--- @param path         Controller base directory
--- @param suffixes     Controller file suffixes
-function createindex_plain(path, suffixes)
-       local controllers = { }
-       for _, suffix in ipairs(suffixes) do
-               nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
-               nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
+       for path in (fs.glob("%s*/*.lua" % base) or function() end) do
+               controllers[#controllers+1] = path
        end
 
        if indexcache then
@@ -520,7 +547,7 @@ function createindex_plain(path, suffixes)
                                realdate = (omtime and omtime > realdate) and omtime or realdate
                        end
 
-                       if cachedate > realdate then
+                       if cachedate > realdate and sys.process.info("uid") == 0 then
                                assert(
                                        sys.process.info("uid") == fs.stat(indexcache, "uid")
                                        and fs.stat(indexcache, "modestr") == "rw-------",
@@ -535,23 +562,19 @@ function createindex_plain(path, suffixes)
 
        index = {}
 
-       for i,c in ipairs(controllers) do
-               local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
-               for _, suffix in ipairs(suffixes) do
-                       modname = modname:gsub(suffix.."$", "")
-               end
-
+       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 '" .. c .. "' contains an invalid module line.\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
                assert(type(idx) == "function",
                       "Invalid controller file found\n" ..
-                      "The file '" .. c .. "' contains no index() function.\n" ..
+                      "The file '" .. path .. "' contains no index() function.\n" ..
                       "Please make sure that the controller contains a valid " ..
                       "index function and verify the spelling!")
 
@@ -565,7 +588,6 @@ function createindex_plain(path, suffixes)
        end
 end
 
---- Create the dispatching tree from the index.
 -- Build the index before if it does not exist yet.
 function createtree()
        if not index then
@@ -604,9 +626,6 @@ function createtree()
        return tree
 end
 
---- Register a tree modifier.
--- @param      func    Modifier function
--- @param      order   Modifier order value (optional)
 function modifier(func, order)
        context.modifiers[#context.modifiers+1] = {
                func = func,
@@ -616,12 +635,6 @@ function modifier(func, order)
        }
 end
 
---- Clone a node of the dispatching tree to another position.
--- @param      path    Virtual path destination
--- @param      clone   Virtual path source
--- @param      title   Destination node title (optional)
--- @param      order   Destination node order value (optional)
--- @return                     Dispatching tree node
 function assign(path, clone, title, order)
        local obj  = node(unpack(path))
        obj.nodes  = nil
@@ -635,12 +648,6 @@ function assign(path, clone, title, order)
        return obj
 end
 
---- Create a new dispatching node and define common parameters.
--- @param      path    Virtual path
--- @param      target  Target function to call when dispatched.
--- @param      title   Destination node title
--- @param      order   Destination node order value (optional)
--- @return                     Dispatching tree node
 function entry(path, target, title, order)
        local c = node(unpack(path))
 
@@ -652,17 +659,11 @@ function entry(path, target, title, order)
        return c
 end
 
---- Fetch or create a dispatching node without setting the target module or
 -- enabling the node.
--- @param      ...             Virtual path
--- @return                     Dispatching tree node
 function get(...)
        return _create_node({...})
 end
 
---- Fetch or create a new dispatching node.
--- @param      ...             Virtual path
--- @return                     Dispatching tree node
 function node(...)
        local c = _create_node({...})
 
@@ -672,6 +673,23 @@ function node(...)
        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
@@ -722,13 +740,10 @@ function _firstchild()
    dispatch(path)
 end
 
---- Alias the first (lowest order) page automatically
 function firstchild()
    return { type = "firstchild", target = _firstchild }
 end
 
---- Create a redirect to another dispatching node.
--- @param      ...             Virtual path destination
 function alias(...)
        local req = {...}
        return function(...)
@@ -740,9 +755,6 @@ function alias(...)
        end
 end
 
---- Rewrite the first x path values of the request.
--- @param      n               Number of path values to replace
--- @param      ...             Virtual path to replace removed path values with
 function rewrite(n, ...)
        local req = {...}
        return function(...)
@@ -781,20 +793,29 @@ local function _call(self, ...)
        end
 end
 
---- Create a function-call dispatching target.
--- @param      name    Target function of local controller
--- @param      ...             Additional parameters passed to the function
 function call(name, ...)
        return {type = "call", argv = {...}, name = name, target = _call}
 end
 
+function post_on(params, name, ...)
+       return {
+               type = "call",
+               post = params,
+               argv = { ... },
+               name = name,
+               target = _call
+       }
+end
+
+function post(...)
+       return post_on(true, ...)
+end
+
 
 local _template = function(self, ...)
        require "luci.template".render(self.view)
 end
 
---- Create a template render dispatching target.
--- @param      name    Template to be rendered
 function template(name)
        return {type = "template", view = name, target = _template}
 end
@@ -810,7 +831,16 @@ local function _cbi(self, ...)
 
        local state = nil
 
+       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
@@ -855,6 +885,8 @@ 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
@@ -882,6 +914,7 @@ local function _cbi(self, ...)
                res:render({
                        firstmap   = (i == 1),
                        applymap   = applymap,
+                       confirmmap = (is_rollback and time_remaining or nil),
                        redirect   = redirect,
                        messages   = messages,
                        pageaction = pageaction,
@@ -900,10 +933,14 @@ local function _cbi(self, ...)
        end
 end
 
---- Create a CBI model dispatching target.
--- @param      model   CBI model to be rendered
 function cbi(model, config)
-       return {type = "cbi", config = config, model = model, target = _cbi}
+       return {
+               type = "cbi",
+               post = { ["cbi.submit"] = true },
+               config = config,
+               model = model,
+               target = _cbi
+       }
 end
 
 
@@ -914,9 +951,6 @@ local function _arcombine(self, ...)
        target:target(unpack(argv))
 end
 
---- Create a combined dispatching target for non argv and argv requests.
--- @param trg1 Overview Target
--- @param trg2 Detail Target
 function arcombine(trg1, trg2)
        return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
 end
@@ -930,6 +964,7 @@ local function _form(self, ...)
        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
@@ -945,19 +980,17 @@ local function _form(self, ...)
        tpl.render("footer")
 end
 
---- Create a CBI form model dispatching target.
--- @param      model   CBI form model tpo be rendered
 function form(model)
-       return {type = "cbi", model = model, target = _form}
+       return {
+               type = "cbi",
+               post = { ["cbi.submit"] = true },
+               model = model,
+               target = _form
+       }
 end
 
---- Access the luci.i18n translate() api.
--- @class  function
--- @name   translate
--- @param  text    Text to translate
 translate = i18n.translate
 
---- No-op function used to mark translation entries for menu labels.
 -- This function does not actually translate the given argument but
 -- is used by build/i18n-scan.pl to find translatable entries.
 function _(text)