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