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