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