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