Merge pull request #2043 from Ansuel/materialfix
[oweals/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") or "", 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; HttpOnly%s' %{
433                                 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
434                         })
435                         http.redirect(build_url(unpack(ctx.requestpath)))
436                 end
437
438                 if not sid or not sdat then
439                         http.status(403, "Forbidden")
440                         return
441                 end
442
443                 ctx.authsession = sid
444                 ctx.authtoken = sdat.token
445                 ctx.authuser = sdat.username
446         end
447
448         if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
449                 luci.http.status(200, "OK")
450                 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
451                 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
452                 return
453         end
454
455         if c and require_post_security(c.target) then
456                 if not test_post_security(c) then
457                         return
458                 end
459         end
460
461         if track.setgroup then
462                 sys.process.setgroup(track.setgroup)
463         end
464
465         if track.setuser then
466                 sys.process.setuser(track.setuser)
467         end
468
469         local target = nil
470         if c then
471                 if type(c.target) == "function" then
472                         target = c.target
473                 elseif type(c.target) == "table" then
474                         target = c.target.target
475                 end
476         end
477
478         if c and (c.index or type(target) == "function") then
479                 ctx.dispatched = c
480                 ctx.requested = ctx.requested or ctx.dispatched
481         end
482
483         if c and c.index then
484                 local tpl = require "luci.template"
485
486                 if util.copcall(tpl.render, "indexer", {}) then
487                         return true
488                 end
489         end
490
491         if type(target) == "function" then
492                 util.copcall(function()
493                         local oldenv = getfenv(target)
494                         local module = require(c.module)
495                         local env = setmetatable({}, {__index=
496
497                         function(tbl, key)
498                                 return rawget(tbl, key) or module[key] or oldenv[key]
499                         end})
500
501                         setfenv(target, env)
502                 end)
503
504                 local ok, err
505                 if type(c.target) == "table" then
506                         ok, err = util.copcall(target, c.target, unpack(args))
507                 else
508                         ok, err = util.copcall(target, unpack(args))
509                 end
510                 if not ok then
511                         error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
512                                  " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
513                                  "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
514                 end
515         else
516                 local root = node()
517                 if not root or not root.target then
518                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
519                                  "Install luci-mod-admin-full and retry. " ..
520                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
521                 else
522                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
523                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
524                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
525                 end
526         end
527 end
528
529 function createindex()
530         local controllers = { }
531         local base = "%s/controller/" % util.libpath()
532         local _, path
533
534         for path in (fs.glob("%s*.lua" % base) or function() end) do
535                 controllers[#controllers+1] = path
536         end
537
538         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
539                 controllers[#controllers+1] = path
540         end
541
542         if indexcache then
543                 local cachedate = fs.stat(indexcache, "mtime")
544                 if cachedate then
545                         local realdate = 0
546                         for _, obj in ipairs(controllers) do
547                                 local omtime = fs.stat(obj, "mtime")
548                                 realdate = (omtime and omtime > realdate) and omtime or realdate
549                         end
550
551                         if cachedate > realdate and sys.process.info("uid") == 0 then
552                                 assert(
553                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
554                                         and fs.stat(indexcache, "modestr") == "rw-------",
555                                         "Fatal: Indexcache is not sane!"
556                                 )
557
558                                 index = loadfile(indexcache)()
559                                 return index
560                         end
561                 end
562         end
563
564         index = {}
565
566         for _, path in ipairs(controllers) do
567                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
568                 local mod = require(modname)
569                 assert(mod ~= true,
570                        "Invalid controller file found\n" ..
571                        "The file '" .. path .. "' contains an invalid module line.\n" ..
572                        "Please verify whether the module name is set to '" .. modname ..
573                        "' - It must correspond to the file path!")
574
575                 local idx = mod.index
576                 assert(type(idx) == "function",
577                        "Invalid controller file found\n" ..
578                        "The file '" .. path .. "' contains no index() function.\n" ..
579                        "Please make sure that the controller contains a valid " ..
580                        "index function and verify the spelling!")
581
582                 index[modname] = idx
583         end
584
585         if indexcache then
586                 local f = nixio.open(indexcache, "w", 600)
587                 f:writeall(util.get_bytecode(index))
588                 f:close()
589         end
590 end
591
592 -- Build the index before if it does not exist yet.
593 function createtree()
594         if not index then
595                 createindex()
596         end
597
598         local ctx  = context
599         local tree = {nodes={}, inreq=true}
600         local modi = {}
601
602         ctx.treecache = setmetatable({}, {__mode="v"})
603         ctx.tree = tree
604         ctx.modifiers = modi
605
606         -- Load default translation
607         require "luci.i18n".loadc("base")
608
609         local scope = setmetatable({}, {__index = luci.dispatcher})
610
611         for k, v in pairs(index) do
612                 scope._NAME = k
613                 setfenv(v, scope)
614                 v()
615         end
616
617         local function modisort(a,b)
618                 return modi[a].order < modi[b].order
619         end
620
621         for _, v in util.spairs(modi, modisort) do
622                 scope._NAME = v.module
623                 setfenv(v.func, scope)
624                 v.func()
625         end
626
627         return tree
628 end
629
630 function modifier(func, order)
631         context.modifiers[#context.modifiers+1] = {
632                 func = func,
633                 order = order or 0,
634                 module
635                         = getfenv(2)._NAME
636         }
637 end
638
639 function assign(path, clone, title, order)
640         local obj  = node(unpack(path))
641         obj.nodes  = nil
642         obj.module = nil
643
644         obj.title = title
645         obj.order = order
646
647         setmetatable(obj, {__index = _create_node(clone)})
648
649         return obj
650 end
651
652 function entry(path, target, title, order)
653         local c = node(unpack(path))
654
655         c.target = target
656         c.title  = title
657         c.order  = order
658         c.module = getfenv(2)._NAME
659
660         return c
661 end
662
663 -- enabling the node.
664 function get(...)
665         return _create_node({...})
666 end
667
668 function node(...)
669         local c = _create_node({...})
670
671         c.module = getfenv(2)._NAME
672         c.auto = nil
673
674         return c
675 end
676
677 function lookup(...)
678         local i, path = nil, {}
679         for i = 1, select('#', ...) do
680                 local name, arg = nil, tostring(select(i, ...))
681                 for name in arg:gmatch("[^/]+") do
682                         path[#path+1] = name
683                 end
684         end
685
686         for i = #path, 1, -1 do
687                 local node = context.treecache[table.concat(path, ".", 1, i)]
688                 if node and (i == #path or node.leaf) then
689                         return node, build_url(unpack(path))
690                 end
691         end
692 end
693
694 function _create_node(path)
695         if #path == 0 then
696                 return context.tree
697         end
698
699         local name = table.concat(path, ".")
700         local c = context.treecache[name]
701
702         if not c then
703                 local last = table.remove(path)
704                 local parent = _create_node(path)
705
706                 c = {nodes={}, auto=true, inreq=true}
707
708                 local _, n
709                 for _, n in ipairs(path) do
710                         if context.path[_] ~= n then
711                                 c.inreq = false
712                                 break
713                         end
714                 end
715
716                 c.inreq = c.inreq and (context.path[#path + 1] == last)
717
718                 parent.nodes[last] = c
719                 context.treecache[name] = c
720         end
721
722         return c
723 end
724
725 -- Subdispatchers --
726
727 function _firstchild()
728    local path = { unpack(context.path) }
729    local name = table.concat(path, ".")
730    local node = context.treecache[name]
731
732    local lowest
733    if node and node.nodes and next(node.nodes) then
734           local k, v
735           for k, v in pairs(node.nodes) do
736                  if not lowest or
737                         (v.order or 100) < (node.nodes[lowest].order or 100)
738                  then
739                         lowest = k
740                  end
741           end
742    end
743
744    assert(lowest ~= nil,
745                   "The requested node contains no childs, unable to redispatch")
746
747    path[#path+1] = lowest
748    dispatch(path)
749 end
750
751 function firstchild()
752    return { type = "firstchild", target = _firstchild }
753 end
754
755 function alias(...)
756         local req = {...}
757         return function(...)
758                 for _, r in ipairs({...}) do
759                         req[#req+1] = r
760                 end
761
762                 dispatch(req)
763         end
764 end
765
766 function rewrite(n, ...)
767         local req = {...}
768         return function(...)
769                 local dispatched = util.clone(context.dispatched)
770
771                 for i=1,n do
772                         table.remove(dispatched, 1)
773                 end
774
775                 for i, r in ipairs(req) do
776                         table.insert(dispatched, i, r)
777                 end
778
779                 for _, r in ipairs({...}) do
780                         dispatched[#dispatched+1] = r
781                 end
782
783                 dispatch(dispatched)
784         end
785 end
786
787
788 local function _call(self, ...)
789         local func = getfenv()[self.name]
790         assert(func ~= nil,
791                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
792
793         assert(type(func) == "function",
794                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
795                'of type "' .. type(func) .. '".')
796
797         if #self.argv > 0 then
798                 return func(unpack(self.argv), ...)
799         else
800                 return func(...)
801         end
802 end
803
804 function call(name, ...)
805         return {type = "call", argv = {...}, name = name, target = _call}
806 end
807
808 function post_on(params, name, ...)
809         return {
810                 type = "call",
811                 post = params,
812                 argv = { ... },
813                 name = name,
814                 target = _call
815         }
816 end
817
818 function post(...)
819         return post_on(true, ...)
820 end
821
822
823 local _template = function(self, ...)
824         require "luci.template".render(self.view)
825 end
826
827 function template(name)
828         return {type = "template", view = name, target = _template}
829 end
830
831
832 local function _cbi(self, ...)
833         local cbi = require "luci.cbi"
834         local tpl = require "luci.template"
835         local http = require "luci.http"
836
837         local config = self.config or {}
838         local maps = cbi.load(self.model, ...)
839
840         local state = nil
841
842         local i, res
843         for i, res in ipairs(maps) do
844                 if util.instanceof(res, cbi.SimpleForm) then
845                         io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
846                                 % self.model)
847
848                         io.stderr:write("please change %s to use the form() action instead.\n"
849                                 % table.concat(context.request, "/"))
850                 end
851
852                 res.flow = config
853                 local cstate = res:parse()
854                 if cstate and (not state or cstate < state) then
855                         state = cstate
856                 end
857         end
858
859         local function _resolve_path(path)
860                 return type(path) == "table" and build_url(unpack(path)) or path
861         end
862
863         if config.on_valid_to and state and state > 0 and state < 2 then
864                 http.redirect(_resolve_path(config.on_valid_to))
865                 return
866         end
867
868         if config.on_changed_to and state and state > 1 then
869                 http.redirect(_resolve_path(config.on_changed_to))
870                 return
871         end
872
873         if config.on_success_to and state and state > 0 then
874                 http.redirect(_resolve_path(config.on_success_to))
875                 return
876         end
877
878         if config.state_handler then
879                 if not config.state_handler(state, maps) then
880                         return
881                 end
882         end
883
884         http.header("X-CBI-State", state or 0)
885
886         if not config.noheader then
887                 tpl.render("cbi/header", {state = state})
888         end
889
890         local redirect
891         local messages
892         local applymap   = false
893         local pageaction = true
894         local parsechain = { }
895
896         for i, res in ipairs(maps) do
897                 if res.apply_needed and res.parsechain then
898                         local c
899                         for _, c in ipairs(res.parsechain) do
900                                 parsechain[#parsechain+1] = c
901                         end
902                         applymap = true
903                 end
904
905                 if res.redirect then
906                         redirect = redirect or res.redirect
907                 end
908
909                 if res.pageaction == false then
910                         pageaction = false
911                 end
912
913                 if res.message then
914                         messages = messages or { }
915                         messages[#messages+1] = res.message
916                 end
917         end
918
919         for i, res in ipairs(maps) do
920                 res:render({
921                         firstmap   = (i == 1),
922                         redirect   = redirect,
923                         messages   = messages,
924                         pageaction = pageaction,
925                         parsechain = parsechain
926                 })
927         end
928
929         if not config.nofooter then
930                 tpl.render("cbi/footer", {
931                         flow          = config,
932                         pageaction    = pageaction,
933                         redirect      = redirect,
934                         state         = state,
935                         autoapply     = config.autoapply,
936                         trigger_apply = applymap
937                 })
938         end
939 end
940
941 function cbi(model, config)
942         return {
943                 type = "cbi",
944                 post = { ["cbi.submit"] = true },
945                 config = config,
946                 model = model,
947                 target = _cbi
948         }
949 end
950
951
952 local function _arcombine(self, ...)
953         local argv = {...}
954         local target = #argv > 0 and self.targets[2] or self.targets[1]
955         setfenv(target.target, self.env)
956         target:target(unpack(argv))
957 end
958
959 function arcombine(trg1, trg2)
960         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
961 end
962
963
964 local function _form(self, ...)
965         local cbi = require "luci.cbi"
966         local tpl = require "luci.template"
967         local http = require "luci.http"
968
969         local maps = luci.cbi.load(self.model, ...)
970         local state = nil
971
972         local i, res
973         for i, res in ipairs(maps) do
974                 local cstate = res:parse()
975                 if cstate and (not state or cstate < state) then
976                         state = cstate
977                 end
978         end
979
980         http.header("X-CBI-State", state or 0)
981         tpl.render("header")
982         for i, res in ipairs(maps) do
983                 res:render()
984         end
985         tpl.render("footer")
986 end
987
988 function form(model)
989         return {
990                 type = "cbi",
991                 post = { ["cbi.submit"] = true },
992                 model = model,
993                 target = _form
994         }
995 end
996
997 translate = i18n.translate
998
999 -- This function does not actually translate the given argument but
1000 -- is used by build/i18n-scan.pl to find translatable entries.
1001 function _(text)
1002         return text
1003 end