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