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