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