From 4e7e4c2cbed8209974691f18180fa7ea8f00e785 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 20 Apr 2018 08:28:12 +0200 Subject: [PALEMOON] Fix: The profile resseting - partially https://github.com/MoonchildProductions/UXP/commit/aaf3409c46a5e472bc539a6ad5902f886c980e1f - Revert migration - Partially - no manual migration from Places - no migration from other browsers - no the cmd `-migration` - Fix migration files (`// Tycho:`) - Something in the last commit (a typo) --- .../migration/BrowserProfileMigrators.manifest | 4 + .../components/migration/FirefoxProfileMigrator.js | 126 ++++++ .../components/migration/MigrationUtils.jsm | 20 +- .../components/migration/ProfileMigrator.js | 21 + .../components/migration/content/migration.js | 465 +++++++++++++++++++++ .../components/migration/content/migration.xul | 86 ++++ application/palemoon/components/migration/jar.mn | 7 + .../palemoon/components/migration/moz.build | 26 ++ .../migration/nsIBrowserProfileMigrator.idl | 63 +++ .../palemoon/components/nsBrowserContentHandler.js | 14 +- 10 files changed, 812 insertions(+), 20 deletions(-) create mode 100644 application/palemoon/components/migration/BrowserProfileMigrators.manifest create mode 100644 application/palemoon/components/migration/FirefoxProfileMigrator.js create mode 100644 application/palemoon/components/migration/ProfileMigrator.js create mode 100644 application/palemoon/components/migration/content/migration.js create mode 100644 application/palemoon/components/migration/content/migration.xul create mode 100644 application/palemoon/components/migration/jar.mn create mode 100644 application/palemoon/components/migration/moz.build create mode 100644 application/palemoon/components/migration/nsIBrowserProfileMigrator.idl (limited to 'application/palemoon/components') diff --git a/application/palemoon/components/migration/BrowserProfileMigrators.manifest b/application/palemoon/components/migration/BrowserProfileMigrators.manifest new file mode 100644 index 000000000..d2bf0901d --- /dev/null +++ b/application/palemoon/components/migration/BrowserProfileMigrators.manifest @@ -0,0 +1,4 @@ +component {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} ProfileMigrator.js +contract @mozilla.org/toolkit/profile-migrator;1 {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} +component {91185366-ba97-4438-acba-48deaca63386} FirefoxProfileMigrator.js +contract @mozilla.org/profile/migrator;1?app=browser&type=firefox {91185366-ba97-4438-acba-48deaca63386} diff --git a/application/palemoon/components/migration/FirefoxProfileMigrator.js b/application/palemoon/components/migration/FirefoxProfileMigrator.js new file mode 100644 index 000000000..9101974cf --- /dev/null +++ b/application/palemoon/components/migration/FirefoxProfileMigrator.js @@ -0,0 +1,126 @@ +/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +"use strict"; + +/* + * Migrates from a Pale Moon profile in a lossy manner in order to clean up a + * user's profile. Data is only migrated where the benefits outweigh the + * potential problems caused by importing undesired/invalid configurations + * from the source profile. + */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource:///modules/MigrationUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", + "resource://gre/modules/PlacesBackups.jsm"); + +function FirefoxProfileMigrator() { } + +FirefoxProfileMigrator.prototype = Object.create(MigratorPrototype); + +FirefoxProfileMigrator.prototype._getAllProfiles = function () { + let allProfiles = new Map(); + let profiles = + Components.classes["@mozilla.org/toolkit/profile-service;1"] + .getService(Components.interfaces.nsIToolkitProfileService) + .profiles; + while (profiles.hasMoreElements()) { + let profile = profiles.getNext().QueryInterface(Components.interfaces.nsIToolkitProfile); + let rootDir = profile.rootDir; + + if (rootDir.exists() && rootDir.isReadable() && + !rootDir.equals(MigrationUtils.profileStartup.directory)) { + allProfiles.set(profile.name, rootDir); + } + } + return allProfiles; +}; + +function sorter(a, b) { + return a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase()); +} + +Object.defineProperty(FirefoxProfileMigrator.prototype, "sourceProfiles", { + get: function() { + // Tycho: return [{id: x, name: x} for (x of this._getAllProfiles().keys())].sort(sorter); + let items = []; + for (let x of this._getAllProfiles().keys()) { + items.push({id: x, name: x}); + } + return items.sort(sorter); + } +}); + +FirefoxProfileMigrator.prototype.getResources = function(aProfile) { + let sourceProfileDir = aProfile ? this._getAllProfiles().get(aProfile.id) : + Components.classes["@mozilla.org/toolkit/profile-service;1"] + .getService(Components.interfaces.nsIToolkitProfileService) + .selectedProfile.rootDir; + if (!sourceProfileDir || !sourceProfileDir.exists() || + !sourceProfileDir.isReadable()) + return null; + + // Being a startup-only migrator, we can rely on + // MigrationUtils.profileStartup being set. + let currentProfileDir = MigrationUtils.profileStartup.directory; + + // Surely data cannot be imported from the current profile. + if (sourceProfileDir.equals(currentProfileDir)) + return null; + + let getFileResource = function(aMigrationType, aFileNames) { + let files = []; + for (let fileName of aFileNames) { + let file = sourceProfileDir.clone(); + file.append(fileName); + + if (file.exists()) { + files.push(file); + } + } + if (!files.length) { + return null; + } + return { + type: aMigrationType, + migrate: function(aCallback) { + for (let file of files) { + file.copyTo(currentProfileDir, ""); + } + aCallback(true); + } + }; + }; + + let types = MigrationUtils.resourceTypes; + let places = getFileResource(types.HISTORY, ["places.sqlite", "places.sqlite-wal"]); + let cookies = getFileResource(types.COOKIES, ["cookies.sqlite", "cookies.sqlite-wal"]); + let passwords = getFileResource(types.PASSWORDS, + ["signons.sqlite", "logins.json", "key3.db"]); + let formData = getFileResource(types.FORMDATA, ["formhistory.sqlite"]); + let bookmarksBackups = getFileResource(types.OTHERDATA, + [PlacesBackups.profileRelativeFolderPath]); + let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]); + + /* Tycho: + return [r for each (r in [places, cookies, passwords, formData, + dictionary, bookmarksBackups]) if (r)]; + */ + return [places, cookies, passwords, formData, dictionary, bookmarksBackups] + .filter(r => r); +} + +Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", { + get: function() true +}); + + +FirefoxProfileMigrator.prototype.classDescription = "Firefox Profile Migrator"; +FirefoxProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=firefox"; +FirefoxProfileMigrator.prototype.classID = Components.ID("{91185366-ba97-4438-acba-48deaca63386}"); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FirefoxProfileMigrator]); diff --git a/application/palemoon/components/migration/MigrationUtils.jsm b/application/palemoon/components/migration/MigrationUtils.jsm index 4461b8af0..243bf098a 100644 --- a/application/palemoon/components/migration/MigrationUtils.jsm +++ b/application/palemoon/components/migration/MigrationUtils.jsm @@ -177,12 +177,11 @@ this.MigratorPrototype = { */ getMigrateData: function MP_getMigrateData(aProfile) { // Tycho: let types = [r.type for each (r in this._getMaybeCachedResources(aProfile))]; - let types = []; - - for each (r in this._getMaybeCachedResources(aProfile)) { - types.push(r.type); + let resources = this._getMaybeCachedResources(aProfile); + if (!resources) { + return []; } - + let types = resources.map(r => r.type); return types.reduce(function(a, b) a |= b, 0); }, @@ -197,15 +196,10 @@ this.MigratorPrototype = { if (resources.length == 0) throw new Error("migrate called for a non-existent source"); - if (aItems != Ci.nsIBrowserProfileMigrator.ALL) + if (aItems != Ci.nsIBrowserProfileMigrator.ALL) { // Tycho: resources = [r for each (r in resources) if (aItems & r.type)]; - resources = []; - - for each (r in resources) { - if (aItems & r.type) { - resources.push(r); - } - } + resources = resources.filter(r => aItems & r.type); + } // Called either directly or through the bookmarks import callback. function doMigrate() { diff --git a/application/palemoon/components/migration/ProfileMigrator.js b/application/palemoon/components/migration/ProfileMigrator.js new file mode 100644 index 000000000..f67823bae --- /dev/null +++ b/application/palemoon/components/migration/ProfileMigrator.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource:///modules/MigrationUtils.jsm"); + +function ProfileMigrator() { +} + +ProfileMigrator.prototype = { + migrate: MigrationUtils.startupMigration.bind(MigrationUtils), + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProfileMigrator]), + classDescription: "Profile Migrator", + contractID: "@mozilla.org/toolkit/profile-migrator;1", + classID: Components.ID("6F8BB968-C14F-4D6F-9733-6C6737B35DCE") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ProfileMigrator]); diff --git a/application/palemoon/components/migration/content/migration.js b/application/palemoon/components/migration/content/migration.js new file mode 100644 index 000000000..908354d72 --- /dev/null +++ b/application/palemoon/components/migration/content/migration.js @@ -0,0 +1,465 @@ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const kIMig = Ci.nsIBrowserProfileMigrator; +const kIPStartup = Ci.nsIProfileStartup; + +Cu.import("resource:///modules/MigrationUtils.jsm"); + +var MigrationWizard = { + _source: "", // Source Profile Migrator ContractID suffix + _itemsFlags: kIMig.ALL, // Selected Import Data Sources (16-bit bitfield) + _selectedProfile: null, // Selected Profile name to import from + _wiz: null, + _migrator: null, + _autoMigrate: null, + + init: function () + { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "Migration:Started", false); + os.addObserver(this, "Migration:ItemBeforeMigrate", false); + os.addObserver(this, "Migration:ItemAfterMigrate", false); + os.addObserver(this, "Migration:ItemError", false); + os.addObserver(this, "Migration:Ended", false); + + this._wiz = document.documentElement; + + if ("arguments" in window && window.arguments.length > 1) { + this._source = window.arguments[0]; + this._migrator = window.arguments[1] instanceof kIMig ? + window.arguments[1] : null; + this._autoMigrate = window.arguments[2].QueryInterface(kIPStartup); + this._skipImportSourcePage = window.arguments[3]; + if (this._migrator && window.arguments[4]) { + let sourceProfiles = this._migrator.sourceProfiles; + this._selectedProfile = sourceProfiles.find(profile => profile.id == window.arguments[4]); + } + + if (this._autoMigrate) { + // Show the "nothing" option in the automigrate case to provide an + // easily identifiable way to avoid migration and create a new profile. + var nothing = document.getElementById("nothing"); + nothing.hidden = false; + } + } + + this.onImportSourcePageShow(); + }, + + uninit: function () + { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(this, "Migration:Started"); + os.removeObserver(this, "Migration:ItemBeforeMigrate"); + os.removeObserver(this, "Migration:ItemAfterMigrate"); + os.removeObserver(this, "Migration:ItemError"); + os.removeObserver(this, "Migration:Ended"); + MigrationUtils.finishMigration(); + }, + + // 1 - Import Source + onImportSourcePageShow: function () + { + this._wiz.canRewind = false; + + var selectedMigrator = null; + + // Figure out what source apps are are available to import from: + var group = document.getElementById("importSourceGroup"); + for (var i = 0; i < group.childNodes.length; ++i) { + var migratorKey = group.childNodes[i].id; + if (migratorKey != "nothing") { + var migrator = MigrationUtils.getMigrator(migratorKey); + if (migrator) { + // Save this as the first selectable item, if we don't already have + // one, or if it is the migrator that was passed to us. + if (!selectedMigrator || this._source == migratorKey) + selectedMigrator = group.childNodes[i]; + } else { + // Hide this option + group.childNodes[i].hidden = true; + } + } + } + + if (selectedMigrator) + group.selectedItem = selectedMigrator; + else { + // We didn't find a migrator, notify the user + document.getElementById("noSources").hidden = false; + + this._wiz.canAdvance = false; + + document.getElementById("importBookmarks").hidden = true; + document.getElementById("importAll").hidden = true; + } + + // Advance to the next page if the caller told us to. + if (this._migrator && this._skipImportSourcePage) { + this._wiz.advance(); + this._wiz.canRewind = false; + } + }, + + onImportSourcePageAdvanced: function () + { + var newSource = document.getElementById("importSourceGroup").selectedItem.id; + + if (newSource == "nothing") { + document.documentElement.cancel(); + return false; + } + + if (!this._migrator || (newSource != this._source)) { + // Create the migrator for the selected source. + this._migrator = MigrationUtils.getMigrator(newSource); + + this._itemsFlags = kIMig.ALL; + this._selectedProfile = null; + } + this._source = newSource; + + // check for more than one source profile + var sourceProfiles = this._migrator.sourceProfiles; + if (this._skipImportSourcePage) { + this._wiz.currentPage.next = "homePageImport"; + } + else if (sourceProfiles && sourceProfiles.length > 1) { + this._wiz.currentPage.next = "selectProfile"; + } + else { + if (this._autoMigrate) + this._wiz.currentPage.next = "homePageImport"; + else + this._wiz.currentPage.next = "importItems"; + + if (sourceProfiles && sourceProfiles.length == 1) + this._selectedProfile = sourceProfiles[0]; + else + this._selectedProfile = null; + } + }, + + // 2 - [Profile Selection] + onSelectProfilePageShow: function () + { + // Disabling this for now, since we ask about import sources in automigration + // too and don't want to disable the back button + // if (this._autoMigrate) + // document.documentElement.getButton("back").disabled = true; + + var profiles = document.getElementById("profiles"); + while (profiles.hasChildNodes()) + profiles.removeChild(profiles.firstChild); + + // Note that this block is still reached even if the user chose 'From File' + // and we canceled the dialog. When that happens, _migrator will be null. + if (this._migrator) { + var sourceProfiles = this._migrator.sourceProfiles; + + for (let profile of sourceProfiles) { + var item = document.createElement("radio"); + item.id = profile.id; + item.setAttribute("label", profile.name); + profiles.appendChild(item); + } + } + + profiles.selectedItem = this._selectedProfile ? document.getElementById(this._selectedProfile.id) : profiles.firstChild; + }, + + onSelectProfilePageRewound: function () + { + var profiles = document.getElementById("profiles"); + this._selectedProfile = this._migrator.sourceProfiles.find( + profile => profile.id == profiles.selectedItem.id + ) || null; + }, + + onSelectProfilePageAdvanced: function () + { + var profiles = document.getElementById("profiles"); + this._selectedProfile = this._migrator.sourceProfiles.find( + profile => profile.id == profiles.selectedItem.id + ) || null; + + // If we're automigrating or just doing bookmarks don't show the item selection page + if (this._autoMigrate) + this._wiz.currentPage.next = "homePageImport"; + }, + + // 3 - ImportItems + onImportItemsPageShow: function () + { + var dataSources = document.getElementById("dataSources"); + while (dataSources.hasChildNodes()) + dataSources.removeChild(dataSources.firstChild); + + var items = this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate); + for (var i = 0; i < 16; ++i) { + var itemID = (items >> i) & 0x1 ? Math.pow(2, i) : 0; + if (itemID > 0) { + var checkbox = document.createElement("checkbox"); + checkbox.id = itemID; + checkbox.setAttribute("label", + MigrationUtils.getLocalizedString(itemID + "_" + this._source)); + dataSources.appendChild(checkbox); + if (!this._itemsFlags || this._itemsFlags & itemID) + checkbox.checked = true; + } + } + }, + + onImportItemsPageRewound: function () + { + this._wiz.canAdvance = true; + this.onImportItemsPageAdvanced(); + }, + + onImportItemsPageAdvanced: function () + { + var dataSources = document.getElementById("dataSources"); + this._itemsFlags = 0; + for (var i = 0; i < dataSources.childNodes.length; ++i) { + var checkbox = dataSources.childNodes[i]; + if (checkbox.localName == "checkbox" && checkbox.checked) + this._itemsFlags |= parseInt(checkbox.id); + } + }, + + onImportItemCommand: function (aEvent) + { + var items = document.getElementById("dataSources"); + var checkboxes = items.getElementsByTagName("checkbox"); + + var oneChecked = false; + for (var i = 0; i < checkboxes.length; ++i) { + if (checkboxes[i].checked) { + oneChecked = true; + break; + } + } + + this._wiz.canAdvance = oneChecked; + }, + + // 4 - Home Page Selection + onHomePageMigrationPageShow: function () + { + // only want this on the first run + if (!this._autoMigrate) { + this._wiz.advance(); + return; + } + + var brandBundle = document.getElementById("brandBundle"); + // These strings don't exist when not using official branding. If that's + // the case, just skip this page. + try { + var pageTitle = brandBundle.getString("homePageMigrationPageTitle"); + var pageDesc = brandBundle.getString("homePageMigrationDescription"); + var mainStr = brandBundle.getString("homePageSingleStartMain"); + } + catch (e) { + this._wiz.advance(); + return; + } + + document.getElementById("homePageImport").setAttribute("label", pageTitle); + document.getElementById("homePageImportDesc").setAttribute("value", pageDesc); + + this._wiz._adjustWizardHeader(); + + var singleStart = document.getElementById("homePageSingleStart"); + singleStart.setAttribute("label", mainStr); + singleStart.setAttribute("value", "DEFAULT"); + + var source = null; + switch (this._source) { + case "firefox": + source = "sourceNameFirefox"; + break; + } + + // semi-wallpaper for crash when multiple profiles exist, since we haven't initialized mSourceProfile in places + this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate); + + var oldHomePageURL = this._migrator.sourceHomePageURL; + + if (oldHomePageURL && source) { + var appName = MigrationUtils.getLocalizedString(source); + var oldHomePageLabel = + brandBundle.getFormattedString("homePageImport", [appName]); + var oldHomePage = document.getElementById("oldHomePage"); + oldHomePage.setAttribute("label", oldHomePageLabel); + oldHomePage.setAttribute("value", oldHomePageURL); + oldHomePage.removeAttribute("hidden"); + } + else { + // if we don't have at least two options, just advance + this._wiz.advance(); + } + }, + + onHomePageMigrationPageAdvanced: function () + { + // we might not have a selectedItem if we're in fallback mode + try { + var radioGroup = document.getElementById("homePageRadiogroup"); + + this._newHomePage = radioGroup.selectedItem.value; + } catch(ex) {} + }, + + // 5 - Migrating + onMigratingPageShow: function () + { + this._wiz.getButton("cancel").disabled = true; + this._wiz.canRewind = false; + this._wiz.canAdvance = false; + + // When automigrating, show all of the data that can be received from this source. + if (this._autoMigrate) + this._itemsFlags = this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate); + + this._listItems("migratingItems"); + setTimeout(this.onMigratingMigrate, 0, this); + }, + + onMigratingMigrate: function (aOuter) + { + aOuter._migrator.migrate(aOuter._itemsFlags, aOuter._autoMigrate, aOuter._selectedProfile); + }, + + _listItems: function (aID) + { + var items = document.getElementById(aID); + while (items.hasChildNodes()) + items.removeChild(items.firstChild); + + var brandBundle = document.getElementById("brandBundle"); + var itemID; + for (var i = 0; i < 16; ++i) { + var itemID = (this._itemsFlags >> i) & 0x1 ? Math.pow(2, i) : 0; + if (itemID > 0) { + var label = document.createElement("label"); + label.id = itemID + "_migrated"; + try { + label.setAttribute("value", + MigrationUtils.getLocalizedString(itemID + "_" + this._source)); + items.appendChild(label); + } + catch (e) { + // if the block above throws, we've enumerated all the import data types we + // currently support and are now just wasting time, break. + break; + } + } + } + }, + + observe: function (aSubject, aTopic, aData) + { + switch (aTopic) { + case "Migration:Started": + break; + case "Migration:ItemBeforeMigrate": + var label = document.getElementById(aData + "_migrated"); + if (label) + label.setAttribute("style", "font-weight: bold"); + break; + case "Migration:ItemAfterMigrate": + var label = document.getElementById(aData + "_migrated"); + if (label) + label.removeAttribute("style"); + break; + case "Migration:Ended": + if (this._autoMigrate) { + if (this._newHomePage) { + try { + // set homepage properly + var prefSvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + var prefBranch = prefSvc.getBranch(null); + + if (this._newHomePage == "DEFAULT") { + prefBranch.clearUserPref("browser.startup.homepage"); + } + else { + var str = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + str.data = this._newHomePage; + prefBranch.setComplexValue("browser.startup.homepage", + Components.interfaces.nsISupportsString, + str); + } + + var dirSvc = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + var prefFile = dirSvc.get("ProfDS", Components.interfaces.nsIFile); + prefFile.append("prefs.js"); + prefSvc.savePrefFile(prefFile); + } catch(ex) { + dump(ex); + } + } + + // We're done now. + this._wiz.canAdvance = true; + this._wiz.advance(); + + setTimeout(close, 5000); + } + else { + this._wiz.canAdvance = true; + var nextButton = this._wiz.getButton("next"); + nextButton.click(); + } + break; + case "Migration:ItemError": + var type = "undefined"; + switch (parseInt(aData)) { + case Ci.nsIBrowserProfileMigrator.SETTINGS: + type = "settings"; + break; + case Ci.nsIBrowserProfileMigrator.COOKIES: + type = "cookies"; + break; + case Ci.nsIBrowserProfileMigrator.HISTORY: + type = "history"; + break; + case Ci.nsIBrowserProfileMigrator.FORMDATA: + type = "form data"; + break; + case Ci.nsIBrowserProfileMigrator.PASSWORDS: + type = "passwords"; + break; + case Ci.nsIBrowserProfileMigrator.BOOKMARKS: + type = "bookmarks"; + break; + case Ci.nsIBrowserProfileMigrator.OTHERDATA: + type = "misc. data"; + break; + } + Cc["@mozilla.org/consoleservice;1"] + .getService(Ci.nsIConsoleService) + .logStringMessage("some " + type + " did not successfully migrate."); + break; + } + }, + + onDonePageShow: function () + { + this._wiz.getButton("cancel").disabled = true; + this._wiz.canRewind = false; + this._listItems("doneItems"); + } +}; diff --git a/application/palemoon/components/migration/content/migration.xul b/application/palemoon/components/migration/content/migration.xul new file mode 100644 index 000000000..1f205fba7 --- /dev/null +++ b/application/palemoon/components/migration/content/migration.xul @@ -0,0 +1,86 @@ + + + + + + + + + +