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