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