1e610e748919edd1601e5026ea12f62755b3bfe9
[project/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