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