Bug 1365709 Crude strawman for webextension langpacks
MozReview-Commit-ID: 8vkj14nougX
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -290,16 +290,17 @@ UninstallObserver.init();
// No functionality of this class is guaranteed to work before
// |loadManifest| has been called, and completed.
this.ExtensionData = class {
constructor(rootURI) {
this.rootURI = rootURI;
this.resourceURL = rootURI.spec;
this.manifest = null;
+ this.type = null;
this.id = null;
this.uuid = null;
this.localeData = null;
this._promiseLocales = null;
this.apiNames = new Set();
this.dependencies = new Set();
this.permissions = new Set();
@@ -448,16 +449,20 @@ this.ExtensionData = class {
}
// This method should return a structured representation of any
// capabilities this extension has access to, as derived from the
// manifest. The current implementation just returns the contents
// of the permissions attribute, if we add things like url_overrides,
// they should also be added here.
get userPermissions() {
+ if (this.type != "extension") {
+ return null;
+ }
+
let result = {
origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern),
apis: [...this.apiNames],
};
if (Array.isArray(this.manifest.content_scripts)) {
for (let entry of this.manifest.content_scripts) {
result.origins.push(...entry.matches);
@@ -503,32 +508,40 @@ this.ExtensionData = class {
logError: error => {
this.manifestWarning(error);
},
preprocessors: {},
};
+ let manifestType = "manifest.WebExtensionManifest";
if (this.manifest.theme) {
+ this.type = "theme";
+ // XXX create a separate manifest type for themes
let invalidProps = validateThemeManifest(Object.getOwnPropertyNames(this.manifest));
if (invalidProps.length) {
let message = `Themes defined in the manifest may only contain static resources. ` +
`If you would like to use additional properties, please use the "theme" permission instead. ` +
`(the invalid properties found are: ${invalidProps})`;
this.manifestError(message);
}
+ } else if (this.manifest["langpack-id"]) {
+ this.type = "langpack";
+ manifestType = "manifest.WebExtensionLangpackManifest";
+ } else {
+ this.type = "extension";
}
if (this.localeData) {
context.preprocessors.localize = (value, context) => this.localize(value);
}
- let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
+ let normalized = Schemas.normalize(this.manifest, manifestType, context);
if (normalized.error) {
this.manifestError(normalized.error);
return null;
}
manifest = normalized.value;
let id;
@@ -543,62 +556,79 @@ this.ExtensionData = class {
if (!this.id) {
this.id = id;
}
let apiNames = new Set();
let dependencies = new Set();
let originPermissions = new Set();
let permissions = new Set();
+ let webAccessibleResources = [];
- for (let perm of manifest.permissions) {
- if (perm === "geckoProfiler") {
- const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
- if (!acceptedExtensions.split(",").includes(id)) {
- this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
- continue;
+ if (this.type == "extension") {
+ for (let perm of manifest.permissions) {
+ if (perm === "geckoProfiler") {
+ const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
+ if (!acceptedExtensions.split(",").includes(id)) {
+ this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
+ continue;
+ }
+
+ let type = classifyPermission(perm);
+ if (type.origin) {
+ let matcher = new MatchPattern(perm, {ignorePath: true});
+
+ whitelist.push(matcher);
+ perm = matcher.pattern;
+ } else if (type.api) {
+ this.apiNames.add(type.api);
+ }
+
+ this.permissions.add(perm);
+ }
+
+ let type = classifyPermission(perm);
+ if (type.origin) {
+ let matcher = new MatchPattern(perm, {ignorePath: true});
+
+ perm = matcher.pattern;
+ originPermissions.add(perm);
+ } else if (type.api) {
+ apiNames.add(type.api);
+ }
+
+ permissions.add(perm);
+ }
+
+ if (this.id) {
+ // An extension always gets permission to its own url.
+ let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
+ originPermissions.add(matcher.pattern);
+
+ // Apply optional permissions
+ let perms = await ExtensionPermissions.get(this);
+ for (let perm of perms.permissions) {
+ permissions.add(perm);
+ }
+ for (let origin of perms.origins) {
+ originPermissions.add(origin);
}
}
- let type = classifyPermission(perm);
- if (type.origin) {
- let matcher = new MatchPattern(perm, {ignorePath: true});
-
- perm = matcher.pattern;
- originPermissions.add(perm);
- } else if (type.api) {
- apiNames.add(type.api);
+ for (let api of apiNames) {
+ dependencies.add(`${api}@experiments.addons.mozilla.org`);
}
- permissions.add(perm);
- }
-
- if (this.id) {
- // An extension always gets permission to its own url.
- let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
- originPermissions.add(matcher.pattern);
-
- // Apply optional permissions
- let perms = await ExtensionPermissions.get(this);
- for (let perm of perms.permissions) {
- permissions.add(perm);
- }
- for (let origin of perms.origins) {
- originPermissions.add(origin);
+ // Normalize all patterns to contain a single leading /
+ if (manifest.web_accessible_resources) {
+ webAccessibleResources = manifest.web_accessible_resources
+ .map(path => path.replace(/^\/*/, "/"));
}
}
- for (let api of apiNames) {
- dependencies.add(`${api}@experiments.addons.mozilla.org`);
- }
-
- // Normalize all patterns to contain a single leading /
- let webAccessibleResources = (manifest.web_accessible_resources || [])
- .map(path => path.replace(/^\/*/, "/"));
-
return {apiNames, dependencies, originPermissions, id, manifest, permissions,
webAccessibleResources};
}
// Reads the extension's |manifest.json| file, and stores its
// parsed contents in |this.manifest|.
async loadManifest() {
let [manifestData] = await Promise.all([
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -207,16 +207,110 @@
}
}
},
"additionalProperties": { "$ref": "UnrecognizedProperty" }
},
{
+ "id": "WebExtensionLangpackManifest",
+ "type": "object",
+ "description": "Represents a WebExtension language pack manifest.json file",
+ "properties": {
+ "manifest_version": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 2
+ },
+
+ "minimum_chrome_version":{
+ "type": "string",
+ "optional": true
+ },
+
+ "applications": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "gecko": {
+ "$ref": "FirefoxSpecificProperties",
+ "optional": true
+ }
+ }
+ },
+
+ "browser_specific_settings": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "gecko": {
+ "$ref": "FirefoxSpecificProperties",
+ "optional": true
+ }
+ }
+ },
+
+ "name": {
+ "type": "string",
+ "optional": false,
+ "preprocess": "localize"
+ },
+
+ "short_name": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "description": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "author": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize",
+ "onError": "warn"
+ },
+
+ "version": {
+ "type": "string",
+ "optional": false
+ },
+
+ "homepage_url": {
+ "type": "string",
+ "format": "url",
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "langpack-id": {
+ "type": "string",
+ "pattern": "^[-A-Za-z]+$"
+ },
+
+ "languages": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^[-A-Za-z]+$"
+ }
+ },
+
+ "chrome_entries": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ }
+ },
+ {
"id": "ThemeIcons",
"type": "object",
"properties": {
"light": {
"$ref": "ExtensionURL",
"description": "A light icon to use for dark themes"
},
"dark": {
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -329,33 +329,32 @@ async function loadManifestFromWebManife
// all locales.
let locales = (extension.errors.length == 0) ?
await extension.initAllLocales() : null;
if (extension.errors.length > 0) {
throw new Error("Extension is invalid");
}
- let theme = Boolean(manifest.theme);
-
let bss = (manifest.browser_specific_settings && manifest.browser_specific_settings.gecko)
|| (manifest.applications && manifest.applications.gecko) || {};
if (manifest.browser_specific_settings && manifest.applications) {
logger.warn("Ignoring applications property in manifest");
}
// A * is illegal in strict_min_version
if (bss.strict_min_version && bss.strict_min_version.split(".").some(part => part == "*")) {
throw new Error("The use of '*' in strict_min_version is invalid");
}
let addon = new AddonInternal();
addon.id = bss.id;
addon.version = manifest.version;
- addon.type = "webextension" + (theme ? "-theme" : "");
+ addon.type = extension.type == "extension" ?
+ "webextension" : `webextension-${extension.type}`;
addon.unpack = false;
addon.strictCompatibility = true;
addon.bootstrap = true;
addon.hasBinaryComponents = false;
addon.multiprocessCompatible = true;
addon.internalName = null;
addon.updateURL = bss.update_url;
addon.updateKey = null;
@@ -430,17 +429,17 @@ async function loadManifestFromWebManife
addon.targetApplications = [{
id: TOOLKIT_ID,
minVersion: bss.strict_min_version,
maxVersion: bss.strict_max_version,
}];
addon.targetPlatforms = [];
// Themes are disabled by default, except when they're installed from a web page.
- addon.userDisabled = theme;
+ addon.userDisabled = (extension.type == "theme");
addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;
return addon;
}
/**
* Reads an AddonInternal object from an RDF stream.
*
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -18,16 +18,19 @@ Cu.import("resource://gre/modules/XPCOMU
Cu.import("resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
ChromeManifestParser: "resource://gre/modules/ChromeManifestParser.jsm",
Extension: "resource://gre/modules/Extension.jsm",
+ ExtensionData: "resource://gre/modules/Extension.jsm",
+ FileSource: "resource://gre/modules/L10nRegistry.jsm",
+ L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
FileUtils: "resource://gre/modules/FileUtils.jsm",
ZipUtils: "resource://gre/modules/ZipUtils.jsm",
NetUtil: "resource://gre/modules/NetUtil.jsm",
PermissionsUtils: "resource://gre/modules/PermissionsUtils.jsm",
OS: "resource://gre/modules/osfile.jsm",
ConsoleAPI: "resource://gre/modules/Console.jsm",
ProductAddonChecker: "resource://gre/modules/addons/ProductAddonChecker.jsm",
@@ -205,16 +208,17 @@ const BOOTSTRAP_REASONS = {
};
// Some add-on types that we track internally are presented as other types
// externally
const TYPE_ALIASES = {
"apiextension": "extension",
"webextension": "extension",
"webextension-theme": "theme",
+ "webextension-langpack": "locale",
};
const CHROME_TYPES = new Set([
"extension",
"locale",
"experiment",
]);
@@ -1105,16 +1109,38 @@ function recordAddonTelemetry(aAddon) {
if (locale) {
if (locale.name)
XPIProvider.setTelemetry(aAddon.id, "name", locale.name);
if (locale.creator)
XPIProvider.setTelemetry(aAddon.id, "creator", locale.creator);
}
}
+class LangpackBootstrapScope {
+ install(data, reason) {}
+ uninstall(data, reason) {}
+
+ async startup(data, reason) {
+ let manifest = data.manifest;
+
+ this.langpackId = manifest["langpack-id"];
+ this.handle = aomStartup.registerChrome(data.resourceURI, manifest.entries);
+
+ let locales = manifest.languages;
+ let basePath = `resource://${this.langpackId}`;
+ const source = new FileSource(this.langpackId, locales, basePath);
+ L10nRegistry.registerSource(source);
+ }
+
+ shutdown(data, reason) {
+ L10nRegistry.unregisterSource(this.langpackId);
+ this.handle.destruct();
+ }
+}
+
/**
* The on-disk state of an individual XPI, created from an Object
* as stored in the addonStartup.json file.
*/
const JSON_FIELDS = Object.freeze([
"bootstrapped",
"changed",
"dependencies",
@@ -4216,16 +4242,18 @@ this.XPIProvider = {
addonId: aId,
metadata: { addonID: aId } });
logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
return;
}
if (isWebExtension(aType)) {
activeAddon.bootstrapScope = Extension.getBootstrapScope(aId, aFile);
+ } else if (aType == "webextension-langpack") {
+ activeAddon.bootstrapScope = new LangpackBootstrapScope();
} else {
let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
if (aType == "dictionary")
uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
else if (aType == "apiextension")
uri = "resource://gre/modules/addons/APIExtensionBootstrap.js"
activeAddon.bootstrapScope =