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