summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/content
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/migration/content/aboutWelcomeBack.xhtml126
-rw-r--r--browser/components/migration/content/brands/360.pngbin0 -> 21075 bytes
-rw-r--r--browser/components/migration/content/brands/brave.pngbin0 -> 7099 bytes
-rw-r--r--browser/components/migration/content/brands/canary.pngbin0 -> 7463 bytes
-rw-r--r--browser/components/migration/content/brands/chrome.pngbin0 -> 8353 bytes
-rw-r--r--browser/components/migration/content/brands/chromium.pngbin0 -> 6408 bytes
-rw-r--r--browser/components/migration/content/brands/edge.pngbin0 -> 11899 bytes
-rw-r--r--browser/components/migration/content/brands/edgebeta.pngbin0 -> 12273 bytes
-rw-r--r--browser/components/migration/content/brands/ie.pngbin0 -> 6871 bytes
-rw-r--r--browser/components/migration/content/brands/opera.pngbin0 -> 5403 bytes
-rw-r--r--browser/components/migration/content/brands/operagx.pngbin0 -> 8222 bytes
-rw-r--r--browser/components/migration/content/brands/safari.pngbin0 -> 20520 bytes
-rw-r--r--browser/components/migration/content/brands/vivaldi.pngbin0 -> 7535 bytes
-rw-r--r--browser/components/migration/content/migration-dialog-window.html34
-rw-r--r--browser/components/migration/content/migration-dialog-window.js82
-rw-r--r--browser/components/migration/content/migration-wizard-constants.mjs76
-rw-r--r--browser/components/migration/content/migration-wizard.mjs1088
-rw-r--r--browser/components/migration/content/migration.js812
-rw-r--r--browser/components/migration/content/migration.xhtml113
19 files changed, 2331 insertions, 0 deletions
diff --git a/browser/components/migration/content/aboutWelcomeBack.xhtml b/browser/components/migration/content/aboutWelcomeBack.xhtml
new file mode 100644
index 0000000000..0777cc56e9
--- /dev/null
+++ b/browser/components/migration/content/aboutWelcomeBack.xhtml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# 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/.
+-->
+<!DOCTYPE html [ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+%htmlDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome: resource:; img-src chrome: resource: data:; object-src 'none'"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <title data-l10n-id="welcome-back-tab-title"></title>
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/in-content/info-pages.css"
+ media="all"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/skin/aboutWelcomeBack.css"
+ media="all"
+ />
+ <link rel="icon" href="chrome://global/skin/icons/info-filled.svg" />
+ <link rel="localization" href="browser/aboutSessionRestore.ftl" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <script src="chrome://browser/content/aboutSessionRestore.js" />
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-support-link.mjs"
+ />
+ </head>
+
+ <body>
+ <div class="container tab-list-tree-container">
+ <div class="description-wrapper">
+ <div class="title">
+ <h1 class="title-text" data-l10n-id="welcome-back-page-title"></h1>
+ </div>
+
+ <div class="description">
+ <p data-l10n-id="welcome-back-page-info"></p>
+ <p data-l10n-id="welcome-back-page-info-link">
+ <a
+ is="moz-support-link"
+ id="linkMoreTroubleshooting"
+ target="_blank"
+ data-l10n-name="link-more"
+ support-page="troubleshooting"
+ />
+ </p>
+ <div>
+ <label class="radioRestoreContainer radio-container-with-text">
+ <input
+ class="radioRestoreButton"
+ id="radioRestoreAll"
+ type="radio"
+ name="restore"
+ checked="checked"
+ />
+ <span
+ class="radioRestoreLabel"
+ data-l10n-id="welcome-back-restore-all-label"
+ ></span>
+ </label>
+
+ <label class="radioRestoreContainer radio-container-with-text">
+ <input
+ class="radioRestoreButton"
+ id="radioRestoreChoose"
+ type="radio"
+ name="restore"
+ />
+ <span
+ class="radioRestoreLabel"
+ data-l10n-id="welcome-back-restore-some-label"
+ ></span>
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <xul:tree
+ id="tabList"
+ flex="1"
+ seltype="single"
+ hidecolumnpicker="true"
+ hidden="true"
+ >
+ <xul:treecols>
+ <xul:treecol
+ cycler="true"
+ id="restore"
+ type="checkbox"
+ data-l10n-id="restore-page-restore-header"
+ />
+ <xul:splitter class="tree-splitter" />
+ <xul:treecol
+ primary="true"
+ id="title"
+ data-l10n-id="restore-page-list-header"
+ flex="1"
+ />
+ </xul:treecols>
+ <xul:treechildren flex="1" />
+ </xul:tree>
+
+ <div class="button-container">
+ <xul:button
+ class="primary"
+ id="errorTryAgain"
+ data-l10n-id="welcome-back-restore-button"
+ />
+ </div>
+
+ <input type="text" id="sessionData" hidden="true" />
+ </div>
+ </body>
+</html>
diff --git a/browser/components/migration/content/brands/360.png b/browser/components/migration/content/brands/360.png
new file mode 100644
index 0000000000..50562a0f70
--- /dev/null
+++ b/browser/components/migration/content/brands/360.png
Binary files differ
diff --git a/browser/components/migration/content/brands/brave.png b/browser/components/migration/content/brands/brave.png
new file mode 100644
index 0000000000..27d66e9a8e
--- /dev/null
+++ b/browser/components/migration/content/brands/brave.png
Binary files differ
diff --git a/browser/components/migration/content/brands/canary.png b/browser/components/migration/content/brands/canary.png
new file mode 100644
index 0000000000..8d46be18c6
--- /dev/null
+++ b/browser/components/migration/content/brands/canary.png
Binary files differ
diff --git a/browser/components/migration/content/brands/chrome.png b/browser/components/migration/content/brands/chrome.png
new file mode 100644
index 0000000000..3fecf6bb02
--- /dev/null
+++ b/browser/components/migration/content/brands/chrome.png
Binary files differ
diff --git a/browser/components/migration/content/brands/chromium.png b/browser/components/migration/content/brands/chromium.png
new file mode 100644
index 0000000000..0e5bb5d0cb
--- /dev/null
+++ b/browser/components/migration/content/brands/chromium.png
Binary files differ
diff --git a/browser/components/migration/content/brands/edge.png b/browser/components/migration/content/brands/edge.png
new file mode 100644
index 0000000000..b2e2ad4065
--- /dev/null
+++ b/browser/components/migration/content/brands/edge.png
Binary files differ
diff --git a/browser/components/migration/content/brands/edgebeta.png b/browser/components/migration/content/brands/edgebeta.png
new file mode 100644
index 0000000000..4e3977735e
--- /dev/null
+++ b/browser/components/migration/content/brands/edgebeta.png
Binary files differ
diff --git a/browser/components/migration/content/brands/ie.png b/browser/components/migration/content/brands/ie.png
new file mode 100644
index 0000000000..e01c0f35ed
--- /dev/null
+++ b/browser/components/migration/content/brands/ie.png
Binary files differ
diff --git a/browser/components/migration/content/brands/opera.png b/browser/components/migration/content/brands/opera.png
new file mode 100644
index 0000000000..71443f36a9
--- /dev/null
+++ b/browser/components/migration/content/brands/opera.png
Binary files differ
diff --git a/browser/components/migration/content/brands/operagx.png b/browser/components/migration/content/brands/operagx.png
new file mode 100644
index 0000000000..ed690aa200
--- /dev/null
+++ b/browser/components/migration/content/brands/operagx.png
Binary files differ
diff --git a/browser/components/migration/content/brands/safari.png b/browser/components/migration/content/brands/safari.png
new file mode 100644
index 0000000000..3b23caefb2
--- /dev/null
+++ b/browser/components/migration/content/brands/safari.png
Binary files differ
diff --git a/browser/components/migration/content/brands/vivaldi.png b/browser/components/migration/content/brands/vivaldi.png
new file mode 100644
index 0000000000..e9c2029006
--- /dev/null
+++ b/browser/components/migration/content/brands/vivaldi.png
Binary files differ
diff --git a/browser/components/migration/content/migration-dialog-window.html b/browser/components/migration/content/migration-dialog-window.html
new file mode 100644
index 0000000000..79f31c4606
--- /dev/null
+++ b/browser/components/migration/content/migration-dialog-window.html
@@ -0,0 +1,34 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="color-scheme" content="light dark" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src 'none'; object-src 'none'; script-src chrome:; media-src chrome:; img-src chrome:; style-src chrome:;"
+ />
+ <link
+ rel="icon"
+ type="image/png"
+ href="chrome://branding/content/icon32.png"
+ />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/skin/migration/migration-dialog-window.css"
+ />
+ <script
+ src="chrome://browser/content/migration/migration-wizard.mjs"
+ type="module"
+ ></script>
+ <script src="chrome://browser/content/migration/migration-dialog-window.js"></script>
+ </head>
+ <body>
+ <migration-wizard id="wizard" dialog-mode>
+ <panel-list></panel-list>
+ </migration-wizard>
+ </body>
+</html>
diff --git a/browser/components/migration/content/migration-dialog-window.js b/browser/components/migration/content/migration-dialog-window.js
new file mode 100644
index 0000000000..feaa70f565
--- /dev/null
+++ b/browser/components/migration/content/migration-dialog-window.js
@@ -0,0 +1,82 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * This file manages a MigrationWizard embedded in a dialog that runs
+ * in a top-level dialog window. It's main responsibility is to listen
+ * for dialog-specific events from the embedded MigrationWizard and to
+ * respond appropriately to them.
+ *
+ * A single object argument is expected to be passed when opening
+ * this dialog.
+ *
+ * @param {object} window.arguments.0
+ * @param {Function} window.arguments.0.onResize
+ * A callback to resize the container of this document when the
+ * MigrationWizard resizes.
+ * @param {object} window.arguments.0.options
+ * A series of options for configuring the dialog. See
+ * MigrationUtils.showMigrationWizard for a description of this
+ * object.
+ */
+
+const MigrationDialog = {
+ _wiz: null,
+
+ init() {
+ addEventListener("load", this);
+ },
+
+ onLoad() {
+ this._wiz = document.getElementById("wizard");
+ this._wiz.addEventListener("MigrationWizard:Close", this);
+ document.addEventListener("keypress", this);
+
+ let args = window.arguments[0];
+ // When opened via nsIWindowWatcher.openWindow, the arguments are
+ // passed through C++, and they arrive to us wrapped as an XPCOM
+ // object. We use wrappedJSObject to get at the underlying JS
+ // object.
+ if (args instanceof Ci.nsISupports) {
+ args = args.wrappedJSObject;
+ }
+
+ // We have to inform the container of this document that the
+ // MigrationWizard has changed size in order for it to update
+ // its dimensions too.
+ let observer = new ResizeObserver(() => {
+ args.onResize();
+ });
+ observer.observe(this._wiz);
+
+ let panelList = this._wiz.querySelector("panel-list");
+ let panel = document.createXULElement("panel");
+ panel.appendChild(panelList);
+ this._wiz.appendChild(panel);
+ this._wiz.requestState();
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "load": {
+ this.onLoad();
+ break;
+ }
+ case "keypress": {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ break;
+ }
+ case "MigrationWizard:Close": {
+ window.close();
+ break;
+ }
+ }
+ },
+};
+
+MigrationDialog.init();
diff --git a/browser/components/migration/content/migration-wizard-constants.mjs b/browser/components/migration/content/migration-wizard-constants.mjs
new file mode 100644
index 0000000000..30e1af3d79
--- /dev/null
+++ b/browser/components/migration/content/migration-wizard-constants.mjs
@@ -0,0 +1,76 @@
+/* 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/. */
+
+export const MigrationWizardConstants = Object.freeze({
+ MIGRATOR_TYPES: Object.freeze({
+ BROWSER: "browser",
+ FILE: "file",
+ }),
+
+ /**
+ * A mapping of a page identification string to the IDs used by the
+ * various wizard pages. These are used by MigrationWizard.setState
+ * to set the current page.
+ *
+ * @type {Object<string, string>}
+ */
+ PAGES: Object.freeze({
+ LOADING: "loading",
+ SELECTION: "selection",
+ PROGRESS: "progress",
+ FILE_IMPORT_PROGRESS: "file-import-progress",
+ SAFARI_PERMISSION: "safari-permission",
+ SAFARI_PASSWORD_PERMISSION: "safari-password-permission",
+ NO_BROWSERS_FOUND: "no-browsers-found",
+ }),
+
+ /**
+ * Returns a mapping of a resource type to a string used to identify
+ * the associated resource group in the wizard via a data-resource-type
+ * attribute. The keys are used to set which items should be shown and
+ * in what state in #onShowingProgress.
+ *
+ * @type {Object<string, string>}
+ */
+ DISPLAYED_RESOURCE_TYPES: Object.freeze({
+ // The DISPLAYED_RESOURCE_TYPES should have their keys match those
+ // in MigrationUtils.resourceTypes.
+
+ // This is a little silly, but JavaScript doesn't have a notion of
+ // enums. The advantage of this set-up is that these constants values
+ // can be used to access the MigrationUtils.resourceTypes constants,
+ // are reasonably readable as DOM attributes, and easily serialize /
+ // deserialize.
+ HISTORY: "HISTORY",
+ FORMDATA: "FORMDATA",
+ PASSWORDS: "PASSWORDS",
+ BOOKMARKS: "BOOKMARKS",
+ PAYMENT_METHODS: "PAYMENT_METHODS",
+
+ // We don't yet show OTHERDATA or SESSION resources.
+ }),
+
+ DISPLAYED_FILE_RESOURCE_TYPES: Object.freeze({
+ // When migrating passwords from a file, we first show the progress
+ // for a single PASSWORDS_FROM_FILE resource type, and then upon
+ // completion, show two different resource types - one for new
+ // passwords imported from the file, and one for existing passwords
+ // that were updated from the file.
+ PASSWORDS_FROM_FILE: "PASSWORDS_FROM_FILE",
+ PASSWORDS_NEW: "PASSWORDS_NEW",
+ PASSWORDS_UPDATED: "PASSWORDS_UPDATED",
+ BOOKMARKS_FROM_FILE: "BOOKMARKS_FROM_FILE",
+ }),
+
+ /**
+ * The set of keys that maps to migrators that use the term "favorites"
+ * in the place of "bookmarks". This tends to be browsers from Microsoft.
+ */
+ USES_FAVORITES: Object.freeze([
+ "chromium-edge",
+ "chromium-edge-beta",
+ "edge",
+ "ie",
+ ]),
+});
diff --git a/browser/components/migration/content/migration-wizard.mjs b/browser/components/migration/content/migration-wizard.mjs
new file mode 100644
index 0000000000..e1462ab9d1
--- /dev/null
+++ b/browser/components/migration/content/migration-wizard.mjs
@@ -0,0 +1,1088 @@
+/* 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/. */
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button-group.mjs";
+import { MigrationWizardConstants } from "chrome://browser/content/migration/migration-wizard-constants.mjs";
+
+/**
+ * This component contains the UI that steps users through migrating their
+ * data from other browsers to this one. This component only contains very
+ * basic logic and structure for the UI, and most of the state management
+ * occurs in the MigrationWizardChild JSWindowActor.
+ */
+export class MigrationWizard extends HTMLElement {
+ static #template = null;
+
+ #deck = null;
+ #browserProfileSelector = null;
+ #browserProfileSelectorList = null;
+ #resourceTypeList = null;
+ #shadowRoot = null;
+ #importButton = null;
+ #importFromFileButton = null;
+ #chooseImportFromFile = null;
+ #safariPermissionButton = null;
+ #safariPasswordImportSkipButton = null;
+ #safariPasswordImportSelectButton = null;
+ #selectAllCheckbox = null;
+ #resourceSummary = null;
+ #expandedDetails = false;
+
+ static get markup() {
+ return `
+ <template>
+ <link rel="stylesheet" href="chrome://browser/skin/migration/migration-wizard.css">
+ <named-deck id="wizard-deck" selected-view="page-loading" aria-busy="true" part="deck">
+ <div name="page-loading">
+ <h1 data-l10n-id="migration-wizard-selection-header"></h1>
+ <div class="loading-block large"></div>
+ <div class="loading-block small"></div>
+ <div class="loading-block small"></div>
+ <moz-button-group class="buttons" part="buttons">
+ <!-- If possible, use the same button labels as the SELECTION page with the same strings.
+ That'll prevent flicker when the load state exits if we then enter the SELECTION page. -->
+ <button class="cancel-close" data-l10n-id="migration-cancel-button-label" disabled></button>
+ <button data-l10n-id="migration-import-button-label" disabled></button>
+ </moz-button-group>
+ </div>
+
+ <div name="page-selection">
+ <h1 data-l10n-id="migration-wizard-selection-header"></h1>
+ <button id="browser-profile-selector" aria-haspopup="menu" aria-labelledby="migrator-name profile-name">
+ <span class="migrator-icon" role="img"></span>
+ <div class="migrator-description" role="presentation">
+ <div id="migrator-name">&nbsp;</div>
+ <div id="profile-name" class="deemphasized-text"></div>
+ </div>
+ <span class="dropdown-icon" role="img"></span>
+ </button>
+ <div class="no-resources-found error-message">
+ <span class="error-icon" role="img"></span>
+ <div data-l10n-id="migration-wizard-import-browser-no-resources"></div>
+ </div>
+ <div data-l10n-id="migration-wizard-selection-list" class="resource-selection-preamble deemphasized-text hide-on-error"></div>
+ <details class="resource-selection-details hide-on-error">
+ <summary id="resource-selection-summary">
+ <div class="selected-data-header" data-l10n-id="migration-all-available-data-label"></div>
+ <div class="selected-data deemphasized-text">&nbsp;</div>
+ <span class="expand-collapse-icon" role="img"></span>
+ </summary>
+ <fieldset id="resource-type-list">
+ <label id="select-all">
+ <input type="checkbox" class="select-all-checkbox"/><span data-l10n-id="migration-select-all-option-label"></span>
+ </label>
+ <label id="bookmarks" data-resource-type="BOOKMARKS"/>
+ <input type="checkbox"/><span default-data-l10n-id="migration-bookmarks-option-label" ie-edge-data-l10n-id="migration-favorites-option-label"></span>
+ </label>
+ <label id="logins-and-passwords" data-resource-type="PASSWORDS">
+ <input type="checkbox"/><span data-l10n-id="migration-logins-and-passwords-option-label"></span>
+ </label>
+ <label id="history" data-resource-type="HISTORY">
+ <input type="checkbox"/><span data-l10n-id="migration-history-option-label"></span>
+ </label>
+ <label id="form-autofill" data-resource-type="FORMDATA">
+ <input type="checkbox"/><span data-l10n-id="migration-form-autofill-option-label"></span>
+ </label>
+ <label id="payment-methods" data-resource-type="PAYMENT_METHODS">
+ <input type="checkbox"/><span data-l10n-id="migration-payment-methods-option-label"></span>
+ </label>
+ </fieldset>
+ </details>
+
+ <moz-button-group class="buttons" part="buttons">
+ <button class="cancel-close" data-l10n-id="migration-cancel-button-label"></button>
+ <button id="import-from-file" class="primary" data-l10n-id="migration-import-from-file-button-label"></button>
+ <button id="import" class="primary" data-l10n-id="migration-import-button-label"></button>
+ </moz-button-group>
+ </div>
+
+ <div name="page-progress">
+ <h1 id="progress-header" data-l10n-id="migration-wizard-progress-header"></h1>
+ <div class="resource-progress">
+ <div data-resource-type="BOOKMARKS" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span default-data-l10n-id="migration-bookmarks-option-label" ie-edge-data-l10n-id="migration-favorites-option-label"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="PASSWORDS" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-logins-and-passwords-option-label"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="HISTORY" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-history-option-label"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="FORMDATA" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-form-autofill-option-label"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="PAYMENT_METHODS" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-payment-methods-option-label"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+ </div>
+ <moz-button-group class="buttons" part="buttons">
+ <button class="cancel-close" data-l10n-id="migration-cancel-button-label" disabled></button>
+ <button class="primary finish-button done-button" data-l10n-id="migration-done-button-label"></button>
+ <button class="primary finish-button continue-button" data-l10n-id="migration-continue-button-label"></button>
+ </moz-button-group>
+ </div>
+
+ <div name="page-file-import-progress">
+ <h1 id="file-import-progress-header"></h1>
+ <div class="resource-progress">
+ <div data-resource-type="PASSWORDS_FROM_FILE" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-passwords-from-file"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="PASSWORDS_NEW" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-passwords-new"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="PASSWORDS_UPDATED" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-passwords-updated"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+
+ <div data-resource-type="BOOKMARKS_FROM_FILE" class="resource-progress-group">
+ <span class="progress-icon-parent"><span class="progress-icon" role="img"></span></span>
+ <span data-l10n-id="migration-bookmarks-from-file"></span>
+ <span class="success-text deemphasized-text">&nbsp;</span>
+ </div>
+ </div>
+ <moz-button-group class="buttons" part="buttons">
+ <button class="cancel-close" data-l10n-id="migration-cancel-button-label" disabled></button>
+ <button class="primary finish-button done-button" data-l10n-id="migration-done-button-label"></button>
+ <button class="primary finish-button continue-button" data-l10n-id="migration-continue-button-label"></button>
+ </moz-button-group>
+ </div>
+
+ <div name="page-safari-password-permission">
+ <h1 data-l10n-id="migration-safari-password-import-header"></h1>
+ <span data-l10n-id="migration-safari-password-import-steps-header"></span>
+ <ol>
+ <li data-l10n-id="migration-safari-password-import-step1"></li>
+ <li data-l10n-id="migration-safari-password-import-step2"><img class="safari-icon-3dots" data-l10n-name="safari-icon-3dots"/></li>
+ <li data-l10n-id="migration-safari-password-import-step3"></li>
+ <li class="safari-icons-group">
+ <span data-l10n-id="migration-safari-password-import-step4"></span>
+ <span class="page-portrait-icon"></span>
+ </li>
+ </ol>
+ <moz-button-group class="buttons" part="buttons">
+ <button id="safari-password-import-skip" data-l10n-id="migration-safari-password-import-skip-button"></button>
+ <button id="safari-password-import-select" class="primary" data-l10n-id="migration-safari-password-import-select-button"></button>
+ </moz-button-group>
+ </div>
+
+ <div name="page-safari-permission">
+ <h1 data-l10n-id="migration-wizard-selection-header"></h1>
+ <div data-l10n-id="migration-wizard-safari-permissions-sub-header"></div>
+ <ol>
+ <li data-l10n-id="migration-wizard-safari-instructions-continue"></li>
+ <li data-l10n-id="migration-wizard-safari-instructions-folder"></li>
+ </ol>
+ <moz-button-group class="buttons" part="buttons">
+ <button class="cancel-close" data-l10n-id="migration-cancel-button-label"></button>
+ <button id="safari-request-permissions" class="primary" data-l10n-id="migration-continue-button-label"></button>
+ </moz-button-group>
+ </div>
+
+ <div name="page-no-browsers-found">
+ <h1 data-l10n-id="migration-wizard-selection-header"></h1>
+ <div class="no-browsers-found error-message">
+ <span class="error-icon" role="img"></span>
+ <div class="no-browsers-found-message" data-l10n-id="migration-wizard-import-browser-no-browsers"></div>
+ </div>
+ <moz-button-group class="buttons" part="buttons">
+ <button class="cancel-close" data-l10n-id="migration-cancel-button-label"></button>
+ <button id="choose-import-from-file" class="primary" data-l10n-id="migration-choose-to-import-from-file-button-label"></button>
+ </moz-button-group>
+ </div>
+ </named-deck>
+ <slot></slot>
+ </template>
+ `;
+ }
+
+ static get fragment() {
+ if (!MigrationWizard.#template) {
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(MigrationWizard.markup, "text/html");
+ MigrationWizard.#template = document.importNode(
+ doc.querySelector("template"),
+ true
+ );
+ }
+ let fragment = MigrationWizard.#template.content.cloneNode(true);
+ if (window.IS_STORYBOOK) {
+ // If we're using Storybook, load the CSS from the static local file
+ // system rather than chrome:// to take advantage of auto-reloading.
+ fragment.querySelector("link[rel=stylesheet]").href =
+ "./migration/migration-wizard.css";
+ }
+ return fragment;
+ }
+
+ constructor() {
+ super();
+ const shadow = this.attachShadow({ mode: "closed" });
+
+ if (window.MozXULElement) {
+ window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
+ window.MozXULElement.insertFTLIfNeeded("browser/migrationWizard.ftl");
+ }
+ document.l10n.connectRoot(shadow);
+
+ shadow.appendChild(MigrationWizard.fragment);
+
+ this.#deck = shadow.querySelector("#wizard-deck");
+ this.#browserProfileSelector = shadow.querySelector(
+ "#browser-profile-selector"
+ );
+ this.#resourceSummary = shadow.querySelector("#resource-selection-summary");
+ this.#resourceSummary.addEventListener("click", this);
+
+ let cancelCloseButtons = shadow.querySelectorAll(".cancel-close");
+ for (let button of cancelCloseButtons) {
+ button.addEventListener("click", this);
+ }
+
+ let finishButtons = shadow.querySelectorAll(".finish-button");
+ for (let button of finishButtons) {
+ button.addEventListener("click", this);
+ }
+
+ this.#importButton = shadow.querySelector("#import");
+ this.#importButton.addEventListener("click", this);
+ this.#importFromFileButton = shadow.querySelector("#import-from-file");
+ this.#importFromFileButton.addEventListener("click", this);
+ this.#chooseImportFromFile = shadow.querySelector(
+ "#choose-import-from-file"
+ );
+ this.#chooseImportFromFile.addEventListener("click", this);
+
+ this.#browserProfileSelector.addEventListener("click", this);
+ this.#resourceTypeList = shadow.querySelector("#resource-type-list");
+ this.#resourceTypeList.addEventListener("change", this);
+
+ this.#safariPermissionButton = shadow.querySelector(
+ "#safari-request-permissions"
+ );
+ this.#safariPermissionButton.addEventListener("click", this);
+
+ this.#selectAllCheckbox = shadow.querySelector("#select-all").control;
+
+ this.#safariPasswordImportSkipButton = shadow.querySelector(
+ "#safari-password-import-skip"
+ );
+ this.#safariPasswordImportSkipButton.addEventListener("click", this);
+
+ this.#safariPasswordImportSelectButton = shadow.querySelector(
+ "#safari-password-import-select"
+ );
+ this.#safariPasswordImportSelectButton.addEventListener("click", this);
+
+ this.#shadowRoot = shadow;
+ }
+
+ connectedCallback() {
+ if (this.hasAttribute("auto-request-state")) {
+ this.requestState();
+ }
+ }
+
+ requestState() {
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:RequestState", { bubbles: true })
+ );
+ }
+
+ /**
+ * This setter can be used in the event that the MigrationWizard is being
+ * inserted via Lit, and the caller wants to set state declaratively using
+ * a property expression.
+ *
+ * @param {object} state
+ * The state object to pass to setState.
+ * @see MigrationWizard.setState.
+ */
+ set state(state) {
+ this.setState(state);
+ }
+
+ /**
+ * This is the main entrypoint for updating the state and appearance of
+ * the wizard.
+ *
+ * @param {object} state The state to be represented by the component.
+ * @param {string} state.page The page of the wizard to display. This should
+ * be one of the MigrationWizardConstants.PAGES constants.
+ */
+ setState(state) {
+ switch (state.page) {
+ case MigrationWizardConstants.PAGES.SELECTION: {
+ this.#onShowingSelection(state);
+ break;
+ }
+ case MigrationWizardConstants.PAGES.PROGRESS: {
+ this.#onShowingProgress(state);
+ break;
+ }
+ case MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS: {
+ this.#onShowingFileImportProgress(state);
+ break;
+ }
+ case MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND: {
+ this.#onShowingNoBrowsersFound(state);
+ break;
+ }
+ }
+
+ this.#deck.toggleAttribute(
+ "aria-busy",
+ state.page == MigrationWizardConstants.PAGES.LOADING
+ );
+ this.#deck.setAttribute("selected-view", `page-${state.page}`);
+
+ if (window.IS_STORYBOOK) {
+ this.#updateForStorybook();
+ }
+ }
+
+ get #dialogMode() {
+ return this.hasAttribute("dialog-mode");
+ }
+
+ #ensureSelectionDropdown() {
+ if (this.#browserProfileSelectorList) {
+ return;
+ }
+ this.#browserProfileSelectorList = this.querySelector("panel-list");
+ if (!this.#browserProfileSelectorList) {
+ throw new Error(
+ "Could not find a <panel-list> under the MigrationWizard during initialization."
+ );
+ }
+ this.#browserProfileSelectorList.toggleAttribute(
+ "min-width-from-anchor",
+ true
+ );
+ this.#browserProfileSelectorList.addEventListener("click", this);
+ // Until bug 1823489 is fixed, this is the easiest way for the
+ // migration wizard to style the selector dropdown so that it more
+ // closely lines up with the edges of the selector button.
+ this.#browserProfileSelectorList.style.boxSizing = "border-box";
+ this.#browserProfileSelectorList.style.overflowY = "auto";
+ }
+
+ /**
+ * Reacts to changes to the browser / profile selector dropdown. This
+ * should update the list of resource types to match what's supported
+ * by the selected migrator and profile.
+ *
+ * @param {Element} panelItem the selected <panel-item>
+ */
+ #onBrowserProfileSelectionChanged(panelItem) {
+ this.#browserProfileSelector.selectedPanelItem = panelItem;
+ this.#browserProfileSelector.querySelector("#migrator-name").textContent =
+ panelItem.displayName;
+ this.#browserProfileSelector.querySelector("#profile-name").textContent =
+ panelItem.profile?.name || "";
+
+ if (panelItem.brandImage) {
+ this.#browserProfileSelector.querySelector(
+ ".migrator-icon"
+ ).style.content = `url(${panelItem.brandImage})`;
+ } else {
+ this.#browserProfileSelector.querySelector(
+ ".migrator-icon"
+ ).style.content = "url(chrome://global/skin/icons/defaultFavicon.svg)";
+ }
+
+ let key = panelItem.getAttribute("key");
+ let resourceTypes = panelItem.resourceTypes;
+
+ for (let child of this.#resourceTypeList.querySelectorAll(
+ "label[data-resource-type]"
+ )) {
+ child.hidden = true;
+ child.control.checked = false;
+ }
+
+ for (let resourceType of resourceTypes) {
+ let resourceLabel = this.#resourceTypeList.querySelector(
+ `label[data-resource-type="${resourceType}"]`
+ );
+ if (resourceLabel) {
+ resourceLabel.hidden = false;
+ resourceLabel.control.checked = true;
+
+ let labelSpan = resourceLabel.querySelector(
+ "span[default-data-l10n-id]"
+ );
+ if (labelSpan) {
+ if (MigrationWizardConstants.USES_FAVORITES.includes(key)) {
+ document.l10n.setAttributes(
+ labelSpan,
+ labelSpan.getAttribute("ie-edge-data-l10n-id")
+ );
+ } else {
+ document.l10n.setAttributes(
+ labelSpan,
+ labelSpan.getAttribute("default-data-l10n-id")
+ );
+ }
+ }
+ }
+ }
+ let selectAll = this.#shadowRoot.querySelector("#select-all").control;
+ selectAll.checked = true;
+
+ this.#displaySelectedResources();
+ this.#browserProfileSelector.selectedPanelItem = panelItem;
+
+ let selectionPage = this.#shadowRoot.querySelector(
+ "div[name='page-selection']"
+ );
+ selectionPage.setAttribute("migrator-type", panelItem.getAttribute("type"));
+ selectionPage.toggleAttribute(
+ "no-resources",
+ panelItem.getAttribute("type") ==
+ MigrationWizardConstants.MIGRATOR_TYPES.BROWSER && !resourceTypes.length
+ );
+ }
+
+ /**
+ * Called when showing the browser/profile selection page of the wizard.
+ *
+ * @param {object} state
+ * The state object passed into setState. The following properties are
+ * used:
+ * @param {string[]} state.migrators An array of source browser names that
+ * can be migrated from.
+ */
+ #onShowingSelection(state) {
+ this.#ensureSelectionDropdown();
+ this.#browserProfileSelectorList.textContent = "";
+
+ let selectionPage = this.#shadowRoot.querySelector(
+ "div[name='page-selection']"
+ );
+
+ let details = this.#shadowRoot.querySelector("details");
+ selectionPage.toggleAttribute("show-import-all", state.showImportAll);
+ details.open = !state.showImportAll;
+
+ this.#expandedDetails = false;
+
+ for (let migrator of state.migrators) {
+ let opt = document.createElement("panel-item");
+ opt.setAttribute("key", migrator.key);
+ opt.setAttribute("type", migrator.type);
+ opt.profile = migrator.profile;
+ opt.displayName = migrator.displayName;
+ opt.resourceTypes = migrator.resourceTypes;
+ opt.hasPermissions = migrator.hasPermissions;
+ opt.brandImage = migrator.brandImage;
+
+ // Bug 1823489 - since the panel-list and panel-items are slotted, we
+ // cannot style them directly from migration-wizard.css. We use inline
+ // styles for now to achieve the desired appearance, but bug 1823489
+ // will investigate having MigrationWizard own the <xul:panel>,
+ // <panel-list> and <panel-item>'s so that styling can be done in the
+ // stylesheet instead.
+ let button = opt.shadowRoot.querySelector("button");
+ button.style.minHeight = "40px";
+ if (migrator.brandImage) {
+ button.style.backgroundImage = `url(${migrator.brandImage})`;
+ } else {
+ button.style.backgroundImage = `url("chrome://global/skin/icons/defaultFavicon.svg")`;
+ }
+
+ // Bug 1823489 - since the panel-list and panel-items are slotted, we
+ // cannot style them or their children in migration-wizard.css. We use
+ // inline styles for now to achieve the desired appearance, but bug 1823489
+ // will investigate having MigrationWizard own the <xul:panel>,
+ // <panel-list> and <panel-item>'s so that styling can be done in the
+ // stylesheet instead.
+ if (migrator.type == MigrationWizardConstants.MIGRATOR_TYPES.FILE) {
+ button.style.backgroundSize = "20px";
+ button.style.backgroundPosition = "6px center";
+ if (this.#browserProfileSelectorList.isDocumentRTL()) {
+ button.style.backgroundPositionX = "right 6px";
+ }
+ }
+
+ if (migrator.profile) {
+ document.l10n.setAttributes(
+ opt,
+ "migration-wizard-selection-option-with-profile",
+ {
+ sourceBrowser: migrator.displayName,
+ profileName: migrator.profile.name,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ opt,
+ "migration-wizard-selection-option-without-profile",
+ {
+ sourceBrowser: migrator.displayName,
+ }
+ );
+ }
+
+ this.#browserProfileSelectorList.appendChild(opt);
+ }
+
+ if (state.migrators.length) {
+ this.#onBrowserProfileSelectionChanged(
+ this.#browserProfileSelectorList.firstElementChild
+ );
+ }
+
+ // Since this is called before the named-deck actually switches to
+ // show the selection page, we cannot focus this button immediately.
+ // Instead, we use a rAF to queue this up for focusing before the
+ // next paint.
+ requestAnimationFrame(() => {
+ this.#browserProfileSelector.focus({ focusVisible: false });
+ });
+ }
+
+ /**
+ * @typedef {object} ProgressState
+ * The migration progress state for a resource.
+ * @property {boolean} inProgress
+ * True if progress is still underway.
+ * @property {string} [message=undefined]
+ * An optional message to display underneath the resource in
+ * the progress dialog. This message is only shown when inProgress
+ * is `false`.
+ */
+
+ /**
+ * Called when showing the progress / success page of the wizard.
+ *
+ * @param {object} state
+ * The state object passed into setState. The following properties are
+ * used:
+ * @param {string} state.key
+ * The key of the migrator being used.
+ * @param {Object<string, ProgressState>} state.progress
+ * An object whose keys match one of DISPLAYED_RESOURCE_TYPES.
+ *
+ * Any resource type not included in state.progress will be hidden.
+ */
+ #onShowingProgress(state) {
+ // Any resource progress group not included in state.progress is hidden.
+ let progressPage = this.#shadowRoot.querySelector(
+ "div[name='page-progress']"
+ );
+ let resourceGroups = progressPage.querySelectorAll(
+ ".resource-progress-group"
+ );
+ let totalProgressGroups = Object.keys(state.progress).length;
+ let remainingProgressGroups = totalProgressGroups;
+
+ for (let group of resourceGroups) {
+ let resourceType = group.dataset.resourceType;
+ if (!state.progress.hasOwnProperty(resourceType)) {
+ group.hidden = true;
+ continue;
+ }
+ group.hidden = false;
+
+ let progressIcon = group.querySelector(".progress-icon");
+ let successText = group.querySelector(".success-text");
+
+ let labelSpan = group.querySelector("span[default-data-l10n-id]");
+ if (labelSpan) {
+ if (MigrationWizardConstants.USES_FAVORITES.includes(state.key)) {
+ document.l10n.setAttributes(
+ labelSpan,
+ labelSpan.getAttribute("ie-edge-data-l10n-id")
+ );
+ } else {
+ document.l10n.setAttributes(
+ labelSpan,
+ labelSpan.getAttribute("default-data-l10n-id")
+ );
+ }
+ }
+
+ if (state.progress[resourceType].inProgress) {
+ document.l10n.setAttributes(
+ progressIcon,
+ "migration-wizard-progress-icon-in-progress"
+ );
+ progressIcon.classList.remove("completed");
+ successText.textContent = "";
+ // With no status text, we re-insert the &nbsp; so that the status
+ // text area does not fully collapse.
+ successText.appendChild(document.createTextNode("\u00A0"));
+ } else {
+ document.l10n.setAttributes(
+ progressIcon,
+ "migration-wizard-progress-icon-completed"
+ );
+ progressIcon.classList.add("completed");
+ successText.textContent = state.progress[resourceType].message;
+ remainingProgressGroups--;
+ }
+ }
+
+ let migrationDone = remainingProgressGroups == 0;
+ let headerL10nID = migrationDone
+ ? "migration-wizard-progress-done-header"
+ : "migration-wizard-progress-header";
+ let header = this.#shadowRoot.getElementById("progress-header");
+ document.l10n.setAttributes(header, headerL10nID);
+
+ let finishButtons = progressPage.querySelectorAll(".finish-button");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+
+ for (let finishButton of finishButtons) {
+ finishButton.hidden = !migrationDone;
+ }
+
+ cancelButton.hidden = migrationDone;
+
+ if (migrationDone) {
+ // Since this might be called before the named-deck actually switches to
+ // show the progress page, we cannot focus this button immediately.
+ // Instead, we use a rAF to queue this up for focusing before the
+ // next paint.
+ requestAnimationFrame(() => {
+ let button = this.#dialogMode
+ ? progressPage.querySelector(".done-button")
+ : progressPage.querySelector(".continue-button");
+ button.focus({ focusVisible: false });
+ });
+ }
+ }
+
+ /**
+ * Called when showing the progress / success page of the wizard for
+ * files.
+ *
+ * @param {object} state
+ * The state object passed into setState. The following properties are
+ * used:
+ * @param {string} state.title
+ * The string to display in the header.
+ * @param {Object<string, ProgressState>} state.progress
+ * An object whose keys match one of DISPLAYED_FILE_RESOURCE_TYPES.
+ *
+ * Any resource type not included in state.progress will be hidden.
+ */
+ #onShowingFileImportProgress(state) {
+ // Any resource progress group not included in state.progress is hidden.
+ let progressPage = this.#shadowRoot.querySelector(
+ "div[name='page-file-import-progress']"
+ );
+ let resourceGroups = progressPage.querySelectorAll(
+ ".resource-progress-group"
+ );
+ let totalProgressGroups = Object.keys(state.progress).length;
+ let remainingProgressGroups = totalProgressGroups;
+
+ for (let group of resourceGroups) {
+ let resourceType = group.dataset.resourceType;
+ if (!state.progress.hasOwnProperty(resourceType)) {
+ group.hidden = true;
+ continue;
+ }
+ group.hidden = false;
+
+ let progressIcon = group.querySelector(".progress-icon");
+ let successText = group.querySelector(".success-text");
+
+ if (state.progress[resourceType].inProgress) {
+ document.l10n.setAttributes(
+ progressIcon,
+ "migration-wizard-progress-icon-in-progress"
+ );
+ progressIcon.classList.remove("completed");
+ successText.textContent = "";
+ // With no status text, we re-insert the &nbsp; so that the status
+ // text area does not fully collapse.
+ successText.appendChild(document.createTextNode("\u00A0"));
+ } else {
+ document.l10n.setAttributes(
+ progressIcon,
+ "migration-wizard-progress-icon-completed"
+ );
+ progressIcon.classList.add("completed");
+ successText.textContent = state.progress[resourceType].message;
+ remainingProgressGroups--;
+ }
+ }
+
+ let migrationDone = remainingProgressGroups == 0;
+ let header = this.#shadowRoot.getElementById("file-import-progress-header");
+ header.textContent = state.title;
+
+ let doneButton = progressPage.querySelector(".primary");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ doneButton.hidden = !migrationDone;
+ cancelButton.hidden = migrationDone;
+
+ if (migrationDone) {
+ // Since this might be called before the named-deck actually switches to
+ // show the progress page, we cannot focus this button immediately.
+ // Instead, we use a rAF to queue this up for focusing before the
+ // next paint.
+ requestAnimationFrame(() => {
+ doneButton.focus({ focusVisible: false });
+ });
+ }
+ }
+
+ /**
+ * Called when showing the "no browsers found" page of the wizard.
+ *
+ * @param {object} state
+ * The state object passed into setState. The following properties are
+ * used:
+ * @param {string} state.hasFileMigrators
+ * True if at least one FileMigrator is available for use.
+ */
+ #onShowingNoBrowsersFound(state) {
+ this.#chooseImportFromFile.hidden = !state.hasFileMigrators;
+ }
+
+ /**
+ * Certain parts of the MigrationWizard need to be modified slightly
+ * in order to work properly with Storybook. This method should be called
+ * to apply those changes after changing state.
+ */
+ #updateForStorybook() {
+ // The CSS mask used for the progress spinner cannot be loaded via
+ // chrome:// URIs in Storybook. We work around this by exposing the
+ // progress elements as custom parts that the MigrationWizard story
+ // can style on its own.
+ this.#shadowRoot.querySelectorAll(".progress-icon").forEach(progressEl => {
+ if (progressEl.classList.contains("completed")) {
+ progressEl.removeAttribute("part");
+ } else {
+ progressEl.setAttribute("part", "progress-spinner");
+ }
+ });
+ }
+
+ /**
+ * Takes the current state of the selections page and bundles them
+ * up into a MigrationWizard:BeginMigration event that can be handled
+ * externally to perform the actual migration.
+ */
+ #doImport() {
+ let migrationEventDetail = this.#gatherMigrationEventDetails();
+
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:BeginMigration", {
+ bubbles: true,
+ detail: migrationEventDetail,
+ })
+ );
+ }
+
+ /**
+ * @typedef {object} MigrationDetails
+ * @property {string} key
+ * The key for a MigratorBase subclass.
+ * @property {object|null} profile
+ * A representation of a browser profile. This is serialized and originally
+ * sent down from the parent via the GetAvailableMigrators message.
+ * @property {string[]} resourceTypes
+ * An array of resource types that the user is attempted to import. These
+ * strings should be from MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
+ * @property {boolean} hasPermissions
+ * True if this MigrationWizardChild told us that the associated
+ * MigratorBase subclass for the key has enough permission to read
+ * the requested resources.
+ * @property {boolean} expandedDetails
+ * True if the user clicked on the <summary> element to expand the resource
+ * type list.
+ */
+
+ /**
+ * Pulls information from the DOM state of the MigrationWizard and constructs
+ * and returns an object that can be used to begin migration via and event
+ * sent to the MigrationWizardChild.
+ *
+ * @returns {MigrationDetails} details
+ */
+ #gatherMigrationEventDetails() {
+ let panelItem = this.#browserProfileSelector.selectedPanelItem;
+ let key = panelItem.getAttribute("key");
+ let type = panelItem.getAttribute("type");
+ let profile = panelItem.profile;
+ let hasPermissions = panelItem.hasPermissions;
+
+ let resourceTypeFields = this.#resourceTypeList.querySelectorAll(
+ "label[data-resource-type]"
+ );
+ let resourceTypes = [];
+ for (let resourceTypeField of resourceTypeFields) {
+ if (resourceTypeField.control.checked) {
+ resourceTypes.push(resourceTypeField.dataset.resourceType);
+ }
+ }
+
+ return {
+ key,
+ type,
+ profile,
+ resourceTypes,
+ hasPermissions,
+ expandedDetails: this.#expandedDetails,
+ };
+ }
+
+ /**
+ * Sends a request to gain read access to the Safari profile folder on
+ * macOS, and upon gaining access, performs a migration using the current
+ * settings as gathered by #gatherMigrationEventDetails
+ */
+ #requestSafariPermissions() {
+ let migrationEventDetail = this.#gatherMigrationEventDetails();
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:RequestSafariPermissions", {
+ bubbles: true,
+ detail: migrationEventDetail,
+ })
+ );
+ }
+
+ /**
+ * Sends a request to get a string path for a passwords file exported
+ * from Safari.
+ */
+ #selectSafariPasswordFile() {
+ let migrationEventDetail = this.#gatherMigrationEventDetails();
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:SelectSafariPasswordFile", {
+ bubbles: true,
+ detail: migrationEventDetail,
+ })
+ );
+ }
+
+ /**
+ * Changes selected-data-header text and selected-data text based on
+ * how many resources are checked
+ */
+ async #displaySelectedResources() {
+ let resourceTypeLabels = this.#resourceTypeList.querySelectorAll(
+ "label:not([hidden])[data-resource-type]"
+ );
+ let panelItem = this.#browserProfileSelector.selectedPanelItem;
+ let key = panelItem.getAttribute("key");
+
+ let totalResources = resourceTypeLabels.length;
+ let checkedResources = 0;
+
+ let selectedData = this.#shadowRoot.querySelector(".selected-data");
+ let selectedDataArray = [];
+ let resourceTypeToLabelIDs = {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]:
+ "migration-list-bookmark-label",
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS]:
+ "migration-list-password-label",
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY]:
+ "migration-list-history-label",
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA]:
+ "migration-list-autofill-label",
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS]:
+ "migration-list-payment-methods-label",
+ };
+
+ if (MigrationWizardConstants.USES_FAVORITES.includes(key)) {
+ resourceTypeToLabelIDs[
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS
+ ] = "migration-list-favorites-label";
+ }
+
+ let resourceTypes = Object.keys(resourceTypeToLabelIDs);
+ let labelIds = Object.values(resourceTypeToLabelIDs).map(id => {
+ return { id };
+ });
+ let labels = await document.l10n.formatValues(labelIds);
+ let resourceTypeLabelMapping = new Map();
+ for (let i = 0; i < resourceTypes.length; ++i) {
+ let resourceType = resourceTypes[i];
+ resourceTypeLabelMapping.set(resourceType, labels[i]);
+ }
+ let formatter = new Intl.ListFormat(undefined, {
+ style: "long",
+ type: "conjunction",
+ });
+ for (let resourceTypeLabel of resourceTypeLabels) {
+ if (resourceTypeLabel.control.checked) {
+ selectedDataArray.push(
+ resourceTypeLabelMapping.get(resourceTypeLabel.dataset.resourceType)
+ );
+ checkedResources++;
+ }
+ }
+ if (selectedDataArray.length) {
+ selectedDataArray[0] =
+ selectedDataArray[0].charAt(0).toLocaleUpperCase() +
+ selectedDataArray[0].slice(1);
+ selectedData.textContent = formatter.format(selectedDataArray);
+ } else {
+ selectedData.textContent = "\u00A0";
+ }
+
+ let selectedDataHeader = this.#shadowRoot.querySelector(
+ ".selected-data-header"
+ );
+
+ let importButton = this.#shadowRoot.querySelector("#import");
+ importButton.disabled = checkedResources == 0;
+
+ if (checkedResources == 0) {
+ document.l10n.setAttributes(
+ selectedDataHeader,
+ "migration-no-selected-data-label"
+ );
+ } else if (checkedResources < totalResources) {
+ document.l10n.setAttributes(
+ selectedDataHeader,
+ "migration-selected-data-label"
+ );
+ } else {
+ document.l10n.setAttributes(
+ selectedDataHeader,
+ "migration-all-available-data-label"
+ );
+ }
+
+ let selectionPage = this.#shadowRoot.querySelector(
+ "div[name='page-selection']"
+ );
+ selectionPage.toggleAttribute("single-item", totalResources == 1);
+
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:ResourcesUpdated", { bubbles: true })
+ );
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "click": {
+ if (
+ event.target == this.#importButton ||
+ event.target == this.#importFromFileButton
+ ) {
+ this.#doImport();
+ } else if (
+ event.target.classList.contains("cancel-close") ||
+ event.target.classList.contains("finish-button")
+ ) {
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:Close", { bubbles: true })
+ );
+ } else if (event.target == this.#browserProfileSelector) {
+ this.#browserProfileSelectorList.show(event);
+ } else if (
+ event.currentTarget == this.#browserProfileSelectorList &&
+ event.target != this.#browserProfileSelectorList
+ ) {
+ this.#onBrowserProfileSelectionChanged(event.target);
+ // If the user selected a file migration type from the selector, we'll
+ // help the user out by immediately starting the file migration flow,
+ // rather than waiting for them to click the "Select File".
+ if (
+ event.target.getAttribute("type") ==
+ MigrationWizardConstants.MIGRATOR_TYPES.FILE
+ ) {
+ this.#doImport();
+ }
+ } else if (event.target == this.#safariPermissionButton) {
+ this.#requestSafariPermissions();
+ } else if (event.currentTarget == this.#resourceSummary) {
+ this.#expandedDetails = true;
+ } else if (event.target == this.#chooseImportFromFile) {
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:RequestState", {
+ bubbles: true,
+ detail: {
+ allowOnlyFileMigrators: true,
+ },
+ })
+ );
+ } else if (event.target == this.#safariPasswordImportSkipButton) {
+ // If the user chose to skip importing passwords from Safari, we
+ // programmatically uncheck the PASSWORDS resource type and re-request
+ // import.
+ let checkbox = this.#shadowRoot.querySelector(
+ `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]`
+ ).control;
+ checkbox.checked = false;
+
+ // If there are no other checked checkboxes, go back to the selection
+ // screen.
+ let checked = this.#shadowRoot.querySelectorAll(
+ `label[data-resource-type] > input:checked`
+ ).length;
+
+ if (!checked) {
+ this.requestState();
+ } else {
+ this.#doImport();
+ }
+ } else if (event.target == this.#safariPasswordImportSelectButton) {
+ this.#selectSafariPasswordFile();
+ }
+ break;
+ }
+ case "change": {
+ if (event.target == this.#browserProfileSelector) {
+ this.#onBrowserProfileSelectionChanged();
+ } else if (event.target == this.#selectAllCheckbox) {
+ let checkboxes = this.#shadowRoot.querySelectorAll(
+ 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]'
+ );
+ for (let checkbox of checkboxes) {
+ checkbox.checked = this.#selectAllCheckbox.checked;
+ }
+ this.#displaySelectedResources();
+ } else {
+ let checkboxes = this.#shadowRoot.querySelectorAll(
+ 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]'
+ );
+
+ let allVisibleChecked = Array.from(checkboxes).every(checkbox => {
+ return checkbox.checked;
+ });
+
+ this.#selectAllCheckbox.checked = allVisibleChecked;
+ this.#displaySelectedResources();
+ }
+ break;
+ }
+ }
+ }
+}
+
+if (globalThis.customElements) {
+ customElements.define("migration-wizard", MigrationWizard);
+}
diff --git a/browser/components/migration/content/migration.js b/browser/components/migration/content/migration.js
new file mode 100644
index 0000000000..bc27bb4c9d
--- /dev/null
+++ b/browser/components/migration/content/migration.js
@@ -0,0 +1,812 @@
+/* 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/. */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MigrationUtils.sys.mjs"
+);
+const { MigratorBase } = ChromeUtils.importESModule(
+ "resource:///modules/MigratorBase.sys.mjs"
+);
+
+/**
+ * Map from data types that match Ci.nsIBrowserProfileMigrator's types to
+ * prefixes for strings used to label these data types in the migration
+ * dialog. We use these strings with -checkbox and -label suffixes for the
+ * checkboxes on the "importItems" page, and for the labels on the "migrating"
+ * and "done" pages, respectively.
+ */
+const kDataToStringMap = new Map([
+ ["cookies", "browser-data-cookies"],
+ ["history", "browser-data-history"],
+ ["formdata", "browser-data-formdata"],
+ ["passwords", "browser-data-passwords"],
+ ["bookmarks", "browser-data-bookmarks"],
+ ["otherdata", "browser-data-otherdata"],
+ ["session", "browser-data-session"],
+ ["payment_methods", "browser-data-payment-methods"],
+]);
+
+var MigrationWizard = {
+ /* exported MigrationWizard */
+ _source: "", // Source Profile Migrator ContractID suffix
+ _itemsFlags: MigrationUtils.resourceTypes.ALL, // Selected Import Data Sources (16-bit bitfield)
+ _selectedProfile: null, // Selected Profile name to import from
+ _wiz: null,
+ _migrator: null,
+ _autoMigrate: null,
+ _receivedPermissions: new Set(),
+ _succeededMigrationEventArgs: null,
+
+ init() {
+ Services.telemetry.setEventRecordingEnabled("browser.migration", true);
+
+ let os = Services.obs;
+ os.addObserver(this, "Migration:Started");
+ os.addObserver(this, "Migration:ItemBeforeMigrate");
+ os.addObserver(this, "Migration:ItemAfterMigrate");
+ os.addObserver(this, "Migration:ItemError");
+ os.addObserver(this, "Migration:Ended");
+
+ this._wiz = document.querySelector("wizard");
+
+ let args = window.arguments[0]?.wrappedJSObject || {};
+ let entrypoint =
+ args.entrypoint || MigrationUtils.MIGRATION_ENTRYPOINTS.UNKNOWN;
+ Services.telemetry
+ .getHistogramById("FX_MIGRATION_ENTRY_POINT_CATEGORICAL")
+ .add(entrypoint);
+
+ // The legacy entrypoint Histogram wasn't categorical, so we translate to the right
+ // numeric value before writing it. We'll keep this Histogram around to ensure a
+ // smooth transition to the new FX_MIGRATION_ENTRY_POINT_CATEGORICAL categorical
+ // histogram.
+ let entryPointId = MigrationUtils.getLegacyMigrationEntrypoint(entrypoint);
+ Services.telemetry
+ .getHistogramById("FX_MIGRATION_ENTRY_POINT")
+ .add(entryPointId);
+
+ this.isInitialMigration =
+ entrypoint == MigrationUtils.MIGRATION_ENTRYPOINTS.FIRSTRUN;
+
+ // Record that the uninstaller requested a profile refresh
+ if (Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH")) {
+ Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", "");
+ Services.telemetry.scalarSet(
+ "migration.uninstaller_profile_refresh",
+ true
+ );
+ }
+
+ this._source = args.migratorKey;
+ this._migrator =
+ args.migrator instanceof MigratorBase ? args.migrator : null;
+ this._autoMigrate = !!args.isStartupMigration;
+ this._skipImportSourcePage = !!args.skipSourceSelection;
+
+ if (this._migrator && args.profileId) {
+ let sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+ this._selectedProfile = sourceProfiles.find(
+ profile => profile.id == args.profileId
+ );
+ }
+
+ if (this._autoMigrate) {
+ // Show the "nothing" option in the automigrate case to provide an
+ // easily identifiable way to avoid migration and create a new profile.
+ document.getElementById("nothing").hidden = false;
+ }
+
+ this._setSourceForDataLocalization();
+
+ document.addEventListener("wizardcancel", function () {
+ MigrationWizard.onWizardCancel();
+ });
+
+ document
+ .getElementById("selectProfile")
+ .addEventListener("pageshow", function () {
+ MigrationWizard.onSelectProfilePageShow();
+ });
+ document
+ .getElementById("importItems")
+ .addEventListener("pageshow", function () {
+ MigrationWizard.onImportItemsPageShow();
+ });
+ document
+ .getElementById("migrating")
+ .addEventListener("pageshow", function () {
+ MigrationWizard.onMigratingPageShow();
+ });
+ document.getElementById("done").addEventListener("pageshow", function () {
+ MigrationWizard.onDonePageShow();
+ });
+
+ document
+ .getElementById("selectProfile")
+ .addEventListener("pagerewound", function () {
+ MigrationWizard.onSelectProfilePageRewound();
+ });
+ document
+ .getElementById("importItems")
+ .addEventListener("pagerewound", function () {
+ MigrationWizard.onImportItemsPageRewound();
+ });
+
+ document
+ .getElementById("selectProfile")
+ .addEventListener("pageadvanced", function () {
+ MigrationWizard.onSelectProfilePageAdvanced();
+ });
+ document
+ .getElementById("importItems")
+ .addEventListener("pageadvanced", function () {
+ MigrationWizard.onImportItemsPageAdvanced();
+ });
+ document
+ .getElementById("importPermissions")
+ .addEventListener("pageadvanced", function (e) {
+ MigrationWizard.onImportPermissionsPageAdvanced(e);
+ });
+ document
+ .getElementById("importSource")
+ .addEventListener("pageadvanced", function (e) {
+ MigrationWizard.onImportSourcePageAdvanced(e);
+ });
+
+ this.recordEvent("opened");
+
+ this.onImportSourcePageShow();
+ },
+
+ uninit() {
+ var os = Services.obs;
+ os.removeObserver(this, "Migration:Started");
+ os.removeObserver(this, "Migration:ItemBeforeMigrate");
+ os.removeObserver(this, "Migration:ItemAfterMigrate");
+ os.removeObserver(this, "Migration:ItemError");
+ os.removeObserver(this, "Migration:Ended");
+ os.notifyObservers(this, "MigrationWizard:Destroyed");
+ MigrationUtils.finishMigration();
+ },
+
+ /**
+ * Used for recording telemetry in the migration wizard.
+ *
+ * @param {string} type
+ * The type of event being recorded.
+ * @param {object} args
+ * The data to pass to telemetry when the event is recorded.
+ */
+ recordEvent(type, args = null) {
+ Services.telemetry.recordEvent(
+ "browser.migration",
+ type,
+ "legacy_wizard",
+ null,
+ args
+ );
+ },
+
+ spinResolve(promise) {
+ let canAdvance = this._wiz.canAdvance;
+ let canRewind = this._wiz.canRewind;
+ this._wiz.canAdvance = false;
+ this._wiz.canRewind = false;
+ let result = MigrationUtils.spinResolve(promise);
+ this._wiz.canAdvance = canAdvance;
+ this._wiz.canRewind = canRewind;
+ return result;
+ },
+
+ _setSourceForDataLocalization() {
+ this._sourceForDataLocalization = this._source;
+ // Ensure consistency for various channels, brandings and versions of
+ // Chromium and MS Edge.
+ if (this._sourceForDataLocalization) {
+ this._sourceForDataLocalization = this._sourceForDataLocalization
+ .replace(/^(chromium-edge-beta|chromium-edge)$/, "edge")
+ .replace(/^(canary|chromium|chrome-beta|chrome-dev)$/, "chrome");
+ }
+ },
+
+ onWizardCancel() {
+ MigrationUtils.forceExitSpinResolve();
+ return true;
+ },
+
+ // 1 - Import Source
+ onImportSourcePageShow() {
+ this._wiz.canRewind = false;
+
+ var selectedMigrator = null;
+ this._availableMigrators = [];
+
+ // Figure out what source apps are are available to import from:
+ var group = document.getElementById("importSourceGroup");
+ for (var i = 0; i < group.childNodes.length; ++i) {
+ var migratorKey = group.childNodes[i].id;
+ if (migratorKey != "nothing") {
+ var migrator = this.spinResolve(
+ MigrationUtils.getMigrator(migratorKey)
+ );
+
+ if (migrator?.enabled) {
+ // Save this as the first selectable item, if we don't already have
+ // one, or if it is the migrator that was passed to us.
+ if (!selectedMigrator || this._source == migratorKey) {
+ selectedMigrator = group.childNodes[i];
+ }
+
+ let profiles = this.spinResolve(migrator.getSourceProfiles());
+ if (profiles?.length) {
+ Services.telemetry.keyedScalarAdd(
+ "migration.discovered_migrators",
+ migratorKey,
+ profiles.length
+ );
+ } else {
+ Services.telemetry.keyedScalarAdd(
+ "migration.discovered_migrators",
+ migratorKey,
+ 1
+ );
+ }
+
+ this._availableMigrators.push([migratorKey, migrator]);
+ } else {
+ // Hide this option
+ group.childNodes[i].hidden = true;
+ }
+ }
+ }
+ if (this.isInitialMigration) {
+ Services.telemetry
+ .getHistogramById("FX_STARTUP_MIGRATION_BROWSER_COUNT")
+ .add(this._availableMigrators.length);
+ let defaultBrowser = MigrationUtils.getMigratorKeyForDefaultBrowser();
+ // This will record 0 for unknown default browser IDs.
+ defaultBrowser = MigrationUtils.getSourceIdForTelemetry(defaultBrowser);
+ Services.telemetry
+ .getHistogramById("FX_STARTUP_MIGRATION_EXISTING_DEFAULT_BROWSER")
+ .add(defaultBrowser);
+ }
+
+ if (selectedMigrator) {
+ group.selectedItem = selectedMigrator;
+ } else {
+ this.recordEvent("no_browsers_found");
+ // We didn't find a migrator, notify the user
+ document.getElementById("noSources").hidden = false;
+
+ this._wiz.canAdvance = false;
+
+ document.getElementById("importAll").hidden = true;
+ }
+
+ // Advance to the next page if the caller told us to.
+ if (this._migrator && this._skipImportSourcePage) {
+ this._wiz.advance();
+ this._wiz.canRewind = false;
+ }
+ },
+
+ onImportSourcePageAdvanced(event) {
+ var newSource =
+ document.getElementById("importSourceGroup").selectedItem.id;
+
+ this.recordEvent("browser_selected", { migrator_key: newSource });
+
+ if (newSource == "nothing") {
+ // Need to do telemetry here because we're closing the dialog before we get to
+ // do actual migration. For actual migration, this doesn't happen until after
+ // migration takes place.
+ Services.telemetry
+ .getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
+ .add(MigrationUtils.getSourceIdForTelemetry("nothing"));
+ this._wiz.cancel();
+ event.preventDefault();
+ }
+
+ if (!this._migrator || newSource != this._source) {
+ // Create the migrator for the selected source.
+ this._migrator = this.spinResolve(MigrationUtils.getMigrator(newSource));
+
+ this._itemsFlags = MigrationUtils.resourceTypes.ALL;
+ this._selectedProfile = null;
+ }
+ this._source = newSource;
+ this._setSourceForDataLocalization();
+
+ // check for more than one source profile
+ var sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+ if (this._skipImportSourcePage) {
+ this._updateNextPageForPermissions();
+ } else if (sourceProfiles && sourceProfiles.length > 1) {
+ this._wiz.currentPage.next = "selectProfile";
+ } else {
+ if (this._autoMigrate) {
+ this._updateNextPageForPermissions();
+ } else {
+ this._wiz.currentPage.next = "importItems";
+ }
+
+ if (sourceProfiles && sourceProfiles.length == 1) {
+ this._selectedProfile = sourceProfiles[0];
+ } else {
+ this._selectedProfile = null;
+ }
+ }
+ },
+
+ // 2 - [Profile Selection]
+ onSelectProfilePageShow() {
+ // Disabling this for now, since we ask about import sources in automigration
+ // too and don't want to disable the back button
+ // if (this._autoMigrate)
+ // document.documentElement.getButton("back").disabled = true;
+
+ var profiles = document.getElementById("profiles");
+ while (profiles.hasChildNodes()) {
+ profiles.firstChild.remove();
+ }
+
+ // Note that this block is still reached even if the user chose 'From File'
+ // and we canceled the dialog. When that happens, _migrator will be null.
+ if (this._migrator) {
+ var sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+
+ for (let profile of sourceProfiles) {
+ var item = document.createXULElement("radio");
+ item.id = profile.id;
+ item.setAttribute("label", profile.name);
+ profiles.appendChild(item);
+ }
+ }
+
+ profiles.selectedItem = this._selectedProfile
+ ? document.getElementById(this._selectedProfile.id)
+ : profiles.firstChild;
+ },
+
+ onSelectProfilePageRewound() {
+ var profiles = document.getElementById("profiles");
+ let sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+ this._selectedProfile =
+ sourceProfiles.find(profile => profile.id == profiles.selectedItem.id) ||
+ null;
+ },
+
+ onSelectProfilePageAdvanced() {
+ this.recordEvent("profile_selected", {
+ migrator_key: this._source,
+ });
+ var profiles = document.getElementById("profiles");
+ let sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+ this._selectedProfile =
+ sourceProfiles.find(profile => profile.id == profiles.selectedItem.id) ||
+ null;
+
+ // If we're automigrating or just doing bookmarks don't show the item selection page
+ if (this._autoMigrate) {
+ this._updateNextPageForPermissions();
+ }
+ },
+
+ // 3 - ImportItems
+ onImportItemsPageShow() {
+ var dataSources = document.getElementById("dataSources");
+ while (dataSources.hasChildNodes()) {
+ dataSources.firstChild.remove();
+ }
+
+ var items = this.spinResolve(
+ this._migrator.getMigrateData(this._selectedProfile)
+ );
+
+ for (let itemType of kDataToStringMap.keys()) {
+ let itemValue = MigrationUtils.resourceTypes[itemType.toUpperCase()];
+ if (items & itemValue) {
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.id = itemValue;
+ checkbox.setAttribute("native", true);
+ document.l10n.setAttributes(
+ checkbox,
+ kDataToStringMap.get(itemType) + "-checkbox",
+ { browser: this._sourceForDataLocalization }
+ );
+ dataSources.appendChild(checkbox);
+ if (!this._itemsFlags || this._itemsFlags & itemValue) {
+ checkbox.checked = true;
+ }
+ }
+ }
+ },
+
+ onImportItemsPageRewound() {
+ this._wiz.canAdvance = true;
+ this.onImportItemsPageAdvanced(true /* viaRewind */);
+ },
+
+ onImportItemsPageAdvanced(viaRewind = false) {
+ let extraKeys = {
+ migrator_key: this._source,
+ history: "0",
+ formdata: "0",
+ passwords: "0",
+ bookmarks: "0",
+ payment_methods: "0",
+
+ // "other" will get incremented, so we keep this as a number for
+ // now, and will cast to a string before submitting to Event telemetry.
+ other: 0,
+
+ configured: "0",
+ };
+
+ var dataSources = document.getElementById("dataSources");
+ this._itemsFlags = 0;
+
+ for (var i = 0; i < dataSources.childNodes.length; ++i) {
+ var checkbox = dataSources.childNodes[i];
+ if (checkbox.localName == "checkbox" && checkbox.checked) {
+ let flag = parseInt(checkbox.id);
+
+ switch (flag) {
+ case MigrationUtils.resourceTypes.HISTORY:
+ extraKeys.history = "1";
+ break;
+ case MigrationUtils.resourceTypes.FORMDATA:
+ extraKeys.formdata = "1";
+ break;
+ case MigrationUtils.resourceTypes.PASSWORDS:
+ extraKeys.passwords = "1";
+ break;
+ case MigrationUtils.resourceTypes.BOOKMARKS:
+ extraKeys.bookmarks = "1";
+ break;
+ case MigrationUtils.resourceTypes.PAYMENT_METHODS:
+ extraKeys.payment_methods = "1";
+ break;
+ default:
+ extraKeys.other++;
+ }
+
+ this._itemsFlags |= parseInt(checkbox.id);
+ }
+ }
+
+ extraKeys.other = String(extraKeys.other);
+
+ if (!viaRewind) {
+ this.recordEvent("resources_selected", extraKeys);
+ }
+
+ this._updateNextPageForPermissions();
+ },
+
+ onImportItemCommand() {
+ var items = document.getElementById("dataSources");
+ var checkboxes = items.getElementsByTagName("checkbox");
+
+ var oneChecked = false;
+ for (var i = 0; i < checkboxes.length; ++i) {
+ if (checkboxes[i].checked) {
+ oneChecked = true;
+ break;
+ }
+ }
+
+ this._wiz.canAdvance = oneChecked;
+
+ this._updateNextPageForPermissions();
+ },
+
+ _updateNextPageForPermissions() {
+ // We would like to just go straight to work:
+ this._wiz.currentPage.next = "migrating";
+ // If we already have permissions, this is easy:
+ if (this._receivedPermissions.has(this._source)) {
+ return;
+ }
+
+ // Otherwise, if we're on mojave or later and importing from
+ // Safari, prompt for the bookmarks file.
+ // We may add other browser/OS combos here in future.
+ if (
+ this._source == "safari" &&
+ AppConstants.isPlatformAndVersionAtLeast("macosx", "18") &&
+ (this._itemsFlags & MigrationUtils.resourceTypes.BOOKMARKS ||
+ this._itemsFlags == MigrationUtils.resourceTypes.ALL)
+ ) {
+ let havePermissions = this.spinResolve(this._migrator.hasPermissions());
+
+ if (!havePermissions) {
+ this._wiz.currentPage.next = "importPermissions";
+ this.recordEvent("safari_perms");
+ }
+ }
+ },
+
+ // 3b: permissions. This gets invoked when the user clicks "Next"
+ async onImportPermissionsPageAdvanced(event) {
+ // We're done if we have permission:
+ if (this._receivedPermissions.has(this._source)) {
+ return;
+ }
+ // The wizard helper is sync, and we need to check some stuff, so just stop
+ // advancing for now and prompt the user, then advance the wizard if everything
+ // worked.
+ event.preventDefault();
+
+ await this._migrator.getPermissions(window);
+ if (await this._migrator.hasPermissions()) {
+ this._receivedPermissions.add(this._source);
+ // Re-enter (we'll then allow the advancement through the early return above)
+ this._wiz.advance();
+ }
+ // if we didn't have permissions after the `getPermissions` call, the user
+ // cancelled the dialog. Just no-op out now; the user can re-try by clicking
+ // the 'Continue' button again, or go back and pick a different browser.
+ },
+
+ // 4 - Migrating
+ onMigratingPageShow() {
+ this._wiz.getButton("cancel").disabled = true;
+ this._wiz.canRewind = false;
+ this._wiz.canAdvance = false;
+
+ // When automigrating, show all of the data that can be received from this source.
+ if (this._autoMigrate) {
+ this._itemsFlags = this.spinResolve(
+ this._migrator.getMigrateData(this._selectedProfile)
+ );
+ }
+
+ this._listItems("migratingItems");
+ setTimeout(() => this.onMigratingMigrate(), 0);
+ },
+
+ async onMigratingMigrate() {
+ await this._migrator.migrate(
+ this._itemsFlags,
+ this._autoMigrate,
+ this._selectedProfile
+ );
+
+ Services.telemetry
+ .getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
+ .add(MigrationUtils.getSourceIdForTelemetry(this._source));
+ if (!this._autoMigrate) {
+ let hist = Services.telemetry.getKeyedHistogramById("FX_MIGRATION_USAGE");
+ let exp = 0;
+ let items = this._itemsFlags;
+ while (items) {
+ if (items & 1) {
+ hist.add(this._source, exp);
+ }
+ items = items >> 1;
+ exp++;
+ }
+ }
+ },
+
+ _listItems(aID) {
+ var items = document.getElementById(aID);
+ while (items.hasChildNodes()) {
+ items.firstChild.remove();
+ }
+
+ for (let itemType of kDataToStringMap.keys()) {
+ let itemValue = MigrationUtils.resourceTypes[itemType.toUpperCase()];
+ if (this._itemsFlags & itemValue) {
+ var label = document.createXULElement("label");
+ label.id = itemValue + "_migrated";
+ try {
+ document.l10n.setAttributes(
+ label,
+ kDataToStringMap.get(itemType) + "-label",
+ { browser: this._sourceForDataLocalization }
+ );
+ items.appendChild(label);
+ } catch (e) {
+ // if the block above throws, we've enumerated all the import data types we
+ // currently support and are now just wasting time, break.
+ break;
+ }
+ }
+ }
+ },
+
+ recordResourceMigration(obj, resourceType) {
+ // Sometimes, the resourceType that gets passed here is a string, which
+ // is bizarre. We'll hold our nose and accept either a string or a
+ // number.
+ resourceType = parseInt(resourceType, 10);
+
+ switch (resourceType) {
+ case MigrationUtils.resourceTypes.HISTORY:
+ obj.history = "1";
+ break;
+ case MigrationUtils.resourceTypes.FORMDATA:
+ obj.formdata = "1";
+ break;
+ case MigrationUtils.resourceTypes.PASSWORDS:
+ obj.passwords = "1";
+ break;
+ case MigrationUtils.resourceTypes.BOOKMARKS:
+ obj.bookmarks = "1";
+ break;
+ case MigrationUtils.resourceTypes.PAYMENT_METHODS:
+ obj.payment_methods = "1";
+ break;
+ default:
+ obj.other++;
+ }
+ },
+
+ recordMigrationStartEvent(resourceFlags) {
+ let extraKeys = {
+ migrator_key: this._source,
+ history: "0",
+ formdata: "0",
+ passwords: "0",
+ bookmarks: "0",
+ payment_methods: "0",
+ // "other" will get incremented, so we keep this as a number for
+ // now, and will cast to a string before submitting to Event telemetry.
+ other: 0,
+ };
+
+ for (let resourceTypeKey in MigrationUtils.resourceTypes) {
+ let resourceType = MigrationUtils.resourceTypes[resourceTypeKey];
+ if (resourceFlags & resourceType) {
+ this.recordResourceMigration(extraKeys, resourceType);
+ }
+ }
+
+ extraKeys.other = String(extraKeys.other);
+ this.recordEvent("migration_started", extraKeys);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ var label;
+ switch (aTopic) {
+ case "Migration:Started":
+ this._succeededMigrationEventArgs = {
+ migrator_key: this._source,
+ history: "0",
+ formdata: "0",
+ passwords: "0",
+ bookmarks: "0",
+ payment_methods: "0",
+ // "other" will get incremented, so we keep this as a number for
+ // now, and will cast to a string before submitting to Event telemetry.
+ other: 0,
+ };
+ this.recordMigrationStartEvent(this._itemsFlags);
+ break;
+ case "Migration:ItemBeforeMigrate":
+ label = document.getElementById(aData + "_migrated");
+ if (label) {
+ label.setAttribute("style", "font-weight: bold");
+ }
+ break;
+ case "Migration:ItemAfterMigrate":
+ this.recordResourceMigration(this._succeededMigrationEventArgs, aData);
+ label = document.getElementById(aData + "_migrated");
+ if (label) {
+ label.removeAttribute("style");
+ }
+ break;
+ case "Migration:Ended":
+ this._succeededMigrationEventArgs.other = String(
+ this._succeededMigrationEventArgs.other
+ );
+ this.recordEvent(
+ "migration_finished",
+ this._succeededMigrationEventArgs
+ );
+
+ if (this.isInitialMigration) {
+ // Ensure errors in reporting data recency do not affect the rest of the migration.
+ try {
+ this.reportDataRecencyTelemetry();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ if (this._autoMigrate) {
+ // We're done now.
+ this._wiz.canAdvance = true;
+ this._wiz.advance();
+
+ setTimeout(close, 5000);
+ } else {
+ this._wiz.canAdvance = true;
+ var nextButton = this._wiz.getButton("next");
+ nextButton.click();
+ }
+ break;
+ case "Migration:ItemError":
+ let type = "undefined";
+ let numericType = parseInt(aData);
+ switch (numericType) {
+ case MigrationUtils.resourceTypes.COOKIES:
+ type = "cookies";
+ break;
+ case MigrationUtils.resourceTypes.HISTORY:
+ type = "history";
+ break;
+ case MigrationUtils.resourceTypes.FORMDATA:
+ type = "form data";
+ break;
+ case MigrationUtils.resourceTypes.PASSWORDS:
+ type = "passwords";
+ break;
+ case MigrationUtils.resourceTypes.BOOKMARKS:
+ type = "bookmarks";
+ break;
+ case MigrationUtils.resourceTypes.PAYMENT_METHODS:
+ type = "payment methods";
+ break;
+ case MigrationUtils.resourceTypes.OTHERDATA:
+ type = "misc. data";
+ break;
+ }
+ Services.console.logStringMessage(
+ "some " + type + " did not successfully migrate."
+ );
+ Services.telemetry
+ .getKeyedHistogramById("FX_MIGRATION_ERRORS")
+ .add(this._source, Math.log2(numericType));
+ break;
+ }
+ },
+
+ onDonePageShow() {
+ this._wiz.getButton("cancel").disabled = true;
+ this._wiz.canRewind = false;
+ this._listItems("doneItems");
+ },
+
+ reportDataRecencyTelemetry() {
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "FX_STARTUP_MIGRATION_DATA_RECENCY"
+ );
+ let lastUsedPromises = [];
+ for (let [key, migrator] of this._availableMigrators) {
+ // No block-scoped let in for...of loop conditions, so get the source:
+ let localKey = key;
+ lastUsedPromises.push(
+ migrator.getLastUsedDate().then(date => {
+ const ONE_YEAR = 24 * 365;
+ let diffInHours = Math.round((Date.now() - date) / (60 * 60 * 1000));
+ if (diffInHours > ONE_YEAR) {
+ diffInHours = ONE_YEAR;
+ }
+ histogram.add(localKey, diffInHours);
+ return [localKey, diffInHours];
+ })
+ );
+ }
+ Promise.all(lastUsedPromises).then(migratorUsedTimeDiff => {
+ // Sort low to high.
+ migratorUsedTimeDiff.sort(
+ ([keyA, diffA], [keyB, diffB]) => diffA - diffB
+ ); /* eslint no-unused-vars: off */
+ let usedMostRecentBrowser =
+ migratorUsedTimeDiff.length &&
+ this._source == migratorUsedTimeDiff[0][0];
+ let usedRecentBrowser = Services.telemetry.getKeyedHistogramById(
+ "FX_STARTUP_MIGRATION_USED_RECENT_BROWSER"
+ );
+ usedRecentBrowser.add(this._source, usedMostRecentBrowser);
+ });
+ },
+};
diff --git a/browser/components/migration/content/migration.xhtml b/browser/components/migration/content/migration.xhtml
new file mode 100644
index 0000000000..be6c06c6fb
--- /dev/null
+++ b/browser/components/migration/content/migration.xhtml
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window id="migrationWizard"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="migration-wizard"
+ windowtype="Browser:MigrationWizard"
+ onload="MigrationWizard.init()"
+ onunload="MigrationWizard.uninit()"
+ style="min-width: 40em;"
+ buttons="accept,cancel">
+<linkset>
+ <html:link rel="localization" href="branding/brand.ftl"/>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl"/>
+ <html:link rel="localization" href="browser/migration.ftl"/>
+</linkset>
+
+<script src="chrome://global/content/customElements.js"/>
+<script src="chrome://browser/content/migration/migration.js"/>
+
+<wizard data-branded="true">
+ <wizardpage id="importSource" pageid="importSource" next="selectProfile"
+ data-header-label-id="import-source-page-title">
+ <description id="importAll" control="importSourceGroup" data-l10n-id="import-from"></description>
+ <description id="importBookmarks" control="importSourceGroup" data-l10n-id="import-from-bookmarks" hidden="true" ></description>
+
+ <radiogroup id="importSourceGroup" align="start">
+# NB: if you add items to this list, please also assign them a unique migrator ID in MigrationUtils.jsm
+ <radio id="firefox" data-l10n-id="import-from-firefox"/>
+#ifdef XP_WIN
+ <radio id="chromium-edge" data-l10n-id="import-from-edge"/>
+ <radio id="edge" data-l10n-id="import-from-edge-legacy" />
+ <radio id="chromium-edge-beta" data-l10n-id="import-from-edge-beta"/>
+ <radio id="ie" data-l10n-id="import-from-ie"/>
+ <radio id="opera" data-l10n-id="import-from-opera"/>
+ <radio id="brave" data-l10n-id="import-from-brave"/>
+ <radio id="chrome" data-l10n-id="import-from-chrome"/>
+ <radio id="chrome-beta" data-l10n-id="import-from-chrome-beta"/>
+ <radio id="chromium" data-l10n-id="import-from-chromium"/>
+ <radio id="canary" data-l10n-id="import-from-canary" />
+ <radio id="vivaldi" data-l10n-id="import-from-vivaldi"/>
+ <radio id="chromium-360se" data-l10n-id="import-from-360se"/>
+ <radio id="opera-gx" data-l10n-id="import-from-opera-gx"/>
+#elifdef XP_MACOSX
+ <radio id="safari" data-l10n-id="import-from-safari"/>
+ <radio id="opera" data-l10n-id="import-from-opera"/>
+ <radio id="brave" data-l10n-id="import-from-brave"/>
+ <radio id="chrome" data-l10n-id="import-from-chrome"/>
+ <radio id="chromium-edge" data-l10n-id="import-from-edge"/>
+ <radio id="chromium-edge-beta" data-l10n-id="import-from-edge-beta"/>
+ <radio id="chromium" data-l10n-id="import-from-chromium"/>
+ <radio id="canary" data-l10n-id="import-from-canary"/>
+ <radio id="vivaldi" data-l10n-id="import-from-vivaldi"/>
+ <radio id="opera-gx" data-l10n-id="import-from-opera-gx"/>
+#elifdef XP_UNIX
+ <radio id="opera" data-l10n-id="import-from-opera"/>
+ <radio id="vivaldi" data-l10n-id="import-from-vivaldi"/>
+ <radio id="brave" data-l10n-id="import-from-brave"/>
+ <radio id="chrome" data-l10n-id="import-from-chrome"/>
+ <radio id="chrome-beta" data-l10n-id="import-from-chrome-beta"/>
+ <radio id="chrome-dev" data-l10n-id="import-from-chrome-dev"/>
+ <radio id="chromium" data-l10n-id="import-from-chromium"/>
+#endif
+ <radio id="nothing" data-l10n-id="import-from-nothing" hidden="true"/>
+ </radiogroup>
+ <label id="noSources" hidden="true" data-l10n-id="no-migration-sources"></label>
+ </wizardpage>
+
+ <wizardpage id="selectProfile" pageid="selectProfile"
+ data-header-label-id="import-select-profile-page-title"
+ next="importItems">
+ <description control="profiles" data-l10n-id="import-select-profile-description"></description>
+
+ <radiogroup id="profiles" align="start"/>
+ </wizardpage>
+
+ <wizardpage id="importItems" pageid="importItems"
+ data-header-label-id="import-items-page-title"
+ next="migrating"
+ oncommand="MigrationWizard.onImportItemCommand();">
+ <description control="dataSources" data-l10n-id="import-items-description"></description>
+
+ <vbox id="dataSources" style="overflow: auto; appearance: auto; -moz-default-appearance: listbox" align="start" flex="1" role="group"/>
+ </wizardpage>
+
+ <wizardpage id="importPermissions" pageid="importPermissions"
+ data-header-label-id="import-permissions-page-title"
+ next="migrating">
+ <description data-l10n-id="import-safari-permissions-string"></description>
+ </wizardpage>
+
+ <wizardpage id="migrating" pageid="migrating"
+ data-header-label-id="import-migrating-page-title"
+ next="done">
+ <description control="migratingItems" data-l10n-id="import-migrating-description"></description>
+
+ <vbox id="migratingItems" style="overflow: auto;" align="start" role="group"/>
+ </wizardpage>
+
+ <wizardpage id="done" pageid="done"
+ data-header-label-id="import-done-page-title">
+ <description control="doneItems" data-l10n-id="import-done-description"></description>
+
+ <vbox id="doneItems" style="overflow: auto;" align="start" role="group"/>
+ </wizardpage>
+
+</wizard>
+</window>