mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 06:11:37 +00:00
Merge mozilla-central and fx-team
This commit is contained in:
commit
736101e1e8
@ -486,7 +486,7 @@
|
||||
@BINPATH@/components/FormHistoryStartup.js
|
||||
@BINPATH@/components/nsInputListAutoComplete.js
|
||||
@BINPATH@/components/formautofill.manifest
|
||||
@BINPATH@/components/AutofillController.js
|
||||
@BINPATH@/components/FormAutofillContentService.js
|
||||
@BINPATH@/components/contentSecurityPolicy.manifest
|
||||
@BINPATH@/components/contentSecurityPolicy.js
|
||||
@BINPATH@/components/contentAreaDropListener.manifest
|
||||
|
@ -1607,8 +1607,6 @@
|
||||
// allows the TabLabelModified event to be properly dispatched.
|
||||
if (!aURI || isBlankPageURL(aURI)) {
|
||||
t.label = this.mStringBundle.getString("tabs.emptyTabTitle");
|
||||
} else {
|
||||
t.label = aURI;
|
||||
}
|
||||
|
||||
this.tabContainer.updateVisibility();
|
||||
|
@ -372,16 +372,10 @@ function test18a() {
|
||||
var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
|
||||
ok(updateLink.style.visibility != "hidden", "Test 18a, Plugin should have an update link");
|
||||
|
||||
var tabOpenListener = new TabOpenListener(Services.urlFormatter.formatURLPref("plugins.update.url"), false, false);
|
||||
tabOpenListener.handleEvent = function(event) {
|
||||
if (event.type == "TabOpen") {
|
||||
gBrowser.tabContainer.removeEventListener("TabOpen", this, false);
|
||||
this.tab = event.originalTarget;
|
||||
is(event.target.label, this.url, "Test 18a, Update link should open up the plugin check page");
|
||||
gBrowser.removeTab(this.tab);
|
||||
test18b();
|
||||
}
|
||||
};
|
||||
var pluginUpdateURL = Services.urlFormatter.formatURLPref("plugins.update.url");
|
||||
var tabOpenListener = new TabOpenListener(pluginUpdateURL, function(tab) {
|
||||
gBrowser.removeTab(tab);
|
||||
}, test18b);
|
||||
EventUtils.synthesizeMouseAtCenter(updateLink, {}, gTestBrowser.contentWindow);
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@
|
||||
#ifdef USE_WIN_TITLE_STYLE
|
||||
title="&prefWindow.titleWin;">
|
||||
#else
|
||||
title="&prefWindow.titleGNOME;">
|
||||
title="&prefWindow.title;">
|
||||
#endif
|
||||
|
||||
<html:link rel="shortcut icon"
|
||||
|
@ -9,6 +9,10 @@ this.EXPORTED_SYMBOLS = ["PrivacyLevel"];
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
|
||||
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
|
||||
|
||||
const PREF_NORMAL = "browser.sessionstore.privacy_level";
|
||||
const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred";
|
||||
@ -24,14 +28,6 @@ const PRIVACY_ENCRYPTED = 1;
|
||||
// Collect no data.
|
||||
const PRIVACY_FULL = 2;
|
||||
|
||||
/**
|
||||
* Returns whether we will resume the session automatically on next startup.
|
||||
*/
|
||||
function willResumeAutomatically() {
|
||||
return Services.prefs.getIntPref("browser.startup.page") == 3 ||
|
||||
Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the current privacy level as set by the user.
|
||||
*
|
||||
@ -44,7 +40,7 @@ function getCurrentLevel(isPinned) {
|
||||
|
||||
// If we're in the process of quitting and we're not autoresuming the session
|
||||
// then we will use the deferred privacy level for non-pinned tabs.
|
||||
if (!isPinned && Services.startup.shuttingDown && !willResumeAutomatically()) {
|
||||
if (!isPinned && Services.startup.shuttingDown && !gSessionStartup.isAutomaticRestoreEnabled()) {
|
||||
pref = PREF_DEFERRED;
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
|
||||
"@mozilla.org/base/telemetry;1", "nsITelemetry");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "sessionStartup",
|
||||
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionWorker",
|
||||
"resource:///modules/sessionstore/SessionWorker.jsm");
|
||||
|
||||
const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
|
||||
|
||||
this.SessionFile = {
|
||||
/**
|
||||
@ -75,43 +81,117 @@ this.SessionFile = {
|
||||
gatherTelemetry: function(aData) {
|
||||
return SessionFileInternal.gatherTelemetry(aData);
|
||||
},
|
||||
/**
|
||||
* Create a backup copy, asynchronously.
|
||||
* This is designed to perform backup on upgrade.
|
||||
*/
|
||||
createBackupCopy: function (ext) {
|
||||
return SessionFileInternal.createBackupCopy(ext);
|
||||
},
|
||||
/**
|
||||
* Remove a backup copy, asynchronously.
|
||||
* This is designed to clean up a backup on upgrade.
|
||||
*/
|
||||
removeBackupCopy: function (ext) {
|
||||
return SessionFileInternal.removeBackupCopy(ext);
|
||||
},
|
||||
/**
|
||||
* Wipe the contents of the session file, asynchronously.
|
||||
*/
|
||||
wipe: function () {
|
||||
SessionFileInternal.wipe();
|
||||
return SessionFileInternal.wipe();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the paths to the files used to store, backup, etc.
|
||||
* the state of the file.
|
||||
*/
|
||||
get Paths() {
|
||||
return SessionFileInternal.Paths;
|
||||
}
|
||||
};
|
||||
|
||||
Object.freeze(SessionFile);
|
||||
|
||||
/**
|
||||
* Utilities for dealing with promises and Task.jsm
|
||||
*/
|
||||
let SessionFileInternal = {
|
||||
/**
|
||||
* The path to sessionstore.js
|
||||
*/
|
||||
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
|
||||
let Path = OS.Path;
|
||||
let profileDir = OS.Constants.Path.profileDir;
|
||||
|
||||
/**
|
||||
* The path to sessionstore.bak
|
||||
*/
|
||||
backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
|
||||
let SessionFileInternal = {
|
||||
Paths: Object.freeze({
|
||||
// The path to the latest version of sessionstore written during a clean
|
||||
// shutdown. After startup, it is renamed `cleanBackup`.
|
||||
clean: Path.join(profileDir, "sessionstore.js"),
|
||||
|
||||
// The path at which we store the previous version of `clean`. Updated
|
||||
// whenever we successfully load from `clean`.
|
||||
cleanBackup: Path.join(profileDir, "sessionstore-backups", "previous.js"),
|
||||
|
||||
// The directory containing all sessionstore backups.
|
||||
backups: Path.join(profileDir, "sessionstore-backups"),
|
||||
|
||||
// The path to the latest version of the sessionstore written
|
||||
// during runtime. Generally, this file contains more
|
||||
// privacy-sensitive information than |clean|, and this file is
|
||||
// therefore removed during clean shutdown. This file is designed to protect
|
||||
// against crashes / sudden shutdown.
|
||||
recovery: Path.join(profileDir, "sessionstore-backups", "recovery.js"),
|
||||
|
||||
// The path to the previous version of the sessionstore written
|
||||
// during runtime (e.g. 15 seconds before recovery). In case of a
|
||||
// clean shutdown, this file is removed. Generally, this file
|
||||
// contains more privacy-sensitive information than |clean|, and
|
||||
// this file is therefore removed during clean shutdown. This
|
||||
// file is designed to protect against crashes that are nasty
|
||||
// enough to corrupt |recovery|.
|
||||
recoveryBackup: Path.join(profileDir, "sessionstore-backups", "recovery.bak"),
|
||||
|
||||
// The path to a backup created during an upgrade of Firefox.
|
||||
// Having this backup protects the user essentially from bugs in
|
||||
// Firefox or add-ons, especially for users of Nightly. This file
|
||||
// does not contain any information more sensitive than |clean|.
|
||||
upgradeBackupPrefix: Path.join(profileDir, "sessionstore-backups", "upgrade.js-"),
|
||||
|
||||
// The path to the backup of the version of the session store used
|
||||
// during the latest upgrade of Firefox. During load/recovery,
|
||||
// this file should be used if both |path|, |backupPath| and
|
||||
// |latestStartPath| are absent/incorrect. May be "" if no
|
||||
// upgrade backup has ever been performed. This file does not
|
||||
// contain any information more sensitive than |clean|.
|
||||
get upgradeBackup() {
|
||||
let latestBackupID = SessionFileInternal.latestUpgradeBackupID;
|
||||
if (!latestBackupID) {
|
||||
return "";
|
||||
}
|
||||
return this.upgradeBackupPrefix + latestBackupID;
|
||||
},
|
||||
|
||||
// The path to a backup created during an upgrade of Firefox.
|
||||
// Having this backup protects the user essentially from bugs in
|
||||
// Firefox, especially for users of Nightly.
|
||||
get nextUpgradeBackup() {
|
||||
return this.upgradeBackupPrefix + Services.appinfo.platformBuildID;
|
||||
},
|
||||
|
||||
/**
|
||||
* The order in which to search for a valid sessionstore file.
|
||||
*/
|
||||
get loadOrder() {
|
||||
// If `clean` exists and has been written without corruption during
|
||||
// the latest shutdown, we need to use it.
|
||||
//
|
||||
// Otherwise, `recovery` and `recoveryBackup` represent the most
|
||||
// recent state of the session store.
|
||||
//
|
||||
// Finally, if nothing works, fall back to the last known state
|
||||
// that can be loaded (`cleanBackup`) or, if available, to the
|
||||
// backup performed during the latest upgrade.
|
||||
let order = ["clean",
|
||||
"recovery",
|
||||
"recoveryBackup",
|
||||
"cleanBackup"];
|
||||
if (SessionFileInternal.latestUpgradeBackupID) {
|
||||
// We have an upgradeBackup
|
||||
order.push("upgradeBackup");
|
||||
}
|
||||
return order;
|
||||
},
|
||||
}),
|
||||
|
||||
// The ID of the latest version of Gecko for which we have an upgrade backup
|
||||
// or |undefined| if no upgrade backup was ever written.
|
||||
get latestUpgradeBackupID() {
|
||||
try {
|
||||
return Services.prefs.getCharPref(PREF_UPGRADE_BACKUP);
|
||||
} catch (ex) {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The promise returned by the latest call to |write|.
|
||||
@ -125,32 +205,57 @@ let SessionFileInternal = {
|
||||
*/
|
||||
_isClosed: false,
|
||||
|
||||
read: function () {
|
||||
// We must initialize the worker during startup so it will be ready to
|
||||
// perform the final write. If shutdown happens soon after startup and
|
||||
// the worker has not started yet we may not write.
|
||||
// See Bug 964531.
|
||||
SessionWorker.post("init");
|
||||
|
||||
return Task.spawn(function*() {
|
||||
for (let filename of [this.path, this.backupPath]) {
|
||||
try {
|
||||
let startMs = Date.now();
|
||||
|
||||
let data = yield OS.File.read(filename, { encoding: "utf-8" });
|
||||
|
||||
Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
|
||||
.add(Date.now() - startMs);
|
||||
|
||||
return data;
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
// Ignore exceptions about non-existent files.
|
||||
read: Task.async(function* () {
|
||||
let result;
|
||||
// Attempt to load by order of priority from the various backups
|
||||
for (let key of this.Paths.loadOrder) {
|
||||
let corrupted = false;
|
||||
let exists = true;
|
||||
try {
|
||||
let path = this.Paths[key];
|
||||
let startMs = Date.now();
|
||||
let source = yield OS.File.read(path, { encoding: "utf-8" });
|
||||
let parsed = JSON.parse(source);
|
||||
result = {
|
||||
origin: key,
|
||||
source: source,
|
||||
parsed: parsed
|
||||
};
|
||||
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
|
||||
add(false);
|
||||
Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS").
|
||||
add(Date.now() - startMs);
|
||||
break;
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
exists = false;
|
||||
} catch (ex if ex instanceof SyntaxError) {
|
||||
// File is corrupted, try next file
|
||||
corrupted = true;
|
||||
} finally {
|
||||
if (exists) {
|
||||
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
|
||||
add(corrupted);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
// If everything fails, start with an empty session.
|
||||
result = {
|
||||
origin: "empty",
|
||||
source: "",
|
||||
parsed: null
|
||||
};
|
||||
}
|
||||
|
||||
return "";
|
||||
}.bind(this));
|
||||
},
|
||||
// Initialize the worker to let it handle backups and also
|
||||
// as a workaround for bug 964531.
|
||||
SessionWorker.post("init", [
|
||||
result.origin,
|
||||
this.Paths,
|
||||
]);
|
||||
|
||||
return result;
|
||||
}),
|
||||
|
||||
gatherTelemetry: function(aStateString) {
|
||||
return Task.spawn(function() {
|
||||
@ -173,20 +278,32 @@ let SessionFileInternal = {
|
||||
isFinalWrite = this._isClosed = true;
|
||||
}
|
||||
|
||||
return this._latestWrite = Task.spawn(function task() {
|
||||
return this._latestWrite = Task.spawn(function* task() {
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
|
||||
|
||||
try {
|
||||
let promise = SessionWorker.post("write", [aData]);
|
||||
let performShutdownCleanup = isFinalWrite &&
|
||||
!sessionStartup.isAutomaticRestoreEnabled();
|
||||
let options = {
|
||||
isFinalWrite: isFinalWrite,
|
||||
performShutdownCleanup: performShutdownCleanup
|
||||
};
|
||||
let promise = SessionWorker.post("write", [aData, options]);
|
||||
// At this point, we measure how long we stop the main thread
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
|
||||
|
||||
// Now wait for the result and record how long the write took
|
||||
let msg = yield promise;
|
||||
this._recordTelemetry(msg.telemetry);
|
||||
|
||||
if (msg.ok && msg.ok.upgradeBackup) {
|
||||
// We have just completed a backup-on-upgrade, store the information
|
||||
// in preferences.
|
||||
Services.prefs.setCharPref(PREF_UPGRADE_BACKUP, Services.appinfo.platformBuildID);
|
||||
}
|
||||
} catch (ex) {
|
||||
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
|
||||
console.error("Could not write session state file ", this.path, ex);
|
||||
console.error("Could not write session state file ", ex, ex.stack);
|
||||
}
|
||||
|
||||
if (isFinalWrite) {
|
||||
@ -195,16 +312,8 @@ let SessionFileInternal = {
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
createBackupCopy: function (ext) {
|
||||
return SessionWorker.post("createBackupCopy", [ext]);
|
||||
},
|
||||
|
||||
removeBackupCopy: function (ext) {
|
||||
return SessionWorker.post("removeBackupCopy", [ext]);
|
||||
},
|
||||
|
||||
wipe: function () {
|
||||
SessionWorker.post("wipe");
|
||||
return SessionWorker.post("wipe");
|
||||
},
|
||||
|
||||
_recordTelemetry: function(telemetry) {
|
||||
@ -224,31 +333,6 @@ let SessionFileInternal = {
|
||||
}
|
||||
};
|
||||
|
||||
// Interface to a dedicated thread handling I/O
|
||||
let SessionWorker = (function () {
|
||||
let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
|
||||
OS.Shared.LOG.bind("SessionWorker"));
|
||||
return {
|
||||
post: function post(...args) {
|
||||
let promise = worker.post.apply(worker, args);
|
||||
return promise.then(
|
||||
null,
|
||||
function onError(error) {
|
||||
// Decode any serialized error
|
||||
if (error instanceof PromiseWorker.WorkerError) {
|
||||
throw OS.File.Error.fromMsg(error.data);
|
||||
}
|
||||
// Extract something meaningful from ErrorEvent
|
||||
if (error instanceof ErrorEvent) {
|
||||
throw new Error(error.message, error.filename, error.lineno);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// Ensure that we can write sessionstore.js cleanly before the profile
|
||||
// becomes unaccessible.
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
|
@ -464,41 +464,10 @@ let SessionStoreInternal = {
|
||||
this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
|
||||
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
|
||||
|
||||
this._performUpgradeBackup();
|
||||
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
|
||||
return state;
|
||||
},
|
||||
|
||||
/**
|
||||
* If this is the first time we launc this build of Firefox,
|
||||
* backup sessionstore.js.
|
||||
*/
|
||||
_performUpgradeBackup: function ssi_performUpgradeBackup() {
|
||||
// Perform upgrade backup, if necessary
|
||||
const PREF_UPGRADE = "sessionstore.upgradeBackup.latestBuildID";
|
||||
|
||||
let buildID = Services.appinfo.platformBuildID;
|
||||
let latestBackup = this._prefBranch.getCharPref(PREF_UPGRADE);
|
||||
if (latestBackup == buildID) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Task.spawn(function task() {
|
||||
try {
|
||||
// Perform background backup
|
||||
yield SessionFile.createBackupCopy("-" + buildID);
|
||||
|
||||
this._prefBranch.setCharPref(PREF_UPGRADE, buildID);
|
||||
|
||||
// In case of success, remove previous backup.
|
||||
yield SessionFile.removeBackupCopy("-" + latestBackup);
|
||||
} catch (ex) {
|
||||
debug("Could not perform upgrade backup " + ex);
|
||||
debug(ex.stack);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_initPrefs : function() {
|
||||
this._prefBranch = Services.prefs.getBranch("browser.");
|
||||
|
||||
|
@ -53,6 +53,39 @@ self.onmessage = function (msg) {
|
||||
});
|
||||
};
|
||||
|
||||
// The various possible states
|
||||
|
||||
/**
|
||||
* We just started (we haven't written anything to disk yet) from
|
||||
* `Paths.clean`. The backup directory may not exist.
|
||||
*/
|
||||
const STATE_CLEAN = "clean";
|
||||
/**
|
||||
* We know that `Paths.recovery` is good, either because we just read
|
||||
* it (we haven't written anything to disk yet) or because have
|
||||
* already written once to `Paths.recovery` during this session.
|
||||
* `Paths.clean` is absent or invalid. The backup directory exists.
|
||||
*/
|
||||
const STATE_RECOVERY = "recovery";
|
||||
/**
|
||||
* We just started from `Paths.recoverBackupy` (we haven't written
|
||||
* anything to disk yet). Both `Paths.clean` and `Paths.recovery` are
|
||||
* absent or invalid. The backup directory exists.
|
||||
*/
|
||||
const STATE_RECOVERY_BACKUP = "recoveryBackup";
|
||||
/**
|
||||
* We just started from `Paths.upgradeBackup` (we haven't written
|
||||
* anything to disk yet). Both `Paths.clean`, `Paths.recovery` and
|
||||
* `Paths.recoveryBackup` are absent or invalid. The backup directory
|
||||
* exists.
|
||||
*/
|
||||
const STATE_UPGRADE_BACKUP = "upgradeBackup";
|
||||
/**
|
||||
* We just started without a valid session store file (we haven't
|
||||
* written anything to disk yet). The backup directory may not exist.
|
||||
*/
|
||||
const STATE_EMPTY = "empty";
|
||||
|
||||
let Agent = {
|
||||
// Boolean that tells whether we already made a
|
||||
// call to write(). We will only attempt to move
|
||||
@ -60,49 +93,154 @@ let Agent = {
|
||||
// first write.
|
||||
hasWrittenState: false,
|
||||
|
||||
// The path to sessionstore.js
|
||||
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
|
||||
|
||||
// The path to sessionstore.bak
|
||||
backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
|
||||
// Path to the files used by the SessionWorker
|
||||
Paths: null,
|
||||
|
||||
/**
|
||||
* NO-OP to start the worker.
|
||||
* The current state of the worker, as one of the following strings:
|
||||
* - "permanent", once the first write has been completed;
|
||||
* - "empty", before the first write has been completed,
|
||||
* if we have started without any sessionstore;
|
||||
* - one of "clean", "recovery", "recoveryBackup", "cleanBackup",
|
||||
* "upgradeBackup", before the first write has been completed, if
|
||||
* we have started by loading the corresponding file.
|
||||
*/
|
||||
init: function () {
|
||||
state: null,
|
||||
|
||||
/**
|
||||
* Initialize (or reinitialize) the worker
|
||||
*
|
||||
* @param {string} origin Which of sessionstore.js or its backups
|
||||
* was used. One of the `STATE_*` constants defined above.
|
||||
* @param {object} paths The paths at which to find the various files.
|
||||
*/
|
||||
init: function (origin, paths) {
|
||||
if (!(origin in paths || origin == STATE_EMPTY)) {
|
||||
throw new TypeError("Invalid origin: " + origin);
|
||||
}
|
||||
this.state = origin;
|
||||
this.Paths = paths;
|
||||
this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
|
||||
return {result: true};
|
||||
},
|
||||
|
||||
/**
|
||||
* Write the session to disk.
|
||||
* Write the session to disk, performing any necessary backup
|
||||
* along the way.
|
||||
*
|
||||
* @param {string} stateString The state to write to disk.
|
||||
* @param {object} options
|
||||
* - performShutdownCleanup If |true|, we should
|
||||
* perform shutdown-time cleanup to ensure that private data
|
||||
* is not left lying around;
|
||||
* - isFinalWrite If |true|, write to Paths.clean instead of
|
||||
* Paths.recovery
|
||||
*/
|
||||
write: function (stateString) {
|
||||
write: function (stateString, options = {}) {
|
||||
let exn;
|
||||
let telemetry = {};
|
||||
|
||||
if (!this.hasWrittenState) {
|
||||
try {
|
||||
let startMs = Date.now();
|
||||
File.move(this.path, this.backupPath);
|
||||
telemetry.FX_SESSION_RESTORE_BACKUP_FILE_MS = Date.now() - startMs;
|
||||
} catch (ex if isNoSuchFileEx(ex)) {
|
||||
// Ignore exceptions about non-existent files.
|
||||
} catch (ex) {
|
||||
// Throw the exception after we wrote the state to disk
|
||||
// so that the backup can't interfere with the actual write.
|
||||
exn = ex;
|
||||
let data = Encoder.encode(stateString);
|
||||
let startWriteMs, stopWriteMs;
|
||||
|
||||
try {
|
||||
|
||||
if (this.state == STATE_CLEAN || this.state == STATE_EMPTY) {
|
||||
// The backups directory may not exist yet. In all other cases,
|
||||
// we have either already read from or already written to this
|
||||
// directory, so we are satisfied that it exists.
|
||||
File.makeDir(this.Paths.backups);
|
||||
}
|
||||
|
||||
this.hasWrittenState = true;
|
||||
if (this.state == STATE_CLEAN) {
|
||||
// Move $Path.clean out of the way, to avoid any ambiguity as
|
||||
// to which file is more recent.
|
||||
File.move(this.Paths.clean, this.Paths.cleanBackup);
|
||||
}
|
||||
|
||||
startWriteMs = Date.now();
|
||||
|
||||
if (options.isFinalWrite) {
|
||||
// We are shutting down. At this stage, we know that
|
||||
// $Paths.clean is either absent or corrupted. If it was
|
||||
// originally present and valid, it has been moved to
|
||||
// $Paths.cleanBackup a long time ago. We can therefore write
|
||||
// with the guarantees that we erase no important data.
|
||||
File.writeAtomic(this.Paths.clean, data, {
|
||||
tmpPath: this.Paths.clean + ".tmp"
|
||||
});
|
||||
} else if (this.state == STATE_RECOVERY) {
|
||||
// At this stage, either $Paths.recovery was written >= 15
|
||||
// seconds ago during this session or we have just started
|
||||
// from $Paths.recovery left from the previous session. Either
|
||||
// way, $Paths.recovery is good. We can move $Path.backup to
|
||||
// $Path.recoveryBackup without erasing a good file with a bad
|
||||
// file.
|
||||
File.writeAtomic(this.Paths.recovery, data, {
|
||||
tmpPath: this.Paths.recovery + ".tmp",
|
||||
backupTo: this.Paths.recoveryBackup
|
||||
});
|
||||
} else {
|
||||
// In other cases, either $Path.recovery is not necessary, or
|
||||
// it doesn't exist or it has been corrupted. Regardless,
|
||||
// don't backup $Path.recovery.
|
||||
File.writeAtomic(this.Paths.recovery, data, {
|
||||
tmpPath: this.Paths.recovery + ".tmp"
|
||||
});
|
||||
}
|
||||
|
||||
stopWriteMs = Date.now();
|
||||
|
||||
} catch (ex) {
|
||||
// Don't throw immediately
|
||||
exn = exn || ex;
|
||||
}
|
||||
|
||||
let ret = this._write(stateString, telemetry);
|
||||
// If necessary, perform an upgrade backup
|
||||
let upgradeBackupComplete = false;
|
||||
if (this.upgradeBackupNeeded
|
||||
&& (this.state == STATE_CLEAN || this.state == STATE_UPGRADE_BACKUP)) {
|
||||
try {
|
||||
// If we loaded from `clean`, the file has since then been renamed to `cleanBackup`.
|
||||
let path = this.state == STATE_CLEAN ? this.Paths.cleanBackup : this.Paths.upgradeBackup;
|
||||
File.copy(path, this.Paths.nextUpgradeBackup);
|
||||
this.upgradeBackupNeeded = false;
|
||||
upgradeBackupComplete = true;
|
||||
} catch (ex) {
|
||||
// Don't throw immediately
|
||||
exn = exn || ex;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.performShutdownCleanup && !exn) {
|
||||
|
||||
// During shutdown, if auto-restore is disabled, we need to
|
||||
// remove possibly sensitive data that has been stored purely
|
||||
// for crash recovery. Note that this slightly decreases our
|
||||
// ability to recover from OS-level/hardware-level issue.
|
||||
|
||||
// If an exception was raised, we assume that we still need
|
||||
// these files.
|
||||
File.remove(this.Paths.recoveryBackup);
|
||||
File.remove(this.Paths.recovery);
|
||||
}
|
||||
|
||||
this.state = STATE_RECOVERY;
|
||||
|
||||
if (exn) {
|
||||
throw exn;
|
||||
}
|
||||
|
||||
return ret;
|
||||
return {
|
||||
result: {
|
||||
upgradeBackup: upgradeBackupComplete
|
||||
},
|
||||
telemetry: {
|
||||
FX_SESSION_RESTORE_WRITE_FILE_MS: stopWriteMs - startWriteMs,
|
||||
FX_SESSION_RESTORE_FILE_SIZE_BYTES: data.byteLength,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
@ -115,66 +253,79 @@ let Agent = {
|
||||
return Statistics.collect(stateString);
|
||||
},
|
||||
|
||||
/**
|
||||
* Write a stateString to disk
|
||||
*/
|
||||
_write: function (stateString, telemetry = {}) {
|
||||
let bytes = Encoder.encode(stateString);
|
||||
let startMs = Date.now();
|
||||
let result = File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
|
||||
telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startMs;
|
||||
telemetry.FX_SESSION_RESTORE_FILE_SIZE_BYTES = bytes.byteLength;
|
||||
return {result: result, telemetry: telemetry};
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a copy of sessionstore.js.
|
||||
*/
|
||||
createBackupCopy: function (ext) {
|
||||
try {
|
||||
return {result: File.copy(this.path, this.backupPath + ext)};
|
||||
} catch (ex if isNoSuchFileEx(ex)) {
|
||||
// Ignore exceptions about non-existent files.
|
||||
return {result: true};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a backup copy.
|
||||
*/
|
||||
removeBackupCopy: function (ext) {
|
||||
try {
|
||||
return {result: File.remove(this.backupPath + ext)};
|
||||
} catch (ex if isNoSuchFileEx(ex)) {
|
||||
// Ignore exceptions about non-existent files.
|
||||
return {result: true};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Wipes all files holding session data from disk.
|
||||
*/
|
||||
wipe: function () {
|
||||
let exn;
|
||||
|
||||
// Erase session state file
|
||||
// Don't stop immediately in case of error.
|
||||
let exn = null;
|
||||
|
||||
// Erase main session state file
|
||||
try {
|
||||
File.remove(this.path);
|
||||
} catch (ex if isNoSuchFileEx(ex)) {
|
||||
// Ignore exceptions about non-existent files.
|
||||
File.remove(this.Paths.clean);
|
||||
} catch (ex) {
|
||||
// Don't stop immediately.
|
||||
exn = ex;
|
||||
exn = exn || ex;
|
||||
}
|
||||
|
||||
// Erase any backup, any file named "sessionstore.bak[-buildID]".
|
||||
let iter = new File.DirectoryIterator(OS.Constants.Path.profileDir);
|
||||
for (let entry in iter) {
|
||||
if (!entry.isDir && entry.path.startsWith(this.backupPath)) {
|
||||
// Wipe the Session Restore directory
|
||||
try {
|
||||
this._wipeFromDir(this.Paths.backups, null);
|
||||
} catch (ex) {
|
||||
exn = exn || ex;
|
||||
}
|
||||
|
||||
try {
|
||||
File.removeDir(this.Paths.backups);
|
||||
} catch (ex) {
|
||||
exn = exn || ex;
|
||||
}
|
||||
|
||||
// Wipe legacy Ression Restore files from the profile directory
|
||||
try {
|
||||
this._wipeFromDir(OS.Constants.Path.profileDir, "sessionstore.bak");
|
||||
} catch (ex) {
|
||||
exn = exn || ex;
|
||||
}
|
||||
|
||||
|
||||
this.state = STATE_EMPTY;
|
||||
if (exn) {
|
||||
throw exn;
|
||||
}
|
||||
|
||||
return { result: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Wipe a number of files from a directory.
|
||||
*
|
||||
* @param {string} path The directory.
|
||||
* @param {string|null} prefix If provided, only remove files whose
|
||||
* name starts with a specific prefix.
|
||||
*/
|
||||
_wipeFromDir: function(path, prefix) {
|
||||
// Sanity check
|
||||
if (typeof prefix == "undefined" || prefix == "") {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
let exn = null;
|
||||
|
||||
let iterator = new File.DirectoryIterator(path);
|
||||
if (!iterator.exists()) {
|
||||
return;
|
||||
}
|
||||
for (let entry in iterator) {
|
||||
if (entry.isDir) {
|
||||
continue;
|
||||
}
|
||||
if (!prefix || entry.name.startsWith(prefix)) {
|
||||
try {
|
||||
File.remove(entry.path);
|
||||
} catch (ex) {
|
||||
// Don't stop immediately.
|
||||
// Don't stop immediately
|
||||
exn = exn || ex;
|
||||
}
|
||||
}
|
||||
@ -183,9 +334,7 @@ let Agent = {
|
||||
if (exn) {
|
||||
throw exn;
|
||||
}
|
||||
|
||||
return {result: true};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function isNoSuchFileEx(aReason) {
|
||||
|
43
browser/components/sessionstore/src/SessionWorker.jsm
Normal file
43
browser/components/sessionstore/src/SessionWorker.jsm
Normal file
@ -0,0 +1,43 @@
|
||||
/* 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";
|
||||
|
||||
/**
|
||||
* Interface to a dedicated thread handling I/O
|
||||
*/
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile.jsm", this);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SessionWorker"];
|
||||
|
||||
this.SessionWorker = (function () {
|
||||
let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
|
||||
OS.Shared.LOG.bind("SessionWorker"));
|
||||
return {
|
||||
post: function post(...args) {
|
||||
let promise = worker.post.apply(worker, args);
|
||||
return promise.then(
|
||||
null,
|
||||
function onError(error) {
|
||||
// Decode any serialized error
|
||||
if (error instanceof PromiseWorker.WorkerError) {
|
||||
throw OS.File.Error.fromMsg(error.data);
|
||||
}
|
||||
// Extract something meaningful from ErrorEvent
|
||||
if (error instanceof ErrorEvent) {
|
||||
throw new Error(error.message, error.filename, error.lineno);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
})();
|
@ -27,6 +27,7 @@ EXTRA_JS_MODULES = [
|
||||
'SessionMigration.jsm',
|
||||
'SessionStorage.jsm',
|
||||
'SessionWorker.js',
|
||||
'SessionWorker.jsm',
|
||||
'TabAttributes.jsm',
|
||||
'TabState.jsm',
|
||||
'TabStateCache.jsm',
|
||||
|
@ -57,6 +57,11 @@ function debug(aMsg) {
|
||||
aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
|
||||
Services.console.logStringMessage(aMsg);
|
||||
}
|
||||
function warning(aMsg, aException) {
|
||||
let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
|
||||
consoleMsg.init(aMsg, aException.fileName, null, aException.lineNumber, 0, Ci.nsIScriptError.warningFlag, "component javascript");
|
||||
Services.console.logMessage(consoleMsg);
|
||||
}
|
||||
|
||||
let gOnceInitializedDeferred = Promise.defer();
|
||||
|
||||
@ -107,27 +112,39 @@ SessionStartup.prototype = {
|
||||
/**
|
||||
* Complete initialization once the Session File has been read
|
||||
*
|
||||
* @param stateString
|
||||
* string The Session State string read from disk
|
||||
* @param source The Session State string read from disk.
|
||||
* @param parsed The object obtained by parsing |source| as JSON.
|
||||
*/
|
||||
_onSessionFileRead: function (stateString) {
|
||||
_onSessionFileRead: function ({source, parsed}) {
|
||||
this._initialized = true;
|
||||
|
||||
// Let observers modify the state before it is used
|
||||
let supportsStateString = this._createSupportsString(stateString);
|
||||
let supportsStateString = this._createSupportsString(source);
|
||||
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
|
||||
stateString = supportsStateString.data;
|
||||
let stateString = supportsStateString.data;
|
||||
|
||||
// No valid session found.
|
||||
if (!stateString) {
|
||||
if (stateString != source) {
|
||||
// The session has been modified by an add-on, reparse.
|
||||
try {
|
||||
this._initialState = JSON.parse(stateString);
|
||||
} catch (ex) {
|
||||
// That's not very good, an add-on has rewritten the initial
|
||||
// state to something that won't parse.
|
||||
warning("Observer rewrote the state to something that won't parse", ex);
|
||||
}
|
||||
} else {
|
||||
// No need to reparse
|
||||
this._initialState = parsed;
|
||||
}
|
||||
|
||||
if (this._initialState == null) {
|
||||
// No valid session found.
|
||||
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
|
||||
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
|
||||
gOnceInitializedDeferred.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this._initialState = this._parseStateString(stateString);
|
||||
|
||||
let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
|
||||
let shouldResumeSession = shouldResumeSessionOnce ||
|
||||
Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
|
||||
@ -194,29 +211,6 @@ SessionStartup.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Convert the Session State string into a state object
|
||||
*
|
||||
* @param stateString
|
||||
* string The Session State string read from disk
|
||||
* @returns {State} a Session State object
|
||||
*/
|
||||
_parseStateString: function (stateString) {
|
||||
let state = null;
|
||||
let corruptFile = false;
|
||||
|
||||
try {
|
||||
state = JSON.parse(stateString);
|
||||
} catch (ex) {
|
||||
debug("The session file contained un-parse-able JSON: " + ex);
|
||||
corruptFile = true;
|
||||
}
|
||||
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle notifications
|
||||
*/
|
||||
|
@ -60,6 +60,7 @@ support-files =
|
||||
[browser_aboutPrivateBrowsing.js]
|
||||
[browser_aboutSessionRestore.js]
|
||||
[browser_attributes.js]
|
||||
[browser_backup_recovery.js]
|
||||
[browser_broadcast.js]
|
||||
[browser_capabilities.js]
|
||||
[browser_cleaner.js]
|
||||
@ -180,7 +181,6 @@ skip-if = true # Needs to be rewritten as Marionette test, bug 995916
|
||||
[browser_739805.js]
|
||||
[browser_819510_perwindowpb.js]
|
||||
skip-if = os == "linux" # Intermittent failures, bug 894063
|
||||
[browser_833286_atomic_backup.js]
|
||||
|
||||
# Disabled for frequent intermittent failures
|
||||
[browser_464620_a.js]
|
||||
|
@ -3,63 +3,56 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/** Private Browsing Test for Bug 394759 **/
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
let windowsToClose = [];
|
||||
let closedWindowCount = 0;
|
||||
// Prevent VM timers issues, cache now and increment it manually.
|
||||
let now = Date.now();
|
||||
const TESTS = [
|
||||
{ url: "about:config",
|
||||
key: "bug 394759 Non-PB",
|
||||
value: "uniq" + (++now) },
|
||||
{ url: "about:mozilla",
|
||||
key: "bug 394759 PB",
|
||||
value: "uniq" + (++now) },
|
||||
];
|
||||
let closedWindowCount = 0;
|
||||
// Prevent VM timers issues, cache now and increment it manually.
|
||||
let now = Date.now();
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
Services.prefs.clearUserPref("browser.sessionstore.interval");
|
||||
windowsToClose.forEach(function(win) {
|
||||
win.close();
|
||||
});
|
||||
const TESTS = [
|
||||
{ url: "about:config",
|
||||
key: "bug 394759 Non-PB",
|
||||
value: "uniq" + (++now) },
|
||||
{ url: "about:mozilla",
|
||||
key: "bug 394759 PB",
|
||||
value: "uniq" + (++now) },
|
||||
];
|
||||
|
||||
function promiseTestOpenCloseWindow(aIsPrivate, aTest) {
|
||||
return Task.spawn(function*() {
|
||||
let win = yield promiseNewWindowLoaded({ "private": aIsPrivate });
|
||||
win.gBrowser.selectedBrowser.loadURI(aTest.url);
|
||||
yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
|
||||
yield Promise.resolve();
|
||||
// Mark the window with some unique data to be restored later on.
|
||||
ss.setWindowValue(win, aTest.key, aTest.value);
|
||||
// Close.
|
||||
yield promiseWindowClosed(win);
|
||||
});
|
||||
}
|
||||
|
||||
function testOpenCloseWindow(aIsPrivate, aTest, aCallback) {
|
||||
whenNewWindowLoaded({ private: aIsPrivate }, function(win) {
|
||||
whenBrowserLoaded(win.gBrowser.selectedBrowser, function() {
|
||||
executeSoon(function() {
|
||||
// Mark the window with some unique data to be restored later on.
|
||||
ss.setWindowValue(win, aTest.key, aTest.value);
|
||||
// Close.
|
||||
win.close();
|
||||
aCallback();
|
||||
});
|
||||
});
|
||||
win.gBrowser.selectedBrowser.loadURI(aTest.url);
|
||||
});
|
||||
}
|
||||
function promiseTestOnWindow(aIsPrivate, aValue) {
|
||||
return Task.spawn(function*() {
|
||||
let win = yield promiseNewWindowLoaded({ "private": aIsPrivate });
|
||||
yield promiseCheckClosedWindows(aIsPrivate, aValue);
|
||||
registerCleanupFunction(() => promiseWindowClosed(win));
|
||||
});
|
||||
}
|
||||
|
||||
function testOnWindow(aIsPrivate, aValue, aCallback) {
|
||||
whenNewWindowLoaded({ private: aIsPrivate }, function(win) {
|
||||
windowsToClose.push(win);
|
||||
executeSoon(function() checkClosedWindows(aIsPrivate, aValue, aCallback));
|
||||
});
|
||||
}
|
||||
|
||||
function checkClosedWindows(aIsPrivate, aValue, aCallback) {
|
||||
function promiseCheckClosedWindows(aIsPrivate, aValue) {
|
||||
return Task.spawn(function*() {
|
||||
let data = JSON.parse(ss.getClosedWindowData())[0];
|
||||
is(ss.getClosedWindowCount(), 1, "Check the closed window count");
|
||||
is(ss.getClosedWindowCount(), 1, "Check that the closed window count hasn't changed");
|
||||
ok(JSON.stringify(data).indexOf(aValue) > -1,
|
||||
"Check the closed window data was stored correctly");
|
||||
aCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupBlankState(aCallback) {
|
||||
function promiseBlankState() {
|
||||
return Task.spawn(function*() {
|
||||
// Set interval to a large time so state won't be written while we setup
|
||||
// environment.
|
||||
Services.prefs.setIntPref("browser.sessionstore.interval", 100000);
|
||||
registerCleanupFunction(() => Services.prefs.clearUserPref("browser.sessionstore.interval"));
|
||||
|
||||
// Set up the browser in a blank state. Popup windows in previous tests
|
||||
// result in different states on different platforms.
|
||||
@ -70,40 +63,39 @@ function test() {
|
||||
}],
|
||||
_closedWindows: []
|
||||
});
|
||||
|
||||
ss.setBrowserState(blankState);
|
||||
|
||||
// Wait for the sessionstore.js file to be written before going on.
|
||||
// Note: we don't wait for the complete event, since if asyncCopy fails we
|
||||
// would timeout.
|
||||
waitForSaveState(function(writing) {
|
||||
ok(writing, "sessionstore.js is being written");
|
||||
closedWindowCount = ss.getClosedWindowCount();
|
||||
is(closedWindowCount, 0, "Correctly set window count");
|
||||
|
||||
executeSoon(aCallback);
|
||||
});
|
||||
yield forceSaveState();
|
||||
closedWindowCount = ss.getClosedWindowCount();
|
||||
is(closedWindowCount, 0, "Correctly set window count");
|
||||
|
||||
// Remove the sessionstore.js file before setting the interval to 0
|
||||
let profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||
let sessionStoreJS = profilePath.clone();
|
||||
sessionStoreJS.append("sessionstore.js");
|
||||
if (sessionStoreJS.exists())
|
||||
sessionStoreJS.remove(false);
|
||||
info("sessionstore.js was correctly removed: " + (!sessionStoreJS.exists()));
|
||||
yield SessionFile.wipe();
|
||||
|
||||
// Make sure that sessionstore.js can be forced to be created by setting
|
||||
// the interval pref to 0.
|
||||
Services.prefs.setIntPref("browser.sessionstore.interval", 0);
|
||||
}
|
||||
|
||||
setupBlankState(function() {
|
||||
testOpenCloseWindow(false, TESTS[0], function() {
|
||||
testOpenCloseWindow(true, TESTS[1], function() {
|
||||
testOnWindow(false, TESTS[0].value, function() {
|
||||
testOnWindow(true, TESTS[0].value, finish);
|
||||
});
|
||||
});
|
||||
});
|
||||
yield forceSaveState();
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* init() {
|
||||
while (ss.getClosedWindowCount() > 0) {
|
||||
ss.forgetClosedWindow(0);
|
||||
}
|
||||
while (ss.getClosedTabCount(window) > 0) {
|
||||
ss.forgetClosedTab(window, 0);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* main() {
|
||||
yield promiseTestOpenCloseWindow(false, TESTS[0]);
|
||||
yield promiseTestOpenCloseWindow(true, TESTS[1]);
|
||||
yield promiseTestOnWindow(false, TESTS[0].value);
|
||||
yield promiseTestOnWindow(true, TESTS[0].value);
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,7 @@ const PASS = "pwd-" + Math.random();
|
||||
/**
|
||||
* Bug 454908 - Don't save/restore values of password fields.
|
||||
*/
|
||||
add_task(function test_dont_save_passwords() {
|
||||
add_task(function* test_dont_save_passwords() {
|
||||
// Make sure we do save form data.
|
||||
Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
|
||||
|
||||
@ -40,13 +40,12 @@ add_task(function test_dont_save_passwords() {
|
||||
is(passwd, "", "password wasn't saved/restored");
|
||||
|
||||
// Write to disk and read our file.
|
||||
yield SessionSaver.run();
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
let data = yield OS.File.read(path);
|
||||
let state = new TextDecoder().decode(data);
|
||||
yield forceSaveState();
|
||||
yield promiseForEachSessionRestoreFile((state, key) =>
|
||||
// Ensure that we have not saved our password.
|
||||
ok(!state.contains(PASS), "password has not been written to file " + key)
|
||||
);
|
||||
|
||||
// Ensure that sessionstore.js doesn't contain our password.
|
||||
is(state.indexOf(PASS), -1, "password has not been written to disk");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
|
@ -36,7 +36,7 @@ add_task(function* new_window() {
|
||||
yield promiseWindowClosed(newWin);
|
||||
newWin = null;
|
||||
|
||||
let state = JSON.parse((yield promiseSaveFileContents()));
|
||||
let state = JSON.parse((yield promiseRecoveryFileContents()));
|
||||
is(state.windows.length, 2,
|
||||
"observe1: 2 windows in data written to disk");
|
||||
is(state._closedWindows.length, 0,
|
||||
@ -60,7 +60,7 @@ add_task(function* new_tab() {
|
||||
try {
|
||||
newTab = gBrowser.addTab("about:mozilla");
|
||||
|
||||
let state = JSON.parse((yield promiseSaveFileContents()));
|
||||
let state = JSON.parse((yield promiseRecoveryFileContents()));
|
||||
is(state.windows.length, 1,
|
||||
"observe2: 1 window in data being written to disk");
|
||||
is(state._closedWindows.length, 1,
|
||||
|
@ -164,7 +164,7 @@ function waitForWindowClose(aWin, aCallback) {
|
||||
}
|
||||
|
||||
function forceWriteState(aCallback) {
|
||||
return promiseSaveFileContents().then(function(data) {
|
||||
return promiseRecoveryFileContents().then(function(data) {
|
||||
aCallback(JSON.parse(data));
|
||||
});
|
||||
}
|
||||
|
@ -1,99 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// This tests are for a sessionstore.js atomic backup.
|
||||
// Each test will wait for a write to the Session Store
|
||||
// before executing.
|
||||
|
||||
let tmp = {};
|
||||
Cu.import("resource://gre/modules/osfile.jsm", tmp);
|
||||
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", tmp);
|
||||
|
||||
const {OS, SessionFile} = tmp;
|
||||
|
||||
const PREF_SS_INTERVAL = "browser.sessionstore.interval";
|
||||
// Full paths for sessionstore.js and sessionstore.bak.
|
||||
const path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
const backupPath = OS.Path.join(OS.Constants.Path.profileDir,
|
||||
"sessionstore.bak");
|
||||
|
||||
// A text decoder.
|
||||
let gDecoder = new TextDecoder();
|
||||
// Global variables that contain sessionstore.js and sessionstore.bak data for
|
||||
// comparison between tests.
|
||||
let gSSData;
|
||||
let gSSBakData;
|
||||
|
||||
|
||||
|
||||
add_task(function* testAfterFirstWrite() {
|
||||
// Ensure sessionstore.bak is not created. We start with a clean
|
||||
// profile so there was nothing to move to sessionstore.bak before
|
||||
// initially writing sessionstore.js
|
||||
let ssExists = yield OS.File.exists(path);
|
||||
let ssBackupExists = yield OS.File.exists(backupPath);
|
||||
ok(ssExists, "sessionstore.js should exist.");
|
||||
ok(!ssBackupExists, "sessionstore.bak should not have been created, yet");
|
||||
|
||||
// Save sessionstore.js data to compare to the sessionstore.bak data in the
|
||||
// next test.
|
||||
let array = yield OS.File.read(path);
|
||||
gSSData = gDecoder.decode(array);
|
||||
|
||||
// Manually move to the backup since the first write has already happened
|
||||
// and a backup would not be triggered again.
|
||||
yield OS.File.move(path, backupPath);
|
||||
|
||||
yield forceSaveState();
|
||||
});
|
||||
|
||||
add_task(function* testReadBackup() {
|
||||
// Ensure sessionstore.bak is finally created.
|
||||
let ssExists = yield OS.File.exists(path);
|
||||
let ssBackupExists = yield OS.File.exists(backupPath);
|
||||
ok(ssExists, "sessionstore.js exists.");
|
||||
ok(ssBackupExists, "sessionstore.bak should now be created.");
|
||||
|
||||
// Read sessionstore.bak data.
|
||||
let array = yield OS.File.read(backupPath);
|
||||
gSSBakData = gDecoder.decode(array);
|
||||
|
||||
// Make sure that the sessionstore.bak is identical to the last
|
||||
// sessionstore.js.
|
||||
is(gSSBakData, gSSData, "sessionstore.js is backed up correctly.");
|
||||
|
||||
// Read latest sessionstore.js.
|
||||
array = yield OS.File.read(path);
|
||||
gSSData = gDecoder.decode(array);
|
||||
|
||||
// Read sessionstore.js with SessionFile.read.
|
||||
let ssDataRead = yield SessionFile.read();
|
||||
is(ssDataRead, gSSData, "SessionFile.read read sessionstore.js correctly.");
|
||||
|
||||
// Remove sessionstore.js to test fallback onto sessionstore.bak.
|
||||
yield OS.File.remove(path);
|
||||
ssExists = yield OS.File.exists(path);
|
||||
ok(!ssExists, "sessionstore.js should be removed now.");
|
||||
|
||||
// Read sessionstore.bak with SessionFile.read.
|
||||
ssDataRead = yield SessionFile.read();
|
||||
is(ssDataRead, gSSBakData,
|
||||
"SessionFile.read read sessionstore.bak correctly.");
|
||||
|
||||
yield forceSaveState();
|
||||
});
|
||||
|
||||
add_task(function* testBackupUnchanged() {
|
||||
// Ensure sessionstore.bak is backed up only once.
|
||||
|
||||
// Read sessionstore.bak data.
|
||||
let array = yield OS.File.read(backupPath);
|
||||
let ssBakData = gDecoder.decode(array);
|
||||
// Ensure the sessionstore.bak did not change.
|
||||
is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
|
||||
});
|
||||
|
||||
add_task(function* cleanup() {
|
||||
// Cleaning up after the test: removing the sessionstore.bak file.
|
||||
yield OS.File.remove(backupPath);
|
||||
});
|
132
browser/components/sessionstore/test/browser_backup_recovery.js
Normal file
132
browser/components/sessionstore/test/browser_backup_recovery.js
Normal file
@ -0,0 +1,132 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// This tests are for a sessionstore.js atomic backup.
|
||||
// Each test will wait for a write to the Session Store
|
||||
// before executing.
|
||||
|
||||
let OS = Cu.import("resource://gre/modules/osfile.jsm", {}).OS;
|
||||
let {File, Constants, Path} = OS;
|
||||
|
||||
const PREF_SS_INTERVAL = "browser.sessionstore.interval";
|
||||
const Paths = SessionFile.Paths;
|
||||
|
||||
// A text decoder.
|
||||
let gDecoder = new TextDecoder();
|
||||
// Global variables that contain sessionstore.js and sessionstore.bak data for
|
||||
// comparison between tests.
|
||||
let gSSData;
|
||||
let gSSBakData;
|
||||
|
||||
function promiseRead(path) {
|
||||
return File.read(path, {encoding: "utf-8"});
|
||||
}
|
||||
|
||||
add_task(function* init() {
|
||||
// Make sure that we are not racing with SessionSaver's time based
|
||||
// saves.
|
||||
Services.prefs.setIntPref(PREF_SS_INTERVAL, 10000000);
|
||||
registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_SS_INTERVAL));
|
||||
});
|
||||
|
||||
add_task(function* test_creation() {
|
||||
|
||||
let OLD_BACKUP = Path.join(Constants.Path.profileDir, "sessionstore.bak");
|
||||
let OLD_UPGRADE_BACKUP = Path.join(Constants.Path.profileDir, "sessionstore.bak-0000000");
|
||||
|
||||
yield File.writeAtomic(OLD_BACKUP, "sessionstore.bak");
|
||||
yield File.writeAtomic(OLD_UPGRADE_BACKUP, "sessionstore upgrade backup");
|
||||
|
||||
yield SessionFile.wipe();
|
||||
yield SessionFile.read(); // Reinitializes SessionFile
|
||||
for (let k of Paths.loadOrder) {
|
||||
ok(!(yield File.exists(Paths[k])), "After wipe " + k + " sessionstore file doesn't exist");
|
||||
}
|
||||
ok(!(yield File.exists(OLD_BACKUP)), "After wipe, old backup doesn't exist");
|
||||
ok(!(yield File.exists(OLD_UPGRADE_BACKUP)), "After wipe, old upgrade backup doesn't exist");
|
||||
|
||||
let URL_BASE = "http://example.com/?atomic_backup_test_creation=" + Math.random();
|
||||
let URL = URL_BASE + "?first_write";
|
||||
let tab = gBrowser.addTab(URL);
|
||||
|
||||
info("Testing situation after a single write");
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
SyncHandlers.get(tab.linkedBrowser).flush();
|
||||
yield SessionSaver.run();
|
||||
|
||||
ok((yield File.exists(Paths.recovery)), "After write, recovery sessionstore file exists again");
|
||||
ok(!(yield File.exists(Paths.recoveryBackup)), "After write, recoveryBackup sessionstore doesn't exist");
|
||||
ok((yield promiseRead(Paths.recovery)).indexOf(URL) != -1, "Recovery sessionstore file contains the required tab");
|
||||
ok(!(yield File.exists(Paths.clean)), "After first write, clean shutdown sessionstore doesn't exist, since we haven't shutdown yet");
|
||||
|
||||
info("Testing situation after a second write");
|
||||
let URL2 = URL_BASE + "?second_write";
|
||||
tab.linkedBrowser.loadURI(URL2);
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
SyncHandlers.get(tab.linkedBrowser).flush();
|
||||
yield SessionSaver.run();
|
||||
|
||||
ok((yield File.exists(Paths.recovery)), "After second write, recovery sessionstore file still exists");
|
||||
ok((yield promiseRead(Paths.recovery)).indexOf(URL2) != -1, "Recovery sessionstore file contains the latest url");
|
||||
ok((yield File.exists(Paths.recoveryBackup)), "After write, recoveryBackup sessionstore now exists");
|
||||
let backup = yield promiseRead(Paths.recoveryBackup);
|
||||
ok(backup.indexOf(URL2) == -1, "Recovery backup doesn't contain the latest url");
|
||||
ok(backup.indexOf(URL) != -1, "Recovery backup contains the original url");
|
||||
ok(!(yield File.exists(Paths.clean)), "After first write, clean shutdown sessinstore doesn't exist, since we haven't shutdown yet");
|
||||
|
||||
info("Reinitialize, ensure that we haven't leaked sensitive files");
|
||||
yield SessionFile.read(); // Reinitializes SessionFile
|
||||
yield SessionSaver.run();
|
||||
ok(!(yield File.exists(Paths.clean)), "After second write, clean shutdown sessonstore doesn't exist, since we haven't shutdown yet");
|
||||
ok(!(yield File.exists(Paths.upgradeBackup)), "After second write, clean shutdwn sessionstore doesn't exist, since we haven't shutdown yet");
|
||||
ok(!(yield File.exists(Paths.nextUpgradeBackup)), "After second write, clean sutdown sessionstore doesn't exist, since we haven't shutdown yet");
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
yield SessionFile.wipe();
|
||||
});
|
||||
|
||||
let promiseSource = Task.async(function*(name) {
|
||||
let URL = "http://example.com/?atomic_backup_test_recovery=" + Math.random() + "&name=" + name;
|
||||
let tab = gBrowser.addTab(URL);
|
||||
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
SyncHandlers.get(tab.linkedBrowser).flush();
|
||||
yield SessionSaver.run();
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
let SOURCE = yield promiseRead(Paths.recovery);
|
||||
yield SessionFile.wipe();
|
||||
return SOURCE;
|
||||
});
|
||||
|
||||
add_task(function* test_recovery() {
|
||||
yield SessionFile.wipe();
|
||||
info("Attempting to recover from the recovery file");
|
||||
let SOURCE = yield promiseSource("Paths.recovery");
|
||||
// Ensure that we can recover from Paths.recovery
|
||||
yield File.makeDir(Paths.backups);
|
||||
yield File.writeAtomic(Paths.recovery, SOURCE);
|
||||
is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
|
||||
yield SessionFile.wipe();
|
||||
|
||||
info("Corrupting recovery file, attempting to recover from recovery backup");
|
||||
SOURCE = yield promiseSource("Paths.recoveryBackup");
|
||||
yield File.makeDir(Paths.backups);
|
||||
yield File.writeAtomic(Paths.recoveryBackup, SOURCE);
|
||||
yield File.writeAtomic(Paths.recovery, "<Invalid JSON>");
|
||||
is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
|
||||
});
|
||||
|
||||
add_task(function* test_clean() {
|
||||
yield SessionFile.wipe();
|
||||
let SOURCE = yield promiseSource("Paths.clean");
|
||||
yield File.writeAtomic(Paths.clean, SOURCE);
|
||||
yield SessionFile.read();
|
||||
yield SessionSaver.run();
|
||||
is((yield promiseRead(Paths.cleanBackup)), SOURCE, "After first read/write, clean shutdown file has been moved to cleanBackup");
|
||||
});
|
||||
|
||||
add_task(function* cleanup() {
|
||||
yield SessionFile.wipe();
|
||||
});
|
||||
|
@ -34,10 +34,8 @@ add_task(function() {
|
||||
SyncHandlers.get(tab2.linkedBrowser).flush();
|
||||
|
||||
info("Checking out state");
|
||||
yield SessionSaver.run();
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
let data = yield OS.File.read(path);
|
||||
let state = new TextDecoder().decode(data);
|
||||
let state = yield promiseRecoveryFileContents();
|
||||
|
||||
info("State: " + state);
|
||||
// Ensure that sessionstore.js only knows about the public tab
|
||||
ok(state.indexOf(URL_PUBLIC) != -1, "State contains public tab");
|
||||
|
@ -5,45 +5,36 @@ Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile.jsm", this);
|
||||
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
const Paths = SessionFile.Paths;
|
||||
|
||||
Task.spawn(function task() {
|
||||
try {
|
||||
// Wait until initialization is complete
|
||||
yield SessionStore.promiseInitialized;
|
||||
add_task(function* init() {
|
||||
// Wait until initialization is complete
|
||||
yield SessionStore.promiseInitialized;
|
||||
yield SessionFile.wipe();
|
||||
});
|
||||
|
||||
const PREF_UPGRADE = "browser.sessionstore.upgradeBackup.latestBuildID";
|
||||
let buildID = Services.appinfo.platformBuildID;
|
||||
add_task(function* test_upgrade_backup() {
|
||||
const PREF_UPGRADE = "browser.sessionstore.upgradeBackup.latestBuildID";
|
||||
let buildID = Services.appinfo.platformBuildID;
|
||||
info("Let's check if we create an upgrade backup");
|
||||
Services.prefs.setCharPref(PREF_UPGRADE, "");
|
||||
let contents = JSON.stringify({"browser_upgrade_backup.js": Math.random()});
|
||||
yield OS.File.writeAtomic(Paths.clean, contents);
|
||||
yield SessionFile.read(); // First call to read() initializes the SessionWorker
|
||||
yield SessionFile.write(""); // First call to write() triggers the backup
|
||||
|
||||
// Write state once before starting the test to
|
||||
// ensure sessionstore.js writes won't happen in between.
|
||||
yield forceSaveState();
|
||||
is(Services.prefs.getCharPref(PREF_UPGRADE), buildID, "upgrade backup should be set");
|
||||
|
||||
// Force backup to take place with a file decided by us
|
||||
Services.prefs.setCharPref(PREF_UPGRADE, "");
|
||||
let contents = "browser_upgrade_backup.js";
|
||||
let pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
yield OS.File.writeAtomic(pathStore, contents, { tmpPath: pathStore + ".tmp" });
|
||||
yield SessionStore._internal._performUpgradeBackup();
|
||||
is(Services.prefs.getCharPref(PREF_UPGRADE), buildID, "upgrade backup should be set (again)");
|
||||
is((yield OS.File.exists(Paths.upgradeBackup)), true, "upgrade backup file has been created");
|
||||
|
||||
let pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak-" + Services.appinfo.platformBuildID);
|
||||
is((yield OS.File.exists(pathBackup)), true, "upgrade backup file has been created");
|
||||
let data = yield OS.File.read(Paths.upgradeBackup);
|
||||
is(contents, (new TextDecoder()).decode(data), "upgrade backup contains the expected contents");
|
||||
|
||||
let data = yield OS.File.read(pathBackup);
|
||||
is(new TextDecoder().decode(data), contents, "upgrade backup contains the expected contents");
|
||||
|
||||
// Ensure that we don't re-backup by accident
|
||||
yield OS.File.writeAtomic(pathStore, "something else entirely", { tmpPath: pathStore + ".tmp" });
|
||||
yield SessionStore._internal._performUpgradeBackup();
|
||||
data = yield OS.File.read(pathBackup);
|
||||
is(new TextDecoder().decode(data), contents, "upgrade backup hasn't changed");
|
||||
|
||||
} catch (ex) {
|
||||
ok(false, "Uncaught error: " + ex + " at " + ex.stack);
|
||||
} finally {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
info("Let's check that we don't overwrite this upgrade backup");
|
||||
let new_contents = JSON.stringify({"something else entirely": Math.random()});
|
||||
yield OS.File.writeAtomic(Paths.clean, new_contents);
|
||||
yield SessionFile.read(); // Reinitialize the SessionWorker
|
||||
yield SessionFile.write(""); // Next call to write() shouldn't trigger the backup
|
||||
data = yield OS.File.read(Paths.upgradeBackup);
|
||||
is(contents, (new TextDecoder()).decode(data), "upgrade backup hasn't changed");
|
||||
});
|
||||
|
@ -39,9 +39,11 @@ registerCleanupFunction(() => {
|
||||
|
||||
let tmp = {};
|
||||
Cu.import("resource://gre/modules/Promise.jsm", tmp);
|
||||
Cu.import("resource://gre/modules/Task.jsm", tmp);
|
||||
Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp);
|
||||
Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", tmp);
|
||||
let {Promise, SessionStore, SessionSaver} = tmp;
|
||||
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", tmp);
|
||||
let {Promise, Task, SessionStore, SessionSaver, SessionFile} = tmp;
|
||||
|
||||
let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
|
||||
|
||||
@ -282,13 +284,26 @@ function forceSaveState() {
|
||||
return SessionSaver.run();
|
||||
}
|
||||
|
||||
function promiseSaveFileContents() {
|
||||
function promiseRecoveryFileContents() {
|
||||
let promise = forceSaveState();
|
||||
return promise.then(function() {
|
||||
return OS.File.read(OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), { encoding: "utf-8" });
|
||||
return OS.File.read(SessionFile.Paths.recovery, { encoding: "utf-8" });
|
||||
});
|
||||
}
|
||||
|
||||
let promiseForEachSessionRestoreFile = Task.async(function*(cb) {
|
||||
for (let key of SessionFile.Paths.loadOrder) {
|
||||
let data = "";
|
||||
try {
|
||||
data = yield OS.File.read(SessionFile.Paths[key], { encoding: "utf-8" });
|
||||
} catch (ex if ex instanceof OS.File.Error
|
||||
&& ex.becauseNoSuchFile) {
|
||||
// Ignore missing files
|
||||
}
|
||||
cb(data, key);
|
||||
}
|
||||
});
|
||||
|
||||
function whenBrowserLoaded(aBrowser, aCallback = next, ignoreSubFrames = true) {
|
||||
aBrowser.addEventListener("load", function onLoad(event) {
|
||||
if (!ignoreSubFrames || event.target == aBrowser.contentDocument) {
|
||||
|
@ -5,29 +5,28 @@ let Ci = Components.interfaces;
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// Call a function once initialization of SessionStartup is complete
|
||||
let afterSessionStartupInitialization =
|
||||
function afterSessionStartupInitialization(cb) {
|
||||
do_print("Waiting for session startup initialization");
|
||||
let observer = function() {
|
||||
try {
|
||||
do_print("Session startup initialization observed");
|
||||
Services.obs.removeObserver(observer, "sessionstore-state-finalized");
|
||||
cb();
|
||||
} catch (ex) {
|
||||
do_throw(ex);
|
||||
}
|
||||
};
|
||||
function afterSessionStartupInitialization(cb) {
|
||||
do_print("Waiting for session startup initialization");
|
||||
let observer = function() {
|
||||
try {
|
||||
do_print("Session startup initialization observed");
|
||||
Services.obs.removeObserver(observer, "sessionstore-state-finalized");
|
||||
cb();
|
||||
} catch (ex) {
|
||||
do_throw(ex);
|
||||
}
|
||||
};
|
||||
|
||||
// We need the Crash Monitor initialized for sessionstartup to run
|
||||
// successfully.
|
||||
Components.utils.import("resource://gre/modules/CrashMonitor.jsm");
|
||||
CrashMonitor.init();
|
||||
// We need the Crash Monitor initialized for sessionstartup to run
|
||||
// successfully.
|
||||
Components.utils.import("resource://gre/modules/CrashMonitor.jsm");
|
||||
CrashMonitor.init();
|
||||
|
||||
// Start sessionstartup initialization.
|
||||
let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
|
||||
getService(Ci.nsIObserver);
|
||||
Services.obs.addObserver(startup, "final-ui-startup", false);
|
||||
Services.obs.addObserver(startup, "quit-application", false);
|
||||
Services.obs.notifyObservers(null, "final-ui-startup", "");
|
||||
Services.obs.addObserver(observer, "sessionstore-state-finalized", false);
|
||||
// Start sessionstartup initialization.
|
||||
let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
|
||||
getService(Ci.nsIObserver);
|
||||
Services.obs.addObserver(startup, "final-ui-startup", false);
|
||||
Services.obs.addObserver(startup, "quit-application", false);
|
||||
Services.obs.notifyObservers(null, "final-ui-startup", "");
|
||||
Services.obs.addObserver(observer, "sessionstore-state-finalized", false);
|
||||
};
|
||||
|
@ -1,42 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
|
||||
let toplevel = this;
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", toplevel);
|
||||
pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
let pathStore;
|
||||
function pathBackup(ext) {
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak" + ext);
|
||||
}
|
||||
|
||||
// Ensure that things proceed smoothly if there is no file to back up
|
||||
add_task(function test_nothing_to_backup() {
|
||||
yield SessionFile.createBackupCopy("");
|
||||
});
|
||||
|
||||
// Create a file, back it up, remove it
|
||||
add_task(function test_do_backup() {
|
||||
let content = "test_1";
|
||||
let ext = ".upgrade_test_1";
|
||||
yield OS.File.writeAtomic(pathStore, content, {tmpPath: pathStore + ".tmp"});
|
||||
|
||||
do_print("Ensuring that the backup is created");
|
||||
yield SessionFile.createBackupCopy(ext);
|
||||
do_check_true((yield OS.File.exists(pathBackup(ext))));
|
||||
|
||||
let data = yield OS.File.read(pathBackup(ext));
|
||||
do_check_eq((new TextDecoder()).decode(data), content);
|
||||
|
||||
do_print("Ensuring that we can remove the backup");
|
||||
yield SessionFile.removeBackupCopy(ext);
|
||||
do_check_false((yield OS.File.exists(pathBackup(ext))));
|
||||
});
|
||||
|
@ -1,48 +1,151 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
let toplevel = this;
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
"use strict";
|
||||
|
||||
let {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
|
||||
let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
let {SessionWorker} = Cu.import("resource:///modules/sessionstore/SessionWorker.jsm", {});
|
||||
|
||||
let File = OS.File;
|
||||
let Paths;
|
||||
let SessionFile;
|
||||
|
||||
// We need a XULAppInfo to initialize SessionFile
|
||||
let (XULAppInfo = {
|
||||
vendor: "Mozilla",
|
||||
name: "SessionRestoreTest",
|
||||
ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
|
||||
version: "1",
|
||||
appBuildID: "2007010101",
|
||||
platformVersion: "",
|
||||
platformBuildID: "2007010101",
|
||||
inSafeMode: false,
|
||||
logConsoleErrors: true,
|
||||
OS: "XPCShell",
|
||||
XPCOMABI: "noarch-spidermonkey",
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsIXULAppInfo,
|
||||
Ci.nsIXULRuntime,
|
||||
])
|
||||
}) {
|
||||
let XULAppInfoFactory = {
|
||||
createInstance: function (outer, iid) {
|
||||
if (outer != null)
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
return XULAppInfo.QueryInterface(iid);
|
||||
}
|
||||
};
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
|
||||
"XULAppInfo", "@mozilla.org/xre/app-info;1",
|
||||
XULAppInfoFactory);
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
let profd = do_get_profile();
|
||||
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", toplevel);
|
||||
decoder = new TextDecoder();
|
||||
pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
|
||||
let source = do_get_file("data/sessionstore_valid.js");
|
||||
source.copyTo(profd, "sessionstore.js");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* init() {
|
||||
// Make sure that we have a profile before initializing SessionFile
|
||||
let profd = do_get_profile();
|
||||
SessionFile = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {}).SessionFile;
|
||||
Paths = SessionFile.Paths;
|
||||
|
||||
|
||||
let source = do_get_file("data/sessionstore_valid.js");
|
||||
source.copyTo(profd, "sessionstore.js");
|
||||
|
||||
// Finish initialization of SessionFile
|
||||
yield SessionFile.read();
|
||||
});
|
||||
|
||||
let pathStore;
|
||||
let pathBackup;
|
||||
let decoder;
|
||||
|
||||
// Write to the store, and check that a backup is created first
|
||||
add_task(function test_first_write_backup() {
|
||||
let content = "test_1";
|
||||
let initial_content = decoder.decode(yield OS.File.read(pathStore));
|
||||
function promise_check_exist(path, shouldExist) {
|
||||
return Task.spawn(function*() {
|
||||
do_print("Ensuring that " + path + (shouldExist?" exists":" does not exist"));
|
||||
if ((yield OS.File.exists(path)) != shouldExist) {
|
||||
throw new Error("File " + path + " should " + (shouldExist?"exist":"not exist"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
do_check_true(!(yield OS.File.exists(pathBackup)));
|
||||
yield SessionFile.write(content);
|
||||
do_check_true(yield OS.File.exists(pathBackup));
|
||||
function promise_check_contents(path, expect) {
|
||||
return Task.spawn(function*() {
|
||||
do_print("Checking whether " + path + " has the right contents");
|
||||
let actual = yield OS.File.read(path, { encoding: "utf-8"});
|
||||
if (actual != expect) {
|
||||
throw new Error("File " + path + " should contain\n\t" + expect + "\nbut contains " + actual);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let backup_content = decoder.decode(yield OS.File.read(pathBackup));
|
||||
do_check_eq(initial_content, backup_content);
|
||||
// Write to the store, and check that it creates:
|
||||
// - $Path.recovery with the new data
|
||||
// - $Path.nextUpgradeBackup with the old data
|
||||
add_task(function* test_first_write_backup() {
|
||||
let initial_content = "initial content " + Math.random();
|
||||
let new_content = "test_1 " + Math.random();
|
||||
|
||||
do_print("Before the first write, none of the files should exist");
|
||||
yield promise_check_exist(Paths.backups, false);
|
||||
|
||||
yield File.makeDir(Paths.backups);
|
||||
yield File.writeAtomic(Paths.clean, initial_content, { encoding: "utf-8" });
|
||||
yield SessionFile.write(new_content);
|
||||
|
||||
do_print("After first write, a few files should have been created");
|
||||
yield promise_check_exist(Paths.backups, true);
|
||||
yield promise_check_exist(Paths.clean, false);
|
||||
yield promise_check_exist(Paths.cleanBackup, true);
|
||||
yield promise_check_exist(Paths.recovery, true);
|
||||
yield promise_check_exist(Paths.recoveryBackup, false);
|
||||
yield promise_check_exist(Paths.nextUpgradeBackup, true);
|
||||
|
||||
yield promise_check_contents(Paths.recovery, new_content);
|
||||
yield promise_check_contents(Paths.nextUpgradeBackup, initial_content);
|
||||
});
|
||||
|
||||
// Write to the store again, and check that the backup is not updated
|
||||
add_task(function test_second_write_no_backup() {
|
||||
let content = "test_2";
|
||||
let initial_content = decoder.decode(yield OS.File.read(pathStore));
|
||||
let initial_backup_content = decoder.decode(yield OS.File.read(pathBackup));
|
||||
// Write to the store again, and check that
|
||||
// - $Path.clean is not written
|
||||
// - $Path.recovery contains the new data
|
||||
// - $Path.recoveryBackup contains the previous data
|
||||
add_task(function* test_second_write_no_backup() {
|
||||
let new_content = "test_2 " + Math.random();
|
||||
let previous_backup_content = yield File.read(Paths.recovery, { encoding: "utf-8" });
|
||||
|
||||
yield SessionFile.write(content);
|
||||
yield OS.File.remove(Paths.cleanBackup);
|
||||
|
||||
let written_content = decoder.decode(yield OS.File.read(pathStore));
|
||||
do_check_eq(content, written_content);
|
||||
yield SessionFile.write(new_content);
|
||||
|
||||
yield promise_check_exist(Paths.backups, true);
|
||||
yield promise_check_exist(Paths.clean, false);
|
||||
yield promise_check_exist(Paths.cleanBackup, false);
|
||||
yield promise_check_exist(Paths.recovery, true);
|
||||
yield promise_check_exist(Paths.nextUpgradeBackup, true);
|
||||
|
||||
yield promise_check_contents(Paths.recovery, new_content);
|
||||
yield promise_check_contents(Paths.recoveryBackup, previous_backup_content);
|
||||
});
|
||||
|
||||
// Make sure that we create $Paths.clean and remove $Paths.recovery*
|
||||
// upon shutdown
|
||||
add_task(function* test_shutdown() {
|
||||
let output = "test_3 " + Math.random();
|
||||
|
||||
yield File.writeAtomic(Paths.recovery, "I should disappear");
|
||||
yield File.writeAtomic(Paths.recoveryBackup, "I should also disappear");
|
||||
|
||||
yield SessionWorker.post("write", [output, { isFinalWrite: true, performShutdownCleanup: true}]);
|
||||
|
||||
do_check_false((yield File.exists(Paths.recovery)));
|
||||
do_check_false((yield File.exists(Paths.recoveryBackup)));
|
||||
let input = yield File.read(Paths.clean, { encoding: "utf-8"});
|
||||
do_check_eq(input, output);
|
||||
|
||||
let backup_content = decoder.decode(yield OS.File.read(pathBackup));
|
||||
do_check_eq(initial_backup_content, backup_content);
|
||||
});
|
||||
|
@ -7,7 +7,6 @@ support-files =
|
||||
data/sessionstore_invalid.js
|
||||
data/sessionstore_valid.js
|
||||
|
||||
[test_backup.js]
|
||||
[test_backup_once.js]
|
||||
[test_startup_nosession_async.js]
|
||||
[test_startup_session_async.js]
|
||||
|
@ -124,6 +124,16 @@ TranslationUI.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state == Translation.STATE_OFFER) {
|
||||
if (this.detectedLanguage != aFrom)
|
||||
TranslationHealthReport.recordDetectedLanguageChange(true);
|
||||
} else {
|
||||
if (this.translatedFrom != aFrom)
|
||||
TranslationHealthReport.recordDetectedLanguageChange(false);
|
||||
if (this.translatedTo != aTo)
|
||||
TranslationHealthReport.recordTargetLanguageChange();
|
||||
}
|
||||
|
||||
this.state = Translation.STATE_TRANSLATING;
|
||||
this.translatedFrom = aFrom;
|
||||
this.translatedTo = aTo;
|
||||
@ -184,6 +194,7 @@ TranslationUI.prototype = {
|
||||
this.originalShown = true;
|
||||
this.showURLBarIcon();
|
||||
this.browser.messageManager.sendAsyncMessage("Translation:ShowOriginal");
|
||||
TranslationHealthReport.recordShowOriginalContent();
|
||||
},
|
||||
|
||||
showTranslatedContent: function() {
|
||||
@ -255,6 +266,11 @@ TranslationUI.prototype = {
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
infobarClosed: function() {
|
||||
if (this.state == Translation.STATE_OFFER)
|
||||
TranslationHealthReport.recordDeniedTranslationOffer();
|
||||
}
|
||||
};
|
||||
|
||||
@ -297,7 +313,7 @@ let TranslationHealthReport = {
|
||||
|
||||
/**
|
||||
* Record a change of the detected language in the health report. This should
|
||||
* only be called when actually executing a translation not every time the
|
||||
* only be called when actually executing a translation, not every time the
|
||||
* user changes in the language in the UI.
|
||||
*
|
||||
* @param beforeFirstTranslation
|
||||
@ -307,8 +323,17 @@ let TranslationHealthReport = {
|
||||
* the user has manually adjusted the detected language false should
|
||||
* be passed.
|
||||
*/
|
||||
recordLanguageChange: function (beforeFirstTranslation) {
|
||||
this._withProvider(provider => provider.recordLanguageChange(beforeFirstTranslation));
|
||||
recordDetectedLanguageChange: function (beforeFirstTranslation) {
|
||||
this._withProvider(provider => provider.recordDetectedLanguageChange(beforeFirstTranslation));
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a change of the target language in the health report. This should
|
||||
* only be called when actually executing a translation, not every time the
|
||||
* user changes in the language in the UI.
|
||||
*/
|
||||
recordTargetLanguageChange: function () {
|
||||
this._withProvider(provider => provider.recordTargetLanguageChange());
|
||||
},
|
||||
|
||||
/**
|
||||
@ -318,6 +343,13 @@ let TranslationHealthReport = {
|
||||
this._withProvider(provider => provider.recordDeniedTranslationOffer());
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a "Show Original" command use.
|
||||
*/
|
||||
recordShowOriginalContent: function () {
|
||||
this._withProvider(provider => provider.recordShowOriginalContent());
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the translation provider and pass it to the given function.
|
||||
*
|
||||
@ -376,7 +408,9 @@ TranslationMeasurement1.prototype = Object.freeze({
|
||||
pageTranslatedCountsByLanguage: DAILY_LAST_TEXT_FIELD,
|
||||
detectedLanguageChangedBefore: DAILY_COUNTER_FIELD,
|
||||
detectedLanguageChangedAfter: DAILY_COUNTER_FIELD,
|
||||
targetLanguageChanged: DAILY_COUNTER_FIELD,
|
||||
deniedTranslationOffer: DAILY_COUNTER_FIELD,
|
||||
showOriginalContent: DAILY_COUNTER_FIELD,
|
||||
detectLanguageEnabled: DAILY_LAST_NUMERIC_FIELD,
|
||||
showTranslationUI: DAILY_LAST_NUMERIC_FIELD,
|
||||
},
|
||||
@ -500,7 +534,7 @@ TranslationProvider.prototype = Object.freeze({
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
recordLanguageChange: function (beforeFirstTranslation) {
|
||||
recordDetectedLanguageChange: function (beforeFirstTranslation) {
|
||||
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
|
||||
TranslationMeasurement1.prototype.version);
|
||||
|
||||
@ -513,6 +547,15 @@ TranslationProvider.prototype = Object.freeze({
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
recordTargetLanguageChange: function () {
|
||||
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
|
||||
TranslationMeasurement1.prototype.version);
|
||||
|
||||
return this._enqueueTelemetryStorageTask(function* recordTask() {
|
||||
yield m.incrementDailyCounter("targetLanguageChanged");
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
recordDeniedTranslationOffer: function () {
|
||||
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
|
||||
TranslationMeasurement1.prototype.version);
|
||||
@ -522,6 +565,15 @@ TranslationProvider.prototype = Object.freeze({
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
recordShowOriginalContent: function () {
|
||||
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
|
||||
TranslationMeasurement1.prototype.version);
|
||||
|
||||
return this._enqueueTelemetryStorageTask(function* recordTask() {
|
||||
yield m.incrementDailyCounter("showOriginalContent");
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
collectDailyData: function () {
|
||||
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
|
||||
TranslationMeasurement1.prototype.version);
|
||||
|
@ -85,8 +85,15 @@ TranslationContentHandler.prototype = {
|
||||
return;
|
||||
|
||||
LanguageDetector.detectLanguage(string).then(result => {
|
||||
if (!result.confident)
|
||||
// Bail if we're not confident.
|
||||
if (!result.confident) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The window might be gone by now.
|
||||
if (Cu.isDeadWrapper(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
content.detectedLanguage = result.language;
|
||||
|
||||
|
@ -7,6 +7,62 @@ let tmp = {};
|
||||
Cu.import("resource:///modules/translation/Translation.jsm", tmp);
|
||||
let {Translation} = tmp;
|
||||
|
||||
let MetricsChecker = {
|
||||
_metricsTime: new Date(),
|
||||
_midnightError: new Error("Getting metrics around midnight may fail sometimes"),
|
||||
|
||||
updateMetrics: Task.async(function* () {
|
||||
let svc = Cc["@mozilla.org/datareporting/service;1"].getService();
|
||||
let reporter = svc.wrappedJSObject.healthReporter;
|
||||
yield reporter.onInit();
|
||||
|
||||
// Get the provider.
|
||||
let provider = reporter.getProvider("org.mozilla.translation");
|
||||
let measurement = provider.getMeasurement("translation", 1);
|
||||
let values = yield measurement.getValues();
|
||||
|
||||
let metricsTime = new Date();
|
||||
let day = values.days.getDay(metricsTime);
|
||||
if (!day) {
|
||||
// This should never happen except when the test runs at midnight.
|
||||
throw this._midnightError;
|
||||
}
|
||||
|
||||
// .get() may return `undefined`, which we can't compute.
|
||||
this._metrics = {
|
||||
pageCount: day.get("pageTranslatedCount") || 0,
|
||||
charCount: day.get("charactersTranslatedCount") || 0,
|
||||
deniedOffers: day.get("deniedTranslationOffer") || 0,
|
||||
showOriginal: day.get("showOriginalContent") || 0,
|
||||
detectedLanguageChangedBefore: day.get("detectedLanguageChangedBefore") || 0,
|
||||
detectedLanguageChangeAfter: day.get("detectedLanguageChangedAfter") || 0,
|
||||
targetLanguageChanged: day.get("targetLanguageChanged") || 0
|
||||
};
|
||||
this._metricsTime = metricsTime;
|
||||
}),
|
||||
|
||||
checkAdditions: Task.async(function* (additions) {
|
||||
let prevMetrics = this._metrics, prevMetricsTime = this._metricsTime;
|
||||
try {
|
||||
yield this.updateMetrics();
|
||||
} catch(ex if ex == this._midnightError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that it's still the same day of the month as when we started. This
|
||||
// prevents intermittent failures when the test starts before and ends after
|
||||
// midnight.
|
||||
if (this._metricsTime.getDate() != prevMetricsTime.getDate()) {
|
||||
for (let metric of Object.keys(prevMetrics)) {
|
||||
prevMetrics[metric] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (let metric of Object.keys(additions)) {
|
||||
Assert.equal(prevMetrics[metric] + additions[metric], this._metrics[metric]);
|
||||
}
|
||||
})
|
||||
};
|
||||
add_task(function* setup() {
|
||||
Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
|
||||
Services.prefs.setBoolPref("browser.translation.detectLanguage", true);
|
||||
@ -17,53 +73,100 @@ add_task(function* setup() {
|
||||
Services.prefs.clearUserPref("browser.translation.detectLanguage");
|
||||
Services.prefs.clearUserPref("browser.translation.ui.show");
|
||||
});
|
||||
|
||||
// Make sure there are some initial metrics in place when the test starts.
|
||||
yield translate("<h1>Hallo Welt!</h1>", "de");
|
||||
yield MetricsChecker.updateMetrics();
|
||||
});
|
||||
|
||||
add_task(function* test_fhr() {
|
||||
let start = new Date();
|
||||
|
||||
// Translate a page.
|
||||
yield translate("<h1>Hallo Welt!</h1>", "de", "en");
|
||||
let [pageCount, charCount] = yield retrieveTranslationCounts();
|
||||
yield translate("<h1>Hallo Welt!</h1>", "de");
|
||||
|
||||
// Translate another page.
|
||||
yield translate("<h1>Hallo Welt!</h1><h1>Bratwurst!</h1>", "de", "en");
|
||||
|
||||
let [pageCount2, charCount2] = yield retrieveTranslationCounts();
|
||||
|
||||
// Check that it's still the same day of the month as when we started. This
|
||||
// prevents intermittent failures when the test starts before and ends after
|
||||
// midnight.
|
||||
if (start.getDate() == new Date().getDate()) {
|
||||
Assert.equal(pageCount2, pageCount + 1);
|
||||
Assert.equal(charCount2, charCount + 21);
|
||||
}
|
||||
yield translate("<h1>Hallo Welt!</h1><h1>Bratwurst!</h1>", "de");
|
||||
yield MetricsChecker.checkAdditions({ pageCount: 1, charCount: 21, deniedOffers: 0});
|
||||
});
|
||||
|
||||
function retrieveTranslationCounts() {
|
||||
return Task.spawn(function* task_retrieve_counts() {
|
||||
let svc = Cc["@mozilla.org/datareporting/service;1"].getService();
|
||||
let reporter = svc.wrappedJSObject.healthReporter;
|
||||
yield reporter.onInit();
|
||||
add_task(function* test_deny_translation_metric() {
|
||||
function* offerAndDeny(elementAnonid) {
|
||||
let tab = yield offerTranslatationFor("<h1>Hallo Welt!</h1>", "de", "en");
|
||||
getInfobarElement(tab.linkedBrowser, elementAnonid).doCommand();
|
||||
yield MetricsChecker.checkAdditions({ deniedOffers: 1 });
|
||||
gBrowser.removeTab(tab);
|
||||
}
|
||||
|
||||
// Get the provider.
|
||||
let provider = reporter.getProvider("org.mozilla.translation");
|
||||
let measurement = provider.getMeasurement("translation", 1);
|
||||
let values = yield measurement.getValues();
|
||||
yield offerAndDeny("notNow");
|
||||
yield offerAndDeny("neverForSite");
|
||||
yield offerAndDeny("neverForLanguage");
|
||||
yield offerAndDeny("closeButton");
|
||||
|
||||
let day = values.days.getDay(new Date());
|
||||
if (!day) {
|
||||
// This should never happen except when the test runs at midnight.
|
||||
return [0, 0];
|
||||
}
|
||||
// Test that the close button doesn't record a denied translation if
|
||||
// the infobar is not in its "offer" state.
|
||||
let tab = yield translate("<h1>Hallo Welt!</h1>", "de", false);
|
||||
yield MetricsChecker.checkAdditions({ deniedOffers: 0 });
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
// .get() may return `undefined`, which we can't compute.
|
||||
return [day.get("pageTranslatedCount") || 0, day.get("charactersTranslatedCount") || 0];
|
||||
add_task(function* test_show_original() {
|
||||
let tab =
|
||||
yield translate("<h1>Hallo Welt!</h1><h1>Bratwurst!</h1>", "de", false);
|
||||
yield MetricsChecker.checkAdditions({ pageCount: 1, showOriginal: 0 });
|
||||
getInfobarElement(tab.linkedBrowser, "showOriginal").doCommand();
|
||||
yield MetricsChecker.checkAdditions({ pageCount: 0, showOriginal: 1 });
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(function* test_language_change() {
|
||||
for (let i of Array(4)) {
|
||||
let tab = yield offerTranslatationFor("<h1>Hallo Welt!</h1>", "fr");
|
||||
let browser = tab.linkedBrowser;
|
||||
// In the offer state, translation is executed by the Translate button,
|
||||
// so we expect just a single recoding.
|
||||
let detectedLangMenulist = getInfobarElement(browser, "detectedLanguage");
|
||||
simulateUserSelectInMenulist(detectedLangMenulist, "de");
|
||||
simulateUserSelectInMenulist(detectedLangMenulist, "it");
|
||||
simulateUserSelectInMenulist(detectedLangMenulist, "de");
|
||||
yield acceptTranslationOffer(tab);
|
||||
|
||||
// In the translated state, a change in the form or to menulists
|
||||
// triggers re-translation right away.
|
||||
let fromLangMenulist = getInfobarElement(browser, "fromLanguage");
|
||||
simulateUserSelectInMenulist(fromLangMenulist, "it");
|
||||
simulateUserSelectInMenulist(fromLangMenulist, "de");
|
||||
|
||||
// Selecting the same item shouldn't count.
|
||||
simulateUserSelectInMenulist(fromLangMenulist, "de");
|
||||
|
||||
let toLangMenulist = getInfobarElement(browser, "toLanguage");
|
||||
simulateUserSelectInMenulist(toLangMenulist, "fr");
|
||||
simulateUserSelectInMenulist(toLangMenulist, "en");
|
||||
simulateUserSelectInMenulist(toLangMenulist, "it");
|
||||
|
||||
// Selecting the same item shouldn't count.
|
||||
simulateUserSelectInMenulist(toLangMenulist, "it");
|
||||
|
||||
// Setting the target language to the source language is a no-op,
|
||||
// so it shouldn't count.
|
||||
simulateUserSelectInMenulist(toLangMenulist, "de");
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
}
|
||||
yield MetricsChecker.checkAdditions({
|
||||
detectedLanguageChangedBefore: 4,
|
||||
detectedLanguageChangeAfter: 8,
|
||||
targetLanguageChanged: 12
|
||||
});
|
||||
});
|
||||
|
||||
function getInfobarElement(browser, anonid) {
|
||||
let notif = browser.translationUI
|
||||
.notificationBox.getNotificationWithValue("translation");
|
||||
return notif._getAnonElt(anonid);
|
||||
}
|
||||
|
||||
function translate(text, from, to) {
|
||||
return Task.spawn(function* task_translate() {
|
||||
function offerTranslatationFor(text, from) {
|
||||
return Task.spawn(function* task_offer_translation() {
|
||||
// Create some content to translate.
|
||||
let tab = gBrowser.selectedTab =
|
||||
gBrowser.addTab("data:text/html;charset=utf-8," + text);
|
||||
@ -77,12 +180,27 @@ function translate(text, from, to) {
|
||||
originalShown: true,
|
||||
detectedLanguage: from});
|
||||
|
||||
// Translate the page.
|
||||
browser.translationUI.translate(from, to);
|
||||
yield waitForMessage(browser, "Translation:Finished");
|
||||
return tab;
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
function acceptTranslationOffer(tab) {
|
||||
return Task.spawn(function* task_accept_translation_offer() {
|
||||
let browser = tab.linkedBrowser;
|
||||
getInfobarElement(browser, "translate").doCommand();
|
||||
yield waitForMessage(browser, "Translation:Finished");
|
||||
});
|
||||
}
|
||||
|
||||
function translate(text, from, closeTab = true) {
|
||||
return Task.spawn(function* task_translate() {
|
||||
let tab = yield offerTranslatationFor(text, from);
|
||||
yield acceptTranslationOffer(tab);
|
||||
if (closeTab) {
|
||||
gBrowser.removeTab(tab);
|
||||
} else {
|
||||
return tab;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -105,3 +223,8 @@ function promiseBrowserLoaded(browser) {
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
function simulateUserSelectInMenulist(menulist, value) {
|
||||
menulist.value = value;
|
||||
menulist.doCommand();
|
||||
}
|
||||
|
@ -173,13 +173,16 @@ add_task(function* test_record_translation() {
|
||||
yield provider.init(storage);
|
||||
let now = new Date();
|
||||
|
||||
// Record a language change before translation.
|
||||
yield provider.recordLanguageChange(true);
|
||||
// Record a change to the source language changes before translation.
|
||||
yield provider.recordDetectedLanguageChange(true);
|
||||
|
||||
// Record two language changes after translation.
|
||||
yield provider.recordLanguageChange(false);
|
||||
yield provider.recordLanguageChange(false);
|
||||
// Record two changes to the source language changes after translation.
|
||||
yield provider.recordDetectedLanguageChange(false);
|
||||
yield provider.recordDetectedLanguageChange(false);
|
||||
|
||||
// Record two changes to the target language.
|
||||
yield provider.recordTargetLanguageChange();
|
||||
yield provider.recordTargetLanguageChange();
|
||||
|
||||
let m = provider.getMeasurement("translation", 1);
|
||||
let values = yield m.getValues();
|
||||
@ -189,21 +192,24 @@ add_task(function* test_record_translation() {
|
||||
|
||||
Assert.ok(day.has("detectedLanguageChangedBefore"));
|
||||
Assert.equal(day.get("detectedLanguageChangedBefore"), 1);
|
||||
|
||||
Assert.ok(day.has("detectedLanguageChangedAfter"));
|
||||
Assert.equal(day.get("detectedLanguageChangedAfter"), 2);
|
||||
Assert.ok(day.has("targetLanguageChanged"));
|
||||
Assert.equal(day.get("targetLanguageChanged"), 2);
|
||||
|
||||
yield provider.shutdown();
|
||||
yield storage.close();
|
||||
});
|
||||
|
||||
add_task(function* test_denied_translation_offer() {
|
||||
function* test_simple_counter(aProviderFuncName, aCounterName) {
|
||||
let storage = yield Metrics.Storage("translation");
|
||||
let provider = new TranslationProvider();
|
||||
yield provider.init(storage);
|
||||
let now = new Date();
|
||||
|
||||
yield provider.recordDeniedTranslationOffer();
|
||||
yield provider.recordDeniedTranslationOffer();
|
||||
yield provider[aProviderFuncName]();
|
||||
yield provider[aProviderFuncName]();
|
||||
|
||||
let m = provider.getMeasurement("translation", 1);
|
||||
let values = yield m.getValues();
|
||||
@ -211,11 +217,19 @@ add_task(function* test_denied_translation_offer() {
|
||||
Assert.ok(values.days.hasDay(now));
|
||||
let day = values.days.getDay(now);
|
||||
|
||||
Assert.ok(day.has("deniedTranslationOffer"));
|
||||
Assert.equal(day.get("deniedTranslationOffer"), 2);
|
||||
Assert.ok(day.has(aCounterName));
|
||||
Assert.equal(day.get(aCounterName), 2);
|
||||
|
||||
yield provider.shutdown();
|
||||
yield storage.close();
|
||||
}
|
||||
|
||||
add_task(function* test_denied_translation_offer() {
|
||||
yield test_simple_counter("recordDeniedTranslationOffer", "deniedTranslationOffer");
|
||||
});
|
||||
|
||||
add_task(function* test_show_original() {
|
||||
yield test_simple_counter("recordShowOriginalContent", "showOriginalContent");
|
||||
});
|
||||
|
||||
add_task(function* test_collect_daily() {
|
||||
@ -268,15 +282,17 @@ add_task(function* test_healthreporter_json() {
|
||||
yield reporter._providerManager.registerProvider(provider);
|
||||
|
||||
yield provider.recordTranslationOpportunity("fr", now);
|
||||
yield provider.recordLanguageChange(true);
|
||||
yield provider.recordDetectedLanguageChange(true);
|
||||
yield provider.recordTranslation("fr", "en", 1000, now);
|
||||
yield provider.recordLanguageChange(false);
|
||||
yield provider.recordDetectedLanguageChange(false);
|
||||
|
||||
yield provider.recordTranslationOpportunity("es", now);
|
||||
yield provider.recordTranslation("es", "en", 1000, now);
|
||||
|
||||
yield provider.recordDeniedTranslationOffer();
|
||||
|
||||
yield provider.recordShowOriginalContent();
|
||||
|
||||
yield reporter.collectMeasurements();
|
||||
let payload = yield reporter.getJSONPayload(true);
|
||||
let today = reporter._formatDate(now);
|
||||
@ -312,6 +328,9 @@ add_task(function* test_healthreporter_json() {
|
||||
|
||||
Assert.ok("deniedTranslationOffer" in translations);
|
||||
Assert.equal(translations["deniedTranslationOffer"], 1);
|
||||
|
||||
Assert.ok("showOriginalContent" in translations);
|
||||
Assert.equal(translations["showOriginalContent"], 1);
|
||||
} finally {
|
||||
reporter._shutdown();
|
||||
}
|
||||
@ -329,15 +348,17 @@ add_task(function* test_healthreporter_json2() {
|
||||
yield reporter._providerManager.registerProvider(provider);
|
||||
|
||||
yield provider.recordTranslationOpportunity("fr", now);
|
||||
yield provider.recordLanguageChange(true);
|
||||
yield provider.recordDetectedLanguageChange(true);
|
||||
yield provider.recordTranslation("fr", "en", 1000, now);
|
||||
yield provider.recordLanguageChange(false);
|
||||
yield provider.recordDetectedLanguageChange(false);
|
||||
|
||||
yield provider.recordTranslationOpportunity("es", now);
|
||||
yield provider.recordTranslation("es", "en", 1000, now);
|
||||
|
||||
yield provider.recordDeniedTranslationOffer();
|
||||
|
||||
yield provider.recordShowOriginalContent();
|
||||
|
||||
yield reporter.collectMeasurements();
|
||||
let payload = yield reporter.getJSONPayload(true);
|
||||
let today = reporter._formatDate(now);
|
||||
@ -357,6 +378,7 @@ add_task(function* test_healthreporter_json2() {
|
||||
Assert.ok(!("detectedLanguageChangedBefore" in translations));
|
||||
Assert.ok(!("detectedLanguageChangedAfter" in translations));
|
||||
Assert.ok(!("deniedTranslationOffer" in translations));
|
||||
Assert.ok(!("showOriginalContent" in translations));
|
||||
} finally {
|
||||
reporter._shutdown();
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
oncommand="document.getBindingParent(this).translate();"/>
|
||||
<xul:button class="translate-infobar-element"
|
||||
label="&translation.notNow.button;" anonid="notNow"
|
||||
oncommand="document.getBindingParent(this).close();"/>
|
||||
oncommand="document.getBindingParent(this).closeCommand();"/>
|
||||
</xul:hbox>
|
||||
|
||||
<!-- translating -->
|
||||
@ -122,10 +122,11 @@
|
||||
|
||||
</xul:hbox>
|
||||
<xul:toolbarbutton ondblclick="event.stopPropagation();"
|
||||
anonid="closeButton"
|
||||
class="messageCloseButton close-icon tabbable"
|
||||
xbl:inherits="hidden=hideclose"
|
||||
tooltiptext="&closeNotification.tooltip;"
|
||||
oncommand="document.getBindingParent(this).close();"/>
|
||||
oncommand="document.getBindingParent(this).closeCommand();"/>
|
||||
</xul:hbox>
|
||||
</content>
|
||||
<implementation>
|
||||
@ -216,6 +217,16 @@
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<!-- To be called when the infobar should be closed per user's wish (e.g.
|
||||
by clicking the notification's close button -->
|
||||
<method name="closeCommand">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.close();
|
||||
this.translation.infobarClosed();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
<method name="_handleButtonHiding">
|
||||
<body>
|
||||
<![CDATA[
|
||||
@ -301,7 +312,7 @@
|
||||
|
||||
Services.prefs.setCharPref(kPrefName, val);
|
||||
|
||||
this.close();
|
||||
this.closeCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
@ -313,7 +324,7 @@
|
||||
let perms = Services.perms;
|
||||
perms.add(uri, "translate", perms.DENY_ACTION);
|
||||
|
||||
this.close();
|
||||
this.closeCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -455,7 +455,7 @@
|
||||
@BINPATH@/components/FormHistoryStartup.js
|
||||
@BINPATH@/components/nsInputListAutoComplete.js
|
||||
@BINPATH@/components/formautofill.manifest
|
||||
@BINPATH@/components/AutofillController.js
|
||||
@BINPATH@/components/FormAutofillContentService.js
|
||||
@BINPATH@/components/contentSecurityPolicy.manifest
|
||||
@BINPATH@/components/contentSecurityPolicy.js
|
||||
@BINPATH@/components/contentAreaDropListener.manifest
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
|
||||
<!ENTITY prefWindow.titleWin "Options">
|
||||
<!ENTITY prefWindow.title "Preferences">
|
||||
<!-- LOCALIZATION NOTE (prefWindow.titleGNOME): This is not used for in-content preferences -->
|
||||
<!ENTITY prefWindow.titleGNOME "&brandShortName; Preferences">
|
||||
<!-- When making changes to prefWindow.styleWin test both Windows Classic and
|
||||
Luna since widget heights are different based on the OS theme -->
|
||||
|
@ -29,7 +29,7 @@
|
||||
#include "nsAutoPtr.h"
|
||||
#include "nsTArray.h"
|
||||
#include "nsIMutableArray.h"
|
||||
#include "nsIAutofillController.h"
|
||||
#include "nsIFormAutofillContentService.h"
|
||||
|
||||
// form submission
|
||||
#include "nsIFormSubmitObserver.h"
|
||||
@ -306,10 +306,12 @@ void
|
||||
HTMLFormElement::RequestAutocomplete()
|
||||
{
|
||||
bool dummy;
|
||||
nsCOMPtr<nsIDOMWindow> win = do_QueryInterface(OwnerDoc()->GetScriptHandlingObject(dummy));
|
||||
nsCOMPtr<nsIAutofillController> controller(do_GetService("@mozilla.org/autofill-controller;1"));
|
||||
nsCOMPtr<nsIDOMWindow> window =
|
||||
do_QueryInterface(OwnerDoc()->GetScriptHandlingObject(dummy));
|
||||
nsCOMPtr<nsIFormAutofillContentService> formAutofillContentService =
|
||||
do_GetService("@mozilla.org/formautofill/content-service;1");
|
||||
|
||||
if (!controller || !win) {
|
||||
if (!formAutofillContentService || !window) {
|
||||
AutocompleteErrorEventInit init;
|
||||
init.mBubbles = true;
|
||||
init.mCancelable = false;
|
||||
@ -317,11 +319,12 @@ HTMLFormElement::RequestAutocomplete()
|
||||
|
||||
nsRefPtr<AutocompleteErrorEvent> event =
|
||||
AutocompleteErrorEvent::Constructor(this, NS_LITERAL_STRING("autocompleteerror"), init);
|
||||
|
||||
(new AsyncEventDispatcher(this, event))->PostDOMEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
controller->RequestAutocomplete(this, win);
|
||||
formAutofillContentService->RequestAutocomplete(this, window);
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -379,7 +379,7 @@
|
||||
@BINPATH@/components/FormHistoryStartup.js
|
||||
@BINPATH@/components/nsInputListAutoComplete.js
|
||||
@BINPATH@/components/formautofill.manifest
|
||||
@BINPATH@/components/AutofillController.js
|
||||
@BINPATH@/components/FormAutofillContentService.js
|
||||
@BINPATH@/components/contentSecurityPolicy.manifest
|
||||
@BINPATH@/components/contentSecurityPolicy.js
|
||||
@BINPATH@/components/contentAreaDropListener.manifest
|
||||
|
@ -1556,10 +1556,16 @@ detectedLanguageChangedBefore
|
||||
detectedLanguageChangedAfter
|
||||
Integer count of the number of times the user manually adjusted the detected
|
||||
language after having first translated the page.
|
||||
targetLanguageChanged
|
||||
Integer count of the number of times the user manually adjusted the target
|
||||
language.
|
||||
deniedTranslationOffer
|
||||
Integer count of the numbers of times the user opted-out offered
|
||||
Integer count of the number of times the user opted-out offered
|
||||
page translation, either by the Not Now button or by the notification's
|
||||
close button in the "offer" state.
|
||||
showOriginalContent
|
||||
Integer count of the number of times the user activated the Show Original
|
||||
command.
|
||||
|
||||
Additional daily counts broken down by language are reported in the following
|
||||
properties:
|
||||
@ -1597,7 +1603,9 @@ Example
|
||||
"charactersTranslatedCount": "1126",
|
||||
"detectedLanguageChangedBefore": 1,
|
||||
"detectedLanguageChangedAfter": 2,
|
||||
"deniedTranslationOffer": 3
|
||||
"targetLanguageChanged": 0,
|
||||
"deniedTranslationOffer": 3,
|
||||
"showOriginalContent": 2,
|
||||
"translationOpportunityCountsByLanguage": {
|
||||
"fr": 100,
|
||||
"es": 34
|
||||
|
@ -1,32 +0,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/. */
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
function AutofillController() {}
|
||||
|
||||
AutofillController.prototype = {
|
||||
_dispatchAsync: function (fn) {
|
||||
Services.tm.currentThread.dispatch(fn, Ci.nsIThread.DISPATCH_NORMAL);
|
||||
},
|
||||
|
||||
_dispatchDisabled: function (form, win, message) {
|
||||
Services.console.logStringMessage("requestAutocomplete disabled: " + message);
|
||||
let evt = new win.AutocompleteErrorEvent("autocompleteerror", { bubbles: true, reason: "disabled" });
|
||||
form.dispatchEvent(evt);
|
||||
},
|
||||
|
||||
requestAutocomplete: function (form, win) {
|
||||
this._dispatchAsync(() => this._dispatchDisabled(form, win, "not implemented"));
|
||||
},
|
||||
|
||||
classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutofillController])
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AutofillController]);
|
@ -0,0 +1,41 @@
|
||||
/* 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/. */
|
||||
|
||||
/*
|
||||
* Implements a service used by DOM content to request Form Autofill, in
|
||||
* particular when the requestAutocomplete method of Form objects is invoked.
|
||||
*
|
||||
* See the nsIFormAutofillContentService documentation for details.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
function FormAutofillContentService() {
|
||||
}
|
||||
|
||||
FormAutofillContentService.prototype = {
|
||||
classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutofillContentService]),
|
||||
|
||||
// nsIFormAutofillContentService
|
||||
requestAutocomplete: function (aForm, aWindow) {
|
||||
Services.console.logStringMessage("requestAutocomplete not implemented.");
|
||||
|
||||
// We will return "disabled" for now.
|
||||
let event = new aWindow.AutocompleteErrorEvent("autocompleteerror",
|
||||
{ bubbles: true,
|
||||
reason: "disabled" });
|
||||
|
||||
// Ensure the event is always dispatched on the next tick.
|
||||
Services.tm.currentThread.dispatch(() => aForm.dispatchEvent(event),
|
||||
Ci.nsIThread.DISPATCH_NORMAL);
|
||||
},
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillContentService]);
|
@ -1,2 +1,2 @@
|
||||
component {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6} AutofillController.js
|
||||
contract @mozilla.org/autofill-controller;1 {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}
|
||||
component {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6} FormAutofillContentService.js
|
||||
contract @mozilla.org/formautofill/content-service;1 {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}
|
||||
|
@ -18,12 +18,12 @@ XPCSHELL_TESTS_MANIFESTS += [
|
||||
]
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIAutofillController.idl',
|
||||
'nsIFormAutofillContentService.idl',
|
||||
]
|
||||
|
||||
XPIDL_MODULE = 'toolkit_formautofill'
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'AutofillController.js',
|
||||
'formautofill.manifest',
|
||||
'FormAutofillContentService.js',
|
||||
]
|
||||
|
@ -1,14 +0,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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
interface nsIDOMHTMLFormElement;
|
||||
interface nsIDOMWindow;
|
||||
|
||||
[scriptable, uuid(cbf47f9d-a13d-4fad-8221-8964086b1b8a)]
|
||||
interface nsIAutofillController : nsISupports
|
||||
{
|
||||
void requestAutocomplete(in nsIDOMHTMLFormElement form, in nsIDOMWindow window);
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
/* 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 nsIDOMHTMLFormElement;
|
||||
interface nsIDOMWindow;
|
||||
|
||||
/**
|
||||
* Defines a service used by DOM content to request Form Autofill, in particular
|
||||
* when the requestAutocomplete method of Form objects is invoked.
|
||||
*
|
||||
* This service lives in the process that hosts the requesting DOM content.
|
||||
* This means that, in a multi-process (e10s) environment, there can be an
|
||||
* instance of the service for each content process, in addition to an instance
|
||||
* for the chrome process.
|
||||
*
|
||||
* @remarks The service implementation uses a child-side message manager to
|
||||
* communicate with a parent-side message manager living in the chrome
|
||||
* process, where most of the processing is located.
|
||||
*/
|
||||
[scriptable, uuid(1db29340-99df-4845-9102-0c5d281b2fe8)]
|
||||
interface nsIFormAutofillContentService : nsISupports
|
||||
{
|
||||
/**
|
||||
* Invoked by the requestAutocomplete method of the DOM Form object.
|
||||
*
|
||||
* The application is expected to display a user interface asking for the
|
||||
* details that are relevant to the form being filled in. The application
|
||||
* should use the "autocomplete" attributes on the input elements as hints
|
||||
* about which type of information is being requested.
|
||||
*
|
||||
* The processing will result in either an "autocomplete" simple DOM Event or
|
||||
* an AutocompleteErrorEvent being fired on the form.
|
||||
*
|
||||
* @param aForm
|
||||
* The form on which the requestAutocomplete method was invoked.
|
||||
* @param aWindow
|
||||
* The window where the form is located. This must be specified even
|
||||
* for elements that are not in a document, and is used to generate the
|
||||
* DOM events resulting from the operation.
|
||||
*/
|
||||
void requestAutocomplete(in nsIDOMHTMLFormElement aForm,
|
||||
in nsIDOMWindow aWindow);
|
||||
};
|
@ -4,3 +4,4 @@ support-files =
|
||||
head.js
|
||||
|
||||
[test_infrastructure.html]
|
||||
[test_requestAutocomplete_disabled.html]
|
||||
|
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE HTML><html><head><meta charset="utf-8"></head><body>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
|
||||
<form id="form">
|
||||
</form>
|
||||
|
||||
<script type="application/javascript;version=1.7" src="head.js"></script>
|
||||
<script type="application/javascript;version=1.7">
|
||||
|
||||
/*
|
||||
* Tests the cases where the requestAutocomplete method returns "disabled".
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Tests the case where the feature is disabled globally.
|
||||
*/
|
||||
add_task(function* test_disabled_globally() {
|
||||
let promise = TestUtils.waitForEvent($("form"), "autocompleteerror");
|
||||
$("form").requestAutocomplete();
|
||||
let errorEvent = yield promise;
|
||||
|
||||
Assert.equal(errorEvent.reason, "disabled");
|
||||
});
|
||||
|
||||
add_task(terminationTaskFn);
|
||||
|
||||
</script>
|
||||
</body></html>
|
@ -1026,7 +1026,7 @@
|
||||
*
|
||||
* Note: This function will remove a symlink even if it points a directory.
|
||||
*/
|
||||
File.removeDir = function(path, options) {
|
||||
File.removeDir = function(path, options = {}) {
|
||||
let isSymLink;
|
||||
try {
|
||||
let info = File.stat(path, {unixNoFollowingLinks: true});
|
||||
|
@ -1031,7 +1031,7 @@
|
||||
* @throws {OS.File.Error} In case of I/O error, in particular if |path| is
|
||||
* not a directory.
|
||||
*/
|
||||
File.removeDir = function(path, options) {
|
||||
File.removeDir = function(path, options = {}) {
|
||||
// We can't use File.stat here because it will follow the symlink.
|
||||
let attributes = WinFile.GetFileAttributes(path);
|
||||
if (attributes == Const.INVALID_FILE_ATTRIBUTES) {
|
||||
|
@ -3522,14 +3522,6 @@
|
||||
"kind": "boolean",
|
||||
"description": "Session restore: Whether the file read on startup contained parse-able JSON"
|
||||
},
|
||||
"FX_SESSION_RESTORE_BACKUP_FILE_MS": {
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": "30000",
|
||||
"n_buckets": 10,
|
||||
"extended_statistics_ok": true,
|
||||
"description": "Session restore: Time to make a backup copy of the session file (ms)"
|
||||
},
|
||||
"FX_SESSION_RESTORE_RESTORE_WINDOW_MS": {
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
|
@ -622,6 +622,10 @@ function isToolbarItem(aElt)
|
||||
|
||||
function onToolbarDragExit(aEvent)
|
||||
{
|
||||
if (isUnwantedDragEvent(aEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gCurrentDragOverItem)
|
||||
setDragActive(gCurrentDragOverItem, false);
|
||||
}
|
||||
@ -645,6 +649,10 @@ function onToolbarDragStart(aEvent)
|
||||
|
||||
function onToolbarDragOver(aEvent)
|
||||
{
|
||||
if (isUnwantedDragEvent(aEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var documentId = gToolboxDocument.documentElement.id;
|
||||
if (!aEvent.dataTransfer.types.contains("text/toolbarwrapper-id/" + documentId.toLowerCase()))
|
||||
return;
|
||||
@ -697,6 +705,10 @@ function onToolbarDragOver(aEvent)
|
||||
|
||||
function onToolbarDrop(aEvent)
|
||||
{
|
||||
if (isUnwantedDragEvent(aEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gCurrentDragOverItem)
|
||||
return;
|
||||
|
||||
@ -767,13 +779,19 @@ function onToolbarDrop(aEvent)
|
||||
|
||||
function onPaletteDragOver(aEvent)
|
||||
{
|
||||
if (isUnwantedDragEvent(aEvent)) {
|
||||
return;
|
||||
}
|
||||
var documentId = gToolboxDocument.documentElement.id;
|
||||
if (aEvent.dataTransfer.types.contains("text/toolbarwrapper-id/" + documentId.toLowerCase()))
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
|
||||
function onPaletteDrop(aEvent)
|
||||
{
|
||||
{
|
||||
if (isUnwantedDragEvent(aEvent)) {
|
||||
return;
|
||||
}
|
||||
var documentId = gToolboxDocument.documentElement.id;
|
||||
var itemId = aEvent.dataTransfer.getData("text/toolbarwrapper-id/" + documentId);
|
||||
|
||||
@ -798,3 +816,18 @@ function onPaletteDrop(aEvent)
|
||||
|
||||
toolboxChanged();
|
||||
}
|
||||
|
||||
|
||||
function isUnwantedDragEvent(aEvent) {
|
||||
/* Discard drag events that originated from a separate window to
|
||||
prevent content->chrome privilege escalations. */
|
||||
let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
|
||||
// mozSourceNode is null in the dragStart event handler or if
|
||||
// the drag event originated in an external application.
|
||||
if (!mozSourceNode) {
|
||||
return true;
|
||||
}
|
||||
let sourceWindow = mozSourceNode.ownerDocument.defaultView;
|
||||
return sourceWindow != window && sourceWindow != gToolboxDocument.defaultView;
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,10 @@ function test_socket_shutdown()
|
||||
|
||||
onClosed: function(aStatus) {
|
||||
do_print("test_socket_shutdown onClosed called at " + new Date().toTimeString());
|
||||
do_check_eq(aStatus, Cr.NS_ERROR_CONNECTION_REFUSED);
|
||||
// The connection should be refused here, but on slow or overloaded
|
||||
// machines it may just time out.
|
||||
let expected = [ Cr.NS_ERROR_CONNECTION_REFUSED, Cr.NS_ERROR_NET_TIMEOUT ];
|
||||
do_check_neq(expected.indexOf(aStatus), -1);
|
||||
run_next_test();
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user