diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index 3cb7c4eddf47..857652c39a2c 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -599,6 +599,10 @@ toolbarbutton.bookmark-item {
#personal-bookmarks[cui-areatype="toolbar"] > #bookmarks-toolbar-placeholder > .toolbarbutton-icon {
image-rendering: -moz-crisp-edges;
}
+ /* Synced Tabs sidebar */
+ html|*.tabs-container html|*.item-tabs-list html|*.item-icon-container {
+ image-rendering: -moz-crisp-edges;
+ }
}
#editBMPanel_tagsSelector {
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 981a29651986..fd785de5d8a2 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6443,13 +6443,14 @@ function checkEmptyPageOrigin(browser = gBrowser.selectedBrowser,
}
// Not all principals have URIs...
if (contentPrincipal.URI) {
+ // A manually entered about:blank URI is slightly magical:
if (uri.spec == "about:blank" && contentPrincipal.isNullPrincipal) {
return true;
}
return contentPrincipal.URI.equals(uri);
}
// ... so for those that don't have them, enforce that the page has the
- // system principal (this matches e.g. on about:home).
+ // system principal (this matches e.g. on about:newtab).
let ssm = Services.scriptSecurityManager;
return ssm.isSystemPrincipal(contentPrincipal);
}
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
index af498a920052..e3b11da26bc8 100644
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -564,12 +564,18 @@ nsContextMenu.prototype = {
LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
},
- inspectNode: function CM_inspectNode() {
+ inspectNode: function() {
let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
- let tt = devtools.TargetFactory.forTab(gBrowser.selectedTab);
- return gDevTools.showToolbox(tt, "inspector").then(function(toolbox) {
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+
+ return gDevTools.showToolbox(target, "inspector").then(toolbox => {
let inspector = toolbox.getCurrentPanel();
+
+ // new-node-front tells us when the node has been selected, whether the
+ // browser is remote or not.
+ let onNewNode = inspector.selection.once("new-node-front");
+
if (this.isRemote) {
this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, {node: this.target});
inspector.walker.findInspectingNode().then(nodeFront => {
@@ -578,7 +584,13 @@ nsContextMenu.prototype = {
} else {
inspector.selection.setNode(this.target, "browser-context-menu");
}
- }.bind(this));
+
+ return onNewNode.then(() => {
+ // Now that the node has been selected, wait until the inspector is
+ // fully updated.
+ return inspector.once("inspector-updated");
+ });
+ });
},
// Set various context menu attributes based on the state of the world.
diff --git a/browser/base/content/sanitize.js b/browser/base/content/sanitize.js
index 2639489cd926..943ff31f4d9f 100644
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -111,7 +111,8 @@ Sanitizer.prototype = {
// Store the list of items to clear, in case we are killed before we
// get a chance to complete.
- Preferences.set(Sanitizer.PREF_SANITIZE_IN_PROGRESS, JSON.stringify(itemsToClear));
+ Preferences.set(Sanitizer.PREF_SANITIZE_IN_PROGRESS,
+ JSON.stringify(itemsToClear));
// Store the list of items to clear, for debugging/forensics purposes
for (let k of itemsToClear) {
@@ -677,10 +678,18 @@ Sanitizer.prototype = {
}
};
-// "Static" members
+// The preferences branch for the sanitizer.
Sanitizer.PREF_DOMAIN = "privacy.sanitize.";
+// Whether we should sanitize on shutdown.
Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown";
+// During a sanitization this is set to a json containing the array of items
+// being sanitized, then cleared once the sanitization is complete.
+// This allows to retry a sanitization on startup in case it was interrupted
+// by a crash.
Sanitizer.PREF_SANITIZE_IN_PROGRESS = "privacy.sanitize.sanitizeInProgress";
+// Whether the previous shutdown sanitization completed successfully.
+// Note that PREF_SANITIZE_IN_PROGRESS would be enough to detect an interrupted
+// sanitization, but this is still supported for backwards compatibility.
Sanitizer.PREF_SANITIZE_DID_SHUTDOWN = "privacy.sanitize.didShutdownSanitize";
// Time span constants corresponding to values of the privacy.sanitize.timeSpan
@@ -766,6 +775,15 @@ Sanitizer.sanitize = function(aParentWindow)
};
Sanitizer.onStartup = Task.async(function*() {
+ // Check if we were interrupted during the last shutdown sanitization.
+ let shutownSanitizationWasInterrupted =
+ Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) &&
+ !Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
+ // Regardless, reset the pref, since we want to check it at the next startup
+ // even if the browser exits abruptly.
+ Preferences.reset(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
+ Services.prefs.savePrefFile(null);
+
// Make sure that we are triggered during shutdown, at the right time,
// and only once.
let placesClient = Cc["@mozilla.org/browser/nav-history-service;1"]
@@ -786,18 +804,19 @@ Sanitizer.onStartup = Task.async(function*() {
}
placesClient.addBlocker("sanitize.js: Sanitize on shutdown", doSanitize);
- // Handle incomplete sanitizations
- if (Preferences.has(Sanitizer.PREF_SANITIZE_IN_PROGRESS)) {
- // Firefox crashed during sanitization.
+ // Check if Firefox crashed before completing a sanitization.
+ let lastInterruptedSanitization = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
+ if (lastInterruptedSanitization) {
let s = new Sanitizer();
- let json = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
- let itemsToClear = JSON.parse(json);
+ // If the json is invalid this will just throw and reject the Task.
+ let itemsToClear = JSON.parse(lastInterruptedSanitization);
yield s.sanitize(itemsToClear);
- }
- if (Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
- // Firefox crashed before having a chance to sanitize during shutdown.
- // (note that if Firefox crashed during shutdown sanitization, we
- // will hit both `if` so we will run a second double-sanitization).
+ } else if (shutownSanitizationWasInterrupted) {
+ // Ideally lastInterruptedSanitization should always be set when a
+ // sanitization is interrupted, but some add-ons or Firefox previous
+ // versions may not set the pref.
+ // In such a case, we can still detect an interrupted shutdown sanitization,
+ // and just redo it.
yield Sanitizer.onShutdown();
}
});
@@ -810,5 +829,8 @@ Sanitizer.onShutdown = Task.async(function*() {
let s = new Sanitizer();
s.prefDomain = "privacy.clearOnShutdown.";
yield s.sanitize();
+ // We didn't crash during shutdown sanitization, so annotate it to avoid
+ // sanitizing again on startup.
Preferences.set(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
+ Services.prefs.savePrefFile(null);
});
diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml
index 091a67bfe9c6..3224de12a1ef 100644
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -6243,9 +6243,11 @@
if (browser.audioMuted) {
browser.unmute();
this.removeAttribute("muted");
+ BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason);
} else {
browser.mute();
this.setAttribute("muted", "true");
+ BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason);
}
this.muteReason = aMuteReason || null;
tabContainer.tabbrowser._tabAttrModified(this, ["muted"]);
diff --git a/browser/components/feeds/WebContentConverter.js b/browser/components/feeds/WebContentConverter.js
index f8c7c36ba1cb..0c12583827c5 100644
--- a/browser/components/feeds/WebContentConverter.js
+++ b/browser/components/feeds/WebContentConverter.js
@@ -413,7 +413,7 @@ WebContentConverterRegistrar.prototype = {
let notificationValue = "Protocol Registration: " + aProtocol;
let addButton = {
label: this._getString("addProtocolHandlerAddButton"),
- accessKey: this._getString("addHandlerAddButtonAccesskey"),
+ accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"),
protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle },
callback(aNotification, aButtonInfo) {
diff --git a/browser/components/migration/ESEDBReader.jsm b/browser/components/migration/ESEDBReader.jsm
index 74dc8de7cc9f..b94e2754ee6b 100644
--- a/browser/components/migration/ESEDBReader.jsm
+++ b/browser/components/migration/ESEDBReader.jsm
@@ -103,6 +103,9 @@ 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) {
@@ -285,7 +288,10 @@ ESEDB.prototype = {
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);
diff --git a/browser/components/migration/EdgeProfileMigrator.js b/browser/components/migration/EdgeProfileMigrator.js
index c63cf6718713..b4eb8d5ffac4 100644
--- a/browser/components/migration/EdgeProfileMigrator.js
+++ b/browser/components/migration/EdgeProfileMigrator.js
@@ -52,15 +52,17 @@ XPCOMUtils.defineLazyGetter(this, "gEdgeDatabase", function() {
* @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) {
+function readTableFromEdgeDB(tableName, columns, filterFn, dbFile=gEdgeDatabase) {
let database;
let rows = [];
try {
- let logFile = gEdgeDatabase.parent;
+ let logFile = dbFile.parent;
logFile.append("LogFiles");
- database = ESEDBReader.openDB(gEdgeDatabase.parent, gEdgeDatabase, logFile);
+ database = ESEDBReader.openDB(dbFile.parent, dbFile, logFile);
if (typeof columns == "function") {
columns = columns(database);
@@ -74,7 +76,7 @@ function readTableFromEdgeDB(tableName, columns, filterFn) {
}
} catch (ex) {
Cu.reportError("Failed to extract items from table " + tableName + " in Edge database at " +
- gEdgeDatabase.path + " due to the following error: " + ex);
+ dbFile.path + " due to the following error: " + ex);
// Deliberately make this fail so we expose failure in the UI:
throw ex;
} finally {
@@ -221,33 +223,36 @@ EdgeReadingListMigrator.prototype = {
}),
};
-function EdgeBookmarksMigrator() {
+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) {
return this._exists;
}
- return this._exists = (!!gEdgeDatabase && this._checkTableExists());
+ return this._exists = (!!this.db && this._checkTableExists());
},
_checkTableExists() {
let database;
let rv;
try {
- let logFile = gEdgeDatabase.parent;
+ let logFile = this.db.parent;
logFile.append("LogFiles");
- database = ESEDBReader.openDB(gEdgeDatabase.parent, gEdgeDatabase, logFile);
+ database = ESEDBReader.openDB(this.db.parent, this.db, logFile);
rv = database.tableExists(this.TABLE_NAME);
} catch (ex) {
- Cu.reportError("Failed to check for table " + tableName + " in Edge database at " +
- gEdgeDatabase.path + " due to the following error: " + ex);
+ Cu.reportError("Failed to check for table " + this.TABLE_NAME + " in Edge database at " +
+ this.db.path + " due to the following error: " + ex);
return false;
} finally {
if (database) {
@@ -348,7 +353,7 @@ EdgeBookmarksMigrator.prototype = {
}
return true;
}
- let bookmarks = readTableFromEdgeDB(this.TABLE_NAME, columns, filterFn);
+ let bookmarks = readTableFromEdgeDB(this.TABLE_NAME, columns, filterFn, this.db);
return {bookmarks, folderMap};
},
@@ -388,10 +393,15 @@ EdgeBookmarksMigrator.prototype = {
}
function EdgeProfileMigrator() {
+ this.wrappedJSObject = this;
}
EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
+EdgeProfileMigrator.prototype.getESEMigratorForTesting = function(dbOverride) {
+ return new EdgeBookmarksMigrator(dbOverride);
+};
+
EdgeProfileMigrator.prototype.getResources = function() {
let bookmarksMigrator = new EdgeBookmarksMigrator();
if (!bookmarksMigrator.exists) {
diff --git a/browser/components/migration/tests/unit/test_Edge_db_migration.js b/browser/components/migration/tests/unit/test_Edge_db_migration.js
new file mode 100644
index 000000000000..9da368c6c733
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -0,0 +1,465 @@
+"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;
+ 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 "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();
+ }
+ let rv = 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 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.");
+
+ 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(dbItem => bookmark.title == dbItem.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");
+ }
+ }
+});
+
diff --git a/browser/components/migration/tests/unit/xpcshell.ini b/browser/components/migration/tests/unit/xpcshell.ini
index 050310fd39aa..a75dd2be5171 100644
--- a/browser/components/migration/tests/unit/xpcshell.ini
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -12,6 +12,8 @@ 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"
diff --git a/browser/components/preferences/in-content/sync.js b/browser/components/preferences/in-content/sync.js
index 0fab01450b4b..7c59cb3bf0f7 100644
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -271,9 +271,7 @@ var gSyncPane = {
gSyncPane.signIn();
return false;
});
- setEventListener("verifiedManage", "click",
- gSyncPane.manageFirefoxAccount);
- setEventListener("fxaUnlinkButton", "click", function () {
+ setEventListener("fxaUnlinkButton", "command", function () {
gSyncPane.unlinkFirefoxAccount(true);
});
setEventListener("verifyFxaAccount", "command",
@@ -550,13 +548,34 @@ var gSyncPane = {
this._openAboutAccounts("reauth");
},
- openChangeProfileImage: function() {
- fxAccounts.promiseAccountsChangeProfileURI(this._getEntryPoint(), "avatar")
- .then(url => {
+
+ clickOrSpaceOrEnterPressed: function(event) {
+ // Note: charCode is deprecated, but 'char' not yet implemented.
+ // Replace charCode with char when implemented, see Bug 680830
+ if ((event.type == "click" && event.button == 0) || // button 0 = 'main button', typically left click.
+ (event.type == "keypress" &&
+ (event.charCode == KeyEvent.DOM_VK_SPACE || event.keyCode == KeyEvent.DOM_VK_RETURN))) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ openChangeProfileImage: function(event) {
+ if (this.clickOrSpaceOrEnterPressed(event)) {
+ fxAccounts.promiseAccountsChangeProfileURI(this._getEntryPoint(), "avatar")
+ .then(url => {
this.openContentInBrowser(url, {
replaceQueryString: true
});
});
+ }
+ },
+
+ openManageFirefoxAccount: function(event) {
+ if (this.clickOrSpaceOrEnterPressed(event)) {
+ this.manageFirefoxAccount();
+ }
},
manageFirefoxAccount: function() {
diff --git a/browser/components/preferences/in-content/sync.xul b/browser/components/preferences/in-content/sync.xul
index 4101cab87834..b54ddee5e021 100644
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -199,8 +199,8 @@