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