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