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