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