Merge pull request #1794 from dibdot/adblock
[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; HttpOnly%s' %{
432                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
433                         })
434                         http.redirect(build_url(unpack(ctx.requestpath)))
435                 end
436
437                 if not sid or not sdat then
438                         http.status(403, "Forbidden")
439                         return
440                 end
441
442                 ctx.authsession = sid
443                 ctx.authtoken = sdat.token
444                 ctx.authuser = sdat.username
445         end
446
447         if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
448                 luci.http.status(200, "OK")
449                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
450                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
451                 return
452         end
453
454         if c and require_post_security(c.target) then
455                 if not test_post_security(c) then
456                         return
457                 end
458         end
459
460         if track.setgroup then
461                 sys.process.setgroup(track.setgroup)
462         end
463
464         if track.setuser then
465                 sys.process.setuser(track.setuser)
466         end
467
468         local target = nil
469         if c then
470                 if type(c.target) == "function" then
471                         target = c.target
472                 elseif type(c.target) == "table" then
473                         target = c.target.target
474                 end
475         end
476
477         if c and (c.index or type(target) == "function") then
478                 ctx.dispatched = c
479                 ctx.requested = ctx.requested or ctx.dispatched
480         end
481
482         if c and c.index then
483                 local tpl = require "luci.template"
484
485                 if util.copcall(tpl.render, "indexer", {}) then
486                         return true
487                 end
488         end
489
490         if type(target) == "function" then
491                 util.copcall(function()
492                         local oldenv = getfenv(target)
493                         local module = require(c.module)
494                         local env = setmetatable({}, {__index=
495
496                         function(tbl, key)
497                                 return rawget(tbl, key) or module[key] or oldenv[key]
498                         end})
499
500                         setfenv(target, env)
501                 end)
502
503                 local ok, err
504                 if type(c.target) == "table" then
505                         ok, err = util.copcall(target, c.target, unpack(args))
506                 else
507                         ok, err = util.copcall(target, unpack(args))
508                 end
509                 assert(ok,
510                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
511                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
512                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
513         else
514                 local root = node()
515                 if not root or not root.target then
516                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
517                                  "Install luci-mod-admin-full and retry. " ..
518                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
519                 else
520                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
521                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
522                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
523                 end
524         end
525 end
526
527 function createindex()
528         local controllers = { }
529         local base = "%s/controller/" % util.libpath()
530         local _, path
531
532         for path in (fs.glob("%s*.lua" % base) or function() end) do
533                 controllers[#controllers+1] = path
534         end
535
536         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
537                 controllers[#controllers+1] = path
538         end
539
540         if indexcache then
541                 local cachedate = fs.stat(indexcache, "mtime")
542                 if cachedate then
543                         local realdate = 0
544                         for _, obj in ipairs(controllers) do
545                                 local omtime = fs.stat(obj, "mtime")
546                                 realdate = (omtime and omtime > realdate) and omtime or realdate
547                         end
548
549                         if cachedate > realdate and sys.process.info("uid") == 0 then
550                                 assert(
551                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
552                                         and fs.stat(indexcache, "modestr") == "rw-------",
553                                         "Fatal: Indexcache is not sane!"
554                                 )
555
556                                 index = loadfile(indexcache)()
557                                 return index
558                         end
559                 end
560         end
561
562         index = {}
563
564         for _, path in ipairs(controllers) do
565                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
566                 local mod = require(modname)
567                 assert(mod ~= true,
568                        "Invalid controller file found\n" ..
569                        "The file '" .. path .. "' contains an invalid module line.\n" ..
570                        "Please verify whether the module name is set to '" .. modname ..
571                        "' - It must correspond to the file path!")
572
573                 local idx = mod.index
574                 assert(type(idx) == "function",
575                        "Invalid controller file found\n" ..
576                        "The file '" .. path .. "' contains no index() function.\n" ..
577                        "Please make sure that the controller contains a valid " ..
578                        "index function and verify the spelling!")
579
580                 index[modname] = idx
581         end
582
583         if indexcache then
584                 local f = nixio.open(indexcache, "w", 600)
585                 f:writeall(util.get_bytecode(index))
586                 f:close()
587         end
588 end
589
590 -- Build the index before if it does not exist yet.
591 function createtree()
592         if not index then
593                 createindex()
594         end
595
596         local ctx  = context
597         local tree = {nodes={}, inreq=true}
598         local modi = {}
599
600         ctx.treecache = setmetatable({}, {__mode="v"})
601         ctx.tree = tree
602         ctx.modifiers = modi
603
604         -- Load default translation
605         require "luci.i18n".loadc("base")
606
607         local scope = setmetatable({}, {__index = luci.dispatcher})
608
609         for k, v in pairs(index) do
610                 scope._NAME = k
611                 setfenv(v, scope)
612                 v()
613         end
614
615         local function modisort(a,b)
616                 return modi[a].order < modi[b].order
617         end
618
619         for _, v in util.spairs(modi, modisort) do
620                 scope._NAME = v.module
621                 setfenv(v.func, scope)
622                 v.func()
623         end
624
625         return tree
626 end
627
628 function modifier(func, order)
629         context.modifiers[#context.modifiers+1] = {
630                 func = func,
631                 order = order or 0,
632                 module
633                         = getfenv(2)._NAME
634         }
635 end
636
637 function assign(path, clone, title, order)
638         local obj  = node(unpack(path))
639         obj.nodes  = nil
640         obj.module = nil
641
642         obj.title = title
643         obj.order = order
644
645         setmetatable(obj, {__index = _create_node(clone)})
646
647         return obj
648 end
649
650 function entry(path, target, title, order)
651         local c = node(unpack(path))
652
653         c.target = target
654         c.title  = title
655         c.order  = order
656         c.module = getfenv(2)._NAME
657
658         return c
659 end
660
661 -- enabling the node.
662 function get(...)
663         return _create_node({...})
664 end
665
666 function node(...)
667         local c = _create_node({...})
668
669         c.module = getfenv(2)._NAME
670         c.auto = nil
671
672         return c
673 end
674
675 function lookup(...)
676         local i, path = nil, {}
677         for i = 1, select('#', ...) do
678                 local name, arg = nil, tostring(select(i, ...))
679                 for name in arg:gmatch("[^/]+") do
680                         path[#path+1] = name
681                 end
682         end
683
684         for i = #path, 1, -1 do
685                 local node = context.treecache[table.concat(path, ".", 1, i)]
686                 if node and (i == #path or node.leaf) then
687                         return node, build_url(unpack(path))
688                 end
689         end
690 end
691
692 function _create_node(path)
693         if #path == 0 then
694                 return context.tree
695         end
696
697         local name = table.concat(path, ".")
698         local c = context.treecache[name]
699
700         if not c then
701                 local last = table.remove(path)
702                 local parent = _create_node(path)
703
704                 c = {nodes={}, auto=true}
705                 -- the node is "in request" if the request path matches
706                 -- at least up to the length of the node path
707                 if parent.inreq and context.path[#path+1] == last then
708                   c.inreq = true
709                 end
710                 parent.nodes[last] = c
711                 context.treecache[name] = c
712         end
713         return c
714 end
715
716 -- Subdispatchers --
717
718 function _firstchild()
719    local path = { unpack(context.path) }
720    local name = table.concat(path, ".")
721    local node = context.treecache[name]
722
723    local lowest
724    if node and node.nodes and next(node.nodes) then
725           local k, v
726           for k, v in pairs(node.nodes) do
727                  if not lowest or
728                         (v.order or 100) < (node.nodes[lowest].order or 100)
729                  then
730                         lowest = k
731                  end
732           end
733    end
734
735    assert(lowest ~= nil,
736                   "The requested node contains no childs, unable to redispatch")
737
738    path[#path+1] = lowest
739    dispatch(path)
740 end
741
742 function firstchild()
743    return { type = "firstchild", target = _firstchild }
744 end
745
746 function alias(...)
747         local req = {...}
748         return function(...)
749                 for _, r in ipairs({...}) do
750                         req[#req+1] = r
751                 end
752
753                 dispatch(req)
754         end
755 end
756
757 function rewrite(n, ...)
758         local req = {...}
759         return function(...)
760                 local dispatched = util.clone(context.dispatched)
761
762                 for i=1,n do
763                         table.remove(dispatched, 1)
764                 end
765
766                 for i, r in ipairs(req) do
767                         table.insert(dispatched, i, r)
768                 end
769
770                 for _, r in ipairs({...}) do
771                         dispatched[#dispatched+1] = r
772                 end
773
774                 dispatch(dispatched)
775         end
776 end
777
778
779 local function _call(self, ...)
780         local func = getfenv()[self.name]
781         assert(func ~= nil,
782                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
783
784         assert(type(func) == "function",
785                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
786                'of type "' .. type(func) .. '".')
787
788         if #self.argv > 0 then
789                 return func(unpack(self.argv), ...)
790         else
791                 return func(...)
792         end
793 end
794
795 function call(name, ...)
796         return {type = "call", argv = {...}, name = name, target = _call}
797 end
798
799 function post_on(params, name, ...)
800         return {
801                 type = "call",
802                 post = params,
803                 argv = { ... },
804                 name = name,
805                 target = _call
806         }
807 end
808
809 function post(...)
810         return post_on(true, ...)
811 end
812
813
814 local _template = function(self, ...)
815         require "luci.template".render(self.view)
816 end
817
818 function template(name)
819         return {type = "template", view = name, target = _template}
820 end
821
822
823 local function _cbi(self, ...)
824         local cbi = require "luci.cbi"
825         local tpl = require "luci.template"
826         local http = require "luci.http"
827
828         local config = self.config or {}
829         local maps = cbi.load(self.model, ...)
830
831         local state = nil
832
833         local i, res
834         for i, res in ipairs(maps) do
835                 if util.instanceof(res, cbi.SimpleForm) then
836                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
837                                 % self.model)
838
839                         io.stderr:write("please change %s to use the form() action instead.\n"
840                                 % table.concat(context.request, "/"))
841                 end
842
843                 res.flow = config
844                 local cstate = res:parse()
845                 if cstate and (not state or cstate < state) then
846                         state = cstate
847                 end
848         end
849
850         local function _resolve_path(path)
851                 return type(path) == "table" and build_url(unpack(path)) or path
852         end
853
854         if config.on_valid_to and state and state > 0 and state < 2 then
855                 http.redirect(_resolve_path(config.on_valid_to))
856                 return
857         end
858
859         if config.on_changed_to and state and state > 1 then
860                 http.redirect(_resolve_path(config.on_changed_to))
861                 return
862         end
863
864         if config.on_success_to and state and state > 0 then
865                 http.redirect(_resolve_path(config.on_success_to))
866                 return
867         end
868
869         if config.state_handler then
870                 if not config.state_handler(state, maps) then
871                         return
872                 end
873         end
874
875         http.header("X-CBI-State", state or 0)
876
877         if not config.noheader then
878                 tpl.render("cbi/header", {state = state})
879         end
880
881         local redirect
882         local messages
883         local applymap   = false
884         local pageaction = true
885         local parsechain = { }
886
887         for i, res in ipairs(maps) do
888                 if res.apply_needed and res.parsechain then
889                         local c
890                         for _, c in ipairs(res.parsechain) do
891                                 parsechain[#parsechain+1] = c
892                         end
893                         applymap = true
894                 end
895
896                 if res.redirect then
897                         redirect = redirect or res.redirect
898                 end
899
900                 if res.pageaction == false then
901                         pageaction = false
902                 end
903
904                 if res.message then
905                         messages = messages or { }
906                         messages[#messages+1] = res.message
907                 end
908         end
909
910         for i, res in ipairs(maps) do
911                 res:render({
912                         firstmap   = (i == 1),
913                         applymap   = applymap,
914                         redirect   = redirect,
915                         messages   = messages,
916                         pageaction = pageaction,
917                         parsechain = parsechain
918                 })
919         end
920
921         if not config.nofooter then
922                 tpl.render("cbi/footer", {
923                         flow       = config,
924                         pageaction = pageaction,
925                         redirect   = redirect,
926                         state      = state,
927                         autoapply  = config.autoapply
928                 })
929         end
930 end
931
932 function cbi(model, config)
933         return {
934                 type = "cbi",
935                 post = { ["cbi.submit"] = true },
936                 config = config,
937                 model = model,
938                 target = _cbi
939         }
940 end
941
942
943 local function _arcombine(self, ...)
944         local argv = {...}
945         local target = #argv > 0 and self.targets[2] or self.targets[1]
946         setfenv(target.target, self.env)
947         target:target(unpack(argv))
948 end
949
950 function arcombine(trg1, trg2)
951         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
952 end
953
954
955 local function _form(self, ...)
956         local cbi = require "luci.cbi"
957         local tpl = require "luci.template"
958         local http = require "luci.http"
959
960         local maps = luci.cbi.load(self.model, ...)
961         local state = nil
962
963         local i, res
964         for i, res in ipairs(maps) do
965                 local cstate = res:parse()
966                 if cstate and (not state or cstate < state) then
967                         state = cstate
968                 end
969         end
970
971         http.header("X-CBI-State", state or 0)
972         tpl.render("header")
973         for i, res in ipairs(maps) do
974                 res:render()
975         end
976         tpl.render("footer")
977 end
978
979 function form(model)
980         return {
981                 type = "cbi",
982                 post = { ["cbi.submit"] = true },
983                 model = model,
984                 target = _form
985         }
986 end
987
988 translate = i18n.translate
989
990 -- This function does not actually translate the given argument but
991 -- is used by build/i18n-scan.pl to find translatable entries.
992 function _(text)
993         return text
994 end