]> git.gir.st - LegacyFox.git/blob - legacy/BootstrapLoader.sys.mjs
provide easy way to find modifications to BootstrapLoader
[LegacyFox.git] / legacy / BootstrapLoader.sys.mjs
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/. */
4
5 "use strict";
6
7 var EXPORTED_SYMBOLS = ["BootstrapLoader"];
8
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";
13
14 const BOOTSTRAP_REASONS = XPIProvider.BOOTSTRAP_REASONS;
15
16 var logger = console.createInstance({ prefix: "addons.bootstrap" });
17
18 /**
19 * Valid IDs fit this pattern.
20 */
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;
22
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"];
28
29 // Map new string type identifiers to old style nsIUpdateItem types.
30 // Retired values:
31 // 32 = multipackage xpi file
32 // 8 = locale
33 // 256 = apiextension
34 // 128 = experiment
35 // theme = 4
36 const TYPES = {
37 extension: 2,
38 dictionary: 64,
39 };
40
41 const COMPATIBLE_BY_DEFAULT_TYPES = {
42 extension: true,
43 dictionary: true,
44 };
45
46 const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
47
48 function isXPI(filename) {
49 let ext = filename.slice(-4).toLowerCase();
50 return ext === ".xpi" || ext === ".zip";
51 }
52
53 /**
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.
57 *
58 * @param {nsIFile} aFile
59 * The file containing the resources, must be either a directory or an
60 * XPI file
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
64 * @returns {nsIURI}
65 * An nsIURI pointing at the resource
66 */
67 function getURIForResourceInFile(aFile, aPath) {
68 if (!isXPI(aFile.leafName)) {
69 let resource = aFile.clone();
70 if (aPath)
71 aPath.split("/").forEach(part => resource.append(part));
72
73 return Services.io.newFileURI(resource);
74 }
75
76 return buildJarURI(aFile, aPath);
77 }
78
79 /**
80 * Creates a jar: URI for a file inside a ZIP file.
81 *
82 * @param {nsIFile} aJarfile
83 * The ZIP file as an nsIFile
84 * @param {string} aPath
85 * The path inside the ZIP file
86 * @returns {nsIURI}
87 * An nsIURI for the file
88 */
89 function buildJarURI(aJarfile, aPath) {
90 let uri = Services.io.newFileURI(aJarfile);
91 uri = "jar:" + uri.spec + "!/" + aPath;
92 return Services.io.newURI(uri);
93 }
94
95 export var BootstrapLoader = {
96 name: "bootstrap",
97 manifestFile: "install.rdf",
98 async loadManifest(pkg) {
99 /**
100 * Reads locale properties from either the main install manifest root or
101 * an em:localized section in the install manifest.
102 *
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
107 * root
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
111 * this array
112 * @returns {Object}
113 * an object containing the locale properties
114 */
115 function readLocale(aSource, isDefault, aSeenLocales) {
116 let locale = {};
117 if (!isDefault) {
118 locale.locales = [];
119 for (let localeName of aSource.locales || []) {
120 if (!localeName) {
121 logger.warn("Ignoring empty locale in localized properties");
122 continue;
123 }
124 if (aSeenLocales.includes(localeName)) {
125 logger.warn("Ignoring duplicate locale in localized properties");
126 continue;
127 }
128 aSeenLocales.push(localeName);
129 locale.locales.push(localeName);
130 }
131
132 if (locale.locales.length == 0) {
133 logger.warn("Ignoring localized properties with no listed locales");
134 return null;
135 }
136 }
137
138 for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) {
139 if (hasOwnProperty(aSource, prop)) {
140 locale[prop] = aSource[prop];
141 }
142 }
143
144 return locale;
145 }
146
147 let manifestData = await pkg.readString("install.rdf");
148 let manifest = InstallRDF.loadFromString(manifestData).decode();
149
150 let addon = new AddonInternal();
151 for (let prop of PROP_METADATA) {
152 if (hasOwnProperty(manifest, prop)) {
153 addon[prop] = manifest[prop];
154 }
155 }
156
157 if (!addon.type) {
158 addon.type = "extension";
159 } else {
160 let type = addon.type;
161 addon.type = null;
162 for (let name in TYPES) {
163 if (TYPES[name] == type) {
164 addon.type = name;
165 break;
166 }
167 }
168 }
169
170 if (!(addon.type in TYPES))
171 throw new Error("Install manifest specifies unknown type: " + addon.type);
172
173 if (!addon.id)
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);
177 if (!addon.version)
178 throw new Error("No version in install manifest");
179
180 addon.strictCompatibility = (!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
181 manifest.strictCompatibility == "true");
182
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");
187 }
188
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);
193 }
194 } else {
195 // Convert legacy dictionaries into a format the WebExtension
196 // dictionary loader can process.
197 if (addon.type === "dictionary") {
198 addon.loader = null;
199 let dictionaries = {};
200 await pkg.iterFiles(({path}) => {
201 let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path);
202 if (match) {
203 let lang = match[1].replace(/_/g, "-");
204 dictionaries[lang] = match[0];
205 }
206 });
207 addon.startupData = {dictionaries};
208 }
209
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;
216 }
217
218 addon.defaultLocale = readLocale(manifest, true);
219
220 let seenLocales = [];
221 addon.locales = [];
222 for (let localeData of manifest.localized || []) {
223 let locale = readLocale(localeData, false, seenLocales);
224 if (locale)
225 addon.locales.push(locale);
226 }
227
228 let dependencies = new Set(manifest.dependencies);
229 addon.dependencies = Object.freeze(Array.from(dependencies));
230
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");
237 continue;
238 }
239 if (seenApplications.includes(targetApp.id)) {
240 logger.warn("Ignoring duplicate targetApplication entry for " + targetApp.id +
241 " in install manifest");
242 continue;
243 }
244 seenApplications.push(targetApp.id);
245 addon.targetApplications.push(targetApp);
246 }
247
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 || []) {
252 let platform = {
253 os: null,
254 abi: null,
255 };
256
257 let pos = targetPlatform.indexOf("_");
258 if (pos != -1) {
259 platform.os = targetPlatform.substring(0, pos);
260 platform.abi = targetPlatform.substring(pos + 1);
261 } else {
262 platform.os = targetPlatform;
263 }
264
265 addon.targetPlatforms.push(platform);
266 }
267
268 addon.userDisabled = false;
269 addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
270 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
271
272 addon.userPermissions = null;
273
274 addon.icons = {};
275 if (await pkg.hasResource("icon.png")) {
276 addon.icons[32] = "icon.png";
277 addon.icons[48] = "icon.png";
278 }
279
280 if (await pkg.hasResource("icon64.png")) {
281 addon.icons[64] = "icon64.png";
282 }
283
284 return addon;
285 },
286
287 loadScope(addon) {
288 let file = addon.file || addon._sourceBundle;
289 let uri = getURIForResourceInFile(file, "bootstrap.js").spec;
290 let principal = Services.scriptSecurityManager.getSystemPrincipal();
291
292 let sandbox = new Cu.Sandbox(principal, {
293 sandboxName: uri,
294 addonId: addon.id,
295 wantGlobalProperties: ["ChromeUtils"],
296 metadata: { addonID: addon.id, URI: uri },
297 });
298
299 try {
300 Object.assign(sandbox, BOOTSTRAP_REASONS);
301
302 ChromeUtils.defineLazyGetter(sandbox, "console", () =>
303 console.createInstance({ consoleID: `addon/${addon.id}` }));
304
305 Services.scriptloader.loadSubScript(uri, sandbox);
306 } catch (e) {
307 logger.warn(`Error loading bootstrap.js for ${addon.id}`, e);
308 }
309
310 function findMethod(name) {
311 if (sandbox[name]) {
312 return sandbox[name];
313 }
314
315 try {
316 let method = Cu.evalInSandbox(name, sandbox);
317 return method;
318 } catch (err) { }
319
320 return () => {
321 logger.warn(`Add-on ${addon.id} is missing bootstrap method ${name}`);
322 };
323 }
324
325 let install = findMethod("install");
326 let uninstall = findMethod("uninstall");
327 let startup = findMethod("startup");
328 let shutdown = findMethod("shutdown");
329
330 return {
331 install: (...args) => install(...args),
332
333 uninstall(...args) {
334 uninstall(...args);
335 // Forget any cached files we might've had from this extension.
336 Services.obs.notifyObservers(null, "startupcache-invalidate");
337 },
338
339 startup(...args) {
340 if (addon.type == "extension") {
341 logger.debug(`Registering manifest for ${file.path}\n`);
342 Components.manager.addBootstrappedManifestLocation(file);
343 }
344 return startup(...args);
345 },
346
347 shutdown(data, reason) {
348 try {
349 return shutdown(data, reason);
350 } catch (err) {
351 throw err;
352 } finally {
353 if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
354 logger.debug(`Removing manifest for ${file.path}\n`);
355 Components.manager.removeBootstrappedManifestLocation(file);
356 }
357 }
358 },
359 };
360 },
361 };
362
Imprint / Impressum