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