author | Andrew Swan <aswan@mozilla.com> |
Tue, 20 Nov 2018 20:19:59 -0800 | |
changeset 448840 | 061b97e02ede2133f50956feba25fd8798745347 |
parent 448839 | 51bb2e2f30d2c1486ebbc75568e09976a374fe6c |
child 448841 | e3b6db3b34ab06b60ab5b0f9d8d5695df754b7e1 |
push id | 35128 |
push user | rmaries@mozilla.com |
push date | Fri, 30 Nov 2018 03:06:13 +0000 |
treeherder | mozilla-central@e18555e129e7 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | kmag |
bugs | 857456 |
milestone | 65.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -546,16 +546,17 @@ var AddonManagerInternal = { pendingProviders: new Set(), providers: new Set(), providerShutdowns: new Map(), types: {}, startupChanges: {}, // Store telemetry details per addon provider telemetryDetails: {}, upgradeListeners: new Map(), + externalExtensionLoaders: new Map(), recordTimestamp(name, value) { this.TelemetryTimestamps.add(name, value); }, validateBlocklist() { let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]); @@ -774,16 +775,20 @@ var AddonManagerInternal = { } logger.debug("Loaded provider scope for " + url + ": " + Object.keys(scope).toSource()); } catch (e) { AddonManagerPrivate.recordException("AMI", "provider " + url + " load failed", e); logger.error("Exception loading default provider \"" + url + "\"", e); } } + // XXX temporary + ChromeUtils.import("resource://gre/modules/addons/BootstrapLoader.jsm"); + AddonManager.addExternalExtensionLoader(BootstrapLoader); + // Load any providers registered in the category manager for (let {entry, value: url} of Services.catMan.enumerateCategory(CATEGORY_PROVIDER_MODULE)) { try { ChromeUtils.import(url, {}); logger.debug(`Loaded provider scope for ${url}`); } catch (e) { AddonManagerPrivate.recordException("AMI", "provider " + url + " load failed", e); logger.error("Exception loading provider " + entry + " from category \"" + @@ -2027,16 +2032,20 @@ var AddonManagerInternal = { } if (this.upgradeListeners.has(addonId)) { this.upgradeListeners.delete(addonId); } else { throw Error(`No upgrade listener registered for addon ID: ${addonId}`); } }, + addExternalExtensionLoader(loader) { + this.externalExtensionLoaders.set(loader.name, loader); + }, + /** * Installs a temporary add-on from a local file or directory. * * @param aFile * An nsIFile for the file or directory of the add-on to be * temporarily installed. * @returns a Promise that rejects if the add-on is not a valid restartless * add-on or if the same ID is already temporarily installed. @@ -2936,16 +2945,20 @@ var AddonManagerPrivate = { hasUpgradeListener(aId) { return AddonManagerInternal.upgradeListeners.has(aId); }, getUpgradeListener(aId) { return AddonManagerInternal.upgradeListeners.get(aId); }, + get externalExtensionLoaders() { + return AddonManagerInternal.externalExtensionLoaders; + }, + /** * Predicate that returns true if we think the given extension ID * might have been generated by XPIProvider. */ isTemporaryInstallID(extensionId) { if (!gStarted) throw Components.Exception("AddonManager is not initialized", Cr.NS_ERROR_NOT_INITIALIZED); @@ -3348,16 +3361,21 @@ var AddonManager = { addUpgradeListener(aInstanceID, aCallback) { AddonManagerInternal.addUpgradeListener(aInstanceID, aCallback); }, removeUpgradeListener(aInstanceID) { return AddonManagerInternal.removeUpgradeListener(aInstanceID); }, + + addExternalExtensionLoader(types, loader) { + return AddonManagerInternal.addExternalExtensionLoader(types, loader); + }, + addAddonListener(aListener) { AddonManagerInternal.addAddonListener(aListener); }, removeAddonListener(aListener) { AddonManagerInternal.removeAddonListener(aListener); },
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm +++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm @@ -838,16 +838,19 @@ var AddonTestUtils = { // the AddonManagerInternal.shutdown() promise let shutdownError = XPIscope.XPIDatabase._saveError; AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider); Cu.unload("resource://gre/modules/addons/XPIProvider.jsm"); Cu.unload("resource://gre/modules/addons/XPIDatabase.jsm"); Cu.unload("resource://gre/modules/addons/XPIInstall.jsm"); + // XXX + Cu.unload("resource://gre/modules/addons/BootstrapLoader.jsm"); + let ExtensionScope = ChromeUtils.import("resource://gre/modules/Extension.jsm", null); ChromeUtils.defineModuleGetter(ExtensionScope, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm"); if (shutdownError) throw shutdownError; return true;
new file mode 100644 --- /dev/null +++ b/toolkit/mozapps/extensions/internal/BootstrapLoader.jsm @@ -0,0 +1,363 @@ + +"use strict"; + +var EXPORTED_SYMBOLS = ["BootstrapLoader"]; + +ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + AddonInternal: "resource://gre/modules/addons/XPIDatabase.jsm", + Blocklist: "resource://gre/modules/Blocklist.jsm", + ConsoleAPI: "resource://gre/modules/Console.jsm", + InstallRDF: "resource://gre/modules/addons/RDFManifestConverter.jsm", + Services: "resource://gre/modules/Services.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "BOOTSTRAP_REASONS", () => { + const {XPIProvider} = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", {}); + return XPIProvider.BOOTSTRAP_REASONS; +}); + +ChromeUtils.import("resource://gre/modules/Log.jsm"); +var logger = Log.repository.getLogger("addons.bootstrap"); + +/** + * Valid IDs fit this pattern. + */ +var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i; + +// Properties that exist in the install manifest +const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", + "optionsURL", "optionsType", "aboutURL", "iconURL"]; +const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; +const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; + +// Map new string type identifiers to old style nsIUpdateItem types. +// Retired values: +// 32 = multipackage xpi file +// 8 = locale +// 256 = apiextension +// 128 = experiment +// theme = 4 +const TYPES = { + extension: 2, + dictionary: 64, +}; + +const COMPATIBLE_BY_DEFAULT_TYPES = { + extension: true, + dictionary: true, +}; + +const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + +function isXPI(filename) { + let ext = filename.slice(-4).toLowerCase(); + return ext === ".xpi" || ext === ".zip"; +} + +/** + * Gets an nsIURI for a file within another file, either a directory or an XPI + * file. If aFile is a directory then this will return a file: URI, if it is an + * XPI file then it will return a jar: URI. + * + * @param {nsIFile} aFile + * The file containing the resources, must be either a directory or an + * XPI file + * @param {string} aPath + * The path to find the resource at, "/" separated. If aPath is empty + * then the uri to the root of the contained files will be returned + * @returns {nsIURI} + * An nsIURI pointing at the resource + */ +function getURIForResourceInFile(aFile, aPath) { + if (!isXPI(aFile.leafName)) { + let resource = aFile.clone(); + if (aPath) + aPath.split("/").forEach(part => resource.append(part)); + + return Services.io.newFileURI(resource); + } + + return buildJarURI(aFile, aPath); +} + +/** + * Creates a jar: URI for a file inside a ZIP file. + * + * @param {nsIFile} aJarfile + * The ZIP file as an nsIFile + * @param {string} aPath + * The path inside the ZIP file + * @returns {nsIURI} + * An nsIURI for the file + */ +function buildJarURI(aJarfile, aPath) { + let uri = Services.io.newFileURI(aJarfile); + uri = "jar:" + uri.spec + "!/" + aPath; + return Services.io.newURI(uri); +} + +var BootstrapLoader = { + name: "bootstrap", + manifestFile: "install.rdf", + async loadManifest(pkg) { + /** + * Reads locale properties from either the main install manifest root or + * an em:localized section in the install manifest. + * + * @param {Object} aSource + * The resource to read the properties from. + * @param {boolean} isDefault + * True if the locale is to be read from the main install manifest + * root + * @param {string[]} aSeenLocales + * An array of locale names already seen for this install manifest. + * Any locale names seen as a part of this function will be added to + * this array + * @returns {Object} + * an object containing the locale properties + */ + function readLocale(aSource, isDefault, aSeenLocales) { + let locale = {}; + if (!isDefault) { + locale.locales = []; + for (let localeName of aSource.locales || []) { + if (!localeName) { + logger.warn("Ignoring empty locale in localized properties"); + continue; + } + if (aSeenLocales.includes(localeName)) { + logger.warn("Ignoring duplicate locale in localized properties"); + continue; + } + aSeenLocales.push(localeName); + locale.locales.push(localeName); + } + + if (locale.locales.length == 0) { + logger.warn("Ignoring localized properties with no listed locales"); + return null; + } + } + + for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) { + if (hasOwnProperty(aSource, prop)) { + locale[prop] = aSource[prop]; + } + } + + return locale; + } + + let manifestData = await pkg.readString("install.rdf"); + let manifest = InstallRDF.loadFromString(manifestData).decode(); + + let addon = new AddonInternal(); + for (let prop of PROP_METADATA) { + if (hasOwnProperty(manifest, prop)) { + addon[prop] = manifest[prop]; + } + } + + if (!addon.type) { + addon.type = "extension"; + } else { + let type = addon.type; + addon.type = null; + for (let name in TYPES) { + if (TYPES[name] == type) { + addon.type = name; + break; + } + } + } + + if (!(addon.type in TYPES)) + throw new Error("Install manifest specifies unknown type: " + addon.type); + + if (!addon.id) + throw new Error("No ID in install manifest"); + if (!gIDTest.test(addon.id)) + throw new Error("Illegal add-on ID " + addon.id); + if (!addon.version) + throw new Error("No version in install manifest"); + + addon.strictCompatibility = (!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || + manifest.strictCompatibility == "true"); + + // Only read these properties for extensions. + if (addon.type == "extension") { + if (manifest.bootstrap != "true") { + throw new Error("Non-restartless extensions no longer supported"); + } + + if (addon.optionsType && + addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER && + addon.optionsType != AddonManager.OPTIONS_TYPE_TAB) { + throw new Error("Install manifest specifies unknown optionsType: " + addon.optionsType); + } + } else { + // Convert legacy dictionaries into a format the WebExtension + // dictionary loader can process. + if (addon.type === "dictionary") { + addon.loader = null; + let dictionaries = {}; + await pkg.iterFiles(({path}) => { + let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path); + if (match) { + let lang = match[1].replace(/_/g, "-"); + dictionaries[lang] = match[0]; + } + }); + addon.startupData = {dictionaries}; + } + + // Only extensions are allowed to provide an optionsURL, optionsType, + // optionsBrowserStyle, or aboutURL. For all other types they are silently ignored + addon.aboutURL = null; + addon.optionsBrowserStyle = null; + addon.optionsType = null; + addon.optionsURL = null; + } + + addon.defaultLocale = readLocale(manifest, true); + + let seenLocales = []; + addon.locales = []; + for (let localeData of manifest.localized || []) { + let locale = readLocale(localeData, false, seenLocales); + if (locale) + addon.locales.push(locale); + } + + let dependencies = new Set(manifest.dependencies); + addon.dependencies = Object.freeze(Array.from(dependencies)); + + let seenApplications = []; + addon.targetApplications = []; + for (let targetApp of manifest.targetApplications || []) { + if (!targetApp.id || !targetApp.minVersion || + !targetApp.maxVersion) { + logger.warn("Ignoring invalid targetApplication entry in install manifest"); + continue; + } + if (seenApplications.includes(targetApp.id)) { + logger.warn("Ignoring duplicate targetApplication entry for " + targetApp.id + + " in install manifest"); + continue; + } + seenApplications.push(targetApp.id); + addon.targetApplications.push(targetApp); + } + + // Note that we don't need to check for duplicate targetPlatform entries since + // the RDF service coalesces them for us. + addon.targetPlatforms = []; + for (let targetPlatform of manifest.targetPlatforms || []) { + let platform = { + os: null, + abi: null, + }; + + let pos = targetPlatform.indexOf("_"); + if (pos != -1) { + platform.os = targetPlatform.substring(0, pos); + platform.abi = targetPlatform.substring(pos + 1); + } else { + platform.os = targetPlatform; + } + + addon.targetPlatforms.push(platform); + } + + addon.userDisabled = false; + addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED; + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; + + addon.userPermissions = null; + + addon.icons = {}; + if (await pkg.hasResource("icon.png")) { + addon.icons[32] = "icon.png"; + addon.icons[48] = "icon.png"; + } + + if (await pkg.hasResource("icon64.png")) { + addon.icons[64] = "icon64.png"; + } + + return addon; + }, + + loadScope(addon, file) { + let uri = getURIForResourceInFile(file, "bootstrap.js").spec; + let principal = Services.scriptSecurityManager.getSystemPrincipal(); + + let sandbox = new Cu.Sandbox(principal, { + sandboxName: uri, + addonId: addon.id, + wantGlobalProperties: ["ChromeUtils"], + metadata: { addonID: addon.id, URI: uri }, + }); + + try { + Object.assign(sandbox, BOOTSTRAP_REASONS); + + XPCOMUtils.defineLazyGetter(sandbox, "console", () => + new ConsoleAPI({ consoleID: `addon/${addon.id}` })); + + Services.scriptloader.loadSubScript(uri, sandbox); + } catch (e) { + logger.warn(`Error loading bootstrap.js for ${addon.id}`, e); + } + + function findMethod(name) { + if (sandbox.name) { + return sandbox.name; + } + + try { + let method = Cu.evalInSandbox(name, sandbox); + return method; + } catch (err) { } + + return () => { + logger.warn(`Add-on ${addon.id} is missing bootstrap method ${name}`); + }; + } + + let install = findMethod("install"); + let uninstall = findMethod("uninstall"); + let startup = findMethod("startup"); + let shutdown = findMethod("shutdown"); + + return { + install: (...args) => install(...args), + uninstall: (...args) => uninstall(...args), + + startup(...args) { + if (addon.type == "extension") { + logger.debug(`Registering manifest for ${file.path}\n`); + Components.manager.addBootstrappedManifestLocation(file); + } + return startup(...args); + }, + + shutdown(data, reason) { + try { + return shutdown(data, reason); + } catch (err) { + throw err; + } finally { + if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { + logger.debug(`Removing manifest for ${file.path}\n`); + Components.manager.removeBootstrappedManifestLocation(file); + } + } + }, + }; + }, +}; +
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm +++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm @@ -97,17 +97,17 @@ const PENDING_INSTALL_METADATA = const COMPATIBLE_BY_DEFAULT_TYPES = { extension: true, dictionary: true, }; // Properties to save in JSON file const PROP_JSON_FIELDS = ["id", "syncGUID", "version", "type", - "isWebExtension", "updateURL", "optionsURL", + "loader", "updateURL", "optionsURL", "optionsType", "optionsBrowserStyle", "aboutURL", "defaultLocale", "visible", "active", "userDisabled", "appDisabled", "pendingUninstall", "installDate", "updateDate", "applyBackgroundUpdates", "path", "skinnable", "sourceURI", "releaseNotesURI", "softDisabled", "foreignInstall", "strictCompatibility", "locales", "targetApplications", "targetPlatforms", "signedState", @@ -276,16 +276,20 @@ class AddonInternal { return this._wrapper; } addedToDatabase() { this._key = `${this.location.name}:${this.id}`; this.inDatabase = true; } + get isWebExtension() { + return this.loader == null; + } + get selectedLocale() { if (this._selectedLocale) return this._selectedLocale; /** * this.locales is a list of objects that have property `locales`. * It's value is an array of locale codes. * @@ -1313,17 +1317,52 @@ this.XPIDatabase = { let inputAddons = JSON.parse(aData); if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) { let error = new Error("Bad JSON file contents"); error.rebuildReason = "XPIDB_rebuildBadJSON_MS"; throw error; } - if (inputAddons.schemaVersion != DB_SCHEMA) { + if (inputAddons.schemaVersion == 27) { + // Types were translated in bug 857456. + for (let addon of inputAddons.addons) { + switch (addon.type) { + case "extension": + case "dictionary": + case "locale": + case "theme": + addon.loader = "bootstrap"; + break; + + case "webbextension": + addon.type = "extension"; + addon.loader = null; + break; + + case "webextension-dictionary": + addon.type = "dictionary"; + addon.loader = null; + break; + + case "webextension-langpack": + addon.type = "locale"; + addon.loader = null; + break; + + case "webextension-theme": + addon.type = "theme"; + addon.loader = null; + break; + + default: + logger.warn(`Not converting unknown addon type ${addon.type}`); + } + } + } else if (inputAddons.schemaVersion != DB_SCHEMA) { // For now, we assume compatibility for JSON data with a // mismatched schema version, though we throw away any fields we // don't know about (bug 902956) this._recordStartupError(`schemaMismatch-${inputAddons.schemaVersion}`); logger.debug(`JSON schema mismatch: expected ${DB_SCHEMA}, actual ${inputAddons.schemaVersion}`); } let forEach = this.syncLoadingDB ? arrayForEach : idleForEach;
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm +++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm @@ -38,17 +38,16 @@ XPCOMUtils.defineLazyModuleGetters(this, ExtensionData: "resource://gre/modules/Extension.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", NetUtil: "resource://gre/modules/NetUtil.jsm", OS: "resource://gre/modules/osfile.jsm", ProductAddonChecker: "resource://gre/modules/addons/ProductAddonChecker.jsm", UpdateUtils: "resource://gre/modules/UpdateUtils.jsm", AddonInternal: "resource://gre/modules/addons/XPIDatabase.jsm", - InstallRDF: "resource://gre/modules/addons/RDFManifestConverter.jsm", XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm", XPIInternal: "resource://gre/modules/addons/XPIProvider.jsm", }); XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); XPCOMUtils.defineLazyGetter(this, "IconDetails", () => { @@ -70,18 +69,16 @@ const FileOutputStream = Components.Cons "nsIFileOutputStream", "init"); const ZipReader = Components.Constructor("@mozilla.org/libjar/zip-reader;1", "nsIZipReader", "open"); XPCOMUtils.defineLazyServiceGetters(this, { gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"], }); -const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); - const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin"; const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url"; const PREF_XPI_ENABLED = "xpinstall.enabled"; const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; @@ -152,34 +149,16 @@ const PREF_INSTALL_REQUIREBUILTINCERTS = const KEY_PROFILEDIR = "ProfD"; const KEY_TEMPDIR = "TmpD"; const KEY_APP_PROFILE = "app-profile"; const DIR_STAGE = "staged"; const DIR_TRASH = "trash"; -// Properties that exist in the install manifest -const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", - "optionsURL", "optionsType", "aboutURL", "iconURL"]; -const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; -const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; - -// Map new string type identifiers to old style nsIUpdateItem types. -// Retired values: -// 32 = multipackage xpi file -// 8 = locale -// 256 = apiextension -// 128 = experiment -// theme = 4 -const TYPES = { - extension: 2, - dictionary: 64, -}; - const COMPATIBLE_BY_DEFAULT_TYPES = { extension: true, dictionary: true, }; // This is a random number array that can be used as "salt" when generating // an automatic ID based on the directory path of an add-on. It will prevent // someone from creating an ID for a permanent add-on that could be replaced @@ -401,17 +380,17 @@ function waitForAllPromises(promises) { }) ); Promise.all(newPromises) .then((results) => shouldReject ? reject(rejectValue) : resolve(results)); }); } /** - * Reads an AddonInternal object from a manifest stream. + * Reads an AddonInternal object from a webextension manifest.json * * @param {nsIURI} aUri * A |file:| or |jar:| URL for the manifest * @param {Package} aPackage * The install package for the add-on * @returns {AddonInternal} * @throws if the install manifest in the stream is corrupt or could not * be read @@ -444,17 +423,17 @@ async function loadManifestFromWebManife 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 = extension.type === "langpack" ? "locale" : extension.type; - addon.isWebExtension = true; + addon.loader = null; addon.strictCompatibility = true; addon.internalName = null; addon.updateURL = bss.update_url; addon.optionsBrowserStyle = true; addon.optionsURL = null; addon.optionsType = null; addon.aboutURL = null; addon.dependencies = Object.freeze(Array.from(extension.dependencies)); @@ -531,210 +510,16 @@ async function loadManifestFromWebManife addon.targetPlatforms = []; // Themes are disabled by default, except when they're installed from a web page. addon.userDisabled = (extension.type === "theme"); addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED; return addon; } -/** - * Reads an AddonInternal object from an RDF stream. - * - * @param {nsIURI} aUri - * The URI that the manifest is being read from - * @param {string} aData - * The manifest text - * @param {InstallPackage} aPackage - * An install package instance for the extension. - * @returns {AddonInternal} - * @throws if the install manifest in the RDF stream is corrupt or could not - * be read - */ -async function loadManifestFromRDF(aUri, aData, aPackage) { - /** - * Reads locale properties from either the main install manifest root or - * an em:localized section in the install manifest. - * - * @param {Object} aSource - * The resource to read the properties from. - * @param {boolean} isDefault - * True if the locale is to be read from the main install manifest - * root - * @param {string[]} aSeenLocales - * An array of locale names already seen for this install manifest. - * Any locale names seen as a part of this function will be added to - * this array - * @returns {Object} - * an object containing the locale properties - */ - function readLocale(aSource, isDefault, aSeenLocales) { - let locale = {}; - if (!isDefault) { - locale.locales = []; - for (let localeName of aSource.locales || []) { - if (!localeName) { - logger.warn("Ignoring empty locale in localized properties"); - continue; - } - if (aSeenLocales.includes(localeName)) { - logger.warn("Ignoring duplicate locale in localized properties"); - continue; - } - aSeenLocales.push(localeName); - locale.locales.push(localeName); - } - - if (locale.locales.length == 0) { - logger.warn("Ignoring localized properties with no listed locales"); - return null; - } - } - - for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) { - if (hasOwnProperty(aSource, prop)) { - locale[prop] = aSource[prop]; - } - } - - return locale; - } - - let manifest = InstallRDF.loadFromString(aData).decode(); - - let addon = new AddonInternal(); - for (let prop of PROP_METADATA) { - if (hasOwnProperty(manifest, prop)) { - addon[prop] = manifest[prop]; - } - } - - if (!addon.type) { - addon.type = "extension"; - } else { - let type = addon.type; - addon.type = null; - for (let name in TYPES) { - if (TYPES[name] == type) { - addon.type = name; - break; - } - } - } - addon.isWebExtension = false; - - if (!(addon.type in TYPES)) - throw new Error("Install manifest specifies unknown type: " + addon.type); - - if (!addon.id) - throw new Error("No ID in install manifest"); - if (!gIDTest.test(addon.id)) - throw new Error("Illegal add-on ID " + addon.id); - if (!addon.version) - throw new Error("No version in install manifest"); - - addon.strictCompatibility = (!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || - manifest.strictCompatibility == "true"); - - // Only read these properties for extensions. - if (addon.type == "extension") { - if (manifest.bootstrap != "true") { - throw new Error("Non-restartless extensions no longer supported"); - } - - if (addon.optionsType && - addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER && - addon.optionsType != AddonManager.OPTIONS_TYPE_TAB) { - throw new Error("Install manifest specifies unknown optionsType: " + addon.optionsType); - } - } else { - // Convert legacy dictionaries into a format the WebExtension - // dictionary loader can process. - if (addon.type === "dictionary") { - addon.isWebExtension = true; - let dictionaries = {}; - await aPackage.iterFiles(({path}) => { - let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path); - if (match) { - let lang = match[1].replace(/_/g, "-"); - dictionaries[lang] = match[0]; - } - }); - addon.startupData = {dictionaries}; - } - - // Only extensions are allowed to provide an optionsURL, optionsType, - // optionsBrowserStyle, or aboutURL. For all other types they are silently ignored - addon.aboutURL = null; - addon.optionsBrowserStyle = null; - addon.optionsType = null; - addon.optionsURL = null; - } - - addon.defaultLocale = readLocale(manifest, true); - - let seenLocales = []; - addon.locales = []; - for (let localeData of manifest.localized || []) { - let locale = readLocale(localeData, false, seenLocales); - if (locale) - addon.locales.push(locale); - } - - let dependencies = new Set(manifest.dependencies); - addon.dependencies = Object.freeze(Array.from(dependencies)); - - let seenApplications = []; - addon.targetApplications = []; - for (let targetApp of manifest.targetApplications || []) { - if (!targetApp.id || !targetApp.minVersion || - !targetApp.maxVersion) { - logger.warn("Ignoring invalid targetApplication entry in install manifest"); - continue; - } - if (seenApplications.includes(targetApp.id)) { - logger.warn("Ignoring duplicate targetApplication entry for " + targetApp.id + - " in install manifest"); - continue; - } - seenApplications.push(targetApp.id); - addon.targetApplications.push(targetApp); - } - - // Note that we don't need to check for duplicate targetPlatform entries since - // the RDF service coalesces them for us. - addon.targetPlatforms = []; - for (let targetPlatform of manifest.targetPlatforms || []) { - let platform = { - os: null, - abi: null, - }; - - let pos = targetPlatform.indexOf("_"); - if (pos != -1) { - platform.os = targetPlatform.substring(0, pos); - platform.abi = targetPlatform.substring(pos + 1); - } else { - platform.os = targetPlatform; - } - - addon.targetPlatforms.push(platform); - } - - addon.userDisabled = false; - addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED; - addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; - - // icons will be filled by the calling function - addon.icons = {}; - addon.userPermissions = null; - - return addon; -} - function defineSyncGUID(aAddon) { // Define .syncGUID as a lazy property which is also settable Object.defineProperty(aAddon, "syncGUID", { get: () => { aAddon.syncGUID = uuidGen.generateUUID().toString(); return aAddon.syncGUID; }, set: (val) => { @@ -755,56 +540,47 @@ function generateTemporaryInstallID(aFil hasher.update(sess, sess.length); hasher.update(data, data.length); let id = `${getHashStringForCrypto(hasher)}${TEMPORARY_ADDON_SUFFIX}`; logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`); return id; } var loadManifest = async function(aPackage, aLocation, aOldAddon) { - async function loadFromRDF(aUri) { - let manifest = await aPackage.readString("install.rdf"); - let addon = await loadManifestFromRDF(aUri, manifest, aPackage); - - if (await aPackage.hasResource("icon.png")) { - addon.icons[32] = "icon.png"; - addon.icons[48] = "icon.png"; + let addon; + if (await aPackage.hasResource("manifest.json")) { + addon = await loadManifestFromWebManifest(aPackage.rootURI, aPackage); + } else { + for (let loader of AddonManagerPrivate.externalExtensionLoaders.values()) { + if (await aPackage.hasResource(loader.manifestFile)) { + addon = await loader.loadManifest(aPackage); + addon.loader = loader.name; + break; + } } - - if (await aPackage.hasResource("icon64.png")) { - addon.icons[64] = "icon64.png"; - } - - return addon; } - let entry = await aPackage.getManifestFile(); - if (!entry) { + if (!addon) { throw new Error(`File ${aPackage.filePath} does not contain a valid manifest`); } - let isWebExtension = entry == "manifest.json"; - let addon = isWebExtension ? - await loadManifestFromWebManifest(aPackage.rootURI, aPackage) : - await loadFromRDF(aPackage.getURI("install.rdf")); - addon._sourceBundle = aPackage.file; addon.location = aLocation; let {signedState, cert} = await aPackage.verifySignedState(addon); addon.signedState = signedState; if (signedState != AddonManager.SIGNEDSTATE_PRIVILEGED) { addon.hidden = false; } - if (isWebExtension && !addon.id) { + if (!addon.id) { if (cert) { addon.id = cert.commonName; if (!gIDTest.test(addon.id)) { - throw new Error(`Webextension is signed with an invalid id (${addon.id})`); + throw new Error(`Extension is signed with an invalid id (${addon.id})`); } } if (!addon.id && aLocation.isTemporary) { addon.id = generateTemporaryInstallID(aPackage.file); } } addon.propagateDisabledState(aOldAddon);
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -28,17 +28,16 @@ ChromeUtils.import("resource://gre/modul XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm", Dictionary: "resource://gre/modules/Extension.jsm", Extension: "resource://gre/modules/Extension.jsm", Langpack: "resource://gre/modules/Extension.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", OS: "resource://gre/modules/osfile.jsm", - ConsoleAPI: "resource://gre/modules/Console.jsm", JSONFile: "resource://gre/modules/JSONFile.jsm", TelemetrySession: "resource://gre/modules/TelemetrySession.jsm", XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm", XPIDatabaseReconcile: "resource://gre/modules/addons/XPIDatabase.jsm", XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm", }); @@ -100,17 +99,17 @@ const STARTUP_MTIME_SCOPES = [KEY_APP_GL KEY_APP_SYSTEM_SHARE, KEY_APP_SYSTEM_USER]; const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; const XPI_PERMISSION = "install"; const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60; -const DB_SCHEMA = 27; +const DB_SCHEMA = 28; const NOTIFICATION_TOOLBOX_CONNECTION_CHANGE = "toolbox-connection-change"; function encoded(strings, ...values) { let result = []; for (let [i, string] of strings.entries()) { result.push(string); @@ -358,17 +357,17 @@ function* iterDirectory(aDir) { * The on-disk state of an individual XPI, created from an Object * as stored in the addonStartup.json file. */ const JSON_FIELDS = Object.freeze([ "changed", "dependencies", "enabled", "file", - "isWebExtension", + "loader", "lastModifiedTime", "path", "runInSafeMode", "signedState", "startupData", "telemetryKey", "type", "version", @@ -442,33 +441,37 @@ class XPIState { * data, to be saved to addonStartup.json. * * @returns {Object} */ toJSON() { let json = { dependencies: this.dependencies, enabled: this.enabled, - isWebExtension: this.isWebExtension, lastModifiedTime: this.lastModifiedTime, + loader: this.loader, path: this.relativePath, runInSafeMode: this.runInSafeMode, signedState: this.signedState, telemetryKey: this.telemetryKey, version: this.version, }; if (this.type != "extension") { json.type = this.type; } if (this.startupData) { json.startupData = this.startupData; } return json; } + get isWebExtension() { + return this.loader == null; + } + /** * Update the last modified time for an add-on on disk. * * @param {nsIFile} aFile * The location of the add-on. * @returns {boolean} * True if the time stamp has changed. */ @@ -516,17 +519,18 @@ class XPIState { // did a full recursive scan in that case, so we don't need to do it again. // We don't use aDBAddon.active here because it's not updated until after restart. let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled); this.enabled = aDBAddon.visible && !aDBAddon.disabled; this.version = aDBAddon.version; this.type = aDBAddon.type; - this.isWebExtension = aDBAddon.isWebExtension; + this.loader = aDBAddon.loader; + if (aDBAddon.startupData) { this.startupData = aDBAddon.startupData; } this.telemetryKey = this.getTelemetryKey(); this.dependencies = aDBAddon.dependencies; this.runInSafeMode = canRunInSafeMode(aDBAddon); @@ -1507,35 +1511,29 @@ class BootstrapScope { * @returns {any} * The return value of the bootstrap method. */ async callBootstrapMethod(aMethod, aReason, aExtraParams = {}) { let {addon, runInSafeMode} = this; if (Services.appinfo.inSafeMode && !runInSafeMode) return null; - if (!addon.isWebExtension && addon.type == "extension" && - aMethod == "startup") { - logger.debug(`Registering manifest for ${this.file.path}`); - Components.manager.addBootstrappedManifestLocation(this.file); - } - try { if (!this.scope) { this.loadBootstrapScope(aReason); } if (aMethod == "startup" || aMethod == "shutdown") { aExtraParams.instanceID = this.instanceID; } let method = undefined; let {scope} = this; try { - method = scope[aMethod] || Cu.evalInSandbox(`${aMethod};`, scope); + method = scope[aMethod]; } catch (e) { // An exception will be caught if the expected method is not defined. // That will be logged below. } if (aMethod == "startup") { this.started = true; } else if (aMethod == "shutdown") { @@ -1584,22 +1582,16 @@ class BootstrapScope { return result; } finally { // Extensions are automatically initialized in the correct order at startup. if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) { for (let addon of XPIProvider.getDependentAddons(this.addon)) { XPIDatabase.updateAddonDisabledState(addon); } } - - if (addon.type == "extension" && aMethod == "shutdown" && - aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { - logger.debug(`Removing manifest for ${this.file.path}`); - Components.manager.removeBootstrappedManifestLocation(this.file); - } } } // No-op method to be overridden by tests. _beforeCallBootstrapMethod() {} /** * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason @@ -1638,36 +1630,22 @@ class BootstrapScope { case "dictionary": this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file); break; default: throw new Error(`Unknown webextension type ${this.addon.type}`); } } else { - let uri = getURIForResourceInFile(this.file, "bootstrap.js").spec; - - let principal = Services.scriptSecurityManager.getSystemPrincipal(); - this.scope = - new Cu.Sandbox(principal, { sandboxName: uri, - addonId: this.addon.id, - wantGlobalProperties: ["ChromeUtils"], - metadata: { addonID: this.addon.id, URI: uri } }); + let loader = AddonManagerPrivate.externalExtensionLoaders.get(this.addon.loader); + if (!loader) { + throw new Error(`Cannot find loader for ${this.addon.loader}`); + } - try { - Object.assign(this.scope, BOOTSTRAP_REASONS); - - XPCOMUtils.defineLazyGetter( - this.scope, "console", - () => new ConsoleAPI({ consoleID: `addon/${this.addon.id}` })); - - Services.scriptloader.loadSubScript(uri, this.scope); - } catch (e) { - logger.warn(`Error loading bootstrap.js for ${this.addon.id}`, e); - } + this.scope = loader.loadScope(this.addon, this.file); } // Notify the BrowserToolboxProcess that a new addon has been loaded. let wrappedJSObject = { id: this.addon.id, options: { global: this.scope }}; Services.obs.notifyObservers({ wrappedJSObject }, "toolbox-update-addon-options"); } /**
--- a/toolkit/mozapps/extensions/internal/moz.build +++ b/toolkit/mozapps/extensions/internal/moz.build @@ -3,16 +3,17 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES.addons += [ 'AddonRepository.jsm', 'AddonSettings.jsm', 'AddonUpdateChecker.jsm', + 'BootstrapLoader.jsm', 'Content.js', 'GMPProvider.jsm', 'LightweightThemeImageOptimizer.jsm', 'LightweightThemePersister.jsm', 'ProductAddonChecker.jsm', 'RDFDataSource.jsm', 'RDFManifestConverter.jsm', 'XPIDatabase.jsm',
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js @@ -5,16 +5,17 @@ // Verify that API functions fail if the Add-ons Manager isn't initialised. const IGNORE = ["getPreferredIconURL", "escapeAddonURI", "shouldAutoUpdate", "getStartupChanges", "addTypeListener", "removeTypeListener", "addAddonListener", "removeAddonListener", "addInstallListener", "removeInstallListener", "addManagerListener", "removeManagerListener", + "addExternalExtensionLoader", "shutdown", "init", "stateToString", "errorToString", "getUpgradeListener", "addUpgradeListener", "removeUpgradeListener", "getInstallSourceFromHost", "getInstallSourceFromPrincipal", ]; const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride", "AddonScreenshot", "AddonType", "startup", "shutdown",