Merge mozilla-central and fx-team

This commit is contained in:
Ed Morley 2014-06-24 17:24:53 +01:00
commit 736101e1e8
48 changed files with 1336 additions and 692 deletions

View File

@ -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

View File

@ -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();

View File

@ -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);
}

View File

@ -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"

View File

@ -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;
}

View File

@ -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(

View File

@ -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.");

View File

@ -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) {

View 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;
}
);
}
};
})();

View File

@ -27,6 +27,7 @@ EXTRA_JS_MODULES = [
'SessionMigration.jsm',
'SessionStorage.jsm',
'SessionWorker.js',
'SessionWorker.jsm',
'TabAttributes.jsm',
'TabState.jsm',
'TabStateCache.jsm',

View File

@ -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
*/

View File

@ -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]

View File

@ -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);
});

View File

@ -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);

View File

@ -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,

View File

@ -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));
});
}

View File

@ -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);
});

View 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();
});

View File

@ -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");

View File

@ -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");
});

View File

@ -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) {

View File

@ -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);
};

View File

@ -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))));
});

View File

@ -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);
});

View File

@ -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]

View File

@ -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);

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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

View File

@ -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 -->

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]);

View File

@ -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]);

View File

@ -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}

View File

@ -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',
]

View File

@ -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);
};

View File

@ -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);
};

View File

@ -4,3 +4,4 @@ support-files =
head.js
[test_infrastructure.html]
[test_requestAutocomplete_disabled.html]

View File

@ -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>

View File

@ -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});

View File

@ -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) {

View File

@ -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",

View File

@ -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;
}

View File

@ -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();
}
};