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