Merge pull request #1769 from jow-/master
[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"), 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 assert(ok,
511 "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 else
515 local root = node()
516 if not root or not root.target then
517 error404("No root node was registered, this usually happens if no module was installed.\n" ..
518 "Install luci-mod-admin-full and retry. " ..
519 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
520 else
521 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
522 "If this url belongs to an extension, make sure it is properly installed.\n" ..
523 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
524 end
525 end
526 end
527
528 function createindex()
529 local controllers = { }
530 local base = "%s/controller/" % util.libpath()
531 local _, path
532
533 for path in (fs.glob("%s*.lua" % base) or function() end) do
534 controllers[#controllers+1] = path
535 end
536
537 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
538 controllers[#controllers+1] = path
539 end
540
541 if indexcache then
542 local cachedate = fs.stat(indexcache, "mtime")
543 if cachedate then
544 local realdate = 0
545 for _, obj in ipairs(controllers) do
546 local omtime = fs.stat(obj, "mtime")
547 realdate = (omtime and omtime > realdate) and omtime or realdate
548 end
549
550 if cachedate > realdate and sys.process.info("uid") == 0 then
551 assert(
552 sys.process.info("uid") == fs.stat(indexcache, "uid")
553 and fs.stat(indexcache, "modestr") == "rw-------",
554 "Fatal: Indexcache is not sane!"
555 )
556
557 index = loadfile(indexcache)()
558 return index
559 end
560 end
561 end
562
563 index = {}
564
565 for _, path in ipairs(controllers) do
566 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
567 local mod = require(modname)
568 assert(mod ~= true,
569 "Invalid controller file found\n" ..
570 "The file '" .. path .. "' contains an invalid module line.\n" ..
571 "Please verify whether the module name is set to '" .. modname ..
572 "' - It must correspond to the file path!")
573
574 local idx = mod.index
575 assert(type(idx) == "function",
576 "Invalid controller file found\n" ..
577 "The file '" .. path .. "' contains no index() function.\n" ..
578 "Please make sure that the controller contains a valid " ..
579 "index function and verify the spelling!")
580
581 index[modname] = idx
582 end
583
584 if indexcache then
585 local f = nixio.open(indexcache, "w", 600)
586 f:writeall(util.get_bytecode(index))
587 f:close()
588 end
589 end
590
591 -- Build the index before if it does not exist yet.
592 function createtree()
593 if not index then
594 createindex()
595 end
596
597 local ctx = context
598 local tree = {nodes={}, inreq=true}
599 local modi = {}
600
601 ctx.treecache = setmetatable({}, {__mode="v"})
602 ctx.tree = tree
603 ctx.modifiers = modi
604
605 -- Load default translation
606 require "luci.i18n".loadc("base")
607
608 local scope = setmetatable({}, {__index = luci.dispatcher})
609
610 for k, v in pairs(index) do
611 scope._NAME = k
612 setfenv(v, scope)
613 v()
614 end
615
616 local function modisort(a,b)
617 return modi[a].order < modi[b].order
618 end
619
620 for _, v in util.spairs(modi, modisort) do
621 scope._NAME = v.module
622 setfenv(v.func, scope)
623 v.func()
624 end
625
626 return tree
627 end
628
629 function modifier(func, order)
630 context.modifiers[#context.modifiers+1] = {
631 func = func,
632 order = order or 0,
633 module
634 = getfenv(2)._NAME
635 }
636 end
637
638 function assign(path, clone, title, order)
639 local obj = node(unpack(path))
640 obj.nodes = nil
641 obj.module = nil
642
643 obj.title = title
644 obj.order = order
645
646 setmetatable(obj, {__index = _create_node(clone)})
647
648 return obj
649 end
650
651 function entry(path, target, title, order)
652 local c = node(unpack(path))
653
654 c.target = target
655 c.title = title
656 c.order = order
657 c.module = getfenv(2)._NAME
658
659 return c
660 end
661
662 -- enabling the node.
663 function get(...)
664 return _create_node({...})
665 end
666
667 function node(...)
668 local c = _create_node({...})
669
670 c.module = getfenv(2)._NAME
671 c.auto = nil
672
673 return c
674 end
675
676 function lookup(...)
677 local i, path = nil, {}
678 for i = 1, select('#', ...) do
679 local name, arg = nil, tostring(select(i, ...))
680 for name in arg:gmatch("[^/]+") do
681 path[#path+1] = name
682 end
683 end
684
685 for i = #path, 1, -1 do
686 local node = context.treecache[table.concat(path, ".", 1, i)]
687 if node and (i == #path or node.leaf) then
688 return node, build_url(unpack(path))
689 end
690 end
691 end
692
693 function _create_node(path)
694 if #path == 0 then
695 return context.tree
696 end
697
698 local name = table.concat(path, ".")
699 local c = context.treecache[name]
700
701 if not c then
702 local last = table.remove(path)
703 local parent = _create_node(path)
704
705 c = {nodes={}, auto=true}
706 -- the node is "in request" if the request path matches
707 -- at least up to the length of the node path
708 if parent.inreq and context.path[#path+1] == last then
709 c.inreq = true
710 end
711 parent.nodes[last] = c
712 context.treecache[name] = c
713 end
714 return c
715 end
716
717 -- Subdispatchers --
718
719 function _firstchild()
720 local path = { unpack(context.path) }
721 local name = table.concat(path, ".")
722 local node = context.treecache[name]
723
724 local lowest
725 if node and node.nodes and next(node.nodes) then
726 local k, v
727 for k, v in pairs(node.nodes) do
728 if not lowest or
729 (v.order or 100) < (node.nodes[lowest].order or 100)
730 then
731 lowest = k
732 end
733 end
734 end
735
736 assert(lowest ~= nil,
737 "The requested node contains no childs, unable to redispatch")
738
739 path[#path+1] = lowest
740 dispatch(path)
741 end
742
743 function firstchild()
744 return { type = "firstchild", target = _firstchild }
745 end
746
747 function alias(...)
748 local req = {...}
749 return function(...)
750 for _, r in ipairs({...}) do
751 req[#req+1] = r
752 end
753
754 dispatch(req)
755 end
756 end
757
758 function rewrite(n, ...)
759 local req = {...}
760 return function(...)
761 local dispatched = util.clone(context.dispatched)
762
763 for i=1,n do
764 table.remove(dispatched, 1)
765 end
766
767 for i, r in ipairs(req) do
768 table.insert(dispatched, i, r)
769 end
770
771 for _, r in ipairs({...}) do
772 dispatched[#dispatched+1] = r
773 end
774
775 dispatch(dispatched)
776 end
777 end
778
779
780 local function _call(self, ...)
781 local func = getfenv()[self.name]
782 assert(func ~= nil,
783 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
784
785 assert(type(func) == "function",
786 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
787 'of type "' .. type(func) .. '".')
788
789 if #self.argv > 0 then
790 return func(unpack(self.argv), ...)
791 else
792 return func(...)
793 end
794 end
795
796 function call(name, ...)
797 return {type = "call", argv = {...}, name = name, target = _call}
798 end
799
800 function post_on(params, name, ...)
801 return {
802 type = "call",
803 post = params,
804 argv = { ... },
805 name = name,
806 target = _call
807 }
808 end
809
810 function post(...)
811 return post_on(true, ...)
812 end
813
814
815 local _template = function(self, ...)
816 require "luci.template".render(self.view)
817 end
818
819 function template(name)
820 return {type = "template", view = name, target = _template}
821 end
822
823
824 local function _cbi(self, ...)
825 local cbi = require "luci.cbi"
826 local tpl = require "luci.template"
827 local http = require "luci.http"
828
829 local config = self.config or {}
830 local maps = cbi.load(self.model, ...)
831
832 local state = nil
833
834 local i, res
835 for i, res in ipairs(maps) do
836 if util.instanceof(res, cbi.SimpleForm) then
837 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
838 % self.model)
839
840 io.stderr:write("please change %s to use the form() action instead.\n"
841 % table.concat(context.request, "/"))
842 end
843
844 res.flow = config
845 local cstate = res:parse()
846 if cstate and (not state or cstate < state) then
847 state = cstate
848 end
849 end
850
851 local function _resolve_path(path)
852 return type(path) == "table" and build_url(unpack(path)) or path
853 end
854
855 if config.on_valid_to and state and state > 0 and state < 2 then
856 http.redirect(_resolve_path(config.on_valid_to))
857 return
858 end
859
860 if config.on_changed_to and state and state > 1 then
861 http.redirect(_resolve_path(config.on_changed_to))
862 return
863 end
864
865 if config.on_success_to and state and state > 0 then
866 http.redirect(_resolve_path(config.on_success_to))
867 return
868 end
869
870 if config.state_handler then
871 if not config.state_handler(state, maps) then
872 return
873 end
874 end
875
876 http.header("X-CBI-State", state or 0)
877
878 if not config.noheader then
879 tpl.render("cbi/header", {state = state})
880 end
881
882 local redirect
883 local messages
884 local applymap = false
885 local pageaction = true
886 local parsechain = { }
887
888 local is_rollback, time_remaining = uci:rollback_pending()
889
890 for i, res in ipairs(maps) do
891 if res.apply_needed and res.parsechain then
892 local c
893 for _, c in ipairs(res.parsechain) do
894 parsechain[#parsechain+1] = c
895 end
896 applymap = true
897 end
898
899 if res.redirect then
900 redirect = redirect or res.redirect
901 end
902
903 if res.pageaction == false then
904 pageaction = false
905 end
906
907 if res.message then
908 messages = messages or { }
909 messages[#messages+1] = res.message
910 end
911 end
912
913 for i, res in ipairs(maps) do
914 res:render({
915 firstmap = (i == 1),
916 applymap = applymap,
917 confirmmap = (is_rollback and time_remaining or nil),
918 redirect = redirect,
919 messages = messages,
920 pageaction = pageaction,
921 parsechain = parsechain
922 })
923 end
924
925 if not config.nofooter then
926 tpl.render("cbi/footer", {
927 flow = config,
928 pageaction = pageaction,
929 redirect = redirect,
930 state = state,
931 autoapply = config.autoapply
932 })
933 end
934 end
935
936 function cbi(model, config)
937 return {
938 type = "cbi",
939 post = { ["cbi.submit"] = true },
940 config = config,
941 model = model,
942 target = _cbi
943 }
944 end
945
946
947 local function _arcombine(self, ...)
948 local argv = {...}
949 local target = #argv > 0 and self.targets[2] or self.targets[1]
950 setfenv(target.target, self.env)
951 target:target(unpack(argv))
952 end
953
954 function arcombine(trg1, trg2)
955 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
956 end
957
958
959 local function _form(self, ...)
960 local cbi = require "luci.cbi"
961 local tpl = require "luci.template"
962 local http = require "luci.http"
963
964 local maps = luci.cbi.load(self.model, ...)
965 local state = nil
966
967 local i, res
968 for i, res in ipairs(maps) do
969 local cstate = res:parse()
970 if cstate and (not state or cstate < state) then
971 state = cstate
972 end
973 end
974
975 http.header("X-CBI-State", state or 0)
976 tpl.render("header")
977 for i, res in ipairs(maps) do
978 res:render()
979 end
980 tpl.render("footer")
981 end
982
983 function form(model)
984 return {
985 type = "cbi",
986 post = { ["cbi.submit"] = true },
987 model = model,
988 target = _form
989 }
990 end
991
992 translate = i18n.translate
993
994 -- This function does not actually translate the given argument but
995 -- is used by build/i18n-scan.pl to find translatable entries.
996 function _(text)
997 return text
998 end