1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = ["BootstrapLoader"];
9 import {AddonManager} from "resource://gre/modules/AddonManager.sys.mjs";
10 import {AddonInternal} from "resource://gre/modules/addons/XPIDatabase.sys.mjs";
11 import {InstallRDF} from "resource://legacy/RDFManifestConverter.sys.mjs";
12 import {XPIProvider} from "resource://gre/modules/addons/XPIProvider.sys.mjs";
14 const BOOTSTRAP_REASONS = XPIProvider.BOOTSTRAP_REASONS;
16 var logger = console.createInstance({ prefix: "addons.bootstrap" });
19 * Valid IDs fit this pattern.
21 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;
23 // Properties that exist in the install manifest
24 const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL",
25 "optionsURL", "optionsType", "aboutURL", "iconURL"];
26 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
27 const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
29 // Map new string type identifiers to old style nsIUpdateItem types.
31 // 32 = multipackage xpi file
41 const COMPATIBLE_BY_DEFAULT_TYPES = {
46 const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
48 function isXPI(filename) {
49 let ext = filename.slice(-4).toLowerCase();
50 return ext === ".xpi" || ext === ".zip";
54 * Gets an nsIURI for a file within another file, either a directory or an XPI
55 * file. If aFile is a directory then this will return a file: URI, if it is an
56 * XPI file then it will return a jar: URI.
58 * @param {nsIFile} aFile
59 * The file containing the resources, must be either a directory or an
61 * @param {string} aPath
62 * The path to find the resource at, "/" separated. If aPath is empty
63 * then the uri to the root of the contained files will be returned
65 * An nsIURI pointing at the resource
67 function getURIForResourceInFile(aFile, aPath) {
68 if (!isXPI(aFile.leafName)) {
69 let resource = aFile.clone();
71 aPath.split("/").forEach(part => resource.append(part));
73 return Services.io.newFileURI(resource);
76 return buildJarURI(aFile, aPath);
80 * Creates a jar: URI for a file inside a ZIP file.
82 * @param {nsIFile} aJarfile
83 * The ZIP file as an nsIFile
84 * @param {string} aPath
85 * The path inside the ZIP file
87 * An nsIURI for the file
89 function buildJarURI(aJarfile, aPath) {
90 let uri = Services.io.newFileURI(aJarfile);
91 uri = "jar:" + uri.spec + "!/" + aPath;
92 return Services.io.newURI(uri);
95 export var BootstrapLoader = {
97 manifestFile: "install.rdf",
98 async loadManifest(pkg) {
100 * Reads locale properties from either the main install manifest root or
101 * an em:localized section in the install manifest.
103 * @param {Object} aSource
104 * The resource to read the properties from.
105 * @param {boolean} isDefault
106 * True if the locale is to be read from the main install manifest
108 * @param {string[]} aSeenLocales
109 * An array of locale names already seen for this install manifest.
110 * Any locale names seen as a part of this function will be added to
113 * an object containing the locale properties
115 function readLocale(aSource, isDefault, aSeenLocales) {
119 for (let localeName of aSource.locales || []) {
121 logger.warn("Ignoring empty locale in localized properties");
124 if (aSeenLocales.includes(localeName)) {
125 logger.warn("Ignoring duplicate locale in localized properties");
128 aSeenLocales.push(localeName);
129 locale.locales.push(localeName);
132 if (locale.locales.length == 0) {
133 logger.warn("Ignoring localized properties with no listed locales");
138 for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) {
139 if (hasOwnProperty(aSource, prop)) {
140 locale[prop] = aSource[prop];
147 let manifestData = await pkg.readString("install.rdf");
148 let manifest = InstallRDF.loadFromString(manifestData).decode();
150 let addon = new AddonInternal();
151 for (let prop of PROP_METADATA) {
152 if (hasOwnProperty(manifest, prop)) {
153 addon[prop] = manifest[prop];
158 addon.type = "extension";
160 let type = addon.type;
162 for (let name in TYPES) {
163 if (TYPES[name] == type) {
170 if (!(addon.type in TYPES))
171 throw new Error("Install manifest specifies unknown type: " + addon.type);
174 throw new Error("No ID in install manifest");
175 if (!gIDTest.test(addon.id))
176 throw new Error("Illegal add-on ID " + addon.id);
178 throw new Error("No version in install manifest");
180 addon.strictCompatibility = (!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
181 manifest.strictCompatibility == "true");
183 // Only read these properties for extensions.
184 if (addon.type == "extension") {
185 if (manifest.bootstrap != "true") {
186 throw new Error("Non-restartless extensions no longer supported");
189 if (addon.optionsType &&
190 addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER &&
191 addon.optionsType != AddonManager.OPTIONS_TYPE_TAB) {
192 throw new Error("Install manifest specifies unknown optionsType: " + addon.optionsType);
195 // Convert legacy dictionaries into a format the WebExtension
196 // dictionary loader can process.
197 if (addon.type === "dictionary") {
199 let dictionaries = {};
200 await pkg.iterFiles(({path}) => {
201 let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path);
203 let lang = match[1].replace(/_/g, "-");
204 dictionaries[lang] = match[0];
207 addon.startupData = {dictionaries};
210 // Only extensions are allowed to provide an optionsURL, optionsType,
211 // optionsBrowserStyle, or aboutURL. For all other types they are silently ignored
212 addon.aboutURL = null;
213 addon.optionsBrowserStyle = null;
214 addon.optionsType = null;
215 addon.optionsURL = null;
218 addon.defaultLocale = readLocale(manifest, true);
220 let seenLocales = [];
222 for (let localeData of manifest.localized || []) {
223 let locale = readLocale(localeData, false, seenLocales);
225 addon.locales.push(locale);
228 let dependencies = new Set(manifest.dependencies);
229 addon.dependencies = Object.freeze(Array.from(dependencies));
231 let seenApplications = [];
232 addon.targetApplications = [];
233 for (let targetApp of manifest.targetApplications || []) {
234 if (!targetApp.id || !targetApp.minVersion ||
235 !targetApp.maxVersion) {
236 logger.warn("Ignoring invalid targetApplication entry in install manifest");
239 if (seenApplications.includes(targetApp.id)) {
240 logger.warn("Ignoring duplicate targetApplication entry for " + targetApp.id +
241 " in install manifest");
244 seenApplications.push(targetApp.id);
245 addon.targetApplications.push(targetApp);
248 // Note that we don't need to check for duplicate targetPlatform entries since
249 // the RDF service coalesces them for us.
250 addon.targetPlatforms = [];
251 for (let targetPlatform of manifest.targetPlatforms || []) {
257 let pos = targetPlatform.indexOf("_");
259 platform.os = targetPlatform.substring(0, pos);
260 platform.abi = targetPlatform.substring(pos + 1);
262 platform.os = targetPlatform;
265 addon.targetPlatforms.push(platform);
268 addon.userDisabled = false;
269 addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
270 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
272 addon.userPermissions = null;
275 if (await pkg.hasResource("icon.png")) {
276 addon.icons[32] = "icon.png";
277 addon.icons[48] = "icon.png";
280 if (await pkg.hasResource("icon64.png")) {
281 addon.icons[64] = "icon64.png";
288 let file = addon.file || addon._sourceBundle;
289 let uri = getURIForResourceInFile(file, "bootstrap.js").spec;
290 let principal = Services.scriptSecurityManager.getSystemPrincipal();
292 let sandbox = new Cu.Sandbox(principal, {
295 wantGlobalProperties: ["ChromeUtils"],
296 metadata: { addonID: addon.id, URI: uri },
300 Object.assign(sandbox, BOOTSTRAP_REASONS);
302 ChromeUtils.defineLazyGetter(sandbox, "console", () =>
303 console.createInstance({ consoleID: `addon/${addon.id}` }));
305 Services.scriptloader.loadSubScript(uri, sandbox);
307 logger.warn(`Error loading bootstrap.js for ${addon.id}`, e);
310 function findMethod(name) {
312 return sandbox[name];
316 let method = Cu.evalInSandbox(name, sandbox);
321 logger.warn(`Add-on ${addon.id} is missing bootstrap method ${name}`);
325 let install = findMethod("install");
326 let uninstall = findMethod("uninstall");
327 let startup = findMethod("startup");
328 let shutdown = findMethod("shutdown");
331 install: (...args) => install(...args),
335 // Forget any cached files we might've had from this extension.
336 Services.obs.notifyObservers(null, "startupcache-invalidate");
340 if (addon.type == "extension") {
341 logger.debug(`Registering manifest for ${file.path}\n`);
342 Components.manager.addBootstrappedManifestLocation(file);
344 return startup(...args);
347 shutdown(data, reason) {
349 return shutdown(data, reason);
353 if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
354 logger.debug(`Removing manifest for ${file.path}\n`);
355 Components.manager.removeBootstrappedManifestLocation(file);