XPIProvider.jsm 90.7 KB
Newer Older
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
3
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
6
"use strict";

7
8
9
10
11
12
13
/**
 * This file contains most of the logic required to load and run
 * extensions at startup. Anything which is not required immediately at
 * startup should go in XPIInstall.jsm or XPIDatabase.jsm if at all
 * possible, in order to minimize the impact on startup performance.
 */

14
15
16
17
/**
 * @typedef {number} integer
 */

18
19
/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */

20
var EXPORTED_SYMBOLS = ["XPIProvider", "XPIInternal"];
21

22
23
24
25
26
27
28
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { AddonManager, AddonManagerPrivate } = ChromeUtils.import(
  "resource://gre/modules/AddonManager.jsm"
);
29

30
XPCOMUtils.defineLazyModuleGetters(this, {
31
  AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
32
  AppConstants: "resource://gre/modules/AppConstants.jsm",
33
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
34
  Dictionary: "resource://gre/modules/Extension.jsm",
35
  Extension: "resource://gre/modules/Extension.jsm",
36
  Langpack: "resource://gre/modules/Extension.jsm",
37
38
39
  FileUtils: "resource://gre/modules/FileUtils.jsm",
  OS: "resource://gre/modules/osfile.jsm",
  JSONFile: "resource://gre/modules/JSONFile.jsm",
40
  TelemetrySession: "resource://gre/modules/TelemetrySession.jsm",
41

42
43
  XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm",
  XPIDatabaseReconcile: "resource://gre/modules/addons/XPIDatabase.jsm",
44
  XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
45
});
46

47
XPCOMUtils.defineLazyServiceGetters(this, {
48
49
50
51
52
53
54
55
  aomStartup: [
    "@mozilla.org/addons/addon-manager-startup;1",
    "amIAddonManagerStartup",
  ],
  resProto: [
    "@mozilla.org/network/protocol;1?name=resource",
    "nsISubstitutingProtocolHandler",
  ],
56
  spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
57
58
59
60
  timerManager: [
    "@mozilla.org/updates/timer-manager;1",
    "nsIUpdateTimerManager",
  ],
61
});
62

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const nsIFile = Components.Constructor(
  "@mozilla.org/file/local;1",
  "nsIFile",
  "initWithPath"
);
const FileInputStream = Components.Constructor(
  "@mozilla.org/network/file-input-stream;1",
  "nsIFileInputStream",
  "init"
);

const PREF_DB_SCHEMA = "extensions.databaseSchema";
const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes";
const PREF_EM_STARTUP_SCAN_SCOPES = "extensions.startupScanScopes";
78
// xpinstall.signatures.required only supported in dev builds
79
80
81
82
83
const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required";
const PREF_LANGPACK_SIGNATURES = "extensions.langpacks.signatures.required";
const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
84

85
const PREF_EM_LAST_APP_BUILD_ID = "extensions.lastAppBuildId";
86

87
// Specify a list of valid built-in add-ons to load.
88
const BUILT_IN_ADDONS_URI = "chrome://browser/content/built_in_addons.json";
89

90
91
const URI_EXTENSION_STRINGS =
  "chrome://mozapps/locale/extensions/extensions.properties";
92

93
94
const DIR_EXTENSIONS = "extensions";
const DIR_SYSTEM_ADDONS = "features";
95
const DIR_APP_SYSTEM_PROFILE = "system-extensions";
96
97
const DIR_STAGE = "staged";
const DIR_TRASH = "trash";
98

99
const FILE_XPI_STATES = "addonStartup.json.lz4";
100

101
102
103
104
const KEY_PROFILEDIR = "ProfD";
const KEY_ADDON_APP_DIR = "XREAddonAppDir";
const KEY_APP_DISTRIBUTION = "XREAppDist";
const KEY_APP_FEATURES = "XREAppFeat";
105

106
const KEY_APP_PROFILE = "app-profile";
107
const KEY_APP_SYSTEM_PROFILE = "app-system-profile";
108
109
110
111
112
113
114
115
const KEY_APP_SYSTEM_ADDONS = "app-system-addons";
const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults";
const KEY_APP_BUILTINS = "app-builtin";
const KEY_APP_GLOBAL = "app-global";
const KEY_APP_SYSTEM_LOCAL = "app-system-local";
const KEY_APP_SYSTEM_SHARE = "app-system-share";
const KEY_APP_SYSTEM_USER = "app-system-user";
const KEY_APP_TEMPORARY = "app-temporary";
116

117
118
const TEMPORARY_ADDON_SUFFIX = "@temporary-addon";

119
120
121
122
123
124
const STARTUP_MTIME_SCOPES = [
  KEY_APP_GLOBAL,
  KEY_APP_SYSTEM_LOCAL,
  KEY_APP_SYSTEM_SHARE,
  KEY_APP_SYSTEM_USER,
];
125

126
127
const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
const XPI_PERMISSION = "install";
128

129
const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
130

131
const DB_SCHEMA = 32;
132

133
134
135
136
137
138
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "enabledScopesPref",
  PREF_EM_ENABLED_SCOPES,
  AddonManager.SCOPE_ALL
);
139
140
141
142
143
144
145
146

Object.defineProperty(this, "enabledScopes", {
  get() {
    // The profile location is always enabled
    return enabledScopesPref | AddonManager.SCOPE_PROFILE;
  },
});

147
148
149
150
151
function encoded(strings, ...values) {
  let result = [];

  for (let [i, string] of strings.entries()) {
    result.push(string);
152
    if (i < values.length) {
153
      result.push(encodeURIComponent(values[i]));
154
    }
155
156
157
158
  }

  return result.join("");
}
159

160
const BOOTSTRAP_REASONS = {
161
162
163
164
165
166
167
  APP_STARTUP: 1,
  APP_SHUTDOWN: 2,
  ADDON_ENABLE: 3,
  ADDON_DISABLE: 4,
  ADDON_INSTALL: 5,
  ADDON_UNINSTALL: 6,
  ADDON_UPGRADE: 7,
168
  ADDON_DOWNGRADE: 8,
169
170
};

171
const ALL_EXTERNAL_TYPES = new Set([
172
173
174
175
176
177
  "dictionary",
  "extension",
  "locale",
  "theme",
]);

178
179
var gGlobalScope = this;

180
181
182
183
184
/**
 * Valid IDs fit this pattern.
 */
var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;

185
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
186
const LOGGER_ID = "addons.xpi";
187

188
189
// Create a new logger for use by all objects in this Addons XPI Provider module
// (Requires AddonManager.jsm)
190
var logger = Log.repository.getLogger(LOGGER_ID);
191

192
193
194
195
196
197
198
199
200
201
202
XPCOMUtils.defineLazyGetter(this, "gStartupScanScopes", () => {
  let appBuildID = Services.appinfo.appBuildID;
  let oldAppBuildID = Services.prefs.getCharPref(PREF_EM_LAST_APP_BUILD_ID, "");
  Services.prefs.setCharPref(PREF_EM_LAST_APP_BUILD_ID, appBuildID);
  if (appBuildID !== oldAppBuildID) {
    // If the build id changed, scan all scopes
    return AddonManager.SCOPE_ALL;
  }

  return Services.prefs.getIntPref(PREF_EM_STARTUP_SCAN_SCOPES, 0);
});
203

204
205
206
207
208
209
210
211
212
213
214
215
216
/**
 * Spins the event loop until the given promise resolves, and then eiter returns
 * its success value or throws its rejection value.
 *
 * @param {Promise} promise
 *        The promise to await.
 * @returns {any}
 *        The promise's resolution value, if any.
 */
function awaitPromise(promise) {
  let success = undefined;
  let result = null;

217
218
219
220
221
222
223
224
225
226
  promise.then(
    val => {
      success = true;
      result = val;
    },
    val => {
      success = false;
      result = val;
    }
  );
227
228
229

  Services.tm.spinEventLoopUntil(() => success !== undefined);

230
  if (!success) {
231
    throw result;
232
  }
233
234
235
  return result;
}

236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/**
 * Returns a nsIFile instance for the given path, relative to the given
 * base file, if provided.
 *
 * @param {string} path
 *        The (possibly relative) path of the file.
 * @param {nsIFile} [base]
 *        An optional file to use as a base path if `path` is relative.
 * @returns {nsIFile}
 */
function getFile(path, base = null) {
  // First try for an absolute path, as we get in the case of proxy
  // files. Ideally we would try a relative path first, but on Windows,
  // paths which begin with a drive letter are valid as relative paths,
  // and treated as such.
  try {
    return new nsIFile(path);
  } catch (e) {
    // Ignore invalid relative paths. The only other error we should see
    // here is EOM, and either way, any errors that we care about should
    // be re-thrown below.
  }

  // If the path isn't absolute, we must have a base path.
  let file = base.clone();
  file.appendRelativePath(path);
  return file;
}

265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/**
 * Returns true if the given file, based on its name, should be treated
 * as an XPI. If the file does not have an appropriate extension, it is
 * assumed to be an unpacked add-on.
 *
 * @param {string} filename
 *        The filename to check.
 * @param {boolean} [strict = false]
 *        If true, this file is in a location maintained by the browser, and
 *        must have a strict, lower-case ".xpi" extension.
 * @returns {boolean}
 *        True if the file is an XPI.
 */
function isXPI(filename, strict) {
  if (strict) {
    return filename.endsWith(".xpi");
  }
  let ext = filename.slice(-4).toLowerCase();
  return ext === ".xpi" || ext === ".zip";
}

/**
 * Returns the extension expected ID for a given file in an extension install
 * directory.
 *
 * @param {nsIFile} file
 *        The extension XPI file or unpacked directory.
 * @returns {AddonId?}
 *        The add-on ID, if valid, or null otherwise.
 */
function getExpectedID(file) {
296
297
  let { leafName } = file;
  let id = isXPI(leafName, true) ? leafName.slice(0, -4) : leafName;
298
299
300
301
302
303
  if (gIDTest.test(id)) {
    return id;
  }
  return null;
}

304
305
306
/**
 * Evaluates whether an add-on is allowed to run in safe mode.
 *
307
308
309
310
 * @param {AddonInternal} aAddon
 *        The add-on to check
 * @returns {boolean}
 *        True if the add-on should run in safe mode
311
312
 */
function canRunInSafeMode(aAddon) {
313
314
315
316
317
  let location = aAddon.location || null;
  if (!location) {
    return false;
  }

318
319
320
  // Even though the updated system add-ons aren't generally run in safe mode we
  // include them here so their uninstall functions get called when switching
  // back to the default set.
321
322
323

  // TODO product should make the call about temporary add-ons running
  // in safe mode. assuming for now that they are.
324
  return location.isTemporary || location.isSystem || location.isBuiltin;
325
326
}

327
/**
328
329
330
 * Gets an nsIURI for a file within another file, either a directory or an XPI
 * file. If aFile is a directory then this will return a file: URI, if it is an
 * XPI file then it will return a jar: URI.
331
 *
332
333
334
335
336
337
338
339
 * @param {nsIFile} aFile
 *        The file containing the resources, must be either a directory or an
 *        XPI file
 * @param {string} aPath
 *        The path to find the resource at, "/" separated. If aPath is empty
 *        then the uri to the root of the contained files will be returned
 * @returns {nsIURI}
 *        An nsIURI pointing at the resource
340
 */
341
function getURIForResourceInFile(aFile, aPath) {
342
  if (!isXPI(aFile.leafName)) {
343
    let resource = aFile.clone();
344
    if (aPath) {
345
      aPath.split("/").forEach(part => resource.append(part));
346
    }
347

348
    return Services.io.newFileURI(resource);
349
350
  }

351
352
  return buildJarURI(aFile, aPath);
}
353

354
355
356
/**
 * Creates a jar: URI for a file inside a ZIP file.
 *
357
358
359
360
361
362
 * @param {nsIFile} aJarfile
 *        The ZIP file as an nsIFile
 * @param {string} aPath
 *        The path inside the ZIP file
 * @returns {nsIURI}
 *        An nsIURI for the file
363
364
365
366
367
368
 */
function buildJarURI(aJarfile, aPath) {
  let uri = Services.io.newFileURI(aJarfile);
  uri = "jar:" + uri.spec + "!/" + aPath;
  return Services.io.newURI(uri);
}
369

370
371
372
373
374
375
376
function maybeResolveURI(uri) {
  if (uri.schemeIs("resource")) {
    return Services.io.newURI(resProto.resolveURI(uri));
  }
  return uri;
}

377
/**
378
 * Iterates over the entries in a given directory.
379
 *
380
381
382
383
 * Fails silently if the given directory does not exist.
 *
 * @param {nsIFile} aDir
 *        Directory to iterate.
384
 */
385
function* iterDirectory(aDir) {
386
387
  let dirEnum;
  try {
388
389
390
391
    dirEnum = aDir.directoryEntries;
    let file;
    while ((file = dirEnum.nextFile)) {
      yield file;
392
    }
393
394
  } catch (e) {
    if (aDir.exists()) {
395
      logger.warn(`Can't iterate directory ${aDir.path}`, e);
396
397
398
399
400
401
402
    }
  } finally {
    if (dirEnum) {
      dirEnum.close();
    }
  }
}
403

404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/**
 * Migrate data about an addon to match the change made in bug 857456
 * in which "webextension-foo" types were converted to "foo" and the
 * "loader" property was added to distinguish different addon types.
 *
 * @param {Object} addon  The addon info to migrate.
 * @returns {boolean} True if the addon data was converted, false if not.
 */
function migrateAddonLoader(addon) {
  if (addon.hasOwnProperty("loader")) {
    return false;
  }

  switch (addon.type) {
    case "extension":
    case "dictionary":
    case "locale":
    case "theme":
      addon.loader = "bootstrap";
      break;

    case "webextension":
      addon.type = "extension";
      addon.loader = null;
      break;

    case "webextension-dictionary":
      addon.type = "dictionary";
      addon.loader = null;
      break;

    case "webextension-langpack":
      addon.type = "locale";
      addon.loader = null;
      break;

    case "webextension-theme":
      addon.type = "theme";
      addon.loader = null;
      break;

    default:
      logger.warn(`Not converting unknown addon type ${addon.type}`);
  }
  return true;
}

451
452
453
454
455
456
457
458
/**
 * The on-disk state of an individual XPI, created from an Object
 * as stored in the addonStartup.json file.
 */
const JSON_FIELDS = Object.freeze([
  "dependencies",
  "enabled",
  "file",
459
  "loader",
460
461
  "lastModifiedTime",
  "path",
462
  "rootURI",
463
  "runInSafeMode",
464
  "signedState",
465
  "signedDate",
466
  "startupData",
467
  "telemetryKey",
468
469
470
  "type",
  "version",
]);
471

472
473
474
475
class XPIState {
  constructor(location, id, saved = {}) {
    this.location = location;
    this.id = id;
476

477
478
    // Set default values.
    this.type = "extension";
479

480
481
482
    for (let prop of JSON_FIELDS) {
      if (prop in saved) {
        this[prop] = saved[prop];
483
484
      }
    }
485

486
487
    // Builds prior to be 1512436 did not include the rootURI property.
    // If we're updating from such a build, add that property now.
488
    if (this.file) {
489
490
491
      this.rootURI = getURIForResourceInFile(this.file, "").spec;
    }

492
493
494
495
    if (!this.telemetryKey) {
      this.telemetryKey = this.getTelemetryKey();
    }

496
497
498
499
    if (
      saved.currentModifiedTime &&
      saved.currentModifiedTime != this.lastModifiedTime
    ) {
500
      this.lastModifiedTime = saved.currentModifiedTime;
501
502
503
504
    } else if (
      saved.currentModifiedTime === null &&
      (!this.file || !this.file.exists())
    ) {
505
      this.missing = true;
506
    }
507
508
  }

509
  // Compatibility shim getters for legacy callers in XPIDatabase.jsm.
510
511
  get mtime() {
    return this.lastModifiedTime;
512
  }
513
514
515
  get active() {
    return this.enabled;
  }
516

517
518
519
520
521
522
523
524
  /**
   * @property {string} path
   *        The full on-disk path of the add-on.
   */
  get path() {
    return this.file && this.file.path;
  }
  set path(path) {
525
    this.file = path ? getFile(path, this.location.dir) : null;
526
527
  }

528
529
530
531
532
533
534
535
536
537
538
539
  /**
   * @property {string} relativePath
   *        The path to the add-on relative to its parent location, or
   *        the full path if its parent location has no on-disk path.
   */
  get relativePath() {
    if (this.location.dir && this.location.dir.contains(this.file)) {
      let path = this.file.getRelativePath(this.location.dir);
      if (AppConstants.platform == "win") {
        path = path.replace(/\//g, "\\");
      }
      return path;
540
    }
541
542
    return this.path;
  }
543

544
545
546
  /**
   * Returns a JSON-compatible representation of this add-on's state
   * data, to be saved to addonStartup.json.
547
548
   *
   * @returns {Object}
549
550
551
   */
  toJSON() {
    let json = {
552
      dependencies: this.dependencies,
553
554
      enabled: this.enabled,
      lastModifiedTime: this.lastModifiedTime,
555
      loader: this.loader,
556
      path: this.relativePath,
557
      rootURI: this.rootURI,
558
559
      runInSafeMode: this.runInSafeMode,
      signedState: this.signedState,
560
      signedDate: this.signedDate,
561
      telemetryKey: this.telemetryKey,
562
      version: this.version,
563
564
565
    };
    if (this.type != "extension") {
      json.type = this.type;
566
    }
567
568
569
    if (this.startupData) {
      json.startupData = this.startupData;
    }
570
    return json;
571
572
  }

573
574
575
576
  get isWebExtension() {
    return this.loader == null;
  }

577
578
  /**
   * Update the last modified time for an add-on on disk.
579
580
581
582
583
   *
   * @param {nsIFile} aFile
   *        The location of the add-on.
   * @returns {boolean}
   *       True if the time stamp has changed.
584
   */
585
586
587
588
589
590
591
592
  getModTime(aFile) {
    let mtime = 0;
    try {
      // Clone the file object so we always get the actual mtime, rather
      // than whatever value it may have cached.
      mtime = aFile.clone().lastModifiedTime;
    } catch (e) {
      logger.warn("Can't get modified time of ${path}", aFile, e);
593
594
    }

595
    let changed = mtime != this.lastModifiedTime;
596
    this.lastModifiedTime = mtime;
597
    return changed;
598
  }
599

600
601
602
603
604
605
606
607
608
609
  /**
   * Returns a string key by which to identify this add-on in telemetry
   * and crash reports.
   *
   * @returns {string}
   */
  getTelemetryKey() {
    return encoded`${this.id}:${this.version}`;
  }

610
611
612
613
  get resolvedRootURI() {
    return maybeResolveURI(Services.io.newURI(this.rootURI));
  }

614
615
616
617
  /**
   * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
   * update the last-modified time. This should probably be made async, but for now we
   * don't want to maintain parallel sync and async versions of the scan.
618
   *
619
   * Caller is responsible for doing XPIStates.save() if necessary.
620
621
622
623
624
   *
   * @param {DBAddonInternal} aDBAddon
   *        The DBAddonInternal for this add-on.
   * @param {boolean} [aUpdated = false]
   *        The add-on was updated, so we must record new modified time.
625
626
627
628
629
630
631
   */
  syncWithDB(aDBAddon, aUpdated = false) {
    logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon));
    // If the add-on changes from disabled to enabled, we should re-check the modified time.
    // If this is a newly found add-on, it won't have an 'enabled' field but we
    // did a full recursive scan in that case, so we don't need to do it again.
    // We don't use aDBAddon.active here because it's not updated until after restart.
632
    let mustGetMod = aDBAddon.visible && !aDBAddon.disabled && !this.enabled;
633

634
    this.enabled = aDBAddon.visible && !aDBAddon.disabled;
635

636
637
    this.version = aDBAddon.version;
    this.type = aDBAddon.type;
638
639
    this.loader = aDBAddon.loader;

640
641
642
    if (aDBAddon.startupData) {
      this.startupData = aDBAddon.startupData;
    }
643

644
645
    this.telemetryKey = this.getTelemetryKey();

646
647
    this.dependencies = aDBAddon.dependencies;
    this.runInSafeMode = canRunInSafeMode(aDBAddon);
648
    this.signedState = aDBAddon.signedState;
649
    this.signedDate = aDBAddon.signedDate;
650
    this.file = aDBAddon._sourceBundle;
651
    this.rootURI = aDBAddon.rootURI;
652

653
654
655
656
657
658
    if ((aUpdated || mustGetMod) && this.file) {
      this.getModTime(this.file);
      if (this.lastModifiedTime != aDBAddon.updateDate) {
        aDBAddon.updateDate = this.lastModifiedTime;
        if (XPIDatabase.initialized) {
          XPIDatabase.saveChanges();
659
660
661
662
663
        }
      }
    }
  }
}
664

665
/**
666
 * Manages the state data for add-ons in a given install location.
667
 *
668
669
 * @param {string} name
 *        The name of the install location (e.g., "app-profile").
670
 * @param {string | nsIFile | null} path
671
672
 *        The on-disk path of the install location. May be null for some
 *        locations which do not map to a specific on-disk path.
673
674
675
 * @param {integer} scope
 *        The scope of add-ons installed in this location.
 * @param {object} [saved]
676
 *        The persisted JSON state data to restore.
677
 */
678
class XPIStateLocation extends Map {
679
  constructor(name, path, scope, saved) {
680
    super();
681

682
    this.name = name;
683
684
685
686
687
688
689
690
691
692
693
    this.scope = scope;
    if (path instanceof Ci.nsIFile) {
      this.dir = path;
      this.path = path.path;
    } else {
      this.path = path;
      this.dir = this.path && new nsIFile(this.path);
    }
    this.staged = {};
    this.changed = false;

694
695
696
697
698
699
    // The profile extensions directory is whitelisted for access by the
    // content process sandbox if, and only if it already exists. Since
    // we want it to be available for newly-installed extensions even if
    // no profile extensions were present at startup, make sure it
    // exists now.
    if (name === KEY_APP_PROFILE) {
700
      OS.File.makeDir(this.path, { ignoreExisting: true });
701
702
    }

703
704
705
706
707
708
709
    if (saved) {
      this.restore(saved);
    }

    this._installler = undefined;
  }

710
711
712
713
714
  hasPrecedence(otherLocation) {
    let locations = Array.from(XPIStates.locations());
    return locations.indexOf(this) <= locations.indexOf(otherLocation);
  }

715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
  get installer() {
    if (this._installer === undefined) {
      this._installer = this.makeInstaller();
    }
    return this._installer;
  }

  makeInstaller() {
    return null;
  }

  restore(saved) {
    if (!this.path && saved.path) {
      this.path = saved.path;
      this.dir = new nsIFile(this.path);
    }
731
    this.staged = saved.staged || {};
732
    this.changed = saved.changed || false;
733

734
735
    for (let [id, data] of Object.entries(saved.addons || {})) {
      let xpiState = this._addState(id, data);
736
737
738
739

      // Make a note that this state was restored from saved data. But
      // only if this location hasn't moved since the last startup,
      // since that causes problems for new system add-on bundles.
740
      if (!this.path || this.path == saved.path) {
741
742
        xpiState.wasRestored = true;
      }
743
744
745
    }
  }

746
747
748
  /**
   * Returns a JSON-compatible representation of this location's state
   * data, to be saved to addonStartup.json.
749
750
   *
   * @returns {Object}
751
752
   */
  toJSON() {
753
754
755
756
    let json = {
      addons: {},
      staged: this.staged,
    };
757

758
759
760
    if (this.path) {
      json.path = this.path;
    }
761

762
763
764
    if (STARTUP_MTIME_SCOPES.includes(this.name)) {
      json.checkStartupModifications = true;
    }
765

766
    for (let [id, addon] of this.entries()) {
767
      json.addons[id] = addon;
768
    }
769
    return json;
770
  }
771

772
773
774
775
776
777
778
  get hasStaged() {
    for (let key in this.staged) {
      return true;
    }
    return false;
  }

779
780
781
782
783
  _addState(addonId, saved) {
    let xpiState = new XPIState(this, addonId, saved);
    this.set(addonId, xpiState);
    return xpiState;
  }
784

785
786
787
788
789
790
791
  /**
   * Adds state data for the given DB add-on to the DB.
   *
   * @param {DBAddon} addon
   *        The DBAddon to add.
   */
  addAddon(addon) {
792
793
794
795
    logger.debug(
      "XPIStates adding add-on ${id} in ${location}: ${path}",
      addon
    );
796

797
    let xpiState = this._addState(addon.id, { file: addon._sourceBundle });
798
    xpiState.syncWithDB(addon, true);
799

800
    XPIProvider.addTelemetry(addon.id, { location: this.name });
801
802
  }

803
804
805
806
807
808
809
810
811
812
813
814
815
  /**
   * Remove the XPIState for an add-on and save the new state.
   *
   * @param {string} aId
   *        The ID of the add-on.
   */
  removeAddon(aId) {
    if (this.has(aId)) {
      this.delete(aId);
      XPIStates.save();
    }
  }

816
817
818
819
820
821
822
823
824
825
  /**
   * Adds stub state data for the local file to the DB.
   *
   * @param {string} addonId
   *        The ID of the add-on represented by the given file.
   * @param {nsIFile} file
   *        The local file or directory containing the add-on.
   * @returns {XPIState}
   */
  addFile(addonId, file) {
826
827
828
829
    let xpiState = this._addState(addonId, {
      enabled: false,
      file: file.clone(),
    });
830
    xpiState.getModTime(xpiState.file);
831
    return xpiState;
832
833
  }

834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
  /**
   * Adds metadata for a staged install which should be performed after
   * the next restart.
   *
   * @param {string} addonId
   *        The ID of the staged install. The leaf name of the XPI
   *        within the location's staging directory must correspond to
   *        this ID.
   * @param {object} metadata
   *        The JSON metadata of the parsed install, to be used during
   *        the next startup.
   */
  stageAddon(addonId, metadata) {
    this.staged[addonId] = metadata;
    XPIStates.save();
  }

  /**
   * Removes staged install metadata for the given add-on ID.
   *
   * @param {string} addonId
   *        The ID of the staged install.
   */
  unstageAddon(addonId) {
    if (addonId in this.staged) {
      delete this.staged[addonId];
      XPIStates.save();
    }
  }

864
  *getStagedAddons() {
865
866
867
868
869
    for (let [id, metadata] of Object.entries(this.staged)) {
      yield [id, metadata];
    }
  }

870
  /**
871
872
873
874
875
876
   * Returns true if the given addon was installed in this location by a text
   * file pointing to its real path.
   *
   * @param {string} aId
   *        The ID of the addon
   * @returns {boolean}
877
   */
878
879
880
  isLinkedAddon(aId) {
    if (!this.dir) {
      return true;
881
    }
882
883
884
885
886
887
888
889
890
891
    return this.has(aId) && !this.dir.contains(this.get(aId).file);
  }

  get isTemporary() {
    return false;
  }

  get isSystem() {
    return false;
  }
892
893
894
895
896

  get isBuiltin() {
    return false;
  }

897
898
899
900
  get hidden() {
    return this.isBuiltin;
  }

901
902
903
904
905
906
  // If this property is false, it does not implement readAddons()
  // interface.  This is used for the temporary and built-in locations
  // that do not correspond to a physical location that can be scanned.
  get enumerable() {
    return true;
  }
907
}
908

909
class TemporaryLocation extends XPIStateLocation {
910
  /**
911
912
   * @param {string} name
   *        The string identifier for the install location.
913
   */
914
  constructor(name) {
915
    super(name, null, AddonManager.SCOPE_TEMPORARY);
916
917
    this.locked = false;
  }
918

919
920
921
922
923
924
925
926
  makeInstaller() {
    // Installs are a no-op. We only register that add-ons exist, and
    // run them from their current location.
    return {
      installAddon() {},
      uninstallAddon() {},
    };
  }
927

928
929
930
  toJSON() {
    return {};
  }
931

932
933
  get isTemporary() {
    return true;
934
  }
935
936
937
938

  get enumerable() {
    return false;
  }
939
}
940

941
var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);
942

943
944
945
/**
 * A "location" for addons installed from assets packged into the app.
 */
946
var BuiltInLocation = new (class _BuiltInLocation extends XPIStateLocation {
947
  constructor() {
948
    super(KEY_APP_BUILTINS, null, AddonManager.SCOPE_APPLICATION);
949
950
951
952
953
954
955
956
957
958
959
960
961
    this.locked = false;
  }

  // The installer object is responsible for moving files around on disk
  // when (un)installing an addon.  Since this location handles only addons
  // that are embedded within the browser, these are no-ops.
  makeInstaller() {
    return {
      installAddon() {},
      uninstallAddon() {},
    };
  }

962
963
964
965
  get hidden() {
    return false;
  }

966
967
968
969
970
971
972
  get isBuiltin() {
    return true;
  }

  get enumerable() {
    return false;
  }
973
})();
974

975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
/**
 * An object which identifies a directory install location for add-ons. The
 * location consists of a directory which contains the add-ons installed in the
 * location.
 *
 */
class DirectoryLocation extends XPIStateLocation {
  /**
   * Each add-on installed in the location is either a directory containing the
   * add-on's files or a text file containing an absolute path to the directory
   * containing the add-ons files. The directory or text file must have the same
   * name as the add-on's ID.
   *
   * @param {string} name
   *        The string identifier for the install location.
   * @param {nsIFile} dir
   *        The directory for the install location.
   * @param {integer} scope
   *        The scope of add-ons installed in this location.
   * @param {boolean} [locked = true]
   *        If false, the location accepts new add-on installs.
996
997
   * @param {boolean} [system = false]
   *        If true, the location is a system addon location.
998
   */
999
  constructor(name, dir, scope, locked = true, system = false) {
1000
1001
    super(name, dir, scope);
    this.locked = locked;
1002
    this._isSystem = system;
1003
  }
1004

1005
1006
1007
1008
1009
1010
  makeInstaller() {
    if (this.locked) {
      return null;
    }
    return new XPIInstall.DirectoryInstaller(this);
  }
1011

1012
  /**
1013
1014
   * Reads a single-line file containing the path to a directory, and
   * returns an nsIFile pointing to that directory, if successful.
1015
   *
1016
1017
1018
1019
1020
   * @param {nsIFile} aFile
   *        The file containing the directory path
   * @returns {nsIFile?}
   *        An nsIFile object representing the linked directory, or null
   *        on error.
1021
   */
1022
1023
1024
1025
  _readLinkFile(aFile) {
    let linkedDirectory;
    if (aFile.isSymlink()) {
      linkedDirectory = aFile.clone();
1026
      try {
1027
        linkedDirectory.normalize();
1028
      } catch (e) {
1029
1030
1031
1032
        logger.warn(
          `Symbolic link ${aFile.path} points to a path ` +
            `which does not exist`
        );
1033
1034
1035
1036
1037
1038
1039
1040
1041
        return null;
      }
    } else {
      let fis = new FileInputStream(aFile, -1, -1, false);
      let line = {};
      fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
      fis.close();

      if (line.value) {
1042
1043
1044
        linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(
          Ci.nsIFile
        );
1045
1046
1047
1048
1049
        try {
          linkedDirectory.initWithPath(line.value);
        } catch (e) {
          linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
        }
1050
      }
1051
    }
1052

1053
1054
    if (linkedDirectory) {
      if (!linkedDirectory.exists()) {
1055
1056
1057
1058
        logger.warn(
          `File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
            "which does not exist"
        );
1059
1060
        return null;
      }
1061

1062
      if (!linkedDirectory.isDirectory()) {
1063
1064
1065
1066
        logger.warn(
          `File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
            "which is not a directory"
        );
1067
1068
        return null;
      }
1069

1070
1071
      return linkedDirectory;
    }
1072

1073
1074
1075
    logger.warn(`File pointer ${aFile.path} does not contain a path`);
    return null;
  }
1076

1077
1078
1079
1080
1081
1082
1083
1084
  /**
   * Finds all the add-ons installed in this location.
   *
   * @returns {Map<AddonID, nsIFile>}
   *        A map of add-ons present in this location.
   */
  readAddons() {
    let addons = new Map();
1085

1086
1087
1088
    if (!this.dir) {
      return addons;
    }
1089

1090
1091
1092
    // Use a snapshot of the directory contents to avoid possible issues with
    // iterating over a directory while removing files from it (the YAFFS2
    // embedded filesystem has this issue, see bug 772238).
1093
    for (let entry of Array.from(iterDirectory(this.dir))) {
1094
1095
      let id = getExpectedID(entry);
      if (!id) {
1096
1097
1098
1099
1100
1101
        if (![DIR_STAGE, DIR_TRASH].includes(entry.leafName)) {
          logger.debug(
            "Ignoring file: name is not a valid add-on ID: ${}",
            entry.path
          );
        }
1102
1103
        continue;
      }
1104

1105
      if (id == entry.leafName && (entry.isFile() || entry.isSymlink())) {
1106
1107
1108
1109
1110
1111
1112
1113
        let newEntry = this._readLinkFile(entry);
        if (!newEntry) {
          logger.debug(`Deleting stale pointer file ${entry.path}`);
          try {
            entry.remove(true);
          } catch (e) {
            logger.warn(`Failed to remove stale pointer file ${entry.path}`, e);
            // Failing to remove the stale pointer file is ignorable
1114
          }
1115
          continue;
1116
        }
1117

1118
        entry = newEntry;
1119
      }
1120

1121
1122
1123
1124
      addons.set(id, entry);
    }
    return addons;
  }
1125
1126
1127
1128

  get isSystem() {
    return this._isSystem;
  }
1129
1130
1131
1132
1133
1134
1135
1136
}

/**
 * An object which identifies a built-in install location for add-ons, such
 * as default system add-ons.
 *
 * This location should point either to a XPI, or a directory in a local build.
 */
1137
class SystemAddonDefaults extends DirectoryLocation {
1138
  /**
1139
1140
   * Read the manifest of allowed add-ons and build a mapping between ID and URI
   * for each.
1141
   *
1142
1143
   * @returns {Map<AddonID, nsIFile>}
   *        A map of add-ons present in this location.
1144
   */
1145
1146
1147
  readAddons() {
    let addons = new Map();

1148
    let manifest = XPIProvider.builtInAddons;
1149

1150
    if (!("system" in manifest)) {
1151
      logger.debug("No list of valid system add-ons found.");
1152
      return addons;
1153
1154
    }

1155
1156
1157
1158
1159
1160
1161
1162
    for (let id of manifest.system) {
      let file = this.dir.clone();
      file.append(`${id}.xpi`);

      // Only attempt to load unpacked directory if unofficial build.
      if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
        file = this.dir.clone();
        file.append(`${id}`);
1163
      }
1164
1165

      addons.set(id, file);
1166
    }
1167

1168
1169
    return addons;
  }
1170

1171
1172
1173
  get isSystem() {
    return true;
  }
1174
1175
1176
1177

  get isBuiltin() {
    return true;
  }
1178
1179
1180
1181
1182
1183
1184
}

/**
 * An object which identifies a directory install location for system add-ons
 * updates.
 */
class SystemAddonLocation extends DirectoryLocation {
1185
  /**
1186
   * The location consists of a directory which contains the add-ons installed.
1187
   *
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
   * @param {string} name
   *        The string identifier for the install location.
   * @param {nsIFile} dir
   *        The directory for the install location.
   * @param {integer} scope
   *        The scope of add-ons installed in this location.
   * @param {boolean} resetSet
   *        True to throw away the current add-on set
   */
  constructor(name, dir, scope, resetSet) {
    let addonSet = SystemAddonLocation._loadAddonSet();
    let directory = null;

    // The system add-on update directory is stored in a pref.
    // Therefore, this is looked up before calling the
    // constructor on the superclass.
    if (addonSet.directory) {
      directory = getFile(addonSet.directory, dir);
      logger.info(`SystemAddonLocation scanning directory ${directory.path}`);
    } else {
      logger.info("SystemAddonLocation directory is missing");
1209
    }
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226

    super(name, directory, scope, false);

    this._addonSet = addonSet;
    this._baseDir = dir;

    if (resetSet) {
      this.installer.resetAddonSet();
    }
  }

  makeInstaller() {
    if (this.locked) {
      return null;
    }
    return new XPIInstall.SystemAddonInstaller(this);
  }
1227

1228
  /**
1229
1230
1231
   * Reads the current set of system add-ons
   *
   * @returns {Object}
1232
   */
1233
1234
1235
1236
1237
  static _loadAddonSet() {
    try {
      let setStr = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_SET, null);
      if (setStr) {
        let addonSet = JSON.parse(setStr);
1238
        if (typeof addonSet == "object" && addonSet.schema == 1) {
1239
          return addonSet;
1240
1241
        }
      }
1242
1243
    } catch (e) {
      logger.error("Malformed system add-on set, resetting.");
1244
1245
    }

1246
1247
    return { schema: 1, addons: {} };
  }
1248

1249
1250
1251
1252
  readAddons() {
    // Updated system add-ons are ignored in safe mode
    if (Services.appinfo.inSafeMode) {
      return new Map();
1253
1254
    }

1255
    let addons = super.readAddons();