luci-mod-admin-full: add suggested italian translations
[oweals/luci.git] / modules / luci-base / luasrc / dispatcher.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
4
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
10
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
15 _M.fs = fs
16
17 -- Index table
18 local index = nil
19
20 -- Fastindex
21 local fi
22
23
24 function build_url(...)
25         local path = {...}
26         local url = { http.getenv("SCRIPT_NAME") or "" }
27
28         local p
29         for _, p in ipairs(path) do
30                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
31                         url[#url+1] = "/"
32                         url[#url+1] = p
33                 end
34         end
35
36         if #path == 0 then
37                 url[#url+1] = "/"
38         end
39
40         return table.concat(url, "")
41 end
42
43 function node_visible(node)
44    if node then
45           return not (
46                  (not node.title or #node.title == 0) or
47                  (not node.target or node.hidden == true) or
48                  (type(node.target) == "table" and node.target.type == "firstchild" and
49                   (type(node.nodes) ~= "table" or not next(node.nodes)))
50           )
51    end
52    return false
53 end
54
55 function node_childs(node)
56         local rv = { }
57         if node then
58                 local k, v
59                 for k, v in util.spairs(node.nodes,
60                         function(a, b)
61                                 return (node.nodes[a].order or 100)
62                                      < (node.nodes[b].order or 100)
63                         end)
64                 do
65                         if node_visible(v) then
66                                 rv[#rv+1] = k
67                         end
68                 end
69         end
70         return rv
71 end
72
73
74 function error404(message)
75         http.status(404, "Not Found")
76         message = message or "Not Found"
77
78         local function render()
79                 local template = require "luci.template"
80                 template.render("error404")
81         end
82
83         if not util.copcall(render) then
84                 http.prepare_content("text/plain")
85                 http.write(message)
86         end
87
88         return false
89 end
90
91 function error500(message)
92         util.perror(message)
93         if not context.template_header_sent then
94                 http.status(500, "Internal Server Error")
95                 http.prepare_content("text/plain")
96                 http.write(message)
97         else
98                 require("luci.template")
99                 if not util.copcall(luci.template.render, "error500", {message=message}) then
100                         http.prepare_content("text/plain")
101                         http.write(message)
102                 end
103         end
104         return false
105 end
106
107 function httpdispatch(request, prefix)
108         http.context.request = request
109
110         local r = {}
111         context.request = r
112
113         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
114
115         if prefix then
116                 for _, node in ipairs(prefix) do
117                         r[#r+1] = node
118                 end
119         end
120
121         local node
122         for node in pathinfo:gmatch("[^/%z]+") do
123                 r[#r+1] = node
124         end
125
126         local stat, err = util.coxpcall(function()
127                 dispatch(context.request)
128         end, error500)
129
130         http.close()
131
132         --context._disable_memtrace()
133 end
134
135 local function require_post_security(target)
136         if type(target) == "table" then
137                 if type(target.post) == "table" then
138                         local param_name, required_val, request_val
139
140                         for param_name, required_val in pairs(target.post) do
141                                 request_val = http.formvalue(param_name)
142
143                                 if (type(required_val) == "string" and
144                                     request_val ~= required_val) or
145                                    (required_val == true and request_val == nil)
146                                 then
147                                         return false
148                                 end
149                         end
150
151                         return true
152                 end
153
154                 return (target.post == true)
155         end
156
157         return false
158 end
159
160 function test_post_security()
161         if http.getenv("REQUEST_METHOD") ~= "POST" then
162                 http.status(405, "Method Not Allowed")
163                 http.header("Allow", "POST")
164                 return false
165         end
166
167         if http.formvalue("token") ~= context.authtoken then
168                 http.status(403, "Forbidden")
169                 luci.template.render("csrftoken")
170                 return false
171         end
172
173         return true
174 end
175
176 local function session_retrieve(sid, allowed_users)
177         local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
178
179         if type(sdat) == "table" and
180            type(sdat.values) == "table" and
181            type(sdat.values.token) == "string" and
182            (not allowed_users or
183             util.contains(allowed_users, sdat.values.username))
184         then
185                 uci:set_session_id(sid)
186                 return sid, sdat.values
187         end
188
189         return nil, nil
190 end
191
192 local function session_setup(user, pass, allowed_users)
193         if util.contains(allowed_users, user) then
194                 local login = util.ubus("session", "login", {
195                         username = user,
196                         password = pass,
197                         timeout  = tonumber(luci.config.sauth.sessiontime)
198                 })
199
200                 local rp = context.requestpath
201                         and table.concat(context.requestpath, "/") or ""
202
203                 if type(login) == "table" and
204                    type(login.ubus_rpc_session) == "string"
205                 then
206                         util.ubus("session", "set", {
207                                 ubus_rpc_session = login.ubus_rpc_session,
208                                 values = { token = sys.uniqueid(16) }
209                         })
210
211                         io.stderr:write("luci: accepted login on /%s for %s from %s\n"
212                                 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
213
214                         return session_retrieve(login.ubus_rpc_session)
215                 end
216
217                 io.stderr:write("luci: failed login on /%s for %s from %s\n"
218                         %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
219         end
220
221         return nil, nil
222 end
223
224 function dispatch(request)
225         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
226         local ctx = context
227         ctx.path = request
228
229         local conf = require "luci.config"
230         assert(conf.main,
231                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
232
233         local i18n = require "luci.i18n"
234         local lang = conf.main.lang or "auto"
235         if lang == "auto" then
236                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
237                 for aclang in aclang:gmatch("[%w_-]+") do
238                         local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
239                         if country and culture then
240                                 local cc = "%s_%s" %{ country, culture:lower() }
241                                 if conf.languages[cc] then
242                                         lang = cc
243                                         break
244                                 elseif conf.languages[country] then
245                                         lang = country
246                                         break
247                                 end
248                         elseif conf.languages[aclang] then
249                                 lang = aclang
250                                 break
251                         end
252                 end
253         end
254         if lang == "auto" then
255                 lang = i18n.default
256         end
257         i18n.setlanguage(lang)
258
259         local c = ctx.tree
260         local stat
261         if not c then
262                 c = createtree()
263         end
264
265         local track = {}
266         local args = {}
267         ctx.args = args
268         ctx.requestargs = ctx.requestargs or args
269         local n
270         local preq = {}
271         local freq = {}
272
273         for i, s in ipairs(request) do
274                 preq[#preq+1] = s
275                 freq[#freq+1] = s
276                 c = c.nodes[s]
277                 n = i
278                 if not c then
279                         break
280                 end
281
282                 util.update(track, c)
283
284                 if c.leaf then
285                         break
286                 end
287         end
288
289         if c and c.leaf then
290                 for j=n+1, #request do
291                         args[#args+1] = request[j]
292                         freq[#freq+1] = request[j]
293                 end
294         end
295
296         ctx.requestpath = ctx.requestpath or freq
297         ctx.path = preq
298
299         if track.i18n then
300                 i18n.loadc(track.i18n)
301         end
302
303         -- Init template engine
304         if (c and c.index) or not track.notemplate then
305                 local tpl = require("luci.template")
306                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
307                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
308                         media = nil
309                         for name, theme in pairs(luci.config.themes) do
310                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
311                                  "themes/%s/header" % fs.basename(theme)) then
312                                         media = theme
313                                 end
314                         end
315                         assert(media, "No valid theme found")
316                 end
317
318                 local function _ifattr(cond, key, val)
319                         if cond then
320                                 local env = getfenv(3)
321                                 local scope = (type(env.self) == "table") and env.self
322                                 if type(val) == "table" then
323                                         if not next(val) then
324                                                 return ''
325                                         else
326                                                 val = util.serialize_json(val)
327                                         end
328                                 end
329                                 return string.format(
330                                         ' %s="%s"', tostring(key),
331                                         util.pcdata(tostring( val
332                                          or (type(env[key]) ~= "function" and env[key])
333                                          or (scope and type(scope[key]) ~= "function" and scope[key])
334                                          or "" ))
335                                 )
336                         else
337                                 return ''
338                         end
339                 end
340
341                 tpl.context.viewns = setmetatable({
342                    write       = http.write;
343                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
344                    translate   = i18n.translate;
345                    translatef  = i18n.translatef;
346                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
347                    striptags   = util.striptags;
348                    pcdata      = util.pcdata;
349                    media       = media;
350                    theme       = fs.basename(media);
351                    resource    = luci.config.main.resourcebase;
352                    ifattr      = function(...) return _ifattr(...) end;
353                    attr        = function(...) return _ifattr(true, ...) end;
354                    url         = build_url;
355                 }, {__index=function(tbl, key)
356                         if key == "controller" then
357                                 return build_url()
358                         elseif key == "REQUEST_URI" then
359                                 return build_url(unpack(ctx.requestpath))
360                         elseif key == "FULL_REQUEST_URI" then
361                                 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
362                                 local query = http.getenv("QUERY_STRING")
363                                 if query and #query > 0 then
364                                         url[#url+1] = "?"
365                                         url[#url+1] = query
366                                 end
367                                 return table.concat(url, "")
368                         elseif key == "token" then
369                                 return ctx.authtoken
370                         else
371                                 return rawget(tbl, key) or _G[key]
372                         end
373                 end})
374         end
375
376         track.dependent = (track.dependent ~= false)
377         assert(not track.dependent or not track.auto,
378                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
379                 "has no parent node so the access to this location has been denied.\n" ..
380                 "This is a software bug, please report this message at " ..
381                 "https://github.com/openwrt/luci/issues"
382         )
383
384         if track.sysauth and not ctx.authsession then
385                 local authen = track.sysauth_authenticator
386                 local _, sid, sdat, default_user, allowed_users
387
388                 if type(authen) == "string" and authen ~= "htmlauth" then
389                         error500("Unsupported authenticator %q configured" % authen)
390                         return
391                 end
392
393                 if type(track.sysauth) == "table" then
394                         default_user, allowed_users = nil, track.sysauth
395                 else
396                         default_user, allowed_users = track.sysauth, { track.sysauth }
397                 end
398
399                 if type(authen) == "function" then
400                         _, sid = authen(sys.user.checkpasswd, allowed_users)
401                 else
402                         sid = http.getcookie("sysauth")
403                 end
404
405                 sid, sdat = session_retrieve(sid, allowed_users)
406
407                 if not (sid and sdat) and authen == "htmlauth" then
408                         local user = http.getenv("HTTP_AUTH_USER")
409                         local pass = http.getenv("HTTP_AUTH_PASS")
410
411                         if user == nil and pass == nil then
412                                 user = http.formvalue("luci_username")
413                                 pass = http.formvalue("luci_password")
414                         end
415
416                         sid, sdat = session_setup(user, pass, allowed_users)
417
418                         if not sid then
419                                 local tmpl = require "luci.template"
420
421                                 context.path = {}
422
423                                 http.status(403, "Forbidden")
424                                 tmpl.render(track.sysauth_template or "sysauth", {
425                                         duser = default_user,
426                                         fuser = user
427                                 })
428
429                                 return
430                         end
431
432                         http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
433                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
434                         })
435                         http.redirect(build_url(unpack(ctx.requestpath)))
436                 end
437
438                 if not sid or not sdat then
439                         http.status(403, "Forbidden")
440                         return
441                 end
442
443                 ctx.authsession = sid
444                 ctx.authtoken = sdat.token
445                 ctx.authuser = sdat.username
446         end
447
448         if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
449                 luci.http.status(200, "OK")
450                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
451                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
452                 return
453         end
454
455         if c and require_post_security(c.target) then
456                 if not test_post_security(c) then
457                         return
458                 end
459         end
460
461         if track.setgroup then
462                 sys.process.setgroup(track.setgroup)
463         end
464
465         if track.setuser then
466                 sys.process.setuser(track.setuser)
467         end
468
469         local target = nil
470         if c then
471                 if type(c.target) == "function" then
472                         target = c.target
473                 elseif type(c.target) == "table" then
474                         target = c.target.target
475                 end
476         end
477
478         if c and (c.index or type(target) == "function") then
479                 ctx.dispatched = c
480                 ctx.requested = ctx.requested or ctx.dispatched
481         end
482
483         if c and c.index then
484                 local tpl = require "luci.template"
485
486                 if util.copcall(tpl.render, "indexer", {}) then
487                         return true
488                 end
489         end
490
491         if type(target) == "function" then
492                 util.copcall(function()
493                         local oldenv = getfenv(target)
494                         local module = require(c.module)
495                         local env = setmetatable({}, {__index=
496
497                         function(tbl, key)
498                                 return rawget(tbl, key) or module[key] or oldenv[key]
499                         end})
500
501                         setfenv(target, env)
502                 end)
503
504                 local ok, err
505                 if type(c.target) == "table" then
506                         ok, err = util.copcall(target, c.target, unpack(args))
507                 else
508                         ok, err = util.copcall(target, unpack(args))
509                 end
510                 if not ok then
511                         error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
512                                  " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
513                                  "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
514                 end
515         else
516                 local root = node()
517                 if not root or not root.target then
518                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
519                                  "Install luci-mod-admin-full and retry. " ..
520                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
521                 else
522                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
523                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
524                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
525                 end
526         end
527 end
528
529 function createindex()
530         local controllers = { }
531         local base = "%s/controller/" % util.libpath()
532         local _, path
533
534         for path in (fs.glob("%s*.lua" % base) or function() end) do
535                 controllers[#controllers+1] = path
536         end
537
538         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
539                 controllers[#controllers+1] = path
540         end
541
542         if indexcache then
543                 local cachedate = fs.stat(indexcache, "mtime")
544                 if cachedate then
545                         local realdate = 0
546                         for _, obj in ipairs(controllers) do
547                                 local omtime = fs.stat(obj, "mtime")
548                                 realdate = (omtime and omtime > realdate) and omtime or realdate
549                         end
550
551                         if cachedate > realdate and sys.process.info("uid") == 0 then
552                                 assert(
553                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
554                                         and fs.stat(indexcache, "modestr") == "rw-------",
555                                         "Fatal: Indexcache is not sane!"
556                                 )
557
558                                 index = loadfile(indexcache)()
559                                 return index
560                         end
561                 end
562         end
563
564         index = {}
565
566         for _, path in ipairs(controllers) do
567                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
568                 local mod = require(modname)
569                 assert(mod ~= true,
570                        "Invalid controller file found\n" ..
571                        "The file '" .. path .. "' contains an invalid module line.\n" ..
572                        "Please verify whether the module name is set to '" .. modname ..
573                        "' - It must correspond to the file path!")
574
575                 local idx = mod.index
576                 assert(type(idx) == "function",
577                        "Invalid controller file found\n" ..
578                        "The file '" .. path .. "' contains no index() function.\n" ..
579                        "Please make sure that the controller contains a valid " ..
580                        "index function and verify the spelling!")
581
582                 index[modname] = idx
583         end
584
585         if indexcache then
586                 local f = nixio.open(indexcache, "w", 600)
587                 f:writeall(util.get_bytecode(index))
588                 f:close()
589         end
590 end
591
592 -- Build the index before if it does not exist yet.
593 function createtree()
594         if not index then
595                 createindex()
596         end
597
598         local ctx  = context
599         local tree = {nodes={}, inreq=true}
600         local modi = {}
601
602         ctx.treecache = setmetatable({}, {__mode="v"})
603         ctx.tree = tree
604         ctx.modifiers = modi
605
606         -- Load default translation
607         require "luci.i18n".loadc("base")
608
609         local scope = setmetatable({}, {__index = luci.dispatcher})
610
611         for k, v in pairs(index) do
612                 scope._NAME = k
613                 setfenv(v, scope)
614                 v()
615         end
616
617         local function modisort(a,b)
618                 return modi[a].order < modi[b].order
619         end
620
621         for _, v in util.spairs(modi, modisort) do
622                 scope._NAME = v.module
623                 setfenv(v.func, scope)
624                 v.func()
625         end
626
627         return tree
628 end
629
630 function modifier(func, order)
631         context.modifiers[#context.modifiers+1] = {
632                 func = func,
633                 order = order or 0,
634                 module
635                         = getfenv(2)._NAME
636         }
637 end
638
639 function assign(path, clone, title, order)
640         local obj  = node(unpack(path))
641         obj.nodes  = nil
642         obj.module = nil
643
644         obj.title = title
645         obj.order = order
646
647         setmetatable(obj, {__index = _create_node(clone)})
648
649         return obj
650 end
651
652 function entry(path, target, title, order)
653         local c = node(unpack(path))
654
655         c.target = target
656         c.title  = title
657         c.order  = order
658         c.module = getfenv(2)._NAME
659
660         return c
661 end
662
663 -- enabling the node.
664 function get(...)
665         return _create_node({...})
666 end
667
668 function node(...)
669         local c = _create_node({...})
670
671         c.module = getfenv(2)._NAME
672         c.auto = nil
673
674         return c
675 end
676
677 function lookup(...)
678         local i, path = nil, {}
679         for i = 1, select('#', ...) do
680                 local name, arg = nil, tostring(select(i, ...))
681                 for name in arg:gmatch("[^/]+") do
682                         path[#path+1] = name
683                 end
684         end
685
686         for i = #path, 1, -1 do
687                 local node = context.treecache[table.concat(path, ".", 1, i)]
688                 if node and (i == #path or node.leaf) then
689                         return node, build_url(unpack(path))
690                 end
691         end
692 end
693
694 function _create_node(path)
695         if #path == 0 then
696                 return context.tree
697         end
698
699         local name = table.concat(path, ".")
700         local c = context.treecache[name]
701
702         if not c then
703                 local last = table.remove(path)
704                 local parent = _create_node(path)
705
706                 c = {nodes={}, auto=true}
707                 -- the node is "in request" if the request path matches
708                 -- at least up to the length of the node path
709                 if parent.inreq and context.path[#path+1] == last then
710                   c.inreq = true
711                 end
712                 parent.nodes[last] = c
713                 context.treecache[name] = c
714         end
715         return c
716 end
717
718 -- Subdispatchers --
719
720 function _firstchild()
721    local path = { unpack(context.path) }
722    local name = table.concat(path, ".")
723    local node = context.treecache[name]
724
725    local lowest
726    if node and node.nodes and next(node.nodes) then
727           local k, v
728           for k, v in pairs(node.nodes) do
729                  if not lowest or
730                         (v.order or 100) < (node.nodes[lowest].order or 100)
731                  then
732                         lowest = k
733                  end
734           end
735    end
736
737    assert(lowest ~= nil,
738                   "The requested node contains no childs, unable to redispatch")
739
740    path[#path+1] = lowest
741    dispatch(path)
742 end
743
744 function firstchild()
745    return { type = "firstchild", target = _firstchild }
746 end
747
748 function alias(...)
749         local req = {...}
750         return function(...)
751                 for _, r in ipairs({...}) do
752                         req[#req+1] = r
753                 end
754
755                 dispatch(req)
756         end
757 end
758
759 function rewrite(n, ...)
760         local req = {...}
761         return function(...)
762                 local dispatched = util.clone(context.dispatched)
763
764                 for i=1,n do
765                         table.remove(dispatched, 1)
766                 end
767
768                 for i, r in ipairs(req) do
769                         table.insert(dispatched, i, r)
770                 end
771
772                 for _, r in ipairs({...}) do
773                         dispatched[#dispatched+1] = r
774                 end
775
776                 dispatch(dispatched)
777         end
778 end
779
780
781 local function _call(self, ...)
782         local func = getfenv()[self.name]
783         assert(func ~= nil,
784                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
785
786         assert(type(func) == "function",
787                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
788                'of type "' .. type(func) .. '".')
789
790         if #self.argv > 0 then
791                 return func(unpack(self.argv), ...)
792         else
793                 return func(...)
794         end
795 end
796
797 function call(name, ...)
798         return {type = "call", argv = {...}, name = name, target = _call}
799 end
800
801 function post_on(params, name, ...)
802         return {
803                 type = "call",
804                 post = params,
805                 argv = { ... },
806                 name = name,
807                 target = _call
808         }
809 end
810
811 function post(...)
812         return post_on(true, ...)
813 end
814
815
816 local _template = function(self, ...)
817         require "luci.template".render(self.view)
818 end
819
820 function template(name)
821         return {type = "template", view = name, target = _template}
822 end
823
824
825 local function _cbi(self, ...)
826         local cbi = require "luci.cbi"
827         local tpl = require "luci.template"
828         local http = require "luci.http"
829
830         local config = self.config or {}
831         local maps = cbi.load(self.model, ...)
832
833         local state = nil
834
835         local i, res
836         for i, res in ipairs(maps) do
837                 if util.instanceof(res, cbi.SimpleForm) then
838                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
839                                 % self.model)
840
841                         io.stderr:write("please change %s to use the form() action instead.\n"
842                                 % table.concat(context.request, "/"))
843                 end
844
845                 res.flow = config
846                 local cstate = res:parse()
847                 if cstate and (not state or cstate < state) then
848                         state = cstate
849                 end
850         end
851
852         local function _resolve_path(path)
853                 return type(path) == "table" and build_url(unpack(path)) or path
854         end
855
856         if config.on_valid_to and state and state > 0 and state < 2 then
857                 http.redirect(_resolve_path(config.on_valid_to))
858                 return
859         end
860
861         if config.on_changed_to and state and state > 1 then
862                 http.redirect(_resolve_path(config.on_changed_to))
863                 return
864         end
865
866         if config.on_success_to and state and state > 0 then
867                 http.redirect(_resolve_path(config.on_success_to))
868                 return
869         end
870
871         if config.state_handler then
872                 if not config.state_handler(state, maps) then
873                         return
874                 end
875         end
876
877         http.header("X-CBI-State", state or 0)
878
879         if not config.noheader then
880                 tpl.render("cbi/header", {state = state})
881         end
882
883         local redirect
884         local messages
885         local applymap   = false
886         local pageaction = true
887         local parsechain = { }
888
889         local is_rollback, time_remaining = uci:rollback_pending()
890
891         for i, res in ipairs(maps) do
892                 if res.apply_needed and res.parsechain then
893                         local c
894                         for _, c in ipairs(res.parsechain) do
895                                 parsechain[#parsechain+1] = c
896                         end
897                         applymap = true
898                 end
899
900                 if res.redirect then
901                         redirect = redirect or res.redirect
902                 end
903
904                 if res.pageaction == false then
905                         pageaction = false
906                 end
907
908                 if res.message then
909                         messages = messages or { }
910                         messages[#messages+1] = res.message
911                 end
912         end
913
914         for i, res in ipairs(maps) do
915                 res:render({
916                         firstmap   = (i == 1),
917                         applymap   = applymap,
918                         confirmmap = (is_rollback and time_remaining or nil),
919                         redirect   = redirect,
920                         messages   = messages,
921                         pageaction = pageaction,
922                         parsechain = parsechain
923                 })
924         end
925
926         if not config.nofooter then
927                 tpl.render("cbi/footer", {
928                         flow       = config,
929                         pageaction = pageaction,
930                         redirect   = redirect,
931                         state      = state,
932                         autoapply  = config.autoapply
933                 })
934         end
935 end
936
937 function cbi(model, config)
938         return {
939                 type = "cbi",
940                 post = { ["cbi.submit"] = true },
941                 config = config,
942                 model = model,
943                 target = _cbi
944         }
945 end
946
947
948 local function _arcombine(self, ...)
949         local argv = {...}
950         local target = #argv > 0 and self.targets[2] or self.targets[1]
951         setfenv(target.target, self.env)
952         target:target(unpack(argv))
953 end
954
955 function arcombine(trg1, trg2)
956         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
957 end
958
959
960 local function _form(self, ...)
961         local cbi = require "luci.cbi"
962         local tpl = require "luci.template"
963         local http = require "luci.http"
964
965         local maps = luci.cbi.load(self.model, ...)
966         local state = nil
967
968         local i, res
969         for i, res in ipairs(maps) do
970                 local cstate = res:parse()
971                 if cstate and (not state or cstate < state) then
972                         state = cstate
973                 end
974         end
975
976         http.header("X-CBI-State", state or 0)
977         tpl.render("header")
978         for i, res in ipairs(maps) do
979                 res:render()
980         end
981         tpl.render("footer")
982 end
983
984 function form(model)
985         return {
986                 type = "cbi",
987                 post = { ["cbi.submit"] = true },
988                 model = model,
989                 target = _form
990         }
991 end
992
993 translate = i18n.translate
994
995 -- This function does not actually translate the given argument but
996 -- is used by build/i18n-scan.pl to find translatable entries.
997 function _(text)
998         return text
999 end