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