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