9c0bf60b880fe0cfbeb3b47eb728e28a257245b7
[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(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                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
308                    striptags   = util.striptags;
309                    pcdata      = util.pcdata;
310                    media       = media;
311                    theme       = fs.basename(media);
312                    resource    = luci.config.main.resourcebase;
313                    ifattr      = function(...) return _ifattr(...) end;
314                    attr        = function(...) return _ifattr(true, ...) end;
315                 }, {__index=function(table, key)
316                         if key == "controller" then
317                                 return build_url()
318                         elseif key == "REQUEST_URI" then
319                                 return build_url(unpack(ctx.requestpath))
320                         else
321                                 return rawget(table, key) or _G[key]
322                         end
323                 end})
324         end
325
326         track.dependent = (track.dependent ~= false)
327         assert(not track.dependent or not track.auto,
328                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
329                 "has no parent node so the access to this location has been denied.\n" ..
330                 "This is a software bug, please report this message at " ..
331                 "http://luci.subsignal.org/trac/newticket"
332         )
333
334         if track.sysauth then
335                 local sauth = require "luci.sauth"
336
337                 local authen = type(track.sysauth_authenticator) == "function"
338                  and track.sysauth_authenticator
339                  or authenticator[track.sysauth_authenticator]
340
341                 local def  = (type(track.sysauth) == "string") and track.sysauth
342                 local accs = def and {track.sysauth} or track.sysauth
343                 local sess = ctx.authsession
344                 local verifytoken = false
345                 if not sess then
346                         sess = luci.http.getcookie("sysauth")
347                         sess = sess and sess:match("^[a-f0-9]*$")
348                         verifytoken = true
349                 end
350
351                 local sdat = sauth.read(sess)
352                 local user
353
354                 if sdat then
355                         if not verifytoken or ctx.urltoken.stok == sdat.token then
356                                 user = sdat.user
357                         end
358                 else
359                         local eu = http.getenv("HTTP_AUTH_USER")
360                         local ep = http.getenv("HTTP_AUTH_PASS")
361                         if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
362                                 authen = function() return eu end
363                         end
364                 end
365
366                 if not util.contains(accs, user) then
367                         if authen then
368                                 ctx.urltoken.stok = nil
369                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
370                                 if not user or not util.contains(accs, user) then
371                                         return
372                                 else
373                                         local sid = sess or luci.sys.uniqueid(16)
374                                         if not sess then
375                                                 local token = luci.sys.uniqueid(16)
376                                                 sauth.reap()
377                                                 sauth.write(sid, {
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         local func = getfenv()[self.name]
763         assert(func ~= nil,
764                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
765
766         assert(type(func) == "function",
767                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
768                'of type "' .. type(func) .. '".')
769
770         if #self.argv > 0 then
771                 return func(unpack(self.argv), ...)
772         else
773                 return func(...)
774         end
775 end
776
777 --- Create a function-call dispatching target.
778 -- @param       name    Target function of local controller
779 -- @param       ...             Additional parameters passed to the function
780 function call(name, ...)
781         return {type = "call", argv = {...}, name = name, target = _call}
782 end
783
784
785 local _template = function(self, ...)
786         require "luci.template".render(self.view)
787 end
788
789 --- Create a template render dispatching target.
790 -- @param       name    Template to be rendered
791 function template(name)
792         return {type = "template", view = name, target = _template}
793 end
794
795
796 local function _cbi(self, ...)
797         local cbi = require "luci.cbi"
798         local tpl = require "luci.template"
799         local http = require "luci.http"
800
801         local config = self.config or {}
802         local maps = cbi.load(self.model, ...)
803
804         local state = nil
805
806         for i, res in ipairs(maps) do
807                 res.flow = config
808                 local cstate = res:parse()
809                 if cstate and (not state or cstate < state) then
810                         state = cstate
811                 end
812         end
813
814         local function _resolve_path(path)
815                 return type(path) == "table" and build_url(unpack(path)) or path
816         end
817
818         if config.on_valid_to and state and state > 0 and state < 2 then
819                 http.redirect(_resolve_path(config.on_valid_to))
820                 return
821         end
822
823         if config.on_changed_to and state and state > 1 then
824                 http.redirect(_resolve_path(config.on_changed_to))
825                 return
826         end
827
828         if config.on_success_to and state and state > 0 then
829                 http.redirect(_resolve_path(config.on_success_to))
830                 return
831         end
832
833         if config.state_handler then
834                 if not config.state_handler(state, maps) then
835                         return
836                 end
837         end
838
839         http.header("X-CBI-State", state or 0)
840
841         if not config.noheader then
842                 tpl.render("cbi/header", {state = state})
843         end
844
845         local redirect
846         local messages
847         local applymap   = false
848         local pageaction = true
849         local parsechain = { }
850
851         for i, res in ipairs(maps) do
852                 if res.apply_needed and res.parsechain then
853                         local c
854                         for _, c in ipairs(res.parsechain) do
855                                 parsechain[#parsechain+1] = c
856                         end
857                         applymap = true
858                 end
859
860                 if res.redirect then
861                         redirect = redirect or res.redirect
862                 end
863
864                 if res.pageaction == false then
865                         pageaction = false
866                 end
867
868                 if res.message then
869                         messages = messages or { }
870                         messages[#messages+1] = res.message
871                 end
872         end
873
874         for i, res in ipairs(maps) do
875                 res:render({
876                         firstmap   = (i == 1),
877                         applymap   = applymap,
878                         redirect   = redirect,
879                         messages   = messages,
880                         pageaction = pageaction,
881                         parsechain = parsechain
882                 })
883         end
884
885         if not config.nofooter then
886                 tpl.render("cbi/footer", {
887                         flow       = config,
888                         pageaction = pageaction,
889                         redirect   = redirect,
890                         state      = state,
891                         autoapply  = config.autoapply
892                 })
893         end
894 end
895
896 --- Create a CBI model dispatching target.
897 -- @param       model   CBI model to be rendered
898 function cbi(model, config)
899         return {type = "cbi", config = config, model = model, target = _cbi}
900 end
901
902
903 local function _arcombine(self, ...)
904         local argv = {...}
905         local target = #argv > 0 and self.targets[2] or self.targets[1]
906         setfenv(target.target, self.env)
907         target:target(unpack(argv))
908 end
909
910 --- Create a combined dispatching target for non argv and argv requests.
911 -- @param trg1  Overview Target
912 -- @param trg2  Detail Target
913 function arcombine(trg1, trg2)
914         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
915 end
916
917
918 local function _form(self, ...)
919         local cbi = require "luci.cbi"
920         local tpl = require "luci.template"
921         local http = require "luci.http"
922
923         local maps = luci.cbi.load(self.model, ...)
924         local state = nil
925
926         for i, res in ipairs(maps) do
927                 local cstate = res:parse()
928                 if cstate and (not state or cstate < state) then
929                         state = cstate
930                 end
931         end
932
933         http.header("X-CBI-State", state or 0)
934         tpl.render("header")
935         for i, res in ipairs(maps) do
936                 res:render()
937         end
938         tpl.render("footer")
939 end
940
941 --- Create a CBI form model dispatching target.
942 -- @param       model   CBI form model tpo be rendered
943 function form(model)
944         return {type = "cbi", model = model, target = _form}
945 end
946
947 --- Access the luci.i18n translate() api.
948 -- @class  function
949 -- @name   translate
950 -- @param  text    Text to translate
951 translate = i18n.translate
952
953 --- No-op function used to mark translation entries for menu labels.
954 -- This function does not actually translate the given argument but
955 -- is used by build/i18n-scan.pl to find translatable entries.
956 function _(text)
957         return text
958 end