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