XPIProvider.jsm 92.3 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
  FileUtils: "resource://gre/modules/FileUtils.jsm",
  JSONFile: "resource://gre/modules/JSONFile.jsm",
39
  TelemetrySession: "resource://gre/modules/TelemetrySession.jsm",
40

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

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

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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";
77
// xpinstall.signatures.required only supported in dev builds
78
79
80
81
82
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";
83

84
const PREF_EM_LAST_APP_BUILD_ID = "extensions.lastAppBuildId";
85

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

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

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

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

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

105
const KEY_APP_PROFILE = "app-profile";
106
const KEY_APP_SYSTEM_PROFILE = "app-system-profile";
107
108
109
110
111
112
113
114
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";
115

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

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

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

128
const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
129

130
const DB_SCHEMA = 33;
131

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

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

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

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

  return result.join("");
}
158

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

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

177
178
var gGlobalScope = this;

179
180
181
182
183
/**
 * 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;

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

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

191
192
193
194
195
196
197
198
199
200
201
202
203
/**
 * 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;

204
205
206
207
208
209
210
211
212
213
  promise.then(
    val => {
      success = true;
      result = val;
    },
    val => {
      success = false;
      result = val;
    }
  );
214

215
216
217
218
  Services.tm.spinEventLoopUntil(
    "XPIProvider.jsm:awaitPromise",
    () => success !== undefined
  );
219

220
  if (!success) {
221
    throw result;
222
  }
223
224
225
  return result;
}

226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/**
 * 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;
}

255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/**
 * 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) {
286
287
  let { leafName } = file;
  let id = isXPI(leafName, true) ? leafName.slice(0, -4) : leafName;
288
289
290
291
292
293
  if (gIDTest.test(id)) {
    return id;
  }
  return null;
}

294
295
296
/**
 * Evaluates whether an add-on is allowed to run in safe mode.
 *
297
298
299
300
 * @param {AddonInternal} aAddon
 *        The add-on to check
 * @returns {boolean}
 *        True if the add-on should run in safe mode
301
302
 */
function canRunInSafeMode(aAddon) {
303
304
305
306
307
  let location = aAddon.location || null;
  if (!location) {
    return false;
  }

308
309
310
  // 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.
311
312
313

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

317
/**
318
319
320
 * 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.
321
 *
322
323
324
325
326
327
328
329
 * @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
330
 */
331
function getURIForResourceInFile(aFile, aPath) {
332
  if (!isXPI(aFile.leafName)) {
333
    let resource = aFile.clone();
334
    if (aPath) {
335
      aPath.split("/").forEach(part => resource.append(part));
336
    }
337

338
    return Services.io.newFileURI(resource);
339
340
  }

341
342
  return buildJarURI(aFile, aPath);
}
343

344
345
346
/**
 * Creates a jar: URI for a file inside a ZIP file.
 *
347
348
349
350
351
352
 * @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
353
354
355
356
357
358
 */
function buildJarURI(aJarfile, aPath) {
  let uri = Services.io.newFileURI(aJarfile);
  uri = "jar:" + uri.spec + "!/" + aPath;
  return Services.io.newURI(uri);
}
359

360
361
362
363
364
365
366
function maybeResolveURI(uri) {
  if (uri.schemeIs("resource")) {
    return Services.io.newURI(resProto.resolveURI(uri));
  }
  return uri;
}

367
/**
368
 * Iterates over the entries in a given directory.
369
 *
370
371
372
373
 * Fails silently if the given directory does not exist.
 *
 * @param {nsIFile} aDir
 *        Directory to iterate.
374
 */
375
function* iterDirectory(aDir) {
376
377
  let dirEnum;
  try {
378
379
380
381
    dirEnum = aDir.directoryEntries;
    let file;
    while ((file = dirEnum.nextFile)) {
      yield file;
382
    }
383
384
  } catch (e) {
    if (aDir.exists()) {
385
      logger.warn(`Can't iterate directory ${aDir.path}`, e);
386
387
388
389
390
391
392
    }
  } finally {
    if (dirEnum) {
      dirEnum.close();
    }
  }
}
393

394
395
396
397
398
399
400
401
402
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
/**
 * 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;
}

441
442
443
444
445
446
447
448
/**
 * 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",
449
  "loader",
450
451
  "lastModifiedTime",
  "path",
452
  "rootURI",
453
  "runInSafeMode",
454
  "signedState",
455
  "signedDate",
456
  "startupData",
457
  "telemetryKey",
458
459
460
  "type",
  "version",
]);
461

462
463
464
465
class XPIState {
  constructor(location, id, saved = {}) {
    this.location = location;
    this.id = id;
466

467
468
    // Set default values.
    this.type = "extension";
469

470
471
472
    for (let prop of JSON_FIELDS) {
      if (prop in saved) {
        this[prop] = saved[prop];
473
474
      }
    }
475

476
477
478
479
480
481
    // Builds prior to be 1512436 did not include the rootURI property.
    // If we're updating from such a build, add that property now.
    if (!("rootURI" in this) && this.file) {
      this.rootURI = getURIForResourceInFile(this.file, "").spec;
    }

482
483
484
485
    if (!this.telemetryKey) {
      this.telemetryKey = this.getTelemetryKey();
    }

486
487
488
489
    if (
      saved.currentModifiedTime &&
      saved.currentModifiedTime != this.lastModifiedTime
    ) {
490
      this.lastModifiedTime = saved.currentModifiedTime;
491
492
    } else if (saved.currentModifiedTime === null) {
      this.missing = true;
493
    }
494
495
  }

496
  // Compatibility shim getters for legacy callers in XPIDatabase.jsm.
497
498
  get mtime() {
    return this.lastModifiedTime;
499
  }
500
501
502
  get active() {
    return this.enabled;
  }
503

504
505
506
507
508
509
510
511
  /**
   * @property {string} path
   *        The full on-disk path of the add-on.
   */
  get path() {
    return this.file && this.file.path;
  }
  set path(path) {
512
    this.file = path ? getFile(path, this.location.dir) : null;
513
514
  }

515
516
517
518
519
520
521
522
523
524
525
526
  /**
   * @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;
527
    }
528
529
    return this.path;
  }
530

531
532
533
  /**
   * Returns a JSON-compatible representation of this add-on's state
   * data, to be saved to addonStartup.json.
534
535
   *
   * @returns {Object}
536
537
538
   */
  toJSON() {
    let json = {
539
      dependencies: this.dependencies,
540
541
      enabled: this.enabled,
      lastModifiedTime: this.lastModifiedTime,
542
      loader: this.loader,
543
      path: this.relativePath,
544
      rootURI: this.rootURI,
545
546
      runInSafeMode: this.runInSafeMode,
      signedState: this.signedState,
547
      signedDate: this.signedDate,
548
      telemetryKey: this.telemetryKey,
549
      version: this.version,
550
551
552
    };
    if (this.type != "extension") {
      json.type = this.type;
553
    }
554
555
556
    if (this.startupData) {
      json.startupData = this.startupData;
    }
557
    return json;
558
559
  }

560
561
562
563
  get isWebExtension() {
    return this.loader == null;
  }

564
565
  /**
   * Update the last modified time for an add-on on disk.
566
567
568
569
570
   *
   * @param {nsIFile} aFile
   *        The location of the add-on.
   * @returns {boolean}
   *       True if the time stamp has changed.
571
   */
572
573
574
575
576
577
578
579
  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);
580
581
    }

582
    let changed = mtime != this.lastModifiedTime;
583
    this.lastModifiedTime = mtime;
584
    return changed;
585
  }
586

587
588
589
590
591
592
593
594
595
596
  /**
   * 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}`;
  }

597
598
599
600
  get resolvedRootURI() {
    return maybeResolveURI(Services.io.newURI(this.rootURI));
  }

601
602
603
604
  /**
   * 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.
605
   *
606
   * Caller is responsible for doing XPIStates.save() if necessary.
607
608
609
610
611
   *
   * @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.
612
613
614
615
616
617
618
   */
  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.
619
    let mustGetMod = aDBAddon.visible && !aDBAddon.disabled && !this.enabled;
620

621
    this.enabled = aDBAddon.visible && !aDBAddon.disabled;
622

623
624
    this.version = aDBAddon.version;
    this.type = aDBAddon.type;
625
626
    this.loader = aDBAddon.loader;

627
628
629
    if (aDBAddon.startupData) {
      this.startupData = aDBAddon.startupData;
    }
630

631
632
    this.telemetryKey = this.getTelemetryKey();

633
634
    this.dependencies = aDBAddon.dependencies;
    this.runInSafeMode = canRunInSafeMode(aDBAddon);
635
    this.signedState = aDBAddon.signedState;
636
    this.signedDate = aDBAddon.signedDate;
637
    this.file = aDBAddon._sourceBundle;
638
    this.rootURI = aDBAddon.rootURI;
639

640
641
642
643
644
645
    if ((aUpdated || mustGetMod) && this.file) {
      this.getModTime(this.file);
      if (this.lastModifiedTime != aDBAddon.updateDate) {
        aDBAddon.updateDate = this.lastModifiedTime;
        if (XPIDatabase.initialized) {
          XPIDatabase.saveChanges();
646
647
648
649
650
        }
      }
    }
  }
}
651

652
/**
653
 * Manages the state data for add-ons in a given install location.
654
 *
655
656
 * @param {string} name
 *        The name of the install location (e.g., "app-profile").
657
 * @param {string | nsIFile | null} path
658
659
 *        The on-disk path of the install location. May be null for some
 *        locations which do not map to a specific on-disk path.
660
661
662
 * @param {integer} scope
 *        The scope of add-ons installed in this location.
 * @param {object} [saved]
663
 *        The persisted JSON state data to restore.
664
 */
665
class XPIStateLocation extends Map {
666
  constructor(name, path, scope, saved) {
667
    super();
668

669
    this.name = name;
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
    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;

    if (saved) {
      this.restore(saved);
    }

685
    this._installer = undefined;
686
687
  }

688
689
690
691
692
  hasPrecedence(otherLocation) {
    let locations = Array.from(XPIStates.locations());
    return locations.indexOf(this) <= locations.indexOf(otherLocation);
  }

693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
  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);
    }
709
    this.staged = saved.staged || {};
710
    this.changed = saved.changed || false;
711

712
713
    for (let [id, data] of Object.entries(saved.addons || {})) {
      let xpiState = this._addState(id, data);
714
715
716
717

      // 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.
718
      if (!this.path || this.path == saved.path) {
719
720
        xpiState.wasRestored = true;
      }
721
722
723
    }
  }

724
725
726
  /**
   * Returns a JSON-compatible representation of this location's state
   * data, to be saved to addonStartup.json.
727
728
   *
   * @returns {Object}
729
730
   */
  toJSON() {
731
732
733
734
    let json = {
      addons: {},
      staged: this.staged,
    };
735

736
737
738
    if (this.path) {
      json.path = this.path;
    }
739

740
741
742
    if (STARTUP_MTIME_SCOPES.includes(this.name)) {
      json.checkStartupModifications = true;
    }
743

744
    for (let [id, addon] of this.entries()) {
745
      json.addons[id] = addon;
746
    }
747
    return json;
748
  }
749

750
751
752
753
754
755
756
  get hasStaged() {
    for (let key in this.staged) {
      return true;
    }
    return false;
  }

757
758
759
760
761
  _addState(addonId, saved) {
    let xpiState = new XPIState(this, addonId, saved);
    this.set(addonId, xpiState);
    return xpiState;
  }
762

763
764
765
766
767
768
769
  /**
   * Adds state data for the given DB add-on to the DB.
   *
   * @param {DBAddon} addon
   *        The DBAddon to add.
   */
  addAddon(addon) {
770
771
772
773
    logger.debug(
      "XPIStates adding add-on ${id} in ${location}: ${path}",
      addon
    );
774

775
776
    XPIProvider.persistStartupData(addon);

777
    let xpiState = this._addState(addon.id, { file: addon._sourceBundle });
778
    xpiState.syncWithDB(addon, true);
779

780
    XPIProvider.addTelemetry(addon.id, { location: this.name });
781
782
  }

783
784
785
786
787
788
789
790
791
792
793
794
795
  /**
   * 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();
    }
  }

796
797
798
799
800
801
802
803
804
805
  /**
   * 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) {
806
807
808
809
    let xpiState = this._addState(addonId, {
      enabled: false,
      file: file.clone(),
    });
810
    xpiState.getModTime(xpiState.file);
811
    return xpiState;
812
813
  }

814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
  /**
   * 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();
    }
  }

844
  *getStagedAddons() {
845
846
847
848
849
    for (let [id, metadata] of Object.entries(this.staged)) {
      yield [id, metadata];
    }
  }

850
  /**
851
852
853
854
855
856
   * 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}
857
   */
858
859
860
  isLinkedAddon(aId) {
    if (!this.dir) {
      return true;
861
    }
862
863
864
865
866
867
868
869
870
871
    return this.has(aId) && !this.dir.contains(this.get(aId).file);
  }

  get isTemporary() {
    return false;
  }

  get isSystem() {
    return false;
  }
872
873
874
875
876

  get isBuiltin() {
    return false;
  }

877
878
879
880
  get hidden() {
    return this.isBuiltin;
  }

881
882
883
884
885
886
  // 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;
  }
887
}
888

889
class TemporaryLocation extends XPIStateLocation {
890
  /**
891
892
   * @param {string} name
   *        The string identifier for the install location.
893
   */
894
  constructor(name) {
895
    super(name, null, AddonManager.SCOPE_TEMPORARY);
896
897
    this.locked = false;
  }
898

899
900
901
902
903
904
905
906
  makeInstaller() {
    // Installs are a no-op. We only register that add-ons exist, and
    // run them from their current location.
    return {
      installAddon() {},
      uninstallAddon() {},
    };
  }
907

908
909
910
  toJSON() {
    return {};
  }
911

912
913
  get isTemporary() {
    return true;
914
  }
915
916
917
918

  get enumerable() {
    return false;
  }
919
}
920

921
var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);
922

923
924
925
/**
 * A "location" for addons installed from assets packged into the app.
 */
926
var BuiltInLocation = new (class _BuiltInLocation extends XPIStateLocation {
927
  constructor() {
928
    super(KEY_APP_BUILTINS, null, AddonManager.SCOPE_APPLICATION);
929
930
931
932
933
934
935
936
937
938
939
940
941
    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() {},
    };
  }

942
943
944
945
  get hidden() {
    return false;
  }

946
947
948
949
950
951
952
  get isBuiltin() {
    return true;
  }

  get enumerable() {
    return false;
  }
953
954
955
956
957
958

  // Builtin addons are never linked, return false
  // here for correct behavior elsewhere.
  isLinkedAddon(/* aId */) {
    return false;
  }
959
})();
960

961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
/**
 * 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.
982
983
   * @param {boolean} [system = false]
   *        If true, the location is a system addon location.
984
   */
985
  constructor(name, dir, scope, locked = true, system = false) {
986
987
    super(name, dir, scope);
    this.locked = locked;
988
    this._isSystem = system;
989
  }
990

991
992
993
994
995
996
  makeInstaller() {
    if (this.locked) {
      return null;
    }
    return new XPIInstall.DirectoryInstaller(this);
  }
997

998
  /**
999
1000
   * Reads a single-line file containing the path to a directory, and
   * returns an nsIFile pointing to that directory, if successful.