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