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