summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/ChromeProfileMigrator.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/components/migration/ChromeProfileMigrator.sys.mjs
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/migration/ChromeProfileMigrator.sys.mjs')
-rw-r--r--browser/components/migration/ChromeProfileMigrator.sys.mjs768
1 files changed, 768 insertions, 0 deletions
diff --git a/browser/components/migration/ChromeProfileMigrator.sys.mjs b/browser/components/migration/ChromeProfileMigrator.sys.mjs
new file mode 100644
index 0000000000..748515a71d
--- /dev/null
+++ b/browser/components/migration/ChromeProfileMigrator.sys.mjs
@@ -0,0 +1,768 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et */
+/* 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 AUTH_TYPE = {
+ SCHEME_HTML: 0,
+ SCHEME_BASIC: 1,
+ SCHEME_DIGEST: 2,
+};
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
+import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ChromeMigrationUtils: "resource:///modules/ChromeMigrationUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ Qihoo360seMigrationUtils: "resource:///modules/360seMigrationUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+/**
+ * Converts an array of chrome bookmark objects into one our own places code
+ * understands.
+ *
+ * @param {object[]} items Chrome Bookmark items to be inserted on this parent
+ * @param {Function} errorAccumulator function that gets called with any errors
+ * thrown so we don't drop them on the floor.
+ * @returns {object[]}
+ */
+function convertBookmarks(items, errorAccumulator) {
+ let itemsToInsert = [];
+ for (let item of items) {
+ try {
+ if (item.type == "url") {
+ if (item.url.trim().startsWith("chrome:")) {
+ // Skip invalid internal URIs. Creating an actual URI always reports
+ // messages to the console because Gecko has its own concept of how
+ // chrome:// URIs should be formed, so we avoid doing that.
+ continue;
+ }
+ if (item.url.trim().startsWith("edge:")) {
+ // Don't import internal Microsoft Edge URIs as they won't resolve within Firefox.
+ continue;
+ }
+ itemsToInsert.push({ url: item.url, title: item.name });
+ } else if (item.type == "folder") {
+ let folderItem = {
+ type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: item.name,
+ };
+ folderItem.children = convertBookmarks(item.children, errorAccumulator);
+ itemsToInsert.push(folderItem);
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ errorAccumulator(ex);
+ }
+ }
+ return itemsToInsert;
+}
+
+/**
+ * Chrome profile migrator. This can also be used as a parent class for
+ * migrators for browsers that are variants of Chrome.
+ */
+export class ChromeProfileMigrator extends MigratorBase {
+ static get key() {
+ return "chrome";
+ }
+
+ get _chromeUserDataPathSuffix() {
+ return "Chrome";
+ }
+
+ _keychainServiceName = "Chrome Safe Storage";
+
+ _keychainAccountName = "Chrome";
+
+ async _getChromeUserDataPathIfExists() {
+ if (this._chromeUserDataPath) {
+ return this._chromeUserDataPath;
+ }
+ let path = lazy.ChromeMigrationUtils.getDataPath(
+ this._chromeUserDataPathSuffix
+ );
+ let exists = await IOUtils.exists(path);
+ if (exists) {
+ this._chromeUserDataPath = path;
+ } else {
+ this._chromeUserDataPath = null;
+ }
+ return this._chromeUserDataPath;
+ }
+
+ async getResources(aProfile) {
+ let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+ if (chromeUserDataPath) {
+ let profileFolder = chromeUserDataPath;
+ if (aProfile) {
+ profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id);
+ }
+ if (await IOUtils.exists(profileFolder)) {
+ let possibleResourcePromises = [
+ GetBookmarksResource(profileFolder, this.constructor.key),
+ GetHistoryResource(profileFolder),
+ GetCookiesResource(profileFolder),
+ ];
+ if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) {
+ possibleResourcePromises.push(
+ this._GetPasswordsResource(profileFolder)
+ );
+ }
+ let possibleResources = await Promise.all(possibleResourcePromises);
+ return possibleResources.filter(r => r != null);
+ }
+ }
+ return [];
+ }
+
+ async getLastUsedDate() {
+ let sourceProfiles = await this.getSourceProfiles();
+ let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+ if (!chromeUserDataPath) {
+ return new Date(0);
+ }
+ let datePromises = sourceProfiles.map(async profile => {
+ let basePath = PathUtils.join(chromeUserDataPath, profile.id);
+ let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(
+ async leafName => {
+ let path = PathUtils.join(basePath, leafName);
+ let info = await IOUtils.stat(path).catch(() => null);
+ return info ? info.lastModificationDate : 0;
+ }
+ );
+ let dates = await Promise.all(fileDatePromises);
+ return Math.max(...dates);
+ });
+ let datesOuter = await Promise.all(datePromises);
+ datesOuter.push(0);
+ return new Date(Math.max(...datesOuter));
+ }
+
+ async getSourceProfiles() {
+ if ("__sourceProfiles" in this) {
+ return this.__sourceProfiles;
+ }
+
+ let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+ if (!chromeUserDataPath) {
+ return [];
+ }
+
+ let localState;
+ let profiles = [];
+ try {
+ localState = await lazy.ChromeMigrationUtils.getLocalState(
+ this._chromeUserDataPathSuffix
+ );
+ let info_cache = localState.profile.info_cache;
+ for (let profileFolderName in info_cache) {
+ profiles.push({
+ id: profileFolderName,
+ name: info_cache[profileFolderName].name || profileFolderName,
+ });
+ }
+ } catch (e) {
+ // Avoid reporting NotFoundErrors from trying to get local state.
+ if (localState || e.name != "NotFoundError") {
+ Cu.reportError("Error detecting Chrome profiles: " + e);
+ }
+ // If we weren't able to detect any profiles above, fallback to the Default profile.
+ let defaultProfilePath = PathUtils.join(chromeUserDataPath, "Default");
+ if (await IOUtils.exists(defaultProfilePath)) {
+ profiles = [
+ {
+ id: "Default",
+ name: "Default",
+ },
+ ];
+ }
+ }
+
+ let profileResources = await Promise.all(
+ profiles.map(async profile => ({
+ profile,
+ resources: await this.getResources(profile),
+ }))
+ );
+
+ // Only list profiles from which any data can be imported
+ this.__sourceProfiles = profileResources
+ .filter(({ resources }) => {
+ return resources && !!resources.length;
+ }, this)
+ .map(({ profile }) => profile);
+ return this.__sourceProfiles;
+ }
+
+ async _GetPasswordsResource(aProfileFolder) {
+ let loginPath = PathUtils.join(aProfileFolder, "Login Data");
+ if (!(await IOUtils.exists(loginPath))) {
+ return null;
+ }
+
+ let {
+ _chromeUserDataPathSuffix,
+ _keychainServiceName,
+ _keychainAccountName,
+ _keychainMockPassphrase = null,
+ } = this;
+
+ return {
+ type: MigrationUtils.resourceTypes.PASSWORDS,
+
+ async migrate(aCallback) {
+ let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
+ loginPath,
+ "Chrome passwords",
+ `SELECT origin_url, action_url, username_element, username_value,
+ password_element, password_value, signon_realm, scheme, date_created,
+ times_used FROM logins WHERE blacklisted_by_user = 0`
+ ).catch(ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ });
+ // If the promise was rejected we will have already called aCallback,
+ // so we can just return here.
+ if (!rows) {
+ return;
+ }
+
+ // If there are no relevant rows, return before initializing crypto and
+ // thus prompting for Keychain access on macOS.
+ if (!rows.length) {
+ aCallback(true);
+ return;
+ }
+
+ let crypto;
+ try {
+ if (AppConstants.platform == "win") {
+ let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
+ );
+ crypto = new ChromeWindowsLoginCrypto(_chromeUserDataPathSuffix);
+ } else if (AppConstants.platform == "macosx") {
+ let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
+ );
+ crypto = new ChromeMacOSLoginCrypto(
+ _keychainServiceName,
+ _keychainAccountName,
+ _keychainMockPassphrase
+ );
+ } else {
+ aCallback(false);
+ return;
+ }
+ } catch (ex) {
+ // Handle the user canceling Keychain access or other OSCrypto errors.
+ Cu.reportError(ex);
+ aCallback(false);
+ return;
+ }
+
+ let logins = [];
+ let fallbackCreationDate = new Date();
+ for (let row of rows) {
+ try {
+ let origin_url = lazy.NetUtil.newURI(
+ row.getResultByName("origin_url")
+ );
+ // Ignore entries for non-http(s)/ftp URLs because we likely can't
+ // use them anyway.
+ const kValidSchemes = new Set(["https", "http", "ftp"]);
+ if (!kValidSchemes.has(origin_url.scheme)) {
+ continue;
+ }
+ let loginInfo = {
+ username: row.getResultByName("username_value"),
+ password: await crypto.decryptData(
+ row.getResultByName("password_value"),
+ null
+ ),
+ origin: origin_url.prePath,
+ formActionOrigin: null,
+ httpRealm: null,
+ usernameElement: row.getResultByName("username_element"),
+ passwordElement: row.getResultByName("password_element"),
+ timeCreated: lazy.ChromeMigrationUtils.chromeTimeToDate(
+ row.getResultByName("date_created") + 0,
+ fallbackCreationDate
+ ).getTime(),
+ timesUsed: row.getResultByName("times_used") + 0,
+ };
+
+ switch (row.getResultByName("scheme")) {
+ case AUTH_TYPE.SCHEME_HTML:
+ let action_url = row.getResultByName("action_url");
+ if (!action_url) {
+ // If there is no action_url, store the wildcard "" value.
+ // See the `formActionOrigin` IDL comments.
+ loginInfo.formActionOrigin = "";
+ break;
+ }
+ let action_uri = lazy.NetUtil.newURI(action_url);
+ if (!kValidSchemes.has(action_uri.scheme)) {
+ continue; // This continues the outer for loop.
+ }
+ loginInfo.formActionOrigin = action_uri.prePath;
+ break;
+ case AUTH_TYPE.SCHEME_BASIC:
+ case AUTH_TYPE.SCHEME_DIGEST:
+ // signon_realm format is URIrealm, so we need remove URI
+ loginInfo.httpRealm = row
+ .getResultByName("signon_realm")
+ .substring(loginInfo.origin.length + 1);
+ break;
+ default:
+ throw new Error(
+ "Login data scheme type not supported: " +
+ row.getResultByName("scheme")
+ );
+ }
+ logins.push(loginInfo);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ try {
+ if (logins.length) {
+ await MigrationUtils.insertLoginsWrapper(logins);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ if (crypto.finalize) {
+ crypto.finalize();
+ }
+ aCallback(true);
+ },
+ };
+ }
+}
+
+async function GetBookmarksResource(aProfileFolder, aBrowserKey) {
+ let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks");
+
+ if (aBrowserKey === "chromium-360se") {
+ let localState = {};
+ try {
+ localState = await lazy.ChromeMigrationUtils.getLocalState("360 SE");
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ let alternativeBookmarks = await lazy.Qihoo360seMigrationUtils.getAlternativeBookmarks(
+ { bookmarksPath, localState }
+ );
+ if (alternativeBookmarks.resource) {
+ return alternativeBookmarks.resource;
+ }
+
+ bookmarksPath = alternativeBookmarks.path;
+ }
+
+ if (!(await IOUtils.exists(bookmarksPath))) {
+ return null;
+ }
+
+ return {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ migrate(aCallback) {
+ return (async function() {
+ let gotErrors = false;
+ let errorGatherer = function() {
+ gotErrors = true;
+ };
+ // Parse Chrome bookmark file that is JSON format
+ let bookmarkJSON = await IOUtils.readJSON(bookmarksPath);
+ let roots = bookmarkJSON.roots;
+
+ // Importing bookmark bar items
+ if (roots.bookmark_bar.children && roots.bookmark_bar.children.length) {
+ // Toolbar
+ let parentGuid = lazy.PlacesUtils.bookmarks.toolbarGuid;
+ let bookmarks = convertBookmarks(
+ roots.bookmark_bar.children,
+ errorGatherer
+ );
+ await MigrationUtils.insertManyBookmarksWrapper(
+ bookmarks,
+ parentGuid
+ );
+ }
+
+ // Importing Other Bookmarks items
+ if (roots.other.children && roots.other.children.length) {
+ // Other Bookmarks
+ let parentGuid = lazy.PlacesUtils.bookmarks.unfiledGuid;
+ let bookmarks = convertBookmarks(roots.other.children, errorGatherer);
+ await MigrationUtils.insertManyBookmarksWrapper(
+ bookmarks,
+ parentGuid
+ );
+ }
+ if (gotErrors) {
+ throw new Error("The migration included errors.");
+ }
+ })().then(
+ () => aCallback(true),
+ () => aCallback(false)
+ );
+ },
+ };
+}
+
+async function GetHistoryResource(aProfileFolder) {
+ let historyPath = PathUtils.join(aProfileFolder, "History");
+ if (!(await IOUtils.exists(historyPath))) {
+ return null;
+ }
+
+ return {
+ type: MigrationUtils.resourceTypes.HISTORY,
+
+ migrate(aCallback) {
+ (async function() {
+ const MAX_AGE_IN_DAYS = Services.prefs.getIntPref(
+ "browser.migrate.chrome.history.maxAgeInDays"
+ );
+ const LIMIT = Services.prefs.getIntPref(
+ "browser.migrate.chrome.history.limit"
+ );
+
+ let query =
+ "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
+ if (MAX_AGE_IN_DAYS) {
+ let maxAge = lazy.ChromeMigrationUtils.dateToChromeTime(
+ Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000
+ );
+ query += " AND last_visit_time > " + maxAge;
+ }
+ if (LIMIT) {
+ query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
+ }
+
+ let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
+ historyPath,
+ "Chrome history",
+ query
+ );
+ let pageInfos = [];
+ let fallbackVisitDate = new Date();
+ for (let row of rows) {
+ try {
+ // if having typed_count, we changes transition type to typed.
+ let transition = lazy.PlacesUtils.history.TRANSITIONS.LINK;
+ if (row.getResultByName("typed_count") > 0) {
+ transition = lazy.PlacesUtils.history.TRANSITIONS.TYPED;
+ }
+
+ pageInfos.push({
+ title: row.getResultByName("title"),
+ url: new URL(row.getResultByName("url")),
+ visits: [
+ {
+ transition,
+ date: lazy.ChromeMigrationUtils.chromeTimeToDate(
+ row.getResultByName("last_visit_time"),
+ fallbackVisitDate
+ ),
+ },
+ ],
+ });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ if (pageInfos.length) {
+ await MigrationUtils.insertVisitsWrapper(pageInfos);
+ }
+ })().then(
+ () => {
+ aCallback(true);
+ },
+ ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ }
+ );
+ },
+ };
+}
+
+async function GetCookiesResource(aProfileFolder) {
+ let cookiesPath = PathUtils.join(aProfileFolder, "Cookies");
+ if (!(await IOUtils.exists(cookiesPath))) {
+ return null;
+ }
+
+ return {
+ type: MigrationUtils.resourceTypes.COOKIES,
+
+ async migrate(aCallback) {
+ // Get columns names and set is_sceure, is_httponly fields accordingly.
+ let columns = await MigrationUtils.getRowsFromDBWithoutLocks(
+ cookiesPath,
+ "Chrome cookies",
+ `PRAGMA table_info(cookies)`
+ ).catch(ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ });
+ // If the promise was rejected we will have already called aCallback,
+ // so we can just return here.
+ if (!columns) {
+ return;
+ }
+ columns = columns.map(c => c.getResultByName("name"));
+ let isHttponly = columns.includes("is_httponly")
+ ? "is_httponly"
+ : "httponly";
+ let isSecure = columns.includes("is_secure") ? "is_secure" : "secure";
+
+ let source_scheme = columns.includes("source_scheme")
+ ? "source_scheme"
+ : `"${Ci.nsICookie.SCHEME_UNSET}" as source_scheme`;
+
+ // We don't support decrypting cookies yet so only import plaintext ones.
+ let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
+ cookiesPath,
+ "Chrome cookies",
+ `SELECT host_key, name, value, path, expires_utc, ${isSecure}, ${isHttponly}, encrypted_value, ${source_scheme}
+ FROM cookies
+ WHERE length(encrypted_value) = 0`
+ ).catch(ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ });
+
+ // If the promise was rejected we will have already called aCallback,
+ // so we can just return here.
+ if (!rows) {
+ return;
+ }
+
+ let fallbackExpiryDate = 0;
+ for (let row of rows) {
+ let host_key = row.getResultByName("host_key");
+ if (host_key.match(/^\./)) {
+ // 1st character of host_key may be ".", so we have to remove it
+ host_key = host_key.substr(1);
+ }
+
+ let schemeType = Ci.nsICookie.SCHEME_UNSET;
+ switch (row.getResultByName("source_scheme")) {
+ case 1:
+ schemeType = Ci.nsICookie.SCHEME_HTTP;
+ break;
+ case 2:
+ schemeType = Ci.nsICookie.SCHEME_HTTPS;
+ break;
+ }
+
+ try {
+ let expiresUtc =
+ lazy.ChromeMigrationUtils.chromeTimeToDate(
+ row.getResultByName("expires_utc"),
+ fallbackExpiryDate
+ ) / 1000;
+ // No point adding cookies that don't have a valid expiry.
+ if (!expiresUtc) {
+ continue;
+ }
+
+ Services.cookies.add(
+ host_key,
+ row.getResultByName("path"),
+ row.getResultByName("name"),
+ row.getResultByName("value"),
+ row.getResultByName(isSecure),
+ row.getResultByName(isHttponly),
+ false,
+ parseInt(expiresUtc),
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ schemeType
+ );
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ aCallback(true);
+ },
+ };
+}
+
+/**
+ * Chromium migrator
+ */
+export class ChromiumProfileMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "chromium";
+ }
+
+ _chromeUserDataPathSuffix = "Chromium";
+ _keychainServiceName = "Chromium Safe Storage";
+ _keychainAccountName = "Chromium";
+}
+
+/**
+ * Chrome Canary
+ * Not available on Linux
+ */
+export class CanaryProfileMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "canary";
+ }
+
+ get _chromeUserDataPathSuffix() {
+ return "Canary";
+ }
+
+ get _keychainServiceName() {
+ return "Chromium Safe Storage";
+ }
+
+ get _keychainAccountName() {
+ return "Chromium";
+ }
+}
+
+/**
+ * Chrome Dev - Linux only (not available in Mac and Windows)
+ */
+export class ChromeDevMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "chrome-dev";
+ }
+
+ _chromeUserDataPathSuffix = "Chrome Dev";
+ _keychainServiceName = "Chromium Safe Storage";
+ _keychainAccountName = "Chromium";
+}
+
+/**
+ * Chrome Beta migrator
+ */
+export class ChromeBetaMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "chrome-beta";
+ }
+
+ _chromeUserDataPathSuffix = "Chrome Beta";
+ _keychainServiceName = "Chromium Safe Storage";
+ _keychainAccountName = "Chromium";
+}
+
+/**
+ * Brave migrator
+ */
+export class BraveProfileMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "brave";
+ }
+
+ _chromeUserDataPathSuffix = "Brave";
+ _keychainServiceName = "Brave Browser Safe Storage";
+ _keychainAccountName = "Brave Browser";
+}
+
+/**
+ * Edge (Chromium-based) migrator
+ */
+export class ChromiumEdgeMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "chromium-edge";
+ }
+
+ _chromeUserDataPathSuffix = "Edge";
+ _keychainServiceName = "Microsoft Edge Safe Storage";
+ _keychainAccountName = "Microsoft Edge";
+}
+
+/**
+ * Edge Beta (Chromium-based) migrator
+ */
+export class ChromiumEdgeBetaMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "chromium-edge-beta";
+ }
+
+ _chromeUserDataPathSuffix = "Edge Beta";
+ _keychainServiceName = "Microsoft Edge Safe Storage";
+ _keychainAccountName = "Microsoft Edge";
+}
+
+/**
+ * Chromium 360 migrator
+ */
+export class Chromium360seMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "chromium-360se";
+ }
+
+ _chromeUserDataPathSuffix = "360 SE";
+ _keychainServiceName = "Microsoft Edge Safe Storage";
+ _keychainAccountName = "Microsoft Edge";
+}
+
+/**
+ * Opera migrator
+ */
+export class OperaProfileMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "opera";
+ }
+
+ _chromeUserDataPathSuffix = "Opera";
+ _keychainServiceName = "Opera Safe Storage";
+ _keychainAccountName = "Opera";
+
+ getSourceProfiles() {
+ return null;
+ }
+}
+
+/**
+ * Opera GX migrator
+ */
+export class OperaGXProfileMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "opera-gx";
+ }
+
+ _chromeUserDataPathSuffix = "Opera GX";
+ _keychainServiceName = "Opera Safe Storage";
+ _keychainAccountName = "Opera";
+
+ getSourceProfiles() {
+ return null;
+ }
+}
+
+/**
+ * Vivaldi migrator
+ */
+export class VivaldiProfileMigrator extends ChromeProfileMigrator {
+ static get key() {
+ return "vivaldi";
+ }
+
+ _chromeUserDataPathSuffix = "Vivaldi";
+ _keychainServiceName = "Vivaldi Safe Storage";
+ _keychainAccountName = "Vivaldi";
+}