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