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