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