5d9d1b0b1a5eaa9e7682f1fe51dda057ae768e88
[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                    url         = build_url;
290                 }, {__index=function(table, key)
291                         if key == "controller" then
292                                 return build_url()
293                         elseif key == "REQUEST_URI" then
294                                 return build_url(unpack(ctx.requestpath))
295                         else
296                                 return rawget(table, key) or _G[key]
297                         end
298                 end})
299         end
300
301         track.dependent = (track.dependent ~= false)
302         assert(not track.dependent or not track.auto,
303                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
304                 "has no parent node so the access to this location has been denied.\n" ..
305                 "This is a software bug, please report this message at " ..
306                 "http://luci.subsignal.org/trac/newticket"
307         )
308
309         if track.sysauth then
310                 local authen = type(track.sysauth_authenticator) == "function"
311                  and track.sysauth_authenticator
312                  or authenticator[track.sysauth_authenticator]
313
314                 local def  = (type(track.sysauth) == "string") and track.sysauth
315                 local accs = def and {track.sysauth} or track.sysauth
316                 local sess = ctx.authsession
317                 local verifytoken = false
318                 if not sess then
319                         sess = http.getcookie("sysauth")
320                         sess = sess and sess:match("^[a-f0-9]*$")
321                         verifytoken = true
322                 end
323
324                 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
325                 local user
326
327                 if sdat then
328                         if not verifytoken or ctx.urltoken.stok == sdat.token then
329                                 user = sdat.user
330                         end
331                 else
332                         local eu = http.getenv("HTTP_AUTH_USER")
333                         local ep = http.getenv("HTTP_AUTH_PASS")
334                         if eu and ep and sys.user.checkpasswd(eu, ep) then
335                                 authen = function() return eu end
336                         end
337                 end
338
339                 if not util.contains(accs, user) then
340                         if authen then
341                                 local user, sess = authen(sys.user.checkpasswd, accs, def)
342                                 local token
343                                 if not user or not util.contains(accs, user) then
344                                         return
345                                 else
346                                         if not sess then
347                                                 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
348                                                 if sdat then
349                                                         token = sys.uniqueid(16)
350                                                         util.ubus("session", "set", {
351                                                                 ubus_rpc_session = sdat.ubus_rpc_session,
352                                                                 values = {
353                                                                         user = user,
354                                                                         token = token,
355                                                                         section = sys.uniqueid(16)
356                                                                 }
357                                                         })
358                                                         sess = sdat.ubus_rpc_session
359                                                 end
360                                         end
361
362                                         if sess and token then
363                                                 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
364                                                    sess, build_url()
365                                                 })
366
367                                                 ctx.urltoken.stok = token
368                                                 ctx.authsession = sess
369                                                 ctx.authuser = user
370
371                                                 http.redirect(build_url(unpack(ctx.requestpath)))
372                                         end
373                                 end
374                         else
375                                 http.status(403, "Forbidden")
376                                 return
377                         end
378                 else
379                         ctx.authsession = sess
380                         ctx.authuser = user
381                 end
382         end
383
384         if c and type(c.target) == "table" and c.target.post == true then
385                 if http.getenv("REQUEST_METHOD") ~= "POST" then
386                         http.status(405, "Method Not Allowed")
387                         http.header("Allow", "POST")
388                         return
389                 end
390
391                 if http.formvalue("token") ~= ctx.urltoken.stok then
392                         http.status(403, "Forbidden")
393                         luci.template.render("csrftoken")
394                         return
395                 end
396         end
397
398         if track.setgroup then
399                 sys.process.setgroup(track.setgroup)
400         end
401
402         if track.setuser then
403                 -- trigger ubus connection before dropping root privs
404                 util.ubus()
405
406                 sys.process.setuser(track.setuser)
407         end
408
409         local target = nil
410         if c then
411                 if type(c.target) == "function" then
412                         target = c.target
413                 elseif type(c.target) == "table" then
414                         target = c.target.target
415                 end
416         end
417
418         if c and (c.index or type(target) == "function") then
419                 ctx.dispatched = c
420                 ctx.requested = ctx.requested or ctx.dispatched
421         end
422
423         if c and c.index then
424                 local tpl = require "luci.template"
425
426                 if util.copcall(tpl.render, "indexer", {}) then
427                         return true
428                 end
429         end
430
431         if type(target) == "function" then
432                 util.copcall(function()
433                         local oldenv = getfenv(target)
434                         local module = require(c.module)
435                         local env = setmetatable({}, {__index=
436
437                         function(tbl, key)
438                                 return rawget(tbl, key) or module[key] or oldenv[key]
439                         end})
440
441                         setfenv(target, env)
442                 end)
443
444                 local ok, err
445                 if type(c.target) == "table" then
446                         ok, err = util.copcall(target, c.target, unpack(args))
447                 else
448                         ok, err = util.copcall(target, unpack(args))
449                 end
450                 assert(ok,
451                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
452                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
453                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
454         else
455                 local root = node()
456                 if not root or not root.target then
457                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
458                                  "Install luci-mod-admin-full and retry. " ..
459                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
460                 else
461                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
462                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
463                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
464                 end
465         end
466 end
467
468 function createindex()
469         local controllers = { }
470         local base = "%s/controller/" % util.libpath()
471         local _, path
472
473         for path in (fs.glob("%s*.lua" % base) or function() end) do
474                 controllers[#controllers+1] = path
475         end
476
477         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
478                 controllers[#controllers+1] = path
479         end
480
481         if indexcache then
482                 local cachedate = fs.stat(indexcache, "mtime")
483                 if cachedate then
484                         local realdate = 0
485                         for _, obj in ipairs(controllers) do
486                                 local omtime = fs.stat(obj, "mtime")
487                                 realdate = (omtime and omtime > realdate) and omtime or realdate
488                         end
489
490                         if cachedate > realdate and sys.process.info("uid") == 0 then
491                                 assert(
492                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
493                                         and fs.stat(indexcache, "modestr") == "rw-------",
494                                         "Fatal: Indexcache is not sane!"
495                                 )
496
497                                 index = loadfile(indexcache)()
498                                 return index
499                         end
500                 end
501         end
502
503         index = {}
504
505         for _, path in ipairs(controllers) do
506                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
507                 local mod = require(modname)
508                 assert(mod ~= true,
509                        "Invalid controller file found\n" ..
510                        "The file '" .. path .. "' contains an invalid module line.\n" ..
511                        "Please verify whether the module name is set to '" .. modname ..
512                        "' - It must correspond to the file path!")
513
514                 local idx = mod.index
515                 assert(type(idx) == "function",
516                        "Invalid controller file found\n" ..
517                        "The file '" .. path .. "' contains no index() function.\n" ..
518                        "Please make sure that the controller contains a valid " ..
519                        "index function and verify the spelling!")
520
521                 index[modname] = idx
522         end
523
524         if indexcache then
525                 local f = nixio.open(indexcache, "w", 600)
526                 f:writeall(util.get_bytecode(index))
527                 f:close()
528         end
529 end
530
531 -- Build the index before if it does not exist yet.
532 function createtree()
533         if not index then
534                 createindex()
535         end
536
537         local ctx  = context
538         local tree = {nodes={}, inreq=true}
539         local modi = {}
540
541         ctx.treecache = setmetatable({}, {__mode="v"})
542         ctx.tree = tree
543         ctx.modifiers = modi
544
545         -- Load default translation
546         require "luci.i18n".loadc("base")
547
548         local scope = setmetatable({}, {__index = luci.dispatcher})
549
550         for k, v in pairs(index) do
551                 scope._NAME = k
552                 setfenv(v, scope)
553                 v()
554         end
555
556         local function modisort(a,b)
557                 return modi[a].order < modi[b].order
558         end
559
560         for _, v in util.spairs(modi, modisort) do
561                 scope._NAME = v.module
562                 setfenv(v.func, scope)
563                 v.func()
564         end
565
566         return tree
567 end
568
569 function modifier(func, order)
570         context.modifiers[#context.modifiers+1] = {
571                 func = func,
572                 order = order or 0,
573                 module
574                         = getfenv(2)._NAME
575         }
576 end
577
578 function assign(path, clone, title, order)
579         local obj  = node(unpack(path))
580         obj.nodes  = nil
581         obj.module = nil
582
583         obj.title = title
584         obj.order = order
585
586         setmetatable(obj, {__index = _create_node(clone)})
587
588         return obj
589 end
590
591 function entry(path, target, title, order)
592         local c = node(unpack(path))
593
594         c.target = target
595         c.title  = title
596         c.order  = order
597         c.module = getfenv(2)._NAME
598
599         return c
600 end
601
602 -- enabling the node.
603 function get(...)
604         return _create_node({...})
605 end
606
607 function node(...)
608         local c = _create_node({...})
609
610         c.module = getfenv(2)._NAME
611         c.auto = nil
612
613         return c
614 end
615
616 function _create_node(path)
617         if #path == 0 then
618                 return context.tree
619         end
620
621         local name = table.concat(path, ".")
622         local c = context.treecache[name]
623
624         if not c then
625                 local last = table.remove(path)
626                 local parent = _create_node(path)
627
628                 c = {nodes={}, auto=true}
629                 -- the node is "in request" if the request path matches
630                 -- at least up to the length of the node path
631                 if parent.inreq and context.path[#path+1] == last then
632                   c.inreq = true
633                 end
634                 parent.nodes[last] = c
635                 context.treecache[name] = c
636         end
637         return c
638 end
639
640 -- Subdispatchers --
641
642 function _firstchild()
643    local path = { unpack(context.path) }
644    local name = table.concat(path, ".")
645    local node = context.treecache[name]
646
647    local lowest
648    if node and node.nodes and next(node.nodes) then
649           local k, v
650           for k, v in pairs(node.nodes) do
651                  if not lowest or
652                         (v.order or 100) < (node.nodes[lowest].order or 100)
653                  then
654                         lowest = k
655                  end
656           end
657    end
658
659    assert(lowest ~= nil,
660                   "The requested node contains no childs, unable to redispatch")
661
662    path[#path+1] = lowest
663    dispatch(path)
664 end
665
666 function firstchild()
667    return { type = "firstchild", target = _firstchild }
668 end
669
670 function alias(...)
671         local req = {...}
672         return function(...)
673                 for _, r in ipairs({...}) do
674                         req[#req+1] = r
675                 end
676
677                 dispatch(req)
678         end
679 end
680
681 function rewrite(n, ...)
682         local req = {...}
683         return function(...)
684                 local dispatched = util.clone(context.dispatched)
685
686                 for i=1,n do
687                         table.remove(dispatched, 1)
688                 end
689
690                 for i, r in ipairs(req) do
691                         table.insert(dispatched, i, r)
692                 end
693
694                 for _, r in ipairs({...}) do
695                         dispatched[#dispatched+1] = r
696                 end
697
698                 dispatch(dispatched)
699         end
700 end
701
702
703 local function _call(self, ...)
704         local func = getfenv()[self.name]
705         assert(func ~= nil,
706                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
707
708         assert(type(func) == "function",
709                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
710                'of type "' .. type(func) .. '".')
711
712         if #self.argv > 0 then
713                 return func(unpack(self.argv), ...)
714         else
715                 return func(...)
716         end
717 end
718
719 function call(name, ...)
720         return {type = "call", argv = {...}, name = name, target = _call}
721 end
722
723 function post(name, ...)
724         return {
725                 type = "call",
726                 post = true,
727                 argv = { ... },
728                 name = name,
729                 target = _call
730         }
731 end
732
733
734 local _template = function(self, ...)
735         require "luci.template".render(self.view)
736 end
737
738 function template(name)
739         return {type = "template", view = name, target = _template}
740 end
741
742
743 local function _cbi(self, ...)
744         local cbi = require "luci.cbi"
745         local tpl = require "luci.template"
746         local http = require "luci.http"
747         local disp = require "luci.dispatcher"
748
749         if http.formvalue("cbi.submit") == "1" and
750            http.formvalue("token") ~= disp.context.urltoken.stok
751         then
752                 http.status(403, "Forbidden")
753                 luci.template.render("csrftoken")
754                 return
755         end
756
757         local config = self.config or {}
758         local maps = cbi.load(self.model, ...)
759
760         local state = nil
761
762         for i, res in ipairs(maps) do
763                 res.flow = config
764                 local cstate = res:parse()
765                 if cstate and (not state or cstate < state) then
766                         state = cstate
767                 end
768         end
769
770         local function _resolve_path(path)
771                 return type(path) == "table" and build_url(unpack(path)) or path
772         end
773
774         if config.on_valid_to and state and state > 0 and state < 2 then
775                 http.redirect(_resolve_path(config.on_valid_to))
776                 return
777         end
778
779         if config.on_changed_to and state and state > 1 then
780                 http.redirect(_resolve_path(config.on_changed_to))
781                 return
782         end
783
784         if config.on_success_to and state and state > 0 then
785                 http.redirect(_resolve_path(config.on_success_to))
786                 return
787         end
788
789         if config.state_handler then
790                 if not config.state_handler(state, maps) then
791                         return
792                 end
793         end
794
795         http.header("X-CBI-State", state or 0)
796
797         if not config.noheader then
798                 tpl.render("cbi/header", {state = state})
799         end
800
801         local redirect
802         local messages
803         local applymap   = false
804         local pageaction = true
805         local parsechain = { }
806
807         for i, res in ipairs(maps) do
808                 if res.apply_needed and res.parsechain then
809                         local c
810                         for _, c in ipairs(res.parsechain) do
811                                 parsechain[#parsechain+1] = c
812                         end
813                         applymap = true
814                 end
815
816                 if res.redirect then
817                         redirect = redirect or res.redirect
818                 end
819
820                 if res.pageaction == false then
821                         pageaction = false
822                 end
823
824                 if res.message then
825                         messages = messages or { }
826                         messages[#messages+1] = res.message
827                 end
828         end
829
830         for i, res in ipairs(maps) do
831                 res:render({
832                         firstmap   = (i == 1),
833                         applymap   = applymap,
834                         redirect   = redirect,
835                         messages   = messages,
836                         pageaction = pageaction,
837                         parsechain = parsechain
838                 })
839         end
840
841         if not config.nofooter then
842                 tpl.render("cbi/footer", {
843                         flow       = config,
844                         pageaction = pageaction,
845                         redirect   = redirect,
846                         state      = state,
847                         autoapply  = config.autoapply
848                 })
849         end
850 end
851
852 function cbi(model, config)
853         return {type = "cbi", config = config, model = model, target = _cbi}
854 end
855
856
857 local function _arcombine(self, ...)
858         local argv = {...}
859         local target = #argv > 0 and self.targets[2] or self.targets[1]
860         setfenv(target.target, self.env)
861         target:target(unpack(argv))
862 end
863
864 function arcombine(trg1, trg2)
865         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
866 end
867
868
869 local function _form(self, ...)
870         local cbi = require "luci.cbi"
871         local tpl = require "luci.template"
872         local http = require "luci.http"
873         local disp = require "luci.dispatcher"
874
875         if http.formvalue("cbi.submit") == "1" and
876            http.formvalue("token") ~= disp.context.urltoken.stok
877         then
878                 http.status(403, "Forbidden")
879                 luci.template.render("csrftoken")
880                 return
881         end
882
883         local maps = luci.cbi.load(self.model, ...)
884         local state = nil
885
886         for i, res in ipairs(maps) do
887                 local cstate = res:parse()
888                 if cstate and (not state or cstate < state) then
889                         state = cstate
890                 end
891         end
892
893         http.header("X-CBI-State", state or 0)
894         tpl.render("header")
895         for i, res in ipairs(maps) do
896                 res:render()
897         end
898         tpl.render("footer")
899 end
900
901 function form(model)
902         return {type = "cbi", model = model, target = _form}
903 end
904
905 translate = i18n.translate
906
907 -- This function does not actually translate the given argument but
908 -- is used by build/i18n-scan.pl to find translatable entries.
909 function _(text)
910         return text
911 end