summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/ChromeMigrationUtils.sys.mjs
blob: 055dc804526373ea321fdcba89b31506964d8e18 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
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
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
/* This Source Code Form is subject to the terms of the Mozilla Public
 * 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
});

XPCOMUtils.defineLazyModuleGetters(lazy, {
  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
});

const S100NS_FROM1601TO1970 = 0x19db1ded53e8000;
const S100NS_PER_MS = 10;

export var ChromeMigrationUtils = {
  // Supported browsers with importable logins.
  CONTEXTUAL_LOGIN_IMPORT_BROWSERS: ["chrome", "chromium-edge", "chromium"],

  _extensionVersionDirectoryNames: {},

  // The cache for the locale strings.
  // For example, the data could be:
  // {
  //   "profile-id-1": {
  //     "extension-id-1": {
  //       "name": {
  //         "message": "Fake App 1"
  //       }
  //   },
  // }
  _extensionLocaleStrings: {},

  get supportsLoginsForPlatform() {
    return ["macosx", "win"].includes(AppConstants.platform);
  },

  /**
   * Get all extensions installed in a specific profile.
   *
   * @param {string} profileId - A Chrome user profile ID. For example, "Profile 1".
   * @returns {Array} All installed Chrome extensions information.
   */
  async getExtensionList(profileId) {
    if (profileId === undefined) {
      profileId = await this.getLastUsedProfileId();
    }
    let path = this.getExtensionPath(profileId);
    let extensionList = [];
    try {
      for (const child of await IOUtils.getChildren(path)) {
        const info = await IOUtils.stat(child);
        if (info.type === "directory") {
          const name = PathUtils.filename(child);
          let extensionInformation = await this.getExtensionInformation(
            name,
            profileId
          );
          if (extensionInformation) {
            extensionList.push(extensionInformation);
          }
        }
      }
    } catch (ex) {
      Cu.reportError(ex);
    }
    return extensionList;
  },

  /**
   * Get information of a specific Chrome extension.
   *
   * @param {string} extensionId - The extension ID.
   * @param {string} profileId - The user profile's ID.
   * @returns {object} The Chrome extension information.
   */
  async getExtensionInformation(extensionId, profileId) {
    if (profileId === undefined) {
      profileId = await this.getLastUsedProfileId();
    }
    let extensionInformation = null;
    try {
      let manifestPath = this.getExtensionPath(profileId);
      manifestPath = PathUtils.join(manifestPath, extensionId);
      // If there are multiple sub-directories in the extension directory,
      // read the files in the latest directory.
      let directories = await this._getSortedByVersionSubDirectoryNames(
        manifestPath
      );
      if (!directories[0]) {
        return null;
      }

      manifestPath = PathUtils.join(
        manifestPath,
        directories[0],
        "manifest.json"
      );
      let manifest = await IOUtils.readJSON(manifestPath);
      // No app attribute means this is a Chrome extension not a Chrome app.
      if (!manifest.app) {
        const DEFAULT_LOCALE = manifest.default_locale;
        let name = await this._getLocaleString(
          manifest.name,
          DEFAULT_LOCALE,
          extensionId,
          profileId
        );
        let description = await this._getLocaleString(
          manifest.description,
          DEFAULT_LOCALE,
          extensionId,
          profileId
        );
        if (name) {
          extensionInformation = {
            id: extensionId,
            name,
            description,
          };
        } else {
          throw new Error("Cannot read the Chrome extension's name property.");
        }
      }
    } catch (ex) {
      Cu.reportError(ex);
    }
    return extensionInformation;
  },

  /**
   * Get the manifest's locale string.
   *
   * @param {string} key - The key of a locale string, for example __MSG_name__.
   * @param {string} locale - The specific language of locale string.
   * @param {string} extensionId - The extension ID.
   * @param {string} profileId - The user profile's ID.
   * @returns {string} The locale string.
   */
  async _getLocaleString(key, locale, extensionId, profileId) {
    // Return the key string if it is not a locale key.
    // The key string starts with "__MSG_" and ends with "__".
    // For example, "__MSG_name__".
    // https://developer.chrome.com/apps/i18n
    if (!key.startsWith("__MSG_") || !key.endsWith("__")) {
      return key;
    }

    let localeString = null;
    try {
      let localeFile;
      if (
        this._extensionLocaleStrings[profileId] &&
        this._extensionLocaleStrings[profileId][extensionId]
      ) {
        localeFile = this._extensionLocaleStrings[profileId][extensionId];
      } else {
        if (!this._extensionLocaleStrings[profileId]) {
          this._extensionLocaleStrings[profileId] = {};
        }
        let localeFilePath = this.getExtensionPath(profileId);
        localeFilePath = PathUtils.join(localeFilePath, extensionId);
        let directories = await this._getSortedByVersionSubDirectoryNames(
          localeFilePath
        );
        // If there are multiple sub-directories in the extension directory,
        // read the files in the latest directory.
        localeFilePath = PathUtils.join(
          localeFilePath,
          directories[0],
          "_locales",
          locale,
          "messages.json"
        );
        localeFile = await IOUtils.readJSON(localeFilePath);
        this._extensionLocaleStrings[profileId][extensionId] = localeFile;
      }
      const PREFIX_LENGTH = 6;
      const SUFFIX_LENGTH = 2;
      // Get the locale key from the string with locale prefix and suffix.
      // For example, it will get the "name" sub-string from the "__MSG_name__" string.
      key = key.substring(PREFIX_LENGTH, key.length - SUFFIX_LENGTH);
      if (localeFile[key] && localeFile[key].message) {
        localeString = localeFile[key].message;
      }
    } catch (ex) {
      Cu.reportError(ex);
    }
    return localeString;
  },

  /**
   * Check that a specific extension is installed or not.
   *
   * @param {string} extensionId - The extension ID.
   * @param {string} profileId - The user profile's ID.
   * @returns {boolean} Return true if the extension is installed otherwise return false.
   */
  async isExtensionInstalled(extensionId, profileId) {
    if (profileId === undefined) {
      profileId = await this.getLastUsedProfileId();
    }
    let extensionPath = this.getExtensionPath(profileId);
    let isInstalled = await IOUtils.exists(
      PathUtils.join(extensionPath, extensionId)
    );
    return isInstalled;
  },

  /**
   * Get the last used user profile's ID.
   *
   * @returns {string} The last used user profile's ID.
   */
  async getLastUsedProfileId() {
    let localState = await this.getLocalState();
    return localState ? localState.profile.last_used : "Default";
  },

  /**
   * Get the local state file content.
   *
   * @param {string} dataPath the type of Chrome data we're looking for (Chromium, Canary, etc.)
   * @returns {object} The JSON-based content.
   */
  async getLocalState(dataPath = "Chrome") {
    let localState = null;
    try {
      let localStatePath = PathUtils.join(
        this.getDataPath(dataPath),
        "Local State"
      );
      localState = JSON.parse(await IOUtils.readUTF8(localStatePath));
    } catch (ex) {
      // Don't report the error if it's just a file not existing.
      if (ex.name != "NotFoundError") {
        Cu.reportError(ex);
      }
      throw ex;
    }
    return localState;
  },

  /**
   * Get the path of Chrome extension directory.
   *
   * @param {string} profileId - The user profile's ID.
   * @returns {string} The path of Chrome extension directory.
   */
  getExtensionPath(profileId) {
    return PathUtils.join(this.getDataPath(), profileId, "Extensions");
  },

  /**
   * Get the path of an application data directory.
   *
   * @param {string} chromeProjectName - The Chrome project name, e.g. "Chrome", "Canary", etc.
   *                                     Defaults to "Chrome".
   * @returns {string} The path of application data directory.
   */
  getDataPath(chromeProjectName = "Chrome") {
    const SUB_DIRECTORIES = {
      win: {
        Brave: ["BraveSoftware", "Brave-Browser"],
        Chrome: ["Google", "Chrome"],
        "Chrome Beta": ["Google", "Chrome Beta"],
        Chromium: ["Chromium"],
        Canary: ["Google", "Chrome SxS"],
        Edge: ["Microsoft", "Edge"],
        "Edge Beta": ["Microsoft", "Edge Beta"],
        "360 SE": ["360se6"],
        Opera: ["Opera Software", "Opera Stable"],
        "Opera GX": ["Opera Software", "Opera GX Stable"],
        Vivaldi: ["Vivaldi"],
      },
      macosx: {
        Brave: ["BraveSoftware", "Brave-Browser"],
        Chrome: ["Google", "Chrome"],
        Chromium: ["Chromium"],
        Canary: ["Google", "Chrome Canary"],
        Edge: ["Microsoft Edge"],
        "Edge Beta": ["Microsoft Edge Beta"],
        "Opera GX": ["com.operasoftware.OperaGX"],
        Opera: ["com.operasoftware.Opera"],
        Vivaldi: ["Vivaldi"],
      },
      linux: {
        Brave: ["BraveSoftware", "Brave-Browser"],
        Chrome: ["google-chrome"],
        "Chrome Beta": ["google-chrome-beta"],
        "Chrome Dev": ["google-chrome-unstable"],
        Chromium: ["chromium"],
        // Opera GX is not available on Linux.
        // Canary is not available on Linux.
        // Edge is not available on Linux.
        Opera: ["opera"],
        Vivaldi: ["vivaldi"],
      },
    };
    let subfolders = SUB_DIRECTORIES[AppConstants.platform][chromeProjectName];
    if (!subfolders) {
      return null;
    }

    let rootDir;
    if (AppConstants.platform == "win") {
      if (
        chromeProjectName === "360 SE" ||
        chromeProjectName === "Opera" ||
        chromeProjectName === "Opera GX"
      ) {
        rootDir = "AppData";
      } else {
        rootDir = "LocalAppData";
      }
      if (chromeProjectName != "Opera" && chromeProjectName != "Opera GX") {
        subfolders = subfolders.concat(["User Data"]);
      }
    } else if (AppConstants.platform == "macosx") {
      rootDir = "ULibDir";
      subfolders = ["Application Support"].concat(subfolders);
    } else {
      rootDir = "Home";
      subfolders = [".config"].concat(subfolders);
    }
    try {
      let target = Services.dirsvc.get(rootDir, Ci.nsIFile);
      for (let subfolder of subfolders) {
        target.append(subfolder);
      }
      return target.path;
    } catch (ex) {
      // The path logic here shouldn't error, so log it:
      Cu.reportError(ex);
    }
    return null;
  },

  /**
   * Get the directory objects sorted by version number.
   *
   * @param {string} path - The path to the extension directory.
   * otherwise return all file/directory object.
   * @returns {Array} The file/directory object array.
   */
  async _getSortedByVersionSubDirectoryNames(path) {
    if (this._extensionVersionDirectoryNames[path]) {
      return this._extensionVersionDirectoryNames[path];
    }

    let entries = [];
    try {
      for (const child of await IOUtils.getChildren(path)) {
        const info = await IOUtils.stat(child);
        if (info.type === "directory") {
          const name = PathUtils.filename(child);
          entries.push(name);
        }
      }
    } catch (ex) {
      Cu.reportError(ex);
      entries = [];
    }

    // The directory name is the version number string of the extension.
    // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2.
    // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again.
    // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc
    entries.sort((a, b) => Services.vc.compare(b, a));

    this._extensionVersionDirectoryNames[path] = entries;
    return entries;
  },

  /**
   * Convert Chrome time format to Date object. Google Chrome uses FILETIME / 10 as time.
   * FILETIME is based on the same structure of Windows.
   *
   * @param {number} aTime Chrome time
   * @param {string|number|Date} aFallbackValue a date or timestamp (valid argument
   *   for the Date constructor) that will be used if the chrometime value passed is
   *   invalid.
   * @returns {Date} converted Date object
   */
  chromeTimeToDate(aTime, aFallbackValue) {
    // The date value may be 0 in some cases. Because of the subtraction below,
    // that'd generate a date before the unix epoch, which can upset consumers
    // due to the unix timestamp then being negative. Catch this case:
    if (!aTime) {
      return new Date(aFallbackValue);
    }
    return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
  },

  /**
   * Convert Date object to Chrome time format. For details on Chrome time, see
   * chromeTimeToDate.
   *
   * @param {Date|number} aDate Date object or integer equivalent
   * @returns {number} Chrome time
   */
  dateToChromeTime(aDate) {
    return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
  },

  /**
   * Returns an array of chromium browser ids that have importable logins.
   */
  _importableLoginsCache: null,
  async getImportableLogins(formOrigin) {
    // Only provide importable if we actually support importing.
    if (!this.supportsLoginsForPlatform) {
      return undefined;
    }

    // Lazily fill the cache with all importable login browsers.
    if (!this._importableLoginsCache) {
      this._importableLoginsCache = new Map();

      // Just handle these chromium-based browsers for now.
      for (const browserId of this.CONTEXTUAL_LOGIN_IMPORT_BROWSERS) {
        // Skip if there's no profile data.
        const migrator = await lazy.MigrationUtils.getMigrator(browserId);
        if (!migrator) {
          continue;
        }

        // Check each profile for logins.
        const dataPath = await migrator._getChromeUserDataPathIfExists();
        for (const profile of await migrator.getSourceProfiles()) {
          const path = PathUtils.join(dataPath, profile.id, "Login Data");
          // Skip if login data is missing.
          if (!(await IOUtils.exists(path))) {
            Cu.reportError(`Missing file at ${path}`);
            continue;
          }

          try {
            for (const row of await lazy.MigrationUtils.getRowsFromDBWithoutLocks(
              path,
              `Importable ${browserId} logins`,
              `SELECT origin_url
               FROM logins
               WHERE blacklisted_by_user = 0`
            )) {
              const url = row.getString(0);
              try {
                // Initialize an array if it doesn't exist for the origin yet.
                const origin = lazy.LoginHelper.getLoginOrigin(url);
                const entries = this._importableLoginsCache.get(origin) || [];
                if (!entries.length) {
                  this._importableLoginsCache.set(origin, entries);
                }

                // Add the browser if it doesn't exist yet.
                if (!entries.includes(browserId)) {
                  entries.push(browserId);
                }
              } catch (ex) {
                Cu.reportError(
                  `Failed to process importable url ${url} from ${browserId} ${ex}`
                );
              }
            }
          } catch (ex) {
            Cu.reportError(
              `Failed to get importable logins from ${browserId} ${ex}`
            );
          }
        }
      }
    }
    return this._importableLoginsCache.get(formOrigin);
  },
};