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