libs/web: fix package name in error suggestion
[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 --- Create a redirect to another dispatching node.
642 -- @param       ...             Virtual path destination
643 function alias(...)
644         local req = {...}
645         return function(...)
646                 for _, r in ipairs({...}) do
647                         req[#req+1] = r
648                 end
649
650                 dispatch(req)
651         end
652 end
653
654 --- Rewrite the first x path values of the request.
655 -- @param       n               Number of path values to replace
656 -- @param       ...             Virtual path to replace removed path values with
657 function rewrite(n, ...)
658         local req = {...}
659         return function(...)
660                 local dispatched = util.clone(context.dispatched)
661
662                 for i=1,n do
663                         table.remove(dispatched, 1)
664                 end
665
666                 for i, r in ipairs(req) do
667                         table.insert(dispatched, i, r)
668                 end
669
670                 for _, r in ipairs({...}) do
671                         dispatched[#dispatched+1] = r
672                 end
673
674                 dispatch(dispatched)
675         end
676 end
677
678
679 local function _call(self, ...)
680         if #self.argv > 0 then
681                 return getfenv()[self.name](unpack(self.argv), ...)
682         else
683                 return getfenv()[self.name](...)
684         end
685 end
686
687 --- Create a function-call dispatching target.
688 -- @param       name    Target function of local controller
689 -- @param       ...             Additional parameters passed to the function
690 function call(name, ...)
691         return {type = "call", argv = {...}, name = name, target = _call}
692 end
693
694
695 local _template = function(self, ...)
696         require "luci.template".render(self.view)
697 end
698
699 --- Create a template render dispatching target.
700 -- @param       name    Template to be rendered
701 function template(name)
702         return {type = "template", view = name, target = _template}
703 end
704
705
706 local function _cbi(self, ...)
707         local cbi = require "luci.cbi"
708         local tpl = require "luci.template"
709         local http = require "luci.http"
710
711         local config = self.config or {}
712         local maps = cbi.load(self.model, ...)
713
714         local state = nil
715
716         for i, res in ipairs(maps) do
717                 res.flow = config
718                 local cstate = res:parse()
719                 if cstate and (not state or cstate < state) then
720                         state = cstate
721                 end
722         end
723
724         local function _resolve_path(path)
725                 return type(path) == "table" and build_url(unpack(path)) or path
726         end
727
728         if config.on_valid_to and state and state > 0 and state < 2 then
729                 http.redirect(_resolve_path(config.on_valid_to))
730                 return
731         end
732
733         if config.on_changed_to and state and state > 1 then
734                 http.redirect(_resolve_path(config.on_changed_to))
735                 return
736         end
737
738         if config.on_success_to and state and state > 0 then
739                 http.redirect(_resolve_path(config.on_success_to))
740                 return
741         end
742
743         if config.state_handler then
744                 if not config.state_handler(state, maps) then
745                         return
746                 end
747         end
748
749         http.header("X-CBI-State", state or 0)
750
751         if not config.noheader then
752                 tpl.render("cbi/header", {state = state})
753         end
754
755         local redirect
756         local messages
757         local applymap   = false
758         local pageaction = true
759         local parsechain = { }
760
761         for i, res in ipairs(maps) do
762                 if res.apply_needed and res.parsechain then
763                         local c
764                         for _, c in ipairs(res.parsechain) do
765                                 parsechain[#parsechain+1] = c
766                         end
767                         applymap = true
768                 end
769
770                 if res.redirect then
771                         redirect = redirect or res.redirect
772                 end
773
774                 if res.pageaction == false then
775                         pageaction = false
776                 end
777
778                 if res.message then
779                         messages = messages or { }
780                         messages[#messages+1] = res.message
781                 end
782         end
783
784         for i, res in ipairs(maps) do
785                 res:render({
786                         firstmap   = (i == 1),
787                         applymap   = applymap,
788                         redirect   = redirect,
789                         messages   = messages,
790                         pageaction = pageaction,
791                         parsechain = parsechain
792                 })
793         end
794
795         if not config.nofooter then
796                 tpl.render("cbi/footer", {
797                         flow       = config,
798                         pageaction = pageaction,
799                         redirect   = redirect,
800                         state      = state,
801                         autoapply  = config.autoapply
802                 })
803         end
804 end
805
806 --- Create a CBI model dispatching target.
807 -- @param       model   CBI model to be rendered
808 function cbi(model, config)
809         return {type = "cbi", config = config, model = model, target = _cbi}
810 end
811
812
813 local function _arcombine(self, ...)
814         local argv = {...}
815         local target = #argv > 0 and self.targets[2] or self.targets[1]
816         setfenv(target.target, self.env)
817         target:target(unpack(argv))
818 end
819
820 --- Create a combined dispatching target for non argv and argv requests.
821 -- @param trg1  Overview Target
822 -- @param trg2  Detail Target
823 function arcombine(trg1, trg2)
824         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
825 end
826
827
828 local function _form(self, ...)
829         local cbi = require "luci.cbi"
830         local tpl = require "luci.template"
831         local http = require "luci.http"
832
833         local maps = luci.cbi.load(self.model, ...)
834         local state = nil
835
836         for i, res in ipairs(maps) do
837                 local cstate = res:parse()
838                 if cstate and (not state or cstate < state) then
839                         state = cstate
840                 end
841         end
842
843         http.header("X-CBI-State", state or 0)
844         tpl.render("header")
845         for i, res in ipairs(maps) do
846                 res:render()
847         end
848         tpl.render("footer")
849 end
850
851 --- Create a CBI form model dispatching target.
852 -- @param       model   CBI form model tpo be rendered
853 function form(model)
854         return {type = "cbi", model = model, target = _form}
855 end
856
857 --- Access the luci.i18n translate() api.
858 -- @class  function
859 -- @name   translate
860 -- @param  text    Text to translate
861 translate = i18n.translate
862
863 --- No-op function used to mark translation entries for menu labels.
864 -- This function does not actually translate the given argument but
865 -- is used by build/i18n-scan.pl to find translatable entries.
866 function _(text)
867         return text
868 end