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