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