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