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.
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"
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
20 local function check_fs_depends(fs)
21 local fs = require "nixio.fs"
23 for path, kind in pairs(fs) do
24 if kind == "directory" then
26 for entry in (fs.dir(path) or function() end) do
33 elseif kind == "executable" then
34 if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then
37 elseif kind == "file" then
38 if fs.stat(path, "type") ~= "reg" then
47 local function check_uci_depends_options(conf, s, opts)
48 local uci = require "luci.model.uci"
50 if type(opts) == "string" then
51 return (s[".type"] == opts)
52 elseif opts == true then
53 for option, value in pairs(s) do
54 if option:byte(1) ~= 46 then
58 elseif type(opts) == "table" then
59 for option, value in pairs(opts) do
60 local sval = s[option]
61 if type(sval) == "table" then
63 for _, v in ipairs(sval) do
72 elseif value == true then
87 local function check_uci_depends_section(conf, sect)
88 local uci = require "luci.model.uci"
90 for section, options in pairs(sect) do
91 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
94 uci:foreach(conf, stype, function(s)
95 if check_uci_depends_options(conf, s, options) then
104 local s = uci:get_all(conf, section)
105 if not s or not check_uci_depends_options(conf, s, options) then
114 local function check_uci_depends(conf)
115 local uci = require "luci.model.uci"
117 for config, values in pairs(conf) do
118 if values == true then
120 uci:foreach(config, nil, function(s)
127 elseif type(values) == "table" then
128 if not check_uci_depends_section(config, values) then
137 local function check_depends(spec)
138 if type(spec.depends) ~= "table" then
142 if type(spec.depends.fs) == "table" and not check_fs_depends(spec.depends.fs) then
143 local satisfied = false
144 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
145 for _, alternative in ipairs(alternatives) do
146 if check_fs_depends(alternative) then
151 if not satisfied then
156 if type(spec.depends.uci) == "table" then
157 local satisfied = false
158 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
159 for _, alternative in ipairs(alternatives) do
160 if check_uci_depends(alternative) then
165 if not satisfied then
173 local function target_to_json(target, module)
176 if target.type == "call" then
180 ["function"] = target.name,
181 ["parameters"] = target.argv
183 elseif target.type == "view" then
186 ["path"] = target.view
188 elseif target.type == "template" then
190 ["type"] = "template",
191 ["path"] = target.view
193 elseif target.type == "cbi" then
196 ["path"] = target.model
198 elseif target.type == "form" then
201 ["path"] = target.model
203 elseif target.type == "firstchild" then
205 ["type"] = "firstchild"
207 elseif target.type == "firstnode" then
209 ["type"] = "firstchild",
212 elseif target.type == "arcombine" then
213 if type(target.targets) == "table" then
215 ["type"] = "arcombine",
217 target_to_json(target.targets[1], module),
218 target_to_json(target.targets[2], module)
222 elseif target.type == "alias" then
225 ["path"] = table.concat(target.req, "/")
227 elseif target.type == "rewrite" then
229 ["type"] = "rewrite",
230 ["path"] = table.concat(target.req, "/"),
231 ["remove"] = target.n
235 if target.post and action then
236 action.post = target.post
242 local function tree_to_json(node, json)
243 local fs = require "nixio.fs"
244 local util = require "luci.util"
246 if type(node.nodes) == "table" then
247 for subname, subnode in pairs(node.nodes) do
249 title = util.striptags(subnode.title),
250 order = subnode.order
261 if subnode.setuser then
262 spec.setuser = subnode.setuser
265 if subnode.setgroup then
266 spec.setgroup = subnode.setgroup
269 if type(subnode.target) == "table" then
270 spec.action = target_to_json(subnode.target, subnode.module)
273 if type(subnode.file_depends) == "table" then
274 for _, v in ipairs(subnode.file_depends) do
275 spec.depends = spec.depends or {}
276 spec.depends.fs = spec.depends.fs or {}
278 local ft = fs.stat(v, "type")
280 spec.depends.fs[v] = "directory"
281 elseif v:match("/s?bin/") then
282 spec.depends.fs[v] = "executable"
284 spec.depends.fs[v] = "file"
289 if type(subnode.uci_depends) == "table" then
290 for k, v in pairs(subnode.uci_depends) do
291 spec.depends = spec.depends or {}
292 spec.depends.uci = spec.depends.uci or {}
293 spec.depends.uci[k] = v
297 if (subnode.sysauth_authenticator ~= nil) or
298 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
300 if subnode.sysauth_authenticator == "htmlauth" then
303 methods = { "cookie:sysauth" }
305 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
308 methods = { "param:auth", "cookie:sysauth" }
310 elseif subnode.module == "luci.controller.admin.uci" then
313 methods = { "param:sid" }
316 elseif subnode.sysauth == false then
320 if not spec.action then
324 spec.satisfied = check_depends(spec)
325 json.children = json.children or {}
326 json.children[subname] = tree_to_json(subnode, spec)
333 function build_url(...)
335 local url = { http.getenv("SCRIPT_NAME") or "" }
338 for _, p in ipairs(path) do
339 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
349 return table.concat(url, "")
353 function error404(message)
354 http.status(404, "Not Found")
355 message = message or "Not Found"
357 local function render()
358 local template = require "luci.template"
359 template.render("error404")
362 if not util.copcall(render) then
363 http.prepare_content("text/plain")
370 function error500(message)
372 if not context.template_header_sent then
373 http.status(500, "Internal Server Error")
374 http.prepare_content("text/plain")
377 require("luci.template")
378 if not util.copcall(luci.template.render, "error500", {message=message}) then
379 http.prepare_content("text/plain")
386 local function determine_request_language()
387 local conf = require "luci.config"
388 assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
390 local lang = conf.main.lang or "auto"
391 if lang == "auto" then
392 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
393 for aclang in aclang:gmatch("[%w_-]+") do
394 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
395 if country and culture then
396 local cc = "%s_%s" %{ country, culture:lower() }
397 if conf.languages[cc] then
400 elseif conf.languages[country] then
404 elseif conf.languages[aclang] then
411 if lang == "auto" then
415 i18n.setlanguage(lang)
418 function httpdispatch(request, prefix)
419 http.context.request = request
424 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
427 for _, node in ipairs(prefix) do
433 for node in pathinfo:gmatch("[^/%z]+") do
437 determine_request_language()
439 local stat, err = util.coxpcall(function()
440 dispatch(context.request)
445 --context._disable_memtrace()
448 local function require_post_security(target, args)
449 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
450 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
453 if type(target) == "table" then
454 if type(target.post) == "table" then
455 local param_name, required_val, request_val
457 for param_name, required_val in pairs(target.post) do
458 request_val = http.formvalue(param_name)
460 if (type(required_val) == "string" and
461 request_val ~= required_val) or
462 (required_val == true and request_val == nil)
471 return (target.post == true)
477 function test_post_security()
478 if http.getenv("REQUEST_METHOD") ~= "POST" then
479 http.status(405, "Method Not Allowed")
480 http.header("Allow", "POST")
484 if http.formvalue("token") ~= context.authtoken then
485 http.status(403, "Forbidden")
486 luci.template.render("csrftoken")
493 local function session_retrieve(sid, allowed_users)
494 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
496 if type(sdat) == "table" and
497 type(sdat.values) == "table" and
498 type(sdat.values.token) == "string" and
499 (not allowed_users or
500 util.contains(allowed_users, sdat.values.username))
502 uci:set_session_id(sid)
503 return sid, sdat.values
509 local function session_setup(user, pass, allowed_users)
510 if util.contains(allowed_users, user) then
511 local login = util.ubus("session", "login", {
514 timeout = tonumber(luci.config.sauth.sessiontime)
517 local rp = context.requestpath
518 and table.concat(context.requestpath, "/") or ""
520 if type(login) == "table" and
521 type(login.ubus_rpc_session) == "string"
523 util.ubus("session", "set", {
524 ubus_rpc_session = login.ubus_rpc_session,
525 values = { token = sys.uniqueid(16) }
528 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
529 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
531 return session_retrieve(login.ubus_rpc_session)
534 io.stderr:write("luci: failed login on /%s for %s from %s\n"
535 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
541 local function check_authentication(method)
542 local auth_type, auth_param = method:match("^(%w+):(.+)$")
545 if auth_type == "cookie" then
546 sid = http.getcookie(auth_param)
547 elseif auth_type == "param" then
548 sid = http.formvalue(auth_param)
551 return session_retrieve(sid)
554 local function get_children(node)
557 if not node.wildcard and type(node.children) == "table" then
558 for name, child in pairs(node.children) do
559 children[#children+1] = {
562 order = child.order or 1000
566 table.sort(children, function(a, b)
567 if a.order == b.order then
568 return a.name < b.name
570 return a.order < b.order
578 local function find_subnode(root, prefix, recurse, descended)
579 local children = get_children(root)
581 if #children > 0 and (not descended or recurse) then
582 local sub_path = { unpack(prefix) }
584 if recurse == false then
588 for _, child in ipairs(children) do
589 sub_path[#prefix+1] = child.name
591 local res_path = find_subnode(child.node, sub_path, recurse, true)
601 root.action.type == "cbi" or
602 root.action.type == "form" or
603 root.action.type == "view" or
604 root.action.type == "template" or
605 root.action.type == "arcombine"
613 local tree = context.tree or createtree()
614 return tree_to_json(tree, {
616 ["type"] = "firstchild",
622 local function init_template_engine(ctx)
623 local tpl = require "luci.template"
624 local media = luci.config.main.mediaurlbase
626 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
628 for name, theme in pairs(luci.config.themes) do
629 if name:sub(1,1) ~= "." and pcall(tpl.Template,
630 "themes/%s/header" % fs.basename(theme)) then
634 assert(media, "No valid theme found")
637 local function _ifattr(cond, key, val, noescape)
639 local env = getfenv(3)
640 local scope = (type(env.self) == "table") and env.self
641 if type(val) == "table" then
642 if not next(val) then
645 val = util.serialize_json(val)
649 val = tostring(val or
650 (type(env[key]) ~= "function" and env[key]) or
651 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
653 if noescape ~= true then
654 val = util.pcdata(val)
657 return string.format(' %s="%s"', tostring(key), val)
663 tpl.context.viewns = setmetatable({
665 include = function(name) tpl.Template(name):render(getfenv(2)) end;
666 translate = i18n.translate;
667 translatef = i18n.translatef;
668 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
669 striptags = util.striptags;
670 pcdata = util.pcdata;
672 theme = fs.basename(media);
673 resource = luci.config.main.resourcebase;
674 ifattr = function(...) return _ifattr(...) end;
675 attr = function(...) return _ifattr(true, ...) end;
677 }, {__index=function(tbl, key)
678 if key == "controller" then
680 elseif key == "REQUEST_URI" then
681 return build_url(unpack(ctx.requestpath))
682 elseif key == "FULL_REQUEST_URI" then
683 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
684 local query = http.getenv("QUERY_STRING")
685 if query and #query > 0 then
689 return table.concat(url, "")
690 elseif key == "token" then
693 return rawget(tbl, key) or _G[key]
700 function dispatch(request)
701 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
704 local auth, cors, suid, sgid
705 local menu = menu_json()
708 local requested_path_full = {}
709 local requested_path_node = {}
710 local requested_path_args = {}
712 for i, s in ipairs(request) do
713 if type(page.children) ~= "table" or not page.children[s] then
718 if not page.children[s].satisfied then
723 page = page.children[s]
724 auth = page.auth or auth
725 cors = page.cors or cors
726 suid = page.setuser or suid
727 sgid = page.setgroup or sgid
729 requested_path_full[i] = s
730 requested_path_node[i] = s
732 if page.wildcard then
733 for j = i + 1, #request do
734 requested_path_args[j - i] = request[j]
735 requested_path_full[j] = request[j]
741 local tpl = init_template_engine(ctx)
743 ctx.args = requested_path_args
744 ctx.path = requested_path_node
745 ctx.dispatched = page
747 ctx.requestpath = ctx.requestpath or requested_path_full
748 ctx.requestargs = ctx.requestargs or requested_path_args
749 ctx.requested = ctx.requested or page
751 if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
753 for _, method in ipairs(auth.methods) do
754 sid, sdat = check_authentication(method)
761 if not (sid and sdat) and auth.login then
762 local user = http.getenv("HTTP_AUTH_USER")
763 local pass = http.getenv("HTTP_AUTH_PASS")
765 if user == nil and pass == nil then
766 user = http.formvalue("luci_username")
767 pass = http.formvalue("luci_password")
770 sid, sdat = session_setup(user, pass, { "root" })
775 http.status(403, "Forbidden")
776 http.header("X-LuCI-Login-Required", "yes")
778 return tpl.render("sysauth", { duser = "root", fuser = user })
781 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
782 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
785 http.redirect(build_url(unpack(ctx.requestpath)))
789 if not sid or not sdat then
790 http.status(403, "Forbidden")
791 http.header("X-LuCI-Login-Required", "yes")
795 ctx.authsession = sid
796 ctx.authtoken = sdat.token
797 ctx.authuser = sdat.username
800 local action = (page and type(page.action) == "table") and page.action or {}
802 if action.type == "arcombine" then
803 action = (#requested_path_args > 0) and action.targets[2] or action.targets[1]
806 if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
807 luci.http.status(200, "OK")
808 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
809 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
813 if require_post_security(action) then
814 if not test_post_security() then
820 sys.process.setgroup(sgid)
824 sys.process.setuser(suid)
827 if action.type == "view" then
828 tpl.render("view", { view = action.path })
830 elseif action.type == "call" then
831 local ok, mod = util.copcall(require, action.module)
837 local func = mod[action["function"]]
840 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
842 assert(type(func) == "function",
843 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
844 'of type "' .. type(func) .. '".')
846 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
847 for _, s in ipairs(requested_path_args) do
851 local ok, err = util.copcall(func, unpack(argv))
856 elseif action.type == "firstchild" then
857 local sub_request = find_subnode(page, requested_path_full, action.recurse)
859 dispatch(sub_request)
861 tpl.render("empty_node_placeholder", getfenv(1))
864 elseif action.type == "alias" then
865 local sub_request = {}
866 for name in action.path:gmatch("[^/]+") do
867 sub_request[#sub_request + 1] = name
870 for _, s in ipairs(requested_path_args) do
871 sub_request[#sub_request + 1] = s
874 dispatch(sub_request)
876 elseif action.type == "rewrite" then
877 local sub_request = { unpack(request) }
878 for i = 1, action.remove do
879 table.remove(sub_request, 1)
883 for s in action.path:gmatch("[^/]+") do
884 table.insert(sub_request, n, s)
888 for _, s in ipairs(requested_path_args) do
889 sub_request[#sub_request + 1] = s
892 dispatch(sub_request)
894 elseif action.type == "template" then
895 tpl.render(action.path, getfenv(1))
897 elseif action.type == "cbi" then
898 _cbi({ config = action.config, model = action.path }, unpack(requested_path_args))
900 elseif action.type == "form" then
901 _form({ model = action.path }, unpack(requested_path_args))
904 local root = find_subnode(menu, {}, true)
906 error404("No root node was registered, this usually happens if no module was installed.\n" ..
907 "Install luci-mod-admin-full and retry. " ..
908 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
910 error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" ..
911 "If this url belongs to an extension, make sure it is properly installed.\n" ..
912 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
917 function createindex()
918 local controllers = { }
919 local base = "%s/controller/" % util.libpath()
922 for path in (fs.glob("%s*.lua" % base) or function() end) do
923 controllers[#controllers+1] = path
926 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
927 controllers[#controllers+1] = path
931 local cachedate = fs.stat(indexcache, "mtime")
934 for _, obj in ipairs(controllers) do
935 local omtime = fs.stat(obj, "mtime")
936 realdate = (omtime and omtime > realdate) and omtime or realdate
939 if cachedate > realdate and sys.process.info("uid") == 0 then
941 sys.process.info("uid") == fs.stat(indexcache, "uid")
942 and fs.stat(indexcache, "modestr") == "rw-------",
943 "Fatal: Indexcache is not sane!"
946 index = loadfile(indexcache)()
954 for _, path in ipairs(controllers) do
955 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
956 local mod = require(modname)
958 "Invalid controller file found\n" ..
959 "The file '" .. path .. "' contains an invalid module line.\n" ..
960 "Please verify whether the module name is set to '" .. modname ..
961 "' - It must correspond to the file path!")
963 local idx = mod.index
964 assert(type(idx) == "function",
965 "Invalid controller file found\n" ..
966 "The file '" .. path .. "' contains no index() function.\n" ..
967 "Please make sure that the controller contains a valid " ..
968 "index function and verify the spelling!")
974 local f = nixio.open(indexcache, "w", 600)
975 f:writeall(util.get_bytecode(index))
980 -- Build the index before if it does not exist yet.
981 function createtree()
987 local tree = {nodes={}, inreq=true}
989 ctx.treecache = setmetatable({}, {__mode="v"})
992 local scope = setmetatable({}, {__index = luci.dispatcher})
994 for k, v in pairs(index) do
1003 function assign(path, clone, title, order)
1004 local obj = node(unpack(path))
1011 setmetatable(obj, {__index = _create_node(clone)})
1016 function entry(path, target, title, order)
1017 local c = node(unpack(path))
1022 c.module = getfenv(2)._NAME
1027 -- enabling the node.
1029 return _create_node({...})
1033 local c = _create_node({...})
1035 c.module = getfenv(2)._NAME
1041 function lookup(...)
1042 local i, path = nil, {}
1043 for i = 1, select('#', ...) do
1044 local name, arg = nil, tostring(select(i, ...))
1045 for name in arg:gmatch("[^/]+") do
1046 path[#path+1] = name
1050 for i = #path, 1, -1 do
1051 local node = context.treecache[table.concat(path, ".", 1, i)]
1052 if node and (i == #path or node.leaf) then
1053 return node, build_url(unpack(path))
1058 function _create_node(path)
1063 local name = table.concat(path, ".")
1064 local c = context.treecache[name]
1067 local last = table.remove(path)
1068 local parent = _create_node(path)
1070 c = {nodes={}, auto=true, inreq=true}
1072 parent.nodes[last] = c
1073 context.treecache[name] = c
1079 -- Subdispatchers --
1081 function firstchild()
1082 return { type = "firstchild" }
1085 function firstnode()
1086 return { type = "firstnode" }
1090 return { type = "alias", req = { ... } }
1093 function rewrite(n, ...)
1094 return { type = "rewrite", n = n, req = { ... } }
1097 function call(name, ...)
1098 return { type = "call", argv = {...}, name = name }
1101 function post_on(params, name, ...)
1111 return post_on(true, ...)
1115 function template(name)
1116 return { type = "template", view = name }
1120 return { type = "view", view = name }
1124 function _cbi(self, ...)
1125 local cbi = require "luci.cbi"
1126 local tpl = require "luci.template"
1127 local http = require "luci.http"
1129 local config = self.config or {}
1130 local maps = cbi.load(self.model, ...)
1135 for i, res in ipairs(maps) do
1136 if util.instanceof(res, cbi.SimpleForm) then
1137 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1140 io.stderr:write("please change %s to use the form() action instead.\n"
1141 % table.concat(context.request, "/"))
1145 local cstate = res:parse()
1146 if cstate and (not state or cstate < state) then
1151 local function _resolve_path(path)
1152 return type(path) == "table" and build_url(unpack(path)) or path
1155 if config.on_valid_to and state and state > 0 and state < 2 then
1156 http.redirect(_resolve_path(config.on_valid_to))
1160 if config.on_changed_to and state and state > 1 then
1161 http.redirect(_resolve_path(config.on_changed_to))
1165 if config.on_success_to and state and state > 0 then
1166 http.redirect(_resolve_path(config.on_success_to))
1170 if config.state_handler then
1171 if not config.state_handler(state, maps) then
1176 http.header("X-CBI-State", state or 0)
1178 if not config.noheader then
1179 tpl.render("cbi/header", {state = state})
1184 local applymap = false
1185 local pageaction = true
1186 local parsechain = { }
1188 for i, res in ipairs(maps) do
1189 if res.apply_needed and res.parsechain then
1191 for _, c in ipairs(res.parsechain) do
1192 parsechain[#parsechain+1] = c
1197 if res.redirect then
1198 redirect = redirect or res.redirect
1201 if res.pageaction == false then
1206 messages = messages or { }
1207 messages[#messages+1] = res.message
1211 for i, res in ipairs(maps) do
1213 firstmap = (i == 1),
1214 redirect = redirect,
1215 messages = messages,
1216 pageaction = pageaction,
1217 parsechain = parsechain
1221 if not config.nofooter then
1222 tpl.render("cbi/footer", {
1224 pageaction = pageaction,
1225 redirect = redirect,
1227 autoapply = config.autoapply,
1228 trigger_apply = applymap
1233 function cbi(model, config)
1236 post = { ["cbi.submit"] = true },
1243 function arcombine(trg1, trg2)
1247 targets = {trg1, trg2}
1252 function _form(self, ...)
1253 local cbi = require "luci.cbi"
1254 local tpl = require "luci.template"
1255 local http = require "luci.http"
1257 local maps = luci.cbi.load(self.model, ...)
1261 for i, res in ipairs(maps) do
1262 local cstate = res:parse()
1263 if cstate and (not state or cstate < state) then
1268 http.header("X-CBI-State", state or 0)
1269 tpl.render("header")
1270 for i, res in ipairs(maps) do
1273 tpl.render("footer")
1276 function form(model)
1279 post = { ["cbi.submit"] = true },
1284 translate = i18n.translate
1286 -- This function does not actually translate the given argument but
1287 -- is used by build/i18n-scan.pl to find translatable entries.