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