--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -28,30 +28,23 @@ function BrowserAction(options, extensio
let widgetId = makeWidgetId(extension.id);
this.id = `${widgetId}-browser-action`;
this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
this.widget = null;
this.tabManager = TabManager.for(extension);
- let title = extension.localize(options.default_title || "");
- let popup = extension.localize(options.default_popup || "");
- if (popup) {
- popup = extension.baseURI.resolve(popup);
- }
-
this.defaults = {
enabled: true,
- title: title || extension.name,
+ title: options.default_title || extension.name,
badgeText: "",
badgeBackgroundColor: null,
- icon: IconDetails.normalize({path: options.default_icon}, extension,
- null, true),
- popup: popup,
+ icon: IconDetails.normalize({path: options.default_icon}, extension),
+ popup: options.default_popup || "",
};
this.tabContext = new TabContext(tab => Object.create(this.defaults),
extension);
EventEmitter.decorate(this);
}
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -14,28 +14,21 @@ var pageActionMap = new WeakMap();
// Handles URL bar icons, including the |page_action| manifest entry
// and associated API.
function PageAction(options, extension) {
this.extension = extension;
this.id = makeWidgetId(extension.id) + "-page-action";
this.tabManager = TabManager.for(extension);
- let title = extension.localize(options.default_title || "");
- let popup = extension.localize(options.default_popup || "");
- if (popup) {
- popup = extension.baseURI.resolve(popup);
- }
-
this.defaults = {
show: false,
- title: title || extension.name,
- icon: IconDetails.normalize({path: options.default_icon}, extension,
- null, true),
- popup: popup && extension.baseURI.resolve(popup),
+ title: options.default_title || extension.name,
+ icon: IconDetails.normalize({path: options.default_icon}, extension),
+ popup: options.default_popup || "",
};
this.tabContext = new TabContext(tab => Object.create(this.defaults),
extension);
this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
// WeakMap[ChromeWindow -> <xul:image>]
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -31,17 +31,17 @@ global.IconDetails = {
// with icon size as key and icon URL as value.
//
// If a context is specified (function is called from an extension):
// Throws an error if an invalid icon size was provided or the
// extension is not allowed to load the specified resources.
//
// If no context is specified, instead of throwing an error, this
// function simply logs a warning message.
- normalize(details, extension, context = null, localize = false) {
+ normalize(details, extension, context = null) {
let result = {};
try {
if (details.imageData) {
let imageData = details.imageData;
// The global might actually be from Schema.jsm, which
// normalizes most of our arguments. In that case it won't have
@@ -68,22 +68,17 @@ global.IconDetails = {
let baseURI = context ? context.uri : extension.baseURI;
for (let size of Object.keys(path)) {
if (!INTEGER.test(size)) {
throw new Error(`Invalid icon size ${size}, must be an integer`);
}
- let url = path[size];
- if (localize) {
- url = extension.localize(url);
- }
-
- url = baseURI.resolve(path[size]);
+ let url = baseURI.resolve(path[size]);
// The Chrome documentation specifies these parameters as
// relative paths. We currently accept absolute URLs as well,
// which means we need to check that the extension is allowed
// to load them. This will throw an error if it's not allowed.
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
extension.principal, url,
Services.scriptSecurityManager.DISALLOW_SCRIPT);
--- a/browser/components/extensions/schemas/browser_action.json
+++ b/browser/components/extensions/schemas/browser_action.json
@@ -7,19 +7,31 @@
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"browser_action": {
"type": "object",
"properties": {
- "default_title": { "type": "string", "optional": true },
- "default_icon": { "$ref": "IconPath", "optional": true },
- "default_popup": { "type": "string", "format": "relativeUrl", "optional": true }
+ "default_title": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "default_icon": {
+ "$ref": "IconPath",
+ "optional": true
+ },
+ "default_popup": {
+ "type": "string",
+ "format": "relativeUrl",
+ "optional": true,
+ "preprocess": "localize"
+ }
},
"optional": true
}
}
}
]
},
{
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -7,19 +7,31 @@
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"page_action": {
"type": "object",
"properties": {
- "default_title": { "type": "string", "optional": true },
- "default_icon": { "$ref": "IconPath", "optional": true },
- "default_popup": { "type": "string", "format": "relativeUrl", "optional": true }
+ "default_title": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "default_icon": {
+ "$ref": "IconPath",
+ "optional": true
+ },
+ "default_popup": {
+ "type": "string",
+ "format": "relativeUrl",
+ "optional": true,
+ "preprocess": "localize"
+ }
},
"optional": true
}
}
}
]
},
{
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -77,16 +77,18 @@ function* runTests(options) {
nextTest();
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: options.manifest,
+ files: options.files || {},
+
background: `(${background})(${options.getTests})`,
});
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
function checkDetails(details) {
let button = document.getElementById(browserActionId);
@@ -135,22 +137,39 @@ function* runTests(options) {
yield extension.unload();
}
add_task(function* testTabSwitchContext() {
yield runTests({
manifest: {
"browser_action": {
"default_icon": "default.png",
- "default_popup": "default.html",
- "default_title": "Default Title",
+ "default_popup": "__MSG_popup__",
+ "default_title": "Default __MSG_title__",
},
+
+ "default_locale": "en",
+
"permissions": ["tabs"],
},
+ "files": {
+ "_locales/en/messages.json": {
+ "popup": {
+ "message": "default.html",
+ "description": "Popup",
+ },
+
+ "title": {
+ "message": "Title",
+ "description": "Title",
+ },
+ },
+ },
+
getTests(tabs, expectDefaults) {
let details = [
{"icon": browser.runtime.getURL("default.png"),
"popup": browser.runtime.getURL("default.html"),
"title": "Default Title",
"badge": "",
"badgeBackgroundColor": null},
{"icon": browser.runtime.getURL("1.png"),
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -77,16 +77,18 @@ function* runTests(options) {
});
runTests();
}
let extension = ExtensionTestUtils.loadExtension({
manifest: options.manifest,
+ files: options.files || {},
+
background: `(${background})(${options.getTests})`,
});
let pageActionId = makeWidgetId(extension.id) + "-page-action";
let currentWindow = window;
let windows = [];
function checkDetails(details) {
@@ -149,23 +151,39 @@ function* runTests(options) {
add_task(function* testTabSwitchContext() {
yield runTests({
manifest: {
"name": "Foo Extension",
"page_action": {
"default_icon": "default.png",
- "default_popup": "default.html",
- "default_title": "Default Title \u263a",
+ "default_popup": "__MSG_popup__",
+ "default_title": "Default __MSG_title__ \u263a",
},
+ "default_locale": "en",
+
"permissions": ["tabs"],
},
+ "files": {
+ "_locales/en/messages.json": {
+ "popup": {
+ "message": "default.html",
+ "description": "Popup",
+ },
+
+ "title": {
+ "message": "Title",
+ "description": "Title",
+ },
+ },
+ },
+
getTests(tabs) {
let details = [
{"icon": browser.runtime.getURL("default.png"),
"popup": browser.runtime.getURL("default.html"),
"title": "Default Title \u263a"},
{"icon": browser.runtime.getURL("1.png"),
"popup": browser.runtime.getURL("default.html"),
"title": "Default Title \u263a"},
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -634,30 +634,42 @@ ExtensionData.prototype = {
// Reads the extension's |manifest.json| file, and stores its
// parsed contents in |this.manifest|.
readManifest() {
return Promise.all([
this.readJSON("manifest.json"),
Management.lazyInit(),
]).then(([manifest]) => {
+ this.manifest = manifest;
+ this.rawManifest = manifest;
+
+ if (manifest && manifest.default_locale) {
+ return this.initLocale();
+ }
+ }).then(() => {
let context = {
url: this.baseURI && this.baseURI.spec,
principal: this.principal,
logError: error => {
this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
},
+
+ preprocessors: {},
};
- let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
+ if (this.localeData) {
+ context.preprocessors.localize = this.localize.bind(this);
+ }
+
+ let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
if (normalized.error) {
this.manifestError(normalized.error);
- this.manifest = manifest;
} else {
this.manifest = normalized.value;
}
try {
this.id = this.manifest.applications.gecko.id;
} catch (e) {
// Errors are handled by the type checks above.
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -78,29 +78,27 @@ function getValueBaseType(value) {
return t;
}
class Context {
constructor(params) {
this.params = params;
this.path = [];
+ this.preprocessors = {};
let props = ["addListener", "callFunction", "callAsyncFunction",
"hasListener", "removeListener",
- "getProperty", "setProperty"];
+ "getProperty", "setProperty",
+ "checkLoadURL", "logError",
+ "preprocessors"];
for (let prop of props) {
- this[prop] = params[prop];
- }
-
- if ("checkLoadURL" in params) {
- this.checkLoadURL = params.checkLoadURL;
- }
- if ("logError" in params) {
- this.logError = params.logError;
+ if (prop in params) {
+ this[prop] = params[prop];
+ }
}
}
get cloneScope() {
return this.params.cloneScope;
}
get url() {
@@ -262,16 +260,34 @@ class Entry {
*
* If the value is any other truthy value, a generic deprecation
* message will be emitted.
*/
this.deprecated = false;
if ("deprecated" in schema) {
this.deprecated = schema.deprecated;
}
+
+ /**
+ * If set to a string value, and a preprocessor of the same is
+ * defined in the validation context, it will be applied to this
+ * value prior to any normalization.
+ */
+ this.preprocessor = schema.preprocess || null;
+ }
+
+ /**
+ * Preprocess the given value with the preprocessor declared in
+ * `preprocessor`.
+ */
+ preprocess(value, context) {
+ if (this.preprocessor in context.preprocessors) {
+ return context.preprocessors[this.preprocessor](value, context);
+ }
+ return value;
}
/**
* Logs a deprecation warning for this entry, based on the value of
* its `deprecated` property.
*/
logDeprecation(context, value = null) {
let message = "This property is deprecated";
@@ -330,17 +346,17 @@ class Type extends Entry {
return false;
}
// Helper method that simply relies on checkBaseType to implement
// normalize. Subclasses can choose to use it or not.
normalizeBase(type, value, context) {
if (this.checkBaseType(getValueBaseType(value))) {
this.checkDeprecated(context, value);
- return {value};
+ return {value: this.preprocess(value, context)};
}
return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`);
}
}
// Type that allows any value.
class AnyType extends Type {
normalize(value, context) {
@@ -429,16 +445,17 @@ class StringType extends Type {
this.format = format;
}
normalize(value, context) {
let r = this.normalizeBase("string", value, context);
if (r.error) {
return r;
}
+ value = r.value;
if (this.enumeration) {
if (this.enumeration.includes(value)) {
return {value};
}
return context.error(`Invalid enumeration value ${JSON.stringify(value)}`);
}
@@ -505,16 +522,17 @@ class ObjectType extends Type {
return baseType == "object";
}
normalize(value, context) {
let v = this.normalizeBase("object", value, context);
if (v.error) {
return v;
}
+ value = v.value;
if (this.isInstanceOf) {
if (Object.keys(this.properties).length ||
this.patternProperties.length ||
!(this.additionalProperties instanceof AnyType)) {
throw new Error("InternalError: isInstanceOf can only be used with objects that are otherwise unrestricted");
}
@@ -634,17 +652,17 @@ class SubModuleType extends Type {
class NumberType extends Type {
normalize(value, context) {
let r = this.normalizeBase("number", value, context);
if (r.error) {
return r;
}
- if (isNaN(value) || !Number.isFinite(value)) {
+ if (isNaN(r.value) || !Number.isFinite(r.value)) {
return context.error("NaN or infinity are not valid");
}
return r;
}
checkBaseType(baseType) {
return baseType == "number" || baseType == "integer";
@@ -658,16 +676,17 @@ class IntegerType extends Type {
this.maximum = maximum;
}
normalize(value, context) {
let r = this.normalizeBase("integer", value, context);
if (r.error) {
return r;
}
+ value = r.value;
// Ensure it's between -2**31 and 2**31-1
if (!Number.isSafeInteger(value)) {
return context.error("Integer is out of range");
}
if (value < this.minimum) {
return context.error(`Integer ${value} is too small (must be at least ${this.minimum})`);
@@ -702,16 +721,17 @@ class ArrayType extends Type {
this.maxItems = maxItems;
}
normalize(value, context) {
let v = this.normalizeBase("array", value, context);
if (v.error) {
return v;
}
+ value = v.value;
let result = [];
for (let [i, element] of value.entries()) {
element = context.withPath(String(i), () => this.itemType.normalize(element, context));
if (element.error) {
return element;
}
result.push(element.value);
@@ -1027,17 +1047,17 @@ this.Schemas = {
ns.set(symbol, value);
},
parseType(path, type, extraProperties = []) {
let allowedProperties = new Set(extraProperties);
// Do some simple validation of our own schemas.
function checkTypeProperties(...extra) {
- let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated"]);
+ let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated", "preprocess"]);
for (let prop of Object.keys(type)) {
if (!allowedSet.has(prop)) {
throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`);
}
}
}
if ("choices" in type) {
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -71,25 +71,18 @@ BackgroundPage.prototype = {
if (event.target != window.document) {
return;
}
event.currentTarget.removeEventListener("load", loadListener, true);
if (this.scripts) {
let doc = window.document;
for (let script of this.scripts) {
- let url = this.extension.baseURI.resolve(script);
-
- if (!this.extension.isExtensionURL(url)) {
- this.extension.manifestError("Background scripts must be files within the extension");
- continue;
- }
-
let tag = doc.createElement("script");
- tag.setAttribute("src", url);
+ tag.setAttribute("src", script);
tag.async = false;
doc.body.appendChild(tag);
}
}
if (this.extension.onStartup) {
this.extension.onStartup();
}
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -38,33 +38,48 @@
}
}
}
}
},
"name": {
"type": "string",
- "optional": false
+ "optional": false,
+ "preprocess": "localize"
+ },
+
+ "short_name": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
},
"description": {
"type": "string",
- "optional": true
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "creator": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
},
"version": {
"type": "string",
"optional": false
},
"homepage_url": {
"type": "string",
"format": "url",
- "optional": true
+ "optional": true,
+ "preprocess": "localize"
},
"icons": {
"type": "object",
"optional": true,
"patternProperties": {
"^[1-9]\\d*$": { "type": "string" }
}
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -893,21 +893,25 @@ var loadManifestFromWebManifest = Task.a
// WebExtensions don't use iconURLs
addon.iconURL = null;
addon.icon64URL = null;
addon.icons = manifest.icons || {};
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
function getLocale(aLocale) {
+ // Use the raw manifest, here, since we need values with their
+ // localization placeholders still in place.
+ let rawManifest = extension.rawManifest;
+
let result = {
- name: extension.localize(manifest.name, aLocale),
- description: extension.localize(manifest.description, aLocale),
- creator: null,
- homepageURL: null,
+ name: extension.localize(rawManifest.name, aLocale),
+ description: extension.localize(rawManifest.description, aLocale),
+ creator: extension.localize(rawManifest.creator, aLocale),
+ homepageURL: extension.localize(rawManifest.homepage_url, aLocale),
developers: null,
translators: null,
contributors: null,
locales: [aLocale],
};
return result;
}