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