luci-theme-material: fix theme for applyreboot page changes
[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 _firstchild()
728 local path = { unpack(context.path) }
729 local name = table.concat(path, ".")
730 local node = context.treecache[name]
731
732 local lowest
733 if node and node.nodes and next(node.nodes) then
734 local k, v
735 for k, v in pairs(node.nodes) do
736 if not lowest or
737 (v.order or 100) < (node.nodes[lowest].order or 100)
738 then
739 lowest = k
740 end
741 end
742 end
743
744 assert(lowest ~= nil,
745 "The requested node contains no childs, unable to redispatch")
746
747 path[#path+1] = lowest
748 dispatch(path)
749 end
750
751 function firstchild()
752 return { type = "firstchild", target = _firstchild }
753 end
754
755 function alias(...)
756 local req = {...}
757 return function(...)
758 for _, r in ipairs({...}) do
759 req[#req+1] = r
760 end
761
762 dispatch(req)
763 end
764 end
765
766 function rewrite(n, ...)
767 local req = {...}
768 return function(...)
769 local dispatched = util.clone(context.dispatched)
770
771 for i=1,n do
772 table.remove(dispatched, 1)
773 end
774
775 for i, r in ipairs(req) do
776 table.insert(dispatched, i, r)
777 end
778
779 for _, r in ipairs({...}) do
780 dispatched[#dispatched+1] = r
781 end
782
783 dispatch(dispatched)
784 end
785 end
786
787
788 local function _call(self, ...)
789 local func = getfenv()[self.name]
790 assert(func ~= nil,
791 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
792
793 assert(type(func) == "function",
794 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
795 'of type "' .. type(func) .. '".')
796
797 if #self.argv > 0 then
798 return func(unpack(self.argv), ...)
799 else
800 return func(...)
801 end
802 end
803
804 function call(name, ...)
805 return {type = "call", argv = {...}, name = name, target = _call}
806 end
807
808 function post_on(params, name, ...)
809 return {
810 type = "call",
811 post = params,
812 argv = { ... },
813 name = name,
814 target = _call
815 }
816 end
817
818 function post(...)
819 return post_on(true, ...)
820 end
821
822
823 local _template = function(self, ...)
824 require "luci.template".render(self.view)
825 end
826
827 function template(name)
828 return {type = "template", view = name, target = _template}
829 end
830
831
832 local function _cbi(self, ...)
833 local cbi = require "luci.cbi"
834 local tpl = require "luci.template"
835 local http = require "luci.http"
836
837 local config = self.config or {}
838 local maps = cbi.load(self.model, ...)
839
840 local state = nil
841
842 local i, res
843 for i, res in ipairs(maps) do
844 if util.instanceof(res, cbi.SimpleForm) then
845 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
846 % self.model)
847
848 io.stderr:write("please change %s to use the form() action instead.\n"
849 % table.concat(context.request, "/"))
850 end
851
852 res.flow = config
853 local cstate = res:parse()
854 if cstate and (not state or cstate < state) then
855 state = cstate
856 end
857 end
858
859 local function _resolve_path(path)
860 return type(path) == "table" and build_url(unpack(path)) or path
861 end
862
863 if config.on_valid_to and state and state > 0 and state < 2 then
864 http.redirect(_resolve_path(config.on_valid_to))
865 return
866 end
867
868 if config.on_changed_to and state and state > 1 then
869 http.redirect(_resolve_path(config.on_changed_to))
870 return
871 end
872
873 if config.on_success_to and state and state > 0 then
874 http.redirect(_resolve_path(config.on_success_to))
875 return
876 end
877
878 if config.state_handler then
879 if not config.state_handler(state, maps) then
880 return
881 end
882 end
883
884 http.header("X-CBI-State", state or 0)
885
886 if not config.noheader then
887 tpl.render("cbi/header", {state = state})
888 end
889
890 local redirect
891 local messages
892 local applymap = false
893 local pageaction = true
894 local parsechain = { }
895
896 local is_rollback, time_remaining = uci:rollback_pending()
897
898 for i, res in ipairs(maps) do
899 if res.apply_needed and res.parsechain then
900 local c
901 for _, c in ipairs(res.parsechain) do
902 parsechain[#parsechain+1] = c
903 end
904 applymap = true
905 end
906
907 if res.redirect then
908 redirect = redirect or res.redirect
909 end
910
911 if res.pageaction == false then
912 pageaction = false
913 end
914
915 if res.message then
916 messages = messages or { }
917 messages[#messages+1] = res.message
918 end
919 end
920
921 for i, res in ipairs(maps) do
922 res:render({
923 firstmap = (i == 1),
924 applymap = applymap,
925 confirmmap = (is_rollback and time_remaining or nil),
926 redirect = redirect,
927 messages = messages,
928 pageaction = pageaction,
929 parsechain = parsechain
930 })
931 end
932
933 if not config.nofooter then
934 tpl.render("cbi/footer", {
935 flow = config,
936 pageaction = pageaction,
937 redirect = redirect,
938 state = state,
939 autoapply = config.autoapply
940 })
941 end
942 end
943
944 function cbi(model, config)
945 return {
946 type = "cbi",
947 post = { ["cbi.submit"] = true },
948 config = config,
949 model = model,
950 target = _cbi
951 }
952 end
953
954
955 local function _arcombine(self, ...)
956 local argv = {...}
957 local target = #argv > 0 and self.targets[2] or self.targets[1]
958 setfenv(target.target, self.env)
959 target:target(unpack(argv))
960 end
961
962 function arcombine(trg1, trg2)
963 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
964 end
965
966
967 local function _form(self, ...)
968 local cbi = require "luci.cbi"
969 local tpl = require "luci.template"
970 local http = require "luci.http"
971
972 local maps = luci.cbi.load(self.model, ...)
973 local state = nil
974
975 local i, res
976 for i, res in ipairs(maps) do
977 local cstate = res:parse()
978 if cstate and (not state or cstate < state) then
979 state = cstate
980 end
981 end
982
983 http.header("X-CBI-State", state or 0)
984 tpl.render("header")
985 for i, res in ipairs(maps) do
986 res:render()
987 end
988 tpl.render("footer")
989 end
990
991 function form(model)
992 return {
993 type = "cbi",
994 post = { ["cbi.submit"] = true },
995 model = model,
996 target = _form
997 }
998 end
999
1000 translate = i18n.translate
1001
1002 -- This function does not actually translate the given argument but
1003 -- is used by build/i18n-scan.pl to find translatable entries.
1004 function _(text)
1005 return text
1006 end