libs/web: fix possible dispatcher crash
[project/luci.git] / libs / web / luasrc / dispatcher.lua
1 --[[
2 LuCI - Dispatcher
3
4 Description:
5 The request dispatcher and module dispatcher generators
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17         http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27 --- LuCI web dispatcher.
28 local fs = require "nixio.fs"
29 local sys = require "luci.sys"
30 local init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33 local nixio = require "nixio", require "nixio.util"
34
35 module("luci.dispatcher", package.seeall)
36 context = util.threadlocal()
37 uci = require "luci.model.uci"
38 i18n = require "luci.i18n"
39 _M.fs = fs
40
41 authenticator = {}
42
43 -- Index table
44 local index = nil
45
46 -- Fastindex
47 local fi
48
49
50 --- Build the URL relative to the server webroot from given virtual path.
51 -- @param ...   Virtual path
52 -- @return              Relative URL
53 function build_url(...)
54         local path = {...}
55         local url = { http.getenv("SCRIPT_NAME") or "" }
56
57         local k, v
58         for k, v in pairs(context.urltoken) do
59                 url[#url+1] = "/;"
60                 url[#url+1] = http.urlencode(k)
61                 url[#url+1] = "="
62                 url[#url+1] = http.urlencode(v)
63         end
64
65         local p
66         for _, p in ipairs(path) do
67                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
68                         url[#url+1] = "/"
69                         url[#url+1] = p
70                 end
71         end
72
73         return table.concat(url, "")
74 end
75
76 --- Check whether a dispatch node shall be visible
77 -- @param node  Dispatch node
78 -- @return              Boolean indicating whether the node should be visible
79 function node_visible(node)
80    if node then
81           return not (
82                  (not node.title or #node.title == 0) or
83                  (not node.target or node.hidden == true) or
84                  (type(node.target) == "table" and node.target.type == "firstchild" and
85                   (type(node.nodes) ~= "table" or not next(node.nodes)))
86           )
87    end
88    return false
89 end
90
91 --- Return a sorted table of visible childs within a given node
92 -- @param node  Dispatch node
93 -- @return              Ordered table of child node names
94 function node_childs(node)
95         local rv = { }
96         if node then
97                 local k, v
98                 for k, v in util.spairs(node.nodes,
99                         function(a, b)
100                                 return (node.nodes[a].order or 100)
101                                      < (node.nodes[b].order or 100)
102                         end)
103                 do
104                         if node_visible(v) then
105                                 rv[#rv+1] = k
106                         end
107                 end
108         end
109         return rv
110 end
111
112
113 --- Send a 404 error code and render the "error404" template if available.
114 -- @param message       Custom error message (optional)
115 -- @return                      false
116 function error404(message)
117         luci.http.status(404, "Not Found")
118         message = message or "Not Found"
119
120         require("luci.template")
121         if not luci.util.copcall(luci.template.render, "error404") then
122                 luci.http.prepare_content("text/plain")
123                 luci.http.write(message)
124         end
125         return false
126 end
127
128 --- Send a 500 error code and render the "error500" template if available.
129 -- @param message       Custom error message (optional)#
130 -- @return                      false
131 function error500(message)
132         luci.util.perror(message)
133         if not context.template_header_sent then
134                 luci.http.status(500, "Internal Server Error")
135                 luci.http.prepare_content("text/plain")
136                 luci.http.write(message)
137         else
138                 require("luci.template")
139                 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
140                         luci.http.prepare_content("text/plain")
141                         luci.http.write(message)
142                 end
143         end
144         return false
145 end
146
147 function authenticator.htmlauth(validator, accs, default)
148         local user = luci.http.formvalue("username")
149         local pass = luci.http.formvalue("password")
150
151         if user and validator(user, pass) then
152                 return user
153         end
154
155         require("luci.i18n")
156         require("luci.template")
157         context.path = {}
158         luci.template.render("sysauth", {duser=default, fuser=user})
159         return false
160
161 end
162
163 --- Dispatch an HTTP request.
164 -- @param request       LuCI HTTP Request object
165 function httpdispatch(request, prefix)
166         luci.http.context.request = request
167
168         local r = {}
169         context.request = r
170         context.urltoken = {}
171
172         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
173
174         if prefix then
175                 for _, node in ipairs(prefix) do
176                         r[#r+1] = node
177                 end
178         end
179
180         local tokensok = true
181         for node in pathinfo:gmatch("[^/]+") do
182                 local tkey, tval
183                 if tokensok then
184                         tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
185                 end
186                 if tkey then
187                         context.urltoken[tkey] = tval
188                 else
189                         tokensok = false
190                         r[#r+1] = node
191                 end
192         end
193
194         local stat, err = util.coxpcall(function()
195                 dispatch(context.request)
196         end, error500)
197
198         luci.http.close()
199
200         --context._disable_memtrace()
201 end
202
203 --- Dispatches a LuCI virtual path.
204 -- @param request       Virtual path
205 function dispatch(request)
206         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
207         local ctx = context
208         ctx.path = request
209
210         local conf = require "luci.config"
211         assert(conf.main,
212                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
213
214         local lang = conf.main.lang or "auto"
215         if lang == "auto" then
216                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
217                 for lpat in aclang:gmatch("[%w-]+") do
218                         lpat = lpat and lpat:gsub("-", "_")
219                         if conf.languages[lpat] then
220                                 lang = lpat
221                                 break
222                         end
223                 end
224         end
225         require "luci.i18n".setlanguage(lang)
226
227         local c = ctx.tree
228         local stat
229         if not c then
230                 c = createtree()
231         end
232
233         local track = {}
234         local args = {}
235         ctx.args = args
236         ctx.requestargs = ctx.requestargs or args
237         local n
238         local token = ctx.urltoken
239         local preq = {}
240         local freq = {}
241
242         for i, s in ipairs(request) do
243                 preq[#preq+1] = s
244                 freq[#freq+1] = s
245                 c = c.nodes[s]
246                 n = i
247                 if not c then
248                         break
249                 end
250
251                 util.update(track, c)
252
253                 if c.leaf then
254                         break
255                 end
256         end
257
258         if c and c.leaf then
259                 for j=n+1, #request do
260                         args[#args+1] = request[j]
261                         freq[#freq+1] = request[j]
262                 end
263         end
264
265         ctx.requestpath = ctx.requestpath or freq
266         ctx.path = preq
267
268         if track.i18n then
269                 i18n.loadc(track.i18n)
270         end
271
272         -- Init template engine
273         if (c and c.index) or not track.notemplate then
274                 local tpl = require("luci.template")
275                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
276                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
277                         media = nil
278                         for name, theme in pairs(luci.config.themes) do
279                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
280                                  "themes/%s/header" % fs.basename(theme)) then
281                                         media = theme
282                                 end
283                         end
284                         assert(media, "No valid theme found")
285                 end
286
287                 local function ifattr(cond, key, val)
288                         if cond then
289                                 local env = getfenv(1)
290                                 return string.format(
291                                         ' %s="%s"', tostring(key),
292                                         luci.util.pcdata(tostring( val
293                                          or (type(env[key]) ~= "function" and env[key])
294                                          or "" ))
295                                 )
296                         else
297                                 return ''
298                         end
299                 end
300
301                 tpl.context.viewns = setmetatable({
302                    write       = luci.http.write;
303                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
304                    translate   = i18n.translate;
305                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
306                    striptags   = util.striptags;
307                    pcdata      = util.pcdata;
308                    media       = media;
309                    theme       = fs.basename(media);
310                    resource    = luci.config.main.resourcebase;
311                    ifattr      = ifattr;
312                    attr        = function(...) return ifattr(true, ...) end
313                 }, {__index=function(table, key)
314                         if key == "controller" then
315                                 return build_url()
316                         elseif key == "REQUEST_URI" then
317                                 return build_url(unpack(ctx.requestpath))
318                         else
319                                 return rawget(table, key) or _G[key]
320                         end
321                 end})
322         end
323
324         track.dependent = (track.dependent ~= false)
325         assert(not track.dependent or not track.auto,
326                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
327                 "has no parent node so the access to this location has been denied.\n" ..
328                 "This is a software bug, please report this message at " ..
329                 "http://luci.subsignal.org/trac/newticket"
330         )
331
332         if track.sysauth then
333                 local sauth = require "luci.sauth"
334
335                 local authen = type(track.sysauth_authenticator) == "function"
336                  and track.sysauth_authenticator
337                  or authenticator[track.sysauth_authenticator]
338
339                 local def  = (type(track.sysauth) == "string") and track.sysauth
340                 local accs = def and {track.sysauth} or track.sysauth
341                 local sess = ctx.authsession
342                 local verifytoken = false
343                 if not sess then
344                         sess = luci.http.getcookie("sysauth")
345                         sess = sess and sess:match("^[a-f0-9]*$")
346                         verifytoken = true
347                 end
348
349                 local sdat = sauth.read(sess)
350                 local user
351
352                 if sdat then
353                         sdat = loadstring(sdat)
354                         setfenv(sdat, {})
355                         sdat = sdat()
356                         if not verifytoken or ctx.urltoken.stok == sdat.token then
357                                 user = sdat.user
358                         end
359                 else
360                         local eu = http.getenv("HTTP_AUTH_USER")
361                         local ep = http.getenv("HTTP_AUTH_PASS")
362                         if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
363                                 authen = function() return eu end
364                         end
365                 end
366
367                 if not util.contains(accs, user) then
368                         if authen then
369                                 ctx.urltoken.stok = nil
370                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
371                                 if not user or not util.contains(accs, user) then
372                                         return
373                                 else
374                                         local sid = sess or luci.sys.uniqueid(16)
375                                         if not sess then
376                                                 local token = luci.sys.uniqueid(16)
377                                                 sauth.write(sid, util.get_bytecode({
378                                                         user=user,
379                                                         token=token,
380                                                         secret=luci.sys.uniqueid(16)
381                                                 }))
382                                                 ctx.urltoken.stok = token
383                                         end
384                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
385                                         ctx.authsession = sid
386                                         ctx.authuser = user
387                                 end
388                         else
389                                 luci.http.status(403, "Forbidden")
390                                 return
391                         end
392                 else
393                         ctx.authsession = sess
394                         ctx.authuser = user
395                 end
396         end
397
398         if track.setgroup then
399                 luci.sys.process.setgroup(track.setgroup)
400         end
401
402         if track.setuser then
403                 luci.sys.process.setuser(track.setuser)
404         end
405
406         local target = nil
407         if c then
408                 if type(c.target) == "function" then
409                         target = c.target
410                 elseif type(c.target) == "table" then
411                         target = c.target.target
412                 end
413         end
414
415         if c and (c.index or type(target) == "function") then
416                 ctx.dispatched = c
417                 ctx.requested = ctx.requested or ctx.dispatched
418         end
419
420         if c and c.index then
421                 local tpl = require "luci.template"
422
423                 if util.copcall(tpl.render, "indexer", {}) then
424                         return true
425                 end
426         end
427
428         if type(target) == "function" then
429                 util.copcall(function()
430                         local oldenv = getfenv(target)
431                         local module = require(c.module)
432                         local env = setmetatable({}, {__index=
433
434                         function(tbl, key)
435                                 return rawget(tbl, key) or module[key] or oldenv[key]
436                         end})
437
438                         setfenv(target, env)
439                 end)
440
441                 local ok, err
442                 if type(c.target) == "table" then
443                         ok, err = util.copcall(target, c.target, unpack(args))
444                 else
445                         ok, err = util.copcall(target, unpack(args))
446                 end
447                 assert(ok,
448                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
449                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
450                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
451         else
452                 local root = node()
453                 if not root or not root.target then
454                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
455                                  "Install luci-mod-admin-full and retry. " ..
456                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
457                 else
458                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
459                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
460                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
461                 end
462         end
463 end
464
465 --- Generate the dispatching index using the best possible strategy.
466 function createindex()
467         local path = luci.util.libpath() .. "/controller/"
468         local suff = { ".lua", ".lua.gz" }
469
470         if luci.util.copcall(require, "luci.fastindex") then
471                 createindex_fastindex(path, suff)
472         else
473                 createindex_plain(path, suff)
474         end
475 end
476
477 --- Generate the dispatching index using the fastindex C-indexer.
478 -- @param path          Controller base directory
479 -- @param suffixes      Controller file suffixes
480 function createindex_fastindex(path, suffixes)
481         index = {}
482
483         if not fi then
484                 fi = luci.fastindex.new("index")
485                 for _, suffix in ipairs(suffixes) do
486                         fi.add(path .. "*" .. suffix)
487                         fi.add(path .. "*/*" .. suffix)
488                 end
489         end
490         fi.scan()
491
492         for k, v in pairs(fi.indexes) do
493                 index[v[2]] = v[1]
494         end
495 end
496
497 --- Generate the dispatching index using the native file-cache based strategy.
498 -- @param path          Controller base directory
499 -- @param suffixes      Controller file suffixes
500 function createindex_plain(path, suffixes)
501         local controllers = { }
502         for _, suffix in ipairs(suffixes) do
503                 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
504                 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
505         end
506
507         if indexcache then
508                 local cachedate = fs.stat(indexcache, "mtime")
509                 if cachedate then
510                         local realdate = 0
511                         for _, obj in ipairs(controllers) do
512                                 local omtime = fs.stat(obj, "mtime")
513                                 realdate = (omtime and omtime > realdate) and omtime or realdate
514                         end
515
516                         if cachedate > realdate then
517                                 assert(
518                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
519                                         and fs.stat(indexcache, "modestr") == "rw-------",
520                                         "Fatal: Indexcache is not sane!"
521                                 )
522
523                                 index = loadfile(indexcache)()
524                                 return index
525                         end
526                 end
527         end
528
529         index = {}
530
531         for i,c in ipairs(controllers) do
532                 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
533                 for _, suffix in ipairs(suffixes) do
534                         modname = modname:gsub(suffix.."$", "")
535                 end
536
537                 local mod = require(modname)
538                 assert(mod ~= true,
539                        "Invalid controller file found\n" ..
540                        "The file '" .. c .. "' contains an invalid module line.\n" ..
541                        "Please verify whether the module name is set to '" .. modname ..
542                        "' - It must correspond to the file path!")
543
544                 local idx = mod.index
545                 assert(type(idx) == "function",
546                        "Invalid controller file found\n" ..
547                        "The file '" .. c .. "' contains no index() function.\n" ..
548                        "Please make sure that the controller contains a valid " ..
549                        "index function and verify the spelling!")
550
551                 index[modname] = idx
552         end
553
554         if indexcache then
555                 local f = nixio.open(indexcache, "w", 600)
556                 f:writeall(util.get_bytecode(index))
557                 f:close()
558         end
559 end
560
561 --- Create the dispatching tree from the index.
562 -- Build the index before if it does not exist yet.
563 function createtree()
564         if not index then
565                 createindex()
566         end
567
568         local ctx  = context
569         local tree = {nodes={}, inreq=true}
570         local modi = {}
571
572         ctx.treecache = setmetatable({}, {__mode="v"})
573         ctx.tree = tree
574         ctx.modifiers = modi
575
576         -- Load default translation
577         require "luci.i18n".loadc("base")
578
579         local scope = setmetatable({}, {__index = luci.dispatcher})
580
581         for k, v in pairs(index) do
582                 scope._NAME = k
583                 setfenv(v, scope)
584                 v()
585         end
586
587         local function modisort(a,b)
588                 return modi[a].order < modi[b].order
589         end
590
591         for _, v in util.spairs(modi, modisort) do
592                 scope._NAME = v.module
593                 setfenv(v.func, scope)
594                 v.func()
595         end
596
597         return tree
598 end
599
600 --- Register a tree modifier.
601 -- @param       func    Modifier function
602 -- @param       order   Modifier order value (optional)
603 function modifier(func, order)
604         context.modifiers[#context.modifiers+1] = {
605                 func = func,
606                 order = order or 0,
607                 module
608                         = getfenv(2)._NAME
609         }
610 end
611
612 --- Clone a node of the dispatching tree to another position.
613 -- @param       path    Virtual path destination
614 -- @param       clone   Virtual path source
615 -- @param       title   Destination node title (optional)
616 -- @param       order   Destination node order value (optional)
617 -- @return                      Dispatching tree node
618 function assign(path, clone, title, order)
619         local obj  = node(unpack(path))
620         obj.nodes  = nil
621         obj.module = nil
622
623         obj.title = title
624         obj.order = order
625
626         setmetatable(obj, {__index = _create_node(clone)})
627
628         return obj
629 end
630
631 --- Create a new dispatching node and define common parameters.
632 -- @param       path    Virtual path
633 -- @param       target  Target function to call when dispatched.
634 -- @param       title   Destination node title
635 -- @param       order   Destination node order value (optional)
636 -- @return                      Dispatching tree node
637 function entry(path, target, title, order)
638         local c = node(unpack(path))
639
640         c.target = target
641         c.title  = title
642         c.order  = order
643         c.module = getfenv(2)._NAME
644
645         return c
646 end
647
648 --- Fetch or create a dispatching node without setting the target module or
649 -- enabling the node.
650 -- @param       ...             Virtual path
651 -- @return                      Dispatching tree node
652 function get(...)
653         return _create_node({...})
654 end
655
656 --- Fetch or create a new dispatching node.
657 -- @param       ...             Virtual path
658 -- @return                      Dispatching tree node
659 function node(...)
660         local c = _create_node({...})
661
662         c.module = getfenv(2)._NAME
663         c.auto = nil
664
665         return c
666 end
667
668 function _create_node(path)
669         if #path == 0 then
670                 return context.tree
671         end
672
673         local name = table.concat(path, ".")
674         local c = context.treecache[name]
675
676         if not c then
677                 local last = table.remove(path)
678                 local parent = _create_node(path)
679
680                 c = {nodes={}, auto=true}
681                 -- the node is "in request" if the request path matches
682                 -- at least up to the length of the node path
683                 if parent.inreq and context.path[#path+1] == last then
684                   c.inreq = true
685                 end
686                 parent.nodes[last] = c
687                 context.treecache[name] = c
688         end
689         return c
690 end
691
692 -- Subdispatchers --
693
694 function _firstchild()
695    local path = { unpack(context.path) }
696    local name = table.concat(path, ".")
697    local node = context.treecache[name]
698
699    local lowest
700    if node and node.nodes and next(node.nodes) then
701           local k, v
702           for k, v in pairs(node.nodes) do
703                  if not lowest or
704                         (v.order or 100) < (node.nodes[lowest].order or 100)
705                  then
706                         lowest = k
707                  end
708           end
709    end
710
711    assert(lowest ~= nil,
712                   "The requested node contains no childs, unable to redispatch")
713
714    path[#path+1] = lowest
715    dispatch(path)
716 end
717
718 --- Alias the first (lowest order) page automatically
719 function firstchild()
720    return { type = "firstchild", target = _firstchild }
721 end
722
723 --- Create a redirect to another dispatching node.
724 -- @param       ...             Virtual path destination
725 function alias(...)
726         local req = {...}
727         return function(...)
728                 for _, r in ipairs({...}) do
729                         req[#req+1] = r
730                 end
731
732                 dispatch(req)
733         end
734 end
735
736 --- Rewrite the first x path values of the request.
737 -- @param       n               Number of path values to replace
738 -- @param       ...             Virtual path to replace removed path values with
739 function rewrite(n, ...)
740         local req = {...}
741         return function(...)
742                 local dispatched = util.clone(context.dispatched)
743
744                 for i=1,n do
745                         table.remove(dispatched, 1)
746                 end
747
748                 for i, r in ipairs(req) do
749                         table.insert(dispatched, i, r)
750                 end
751
752                 for _, r in ipairs({...}) do
753                         dispatched[#dispatched+1] = r
754                 end
755
756                 dispatch(dispatched)
757         end
758 end
759
760
761 local function _call(self, ...)
762         if #self.argv > 0 then
763                 return getfenv()[self.name](unpack(self.argv), ...)
764         else
765                 return getfenv()[self.name](...)
766         end
767 end
768
769 --- Create a function-call dispatching target.
770 -- @param       name    Target function of local controller
771 -- @param       ...             Additional parameters passed to the function
772 function call(name, ...)
773         return {type = "call", argv = {...}, name = name, target = _call}
774 end
775
776
777 local _template = function(self, ...)
778         require "luci.template".render(self.view)
779 end
780
781 --- Create a template render dispatching target.
782 -- @param       name    Template to be rendered
783 function template(name)
784         return {type = "template", view = name, target = _template}
785 end
786
787
788 local function _cbi(self, ...)
789         local cbi = require "luci.cbi"
790         local tpl = require "luci.template"
791         local http = require "luci.http"
792
793         local config = self.config or {}
794         local maps = cbi.load(self.model, ...)
795
796         local state = nil
797
798         for i, res in ipairs(maps) do
799                 res.flow = config
800                 local cstate = res:parse()
801                 if cstate and (not state or cstate < state) then
802                         state = cstate
803                 end
804         end
805
806         local function _resolve_path(path)
807                 return type(path) == "table" and build_url(unpack(path)) or path
808         end
809
810         if config.on_valid_to and state and state > 0 and state < 2 then
811                 http.redirect(_resolve_path(config.on_valid_to))
812                 return
813         end
814
815         if config.on_changed_to and state and state > 1 then
816                 http.redirect(_resolve_path(config.on_changed_to))
817                 return
818         end
819
820         if config.on_success_to and state and state > 0 then
821                 http.redirect(_resolve_path(config.on_success_to))
822                 return
823         end
824
825         if config.state_handler then
826                 if not config.state_handler(state, maps) then
827                         return
828                 end
829         end
830
831         http.header("X-CBI-State", state or 0)
832
833         if not config.noheader then
834                 tpl.render("cbi/header", {state = state})
835         end
836
837         local redirect
838         local messages
839         local applymap   = false
840         local pageaction = true
841         local parsechain = { }
842
843         for i, res in ipairs(maps) do
844                 if res.apply_needed and res.parsechain then
845                         local c
846                         for _, c in ipairs(res.parsechain) do
847                                 parsechain[#parsechain+1] = c
848                         end
849                         applymap = true
850                 end
851
852                 if res.redirect then
853                         redirect = redirect or res.redirect
854                 end
855
856                 if res.pageaction == false then
857                         pageaction = false
858                 end
859
860                 if res.message then
861                         messages = messages or { }
862                         messages[#messages+1] = res.message
863                 end
864         end
865
866         for i, res in ipairs(maps) do
867                 res:render({
868                         firstmap   = (i == 1),
869                         applymap   = applymap,
870                         redirect   = redirect,
871                         messages   = messages,
872                         pageaction = pageaction,
873                         parsechain = parsechain
874                 })
875         end
876
877         if not config.nofooter then
878                 tpl.render("cbi/footer", {
879                         flow       = config,
880                         pageaction = pageaction,
881                         redirect   = redirect,
882                         state      = state,
883                         autoapply  = config.autoapply
884                 })
885         end
886 end
887
888 --- Create a CBI model dispatching target.
889 -- @param       model   CBI model to be rendered
890 function cbi(model, config)
891         return {type = "cbi", config = config, model = model, target = _cbi}
892 end
893
894
895 local function _arcombine(self, ...)
896         local argv = {...}
897         local target = #argv > 0 and self.targets[2] or self.targets[1]
898         setfenv(target.target, self.env)
899         target:target(unpack(argv))
900 end
901
902 --- Create a combined dispatching target for non argv and argv requests.
903 -- @param trg1  Overview Target
904 -- @param trg2  Detail Target
905 function arcombine(trg1, trg2)
906         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
907 end
908
909
910 local function _form(self, ...)
911         local cbi = require "luci.cbi"
912         local tpl = require "luci.template"
913         local http = require "luci.http"
914
915         local maps = luci.cbi.load(self.model, ...)
916         local state = nil
917
918         for i, res in ipairs(maps) do
919                 local cstate = res:parse()
920                 if cstate and (not state or cstate < state) then
921                         state = cstate
922                 end
923         end
924
925         http.header("X-CBI-State", state or 0)
926         tpl.render("header")
927         for i, res in ipairs(maps) do
928                 res:render()
929         end
930         tpl.render("footer")
931 end
932
933 --- Create a CBI form model dispatching target.
934 -- @param       model   CBI form model tpo be rendered
935 function form(model)
936         return {type = "cbi", model = model, target = _form}
937 end
938
939 --- Access the luci.i18n translate() api.
940 -- @class  function
941 -- @name   translate
942 -- @param  text    Text to translate
943 translate = i18n.translate
944
945 --- No-op function used to mark translation entries for menu labels.
946 -- This function does not actually translate the given argument but
947 -- is used by build/i18n-scan.pl to find translatable entries.
948 function _(text)
949         return text
950 end