9e5b78d5e9f93b3bf6737f53fdebe65924697798
[project/luci.git] / modules / base / luasrc / 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(3)
290                                 local scope = (type(env.self) == "table") and env.self
291                                 return string.format(
292                                         ' %s="%s"', tostring(key),
293                                         luci.util.pcdata(tostring( val
294                                          or (type(env[key]) ~= "function" and env[key])
295                                          or (scope and type(scope[key]) ~= "function" and scope[key])
296                                          or "" ))
297                                 )
298                         else
299                                 return ''
300                         end
301                 end
302
303                 tpl.context.viewns = setmetatable({
304                    write       = luci.http.write;
305                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
306                    translate   = i18n.translate;
307                    translatef  = i18n.translatef;
308                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
309                    striptags   = util.striptags;
310                    pcdata      = util.pcdata;
311                    media       = media;
312                    theme       = fs.basename(media);
313                    resource    = luci.config.main.resourcebase;
314                    ifattr      = function(...) return _ifattr(...) end;
315                    attr        = function(...) return _ifattr(true, ...) end;
316                 }, {__index=function(table, key)
317                         if key == "controller" then
318                                 return build_url()
319                         elseif key == "REQUEST_URI" then
320                                 return build_url(unpack(ctx.requestpath))
321                         else
322                                 return rawget(table, key) or _G[key]
323                         end
324                 end})
325         end
326
327         track.dependent = (track.dependent ~= false)
328         assert(not track.dependent or not track.auto,
329                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
330                 "has no parent node so the access to this location has been denied.\n" ..
331                 "This is a software bug, please report this message at " ..
332                 "http://luci.subsignal.org/trac/newticket"
333         )
334
335         if track.sysauth then
336                 local sauth = require "luci.sauth"
337
338                 local authen = type(track.sysauth_authenticator) == "function"
339                  and track.sysauth_authenticator
340                  or authenticator[track.sysauth_authenticator]
341
342                 local def  = (type(track.sysauth) == "string") and track.sysauth
343                 local accs = def and {track.sysauth} or track.sysauth
344                 local sess = ctx.authsession
345                 local verifytoken = false
346                 if not sess then
347                         sess = luci.http.getcookie("sysauth")
348                         sess = sess and sess:match("^[a-f0-9]*$")
349                         verifytoken = true
350                 end
351
352                 local sdat = sauth.read(sess)
353                 local user
354
355                 if sdat then
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.reap()
378                                                 sauth.write(sid, {
379                                                         user=user,
380                                                         token=token,
381                                                         secret=luci.sys.uniqueid(16)
382                                                 })
383                                                 ctx.urltoken.stok = token
384                                         end
385                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
386                                         ctx.authsession = sid
387                                         ctx.authuser = user
388                                 end
389                         else
390                                 luci.http.status(403, "Forbidden")
391                                 return
392                         end
393                 else
394                         ctx.authsession = sess
395                         ctx.authuser = user
396                 end
397         end
398
399         if track.setgroup then
400                 luci.sys.process.setgroup(track.setgroup)
401         end
402
403         if track.setuser then
404                 luci.sys.process.setuser(track.setuser)
405         end
406
407         local target = nil
408         if c then
409                 if type(c.target) == "function" then
410                         target = c.target
411                 elseif type(c.target) == "table" then
412                         target = c.target.target
413                 end
414         end
415
416         if c and (c.index or type(target) == "function") then
417                 ctx.dispatched = c
418                 ctx.requested = ctx.requested or ctx.dispatched
419         end
420
421         if c and c.index then
422                 local tpl = require "luci.template"
423
424                 if util.copcall(tpl.render, "indexer", {}) then
425                         return true
426                 end
427         end
428
429         if type(target) == "function" then
430                 util.copcall(function()
431                         local oldenv = getfenv(target)
432                         local module = require(c.module)
433                         local env = setmetatable({}, {__index=
434
435                         function(tbl, key)
436                                 return rawget(tbl, key) or module[key] or oldenv[key]
437                         end})
438
439                         setfenv(target, env)
440                 end)
441
442                 local ok, err
443                 if type(c.target) == "table" then
444                         ok, err = util.copcall(target, c.target, unpack(args))
445                 else
446                         ok, err = util.copcall(target, unpack(args))
447                 end
448                 assert(ok,
449                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
450                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
451                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
452         else
453                 local root = node()
454                 if not root or not root.target then
455                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
456                                  "Install luci-mod-admin-full and retry. " ..
457                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
458                 else
459                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
460                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
461                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
462                 end
463         end
464 end
465
466 --- Generate the dispatching index using the best possible strategy.
467 function createindex()
468         local path = luci.util.libpath() .. "/controller/"
469         local suff = { ".lua", ".lua.gz" }
470
471         if luci.util.copcall(require, "luci.fastindex") then
472                 createindex_fastindex(path, suff)
473         else
474                 createindex_plain(path, suff)
475         end
476 end
477
478 --- Generate the dispatching index using the fastindex C-indexer.
479 -- @param path          Controller base directory
480 -- @param suffixes      Controller file suffixes
481 function createindex_fastindex(path, suffixes)
482         index = {}
483
484         if not fi then
485                 fi = luci.fastindex.new("index")
486                 for _, suffix in ipairs(suffixes) do
487                         fi.add(path .. "*" .. suffix)
488                         fi.add(path .. "*/*" .. suffix)
489                 end
490         end
491         fi.scan()
492
493         for k, v in pairs(fi.indexes) do
494                 index[v[2]] = v[1]
495         end
496 end
497
498 --- Generate the dispatching index using the native file-cache based strategy.
499 -- @param path          Controller base directory
500 -- @param suffixes      Controller file suffixes
501 function createindex_plain(path, suffixes)
502         local controllers = { }
503         for _, suffix in ipairs(suffixes) do
504                 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
505                 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
506         end
507
508         if indexcache then
509                 local cachedate = fs.stat(indexcache, "mtime")
510                 if cachedate then
511                         local realdate = 0
512                         for _, obj in ipairs(controllers) do
513                                 local omtime = fs.stat(obj, "mtime")
514                                 realdate = (omtime and omtime > realdate) and omtime or realdate
515                         end
516
517                         if cachedate > realdate then
518                                 assert(
519                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
520                                         and fs.stat(indexcache, "modestr") == "rw-------",
521                                         "Fatal: Indexcache is not sane!"
522                                 )
523
524                                 index = loadfile(indexcache)()
525                                 return index
526                         end
527                 end
528         end
529
530         index = {}
531
532         for i,c in ipairs(controllers) do
533                 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
534                 for _, suffix in ipairs(suffixes) do
535                         modname = modname:gsub(suffix.."$", "")
536                 end
537
538                 local mod = require(modname)
539                 assert(mod ~= true,
540                        "Invalid controller file found\n" ..
541                        "The file '" .. c .. "' contains an invalid module line.\n" ..
542                        "Please verify whether the module name is set to '" .. modname ..
543                        "' - It must correspond to the file path!")
544
545                 local idx = mod.index
546                 assert(type(idx) == "function",
547                        "Invalid controller file found\n" ..
548                        "The file '" .. c .. "' contains no index() function.\n" ..
549                        "Please make sure that the controller contains a valid " ..
550                        "index function and verify the spelling!")
551
552                 index[modname] = idx
553         end
554
555         if indexcache then
556                 local f = nixio.open(indexcache, "w", 600)
557                 f:writeall(util.get_bytecode(index))
558                 f:close()
559         end
560 end
561
562 --- Create the dispatching tree from the index.
563 -- Build the index before if it does not exist yet.
564 function createtree()
565         if not index then
566                 createindex()
567         end
568
569         local ctx  = context
570         local tree = {nodes={}, inreq=true}
571         local modi = {}
572
573         ctx.treecache = setmetatable({}, {__mode="v"})
574         ctx.tree = tree
575         ctx.modifiers = modi
576
577         -- Load default translation
578         require "luci.i18n".loadc("base")
579
580         local scope = setmetatable({}, {__index = luci.dispatcher})
581
582         for k, v in pairs(index) do
583                 scope._NAME = k
584                 setfenv(v, scope)
585                 v()
586         end
587
588         local function modisort(a,b)
589                 return modi[a].order < modi[b].order
590         end
591
592         for _, v in util.spairs(modi, modisort) do
593                 scope._NAME = v.module
594                 setfenv(v.func, scope)
595                 v.func()
596         end
597
598         return tree
599 end
600
601 --- Register a tree modifier.
602 -- @param       func    Modifier function
603 -- @param       order   Modifier order value (optional)
604 function modifier(func, order)
605         context.modifiers[#context.modifiers+1] = {
606                 func = func,
607                 order = order or 0,
608                 module
609                         = getfenv(2)._NAME
610         }
611 end
612
613 --- Clone a node of the dispatching tree to another position.
614 -- @param       path    Virtual path destination
615 -- @param       clone   Virtual path source
616 -- @param       title   Destination node title (optional)
617 -- @param       order   Destination node order value (optional)
618 -- @return                      Dispatching tree node
619 function assign(path, clone, title, order)
620         local obj  = node(unpack(path))
621         obj.nodes  = nil
622         obj.module = nil
623
624         obj.title = title
625         obj.order = order
626
627         setmetatable(obj, {__index = _create_node(clone)})
628
629         return obj
630 end
631
632 --- Create a new dispatching node and define common parameters.
633 -- @param       path    Virtual path
634 -- @param       target  Target function to call when dispatched.
635 -- @param       title   Destination node title
636 -- @param       order   Destination node order value (optional)
637 -- @return                      Dispatching tree node
638 function entry(path, target, title, order)
639         local c = node(unpack(path))
640
641         c.target = target
642         c.title  = title
643         c.order  = order
644         c.module = getfenv(2)._NAME
645
646         return c
647 end
648
649 --- Fetch or create a dispatching node without setting the target module or
650 -- enabling the node.
651 -- @param       ...             Virtual path
652 -- @return                      Dispatching tree node
653 function get(...)
654         return _create_node({...})
655 end
656
657 --- Fetch or create a new dispatching node.
658 -- @param       ...             Virtual path
659 -- @return                      Dispatching tree node
660 function node(...)
661         local c = _create_node({...})
662
663         c.module = getfenv(2)._NAME
664         c.auto = nil
665
666         return c
667 end
668
669 function _create_node(path)
670         if #path == 0 then
671                 return context.tree
672         end
673
674         local name = table.concat(path, ".")
675         local c = context.treecache[name]
676
677         if not c then
678                 local last = table.remove(path)
679                 local parent = _create_node(path)
680
681                 c = {nodes={}, auto=true}
682                 -- the node is "in request" if the request path matches
683                 -- at least up to the length of the node path
684                 if parent.inreq and context.path[#path+1] == last then
685                   c.inreq = true
686                 end
687                 parent.nodes[last] = c
688                 context.treecache[name] = c
689         end
690         return c
691 end
692
693 -- Subdispatchers --
694
695 function _firstchild()
696    local path = { unpack(context.path) }
697    local name = table.concat(path, ".")
698    local node = context.treecache[name]
699
700    local lowest
701    if node and node.nodes and next(node.nodes) then
702           local k, v
703           for k, v in pairs(node.nodes) do
704                  if not lowest or
705                         (v.order or 100) < (node.nodes[lowest].order or 100)
706                  then
707                         lowest = k
708                  end
709           end
710    end
711
712    assert(lowest ~= nil,
713                   "The requested node contains no childs, unable to redispatch")
714
715    path[#path+1] = lowest
716    dispatch(path)
717 end
718
719 --- Alias the first (lowest order) page automatically
720 function firstchild()
721    return { type = "firstchild", target = _firstchild }
722 end
723
724 --- Create a redirect to another dispatching node.
725 -- @param       ...             Virtual path destination
726 function alias(...)
727         local req = {...}
728         return function(...)
729                 for _, r in ipairs({...}) do
730                         req[#req+1] = r
731                 end
732
733                 dispatch(req)
734         end
735 end
736
737 --- Rewrite the first x path values of the request.
738 -- @param       n               Number of path values to replace
739 -- @param       ...             Virtual path to replace removed path values with
740 function rewrite(n, ...)
741         local req = {...}
742         return function(...)
743                 local dispatched = util.clone(context.dispatched)
744
745                 for i=1,n do
746                         table.remove(dispatched, 1)
747                 end
748
749                 for i, r in ipairs(req) do
750                         table.insert(dispatched, i, r)
751                 end
752
753                 for _, r in ipairs({...}) do
754                         dispatched[#dispatched+1] = r
755                 end
756
757                 dispatch(dispatched)
758         end
759 end
760
761
762 local function _call(self, ...)
763         local func = getfenv()[self.name]
764         assert(func ~= nil,
765                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
766
767         assert(type(func) == "function",
768                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
769                'of type "' .. type(func) .. '".')
770
771         if #self.argv > 0 then
772                 return func(unpack(self.argv), ...)
773         else
774                 return func(...)
775         end
776 end
777
778 --- Create a function-call dispatching target.
779 -- @param       name    Target function of local controller
780 -- @param       ...             Additional parameters passed to the function
781 function call(name, ...)
782         return {type = "call", argv = {...}, name = name, target = _call}
783 end
784
785
786 local _template = function(self, ...)
787         require "luci.template".render(self.view)
788 end
789
790 --- Create a template render dispatching target.
791 -- @param       name    Template to be rendered
792 function template(name)
793         return {type = "template", view = name, target = _template}
794 end
795
796
797 local function _cbi(self, ...)
798         local cbi = require "luci.cbi"
799         local tpl = require "luci.template"
800         local http = require "luci.http"
801
802         local config = self.config or {}
803         local maps = cbi.load(self.model, ...)
804
805         local state = nil
806
807         for i, res in ipairs(maps) do
808                 res.flow = config
809                 local cstate = res:parse()
810                 if cstate and (not state or cstate < state) then
811                         state = cstate
812                 end
813         end
814
815         local function _resolve_path(path)
816                 return type(path) == "table" and build_url(unpack(path)) or path
817         end
818
819         if config.on_valid_to and state and state > 0 and state < 2 then
820                 http.redirect(_resolve_path(config.on_valid_to))
821                 return
822         end
823
824         if config.on_changed_to and state and state > 1 then
825                 http.redirect(_resolve_path(config.on_changed_to))
826                 return
827         end
828
829         if config.on_success_to and state and state > 0 then
830                 http.redirect(_resolve_path(config.on_success_to))
831                 return
832         end
833
834         if config.state_handler then
835                 if not config.state_handler(state, maps) then
836                         return
837                 end
838         end
839
840         http.header("X-CBI-State", state or 0)
841
842         if not config.noheader then
843                 tpl.render("cbi/header", {state = state})
844         end
845
846         local redirect
847         local messages
848         local applymap   = false
849         local pageaction = true
850         local parsechain = { }
851
852         for i, res in ipairs(maps) do
853                 if res.apply_needed and res.parsechain then
854                         local c
855                         for _, c in ipairs(res.parsechain) do
856                                 parsechain[#parsechain+1] = c
857                         end
858                         applymap = true
859                 end
860
861                 if res.redirect then
862                         redirect = redirect or res.redirect
863                 end
864
865                 if res.pageaction == false then
866                         pageaction = false
867                 end
868
869                 if res.message then
870                         messages = messages or { }
871                         messages[#messages+1] = res.message
872                 end
873         end
874
875         for i, res in ipairs(maps) do
876                 res:render({
877                         firstmap   = (i == 1),
878                         applymap   = applymap,
879                         redirect   = redirect,
880                         messages   = messages,
881                         pageaction = pageaction,
882                         parsechain = parsechain
883                 })
884         end
885
886         if not config.nofooter then
887                 tpl.render("cbi/footer", {
888                         flow       = config,
889                         pageaction = pageaction,
890                         redirect   = redirect,
891                         state      = state,
892                         autoapply  = config.autoapply
893                 })
894         end
895 end
896
897 --- Create a CBI model dispatching target.
898 -- @param       model   CBI model to be rendered
899 function cbi(model, config)
900         return {type = "cbi", config = config, model = model, target = _cbi}
901 end
902
903
904 local function _arcombine(self, ...)
905         local argv = {...}
906         local target = #argv > 0 and self.targets[2] or self.targets[1]
907         setfenv(target.target, self.env)
908         target:target(unpack(argv))
909 end
910
911 --- Create a combined dispatching target for non argv and argv requests.
912 -- @param trg1  Overview Target
913 -- @param trg2  Detail Target
914 function arcombine(trg1, trg2)
915         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
916 end
917
918
919 local function _form(self, ...)
920         local cbi = require "luci.cbi"
921         local tpl = require "luci.template"
922         local http = require "luci.http"
923
924         local maps = luci.cbi.load(self.model, ...)
925         local state = nil
926
927         for i, res in ipairs(maps) do
928                 local cstate = res:parse()
929                 if cstate and (not state or cstate < state) then
930                         state = cstate
931                 end
932         end
933
934         http.header("X-CBI-State", state or 0)
935         tpl.render("header")
936         for i, res in ipairs(maps) do
937                 res:render()
938         end
939         tpl.render("footer")
940 end
941
942 --- Create a CBI form model dispatching target.
943 -- @param       model   CBI form model tpo be rendered
944 function form(model)
945         return {type = "cbi", model = model, target = _form}
946 end
947
948 --- Access the luci.i18n translate() api.
949 -- @class  function
950 -- @name   translate
951 -- @param  text    Text to translate
952 translate = i18n.translate
953
954 --- No-op function used to mark translation entries for menu labels.
955 -- This function does not actually translate the given argument but
956 -- is used by build/i18n-scan.pl to find translatable entries.
957 function _(text)
958         return text
959 end