Source code

Revision control

Other Tools

1
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2
/* vim: set sts=2 sw=2 et tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
"use strict";
7
8
var EXPORTED_SYMBOLS = [
9
"Dictionary",
10
"Extension",
11
"ExtensionData",
12
"Langpack",
13
"Management",
14
];
15
16
/* exported Extension, ExtensionData */
17
/* globals Extension ExtensionData */
18
19
/*
20
* This file is the main entry point for extensions. When an extension
21
* loads, its bootstrap.js file creates a Extension instance
22
* and calls .startup() on it. It calls .shutdown() when the extension
23
* unloads. Extension manages any extension-specific state in
24
* the chrome process.
25
*
26
* TODO(rpl): we are current restricting the extensions to a single process
27
* (set as the current default value of the "dom.ipc.processCount.extension"
28
* preference), if we switch to use more than one extension process, we have to
29
* be sure that all the browser's frameLoader are associated to the same process,
30
* e.g. by using the `sameProcessAsFrameLoader` property.
32
*
33
* At that point we are going to keep track of the existing browsers associated to
34
* a webextension to ensure that they are all running in the same process (and we
35
* are also going to do the same with the browser element provided to the
36
* addon debugging Remote Debugging actor, e.g. because the addon has been
37
* reloaded by the user, we have to ensure that the new extension pages are going
38
* to run in the same process of the existing addon debugging browser element).
39
*/
40
41
const { XPCOMUtils } = ChromeUtils.import(
43
);
44
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
45
46
XPCOMUtils.defineLazyModuleGetters(this, {
48
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
68
});
69
70
// This is used for manipulating jar entry paths, which always use Unix
71
// separators.
72
XPCOMUtils.defineLazyGetter(this, "OSPath", () => {
73
let obj = {};
74
ChromeUtils.import("resource://gre/modules/osfile/ospath_unix.jsm", obj);
75
return obj;
76
});
77
78
XPCOMUtils.defineLazyGetter(this, "resourceProtocol", () =>
79
Services.io
80
.getProtocolHandler("resource")
81
.QueryInterface(Ci.nsIResProtocolHandler)
82
);
83
84
const { ExtensionCommon } = ChromeUtils.import(
86
);
87
const { ExtensionParent } = ChromeUtils.import(
89
);
90
const { ExtensionUtils } = ChromeUtils.import(
92
);
93
94
XPCOMUtils.defineLazyServiceGetters(this, {
95
aomStartup: [
96
"@mozilla.org/addons/addon-manager-startup;1",
97
"amIAddonManagerStartup",
98
],
99
spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
100
uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
101
});
102
103
XPCOMUtils.defineLazyPreferenceGetter(
104
this,
105
"processCount",
106
"dom.ipc.processCount.extension"
107
);
108
109
// Temporary pref to be turned on when ready.
110
XPCOMUtils.defineLazyPreferenceGetter(
111
this,
112
"allowPrivateBrowsingByDefault",
113
"extensions.allowPrivateBrowsingByDefault",
114
true
115
);
116
117
var {
118
GlobalManager,
119
ParentAPIManager,
120
StartupCache,
121
apiManager: Management,
122
} = ExtensionParent;
123
124
const { getUniqueId, promiseTimeout } = ExtensionUtils;
125
126
const { EventEmitter } = ExtensionCommon;
127
128
XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
129
130
XPCOMUtils.defineLazyGetter(
131
this,
132
"LocaleData",
133
() => ExtensionCommon.LocaleData
134
);
135
136
const { sharedData } = Services.ppmm;
137
138
const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";
139
140
// The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
141
// storage used by the browser.storage.local API is not directly accessible from the extension code,
142
// it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.jsm).
143
const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
144
145
// The maximum time to wait for extension child shutdown blockers to complete.
146
const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
147
148
// Permissions that are only available to privileged extensions.
149
const PRIVILEGED_PERMS = new Set([
150
"mozillaAddons",
151
"geckoViewAddons",
152
"telemetry",
153
"urlbar",
154
"normandyAddonStudy",
155
]);
156
157
/**
158
* Classify an individual permission from a webextension manifest
159
* as a host/origin permission, an api permission, or a regular permission.
160
*
161
* @param {string} perm The permission string to classify
162
* @param {boolean} restrictSchemes
163
* @param {boolean} isPrivileged whether or not the webextension is privileged
164
*
165
* @returns {object}
166
* An object with exactly one of the following properties:
167
* "origin" to indicate this is a host/origin permission.
168
* "api" to indicate this is an api permission
169
* (as used for webextensions experiments).
170
* "permission" to indicate this is a regular permission.
171
* "invalid" to indicate that the given permission cannot be used.
172
*/
173
function classifyPermission(perm, restrictSchemes, isPrivileged) {
174
let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
175
if (!match) {
176
try {
177
let { pattern } = new MatchPattern(perm, {
178
restrictSchemes,
179
ignorePath: true,
180
});
181
return { origin: pattern };
182
} catch (e) {
183
return { invalid: perm };
184
}
185
} else if (match[1] == "experiments" && match[2]) {
186
return { api: match[2] };
187
} else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) {
188
return { invalid: perm };
189
}
190
return { permission: perm };
191
}
192
193
const LOGGER_ID_BASE = "addons.webextension.";
194
const UUID_MAP_PREF = "extensions.webextensions.uuids";
195
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
196
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
197
198
const COMMENT_REGEXP = new RegExp(
199
String.raw`
200
^
201
(
202
(?:
203
[^"\n] |
204
" (?:[^"\\\n] | \\.)* "
205
)*?
206
)
207
208
//.*
209
`.replace(/\s+/g, ""),
210
"gm"
211
);
212
213
// All moz-extension URIs use a machine-specific UUID rather than the
214
// extension's own ID in the host component. This makes it more
215
// difficult for web pages to detect whether a user has a given add-on
216
// installed (by trying to load a moz-extension URI referring to a
217
// web_accessible_resource from the extension). UUIDMap.get()
218
// returns the UUID for a given add-on ID.
219
var UUIDMap = {
220
_read() {
221
let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}");
222
try {
223
return JSON.parse(pref);
224
} catch (e) {
225
Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
226
return {};
227
}
228
},
229
230
_write(map) {
231
Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map));
232
},
233
234
get(id, create = true) {
235
let map = this._read();
236
237
if (id in map) {
238
return map[id];
239
}
240
241
let uuid = null;
242
if (create) {
243
uuid = uuidGen.generateUUID().number;
244
uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
245
246
map[id] = uuid;
247
this._write(map);
248
}
249
return uuid;
250
},
251
252
remove(id) {
253
let map = this._read();
254
delete map[id];
255
this._write(map);
256
},
257
};
258
259
// For extensions that have called setUninstallURL(), send an event
260
// so the browser can display the URL.
261
var UninstallObserver = {
262
initialized: false,
263
264
init() {
265
if (!this.initialized) {
266
AddonManager.addAddonListener(this);
267
this.initialized = true;
268
}
269
},
270
271
onUninstalling(addon) {
272
let extension = GlobalManager.extensionMap.get(addon.id);
273
if (extension) {
274
// Let any other interested listeners respond
275
// (e.g., display the uninstall URL)
276
Management.emit("uninstalling", extension);
277
}
278
},
279
280
onUninstalled(addon) {
281
let uuid = UUIDMap.get(addon.id, false);
282
if (!uuid) {
283
return;
284
}
285
286
if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
287
// Clear browser.storage.local backends.
288
AsyncShutdown.profileChangeTeardown.addBlocker(
289
`Clear Extension Storage ${addon.id} (File Backend)`,
290
ExtensionStorage.clear(addon.id, { shouldNotifyListeners: false })
291
);
292
293
// Clear any IndexedDB storage created by the extension
294
// If LSNG is enabled, this also clears localStorage.
295
let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
296
let principal = Services.scriptSecurityManager.createContentPrincipal(
297
baseURI,
298
{}
299
);
300
Services.qms.clearStoragesForPrincipal(principal);
301
302
// Clear any storage.local data stored in the IDBBackend.
303
let storagePrincipal = Services.scriptSecurityManager.createContentPrincipal(
304
baseURI,
305
{
306
userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID,
307
}
308
);
309
Services.qms.clearStoragesForPrincipal(storagePrincipal);
310
311
ExtensionStorageIDB.clearMigratedExtensionPref(addon.id);
312
313
// If LSNG is not enabled, we need to clear localStorage explicitly using
314
// the old API.
315
if (!Services.lsm.nextGenLocalStorageEnabled) {
316
// Clear localStorage created by the extension
317
let storage = Services.domStorageManager.getStorage(
318
null,
319
principal,
320
storagePrincipal
321
);
322
if (storage) {
323
storage.clear();
324
}
325
}
326
327
// Remove any permissions related to the unlimitedStorage permission
328
// if we are also removing all the data stored by the extension.
329
Services.perms.removeFromPrincipal(
330
principal,
331
"WebExtensions-unlimitedStorage"
332
);
333
Services.perms.removeFromPrincipal(principal, "indexedDB");
334
Services.perms.removeFromPrincipal(principal, "persistent-storage");
335
}
336
337
ExtensionPermissions.removeAll(addon.id);
338
339
if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
340
// Clear the entry in the UUID map
341
UUIDMap.remove(addon.id);
342
}
343
},
344
};
345
346
UninstallObserver.init();
347
348
const manifestTypes = new Map([
349
["theme", "manifest.ThemeManifest"],
350
["langpack", "manifest.WebExtensionLangpackManifest"],
351
["dictionary", "manifest.WebExtensionDictionaryManifest"],
352
["extension", "manifest.WebExtensionManifest"],
353
]);
354
355
/**
356
* Represents the data contained in an extension, contained either
357
* in a directory or a zip file, which may or may not be installed.
358
* This class implements the functionality of the Extension class,
359
* primarily related to manifest parsing and localization, which is
360
* useful prior to extension installation or initialization.
361
*
362
* No functionality of this class is guaranteed to work before
363
* `loadManifest` has been called, and completed.
364
*/
365
class ExtensionData {
366
constructor(rootURI) {
367
this.rootURI = rootURI;
368
this.resourceURL = rootURI.spec;
369
370
this.manifest = null;
371
this.type = null;
372
this.id = null;
373
this.uuid = null;
374
this.localeData = null;
375
this._promiseLocales = null;
376
377
this.apiNames = new Set();
378
this.dependencies = new Set();
379
this.permissions = new Set();
380
381
this.startupData = null;
382
383
this.errors = [];
384
this.warnings = [];
385
}
386
387
get builtinMessages() {
388
return null;
389
}
390
391
get logger() {
392
let id = this.id || "<unknown>";
393
return Log.repository.getLogger(LOGGER_ID_BASE + id);
394
}
395
396
/**
397
* Report an error about the extension's manifest file.
398
* @param {string} message The error message
399
*/
400
manifestError(message) {
401
this.packagingError(`Reading manifest: ${message}`);
402
}
403
404
/**
405
* Report a warning about the extension's manifest file.
406
* @param {string} message The warning message
407
*/
408
manifestWarning(message) {
409
this.packagingWarning(`Reading manifest: ${message}`);
410
}
411
412
// Report an error about the extension's general packaging.
413
packagingError(message) {
414
this.errors.push(message);
415
this.logError(message);
416
}
417
418
packagingWarning(message) {
419
this.warnings.push(message);
420
this.logWarning(message);
421
}
422
423
logWarning(message) {
424
this._logMessage(message, "warn");
425
}
426
427
logError(message) {
428
this._logMessage(message, "error");
429
}
430
431
_logMessage(message, severity) {
432
this.logger[severity](`Loading extension '${this.id}': ${message}`);
433
}
434
435
/**
436
* Returns the moz-extension: URL for the given path within this
437
* extension.
438
*
439
* Must not be called unless either the `id` or `uuid` property has
440
* already been set.
441
*
442
* @param {string} path The path portion of the URL.
443
* @returns {string}
444
*/
445
getURL(path = "") {
446
if (!(this.id || this.uuid)) {
447
throw new Error(
448
"getURL may not be called before an `id` or `uuid` has been set"
449
);
450
}
451
if (!this.uuid) {
452
this.uuid = UUIDMap.get(this.id);
453
}
454
return `moz-extension://${this.uuid}/${path}`;
455
}
456
457
async readDirectory(path) {
458
if (this.rootURI instanceof Ci.nsIFileURL) {
459
let uri = Services.io.newURI("./" + path, null, this.rootURI);
460
let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
461
462
let iter = new OS.File.DirectoryIterator(fullPath);
463
let results = [];
464
465
try {
466
await iter.forEach(entry => {
467
results.push(entry);
468
});
469
} catch (e) {
470
// Always return a list, even if the directory does not exist (or is
471
// not a directory) for symmetry with the ZipReader behavior.
472
if (!e.becauseNoSuchFile) {
473
Cu.reportError(e);
474
}
475
}
476
iter.close();
477
478
return results;
479
}
480
481
let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
482
483
// Append the sub-directory path to the base JAR URI and normalize the
484
// result.
485
let entry = `${uri.JAREntry}/${path}/`
486
.replace(/\/\/+/g, "/")
487
.replace(/^\//, "");
488
uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`);
489
490
let results = [];
491
for (let name of aomStartup.enumerateJARSubtree(uri)) {
492
if (!name.startsWith(entry)) {
493
throw new Error("Unexpected ZipReader entry");
494
}
495
496
// The enumerator returns the full path of all entries.
497
// Trim off the leading path, and filter out entries from
498
// subdirectories.
499
name = name.slice(entry.length);
500
if (name && !/\/./.test(name)) {
501
results.push({
502
name: name.replace("/", ""),
503
isDir: name.endsWith("/"),
504
});
505
}
506
}
507
508
return results;
509
}
510
511
readJSON(path) {
512
return new Promise((resolve, reject) => {
513
let uri = this.rootURI.resolve(`./${path}`);
514
515
NetUtil.asyncFetch(
516
{ uri, loadUsingSystemPrincipal: true },
517
(inputStream, status) => {
518
if (!Components.isSuccessCode(status)) {
519
// Convert status code to a string
520
let e = Components.Exception("", status);
521
reject(new Error(`Error while loading '${uri}' (${e.name})`));
522
return;
523
}
524
try {
525
let text = NetUtil.readInputStreamToString(
526
inputStream,
527
inputStream.available(),
528
{ charset: "utf-8" }
529
);
530
531
text = text.replace(COMMENT_REGEXP, "$1");
532
533
resolve(JSON.parse(text));
534
} catch (e) {
535
reject(e);
536
}
537
}
538
);
539
});
540
}
541
542
get restrictSchemes() {
543
// ExtensionData can't check the signature (as it is not yet passed to its constructor
544
// as it is for the Extension class, where this getter is overridden to check both the
545
// signature and the permissions).
546
return !this.hasPermission("mozillaAddons");
547
}
548
549
/**
550
* Returns an object representing any capabilities that the extension
551
* has access to based on fixed properties in the manifest. The result
552
* includes the contents of the "permissions" property as well as other
553
* capabilities that are derived from manifest fields that users should
554
* be informed of (e.g., origins where content scripts are injected).
555
*/
556
get manifestPermissions() {
557
if (this.type !== "extension") {
558
return null;
559
}
560
561
let permissions = new Set();
562
let origins = new Set();
563
let { restrictSchemes, isPrivileged } = this;
564
for (let perm of this.manifest.permissions || []) {
565
let type = classifyPermission(perm, restrictSchemes, isPrivileged);
566
if (type.origin) {
567
origins.add(perm);
568
} else if (type.permission) {
569
permissions.add(perm);
570
}
571
}
572
573
if (this.manifest.devtools_page) {
574
permissions.add("devtools");
575
}
576
577
for (let entry of this.manifest.content_scripts || []) {
578
for (let origin of entry.matches) {
579
origins.add(origin);
580
}
581
}
582
583
return {
584
permissions: Array.from(permissions),
585
origins: Array.from(origins),
586
};
587
}
588
589
/**
590
* Returns an object representing all capabilities this extension has
591
* access to, including fixed ones from the manifest as well as dynamically
592
* granted permissions.
593
*/
594
get activePermissions() {
595
if (this.type !== "extension") {
596
return null;
597
}
598
599
let result = {
600
origins: this.whiteListedHosts.patterns
601
.map(matcher => matcher.pattern)
602
// moz-extension://id/* is always added to whiteListedHosts, but it
603
// is not a valid host permission in the API. So, remove it.
604
.filter(pattern => !pattern.startsWith("moz-extension:")),
605
apis: [...this.apiNames],
606
};
607
608
const EXP_PATTERN = /^experiments\.\w+/;
609
result.permissions = [...this.permissions].filter(
610
p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
611
);
612
return result;
613
}
614
615
// Compute the difference between two sets of permissions, suitable
616
// for presenting to the user.
617
static comparePermissions(oldPermissions, newPermissions) {
618
let oldMatcher = new MatchPatternSet(oldPermissions.origins, {
619
restrictSchemes: false,
620
});
621
return {
622
// formatPermissionStrings ignores any scheme, so only look at the domain.
623
origins: newPermissions.origins.filter(
624
perm =>
625
!oldMatcher.subsumesDomain(
626
new MatchPattern(perm, { restrictSchemes: false })
627
)
628
),
629
permissions: newPermissions.permissions.filter(
630
perm => !oldPermissions.permissions.includes(perm)
631
),
632
};
633
}
634
635
canUseExperiment(manifest) {
636
return this.experimentsAllowed && manifest.experiment_apis;
637
}
638
639
/**
640
* Load a locale and return a localized manifest. The extension must
641
* be initialized, and manifest parsed prior to calling.
642
*
643
* @param {string} locale to load, if necessary.
644
* @returns {object} normalized manifest.
645
*/
646
async getLocalizedManifest(locale) {
647
if (!this.type || !this.localeData) {
648
throw new Error("The extension has not been initialized.");
649
}
650
// Upon update or reinstall, the Extension.manifest may be read from
651
// StartupCache.manifest, however rawManifest is *not*. We need the
652
// raw manifest in order to get a localized manifest.
653
if (!this.rawManifest) {
654
this.rawManifest = await this.readJSON("manifest.json");
655
}
656
657
if (!this.localeData.has(locale)) {
658
// Locales are not avialable until some additional
659
// initialization is done. We could just call initAllLocales,
660
// but that is heavy handed, especially when we likely only
661
// need one out of 20.
662
let locales = await this.promiseLocales();
663
if (locales.get(locale)) {
664
await this.initLocale(locale);
665
}
666
if (!this.localeData.has(locale)) {
667
throw new Error(`The extension does not contain the locale ${locale}`);
668
}
669
}
670
let normalized = await this._getNormalizedManifest(locale);
671
if (normalized.error) {
672
throw new Error(normalized.error);
673
}
674
return normalized.value;
675
}
676
677
async _getNormalizedManifest(locale) {
678
let manifestType = manifestTypes.get(this.type);
679
680
let context = {
681
url: this.baseURI && this.baseURI.spec,
682
principal: this.principal,
683
logError: error => {
684
this.manifestWarning(error);
685
},
686
preprocessors: {},
687
};
688
689
if (this.localeData) {
690
context.preprocessors.localize = (value, context) =>
691
this.localize(value, locale);
692
}
693
694
return Schemas.normalize(this.rawManifest, manifestType, context);
695
}
696
697
// eslint-disable-next-line complexity
698
async parseManifest() {
699
let [manifest] = await Promise.all([
700
this.readJSON("manifest.json"),
701
Management.lazyInit(),
702
]);
703
704
this.manifest = manifest;
705
this.rawManifest = manifest;
706
707
if (
708
allowPrivateBrowsingByDefault &&
709
"incognito" in manifest &&
710
manifest.incognito == "not_allowed"
711
) {
712
throw new Error(
713
`manifest.incognito set to "not_allowed" is currently unvailable for use.`
714
);
715
}
716
717
if (manifest && manifest.default_locale) {
718
await this.initLocale();
719
}
720
721
if (this.manifest.theme) {
722
this.type = "theme";
723
} else if (this.manifest.langpack_id) {
724
this.type = "langpack";
725
} else if (this.manifest.dictionaries) {
726
this.type = "dictionary";
727
} else {
728
this.type = "extension";
729
}
730
731
let normalized = await this._getNormalizedManifest();
732
if (normalized.error) {
733
this.manifestError(normalized.error);
734
return null;
735
}
736
737
manifest = normalized.value;
738
739
let id;
740
try {
741
if (manifest.applications.gecko.id) {
742
id = manifest.applications.gecko.id;
743
}
744
} catch (e) {
745
// Errors are handled by the type checks above.
746
}
747
748
if (!this.id) {
749
this.id = id;
750
}
751
752
let apiNames = new Set();
753
let dependencies = new Set();
754
let originPermissions = new Set();
755
let permissions = new Set();
756
let webAccessibleResources = [];
757
758
let schemaPromises = new Map();
759
760
let result = {
761
apiNames,
762
dependencies,
763
id,
764
manifest,
765
modules: null,
766
originPermissions,
767
permissions,
768
schemaURLs: null,
769
type: this.type,
770
webAccessibleResources,
771
};
772
773
if (this.type === "extension") {
774
let { isPrivileged } = this;
775
let restrictSchemes = !(
776
isPrivileged && manifest.permissions.includes("mozillaAddons")
777
);
778
779
for (let perm of manifest.permissions) {
780
if (perm === "geckoProfiler" && !isPrivileged) {
781
const acceptedExtensions = Services.prefs.getStringPref(
782
"extensions.geckoProfiler.acceptedExtensionIds",
783
""
784
);
785
if (!acceptedExtensions.split(",").includes(id)) {
786
this.manifestError(
787
"Only whitelisted extensions are allowed to access the geckoProfiler."
788
);
789
continue;
790
}
791
}
792
793
let type = classifyPermission(perm, restrictSchemes, isPrivileged);
794
if (type.origin) {
795
perm = type.origin;
796
originPermissions.add(perm);
797
} else if (type.api) {
798
apiNames.add(type.api);
799
} else if (type.invalid) {
800
this.manifestWarning(`Invalid extension permission: ${perm}`);
801
continue;
802
}
803
804
permissions.add(perm);
805
}
806
807
if (this.id) {
808
// An extension always gets permission to its own url.
809
let matcher = new MatchPattern(this.getURL(), { ignorePath: true });
810
originPermissions.add(matcher.pattern);
811
812
// Apply optional permissions
813
let perms = await ExtensionPermissions.get(this.id);
814
for (let perm of perms.permissions) {
815
permissions.add(perm);
816
}
817
for (let origin of perms.origins) {
818
originPermissions.add(origin);
819
}
820
}
821
822
for (let api of apiNames) {
823
dependencies.add(`${api}@experiments.addons.mozilla.org`);
824
}
825
826
let moduleData = data => ({
827
url: this.rootURI.resolve(data.script),
828
events: data.events,
829
paths: data.paths,
830
scopes: data.scopes,
831
});
832
833
let computeModuleInit = (scope, modules) => {
834
let manager = new ExtensionCommon.SchemaAPIManager(scope);
835
return manager.initModuleJSON([modules]);
836
};
837
838
result.contentScripts = [];
839
for (let options of manifest.content_scripts || []) {
840
result.contentScripts.push({
841
allFrames: options.all_frames,
842
matchAboutBlank: options.match_about_blank,
843
frameID: options.frame_id,
844
runAt: options.run_at,
845
846
matches: options.matches,
847
excludeMatches: options.exclude_matches || [],
848
includeGlobs: options.include_globs,
849
excludeGlobs: options.exclude_globs,
850
851
jsPaths: options.js || [],
852
cssPaths: options.css || [],
853
});
854
}
855
856
if (this.canUseExperiment(manifest)) {
857
let parentModules = {};
858
let childModules = {};
859
860
for (let [name, data] of Object.entries(manifest.experiment_apis)) {
861
let schema = this.getURL(data.schema);
862
863
if (!schemaPromises.has(schema)) {
864
schemaPromises.set(
865
schema,
866
this.readJSON(data.schema).then(json =>
867
Schemas.processSchema(json)
868
)
869
);
870
}
871
872
if (data.parent) {
873
parentModules[name] = moduleData(data.parent);
874
}
875
876
if (data.child) {
877
childModules[name] = moduleData(data.child);
878
}
879
}
880
881
result.modules = {
882
child: computeModuleInit("addon_child", childModules),
883
parent: computeModuleInit("addon_parent", parentModules),
884
};
885
}
886
887
// Normalize all patterns to contain a single leading /
888
if (manifest.web_accessible_resources) {
889
webAccessibleResources.push(
890
...manifest.web_accessible_resources.map(path =>
891
path.replace(/^\/*/, "/")
892
)
893
);
894
}
895
} else if (this.type == "langpack") {
896
// Langpack startup is performance critical, so we want to compute as much
897
// as possible here to make startup not trigger async DB reads.
898
// We'll store the four items below in the startupData.
899
900
// 1. Compute the chrome resources to be registered for this langpack.
901
const platform = AppConstants.platform;
902
const chromeEntries = [];
903
for (const [language, entry] of Object.entries(manifest.languages)) {
904
for (const [alias, path] of Object.entries(
905
entry.chrome_resources || {}
906
)) {
907
if (typeof path === "string") {
908
chromeEntries.push(["locale", alias, language, path]);
909
} else if (platform in path) {
910
// If the path is not a string, it's an object with path per
911
// platform where the keys are taken from AppConstants.platform
912
chromeEntries.push(["locale", alias, language, path[platform]]);
913
}
914
}
915
}
916
917
// 2. Compute langpack ID.
918
const productCodeName = AppConstants.MOZ_BUILD_APP.replace("/", "-");
919
920
// The result path looks like this:
921
// Firefox - `langpack-pl-browser`
922
// Fennec - `langpack-pl-mobile-android`
923
const langpackId = `langpack-${manifest.langpack_id}-${productCodeName}`;
924
925
// 3. Compute L10nRegistry sources for this langpack.
926
const l10nRegistrySources = {};
927
928
// Check if there's a root directory `/localization` in the langpack.
929
// If there is one, add it with the name `toolkit` as a FileSource.
930
const entries = await this.readDirectory("localization");
931
if (entries.length > 0) {
932
l10nRegistrySources.toolkit = "";
933
}
934
935
// Add any additional sources listed in the manifest
936
if (manifest.sources) {
937
for (const [sourceName, { base_path }] of Object.entries(
938
manifest.sources
939
)) {
940
l10nRegistrySources[sourceName] = base_path;
941
}
942
}
943
944
// 4. Save the list of languages handled by this langpack.
945
const languages = Object.keys(manifest.languages);
946
947
this.startupData = {
948
chromeEntries,
949
langpackId,
950
l10nRegistrySources,
951
languages,
952
};
953
} else if (this.type == "dictionary") {
954
let dictionaries = {};
955
for (let [lang, path] of Object.entries(manifest.dictionaries)) {
956
path = path.replace(/^\/+/, "");
957
958
let dir = OSPath.dirname(path);
959
if (dir === ".") {
960
dir = "";
961
}
962
let leafName = OSPath.basename(path);
963
let affixPath = leafName.slice(0, -3) + "aff";
964
965
let entries = Array.from(
966
await this.readDirectory(dir),
967
entry => entry.name
968
);
969
if (!entries.includes(leafName)) {
970
this.manifestError(
971
`Invalid dictionary path specified for '${lang}': ${path}`
972
);
973
}
974
if (!entries.includes(affixPath)) {
975
this.manifestError(
976
`Invalid dictionary path specified for '${lang}': Missing affix file: ${path}`
977
);
978
}
979
980
dictionaries[lang] = path;
981
}
982
983
this.startupData = { dictionaries };
984
}
985
986
if (schemaPromises.size) {
987
let schemas = new Map();
988
for (let [url, promise] of schemaPromises) {
989
schemas.set(url, await promise);
990
}
991
result.schemaURLs = schemas;
992
}
993
994
return result;
995
}
996
997
// Reads the extension's |manifest.json| file, and stores its
998
// parsed contents in |this.manifest|.
999
async loadManifest() {
1000
let [manifestData] = await Promise.all([
1001
this.parseManifest(),
1002
Management.lazyInit(),
1003
]);
1004
1005
if (!manifestData) {
1006
return;
1007
}
1008
1009
// Do not override the add-on id that has been already assigned.
1010
if (!this.id) {
1011
this.id = manifestData.id;
1012
}
1013
1014
this.manifest = manifestData.manifest;
1015
this.apiNames = manifestData.apiNames;
1016
this.contentScripts = manifestData.contentScripts;
1017
this.dependencies = manifestData.dependencies;
1018
this.permissions = manifestData.permissions;
1019
this.schemaURLs = manifestData.schemaURLs;
1020
this.type = manifestData.type;
1021
1022
this.modules = manifestData.modules;
1023
1024
this.apiManager = this.getAPIManager();
1025
await this.apiManager.lazyInit();
1026
1027
this.webAccessibleResources = manifestData.webAccessibleResources.map(
1028
res => new MatchGlob(res)
1029
);
1030
this.whiteListedHosts = new MatchPatternSet(
1031
manifestData.originPermissions,
1032
{
1033
restrictSchemes: this.restrictSchemes,
1034
}
1035
);
1036
1037
return this.manifest;
1038
}
1039
1040
hasPermission(perm, includeOptional = false) {
1041
// If the permission is a "manifest property" permission, we check if the extension
1042
// does have the required property in its manifest.
1043
let manifest_ = "manifest:";
1044
if (perm.startsWith(manifest_)) {
1045
// Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
1046
let value = this.manifest;
1047
for (let prop of perm.substr(manifest_.length).split(".")) {
1048
if (!value) {
1049
break;
1050
}
1051
value = value[prop];
1052
}
1053
1054
return value != null;
1055
}
1056
1057
if (this.permissions.has(perm)) {
1058
return true;
1059
}
1060
1061
if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
1062
return true;
1063
}
1064
1065
return false;
1066
}
1067
1068
getAPIManager() {
1069
let apiManagers = [Management];
1070
1071
for (let id of this.dependencies) {
1072
let policy = WebExtensionPolicy.getByID(id);
1073
if (policy) {
1074
if (policy.extension.experimentAPIManager) {
1075
apiManagers.push(policy.extension.experimentAPIManager);
1076
} else if (AppConstants.DEBUG) {
1077
Cu.reportError(`Cannot find experimental API exported from ${id}`);
1078
}
1079
}
1080
}
1081
1082
if (this.modules) {
1083
this.experimentAPIManager = new ExtensionCommon.LazyAPIManager(
1084
"main",
1085
this.modules.parent,
1086
this.schemaURLs
1087
);
1088
1089
apiManagers.push(this.experimentAPIManager);
1090
}
1091
1092
if (apiManagers.length == 1) {
1093
return apiManagers[0];
1094
}
1095
1096
return new ExtensionCommon.MultiAPIManager("main", apiManagers.reverse());
1097
}
1098
1099
localizeMessage(...args) {
1100
return this.localeData.localizeMessage(...args);
1101
}
1102
1103
localize(...args) {
1104
return this.localeData.localize(...args);
1105
}
1106
1107
// If a "default_locale" is specified in that manifest, returns it
1108
// as a Gecko-compatible locale string. Otherwise, returns null.
1109
get defaultLocale() {
1110
if (this.manifest.default_locale != null) {
1111
return this.normalizeLocaleCode(this.manifest.default_locale);
1112
}
1113
1114
return null;
1115
}
1116
1117
// Normalizes a Chrome-compatible locale code to the appropriate
1118
// Gecko-compatible variant. Currently, this means simply
1119
// replacing underscores with hyphens.
1120
normalizeLocaleCode(locale) {
1121
return locale.replace(/_/g, "-");
1122
}
1123
1124
// Reads the locale file for the given Gecko-compatible locale code, and
1125
// stores its parsed contents in |this.localeMessages.get(locale)|.
1126
async readLocaleFile(locale) {
1127
let locales = await this.promiseLocales();
1128
let dir = locales.get(locale) || locale;
1129
let file = `_locales/${dir}/messages.json`;
1130
1131
try {
1132
let messages = await this.readJSON(file);
1133
return this.localeData.addLocale(locale, messages, this);
1134
} catch (e) {
1135
this.packagingError(`Loading locale file ${file}: ${e}`);
1136
return new Map();
1137
}
1138
}
1139
1140
async _promiseLocaleMap() {
1141
let locales = new Map();
1142
1143
let entries = await this.readDirectory("_locales");
1144
for (let file of entries) {
1145
if (file.isDir) {
1146
let locale = this.normalizeLocaleCode(file.name);
1147
locales.set(locale, file.name);
1148
}
1149
}
1150
1151
return locales;
1152
}
1153
1154
_setupLocaleData(locales) {
1155
if (this.localeData) {
1156
return this.localeData.locales;
1157
}
1158
1159
this.localeData = new LocaleData({
1160
defaultLocale: this.defaultLocale,
1161
locales,
1162
builtinMessages: this.builtinMessages,
1163
});
1164
1165
return locales;
1166
}
1167
1168
// Reads the list of locales available in the extension, and returns a
1169
// Promise which resolves to a Map upon completion.
1170
// Each map key is a Gecko-compatible locale code, and each value is the
1171
// "_locales" subdirectory containing that locale:
1172
//
1173
// Map(gecko-locale-code -> locale-directory-name)
1174
promiseLocales() {
1175
if (!this._promiseLocales) {
1176
this._promiseLocales = (async () => {
1177
let locales = this._promiseLocaleMap();
1178
return this._setupLocaleData(locales);
1179
})();
1180
}
1181
1182
return this._promiseLocales;
1183
}
1184
1185
// Reads the locale messages for all locales, and returns a promise which
1186
// resolves to a Map of locale messages upon completion. Each key in the map
1187
// is a Gecko-compatible locale code, and each value is a locale data object
1188
// as returned by |readLocaleFile|.
1189
async initAllLocales() {
1190
let locales = await this.promiseLocales();
1191
1192
await Promise.all(
1193
Array.from(locales.keys(), locale => this.readLocaleFile(locale))
1194
);
1195
1196
let defaultLocale = this.defaultLocale;
1197
if (defaultLocale) {
1198
if (!locales.has(defaultLocale)) {
1199
this.manifestError(
1200
'Value for "default_locale" property must correspond to ' +
1201
'a directory in "_locales/". Not found: ' +
1202
JSON.stringify(`_locales/${this.manifest.default_locale}/`)
1203
);
1204
}
1205
} else if (locales.size) {
1206
this.manifestError(
1207
'The "default_locale" property is required when a ' +
1208
'"_locales/" directory is present.'
1209
);
1210
}
1211
1212
return this.localeData.messages;
1213
}
1214
1215
// Reads the locale file for the given Gecko-compatible locale code, or the
1216
// default locale if no locale code is given, and sets it as the currently
1217
// selected locale on success.
1218
//
1219
// Pre-loads the default locale for fallback message processing, regardless
1220
// of the locale specified.
1221
//
1222
// If no locales are unavailable, resolves to |null|.
1223
async initLocale(locale = this.defaultLocale) {
1224
if (locale == null) {
1225
return null;
1226
}
1227
1228
let promises = [this.readLocaleFile(locale)];
1229
1230
let { defaultLocale } = this;
1231
if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
1232
promises.push(this.readLocaleFile(defaultLocale));
1233
}
1234
1235
let results = await Promise.all(promises);
1236
1237
this.localeData.selectedLocale = locale;
1238
return results[0];
1239
}
1240
1241
/**
1242
* Formats all the strings for a permissions dialog/notification.
1243
*
1244
* @param {object} info Information about the permissions being requested.
1245
*
1246
* @param {array<string>} info.permissions.origins
1247
* Origin permissions requested.
1248
* @param {array<string>} info.permissions.permissions
1249
* Regular (non-origin) permissions requested.
1250
* @param {boolean} info.unsigned
1251
* True if the prompt is for installing an unsigned addon.
1252
* @param {string} info.type
1253
* The type of prompt being shown. May be one of "update",
1254
* "sideload", "optional", or omitted for a regular
1255
* install prompt.
1256
* @param {string} info.appName
1257
* The localized name of the application, to be substituted
1258
* in computed strings as needed.
1259
* @param {nsIStringBundle} bundle
1260
* The string bundle to use for l10n.
1261
*
1262
* @returns {object} An object with properties containing localized strings
1263
* for various elements of a permission dialog. The "header"
1264
* property on this object is the notification header text
1265
* and it has the string "<>" as a placeholder for the
1266
* addon name.
1267
*/
1268
static formatPermissionStrings(info, bundle) {
1269
let result = {};
1270
1271
let perms = info.permissions || { origins: [], permissions: [] };
1272
1273
// First classify our host permissions
1274
let allUrls = false,
1275
wildcards = new Set(),
1276
sites = new Set();
1277
for (let permission of perms.origins) {
1278
if (permission == "<all_urls>") {
1279
allUrls = true;
1280
break;
1281
}
1282
1283
// Privileged extensions may request access to "about:"-URLs, such as
1284
// about:reader.
1285
let match = /^[a-z*]+:\/\/([^/]*)\/|^about:/.exec(permission);
1286
if (!match) {
1287
throw new Error(`Unparseable host permission ${permission}`);
1288
}
1289
// Note: the scheme is ignored in the permission warnings. If this ever
1290
// changes, update the comparePermissions method as needed.
1291
if (!match[1] || match[1] == "*") {
1292
allUrls = true;
1293
} else if (match[1].startsWith("*.")) {
1294
wildcards.add(match[1].slice(2));
1295
} else {
1296
sites.add(match[1]);
1297
}
1298
}
1299
1300
// Format the host permissions. If we have a wildcard for all urls,
1301
// a single string will suffice. Otherwise, show domain wildcards
1302
// first, then individual host permissions.
1303
result.msgs = [];
1304
if (allUrls) {
1305
result.msgs.push(
1306
bundle.GetStringFromName("webextPerms.hostDescription.allUrls")
1307
);
1308
} else {
1309
// Formats a list of host permissions. If we have 4 or fewer, display
1310
// them all, otherwise display the first 3 followed by an item that
1311
// says "...plus N others"
1312
let format = (list, itemKey, moreKey) => {
1313
function formatItems(items) {
1314
result.msgs.push(
1315
...items.map(item => bundle.formatStringFromName(itemKey, [item]))
1316
);
1317
}
1318
if (list.length < 5) {
1319
formatItems(list);
1320
} else {
1321
formatItems(list.slice(0, 3));
1322
1323
let remaining = list.length - 3;
1324
result.msgs.push(
1325
PluralForm.get(
1326
remaining,
1327
bundle.GetStringFromName(moreKey)
1328
).replace("#1", remaining)
1329
);
1330
}
1331
};
1332
1333
format(
1334
Array.from(wildcards),
1335
"webextPerms.hostDescription.wildcard",
1336
"webextPerms.hostDescription.tooManyWildcards"
1337
);
1338
format(
1339
Array.from(sites),
1340
"webextPerms.hostDescription.oneSite",
1341
"webextPerms.hostDescription.tooManySites"
1342
);
1343
}
1344
1345
let permissionKey = perm => `webextPerms.description.${perm}`;
1346
1347
// Next, show the native messaging permission if it is present.
1348
const NATIVE_MSG_PERM = "nativeMessaging";
1349
if (perms.permissions.includes(NATIVE_MSG_PERM)) {
1350
result.msgs.push(
1351
bundle.formatStringFromName(permissionKey(NATIVE_MSG_PERM), [
1352
info.appName,
1353
])
1354
);
1355
}
1356
1357
// Finally, show remaining permissions, in the same order as AMO.
1358
// The permissions are sorted alphabetically by the permission
1359
// string to match AMO.
1360
let permissionsCopy = perms.permissions.slice(0);
1361
for (let permission of permissionsCopy.sort()) {
1362
// Handled above
1363
if (permission == "nativeMessaging") {
1364
continue;
1365
}
1366
try {
1367
result.msgs.push(bundle.GetStringFromName(permissionKey(permission)));
1368
} catch (err) {
1369
// We deliberately do not include all permissions in the prompt.
1370
// So if we don't find one then just skip it.
1371
}
1372
}
1373
1374
const haveAccessKeys = AppConstants.platform !== "android";
1375
1376
result.header = bundle.formatStringFromName("webextPerms.header", ["<>"]);
1377
result.text = info.unsigned
1378
? bundle.GetStringFromName("webextPerms.unsignedWarning")
1379
: "";
1380
result.listIntro = bundle.GetStringFromName("webextPerms.listIntro");
1381
1382
result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
1383
result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
1384
if (haveAccessKeys) {
1385
result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
1386
result.cancelKey = bundle.GetStringFromName(
1387
"webextPerms.cancel.accessKey"
1388
);
1389
}
1390
1391
if (info.type == "sideload") {
1392
result.header = bundle.formatStringFromName(
1393
"webextPerms.sideloadHeader",
1394
["<>"]
1395
);
1396
let key =
1397
result.msgs.length == 0
1398
? "webextPerms.sideloadTextNoPerms"
1399
: "webextPerms.sideloadText2";
1400
result.text = bundle.GetStringFromName(key);
1401
result.acceptText = bundle.GetStringFromName(
1402
"webextPerms.sideloadEnable.label"
1403
);
1404
result.cancelText = bundle.GetStringFromName(
1405
"webextPerms.sideloadCancel.label"
1406
);
1407
if (haveAccessKeys) {
1408
result.acceptKey = bundle.GetStringFromName(
1409
"webextPerms.sideloadEnable.accessKey"
1410
);
1411
result.cancelKey = bundle.GetStringFromName(
1412
"webextPerms.sideloadCancel.accessKey"
1413
);
1414
}
1415
} else if (info.type == "update") {
1416
result.header = bundle.formatStringFromName("webextPerms.updateText", [
1417
"<>",
1418
]);
1419
result.text = "";
1420
result.acceptText = bundle.GetStringFromName(
1421
"webextPerms.updateAccept.label"
1422
);
1423
if (haveAccessKeys) {
1424
result.acceptKey = bundle.GetStringFromName(
1425
"webextPerms.updateAccept.accessKey"
1426
);
1427
}
1428
} else if (info.type == "optional") {
1429
result.header = bundle.formatStringFromName(
1430
"webextPerms.optionalPermsHeader",
1431
["<>"]
1432
);
1433
result.text = "";
1434
result.listIntro = bundle.GetStringFromName(
1435
"webextPerms.optionalPermsListIntro"
1436
);
1437
result.acceptText = bundle.GetStringFromName(
1438
"webextPerms.optionalPermsAllow.label"
1439
);
1440
result.cancelText = bundle.GetStringFromName(
1441
"webextPerms.optionalPermsDeny.label"
1442
);
1443
if (haveAccessKeys) {
1444
result.acceptKey = bundle.GetStringFromName(
1445
"webextPerms.optionalPermsAllow.accessKey"
1446
);
1447
result.cancelKey = bundle.GetStringFromName(
1448
"webextPerms.optionalPermsDeny.accessKey"
1449
);
1450
}
1451
}
1452
1453
return result;
1454
}
1455
}
1456
1457
const PROXIED_EVENTS = new Set([
1458
"test-harness-message",
1459
"add-permissions",
1460
"remove-permissions",
1461
]);
1462
1463
class BootstrapScope {
1464
install(data, reason) {}
1465
uninstall(data, reason) {
1466
AsyncShutdown.profileChangeTeardown.addBlocker(
1467
`Uninstalling add-on: ${data.id}`,
1468
Management.emit("uninstall", { id: data.id }).then(() => {
1469
Management.emit("uninstall-complete", { id: data.id });
1470
})
1471
);
1472
}
1473
1474
fetchState() {
1475
if (this.extension) {
1476
return { state: this.extension.state };
1477
}
1478
return null;
1479
}
1480
1481
update(data, reason) {
1482
return Management.emit("update", {
1483
id: data.id,
1484
resourceURI: data.resourceURI,
1485
});
1486
}
1487
1488
startup(data, reason) {
1489
// eslint-disable-next-line no-use-before-define
1490
this.extension = new Extension(
1491
data,
1492
this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
1493
);
1494
return this.extension.startup();
1495
}
1496
1497
async shutdown(data, reason) {
1498
let result = await this.extension.shutdown(
1499
this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
1500
);
1501
this.extension = null;
1502
return result;
1503
}
1504
}
1505
1506
XPCOMUtils.defineLazyGetter(
1507
BootstrapScope.prototype,
1508
"BOOTSTRAP_REASON_TO_STRING_MAP",
1509
() => {
1510
const { BOOTSTRAP_REASONS } = AddonManagerPrivate;
1511
1512
return Object.freeze({
1513
[BOOTSTRAP_REASONS.APP_STARTUP]: "APP_STARTUP",
1514
[BOOTSTRAP_REASONS.APP_SHUTDOWN]: "APP_SHUTDOWN",
1515
[BOOTSTRAP_REASONS.ADDON_ENABLE]: "ADDON_ENABLE",
1516
[BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE",
1517
[BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL",
1518
[BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL",
1519
[BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE",
1520
[BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE",
1521
});
1522
}
1523
);
1524
1525
class DictionaryBootstrapScope extends BootstrapScope {
1526
install(data, reason) {}
1527
uninstall(data, reason) {}
1528
1529
startup(data, reason) {
1530
// eslint-disable-next-line no-use-before-define
1531
this.dictionary = new Dictionary(data);
1532
return this.dictionary.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
1533
}
1534
1535
shutdown(data, reason) {
1536
this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
1537
this.dictionary = null;
1538
}
1539
}
1540
1541
class LangpackBootstrapScope {
1542
install(data, reason) {}
1543
uninstall(data, reason) {}
1544
1545
startup(data, reason) {
1546
// eslint-disable-next-line no-use-before-define
1547
this.langpack = new Langpack(data);
1548
return this.langpack.startup();
1549
}
1550
1551
shutdown(data, reason) {
1552
this.langpack.shutdown();
1553
this.langpack = null;
1554
}
1555
}
1556
1557
let activeExtensionIDs = new Set();
1558
1559
let pendingExtensions = new Map();
1560
1561
/**
1562
* This class is the main representation of an active WebExtension
1563
* in the main process.
1564
* @extends ExtensionData
1565
*/
1566
class Extension extends ExtensionData {
1567
constructor(addonData, startupReason) {
1568
super(addonData.resourceURI);
1569
1570
this.startupStates = new Set();
1571
this.state = "Not started";
1572
1573
this.sharedDataKeys = new Set();
1574
1575
this.uuid = UUIDMap.get(addonData.id);
1576
this.instanceId = getUniqueId();
1577
1578
this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
1579
Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
1580
1581
if (addonData.cleanupFile) {
1582
Services.obs.addObserver(this, "xpcom-shutdown");
1583
this.cleanupFile = addonData.cleanupFile || null;
1584
delete addonData.cleanupFile;
1585
}
1586
1587
if (addonData.TEST_NO_ADDON_MANAGER) {
1588
this.dontSaveStartupData = true;
1589
}
1590
1591
this.addonData = addonData;
1592
this.startupData = addonData.startupData || {};
1593
this.startupReason = startupReason;
1594
1595
if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) {
1596
StartupCache.clearAddonData(addonData.id);
1597
}
1598
1599
this.remote = !WebExtensionPolicy.isExtensionProcess;
1600
1601
if (this.remote && processCount !== 1) {
1602
throw new Error(
1603
"Out-of-process WebExtensions are not supported with multiple child processes"
1604
);
1605
}
1606
1607
// This is filled in the first time an extension child is created.
1608
this.parentMessageManager = null;
1609
1610
this.id = addonData.id;
1611
this.version = addonData.version;
1612
this.baseURL = this.getURL("");
1613
this.baseURI = Services.io.newURI(this.baseURL).QueryInterface(Ci.nsIURL);
1614
this.principal = this.createPrincipal();
1615
1616
this.views = new Set();
1617
this._backgroundPageFrameLoader = null;
1618
1619
this.onStartup = null;
1620
1621
this.hasShutdown = false;
1622
this.onShutdown = new Set();
1623
1624
this.uninstallURL = null;
1625
1626
this.whiteListedHosts = null;
1627
this._optionalOrigins = null;
1628
this.webAccessibleResources = null;
1629
1630
this.registeredContentScripts = new Map();
1631
1632
this.emitter = new EventEmitter();
1633
1634
if (this.startupData.lwtData && this.startupReason == "APP_STARTUP") {
1635
LightweightThemeManager.fallbackThemeData = this.startupData.lwtData;
1636
}
1637
1638
/* eslint-disable mozilla/balanced-listeners */
1639
this.on("add-permissions", (ignoreEvent, permissions) => {
1640
for (let perm of permissions.permissions) {
1641
this.permissions.add(perm);
1642
}
1643
1644
if (permissions.origins.length > 0) {
1645
let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
1646
1647
this.whiteListedHosts = new MatchPatternSet(
1648
new Set([...patterns, ...permissions.origins]),
1649
{
1650
restrictSchemes: this.restrictSchemes,
1651
ignorePath: true,
1652
}
1653
);
1654
}
1655
1656
this.policy.permissions = Array.from(this.permissions);
1657
this.policy.allowedOrigins = this.whiteListedHosts;
1658
1659
this.cachePermissions();
1660
});
1661
1662
this.on("remove-permissions", (ignoreEvent, permissions) => {
1663
for (let perm of permissions.permissions) {
1664
this.permissions.delete(perm);
1665
}
1666
1667
let origins = permissions.origins.map(
1668
origin => new MatchPattern(origin, { ignorePath: true }).pattern
1669
);
1670
1671
this.whiteListedHosts = new MatchPatternSet(
1672
this.whiteListedHosts.patterns.filter(
1673
host => !origins.includes(host.pattern)
1674
)
1675
);
1676
1677
this.policy.permissions = Array.from(this.permissions);
1678
this.policy.allowedOrigins = this.whiteListedHosts;
1679
1680
this.cachePermissions();
1681
});
1682
/* eslint-enable mozilla/balanced-listeners */
1683
}
1684
1685
set state(startupState) {
1686
this.startupStates.clear();
1687
this.startupStates.add(startupState);
1688
}
1689
1690
get state() {
1691
return `${Array.from(this.startupStates).join(", ")}`;
1692
}
1693
1694
async addStartupStatePromise(name, fn) {
1695
this.startupStates.add(name);
1696
try {
1697
await fn();
1698
} finally {
1699
this.startupStates.delete(name);
1700
}
1701
}
1702
1703
get restrictSchemes() {
1704
return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
1705
}
1706
1707
// Some helpful properties added elsewhere:
1708
/**
1709
* An object used to map between extension-visible tab ids and
1710
* native Tab object
1711
* @property {TabManager} tabManager
1712
*/
1713
1714
static getBootstrapScope() {
1715
return new BootstrapScope();
1716
}
1717
1718
get groupFrameLoader() {
1719
let frameLoader = this._backgroundPageFrameLoader;
1720
for (let view of this.views) {
1721
if (view.viewType === "background" && view.xulBrowser) {
1722
return view.xulBrowser.frameLoader;
1723
}
1724
if (!frameLoader && view.xulBrowser) {
1725
frameLoader = view.xulBrowser.frameLoader;
1726
}
1727
}
1728
return frameLoader || ExtensionParent.DebugUtils.getFrameLoader(this.id);
1729
}
1730
1731
on(hook, f) {
1732
return this.emitter.on(hook, f);
1733
}
1734
1735
off(hook, f) {
1736
return this.emitter.off(hook, f);
1737
}
1738
1739
once(hook, f) {
1740
return this.emitter.once(hook, f);
1741
}
1742
1743
emit(event, ...args) {
1744
if (PROXIED_EVENTS.has(event)) {
1745
Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {
1746
event,
1747
args,
1748
});
1749
}
1750
1751
return this.emitter.emit(event, ...args);
1752
}
1753
1754
receiveMessage({ name, data }) {
1755
if (name === this.MESSAGE_EMIT_EVENT) {
1756
this.emitter.emit(data.event, ...data.args);
1757
}
1758
}
1759
1760
testMessage(...args) {
1761
this.emit("test-harness-message", ...args);
1762
}
1763
1764
createPrincipal(uri = this.baseURI, originAttributes = {}) {
1765
return Services.scriptSecurityManager.createContentPrincipal(
1766
uri,
1767
originAttributes
1768
);
1769
}
1770
1771
// Checks that the given URL is a child of our baseURI.
1772
isExtensionURL(url) {
1773
let uri = Services.io.newURI(url);
1774
1775
let common = this.baseURI.getCommonBaseSpec(uri);
1776
return common == this.baseURL;
1777
}
1778
1779
checkLoadURL(url, options = {}) {
1780
// As an optimization, f the URL starts with the extension's base URL,
1781
// don't do any further checks. It's always allowed to load it.
1782
if (url.startsWith(this.baseURL)) {
1783
return true;
1784
}
1785
1786
return ExtensionCommon.checkLoadURL(url, this.principal, options);
1787
}
1788
1789
async promiseLocales(locale) {
1790
let locales = await StartupCache.locales.get(
1791
[this.id, "@@all_locales"],
1792
() => this._promiseLocaleMap()
1793
);
1794
1795
return this._setupLocaleData(locales);
1796
}
1797
1798
readLocaleFile(locale) {
1799
return StartupCache.locales
1800
.get([this.id, this.version, locale], () => super.readLocaleFile(locale))
1801
.then(result => {
1802
this.localeData.messages.set(locale, result);
1803
});
1804
}
1805
1806
get manifestCacheKey() {
1807
return [this.id, this.version, Services.locale.appLocaleAsLangTag];
1808
}
1809
1810
get isPrivileged() {
1811
return (
1812
this.addonData.signedState === AddonManager.SIGNEDSTATE_PRIVILEGED ||
1813
this.addonData.signedState === AddonManager.SIGNEDSTATE_SYSTEM ||
1814
this.addonData.builtIn ||
1815
(AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
1816
this.addonData.temporarilyInstalled)
1817
);
1818
}
1819
1820
get experimentsAllowed() {
1821
return AddonSettings.ALLOW_LEGACY_EXTENSIONS || this.isPrivileged;
1822
}
1823
1824
saveStartupData() {
1825
if (this.dontSaveStartupData) {
1826
return;
1827
}
1828
XPIProvider.setStartupData(this.id, this.startupData);
1829
}
1830
1831
parseManifest() {
1832
return StartupCache.manifests.get(this.manifestCacheKey, () =>
1833
super.parseManifest()
1834
);
1835
}
1836
1837
async cachePermissions() {
1838
let manifestData = await this.parseManifest();
1839
1840
manifestData.originPermissions = this.whiteListedHosts.patterns.map(
1841
pat => pat.pattern
1842
);
1843
manifestData.permissions = this.permissions;
1844
return StartupCache.manifests.set(this.manifestCacheKey, manifestData);
1845
}
1846
1847
async loadManifest() {
1848
let manifest = await super.loadManifest();
1849
1850
if (this.errors.length) {
1851
return Promise.reject({ errors: this.errors });
1852
}
1853
1854
return manifest;
1855
}
1856
1857
get contentSecurityPolicy() {
1858
return this.manifest.content_security_policy;
1859
}
1860
1861
get backgroundScripts() {
1862
return this.manifest.background && this.manifest.background.scripts;
1863
}
1864
1865
get optionalPermissions() {
1866
return this.manifest.optional_permissions;
1867
}
1868
1869
get privateBrowsingAllowed() {
1870
return this.policy.privateBrowsingAllowed;
1871
}
1872
1873
canAccessWindow(window) {
1874
return this.policy.canAccessWindow(window);
1875
}
1876
1877
// Representation of the extension to send to content
1878
// processes. This should include anything the content process might
1879
// need.
1880
serialize() {
1881
return {
1882
id: this.id,
1883
uuid: this.uuid,
1884
name: this.name,
1885
contentSecurityPolicy: this.contentSecurityPolicy,
1886
instanceId: this.instanceId,
1887
resourceURL: this.resourceURL,
1888
contentScripts: this.contentScripts,
1889
webAccessibleResources: this.webAccessibleResources.map(res => res.glob),
1890
whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
1891
permissions: this.permissions,
1892
optionalPermissions: this.optionalPermissions,
1893
};
1894
}
1895
1896
// Extended serialized data which is only needed in the extensions process,
1897
// and is never deserialized in web content processes.
1898
serializeExtended() {
1899
return {
1900
backgroundScripts: this.backgroundScripts,
1901
childModules: this.modules && this.modules.child,
1902
dependencies: this.dependencies,
1903
schemaURLs: this.schemaURLs,
1904
};
1905
}
1906
1907
broadcast(msg, data) {
1908
return new Promise(resolve => {
1909
let { ppmm } = Services;
1910
let children = new Set();
1911
for (let i = 0; i < ppmm.childCount; i++) {
1912
children.add(ppmm.getChildAt(i));
1913
}
1914
1915
let maybeResolve;
1916
function listener(data) {
1917
children.delete(data.target);
1918
maybeResolve();
1919
}
1920
function observer(subject, topic, data) {
1921
children.delete(subject);
1922
maybeResolve();
1923
}
1924
1925
maybeResolve = () => {
1926
if (children.size === 0) {
1927
ppmm.removeMessageListener(msg + "Complete", listener);
1928
Services.obs.removeObserver(observer, "message-manager-close");
1929
Services.obs.removeObserver(observer, "message-manager-disconnect");
1930
resolve();
1931
}
1932
};
1933
ppmm.addMessageListener(msg + "Complete", listener, true);
1934
Services.obs.addObserver(observer, "message-manager-close");
1935
Services.obs.addObserver(observer, "message-manager-disconnect");
1936
1937
ppmm.broadcastAsyncMessage(msg, data);
1938
});
1939
}
1940
1941
setSharedData(key, value) {
1942
key = `extension/${this.id}/${key}`;
1943
this.sharedDataKeys.add(key);
1944
1945
sharedData.set(key, value);
1946
}
1947
1948
getSharedData(key, value) {
1949
key = `extension/${this.id}/${key}`;
1950
return sharedData.get(key);
1951
}
1952
1953
initSharedData() {
1954
this.setSharedData("", this.serialize());
1955
this.setSharedData("extendedData", this.serializeExtended());
1956
this.setSharedData("locales", this.localeData.serialize());
1957
this.setSharedData("manifest", this.manifest);
1958
this.updateContentScripts();
1959
}
1960
1961
updateContentScripts() {
1962
this.setSharedData("contentScripts", this.registeredContentScripts);
1963
}
1964
1965
runManifest(manifest) {
1966
let promises = [];
1967
let addPromise = (name, fn) => {
1968
promises.push(this.addStartupStatePromise(name, fn));
1969
};
1970
1971
for (let directive in manifest) {
1972
if (manifest[directive] !== null) {
1973
addPromise(`asyncEmitManifestEntry("${directive}")`, () =>
1974
Management.asyncEmitManifestEntry(this, directive)
1975
);
1976
}
1977
}
1978
1979
activeExtensionIDs.add(this.id);
1980
sharedData.set("extensions/activeIDs", activeExtensionIDs);
1981
1982
pendingExtensions.delete(this.id);
1983
sharedData.set("extensions/pending", pendingExtensions);
1984
1985
Services.ppmm.sharedData.flush();
1986
this.broadcast("Extension:Startup", this.id);
1987
1988
return Promise.all(promises);
1989
}
1990
1991
/**
1992
* Call the close() method on the given object when this extension
1993
* is shut down. This can happen during browser shutdown, or when
1994
* an extension is manually disabled or uninstalled.
1995
*
1996
* @param {object} obj
1997
* An object on which to call the close() method when this
1998
* extension is shut down.
1999
*/
2000
callOnClose(obj) {
2001
this.onShutdown.add(obj);
2002
}
2003
2004
forgetOnClose(obj) {
2005
this.onShutdown.delete(obj);
2006
}
2007
2008
get builtinMessages() {
2009
return new Map([["@@extension_id", this.uuid]]);
2010
}
2011
2012
// Reads the locale file for the given Gecko-compatible locale code, or if
2013
// no locale is given, the available locale closest to the UI locale.
2014
// Sets the currently selected locale on success.
2015
async initLocale(locale = undefined) {
2016
if (locale === undefined) {
2017
let locales = await this.promiseLocales();
2018
2019
let matches = Services.locale.negotiateLanguages(
2020
Services.locale.appLocalesAsLangTags,
2021
Array.from(locales.keys()),
2022
this.defaultLocale
2023
);
2024
2025
locale = matches[0];
2026
}
2027
2028
return super.initLocale(locale);
2029
}
2030
2031
updatePermissions(reason) {
2032
const { principal } = this;
2033
2034
const testPermission = perm =>
2035
Services.perms.testPermissionFromPrincipal(principal, perm);
2036
2037
const addUnlimitedStoragePermissions = () => {
2038
// Set the indexedDB permission and a custom "WebExtensions-unlimitedStorage" to
2039
// remember that the permission hasn't been selected manually by the user.
2040
Services.perms.addFromPrincipal(
2041
principal,
2042
"WebExtensions-unlimitedStorage",
2043
Services.perms.ALLOW_ACTION
2044
);
2045
Services.perms.addFromPrincipal(
2046
principal,
2047
"indexedDB",
2048
Services.perms.ALLOW_ACTION
2049
);
2050