From 4deead3275005bcb7dec4e747f073357877496e9 Mon Sep 17 00:00:00 2001 From: Alex Kontos Date: Mon, 7 Jul 2025 19:37:41 +0100 Subject: [PATCH] feat: firefox import migrator --- .../migration/FirefoxImportMigrator.sys.mjs | 852 ++++++++++++++++++ .../migration/MigrationUtils.sys.mjs | 4 + .../migration/MigrationWizardParent.sys.mjs | 2 +- .../migration/content/brands/firefox.png | Bin 0 -> 13513 bytes .../migration/content/migration-wizard.mjs | 73 +- browser/components/migration/jar.mn | 1 + browser/components/migration/moz.build | 1 + 7 files changed, 930 insertions(+), 3 deletions(-) create mode 100644 browser/components/migration/FirefoxImportMigrator.sys.mjs create mode 100644 browser/components/migration/content/brands/firefox.png diff --git a/browser/components/migration/FirefoxImportMigrator.sys.mjs b/browser/components/migration/FirefoxImportMigrator.sys.mjs new file mode 100644 index 000000000000..a1b15f75a9ea --- /dev/null +++ b/browser/components/migration/FirefoxImportMigrator.sys.mjs @@ -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 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 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); + } +} diff --git a/browser/components/migration/MigrationUtils.sys.mjs b/browser/components/migration/MigrationUtils.sys.mjs index 50bd77edb50c..e8eacb6e83b4 100644 --- a/browser/components/migration/MigrationUtils.sys.mjs +++ b/browser/components/migration/MigrationUtils.sys.mjs @@ -52,6 +52,10 @@ const MIGRATOR_MODULES = Object.freeze({ moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs", platforms: ["linux", "macosx", "win"], }, + FirefoxImportMigrator: { + moduleURI: "resource:///modules/FirefoxImportMigrator.sys.mjs", + platforms: ["linux", "macosx", "win"], + }, IEProfileMigrator: { moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs", platforms: ["win"], diff --git a/browser/components/migration/MigrationWizardParent.sys.mjs b/browser/components/migration/MigrationWizardParent.sys.mjs index ca951075557c..8d887663400a 100644 --- a/browser/components/migration/MigrationWizardParent.sys.mjs +++ b/browser/components/migration/MigrationWizardParent.sys.mjs @@ -757,7 +757,7 @@ export class MigrationWizardParent extends JSWindowActorParent { } case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: { let quantity = MigrationUtils.getImportedCount("logins"); - return lazy.gFluentStrings.formatValue( + return quantity === 0 ? "Passwords imported" : lazy.gFluentStrings.formatValue( "migration-wizard-progress-success-passwords", { quantity, diff --git a/browser/components/migration/content/brands/firefox.png b/browser/components/migration/content/brands/firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..b92d78ca6d09c743f9936b97ec42f5fec0755a89 GIT binary patch literal 13513 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 D+wc@I literal 0 HcmV?d00001 diff --git a/browser/components/migration/content/migration-wizard.mjs b/browser/components/migration/content/migration-wizard.mjs index 2014aa2d38cc..f1a8d7a0152e 100644 --- a/browser/components/migration/content/migration-wizard.mjs +++ b/browser/components/migration/content/migration-wizard.mjs @@ -30,6 +30,7 @@ export class MigrationWizard extends HTMLElement { #expandedDetails = false; #extensionsSuccessLink = null; #supportTextLinks = null; + #passwordsMigrated = false; static get markup() { return ` @@ -107,6 +108,9 @@ export class MigrationWizard extends HTMLElement { + @@ -605,6 +609,9 @@ export class MigrationWizard extends HTMLElement { this.#ensureSelectionDropdown(); this.#browserProfileSelectorList.textContent = ""; + // Reset password migration flag when starting new migration + this.#passwordsMigrated = false; + let selectionPage = this.#shadowRoot.querySelector( "div[name='page-selection']" ); @@ -828,6 +835,12 @@ export class MigrationWizard extends HTMLElement { this.#extensionsSuccessLink.textContent = state.progress[resourceType].message; } + if ( + resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ) { + this.#passwordsMigrated = true; + } remainingProgressGroups--; break; } @@ -864,6 +877,12 @@ export class MigrationWizard extends HTMLElement { this.#extensionsSuccessLink.textContent = state.progress[resourceType].message; } + if ( + resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ) { + this.#passwordsMigrated = true; + } remainingProgressGroups--; break; } @@ -898,6 +917,18 @@ export class MigrationWizard extends HTMLElement { cancelButton.hidden = 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 // show the progress page, we cannot focus this button immediately. // 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", [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS]: "migration-list-payment-methods-label", + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.COOKIES]: + "migration-cookies-option-label", }; if (MigrationWizardConstants.USES_FAVORITES.includes(key)) { @@ -1442,12 +1475,22 @@ export class MigrationWizard extends HTMLElement { ) { this.#doImport(); } else if ( - event.target.classList.contains("cancel-close") || - event.target.classList.contains("finish-button") + event.target.classList.contains("cancel-close") ) { this.dispatchEvent( 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 ( event.currentTarget == 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) { diff --git a/browser/components/migration/jar.mn b/browser/components/migration/jar.mn index 4eabe74555a5..b1d3f4d2b07c 100644 --- a/browser/components/migration/jar.mn +++ b/browser/components/migration/jar.mn @@ -22,6 +22,7 @@ browser.jar: content/browser/migration/brands/brave.png (content/brands/brave.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/firefox.png (content/brands/firefox.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/vivaldi.png (content/brands/vivaldi.png) diff --git a/browser/components/migration/moz.build b/browser/components/migration/moz.build index 8c958ab7d4d4..979ada319504 100644 --- a/browser/components/migration/moz.build +++ b/browser/components/migration/moz.build @@ -26,6 +26,7 @@ EXTRA_JS_MODULES += [ "ChromeMigrationUtils.sys.mjs", "ChromeProfileMigrator.sys.mjs", "FileMigrators.sys.mjs", + "FirefoxImportMigrator.sys.mjs", "FirefoxProfileMigrator.sys.mjs", "InternalTestingProfileMigrator.sys.mjs", "MigrationUtils.sys.mjs",