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