mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-16 12:17:34 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
82
browser/components/migration/.eslintrc.js
Normal file
82
browser/components/migration/.eslintrc.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = { // eslint-disable-line no-undef
|
||||
"extends": [
|
||||
"../../.eslintrc.js"
|
||||
],
|
||||
|
||||
"globals": {
|
||||
"Components": true,
|
||||
"dump": true,
|
||||
"Iterator": true
|
||||
},
|
||||
|
||||
"env": { "browser": true },
|
||||
|
||||
"rules": {
|
||||
"block-scoped-var": "error",
|
||||
// "brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"comma-dangle": "off",
|
||||
"comma-spacing": ["warn", {"before": false, "after": true}],
|
||||
"comma-style": ["warn", "last"],
|
||||
// "complexity": "warn",
|
||||
"consistent-return": "error",
|
||||
//"curly": "error",
|
||||
"dot-notation": "error",
|
||||
"eol-last": "error",
|
||||
"indent": ["warn", 2, {"SwitchCase": 1}],
|
||||
// "key-spacing": ["warn", {"beforeColon": false, "afterColon": true}],
|
||||
"keyword-spacing": "warn",
|
||||
"max-nested-callbacks": ["error", 3],
|
||||
"new-parens": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-else-return": "error",
|
||||
"no-eval": "error",
|
||||
"no-extend-native": "error",
|
||||
// "no-extra-bind": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-extra-semi": "warn",
|
||||
"no-fallthrough": ["error", { "commentPattern": ".*[Ii]ntentional(?:ly)?\\s+fall(?:ing)?[\\s-]*through.*" }],
|
||||
"no-lonely-if": "error",
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"no-multi-spaces": "warn",
|
||||
"no-multi-str": "warn",
|
||||
"no-native-reassign": "error",
|
||||
"no-nested-ternary": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-shadow": "warn",
|
||||
"no-shadow-restricted-names": "error",
|
||||
// "no-spaced-func": "warn",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unused-vars": ["error", { "varsIgnorePattern": "^C[ciur]$" }],
|
||||
"no-with": "error",
|
||||
"padded-blocks": ["warn", "never"],
|
||||
"quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"semi": ["error", "always", {"omitLastInOneLineBlock": true }],
|
||||
"semi-spacing": ["warn", {"before": false, "after": true}],
|
||||
"space-before-blocks": ["warn", "always"],
|
||||
// "space-before-function-paren": ["warn", "never"],
|
||||
"space-in-parens": ["warn", "never"],
|
||||
"space-infix-ops": ["warn", {"int32Hint": true}],
|
||||
// "space-unary-ops": ["warn", { "words": true, "nonwords": false }],
|
||||
"strict": ["error", "global"],
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"yoda": "error"
|
||||
}
|
||||
};
|
||||
|
||||
328
browser/components/migration/360seProfileMigrator.js
Normal file
328
browser/components/migration/360seProfileMigrator.js
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
|
||||
const kBookmarksFileName = "360sefav.db";
|
||||
|
||||
function copyToTempUTF8File(file, charset) {
|
||||
let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
inputStream.init(file, -1, -1, 0);
|
||||
let inputStr = NetUtil.readInputStreamToString(
|
||||
inputStream, inputStream.available(), { charset });
|
||||
|
||||
// Use random to reduce the likelihood of a name collision in createUnique.
|
||||
let rand = Math.floor(Math.random() * Math.pow(2, 15));
|
||||
let leafName = "mozilla-temp-" + rand;
|
||||
let tempUTF8File = FileUtils.getFile(
|
||||
"TmpD", ["mozilla-temp-files", leafName], true);
|
||||
tempUTF8File.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
|
||||
|
||||
let out = FileUtils.openAtomicFileOutputStream(tempUTF8File);
|
||||
try {
|
||||
let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]
|
||||
.createInstance(Ci.nsIBufferedOutputStream);
|
||||
bufferedOut.init(out, 4096);
|
||||
try {
|
||||
let converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]
|
||||
.createInstance(Ci.nsIConverterOutputStream);
|
||||
converterOut.init(bufferedOut, "utf-8", 0, 0x0000);
|
||||
try {
|
||||
converterOut.writeString(inputStr || "");
|
||||
bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
|
||||
} finally {
|
||||
converterOut.close();
|
||||
}
|
||||
} finally {
|
||||
bufferedOut.close();
|
||||
}
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
|
||||
return tempUTF8File;
|
||||
}
|
||||
|
||||
function parseINIStrings(file) {
|
||||
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
|
||||
getService(Ci.nsIINIParserFactory);
|
||||
let parser = factory.createINIParser(file);
|
||||
let obj = {};
|
||||
let sections = parser.getSections();
|
||||
while (sections.hasMore()) {
|
||||
let section = sections.getNext();
|
||||
obj[section] = {};
|
||||
|
||||
let keys = parser.getKeys(section);
|
||||
while (keys.hasMore()) {
|
||||
let key = keys.getNext();
|
||||
obj[section][key] = parser.getString(section, key);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getHash(aStr) {
|
||||
// return the two-digit hexadecimal code for a byte
|
||||
let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
|
||||
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"].
|
||||
createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(Ci.nsICryptoHash.MD5);
|
||||
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
|
||||
createInstance(Ci.nsIStringInputStream);
|
||||
stringStream.data = aStr;
|
||||
hasher.updateFromStream(stringStream, -1);
|
||||
|
||||
// convert the binary hash data to a hex string.
|
||||
let binary = hasher.finish(false);
|
||||
return Array.from(binary, (c, i) => toHexString(binary.charCodeAt(i))).join("").toLowerCase();
|
||||
}
|
||||
|
||||
function Bookmarks(aProfileFolder) {
|
||||
let file = aProfileFolder.clone();
|
||||
file.append(kBookmarksFileName);
|
||||
|
||||
this._file = file;
|
||||
}
|
||||
Bookmarks.prototype = {
|
||||
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||
|
||||
get exists() {
|
||||
return this._file.exists() && this._file.isReadable();
|
||||
},
|
||||
|
||||
migrate(aCallback) {
|
||||
return Task.spawn(function* () {
|
||||
let idToGuid = new Map();
|
||||
let folderGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
folderGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("360se", folderGuid);
|
||||
}
|
||||
idToGuid.set(0, folderGuid);
|
||||
|
||||
let connection = yield Sqlite.openConnection({
|
||||
path: this._file.path
|
||||
});
|
||||
|
||||
try {
|
||||
let rows = yield connection.execute(
|
||||
`WITH RECURSIVE
|
||||
bookmark(id, parent_id, is_folder, title, url, pos) AS (
|
||||
VALUES(0, -1, 1, '', '', 0)
|
||||
UNION
|
||||
SELECT f.id, f.parent_id, f.is_folder, f.title, f.url, f.pos
|
||||
FROM tb_fav AS f
|
||||
JOIN bookmark AS b ON f.parent_id = b.id
|
||||
ORDER BY f.pos ASC
|
||||
)
|
||||
SELECT id, parent_id, is_folder, title, url FROM bookmark WHERE id`);
|
||||
|
||||
for (let row of rows) {
|
||||
let id = parseInt(row.getResultByName("id"), 10);
|
||||
let parent_id = parseInt(row.getResultByName("parent_id"), 10);
|
||||
let is_folder = parseInt(row.getResultByName("is_folder"), 10);
|
||||
let title = row.getResultByName("title");
|
||||
let url = row.getResultByName("url");
|
||||
|
||||
let parentGuid = idToGuid.get(parent_id) || idToGuid.get("fallback");
|
||||
if (!parentGuid) {
|
||||
parentGuid = PlacesUtils.bookmarks.unfiledGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
parentGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("360se", parentGuid);
|
||||
}
|
||||
idToGuid.set("fallback", parentGuid);
|
||||
}
|
||||
|
||||
try {
|
||||
if (is_folder == 1) {
|
||||
let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid,
|
||||
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
title
|
||||
})).guid;
|
||||
|
||||
idToGuid.set(id, newFolderGuid);
|
||||
} else {
|
||||
yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid,
|
||||
url,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
}.bind(this)).then(() => aCallback(true),
|
||||
e => { Cu.reportError(e); aCallback(false) });
|
||||
}
|
||||
};
|
||||
|
||||
function Qihoo360seProfileMigrator() {
|
||||
let paths = [
|
||||
// for v6 and above
|
||||
{
|
||||
users: ["360se6", "apps", "data", "users"],
|
||||
defaultUser: "default"
|
||||
},
|
||||
// for earlier versions
|
||||
{
|
||||
users: ["360se"],
|
||||
defaultUser: "data"
|
||||
}
|
||||
];
|
||||
this._usersDir = null;
|
||||
this._defaultUserPath = null;
|
||||
for (let path of paths) {
|
||||
let usersDir = FileUtils.getDir("AppData", path.users, false);
|
||||
if (usersDir.exists()) {
|
||||
this._usersDir = usersDir;
|
||||
this._defaultUserPath = path.defaultUser;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Qihoo360seProfileMigrator.prototype = Object.create(MigratorPrototype);
|
||||
|
||||
Object.defineProperty(Qihoo360seProfileMigrator.prototype, "sourceProfiles", {
|
||||
get: function() {
|
||||
if ("__sourceProfiles" in this)
|
||||
return this.__sourceProfiles;
|
||||
|
||||
if (!this._usersDir) {
|
||||
this.__sourceProfiles = [];
|
||||
return this.__sourceProfiles;
|
||||
}
|
||||
|
||||
let profiles = [];
|
||||
let noLoggedInUser = true;
|
||||
try {
|
||||
let loginIni = this._usersDir.clone();
|
||||
loginIni.append("login.ini");
|
||||
if (!loginIni.exists()) {
|
||||
throw new Error("360 Secure Browser's 'login.ini' does not exist.");
|
||||
}
|
||||
if (!loginIni.isReadable()) {
|
||||
throw new Error("360 Secure Browser's 'login.ini' file could not be read.");
|
||||
}
|
||||
|
||||
let loginIniInUtf8 = copyToTempUTF8File(loginIni, "gbk");
|
||||
let loginIniObj = parseINIStrings(loginIniInUtf8);
|
||||
try {
|
||||
loginIniInUtf8.remove(false);
|
||||
} catch (ex) {}
|
||||
|
||||
let nowLoginEmail = loginIniObj.NowLogin && loginIniObj.NowLogin.email;
|
||||
|
||||
/*
|
||||
* NowLogin section may:
|
||||
* 1. be missing or without email, before any user logs in.
|
||||
* 2. represents the current logged in user
|
||||
* 3. represents the most recent logged in user
|
||||
*
|
||||
* In the second case, user represented by NowLogin should be the first
|
||||
* profile; otherwise the default user should be selected by default.
|
||||
*/
|
||||
if (nowLoginEmail) {
|
||||
if (loginIniObj.NowLogin.IsLogined === "1") {
|
||||
noLoggedInUser = false;
|
||||
}
|
||||
|
||||
profiles.push({
|
||||
id: this._getIdFromConfig(loginIniObj.NowLogin),
|
||||
name: nowLoginEmail,
|
||||
});
|
||||
}
|
||||
|
||||
for (let section in loginIniObj) {
|
||||
if (!loginIniObj[section].email ||
|
||||
(nowLoginEmail && loginIniObj[section].email == nowLoginEmail)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
profiles.push({
|
||||
id: this._getIdFromConfig(loginIniObj[section]),
|
||||
name: loginIniObj[section].email,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError("Error detecting 360 Secure Browser profiles: " + e);
|
||||
} finally {
|
||||
profiles[noLoggedInUser ? "unshift" : "push"]({
|
||||
id: this._defaultUserPath,
|
||||
name: "Default",
|
||||
});
|
||||
}
|
||||
|
||||
this.__sourceProfiles = profiles.filter(profile => {
|
||||
let resources = this.getResources(profile);
|
||||
return resources && resources.length > 0;
|
||||
});
|
||||
return this.__sourceProfiles;
|
||||
}
|
||||
});
|
||||
|
||||
Qihoo360seProfileMigrator.prototype._getIdFromConfig = function(aConfig) {
|
||||
return aConfig.UserMd5 || getHash(aConfig.email);
|
||||
};
|
||||
|
||||
Qihoo360seProfileMigrator.prototype.getResources = function(aProfile) {
|
||||
let profileFolder = this._usersDir.clone();
|
||||
profileFolder.append(aProfile.id);
|
||||
|
||||
if (!profileFolder.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let resources = [
|
||||
new Bookmarks(profileFolder)
|
||||
];
|
||||
return resources.filter(r => r.exists);
|
||||
};
|
||||
|
||||
Qihoo360seProfileMigrator.prototype.getLastUsedDate = function() {
|
||||
let bookmarksPaths = this.sourceProfiles.map(({id}) => {
|
||||
return OS.Path.join(this._usersDir.path, id, kBookmarksFileName);
|
||||
});
|
||||
if (!bookmarksPaths.length) {
|
||||
return Promise.resolve(new Date(0));
|
||||
}
|
||||
let datePromises = bookmarksPaths.map(path => {
|
||||
return OS.File.stat(path).catch(() => null).then(info => {
|
||||
return info ? info.lastModificationDate : 0;
|
||||
});
|
||||
});
|
||||
return Promise.all(datePromises).then(dates => {
|
||||
return new Date(Math.max.apply(Math, dates));
|
||||
});
|
||||
};
|
||||
|
||||
Qihoo360seProfileMigrator.prototype.classDescription = "360 Secure Browser Profile Migrator";
|
||||
Qihoo360seProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=360se";
|
||||
Qihoo360seProfileMigrator.prototype.classID = Components.ID("{d0037b95-296a-4a4e-94b2-c3d075d20ab1}");
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Qihoo360seProfileMigrator]);
|
||||
670
browser/components/migration/AutoMigrate.jsm
Normal file
670
browser/components/migration/AutoMigrate.jsm
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AutoMigrate"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
|
||||
const kAutoMigrateEnabledPref = "browser.migrate.automigrate.enabled";
|
||||
const kUndoUIEnabledPref = "browser.migrate.automigrate.ui.enabled";
|
||||
|
||||
const kAutoMigrateBrowserPref = "browser.migrate.automigrate.browser";
|
||||
const kAutoMigrateImportedItemIds = "browser.migrate.automigrate.imported-items";
|
||||
|
||||
const kAutoMigrateLastUndoPromptDateMsPref = "browser.migrate.automigrate.lastUndoPromptDateMs";
|
||||
const kAutoMigrateDaysToOfferUndoPref = "browser.migrate.automigrate.daysToOfferUndo";
|
||||
|
||||
const kAutoMigrateUndoSurveyPref = "browser.migrate.automigrate.undo-survey";
|
||||
const kAutoMigrateUndoSurveyLocalePref = "browser.migrate.automigrate.undo-survey-locales";
|
||||
|
||||
const kNotificationId = "automigration-undo";
|
||||
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
|
||||
"resource://gre/modules/TelemetryStopwatch.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
|
||||
const kBrandBundle = "chrome://branding/locale/brand.properties";
|
||||
return Services.strings.createBundle(kBrandBundle);
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gHardcodedStringBundle", function() {
|
||||
const kBundleURI = "chrome://browser/content/migration/extra-migration-strings.properties";
|
||||
return Services.strings.createBundle(kBundleURI);
|
||||
});
|
||||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
/* globals kUndoStateFullPath */
|
||||
XPCOMUtils.defineLazyGetter(this, "kUndoStateFullPath", function() {
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4");
|
||||
});
|
||||
|
||||
const AutoMigrate = {
|
||||
get resourceTypesToUse() {
|
||||
let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
|
||||
return BOOKMARKS | HISTORY | PASSWORDS;
|
||||
},
|
||||
|
||||
_checkIfEnabled() {
|
||||
let pref = Preferences.get(kAutoMigrateEnabledPref, false);
|
||||
// User-set values should take precedence:
|
||||
if (Services.prefs.prefHasUserValue(kAutoMigrateEnabledPref)) {
|
||||
return pref;
|
||||
}
|
||||
// If we're using the default value, make sure the distribution.ini
|
||||
// value is taken into account even early on startup.
|
||||
try {
|
||||
let distributionFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
|
||||
distributionFile.append("distribution.ini");
|
||||
let parser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
|
||||
getService(Ci.nsIINIParserFactory).
|
||||
createINIParser(distributionFile);
|
||||
return JSON.parse(parser.getString("Preferences", kAutoMigrateEnabledPref));
|
||||
} catch (ex) { /* ignore exceptions (file doesn't exist, invalid value, etc.) */ }
|
||||
|
||||
return pref;
|
||||
},
|
||||
|
||||
init() {
|
||||
this.enabled = this._checkIfEnabled();
|
||||
},
|
||||
|
||||
/**
|
||||
* Automatically pick a migrator and resources to migrate,
|
||||
* then migrate those and start up.
|
||||
*
|
||||
* @throws if automatically deciding on migrators/data
|
||||
* failed for some reason.
|
||||
*/
|
||||
migrate(profileStartup, migratorKey, profileToMigrate) {
|
||||
let histogram = Services.telemetry.getHistogramById(
|
||||
"FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_PROCESS_SUCCESS");
|
||||
histogram.add(0);
|
||||
let {migrator, pickedKey} = this.pickMigrator(migratorKey);
|
||||
histogram.add(5);
|
||||
|
||||
profileToMigrate = this.pickProfile(migrator, profileToMigrate);
|
||||
histogram.add(10);
|
||||
|
||||
let resourceTypes = migrator.getMigrateData(profileToMigrate, profileStartup);
|
||||
if (!(resourceTypes & this.resourceTypesToUse)) {
|
||||
throw new Error("No usable resources were found for the selected browser!");
|
||||
}
|
||||
histogram.add(15);
|
||||
|
||||
let sawErrors = false;
|
||||
let migrationObserver = (subject, topic) => {
|
||||
if (topic == "Migration:ItemError") {
|
||||
sawErrors = true;
|
||||
} else if (topic == "Migration:Ended") {
|
||||
histogram.add(25);
|
||||
if (sawErrors) {
|
||||
histogram.add(26);
|
||||
}
|
||||
Services.obs.removeObserver(migrationObserver, "Migration:Ended");
|
||||
Services.obs.removeObserver(migrationObserver, "Migration:ItemError");
|
||||
Services.prefs.setCharPref(kAutoMigrateBrowserPref, pickedKey);
|
||||
// Save the undo history and block shutdown on that save completing.
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"AutoMigrate Undo saving", this.saveUndoState(), () => {
|
||||
return {state: this._saveUndoStateTrackerForShutdown};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MigrationUtils.initializeUndoData();
|
||||
Services.obs.addObserver(migrationObserver, "Migration:Ended", false);
|
||||
Services.obs.addObserver(migrationObserver, "Migration:ItemError", false);
|
||||
migrator.migrate(this.resourceTypesToUse, profileStartup, profileToMigrate);
|
||||
histogram.add(20);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick and return a migrator to use for automatically migrating.
|
||||
*
|
||||
* @param {String} migratorKey optional, a migrator key to prefer/pick.
|
||||
* @returns {Object} an object with the migrator to use for migrating, as
|
||||
* well as the key we eventually ended up using to obtain it.
|
||||
*/
|
||||
pickMigrator(migratorKey) {
|
||||
if (!migratorKey) {
|
||||
let defaultKey = MigrationUtils.getMigratorKeyForDefaultBrowser();
|
||||
if (!defaultKey) {
|
||||
throw new Error("Could not determine default browser key to migrate from");
|
||||
}
|
||||
migratorKey = defaultKey;
|
||||
}
|
||||
if (migratorKey == "firefox") {
|
||||
throw new Error("Can't automatically migrate from Firefox.");
|
||||
}
|
||||
|
||||
let migrator = MigrationUtils.getMigrator(migratorKey);
|
||||
if (!migrator) {
|
||||
throw new Error("Migrator specified or a default was found, but the migrator object is not available (or has no data).");
|
||||
}
|
||||
return {migrator, pickedKey: migratorKey};
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick a source profile (from the original browser) to use.
|
||||
*
|
||||
* @param {Migrator} migrator the migrator object to use
|
||||
* @param {String} suggestedId the id of the profile to migrate, if pre-specified, or null
|
||||
* @returns the profile to migrate, or null if migrating
|
||||
* from the default profile.
|
||||
*/
|
||||
pickProfile(migrator, suggestedId) {
|
||||
let profiles = migrator.sourceProfiles;
|
||||
if (profiles && !profiles.length) {
|
||||
throw new Error("No profile data found to migrate.");
|
||||
}
|
||||
if (suggestedId) {
|
||||
if (!profiles) {
|
||||
throw new Error("Profile specified but only a default profile found.");
|
||||
}
|
||||
let suggestedProfile = profiles.find(profile => profile.id == suggestedId);
|
||||
if (!suggestedProfile) {
|
||||
throw new Error("Profile specified was not found.");
|
||||
}
|
||||
return suggestedProfile;
|
||||
}
|
||||
if (profiles && profiles.length > 1) {
|
||||
throw new Error("Don't know how to pick a profile when more than 1 profile is present.");
|
||||
}
|
||||
return profiles ? profiles[0] : null;
|
||||
},
|
||||
|
||||
_pendingUndoTasks: false,
|
||||
canUndo: Task.async(function* () {
|
||||
if (this._savingPromise) {
|
||||
yield this._savingPromise;
|
||||
}
|
||||
if (this._pendingUndoTasks) {
|
||||
return false;
|
||||
}
|
||||
let fileExists = false;
|
||||
try {
|
||||
fileExists = yield OS.File.exists(kUndoStateFullPath);
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
return fileExists;
|
||||
}),
|
||||
|
||||
undo: Task.async(function* () {
|
||||
let browserId = Preferences.get(kAutoMigrateBrowserPref, "unknown");
|
||||
TelemetryStopwatch.startKeyed("FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", browserId);
|
||||
let histogram = Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_UNDO");
|
||||
histogram.add(0);
|
||||
if (!(yield this.canUndo())) {
|
||||
histogram.add(5);
|
||||
throw new Error("Can't undo!");
|
||||
}
|
||||
|
||||
this._pendingUndoTasks = true;
|
||||
this._removeNotificationBars();
|
||||
histogram.add(10);
|
||||
|
||||
let readPromise = OS.File.read(kUndoStateFullPath, {
|
||||
encoding: "utf-8",
|
||||
compression: "lz4",
|
||||
});
|
||||
let stateData = this._dejsonifyUndoState(yield readPromise);
|
||||
histogram.add(12);
|
||||
|
||||
this._errorMap = {bookmarks: 0, visits: 0, logins: 0};
|
||||
let reportErrorTelemetry = (type) => {
|
||||
let histogramId = `FX_STARTUP_MIGRATION_UNDO_${type.toUpperCase()}_ERRORCOUNT`;
|
||||
Services.telemetry.getKeyedHistogramById(histogramId).add(browserId, this._errorMap[type]);
|
||||
};
|
||||
|
||||
let startTelemetryStopwatch = resourceType => {
|
||||
let histogramId = `FX_STARTUP_MIGRATION_UNDO_${resourceType.toUpperCase()}_MS`;
|
||||
TelemetryStopwatch.startKeyed(histogramId, browserId);
|
||||
};
|
||||
let stopTelemetryStopwatch = resourceType => {
|
||||
let histogramId = `FX_STARTUP_MIGRATION_UNDO_${resourceType.toUpperCase()}_MS`;
|
||||
TelemetryStopwatch.finishKeyed(histogramId, browserId);
|
||||
};
|
||||
startTelemetryStopwatch("bookmarks");
|
||||
yield this._removeUnchangedBookmarks(stateData.get("bookmarks")).catch(ex => {
|
||||
Cu.reportError("Uncaught exception when removing unchanged bookmarks!");
|
||||
Cu.reportError(ex);
|
||||
});
|
||||
stopTelemetryStopwatch("bookmarks");
|
||||
reportErrorTelemetry("bookmarks");
|
||||
histogram.add(15);
|
||||
|
||||
startTelemetryStopwatch("visits");
|
||||
yield this._removeSomeVisits(stateData.get("visits")).catch(ex => {
|
||||
Cu.reportError("Uncaught exception when removing history visits!");
|
||||
Cu.reportError(ex);
|
||||
});
|
||||
stopTelemetryStopwatch("visits");
|
||||
reportErrorTelemetry("visits");
|
||||
histogram.add(20);
|
||||
|
||||
startTelemetryStopwatch("logins");
|
||||
yield this._removeUnchangedLogins(stateData.get("logins")).catch(ex => {
|
||||
Cu.reportError("Uncaught exception when removing unchanged logins!");
|
||||
Cu.reportError(ex);
|
||||
});
|
||||
stopTelemetryStopwatch("logins");
|
||||
reportErrorTelemetry("logins");
|
||||
histogram.add(25);
|
||||
|
||||
// This is async, but no need to wait for it.
|
||||
NewTabUtils.links.populateCache(() => {
|
||||
NewTabUtils.allPages.update();
|
||||
}, true);
|
||||
|
||||
this._purgeUndoState(this.UNDO_REMOVED_REASON_UNDO_USED);
|
||||
histogram.add(30);
|
||||
TelemetryStopwatch.finishKeyed("FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", browserId);
|
||||
}),
|
||||
|
||||
_removeNotificationBars() {
|
||||
let browserWindows = Services.wm.getEnumerator("navigator:browser");
|
||||
while (browserWindows.hasMoreElements()) {
|
||||
let win = browserWindows.getNext();
|
||||
if (!win.closed) {
|
||||
for (let browser of win.gBrowser.browsers) {
|
||||
let nb = win.gBrowser.getNotificationBox(browser);
|
||||
let notification = nb.getNotificationWithValue(kNotificationId);
|
||||
if (notification) {
|
||||
nb.removeNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_purgeUndoState(reason) {
|
||||
// We don't wait for the off-main-thread removal to complete. OS.File will
|
||||
// ensure it happens before shutdown.
|
||||
OS.File.remove(kUndoStateFullPath, {ignoreAbsent: true}).then(() => {
|
||||
this._pendingUndoTasks = false;
|
||||
});
|
||||
|
||||
let migrationBrowser = Preferences.get(kAutoMigrateBrowserPref, "unknown");
|
||||
Services.prefs.clearUserPref(kAutoMigrateBrowserPref);
|
||||
|
||||
let histogram =
|
||||
Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_UNDO_REASON");
|
||||
histogram.add(migrationBrowser, reason);
|
||||
},
|
||||
|
||||
getBrowserUsedForMigration() {
|
||||
let browserId = Services.prefs.getCharPref(kAutoMigrateBrowserPref);
|
||||
if (browserId) {
|
||||
return MigrationUtils.getBrowserName(browserId);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the user a notification bar indicating we automatically imported
|
||||
* their data and offering them the possibility of removing it.
|
||||
* @param target (xul:browser)
|
||||
* The browser in which we should show the notification.
|
||||
*/
|
||||
maybeShowUndoNotification: Task.async(function* (target) {
|
||||
if (!(yield this.canUndo())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The tab might have navigated since we requested the undo state:
|
||||
let canUndoFromThisPage = ["about:home", "about:newtab"].includes(target.currentURI.spec);
|
||||
if (!canUndoFromThisPage ||
|
||||
!Preferences.get(kUndoUIEnabledPref, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let win = target.ownerGlobal;
|
||||
let notificationBox = win.gBrowser.getNotificationBox(target);
|
||||
if (!notificationBox || notificationBox.getNotificationWithValue(kNotificationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// At this stage we're committed to show the prompt - unless we shouldn't,
|
||||
// in which case we remove the undo prefs (which will cause canUndo() to
|
||||
// return false from now on.):
|
||||
if (!this.shouldStillShowUndoPrompt()) {
|
||||
this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_EXPIRED);
|
||||
this._removeNotificationBars();
|
||||
return;
|
||||
}
|
||||
|
||||
let browserName = this.getBrowserUsedForMigration();
|
||||
if (!browserName) {
|
||||
browserName = gHardcodedStringBundle.GetStringFromName("automigration.undo.unknownbrowser");
|
||||
}
|
||||
const kMessageId = "automigration.undo.message." +
|
||||
Preferences.get(kAutoMigrateImportedItemIds, "all");
|
||||
const kBrandShortName = gBrandBundle.GetStringFromName("brandShortName");
|
||||
let message = gHardcodedStringBundle.formatStringFromName(kMessageId,
|
||||
[browserName, kBrandShortName], 2);
|
||||
|
||||
let buttons = [
|
||||
{
|
||||
label: gHardcodedStringBundle.GetStringFromName("automigration.undo.keep2.label"),
|
||||
accessKey: gHardcodedStringBundle.GetStringFromName("automigration.undo.keep2.accesskey"),
|
||||
callback: () => {
|
||||
this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_REJECTED);
|
||||
this._removeNotificationBars();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: gHardcodedStringBundle.GetStringFromName("automigration.undo.dontkeep2.label"),
|
||||
accessKey: gHardcodedStringBundle.GetStringFromName("automigration.undo.dontkeep2.accesskey"),
|
||||
callback: () => {
|
||||
this._maybeOpenUndoSurveyTab(win);
|
||||
this.undo();
|
||||
},
|
||||
},
|
||||
];
|
||||
notificationBox.appendNotification(
|
||||
message, kNotificationId, null, notificationBox.PRIORITY_INFO_HIGH, buttons
|
||||
);
|
||||
let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 0);
|
||||
Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_UNDO_OFFERED").add(4 - remainingDays);
|
||||
}),
|
||||
|
||||
shouldStillShowUndoPrompt() {
|
||||
let today = new Date();
|
||||
// Round down to midnight:
|
||||
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
// We store the unix timestamp corresponding to midnight on the last day
|
||||
// on which we prompted. Fetch that and compare it to today's date.
|
||||
// (NB: stored as a string because int prefs are too small for unix
|
||||
// timestamps.)
|
||||
let previousPromptDateMsStr = Preferences.get(kAutoMigrateLastUndoPromptDateMsPref, "0");
|
||||
let previousPromptDate = new Date(parseInt(previousPromptDateMsStr, 10));
|
||||
if (previousPromptDate < today) {
|
||||
let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 4) - 1;
|
||||
Preferences.set(kAutoMigrateDaysToOfferUndoPref, remainingDays);
|
||||
Preferences.set(kAutoMigrateLastUndoPromptDateMsPref, today.valueOf().toString());
|
||||
if (remainingDays <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
UNDO_REMOVED_REASON_UNDO_USED: 0,
|
||||
UNDO_REMOVED_REASON_SYNC_SIGNIN: 1,
|
||||
UNDO_REMOVED_REASON_PASSWORD_CHANGE: 2,
|
||||
UNDO_REMOVED_REASON_BOOKMARK_CHANGE: 3,
|
||||
UNDO_REMOVED_REASON_OFFER_EXPIRED: 4,
|
||||
UNDO_REMOVED_REASON_OFFER_REJECTED: 5,
|
||||
|
||||
_jsonifyUndoState(state) {
|
||||
if (!state) {
|
||||
return "null";
|
||||
}
|
||||
// Deal with date serialization.
|
||||
let bookmarks = state.get("bookmarks");
|
||||
for (let bm of bookmarks) {
|
||||
bm.lastModified = bm.lastModified.getTime();
|
||||
}
|
||||
let serializableState = {
|
||||
bookmarks,
|
||||
logins: state.get("logins"),
|
||||
visits: state.get("visits"),
|
||||
};
|
||||
return JSON.stringify(serializableState);
|
||||
},
|
||||
|
||||
_dejsonifyUndoState(state) {
|
||||
state = JSON.parse(state);
|
||||
if (!state) {
|
||||
return new Map();
|
||||
}
|
||||
for (let bm of state.bookmarks) {
|
||||
bm.lastModified = new Date(bm.lastModified);
|
||||
}
|
||||
return new Map([
|
||||
["bookmarks", state.bookmarks],
|
||||
["logins", state.logins],
|
||||
["visits", state.visits],
|
||||
]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the items we've saved into a pref. We use this to be able to show
|
||||
* a detailed message to the user indicating what we've imported.
|
||||
* @param state (Map)
|
||||
* The 'undo' state for the import, which contains info about
|
||||
* how many items of each kind we've (tried to) import.
|
||||
*/
|
||||
_setImportedItemPrefFromState(state) {
|
||||
let itemsWithData = [];
|
||||
if (state) {
|
||||
for (let itemType of state.keys()) {
|
||||
if (state.get(itemType).length) {
|
||||
itemsWithData.push(itemType);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (itemsWithData.length == 3) {
|
||||
itemsWithData = "all";
|
||||
} else {
|
||||
itemsWithData = itemsWithData.sort().join(".");
|
||||
}
|
||||
if (itemsWithData) {
|
||||
Preferences.set(kAutoMigrateImportedItemIds, itemsWithData);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used for the shutdown blocker's information field.
|
||||
*/
|
||||
_saveUndoStateTrackerForShutdown: "not running",
|
||||
/**
|
||||
* Store the information required for using 'undo' of the automatic
|
||||
* migration in the user's profile.
|
||||
*/
|
||||
saveUndoState: Task.async(function* () {
|
||||
let resolveSavingPromise;
|
||||
this._saveUndoStateTrackerForShutdown = "processing undo history";
|
||||
this._savingPromise = new Promise(resolve => { resolveSavingPromise = resolve });
|
||||
let state = yield MigrationUtils.stopAndRetrieveUndoData();
|
||||
|
||||
if (!state || ![...state.values()].some(ary => ary.length > 0)) {
|
||||
// If we didn't import anything, abort now.
|
||||
resolveSavingPromise();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this._saveUndoStateTrackerForShutdown = "saving imported item list";
|
||||
this._setImportedItemPrefFromState(state);
|
||||
|
||||
this._saveUndoStateTrackerForShutdown = "writing undo history";
|
||||
this._undoSavePromise = OS.File.writeAtomic(
|
||||
kUndoStateFullPath, this._jsonifyUndoState(state), {
|
||||
encoding: "utf-8",
|
||||
compression: "lz4",
|
||||
tmpPath: kUndoStateFullPath + ".tmp",
|
||||
});
|
||||
this._undoSavePromise.then(
|
||||
rv => {
|
||||
resolveSavingPromise(rv);
|
||||
delete this._savingPromise;
|
||||
},
|
||||
e => {
|
||||
Cu.reportError("Could not write undo state for automatic migration.");
|
||||
throw e;
|
||||
});
|
||||
return this._undoSavePromise;
|
||||
}),
|
||||
|
||||
_removeUnchangedBookmarks: Task.async(function* (bookmarks) {
|
||||
if (!bookmarks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let guidToLMMap = new Map(bookmarks.map(b => [b.guid, b.lastModified]));
|
||||
let bookmarksFromDB = [];
|
||||
let bmPromises = Array.from(guidToLMMap.keys()).map(guid => {
|
||||
// Ignore bookmarks where the promise doesn't resolve (ie that are missing)
|
||||
// Also check that the bookmark fetch returns isn't null before adding it.
|
||||
try {
|
||||
return PlacesUtils.bookmarks.fetch(guid).then(bm => bm && bookmarksFromDB.push(bm), () => {});
|
||||
} catch (ex) {
|
||||
// Ignore immediate exceptions, too.
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
// We can't use the result of Promise.all because that would include nulls
|
||||
// for bookmarks that no longer exist (which we're catching above).
|
||||
yield Promise.all(bmPromises);
|
||||
let unchangedBookmarks = bookmarksFromDB.filter(bm => {
|
||||
return bm.lastModified.getTime() == guidToLMMap.get(bm.guid).getTime();
|
||||
});
|
||||
|
||||
// We need to remove items without children first, followed by their
|
||||
// parents, etc. In order to do this, find out how many ancestors each item
|
||||
// has that also appear in our list of things to remove, and sort the items
|
||||
// by those numbers. This ensures that children are always removed before
|
||||
// their parents.
|
||||
function determineAncestorCount(bm) {
|
||||
if (bm._ancestorCount) {
|
||||
return bm._ancestorCount;
|
||||
}
|
||||
let myCount = 0;
|
||||
let parentBM = unchangedBookmarks.find(item => item.guid == bm.parentGuid);
|
||||
if (parentBM) {
|
||||
myCount = determineAncestorCount(parentBM) + 1;
|
||||
}
|
||||
bm._ancestorCount = myCount;
|
||||
return myCount;
|
||||
}
|
||||
unchangedBookmarks.forEach(determineAncestorCount);
|
||||
unchangedBookmarks.sort((a, b) => b._ancestorCount - a._ancestorCount);
|
||||
for (let {guid} of unchangedBookmarks) {
|
||||
// Can't just use a .catch() because Bookmarks.remove() can throw (rather
|
||||
// than returning rejected promises).
|
||||
try {
|
||||
yield PlacesUtils.bookmarks.remove(guid, {preventRemovalOfNonEmptyFolders: true});
|
||||
} catch (err) {
|
||||
if (err && err.message != "Cannot remove a non-empty folder.") {
|
||||
this._errorMap.bookmarks++;
|
||||
Cu.reportError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
_removeUnchangedLogins: Task.async(function* (logins) {
|
||||
for (let login of logins) {
|
||||
let foundLogins = LoginHelper.searchLoginsWithObject({guid: login.guid});
|
||||
if (foundLogins.length) {
|
||||
let foundLogin = foundLogins[0];
|
||||
foundLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
if (foundLogin.timePasswordChanged == login.timePasswordChanged) {
|
||||
try {
|
||||
Services.logins.removeLogin(foundLogin);
|
||||
} catch (ex) {
|
||||
Cu.reportError("Failed to remove a login for " + foundLogins.hostname);
|
||||
Cu.reportError(ex);
|
||||
this._errorMap.logins++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
_removeSomeVisits: Task.async(function* (visits) {
|
||||
for (let urlVisits of visits) {
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(urlVisits.url);
|
||||
} catch (ex) {
|
||||
continue;
|
||||
}
|
||||
let visitData = {
|
||||
url: urlObj,
|
||||
beginDate: PlacesUtils.toDate(urlVisits.first),
|
||||
endDate: PlacesUtils.toDate(urlVisits.last),
|
||||
limit: urlVisits.visitCount,
|
||||
};
|
||||
try {
|
||||
yield PlacesUtils.history.removeVisitsByFilter(visitData);
|
||||
} catch (ex) {
|
||||
this._errorMap.visits++;
|
||||
try {
|
||||
visitData.url = visitData.url.href;
|
||||
} catch (ignoredEx) {}
|
||||
Cu.reportError("Failed to remove a visit: " + JSON.stringify(visitData));
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Maybe open a new tab with a survey. The tab will only be opened if all of
|
||||
* the following are true:
|
||||
* - the 'browser.migrate.automigrate.undo-survey' pref is not empty.
|
||||
* It should contain the URL of the survey to open.
|
||||
* - the 'browser.migrate.automigrate.undo-survey-locales' pref, a
|
||||
* comma-separated list of language codes, contains the language code
|
||||
* that is currently in use for the 'global' chrome pacakge (ie the
|
||||
* locale in which the user is currently using Firefox).
|
||||
* The URL will be passed through nsIURLFormatter to allow including
|
||||
* build ids etc. The special additional formatting variable
|
||||
* "%IMPORTEDBROWSER" is also replaced with the name of the browser
|
||||
* from which we imported data.
|
||||
*
|
||||
* @param {Window} chromeWindow A reference to the window in which to open a link.
|
||||
*/
|
||||
_maybeOpenUndoSurveyTab(chromeWindow) {
|
||||
let canDoSurveyInLocale = false;
|
||||
try {
|
||||
let surveyLocales = Preferences.get(kAutoMigrateUndoSurveyLocalePref, "");
|
||||
surveyLocales = surveyLocales.split(",").map(str => str.trim());
|
||||
// Strip out any empty elements, so an empty pref doesn't
|
||||
// lead to a an array with 1 empty string in it.
|
||||
surveyLocales = new Set(surveyLocales.filter(str => !!str));
|
||||
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIXULChromeRegistry);
|
||||
canDoSurveyInLocale =
|
||||
surveyLocales.has(chromeRegistry.getSelectedLocale("global"));
|
||||
} catch (ex) {
|
||||
/* ignore exceptions and just don't do the survey. */
|
||||
}
|
||||
|
||||
let migrationBrowser = this.getBrowserUsedForMigration();
|
||||
let rawURL = Preferences.get(kAutoMigrateUndoSurveyPref, "");
|
||||
if (!canDoSurveyInLocale || !migrationBrowser || !rawURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = Services.urlFormatter.formatURL(rawURL);
|
||||
url = url.replace("%IMPORTEDBROWSER%", encodeURIComponent(migrationBrowser));
|
||||
chromeWindow.openUILinkIn(url, "tab");
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI(
|
||||
[Ci.nsIObserver, Ci.nsINavBookmarkObserver, Ci.nsISupportsWeakReference]
|
||||
),
|
||||
};
|
||||
|
||||
AutoMigrate.init();
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
component {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} ProfileMigrator.js
|
||||
contract @mozilla.org/toolkit/profile-migrator;1 {6F8BB968-C14F-4D6F-9733-6C6737B35DCE}
|
||||
|
||||
#if defined(XP_WIN) || defined(XP_MACOSX)
|
||||
component {4bf85aa5-4e21-46ca-825f-f9c51a5e8c76} ChromeProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=canary {4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}
|
||||
#endif
|
||||
component {4cec1de4-1671-4fc3-a53e-6c539dc77a26} ChromeProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=chrome {4cec1de4-1671-4fc3-a53e-6c539dc77a26}
|
||||
component {8cece922-9720-42de-b7db-7cef88cb07ca} ChromeProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=chromium {8cece922-9720-42de-b7db-7cef88cb07ca}
|
||||
|
||||
component {91185366-ba97-4438-acba-48deaca63386} FirefoxProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=firefox {91185366-ba97-4438-acba-48deaca63386}
|
||||
|
||||
#ifdef HAS_IE_MIGRATOR
|
||||
component {3d2532e3-4932-4774-b7ba-968f5899d3a4} IEProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=ie {3d2532e3-4932-4774-b7ba-968f5899d3a4}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EDGE_MIGRATOR
|
||||
component {62e8834b-2d17-49f5-96ff-56344903a2ae} EdgeProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=edge {62e8834b-2d17-49f5-96ff-56344903a2ae}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_SAFARI_MIGRATOR
|
||||
component {4b609ecf-60b2-4655-9df4-dc149e474da1} SafariProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=safari {4b609ecf-60b2-4655-9df4-dc149e474da1}
|
||||
#endif
|
||||
#ifdef HAS_360SE_MIGRATOR
|
||||
component {d0037b95-296a-4a4e-94b2-c3d075d20ab1} 360seProfileMigrator.js
|
||||
contract @mozilla.org/profile/migrator;1?app=browser&type=360se {d0037b95-296a-4a4e-94b2-c3d075d20ab1}
|
||||
#endif
|
||||
557
browser/components/migration/ChromeProfileMigrator.js
Normal file
557
browser/components/migration/ChromeProfileMigrator.js
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
* vim: sw=2 ts=2 sts=2 et */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
|
||||
const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
|
||||
|
||||
const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
|
||||
const S100NS_PER_MS = 10;
|
||||
|
||||
const AUTH_TYPE = {
|
||||
SCHEME_HTML: 0,
|
||||
SCHEME_BASIC: 1,
|
||||
SCHEME_DIGEST: 2
|
||||
};
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
|
||||
"resource://gre/modules/OSCrypto.jsm");
|
||||
/**
|
||||
* Get an nsIFile instance representing the expected location of user data
|
||||
* for this copy of Chrome/Chromium/Canary on different OSes.
|
||||
* @param subfoldersWin {Array} an array of subfolders to use for Windows
|
||||
* @param subfoldersOSX {Array} an array of subfolders to use for OS X
|
||||
* @param subfoldersUnix {Array} an array of subfolders to use for *nix systems
|
||||
* @returns {nsIFile} the place we expect data to live. Might not actually exist!
|
||||
*/
|
||||
function getDataFolder(subfoldersWin, subfoldersOSX, subfoldersUnix) {
|
||||
let dirServiceID, subfolders;
|
||||
if (AppConstants.platform == "win") {
|
||||
dirServiceID = "LocalAppData";
|
||||
subfolders = subfoldersWin.concat(["User Data"]);
|
||||
} else if (AppConstants.platform == "macosx") {
|
||||
dirServiceID = "ULibDir";
|
||||
subfolders = ["Application Support"].concat(subfoldersOSX);
|
||||
} else {
|
||||
dirServiceID = "Home";
|
||||
subfolders = [".config"].concat(subfoldersUnix);
|
||||
}
|
||||
return FileUtils.getDir(dirServiceID, subfolders, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Chrome time format to Date object
|
||||
*
|
||||
* @param aTime
|
||||
* Chrome time
|
||||
* @return converted Date object
|
||||
* @note Google Chrome uses FILETIME / 10 as time.
|
||||
* FILETIME is based on same structure of Windows.
|
||||
*/
|
||||
function chromeTimeToDate(aTime)
|
||||
{
|
||||
return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Date object to Chrome time format
|
||||
*
|
||||
* @param aDate
|
||||
* Date object or integer equivalent
|
||||
* @return Chrome time
|
||||
* @note For details on Chrome time, see chromeTimeToDate.
|
||||
*/
|
||||
function dateToChromeTime(aDate) {
|
||||
return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert bookmark items into specific folder.
|
||||
*
|
||||
* @param parentGuid
|
||||
* GUID of the folder where items will be inserted
|
||||
* @param items
|
||||
* bookmark items to be inserted
|
||||
* @param errorAccumulator
|
||||
* function that gets called with any errors thrown so we don't drop them on the floor.
|
||||
*/
|
||||
function* insertBookmarkItems(parentGuid, items, errorAccumulator) {
|
||||
for (let item of items) {
|
||||
try {
|
||||
if (item.type == "url") {
|
||||
if (item.url.trim().startsWith("chrome:")) {
|
||||
// Skip invalid chrome URIs. Creating an actual URI always reports
|
||||
// messages to the console, so we avoid doing that.
|
||||
continue;
|
||||
}
|
||||
yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid, url: item.url, title: item.name
|
||||
});
|
||||
} else if (item.type == "folder") {
|
||||
let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name
|
||||
})).guid;
|
||||
|
||||
yield insertBookmarkItems(newFolderGuid, item.children, errorAccumulator);
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
errorAccumulator(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ChromeProfileMigrator() {
|
||||
let chromeUserDataFolder =
|
||||
getDataFolder(["Google", "Chrome"], ["Google", "Chrome"], ["google-chrome"]);
|
||||
this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
|
||||
chromeUserDataFolder : null;
|
||||
}
|
||||
|
||||
ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
|
||||
|
||||
ChromeProfileMigrator.prototype.getResources =
|
||||
function Chrome_getResources(aProfile) {
|
||||
if (this._chromeUserDataFolder) {
|
||||
let profileFolder = this._chromeUserDataFolder.clone();
|
||||
profileFolder.append(aProfile.id);
|
||||
if (profileFolder.exists()) {
|
||||
let possibleResources = [
|
||||
GetBookmarksResource(profileFolder),
|
||||
GetHistoryResource(profileFolder),
|
||||
GetCookiesResource(profileFolder),
|
||||
];
|
||||
if (AppConstants.platform == "win") {
|
||||
possibleResources.push(GetWindowsPasswordsResource(profileFolder));
|
||||
}
|
||||
return possibleResources.filter(r => r != null);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
ChromeProfileMigrator.prototype.getLastUsedDate =
|
||||
function Chrome_getLastUsedDate() {
|
||||
let datePromises = this.sourceProfiles.map(profile => {
|
||||
let basePath = OS.Path.join(this._chromeUserDataFolder.path, profile.id);
|
||||
let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(leafName => {
|
||||
let path = OS.Path.join(basePath, leafName);
|
||||
return OS.File.stat(path).catch(() => null).then(info => {
|
||||
return info ? info.lastModificationDate : 0;
|
||||
});
|
||||
});
|
||||
return Promise.all(fileDatePromises).then(dates => {
|
||||
return Math.max.apply(Math, dates);
|
||||
});
|
||||
});
|
||||
return Promise.all(datePromises).then(dates => {
|
||||
dates.push(0);
|
||||
return new Date(Math.max.apply(Math, dates));
|
||||
});
|
||||
};
|
||||
|
||||
Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", {
|
||||
get: function Chrome_sourceProfiles() {
|
||||
if ("__sourceProfiles" in this)
|
||||
return this.__sourceProfiles;
|
||||
|
||||
if (!this._chromeUserDataFolder)
|
||||
return [];
|
||||
|
||||
let profiles = [];
|
||||
try {
|
||||
// Local State is a JSON file that contains profile info.
|
||||
let localState = this._chromeUserDataFolder.clone();
|
||||
localState.append("Local State");
|
||||
if (!localState.exists())
|
||||
throw new Error("Chrome's 'Local State' file does not exist.");
|
||||
if (!localState.isReadable())
|
||||
throw new Error("Chrome's 'Local State' file could not be read.");
|
||||
|
||||
let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
|
||||
fstream.init(localState, -1, 0, 0);
|
||||
let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
|
||||
{ charset: "UTF-8" });
|
||||
let info_cache = JSON.parse(inputStream).profile.info_cache;
|
||||
for (let profileFolderName in info_cache) {
|
||||
let profileFolder = this._chromeUserDataFolder.clone();
|
||||
profileFolder.append(profileFolderName);
|
||||
profiles.push({
|
||||
id: profileFolderName,
|
||||
name: info_cache[profileFolderName].name || profileFolderName,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError("Error detecting Chrome profiles: " + e);
|
||||
// If we weren't able to detect any profiles above, fallback to the Default profile.
|
||||
let defaultProfileFolder = this._chromeUserDataFolder.clone();
|
||||
defaultProfileFolder.append("Default");
|
||||
if (defaultProfileFolder.exists()) {
|
||||
profiles = [{
|
||||
id: "Default",
|
||||
name: "Default",
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// Only list profiles from which any data can be imported
|
||||
this.__sourceProfiles = profiles.filter(function(profile) {
|
||||
let resources = this.getResources(profile);
|
||||
return resources && resources.length > 0;
|
||||
}, this);
|
||||
return this.__sourceProfiles;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", {
|
||||
get: function Chrome_sourceHomePageURL() {
|
||||
let prefsFile = this._chromeUserDataFolder.clone();
|
||||
prefsFile.append("Preferences");
|
||||
if (prefsFile.exists()) {
|
||||
// XXX reading and parsing JSON is synchronous.
|
||||
let fstream = Cc[FILE_INPUT_STREAM_CID].
|
||||
createInstance(Ci.nsIFileInputStream);
|
||||
fstream.init(prefsFile, -1, 0, 0);
|
||||
try {
|
||||
return JSON.parse(
|
||||
NetUtil.readInputStreamToString(fstream, fstream.available(),
|
||||
{ charset: "UTF-8" })
|
||||
).homepage;
|
||||
}
|
||||
catch (e) {
|
||||
Cu.reportError("Error parsing Chrome's preferences file: " + e);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(ChromeProfileMigrator.prototype, "sourceLocked", {
|
||||
get: function Chrome_sourceLocked() {
|
||||
// There is an exclusive lock on some SQLite databases. Assume they are locked for now.
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
function GetBookmarksResource(aProfileFolder) {
|
||||
let bookmarksFile = aProfileFolder.clone();
|
||||
bookmarksFile.append("Bookmarks");
|
||||
if (!bookmarksFile.exists())
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||
|
||||
migrate: function(aCallback) {
|
||||
return Task.spawn(function* () {
|
||||
let gotErrors = false;
|
||||
let errorGatherer = function() { gotErrors = true };
|
||||
let jsonStream = yield new Promise((resolve, reject) => {
|
||||
let options = {
|
||||
uri: NetUtil.newURI(bookmarksFile),
|
||||
loadUsingSystemPrincipal: true
|
||||
};
|
||||
NetUtil.asyncFetch(options, (inputStream, resultCode) => {
|
||||
if (Components.isSuccessCode(resultCode)) {
|
||||
resolve(inputStream);
|
||||
} else {
|
||||
reject(new Error("Could not read Bookmarks file"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parse Chrome bookmark file that is JSON format
|
||||
let bookmarkJSON = NetUtil.readInputStreamToString(
|
||||
jsonStream, jsonStream.available(), { charset : "UTF-8" });
|
||||
let roots = JSON.parse(bookmarkJSON).roots;
|
||||
|
||||
// Importing bookmark bar items
|
||||
if (roots.bookmark_bar.children &&
|
||||
roots.bookmark_bar.children.length > 0) {
|
||||
// Toolbar
|
||||
let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
parentGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
|
||||
}
|
||||
yield insertBookmarkItems(parentGuid, roots.bookmark_bar.children, errorGatherer);
|
||||
}
|
||||
|
||||
// Importing bookmark menu items
|
||||
if (roots.other.children &&
|
||||
roots.other.children.length > 0) {
|
||||
// Bookmark menu
|
||||
let parentGuid = PlacesUtils.bookmarks.menuGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
parentGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
|
||||
}
|
||||
yield insertBookmarkItems(parentGuid, roots.other.children, errorGatherer);
|
||||
}
|
||||
if (gotErrors) {
|
||||
throw new Error("The migration included errors.");
|
||||
}
|
||||
}.bind(this)).then(() => aCallback(true),
|
||||
() => aCallback(false));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GetHistoryResource(aProfileFolder) {
|
||||
let historyFile = aProfileFolder.clone();
|
||||
historyFile.append("History");
|
||||
if (!historyFile.exists())
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: MigrationUtils.resourceTypes.HISTORY,
|
||||
|
||||
migrate(aCallback) {
|
||||
Task.spawn(function* () {
|
||||
const MAX_AGE_IN_DAYS = Services.prefs.getIntPref("browser.migrate.chrome.history.maxAgeInDays");
|
||||
const LIMIT = Services.prefs.getIntPref("browser.migrate.chrome.history.limit");
|
||||
|
||||
let query = "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
|
||||
if (MAX_AGE_IN_DAYS) {
|
||||
let maxAge = dateToChromeTime(Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
|
||||
query += " AND last_visit_time > " + maxAge;
|
||||
}
|
||||
if (LIMIT) {
|
||||
query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
|
||||
}
|
||||
|
||||
let rows =
|
||||
yield MigrationUtils.getRowsFromDBWithoutLocks(historyFile.path, "Chrome history", query);
|
||||
let places = [];
|
||||
for (let row of rows) {
|
||||
try {
|
||||
// if having typed_count, we changes transition type to typed.
|
||||
let transType = PlacesUtils.history.TRANSITION_LINK;
|
||||
if (row.getResultByName("typed_count") > 0)
|
||||
transType = PlacesUtils.history.TRANSITION_TYPED;
|
||||
|
||||
places.push({
|
||||
uri: NetUtil.newURI(row.getResultByName("url")),
|
||||
title: row.getResultByName("title"),
|
||||
visits: [{
|
||||
transitionType: transType,
|
||||
visitDate: chromeTimeToDate(
|
||||
row.getResultByName(
|
||||
"last_visit_time")) * 1000,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (places.length > 0) {
|
||||
yield new Promise((resolve, reject) => {
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult: function() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError: function() {},
|
||||
handleCompletion: function() {
|
||||
if (this._success) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Couldn't add visits"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}).then(() => { aCallback(true) },
|
||||
ex => {
|
||||
Cu.reportError(ex);
|
||||
aCallback(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GetCookiesResource(aProfileFolder) {
|
||||
let cookiesFile = aProfileFolder.clone();
|
||||
cookiesFile.append("Cookies");
|
||||
if (!cookiesFile.exists())
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: MigrationUtils.resourceTypes.COOKIES,
|
||||
|
||||
migrate: Task.async(function* (aCallback) {
|
||||
// We don't support decrypting cookies yet so only import plaintext ones.
|
||||
let rows = yield MigrationUtils.getRowsFromDBWithoutLocks(cookiesFile.path, "Chrome cookies",
|
||||
`SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value
|
||||
FROM cookies
|
||||
WHERE length(encrypted_value) = 0`).catch(ex => {
|
||||
Cu.reportError(ex);
|
||||
aCallback(false);
|
||||
});
|
||||
// If the promise was rejected we will have already called aCallback,
|
||||
// so we can just return here.
|
||||
if (!rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let row of rows) {
|
||||
let host_key = row.getResultByName("host_key");
|
||||
if (host_key.match(/^\./)) {
|
||||
// 1st character of host_key may be ".", so we have to remove it
|
||||
host_key = host_key.substr(1);
|
||||
}
|
||||
|
||||
try {
|
||||
let expiresUtc =
|
||||
chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
|
||||
Services.cookies.add(host_key,
|
||||
row.getResultByName("path"),
|
||||
row.getResultByName("name"),
|
||||
row.getResultByName("value"),
|
||||
row.getResultByName("secure"),
|
||||
row.getResultByName("httponly"),
|
||||
false,
|
||||
parseInt(expiresUtc),
|
||||
{});
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
aCallback(true);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function GetWindowsPasswordsResource(aProfileFolder) {
|
||||
let loginFile = aProfileFolder.clone();
|
||||
loginFile.append("Login Data");
|
||||
if (!loginFile.exists())
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: MigrationUtils.resourceTypes.PASSWORDS,
|
||||
|
||||
migrate: Task.async(function* (aCallback) {
|
||||
let rows = yield MigrationUtils.getRowsFromDBWithoutLocks(loginFile.path, "Chrome passwords",
|
||||
`SELECT origin_url, action_url, username_element, username_value,
|
||||
password_element, password_value, signon_realm, scheme, date_created,
|
||||
times_used FROM logins WHERE blacklisted_by_user = 0`).catch(ex => {
|
||||
Cu.reportError(ex);
|
||||
aCallback(false);
|
||||
});
|
||||
// If the promise was rejected we will have already called aCallback,
|
||||
// so we can just return here.
|
||||
if (!rows) {
|
||||
return;
|
||||
}
|
||||
let crypto = new OSCrypto();
|
||||
|
||||
for (let row of rows) {
|
||||
try {
|
||||
let origin_url = NetUtil.newURI(row.getResultByName("origin_url"));
|
||||
// Ignore entries for non-http(s)/ftp URLs because we likely can't
|
||||
// use them anyway.
|
||||
const kValidSchemes = new Set(["https", "http", "ftp"]);
|
||||
if (!kValidSchemes.has(origin_url.scheme)) {
|
||||
continue;
|
||||
}
|
||||
let loginInfo = {
|
||||
username: row.getResultByName("username_value"),
|
||||
password: crypto.
|
||||
decryptData(crypto.arrayToString(row.getResultByName("password_value")),
|
||||
null),
|
||||
hostname: origin_url.prePath,
|
||||
formSubmitURL: null,
|
||||
httpRealm: null,
|
||||
usernameElement: row.getResultByName("username_element"),
|
||||
passwordElement: row.getResultByName("password_element"),
|
||||
timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(),
|
||||
timesUsed: row.getResultByName("times_used") + 0,
|
||||
};
|
||||
|
||||
switch (row.getResultByName("scheme")) {
|
||||
case AUTH_TYPE.SCHEME_HTML:
|
||||
let action_url = NetUtil.newURI(row.getResultByName("action_url"));
|
||||
if (!kValidSchemes.has(action_url.scheme)) {
|
||||
continue; // This continues the outer for loop.
|
||||
}
|
||||
loginInfo.formSubmitURL = action_url.prePath;
|
||||
break;
|
||||
case AUTH_TYPE.SCHEME_BASIC:
|
||||
case AUTH_TYPE.SCHEME_DIGEST:
|
||||
// signon_realm format is URIrealm, so we need remove URI
|
||||
loginInfo.httpRealm = row.getResultByName("signon_realm")
|
||||
.substring(loginInfo.hostname.length + 1);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Login data scheme type not supported: " +
|
||||
row.getResultByName("scheme"));
|
||||
}
|
||||
MigrationUtils.insertLoginWrapper(loginInfo);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
crypto.finalize();
|
||||
aCallback(true);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator";
|
||||
ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
|
||||
ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
|
||||
|
||||
|
||||
/**
|
||||
* Chromium migration
|
||||
**/
|
||||
function ChromiumProfileMigrator() {
|
||||
let chromiumUserDataFolder = getDataFolder(["Chromium"], ["Chromium"], ["chromium"]);
|
||||
this._chromeUserDataFolder = chromiumUserDataFolder.exists() ? chromiumUserDataFolder : null;
|
||||
}
|
||||
|
||||
ChromiumProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
|
||||
ChromiumProfileMigrator.prototype.classDescription = "Chromium Profile Migrator";
|
||||
ChromiumProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium";
|
||||
ChromiumProfileMigrator.prototype.classID = Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}");
|
||||
|
||||
var componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator];
|
||||
|
||||
/**
|
||||
* Chrome Canary
|
||||
* Not available on Linux
|
||||
**/
|
||||
function CanaryProfileMigrator() {
|
||||
let chromeUserDataFolder = getDataFolder(["Google", "Chrome SxS"], ["Google", "Chrome Canary"]);
|
||||
this._chromeUserDataFolder = chromeUserDataFolder.exists() ? chromeUserDataFolder : null;
|
||||
}
|
||||
CanaryProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
|
||||
CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator";
|
||||
CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary";
|
||||
CanaryProfileMigrator.prototype.classID = Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}");
|
||||
|
||||
if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
|
||||
componentsArray.push(CanaryProfileMigrator);
|
||||
}
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray);
|
||||
590
browser/components/migration/ESEDBReader.jsm
Normal file
590
browser/components/migration/ESEDBReader.jsm
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["ESEDBReader"]; /* exported ESEDBReader */
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
|
||||
let consoleOptions = {
|
||||
maxLogLevelPref: "browser.esedbreader.loglevel",
|
||||
prefix: "ESEDBReader",
|
||||
};
|
||||
return new ConsoleAPI(consoleOptions);
|
||||
});
|
||||
|
||||
// We have a globally unique identifier for ESE instances. A new one
|
||||
// is used for each different database opened.
|
||||
let gESEInstanceCounter = 0;
|
||||
|
||||
// We limit the length of strings that we read from databases.
|
||||
const MAX_STR_LENGTH = 64 * 1024;
|
||||
|
||||
// Kernel-related types:
|
||||
const KERNEL = {};
|
||||
KERNEL.FILETIME = new ctypes.StructType("FILETIME", [
|
||||
{dwLowDateTime: ctypes.uint32_t},
|
||||
{dwHighDateTime: ctypes.uint32_t}
|
||||
]);
|
||||
KERNEL.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
|
||||
{wYear: ctypes.uint16_t},
|
||||
{wMonth: ctypes.uint16_t},
|
||||
{wDayOfWeek: ctypes.uint16_t},
|
||||
{wDay: ctypes.uint16_t},
|
||||
{wHour: ctypes.uint16_t},
|
||||
{wMinute: ctypes.uint16_t},
|
||||
{wSecond: ctypes.uint16_t},
|
||||
{wMilliseconds: ctypes.uint16_t}
|
||||
]);
|
||||
|
||||
// DB column types, cribbed from the ESE header
|
||||
var COLUMN_TYPES = {
|
||||
JET_coltypBit: 1, /* True, False, or NULL */
|
||||
JET_coltypUnsignedByte: 2, /* 1-byte integer, unsigned */
|
||||
JET_coltypShort: 3, /* 2-byte integer, signed */
|
||||
JET_coltypLong: 4, /* 4-byte integer, signed */
|
||||
JET_coltypCurrency: 5, /* 8 byte integer, signed */
|
||||
JET_coltypIEEESingle: 6, /* 4-byte IEEE single precision */
|
||||
JET_coltypIEEEDouble: 7, /* 8-byte IEEE double precision */
|
||||
JET_coltypDateTime: 8, /* Integral date, fractional time */
|
||||
JET_coltypBinary: 9, /* Binary data, < 255 bytes */
|
||||
JET_coltypText: 10, /* ANSI text, case insensitive, < 255 bytes */
|
||||
JET_coltypLongBinary: 11, /* Binary data, long value */
|
||||
JET_coltypLongText: 12, /* ANSI text, long value */
|
||||
|
||||
JET_coltypUnsignedLong: 14, /* 4-byte unsigned integer */
|
||||
JET_coltypLongLong: 15, /* 8-byte signed integer */
|
||||
JET_coltypGUID: 16, /* 16-byte globally unique identifier */
|
||||
};
|
||||
|
||||
// Not very efficient, but only used for error messages
|
||||
function getColTypeName(numericValue) {
|
||||
return Object.keys(COLUMN_TYPES).find(t => COLUMN_TYPES[t] == numericValue) || "unknown";
|
||||
}
|
||||
|
||||
// All type constants and method wrappers go on this object:
|
||||
const ESE = {};
|
||||
ESE.JET_ERR = ctypes.long;
|
||||
ESE.JET_PCWSTR = ctypes.char16_t.ptr;
|
||||
// The ESE header calls this JET_API_PTR, but because it isn't ever used as a
|
||||
// pointer and because OS.File code implies that the name you give a type
|
||||
// matters, I opted for a different name.
|
||||
// Note that this is defined differently on 32 vs. 64-bit in the header.
|
||||
ESE.JET_API_ITEM = ctypes.voidptr_t.size == 4 ? ctypes.unsigned_long : ctypes.uint64_t;
|
||||
ESE.JET_INSTANCE = ESE.JET_API_ITEM;
|
||||
ESE.JET_SESID = ESE.JET_API_ITEM;
|
||||
ESE.JET_TABLEID = ESE.JET_API_ITEM;
|
||||
ESE.JET_COLUMNID = ctypes.unsigned_long;
|
||||
ESE.JET_GRBIT = ctypes.unsigned_long;
|
||||
ESE.JET_COLTYP = ctypes.unsigned_long;
|
||||
ESE.JET_DBID = ctypes.unsigned_long;
|
||||
|
||||
ESE.JET_COLUMNDEF = new ctypes.StructType("JET_COLUMNDEF", [
|
||||
{"cbStruct": ctypes.unsigned_long},
|
||||
{"columnid": ESE.JET_COLUMNID },
|
||||
{"coltyp": ESE.JET_COLTYP },
|
||||
{"wCountry": ctypes.unsigned_short }, // sepcifies the country/region for the column definition
|
||||
{"langid": ctypes.unsigned_short },
|
||||
{"cp": ctypes.unsigned_short },
|
||||
{"wCollate": ctypes.unsigned_short }, /* Must be 0 */
|
||||
{"cbMax": ctypes.unsigned_long },
|
||||
{"grbit": ESE.JET_GRBIT }
|
||||
]);
|
||||
|
||||
// Track open databases
|
||||
let gOpenDBs = new Map();
|
||||
|
||||
// Track open libraries
|
||||
let gLibs = {};
|
||||
this.ESE = ESE; // Required for tests.
|
||||
this.KERNEL = KERNEL; // ditto
|
||||
this.gLibs = gLibs; // ditto
|
||||
|
||||
function convertESEError(errorCode) {
|
||||
switch (errorCode) {
|
||||
case -1213 /* JET_errPageSizeMismatch */:
|
||||
case -1002 /* JET_errInvalidName*/:
|
||||
case -1507 /* JET_errColumnNotFound */:
|
||||
// The DB format has changed and we haven't updated this migration code:
|
||||
return "The database format has changed, error code: " + errorCode;
|
||||
case -1207 /* JET_errDatabaseLocked */:
|
||||
case -1302 /* JET_errTableLocked */:
|
||||
return "The database or table is locked, error code: " + errorCode;
|
||||
case -1809 /* JET_errPermissionDenied*/:
|
||||
case -1907 /* JET_errAccessDenied */:
|
||||
return "Access or permission denied, error code: " + errorCode;
|
||||
case -1044 /* JET_errInvalidFilename */:
|
||||
return "Invalid file name";
|
||||
case -1811 /* JET_errFileNotFound */:
|
||||
return "File not found";
|
||||
case -550 /* JET_errDatabaseDirtyShutdown */:
|
||||
return "Database in dirty shutdown state (without the requisite logs?)";
|
||||
case -514 /* JET_errBadLogVersion */:
|
||||
return "Database log version does not match the version of ESE in use.";
|
||||
default:
|
||||
return "Unknown error: " + errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
function handleESEError(method, methodName, shouldThrow = true, errorLog = true) {
|
||||
return function () {
|
||||
let rv;
|
||||
try {
|
||||
rv = method.apply(null, arguments);
|
||||
} catch (ex) {
|
||||
log.error("Error calling into ctypes method", methodName, ex);
|
||||
throw ex;
|
||||
}
|
||||
let resultCode = parseInt(rv.toString(10), 10);
|
||||
if (resultCode < 0) {
|
||||
if (errorLog) {
|
||||
log.error("Got error " + resultCode + " calling " + methodName);
|
||||
}
|
||||
if (shouldThrow) {
|
||||
throw new Error(convertESEError(rv));
|
||||
}
|
||||
} else if (resultCode > 0 && errorLog) {
|
||||
log.warn("Got warning " + resultCode + " calling " + methodName);
|
||||
}
|
||||
return resultCode;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function declareESEFunction(methodName, ...args) {
|
||||
let declaration = ["Jet" + methodName, ctypes.winapi_abi, ESE.JET_ERR].concat(args);
|
||||
let ctypeMethod = gLibs.ese.declare.apply(gLibs.ese, declaration);
|
||||
ESE[methodName] = handleESEError(ctypeMethod, methodName);
|
||||
ESE["FailSafe" + methodName] = handleESEError(ctypeMethod, methodName, false);
|
||||
ESE["Manual" + methodName] = handleESEError(ctypeMethod, methodName, false, false);
|
||||
}
|
||||
|
||||
function declareESEFunctions() {
|
||||
declareESEFunction("GetDatabaseFileInfoW", ESE.JET_PCWSTR, ctypes.voidptr_t,
|
||||
ctypes.unsigned_long, ctypes.unsigned_long);
|
||||
|
||||
declareESEFunction("GetSystemParameterW", ESE.JET_INSTANCE, ESE.JET_SESID,
|
||||
ctypes.unsigned_long, ESE.JET_API_ITEM.ptr,
|
||||
ESE.JET_PCWSTR, ctypes.unsigned_long);
|
||||
declareESEFunction("SetSystemParameterW", ESE.JET_INSTANCE.ptr,
|
||||
ESE.JET_SESID, ctypes.unsigned_long, ESE.JET_API_ITEM,
|
||||
ESE.JET_PCWSTR);
|
||||
declareESEFunction("CreateInstanceW", ESE.JET_INSTANCE.ptr, ESE.JET_PCWSTR);
|
||||
declareESEFunction("Init", ESE.JET_INSTANCE.ptr);
|
||||
|
||||
declareESEFunction("BeginSessionW", ESE.JET_INSTANCE, ESE.JET_SESID.ptr,
|
||||
ESE.JET_PCWSTR, ESE.JET_PCWSTR);
|
||||
declareESEFunction("AttachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
|
||||
ESE.JET_GRBIT);
|
||||
declareESEFunction("DetachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR);
|
||||
declareESEFunction("OpenDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
|
||||
ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
|
||||
declareESEFunction("OpenTableW", ESE.JET_SESID, ESE.JET_DBID, ESE.JET_PCWSTR,
|
||||
ctypes.voidptr_t, ctypes.unsigned_long, ESE.JET_GRBIT,
|
||||
ESE.JET_TABLEID.ptr);
|
||||
|
||||
declareESEFunction("GetColumnInfoW", ESE.JET_SESID, ESE.JET_DBID,
|
||||
ESE.JET_PCWSTR, ESE.JET_PCWSTR, ctypes.voidptr_t,
|
||||
ctypes.unsigned_long, ctypes.unsigned_long);
|
||||
|
||||
declareESEFunction("Move", ESE.JET_SESID, ESE.JET_TABLEID, ctypes.long,
|
||||
ESE.JET_GRBIT);
|
||||
|
||||
declareESEFunction("RetrieveColumn", ESE.JET_SESID, ESE.JET_TABLEID,
|
||||
ESE.JET_COLUMNID, ctypes.voidptr_t, ctypes.unsigned_long,
|
||||
ctypes.unsigned_long.ptr, ESE.JET_GRBIT, ctypes.voidptr_t);
|
||||
|
||||
declareESEFunction("CloseTable", ESE.JET_SESID, ESE.JET_TABLEID);
|
||||
declareESEFunction("CloseDatabase", ESE.JET_SESID, ESE.JET_DBID,
|
||||
ESE.JET_GRBIT);
|
||||
|
||||
declareESEFunction("EndSession", ESE.JET_SESID, ESE.JET_GRBIT);
|
||||
|
||||
declareESEFunction("Term", ESE.JET_INSTANCE);
|
||||
}
|
||||
|
||||
function unloadLibraries() {
|
||||
log.debug("Unloading");
|
||||
if (gOpenDBs.size) {
|
||||
log.error("Shouldn't unload libraries before DBs are closed!");
|
||||
for (let db of gOpenDBs.values()) {
|
||||
db._close();
|
||||
}
|
||||
}
|
||||
for (let k of Object.keys(ESE)) {
|
||||
delete ESE[k];
|
||||
}
|
||||
gLibs.ese.close();
|
||||
gLibs.kernel.close();
|
||||
delete gLibs.ese;
|
||||
delete gLibs.kernel;
|
||||
}
|
||||
|
||||
function loadLibraries() {
|
||||
Services.obs.addObserver(unloadLibraries, "xpcom-shutdown", false);
|
||||
gLibs.ese = ctypes.open("esent.dll");
|
||||
gLibs.kernel = ctypes.open("kernel32.dll");
|
||||
KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
|
||||
ctypes.default_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
|
||||
|
||||
declareESEFunctions();
|
||||
}
|
||||
|
||||
function ESEDB(rootPath, dbPath, logPath) {
|
||||
log.info("Created db");
|
||||
this.rootPath = rootPath;
|
||||
this.dbPath = dbPath;
|
||||
this.logPath = logPath;
|
||||
this._references = 0;
|
||||
this._init();
|
||||
}
|
||||
|
||||
ESEDB.prototype = {
|
||||
rootPath: null,
|
||||
dbPath: null,
|
||||
logPath: null,
|
||||
_opened: false,
|
||||
_attached: false,
|
||||
_sessionCreated: false,
|
||||
_instanceCreated: false,
|
||||
_dbId: null,
|
||||
_sessionId: null,
|
||||
_instanceId: null,
|
||||
|
||||
_init() {
|
||||
if (!gLibs.ese) {
|
||||
loadLibraries();
|
||||
}
|
||||
this.incrementReferenceCounter();
|
||||
this._internalOpen();
|
||||
},
|
||||
|
||||
_internalOpen() {
|
||||
try {
|
||||
let dbinfo = new ctypes.unsigned_long();
|
||||
ESE.GetDatabaseFileInfoW(this.dbPath, dbinfo.address(),
|
||||
ctypes.unsigned_long.size, 17);
|
||||
|
||||
let pageSize = ctypes.UInt64.lo(dbinfo.value);
|
||||
ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/,
|
||||
pageSize, null);
|
||||
|
||||
this._instanceId = new ESE.JET_INSTANCE();
|
||||
ESE.CreateInstanceW(this._instanceId.address(),
|
||||
"firefox-dbreader-" + (gESEInstanceCounter++));
|
||||
this._instanceCreated = true;
|
||||
|
||||
ESE.SetSystemParameterW(this._instanceId.address(), 0,
|
||||
0 /* JET_paramSystemPath*/, 0, this.rootPath);
|
||||
ESE.SetSystemParameterW(this._instanceId.address(), 0,
|
||||
1 /* JET_paramTempPath */, 0, this.rootPath);
|
||||
ESE.SetSystemParameterW(this._instanceId.address(), 0,
|
||||
2 /* JET_paramLogFilePath*/, 0, this.logPath);
|
||||
|
||||
// Shouldn't try to call JetTerm if the following call fails.
|
||||
this._instanceCreated = false;
|
||||
ESE.Init(this._instanceId.address());
|
||||
this._instanceCreated = true;
|
||||
this._sessionId = new ESE.JET_SESID();
|
||||
ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null,
|
||||
null);
|
||||
this._sessionCreated = true;
|
||||
|
||||
const JET_bitDbReadOnly = 1;
|
||||
ESE.AttachDatabaseW(this._sessionId, this.dbPath, JET_bitDbReadOnly);
|
||||
this._attached = true;
|
||||
this._dbId = new ESE.JET_DBID();
|
||||
ESE.OpenDatabaseW(this._sessionId, this.dbPath, null,
|
||||
this._dbId.address(), JET_bitDbReadOnly);
|
||||
this._opened = true;
|
||||
} catch (ex) {
|
||||
try {
|
||||
this._close();
|
||||
} catch (innerException) {
|
||||
Cu.reportError(innerException);
|
||||
}
|
||||
// Make sure caller knows we failed.
|
||||
throw ex;
|
||||
}
|
||||
gOpenDBs.set(this.dbPath, this);
|
||||
},
|
||||
|
||||
checkForColumn(tableName, columnName) {
|
||||
if (!this._opened) {
|
||||
throw new Error("The database was closed!");
|
||||
}
|
||||
|
||||
let columnInfo;
|
||||
try {
|
||||
columnInfo = this._getColumnInfo(tableName, [{name: columnName}]);
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
return columnInfo[0];
|
||||
},
|
||||
|
||||
tableExists(tableName) {
|
||||
if (!this._opened) {
|
||||
throw new Error("The database was closed!");
|
||||
}
|
||||
|
||||
let tableId = new ESE.JET_TABLEID();
|
||||
let rv = ESE.ManualOpenTableW(this._sessionId, this._dbId, tableName, null,
|
||||
0, 4 /* JET_bitTableReadOnly */,
|
||||
tableId.address());
|
||||
if (rv == -1305 /* JET_errObjectNotFound */) {
|
||||
return false;
|
||||
}
|
||||
if (rv < 0) {
|
||||
log.error("Got error " + rv + " calling OpenTableW");
|
||||
throw new Error(convertESEError(rv));
|
||||
}
|
||||
|
||||
if (rv > 0) {
|
||||
log.error("Got warning " + rv + " calling OpenTableW");
|
||||
}
|
||||
ESE.FailSafeCloseTable(this._sessionId, tableId);
|
||||
return true;
|
||||
},
|
||||
|
||||
tableItems: function*(tableName, columns) {
|
||||
if (!this._opened) {
|
||||
throw new Error("The database was closed!");
|
||||
}
|
||||
|
||||
let tableOpened = false;
|
||||
let tableId;
|
||||
try {
|
||||
tableId = this._openTable(tableName);
|
||||
tableOpened = true;
|
||||
|
||||
let columnInfo = this._getColumnInfo(tableName, columns);
|
||||
|
||||
let rv = ESE.ManualMove(this._sessionId, tableId,
|
||||
-2147483648 /* JET_MoveFirst */, 0);
|
||||
if (rv == -1603 /* JET_errNoCurrentRecord */) {
|
||||
// There are no rows in the table.
|
||||
this._closeTable(tableId);
|
||||
return;
|
||||
}
|
||||
if (rv != 0) {
|
||||
throw new Error(convertESEError(rv));
|
||||
}
|
||||
|
||||
do {
|
||||
let rowContents = {};
|
||||
for (let column of columnInfo) {
|
||||
let [buffer, bufferSize] = this._getBufferForColumn(column);
|
||||
// We handle errors manually so we accurately deal with NULL values.
|
||||
let err = ESE.ManualRetrieveColumn(this._sessionId, tableId,
|
||||
column.id, buffer.address(),
|
||||
bufferSize, null, 0, null);
|
||||
rowContents[column.name] = this._convertResult(column, buffer, err);
|
||||
}
|
||||
yield rowContents;
|
||||
} while (ESE.ManualMove(this._sessionId, tableId, 1 /* JET_MoveNext */, 0) === 0);
|
||||
} catch (ex) {
|
||||
if (tableOpened) {
|
||||
this._closeTable(tableId);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
this._closeTable(tableId);
|
||||
},
|
||||
|
||||
_openTable(tableName) {
|
||||
let tableId = new ESE.JET_TABLEID();
|
||||
ESE.OpenTableW(this._sessionId, this._dbId, tableName, null,
|
||||
0, 4 /* JET_bitTableReadOnly */, tableId.address());
|
||||
return tableId;
|
||||
},
|
||||
|
||||
_getBufferForColumn(column) {
|
||||
let buffer;
|
||||
if (column.type == "string") {
|
||||
let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
|
||||
// size on the column is in bytes, 2 bytes to a wchar, so:
|
||||
let charCount = column.dbSize >> 1;
|
||||
buffer = new wchar_tArray(charCount);
|
||||
} else if (column.type == "boolean") {
|
||||
buffer = new ctypes.uint8_t();
|
||||
} else if (column.type == "date") {
|
||||
buffer = new KERNEL.FILETIME();
|
||||
} else if (column.type == "guid") {
|
||||
let byteArray = ctypes.ArrayType(ctypes.uint8_t);
|
||||
buffer = new byteArray(column.dbSize);
|
||||
} else {
|
||||
throw new Error("Unknown type " + column.type);
|
||||
}
|
||||
return [buffer, buffer.constructor.size];
|
||||
},
|
||||
|
||||
_convertResult(column, buffer, err) {
|
||||
if (err != 0) {
|
||||
if (err == 1004) {
|
||||
// Deal with null values:
|
||||
buffer = null;
|
||||
} else {
|
||||
Cu.reportError("Unexpected JET error: " + err + ";" + " retrieving value for column " + column.name);
|
||||
throw new Error(convertESEError(err));
|
||||
}
|
||||
}
|
||||
if (column.type == "string") {
|
||||
return buffer ? buffer.readString() : "";
|
||||
}
|
||||
if (column.type == "boolean") {
|
||||
return buffer ? (buffer.value == 255) : false;
|
||||
}
|
||||
if (column.type == "guid") {
|
||||
if (buffer.length != 16) {
|
||||
Cu.reportError("Buffer size for guid field " + column.id + " should have been 16!");
|
||||
return "";
|
||||
}
|
||||
let rv = "{";
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if (i == 4 || i == 6 || i == 8 || i == 10) {
|
||||
rv += "-";
|
||||
}
|
||||
let byteValue = buffer.addressOfElement(i).contents;
|
||||
// Ensure there's a leading 0
|
||||
rv += ("0" + byteValue.toString(16)).substr(-2);
|
||||
}
|
||||
return rv + "}";
|
||||
}
|
||||
if (column.type == "date") {
|
||||
if (!buffer) {
|
||||
return null;
|
||||
}
|
||||
let systemTime = new KERNEL.SYSTEMTIME();
|
||||
let result = KERNEL.FileTimeToSystemTime(buffer.address(), systemTime.address());
|
||||
if (result == 0) {
|
||||
throw new Error(ctypes.winLastError);
|
||||
}
|
||||
|
||||
// System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
|
||||
// then divide by 1000 to get seconds, and round down:
|
||||
return new Date(Date.UTC(systemTime.wYear,
|
||||
systemTime.wMonth - 1,
|
||||
systemTime.wDay,
|
||||
systemTime.wHour,
|
||||
systemTime.wMinute,
|
||||
systemTime.wSecond,
|
||||
systemTime.wMilliseconds));
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
_getColumnInfo(tableName, columns) {
|
||||
let rv = [];
|
||||
for (let column of columns) {
|
||||
let columnInfoFromDB = new ESE.JET_COLUMNDEF();
|
||||
ESE.GetColumnInfoW(this._sessionId, this._dbId, tableName, column.name,
|
||||
columnInfoFromDB.address(), ESE.JET_COLUMNDEF.size, 0 /* JET_ColInfo */);
|
||||
let dbType = parseInt(columnInfoFromDB.coltyp.toString(10), 10);
|
||||
let dbSize = parseInt(columnInfoFromDB.cbMax.toString(10), 10);
|
||||
if (column.type == "string") {
|
||||
if (dbType != COLUMN_TYPES.JET_coltypLongText &&
|
||||
dbType != COLUMN_TYPES.JET_coltypText) {
|
||||
throw new Error("Invalid column type for column " + column.name +
|
||||
"; expected text type, got type " + getColTypeName(dbType));
|
||||
}
|
||||
if (dbSize > MAX_STR_LENGTH) {
|
||||
throw new Error("Column " + column.name + " has more than 64k data in it. This API is not designed to handle data that large.");
|
||||
}
|
||||
} else if (column.type == "boolean") {
|
||||
if (dbType != COLUMN_TYPES.JET_coltypBit) {
|
||||
throw new Error("Invalid column type for column " + column.name +
|
||||
"; expected bit type, got type " + getColTypeName(dbType));
|
||||
}
|
||||
} else if (column.type == "date") {
|
||||
if (dbType != COLUMN_TYPES.JET_coltypLongLong) {
|
||||
throw new Error("Invalid column type for column " + column.name +
|
||||
"; expected long long type, got type " + getColTypeName(dbType));
|
||||
}
|
||||
} else if (column.type == "guid") {
|
||||
if (dbType != COLUMN_TYPES.JET_coltypGUID) {
|
||||
throw new Error("Invalid column type for column " + column.name +
|
||||
"; expected guid type, got type " + getColTypeName(dbType));
|
||||
}
|
||||
} else if (column.type) {
|
||||
throw new Error("Unknown column type " + column.type + " requested for column " +
|
||||
column.name + ", don't know what to do.");
|
||||
}
|
||||
|
||||
rv.push({name: column.name, id: columnInfoFromDB.columnid, type: column.type, dbSize, dbType});
|
||||
}
|
||||
return rv;
|
||||
},
|
||||
|
||||
_closeTable(tableId) {
|
||||
ESE.FailSafeCloseTable(this._sessionId, tableId);
|
||||
},
|
||||
|
||||
_close() {
|
||||
this._internalClose();
|
||||
gOpenDBs.delete(this.dbPath);
|
||||
},
|
||||
|
||||
_internalClose() {
|
||||
if (this._opened) {
|
||||
log.debug("close db");
|
||||
ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
|
||||
log.debug("finished close db");
|
||||
this._opened = false;
|
||||
}
|
||||
if (this._attached) {
|
||||
log.debug("detach db");
|
||||
ESE.FailSafeDetachDatabaseW(this._sessionId, this.dbPath);
|
||||
this._attached = false;
|
||||
}
|
||||
if (this._sessionCreated) {
|
||||
log.debug("end session");
|
||||
ESE.FailSafeEndSession(this._sessionId, 0);
|
||||
this._sessionCreated = false;
|
||||
}
|
||||
if (this._instanceCreated) {
|
||||
log.debug("term");
|
||||
ESE.FailSafeTerm(this._instanceId);
|
||||
this._instanceCreated = false;
|
||||
}
|
||||
},
|
||||
|
||||
incrementReferenceCounter() {
|
||||
this._references++;
|
||||
},
|
||||
|
||||
decrementReferenceCounter() {
|
||||
this._references--;
|
||||
if (this._references <= 0) {
|
||||
this._close();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let ESEDBReader = {
|
||||
openDB(rootDir, dbFile, logDir) {
|
||||
let dbFilePath = dbFile.path;
|
||||
if (gOpenDBs.has(dbFilePath)) {
|
||||
let db = gOpenDBs.get(dbFilePath);
|
||||
db.incrementReferenceCounter();
|
||||
return db;
|
||||
}
|
||||
// ESE is really picky about the trailing slashes according to the docs,
|
||||
// so we do as we're told and ensure those are there:
|
||||
return new ESEDB(rootDir.path + "\\", dbFilePath, logDir.path + "\\");
|
||||
},
|
||||
|
||||
closeDB(db) {
|
||||
db.decrementReferenceCounter();
|
||||
},
|
||||
|
||||
COLUMN_TYPES,
|
||||
};
|
||||
|
||||
450
browser/components/migration/EdgeProfileMigrator.js
Normal file
450
browser/components/migration/EdgeProfileMigrator.js
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
|
||||
Cu.import("resource:///modules/MSMigrationUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ESEDBReader",
|
||||
"resource:///modules/ESEDBReader.jsm");
|
||||
|
||||
const kEdgeRegistryRoot = "SOFTWARE\\Classes\\Local Settings\\Software\\" +
|
||||
"Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\" +
|
||||
"microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge";
|
||||
const kEdgeDatabasePath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gEdgeDatabase", function() {
|
||||
let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder();
|
||||
if (!edgeDir) {
|
||||
return null;
|
||||
}
|
||||
edgeDir.appendRelativePath(kEdgeDatabasePath);
|
||||
if (!edgeDir.exists() || !edgeDir.isReadable() || !edgeDir.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
let expectedLocation = edgeDir.clone();
|
||||
expectedLocation.appendRelativePath("nouser1\\120712-0049\\DBStore\\spartan.edb");
|
||||
if (expectedLocation.exists() && expectedLocation.isReadable() && expectedLocation.isFile()) {
|
||||
return expectedLocation;
|
||||
}
|
||||
// We used to recurse into arbitrary subdirectories here, but that code
|
||||
// went unused, so it likely isn't necessary, even if we don't understand
|
||||
// where the magic folders above come from, they seem to be the same for
|
||||
// everyone. Just return null if they're not there:
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* Get rows from a table in the Edge DB as an array of JS objects.
|
||||
*
|
||||
* @param {String} tableName the name of the table to read.
|
||||
* @param {String[]|function} columns a list of column specifiers
|
||||
* (see ESEDBReader.jsm) or a function that
|
||||
* generates them based on the database
|
||||
* reference once opened.
|
||||
* @param {function} filterFn a function that is called for each row.
|
||||
* Only rows for which it returns a truthy
|
||||
* value are included in the result.
|
||||
* @param {nsIFile} dbFile the database file to use. Defaults to
|
||||
* the main Edge database.
|
||||
* @returns {Array} An array of row objects.
|
||||
*/
|
||||
function readTableFromEdgeDB(tableName, columns, filterFn, dbFile = gEdgeDatabase) {
|
||||
let database;
|
||||
let rows = [];
|
||||
try {
|
||||
let logFile = dbFile.parent;
|
||||
logFile.append("LogFiles");
|
||||
database = ESEDBReader.openDB(dbFile.parent, dbFile, logFile);
|
||||
|
||||
if (typeof columns == "function") {
|
||||
columns = columns(database);
|
||||
}
|
||||
|
||||
let tableReader = database.tableItems(tableName, columns);
|
||||
for (let row of tableReader) {
|
||||
if (filterFn(row)) {
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError("Failed to extract items from table " + tableName + " in Edge database at " +
|
||||
dbFile.path + " due to the following error: " + ex);
|
||||
// Deliberately make this fail so we expose failure in the UI:
|
||||
throw ex;
|
||||
} finally {
|
||||
if (database) {
|
||||
ESEDBReader.closeDB(database);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function EdgeTypedURLMigrator() {
|
||||
}
|
||||
|
||||
EdgeTypedURLMigrator.prototype = {
|
||||
type: MigrationUtils.resourceTypes.HISTORY,
|
||||
|
||||
get _typedURLs() {
|
||||
if (!this.__typedURLs) {
|
||||
this.__typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
|
||||
}
|
||||
return this.__typedURLs;
|
||||
},
|
||||
|
||||
get exists() {
|
||||
return this._typedURLs.size > 0;
|
||||
},
|
||||
|
||||
migrate: function(aCallback) {
|
||||
let typedURLs = this._typedURLs;
|
||||
let places = [];
|
||||
for (let [urlString, time] of typedURLs) {
|
||||
let uri;
|
||||
try {
|
||||
uri = Services.io.newURI(urlString, null, null);
|
||||
if (["http", "https", "ftp"].indexOf(uri.scheme) == -1) {
|
||||
continue;
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Note that the time will be in microseconds (PRTime),
|
||||
// and Date.now() returns milliseconds. Places expects PRTime,
|
||||
// so we multiply the Date.now return value to make up the difference.
|
||||
let visitDate = time || (Date.now() * 1000);
|
||||
places.push({
|
||||
uri,
|
||||
visits: [{ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
|
||||
visitDate}]
|
||||
});
|
||||
}
|
||||
|
||||
if (places.length == 0) {
|
||||
aCallback(typedURLs.size == 0);
|
||||
return;
|
||||
}
|
||||
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult: function() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError: function() {},
|
||||
handleCompletion: function() {
|
||||
aCallback(this._success);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function EdgeReadingListMigrator() {
|
||||
}
|
||||
|
||||
EdgeReadingListMigrator.prototype = {
|
||||
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||
|
||||
get exists() {
|
||||
return !!gEdgeDatabase;
|
||||
},
|
||||
|
||||
migrate(callback) {
|
||||
this._migrateReadingList(PlacesUtils.bookmarks.menuGuid).then(
|
||||
() => callback(true),
|
||||
ex => {
|
||||
Cu.reportError(ex);
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_migrateReadingList: Task.async(function*(parentGuid) {
|
||||
let columnFn = db => {
|
||||
let columns = [
|
||||
{name: "URL", type: "string"},
|
||||
{name: "Title", type: "string"},
|
||||
{name: "AddedDate", type: "date"}
|
||||
];
|
||||
|
||||
// Later versions have an IsDeleted column:
|
||||
let isDeletedColumn = db.checkForColumn("ReadingList", "IsDeleted");
|
||||
if (isDeletedColumn && isDeletedColumn.dbType == ESEDBReader.COLUMN_TYPES.JET_coltypBit) {
|
||||
columns.push({name: "IsDeleted", type: "boolean"});
|
||||
}
|
||||
return columns;
|
||||
};
|
||||
|
||||
let filterFn = row => {
|
||||
return !row.IsDeleted;
|
||||
};
|
||||
|
||||
let readingListItems = readTableFromEdgeDB("ReadingList", columnFn, filterFn);
|
||||
if (!readingListItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let destFolderGuid = yield this._ensureReadingListFolder(parentGuid);
|
||||
let exceptionThrown;
|
||||
for (let item of readingListItems) {
|
||||
let dateAdded = item.AddedDate || new Date();
|
||||
yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid: destFolderGuid, url: item.URL, title: item.Title, dateAdded
|
||||
}).catch(ex => {
|
||||
if (!exceptionThrown) {
|
||||
exceptionThrown = ex;
|
||||
}
|
||||
Cu.reportError(ex);
|
||||
});
|
||||
}
|
||||
if (exceptionThrown) {
|
||||
throw exceptionThrown;
|
||||
}
|
||||
}),
|
||||
|
||||
_ensureReadingListFolder: Task.async(function*(parentGuid) {
|
||||
if (!this.__readingListFolderGuid) {
|
||||
let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
|
||||
let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
|
||||
this.__readingListFolderGuid = (yield MigrationUtils.insertBookmarkWrapper(folderSpec)).guid;
|
||||
}
|
||||
return this.__readingListFolderGuid;
|
||||
}),
|
||||
};
|
||||
|
||||
function EdgeBookmarksMigrator(dbOverride) {
|
||||
this.dbOverride = dbOverride;
|
||||
}
|
||||
|
||||
EdgeBookmarksMigrator.prototype = {
|
||||
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||
|
||||
get db() { return this.dbOverride || gEdgeDatabase },
|
||||
|
||||
get TABLE_NAME() { return "Favorites" },
|
||||
|
||||
get exists() {
|
||||
if (!("_exists" in this)) {
|
||||
this._exists = !!this.db;
|
||||
}
|
||||
return this._exists;
|
||||
},
|
||||
|
||||
migrate(callback) {
|
||||
this._migrateBookmarks(PlacesUtils.bookmarks.menuGuid).then(
|
||||
() => callback(true),
|
||||
ex => {
|
||||
Cu.reportError(ex);
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_migrateBookmarks: Task.async(function*(rootGuid) {
|
||||
let {bookmarks, folderMap} = this._fetchBookmarksFromDB();
|
||||
if (!bookmarks.length) {
|
||||
return;
|
||||
}
|
||||
yield this._importBookmarks(bookmarks, folderMap, rootGuid);
|
||||
}),
|
||||
|
||||
_importBookmarks: Task.async(function*(bookmarks, folderMap, rootGuid) {
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
rootGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("Edge", rootGuid);
|
||||
}
|
||||
|
||||
let exceptionThrown;
|
||||
for (let bookmark of bookmarks) {
|
||||
// If this is a folder, we might have created it already to put other bookmarks in.
|
||||
if (bookmark.IsFolder && bookmark._guid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this is a folder, just create folders up to and including that folder.
|
||||
// Otherwise, create folders until we have a parent for this bookmark.
|
||||
// This avoids duplicating logic for the bookmarks bar.
|
||||
let folderId = bookmark.IsFolder ? bookmark.ItemId : bookmark.ParentId;
|
||||
let parentGuid = yield this._getGuidForFolder(folderId, folderMap, rootGuid).catch(ex => {
|
||||
if (!exceptionThrown) {
|
||||
exceptionThrown = ex;
|
||||
}
|
||||
Cu.reportError(ex);
|
||||
});
|
||||
|
||||
// If this was a folder, we're done with this item
|
||||
if (bookmark.IsFolder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parentGuid) {
|
||||
// If we couldn't sort out a parent, fall back to importing on the root:
|
||||
parentGuid = rootGuid;
|
||||
}
|
||||
let placesInfo = {
|
||||
parentGuid,
|
||||
url: bookmark.URL,
|
||||
dateAdded: bookmark.DateUpdated || new Date(),
|
||||
title: bookmark.Title,
|
||||
};
|
||||
|
||||
yield MigrationUtils.insertBookmarkWrapper(placesInfo).catch(ex => {
|
||||
if (!exceptionThrown) {
|
||||
exceptionThrown = ex;
|
||||
}
|
||||
Cu.reportError(ex);
|
||||
});
|
||||
}
|
||||
|
||||
if (exceptionThrown) {
|
||||
throw exceptionThrown;
|
||||
}
|
||||
}),
|
||||
|
||||
_fetchBookmarksFromDB() {
|
||||
let folderMap = new Map();
|
||||
let columns = [
|
||||
{name: "URL", type: "string"},
|
||||
{name: "Title", type: "string"},
|
||||
{name: "DateUpdated", type: "date"},
|
||||
{name: "IsFolder", type: "boolean"},
|
||||
{name: "IsDeleted", type: "boolean"},
|
||||
{name: "ParentId", type: "guid"},
|
||||
{name: "ItemId", type: "guid"}
|
||||
];
|
||||
let filterFn = row => {
|
||||
if (row.IsDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (row.IsFolder) {
|
||||
folderMap.set(row.ItemId, row);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
let bookmarks = readTableFromEdgeDB(this.TABLE_NAME, columns, filterFn, this.db);
|
||||
return {bookmarks, folderMap};
|
||||
},
|
||||
|
||||
_getGuidForFolder: Task.async(function*(folderId, folderMap, rootGuid) {
|
||||
// If the folderId is not known as a folder in the folder map, we assume
|
||||
// we just need the root
|
||||
if (!folderMap.has(folderId)) {
|
||||
return rootGuid;
|
||||
}
|
||||
let folder = folderMap.get(folderId);
|
||||
// If the folder already has a places guid, just return that.
|
||||
if (folder._guid) {
|
||||
return folder._guid;
|
||||
}
|
||||
|
||||
// Hacks! The bookmarks bar is special:
|
||||
if (folder.Title == "_Favorites_Bar_") {
|
||||
let toolbarGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
toolbarGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("Edge", toolbarGuid);
|
||||
}
|
||||
folder._guid = toolbarGuid;
|
||||
return folder._guid;
|
||||
}
|
||||
// Otherwise, get the right parent guid recursively:
|
||||
let parentGuid = yield this._getGuidForFolder(folder.ParentId, folderMap, rootGuid);
|
||||
let folderInfo = {
|
||||
title: folder.Title,
|
||||
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
dateAdded: folder.DateUpdated || new Date(),
|
||||
parentGuid,
|
||||
};
|
||||
// and add ourselves as a kid, and return the guid we got.
|
||||
let parentBM = yield MigrationUtils.insertBookmarkWrapper(folderInfo);
|
||||
folder._guid = parentBM.guid;
|
||||
return folder._guid;
|
||||
}),
|
||||
};
|
||||
|
||||
function EdgeProfileMigrator() {
|
||||
this.wrappedJSObject = this;
|
||||
}
|
||||
|
||||
EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
|
||||
|
||||
EdgeProfileMigrator.prototype.getESEMigratorForTesting = function(dbOverride) {
|
||||
return new EdgeBookmarksMigrator(dbOverride);
|
||||
};
|
||||
|
||||
EdgeProfileMigrator.prototype.getResources = function() {
|
||||
let resources = [
|
||||
new EdgeBookmarksMigrator(),
|
||||
MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
|
||||
new EdgeTypedURLMigrator(),
|
||||
new EdgeReadingListMigrator(),
|
||||
];
|
||||
let windowsVaultFormPasswordsMigrator =
|
||||
MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
|
||||
windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords";
|
||||
resources.push(windowsVaultFormPasswordsMigrator);
|
||||
return resources.filter(r => r.exists);
|
||||
};
|
||||
|
||||
EdgeProfileMigrator.prototype.getLastUsedDate = function() {
|
||||
// Don't do this if we don't have a single profile (see the comment for
|
||||
// sourceProfiles) or if we can't find the database file:
|
||||
if (this.sourceProfiles !== null || !gEdgeDatabase) {
|
||||
return Promise.resolve(new Date(0));
|
||||
}
|
||||
let logFilePath = OS.Path.join(gEdgeDatabase.parent.path, "LogFiles", "edb.log");
|
||||
let dbPath = gEdgeDatabase.path;
|
||||
let cookieMigrator = MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE);
|
||||
let cookiePaths = cookieMigrator._cookiesFolders.map(f => f.path);
|
||||
let datePromises = [logFilePath, dbPath, ... cookiePaths].map(path => {
|
||||
return OS.File.stat(path).catch(() => null).then(info => {
|
||||
return info ? info.lastModificationDate : 0;
|
||||
});
|
||||
});
|
||||
datePromises.push(new Promise(resolve => {
|
||||
let typedURLs = new Map();
|
||||
try {
|
||||
typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
|
||||
} catch (ex) {}
|
||||
let times = [0, ... typedURLs.values()];
|
||||
resolve(Math.max.apply(Math, times));
|
||||
}));
|
||||
return Promise.all(datePromises).then(dates => {
|
||||
return new Date(Math.max.apply(Math, dates));
|
||||
});
|
||||
};
|
||||
|
||||
/* Somewhat counterintuitively, this returns:
|
||||
* - |null| to indicate "There is only 1 (default) profile" (on win10+)
|
||||
* - |[]| to indicate "There are no profiles" (on <=win8.1) which will avoid using this migrator.
|
||||
* See MigrationUtils.jsm for slightly more info on how sourceProfiles is used.
|
||||
*/
|
||||
EdgeProfileMigrator.prototype.__defineGetter__("sourceProfiles", function() {
|
||||
let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10");
|
||||
return isWin10OrHigher ? null : [];
|
||||
});
|
||||
|
||||
EdgeProfileMigrator.prototype.__defineGetter__("sourceLocked", function() {
|
||||
// There is an exclusive lock on some databases. Assume they are locked for now.
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator";
|
||||
EdgeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=edge";
|
||||
EdgeProfileMigrator.prototype.classID = Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}");
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EdgeProfileMigrator]);
|
||||
255
browser/components/migration/FirefoxProfileMigrator.js
Normal file
255
browser/components/migration/FirefoxProfileMigrator.js
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
* vim: sw=2 ts=2 sts=2 et */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* Migrates from a Firefox 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.
|
||||
*/
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
|
||||
"resource://gre/modules/PlacesBackups.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionMigration",
|
||||
"resource:///modules/sessionstore/SessionMigration.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
|
||||
"resource://gre/modules/ProfileAge.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
|
||||
function FirefoxProfileMigrator() {
|
||||
this.wrappedJSObject = this; // for testing...
|
||||
}
|
||||
|
||||
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(Ci.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() {
|
||||
return [...this._getAllProfiles().keys()].map(x => ({id: x, name: x})).sort(sorter);
|
||||
}
|
||||
});
|
||||
|
||||
FirefoxProfileMigrator.prototype._getFileObject = function(dir, fileName) {
|
||||
let file = dir.clone();
|
||||
file.append(fileName);
|
||||
|
||||
// File resources are monolithic. We don't make partial copies since
|
||||
// they are not expected to work alone. Return null to avoid trying to
|
||||
// copy non-existing files.
|
||||
return file.exists() ? file : null;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
return this._getResourcesInternal(sourceProfileDir, currentProfileDir);
|
||||
};
|
||||
|
||||
FirefoxProfileMigrator.prototype.getLastUsedDate = function() {
|
||||
// We always pretend we're really old, so that we don't mess
|
||||
// up the determination of which browser is the most 'recent'
|
||||
// to import from.
|
||||
return Promise.resolve(new Date(0));
|
||||
};
|
||||
|
||||
FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileDir, currentProfileDir) {
|
||||
let getFileResource = function(aMigrationType, aFileNames) {
|
||||
let files = [];
|
||||
for (let fileName of aFileNames) {
|
||||
let file = this._getFileObject(sourceProfileDir, fileName);
|
||||
if (file)
|
||||
files.push(file);
|
||||
}
|
||||
if (!files.length) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: aMigrationType,
|
||||
migrate: function(aCallback) {
|
||||
for (let file of files) {
|
||||
file.copyTo(currentProfileDir, "");
|
||||
}
|
||||
aCallback(true);
|
||||
}
|
||||
};
|
||||
}.bind(this);
|
||||
|
||||
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",
|
||||
"signedInUser.json"]);
|
||||
let formData = getFileResource(types.FORMDATA, ["formhistory.sqlite"]);
|
||||
let bookmarksBackups = getFileResource(types.OTHERDATA,
|
||||
[PlacesBackups.profileRelativeFolderPath]);
|
||||
let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]);
|
||||
|
||||
let sessionCheckpoints = this._getFileObject(sourceProfileDir, "sessionCheckpoints.json");
|
||||
let sessionFile = this._getFileObject(sourceProfileDir, "sessionstore.js");
|
||||
let session;
|
||||
if (sessionFile) {
|
||||
session = {
|
||||
type: types.SESSION,
|
||||
migrate: function(aCallback) {
|
||||
sessionCheckpoints.copyTo(currentProfileDir, "sessionCheckpoints.json");
|
||||
let newSessionFile = currentProfileDir.clone();
|
||||
newSessionFile.append("sessionstore.js");
|
||||
let migrationPromise = SessionMigration.migrate(sessionFile.path, newSessionFile.path);
|
||||
migrationPromise.then(function() {
|
||||
let buildID = Services.appinfo.platformBuildID;
|
||||
let mstone = Services.appinfo.platformVersion;
|
||||
// Force the browser to one-off resume the session that we give it:
|
||||
Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true);
|
||||
// Reset the homepage_override prefs so that the browser doesn't override our
|
||||
// session with the "what's new" page:
|
||||
Services.prefs.setCharPref("browser.startup.homepage_override.mstone", mstone);
|
||||
Services.prefs.setCharPref("browser.startup.homepage_override.buildID", buildID);
|
||||
// It's too early in startup for the pref service to have a profile directory,
|
||||
// so we have to manually tell it where to save the prefs file.
|
||||
let newPrefsFile = currentProfileDir.clone();
|
||||
newPrefsFile.append("prefs.js");
|
||||
Services.prefs.savePrefFile(newPrefsFile);
|
||||
aCallback(true);
|
||||
}, function() {
|
||||
aCallback(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Telemetry related migrations.
|
||||
let times = {
|
||||
name: "times", // name is used only by tests.
|
||||
type: types.OTHERDATA,
|
||||
migrate: aCallback => {
|
||||
let file = this._getFileObject(sourceProfileDir, "times.json");
|
||||
if (file) {
|
||||
file.copyTo(currentProfileDir, "");
|
||||
}
|
||||
// And record the fact a migration (ie, a reset) happened.
|
||||
let timesAccessor = new ProfileAge(currentProfileDir.path);
|
||||
timesAccessor.recordProfileReset().then(
|
||||
() => aCallback(true),
|
||||
() => aCallback(false)
|
||||
);
|
||||
}
|
||||
};
|
||||
let telemetry = {
|
||||
name: "telemetry", // name is used only by tests...
|
||||
type: types.OTHERDATA,
|
||||
migrate: aCallback => {
|
||||
let createSubDir = (name) => {
|
||||
let dir = currentProfileDir.clone();
|
||||
dir.append(name);
|
||||
dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
return dir;
|
||||
};
|
||||
|
||||
// If the 'datareporting' directory exists we migrate files from it.
|
||||
let haveStateFile = false;
|
||||
let dataReportingDir = this._getFileObject(sourceProfileDir, "datareporting");
|
||||
if (dataReportingDir && dataReportingDir.isDirectory()) {
|
||||
// Copy only specific files.
|
||||
let toCopy = ["state.json", "session-state.json"];
|
||||
|
||||
let dest = createSubDir("datareporting");
|
||||
let enumerator = dataReportingDir.directoryEntries;
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (file.isDirectory() || toCopy.indexOf(file.leafName) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.leafName == "state.json") {
|
||||
haveStateFile = true;
|
||||
}
|
||||
file.copyTo(dest, "");
|
||||
}
|
||||
}
|
||||
|
||||
if (!haveStateFile) {
|
||||
// Fall back to migrating the state file that contains the client id from healthreport/.
|
||||
// We first moved the client id management from the FHR implementation to the datareporting
|
||||
// service.
|
||||
// Consequently, we try to migrate an existing FHR state file here as a fallback.
|
||||
let healthReportDir = this._getFileObject(sourceProfileDir, "healthreport");
|
||||
if (healthReportDir && healthReportDir.isDirectory()) {
|
||||
let stateFile = this._getFileObject(healthReportDir, "state.json");
|
||||
if (stateFile) {
|
||||
let dest = createSubDir("healthreport");
|
||||
stateFile.copyTo(dest, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aCallback(true);
|
||||
}
|
||||
};
|
||||
|
||||
return [places, cookies, passwords, formData, dictionary, bookmarksBackups,
|
||||
session, times, telemetry].filter(r => r);
|
||||
};
|
||||
|
||||
Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", {
|
||||
get: () => 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]);
|
||||
542
browser/components/migration/IEProfileMigrator.js
Normal file
542
browser/components/migration/IEProfileMigrator.js
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
const kLoginsKey = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
|
||||
const kMainKey = "Software\\Microsoft\\Internet Explorer\\Main";
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
|
||||
Cu.import("resource:///modules/MSMigrationUtils.jsm");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
|
||||
"resource://gre/modules/ctypes.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
|
||||
"resource://gre/modules/OSCrypto.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
|
||||
"resource://gre/modules/WindowsRegistry.jsm");
|
||||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
// Resources
|
||||
|
||||
function History() {
|
||||
}
|
||||
|
||||
History.prototype = {
|
||||
type: MigrationUtils.resourceTypes.HISTORY,
|
||||
|
||||
get exists() {
|
||||
return true;
|
||||
},
|
||||
|
||||
migrate: function H_migrate(aCallback) {
|
||||
let places = [];
|
||||
let typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer");
|
||||
let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"].
|
||||
createInstance(Ci.nsISimpleEnumerator);
|
||||
while (historyEnumerator.hasMoreElements()) {
|
||||
let entry = historyEnumerator.getNext().QueryInterface(Ci.nsIPropertyBag2);
|
||||
let uri = entry.get("uri").QueryInterface(Ci.nsIURI);
|
||||
// MSIE stores some types of URLs in its history that we don't handle,
|
||||
// like HTMLHelp and others. Since we don't properly map handling for
|
||||
// all of them we just avoid importing them.
|
||||
if (["http", "https", "ftp", "file"].indexOf(uri.scheme) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = entry.get("title");
|
||||
// Embed visits have no title and don't need to be imported.
|
||||
if (title.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The typed urls are already fixed-up, so we can use them for comparison.
|
||||
let transitionType = typedURLs.has(uri.spec) ?
|
||||
Ci.nsINavHistoryService.TRANSITION_TYPED :
|
||||
Ci.nsINavHistoryService.TRANSITION_LINK;
|
||||
// use the current date if we have no visits for this entry.
|
||||
// Note that the entry will have a time in microseconds (PRTime),
|
||||
// and Date.now() returns milliseconds. Places expects PRTime,
|
||||
// so we multiply the Date.now return value to make up the difference.
|
||||
let lastVisitTime = entry.get("time") || (Date.now() * 1000);
|
||||
|
||||
places.push(
|
||||
{ uri: uri,
|
||||
title: title,
|
||||
visits: [{ transitionType: transitionType,
|
||||
visitDate: lastVisitTime }]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check whether there is any history to import.
|
||||
if (places.length == 0) {
|
||||
aCallback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult: function() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError: function() {},
|
||||
handleCompletion: function() {
|
||||
aCallback(this._success);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// IE form password migrator supporting windows from XP until 7 and IE from 7 until 11
|
||||
function IE7FormPasswords() {
|
||||
// used to distinguish between this migrator and other passwords migrators in tests.
|
||||
this.name = "IE7FormPasswords";
|
||||
}
|
||||
|
||||
IE7FormPasswords.prototype = {
|
||||
type: MigrationUtils.resourceTypes.PASSWORDS,
|
||||
|
||||
get exists() {
|
||||
// work only on windows until 7
|
||||
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
|
||||
let key = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(nsIWindowsRegKey);
|
||||
key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kLoginsKey,
|
||||
nsIWindowsRegKey.ACCESS_READ);
|
||||
let count = key.valueCount;
|
||||
key.close();
|
||||
return count > 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
migrate(aCallback) {
|
||||
let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"].
|
||||
createInstance(Ci.nsISimpleEnumerator);
|
||||
let uris = []; // the uris of the websites that are going to be migrated
|
||||
while (historyEnumerator.hasMoreElements()) {
|
||||
let entry = historyEnumerator.getNext().QueryInterface(Ci.nsIPropertyBag2);
|
||||
let uri = entry.get("uri").QueryInterface(Ci.nsIURI);
|
||||
// MSIE stores some types of URLs in its history that we don't handle, like HTMLHelp
|
||||
// and others. Since we are not going to import the logins that are performed in these URLs
|
||||
// we can just skip them.
|
||||
if (["http", "https", "ftp"].indexOf(uri.scheme) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uris.push(uri);
|
||||
}
|
||||
this._migrateURIs(uris);
|
||||
aCallback(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Migrate the logins that were saved for the uris arguments.
|
||||
* @param {nsIURI[]} uris - the uris that are going to be migrated.
|
||||
*/
|
||||
_migrateURIs(uris) {
|
||||
this.ctypesKernelHelpers = new MSMigrationUtils.CtypesKernelHelpers();
|
||||
this._crypto = new OSCrypto();
|
||||
let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
|
||||
let key = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(nsIWindowsRegKey);
|
||||
key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kLoginsKey,
|
||||
nsIWindowsRegKey.ACCESS_READ);
|
||||
|
||||
let urlsSet = new Set(); // set of the already processed urls.
|
||||
// number of the successfully decrypted registry values
|
||||
let successfullyDecryptedValues = 0;
|
||||
/* The logins are stored in the registry, where the key is a hashed URL and its
|
||||
* value contains the encrypted details for all logins for that URL.
|
||||
*
|
||||
* First iterate through IE history, hashing each URL and looking for a match. If
|
||||
* found, decrypt the value, using the URL as a salt. Finally add any found logins
|
||||
* to the Firefox password manager.
|
||||
*/
|
||||
|
||||
for (let uri of uris) {
|
||||
try {
|
||||
// remove the query and the ref parts of the URL
|
||||
let urlObject = new URL(uri.spec);
|
||||
let url = urlObject.origin + urlObject.pathname;
|
||||
// if the current url is already processed, it should be skipped
|
||||
if (urlsSet.has(url)) {
|
||||
continue;
|
||||
}
|
||||
urlsSet.add(url);
|
||||
// hash value of the current uri
|
||||
let hashStr = this._crypto.getIELoginHash(url);
|
||||
if (!key.hasValue(hashStr)) {
|
||||
continue;
|
||||
}
|
||||
let value = key.readBinaryValue(hashStr);
|
||||
// if no value was found, the uri is skipped
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
// the url is used as salt to decrypt the registry value
|
||||
data = this._crypto.decryptData(value, url, true);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
// extract the login details from the decrypted data
|
||||
let ieLogins = this._extractDetails(data, uri);
|
||||
// if at least a credential was found in the current data, successfullyDecryptedValues should
|
||||
// be incremented by one
|
||||
if (ieLogins.length) {
|
||||
successfullyDecryptedValues++;
|
||||
}
|
||||
this._addLogins(ieLogins);
|
||||
} catch (e) {
|
||||
Cu.reportError("Error while importing logins for " + uri.spec + ": " + e);
|
||||
}
|
||||
}
|
||||
// if the number of the imported values is less than the number of values in the key, it means
|
||||
// that not all the values were imported and an error should be reported
|
||||
if (successfullyDecryptedValues < key.valueCount) {
|
||||
Cu.reportError("We failed to decrypt and import some logins. " +
|
||||
"This is likely because we didn't find the URLs where these " +
|
||||
"passwords were submitted in the IE history and which are needed to be used " +
|
||||
"as keys in the decryption.");
|
||||
}
|
||||
|
||||
key.close();
|
||||
this._crypto.finalize();
|
||||
this.ctypesKernelHelpers.finalize();
|
||||
},
|
||||
|
||||
_crypto: null,
|
||||
|
||||
/**
|
||||
* Add the logins to the password manager.
|
||||
* @param {Object[]} logins - array of the login details.
|
||||
*/
|
||||
_addLogins(ieLogins) {
|
||||
for (let ieLogin of ieLogins) {
|
||||
try {
|
||||
// create a new login
|
||||
let login = {
|
||||
username: ieLogin.username,
|
||||
password: ieLogin.password,
|
||||
hostname: ieLogin.url,
|
||||
timeCreated: ieLogin.creation,
|
||||
};
|
||||
MigrationUtils.insertLoginWrapper(login);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract the details of one or more logins from the raw decrypted data.
|
||||
* @param {string} data - the decrypted data containing raw information.
|
||||
* @param {nsURI} uri - the nsURI of page where the login has occur.
|
||||
* @returns {Object[]} array of objects where each of them contains the username, password, URL,
|
||||
* and creation time representing all the logins found in the data arguments.
|
||||
*/
|
||||
_extractDetails(data, uri) {
|
||||
// the structure of the header of the IE7 decrypted data for all the logins sharing the same URL
|
||||
let loginData = new ctypes.StructType("loginData", [
|
||||
// Bytes 0-3 are not needed and not documented
|
||||
{"unknown1": ctypes.uint32_t},
|
||||
// Bytes 4-7 are the header size
|
||||
{"headerSize": ctypes.uint32_t},
|
||||
// Bytes 8-11 are the data size
|
||||
{"dataSize": ctypes.uint32_t},
|
||||
// Bytes 12-19 are not needed and not documented
|
||||
{"unknown2": ctypes.uint32_t},
|
||||
{"unknown3": ctypes.uint32_t},
|
||||
// Bytes 20-23 are the data count: each username and password is considered as a data
|
||||
{"dataMax": ctypes.uint32_t},
|
||||
// Bytes 24-35 are not needed and not documented
|
||||
{"unknown4": ctypes.uint32_t},
|
||||
{"unknown5": ctypes.uint32_t},
|
||||
{"unknown6": ctypes.uint32_t}
|
||||
]);
|
||||
|
||||
// the structure of a IE7 decrypted login item
|
||||
let loginItem = new ctypes.StructType("loginItem", [
|
||||
// Bytes 0-3 are the offset of the username
|
||||
{"usernameOffset": ctypes.uint32_t},
|
||||
// Bytes 4-11 are the date
|
||||
{"loDateTime": ctypes.uint32_t},
|
||||
{"hiDateTime": ctypes.uint32_t},
|
||||
// Bytes 12-15 are not needed and not documented
|
||||
{"foo": ctypes.uint32_t},
|
||||
// Bytes 16-19 are the offset of the password
|
||||
{"passwordOffset": ctypes.uint32_t},
|
||||
// Bytes 20-31 are not needed and not documented
|
||||
{"unknown1": ctypes.uint32_t},
|
||||
{"unknown2": ctypes.uint32_t},
|
||||
{"unknown3": ctypes.uint32_t}
|
||||
]);
|
||||
|
||||
let url = uri.prePath;
|
||||
let results = [];
|
||||
let arr = this._crypto.stringToArray(data);
|
||||
// convert data to ctypes.unsigned_char.array(arr.length)
|
||||
let cdata = ctypes.unsigned_char.array(arr.length)(arr);
|
||||
// Bytes 0-35 contain the loginData data structure for all the logins sharing the same URL
|
||||
let currentLoginData = ctypes.cast(cdata, loginData);
|
||||
let headerSize = currentLoginData.headerSize;
|
||||
let currentInfoIndex = loginData.size;
|
||||
// pointer to the current login item
|
||||
let currentLoginItemPointer = ctypes.cast(cdata.addressOfElement(currentInfoIndex),
|
||||
loginItem.ptr);
|
||||
// currentLoginData.dataMax is the data count: each username and password is considered as
|
||||
// a data. So, the number of logins is the number of data dived by 2
|
||||
let numLogins = currentLoginData.dataMax / 2;
|
||||
for (let n = 0; n < numLogins; n++) {
|
||||
// Bytes 0-31 starting from currentInfoIndex contain the loginItem data structure for the
|
||||
// current login
|
||||
let currentLoginItem = currentLoginItemPointer.contents;
|
||||
let creation = this.ctypesKernelHelpers.
|
||||
fileTimeToSecondsSinceEpoch(currentLoginItem.hiDateTime,
|
||||
currentLoginItem.loDateTime) * 1000;
|
||||
let currentResult = {
|
||||
creation: creation,
|
||||
url: url,
|
||||
};
|
||||
// The username is UTF-16 and null-terminated.
|
||||
currentResult.username =
|
||||
ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.usernameOffset),
|
||||
ctypes.char16_t.ptr).readString();
|
||||
// The password is UTF-16 and null-terminated.
|
||||
currentResult.password =
|
||||
ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.passwordOffset),
|
||||
ctypes.char16_t.ptr).readString();
|
||||
results.push(currentResult);
|
||||
// move to the next login item
|
||||
currentLoginItemPointer = currentLoginItemPointer.increment();
|
||||
}
|
||||
return results;
|
||||
},
|
||||
};
|
||||
|
||||
function Settings() {
|
||||
}
|
||||
|
||||
Settings.prototype = {
|
||||
type: MigrationUtils.resourceTypes.SETTINGS,
|
||||
|
||||
get exists() {
|
||||
return true;
|
||||
},
|
||||
|
||||
migrate: function S_migrate(aCallback) {
|
||||
// Converts from yes/no to a boolean.
|
||||
let yesNoToBoolean = v => v == "yes";
|
||||
|
||||
// Converts source format like "en-us,ar-kw;q=0.7,ar-om;q=0.3" into
|
||||
// destination format like "en-us, ar-kw, ar-om".
|
||||
// Final string is sorted by quality (q=) param.
|
||||
function parseAcceptLanguageList(v) {
|
||||
return v.match(/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/gi)
|
||||
.sort(function(a, b) {
|
||||
let qA = parseFloat(a.split(";q=")[1]) || 1.0;
|
||||
let qB = parseFloat(b.split(";q=")[1]) || 1.0;
|
||||
return qB - qA;
|
||||
})
|
||||
.map(a => a.split(";")[0]);
|
||||
}
|
||||
|
||||
// For reference on some of the available IE Registry settings:
|
||||
// * http://msdn.microsoft.com/en-us/library/cc980058%28v=prot.13%29.aspx
|
||||
// * http://msdn.microsoft.com/en-us/library/cc980059%28v=prot.13%29.aspx
|
||||
|
||||
// Note that only settings exposed in our UI should be migrated.
|
||||
|
||||
this._set("Software\\Microsoft\\Internet Explorer\\International",
|
||||
"AcceptLanguage",
|
||||
"intl.accept_languages",
|
||||
parseAcceptLanguageList);
|
||||
// TODO (bug 745853): For now, only x-western font is translated.
|
||||
this._set("Software\\Microsoft\\Internet Explorer\\International\\Scripts\\3",
|
||||
"IEFixedFontName",
|
||||
"font.name.monospace.x-western");
|
||||
this._set(kMainKey,
|
||||
"Use FormSuggest",
|
||||
"browser.formfill.enable",
|
||||
yesNoToBoolean);
|
||||
this._set(kMainKey,
|
||||
"FormSuggest Passwords",
|
||||
"signon.rememberSignons",
|
||||
yesNoToBoolean);
|
||||
this._set(kMainKey,
|
||||
"Anchor Underline",
|
||||
"browser.underline_anchors",
|
||||
yesNoToBoolean);
|
||||
this._set(kMainKey,
|
||||
"Display Inline Images",
|
||||
"permissions.default.image",
|
||||
v => yesNoToBoolean(v) ? 1 : 2);
|
||||
this._set(kMainKey,
|
||||
"Move System Caret",
|
||||
"accessibility.browsewithcaret",
|
||||
yesNoToBoolean);
|
||||
this._set("Software\\Microsoft\\Internet Explorer\\Settings",
|
||||
"Always Use My Colors",
|
||||
"browser.display.document_color_use",
|
||||
v => (!v ? 0 : 2));
|
||||
this._set("Software\\Microsoft\\Internet Explorer\\Settings",
|
||||
"Always Use My Font Face",
|
||||
"browser.display.use_document_fonts",
|
||||
v => !v);
|
||||
this._set(kMainKey,
|
||||
"SmoothScroll",
|
||||
"general.smoothScroll",
|
||||
Boolean);
|
||||
this._set("Software\\Microsoft\\Internet Explorer\\TabbedBrowsing\\",
|
||||
"WarnOnClose",
|
||||
"browser.tabs.warnOnClose",
|
||||
Boolean);
|
||||
this._set("Software\\Microsoft\\Internet Explorer\\TabbedBrowsing\\",
|
||||
"OpenInForeground",
|
||||
"browser.tabs.loadInBackground",
|
||||
v => !v);
|
||||
|
||||
aCallback(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads a setting from the Registry and stores the converted result into
|
||||
* the appropriate Firefox preference.
|
||||
*
|
||||
* @param aPath
|
||||
* Registry path under HKCU.
|
||||
* @param aKey
|
||||
* Name of the key.
|
||||
* @param aPref
|
||||
* Firefox preference.
|
||||
* @param [optional] aTransformFn
|
||||
* Conversion function from the Registry format to the pref format.
|
||||
*/
|
||||
_set: function S__set(aPath, aKey, aPref, aTransformFn) {
|
||||
let value = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
||||
aPath, aKey);
|
||||
// Don't import settings that have never been flipped.
|
||||
if (value === undefined)
|
||||
return;
|
||||
|
||||
if (aTransformFn)
|
||||
value = aTransformFn(value);
|
||||
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
Services.prefs.setCharPref(aPref, value);
|
||||
break;
|
||||
case "number":
|
||||
Services.prefs.setIntPref(aPref, value);
|
||||
break;
|
||||
case "boolean":
|
||||
Services.prefs.setBoolPref(aPref, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected value type: " + (typeof value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function IEProfileMigrator()
|
||||
{
|
||||
this.wrappedJSObject = this; // export this to be able to use it in the unittest.
|
||||
}
|
||||
|
||||
IEProfileMigrator.prototype = Object.create(MigratorPrototype);
|
||||
|
||||
IEProfileMigrator.prototype.getResources = function IE_getResources() {
|
||||
let resources = [
|
||||
MSMigrationUtils.getBookmarksMigrator(),
|
||||
new History(),
|
||||
MSMigrationUtils.getCookiesMigrator(),
|
||||
new Settings(),
|
||||
];
|
||||
// Only support the form password migrator for Windows XP to 7.
|
||||
if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
|
||||
resources.push(new IE7FormPasswords());
|
||||
}
|
||||
let windowsVaultFormPasswordsMigrator =
|
||||
MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
|
||||
windowsVaultFormPasswordsMigrator.name = "IEVaultFormPasswords";
|
||||
resources.push(windowsVaultFormPasswordsMigrator);
|
||||
return resources.filter(r => r.exists);
|
||||
};
|
||||
|
||||
IEProfileMigrator.prototype.getLastUsedDate = function IE_getLastUsedDate() {
|
||||
let datePromises = ["Favs", "CookD"].map(dirId => {
|
||||
let {path} = Services.dirsvc.get(dirId, Ci.nsIFile);
|
||||
return OS.File.stat(path).catch(() => null).then(info => {
|
||||
return info ? info.lastModificationDate : 0;
|
||||
});
|
||||
});
|
||||
datePromises.push(new Promise(resolve => {
|
||||
let typedURLs = new Map();
|
||||
try {
|
||||
typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer");
|
||||
} catch (ex) {}
|
||||
let dates = [0, ... typedURLs.values()];
|
||||
resolve(Math.max.apply(Math, dates));
|
||||
}));
|
||||
return Promise.all(datePromises).then(dates => {
|
||||
return new Date(Math.max.apply(Math, dates));
|
||||
});
|
||||
};
|
||||
|
||||
Object.defineProperty(IEProfileMigrator.prototype, "sourceHomePageURL", {
|
||||
get: function IE_get_sourceHomePageURL() {
|
||||
let defaultStartPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
|
||||
kMainKey, "Default_Page_URL");
|
||||
let startPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
||||
kMainKey, "Start Page");
|
||||
// If the user didn't customize the Start Page, he is still on the default
|
||||
// page, that may be considered the equivalent of our about:home. There's
|
||||
// no reason to retain it, since it is heavily targeted to IE.
|
||||
let homepage = startPage != defaultStartPage ? startPage : "";
|
||||
|
||||
// IE7+ supports secondary home pages located in a REG_MULTI_SZ key. These
|
||||
// are in addition to the Start Page, and no empty entries are possible,
|
||||
// thus a Start Page is always defined if any of these exists, though it
|
||||
// may be the default one.
|
||||
let secondaryPages = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
||||
kMainKey, "Secondary Start Pages");
|
||||
if (secondaryPages) {
|
||||
if (homepage)
|
||||
secondaryPages.unshift(homepage);
|
||||
homepage = secondaryPages.join("|");
|
||||
}
|
||||
|
||||
return homepage;
|
||||
}
|
||||
});
|
||||
|
||||
IEProfileMigrator.prototype.classDescription = "IE Profile Migrator";
|
||||
IEProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=ie";
|
||||
IEProfileMigrator.prototype.classID = Components.ID("{3d2532e3-4932-4774-b7ba-968f5899d3a4}");
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([IEProfileMigrator]);
|
||||
889
browser/components/migration/MSMigrationUtils.jsm
Normal file
889
browser/components/migration/MSMigrationUtils.jsm
Normal file
|
|
@ -0,0 +1,889 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MSMigrationUtils"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm");
|
||||
|
||||
Cu.importGlobalProperties(["FileReader"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
|
||||
"resource://gre/modules/WindowsRegistry.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
|
||||
"resource://gre/modules/ctypes.jsm");
|
||||
|
||||
const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"];
|
||||
const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies";
|
||||
const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites";
|
||||
const FREE_CLOSE_FAILED = 0;
|
||||
const INTERNET_EXPLORER_EDGE_GUID = [0x3CCD5499,
|
||||
0x4B1087A8,
|
||||
0x886015A2,
|
||||
0x553BDD88];
|
||||
const RESULT_SUCCESS = 0;
|
||||
const VAULT_ENUMERATE_ALL_ITEMS = 512;
|
||||
const WEB_CREDENTIALS_VAULT_ID = [0x4BF4C442,
|
||||
0x41A09B8A,
|
||||
0x4ADD80B3,
|
||||
0x28DB4D70];
|
||||
|
||||
Cu.importGlobalProperties(["File"]);
|
||||
|
||||
const wintypes = {
|
||||
BOOL: ctypes.int,
|
||||
DWORD: ctypes.uint32_t,
|
||||
DWORDLONG: ctypes.uint64_t,
|
||||
CHAR: ctypes.char,
|
||||
PCHAR: ctypes.char.ptr,
|
||||
LPCWSTR: ctypes.char16_t.ptr,
|
||||
PDWORD: ctypes.uint32_t.ptr,
|
||||
VOIDP: ctypes.voidptr_t,
|
||||
WORD: ctypes.uint16_t,
|
||||
};
|
||||
|
||||
// TODO: Bug 1202978 - Refactor MSMigrationUtils ctypes helpers
|
||||
function CtypesKernelHelpers() {
|
||||
this._structs = {};
|
||||
this._functions = {};
|
||||
this._libs = {};
|
||||
|
||||
this._structs.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
|
||||
{wYear: wintypes.WORD},
|
||||
{wMonth: wintypes.WORD},
|
||||
{wDayOfWeek: wintypes.WORD},
|
||||
{wDay: wintypes.WORD},
|
||||
{wHour: wintypes.WORD},
|
||||
{wMinute: wintypes.WORD},
|
||||
{wSecond: wintypes.WORD},
|
||||
{wMilliseconds: wintypes.WORD}
|
||||
]);
|
||||
|
||||
this._structs.FILETIME = new ctypes.StructType("FILETIME", [
|
||||
{dwLowDateTime: wintypes.DWORD},
|
||||
{dwHighDateTime: wintypes.DWORD}
|
||||
]);
|
||||
|
||||
try {
|
||||
this._libs.kernel32 = ctypes.open("Kernel32");
|
||||
|
||||
this._functions.FileTimeToSystemTime =
|
||||
this._libs.kernel32.declare("FileTimeToSystemTime",
|
||||
ctypes.default_abi,
|
||||
wintypes.BOOL,
|
||||
this._structs.FILETIME.ptr,
|
||||
this._structs.SYSTEMTIME.ptr);
|
||||
} catch (ex) {
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
CtypesKernelHelpers.prototype = {
|
||||
/**
|
||||
* Must be invoked once after last use of any of the provided helpers.
|
||||
*/
|
||||
finalize() {
|
||||
this._structs = {};
|
||||
this._functions = {};
|
||||
for (let key in this._libs) {
|
||||
let lib = this._libs[key];
|
||||
try {
|
||||
lib.close();
|
||||
} catch (ex) {}
|
||||
}
|
||||
this._libs = {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct,
|
||||
* and then deduces the number of seconds since the epoch (which
|
||||
* is the data we want for the cookie expiry date).
|
||||
*
|
||||
* @param aTimeHi
|
||||
* Least significant DWORD.
|
||||
* @param aTimeLo
|
||||
* Most significant DWORD.
|
||||
* @return the number of seconds since the epoch
|
||||
*/
|
||||
fileTimeToSecondsSinceEpoch(aTimeHi, aTimeLo) {
|
||||
let fileTime = this._structs.FILETIME();
|
||||
fileTime.dwLowDateTime = aTimeLo;
|
||||
fileTime.dwHighDateTime = aTimeHi;
|
||||
let systemTime = this._structs.SYSTEMTIME();
|
||||
let result = this._functions.FileTimeToSystemTime(fileTime.address(),
|
||||
systemTime.address());
|
||||
if (result == 0)
|
||||
throw new Error(ctypes.winLastError);
|
||||
|
||||
// System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
|
||||
// then divide by 1000 to get seconds, and round down:
|
||||
return Math.floor(Date.UTC(systemTime.wYear,
|
||||
systemTime.wMonth - 1,
|
||||
systemTime.wDay,
|
||||
systemTime.wHour,
|
||||
systemTime.wMinute,
|
||||
systemTime.wSecond,
|
||||
systemTime.wMilliseconds) / 1000);
|
||||
}
|
||||
};
|
||||
|
||||
function CtypesVaultHelpers() {
|
||||
this._structs = {};
|
||||
this._functions = {};
|
||||
|
||||
this._structs.GUID = new ctypes.StructType("GUID", [
|
||||
{id: wintypes.DWORD.array(4)},
|
||||
]);
|
||||
|
||||
this._structs.VAULT_ITEM_ELEMENT = new ctypes.StructType("VAULT_ITEM_ELEMENT", [
|
||||
// not documented
|
||||
{schemaElementId: wintypes.DWORD},
|
||||
// not documented
|
||||
{unknown1: wintypes.DWORD},
|
||||
// vault type
|
||||
{type: wintypes.DWORD},
|
||||
// not documented
|
||||
{unknown2: wintypes.DWORD},
|
||||
// value of the item
|
||||
{itemValue: wintypes.LPCWSTR},
|
||||
// not documented
|
||||
{unknown3: wintypes.CHAR.array(12)},
|
||||
]);
|
||||
|
||||
this._structs.VAULT_ELEMENT = new ctypes.StructType("VAULT_ELEMENT", [
|
||||
// vault item schemaId
|
||||
{schemaId: this._structs.GUID},
|
||||
// a pointer to the name of the browser VAULT_ITEM_ELEMENT
|
||||
{pszCredentialFriendlyName: wintypes.LPCWSTR},
|
||||
// a pointer to the url VAULT_ITEM_ELEMENT
|
||||
{pResourceElement: this._structs.VAULT_ITEM_ELEMENT.ptr},
|
||||
// a pointer to the username VAULT_ITEM_ELEMENT
|
||||
{pIdentityElement: this._structs.VAULT_ITEM_ELEMENT.ptr},
|
||||
// not documented
|
||||
{pAuthenticatorElement: this._structs.VAULT_ITEM_ELEMENT.ptr},
|
||||
// not documented
|
||||
{pPackageSid: this._structs.VAULT_ITEM_ELEMENT.ptr},
|
||||
// time stamp in local format
|
||||
{lowLastModified: wintypes.DWORD},
|
||||
{highLastModified: wintypes.DWORD},
|
||||
// not documented
|
||||
{flags: wintypes.DWORD},
|
||||
// not documented
|
||||
{dwPropertiesCount: wintypes.DWORD},
|
||||
// not documented
|
||||
{pPropertyElements: this._structs.VAULT_ITEM_ELEMENT.ptr},
|
||||
]);
|
||||
|
||||
try {
|
||||
this._vaultcliLib = ctypes.open("vaultcli.dll");
|
||||
|
||||
this._functions.VaultOpenVault =
|
||||
this._vaultcliLib.declare("VaultOpenVault",
|
||||
ctypes.winapi_abi,
|
||||
wintypes.DWORD,
|
||||
// GUID
|
||||
this._structs.GUID.ptr,
|
||||
// Flags
|
||||
wintypes.DWORD,
|
||||
// Vault Handle
|
||||
wintypes.VOIDP.ptr);
|
||||
this._functions.VaultEnumerateItems =
|
||||
this._vaultcliLib.declare("VaultEnumerateItems",
|
||||
ctypes.winapi_abi,
|
||||
wintypes.DWORD,
|
||||
// Vault Handle
|
||||
wintypes.VOIDP,
|
||||
// Flags
|
||||
wintypes.DWORD,
|
||||
// Items Count
|
||||
wintypes.PDWORD,
|
||||
// Items
|
||||
ctypes.voidptr_t);
|
||||
this._functions.VaultCloseVault =
|
||||
this._vaultcliLib.declare("VaultCloseVault",
|
||||
ctypes.winapi_abi,
|
||||
wintypes.DWORD,
|
||||
// Vault Handle
|
||||
wintypes.VOIDP);
|
||||
this._functions.VaultGetItem =
|
||||
this._vaultcliLib.declare("VaultGetItem",
|
||||
ctypes.winapi_abi,
|
||||
wintypes.DWORD,
|
||||
// Vault Handle
|
||||
wintypes.VOIDP,
|
||||
// Schema Id
|
||||
this._structs.GUID.ptr,
|
||||
// Resource
|
||||
this._structs.VAULT_ITEM_ELEMENT.ptr,
|
||||
// Identity
|
||||
this._structs.VAULT_ITEM_ELEMENT.ptr,
|
||||
// Package Sid
|
||||
this._structs.VAULT_ITEM_ELEMENT.ptr,
|
||||
// HWND Owner
|
||||
wintypes.DWORD,
|
||||
// Flags
|
||||
wintypes.DWORD,
|
||||
// Items
|
||||
this._structs.VAULT_ELEMENT.ptr.ptr);
|
||||
this._functions.VaultFree =
|
||||
this._vaultcliLib.declare("VaultFree",
|
||||
ctypes.winapi_abi,
|
||||
wintypes.DWORD,
|
||||
// Memory
|
||||
this._structs.VAULT_ELEMENT.ptr);
|
||||
} catch (ex) {
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
CtypesVaultHelpers.prototype = {
|
||||
/**
|
||||
* Must be invoked once after last use of any of the provided helpers.
|
||||
*/
|
||||
finalize() {
|
||||
this._structs = {};
|
||||
this._functions = {};
|
||||
try {
|
||||
this._vaultcliLib.close();
|
||||
} catch (ex) {}
|
||||
this._vaultcliLib = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether an host is an IP (v4 or v6) address.
|
||||
*
|
||||
* @param aHost
|
||||
* The host to check.
|
||||
* @return whether aHost is an IP address.
|
||||
*/
|
||||
function hostIsIPAddress(aHost) {
|
||||
try {
|
||||
Services.eTLD.getBaseDomainFromHost(aHost);
|
||||
} catch (e) {
|
||||
return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var gEdgeDir;
|
||||
function getEdgeLocalDataFolder() {
|
||||
if (gEdgeDir) {
|
||||
return gEdgeDir.clone();
|
||||
}
|
||||
let packages = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
|
||||
packages.append("Packages");
|
||||
let edgeDir = packages.clone();
|
||||
edgeDir.append("Microsoft.MicrosoftEdge_8wekyb3d8bbwe");
|
||||
try {
|
||||
if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
|
||||
gEdgeDir = edgeDir;
|
||||
return edgeDir.clone();
|
||||
}
|
||||
|
||||
// Let's try the long way:
|
||||
let dirEntries = packages.directoryEntries;
|
||||
while (dirEntries.hasMoreElements()) {
|
||||
let subDir = dirEntries.getNext();
|
||||
subDir.QueryInterface(Ci.nsIFile);
|
||||
if (subDir.leafName.startsWith("Microsoft.MicrosoftEdge") && subDir.isReadable() &&
|
||||
subDir.isDirectory()) {
|
||||
gEdgeDir = subDir;
|
||||
return subDir.clone();
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError("Exception trying to find the Edge favorites directory: " + ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function Bookmarks(migrationType) {
|
||||
this._migrationType = migrationType;
|
||||
}
|
||||
|
||||
Bookmarks.prototype = {
|
||||
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||
|
||||
get exists() {
|
||||
return !!this._favoritesFolder;
|
||||
},
|
||||
|
||||
get importedAppLabel() {
|
||||
return this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE ? "IE" : "Edge";
|
||||
},
|
||||
|
||||
__favoritesFolder: null,
|
||||
get _favoritesFolder() {
|
||||
if (!this.__favoritesFolder) {
|
||||
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
|
||||
let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile);
|
||||
if (favoritesFolder.exists() && favoritesFolder.isReadable()) {
|
||||
this.__favoritesFolder = favoritesFolder;
|
||||
}
|
||||
} else if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
|
||||
let edgeDir = getEdgeLocalDataFolder();
|
||||
if (edgeDir) {
|
||||
edgeDir.appendRelativePath(EDGE_FAVORITES);
|
||||
if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
|
||||
this.__favoritesFolder = edgeDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.__favoritesFolder;
|
||||
},
|
||||
|
||||
__toolbarFolderName: null,
|
||||
get _toolbarFolderName() {
|
||||
if (!this.__toolbarFolderName) {
|
||||
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
|
||||
// Retrieve the name of IE's favorites subfolder that holds the bookmarks
|
||||
// in the toolbar. This was previously stored in the registry and changed
|
||||
// in IE7 to always be called "Links".
|
||||
let folderName = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\Internet Explorer\\Toolbar",
|
||||
"LinksFolderName");
|
||||
this.__toolbarFolderName = folderName || "Links";
|
||||
} else {
|
||||
this.__toolbarFolderName = "Links";
|
||||
}
|
||||
}
|
||||
return this.__toolbarFolderName;
|
||||
},
|
||||
|
||||
migrate: function B_migrate(aCallback) {
|
||||
return Task.spawn(function* () {
|
||||
// Import to the bookmarks menu.
|
||||
let folderGuid = PlacesUtils.bookmarks.menuGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
folderGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
|
||||
}
|
||||
yield this._migrateFolder(this._favoritesFolder, folderGuid);
|
||||
}.bind(this)).then(() => aCallback(true),
|
||||
e => { Cu.reportError(e); aCallback(false) });
|
||||
},
|
||||
|
||||
_migrateFolder: Task.async(function* (aSourceFolder, aDestFolderGuid) {
|
||||
// TODO (bug 741993): the favorites order is stored in the Registry, at
|
||||
// HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites
|
||||
// for IE, and in a similar location for Edge.
|
||||
// Until we support it, bookmarks are imported in alphabetical order.
|
||||
let entries = aSourceFolder.directoryEntries;
|
||||
let succeeded = true;
|
||||
while (entries.hasMoreElements()) {
|
||||
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
|
||||
try {
|
||||
// Make sure that entry.path == entry.target to not follow .lnk folder
|
||||
// shortcuts which could lead to infinite cycles.
|
||||
// Don't use isSymlink(), since it would throw for invalid
|
||||
// lnk files pointing to URLs or to unresolvable paths.
|
||||
if (entry.path == entry.target && entry.isDirectory()) {
|
||||
let folderGuid;
|
||||
if (entry.leafName == this._toolbarFolderName &&
|
||||
entry.parent.equals(this._favoritesFolder)) {
|
||||
// Import to the bookmarks toolbar.
|
||||
folderGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
folderGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Import to a new folder.
|
||||
folderGuid = (yield MigrationUtils.insertBookmarkWrapper({
|
||||
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
parentGuid: aDestFolderGuid,
|
||||
title: entry.leafName
|
||||
})).guid;
|
||||
}
|
||||
|
||||
if (entry.isReadable()) {
|
||||
// Recursively import the folder.
|
||||
yield this._migrateFolder(entry, folderGuid);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Strip the .url extension, to both check this is a valid link file,
|
||||
// and get the associated title.
|
||||
let matches = entry.leafName.match(/(.+)\.url$/i);
|
||||
if (matches) {
|
||||
let fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
|
||||
getService(Ci.nsIFileProtocolHandler);
|
||||
let uri = fileHandler.readURLFile(entry);
|
||||
let title = matches[1];
|
||||
|
||||
yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid: aDestFolderGuid, url: uri, title
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex);
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
if (!succeeded) {
|
||||
throw new Error("Failed to import all bookmarks correctly.");
|
||||
}
|
||||
}),
|
||||
|
||||
};
|
||||
|
||||
function Cookies(migrationType) {
|
||||
this._migrationType = migrationType;
|
||||
}
|
||||
|
||||
Cookies.prototype = {
|
||||
type: MigrationUtils.resourceTypes.COOKIES,
|
||||
|
||||
get exists() {
|
||||
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
|
||||
return !!this._cookiesFolder;
|
||||
}
|
||||
return !!this._cookiesFolders;
|
||||
},
|
||||
|
||||
__cookiesFolder: null,
|
||||
get _cookiesFolder() {
|
||||
// Edge stores cookies in a number of places, and this shouldn't get called:
|
||||
if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_IE) {
|
||||
throw new Error("Shouldn't be looking for a single cookie folder unless we're migrating IE");
|
||||
}
|
||||
|
||||
// Cookies are stored in txt files, in a Cookies folder whose path varies
|
||||
// across the different OS versions. CookD takes care of most of these
|
||||
// cases, though, in Windows Vista/7, UAC makes a difference.
|
||||
// If UAC is enabled, the most common destination is CookD/Low. Though,
|
||||
// if the user runs the application in administrator mode or disables UAC,
|
||||
// cookies are stored in the original CookD destination. Cause running the
|
||||
// browser in administrator mode is unsafe and discouraged, we just care
|
||||
// about the UAC state.
|
||||
if (!this.__cookiesFolder) {
|
||||
let cookiesFolder = Services.dirsvc.get("CookD", Ci.nsIFile);
|
||||
if (cookiesFolder.exists() && cookiesFolder.isReadable()) {
|
||||
// Check if UAC is enabled.
|
||||
if (Services.appinfo.QueryInterface(Ci.nsIWinAppHelper).userCanElevate) {
|
||||
cookiesFolder.append("Low");
|
||||
}
|
||||
this.__cookiesFolder = cookiesFolder;
|
||||
}
|
||||
}
|
||||
return this.__cookiesFolder;
|
||||
},
|
||||
|
||||
__cookiesFolders: null,
|
||||
get _cookiesFolders() {
|
||||
if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_EDGE) {
|
||||
throw new Error("Shouldn't be looking for multiple cookie folders unless we're migrating Edge");
|
||||
}
|
||||
|
||||
let folders = [];
|
||||
let edgeDir = getEdgeLocalDataFolder();
|
||||
if (edgeDir) {
|
||||
edgeDir.append("AC");
|
||||
for (let path of EDGE_COOKIE_PATH_OPTIONS) {
|
||||
let folder = edgeDir.clone();
|
||||
let fullPath = path + EDGE_COOKIES_SUFFIX;
|
||||
folder.appendRelativePath(fullPath);
|
||||
if (folder.exists() && folder.isReadable() && folder.isDirectory()) {
|
||||
folders.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.__cookiesFolders = folders.length ? folders : null;
|
||||
return this.__cookiesFolders;
|
||||
},
|
||||
|
||||
migrate(aCallback) {
|
||||
this.ctypesKernelHelpers = new CtypesKernelHelpers();
|
||||
|
||||
let cookiesGenerator = (function* genCookie() {
|
||||
let success = false;
|
||||
let folders = this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE ?
|
||||
this.__cookiesFolders : [this.__cookiesFolder];
|
||||
for (let folder of folders) {
|
||||
let entries = folder.directoryEntries;
|
||||
while (entries.hasMoreElements()) {
|
||||
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
|
||||
// Skip eventual bogus entries.
|
||||
if (!entry.isFile() || !/\.txt$/.test(entry.leafName))
|
||||
continue;
|
||||
|
||||
this._readCookieFile(entry, function(aSuccess) {
|
||||
// Importing even a single cookie file is considered a success.
|
||||
if (aSuccess)
|
||||
success = true;
|
||||
try {
|
||||
cookiesGenerator.next();
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
yield undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.ctypesKernelHelpers.finalize();
|
||||
|
||||
aCallback(success);
|
||||
}).apply(this);
|
||||
cookiesGenerator.next();
|
||||
},
|
||||
|
||||
_readCookieFile(aFile, aCallback) {
|
||||
let fileReader = new FileReader();
|
||||
let onLoadEnd = () => {
|
||||
fileReader.removeEventListener("loadend", onLoadEnd, false);
|
||||
|
||||
if (fileReader.readyState != fileReader.DONE) {
|
||||
Cu.reportError("Could not read cookie contents: " + fileReader.error);
|
||||
aCallback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let success = true;
|
||||
try {
|
||||
this._parseCookieBuffer(fileReader.result);
|
||||
} catch (ex) {
|
||||
Components.utils.reportError("Unable to migrate cookie: " + ex);
|
||||
success = false;
|
||||
} finally {
|
||||
aCallback(success);
|
||||
}
|
||||
};
|
||||
fileReader.addEventListener("loadend", onLoadEnd, false);
|
||||
fileReader.readAsText(File.createFromNsIFile(aFile));
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a cookie file buffer and returns an array of the contained cookies.
|
||||
*
|
||||
* The cookie file format is a newline-separated-values with a "*" used as
|
||||
* delimeter between multiple records.
|
||||
* Each cookie has the following fields:
|
||||
* - name
|
||||
* - value
|
||||
* - host/path
|
||||
* - flags
|
||||
* - Expiration time most significant integer
|
||||
* - Expiration time least significant integer
|
||||
* - Creation time most significant integer
|
||||
* - Creation time least significant integer
|
||||
* - Record delimiter "*"
|
||||
*
|
||||
* Unfortunately, "*" can also occur inside the value of the cookie, so we
|
||||
* can't rely exclusively on it as a record separator.
|
||||
*
|
||||
* @note All the times are in FILETIME format.
|
||||
*/
|
||||
_parseCookieBuffer(aTextBuffer) {
|
||||
// Note the last record is an empty string...
|
||||
let records = [];
|
||||
let lines = aTextBuffer.split("\n");
|
||||
while (lines.length > 0) {
|
||||
let record = lines.splice(0, 9);
|
||||
// ... which means this is going to be a 1-element array for that record
|
||||
if (record.length > 1) {
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
for (let record of records) {
|
||||
let [name, value, hostpath, flags,
|
||||
expireTimeLo, expireTimeHi] = record;
|
||||
|
||||
// IE stores deleted cookies with a zero-length value, skip them.
|
||||
if (value.length == 0)
|
||||
continue;
|
||||
|
||||
// IE sometimes has cookies created by apps that use "~~local~~/local/file/path"
|
||||
// as the hostpath, ignore those:
|
||||
if (hostpath.startsWith("~~local~~"))
|
||||
continue;
|
||||
|
||||
let hostLen = hostpath.indexOf("/");
|
||||
let host = hostpath.substr(0, hostLen);
|
||||
let path = hostpath.substr(hostLen);
|
||||
|
||||
// For a non-null domain, assume it's what Mozilla considers
|
||||
// a domain cookie. See bug 222343.
|
||||
if (host.length > 0) {
|
||||
// Fist delete any possible extant matching host cookie.
|
||||
Services.cookies.remove(host, name, path, false, {});
|
||||
// Now make it a domain cookie.
|
||||
if (host[0] != "." && !hostIsIPAddress(host))
|
||||
host = "." + host;
|
||||
}
|
||||
|
||||
// Fallback: expire in 1h (NB: time is in seconds since epoch, so we have
|
||||
// to divide the result of Date.now() (which is in milliseconds) by 1000).
|
||||
let expireTime = Math.floor(Date.now() / 1000) + 3600;
|
||||
try {
|
||||
expireTime = this.ctypesKernelHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
|
||||
Number(expireTimeLo));
|
||||
} catch (ex) {
|
||||
Cu.reportError("Failed to get expiry time for cookie for " + host);
|
||||
}
|
||||
|
||||
Services.cookies.add(host,
|
||||
path,
|
||||
name,
|
||||
value,
|
||||
Number(flags) & 0x1, // secure
|
||||
false, // httpOnly
|
||||
false, // session
|
||||
expireTime,
|
||||
{});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function getTypedURLs(registryKeyPath) {
|
||||
// The list of typed URLs is a sort of annotation stored in the registry.
|
||||
// The number of entries stored is not UI-configurable, but has changed
|
||||
// between different Windows versions. We just keep reading up to the first
|
||||
// non-existing entry to support different limits / states of the registry.
|
||||
let typedURLs = new Map();
|
||||
let typedURLKey = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(Ci.nsIWindowsRegKey);
|
||||
let typedURLTimeKey = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(Ci.nsIWindowsRegKey);
|
||||
let cTypes = new CtypesKernelHelpers();
|
||||
try {
|
||||
typedURLKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
||||
registryKeyPath + "\\TypedURLs",
|
||||
Ci.nsIWindowsRegKey.ACCESS_READ);
|
||||
try {
|
||||
typedURLTimeKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
||||
registryKeyPath + "\\TypedURLsTime",
|
||||
Ci.nsIWindowsRegKey.ACCESS_READ);
|
||||
} catch (ex) {
|
||||
typedURLTimeKey = null;
|
||||
}
|
||||
let entryName;
|
||||
for (let entry = 1; typedURLKey.hasValue((entryName = "url" + entry)); entry++) {
|
||||
let url = typedURLKey.readStringValue(entryName);
|
||||
let timeTyped = 0;
|
||||
if (typedURLTimeKey && typedURLTimeKey.hasValue(entryName)) {
|
||||
let urlTime = "";
|
||||
try {
|
||||
urlTime = typedURLTimeKey.readBinaryValue(entryName);
|
||||
} catch (ex) {
|
||||
Cu.reportError("Couldn't read url time for " + entryName);
|
||||
}
|
||||
if (urlTime.length == 8) {
|
||||
let urlTimeHex = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let c = urlTime.charCodeAt(i).toString(16);
|
||||
if (c.length == 1)
|
||||
c = "0" + c;
|
||||
urlTimeHex.unshift(c);
|
||||
}
|
||||
try {
|
||||
let hi = parseInt(urlTimeHex.slice(0, 4).join(""), 16);
|
||||
let lo = parseInt(urlTimeHex.slice(4, 8).join(""), 16);
|
||||
// Convert to seconds since epoch:
|
||||
timeTyped = cTypes.fileTimeToSecondsSinceEpoch(hi, lo);
|
||||
// Callers expect PRTime, which is microseconds since epoch:
|
||||
timeTyped *= 1000 * 1000;
|
||||
} catch (ex) {
|
||||
// Ignore conversion exceptions. Callers will have to deal
|
||||
// with the fallback value (0).
|
||||
}
|
||||
}
|
||||
}
|
||||
typedURLs.set(url, timeTyped);
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError("Error reading typed URL history: " + ex);
|
||||
} finally {
|
||||
if (typedURLKey) {
|
||||
typedURLKey.close();
|
||||
}
|
||||
if (typedURLTimeKey) {
|
||||
typedURLTimeKey.close();
|
||||
}
|
||||
cTypes.finalize();
|
||||
}
|
||||
return typedURLs;
|
||||
}
|
||||
|
||||
|
||||
// Migrator for form passwords on Windows 8 and higher.
|
||||
function WindowsVaultFormPasswords () {
|
||||
}
|
||||
|
||||
WindowsVaultFormPasswords.prototype = {
|
||||
type: MigrationUtils.resourceTypes.PASSWORDS,
|
||||
|
||||
get exists() {
|
||||
// work only on windows 8+
|
||||
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
|
||||
// check if there are passwords available for migration.
|
||||
return this.migrate(() => {}, true);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* If aOnlyCheckExists is false, import the form passwords on Windows 8 and higher from the vault
|
||||
* and then call the aCallback.
|
||||
* Otherwise, check if there are passwords in the vault.
|
||||
* @param {function} aCallback - a callback called when the migration is done.
|
||||
* @param {boolean} [aOnlyCheckExists=false] - if aOnlyCheckExists is true, just check if there are some
|
||||
* passwords to migrate. Import the passwords from the vault and call aCallback otherwise.
|
||||
* @return true if there are passwords in the vault and aOnlyCheckExists is set to true,
|
||||
* false if there is no password in the vault and aOnlyCheckExists is set to true, undefined if
|
||||
* aOnlyCheckExists is set to false.
|
||||
*/
|
||||
migrate(aCallback, aOnlyCheckExists = false) {
|
||||
// check if the vault item is an IE/Edge one
|
||||
function _isIEOrEdgePassword(id) {
|
||||
return id[0] == INTERNET_EXPLORER_EDGE_GUID[0] &&
|
||||
id[1] == INTERNET_EXPLORER_EDGE_GUID[1] &&
|
||||
id[2] == INTERNET_EXPLORER_EDGE_GUID[2] &&
|
||||
id[3] == INTERNET_EXPLORER_EDGE_GUID[3];
|
||||
}
|
||||
|
||||
let ctypesVaultHelpers = new CtypesVaultHelpers();
|
||||
let ctypesKernelHelpers = new CtypesKernelHelpers();
|
||||
let migrationSucceeded = true;
|
||||
let successfulVaultOpen = false;
|
||||
let error, vault;
|
||||
try {
|
||||
// web credentials vault id
|
||||
let vaultGuid = new ctypesVaultHelpers._structs.GUID(WEB_CREDENTIALS_VAULT_ID);
|
||||
error = new wintypes.DWORD();
|
||||
// web credentials vault
|
||||
vault = new wintypes.VOIDP();
|
||||
// open the current vault using the vaultGuid
|
||||
error = ctypesVaultHelpers._functions.VaultOpenVault(vaultGuid.address(), 0, vault.address());
|
||||
if (error != RESULT_SUCCESS) {
|
||||
throw new Error("Unable to open Vault: " + error);
|
||||
}
|
||||
successfulVaultOpen = true;
|
||||
|
||||
let item = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr();
|
||||
let itemCount = new wintypes.DWORD();
|
||||
// enumerate all the available items. This api is going to return a table of all the
|
||||
// available items and item is going to point to the first element of this table.
|
||||
error = ctypesVaultHelpers._functions.VaultEnumerateItems(vault, VAULT_ENUMERATE_ALL_ITEMS,
|
||||
itemCount.address(),
|
||||
item.address());
|
||||
if (error != RESULT_SUCCESS) {
|
||||
throw new Error("Unable to enumerate Vault items: " + error);
|
||||
}
|
||||
for (let j = 0; j < itemCount.value; j++) {
|
||||
try {
|
||||
// if it's not an ie/edge password, skip it
|
||||
if (!_isIEOrEdgePassword(item.contents.schemaId.id)) {
|
||||
continue;
|
||||
}
|
||||
let url = item.contents.pResourceElement.contents.itemValue.readString();
|
||||
let realURL;
|
||||
try {
|
||||
realURL = Services.io.newURI(url, null, null);
|
||||
} catch (ex) { /* leave realURL as null */ }
|
||||
if (!realURL || ["http", "https", "ftp"].indexOf(realURL.scheme) == -1) {
|
||||
// Ignore items for non-URLs or URLs that aren't HTTP(S)/FTP
|
||||
continue;
|
||||
}
|
||||
|
||||
// if aOnlyCheckExists is set to true, the purpose of the call is to return true if there is at
|
||||
// least a password which is true in this case because a password was by now already found
|
||||
if (aOnlyCheckExists) {
|
||||
return true;
|
||||
}
|
||||
let username = item.contents.pIdentityElement.contents.itemValue.readString();
|
||||
// the current login credential object
|
||||
let credential = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr();
|
||||
error = ctypesVaultHelpers._functions.VaultGetItem(vault,
|
||||
item.contents.schemaId.address(),
|
||||
item.contents.pResourceElement,
|
||||
item.contents.pIdentityElement, null,
|
||||
0, 0, credential.address());
|
||||
if (error != RESULT_SUCCESS) {
|
||||
throw new Error("Unable to get item: " + error);
|
||||
}
|
||||
|
||||
let password = credential.contents.pAuthenticatorElement.contents.itemValue.readString();
|
||||
let creation = Date.now();
|
||||
try {
|
||||
// login manager wants time in milliseconds since epoch, so convert
|
||||
// to seconds since epoch and multiply to get milliseconds:
|
||||
creation = ctypesKernelHelpers.
|
||||
fileTimeToSecondsSinceEpoch(item.contents.highLastModified,
|
||||
item.contents.lowLastModified) * 1000;
|
||||
} catch (ex) {
|
||||
// Ignore exceptions in the dates and just create the login for right now.
|
||||
}
|
||||
// create a new login
|
||||
let login = {
|
||||
username, password,
|
||||
hostname: realURL.prePath,
|
||||
timeCreated: creation,
|
||||
};
|
||||
MigrationUtils.insertLoginWrapper(login);
|
||||
|
||||
// close current item
|
||||
error = ctypesVaultHelpers._functions.VaultFree(credential);
|
||||
if (error == FREE_CLOSE_FAILED) {
|
||||
throw new Error("Unable to free item: " + error);
|
||||
}
|
||||
} catch (e) {
|
||||
migrationSucceeded = false;
|
||||
Cu.reportError(e);
|
||||
} finally {
|
||||
// move to next item in the table returned by VaultEnumerateItems
|
||||
item = item.increment();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
migrationSucceeded = false;
|
||||
} finally {
|
||||
if (successfulVaultOpen) {
|
||||
// close current vault
|
||||
error = ctypesVaultHelpers._functions.VaultCloseVault(vault);
|
||||
if (error == FREE_CLOSE_FAILED) {
|
||||
Cu.reportError("Unable to close vault: " + error);
|
||||
}
|
||||
}
|
||||
ctypesKernelHelpers.finalize();
|
||||
ctypesVaultHelpers.finalize();
|
||||
aCallback(migrationSucceeded);
|
||||
}
|
||||
if (aOnlyCheckExists) {
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
var MSMigrationUtils = {
|
||||
MIGRATION_TYPE_IE: 1,
|
||||
MIGRATION_TYPE_EDGE: 2,
|
||||
CtypesKernelHelpers: CtypesKernelHelpers,
|
||||
getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
|
||||
return new Bookmarks(migrationType);
|
||||
},
|
||||
getCookiesMigrator(migrationType = this.MIGRATION_TYPE_IE) {
|
||||
return new Cookies(migrationType);
|
||||
},
|
||||
getWindowsVaultFormPasswordsMigrator() {
|
||||
return new WindowsVaultFormPasswords();
|
||||
},
|
||||
getTypedURLs,
|
||||
getEdgeLocalDataFolder,
|
||||
};
|
||||
1117
browser/components/migration/MigrationUtils.jsm
Normal file
1117
browser/components/migration/MigrationUtils.jsm
Normal file
File diff suppressed because it is too large
Load diff
21
browser/components/migration/ProfileMigrator.js
Normal file
21
browser/components/migration/ProfileMigrator.js
Normal file
|
|
@ -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]);
|
||||
650
browser/components/migration/SafariProfileMigrator.js
Normal file
650
browser/components/migration/SafariProfileMigrator.js
Normal file
|
|
@ -0,0 +1,650 @@
|
|||
/* 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";
|
||||
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
var Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
|
||||
"resource://gre/modules/Downloads.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PropertyListUtils",
|
||||
"resource://gre/modules/PropertyListUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
|
||||
"resource://gre/modules/FormHistory.jsm");
|
||||
|
||||
function Bookmarks(aBookmarksFile) {
|
||||
this._file = aBookmarksFile;
|
||||
}
|
||||
Bookmarks.prototype = {
|
||||
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
||||
|
||||
migrate: function B_migrate(aCallback) {
|
||||
return Task.spawn(function* () {
|
||||
let dict = yield new Promise(resolve =>
|
||||
PropertyListUtils.read(this._file, resolve)
|
||||
);
|
||||
if (!dict)
|
||||
throw new Error("Could not read Bookmarks.plist");
|
||||
let children = dict.get("Children");
|
||||
if (!children)
|
||||
throw new Error("Invalid Bookmarks.plist format");
|
||||
|
||||
let collection = dict.get("Title") == "com.apple.ReadingList" ?
|
||||
this.READING_LIST_COLLECTION : this.ROOT_COLLECTION;
|
||||
yield this._migrateCollection(children, collection);
|
||||
}.bind(this)).then(() => aCallback(true),
|
||||
e => { Cu.reportError(e); aCallback(false) });
|
||||
},
|
||||
|
||||
// Bookmarks collections in Safari. Constants for migrateCollection.
|
||||
ROOT_COLLECTION: 0,
|
||||
MENU_COLLECTION: 1,
|
||||
TOOLBAR_COLLECTION: 2,
|
||||
READING_LIST_COLLECTION: 3,
|
||||
|
||||
/**
|
||||
* Recursively migrate a Safari collection of bookmarks.
|
||||
*
|
||||
* @param aEntries
|
||||
* the collection's children
|
||||
* @param aCollection
|
||||
* one of the values above.
|
||||
*/
|
||||
_migrateCollection: Task.async(function* (aEntries, aCollection) {
|
||||
// A collection of bookmarks in Safari resembles places roots. In the
|
||||
// property list files (Bookmarks.plist, ReadingList.plist) they are
|
||||
// stored as regular bookmarks folders, and thus can only be distinguished
|
||||
// from by their names and places in the hierarchy.
|
||||
|
||||
let entriesFiltered = [];
|
||||
if (aCollection == this.ROOT_COLLECTION) {
|
||||
for (let entry of aEntries) {
|
||||
let type = entry.get("WebBookmarkType");
|
||||
if (type == "WebBookmarkTypeList" && entry.has("Children")) {
|
||||
let title = entry.get("Title");
|
||||
let children = entry.get("Children");
|
||||
if (title == "BookmarksBar")
|
||||
yield this._migrateCollection(children, this.TOOLBAR_COLLECTION);
|
||||
else if (title == "BookmarksMenu")
|
||||
yield this._migrateCollection(children, this.MENU_COLLECTION);
|
||||
else if (title == "com.apple.ReadingList")
|
||||
yield this._migrateCollection(children, this.READING_LIST_COLLECTION);
|
||||
else if (entry.get("ShouldOmitFromUI") !== true)
|
||||
entriesFiltered.push(entry);
|
||||
}
|
||||
else if (type == "WebBookmarkTypeLeaf") {
|
||||
entriesFiltered.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
entriesFiltered = aEntries;
|
||||
}
|
||||
|
||||
if (entriesFiltered.length == 0)
|
||||
return;
|
||||
|
||||
let folderGuid = -1;
|
||||
switch (aCollection) {
|
||||
case this.ROOT_COLLECTION: {
|
||||
// In Safari, it is possible (though quite cumbersome) to move
|
||||
// bookmarks to the bookmarks root, which is the parent folder of
|
||||
// all bookmarks "collections". That is somewhat in parallel with
|
||||
// both the places root and the unfiled-bookmarks root.
|
||||
// Because the former is only an implementation detail in our UI,
|
||||
// the unfiled root seems to be the best choice.
|
||||
folderGuid = PlacesUtils.bookmarks.unfiledGuid;
|
||||
break;
|
||||
}
|
||||
case this.MENU_COLLECTION: {
|
||||
folderGuid = PlacesUtils.bookmarks.menuGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
folderGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("Safari", folderGuid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case this.TOOLBAR_COLLECTION: {
|
||||
folderGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
if (!MigrationUtils.isStartupMigration) {
|
||||
folderGuid =
|
||||
yield MigrationUtils.createImportedBookmarksFolder("Safari", folderGuid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case this.READING_LIST_COLLECTION: {
|
||||
// Reading list items are imported as regular bookmarks.
|
||||
// They are imported under their own folder, created either under the
|
||||
// bookmarks menu (in the case of startup migration).
|
||||
folderGuid = (yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid: PlacesUtils.bookmarks.menuGuid,
|
||||
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
title: MigrationUtils.getLocalizedString("importedSafariReadingList"),
|
||||
})).guid;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error("Unexpected value for aCollection!");
|
||||
}
|
||||
if (folderGuid == -1)
|
||||
throw new Error("Invalid folder GUID");
|
||||
|
||||
yield this._migrateEntries(entriesFiltered, folderGuid);
|
||||
}),
|
||||
|
||||
// migrate the given array of safari bookmarks to the given places
|
||||
// folder.
|
||||
_migrateEntries: Task.async(function* (entries, parentGuid) {
|
||||
for (let entry of entries) {
|
||||
let type = entry.get("WebBookmarkType");
|
||||
if (type == "WebBookmarkTypeList" && entry.has("Children")) {
|
||||
let title = entry.get("Title");
|
||||
let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title
|
||||
})).guid;
|
||||
|
||||
// Empty folders may not have a children array.
|
||||
if (entry.has("Children"))
|
||||
yield this._migrateEntries(entry.get("Children"), newFolderGuid, false);
|
||||
}
|
||||
else if (type == "WebBookmarkTypeLeaf" && entry.has("URLString")) {
|
||||
let title;
|
||||
if (entry.has("URIDictionary"))
|
||||
title = entry.get("URIDictionary").get("title");
|
||||
|
||||
try {
|
||||
yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid, url: entry.get("URLString"), title
|
||||
});
|
||||
} catch (ex) {
|
||||
Cu.reportError("Invalid Safari bookmark: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
function History(aHistoryFile) {
|
||||
this._file = aHistoryFile;
|
||||
}
|
||||
History.prototype = {
|
||||
type: MigrationUtils.resourceTypes.HISTORY,
|
||||
|
||||
// Helper method for converting the visit date property to a PRTime value.
|
||||
// The visit date is stored as a string, so it's not read as a Date
|
||||
// object by PropertyListUtils.
|
||||
_parseCocoaDate: function H___parseCocoaDate(aCocoaDateStr) {
|
||||
let asDouble = parseFloat(aCocoaDateStr);
|
||||
if (!isNaN(asDouble)) {
|
||||
// reference date of NSDate.
|
||||
let date = new Date("1 January 2001, GMT");
|
||||
date.setMilliseconds(asDouble * 1000);
|
||||
return date * 1000;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
migrate: function H_migrate(aCallback) {
|
||||
PropertyListUtils.read(this._file, function migrateHistory(aDict) {
|
||||
try {
|
||||
if (!aDict)
|
||||
throw new Error("Could not read history property list");
|
||||
if (!aDict.has("WebHistoryDates"))
|
||||
throw new Error("Unexpected history-property list format");
|
||||
|
||||
// Safari's History file contains only top-level urls. It does not
|
||||
// distinguish between typed urls and linked urls.
|
||||
let transType = PlacesUtils.history.TRANSITION_LINK;
|
||||
|
||||
let places = [];
|
||||
let entries = aDict.get("WebHistoryDates");
|
||||
for (let entry of entries) {
|
||||
if (entry.has("lastVisitedDate")) {
|
||||
let visitDate = this._parseCocoaDate(entry.get("lastVisitedDate"));
|
||||
try {
|
||||
places.push({ uri: NetUtil.newURI(entry.get("")),
|
||||
title: entry.get("title"),
|
||||
visits: [{ transitionType: transType,
|
||||
visitDate: visitDate }] });
|
||||
}
|
||||
catch (ex) {
|
||||
// Safari's History file may contain malformed URIs which
|
||||
// will be ignored.
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (places.length > 0) {
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult: function() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError: function() {},
|
||||
handleCompletion: function() {
|
||||
aCallback(this._success);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
aCallback(false);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
aCallback(false);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Safari's preferences property list is independently used for three purposes:
|
||||
* (a) importation of preferences
|
||||
* (b) importation of search strings
|
||||
* (c) retrieving the home page.
|
||||
*
|
||||
* So, rather than reading it three times, it's cached and managed here.
|
||||
*/
|
||||
function MainPreferencesPropertyList(aPreferencesFile) {
|
||||
this._file = aPreferencesFile;
|
||||
this._callbacks = [];
|
||||
}
|
||||
MainPreferencesPropertyList.prototype = {
|
||||
/**
|
||||
* @see PropertyListUtils.read
|
||||
*/
|
||||
read: function MPPL_read(aCallback) {
|
||||
if ("_dict" in this) {
|
||||
aCallback(this._dict);
|
||||
return;
|
||||
}
|
||||
|
||||
let alreadyReading = this._callbacks.length > 0;
|
||||
this._callbacks.push(aCallback);
|
||||
if (!alreadyReading) {
|
||||
PropertyListUtils.read(this._file, function readPrefs(aDict) {
|
||||
this._dict = aDict;
|
||||
for (let callback of this._callbacks) {
|
||||
try {
|
||||
callback(aDict);
|
||||
}
|
||||
catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
this._callbacks.splice(0);
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
// Workaround for nsIBrowserProfileMigrator.sourceHomePageURL until
|
||||
// it's replaced with an async method.
|
||||
_readSync: function MPPL__readSync() {
|
||||
if ("_dict" in this)
|
||||
return this._dict;
|
||||
|
||||
let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
|
||||
createInstance(Ci.nsIFileInputStream);
|
||||
inputStream.init(this._file, -1, -1, 0);
|
||||
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
|
||||
createInstance(Ci.nsIBinaryInputStream);
|
||||
binaryStream.setInputStream(inputStream);
|
||||
let bytes = binaryStream.readByteArray(inputStream.available());
|
||||
this._dict = PropertyListUtils._readFromArrayBufferSync(
|
||||
new Uint8Array(bytes).buffer);
|
||||
return this._dict;
|
||||
}
|
||||
};
|
||||
|
||||
function Preferences(aMainPreferencesPropertyListInstance) {
|
||||
this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance;
|
||||
}
|
||||
Preferences.prototype = {
|
||||
type: MigrationUtils.resourceTypes.SETTINGS,
|
||||
|
||||
migrate: function MPR_migrate(aCallback) {
|
||||
this._mainPreferencesPropertyList.read(aDict => {
|
||||
Task.spawn(function* () {
|
||||
if (!aDict)
|
||||
throw new Error("Could not read preferences file");
|
||||
|
||||
this._dict = aDict;
|
||||
|
||||
let invert = webkitVal => !webkitVal;
|
||||
this._set("AutoFillPasswords", "signon.rememberSignons");
|
||||
this._set("OpenNewTabsInFront", "browser.tabs.loadInBackground", invert);
|
||||
this._set("WebKitJavaScriptCanOpenWindowsAutomatically",
|
||||
"dom.disable_open_during_load", invert);
|
||||
|
||||
// layout.spellcheckDefault is a boolean stored as a number.
|
||||
this._set("WebContinuousSpellCheckingEnabled",
|
||||
"layout.spellcheckDefault", Number);
|
||||
|
||||
// Auto-load images
|
||||
// Firefox has an elaborate set of Image preferences. The correlation is:
|
||||
// Mode: Safari Firefox
|
||||
// Blocked FALSE 2
|
||||
// Allowed TRUE 1
|
||||
// Allowed, originating site only -- 3
|
||||
this._set("WebKitDisplayImagesKey", "permissions.default.image",
|
||||
webkitVal => webkitVal ? 1 : 2);
|
||||
|
||||
this._migrateFontSettings();
|
||||
yield this._migrateDownloadsFolder();
|
||||
}.bind(this)).then(() => aCallback(true), ex => {
|
||||
Cu.reportError(ex);
|
||||
aCallback(false);
|
||||
}).catch(Cu.reportError);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempts to migrates a preference from Safari. Returns whether the preference
|
||||
* has been migrated.
|
||||
* @param aSafariKey
|
||||
* The dictionary key for the preference of Safari.
|
||||
* @param aMozPref
|
||||
* The gecko/firefox preference to which aSafariKey should be migrated
|
||||
* @param [optional] aConvertFunction(aSafariValue)
|
||||
* a function that converts the safari-preference value to the
|
||||
* appropriate value for aMozPref. If it's not passed, then the
|
||||
* Safari value is set as is.
|
||||
* If aConvertFunction returns undefined, then aMozPref is not set
|
||||
* at all.
|
||||
* @return whether or not aMozPref was set.
|
||||
*/
|
||||
_set: function MPR_set(aSafariKey, aMozPref, aConvertFunction) {
|
||||
if (this._dict.has(aSafariKey)) {
|
||||
let safariVal = this._dict.get(aSafariKey);
|
||||
let mozVal = aConvertFunction !== undefined ?
|
||||
aConvertFunction(safariVal) : safariVal;
|
||||
switch (typeof mozVal) {
|
||||
case "string":
|
||||
Services.prefs.setCharPref(aMozPref, mozVal);
|
||||
break;
|
||||
case "number":
|
||||
Services.prefs.setIntPref(aMozPref, mozVal);
|
||||
break;
|
||||
case "boolean":
|
||||
Services.prefs.setBoolPref(aMozPref, mozVal);
|
||||
break;
|
||||
case "undefined":
|
||||
return false;
|
||||
default:
|
||||
throw new Error("Unexpected value type: " + (typeof mozVal));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// Fonts settings are quite problematic for migration, for a couple of
|
||||
// reasons:
|
||||
// (a) Every font preference in Gecko is set for a particular language.
|
||||
// In Safari, each font preference applies to all languages.
|
||||
// (b) The current underlying implementation of nsIFontEnumerator cannot
|
||||
// really tell you anything about a font: no matter what language or type
|
||||
// you try to enumerate with EnumerateFonts, you get an array of all
|
||||
// fonts in the systems (This also breaks our fonts dialog).
|
||||
// (c) In Gecko, each langauge has a distinct serif and sans-serif font
|
||||
// preference. Safari has only one default font setting. It seems that
|
||||
// it checks if it's a serif or sans serif font, and when a site
|
||||
// explicitly asks to use serif/sans-serif font, it uses the default font
|
||||
// only if it applies to this type.
|
||||
// (d) The solution of guessing the lang-group out of the default charset (as
|
||||
// done in the old Safari migrator) can only work when:
|
||||
// (1) The default charset preference is set.
|
||||
// (2) It's not a unicode charset.
|
||||
// For now, we use the language implied by the system locale as the
|
||||
// lang-group. The only exception is minimal font size, which is an
|
||||
// accessibility preference in Safari (under the Advanced tab). If it is set,
|
||||
// we set it for all languages.
|
||||
// As for the font type of the default font (serif/sans-serif), the default
|
||||
// type for the given language is used (set in font.default.LANGGROUP).
|
||||
_migrateFontSettings: function MPR__migrateFontSettings() {
|
||||
// If "Never use font sizes smaller than [ ] is set", migrate it for all
|
||||
// languages.
|
||||
if (this._dict.has("WebKitMinimumFontSize")) {
|
||||
let minimumSize = this._dict.get("WebKitMinimumFontSize");
|
||||
if (typeof minimumSize == "number") {
|
||||
let prefs = Services.prefs.getChildList("font.minimum-size");
|
||||
for (let pref of prefs) {
|
||||
Services.prefs.setIntPref(pref, minimumSize);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Cu.reportError("WebKitMinimumFontSize was set to an invalid value: " +
|
||||
minimumSize);
|
||||
}
|
||||
}
|
||||
|
||||
// In theory, the lang group could be "x-unicode". This will result
|
||||
// in setting the fonts for "Other Languages".
|
||||
let lang = this._getLocaleLangGroup();
|
||||
|
||||
let anySet = false;
|
||||
let fontType = Services.prefs.getCharPref("font.default." + lang);
|
||||
anySet |= this._set("WebKitFixedFont", "font.name.monospace." + lang);
|
||||
anySet |= this._set("WebKitDefaultFixedFontSize", "font.size.fixed." + lang);
|
||||
anySet |= this._set("WebKitStandardFont",
|
||||
"font.name." + fontType + "." + lang);
|
||||
anySet |= this._set("WebKitDefaultFontSize", "font.size.variable." + lang);
|
||||
|
||||
// If we set font settings for a particular language, we'll also set the
|
||||
// fonts dialog to open with the fonts settings for that langauge.
|
||||
if (anySet)
|
||||
Services.prefs.setCharPref("font.language.group", lang);
|
||||
},
|
||||
|
||||
// Get the language group for the system locale.
|
||||
_getLocaleLangGroup: function MPR__getLocaleLangGroup() {
|
||||
let locale = Services.locale.getLocaleComponentForUserAgent();
|
||||
|
||||
// See nsLanguageAtomService::GetLanguageGroup
|
||||
let localeLangGroup = "x-unicode";
|
||||
let bundle = Services.strings.createBundle(
|
||||
"resource://gre/res/langGroups.properties");
|
||||
try {
|
||||
localeLangGroup = bundle.GetStringFromName(locale);
|
||||
}
|
||||
catch (ex) {
|
||||
let hyphenAt = locale.indexOf("-");
|
||||
if (hyphenAt != -1) {
|
||||
try {
|
||||
localeLangGroup = bundle.GetStringFromName(locale.substr(0, hyphenAt));
|
||||
}
|
||||
catch (ex2) { }
|
||||
}
|
||||
}
|
||||
return localeLangGroup;
|
||||
},
|
||||
|
||||
_migrateDownloadsFolder: Task.async(function* () {
|
||||
if (!this._dict.has("DownloadsPath"))
|
||||
return;
|
||||
|
||||
let downloadsFolder = FileUtils.File(this._dict.get("DownloadsPath"));
|
||||
|
||||
// If the download folder is set to the Desktop or to ~/Downloads, set the
|
||||
// folderList pref appropriately so that "Desktop"/Downloads is shown with
|
||||
// pretty name in the preferences dialog.
|
||||
let folderListVal = 2;
|
||||
if (downloadsFolder.equals(FileUtils.getDir("Desk", []))) {
|
||||
folderListVal = 0;
|
||||
}
|
||||
else {
|
||||
let systemDownloadsPath = yield Downloads.getSystemDownloadsDirectory();
|
||||
let systemDownloadsFolder = FileUtils.File(systemDownloadsPath);
|
||||
if (downloadsFolder.equals(systemDownloadsFolder))
|
||||
folderListVal = 1;
|
||||
}
|
||||
Services.prefs.setIntPref("browser.download.folderList", folderListVal);
|
||||
Services.prefs.setComplexValue("browser.download.dir", Ci.nsILocalFile,
|
||||
downloadsFolder);
|
||||
}),
|
||||
};
|
||||
|
||||
function SearchStrings(aMainPreferencesPropertyListInstance) {
|
||||
this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance;
|
||||
}
|
||||
SearchStrings.prototype = {
|
||||
type: MigrationUtils.resourceTypes.OTHERDATA,
|
||||
|
||||
migrate: function SS_migrate(aCallback) {
|
||||
this._mainPreferencesPropertyList.read(MigrationUtils.wrapMigrateFunction(
|
||||
function migrateSearchStrings(aDict) {
|
||||
if (!aDict)
|
||||
throw new Error("Could not get preferences dictionary");
|
||||
|
||||
if (aDict.has("RecentSearchStrings")) {
|
||||
let recentSearchStrings = aDict.get("RecentSearchStrings");
|
||||
if (recentSearchStrings && recentSearchStrings.length > 0) {
|
||||
let changes = recentSearchStrings.map((searchString) => (
|
||||
{op: "add",
|
||||
fieldname: "searchbar-history",
|
||||
value: searchString}));
|
||||
FormHistory.update(changes);
|
||||
}
|
||||
}
|
||||
}.bind(this), aCallback));
|
||||
}
|
||||
};
|
||||
|
||||
// On OS X, the cookie-accept policy preference is stored in a separate
|
||||
// property list.
|
||||
function WebFoundationCookieBehavior(aWebFoundationFile) {
|
||||
this._file = aWebFoundationFile;
|
||||
}
|
||||
WebFoundationCookieBehavior.prototype = {
|
||||
type: MigrationUtils.resourceTypes.SETTINGS,
|
||||
|
||||
migrate: function WFPL_migrate(aCallback) {
|
||||
PropertyListUtils.read(this._file, MigrationUtils.wrapMigrateFunction(
|
||||
function migrateCookieBehavior(aDict) {
|
||||
if (!aDict)
|
||||
throw new Error("Could not read com.apple.WebFoundation.plist");
|
||||
|
||||
if (aDict.has("NSHTTPAcceptCookies")) {
|
||||
// Setting Safari Firefox
|
||||
// Always Accept always 0
|
||||
// Accept from Originating current page 1
|
||||
// Never Accept never 2
|
||||
let acceptCookies = aDict.get("NSHTTPAcceptCookies");
|
||||
let cookieValue = 0;
|
||||
if (acceptCookies == "never") {
|
||||
cookieValue = 2;
|
||||
} else if (acceptCookies == "current page") {
|
||||
cookieValue = 1;
|
||||
}
|
||||
Services.prefs.setIntPref("network.cookie.cookieBehavior",
|
||||
cookieValue);
|
||||
}
|
||||
}.bind(this), aCallback));
|
||||
}
|
||||
};
|
||||
|
||||
function SafariProfileMigrator() {
|
||||
}
|
||||
|
||||
SafariProfileMigrator.prototype = Object.create(MigratorPrototype);
|
||||
|
||||
SafariProfileMigrator.prototype.getResources = function SM_getResources() {
|
||||
let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false);
|
||||
if (!profileDir.exists())
|
||||
return null;
|
||||
|
||||
let resources = [];
|
||||
let pushProfileFileResource = function(aFileName, aConstructor) {
|
||||
let file = profileDir.clone();
|
||||
file.append(aFileName);
|
||||
if (file.exists())
|
||||
resources.push(new aConstructor(file));
|
||||
};
|
||||
|
||||
pushProfileFileResource("History.plist", History);
|
||||
pushProfileFileResource("Bookmarks.plist", Bookmarks);
|
||||
|
||||
// The Reading List feature was introduced at the same time in Windows and
|
||||
// Mac versions of Safari. Not surprisingly, they are stored in the same
|
||||
// format in both versions. Surpsingly, only on Windows there is a
|
||||
// separate property list for it. This code is used on mac too, because
|
||||
// Apple may fix this at some point.
|
||||
pushProfileFileResource("ReadingList.plist", Bookmarks);
|
||||
|
||||
let prefs = this.mainPreferencesPropertyList;
|
||||
if (prefs) {
|
||||
resources.push(new Preferences(prefs));
|
||||
resources.push(new SearchStrings(prefs));
|
||||
}
|
||||
|
||||
let wfFile = FileUtils.getFile("UsrPrfs", ["com.apple.WebFoundation.plist"]);
|
||||
if (wfFile.exists())
|
||||
resources.push(new WebFoundationCookieBehavior(wfFile));
|
||||
|
||||
return resources;
|
||||
};
|
||||
|
||||
SafariProfileMigrator.prototype.getLastUsedDate = function SM_getLastUsedDate() {
|
||||
let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false);
|
||||
let datePromises = ["Bookmarks.plist", "History.plist"].map(file => {
|
||||
let path = OS.Path.join(profileDir.path, file);
|
||||
return OS.File.stat(path).catch(() => null).then(info => {
|
||||
return info ? info.lastModificationDate : 0;
|
||||
});
|
||||
});
|
||||
return Promise.all(datePromises).then(dates => {
|
||||
return new Date(Math.max.apply(Math, dates));
|
||||
});
|
||||
};
|
||||
|
||||
Object.defineProperty(SafariProfileMigrator.prototype, "mainPreferencesPropertyList", {
|
||||
get: function get_mainPreferencesPropertyList() {
|
||||
if (this._mainPreferencesPropertyList === undefined) {
|
||||
let file = FileUtils.getDir("UsrPrfs", [], false);
|
||||
if (file.exists()) {
|
||||
file.append("com.apple.Safari.plist");
|
||||
if (file.exists()) {
|
||||
this._mainPreferencesPropertyList =
|
||||
new MainPreferencesPropertyList(file);
|
||||
return this._mainPreferencesPropertyList;
|
||||
}
|
||||
}
|
||||
this._mainPreferencesPropertyList = null;
|
||||
return this._mainPreferencesPropertyList;
|
||||
}
|
||||
return this._mainPreferencesPropertyList;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(SafariProfileMigrator.prototype, "sourceHomePageURL", {
|
||||
get: function get_sourceHomePageURL() {
|
||||
if (this.mainPreferencesPropertyList) {
|
||||
let dict = this.mainPreferencesPropertyList._readSync();
|
||||
if (dict.has("HomePage"))
|
||||
return dict.get("HomePage");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
SafariProfileMigrator.prototype.classDescription = "Safari Profile Migrator";
|
||||
SafariProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=safari";
|
||||
SafariProfileMigrator.prototype.classID = Components.ID("{4b609ecf-60b2-4655-9df4-dc149e474da1}");
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SafariProfileMigrator]);
|
||||
82
browser/components/migration/content/aboutWelcomeBack.xhtml
Normal file
82
browser/components/migration/content/aboutWelcomeBack.xhtml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
<!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
|
||||
%netErrorDTD;
|
||||
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
|
||||
%globalDTD;
|
||||
<!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd">
|
||||
%restorepageDTD;
|
||||
]>
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<head>
|
||||
<title>&welcomeback2.tabtitle;</title>
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/aboutWelcomeBack.css" type="text/css" media="all"/>
|
||||
<link rel="icon" type="image/png" href="chrome://global/skin/icons/information-16.png"/>
|
||||
|
||||
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/>
|
||||
</head>
|
||||
|
||||
<body dir="&locale.dir;">
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="title">
|
||||
<h1 class="title-text">&welcomeback2.pageTitle;</h1>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
|
||||
<p>&welcomeback2.pageInfo1;</p>
|
||||
<!-- Note a href in the anchor below is added by JS -->
|
||||
<p>&welcomeback2.beforelink.pageInfo2;<a id="linkMoreTroubleshooting" target="_blank">&welcomeback2.link.pageInfo2;</a>&welcomeback2.afterlink.pageInfo2;</p>
|
||||
|
||||
<div>
|
||||
<div class="radioRestoreContainer">
|
||||
<input class="radioRestoreButton" id="radioRestoreAll" type="radio"
|
||||
name="restore" checked="checked"/>
|
||||
<label class="radioRestoreLabel" for="radioRestoreAll">&welcomeback2.label.restoreAll;</label>
|
||||
</div>
|
||||
|
||||
<div class="radioRestoreContainer">
|
||||
<input class="radioRestoreButton" id="radioRestoreChoose" type="radio"
|
||||
name="restore"/>
|
||||
<label class="radioRestoreLabel" for="radioRestoreChoose">&welcomeback2.label.restoreSome;</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tree-container">
|
||||
<xul:tree id="tabList" flex="1" seltype="single" hidecolumnpicker="true"
|
||||
onclick="onListClick(event);" onkeydown="onListKeyDown(event);"
|
||||
_window_label="&restorepage.windowLabel;">
|
||||
<xul:treecols>
|
||||
<xul:treecol cycler="true" id="restore" type="checkbox" label="&restorepage.restoreHeader;"/>
|
||||
<xul:splitter class="tree-splitter"/>
|
||||
<xul:treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/>
|
||||
</xul:treecols>
|
||||
<xul:treechildren flex="1"/>
|
||||
</xul:tree>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<xul:button class="primary"
|
||||
id="errorTryAgain"
|
||||
label="&welcomeback2.restoreButton;"
|
||||
accesskey="&welcomeback2.restoreButton.access;"
|
||||
oncommand="restoreSession();"/>
|
||||
</div>
|
||||
|
||||
<input type="text" id="sessionData" style="display: none;"/>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Automigration undo notification.
|
||||
# %1$S will be replaced with the name of the browser we imported from, %2$S will be replaced with brandShortName
|
||||
automigration.undo.message.all = Pick up where you left off. We’ve imported these sites and your bookmarks, history and passwords from %1$S into %2$S.
|
||||
automigration.undo.message.bookmarks = Pick up where you left off. We’ve imported these sites and your bookmarks from %1$S into %2$S.
|
||||
automigration.undo.message.bookmarks.logins = Pick up where you left off. We’ve imported these sites and your bookmarks and passwords from %1$S into %2$S.
|
||||
automigration.undo.message.bookmarks.visits = Pick up where you left off. We’ve imported these sites and your bookmarks and history from %1$S into %2$S.
|
||||
automigration.undo.message.logins = Pick up where you left off. We’ve imported your passwords from %1$S into %2$S.
|
||||
automigration.undo.message.logins.visits = Pick up where you left off. We’ve imported these sites and your history and passwords from %1$S into %2$S.
|
||||
automigration.undo.message.visits = Pick up where you left off. We’ve imported these sites and your history from %1$S into %2$S.
|
||||
automigration.undo.keep2.label = OK, Got it
|
||||
automigration.undo.keep2.accesskey = O
|
||||
automigration.undo.dontkeep2.label = No Thanks
|
||||
automigration.undo.dontkeep2.accesskey = N
|
||||
automigration.undo.unknownbrowser = Unknown Browser
|
||||
549
browser/components/migration/content/migration.js
Normal file
549
browser/components/migration/content/migration.js
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
/* 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";
|
||||
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
var Cu = Components.utils;
|
||||
|
||||
const kIMig = Ci.nsIBrowserProfileMigrator;
|
||||
const kIPStartup = Ci.nsIProfileStartup;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm");
|
||||
|
||||
var MigrationWizard = { /* exported 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 ()
|
||||
{
|
||||
let os = Services.obs;
|
||||
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;
|
||||
|
||||
let args = window.arguments;
|
||||
let entryPointId = args[0] || MigrationUtils.MIGRATION_ENTRYPOINT_UNKNOWN;
|
||||
Services.telemetry.getHistogramById("FX_MIGRATION_ENTRY_POINT").add(entryPointId);
|
||||
this.isInitialMigration = entryPointId == MigrationUtils.MIGRATION_ENTRYPOINT_FIRSTRUN;
|
||||
|
||||
if (args.length > 1) {
|
||||
this._source = args[1];
|
||||
this._migrator = args[2] instanceof kIMig ? args[2] : null;
|
||||
this._autoMigrate = args[3].QueryInterface(kIPStartup);
|
||||
this._skipImportSourcePage = args[4];
|
||||
if (this._migrator && args[5]) {
|
||||
let sourceProfiles = this._migrator.sourceProfiles;
|
||||
this._selectedProfile = sourceProfiles.find(profile => profile.id == args[5]);
|
||||
}
|
||||
|
||||
if (this._autoMigrate) {
|
||||
// Show the "nothing" option in the automigrate case to provide an
|
||||
// easily identifiable way to avoid migration and create a new profile.
|
||||
document.getElementById("nothing").hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.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 ()
|
||||
{
|
||||
// Show warning message to close the selected browser when needed
|
||||
function toggleCloseBrowserWarning() {
|
||||
let visibility = "hidden";
|
||||
if (group.selectedItem.id != "nothing") {
|
||||
let migrator = MigrationUtils.getMigrator(group.selectedItem.id);
|
||||
visibility = migrator.sourceLocked ? "visible" : "hidden";
|
||||
}
|
||||
document.getElementById("closeSourceBrowser").style.visibility = visibility;
|
||||
}
|
||||
this._wiz.canRewind = false;
|
||||
|
||||
var selectedMigrator = null;
|
||||
this._availableMigrators = [];
|
||||
|
||||
// Figure out what source apps are are available to import from:
|
||||
var group = document.getElementById("importSourceGroup");
|
||||
for (var i = 0; i < group.childNodes.length; ++i) {
|
||||
var migratorKey = group.childNodes[i].id;
|
||||
if (migratorKey != "nothing") {
|
||||
var migrator = 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];
|
||||
this._availableMigrators.push([migratorKey, migrator]);
|
||||
} else {
|
||||
// Hide this option
|
||||
group.childNodes[i].hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isInitialMigration) {
|
||||
Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_BROWSER_COUNT")
|
||||
.add(this._availableMigrators.length);
|
||||
let defaultBrowser = MigrationUtils.getMigratorKeyForDefaultBrowser();
|
||||
// This will record 0 for unknown default browser IDs.
|
||||
defaultBrowser = MigrationUtils.getSourceIdForTelemetry(defaultBrowser);
|
||||
Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_EXISTING_DEFAULT_BROWSER")
|
||||
.add(defaultBrowser);
|
||||
}
|
||||
|
||||
group.addEventListener("command", toggleCloseBrowserWarning);
|
||||
|
||||
if (selectedMigrator) {
|
||||
group.selectedItem = selectedMigrator;
|
||||
toggleCloseBrowserWarning();
|
||||
} 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") {
|
||||
// Need to do telemetry here because we're closing the dialog before we get to
|
||||
// do actual migration. For actual migration, this doesn't happen until after
|
||||
// migration takes place.
|
||||
Services.telemetry.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
|
||||
.add(MigrationUtils.getSourceIdForTelemetry("nothing"));
|
||||
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;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// 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 ()
|
||||
{
|
||||
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");
|
||||
var pageTitle, pageDesc, mainStr;
|
||||
// These strings don't exist when not using official branding. If that's
|
||||
// the case, just skip this page.
|
||||
try {
|
||||
pageTitle = brandBundle.getString("homePageMigrationPageTitle");
|
||||
pageDesc = brandBundle.getString("homePageMigrationDescription");
|
||||
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 appName = MigrationUtils.getBrowserName(this._source);
|
||||
|
||||
// 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 && appName) {
|
||||
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);
|
||||
},
|
||||
|
||||
onMigratingMigrate: function ()
|
||||
{
|
||||
this._migrator.migrate(this._itemsFlags, this._autoMigrate, this._selectedProfile);
|
||||
|
||||
Services.telemetry.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
|
||||
.add(MigrationUtils.getSourceIdForTelemetry(this._source));
|
||||
if (!this._autoMigrate) {
|
||||
let hist = Services.telemetry.getKeyedHistogramById("FX_MIGRATION_USAGE");
|
||||
let exp = 0;
|
||||
let items = this._itemsFlags;
|
||||
while (items) {
|
||||
if (items & 1) {
|
||||
hist.add(this._source, exp);
|
||||
}
|
||||
items = items >> 1;
|
||||
exp++;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_listItems: function (aID)
|
||||
{
|
||||
var items = document.getElementById(aID);
|
||||
while (items.hasChildNodes())
|
||||
items.removeChild(items.firstChild);
|
||||
|
||||
var itemID;
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
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)
|
||||
{
|
||||
var label;
|
||||
switch (aTopic) {
|
||||
case "Migration:Started":
|
||||
break;
|
||||
case "Migration:ItemBeforeMigrate":
|
||||
label = document.getElementById(aData + "_migrated");
|
||||
if (label)
|
||||
label.setAttribute("style", "font-weight: bold");
|
||||
break;
|
||||
case "Migration:ItemAfterMigrate":
|
||||
label = document.getElementById(aData + "_migrated");
|
||||
if (label)
|
||||
label.removeAttribute("style");
|
||||
break;
|
||||
case "Migration:Ended":
|
||||
if (this.isInitialMigration) {
|
||||
// Ensure errors in reporting data recency do not affect the rest of the migration.
|
||||
try {
|
||||
this.reportDataRecencyTelemetry();
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
if (this._autoMigrate) {
|
||||
let hasImportedHomepage = !!(this._newHomePage && this._newHomePage != "DEFAULT");
|
||||
Services.telemetry.getKeyedHistogramById("FX_MIGRATION_IMPORTED_HOMEPAGE")
|
||||
.add(this._source, hasImportedHomepage);
|
||||
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":
|
||||
let type = "undefined";
|
||||
let numericType = parseInt(aData);
|
||||
switch (numericType) {
|
||||
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.");
|
||||
Services.telemetry.getKeyedHistogramById("FX_MIGRATION_ERRORS")
|
||||
.add(this._source, Math.log2(numericType));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onDonePageShow: function ()
|
||||
{
|
||||
this._wiz.getButton("cancel").disabled = true;
|
||||
this._wiz.canRewind = false;
|
||||
this._listItems("doneItems");
|
||||
},
|
||||
|
||||
reportDataRecencyTelemetry() {
|
||||
let histogram = Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_DATA_RECENCY");
|
||||
let lastUsedPromises = [];
|
||||
for (let [key, migrator] of this._availableMigrators) {
|
||||
// No block-scoped let in for...of loop conditions, so get the source:
|
||||
let localKey = key;
|
||||
lastUsedPromises.push(migrator.getLastUsedDate().then(date => {
|
||||
const ONE_YEAR = 24 * 365;
|
||||
let diffInHours = Math.round((Date.now() - date) / (60 * 60 * 1000));
|
||||
if (diffInHours > ONE_YEAR) {
|
||||
diffInHours = ONE_YEAR;
|
||||
}
|
||||
histogram.add(localKey, diffInHours);
|
||||
return [localKey, diffInHours];
|
||||
}));
|
||||
}
|
||||
Promise.all(lastUsedPromises).then(migratorUsedTimeDiff => {
|
||||
// Sort low to high.
|
||||
migratorUsedTimeDiff.sort(([keyA, diffA], [keyB, diffB]) => diffA - diffB); /* eslint no-unused-vars: off */
|
||||
let usedMostRecentBrowser = migratorUsedTimeDiff.length && this._source == migratorUsedTimeDiff[0][0];
|
||||
let usedRecentBrowser =
|
||||
Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_USED_RECENT_BROWSER");
|
||||
usedRecentBrowser.add(this._source, usedMostRecentBrowser);
|
||||
});
|
||||
},
|
||||
};
|
||||
109
browser/components/migration/content/migration.xul
Normal file
109
browser/components/migration/content/migration.xul
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?xml version="1.0"?>
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog SYSTEM "chrome://browser/locale/migration/migration.dtd" >
|
||||
|
||||
<wizard id="migrationWizard"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
windowtype="Browser:MigrationWizard"
|
||||
title="&migrationWizard.title;"
|
||||
onload="MigrationWizard.init()"
|
||||
onunload="MigrationWizard.uninit()"
|
||||
style="width: 40em;"
|
||||
buttons="accept,cancel"
|
||||
branded="true">
|
||||
|
||||
<script type="application/javascript" src="chrome://browser/content/migration/migration.js"/>
|
||||
|
||||
<stringbundle id="brandBundle" src="chrome://branding/locale/brand.properties"/>
|
||||
|
||||
<wizardpage id="importSource" pageid="importSource" next="selectProfile"
|
||||
label="&importSource.title;"
|
||||
onpageadvanced="return MigrationWizard.onImportSourcePageAdvanced();">
|
||||
#ifdef XP_WIN
|
||||
<description id="importAll" control="importSourceGroup">&importFrom.label;</description>
|
||||
#else
|
||||
<description id="importAll" control="importSourceGroup">&importFromUnix.label;</description>
|
||||
#endif
|
||||
<description id="importBookmarks" control="importSourceGroup" hidden="true">&importFromBookmarks.label;</description>
|
||||
|
||||
<radiogroup id="importSourceGroup" align="start">
|
||||
# NB: if you add items to this list, please also assign them a unique migrator ID in MigrationUtils.jsm
|
||||
<radio id="firefox" label="&importFromFirefox.label;" accesskey="&importFromFirefox.accesskey;"/>
|
||||
#ifdef XP_WIN
|
||||
<radio id="edge" label="&importFromEdge.label;" accesskey="&importFromEdge.accesskey;"/>
|
||||
<radio id="ie" label="&importFromIE.label;" accesskey="&importFromIE.accesskey;"/>
|
||||
<radio id="chrome" label="&importFromChrome.label;" accesskey="&importFromChrome.accesskey;"/>
|
||||
<radio id="chromium" label="&importFromChromium.label;" accesskey="&importFromChromium.accesskey;"/>
|
||||
<radio id="canary" label="&importFromCanary.label;" accesskey="&importFromCanary.accesskey;"/>
|
||||
<radio id="360se" label="&importFrom360se.label;" accesskey="&importFrom360se.accesskey;"/>
|
||||
#elifdef XP_MACOSX
|
||||
<radio id="safari" label="&importFromSafari.label;" accesskey="&importFromSafari.accesskey;"/>
|
||||
<radio id="chrome" label="&importFromChrome.label;" accesskey="&importFromChrome.accesskey;"/>
|
||||
<radio id="chromium" label="&importFromChromium.label;" accesskey="&importFromChromium.accesskey;"/>
|
||||
<radio id="canary" label="&importFromCanary.label;" accesskey="&importFromCanary.accesskey;"/>
|
||||
#elifdef XP_UNIX
|
||||
<radio id="chrome" label="&importFromChrome.label;" accesskey="&importFromChrome.accesskey;"/>
|
||||
<radio id="chromium" label="&importFromChromium.label;" accesskey="&importFromChromium.accesskey;"/>
|
||||
#endif
|
||||
<radio id="nothing" label="&importFromNothing.label;" accesskey="&importFromNothing.accesskey;" hidden="true"/>
|
||||
</radiogroup>
|
||||
<label id="noSources" hidden="true">&noMigrationSources.label;</label>
|
||||
<spacer flex="1"/>
|
||||
<description class="header" id="closeSourceBrowser" style="visibility:hidden">&closeSourceBrowser.label;</description>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="selectProfile" pageid="selectProfile" label="&selectProfile.title;"
|
||||
next="importItems"
|
||||
onpageshow="return MigrationWizard.onSelectProfilePageShow();"
|
||||
onpagerewound="return MigrationWizard.onSelectProfilePageRewound();"
|
||||
onpageadvanced="return MigrationWizard.onSelectProfilePageAdvanced();">
|
||||
<description control="profiles">&selectProfile.label;</description>
|
||||
|
||||
<radiogroup id="profiles" align="left"/>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="importItems" pageid="importItems" label="&importItems.title;"
|
||||
next="homePageImport"
|
||||
onpageshow="return MigrationWizard.onImportItemsPageShow();"
|
||||
onpagerewound="return MigrationWizard.onImportItemsPageRewound();"
|
||||
onpageadvanced="return MigrationWizard.onImportItemsPageAdvanced();"
|
||||
oncommand="MigrationWizard.onImportItemCommand();">
|
||||
<description control="dataSources">&importItems.label;</description>
|
||||
|
||||
<vbox id="dataSources" style="overflow: auto; -moz-appearance: listbox" align="left" flex="1" role="group"/>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="homePageImport" pageid="homePageImport"
|
||||
next="migrating"
|
||||
onpageshow="return MigrationWizard.onHomePageMigrationPageShow();"
|
||||
onpageadvanced="return MigrationWizard.onHomePageMigrationPageAdvanced();">
|
||||
|
||||
<description id="homePageImportDesc" control="homePageRadioGroup"/>
|
||||
<radiogroup id="homePageRadiogroup">
|
||||
<radio id="homePageSingleStart" selected="true" />
|
||||
<radio id="oldHomePage" hidden="true" />
|
||||
</radiogroup>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="migrating" pageid="migrating" label="&migrating.title;"
|
||||
next="done"
|
||||
onpageshow="MigrationWizard.onMigratingPageShow();">
|
||||
<description control="migratingItems">&migrating.label;</description>
|
||||
|
||||
<vbox id="migratingItems" style="overflow: auto;" align="left" role="group"/>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="done" pageid="done" label="&done.title;"
|
||||
onpageshow="MigrationWizard.onDonePageShow();">
|
||||
<description control="doneItems">&done.label;</description>
|
||||
|
||||
<vbox id="doneItems" style="overflow: auto;" align="left" role="group"/>
|
||||
</wizardpage>
|
||||
|
||||
</wizard>
|
||||
|
||||
9
browser/components/migration/jar.mn
Normal file
9
browser/components/migration/jar.mn
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# 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/.
|
||||
|
||||
browser.jar:
|
||||
* content/browser/migration/migration.xul (content/migration.xul)
|
||||
content/browser/migration/migration.js (content/migration.js)
|
||||
content/browser/migration/extra-migration-strings.properties (content/extra-migration-strings.properties)
|
||||
content/browser/aboutWelcomeBack.xhtml (content/aboutWelcomeBack.xhtml)
|
||||
62
browser/components/migration/moz.build
Normal file
62
browser/components/migration/moz.build
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
|
||||
|
||||
MARIONETTE_UNIT_MANIFESTS += ['tests/marionette/manifest.ini']
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += [ 'tests/browser/browser.ini']
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIBrowserProfileMigrator.idl',
|
||||
]
|
||||
|
||||
XPIDL_MODULE = 'migration'
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'ChromeProfileMigrator.js',
|
||||
'FirefoxProfileMigrator.js',
|
||||
'ProfileMigrator.js',
|
||||
]
|
||||
|
||||
EXTRA_PP_COMPONENTS += [
|
||||
'BrowserProfileMigrators.manifest',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'AutoMigrate.jsm',
|
||||
'MigrationUtils.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['OS_ARCH'] == 'WINNT':
|
||||
SOURCES += [
|
||||
'nsIEHistoryEnumerator.cpp',
|
||||
]
|
||||
EXTRA_COMPONENTS += [
|
||||
'360seProfileMigrator.js',
|
||||
'EdgeProfileMigrator.js',
|
||||
'IEProfileMigrator.js',
|
||||
]
|
||||
EXTRA_JS_MODULES += [
|
||||
'ESEDBReader.jsm',
|
||||
'MSMigrationUtils.jsm',
|
||||
]
|
||||
DEFINES['HAS_360SE_MIGRATOR'] = True
|
||||
DEFINES['HAS_IE_MIGRATOR'] = True
|
||||
DEFINES['HAS_EDGE_MIGRATOR'] = True
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
|
||||
EXTRA_COMPONENTS += [
|
||||
'SafariProfileMigrator.js',
|
||||
]
|
||||
DEFINES['HAS_SAFARI_MIGRATOR'] = True
|
||||
|
||||
FINAL_LIBRARY = 'browsercomps'
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Migration')
|
||||
77
browser/components/migration/nsIBrowserProfileMigrator.idl
Normal file
77
browser/components/migration/nsIBrowserProfileMigrator.idl
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
interface nsIArray;
|
||||
interface nsIProfileStartup;
|
||||
|
||||
[scriptable, uuid(22b56ffc-3149-43c5-b5a9-b3a6b678de93)]
|
||||
interface nsIBrowserProfileMigrator : nsISupports
|
||||
{
|
||||
/**
|
||||
* profile items to migrate. use with migrate().
|
||||
*/
|
||||
const unsigned short ALL = 0x0000;
|
||||
const unsigned short SETTINGS = 0x0001;
|
||||
const unsigned short COOKIES = 0x0002;
|
||||
const unsigned short HISTORY = 0x0004;
|
||||
const unsigned short FORMDATA = 0x0008;
|
||||
const unsigned short PASSWORDS = 0x0010;
|
||||
const unsigned short BOOKMARKS = 0x0020;
|
||||
const unsigned short OTHERDATA = 0x0040;
|
||||
const unsigned short SESSION = 0x0080;
|
||||
|
||||
/**
|
||||
* Copy user profile information to the current active profile.
|
||||
* @param aItems list of data items to migrate. see above for values.
|
||||
* @param aStartup helper interface which is non-null if called during startup.
|
||||
* @param aProfile profile to migrate from, if there is more than one.
|
||||
*/
|
||||
void migrate(in unsigned short aItems, in nsIProfileStartup aStartup, in jsval aProfile);
|
||||
|
||||
/**
|
||||
* A bit field containing profile items that this migrator
|
||||
* offers for import.
|
||||
* @param aProfile the profile that we are looking for available data
|
||||
* to import
|
||||
* @param aDoingStartup "true" if the profile is not currently being used.
|
||||
* @return bit field containing profile items (see above)
|
||||
* @note a return value of 0 represents no items rather than ALL.
|
||||
*/
|
||||
unsigned short getMigrateData(in jsval aProfile, in boolean aDoingStartup);
|
||||
|
||||
/**
|
||||
* Get the last time data from this browser was modified
|
||||
* @return a promise that resolves to a JS Date object
|
||||
*/
|
||||
jsval getLastUsedDate();
|
||||
|
||||
/**
|
||||
* Whether or not there is any data that can be imported from this
|
||||
* browser (i.e. whether or not it is installed, and there exists
|
||||
* a user profile)
|
||||
*/
|
||||
readonly attribute boolean sourceExists;
|
||||
|
||||
|
||||
/**
|
||||
* An enumeration of available profiles. If the import source does
|
||||
* not support profiles, this attribute is null.
|
||||
*/
|
||||
readonly attribute jsval sourceProfiles;
|
||||
|
||||
/**
|
||||
* The import source homepage. Returns null if not present/available
|
||||
*/
|
||||
readonly attribute AUTF8String sourceHomePageURL;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the source browser data is locked/in-use meaning migration likely
|
||||
* won't succeed and the user should be warned.
|
||||
*/
|
||||
readonly attribute boolean sourceLocked;
|
||||
};
|
||||
120
browser/components/migration/nsIEHistoryEnumerator.cpp
Normal file
120
browser/components/migration/nsIEHistoryEnumerator.cpp
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* 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/. */
|
||||
|
||||
#include "nsIEHistoryEnumerator.h"
|
||||
|
||||
#include <urlhist.h>
|
||||
#include <shlguid.h>
|
||||
|
||||
#include "nsArrayEnumerator.h"
|
||||
#include "nsCOMArray.h"
|
||||
#include "nsIURI.h"
|
||||
#include "nsIVariant.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "nsString.h"
|
||||
#include "nsWindowsMigrationUtils.h"
|
||||
#include "prtime.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// nsIEHistoryEnumerator
|
||||
|
||||
NS_IMPL_ISUPPORTS(nsIEHistoryEnumerator, nsISimpleEnumerator)
|
||||
|
||||
nsIEHistoryEnumerator::nsIEHistoryEnumerator()
|
||||
{
|
||||
::CoInitialize(nullptr);
|
||||
}
|
||||
|
||||
nsIEHistoryEnumerator::~nsIEHistoryEnumerator()
|
||||
{
|
||||
::CoUninitialize();
|
||||
}
|
||||
|
||||
void
|
||||
nsIEHistoryEnumerator::EnsureInitialized()
|
||||
{
|
||||
if (mURLEnumerator)
|
||||
return;
|
||||
|
||||
HRESULT hr = ::CoCreateInstance(CLSID_CUrlHistory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_IUrlHistoryStg2,
|
||||
getter_AddRefs(mIEHistory));
|
||||
if (FAILED(hr))
|
||||
return;
|
||||
|
||||
hr = mIEHistory->EnumUrls(getter_AddRefs(mURLEnumerator));
|
||||
if (FAILED(hr))
|
||||
return;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsIEHistoryEnumerator::HasMoreElements(bool* _retval)
|
||||
{
|
||||
*_retval = false;
|
||||
|
||||
EnsureInitialized();
|
||||
MOZ_ASSERT(mURLEnumerator, "Should have instanced an IE History URLEnumerator");
|
||||
if (!mURLEnumerator)
|
||||
return NS_OK;
|
||||
|
||||
STATURL statURL;
|
||||
ULONG fetched;
|
||||
|
||||
// First argument is not implemented, so doesn't matter what we pass.
|
||||
HRESULT hr = mURLEnumerator->Next(1, &statURL, &fetched);
|
||||
if (FAILED(hr) || fetched != 1UL) {
|
||||
// Reached the last entry.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
if (statURL.pwcsUrl) {
|
||||
nsDependentString url(statURL.pwcsUrl);
|
||||
nsresult rv = NS_NewURI(getter_AddRefs(uri), url);
|
||||
::CoTaskMemFree(statURL.pwcsUrl);
|
||||
if (NS_FAILED(rv)) {
|
||||
// Got a corrupt or invalid URI, continue to the next entry.
|
||||
return HasMoreElements(_retval);
|
||||
}
|
||||
}
|
||||
|
||||
nsDependentString title(statURL.pwcsTitle ? statURL.pwcsTitle : L"");
|
||||
|
||||
bool lastVisitTimeIsValid;
|
||||
PRTime lastVisited = WinMigrationFileTimeToPRTime(&(statURL.ftLastVisited), &lastVisitTimeIsValid);
|
||||
|
||||
mCachedNextEntry = do_CreateInstance("@mozilla.org/hash-property-bag;1");
|
||||
MOZ_ASSERT(mCachedNextEntry, "Should have instanced a new property bag");
|
||||
if (mCachedNextEntry) {
|
||||
mCachedNextEntry->SetPropertyAsInterface(NS_LITERAL_STRING("uri"), uri);
|
||||
mCachedNextEntry->SetPropertyAsAString(NS_LITERAL_STRING("title"), title);
|
||||
if (lastVisitTimeIsValid) {
|
||||
mCachedNextEntry->SetPropertyAsInt64(NS_LITERAL_STRING("time"), lastVisited);
|
||||
}
|
||||
|
||||
*_retval = true;
|
||||
}
|
||||
|
||||
if (statURL.pwcsTitle)
|
||||
::CoTaskMemFree(statURL.pwcsTitle);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsIEHistoryEnumerator::GetNext(nsISupports** _retval)
|
||||
{
|
||||
*_retval = nullptr;
|
||||
|
||||
if (!mCachedNextEntry)
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
NS_ADDREF(*_retval = mCachedNextEntry);
|
||||
// Release the cached entry, so it can't be returned twice.
|
||||
mCachedNextEntry = nullptr;
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
37
browser/components/migration/nsIEHistoryEnumerator.h
Normal file
37
browser/components/migration/nsIEHistoryEnumerator.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef iehistoryenumerator___h___
|
||||
#define iehistoryenumerator___h___
|
||||
|
||||
#include <urlhist.h>
|
||||
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsISimpleEnumerator.h"
|
||||
#include "nsIWritablePropertyBag2.h"
|
||||
|
||||
class nsIEHistoryEnumerator final : public nsISimpleEnumerator
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
NS_DECL_NSISIMPLEENUMERATOR
|
||||
|
||||
nsIEHistoryEnumerator();
|
||||
|
||||
private:
|
||||
~nsIEHistoryEnumerator();
|
||||
|
||||
/**
|
||||
* Initializes the history reader, if needed.
|
||||
*/
|
||||
void EnsureInitialized();
|
||||
|
||||
RefPtr<IUrlHistoryStg2> mIEHistory;
|
||||
RefPtr<IEnumSTATURL> mURLEnumerator;
|
||||
|
||||
nsCOMPtr<nsIWritablePropertyBag2> mCachedNextEntry;
|
||||
};
|
||||
|
||||
#endif
|
||||
36
browser/components/migration/nsWindowsMigrationUtils.h
Normal file
36
browser/components/migration/nsWindowsMigrationUtils.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef windowsmigrationutils__h__
|
||||
#define windowsmigrationutils__h__
|
||||
|
||||
#include "prtime.h"
|
||||
|
||||
static
|
||||
PRTime WinMigrationFileTimeToPRTime(FILETIME* filetime, bool* isValid)
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
*isValid = ::FileTimeToSystemTime(filetime, &st);
|
||||
if (!*isValid) {
|
||||
return 0;
|
||||
}
|
||||
PRExplodedTime prt;
|
||||
prt.tm_year = st.wYear;
|
||||
// SYSTEMTIME's day-of-month parameter is 1-based,
|
||||
// PRExplodedTime's is 0-based.
|
||||
prt.tm_month = st.wMonth - 1;
|
||||
prt.tm_mday = st.wDay;
|
||||
prt.tm_hour = st.wHour;
|
||||
prt.tm_min = st.wMinute;
|
||||
prt.tm_sec = st.wSecond;
|
||||
prt.tm_usec = st.wMilliseconds * 1000;
|
||||
prt.tm_wday = 0;
|
||||
prt.tm_yday = 0;
|
||||
prt.tm_params.tp_gmt_offset = 0;
|
||||
prt.tm_params.tp_dst_offset = 0;
|
||||
return PR_ImplodeTime(&prt);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
9
browser/components/migration/tests/browser/.eslintrc.js
Normal file
9
browser/components/migration/tests/browser/.eslintrc.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc.js",
|
||||
"../../../../../testing/mochitest/mochitest.eslintrc.js",
|
||||
]
|
||||
};
|
||||
|
||||
3
browser/components/migration/tests/browser/browser.ini
Normal file
3
browser/components/migration/tests/browser/browser.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[browser_undo_notification.js]
|
||||
[browser_undo_notification_wording.js]
|
||||
[browser_undo_notification_multiple_dismissal.js]
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
"use strict";
|
||||
|
||||
let scope = {};
|
||||
Cu.import("resource:///modules/AutoMigrate.jsm", scope);
|
||||
let oldCanUndo = scope.AutoMigrate.canUndo;
|
||||
let oldUndo = scope.AutoMigrate.undo;
|
||||
registerCleanupFunction(function() {
|
||||
scope.AutoMigrate.canUndo = oldCanUndo;
|
||||
scope.AutoMigrate.undo = oldUndo;
|
||||
});
|
||||
|
||||
const kExpectedNotificationId = "automigration-undo";
|
||||
|
||||
add_task(function* autoMigrationUndoNotificationShows() {
|
||||
let getNotification = browser =>
|
||||
gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId);
|
||||
|
||||
scope.AutoMigrate.canUndo = () => true;
|
||||
let undoCalled;
|
||||
scope.AutoMigrate.undo = () => { undoCalled = true };
|
||||
for (let url of ["about:newtab", "about:home"]) {
|
||||
undoCalled = false;
|
||||
// Can't use pushPrefEnv because of bug 1323779
|
||||
Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser");
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false);
|
||||
let browser = tab.linkedBrowser;
|
||||
if (!getNotification(browser)) {
|
||||
info(`Notification for ${url} not immediately present, waiting for it.`);
|
||||
yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId);
|
||||
}
|
||||
|
||||
ok(true, `Got notification for ${url}`);
|
||||
let notification = getNotification(browser);
|
||||
let notificationBox = notification.parentNode;
|
||||
notification.querySelector("button.notification-button-default").click();
|
||||
ok(!undoCalled, "Undo should not be called when clicking the default button");
|
||||
is(notification, notificationBox._closedNotification, "Notification should be closing");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
undoCalled = false;
|
||||
Services.prefs.setCharPref("browser.migrate.automigrate.browser", "chrome");
|
||||
tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false);
|
||||
browser = tab.linkedBrowser;
|
||||
if (!getNotification(browser)) {
|
||||
info(`Notification for ${url} not immediately present, waiting for it.`);
|
||||
yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId);
|
||||
}
|
||||
|
||||
ok(true, `Got notification for ${url}`);
|
||||
notification = getNotification(browser);
|
||||
notificationBox = notification.parentNode;
|
||||
// Set up the survey:
|
||||
yield SpecialPowers.pushPrefEnv({set: [
|
||||
["browser.migrate.automigrate.undo-survey", "https://example.com/?browser=%IMPORTEDBROWSER%"],
|
||||
["browser.migrate.automigrate.undo-survey-locales", "en-US"],
|
||||
]});
|
||||
let tabOpenedPromise = BrowserTestUtils.waitForNewTab(gBrowser, "https://example.com/?browser=Google%20Chrome");
|
||||
notification.querySelector("button:not(.notification-button-default)").click();
|
||||
ok(undoCalled, "Undo should be called when clicking the non-default (Don't Keep) button");
|
||||
is(notification, notificationBox._closedNotification, "Notification should be closing");
|
||||
let surveyTab = yield tabOpenedPromise;
|
||||
ok(surveyTab, "Should have opened a tab with a survey");
|
||||
yield BrowserTestUtils.removeTab(surveyTab);
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
const kExpectedNotificationId = "automigration-undo";
|
||||
|
||||
/**
|
||||
* Pretend we can undo something, trigger a notification, pick the undo option,
|
||||
* and verify that the notifications are all dismissed immediately.
|
||||
*/
|
||||
add_task(function* checkNotificationsDismissed() {
|
||||
yield SpecialPowers.pushPrefEnv({set: [
|
||||
["browser.migrate.automigrate.enabled", true],
|
||||
["browser.migrate.automigrate.ui.enabled", true],
|
||||
]});
|
||||
let getNotification = browser =>
|
||||
gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId);
|
||||
|
||||
Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser");
|
||||
|
||||
let {guid, lastModified} = yield PlacesUtils.bookmarks.insert(
|
||||
{title: "Some imported bookmark", parentGuid: PlacesUtils.bookmarks.toolbarGuid, url: "http://www.example.com"}
|
||||
);
|
||||
|
||||
let testUndoData = {
|
||||
visits: [],
|
||||
bookmarks: [{guid, lastModified: lastModified.getTime()}],
|
||||
logins: [],
|
||||
};
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4");
|
||||
registerCleanupFunction(() => {
|
||||
return OS.File.remove(path, {ignoreAbsent: true});
|
||||
});
|
||||
yield OS.File.writeAtomic(path, JSON.stringify(testUndoData), {
|
||||
encoding: "utf-8",
|
||||
compression: "lz4",
|
||||
tmpPath: path + ".tmp",
|
||||
});
|
||||
|
||||
let firstTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", false);
|
||||
if (!getNotification(firstTab.linkedBrowser)) {
|
||||
info(`Notification not immediately present on first tab, waiting for it.`);
|
||||
yield BrowserTestUtils.waitForNotificationBar(gBrowser, firstTab.linkedBrowser, kExpectedNotificationId);
|
||||
}
|
||||
let secondTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", false);
|
||||
if (!getNotification(secondTab.linkedBrowser)) {
|
||||
info(`Notification not immediately present on second tab, waiting for it.`);
|
||||
yield BrowserTestUtils.waitForNotificationBar(gBrowser, secondTab.linkedBrowser, kExpectedNotificationId);
|
||||
}
|
||||
|
||||
// Create a listener for the removal in the first tab, and a listener for bookmarks removal,
|
||||
// then click 'Don't keep' in the second tab, and verify that the notification is removed
|
||||
// before we start removing bookmarks.
|
||||
let haveRemovedBookmark = false;
|
||||
let bmObserver;
|
||||
let bookmarkRemovedPromise = new Promise(resolve => {
|
||||
bmObserver = {
|
||||
onItemRemoved(itemId, parentId, index, itemType, uri, removedGuid) {
|
||||
if (guid == removedGuid) {
|
||||
haveRemovedBookmark = true;
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
};
|
||||
PlacesUtils.bookmarks.addObserver(bmObserver, false);
|
||||
registerCleanupFunction(() => PlacesUtils.bookmarks.removeObserver(bmObserver));
|
||||
});
|
||||
|
||||
let firstTabNotificationRemovedPromise = new Promise(resolve => {
|
||||
let notification = getNotification(firstTab.linkedBrowser);
|
||||
// Save this reference because notification.parentNode will be null once it's removed.
|
||||
let notificationBox = notification.parentNode;
|
||||
let mut = new MutationObserver(mutations => {
|
||||
// Yucky, but we have to detect either the removal via animation (with marginTop)
|
||||
// or when the element is removed. We can't just detect the element being removed
|
||||
// because this happens asynchronously (after the animation) and so it'd race
|
||||
// with the rest of the undo happening.
|
||||
for (let mutation of mutations) {
|
||||
if (mutation.target == notification && mutation.attributeName == "style" &&
|
||||
parseInt(notification.style.marginTop, 10) < 0) {
|
||||
ok(!haveRemovedBookmark, "Should not have removed bookmark yet");
|
||||
mut.disconnect();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (mutation.target == notificationBox && mutation.removedNodes.length &&
|
||||
mutation.removedNodes[0] == notification) {
|
||||
ok(!haveRemovedBookmark, "Should not have removed bookmark yet");
|
||||
mut.disconnect();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
mut.observe(notification.parentNode, {childList: true});
|
||||
mut.observe(notification, {attributes: true});
|
||||
});
|
||||
|
||||
let prefResetPromise = new Promise(resolve => {
|
||||
const kObservedPref = "browser.migrate.automigrate.browser";
|
||||
let obs = () => {
|
||||
Services.prefs.removeObserver(kObservedPref, obs);
|
||||
ok(!Services.prefs.prefHasUserValue(kObservedPref),
|
||||
"Pref should have been reset");
|
||||
resolve();
|
||||
};
|
||||
Services.prefs.addObserver(kObservedPref, obs, false);
|
||||
});
|
||||
|
||||
// Click "Don't keep" button:
|
||||
let notificationToActivate = getNotification(secondTab.linkedBrowser);
|
||||
notificationToActivate.querySelector("button:not(.notification-button-default)").click();
|
||||
info("Waiting for notification to be removed in first (background) tab");
|
||||
yield firstTabNotificationRemovedPromise;
|
||||
info("Waiting for bookmark to be removed");
|
||||
yield bookmarkRemovedPromise;
|
||||
info("Waiting for prefs to be reset");
|
||||
yield prefResetPromise;
|
||||
|
||||
info("Removing spare tabs");
|
||||
yield BrowserTestUtils.removeTab(firstTab);
|
||||
yield BrowserTestUtils.removeTab(secondTab);
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
"use strict";
|
||||
|
||||
let scope = {};
|
||||
Cu.import("resource:///modules/AutoMigrate.jsm", scope);
|
||||
let oldCanUndo = scope.AutoMigrate.canUndo;
|
||||
registerCleanupFunction(function() {
|
||||
scope.AutoMigrate.canUndo = oldCanUndo;
|
||||
});
|
||||
|
||||
const kExpectedNotificationId = "automigration-undo";
|
||||
|
||||
add_task(function* autoMigrationUndoNotificationShows() {
|
||||
let getNotification = browser =>
|
||||
gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId);
|
||||
let localizedVersionOf = str => {
|
||||
if (str == "logins") {
|
||||
return "passwords";
|
||||
}
|
||||
if (str == "visits") {
|
||||
return "history";
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
scope.AutoMigrate.canUndo = () => true;
|
||||
let url = "about:newtab";
|
||||
Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser");
|
||||
const kSubsets = [
|
||||
["bookmarks", "logins", "visits"],
|
||||
["bookmarks", "logins"],
|
||||
["bookmarks", "visits"],
|
||||
["logins", "visits"],
|
||||
["bookmarks"],
|
||||
["logins"],
|
||||
["visits"],
|
||||
];
|
||||
const kAllItems = ["bookmarks", "logins", "visits"];
|
||||
for (let subset of kSubsets) {
|
||||
let state = new Map(subset.map(item => [item, [{}]]));
|
||||
scope.AutoMigrate._setImportedItemPrefFromState(state);
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false);
|
||||
let browser = tab.linkedBrowser;
|
||||
|
||||
if (!getNotification(browser)) {
|
||||
info(`Notification for ${url} not immediately present, waiting for it.`);
|
||||
yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId);
|
||||
}
|
||||
|
||||
ok(true, `Got notification for ${url}`);
|
||||
let notification = getNotification(browser);
|
||||
let notificationText = document.getAnonymousElementByAttribute(notification, "class", "messageText");
|
||||
notificationText = notificationText.textContent;
|
||||
for (let potentiallyImported of kAllItems) {
|
||||
let localizedImportItem = localizedVersionOf(potentiallyImported);
|
||||
if (subset.includes(potentiallyImported)) {
|
||||
ok(notificationText.includes(localizedImportItem),
|
||||
"Expected notification to contain " + localizedImportItem);
|
||||
} else {
|
||||
ok(!notificationText.includes(localizedImportItem),
|
||||
"Expected notification not to contain " + localizedImportItem);
|
||||
}
|
||||
}
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
[DEFAULT]
|
||||
run-if = buildapp == 'browser'
|
||||
|
||||
[test_refresh_firefox.py]
|
||||
|
||||
|
|
@ -0,0 +1,416 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
from marionette_harness import MarionetteTestCase
|
||||
|
||||
|
||||
class TestFirefoxRefresh(MarionetteTestCase):
|
||||
_username = "marionette-test-login"
|
||||
_password = "marionette-test-password"
|
||||
_bookmarkURL = "about:mozilla"
|
||||
_bookmarkText = "Some bookmark from Marionette"
|
||||
|
||||
_cookieHost = "firefox-refresh.marionette-test.mozilla.org"
|
||||
_cookiePath = "some/cookie/path"
|
||||
_cookieName = "somecookie"
|
||||
_cookieValue = "some cookie value"
|
||||
|
||||
_historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
|
||||
_historyTitle = "Test visit for Firefox Reset"
|
||||
|
||||
_formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
|
||||
_formHistoryValue = "special-pumpkin-value"
|
||||
|
||||
_expectedURLs = ["about:robots", "about:mozilla"]
|
||||
|
||||
def savePassword(self):
|
||||
self.runCode("""
|
||||
let myLogin = new global.LoginInfo(
|
||||
"test.marionette.mozilla.com",
|
||||
"http://test.marionette.mozilla.com/some/form/",
|
||||
null,
|
||||
arguments[0],
|
||||
arguments[1],
|
||||
"username",
|
||||
"password"
|
||||
);
|
||||
Services.logins.addLogin(myLogin)
|
||||
""", script_args=[self._username, self._password])
|
||||
|
||||
def createBookmark(self):
|
||||
self.marionette.execute_script("""
|
||||
let url = arguments[0];
|
||||
let title = arguments[1];
|
||||
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
|
||||
makeURI(url), 0, title);
|
||||
""", script_args=[self._bookmarkURL, self._bookmarkText])
|
||||
|
||||
def createHistory(self):
|
||||
error = self.runAsyncCode("""
|
||||
// Copied from PlacesTestUtils, which isn't available in Marionette tests.
|
||||
let didReturn;
|
||||
PlacesUtils.asyncHistory.updatePlaces(
|
||||
[{title: arguments[1], uri: makeURI(arguments[0]), visits: [{
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
visitDate: (Date.now() - 5000) * 1000,
|
||||
referrerURI: makeURI("about:mozilla"),
|
||||
}]
|
||||
}],
|
||||
{
|
||||
handleError(resultCode, place) {
|
||||
didReturn = true;
|
||||
marionetteScriptFinished("Unexpected error in adding visit: " + resultCode);
|
||||
},
|
||||
handleResult() {},
|
||||
handleCompletion() {
|
||||
if (!didReturn) {
|
||||
marionetteScriptFinished(false);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
""", script_args=[self._historyURL, self._historyTitle])
|
||||
if error:
|
||||
print error
|
||||
|
||||
def createFormHistory(self):
|
||||
error = self.runAsyncCode("""
|
||||
let updateDefinition = {
|
||||
op: "add",
|
||||
fieldname: arguments[0],
|
||||
value: arguments[1],
|
||||
firstUsed: (Date.now() - 5000) * 1000,
|
||||
};
|
||||
let finished = false;
|
||||
global.FormHistory.update(updateDefinition, {
|
||||
handleError(error) {
|
||||
finished = true;
|
||||
marionetteScriptFinished(error);
|
||||
},
|
||||
handleCompletion() {
|
||||
if (!finished) {
|
||||
marionetteScriptFinished(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
""", script_args=[self._formHistoryFieldName, self._formHistoryValue])
|
||||
if error:
|
||||
print error
|
||||
|
||||
def createCookie(self):
|
||||
self.runCode("""
|
||||
// Expire in 15 minutes:
|
||||
let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
|
||||
Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
|
||||
true, false, false, expireTime);
|
||||
""", script_args=[self._cookieHost, self._cookiePath, self._cookieName, self._cookieValue])
|
||||
|
||||
def createSession(self):
|
||||
self.runAsyncCode("""
|
||||
const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
|
||||
Ci.nsIWebProgressListener.STATE_IS_NETWORK;
|
||||
let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
|
||||
let expectedURLs = Array.from(arguments[0])
|
||||
gBrowser.addTabsProgressListener({
|
||||
onStateChange(browser, webprogress, request, flags, status) {
|
||||
try {
|
||||
request && request.QueryInterface(Ci.nsIChannel);
|
||||
} catch (ex) {}
|
||||
let uriLoaded = request.originalURI && request.originalURI.spec;
|
||||
if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
|
||||
expectedURLs.includes(uriLoaded)) {
|
||||
TabStateFlusher.flush(browser).then(function() {
|
||||
expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
|
||||
if (!expectedURLs.length) {
|
||||
gBrowser.removeTabsProgressListener(this);
|
||||
marionetteScriptFinished();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
for (let url of expectedURLs) {
|
||||
gBrowser.addTab(url);
|
||||
}
|
||||
""", script_args=[self._expectedURLs])
|
||||
|
||||
def checkPassword(self):
|
||||
loginInfo = self.marionette.execute_script("""
|
||||
let ary = Services.logins.findLogins({},
|
||||
"test.marionette.mozilla.com",
|
||||
"http://test.marionette.mozilla.com/some/form/",
|
||||
null, {});
|
||||
return ary.length ? ary : {username: "null", password: "null"};
|
||||
""")
|
||||
self.assertEqual(len(loginInfo), 1)
|
||||
self.assertEqual(loginInfo[0]['username'], self._username)
|
||||
self.assertEqual(loginInfo[0]['password'], self._password)
|
||||
|
||||
loginCount = self.marionette.execute_script("""
|
||||
return Services.logins.getAllLogins().length;
|
||||
""")
|
||||
self.assertEqual(loginCount, 1, "No other logins are present")
|
||||
|
||||
def checkBookmark(self):
|
||||
titleInBookmarks = self.marionette.execute_script("""
|
||||
let url = arguments[0];
|
||||
let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(makeURI(url), {}, {});
|
||||
return bookmarkIds.length == 1 ? PlacesUtils.bookmarks.getItemTitle(bookmarkIds[0]) : "";
|
||||
""", script_args=[self._bookmarkURL])
|
||||
self.assertEqual(titleInBookmarks, self._bookmarkText)
|
||||
|
||||
def checkHistory(self):
|
||||
historyResults = self.runAsyncCode("""
|
||||
let placeInfos = [];
|
||||
PlacesUtils.asyncHistory.getPlacesInfo(makeURI(arguments[0]), {
|
||||
handleError(resultCode, place) {
|
||||
placeInfos = null;
|
||||
marionetteScriptFinished("Unexpected error in fetching visit: " + resultCode);
|
||||
},
|
||||
handleResult(placeInfo) {
|
||||
placeInfos.push(placeInfo);
|
||||
},
|
||||
handleCompletion() {
|
||||
if (placeInfos) {
|
||||
if (!placeInfos.length) {
|
||||
marionetteScriptFinished("No visits found");
|
||||
} else {
|
||||
marionetteScriptFinished(placeInfos);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
""", script_args=[self._historyURL])
|
||||
if type(historyResults) == str:
|
||||
self.fail(historyResults)
|
||||
return
|
||||
|
||||
historyCount = len(historyResults)
|
||||
self.assertEqual(historyCount, 1, "Should have exactly 1 entry for URI, got %d" % historyCount)
|
||||
if historyCount == 1:
|
||||
self.assertEqual(historyResults[0]['title'], self._historyTitle)
|
||||
|
||||
def checkFormHistory(self):
|
||||
formFieldResults = self.runAsyncCode("""
|
||||
let results = [];
|
||||
global.FormHistory.search(["value"], {fieldname: arguments[0]}, {
|
||||
handleError(error) {
|
||||
results = error;
|
||||
},
|
||||
handleResult(result) {
|
||||
results.push(result);
|
||||
},
|
||||
handleCompletion() {
|
||||
marionetteScriptFinished(results);
|
||||
},
|
||||
});
|
||||
""", script_args=[self._formHistoryFieldName])
|
||||
if type(formFieldResults) == str:
|
||||
self.fail(formFieldResults)
|
||||
return
|
||||
|
||||
formFieldResultCount = len(formFieldResults)
|
||||
self.assertEqual(formFieldResultCount, 1, "Should have exactly 1 entry for this field, got %d" % formFieldResultCount)
|
||||
if formFieldResultCount == 1:
|
||||
self.assertEqual(formFieldResults[0]['value'], self._formHistoryValue)
|
||||
|
||||
formHistoryCount = self.runAsyncCode("""
|
||||
let count;
|
||||
let callbacks = {
|
||||
handleResult: rv => count = rv,
|
||||
handleCompletion() {
|
||||
marionetteScriptFinished(count);
|
||||
},
|
||||
};
|
||||
global.FormHistory.count({}, callbacks);
|
||||
""")
|
||||
self.assertEqual(formHistoryCount, 1, "There should be only 1 entry in the form history")
|
||||
|
||||
def checkCookie(self):
|
||||
cookieInfo = self.runCode("""
|
||||
try {
|
||||
let cookieEnum = Services.cookies.getCookiesFromHost(arguments[0]);
|
||||
let cookie = null;
|
||||
while (cookieEnum.hasMoreElements()) {
|
||||
let hostCookie = cookieEnum.getNext();
|
||||
hostCookie.QueryInterface(Ci.nsICookie2);
|
||||
// getCookiesFromHost returns any cookie from the BASE host.
|
||||
if (hostCookie.rawHost != arguments[0])
|
||||
continue;
|
||||
if (cookie != null) {
|
||||
return "more than 1 cookie! That shouldn't happen!";
|
||||
}
|
||||
cookie = hostCookie;
|
||||
}
|
||||
return {path: cookie.path, name: cookie.name, value: cookie.value};
|
||||
} catch (ex) {
|
||||
return "got exception trying to fetch cookie: " + ex;
|
||||
}
|
||||
""", script_args=[self._cookieHost])
|
||||
if not isinstance(cookieInfo, dict):
|
||||
self.fail(cookieInfo)
|
||||
return
|
||||
self.assertEqual(cookieInfo['path'], self._cookiePath)
|
||||
self.assertEqual(cookieInfo['value'], self._cookieValue)
|
||||
self.assertEqual(cookieInfo['name'], self._cookieName)
|
||||
|
||||
def checkSession(self):
|
||||
tabURIs = self.runCode("""
|
||||
return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
|
||||
""")
|
||||
self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
|
||||
|
||||
tabURIs = self.runAsyncCode("""
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
let fs = function() {
|
||||
content.document.getElementById("errorTryAgain").click();
|
||||
};
|
||||
let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
|
||||
window.addEventListener("SSWindowStateReady", function testSSPostReset() {
|
||||
window.removeEventListener("SSWindowStateReady", testSSPostReset, false);
|
||||
Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() {
|
||||
marionetteScriptFinished([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec));
|
||||
});
|
||||
}, false);
|
||||
mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
|
||||
""")
|
||||
self.assertSequenceEqual(tabURIs, ["about:blank"] + self._expectedURLs)
|
||||
pass
|
||||
|
||||
def checkProfile(self, hasMigrated=False):
|
||||
self.checkPassword()
|
||||
self.checkBookmark()
|
||||
self.checkHistory()
|
||||
self.checkFormHistory()
|
||||
self.checkCookie()
|
||||
if hasMigrated:
|
||||
self.checkSession()
|
||||
|
||||
def createProfileData(self):
|
||||
self.savePassword()
|
||||
self.createBookmark()
|
||||
self.createHistory()
|
||||
self.createFormHistory()
|
||||
self.createCookie()
|
||||
self.createSession()
|
||||
|
||||
def setUpScriptData(self):
|
||||
self.marionette.set_context(self.marionette.CONTEXT_CHROME)
|
||||
self.marionette.execute_script("""
|
||||
global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
|
||||
global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
|
||||
global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
|
||||
global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory;
|
||||
""", new_sandbox=False, sandbox='system')
|
||||
|
||||
def runCode(self, script, *args, **kwargs):
|
||||
return self.marionette.execute_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
|
||||
|
||||
def runAsyncCode(self, script, *args, **kwargs):
|
||||
return self.marionette.execute_async_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
MarionetteTestCase.setUp(self)
|
||||
self.setUpScriptData()
|
||||
|
||||
self.reset_profile_path = None
|
||||
self.desktop_backup_path = None
|
||||
|
||||
self.createProfileData()
|
||||
|
||||
def tearDown(self):
|
||||
# Force yet another restart with a clean profile to disconnect from the
|
||||
# profile and environment changes we've made, to leave a more or less
|
||||
# blank slate for the next person.
|
||||
self.marionette.restart(clean=True, in_app=False)
|
||||
self.setUpScriptData()
|
||||
|
||||
# Super
|
||||
MarionetteTestCase.tearDown(self)
|
||||
|
||||
# Some helpers to deal with removing a load of files
|
||||
import errno, stat
|
||||
def handleRemoveReadonly(func, path, exc):
|
||||
excvalue = exc[1]
|
||||
if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
|
||||
os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
|
||||
func(path)
|
||||
else:
|
||||
raise
|
||||
|
||||
if self.desktop_backup_path:
|
||||
shutil.rmtree(self.desktop_backup_path, ignore_errors=False, onerror=handleRemoveReadonly)
|
||||
|
||||
if self.reset_profile_path:
|
||||
# Remove ourselves from profiles.ini
|
||||
profileLeafName = os.path.basename(os.path.normpath(self.reset_profile_path))
|
||||
self.runCode("""
|
||||
let [salt, name] = arguments[0].split(".");
|
||||
let profile = global.profSvc.getProfileByName(name);
|
||||
profile.remove(false)
|
||||
global.profSvc.flush();
|
||||
""", script_args=[profileLeafName])
|
||||
# And delete all the files.
|
||||
shutil.rmtree(self.reset_profile_path, ignore_errors=False, onerror=handleRemoveReadonly)
|
||||
|
||||
def doReset(self):
|
||||
self.runCode("""
|
||||
// Ensure the current (temporary) profile is in profiles.ini:
|
||||
let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||
let profileName = "marionette-test-profile-" + Date.now();
|
||||
let myProfile = global.profSvc.createProfile(profD, profileName);
|
||||
global.profSvc.flush()
|
||||
|
||||
// Now add the reset parameters:
|
||||
let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
let allMarionettePrefs = Services.prefs.getChildList("marionette.");
|
||||
let prefObj = {};
|
||||
for (let pref of allMarionettePrefs) {
|
||||
let prefSuffix = pref.substr("marionette.".length);
|
||||
let prefVal = global.Preferences.get(pref);
|
||||
prefObj[prefSuffix] = prefVal;
|
||||
}
|
||||
let marionetteInfo = JSON.stringify(prefObj);
|
||||
env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", marionetteInfo);
|
||||
env.set("MOZ_RESET_PROFILE_RESTART", "1");
|
||||
env.set("XRE_PROFILE_PATH", arguments[0]);
|
||||
env.set("XRE_PROFILE_NAME", profileName);
|
||||
""", script_args=[self.marionette.instance.profile.profile])
|
||||
|
||||
profileLeafName = os.path.basename(os.path.normpath(self.marionette.instance.profile.profile))
|
||||
|
||||
# Now restart the browser to get it reset:
|
||||
self.marionette.restart(clean=False, in_app=True)
|
||||
self.setUpScriptData()
|
||||
|
||||
# Determine the new profile path (we'll need to remove it when we're done)
|
||||
self.reset_profile_path = self.runCode("""
|
||||
let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||
return profD.path;
|
||||
""")
|
||||
|
||||
# Determine the backup path
|
||||
self.desktop_backup_path = self.runCode("""
|
||||
let container;
|
||||
try {
|
||||
container = Services.dirsvc.get("Desk", Ci.nsIFile);
|
||||
} catch (ex) {
|
||||
container = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||
}
|
||||
let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
|
||||
let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name], 1);
|
||||
container.append(dirName);
|
||||
container.append(arguments[0]);
|
||||
return container.path;
|
||||
""", script_args = [profileLeafName])
|
||||
|
||||
self.assertTrue(os.path.isdir(self.reset_profile_path), "Reset profile path should be present")
|
||||
self.assertTrue(os.path.isdir(self.desktop_backup_path), "Backup profile path should be present")
|
||||
|
||||
def testReset(self):
|
||||
self.checkProfile()
|
||||
|
||||
self.doReset()
|
||||
|
||||
# Now check that we're doing OK...
|
||||
self.checkProfile(hasMigrated=True)
|
||||
7
browser/components/migration/tests/unit/.eslintrc.js
Normal file
7
browser/components/migration/tests/unit/.eslintrc.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = { // eslint-disable-line no-undef
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc.js"
|
||||
]
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"profile" : {
|
||||
"info_cache" : {
|
||||
"Default" : {
|
||||
"active_time" : 1430950755.65137,
|
||||
"is_using_default_name" : true,
|
||||
"is_ephemeral" : false,
|
||||
"is_omitted_from_profile_list" : false,
|
||||
"user_name" : "",
|
||||
"background_apps" : false,
|
||||
"is_using_default_avatar" : true,
|
||||
"avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0",
|
||||
"name" : "Person 1"
|
||||
}
|
||||
},
|
||||
"profiles_created" : 1,
|
||||
"last_used" : "Default",
|
||||
"last_active_profiles" : [
|
||||
"Default"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
69
browser/components/migration/tests/unit/head_migration.js
Normal file
69
browser/components/migration/tests/unit/head_migration.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use strict";
|
||||
|
||||
/* exported gProfD, promiseMigration, registerFakePath */
|
||||
|
||||
var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
|
||||
Cu.importGlobalProperties([ "URL" ]);
|
||||
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm");
|
||||
Cu.import("resource://gre/modules/LoginHelper.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://testing-common/TestUtils.jsm");
|
||||
Cu.import("resource://testing-common/PlacesTestUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
// Initialize profile.
|
||||
var gProfD = do_get_profile();
|
||||
|
||||
Cu.import("resource://testing-common/AppInfo.jsm"); /* globals updateAppInfo */
|
||||
updateAppInfo();
|
||||
|
||||
/**
|
||||
* Migrates the requested resource and waits for the migration to be complete.
|
||||
*/
|
||||
function promiseMigration(migrator, resourceType, aProfile = null) {
|
||||
// Ensure resource migration is available.
|
||||
let availableSources = migrator.getMigrateData(aProfile, false);
|
||||
Assert.ok((availableSources & resourceType) > 0, "Resource supported by migrator");
|
||||
|
||||
return new Promise (resolve => {
|
||||
Services.obs.addObserver(function onMigrationEnded() {
|
||||
Services.obs.removeObserver(onMigrationEnded, "Migration:Ended");
|
||||
resolve();
|
||||
}, "Migration:Ended", false);
|
||||
|
||||
migrator.migrate(resourceType, null, aProfile);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a directory service entry with a given nsIFile.
|
||||
*/
|
||||
function registerFakePath(key, file) {
|
||||
// Register our own provider for the Library directory.
|
||||
let provider = {
|
||||
getFile(prop, persistent) {
|
||||
persistent.value = true;
|
||||
if (prop == key) {
|
||||
return file;
|
||||
}
|
||||
throw Cr.NS_ERROR_FAILURE;
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDirectoryServiceProvider ])
|
||||
};
|
||||
Services.dirsvc.QueryInterface(Ci.nsIDirectoryService)
|
||||
.registerProvider(provider);
|
||||
do_register_cleanup(() => {
|
||||
Services.dirsvc.QueryInterface(Ci.nsIDirectoryService)
|
||||
.unregisterProvider(provider);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/ForgetAboutSite.jsm");
|
||||
|
||||
add_task(function* () {
|
||||
registerFakePath("ULibDir", do_get_file("Library/"));
|
||||
let migrator = MigrationUtils.getMigrator("chrome");
|
||||
|
||||
Assert.ok(migrator.sourceExists, "Sanity check the source exists");
|
||||
|
||||
const COOKIE = {
|
||||
expiry: 2145934800,
|
||||
host: "unencryptedcookie.invalid",
|
||||
isHttpOnly: false,
|
||||
isSession: false,
|
||||
name: "testcookie",
|
||||
path: "/",
|
||||
value: "testvalue",
|
||||
};
|
||||
|
||||
// Sanity check.
|
||||
Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
|
||||
"There are no cookies initially");
|
||||
|
||||
const PROFILE = {
|
||||
id: "Default",
|
||||
name: "Person 1",
|
||||
};
|
||||
|
||||
// Migrate unencrypted cookies.
|
||||
yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES, PROFILE);
|
||||
|
||||
Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1,
|
||||
"Migrated the expected number of unencrypted cookies");
|
||||
Assert.equal(Services.cookies.countCookiesFromHost("encryptedcookie.invalid"), 0,
|
||||
"Migrated the expected number of encrypted cookies");
|
||||
|
||||
// Now check the cookie details.
|
||||
let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host, {});
|
||||
Assert.ok(enumerator.hasMoreElements(), "Cookies available");
|
||||
let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
|
||||
|
||||
for (let prop of Object.keys(COOKIE)) {
|
||||
Assert.equal(foundCookie[prop], COOKIE[prop], "Check cookie " + prop);
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
ForgetAboutSite.removeDataFromDomain(COOKIE.host);
|
||||
Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
|
||||
"There are no cookies after cleanup");
|
||||
});
|
||||
219
browser/components/migration/tests/unit/test_Chrome_passwords.js
Normal file
219
browser/components/migration/tests/unit/test_Chrome_passwords.js
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/OSCrypto.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const PROFILE = {
|
||||
id: "Default",
|
||||
name: "Person 1",
|
||||
};
|
||||
|
||||
const TEST_LOGINS = [
|
||||
{
|
||||
id: 1, // id of the row in the chrome login db
|
||||
username: "username",
|
||||
password: "password",
|
||||
hostname: "https://c9.io",
|
||||
formSubmitURL: "https://c9.io",
|
||||
httpRealm: null,
|
||||
usernameField: "inputEmail",
|
||||
passwordField: "inputPassword",
|
||||
timeCreated: 1437418416037,
|
||||
timePasswordChanged: 1437418416037,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "username@gmail.com",
|
||||
password: "password2",
|
||||
hostname: "https://accounts.google.com",
|
||||
formSubmitURL: "https://accounts.google.com",
|
||||
httpRealm: null,
|
||||
usernameField: "Email",
|
||||
passwordField: "Passwd",
|
||||
timeCreated: 1437418446598,
|
||||
timePasswordChanged: 1437418446598,
|
||||
timesUsed: 6,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "username",
|
||||
password: "password3",
|
||||
hostname: "https://www.facebook.com",
|
||||
formSubmitURL: "https://www.facebook.com",
|
||||
httpRealm: null,
|
||||
usernameField: "email",
|
||||
passwordField: "pass",
|
||||
timeCreated: 1437418478851,
|
||||
timePasswordChanged: 1437418478851,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: "user",
|
||||
password: "password",
|
||||
hostname: "http://httpbin.org",
|
||||
formSubmitURL: null,
|
||||
httpRealm: "me@kennethreitz.com", // Digest auth.
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1437787462368,
|
||||
timePasswordChanged: 1437787462368,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
username: "buser",
|
||||
password: "bpassword",
|
||||
hostname: "http://httpbin.org",
|
||||
formSubmitURL: null,
|
||||
httpRealm: "Fake Realm", // Basic auth.
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1437787539233,
|
||||
timePasswordChanged: 1437787539233,
|
||||
timesUsed: 1,
|
||||
},
|
||||
];
|
||||
|
||||
var crypto = new OSCrypto();
|
||||
var dbConn;
|
||||
|
||||
function promiseSetPassword(login) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stmt = dbConn.createAsyncStatement(`
|
||||
UPDATE logins
|
||||
SET password_value = :password_value
|
||||
WHERE rowid = :rowid
|
||||
`);
|
||||
let passwordValue = crypto.stringToArray(crypto.encryptData(login.password));
|
||||
stmt.bindBlobByName("password_value", passwordValue, passwordValue.length);
|
||||
stmt.params.rowid = login.id;
|
||||
|
||||
stmt.executeAsync({
|
||||
handleError(aError) {
|
||||
reject("Error with the query: " + aError.message);
|
||||
},
|
||||
|
||||
handleCompletion(aReason) {
|
||||
if (aReason === Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||
resolve();
|
||||
} else {
|
||||
reject("Query has failed: " + aReason);
|
||||
}
|
||||
},
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
function checkLoginsAreEqual(passwordManagerLogin, chromeLogin, id) {
|
||||
passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
Assert.equal(passwordManagerLogin.username, chromeLogin.username,
|
||||
"The two logins ID " + id + " have the same username");
|
||||
Assert.equal(passwordManagerLogin.password, chromeLogin.password,
|
||||
"The two logins ID " + id + " have the same password");
|
||||
Assert.equal(passwordManagerLogin.hostname, chromeLogin.hostname,
|
||||
"The two logins ID " + id + " have the same hostname");
|
||||
Assert.equal(passwordManagerLogin.formSubmitURL, chromeLogin.formSubmitURL,
|
||||
"The two logins ID " + id + " have the same formSubmitURL");
|
||||
Assert.equal(passwordManagerLogin.httpRealm, chromeLogin.httpRealm,
|
||||
"The two logins ID " + id + " have the same httpRealm");
|
||||
Assert.equal(passwordManagerLogin.usernameField, chromeLogin.usernameField,
|
||||
"The two logins ID " + id + " have the same usernameElement");
|
||||
Assert.equal(passwordManagerLogin.passwordField, chromeLogin.passwordField,
|
||||
"The two logins ID " + id + " have the same passwordElement");
|
||||
Assert.equal(passwordManagerLogin.timeCreated, chromeLogin.timeCreated,
|
||||
"The two logins ID " + id + " have the same timeCreated");
|
||||
Assert.equal(passwordManagerLogin.timePasswordChanged, chromeLogin.timePasswordChanged,
|
||||
"The two logins ID " + id + " have the same timePasswordChanged");
|
||||
Assert.equal(passwordManagerLogin.timesUsed, chromeLogin.timesUsed,
|
||||
"The two logins ID " + id + " have the same timesUsed");
|
||||
}
|
||||
|
||||
function generateDifferentLogin(login) {
|
||||
let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
|
||||
|
||||
newLogin.init(login.hostname, login.formSubmitURL, null,
|
||||
login.username, login.password + 1, login.usernameField + 1,
|
||||
login.passwordField + 1);
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
newLogin.timeCreated = login.timeCreated + 1;
|
||||
newLogin.timePasswordChanged = login.timePasswordChanged + 1;
|
||||
newLogin.timesUsed = login.timesUsed + 1;
|
||||
return newLogin;
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
let loginDataFile = do_get_file("AppData/Local/Google/Chrome/User Data/Default/Login Data");
|
||||
dbConn = Services.storage.openUnsharedDatabase(loginDataFile);
|
||||
registerFakePath("LocalAppData", do_get_file("AppData/Local/"));
|
||||
|
||||
do_register_cleanup(() => {
|
||||
Services.logins.removeAllLogins();
|
||||
dbConn.asyncClose();
|
||||
crypto.finalize();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_importIntoEmptyDB() {
|
||||
for (let login of TEST_LOGINS) {
|
||||
yield promiseSetPassword(login);
|
||||
}
|
||||
|
||||
let migrator = MigrationUtils.getMigrator("chrome");
|
||||
Assert.ok(migrator.sourceExists, "Sanity check the source exists");
|
||||
|
||||
let logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, 0, "There are no logins initially");
|
||||
|
||||
// Migrate the logins.
|
||||
yield promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE);
|
||||
|
||||
logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, TEST_LOGINS.length, "Check login count after importing the data");
|
||||
Assert.equal(logins.length, MigrationUtils._importQuantities.logins,
|
||||
"Check telemetry matches the actual import.");
|
||||
|
||||
for (let i = 0; i < TEST_LOGINS.length; i++) {
|
||||
checkLoginsAreEqual(logins[i], TEST_LOGINS[i], i + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Test that existing logins for the same primary key don't get overwritten
|
||||
add_task(function* test_importExistingLogins() {
|
||||
let migrator = MigrationUtils.getMigrator("chrome");
|
||||
Assert.ok(migrator.sourceExists, "Sanity check the source exists");
|
||||
|
||||
Services.logins.removeAllLogins();
|
||||
let logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, 0, "There are no logins after removing all of them");
|
||||
|
||||
let newLogins = [];
|
||||
|
||||
// Create 3 new logins that are different but where the key properties are still the same.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
newLogins.push(generateDifferentLogin(TEST_LOGINS[i]));
|
||||
Services.logins.addLogin(newLogins[i]);
|
||||
}
|
||||
|
||||
logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, newLogins.length, "Check login count after the insertion");
|
||||
|
||||
for (let i = 0; i < newLogins.length; i++) {
|
||||
checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
|
||||
}
|
||||
// Migrate the logins.
|
||||
yield promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE);
|
||||
|
||||
logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, TEST_LOGINS.length,
|
||||
"Check there are still the same number of logins after re-importing the data");
|
||||
Assert.equal(logins.length, MigrationUtils._importQuantities.logins,
|
||||
"Check telemetry matches the actual import.");
|
||||
|
||||
for (let i = 0; i < newLogins.length; i++) {
|
||||
checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
"use strict";
|
||||
|
||||
const EDGE_AVAILABLE_MIGRATIONS =
|
||||
MigrationUtils.resourceTypes.COOKIES |
|
||||
MigrationUtils.resourceTypes.BOOKMARKS |
|
||||
MigrationUtils.resourceTypes.HISTORY |
|
||||
MigrationUtils.resourceTypes.PASSWORDS;
|
||||
|
||||
add_task(function* () {
|
||||
let migrator = MigrationUtils.getMigrator("edge");
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Assert.equal(!!(migrator && migrator.sourceExists), AppConstants.isPlatformAndVersionAtLeast("win", "10"),
|
||||
"Edge should be available for migration if and only if we're on Win 10+");
|
||||
if (migrator) {
|
||||
let migratableData = migrator.getMigrateData(null, false);
|
||||
Assert.equal(migratableData, EDGE_AVAILABLE_MIGRATIONS,
|
||||
"All the data types we expect should be available");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
let eseBackStage = Cu.import("resource:///modules/ESEDBReader.jsm");
|
||||
let ESE = eseBackStage.ESE;
|
||||
let KERNEL = eseBackStage.KERNEL;
|
||||
let gLibs = eseBackStage.gLibs;
|
||||
let COLUMN_TYPES = eseBackStage.COLUMN_TYPES;
|
||||
let declareESEFunction = eseBackStage.declareESEFunction;
|
||||
let loadLibraries = eseBackStage.loadLibraries;
|
||||
|
||||
let gESEInstanceCounter = 1;
|
||||
|
||||
ESE.JET_COLUMNCREATE_W = new ctypes.StructType("JET_COLUMNCREATE_W", [
|
||||
{"cbStruct": ctypes.unsigned_long},
|
||||
{"szColumnName": ESE.JET_PCWSTR},
|
||||
{"coltyp": ESE.JET_COLTYP },
|
||||
{"cbMax": ctypes.unsigned_long },
|
||||
{"grbit": ESE.JET_GRBIT },
|
||||
{"pvDefault": ctypes.voidptr_t},
|
||||
{"cbDefault": ctypes.unsigned_long },
|
||||
{"cp": ctypes.unsigned_long },
|
||||
{"columnid": ESE.JET_COLUMNID},
|
||||
{"err": ESE.JET_ERR},
|
||||
]);
|
||||
|
||||
function createColumnCreationWrapper({name, type, cbMax}) {
|
||||
// We use a wrapper object because we need to be sure the JS engine won't GC
|
||||
// data that we're "only" pointing to.
|
||||
let wrapper = {};
|
||||
wrapper.column = new ESE.JET_COLUMNCREATE_W();
|
||||
wrapper.column.cbStruct = ESE.JET_COLUMNCREATE_W.size;
|
||||
let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
|
||||
wrapper.name = new wchar_tArray(name.length + 1);
|
||||
wrapper.name.value = String(name);
|
||||
wrapper.column.szColumnName = wrapper.name;
|
||||
wrapper.column.coltyp = type;
|
||||
let fallback = 0;
|
||||
switch (type) {
|
||||
case COLUMN_TYPES.JET_coltypText:
|
||||
fallback = 255;
|
||||
// Intentional fall-through
|
||||
case COLUMN_TYPES.JET_coltypLongText:
|
||||
wrapper.column.cbMax = cbMax || fallback || 64 * 1024;
|
||||
break;
|
||||
case COLUMN_TYPES.JET_coltypGUID:
|
||||
wrapper.column.cbMax = 16;
|
||||
break;
|
||||
case COLUMN_TYPES.JET_coltypBit:
|
||||
wrapper.column.cbMax = 1;
|
||||
break;
|
||||
case COLUMN_TYPES.JET_coltypLongLong:
|
||||
wrapper.column.cbMax = 8;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown column type!");
|
||||
}
|
||||
|
||||
wrapper.column.columnid = new ESE.JET_COLUMNID();
|
||||
wrapper.column.grbit = 0;
|
||||
wrapper.column.pvDefault = null;
|
||||
wrapper.column.cbDefault = 0;
|
||||
wrapper.column.cp = 0;
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// "forward declarations" of indexcreate and setinfo structs, which we don't use.
|
||||
ESE.JET_INDEXCREATE = new ctypes.StructType("JET_INDEXCREATE");
|
||||
ESE.JET_SETINFO = new ctypes.StructType("JET_SETINFO");
|
||||
|
||||
ESE.JET_TABLECREATE_W = new ctypes.StructType("JET_TABLECREATE_W", [
|
||||
{"cbStruct": ctypes.unsigned_long},
|
||||
{"szTableName": ESE.JET_PCWSTR},
|
||||
{"szTemplateTableName": ESE.JET_PCWSTR},
|
||||
{"ulPages": ctypes.unsigned_long},
|
||||
{"ulDensity": ctypes.unsigned_long},
|
||||
{"rgcolumncreate": ESE.JET_COLUMNCREATE_W.ptr},
|
||||
{"cColumns": ctypes.unsigned_long},
|
||||
{"rgindexcreate": ESE.JET_INDEXCREATE.ptr},
|
||||
{"cIndexes": ctypes.unsigned_long},
|
||||
{"grbit": ESE.JET_GRBIT},
|
||||
{"tableid": ESE.JET_TABLEID},
|
||||
{"cCreated": ctypes.unsigned_long},
|
||||
]);
|
||||
|
||||
function createTableCreationWrapper(tableName, columns) {
|
||||
let wrapper = {};
|
||||
let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
|
||||
wrapper.name = new wchar_tArray(tableName.length + 1);
|
||||
wrapper.name.value = String(tableName);
|
||||
wrapper.table = new ESE.JET_TABLECREATE_W();
|
||||
wrapper.table.cbStruct = ESE.JET_TABLECREATE_W.size;
|
||||
wrapper.table.szTableName = wrapper.name;
|
||||
wrapper.table.szTemplateTableName = null;
|
||||
wrapper.table.ulPages = 1;
|
||||
wrapper.table.ulDensity = 0;
|
||||
let columnArrayType = ESE.JET_COLUMNCREATE_W.array(columns.length);
|
||||
wrapper.columnAry = new columnArrayType();
|
||||
wrapper.table.rgcolumncreate = wrapper.columnAry.addressOfElement(0);
|
||||
wrapper.table.cColumns = columns.length;
|
||||
wrapper.columns = [];
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
let column = columns[i];
|
||||
let columnWrapper = createColumnCreationWrapper(column);
|
||||
wrapper.columnAry.addressOfElement(i).contents = columnWrapper.column;
|
||||
wrapper.columns.push(columnWrapper);
|
||||
}
|
||||
wrapper.table.rgindexcreate = null;
|
||||
wrapper.table.cIndexes = 0;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function convertValueForWriting(value, valueType) {
|
||||
let buffer;
|
||||
let valueOfValueType = ctypes.UInt64.lo(valueType);
|
||||
switch (valueOfValueType) {
|
||||
case COLUMN_TYPES.JET_coltypLongLong:
|
||||
if (value instanceof Date) {
|
||||
buffer = new KERNEL.FILETIME();
|
||||
let sysTime = new KERNEL.SYSTEMTIME();
|
||||
sysTime.wYear = value.getUTCFullYear();
|
||||
sysTime.wMonth = value.getUTCMonth() + 1;
|
||||
sysTime.wDay = value.getUTCDate();
|
||||
sysTime.wHour = value.getUTCHours();
|
||||
sysTime.wMinute = value.getUTCMinutes();
|
||||
sysTime.wSecond = value.getUTCSeconds();
|
||||
sysTime.wMilliseconds = value.getUTCMilliseconds();
|
||||
let rv = KERNEL.SystemTimeToFileTime(sysTime.address(), buffer.address());
|
||||
if (!rv) {
|
||||
throw new Error("Failed to get FileTime.");
|
||||
}
|
||||
return [buffer, KERNEL.FILETIME.size];
|
||||
}
|
||||
throw new Error("Unrecognized value for longlong column");
|
||||
case COLUMN_TYPES.JET_coltypLongText:
|
||||
let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
|
||||
buffer = new wchar_tArray(value.length + 1);
|
||||
buffer.value = String(value);
|
||||
return [buffer, buffer.length * 2];
|
||||
case COLUMN_TYPES.JET_coltypBit:
|
||||
buffer = new ctypes.uint8_t();
|
||||
// Bizarre boolean values, but whatever:
|
||||
buffer.value = value ? 255 : 0;
|
||||
return [buffer, 1];
|
||||
case COLUMN_TYPES.JET_coltypGUID:
|
||||
let byteArray = ctypes.ArrayType(ctypes.uint8_t);
|
||||
buffer = new byteArray(16);
|
||||
let j = 0;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (!(/[0-9a-f]/i).test(value[i])) {
|
||||
continue;
|
||||
}
|
||||
let byteAsHex = value.substr(i, 2);
|
||||
buffer[j++] = parseInt(byteAsHex, 16);
|
||||
i++;
|
||||
}
|
||||
return [buffer, 16];
|
||||
}
|
||||
|
||||
throw new Error("Unknown type " + valueType);
|
||||
}
|
||||
|
||||
let initializedESE = false;
|
||||
|
||||
let eseDBWritingHelpers = {
|
||||
setupDB(dbFile, tableName, columns, rows) {
|
||||
if (!initializedESE) {
|
||||
initializedESE = true;
|
||||
loadLibraries();
|
||||
|
||||
KERNEL.SystemTimeToFileTime = gLibs.kernel.declare("SystemTimeToFileTime",
|
||||
ctypes.default_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr);
|
||||
|
||||
declareESEFunction("CreateDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
|
||||
ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
|
||||
declareESEFunction("CreateTableColumnIndexW", ESE.JET_SESID, ESE.JET_DBID,
|
||||
ESE.JET_TABLECREATE_W.ptr);
|
||||
declareESEFunction("BeginTransaction", ESE.JET_SESID);
|
||||
declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT);
|
||||
declareESEFunction("PrepareUpdate", ESE.JET_SESID, ESE.JET_TABLEID,
|
||||
ctypes.unsigned_long);
|
||||
declareESEFunction("Update", ESE.JET_SESID, ESE.JET_TABLEID,
|
||||
ctypes.voidptr_t, ctypes.unsigned_long,
|
||||
ctypes.unsigned_long.ptr);
|
||||
declareESEFunction("SetColumn", ESE.JET_SESID, ESE.JET_TABLEID,
|
||||
ESE.JET_COLUMNID, ctypes.voidptr_t,
|
||||
ctypes.unsigned_long, ESE.JET_GRBIT, ESE.JET_SETINFO.ptr);
|
||||
ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/,
|
||||
8192, null);
|
||||
}
|
||||
|
||||
let rootPath = dbFile.parent.path + "\\";
|
||||
let logPath = rootPath + "LogFiles\\";
|
||||
|
||||
try {
|
||||
this._instanceId = new ESE.JET_INSTANCE();
|
||||
ESE.CreateInstanceW(this._instanceId.address(),
|
||||
"firefox-dbwriter-" + (gESEInstanceCounter++));
|
||||
this._instanceCreated = true;
|
||||
|
||||
ESE.SetSystemParameterW(this._instanceId.address(), 0,
|
||||
0 /* JET_paramSystemPath*/, 0, rootPath);
|
||||
ESE.SetSystemParameterW(this._instanceId.address(), 0,
|
||||
1 /* JET_paramTempPath */, 0, rootPath);
|
||||
ESE.SetSystemParameterW(this._instanceId.address(), 0,
|
||||
2 /* JET_paramLogFilePath*/, 0, logPath);
|
||||
// Shouldn't try to call JetTerm if the following call fails.
|
||||
this._instanceCreated = false;
|
||||
ESE.Init(this._instanceId.address());
|
||||
this._instanceCreated = true;
|
||||
this._sessionId = new ESE.JET_SESID();
|
||||
ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null,
|
||||
null);
|
||||
this._sessionCreated = true;
|
||||
|
||||
this._dbId = new ESE.JET_DBID();
|
||||
this._dbPath = rootPath + "spartan.edb";
|
||||
ESE.CreateDatabaseW(this._sessionId, this._dbPath, null,
|
||||
this._dbId.address(), 0);
|
||||
this._opened = this._attached = true;
|
||||
|
||||
let tableCreationWrapper = createTableCreationWrapper(tableName, columns);
|
||||
ESE.CreateTableColumnIndexW(this._sessionId, this._dbId,
|
||||
tableCreationWrapper.table.address());
|
||||
this._tableId = tableCreationWrapper.table.tableid;
|
||||
|
||||
let columnIdMap = new Map();
|
||||
if (rows.length) {
|
||||
// Iterate over the struct we passed into ESENT because they have the
|
||||
// created column ids.
|
||||
let columnCount = ctypes.UInt64.lo(tableCreationWrapper.table.cColumns);
|
||||
let columnsPassed = tableCreationWrapper.table.rgcolumncreate;
|
||||
for (let i = 0; i < columnCount; i++) {
|
||||
let column = columnsPassed.contents;
|
||||
columnIdMap.set(column.szColumnName.readString(), column);
|
||||
columnsPassed = columnsPassed.increment();
|
||||
}
|
||||
ESE.ManualMove(this._sessionId, this._tableId,
|
||||
-2147483648 /* JET_MoveFirst */, 0);
|
||||
ESE.BeginTransaction(this._sessionId);
|
||||
for (let row of rows) {
|
||||
ESE.PrepareUpdate(this._sessionId, this._tableId, 0 /* JET_prepInsert */);
|
||||
for (let columnName in row) {
|
||||
let col = columnIdMap.get(columnName);
|
||||
let colId = col.columnid;
|
||||
let [val, valSize] = convertValueForWriting(row[columnName], col.coltyp);
|
||||
/* JET_bitSetOverwriteLV */
|
||||
ESE.SetColumn(this._sessionId, this._tableId, colId, val.address(), valSize, 4, null);
|
||||
}
|
||||
let actualBookmarkSize = new ctypes.unsigned_long();
|
||||
ESE.Update(this._sessionId, this._tableId, null, 0, actualBookmarkSize.address());
|
||||
}
|
||||
ESE.CommitTransaction(this._sessionId, 0 /* JET_bitWaitLastLevel0Commit */);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
this._close();
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_close() {
|
||||
if (this._tableId) {
|
||||
ESE.FailSafeCloseTable(this._sessionId, this._tableId);
|
||||
delete this._tableId;
|
||||
}
|
||||
if (this._opened) {
|
||||
ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
|
||||
this._opened = false;
|
||||
}
|
||||
if (this._attached) {
|
||||
ESE.FailSafeDetachDatabaseW(this._sessionId, this._dbPath);
|
||||
this._attached = false;
|
||||
}
|
||||
if (this._sessionCreated) {
|
||||
ESE.FailSafeEndSession(this._sessionId, 0);
|
||||
this._sessionCreated = false;
|
||||
}
|
||||
if (this._instanceCreated) {
|
||||
ESE.FailSafeTerm(this._instanceId);
|
||||
this._instanceCreated = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
add_task(function*() {
|
||||
let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||
tempFile.append("fx-xpcshell-edge-db");
|
||||
tempFile.createUnique(tempFile.DIRECTORY_TYPE, 0o600);
|
||||
|
||||
let db = tempFile.clone();
|
||||
db.append("spartan.edb");
|
||||
|
||||
let logs = tempFile.clone();
|
||||
logs.append("LogFiles");
|
||||
logs.create(tempFile.DIRECTORY_TYPE, 0o600);
|
||||
|
||||
let creationDate = new Date(Date.now() - 5000);
|
||||
const kEdgeMenuParent = "62d07e2b-5f0d-4e41-8426-5f5ec9717beb";
|
||||
let itemsInDB = [
|
||||
{
|
||||
URL: "http://www.mozilla.org/",
|
||||
Title: "Mozilla",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 100),
|
||||
ItemId: "1c00c10a-15f6-4618-92dd-22575102a4da",
|
||||
ParentId: kEdgeMenuParent,
|
||||
IsFolder: false,
|
||||
IsDeleted: false,
|
||||
},
|
||||
{
|
||||
Title: "Folder",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 200),
|
||||
ItemId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
|
||||
ParentId: kEdgeMenuParent,
|
||||
IsFolder: true,
|
||||
IsDeleted: false,
|
||||
},
|
||||
{
|
||||
Title: "Item in folder",
|
||||
URL: "http://www.iteminfolder.org/",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 300),
|
||||
ItemId: "c295ddaf-04a1-424a-866c-0ebde011e7c8",
|
||||
ParentId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
|
||||
IsFolder: false,
|
||||
IsDeleted: false,
|
||||
},
|
||||
{
|
||||
Title: "Deleted folder",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 400),
|
||||
ItemId: "a547573c-4d4d-4406-a736-5b5462d93bca",
|
||||
ParentId: kEdgeMenuParent,
|
||||
IsFolder: true,
|
||||
IsDeleted: true,
|
||||
},
|
||||
{
|
||||
Title: "Deleted item",
|
||||
URL: "http://www.deleteditem.org/",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 500),
|
||||
ItemId: "37a574bb-b44b-4bbc-a414-908615536435",
|
||||
ParentId: kEdgeMenuParent,
|
||||
IsFolder: false,
|
||||
IsDeleted: true,
|
||||
},
|
||||
{
|
||||
Title: "Item in deleted folder (should be in root)",
|
||||
URL: "http://www.itemindeletedfolder.org/",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 600),
|
||||
ItemId: "74dd1cc3-4c5d-471f-bccc-7bc7c72fa621",
|
||||
ParentId: "a547573c-4d4d-4406-a736-5b5462d93bca",
|
||||
IsFolder: false,
|
||||
IsDeleted: false,
|
||||
},
|
||||
{
|
||||
Title: "_Favorites_Bar_",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 700),
|
||||
ItemId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
|
||||
ParentId: kEdgeMenuParent,
|
||||
IsFolder: true,
|
||||
IsDeleted: false,
|
||||
},
|
||||
{
|
||||
Title: "Item in favorites bar",
|
||||
URL: "http://www.iteminfavoritesbar.org/",
|
||||
DateUpdated: new Date(creationDate.valueOf() + 800),
|
||||
ItemId: "9f2b1ff8-b651-46cf-8f41-16da8bcb6791",
|
||||
ParentId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
|
||||
IsFolder: false,
|
||||
IsDeleted: false,
|
||||
},
|
||||
];
|
||||
eseDBWritingHelpers.setupDB(db, "Favorites", [
|
||||
{type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096},
|
||||
{type: COLUMN_TYPES.JET_coltypLongText, name: "Title", cbMax: 4096},
|
||||
{type: COLUMN_TYPES.JET_coltypLongLong, name: "DateUpdated"},
|
||||
{type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId"},
|
||||
{type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted"},
|
||||
{type: COLUMN_TYPES.JET_coltypBit, name: "IsFolder"},
|
||||
{type: COLUMN_TYPES.JET_coltypGUID, name: "ParentId"},
|
||||
], itemsInDB);
|
||||
|
||||
let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=edge"]
|
||||
.createInstance(Ci.nsIBrowserProfileMigrator);
|
||||
let bookmarksMigrator = migrator.wrappedJSObject.getESEMigratorForTesting(db);
|
||||
Assert.ok(bookmarksMigrator.exists, "Should recognize table we just created");
|
||||
|
||||
let source = MigrationUtils.getLocalizedString("sourceNameEdge");
|
||||
let sourceLabel = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
|
||||
|
||||
let seenBookmarks = [];
|
||||
let bookmarkObserver = {
|
||||
onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) {
|
||||
if (title.startsWith("Deleted")) {
|
||||
ok(false, "Should not see deleted items being bookmarked!");
|
||||
}
|
||||
seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
|
||||
},
|
||||
onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch() {},
|
||||
onItemRemoved() {},
|
||||
onItemChanged() {},
|
||||
onItemVisited() {},
|
||||
onItemMoved() {},
|
||||
};
|
||||
PlacesUtils.bookmarks.addObserver(bookmarkObserver, false);
|
||||
|
||||
let migrateResult = yield new Promise(resolve => bookmarksMigrator.migrate(resolve)).catch(ex => {
|
||||
Cu.reportError(ex);
|
||||
Assert.ok(false, "Got an exception trying to migrate data! " + ex);
|
||||
return false;
|
||||
});
|
||||
PlacesUtils.bookmarks.removeObserver(bookmarkObserver);
|
||||
Assert.ok(migrateResult, "Migration should succeed");
|
||||
Assert.equal(seenBookmarks.length, 7, "Should have seen 7 items being bookmarked.");
|
||||
Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length,
|
||||
MigrationUtils._importQuantities.bookmarks,
|
||||
"Telemetry should have items except for 'From Microsoft Edge' folders");
|
||||
|
||||
let menuParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.menuGuid);
|
||||
Assert.equal(menuParents.length, 1, "Should have a single folder added to the menu");
|
||||
let toolbarParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid);
|
||||
Assert.equal(toolbarParents.length, 1, "Should have a single item added to the toolbar");
|
||||
let menuParentGuid = menuParents[0].itemGuid;
|
||||
let toolbarParentGuid = toolbarParents[0].itemGuid;
|
||||
|
||||
let expectedTitlesInMenu = itemsInDB.filter(item => item.ParentId == kEdgeMenuParent).map(item => item.Title);
|
||||
// Hacky, but seems like much the simplest way:
|
||||
expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
|
||||
let expectedTitlesInToolbar = itemsInDB.filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf").map(item => item.Title);
|
||||
|
||||
let edgeNameStr = MigrationUtils.getLocalizedString("sourceNameEdge");
|
||||
let importParentFolderName = MigrationUtils.getLocalizedString("importedBookmarksFolder", [edgeNameStr]);
|
||||
|
||||
for (let bookmark of seenBookmarks) {
|
||||
let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
|
||||
let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
|
||||
if (bookmark.title == "Folder" || bookmark.title == importParentFolderName) {
|
||||
Assert.equal(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
"Bookmark " + bookmark.title + " should be a folder");
|
||||
} else {
|
||||
Assert.notEqual(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
"Bookmark " + bookmark.title + " should not be a folder");
|
||||
}
|
||||
|
||||
if (shouldBeInMenu) {
|
||||
Assert.equal(bookmark.parentGuid, menuParentGuid, "Item '" + bookmark.title + "' should be in menu");
|
||||
} else if (shouldBeInToolbar) {
|
||||
Assert.equal(bookmark.parentGuid, toolbarParentGuid, "Item '" + bookmark.title + "' should be in toolbar");
|
||||
} else if (bookmark.itemGuid == menuParentGuid || bookmark.itemGuid == toolbarParentGuid) {
|
||||
Assert.ok(true, "Expect toolbar and menu folders to not be in menu or toolbar");
|
||||
} else {
|
||||
// Bit hacky, but we do need to check this.
|
||||
Assert.equal(bookmark.title, "Item in folder", "Subfoldered item shouldn't be in menu or toolbar");
|
||||
let parent = seenBookmarks.find(maybeParent => maybeParent.itemGuid == bookmark.parentGuid);
|
||||
Assert.equal(parent && parent.title, "Folder", "Subfoldered item should be in subfolder labeled 'Folder'");
|
||||
}
|
||||
|
||||
let dbItem = itemsInDB.find(someItem => bookmark.title == someItem.Title);
|
||||
if (!dbItem) {
|
||||
Assert.equal(bookmark.title, importParentFolderName, "Only the extra layer of folders isn't in the input we stuck in the DB.");
|
||||
Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.itemGuid), "This item should be one of the containers");
|
||||
} else {
|
||||
Assert.equal(dbItem.URL || null, bookmark.url && bookmark.url.spec, "URL is correct");
|
||||
Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded / 1000)).valueOf(), "Date added is correct");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
397
browser/components/migration/tests/unit/test_IE7_passwords.js
Normal file
397
browser/components/migration/tests/unit/test_IE7_passwords.js
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
|
||||
"resource://gre/modules/WindowsRegistry.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
|
||||
"resource://gre/modules/OSCrypto.jsm");
|
||||
|
||||
const IE7_FORM_PASSWORDS_MIGRATOR_NAME = "IE7FormPasswords";
|
||||
const LOGINS_KEY = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
|
||||
const EXTENSION = "-backup";
|
||||
const TESTED_WEBSITES = {
|
||||
twitter: {
|
||||
uri: makeURI("https://twitter.com"),
|
||||
hash: "A89D42BC6406E27265B1AD0782B6F376375764A301",
|
||||
data: [12, 0, 0, 0, 56, 0, 0, 0, 38, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 8, 0, 0, 0, 18, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 9, 0, 0, 0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0, 103, 0, 104, 0, 0, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 0, 0, 0],
|
||||
logins: [
|
||||
{
|
||||
username: "abcdefgh",
|
||||
password: "123456789",
|
||||
hostname: "https://twitter.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439325854000,
|
||||
timeLastUsed: 1439325854000,
|
||||
timePasswordChanged: 1439325854000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
facebook: {
|
||||
uri: makeURI("https://www.facebook.com/"),
|
||||
hash: "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796",
|
||||
data: [12, 0, 0, 0, 152, 0, 0, 0, 160, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 40, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 60, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 80, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 100, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 120, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 140, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 48, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 48, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 49, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 49, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 50, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 50, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 51, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 51, 0, 0, 0],
|
||||
logins: [
|
||||
{
|
||||
username: "username0",
|
||||
password: "password0",
|
||||
hostname: "https://www.facebook.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439326966000,
|
||||
timeLastUsed: 1439326966000,
|
||||
timePasswordChanged: 1439326966000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
username: "username1",
|
||||
password: "password1",
|
||||
hostname: "https://www.facebook.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439326997000,
|
||||
timeLastUsed: 1439326997000,
|
||||
timePasswordChanged: 1439326997000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
username: "username2",
|
||||
password: "password2",
|
||||
hostname: "https://www.facebook.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439327040000,
|
||||
timeLastUsed: 1439327040000,
|
||||
timePasswordChanged: 1439327040000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
username: "username3",
|
||||
password: "password3",
|
||||
hostname: "https://www.facebook.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439327070000,
|
||||
timeLastUsed: 1439327070000,
|
||||
timePasswordChanged: 1439327070000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
live: {
|
||||
uri: makeURI("https://login.live.com/"),
|
||||
hash: "7B506F2D6B81D939A8E0456F036EE8970856FF705E",
|
||||
data: [12, 0, 0, 0, 56, 0, 0, 0, 44, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 11, 0, 0, 0, 114, 0, 105, 0, 97, 0, 100, 0, 104, 0, 49, 6, 74, 6, 39, 6, 54, 6, 0, 0, 39, 6, 66, 6, 49, 6, 35, 6, 80, 0, 192, 0, 223, 0, 119, 0, 246, 0, 114, 0, 100, 0, 0, 0],
|
||||
logins: [
|
||||
{
|
||||
username: "riadhرياض",
|
||||
password: "اقرأPÀßwörd",
|
||||
hostname: "https://login.live.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439338767000,
|
||||
timeLastUsed: 1439338767000,
|
||||
timePasswordChanged: 1439338767000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
reddit: {
|
||||
uri: makeURI("http://www.reddit.com/"),
|
||||
hash: "B644028D1C109A91EC2C4B9D1F145E55A1FAE42065",
|
||||
data: [12, 0, 0, 0, 152, 0, 0, 0, 212, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 8, 234, 114, 153, 212, 208, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 3, 0, 0, 0, 14, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 16, 0, 0, 0, 48, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 4, 0, 0, 0, 58, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 29, 0, 0, 0, 118, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 15, 0, 0, 0, 150, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 30, 0, 0, 0, 97, 0, 0, 0, 0, 0, 252, 140, 173, 138, 146, 48, 0, 0, 66, 0, 105, 0, 116, 0, 116, 0, 101, 0, 32, 0, 98, 0, 101, 0, 115, 0, 116, 0, 228, 0, 116, 0, 105, 0, 103, 0, 101, 0, 110, 0, 0, 0, 205, 145, 110, 127, 198, 91, 1, 120, 0, 0, 31, 4, 48, 4, 64, 4, 62, 4, 59, 4, 76, 4, 32, 0, 67, 4, 65, 4, 63, 4, 53, 4, 72, 4, 61, 4, 62, 4, 32, 0, 65, 4, 49, 4, 64, 4, 62, 4, 72, 4, 53, 4, 61, 4, 46, 0, 32, 0, 18, 4, 62, 4, 57, 4, 66, 4, 56, 4, 0, 0, 40, 6, 51, 6, 69, 6, 32, 0, 39, 6, 68, 6, 68, 6, 71, 6, 32, 0, 39, 6, 68, 6, 49, 6, 45, 6, 69, 6, 70, 6, 0, 0, 118, 0, 101, 0, 117, 0, 105, 0, 108, 0, 108, 0, 101, 0, 122, 0, 32, 0, 108, 0, 101, 0, 32, 0, 118, 0, 233, 0, 114, 0, 105, 0, 102, 0, 105, 0, 101, 0, 114, 0, 32, 0, 224, 0, 32, 0, 110, 0, 111, 0, 117, 0, 118, 0, 101, 0, 97, 0, 117, 0, 0, 0],
|
||||
logins: [
|
||||
{
|
||||
username: "購読を",
|
||||
password: "Bitte bestätigen",
|
||||
hostname: "http://www.reddit.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439340874000,
|
||||
timeLastUsed: 1439340874000,
|
||||
timePasswordChanged: 1439340874000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
username: "重置密码",
|
||||
password: "Пароль успешно сброшен. Войти",
|
||||
hostname: "http://www.reddit.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439340971000,
|
||||
timeLastUsed: 1439340971000,
|
||||
timePasswordChanged: 1439340971000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
{
|
||||
username: "بسم الله الرحمن",
|
||||
password: "veuillez le vérifier à nouveau",
|
||||
hostname: "http://www.reddit.com",
|
||||
formSubmitURL: "",
|
||||
httpRealm: null,
|
||||
usernameField: "",
|
||||
passwordField: "",
|
||||
timeCreated: 1439341166000,
|
||||
timeLastUsed: 1439341166000,
|
||||
timePasswordChanged: 1439341166000,
|
||||
timesUsed: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const TESTED_URLS = [
|
||||
"http://a.foo.com",
|
||||
"http://b.foo.com",
|
||||
"http://c.foo.com",
|
||||
"http://www.test.net",
|
||||
"http://www.test.net/home",
|
||||
"http://www.test.net/index",
|
||||
"https://a.bar.com",
|
||||
"https://b.bar.com",
|
||||
"https://c.bar.com",
|
||||
];
|
||||
|
||||
var nsIWindowsRegKey = Ci.nsIWindowsRegKey;
|
||||
var Storage2Key;
|
||||
|
||||
/*
|
||||
* If the key value exists, it's going to be backed up and replaced, so the value could be restored.
|
||||
* Otherwise a new value is going to be created.
|
||||
*/
|
||||
function backupAndStore(key, name, value) {
|
||||
if (key.hasValue(name)) {
|
||||
// backup the the current value
|
||||
let type = key.getValueType(name);
|
||||
// create a new value using use the current value name followed by EXTENSION as its new name
|
||||
switch (type) {
|
||||
case nsIWindowsRegKey.TYPE_STRING:
|
||||
key.writeStringValue(name + EXTENSION, key.readStringValue(name));
|
||||
break;
|
||||
case nsIWindowsRegKey.TYPE_BINARY:
|
||||
key.writeBinaryValue(name + EXTENSION, key.readBinaryValue(name));
|
||||
break;
|
||||
case nsIWindowsRegKey.TYPE_INT:
|
||||
key.writeIntValue(name + EXTENSION, key.readIntValue(name));
|
||||
break;
|
||||
case nsIWindowsRegKey.TYPE_INT64:
|
||||
key.writeInt64Value(name + EXTENSION, key.readInt64Value(name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
key.writeBinaryValue(name, value);
|
||||
}
|
||||
|
||||
// Remove all values where their names are members of the names array from the key of registry
|
||||
function removeAllValues(key, names) {
|
||||
for (let name of names) {
|
||||
key.removeValue(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore all the backed up values
|
||||
function restore(key) {
|
||||
let count = key.valueCount;
|
||||
let names = []; // the names of the key values
|
||||
for (let i = 0; i < count; ++i) {
|
||||
names.push(key.getValueName(i));
|
||||
}
|
||||
|
||||
for (let name of names) {
|
||||
// backed up values have EXTENSION at the end of their names
|
||||
if (name.lastIndexOf(EXTENSION) == name.length - EXTENSION.length) {
|
||||
let valueName = name.substr(0, name.length - EXTENSION.length);
|
||||
let type = key.getValueType(name);
|
||||
// create a new value using the name before the backup and removed the backed up one
|
||||
switch (type) {
|
||||
case nsIWindowsRegKey.TYPE_STRING:
|
||||
key.writeStringValue(valueName, key.readStringValue(name));
|
||||
key.removeValue(name);
|
||||
break;
|
||||
case nsIWindowsRegKey.TYPE_BINARY:
|
||||
key.writeBinaryValue(valueName, key.readBinaryValue(name));
|
||||
key.removeValue(name);
|
||||
break;
|
||||
case nsIWindowsRegKey.TYPE_INT:
|
||||
key.writeIntValue(valueName, key.readIntValue(name));
|
||||
key.removeValue(name);
|
||||
break;
|
||||
case nsIWindowsRegKey.TYPE_INT64:
|
||||
key.writeInt64Value(valueName, key.readInt64Value(name));
|
||||
key.removeValue(name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkLoginsAreEqual(passwordManagerLogin, IELogin, id) {
|
||||
passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
for (let attribute in IELogin) {
|
||||
Assert.equal(passwordManagerLogin[attribute], IELogin[attribute],
|
||||
"The two logins ID " + id + " have the same " + attribute);
|
||||
}
|
||||
}
|
||||
|
||||
function createRegistryPath(path) {
|
||||
let loginPath = path.split("\\");
|
||||
let parentKey = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(nsIWindowsRegKey);
|
||||
let currentPath = [];
|
||||
for (let currentKey of loginPath) {
|
||||
parentKey.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, currentPath.join("\\"),
|
||||
nsIWindowsRegKey.ACCESS_ALL);
|
||||
|
||||
if (!parentKey.hasChild(currentKey)) {
|
||||
parentKey.createChild(currentKey, 0);
|
||||
}
|
||||
currentPath.push(currentKey);
|
||||
parentKey.close();
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstResourceOfType(type) {
|
||||
let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=ie"]
|
||||
.createInstance(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
let migrators = migrator.getResources();
|
||||
for (let m of migrators) {
|
||||
if (m.name == IE7_FORM_PASSWORDS_MIGRATOR_NAME && m.type == type) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
throw new Error("failed to find the " + type + " migrator");
|
||||
}
|
||||
|
||||
function makeURI(aURL) {
|
||||
return Services.io.newURI(aURL, null, null);
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
|
||||
Assert.throws(() => getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS),
|
||||
"The migrator doesn't exist for win8+");
|
||||
return;
|
||||
}
|
||||
// create the path to Storage2 in the registry if it doest exist.
|
||||
createRegistryPath(LOGINS_KEY);
|
||||
Storage2Key = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(nsIWindowsRegKey);
|
||||
Storage2Key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, LOGINS_KEY,
|
||||
nsIWindowsRegKey.ACCESS_ALL);
|
||||
|
||||
// create a dummy value otherwise the migrator doesn't exist
|
||||
if (!Storage2Key.hasValue("dummy")) {
|
||||
Storage2Key.writeBinaryValue("dummy", "dummy");
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* test_passwordsNotAvailable() {
|
||||
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
|
||||
Assert.ok(migrator.exists, "The migrator has to exist");
|
||||
let logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, 0, "There are no logins at the beginning of the test");
|
||||
|
||||
let uris = []; // the uris of the migrated logins
|
||||
for (let url of TESTED_URLS) {
|
||||
uris.push(makeURI(url));
|
||||
// in this test, there is no IE login data in the registry, so after the migration, the number
|
||||
// of logins in the store should be 0
|
||||
migrator._migrateURIs(uris);
|
||||
logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, 0,
|
||||
"There are no logins after doing the migration without adding values to the registry");
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* test_passwordsAvailable() {
|
||||
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let crypto = new OSCrypto();
|
||||
let hashes = []; // the hashes of all migrator websites, this is going to be used for the clean up
|
||||
|
||||
do_register_cleanup(() => {
|
||||
Services.logins.removeAllLogins();
|
||||
logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, 0, "There are no logins after the cleanup");
|
||||
// remove all the values created in this test from the registry
|
||||
removeAllValues(Storage2Key, hashes);
|
||||
// restore all backed up values
|
||||
restore(Storage2Key);
|
||||
|
||||
// clean the dummy value
|
||||
if (Storage2Key.hasValue("dummy")) {
|
||||
Storage2Key.removeValue("dummy");
|
||||
}
|
||||
Storage2Key.close();
|
||||
crypto.finalize();
|
||||
});
|
||||
|
||||
let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
|
||||
Assert.ok(migrator.exists, "The migrator has to exist");
|
||||
let logins = Services.logins.getAllLogins({});
|
||||
Assert.equal(logins.length, 0, "There are no logins at the beginning of the test");
|
||||
|
||||
let uris = []; // the uris of the migrated logins
|
||||
|
||||
let loginCount = 0;
|
||||
for (let current in TESTED_WEBSITES) {
|
||||
let website = TESTED_WEBSITES[current];
|
||||
// backup the current the registry value if it exists and replace the existing value/create a
|
||||
// new value with the encrypted data
|
||||
backupAndStore(Storage2Key, website.hash,
|
||||
crypto.encryptData(crypto.arrayToString(website.data),
|
||||
website.uri.spec, true));
|
||||
Assert.ok(migrator.exists, "The migrator has to exist");
|
||||
uris.push(website.uri);
|
||||
hashes.push(website.hash);
|
||||
|
||||
migrator._migrateURIs(uris);
|
||||
logins = Services.logins.getAllLogins({});
|
||||
// check that the number of logins in the password manager has increased as expected which means
|
||||
// that all the values for the current website were imported
|
||||
loginCount += website.logins.length;
|
||||
Assert.equal(logins.length, loginCount,
|
||||
"The number of logins has increased after the migration");
|
||||
// NB: because telemetry records any login data passed to the login manager, it
|
||||
// also gets told about logins that are duplicates or invalid (for one reason
|
||||
// or another) and so its counts might exceed those of the login manager itself.
|
||||
Assert.greaterOrEqual(MigrationUtils._importQuantities.logins, loginCount,
|
||||
"Telemetry quantities equal or exceed the actual import.");
|
||||
// Reset - this normally happens at the start of a new migration, but we're calling
|
||||
// the migrator directly so can't rely on that:
|
||||
MigrationUtils._importQuantities.logins = 0;
|
||||
|
||||
let startIndex = loginCount - website.logins.length;
|
||||
// compares the imported password manager logins with their expected logins
|
||||
for (let i = 0; i < website.logins.length; i++) {
|
||||
checkLoginsAreEqual(logins[startIndex + i], website.logins[i],
|
||||
" " + current + " - " + i + " ");
|
||||
}
|
||||
}
|
||||
});
|
||||
44
browser/components/migration/tests/unit/test_IE_bookmarks.js
Normal file
44
browser/components/migration/tests/unit/test_IE_bookmarks.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"use strict";
|
||||
|
||||
add_task(function* () {
|
||||
let migrator = MigrationUtils.getMigrator("ie");
|
||||
// Sanity check for the source.
|
||||
Assert.ok(migrator.sourceExists);
|
||||
|
||||
// Wait for the imported bookmarks. Check that "From Internet Explorer"
|
||||
// folders are created in the menu and on the toolbar.
|
||||
let source = MigrationUtils.getLocalizedString("sourceNameIE");
|
||||
let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
|
||||
|
||||
let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
|
||||
PlacesUtils.toolbarFolderId ];
|
||||
|
||||
let itemCount = 0;
|
||||
let bmObserver = {
|
||||
onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
|
||||
if (aTitle != label) {
|
||||
itemCount++;
|
||||
}
|
||||
if (expectedParents.length > 0 && aTitle == label) {
|
||||
let index = expectedParents.indexOf(aParentId);
|
||||
Assert.notEqual(index, -1);
|
||||
expectedParents.splice(index, 1);
|
||||
}
|
||||
},
|
||||
onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch() {},
|
||||
onItemRemoved() {},
|
||||
onItemChanged() {},
|
||||
onItemVisited() {},
|
||||
onItemMoved() {},
|
||||
};
|
||||
PlacesUtils.bookmarks.addObserver(bmObserver, false);
|
||||
|
||||
yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
|
||||
PlacesUtils.bookmarks.removeObserver(bmObserver);
|
||||
Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount,
|
||||
"Ensure telemetry matches actual number of imported items.");
|
||||
|
||||
// Check the bookmarks have been imported to all the expected parents.
|
||||
Assert.equal(expectedParents.length, 0, "Got all the expected parents");
|
||||
});
|
||||
111
browser/components/migration/tests/unit/test_IE_cookies.js
Normal file
111
browser/components/migration/tests/unit/test_IE_cookies.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
|
||||
"resource://gre/modules/ctypes.jsm");
|
||||
|
||||
add_task(function* () {
|
||||
let migrator = MigrationUtils.getMigrator("ie");
|
||||
// Sanity check for the source.
|
||||
Assert.ok(migrator.sourceExists);
|
||||
|
||||
const BOOL = ctypes.bool;
|
||||
const LPCTSTR = ctypes.char16_t.ptr;
|
||||
const DWORD = ctypes.uint32_t;
|
||||
const LPDWORD = DWORD.ptr;
|
||||
|
||||
let wininet = ctypes.open("Wininet");
|
||||
|
||||
/*
|
||||
BOOL InternetSetCookieW(
|
||||
_In_ LPCTSTR lpszUrl,
|
||||
_In_ LPCTSTR lpszCookieName,
|
||||
_In_ LPCTSTR lpszCookieData
|
||||
);
|
||||
*/
|
||||
let setIECookie = wininet.declare("InternetSetCookieW",
|
||||
ctypes.default_abi,
|
||||
BOOL,
|
||||
LPCTSTR,
|
||||
LPCTSTR,
|
||||
LPCTSTR);
|
||||
|
||||
/*
|
||||
BOOL InternetGetCookieW(
|
||||
_In_ LPCTSTR lpszUrl,
|
||||
_In_ LPCTSTR lpszCookieName,
|
||||
_Out_ LPCTSTR lpszCookieData,
|
||||
_Inout_ LPDWORD lpdwSize
|
||||
);
|
||||
*/
|
||||
let getIECookie = wininet.declare("InternetGetCookieW",
|
||||
ctypes.default_abi,
|
||||
BOOL,
|
||||
LPCTSTR,
|
||||
LPCTSTR,
|
||||
LPCTSTR,
|
||||
LPDWORD);
|
||||
|
||||
// We need to randomize the cookie to avoid clashing with other cookies
|
||||
// that might have been set by previous tests and not properly cleared.
|
||||
let date = (new Date()).getDate();
|
||||
const COOKIE = {
|
||||
get host() {
|
||||
return new URL(this.href).host;
|
||||
},
|
||||
href: `http://mycookietest.${Math.random()}.com`,
|
||||
name: "testcookie",
|
||||
value: "testvalue",
|
||||
expiry: new Date(new Date().setDate(date + 2))
|
||||
};
|
||||
let data = ctypes.char16_t.array()(256);
|
||||
let sizeRef = DWORD(256).address();
|
||||
|
||||
do_register_cleanup(() => {
|
||||
// Remove the cookie.
|
||||
try {
|
||||
let expired = new Date(new Date().setDate(date - 2));
|
||||
let rv = setIECookie(COOKIE.href, COOKIE.name,
|
||||
`; expires=${expired.toUTCString()}`);
|
||||
Assert.ok(rv, "Expired the IE cookie");
|
||||
Assert.ok(!getIECookie(COOKIE.href, COOKIE.name, data, sizeRef),
|
||||
"The cookie has been properly removed");
|
||||
} catch (ex) {}
|
||||
|
||||
// Close the library.
|
||||
try {
|
||||
wininet.close();
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
// Create the persistent cookie in IE.
|
||||
let value = `${COOKIE.value}; expires=${COOKIE.expiry.toUTCString()}`;
|
||||
let rv = setIECookie(COOKIE.href, COOKIE.name, value);
|
||||
Assert.ok(rv, "Added a persistent IE cookie: " + value);
|
||||
|
||||
// Sanity check the cookie has been created.
|
||||
Assert.ok(getIECookie(COOKIE.href, COOKIE.name, data, sizeRef),
|
||||
"Found the added persistent IE cookie");
|
||||
do_print("Found cookie: " + data.readString());
|
||||
Assert.equal(data.readString(), `${COOKIE.name}=${COOKIE.value}`,
|
||||
"Found the expected cookie");
|
||||
|
||||
// Sanity check that there are no cookies.
|
||||
Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
|
||||
"There are no cookies initially");
|
||||
|
||||
// Migrate cookies.
|
||||
yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES);
|
||||
|
||||
Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1,
|
||||
"Migrated the expected number of cookies");
|
||||
|
||||
// Now check the cookie details.
|
||||
let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host, {});
|
||||
Assert.ok(enumerator.hasMoreElements());
|
||||
let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
|
||||
|
||||
Assert.equal(foundCookie.name, COOKIE.name);
|
||||
Assert.equal(foundCookie.value, COOKIE.value);
|
||||
Assert.equal(foundCookie.host, "." + COOKIE.host);
|
||||
Assert.equal(foundCookie.expiry, Math.floor(COOKIE.expiry / 1000));
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
"use strict";
|
||||
|
||||
add_task(function* () {
|
||||
registerFakePath("ULibDir", do_get_file("Library/"));
|
||||
|
||||
let migrator = MigrationUtils.getMigrator("safari");
|
||||
// Sanity check for the source.
|
||||
Assert.ok(migrator.sourceExists);
|
||||
|
||||
// Wait for the imported bookmarks. Check that "From Safari"
|
||||
// folders are created on the toolbar.
|
||||
let source = MigrationUtils.getLocalizedString("sourceNameSafari");
|
||||
let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
|
||||
|
||||
let expectedParents = [ PlacesUtils.toolbarFolderId ];
|
||||
let itemCount = 0;
|
||||
|
||||
let bmObserver = {
|
||||
onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
|
||||
if (aTitle != label) {
|
||||
itemCount++;
|
||||
}
|
||||
if (expectedParents.length > 0 && aTitle == label) {
|
||||
let index = expectedParents.indexOf(aParentId);
|
||||
Assert.ok(index != -1, "Found expected parent");
|
||||
expectedParents.splice(index, 1);
|
||||
}
|
||||
},
|
||||
onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch() {},
|
||||
onItemRemoved() {},
|
||||
onItemChanged() {},
|
||||
onItemVisited() {},
|
||||
onItemMoved() {},
|
||||
};
|
||||
PlacesUtils.bookmarks.addObserver(bmObserver, false);
|
||||
|
||||
yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
|
||||
PlacesUtils.bookmarks.removeObserver(bmObserver);
|
||||
|
||||
// Check the bookmarks have been imported to all the expected parents.
|
||||
Assert.ok(!expectedParents.length, "No more expected parents");
|
||||
Assert.equal(itemCount, 13, "Should import all 13 items.");
|
||||
// Check that the telemetry matches:
|
||||
Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct.");
|
||||
});
|
||||
695
browser/components/migration/tests/unit/test_automigration.js
Normal file
695
browser/components/migration/tests/unit/test_automigration.js
Normal file
|
|
@ -0,0 +1,695 @@
|
|||
"use strict";
|
||||
|
||||
let AutoMigrateBackstage = Cu.import("resource:///modules/AutoMigrate.jsm"); /* globals AutoMigrate */
|
||||
|
||||
let gShimmedMigratorKeyPicker = null;
|
||||
let gShimmedMigrator = null;
|
||||
|
||||
const kUsecPerMin = 60 * 1000000;
|
||||
|
||||
// This is really a proxy on MigrationUtils, but if we specify that directly,
|
||||
// we get in trouble because the object itself is frozen, and Proxies can't
|
||||
// return a different value to an object when directly proxying a frozen
|
||||
// object.
|
||||
AutoMigrateBackstage.MigrationUtils = new Proxy({}, {
|
||||
get(target, name) {
|
||||
if (name == "getMigratorKeyForDefaultBrowser" && gShimmedMigratorKeyPicker) {
|
||||
return gShimmedMigratorKeyPicker;
|
||||
}
|
||||
if (name == "getMigrator" && gShimmedMigrator) {
|
||||
return function() { return gShimmedMigrator };
|
||||
}
|
||||
return MigrationUtils[name];
|
||||
},
|
||||
});
|
||||
|
||||
do_register_cleanup(function() {
|
||||
AutoMigrateBackstage.MigrationUtils = MigrationUtils;
|
||||
});
|
||||
|
||||
// This should be replaced by using History.fetch with a fetchVisits option,
|
||||
// once that becomes available
|
||||
function* visitsForURL(url)
|
||||
{
|
||||
let visitCount = 0;
|
||||
let db = yield PlacesUtils.promiseDBConnection();
|
||||
visitCount = yield db.execute(
|
||||
`SELECT count(*) FROM moz_historyvisits v
|
||||
JOIN moz_places h ON h.id = v.place_id
|
||||
WHERE url_hash = hash(:url) AND url = :url`,
|
||||
{url});
|
||||
visitCount = visitCount[0].getInt64(0);
|
||||
return visitCount;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test automatically picking a browser to migrate from
|
||||
*/
|
||||
add_task(function* checkMigratorPicking() {
|
||||
Assert.throws(() => AutoMigrate.pickMigrator("firefox"),
|
||||
/Can't automatically migrate from Firefox/,
|
||||
"Should throw when explicitly picking Firefox.");
|
||||
|
||||
Assert.throws(() => AutoMigrate.pickMigrator("gobbledygook"),
|
||||
/migrator object is not available/,
|
||||
"Should throw when passing unknown migrator key");
|
||||
gShimmedMigratorKeyPicker = function() {
|
||||
return "firefox";
|
||||
};
|
||||
Assert.throws(() => AutoMigrate.pickMigrator(),
|
||||
/Can't automatically migrate from Firefox/,
|
||||
"Should throw when implicitly picking Firefox.");
|
||||
gShimmedMigratorKeyPicker = function() {
|
||||
return "gobbledygook";
|
||||
};
|
||||
Assert.throws(() => AutoMigrate.pickMigrator(),
|
||||
/migrator object is not available/,
|
||||
"Should throw when an unknown migrator is the default");
|
||||
gShimmedMigratorKeyPicker = function() {
|
||||
return "";
|
||||
};
|
||||
Assert.throws(() => AutoMigrate.pickMigrator(),
|
||||
/Could not determine default browser key/,
|
||||
"Should throw when an unknown migrator is the default");
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Test automatically picking a profile to migrate from
|
||||
*/
|
||||
add_task(function* checkProfilePicking() {
|
||||
let fakeMigrator = {sourceProfiles: [{id: "a"}, {id: "b"}]};
|
||||
let profB = fakeMigrator.sourceProfiles[1];
|
||||
Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
|
||||
/Don't know how to pick a profile when more/,
|
||||
"Should throw when there are multiple profiles.");
|
||||
Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
|
||||
/Profile specified was not found/,
|
||||
"Should throw when the profile supplied doesn't exist.");
|
||||
let profileToMigrate = AutoMigrate.pickProfile(fakeMigrator, "b");
|
||||
Assert.equal(profileToMigrate, profB, "Should return profile supplied");
|
||||
|
||||
fakeMigrator.sourceProfiles = null;
|
||||
Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
|
||||
/Profile specified but only a default profile found./,
|
||||
"Should throw when the profile supplied doesn't exist.");
|
||||
profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
|
||||
Assert.equal(profileToMigrate, null, "Should return default profile when that's the only one.");
|
||||
|
||||
fakeMigrator.sourceProfiles = [];
|
||||
Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
|
||||
/No profile data found/,
|
||||
"Should throw when no profile data is present.");
|
||||
|
||||
fakeMigrator.sourceProfiles = [{id: "a"}];
|
||||
let profA = fakeMigrator.sourceProfiles[0];
|
||||
profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
|
||||
Assert.equal(profileToMigrate, profA, "Should return the only profile if only one is present.");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test the complete automatic process including browser and profile selection,
|
||||
* and actual migration (which implies startup)
|
||||
*/
|
||||
add_task(function* checkIntegration() {
|
||||
gShimmedMigrator = {
|
||||
get sourceProfiles() {
|
||||
do_print("Read sourceProfiles");
|
||||
return null;
|
||||
},
|
||||
getMigrateData(profileToMigrate) {
|
||||
this._getMigrateDataArgs = profileToMigrate;
|
||||
return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
|
||||
},
|
||||
migrate(types, startup, profileToMigrate) {
|
||||
this._migrateArgs = [types, startup, profileToMigrate];
|
||||
},
|
||||
};
|
||||
gShimmedMigratorKeyPicker = function() {
|
||||
return "gobbledygook";
|
||||
};
|
||||
AutoMigrate.migrate("startup");
|
||||
Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
|
||||
"getMigrateData called with 'null' as a profile");
|
||||
|
||||
let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
|
||||
let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
|
||||
Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
|
||||
"migrate called with 'null' as a profile");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test the undo preconditions and a no-op undo in the automigrator.
|
||||
*/
|
||||
add_task(function* checkUndoPreconditions() {
|
||||
let shouldAddData = false;
|
||||
gShimmedMigrator = {
|
||||
get sourceProfiles() {
|
||||
do_print("Read sourceProfiles");
|
||||
return null;
|
||||
},
|
||||
getMigrateData(profileToMigrate) {
|
||||
this._getMigrateDataArgs = profileToMigrate;
|
||||
return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
|
||||
},
|
||||
migrate(types, startup, profileToMigrate) {
|
||||
this._migrateArgs = [types, startup, profileToMigrate];
|
||||
if (shouldAddData) {
|
||||
// Insert a login and check that that worked.
|
||||
MigrationUtils.insertLoginWrapper({
|
||||
hostname: "www.mozilla.org",
|
||||
formSubmitURL: "http://www.mozilla.org",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
});
|
||||
}
|
||||
TestUtils.executeSoon(function() {
|
||||
Services.obs.notifyObservers(null, "Migration:Ended", undefined);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
gShimmedMigratorKeyPicker = function() {
|
||||
return "gobbledygook";
|
||||
};
|
||||
AutoMigrate.migrate("startup");
|
||||
let migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
|
||||
Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
|
||||
"getMigrateData called with 'null' as a profile");
|
||||
|
||||
let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
|
||||
let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
|
||||
Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
|
||||
"migrate called with 'null' as a profile");
|
||||
|
||||
yield migrationFinishedPromise;
|
||||
Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
|
||||
"Should have set browser pref");
|
||||
Assert.ok(!(yield AutoMigrate.canUndo()), "Should not be able to undo migration, as there's no data");
|
||||
gShimmedMigrator._migrateArgs = null;
|
||||
gShimmedMigrator._getMigrateDataArgs = null;
|
||||
Preferences.reset("browser.migrate.automigrate.browser");
|
||||
shouldAddData = true;
|
||||
|
||||
AutoMigrate.migrate("startup");
|
||||
migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
|
||||
Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
|
||||
"getMigrateData called with 'null' as a profile");
|
||||
Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
|
||||
"migrate called with 'null' as a profile");
|
||||
|
||||
yield migrationFinishedPromise;
|
||||
let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
|
||||
"http://www.mozilla.org", null);
|
||||
Assert.equal(storedLogins.length, 1, "Should have 1 login");
|
||||
|
||||
Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
|
||||
"Should have set browser pref");
|
||||
Assert.ok((yield AutoMigrate.canUndo()), "Should be able to undo migration, as now there's data");
|
||||
|
||||
yield AutoMigrate.undo();
|
||||
Assert.ok(true, "Should be able to finish an undo cycle.");
|
||||
|
||||
// Check that the undo removed the passwords:
|
||||
storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
|
||||
"http://www.mozilla.org", null);
|
||||
Assert.equal(storedLogins.length, 0, "Should have no logins");
|
||||
});
|
||||
|
||||
/**
|
||||
* Fake a migration and then try to undo it to verify all data gets removed.
|
||||
*/
|
||||
add_task(function* checkUndoRemoval() {
|
||||
MigrationUtils.initializeUndoData();
|
||||
Preferences.set("browser.migrate.automigrate.browser", "automationbrowser");
|
||||
// Insert a login and check that that worked.
|
||||
MigrationUtils.insertLoginWrapper({
|
||||
hostname: "www.mozilla.org",
|
||||
formSubmitURL: "http://www.mozilla.org",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
});
|
||||
let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
|
||||
"http://www.mozilla.org", null);
|
||||
Assert.equal(storedLogins.length, 1, "Should have 1 login");
|
||||
|
||||
// Insert a bookmark and check that we have exactly 1 bookmark for that URI.
|
||||
yield MigrationUtils.insertBookmarkWrapper({
|
||||
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
|
||||
url: "http://www.example.org/",
|
||||
title: "Some example bookmark",
|
||||
});
|
||||
|
||||
let bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
|
||||
Assert.ok(bookmark, "Should have a bookmark before undo");
|
||||
Assert.equal(bookmark.title, "Some example bookmark", "Should have correct bookmark before undo.");
|
||||
|
||||
// Insert 2 history visits
|
||||
let now_uSec = Date.now() * 1000;
|
||||
let visitedURI = Services.io.newURI("http://www.example.com/", null, null);
|
||||
let frecencyUpdatePromise = new Promise(resolve => {
|
||||
let expectedChanges = 2;
|
||||
let observer = {
|
||||
onFrecencyChanged: function() {
|
||||
if (!--expectedChanges) {
|
||||
PlacesUtils.history.removeObserver(observer);
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
};
|
||||
PlacesUtils.history.addObserver(observer, false);
|
||||
});
|
||||
yield MigrationUtils.insertVisitsWrapper([{
|
||||
uri: visitedURI,
|
||||
visits: [
|
||||
{
|
||||
transitionType: PlacesUtils.history.TRANSITION_LINK,
|
||||
visitDate: now_uSec,
|
||||
},
|
||||
{
|
||||
transitionType: PlacesUtils.history.TRANSITION_LINK,
|
||||
visitDate: now_uSec - 100 * kUsecPerMin,
|
||||
},
|
||||
]
|
||||
}]);
|
||||
yield frecencyUpdatePromise;
|
||||
|
||||
// Verify that both visits get reported.
|
||||
let opts = PlacesUtils.history.getNewQueryOptions();
|
||||
opts.resultType = opts.RESULTS_AS_VISIT;
|
||||
let query = PlacesUtils.history.getNewQuery();
|
||||
query.uri = visitedURI;
|
||||
let visits = PlacesUtils.history.executeQuery(query, opts);
|
||||
visits.root.containerOpen = true;
|
||||
Assert.equal(visits.root.childCount, 2, "Should have 2 visits");
|
||||
// Clean up:
|
||||
visits.root.containerOpen = false;
|
||||
|
||||
yield AutoMigrate.saveUndoState();
|
||||
|
||||
// Verify that we can undo, then undo:
|
||||
Assert.ok(AutoMigrate.canUndo(), "Should be possible to undo migration");
|
||||
yield AutoMigrate.undo();
|
||||
|
||||
let histograms = [
|
||||
"FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_ERRORCOUNT",
|
||||
"FX_STARTUP_MIGRATION_UNDO_LOGINS_ERRORCOUNT",
|
||||
"FX_STARTUP_MIGRATION_UNDO_VISITS_ERRORCOUNT",
|
||||
];
|
||||
for (let histogramId of histograms) {
|
||||
let keyedHistogram = Services.telemetry.getKeyedHistogramById(histogramId);
|
||||
let histogramData = keyedHistogram.snapshot().automationbrowser;
|
||||
Assert.equal(histogramData.sum, 0, `Should have reported 0 errors to ${histogramId}.`);
|
||||
Assert.greaterOrEqual(histogramData.counts[0], 1, `Should have reported value of 0 one time to ${histogramId}.`);
|
||||
}
|
||||
histograms = [
|
||||
"FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_MS",
|
||||
"FX_STARTUP_MIGRATION_UNDO_LOGINS_MS",
|
||||
"FX_STARTUP_MIGRATION_UNDO_VISITS_MS",
|
||||
"FX_STARTUP_MIGRATION_UNDO_TOTAL_MS",
|
||||
];
|
||||
for (let histogramId of histograms) {
|
||||
Assert.greater(Services.telemetry.getKeyedHistogramById(histogramId).snapshot().automationbrowser.sum, 0,
|
||||
`Should have reported non-zero time spent using undo for ${histogramId}`);
|
||||
}
|
||||
|
||||
// Check that the undo removed the history visits:
|
||||
visits = PlacesUtils.history.executeQuery(query, opts);
|
||||
visits.root.containerOpen = true;
|
||||
Assert.equal(visits.root.childCount, 0, "Should have no more visits");
|
||||
visits.root.containerOpen = false;
|
||||
|
||||
// Check that the undo removed the bookmarks:
|
||||
bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
|
||||
Assert.ok(!bookmark, "Should have no bookmarks after undo");
|
||||
|
||||
// Check that the undo removed the passwords:
|
||||
storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
|
||||
"http://www.mozilla.org", null);
|
||||
Assert.equal(storedLogins.length, 0, "Should have no logins");
|
||||
});
|
||||
|
||||
add_task(function* checkUndoBookmarksState() {
|
||||
MigrationUtils.initializeUndoData();
|
||||
const {TYPE_FOLDER, TYPE_BOOKMARK} = PlacesUtils.bookmarks;
|
||||
let title = "Some example bookmark";
|
||||
let url = "http://www.example.com";
|
||||
let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
let {guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title, url, parentGuid
|
||||
});
|
||||
Assert.deepEqual((yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"),
|
||||
[{lastModified, parentGuid, guid, type: TYPE_BOOKMARK}]);
|
||||
|
||||
MigrationUtils.initializeUndoData();
|
||||
({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title, parentGuid, type: TYPE_FOLDER
|
||||
}));
|
||||
let folder = {guid, lastModified, parentGuid, type: TYPE_FOLDER};
|
||||
let folderGuid = folder.guid;
|
||||
({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title, url, parentGuid: folderGuid
|
||||
}));
|
||||
let kid1 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
|
||||
({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title, url, parentGuid: folderGuid
|
||||
}));
|
||||
let kid2 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
|
||||
|
||||
let bookmarksUndo = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
|
||||
Assert.equal(bookmarksUndo.length, 3);
|
||||
// We expect that the last modified time from first kid #1 and then kid #2
|
||||
// has been propagated to the folder:
|
||||
folder.lastModified = kid2.lastModified;
|
||||
// Not just using deepEqual on the entire array (which should work) because
|
||||
// the failure messages get truncated by xpcshell which is unhelpful.
|
||||
Assert.deepEqual(bookmarksUndo[0], folder);
|
||||
Assert.deepEqual(bookmarksUndo[1], kid1);
|
||||
Assert.deepEqual(bookmarksUndo[2], kid2);
|
||||
yield PlacesUtils.bookmarks.eraseEverything();
|
||||
});
|
||||
|
||||
add_task(function* testBookmarkRemovalByUndo() {
|
||||
const {TYPE_FOLDER} = PlacesUtils.bookmarks;
|
||||
MigrationUtils.initializeUndoData();
|
||||
let title = "Some example bookmark";
|
||||
let url = "http://www.mymagicaluniqueurl.com";
|
||||
let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
|
||||
let {guid} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title: "Some folder", parentGuid, type: TYPE_FOLDER
|
||||
});
|
||||
let folderGuid = guid;
|
||||
let itemsToRemove = [];
|
||||
({guid} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER
|
||||
}));
|
||||
let innerFolderGuid = guid;
|
||||
itemsToRemove.push(innerFolderGuid);
|
||||
|
||||
({guid} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER
|
||||
}));
|
||||
itemsToRemove.push(guid);
|
||||
|
||||
({guid} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid
|
||||
}));
|
||||
itemsToRemove.push(guid);
|
||||
|
||||
({guid} = yield MigrationUtils.insertBookmarkWrapper({
|
||||
title, url, parentGuid: folderGuid
|
||||
}));
|
||||
itemsToRemove.push(guid);
|
||||
|
||||
for (let toBeRemovedGuid of itemsToRemove) {
|
||||
let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(toBeRemovedGuid);
|
||||
Assert.ok(dbResultForGuid, "Should be able to find items that will be removed.");
|
||||
}
|
||||
let bookmarkUndoState = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
|
||||
// Now insert a separate item into this folder, not related to the migration.
|
||||
let newItem = yield PlacesUtils.bookmarks.insert(
|
||||
{title: "Not imported", parentGuid: folderGuid, url: "http://www.example.com"}
|
||||
);
|
||||
|
||||
yield AutoMigrate._removeUnchangedBookmarks(bookmarkUndoState);
|
||||
Assert.ok(true, "Successfully removed imported items.");
|
||||
|
||||
let itemFromDB = yield PlacesUtils.bookmarks.fetch(newItem.guid);
|
||||
Assert.ok(itemFromDB, "Item we inserted outside of migration is still there.");
|
||||
itemFromDB = yield PlacesUtils.bookmarks.fetch(folderGuid);
|
||||
Assert.ok(itemFromDB, "Folder we inserted in migration is still there because of new kids.");
|
||||
for (let removedGuid of itemsToRemove) {
|
||||
let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(removedGuid);
|
||||
let dbgStr = dbResultForGuid && dbResultForGuid.title;
|
||||
Assert.equal(null, dbResultForGuid, "Should not be able to find items that should have been removed, but found " + dbgStr);
|
||||
}
|
||||
yield PlacesUtils.bookmarks.eraseEverything();
|
||||
});
|
||||
|
||||
add_task(function* checkUndoLoginsState() {
|
||||
MigrationUtils.initializeUndoData();
|
||||
MigrationUtils.insertLoginWrapper({
|
||||
username: "foo",
|
||||
password: "bar",
|
||||
hostname: "https://example.com",
|
||||
formSubmitURL: "https://example.com/",
|
||||
timeCreated: new Date(),
|
||||
});
|
||||
let storedLogins = Services.logins.findLogins({}, "https://example.com", "", "");
|
||||
let storedLogin = storedLogins[0];
|
||||
storedLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
let {guid, timePasswordChanged} = storedLogin;
|
||||
let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
|
||||
Assert.deepEqual([{guid, timePasswordChanged}], undoLoginData);
|
||||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
add_task(function* testLoginsRemovalByUndo() {
|
||||
MigrationUtils.initializeUndoData();
|
||||
MigrationUtils.insertLoginWrapper({
|
||||
username: "foo",
|
||||
password: "bar",
|
||||
hostname: "https://example.com",
|
||||
formSubmitURL: "https://example.com/",
|
||||
timeCreated: new Date(),
|
||||
});
|
||||
MigrationUtils.insertLoginWrapper({
|
||||
username: "foo",
|
||||
password: "bar",
|
||||
hostname: "https://example.org",
|
||||
formSubmitURL: "https://example.org/",
|
||||
timeCreated: new Date(new Date().getTime() - 10000),
|
||||
});
|
||||
// This should update the existing login
|
||||
LoginHelper.maybeImportLogin({
|
||||
username: "foo",
|
||||
password: "bazzy",
|
||||
hostname: "https://example.org",
|
||||
formSubmitURL: "https://example.org/",
|
||||
timePasswordChanged: new Date(),
|
||||
});
|
||||
Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
|
||||
"Should be only 1 login for example.org (that was updated)");
|
||||
let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
|
||||
|
||||
yield AutoMigrate._removeUnchangedLogins(undoLoginData);
|
||||
Assert.equal(0, LoginHelper.searchLoginsWithObject({hostname: "https://example.com", formSubmitURL: "https://example.com/"}).length,
|
||||
"unchanged example.com entry should have been removed.");
|
||||
Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
|
||||
"changed example.org entry should have persisted.");
|
||||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
add_task(function* checkUndoVisitsState() {
|
||||
MigrationUtils.initializeUndoData();
|
||||
yield MigrationUtils.insertVisitsWrapper([{
|
||||
uri: NetUtil.newURI("http://www.example.com/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2015-07-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}, {
|
||||
visitDate: new Date("2015-09-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}, {
|
||||
visitDate: new Date("2015-08-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}, {
|
||||
uri: NetUtil.newURI("http://www.example.org/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2016-04-03").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}, {
|
||||
visitDate: new Date("2015-08-03").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}, {
|
||||
uri: NetUtil.newURI("http://www.example.com/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2015-10-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}]);
|
||||
let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
|
||||
Assert.deepEqual(Array.from(undoVisitData.map(v => v.url)).sort(),
|
||||
["http://www.example.com/", "http://www.example.org/"]);
|
||||
Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.com/"), {
|
||||
url: "http://www.example.com/",
|
||||
visitCount: 4,
|
||||
first: new Date("2015-07-10").getTime() * 1000,
|
||||
last: new Date("2015-10-10").getTime() * 1000,
|
||||
});
|
||||
Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.org/"), {
|
||||
url: "http://www.example.org/",
|
||||
visitCount: 2,
|
||||
first: new Date("2015-08-03").getTime() * 1000,
|
||||
last: new Date("2016-04-03").getTime() * 1000,
|
||||
});
|
||||
|
||||
yield PlacesTestUtils.clearHistory();
|
||||
});
|
||||
|
||||
add_task(function* checkUndoVisitsState() {
|
||||
MigrationUtils.initializeUndoData();
|
||||
yield MigrationUtils.insertVisitsWrapper([{
|
||||
uri: NetUtil.newURI("http://www.example.com/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2015-07-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}, {
|
||||
visitDate: new Date("2015-09-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}, {
|
||||
visitDate: new Date("2015-08-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}, {
|
||||
uri: NetUtil.newURI("http://www.example.org/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2016-04-03").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}, {
|
||||
visitDate: new Date("2015-08-03").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}, {
|
||||
uri: NetUtil.newURI("http://www.example.com/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2015-10-10").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}, {
|
||||
uri: NetUtil.newURI("http://www.mozilla.org/"),
|
||||
title: "Example",
|
||||
visits: [{
|
||||
visitDate: new Date("2015-01-01").getTime() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
}]);
|
||||
|
||||
// We have to wait until frecency updates have been handled in order
|
||||
// to accurately determine whether we're doing the right thing.
|
||||
let frecencyUpdatesHandled = new Promise(resolve => {
|
||||
PlacesUtils.history.addObserver({
|
||||
onFrecencyChanged(aURI) {
|
||||
if (aURI.spec == "http://www.unrelated.org/") {
|
||||
PlacesUtils.history.removeObserver(this);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
});
|
||||
yield PlacesUtils.history.insertMany([{
|
||||
url: "http://www.example.com/",
|
||||
title: "Example",
|
||||
visits: [{
|
||||
date: new Date("2015-08-16"),
|
||||
}],
|
||||
}, {
|
||||
url: "http://www.example.org/",
|
||||
title: "Example",
|
||||
visits: [{
|
||||
date: new Date("2016-01-03"),
|
||||
}, {
|
||||
date: new Date("2015-05-03"),
|
||||
}],
|
||||
}, {
|
||||
url: "http://www.unrelated.org/",
|
||||
title: "Unrelated",
|
||||
visits: [{
|
||||
date: new Date("2015-09-01"),
|
||||
}],
|
||||
}]);
|
||||
yield frecencyUpdatesHandled;
|
||||
let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
|
||||
|
||||
let frecencyChangesExpected = new Map([
|
||||
["http://www.example.com/", PromiseUtils.defer()],
|
||||
["http://www.example.org/", PromiseUtils.defer()]
|
||||
]);
|
||||
let uriDeletedExpected = new Map([
|
||||
["http://www.mozilla.org/", PromiseUtils.defer()],
|
||||
]);
|
||||
let wrongMethodDeferred = PromiseUtils.defer();
|
||||
let observer = {
|
||||
onBeginUpdateBatch: function() {},
|
||||
onEndUpdateBatch: function() {},
|
||||
onVisit: function(uri) {
|
||||
wrongMethodDeferred.reject(new Error("Unexpected call to onVisit " + uri.spec));
|
||||
},
|
||||
onTitleChanged: function(uri) {
|
||||
wrongMethodDeferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
|
||||
},
|
||||
onClearHistory: function() {
|
||||
wrongMethodDeferred.reject("Unexpected call to onClearHistory");
|
||||
},
|
||||
onPageChanged: function(uri) {
|
||||
wrongMethodDeferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
|
||||
},
|
||||
onFrecencyChanged: function(aURI) {
|
||||
do_print("frecency change");
|
||||
Assert.ok(frecencyChangesExpected.has(aURI.spec),
|
||||
"Should be expecting frecency change for " + aURI.spec);
|
||||
frecencyChangesExpected.get(aURI.spec).resolve();
|
||||
},
|
||||
onManyFrecenciesChanged: function() {
|
||||
do_print("Many frecencies changed");
|
||||
wrongMethodDeferred.reject(new Error("This test can't deal with onManyFrecenciesChanged to be called"));
|
||||
},
|
||||
onDeleteURI: function(aURI) {
|
||||
do_print("delete uri");
|
||||
Assert.ok(uriDeletedExpected.has(aURI.spec),
|
||||
"Should be expecting uri deletion for " + aURI.spec);
|
||||
uriDeletedExpected.get(aURI.spec).resolve();
|
||||
},
|
||||
};
|
||||
PlacesUtils.history.addObserver(observer, false);
|
||||
|
||||
yield AutoMigrate._removeSomeVisits(undoVisitData);
|
||||
PlacesUtils.history.removeObserver(observer);
|
||||
yield Promise.all(uriDeletedExpected.values());
|
||||
yield Promise.all(frecencyChangesExpected.values());
|
||||
|
||||
Assert.equal(yield visitsForURL("http://www.example.com/"), 1,
|
||||
"1 example.com visit (out of 5) should have persisted despite being within the range, due to limiting");
|
||||
Assert.equal(yield visitsForURL("http://www.mozilla.org/"), 0,
|
||||
"0 mozilla.org visits should have persisted (out of 1).");
|
||||
Assert.equal(yield visitsForURL("http://www.example.org/"), 2,
|
||||
"2 example.org visits should have persisted (out of 4).");
|
||||
Assert.equal(yield visitsForURL("http://www.unrelated.org/"), 1,
|
||||
"1 unrelated.org visits should have persisted as it's not involved in the import.");
|
||||
yield PlacesTestUtils.clearHistory();
|
||||
});
|
||||
|
||||
add_task(function* checkHistoryRemovalCompletion() {
|
||||
AutoMigrate._errorMap = {bookmarks: 0, visits: 0, logins: 0};
|
||||
yield AutoMigrate._removeSomeVisits([{url: "http://www.example.com/", limit: -1}]);
|
||||
ok(true, "Removing visits should complete even if removing some visits failed.");
|
||||
Assert.equal(AutoMigrate._errorMap.visits, 1, "Should have logged the error for visits.");
|
||||
|
||||
// Unfortunately there's not a reliable way to make removing bookmarks be
|
||||
// unhappy unless the DB is messed up (e.g. contains children but has
|
||||
// parents removed already).
|
||||
yield AutoMigrate._removeUnchangedBookmarks([
|
||||
{guid: PlacesUtils.bookmarks, lastModified: new Date(0), parentGuid: 0},
|
||||
{guid: "gobbledygook", lastModified: new Date(0), parentGuid: 0},
|
||||
]);
|
||||
ok(true, "Removing bookmarks should complete even if some items are gone or bogus.");
|
||||
Assert.equal(AutoMigrate._errorMap.bookmarks, 0,
|
||||
"Should have ignored removing non-existing (or builtin) bookmark.");
|
||||
|
||||
|
||||
yield AutoMigrate._removeUnchangedLogins([
|
||||
{guid: "gobbledygook", timePasswordChanged: new Date(0)},
|
||||
]);
|
||||
ok(true, "Removing logins should complete even if logins don't exist.");
|
||||
Assert.equal(AutoMigrate._errorMap.logins, 0,
|
||||
"Should have ignored removing non-existing logins.");
|
||||
});
|
||||
288
browser/components/migration/tests/unit/test_fx_telemetry.js
Normal file
288
browser/components/migration/tests/unit/test_fx_telemetry.js
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/* globals do_get_tempdir */
|
||||
|
||||
"use strict";
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function readFile(file) {
|
||||
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
|
||||
.createInstance(Ci.nsIScriptableInputStream);
|
||||
sis.init(stream);
|
||||
let contents = sis.read(file.fileSize);
|
||||
sis.close();
|
||||
return contents;
|
||||
}
|
||||
|
||||
function checkDirectoryContains(dir, files) {
|
||||
print("checking " + dir.path + " - should contain " + Object.keys(files));
|
||||
let seen = new Set();
|
||||
let enumerator = dir.directoryEntries;
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
|
||||
print("found file: " + file.path);
|
||||
Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't");
|
||||
|
||||
let expectedContents = files[file.leafName];
|
||||
if (typeof expectedContents != "string") {
|
||||
// it's a subdir - recurse!
|
||||
Assert.ok(file.isDirectory(), "should be a subdir");
|
||||
let newDir = dir.clone();
|
||||
newDir.append(file.leafName);
|
||||
checkDirectoryContains(newDir, expectedContents);
|
||||
} else {
|
||||
Assert.ok(!file.isDirectory(), "should be a regular file");
|
||||
let contents = readFile(file);
|
||||
Assert.equal(contents, expectedContents);
|
||||
}
|
||||
seen.add(file.leafName);
|
||||
}
|
||||
let missing = [];
|
||||
for (let x in files) {
|
||||
if (!seen.has(x)) {
|
||||
missing.push(x);
|
||||
}
|
||||
}
|
||||
Assert.deepEqual(missing, [], "no missing files in " + dir.path);
|
||||
}
|
||||
|
||||
function getTestDirs() {
|
||||
// we make a directory structure in a temp dir which mirrors what we are
|
||||
// testing.
|
||||
let tempDir = do_get_tempdir();
|
||||
let srcDir = tempDir.clone();
|
||||
srcDir.append("test_source_dir");
|
||||
srcDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
|
||||
let targetDir = tempDir.clone();
|
||||
targetDir.append("test_target_dir");
|
||||
targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
|
||||
// no need to cleanup these dirs - the xpcshell harness will do it for us.
|
||||
return [srcDir, targetDir];
|
||||
}
|
||||
|
||||
function writeToFile(dir, leafName, contents) {
|
||||
let file = dir.clone();
|
||||
file.append(leafName);
|
||||
|
||||
let outputStream = FileUtils.openFileOutputStream(file);
|
||||
outputStream.write(contents, contents.length);
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
function createSubDir(dir, subDirName) {
|
||||
let subDir = dir.clone();
|
||||
subDir.append(subDirName);
|
||||
subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
return subDir;
|
||||
}
|
||||
|
||||
function promiseMigrator(name, srcDir, targetDir) {
|
||||
let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=firefox"]
|
||||
.createInstance(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
let migrators = migrator._getResourcesInternal(srcDir, targetDir);
|
||||
for (let m of migrators) {
|
||||
if (m.name == name) {
|
||||
return new Promise(resolve => m.migrate(resolve));
|
||||
}
|
||||
}
|
||||
throw new Error("failed to find the " + name + " migrator");
|
||||
}
|
||||
|
||||
function promiseTelemetryMigrator(srcDir, targetDir) {
|
||||
return promiseMigrator("telemetry", srcDir, targetDir);
|
||||
}
|
||||
|
||||
add_task(function* test_empty() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true with empty directories");
|
||||
// check both are empty
|
||||
checkDirectoryContains(srcDir, {});
|
||||
checkDirectoryContains(targetDir, {});
|
||||
});
|
||||
|
||||
add_task(function* test_migrate_files() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Set up datareporting files, some to copy, some not.
|
||||
let stateContent = JSON.stringify({
|
||||
clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c",
|
||||
});
|
||||
let sessionStateContent = "foobar 5432";
|
||||
let subDir = createSubDir(srcDir, "datareporting");
|
||||
writeToFile(subDir, "state.json", stateContent);
|
||||
writeToFile(subDir, "session-state.json", sessionStateContent);
|
||||
writeToFile(subDir, "other.file", "do not copy");
|
||||
|
||||
let archived = createSubDir(subDir, "archived");
|
||||
writeToFile(archived, "other.file", "do not copy");
|
||||
|
||||
// Set up FHR files, they should not be copied.
|
||||
writeToFile(srcDir, "healthreport.sqlite", "do not copy");
|
||||
writeToFile(srcDir, "healthreport.sqlite-wal", "do not copy");
|
||||
subDir = createSubDir(srcDir, "healthreport");
|
||||
writeToFile(subDir, "state.json", "do not copy");
|
||||
writeToFile(subDir, "other.file", "do not copy");
|
||||
|
||||
// Perform migration.
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true with important telemetry files copied");
|
||||
|
||||
checkDirectoryContains(targetDir, {
|
||||
"datareporting": {
|
||||
"state.json": stateContent,
|
||||
"session-state.json": sessionStateContent,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_fallback_fhr_state() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Test that we fall back to migrating FHR state if the datareporting
|
||||
// state file does not exist.
|
||||
let stateContent = JSON.stringify({
|
||||
clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c",
|
||||
});
|
||||
let subDir = createSubDir(srcDir, "healthreport");
|
||||
writeToFile(subDir, "state.json", stateContent);
|
||||
|
||||
// Perform migration.
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
|
||||
checkDirectoryContains(targetDir, {
|
||||
"healthreport": {
|
||||
"state.json": stateContent,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_datareporting_not_dir() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
writeToFile(srcDir, "datareporting", "I'm a file but should be a directory");
|
||||
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true even though the directory was a file");
|
||||
|
||||
checkDirectoryContains(targetDir, {});
|
||||
});
|
||||
|
||||
add_task(function* test_datareporting_empty() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Migrate with an empty 'datareporting' subdir.
|
||||
createSubDir(srcDir, "datareporting");
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
|
||||
// We should end up with no migrated files.
|
||||
checkDirectoryContains(targetDir, {
|
||||
"datareporting": {},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_healthreport_empty() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Migrate with no 'datareporting' and an empty 'healthreport' subdir.
|
||||
createSubDir(srcDir, "healthreport");
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
|
||||
// We should end up with no migrated files.
|
||||
checkDirectoryContains(targetDir, {});
|
||||
});
|
||||
|
||||
add_task(function* test_datareporting_many() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Create some datareporting files.
|
||||
let subDir = createSubDir(srcDir, "datareporting");
|
||||
let shouldBeCopied = "should be copied";
|
||||
writeToFile(subDir, "state.json", shouldBeCopied);
|
||||
writeToFile(subDir, "session-state.json", shouldBeCopied);
|
||||
writeToFile(subDir, "something.else", "should not");
|
||||
createSubDir(subDir, "emptyDir");
|
||||
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
|
||||
checkDirectoryContains(targetDir, {
|
||||
"datareporting" : {
|
||||
"state.json": shouldBeCopied,
|
||||
"session-state.json": shouldBeCopied,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_no_session_state() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Check that migration still works properly if we only have state.json.
|
||||
let subDir = createSubDir(srcDir, "datareporting");
|
||||
let stateContent = "abcd984";
|
||||
writeToFile(subDir, "state.json", stateContent);
|
||||
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
|
||||
checkDirectoryContains(targetDir, {
|
||||
"datareporting" : {
|
||||
"state.json": stateContent,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_no_state() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// Check that migration still works properly if we only have session-state.json.
|
||||
let subDir = createSubDir(srcDir, "datareporting");
|
||||
let sessionStateContent = "abcd512";
|
||||
writeToFile(subDir, "session-state.json", sessionStateContent);
|
||||
|
||||
let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
|
||||
checkDirectoryContains(targetDir, {
|
||||
"datareporting" : {
|
||||
"session-state.json": sessionStateContent,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_times_migration() {
|
||||
let [srcDir, targetDir] = getTestDirs();
|
||||
|
||||
// create a times.json in the source directory.
|
||||
let contents = JSON.stringify({created: 1234});
|
||||
writeToFile(srcDir, "times.json", contents);
|
||||
|
||||
let earliest = Date.now();
|
||||
let ok = yield promiseMigrator("times", srcDir, targetDir);
|
||||
Assert.ok(ok, "callback should have been true");
|
||||
let latest = Date.now();
|
||||
|
||||
let timesFile = targetDir.clone();
|
||||
timesFile.append("times.json");
|
||||
|
||||
let raw = readFile(timesFile);
|
||||
let times = JSON.parse(raw);
|
||||
Assert.ok(times.reset >= earliest && times.reset <= latest);
|
||||
// and it should have left the creation time alone.
|
||||
Assert.equal(times.created, 1234);
|
||||
});
|
||||
26
browser/components/migration/tests/unit/xpcshell.ini
Normal file
26
browser/components/migration/tests/unit/xpcshell.ini
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[DEFAULT]
|
||||
head = head_migration.js
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
skip-if = toolkit == 'android'
|
||||
support-files =
|
||||
Library/**
|
||||
AppData/**
|
||||
|
||||
[test_automigration.js]
|
||||
[test_Chrome_cookies.js]
|
||||
skip-if = os != "mac" # Relies on ULibDir
|
||||
[test_Chrome_passwords.js]
|
||||
skip-if = os != "win"
|
||||
[test_Edge_availability.js]
|
||||
[test_Edge_db_migration.js]
|
||||
skip-if = os != "win" || os_version == "5.1" || os_version == "5.2" # Relies on post-XP bits of ESEDB
|
||||
[test_fx_telemetry.js]
|
||||
[test_IE_bookmarks.js]
|
||||
skip-if = os != "win"
|
||||
[test_IE_cookies.js]
|
||||
skip-if = os != "win"
|
||||
[test_IE7_passwords.js]
|
||||
skip-if = os != "win"
|
||||
[test_Safari_bookmarks.js]
|
||||
skip-if = os != "mac"
|
||||
Loading…
Add table
Add a link
Reference in a new issue