feat: firefox import migrator
This commit is contained in:
852
browser/components/migration/FirefoxImportMigrator.sys.mjs
Normal file
852
browser/components/migration/FirefoxImportMigrator.sys.mjs
Normal file
@@ -0,0 +1,852 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates from external Firefox profiles to Waterfox. This is different from
|
||||||
|
* the FirefoxProfileMigrator which is used for profile refresh within Firefox.
|
||||||
|
* This migrator can import data from Firefox installations on the system.
|
||||||
|
*
|
||||||
|
* Supported platforms: Windows, macOS, Linux (including Flatpak and Snap)
|
||||||
|
* Supported data types: Bookmarks, History, Passwords, Form Data, Cookies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
|
||||||
|
import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
|
||||||
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||||
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||||
|
FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
|
||||||
|
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
||||||
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||||
|
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
|
||||||
|
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firefox profile migrator for importing from external Firefox profiles.
|
||||||
|
* This handles importing data from Firefox installations on the system.
|
||||||
|
*/
|
||||||
|
export class FirefoxImportMigrator extends MigratorBase {
|
||||||
|
static get key() {
|
||||||
|
return "firefox-import";
|
||||||
|
}
|
||||||
|
|
||||||
|
static get displayNameL10nID() {
|
||||||
|
return "migration-wizard-migrator-display-name-firefox";
|
||||||
|
}
|
||||||
|
|
||||||
|
static get brandImage() {
|
||||||
|
return "chrome://browser/content/migration/brands/firefox.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
get enabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Firefox profile directory paths for different platforms.
|
||||||
|
* Supports standard installations as well as Flatpak and Snap on Linux.
|
||||||
|
*
|
||||||
|
* @returns {string[]} Array of Firefox profile directory paths
|
||||||
|
*/
|
||||||
|
_getFirefoxProfilePaths() {
|
||||||
|
const paths = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (AppConstants.platform == "win") {
|
||||||
|
// Windows: %APPDATA%/Mozilla/Firefox
|
||||||
|
try {
|
||||||
|
let appData = Services.dirsvc.get("AppData", Ci.nsIFile);
|
||||||
|
let firefoxDir = appData.clone();
|
||||||
|
firefoxDir.appendRelativePath("Mozilla/Firefox");
|
||||||
|
if (firefoxDir.exists() && firefoxDir.isDirectory()) {
|
||||||
|
paths.push(firefoxDir.path);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Failed to check Windows Firefox path:", ex);
|
||||||
|
}
|
||||||
|
} else if (AppConstants.platform == "macosx") {
|
||||||
|
// macOS: ~/Library/Application Support/Firefox
|
||||||
|
try {
|
||||||
|
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||||
|
let firefoxDir = homeDir.clone();
|
||||||
|
firefoxDir.appendRelativePath("Library/Application Support/Firefox");
|
||||||
|
if (firefoxDir.exists() && firefoxDir.isDirectory()) {
|
||||||
|
paths.push(firefoxDir.path);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Failed to check macOS Firefox path:", ex);
|
||||||
|
}
|
||||||
|
} else if (AppConstants.platform == "linux") {
|
||||||
|
// Linux: ~/.mozilla/firefox
|
||||||
|
try {
|
||||||
|
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||||
|
let firefoxDir = homeDir.clone();
|
||||||
|
firefoxDir.appendRelativePath(".mozilla/firefox");
|
||||||
|
if (firefoxDir.exists() && firefoxDir.isDirectory()) {
|
||||||
|
paths.push(firefoxDir.path);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Failed to check Linux Firefox path:", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for Flatpak installation
|
||||||
|
try {
|
||||||
|
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||||
|
let flatpakDir = homeDir.clone();
|
||||||
|
flatpakDir.appendRelativePath(".var/app/org.mozilla.firefox/.mozilla/firefox");
|
||||||
|
if (flatpakDir.exists() && flatpakDir.isDirectory()) {
|
||||||
|
paths.push(flatpakDir.path);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Failed to check Flatpak Firefox path:", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Snap installation
|
||||||
|
try {
|
||||||
|
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||||
|
let snapDir = homeDir.clone();
|
||||||
|
snapDir.appendRelativePath("snap/firefox/common/.mozilla/firefox");
|
||||||
|
if (snapDir.exists() && snapDir.isDirectory()) {
|
||||||
|
paths.push(snapDir.path);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Failed to check Snap Firefox path:", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Error getting Firefox profile paths:", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Firefox profiles.ini file to get profile information.
|
||||||
|
*
|
||||||
|
* @param {string} firefoxDir - Path to Firefox installation directory
|
||||||
|
* @returns {Promise<Array>} Array of profile objects with name, path, and existence info
|
||||||
|
*/
|
||||||
|
async _parseProfilesIni(firefoxDir) {
|
||||||
|
let profiles = [];
|
||||||
|
let profilesIniPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
profilesIniPath = PathUtils.join(firefoxDir, "profiles.ini");
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(`Failed to construct profiles.ini path for Firefox directory "${firefoxDir}":`, ex);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let exists = await IOUtils.exists(profilesIniPath);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
let content = await IOUtils.readUTF8(profilesIniPath);
|
||||||
|
|
||||||
|
let lines = content.split(/\r?\n/);
|
||||||
|
let currentProfile = null;
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.startsWith("[Profile")) {
|
||||||
|
if (currentProfile && currentProfile.name && currentProfile.path) {
|
||||||
|
profiles.push(currentProfile);
|
||||||
|
}
|
||||||
|
currentProfile = {};
|
||||||
|
} else if (line.startsWith("Name=")) {
|
||||||
|
let name = line.substring(5).trim();
|
||||||
|
if (name) {
|
||||||
|
currentProfile.name = name;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("Path=")) {
|
||||||
|
let path = line.substring(5).trim();
|
||||||
|
// Clean up path separators and invalid characters
|
||||||
|
if (path) {
|
||||||
|
// Remove any quotes that might be around the path
|
||||||
|
path = path.replace(/^["']|["']$/g, "");
|
||||||
|
// Normalize path separators
|
||||||
|
currentProfile.path = path.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("IsRelative=")) {
|
||||||
|
currentProfile.isRelative = line.substring(11).trim() === "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last profile if valid
|
||||||
|
if (currentProfile && currentProfile.name && currentProfile.path) {
|
||||||
|
profiles.push(currentProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve paths and validate profiles
|
||||||
|
for (let profile of profiles) {
|
||||||
|
if (!profile.path || !profile.name) {
|
||||||
|
console.warn(`Profile ${profile.name || 'unnamed'} has missing name or path, skipping`);
|
||||||
|
profile.exists = false;
|
||||||
|
profile.fullPath = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip profiles with obviously invalid characters
|
||||||
|
if (profile.path.includes('\0') || profile.name.includes('\0')) {
|
||||||
|
console.warn(`Profile ${profile.name} contains null characters, skipping`);
|
||||||
|
profile.exists = false;
|
||||||
|
profile.fullPath = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use nsIFile API for more reliable path operations
|
||||||
|
let baseDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||||
|
baseDir.initWithPath(firefoxDir);
|
||||||
|
|
||||||
|
if (profile.isRelative !== false) {
|
||||||
|
// For relative paths, append to Firefox directory
|
||||||
|
let profileDir = baseDir.clone();
|
||||||
|
profileDir.appendRelativePath(profile.path);
|
||||||
|
profile.fullPath = profileDir.path;
|
||||||
|
} else {
|
||||||
|
// For absolute paths, use as-is but validate
|
||||||
|
try {
|
||||||
|
let profileDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||||
|
profileDir.initWithPath(profile.path);
|
||||||
|
profile.fullPath = profileDir.path;
|
||||||
|
} catch (pathEx) {
|
||||||
|
console.warn(`Invalid absolute path for profile ${profile.name}: ${profile.path}`, pathEx);
|
||||||
|
// Try treating as relative path instead
|
||||||
|
let profileDir = baseDir.clone();
|
||||||
|
profileDir.appendRelativePath(profile.path);
|
||||||
|
profile.fullPath = profileDir.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if profile directory exists
|
||||||
|
try {
|
||||||
|
profile.exists = await IOUtils.exists(profile.fullPath);
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn(`Cannot check existence of profile path ${profile.fullPath}:`, ex);
|
||||||
|
profile.exists = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(`Failed to resolve profile path for ${profile.name} (${profile.path}):`, ex);
|
||||||
|
profile.exists = false;
|
||||||
|
profile.fullPath = profile.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(`Error parsing profiles.ini at "${profilesIniPath}":`, ex);
|
||||||
|
console.error("This might indicate a corrupted or inaccessible profiles.ini file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let validProfiles = profiles.filter(p => p.exists);
|
||||||
|
return validProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available Firefox profiles from all Firefox installations.
|
||||||
|
* Combines profiles from all detected Firefox installations.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Array>} Array of all available Firefox profiles
|
||||||
|
*/
|
||||||
|
async _getAllFirefoxProfiles() {
|
||||||
|
let allProfiles = [];
|
||||||
|
let firefoxPaths = this._getFirefoxProfilePaths();
|
||||||
|
|
||||||
|
if (firefoxPaths.length === 0) {
|
||||||
|
console.info("No Firefox installations found on this system");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let firefoxPath of firefoxPaths) {
|
||||||
|
let profiles = await this._parseProfilesIni(firefoxPath);
|
||||||
|
|
||||||
|
// Add source path info to each profile
|
||||||
|
for (let profile of profiles) {
|
||||||
|
profile.firefoxPath = firefoxPath;
|
||||||
|
profile.id = `${firefoxPath}::${profile.name}`;
|
||||||
|
allProfiles.push(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSourceProfiles() {
|
||||||
|
let profiles = await this._getAllFirefoxProfiles();
|
||||||
|
|
||||||
|
// Filter out the current profile to avoid importing from ourselves
|
||||||
|
try {
|
||||||
|
// Get current profile directory from MigrationUtils
|
||||||
|
let currentProfilePath = null;
|
||||||
|
if (MigrationUtils.profileStartup && MigrationUtils.profileStartup.directory) {
|
||||||
|
currentProfilePath = MigrationUtils.profileStartup.directory.path;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Fallback to Services directory service
|
||||||
|
try {
|
||||||
|
let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||||
|
currentProfilePath = profileDir.path;
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Could not get profile directory from Services:", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentProfilePath) {
|
||||||
|
profiles = profiles.filter(profile => {
|
||||||
|
try {
|
||||||
|
let normalizedProfilePath = PathUtils.normalize(profile.fullPath);
|
||||||
|
let normalizedCurrentPath = PathUtils.normalize(currentProfilePath);
|
||||||
|
return normalizedProfilePath !== normalizedCurrentPath;
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn(`Path comparison failed for profile ${profile.name}:`, ex);
|
||||||
|
return true; // Include profile if path comparison fails
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn("Could not determine current profile path for filtering:", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = profiles.map(profile => ({
|
||||||
|
id: profile.id,
|
||||||
|
name: profile.name,
|
||||||
|
path: profile.fullPath
|
||||||
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get a file object if it exists and is readable.
|
||||||
|
*
|
||||||
|
* @param {string} dir - Directory path
|
||||||
|
* @param {string} fileName - File name
|
||||||
|
* @returns {nsIFile|null} File object or null if not accessible
|
||||||
|
*/
|
||||||
|
_getFileObject(dir, fileName) {
|
||||||
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||||
|
file.initWithPath(dir);
|
||||||
|
file.append(fileName);
|
||||||
|
return file.exists() && file.isReadable() ? file : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getFiles(aProfile, aFileName) {
|
||||||
|
const files = [];
|
||||||
|
let file = aProfile.clone();
|
||||||
|
file.append(aFileName);
|
||||||
|
|
||||||
|
if (await IOUtils.exists(file.path)) {
|
||||||
|
files.push(file.path);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also look for SQLite temporary files.
|
||||||
|
for (const suffix of ["-wal", "-shm"]) {
|
||||||
|
file = aProfile.clone();
|
||||||
|
file.append(aFileName + suffix);
|
||||||
|
if (await IOUtils.exists(file.path)) {
|
||||||
|
files.push(file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResources(aProfile) {
|
||||||
|
if (!aProfile) {
|
||||||
|
console.warn("No profile provided to getResources");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let profilePath = aProfile.path;
|
||||||
|
|
||||||
|
if (!profilePath || !(await IOUtils.exists(profilePath))) {
|
||||||
|
console.warn("Profile path does not exist:", profilePath);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let resources = [];
|
||||||
|
|
||||||
|
// Bookmarks and History (Places database)
|
||||||
|
let placesResource = await this._getPlacesResource(profilePath);
|
||||||
|
if (placesResource) {
|
||||||
|
resources.push(placesResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwords
|
||||||
|
let passwordsResource = await this._getPasswordsResource(profilePath);
|
||||||
|
if (passwordsResource) {
|
||||||
|
resources.push(passwordsResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
let formDataResource = await this._getFormDataResource(profilePath);
|
||||||
|
if (formDataResource) {
|
||||||
|
resources.push(formDataResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
let cookiesResource = await this._getCookiesResource(profilePath);
|
||||||
|
if (cookiesResource) {
|
||||||
|
resources.push(cookiesResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bookmarks (separate from Places)
|
||||||
|
let bookmarksResource = await this._getBookmarksResource(profilePath);
|
||||||
|
if (bookmarksResource) {
|
||||||
|
resources.push(bookmarksResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getPlacesResource(profilePath) {
|
||||||
|
let placesPath = PathUtils.join(profilePath, "places.sqlite");
|
||||||
|
if (!(await IOUtils.exists(placesPath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: MigrationUtils.resourceTypes.HISTORY,
|
||||||
|
migrate: async (callback) => {
|
||||||
|
try {
|
||||||
|
await this._migratePlaces(placesPath);
|
||||||
|
callback(true);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to migrate places:", ex);
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _migratePlaces(placesPath) {
|
||||||
|
const sourceDB = await lazy.Sqlite.openConnection({ path: placesPath });
|
||||||
|
try {
|
||||||
|
const historyRows = await sourceDB.execute(`
|
||||||
|
SELECT p.url, p.title, v.visit_date, v.visit_type
|
||||||
|
FROM moz_historyvisits AS v
|
||||||
|
JOIN moz_places AS p ON v.place_id = p.id
|
||||||
|
WHERE p.url NOT LIKE ? AND p.url NOT LIKE ? AND p.visit_count > 0
|
||||||
|
`, ["place:%", "moz-anno:%"]);
|
||||||
|
|
||||||
|
const pageInfos = new Map();
|
||||||
|
const maxAge = Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS;
|
||||||
|
|
||||||
|
for (const row of historyRows) {
|
||||||
|
const url = row.getResultByName("url");
|
||||||
|
const visitDate = new Date(row.getResultByName("visit_date") / 1000);
|
||||||
|
if (!URL.canParse(url) || visitDate.getTime() < maxAge) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visit = {
|
||||||
|
date: visitDate,
|
||||||
|
transition: row.getResultByName("visit_type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pageInfos.has(url)) {
|
||||||
|
pageInfos.get(url).visits.push(visit);
|
||||||
|
} else {
|
||||||
|
pageInfos.set(url, {
|
||||||
|
url: new URL(url),
|
||||||
|
title: row.getResultByName("title") || url,
|
||||||
|
visits: [visit],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pageInfos.size > 0) {
|
||||||
|
await MigrationUtils.insertVisitsWrapper([...pageInfos.values()]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await sourceDB.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getPasswordsResource(profilePath) {
|
||||||
|
let passwordFiles = ["logins.json", "key4.db"];
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
for (let fileName of passwordFiles) {
|
||||||
|
let filePath = PathUtils.join(profilePath, fileName);
|
||||||
|
if (await IOUtils.exists(filePath)) {
|
||||||
|
files.push({name: fileName, path: filePath});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: MigrationUtils.resourceTypes.PASSWORDS,
|
||||||
|
migrate: async (callback) => {
|
||||||
|
try {
|
||||||
|
// Copy password files directly to current profile directory
|
||||||
|
let currentProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
let destPath = PathUtils.join(currentProfileDir.path, file.name);
|
||||||
|
await IOUtils.copy(file.path, destPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(true);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to migrate passwords:", ex);
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getBookmarksResource(profilePath) {
|
||||||
|
let placesPath = PathUtils.join(profilePath, "places.sqlite");
|
||||||
|
if (!(await IOUtils.exists(placesPath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||||
|
migrate: async (callback) => {
|
||||||
|
try {
|
||||||
|
await this._migrateBookmarks(placesPath);
|
||||||
|
callback(true);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to migrate bookmarks:", ex);
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _migrateBookmarks(placesPath) {
|
||||||
|
const sourceDB = await lazy.Sqlite.openConnection({ path: placesPath });
|
||||||
|
try {
|
||||||
|
// Get all folders first
|
||||||
|
const folders = await sourceDB.execute(`
|
||||||
|
SELECT id, parent, title, guid, type, position FROM moz_bookmarks
|
||||||
|
WHERE type = 2
|
||||||
|
ORDER BY parent, position
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get all bookmarks
|
||||||
|
const bookmarks = await sourceDB.execute(`
|
||||||
|
SELECT b.id, b.parent, b.title, p.url, b.dateAdded, b.lastModified, b.guid, b.position
|
||||||
|
FROM moz_bookmarks AS b
|
||||||
|
JOIN moz_places AS p ON b.fk = p.id
|
||||||
|
WHERE b.type = 1
|
||||||
|
ORDER BY b.parent, b.position
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create mapping of source folder IDs to target GUIDs
|
||||||
|
const folderGuidMap = new Map();
|
||||||
|
|
||||||
|
// Map root folders
|
||||||
|
for (const folder of folders) {
|
||||||
|
const guid = folder.getResultByName("guid");
|
||||||
|
const id = folder.getResultByName("id");
|
||||||
|
|
||||||
|
if (guid === "menu________") {
|
||||||
|
folderGuidMap.set(id, lazy.PlacesUtils.bookmarks.menuGuid);
|
||||||
|
} else if (guid === "toolbar_______") {
|
||||||
|
folderGuidMap.set(id, lazy.PlacesUtils.bookmarks.toolbarGuid);
|
||||||
|
} else if (guid === "unfiled_____") {
|
||||||
|
folderGuidMap.set(id, lazy.PlacesUtils.bookmarks.unfiledGuid);
|
||||||
|
} else if (guid === "mobile______") {
|
||||||
|
folderGuidMap.set(id, lazy.PlacesUtils.bookmarks.mobileGuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create custom folders in hierarchy order
|
||||||
|
let processedFolders = new Set([...folderGuidMap.keys()]);
|
||||||
|
let lastProcessedCount = -1;
|
||||||
|
|
||||||
|
while (processedFolders.size < folders.length && processedFolders.size !== lastProcessedCount) {
|
||||||
|
lastProcessedCount = processedFolders.size;
|
||||||
|
|
||||||
|
for (const folder of folders) {
|
||||||
|
const id = folder.getResultByName("id");
|
||||||
|
const parentId = folder.getResultByName("parent");
|
||||||
|
const title = folder.getResultByName("title");
|
||||||
|
const guid = folder.getResultByName("guid");
|
||||||
|
|
||||||
|
// Skip if already processed or is a root folder
|
||||||
|
if (processedFolders.has(id)) continue;
|
||||||
|
if (["menu________", "toolbar_______", "unfiled_____", "mobile______", "tags________", "root________"].includes(guid)) {
|
||||||
|
processedFolders.add(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip tag folders (parent ID 4 is tags folder)
|
||||||
|
if (parentId === 4) {
|
||||||
|
processedFolders.add(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parent has been processed
|
||||||
|
const parentGuid = folderGuidMap.get(parentId);
|
||||||
|
if (parentGuid) {
|
||||||
|
try {
|
||||||
|
const folderSpec = {
|
||||||
|
parentGuid,
|
||||||
|
type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||||
|
title: title || "Untitled Folder",
|
||||||
|
dateAdded: new Date(),
|
||||||
|
};
|
||||||
|
const newFolder = await MigrationUtils.insertBookmarkWrapper(folderSpec);
|
||||||
|
folderGuidMap.set(id, newFolder.guid);
|
||||||
|
processedFolders.add(id);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to create folder:", title, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect bookmarks by folder for batch insertion
|
||||||
|
const bookmarksByFolder = new Map();
|
||||||
|
|
||||||
|
for (const bookmark of bookmarks) {
|
||||||
|
const parentId = bookmark.getResultByName("parent");
|
||||||
|
const url = bookmark.getResultByName("url");
|
||||||
|
const title = bookmark.getResultByName("title");
|
||||||
|
const dateAdded = new Date(bookmark.getResultByName("dateAdded") / 1000);
|
||||||
|
|
||||||
|
if (!url || !URL.canParse(url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentGuid = folderGuidMap.get(parentId);
|
||||||
|
if (parentGuid) {
|
||||||
|
if (!bookmarksByFolder.has(parentGuid)) {
|
||||||
|
bookmarksByFolder.set(parentGuid, []);
|
||||||
|
}
|
||||||
|
bookmarksByFolder.get(parentGuid).push({
|
||||||
|
url,
|
||||||
|
title: title || url,
|
||||||
|
dateAdded,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Check if this is a tagged bookmark (parent is a tag folder)
|
||||||
|
const isTaggedBookmark = folders.some(f =>
|
||||||
|
f.getResultByName("id") === parentId &&
|
||||||
|
f.getResultByName("parent") === 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert bookmarks using MigrationUtils wrapper for proper counting
|
||||||
|
let totalImported = 0;
|
||||||
|
for (const [parentGuid, bookmarks] of bookmarksByFolder) {
|
||||||
|
try {
|
||||||
|
await MigrationUtils.insertManyBookmarksWrapper(bookmarks, parentGuid);
|
||||||
|
totalImported += bookmarks.length;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to insert bookmarks for folder:", parentGuid, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await sourceDB.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getFormDataResource(profilePath) {
|
||||||
|
let formHistoryPath = PathUtils.join(profilePath, "formhistory.sqlite");
|
||||||
|
if (!(await IOUtils.exists(formHistoryPath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: MigrationUtils.resourceTypes.FORMDATA,
|
||||||
|
migrate: async (callback) => {
|
||||||
|
try {
|
||||||
|
await this._migrateFormData(formHistoryPath);
|
||||||
|
callback(true);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to migrate form data:", ex);
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate form history data from Firefox formhistory.sqlite database.
|
||||||
|
*
|
||||||
|
* @param {string} formHistoryPath - Path to formhistory.sqlite file
|
||||||
|
*/
|
||||||
|
async _migrateFormData(formHistoryPath) {
|
||||||
|
let db = await lazy.Sqlite.openConnection({ path: formHistoryPath });
|
||||||
|
|
||||||
|
try {
|
||||||
|
let rows = await db.execute(`
|
||||||
|
SELECT fieldname, value, timesUsed, firstUsed, lastUsed
|
||||||
|
FROM moz_formhistory
|
||||||
|
WHERE fieldname IS NOT NULL AND value IS NOT NULL
|
||||||
|
ORDER BY lastUsed DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
let addOps = [];
|
||||||
|
for (let row of rows) {
|
||||||
|
let fieldname = row.getResultByName("fieldname");
|
||||||
|
let value = row.getResultByName("value");
|
||||||
|
let timesUsed = row.getResultByName("timesUsed") || 1;
|
||||||
|
let firstUsed = row.getResultByName("firstUsed") || 0;
|
||||||
|
let lastUsed = row.getResultByName("lastUsed") || 0;
|
||||||
|
|
||||||
|
if (fieldname && value) {
|
||||||
|
addOps.push({
|
||||||
|
op: "add",
|
||||||
|
fieldname,
|
||||||
|
value,
|
||||||
|
timesUsed,
|
||||||
|
firstUsed: firstUsed / 1000, // Convert from microseconds to milliseconds
|
||||||
|
lastUsed: lastUsed / 1000, // Convert from microseconds to milliseconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addOps.length > 0) {
|
||||||
|
await lazy.FormHistory.update(addOps);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getCookiesResource(profilePath) {
|
||||||
|
let cookiesPath = PathUtils.join(profilePath, "cookies.sqlite");
|
||||||
|
|
||||||
|
if (!(await IOUtils.exists(cookiesPath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: MigrationUtils.resourceTypes.COOKIES,
|
||||||
|
migrate: async (callback) => {
|
||||||
|
try {
|
||||||
|
await this._migrateCookies(cookiesPath);
|
||||||
|
callback(true);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to migrate cookies:", ex);
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate cookies from Firefox cookies.sqlite database.
|
||||||
|
* Only imports cookies that haven't expired.
|
||||||
|
*
|
||||||
|
* @param {string} cookiesPath - Path to cookies.sqlite file
|
||||||
|
*/
|
||||||
|
async _migrateCookies(cookiesPath) {
|
||||||
|
let db;
|
||||||
|
|
||||||
|
try {
|
||||||
|
db = await lazy.Sqlite.openConnection({ path: cookiesPath });
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to open cookies database:", ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, let's see what's in the database
|
||||||
|
let allRows = await db.execute(`
|
||||||
|
SELECT COUNT(*) as total FROM moz_cookies
|
||||||
|
`);
|
||||||
|
|
||||||
|
let rows = await db.execute(`
|
||||||
|
SELECT host, path, name, value, expiry, isSecure, isHttpOnly, sameSite
|
||||||
|
FROM moz_cookies
|
||||||
|
WHERE expiry > ${Math.floor(Date.now() / 1000)}
|
||||||
|
ORDER BY lastAccessed DESC
|
||||||
|
LIMIT 1000
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
// Let's try without the expiry filter to see if there are any cookies at all
|
||||||
|
let allCookies = await db.execute(`
|
||||||
|
SELECT host, path, name, value, expiry, isSecure, isHttpOnly, sameSite
|
||||||
|
FROM moz_cookies
|
||||||
|
ORDER BY lastAccessed DESC
|
||||||
|
LIMIT 100
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (let row of rows) {
|
||||||
|
try {
|
||||||
|
let host = row.getResultByName("host");
|
||||||
|
let path = row.getResultByName("path");
|
||||||
|
let name = row.getResultByName("name");
|
||||||
|
let value = row.getResultByName("value");
|
||||||
|
let expiry = row.getResultByName("expiry");
|
||||||
|
let isSecure = Boolean(row.getResultByName("isSecure"));
|
||||||
|
let isHttpOnly = Boolean(row.getResultByName("isHttpOnly"));
|
||||||
|
let sameSite = row.getResultByName("sameSite") || Ci.nsICookie.SAMESITE_NONE;
|
||||||
|
|
||||||
|
// Skip invalid cookies
|
||||||
|
if (!host || !name) {
|
||||||
|
failCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Services.cookies.add(
|
||||||
|
host,
|
||||||
|
path || "/",
|
||||||
|
name,
|
||||||
|
value || "",
|
||||||
|
isSecure,
|
||||||
|
isHttpOnly,
|
||||||
|
false, // isSession
|
||||||
|
expiry,
|
||||||
|
{}, // originAttributes
|
||||||
|
sameSite,
|
||||||
|
isSecure ? Ci.nsICookie.SCHEME_HTTPS : Ci.nsICookie.SCHEME_HTTP
|
||||||
|
);
|
||||||
|
successCount++;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to add cookie:", ex);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async getLastUsedDate() {
|
||||||
|
let profiles = await this._getAllFirefoxProfiles();
|
||||||
|
let dates = [];
|
||||||
|
|
||||||
|
for (let profile of profiles) {
|
||||||
|
try {
|
||||||
|
let stat = await IOUtils.stat(profile.fullPath);
|
||||||
|
dates.push(stat.lastModified);
|
||||||
|
|
||||||
|
// Also check places.sqlite modification time
|
||||||
|
let placesPath = PathUtils.join(profile.fullPath, "places.sqlite");
|
||||||
|
if (await IOUtils.exists(placesPath)) {
|
||||||
|
let placesStat = await IOUtils.stat(placesPath);
|
||||||
|
dates.push(placesStat.lastModified);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
// Ignore errors for individual profiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates.length > 0 ? new Date(Math.max(...dates)) : new Date(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,10 @@ const MIGRATOR_MODULES = Object.freeze({
|
|||||||
moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
|
moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
|
||||||
platforms: ["linux", "macosx", "win"],
|
platforms: ["linux", "macosx", "win"],
|
||||||
},
|
},
|
||||||
|
FirefoxImportMigrator: {
|
||||||
|
moduleURI: "resource:///modules/FirefoxImportMigrator.sys.mjs",
|
||||||
|
platforms: ["linux", "macosx", "win"],
|
||||||
|
},
|
||||||
IEProfileMigrator: {
|
IEProfileMigrator: {
|
||||||
moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs",
|
moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs",
|
||||||
platforms: ["win"],
|
platforms: ["win"],
|
||||||
|
|||||||
@@ -757,7 +757,7 @@ export class MigrationWizardParent extends JSWindowActorParent {
|
|||||||
}
|
}
|
||||||
case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: {
|
case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: {
|
||||||
let quantity = MigrationUtils.getImportedCount("logins");
|
let quantity = MigrationUtils.getImportedCount("logins");
|
||||||
return lazy.gFluentStrings.formatValue(
|
return quantity === 0 ? "Passwords imported" : lazy.gFluentStrings.formatValue(
|
||||||
"migration-wizard-progress-success-passwords",
|
"migration-wizard-progress-success-passwords",
|
||||||
{
|
{
|
||||||
quantity,
|
quantity,
|
||||||
|
|||||||
BIN
browser/components/migration/content/brands/firefox.png
Normal file
BIN
browser/components/migration/content/brands/firefox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -30,6 +30,7 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
#expandedDetails = false;
|
#expandedDetails = false;
|
||||||
#extensionsSuccessLink = null;
|
#extensionsSuccessLink = null;
|
||||||
#supportTextLinks = null;
|
#supportTextLinks = null;
|
||||||
|
#passwordsMigrated = false;
|
||||||
|
|
||||||
static get markup() {
|
static get markup() {
|
||||||
return `
|
return `
|
||||||
@@ -107,6 +108,9 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
<label id="payment-methods" class="resource-type-label" data-resource-type="PAYMENT_METHODS">
|
<label id="payment-methods" class="resource-type-label" data-resource-type="PAYMENT_METHODS">
|
||||||
<input type="checkbox"/><span data-l10n-id="migration-payment-methods-option-label"></span>
|
<input type="checkbox"/><span data-l10n-id="migration-payment-methods-option-label"></span>
|
||||||
</label>
|
</label>
|
||||||
|
<label id="cookies" class="resource-type-label" data-resource-type="COOKIES">
|
||||||
|
<input type="checkbox"/><span data-l10n-id="migration-cookies-option-label"></span>
|
||||||
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -605,6 +609,9 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
this.#ensureSelectionDropdown();
|
this.#ensureSelectionDropdown();
|
||||||
this.#browserProfileSelectorList.textContent = "";
|
this.#browserProfileSelectorList.textContent = "";
|
||||||
|
|
||||||
|
// Reset password migration flag when starting new migration
|
||||||
|
this.#passwordsMigrated = false;
|
||||||
|
|
||||||
let selectionPage = this.#shadowRoot.querySelector(
|
let selectionPage = this.#shadowRoot.querySelector(
|
||||||
"div[name='page-selection']"
|
"div[name='page-selection']"
|
||||||
);
|
);
|
||||||
@@ -828,6 +835,12 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
this.#extensionsSuccessLink.textContent =
|
this.#extensionsSuccessLink.textContent =
|
||||||
state.progress[resourceType].message;
|
state.progress[resourceType].message;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
resourceType ==
|
||||||
|
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
|
||||||
|
) {
|
||||||
|
this.#passwordsMigrated = true;
|
||||||
|
}
|
||||||
remainingProgressGroups--;
|
remainingProgressGroups--;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -864,6 +877,12 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
this.#extensionsSuccessLink.textContent =
|
this.#extensionsSuccessLink.textContent =
|
||||||
state.progress[resourceType].message;
|
state.progress[resourceType].message;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
resourceType ==
|
||||||
|
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
|
||||||
|
) {
|
||||||
|
this.#passwordsMigrated = true;
|
||||||
|
}
|
||||||
remainingProgressGroups--;
|
remainingProgressGroups--;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -898,6 +917,18 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
cancelButton.hidden = migrationDone;
|
cancelButton.hidden = migrationDone;
|
||||||
|
|
||||||
if (migrationDone) {
|
if (migrationDone) {
|
||||||
|
// If passwords were migrated, change button text to "Restart Now"
|
||||||
|
if (this.#passwordsMigrated) {
|
||||||
|
let doneButton = progressPage.querySelector(".done-button");
|
||||||
|
let continueButton = progressPage.querySelector(".continue-button");
|
||||||
|
if (doneButton) {
|
||||||
|
document.l10n.setAttributes(doneButton, "quickactions-restart");
|
||||||
|
}
|
||||||
|
if (continueButton) {
|
||||||
|
document.l10n.setAttributes(continueButton, "quickactions-restart");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Since this might be called before the named-deck actually switches to
|
// Since this might be called before the named-deck actually switches to
|
||||||
// show the progress page, we cannot focus this button immediately.
|
// show the progress page, we cannot focus this button immediately.
|
||||||
// Instead, we use a rAF to queue this up for focusing before the
|
// Instead, we use a rAF to queue this up for focusing before the
|
||||||
@@ -1254,6 +1285,8 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
"migration-list-autofill-label",
|
"migration-list-autofill-label",
|
||||||
[MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS]:
|
[MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS]:
|
||||||
"migration-list-payment-methods-label",
|
"migration-list-payment-methods-label",
|
||||||
|
[MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.COOKIES]:
|
||||||
|
"migration-cookies-option-label",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (MigrationWizardConstants.USES_FAVORITES.includes(key)) {
|
if (MigrationWizardConstants.USES_FAVORITES.includes(key)) {
|
||||||
@@ -1442,12 +1475,22 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
) {
|
) {
|
||||||
this.#doImport();
|
this.#doImport();
|
||||||
} else if (
|
} else if (
|
||||||
event.target.classList.contains("cancel-close") ||
|
event.target.classList.contains("cancel-close")
|
||||||
event.target.classList.contains("finish-button")
|
|
||||||
) {
|
) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("MigrationWizard:Close", { bubbles: true })
|
new CustomEvent("MigrationWizard:Close", { bubbles: true })
|
||||||
);
|
);
|
||||||
|
} else if (
|
||||||
|
event.target.classList.contains("finish-button")
|
||||||
|
) {
|
||||||
|
if (this.#passwordsMigrated) {
|
||||||
|
// Restart the browser when passwords were migrated
|
||||||
|
this.#restartBrowser();
|
||||||
|
} else {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("MigrationWizard:Close", { bubbles: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
event.currentTarget == this.#browserProfileSelectorList &&
|
event.currentTarget == this.#browserProfileSelectorList &&
|
||||||
event.target != this.#browserProfileSelectorList
|
event.target != this.#browserProfileSelectorList
|
||||||
@@ -1574,6 +1617,32 @@ export class MigrationWizard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#restartBrowser() {
|
||||||
|
try {
|
||||||
|
// Notify all windows that an application quit has been requested
|
||||||
|
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
|
||||||
|
Ci.nsISupportsPRBool
|
||||||
|
);
|
||||||
|
Services.obs.notifyObservers(
|
||||||
|
cancelQuit,
|
||||||
|
"quit-application-requested",
|
||||||
|
"restart"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Something aborted the quit process
|
||||||
|
if (cancelQuit.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the browser
|
||||||
|
Services.startup.quit(
|
||||||
|
Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Failed to restart browser:", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalThis.customElements) {
|
if (globalThis.customElements) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ browser.jar:
|
|||||||
content/browser/migration/brands/brave.png (content/brands/brave.png)
|
content/browser/migration/brands/brave.png (content/brands/brave.png)
|
||||||
content/browser/migration/brands/chrome.png (content/brands/chrome.png)
|
content/browser/migration/brands/chrome.png (content/brands/chrome.png)
|
||||||
content/browser/migration/brands/chromium.png (content/brands/chromium.png)
|
content/browser/migration/brands/chromium.png (content/brands/chromium.png)
|
||||||
|
content/browser/migration/brands/firefox.png (content/brands/firefox.png)
|
||||||
content/browser/migration/brands/opera.png (content/brands/opera.png)
|
content/browser/migration/brands/opera.png (content/brands/opera.png)
|
||||||
content/browser/migration/brands/operagx.png (content/brands/operagx.png)
|
content/browser/migration/brands/operagx.png (content/brands/operagx.png)
|
||||||
content/browser/migration/brands/vivaldi.png (content/brands/vivaldi.png)
|
content/browser/migration/brands/vivaldi.png (content/brands/vivaldi.png)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ EXTRA_JS_MODULES += [
|
|||||||
"ChromeMigrationUtils.sys.mjs",
|
"ChromeMigrationUtils.sys.mjs",
|
||||||
"ChromeProfileMigrator.sys.mjs",
|
"ChromeProfileMigrator.sys.mjs",
|
||||||
"FileMigrators.sys.mjs",
|
"FileMigrators.sys.mjs",
|
||||||
|
"FirefoxImportMigrator.sys.mjs",
|
||||||
"FirefoxProfileMigrator.sys.mjs",
|
"FirefoxProfileMigrator.sys.mjs",
|
||||||
"InternalTestingProfileMigrator.sys.mjs",
|
"InternalTestingProfileMigrator.sys.mjs",
|
||||||
"MigrationUtils.sys.mjs",
|
"MigrationUtils.sys.mjs",
|
||||||
|
|||||||
Reference in New Issue
Block a user