summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/FileMigrators.sys.mjs
blob: 3384011c13cb10910d336e7218a6d1d47f7dfc59 (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
/* 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/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
  BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
  LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs",
  MigrationWizardConstants:
    "chrome://browser/content/migration/migration-wizard-constants.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () {
  return new Localization([
    "branding/brand.ftl",
    "browser/migrationWizard.ftl",
  ]);
});

/**
 * Base class for a migration that involves reading a single file off of
 * the disk that the user picks using a file picker. The file might be
 * generated by another browser or some other application.
 */
export class FileMigratorBase {
  /**
   * This must be overridden to return a simple string identifier for the
   * migrator, for example "password-csv". This key is what
   * is used as an identifier when calling MigrationUtils.getFileMigrator.
   *
   * @type {string}
   */
  static get key() {
    throw new Error("FileMigrator.key must be overridden.");
  }

  /**
   * This must be overridden to return a Fluent string ID mapping to the display
   * name for this migrator. These strings should be defined in migrationWizard.ftl.
   *
   * @type {string}
   */
  static get displayNameL10nID() {
    throw new Error("FileMigrator.displayNameL10nID must be overridden.");
  }

  /**
   * This getter should get overridden to return an icon url to represent the
   * file to be imported from. By default, this will just use the default Favicon
   * image.
   *
   * @type {string}
   */
  static get brandImage() {
    return "chrome://global/skin/icons/defaultFavicon.svg";
  }

  /**
   * Returns true if the migrator is configured to be enabled.
   *
   * @type {boolean}
   *   true if the migrator should be shown in the migration wizard.
   */
  get enabled() {
    throw new Error("FileMigrator.enabled must be overridden.");
  }

  /**
   * This getter should be overridden to return a Fluent string ID for what
   * the migration wizard header should be while the file migration is
   * underway.
   *
   * @type {string}
   */
  get progressHeaderL10nID() {
    throw new Error("FileMigrator.progressHeaderL10nID must be overridden.");
  }

  /**
   * This getter should be overridden to return a Fluent string ID for what
   * the migration wizard header should be while the file migration is
   * done.
   *
   * @type {string}
   */
  get successHeaderL10nID() {
    throw new Error("FileMigrator.progressHeaderL10nID must be overridden.");
  }

  /**
   * @typedef {object} FilePickerConfiguration
   * @property {string} title
   *   The title that should be assigned to the native file picker window.
   * @property {FilePickerConfigurationFilter[]} filters
   *   One or more extension filters that should be applied to the native
   *   file picker window to make selection easier.
   */

  /**
   * @typedef {object} FilePickerConfigurationFilter
   * @property {string} title
   *   The title for the filter. Example: "CSV Files"
   * @property {string} extensionPattern
   *   A matching pattern for the filter. Example: "*.csv"
   */

  /**
   * A subclass of FileMigratorBase will eventually open a native file picker
   * for the user to select the file from their file system.
   *
   * Subclasses need to override this method in order to configure the
   * native file picker.
   *
   * @returns {Promise<FilePickerConfiguration>}
   */
  async getFilePickerConfig() {
    throw new Error("FileMigrator.getFilePickerConfig must be overridden.");
  }

  /**
   * Returns a list of one or more resource types that should appear to be
   * in progress of migrating while the file migration occurs. Notably,
   * this does not need to match the resource types that are returned by
   * `FileMigratorBase.migrate`.
   *
   * @type {string[]}
   *   An array of resource types from the
   *   MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES set.
   */
  get displayedResourceTypes() {
    throw new Error("FileMigrator.displayedResourceTypes must be overridden");
  }

  /**
   * Called to perform the file migration once the user makes a selection
   * from the native file picker. This will not be called if the user
   * chooses to cancel the native file picker.
   *
   * @param {string} filePath
   *   The path that the user selected from the native file picker.
   */
  // eslint-disable-next-line no-unused-vars
  async migrate(filePath) {
    throw new Error("FileMigrator.migrate must be overridden.");
  }
}

/**
 * A file migrator for importing passwords from CSV or TSV files. CSV
 * files are more common, so this is what we show as the file type for
 * the display name, but this FileMigrator accepts both.
 */
export class PasswordFileMigrator extends FileMigratorBase {
  static get key() {
    return "file-password-csv";
  }

  static get displayNameL10nID() {
    return "migration-wizard-migrator-display-name-file-password-csv";
  }

  static get brandImage() {
    return "chrome://branding/content/document.ico";
  }

  get enabled() {
    return Services.prefs.getBoolPref(
      "signon.management.page.fileImport.enabled",
      false
    );
  }

  get displayedResourceTypes() {
    return [
      lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
        .PASSWORDS_FROM_FILE,
    ];
  }

  get progressHeaderL10nID() {
    return "migration-passwords-from-file-progress-header";
  }

  get successHeaderL10nID() {
    return "migration-passwords-from-file-success-header";
  }

  async getFilePickerConfig() {
    let [title, csvFilterTitle, tsvFilterTitle] =
      await lazy.gFluentStrings.formatValues([
        { id: "migration-passwords-from-file-picker-title" },
        { id: "migration-passwords-from-file-csv-filter-title" },
        { id: "migration-passwords-from-file-tsv-filter-title" },
      ]);

    return {
      title,
      filters: [
        {
          title: csvFilterTitle,
          extensionPattern: "*.csv",
        },
        {
          title: tsvFilterTitle,
          extensionPattern: "*.tsv",
        },
      ],
    };
  }

  async migrate(filePath) {
    try {
      let summary = await lazy.LoginCSVImport.importFromCSV(filePath);
      let newEntries = 0;
      let updatedEntries = 0;
      for (let entry of summary) {
        if (entry.result == "added") {
          newEntries++;
        } else if (entry.result == "modified") {
          updatedEntries++;
        }
      }
      let [newMessage, updatedMessage] = await lazy.gFluentStrings.formatValues(
        [
          {
            id: "migration-wizard-progress-success-new-passwords",
            args: { newEntries },
          },
          {
            id: "migration-wizard-progress-success-updated-passwords",
            args: { updatedEntries },
          },
        ]
      );

      Services.prefs.setBoolPref(
        "browser.migrate.interactions.csvpasswords",
        true
      );

      return {
        [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
          .PASSWORDS_NEW]: newMessage,
        [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
          .PASSWORDS_UPDATED]: updatedMessage,
      };
    } catch (e) {
      console.error(e);

      let errorMessage = await lazy.gFluentStrings.formatValue(
        "migration-passwords-from-file-no-valid-data"
      );
      throw new Error(errorMessage);
    }
  }
}

/**
 * A file migrator for importing bookmarks from a HTML or JSON file.
 *
 * @class BookmarksFileMigrator
 * @augments {FileMigratorBase}
 */
export class BookmarksFileMigrator extends FileMigratorBase {
  static get key() {
    return "file-bookmarks";
  }

  static get displayNameL10nID() {
    return "migration-wizard-migrator-display-name-file-bookmarks";
  }

  static get brandImage() {
    return "chrome://branding/content/document.ico";
  }

  get enabled() {
    return Services.prefs.getBoolPref(
      "browser.migrate.bookmarks-file.enabled",
      false
    );
  }

  get displayedResourceTypes() {
    return [
      lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
        .BOOKMARKS_FROM_FILE,
    ];
  }

  get progressHeaderL10nID() {
    return "migration-bookmarks-from-file-progress-header";
  }

  get successHeaderL10nID() {
    return "migration-bookmarks-from-file-success-header";
  }

  async getFilePickerConfig() {
    let [title, htmlFilterTitle, jsonFilterTitle] =
      await lazy.gFluentStrings.formatValues([
        { id: "migration-bookmarks-from-file-picker-title" },
        { id: "migration-bookmarks-from-file-html-filter-title" },
        { id: "migration-bookmarks-from-file-json-filter-title" },
      ]);

    return {
      title,
      filters: [
        {
          title: htmlFilterTitle,
          extensionPattern: "*.html",
        },
        {
          title: jsonFilterTitle,
          extensionPattern: "*.json",
        },
      ],
    };
  }

  async migrate(filePath) {
    try {
      let pathCheck = filePath.toLowerCase();
      let importedCount;

      if (pathCheck.endsWith("html")) {
        importedCount = await lazy.BookmarkHTMLUtils.importFromFile(filePath);
      } else if (pathCheck.endsWith("json") || pathCheck.endsWith("jsonlz4")) {
        importedCount = await lazy.BookmarkJSONUtils.importFromFile(filePath);
      }

      if (!importedCount) {
        // The catch will cause us to show a default error message.
        throw new Error();
      }

      let importedMessage = await lazy.gFluentStrings.formatValue(
        "migration-wizard-progress-success-new-bookmarks",
        {
          newEntries: importedCount,
        }
      );
      return {
        [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
          .BOOKMARKS_FROM_FILE]: importedMessage,
      };
    } catch (e) {
      console.error(e);

      let errorMessage = await lazy.gFluentStrings.formatValue(
        "migration-bookmarks-from-file-no-valid-data"
      );
      throw new Error(errorMessage);
    }
  }
}