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