luci-base: switch to ubus sessions
[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         luci.http.status(404, "Not Found")
118         message = message or "Not Found"
119
120         require("luci.template")
121         if not luci.util.copcall(luci.template.render, "error404") then
122                 luci.http.prepare_content("text/plain")
123                 luci.http.write(message)
124         end
125         return false
126 end
127
128 --- Send a 500 error code and render the "error500" template if available.
129 -- @param message       Custom error message (optional)#
130 -- @return                      false
131 function error500(message)
132         luci.util.perror(message)
133         if not context.template_header_sent then
134                 luci.http.status(500, "Internal Server Error")
135                 luci.http.prepare_content("text/plain")
136                 luci.http.write(message)
137         else
138                 require("luci.template")
139                 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
140                         luci.http.prepare_content("text/plain")
141                         luci.http.write(message)
142                 end
143         end
144         return false
145 end
146
147 function authenticator.htmlauth(validator, accs, default)
148         local user = luci.http.formvalue("luci_username")
149         local pass = luci.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         luci.http.context.request = request
167
168         local r = {}
169         context.request = r
170         context.urltoken = {}
171
172         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
173
174         if prefix then
175                 for _, node in ipairs(prefix) do
176                         r[#r+1] = node
177                 end
178         end
179
180         local tokensok = true
181         for node in pathinfo:gmatch("[^/]+") do
182                 local tkey, tval
183                 if tokensok then
184                         tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
185                 end
186                 if tkey then
187                         context.urltoken[tkey] = tval
188                 else
189                         tokensok = false
190                         r[#r+1] = node
191                 end
192         end
193
194         local stat, err = util.coxpcall(function()
195                 dispatch(context.request)
196         end, error500)
197
198         luci.http.close()
199
200         --context._disable_memtrace()
201 end
202
203 --- Dispatches a LuCI virtual path.
204 -- @param request       Virtual path
205 function dispatch(request)
206         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
207         local ctx = context
208         ctx.path = request
209
210         local conf = require "luci.config"
211         assert(conf.main,
212                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
213
214         local lang = conf.main.lang or "auto"
215         if lang == "auto" then
216                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
217                 for lpat in aclang:gmatch("[%w-]+") do
218                         lpat = lpat and lpat:gsub("-", "_")
219                         if conf.languages[lpat] then
220                                 lang = lpat
221                                 break
222                         end
223                 end
224         end
225         require "luci.i18n".setlanguage(lang)
226
227         local c = ctx.tree
228         local stat
229         if not c then
230                 c = createtree()
231         end
232
233         local track = {}
234         local args = {}
235         ctx.args = args
236         ctx.requestargs = ctx.requestargs or args
237         local n
238         local token = ctx.urltoken
239         local preq = {}
240         local freq = {}
241
242         for i, s in ipairs(request) do
243                 preq[#preq+1] = s
244                 freq[#freq+1] = s
245                 c = c.nodes[s]
246                 n = i
247                 if not c then
248                         break
249                 end
250
251                 util.update(track, c)
252
253                 if c.leaf then
254                         break
255                 end
256         end
257
258         if c and c.leaf then
259                 for j=n+1, #request do
260                         args[#args+1] = request[j]
261                         freq[#freq+1] = request[j]
262                 end
263         end
264
265         ctx.requestpath = ctx.requestpath or freq
266         ctx.path = preq
267
268         if track.i18n then
269                 i18n.loadc(track.i18n)
270         end
271
272         -- Init template engine
273         if (c and c.index) or not track.notemplate then
274                 local tpl = require("luci.template")
275                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
276                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
277                         media = nil
278                         for name, theme in pairs(luci.config.themes) do
279                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
280                                  "themes/%s/header" % fs.basename(theme)) then
281                                         media = theme
282                                 end
283                         end
284                         assert(media, "No valid theme found")
285                 end
286
287                 local function _ifattr(cond, key, val)
288                         if cond then
289                                 local env = getfenv(3)
290                                 local scope = (type(env.self) == "table") and env.self
291                                 return string.format(
292                                         ' %s="%s"', tostring(key),
293                                         luci.util.pcdata(tostring( val
294                                          or (type(env[key]) ~= "function" and env[key])
295                                          or (scope and type(scope[key]) ~= "function" and scope[key])
296                                          or "" ))
297                                 )
298                         else
299                                 return ''
300                         end
301                 end
302
303                 tpl.context.viewns = setmetatable({
304                    write       = luci.http.write;
305                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
306                    translate   = i18n.translate;
307                    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 = luci.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                                                 luci.http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
391                                                 ctx.authsession = sess
392                                                 ctx.authuser = user
393                                         end
394                                 end
395                         else
396                                 luci.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 best possible strategy.
473 function createindex()
474         local path = luci.util.libpath() .. "/controller/"
475         local suff = { ".lua", ".lua.gz" }
476
477         if luci.util.copcall(require, "luci.fastindex") then
478                 createindex_fastindex(path, suff)
479         else
480                 createindex_plain(path, suff)
481         end
482 end
483
484 --- Generate the dispatching index using the fastindex C-indexer.
485 -- @param path          Controller base directory
486 -- @param suffixes      Controller file suffixes
487 function createindex_fastindex(path, suffixes)
488         index = {}
489
490         if not fi then
491                 fi = luci.fastindex.new("index")
492                 for _, suffix in ipairs(suffixes) do
493                         fi.add(path .. "*" .. suffix)
494                         fi.add(path .. "*/*" .. suffix)
495                 end
496         end
497         fi.scan()
498
499         for k, v in pairs(fi.indexes) do
500                 index[v[2]] = v[1]
501         end
502 end
503
504 --- Generate the dispatching index using the native file-cache based strategy.
505 -- @param path          Controller base directory
506 -- @param suffixes      Controller file suffixes
507 function createindex_plain(path, suffixes)
508         local controllers = { }
509         for _, suffix in ipairs(suffixes) do
510                 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
511                 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
512         end
513
514         if indexcache then
515                 local cachedate = fs.stat(indexcache, "mtime")
516                 if cachedate then
517                         local realdate = 0
518                         for _, obj in ipairs(controllers) do
519                                 local omtime = fs.stat(obj, "mtime")
520                                 realdate = (omtime and omtime > realdate) and omtime or realdate
521                         end
522
523                         if cachedate > realdate then
524                                 assert(
525                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
526                                         and fs.stat(indexcache, "modestr") == "rw-------",
527                                         "Fatal: Indexcache is not sane!"
528                                 )
529
530                                 index = loadfile(indexcache)()
531                                 return index
532                         end
533                 end
534         end
535
536         index = {}
537
538         for i,c in ipairs(controllers) do
539                 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
540                 for _, suffix in ipairs(suffixes) do
541                         modname = modname:gsub(suffix.."$", "")
542                 end
543
544                 local mod = require(modname)
545                 assert(mod ~= true,
546                        "Invalid controller file found\n" ..
547                        "The file '" .. c .. "' contains an invalid module line.\n" ..
548                        "Please verify whether the module name is set to '" .. modname ..
549                        "' - It must correspond to the file path!")
550
551                 local idx = mod.index
552                 assert(type(idx) == "function",
553                        "Invalid controller file found\n" ..
554                        "The file '" .. c .. "' contains no index() function.\n" ..
555                        "Please make sure that the controller contains a valid " ..
556                        "index function and verify the spelling!")
557
558                 index[modname] = idx
559         end
560
561         if indexcache then
562                 local f = nixio.open(indexcache, "w", 600)
563                 f:writeall(util.get_bytecode(index))
564                 f:close()
565         end
566 end
567
568 --- Create the dispatching tree from the index.
569 -- Build the index before if it does not exist yet.
570 function createtree()
571         if not index then
572                 createindex()
573         end
574
575         local ctx  = context
576         local tree = {nodes={}, inreq=true}
577         local modi = {}
578
579         ctx.treecache = setmetatable({}, {__mode="v"})
580         ctx.tree = tree
581         ctx.modifiers = modi
582
583         -- Load default translation
584         require "luci.i18n".loadc("base")
585
586         local scope = setmetatable({}, {__index = luci.dispatcher})
587
588         for k, v in pairs(index) do
589                 scope._NAME = k
590                 setfenv(v, scope)
591                 v()
592         end
593
594         local function modisort(a,b)
595                 return modi[a].order < modi[b].order
596         end
597
598         for _, v in util.spairs(modi, modisort) do
599                 scope._NAME = v.module
600                 setfenv(v.func, scope)
601                 v.func()
602         end
603
604         return tree
605 end
606
607 --- Register a tree modifier.
608 -- @param       func    Modifier function
609 -- @param       order   Modifier order value (optional)
610 function modifier(func, order)
611         context.modifiers[#context.modifiers+1] = {
612                 func = func,
613                 order = order or 0,
614                 module
615                         = getfenv(2)._NAME
616         }
617 end
618
619 --- Clone a node of the dispatching tree to another position.
620 -- @param       path    Virtual path destination
621 -- @param       clone   Virtual path source
622 -- @param       title   Destination node title (optional)
623 -- @param       order   Destination node order value (optional)
624 -- @return                      Dispatching tree node
625 function assign(path, clone, title, order)
626         local obj  = node(unpack(path))
627         obj.nodes  = nil
628         obj.module = nil
629
630         obj.title = title
631         obj.order = order
632
633         setmetatable(obj, {__index = _create_node(clone)})
634
635         return obj
636 end
637
638 --- Create a new dispatching node and define common parameters.
639 -- @param       path    Virtual path
640 -- @param       target  Target function to call when dispatched.
641 -- @param       title   Destination node title
642 -- @param       order   Destination node order value (optional)
643 -- @return                      Dispatching tree node
644 function entry(path, target, title, order)
645         local c = node(unpack(path))
646
647         c.target = target
648         c.title  = title
649         c.order  = order
650         c.module = getfenv(2)._NAME
651
652         return c
653 end
654
655 --- Fetch or create a dispatching node without setting the target module or
656 -- enabling the node.
657 -- @param       ...             Virtual path
658 -- @return                      Dispatching tree node
659 function get(...)
660         return _create_node({...})
661 end
662
663 --- Fetch or create a new dispatching node.
664 -- @param       ...             Virtual path
665 -- @return                      Dispatching tree node
666 function node(...)
667         local c = _create_node({...})
668
669         c.module = getfenv(2)._NAME
670         c.auto = nil
671
672         return c
673 end
674
675 function _create_node(path)
676         if #path == 0 then
677                 return context.tree
678         end
679
680         local name = table.concat(path, ".")
681         local c = context.treecache[name]
682
683         if not c then
684                 local last = table.remove(path)
685                 local parent = _create_node(path)
686
687                 c = {nodes={}, auto=true}
688                 -- the node is "in request" if the request path matches
689                 -- at least up to the length of the node path
690                 if parent.inreq and context.path[#path+1] == last then
691                   c.inreq = true
692                 end
693                 parent.nodes[last] = c
694                 context.treecache[name] = c
695         end
696         return c
697 end
698
699 -- Subdispatchers --
700
701 function _firstchild()
702    local path = { unpack(context.path) }
703    local name = table.concat(path, ".")
704    local node = context.treecache[name]
705
706    local lowest
707    if node and node.nodes and next(node.nodes) then
708           local k, v
709           for k, v in pairs(node.nodes) do
710                  if not lowest or
711                         (v.order or 100) < (node.nodes[lowest].order or 100)
712                  then
713                         lowest = k
714                  end
715           end
716    end
717
718    assert(lowest ~= nil,
719                   "The requested node contains no childs, unable to redispatch")
720
721    path[#path+1] = lowest
722    dispatch(path)
723 end
724
725 --- Alias the first (lowest order) page automatically
726 function firstchild()
727    return { type = "firstchild", target = _firstchild }
728 end
729
730 --- Create a redirect to another dispatching node.
731 -- @param       ...             Virtual path destination
732 function alias(...)
733         local req = {...}
734         return function(...)
735                 for _, r in ipairs({...}) do
736                         req[#req+1] = r
737                 end
738
739                 dispatch(req)
740         end
741 end
742
743 --- Rewrite the first x path values of the request.
744 -- @param       n               Number of path values to replace
745 -- @param       ...             Virtual path to replace removed path values with
746 function rewrite(n, ...)
747         local req = {...}
748         return function(...)
749                 local dispatched = util.clone(context.dispatched)
750
751                 for i=1,n do
752                         table.remove(dispatched, 1)
753                 end
754
755                 for i, r in ipairs(req) do
756                         table.insert(dispatched, i, r)
757                 end
758
759                 for _, r in ipairs({...}) do
760                         dispatched[#dispatched+1] = r
761                 end
762
763                 dispatch(dispatched)
764         end
765 end
766
767
768 local function _call(self, ...)
769         local func = getfenv()[self.name]
770         assert(func ~= nil,
771                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
772
773         assert(type(func) == "function",
774                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
775                'of type "' .. type(func) .. '".')
776
777         if #self.argv > 0 then
778                 return func(unpack(self.argv), ...)
779         else
780                 return func(...)
781         end
782 end
783
784 --- Create a function-call dispatching target.
785 -- @param       name    Target function of local controller
786 -- @param       ...             Additional parameters passed to the function
787 function call(name, ...)
788         return {type = "call", argv = {...}, name = name, target = _call}
789 end
790
791
792 local _template = function(self, ...)
793         require "luci.template".render(self.view)
794 end
795
796 --- Create a template render dispatching target.
797 -- @param       name    Template to be rendered
798 function template(name)
799         return {type = "template", view = name, target = _template}
800 end
801
802
803 local function _cbi(self, ...)
804         local cbi = require "luci.cbi"
805         local tpl = require "luci.template"
806         local http = require "luci.http"
807
808         local config = self.config or {}
809         local maps = cbi.load(self.model, ...)
810
811         local state = nil
812
813         for i, res in ipairs(maps) do
814                 res.flow = config
815                 local cstate = res:parse()
816                 if cstate and (not state or cstate < state) then
817                         state = cstate
818                 end
819         end
820
821         local function _resolve_path(path)
822                 return type(path) == "table" and build_url(unpack(path)) or path
823         end
824
825         if config.on_valid_to and state and state > 0 and state < 2 then
826                 http.redirect(_resolve_path(config.on_valid_to))
827                 return
828         end
829
830         if config.on_changed_to and state and state > 1 then
831                 http.redirect(_resolve_path(config.on_changed_to))
832                 return
833         end
834
835         if config.on_success_to and state and state > 0 then
836                 http.redirect(_resolve_path(config.on_success_to))
837                 return
838         end
839
840         if config.state_handler then
841                 if not config.state_handler(state, maps) then
842                         return
843                 end
844         end
845
846         http.header("X-CBI-State", state or 0)
847
848         if not config.noheader then
849                 tpl.render("cbi/header", {state = state})
850         end
851
852         local redirect
853         local messages
854         local applymap   = false
855         local pageaction = true
856         local parsechain = { }
857
858         for i, res in ipairs(maps) do
859                 if res.apply_needed and res.parsechain then
860                         local c
861                         for _, c in ipairs(res.parsechain) do
862                                 parsechain[#parsechain+1] = c
863                         end
864                         applymap = true
865                 end
866
867                 if res.redirect then
868                         redirect = redirect or res.redirect
869                 end
870
871                 if res.pageaction == false then
872                         pageaction = false
873                 end
874
875                 if res.message then
876                         messages = messages or { }
877                         messages[#messages+1] = res.message
878                 end
879         end
880
881         for i, res in ipairs(maps) do
882                 res:render({
883                         firstmap   = (i == 1),
884                         applymap   = applymap,
885                         redirect   = redirect,
886                         messages   = messages,
887                         pageaction = pageaction,
888                         parsechain = parsechain
889                 })
890         end
891
892         if not config.nofooter then
893                 tpl.render("cbi/footer", {
894                         flow       = config,
895                         pageaction = pageaction,
896                         redirect   = redirect,
897                         state      = state,
898                         autoapply  = config.autoapply
899                 })
900         end
901 end
902
903 --- Create a CBI model dispatching target.
904 -- @param       model   CBI model to be rendered
905 function cbi(model, config)
906         return {type = "cbi", config = config, model = model, target = _cbi}
907 end
908
909
910 local function _arcombine(self, ...)
911         local argv = {...}
912         local target = #argv > 0 and self.targets[2] or self.targets[1]
913         setfenv(target.target, self.env)
914         target:target(unpack(argv))
915 end
916
917 --- Create a combined dispatching target for non argv and argv requests.
918 -- @param trg1  Overview Target
919 -- @param trg2  Detail Target
920 function arcombine(trg1, trg2)
921         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
922 end
923
924
925 local function _form(self, ...)
926         local cbi = require "luci.cbi"
927         local tpl = require "luci.template"
928         local http = require "luci.http"
929
930         local maps = luci.cbi.load(self.model, ...)
931         local state = nil
932
933         for i, res in ipairs(maps) do
934                 local cstate = res:parse()
935                 if cstate and (not state or cstate < state) then
936                         state = cstate
937                 end
938         end
939
940         http.header("X-CBI-State", state or 0)
941         tpl.render("header")
942         for i, res in ipairs(maps) do
943                 res:render()
944         end
945         tpl.render("footer")
946 end
947
948 --- Create a CBI form model dispatching target.
949 -- @param       model   CBI form model tpo be rendered
950 function form(model)
951         return {type = "cbi", model = model, target = _form}
952 end
953
954 --- Access the luci.i18n translate() api.
955 -- @class  function
956 -- @name   translate
957 -- @param  text    Text to translate
958 translate = i18n.translate
959
960 --- No-op function used to mark translation entries for menu labels.
961 -- This function does not actually translate the given argument but
962 -- is used by build/i18n-scan.pl to find translatable entries.
963 function _(text)
964         return text
965 end