import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,431 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["ContentRestore"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormData",
"resource://gre/modules/FormData.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
"resource:///modules/sessionstore/PageStyle.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource://gre/modules/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
/**
* This module implements the content side of session restoration. The chrome
* side is handled by SessionStore.jsm. The functions in this module are called
* by content-sessionStore.js based on messages received from SessionStore.jsm
* (or, in one case, based on a "load" event). Each tab has its own
* ContentRestore instance, constructed by content-sessionStore.js.
*
* In a typical restore, content-sessionStore.js will call the following based
* on messages and events it receives:
*
* restoreHistory(tabData, loadArguments, callbacks)
* Restores the tab's history and session cookies.
* restoreTabContent(loadArguments, finishCallback)
* Starts loading the data for the current page to restore.
* restoreDocument()
* Restore form and scroll data.
*
* When the page has been loaded from the network, we call finishCallback. It
* should send a message to SessionStore.jsm, which may cause other tabs to be
* restored.
*
* When the page has finished loading, a "load" event will trigger in
* content-sessionStore.js, which will call restoreDocument. At that point,
* form data is restored and the restore is complete.
*
* At any time, SessionStore.jsm can cancel the ongoing restore by sending a
* reset message, which causes resetRestore to be called. At that point it's
* legal to begin another restore.
*/
function ContentRestore(chromeGlobal) {
let internal = new ContentRestoreInternal(chromeGlobal);
let external = {};
let EXPORTED_METHODS = ["restoreHistory",
"restoreTabContent",
"restoreDocument",
"resetRestore"
];
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
function ContentRestoreInternal(chromeGlobal) {
this.chromeGlobal = chromeGlobal;
// The following fields are only valid during certain phases of the restore
// process.
// The tabData for the restore. Set in restoreHistory and removed in
// restoreTabContent.
this._tabData = null;
// Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a
// single entry from the tabData.entries array. Set in
// restoreTabContent and removed in restoreDocument.
this._restoringDocument = null;
// This listener is used to detect reloads on restoring tabs. Set in
// restoreHistory and removed in restoreTabContent.
this._historyListener = null;
// This listener detects when a pending tab starts loading (when not
// initiated by sessionstore) and when a restoring tab has finished loading
// data from the network. Set in restoreHistory() and restoreTabContent(),
// removed in resetRestore().
this._progressListener = null;
}
/**
* The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
* public.
*/
ContentRestoreInternal.prototype = {
get docShell() {
return this.chromeGlobal.docShell;
},
/**
* Starts the process of restoring a tab. The tabData to be restored is passed
* in here and used throughout the restoration. The epoch (which must be
* non-zero) is passed through to all the callbacks. If a load in the tab
* is started while it is pending, the appropriate callbacks are called.
*/
restoreHistory(tabData, loadArguments, callbacks) {
this._tabData = tabData;
// In case about:blank isn't done yet.
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
// Make sure currentURI is set so that switch-to-tab works before the tab is
// restored. We'll reset this to about:blank when we try to restore the tab
// to ensure that docshell doeesn't get confused. Don't bother doing this if
// we're restoring immediately due to a process switch. It just causes the
// URL bar to be temporarily blank.
let activeIndex = tabData.index - 1;
let activePageData = tabData.entries[activeIndex] || {};
let uri = activePageData.url || null;
if (uri && !loadArguments) {
webNavigation.setCurrentURI(Utils.makeURI(uri));
}
SessionHistory.restore(this.docShell, tabData);
// Add a listener to watch for reloads.
let listener = new HistoryListener(this.docShell, () => {
// On reload, restore tab contents.
this.restoreTabContent(null, false, callbacks.onLoadFinished);
});
webNavigation.sessionHistory.addSHistoryListener(listener);
this._historyListener = listener;
// Make sure to reset the capabilities and attributes in case this tab gets
// reused.
let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
DocShellCapabilities.restore(this.docShell, disallow);
if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) {
SessionStorage.restore(this.docShell, tabData.storage);
delete tabData.storage;
}
// Add a progress listener to correctly handle browser.loadURI()
// calls from foreign code.
this._progressListener = new ProgressListener(this.docShell, {
onStartRequest: () => {
// Some code called browser.loadURI() on a pending tab. It's safe to
// assume we don't care about restoring scroll or form data.
this._tabData = null;
// Listen for the tab to finish loading.
this.restoreTabContentStarted(callbacks.onLoadFinished);
// Notify the parent.
callbacks.onLoadStarted();
}
});
},
/**
* Start loading the current page. When the data has finished loading from the
* network, finishCallback is called. Returns true if the load was successful.
*/
restoreTabContent: function (loadArguments, isRemotenessUpdate, finishCallback) {
let tabData = this._tabData;
this._tabData = null;
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory;
// Listen for the tab to finish loading.
this.restoreTabContentStarted(finishCallback);
// Reset the current URI to about:blank. We changed it above for
// switch-to-tab, but now it must go back to the correct value before the
// load happens. Don't bother doing this if we're restoring immediately
// due to a process switch.
if (!isRemotenessUpdate) {
webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
}
try {
if (loadArguments) {
// A load has been redirected to a new process so get history into the
// same state it was before the load started then trigger the load.
let referrer = loadArguments.referrer ?
Utils.makeURI(loadArguments.referrer) : null;
let referrerPolicy = ('referrerPolicy' in loadArguments
? loadArguments.referrerPolicy
: Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT);
let postData = loadArguments.postData ?
Utils.makeInputStream(loadArguments.postData) : null;
if (loadArguments.userContextId) {
webNavigation.setOriginAttributesBeforeLoading({ userContextId: loadArguments.userContextId });
}
webNavigation.loadURIWithOptions(loadArguments.uri, loadArguments.flags,
referrer, referrerPolicy, postData,
null, null);
} else if (tabData.userTypedValue && tabData.userTypedClear) {
// If the user typed a URL into the URL bar and hit enter right before
// we crashed, we want to start loading that page again. A non-zero
// userTypedClear value means that the load had started.
// Load userTypedValue and fix up the URL if it's partial/broken.
webNavigation.loadURI(tabData.userTypedValue,
Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
null, null, null);
} else if (tabData.entries.length) {
// Stash away the data we need for restoreDocument.
let activeIndex = tabData.index - 1;
this._restoringDocument = {entry: tabData.entries[activeIndex] || {},
formdata: tabData.formdata || {},
pageStyle: tabData.pageStyle || {},
scrollPositions: tabData.scroll || {}};
// In order to work around certain issues in session history, we need to
// force session history to update its internal index and call reload
// instead of gotoIndex. See bug 597315.
history.reloadCurrentEntry();
} else {
// If there's nothing to restore, we should still blank the page.
webNavigation.loadURI("about:blank",
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
null, null, null);
}
return true;
} catch (ex if ex instanceof Ci.nsIException) {
// Ignore page load errors, but return false to signal that the load never
// happened.
return false;
}
},
/**
* To be called after restoreHistory(). Removes all listeners needed for
* pending tabs and makes sure to notify when the tab finished loading.
*/
restoreTabContentStarted(finishCallback) {
// The reload listener is no longer needed.
this._historyListener.uninstall();
this._historyListener = null;
// Remove the old progress listener.
this._progressListener.uninstall();
// We're about to start a load. This listener will be called when the load
// has finished getting everything from the network.
this._progressListener = new ProgressListener(this.docShell, {
onStopRequest: () => {
// Call resetRestore() to reset the state back to normal. The data
// needed for restoreDocument() (which hasn't happened yet) will
// remain in _restoringDocument.
this.resetRestore();
finishCallback();
}
});
},
/**
* Finish restoring the tab by filling in form data and setting the scroll
* position. The restore is complete when this function exits. It should be
* called when the "load" event fires for the restoring tab.
*/
restoreDocument: function () {
if (!this._restoringDocument) {
return;
}
let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
this._restoringDocument = null;
let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
PageStyle.restoreTree(this.docShell, pageStyle);
FormData.restoreTree(window, formdata);
ScrollPosition.restoreTree(window, scrollPositions);
},
/**
* Cancel an ongoing restore. This function can be called any time between
* restoreHistory and restoreDocument.
*
* This function is called externally (if a restore is canceled) and
* internally (when the loads for a restore have finished). In the latter
* case, it's called before restoreDocument, so it cannot clear
* _restoringDocument.
*/
resetRestore: function () {
this._tabData = null;
if (this._historyListener) {
this._historyListener.uninstall();
}
this._historyListener = null;
if (this._progressListener) {
this._progressListener.uninstall();
}
this._progressListener = null;
}
};
/*
* This listener detects when a page being restored is reloaded. It triggers a
* callback and cancels the reload. The callback will send a message to
* SessionStore.jsm so that it can restore the content immediately.
*/
function HistoryListener(docShell, callback) {
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
webNavigation.sessionHistory.addSHistoryListener(this);
this.webNavigation = webNavigation;
this.callback = callback;
}
HistoryListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsISHistoryListener,
Ci.nsISupportsWeakReference
]),
uninstall: function () {
let shistory = this.webNavigation.sessionHistory;
if (shistory) {
shistory.removeSHistoryListener(this);
}
},
OnHistoryGoBack: function(backURI) { return true; },
OnHistoryGoForward: function(forwardURI) { return true; },
OnHistoryGotoIndex: function(index, gotoURI) { return true; },
OnHistoryPurge: function(numEntries) { return true; },
OnHistoryReplaceEntry: function(index) {},
// This will be called for a pending tab when loadURI(uri) is called where
// the given |uri| only differs in the fragment.
OnHistoryNewEntry(newURI) {
let currentURI = this.webNavigation.currentURI;
// Ignore new SHistory entries with the same URI as those do not indicate
// a navigation inside a document by changing the #hash part of the URL.
// We usually hit this when purging session history for browsers.
if (currentURI && (currentURI.spec == newURI.spec)) {
return;
}
// Reset the tab's URL to what it's actually showing. Without this loadURI()
// would use the current document and change the displayed URL only.
this.webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
// Kick off a new load so that we navigate away from about:blank to the
// new URL that was passed to loadURI(). The new load will cause a
// STATE_START notification to be sent and the ProgressListener will then
// notify the parent and do the rest.
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
this.webNavigation.loadURI(newURI.spec, flags, null, null, null);
},
OnHistoryReload(reloadURI, reloadFlags) {
this.callback();
// Cancel the load.
return false;
},
}
/**
* This class informs SessionStore.jsm whenever the network requests for a
* restoring page have completely finished. We only restore three tabs
* simultaneously, so this is the signal for SessionStore.jsm to kick off
* another restore (if there are more to do).
*
* The progress listener is also used to be notified when a load not initiated
* by sessionstore starts. Pending tabs will then need to be marked as no
* longer pending.
*/
function ProgressListener(docShell, callbacks) {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
this.webProgress = webProgress;
this.callbacks = callbacks;
}
ProgressListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference
]),
uninstall: function() {
this.webProgress.removeProgressListener(this);
},
onStateChange: function(webProgress, request, stateFlags, status) {
let {STATE_IS_WINDOW, STATE_STOP, STATE_START} = Ci.nsIWebProgressListener;
if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) {
return;
}
if (stateFlags & STATE_START && this.callbacks.onStartRequest) {
this.callbacks.onStartRequest();
}
if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) {
this.callbacks.onStopRequest();
}
},
onLocationChange: function() {},
onProgressChange: function() {},
onStatusChange: function() {},
onSecurityChange: function() {},
};

View file

@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["DocShellCapabilities"];
/**
* The external API exported by this module.
*/
this.DocShellCapabilities = Object.freeze({
collect: function (docShell) {
return DocShellCapabilitiesInternal.collect(docShell);
},
restore: function (docShell, disallow) {
return DocShellCapabilitiesInternal.restore(docShell, disallow);
},
});
/**
* Internal functionality to save and restore the docShell.allow* properties.
*/
var DocShellCapabilitiesInternal = {
// List of docShell capabilities to (re)store. These are automatically
// retrieved from a given docShell if not already collected before.
// This is made so they're automatically in sync with all nsIDocShell.allow*
// properties.
caps: null,
allCapabilities: function (docShell) {
if (!this.caps) {
let keys = Object.keys(docShell);
this.caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5));
}
return this.caps;
},
collect: function (docShell) {
let caps = this.allCapabilities(docShell);
return caps.filter(cap => !docShell["allow" + cap]);
},
restore: function (docShell, disallow) {
let caps = this.allCapabilities(docShell);
for (let cap of caps)
docShell["allow" + cap] = !disallow.has(cap);
},
};

View file

@ -0,0 +1,254 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FrameTree"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"];
/**
* A FrameTree represents all frames that were reachable when the document
* was loaded. We use this information to ignore frames when collecting
* sessionstore data as we can't currently restore anything for frames that
* have been created dynamically after or at the load event.
*
* @constructor
*/
function FrameTree(chromeGlobal) {
let internal = new FrameTreeInternal(chromeGlobal);
let external = {};
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
/**
* The internal frame tree API that the public one points to.
*
* @constructor
*/
function FrameTreeInternal(chromeGlobal) {
// A WeakMap that uses frames (DOMWindows) as keys and their initial indices
// in their parents' child lists as values. Suppose we have a root frame with
// three subframes i.e. a page with three iframes. The WeakMap would have
// four entries and look as follows:
//
// root -> 0
// subframe1 -> 0
// subframe2 -> 1
// subframe3 -> 2
//
// Should one of the subframes disappear we will stop collecting data for it
// as |this._frames.has(frame) == false|. All other subframes will maintain
// their initial indices to ensure we can restore frame data appropriately.
this._frames = new WeakMap();
// The Set of observers that will be notified when the frame changes.
this._observers = new Set();
// The chrome global we use to retrieve the current DOMWindow.
this._chromeGlobal = chromeGlobal;
// Register a web progress listener to be notified about new page loads.
let docShell = chromeGlobal.docShell;
let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
}
FrameTreeInternal.prototype = {
// Returns the docShell's current global.
get content() {
return this._chromeGlobal.content;
},
/**
* Adds a given observer |obs| to the set of observers that will be notified
* when the frame tree is reset (when a new document starts loading) or
* recollected (when a document finishes loading).
*
* @param obs (object)
*/
addObserver: function (obs) {
this._observers.add(obs);
},
/**
* Notifies all observers that implement the given |method|.
*
* @param method (string)
*/
notifyObservers: function (method) {
for (let obs of this._observers) {
if (obs.hasOwnProperty(method)) {
obs[method]();
}
}
},
/**
* Checks whether a given |frame| is contained in the collected frame tree.
* If it is not, this indicates that we should not collect data for it.
*
* @param frame (nsIDOMWindow)
* @return bool
*/
contains: function (frame) {
return this._frames.has(frame);
},
/**
* Recursively applies the given function |cb| to the stored frame tree. Use
* this method to collect sessionstore data for all reachable frames stored
* in the frame tree.
*
* If a given function |cb| returns a value, it must be an object. It may
* however return "null" to indicate that there is no data to be stored for
* the given frame.
*
* The object returned by |cb| cannot have any property named "children" as
* that is used to store information about subframes in the tree returned
* by |map()| and might be overridden.
*
* @param cb (function)
* @return object
*/
map: function (cb) {
let frames = this._frames;
function walk(frame) {
let obj = cb(frame) || {};
if (frames.has(frame)) {
let children = [];
Array.forEach(frame.frames, subframe => {
// Don't collect any data if the frame is not contained in the
// initial frame tree. It's a dynamic frame added later.
if (!frames.has(subframe)) {
return;
}
// Retrieve the frame's original position in its parent's child list.
let index = frames.get(subframe);
// Recursively collect data for the current subframe.
let result = walk(subframe, cb);
if (result && Object.keys(result).length) {
children[index] = result;
}
});
if (children.length) {
obj.children = children;
}
}
return Object.keys(obj).length ? obj : null;
}
return walk(this.content);
},
/**
* Applies the given function |cb| to all frames stored in the tree. Use this
* method if |map()| doesn't suit your needs and you want more control over
* how data is collected.
*
* @param cb (function)
* This callback receives the current frame as the only argument.
*/
forEach: function (cb) {
let frames = this._frames;
function walk(frame) {
cb(frame);
if (!frames.has(frame)) {
return;
}
Array.forEach(frame.frames, subframe => {
if (frames.has(subframe)) {
cb(subframe);
}
});
}
walk(this.content);
},
/**
* Stores a given |frame| and its children in the frame tree.
*
* @param frame (nsIDOMWindow)
* @param index (int)
* The index in the given frame's parent's child list.
*/
collect: function (frame, index = 0) {
// Mark the given frame as contained in the frame tree.
this._frames.set(frame, index);
// Mark the given frame's subframes as contained in the tree.
Array.forEach(frame.frames, this.collect, this);
},
/**
* @see nsIWebProgressListener.onStateChange
*
* We want to be notified about:
* - new documents that start loading to clear the current frame tree;
* - completed document loads to recollect reachable frames.
*/
onStateChange: function (webProgress, request, stateFlags, status) {
// Ignore state changes for subframes because we're only interested in the
// top-document starting or stopping its load. We thus only care about any
// changes to the root of the frame tree, not to any of its nodes/leafs.
if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) {
return;
}
// onStateChange will be fired when loading the initial about:blank URI for
// a browser, which we don't actually care about. This is particularly for
// the case of unrestored background tabs, where the content has not yet
// been restored: we don't want to accidentally send any updates to the
// parent when the about:blank placeholder page has loaded.
if (!this._chromeGlobal.docShell.hasLoadedNonBlankURI) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
// Clear the list of frames until we can recollect it.
this._frames = new WeakMap();
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeReset");
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
// The document and its resources have finished loading.
this.collect(webProgress.DOMWindow);
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeCollected");
}
},
// Unused nsIWebProgressListener methods.
onLocationChange: function () {},
onProgressChange: function () {},
onSecurityChange: function () {},
onStatusChange: function () {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};

View file

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["GlobalState"];
const EXPORTED_METHODS = ["getState", "clear", "get", "set", "delete", "setFromState"];
/**
* Module that contains global session data.
*/
function GlobalState() {
let internal = new GlobalStateInternal();
let external = {};
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
function GlobalStateInternal() {
// Storage for global state.
this.state = {};
}
GlobalStateInternal.prototype = {
/**
* Get all value from the global state.
*/
getState: function() {
return this.state;
},
/**
* Clear all currently stored global state.
*/
clear: function() {
this.state = {};
},
/**
* Retrieve a value from the global state.
*
* @param aKey
* A key the value is stored under.
* @return The value stored at aKey, or an empty string if no value is set.
*/
get: function(aKey) {
return this.state[aKey] || "";
},
/**
* Set a global value.
*
* @param aKey
* A key to store the value under.
*/
set: function(aKey, aStringValue) {
this.state[aKey] = aStringValue;
},
/**
* Delete a global value.
*
* @param aKey
* A key to delete the value for.
*/
delete: function(aKey) {
delete this.state[aKey];
},
/**
* Set the current global state from a state object. Any previous global
* state will be removed, even if the new state does not contain a matching
* key.
*
* @param aState
* A state object to extract global state from to be set.
*/
setFromState: function (aState) {
this.state = (aState && aState.global) || {};
}
};

View file

@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["PageStyle"];
const Ci = Components.interfaces;
/**
* The external API exported by this module.
*/
this.PageStyle = Object.freeze({
collect: function (docShell, frameTree) {
return PageStyleInternal.collect(docShell, frameTree);
},
restoreTree: function (docShell, data) {
PageStyleInternal.restoreTree(docShell, data);
}
});
// Signifies that author style level is disabled for the page.
const NO_STYLE = "_nostyle";
var PageStyleInternal = {
/**
* Collects the selected style sheet sets for all reachable frames.
*/
collect: function (docShell, frameTree) {
let result = frameTree.map(({document: doc}) => {
let style;
if (doc) {
// http://dev.w3.org/csswg/cssom/#persisting-the-selected-css-style-sheet-set
style = doc.selectedStyleSheetSet || doc.lastStyleSheetSet;
}
return style ? {pageStyle: style} : null;
});
let markupDocumentViewer =
docShell.contentViewer;
if (markupDocumentViewer.authorStyleDisabled) {
result = result || {};
result.disabled = true;
}
return result && Object.keys(result).length ? result : null;
},
/**
* Restores pageStyle data for the current frame hierarchy starting at the
* |docShell's| current DOMWindow using the given pageStyle |data|.
*
* Warning: If the current frame hierarchy doesn't match that of the given
* |data| object we will silently discard data for unreachable frames. We may
* as well assign page styles to the wrong frames if some were reordered or
* removed.
*
* @param docShell (nsIDocShell)
* @param data (object)
* {
* disabled: true, // when true, author styles will be disabled
* pageStyle: "Dusk",
* children: [
* null,
* {pageStyle: "Mozilla", children: [ ... ]}
* ]
* }
*/
restoreTree: function (docShell, data) {
let disabled = data.disabled || false;
let markupDocumentViewer =
docShell.contentViewer;
markupDocumentViewer.authorStyleDisabled = disabled;
function restoreFrame(root, data) {
if (data.hasOwnProperty("pageStyle")) {
root.document.selectedStyleSheetSet = data.pageStyle;
}
if (!data.hasOwnProperty("children")) {
return;
}
let frames = root.frames;
data.children.forEach((child, index) => {
if (child && index < frames.length) {
restoreFrame(frames[index], child);
}
});
}
let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
restoreFrame(ifreq.getInterface(Ci.nsIDOMWindow), data);
}
};

View file

@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["PrivacyFilter"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
/**
* A module that provides methods to filter various kinds of data collected
* from a tab by the current privacy level as set by the user.
*/
this.PrivacyFilter = Object.freeze({
/**
* Filters the given (serialized) session storage |data| according to the
* current privacy level and returns a new object containing only data that
* we're allowed to store.
*
* @param data The session storage data as collected from a tab.
* @return object
*/
filterSessionStorageData: function (data) {
let retval = {};
for (let host of Object.keys(data)) {
if (PrivacyLevel.check(host)) {
retval[host] = data[host];
}
}
return Object.keys(retval).length ? retval : null;
},
/**
* Filters the given (serialized) form |data| according to the current
* privacy level and returns a new object containing only data that we're
* allowed to store.
*
* @param data The form data as collected from a tab.
* @return object
*/
filterFormData: function (data) {
// If the given form data object has an associated URL that we are not
// allowed to store data for, bail out. We explicitly discard data for any
// children as well even if storing data for those frames would be allowed.
if (data.url && !PrivacyLevel.check(data.url)) {
return;
}
let retval = {};
for (let key of Object.keys(data)) {
if (key === "children") {
let recurse = child => this.filterFormData(child);
let children = data.children.map(recurse).filter(child => child);
if (children.length) {
retval.children = children;
}
// Only copy keys other than "children" if we have a valid URL in
// data.url and we thus passed the privacy level check.
} else if (data.url) {
retval[key] = data[key];
}
}
return Object.keys(retval).length ? retval : null;
},
/**
* Removes any private windows and tabs from a given browser state object.
*
* @param browserState (object)
* The browser state for which we remove any private windows and tabs.
* The given object will be modified.
*/
filterPrivateWindowsAndTabs: function (browserState) {
// Remove private opened windows.
for (let i = browserState.windows.length - 1; i >= 0; i--) {
let win = browserState.windows[i];
if (win.isPrivate) {
browserState.windows.splice(i, 1);
if (browserState.selectedWindow >= i) {
browserState.selectedWindow--;
}
} else {
// Remove private tabs from all open non-private windows.
this.filterPrivateTabs(win);
}
}
// Remove private closed windows.
browserState._closedWindows =
browserState._closedWindows.filter(win => !win.isPrivate);
// Remove private tabs from all remaining closed windows.
browserState._closedWindows.forEach(win => this.filterPrivateTabs(win));
},
/**
* Removes open private tabs from a given window state object.
*
* @param winState (object)
* The window state for which we remove any private tabs.
* The given object will be modified.
*/
filterPrivateTabs: function (winState) {
// Remove open private tabs.
for (let i = winState.tabs.length - 1; i >= 0 ; i--) {
let tab = winState.tabs[i];
if (tab.isPrivate) {
winState.tabs.splice(i, 1);
if (winState.selected >= i) {
winState.selected--;
}
}
}
// Note that closed private tabs are only stored for private windows.
// There is no need to call this function for private windows as the
// whole window state should just be discarded so we explicitly don't
// try to remove closed private tabs as an optimization.
}
});

View file

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["PrivacyLevel"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
const PREF = "browser.sessionstore.privacy_level";
// The following constants represent the different possible privacy levels that
// can be set by the user and that we need to consider when collecting text
// data, and cookies.
//
// Collect data from all sites (http and https).
const PRIVACY_NONE = 0;
// Collect data from unencrypted sites (http), only.
const PRIVACY_ENCRYPTED = 1;
// Collect no data.
const PRIVACY_FULL = 2;
/**
* The external API as exposed by this module.
*/
var PrivacyLevel = Object.freeze({
/**
* Returns whether the current privacy level allows saving data for the given
* |url|.
*
* @param url The URL we want to save data for.
* @return bool
*/
check: function (url) {
return PrivacyLevel.canSave({ isHttps: url.startsWith("https:") });
},
/**
* Checks whether we're allowed to save data for a specific site.
*
* @param {isHttps: boolean}
* An object that must have one property: 'isHttps'.
* 'isHttps' tells whether the site us secure communication (HTTPS).
* @return {bool} Whether we can save data for the specified site.
*/
canSave: function ({isHttps}) {
let level = Services.prefs.getIntPref(PREF);
// Never save any data when full privacy is requested.
if (level == PRIVACY_FULL) {
return false;
}
// Don't save data for encrypted sites when requested.
if (isHttps && level == PRIVACY_ENCRYPTED) {
return false;
}
return true;
}
});

View file

@ -0,0 +1,214 @@
/* 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/. */
this.EXPORTED_SYMBOLS = ["RecentlyClosedTabsAndWindowsMenuUtils"];
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
var Ci = Components.interfaces;
var Cc = Components.classes;
var Cr = Components.results;
var Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
var navigatorBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
this.RecentlyClosedTabsAndWindowsMenuUtils = {
/**
* Builds up a document fragment of UI items for the recently closed tabs.
* @param aWindow
* The window that the tabs were closed in.
* @param aTagName
* The tag name that will be used when creating the UI items.
* @param aPrefixRestoreAll (defaults to false)
* Whether the 'restore all tabs' item is suffixed or prefixed to the list.
* If suffixed (the default) a separator will be inserted before it.
* @param aRestoreAllLabel (defaults to "menuRestoreAllTabs.label")
* Which localizable string to use for the 'restore all tabs' item.
* @returns A document fragment with UI items for each recently closed tab.
*/
getTabsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false,
aRestoreAllLabel="menuRestoreAllTabs.label") {
let doc = aWindow.document;
let fragment = doc.createDocumentFragment();
if (SessionStore.getClosedTabCount(aWindow) != 0) {
let closedTabs = SessionStore.getClosedTabData(aWindow, false);
for (let i = 0; i < closedTabs.length; i++) {
createEntry(aTagName, false, i, closedTabs[i], doc,
closedTabs[i].title, fragment);
}
createRestoreAllEntry(doc, fragment, aPrefixRestoreAll, false,
aRestoreAllLabel, closedTabs.length, aTagName)
}
return fragment;
},
/**
* Builds up a document fragment of UI items for the recently closed windows.
* @param aWindow
* A window that can be used to create the elements and document fragment.
* @param aTagName
* The tag name that will be used when creating the UI items.
* @param aPrefixRestoreAll (defaults to false)
* Whether the 'restore all windows' item is suffixed or prefixed to the list.
* If suffixed (the default) a separator will be inserted before it.
* @param aRestoreAllLabel (defaults to "menuRestoreAllWindows.label")
* Which localizable string to use for the 'restore all windows' item.
* @returns A document fragment with UI items for each recently closed window.
*/
getWindowsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false,
aRestoreAllLabel="menuRestoreAllWindows.label") {
let closedWindowData = SessionStore.getClosedWindowData(false);
let doc = aWindow.document;
let fragment = doc.createDocumentFragment();
if (closedWindowData.length != 0) {
let menuLabelString = navigatorBundle.GetStringFromName("menuUndoCloseWindowLabel");
let menuLabelStringSingleTab =
navigatorBundle.GetStringFromName("menuUndoCloseWindowSingleTabLabel");
for (let i = 0; i < closedWindowData.length; i++) {
let undoItem = closedWindowData[i];
let otherTabsCount = undoItem.tabs.length - 1;
let label = (otherTabsCount == 0) ? menuLabelStringSingleTab
: PluralForm.get(otherTabsCount, menuLabelString);
let menuLabel = label.replace("#1", undoItem.title)
.replace("#2", otherTabsCount);
let selectedTab = undoItem.tabs[undoItem.selected - 1];
createEntry(aTagName, true, i, selectedTab, doc, menuLabel,
fragment);
}
createRestoreAllEntry(doc, fragment, aPrefixRestoreAll, true,
aRestoreAllLabel, closedWindowData.length,
aTagName);
}
return fragment;
},
/**
* Re-open a closed tab and put it to the end of the tab strip.
* Used for a middle click.
* @param aEvent
* The event when the user clicks the menu item
*/
_undoCloseMiddleClick: function(aEvent) {
if (aEvent.button != 1)
return;
aEvent.view.undoCloseTab(aEvent.originalTarget.getAttribute("value"));
aEvent.view.gBrowser.moveTabToEnd();
},
};
function setImage(aItem, aElement) {
let iconURL = aItem.image;
// don't initiate a connection just to fetch a favicon (see bug 467828)
if (/^https?:/.test(iconURL))
iconURL = "moz-anno:favicon:" + iconURL;
aElement.setAttribute("image", iconURL);
}
/**
* Create a UI entry for a recently closed tab or window.
* @param aTagName
* the tag name that will be used when creating the UI entry
* @param aIsWindowsFragment
* whether or not this entry will represent a closed window
* @param aIndex
* the index of the closed tab
* @param aClosedTab
* the closed tab
* @param aDocument
* a document that can be used to create the entry
* @param aMenuLabel
* the label the created entry will have
* @param aFragment
* the fragment the created entry will be in
*/
function createEntry(aTagName, aIsWindowsFragment, aIndex, aClosedTab,
aDocument, aMenuLabel, aFragment) {
let element = aDocument.createElementNS(kNSXUL, aTagName);
element.setAttribute("label", aMenuLabel);
if (aClosedTab.image) {
setImage(aClosedTab, element);
}
if (!aIsWindowsFragment) {
element.setAttribute("value", aIndex);
}
if (aTagName == "menuitem") {
element.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon");
}
element.setAttribute("oncommand", "undoClose" + (aIsWindowsFragment ? "Window" : "Tab") +
"(" + aIndex + ");");
// Set the targetURI attribute so it will be shown in tooltip.
// SessionStore uses one-based indexes, so we need to normalize them.
let tabData;
tabData = aIsWindowsFragment ? aClosedTab
: aClosedTab.state;
let activeIndex = (tabData.index || tabData.entries.length) - 1;
if (activeIndex >= 0 && tabData.entries[activeIndex]) {
element.setAttribute("targetURI", tabData.entries[activeIndex].url);
}
if (!aIsWindowsFragment) {
element.addEventListener("click", RecentlyClosedTabsAndWindowsMenuUtils._undoCloseMiddleClick, false);
}
if (aIndex == 0) {
element.setAttribute("key", "key_undoClose" + (aIsWindowsFragment? "Window" : "Tab"));
}
aFragment.appendChild(element);
}
/**
* Create an entry to restore all closed windows or tabs.
* @param aDocument
* a document that can be used to create the entry
* @param aFragment
* the fragment the created entry will be in
* @param aPrefixRestoreAll
* whether the 'restore all windows' item is suffixed or prefixed to the list
* If suffixed a separator will be inserted before it.
* @param aIsWindowsFragment
* whether or not this entry will represent a closed window
* @param aRestoreAllLabel
* which localizable string to use for the entry
* @param aEntryCount
* the number of elements to be restored by this entry
* @param aTagName
* the tag name that will be used when creating the UI entry
*/
function createRestoreAllEntry(aDocument, aFragment, aPrefixRestoreAll,
aIsWindowsFragment, aRestoreAllLabel,
aEntryCount, aTagName) {
let restoreAllElements = aDocument.createElementNS(kNSXUL, aTagName);
restoreAllElements.classList.add("restoreallitem");
restoreAllElements.setAttribute("label", navigatorBundle.GetStringFromName(aRestoreAllLabel));
restoreAllElements.setAttribute("oncommand",
"for (var i = 0; i < " + aEntryCount + "; i++) undoClose" +
(aIsWindowsFragment? "Window" : "Tab") + "();");
if (aPrefixRestoreAll) {
aFragment.insertBefore(restoreAllElements, aFragment.firstChild);
} else {
aFragment.appendChild(aDocument.createElementNS(kNSXUL, "menuseparator"));
aFragment.appendChild(restoreAllElements);
}
}

View file

@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["RunState"];
const STATE_STOPPED = 0;
const STATE_RUNNING = 1;
const STATE_QUITTING = 2;
const STATE_CLOSING = 3;
const STATE_CLOSED = 4;
// We're initially stopped.
var state = STATE_STOPPED;
/**
* This module keeps track of SessionStore's current run state. We will
* always start out at STATE_STOPPED. After the session was read from disk and
* the initial browser window has loaded we switch to STATE_RUNNING. On the
* first notice that a browser shutdown was granted we switch to STATE_QUITTING.
*/
this.RunState = Object.freeze({
// If we're stopped then SessionStore hasn't been initialized yet. As soon
// as the session is read from disk and the initial browser window has loaded
// the run state will change to STATE_RUNNING.
get isStopped() {
return state == STATE_STOPPED;
},
// STATE_RUNNING is our default mode of operation that we'll spend most of
// the time in. After the session was read from disk and the first browser
// window has loaded we remain running until the browser quits.
get isRunning() {
return state == STATE_RUNNING;
},
// We will enter STATE_QUITTING as soon as we receive notice that a browser
// shutdown was granted. SessionStore will use this information to prevent
// us from collecting partial information while the browser is shutting down
// as well as to allow a last single write to disk and block all writes after
// that.
get isQuitting() {
return state >= STATE_QUITTING;
},
// We will enter STATE_CLOSING as soon as SessionStore is uninitialized.
// The SessionFile module will know that a last write will happen in this
// state and it can do some necessary cleanup.
get isClosing() {
return state == STATE_CLOSING;
},
// We will enter STATE_CLOSED as soon as SessionFile has written to disk for
// the last time before shutdown and will not accept any further writes.
get isClosed() {
return state == STATE_CLOSED;
},
// Switch the run state to STATE_RUNNING. This must be called after the
// session was read from, the initial browser window has loaded and we're
// now ready to restore session data.
setRunning() {
if (this.isStopped) {
state = STATE_RUNNING;
}
},
// Switch the run state to STATE_CLOSING. This must be called *before* the
// last SessionFile.write() call so that SessionFile knows we're closing and
// can do some last cleanups and write a proper sessionstore.js file.
setClosing() {
if (this.isQuitting) {
state = STATE_CLOSING;
}
},
// Switch the run state to STATE_CLOSED. This must be called by SessionFile
// after the last write to disk was accepted and no further writes will be
// allowed. Any writes after this stage will cause exceptions.
setClosed() {
if (this.isClosing) {
state = STATE_CLOSED;
}
},
// Switch the run state to STATE_QUITTING. This should be called once we're
// certain that the browser is going away and before we start collecting the
// final window states to save in the session file.
setQuitting() {
if (this.isRunning) {
state = STATE_QUITTING;
}
},
});

View file

@ -0,0 +1,476 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionCookies"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
const MAX_EXPIRY = Math.pow(2, 62);
/**
* The external API implemented by the SessionCookies module.
*/
this.SessionCookies = Object.freeze({
update: function (windows) {
SessionCookiesInternal.update(windows);
},
getHostsForWindow: function (window, checkPrivacy = false) {
return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy);
},
restore(cookies) {
SessionCookiesInternal.restore(cookies);
}
});
/**
* The internal API.
*/
var SessionCookiesInternal = {
/**
* Stores whether we're initialized, yet.
*/
_initialized: false,
/**
* Retrieve the list of all hosts contained in the given windows' session
* history entries (per window) and collect the associated cookies for those
* hosts, if any. The given state object is being modified.
*
* @param windows
* Array of window state objects.
* [{ tabs: [...], cookies: [...] }, ...]
*/
update: function (windows) {
this._ensureInitialized();
for (let window of windows) {
let cookies = [];
// Collect all hosts for the current window.
let hosts = this.getHostsForWindow(window, true);
for (let host of Object.keys(hosts)) {
let isPinned = hosts[host];
for (let cookie of CookieStore.getCookiesForHost(host)) {
// _getCookiesForHost() will only return hosts with the right privacy
// rules, so there is no need to do anything special with this call
// to PrivacyLevel.canSave().
if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) {
cookies.push(cookie);
}
}
}
// Don't include/keep empty cookie sections.
if (cookies.length) {
window.cookies = cookies;
} else if ("cookies" in window) {
delete window.cookies;
}
}
},
/**
* Returns a map of all hosts for a given window that we might want to
* collect cookies for.
*
* @param window
* A window state object containing tabs with history entries.
* @param checkPrivacy (bool)
* Whether to check the privacy level for each host.
* @return {object} A map of hosts for a given window state object. The keys
* will be hosts, the values are boolean and determine
* whether we will use the deferred privacy level when
* checking how much data to save on quitting.
*/
getHostsForWindow: function (window, checkPrivacy = false) {
let hosts = {};
for (let tab of window.tabs) {
for (let entry of tab.entries) {
this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned);
}
}
return hosts;
},
/**
* Restores a given list of session cookies.
*/
restore(cookies) {
for (let cookie of cookies) {
let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY;
let cookieObj = {
host: cookie.host,
path: cookie.path || "",
name: cookie.name || ""
};
if (!Services.cookies.cookieExists(cookieObj, cookie.originAttributes || {})) {
Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "",
cookie.value, !!cookie.secure, !!cookie.httponly,
/* isSession = */ true, expiry, cookie.originAttributes || {});
}
}
},
/**
* Handles observers notifications that are sent whenever cookies are added,
* changed, or removed. Ensures that the storage is updated accordingly.
*/
observe: function (subject, topic, data) {
switch (data) {
case "added":
case "changed":
this._updateCookie(subject);
break;
case "deleted":
this._removeCookie(subject);
break;
case "cleared":
CookieStore.clear();
break;
case "batch-deleted":
this._removeCookies(subject);
break;
case "reload":
CookieStore.clear();
this._reloadCookies();
break;
default:
throw new Error("Unhandled cookie-changed notification.");
}
},
/**
* If called for the first time in a session, iterates all cookies in the
* cookies service and puts them into the store if they're session cookies.
*/
_ensureInitialized: function () {
if (!this._initialized) {
this._reloadCookies();
this._initialized = true;
Services.obs.addObserver(this, "cookie-changed", false);
}
},
/**
* Fill a given map with hosts found in the given entry's session history and
* any child entries.
*
* @param entry
* the history entry, serialized
* @param hosts
* the hash that will be used to store hosts eg, { hostname: true }
* @param checkPrivacy
* should we check the privacy level for https
* @param isPinned
* is the entry we're evaluating for a pinned tab; used only if
* checkPrivacy
*/
_extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) {
let host = entry._host;
let scheme = entry._scheme;
// If host & scheme aren't defined, then we are likely here in the startup
// process via _splitCookiesFromWindow. In that case, we'll turn entry.url
// into an nsIURI and get host/scheme from that. This will throw for about:
// urls in which case we don't need to do anything.
if (!host && !scheme) {
try {
let uri = Utils.makeURI(entry.url);
host = uri.host;
scheme = uri.scheme;
this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned);
}
catch (ex) { }
}
if (entry.children) {
for (let child of entry.children) {
this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned);
}
}
},
/**
* Add a given host to a given map of hosts if the privacy level allows
* saving cookie data for it.
*
* @param host
* the host of a uri (usually via nsIURI.host)
* @param scheme
* the scheme of a uri (usually via nsIURI.scheme)
* @param hosts
* the hash that will be used to store hosts eg, { hostname: true }
* @param checkPrivacy
* should we check the privacy level for https
* @param isPinned
* is the entry we're evaluating for a pinned tab; used only if
* checkPrivacy
*/
_extractHostsFromHostScheme:
function (host, scheme, hosts, checkPrivacy, isPinned) {
// host and scheme may not be set (for about: urls for example), in which
// case testing scheme will be sufficient.
if (/https?/.test(scheme) && !hosts[host] &&
(!checkPrivacy ||
PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) {
// By setting this to true or false, we can determine when looking at
// the host in update() if we should check for privacy.
hosts[host] = isPinned;
} else if (scheme == "file") {
hosts[host] = true;
}
},
/**
* Updates or adds a given cookie to the store.
*/
_updateCookie: function (cookie) {
cookie.QueryInterface(Ci.nsICookie2);
if (cookie.isSession) {
CookieStore.set(cookie);
} else {
CookieStore.delete(cookie);
}
},
/**
* Removes a given cookie from the store.
*/
_removeCookie: function (cookie) {
cookie.QueryInterface(Ci.nsICookie2);
if (cookie.isSession) {
CookieStore.delete(cookie);
}
},
/**
* Removes a given list of cookies from the store.
*/
_removeCookies: function (cookies) {
for (let i = 0; i < cookies.length; i++) {
this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2));
}
},
/**
* Iterates all cookies in the cookies service and puts them into the store
* if they're session cookies.
*/
_reloadCookies: function () {
let iter = Services.cookies.enumerator;
while (iter.hasMoreElements()) {
this._updateCookie(iter.getNext());
}
}
};
/**
* Generates all possible subdomains for a given host and prepends a leading
* dot to all variants.
*
* See http://tools.ietf.org/html/rfc6265#section-5.1.3
* http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path
*
* All cookies belonging to a web page will be internally represented by a
* nsICookie object. nsICookie.host will be the request host if no domain
* parameter was given when setting the cookie. If a specific domain was given
* then nsICookie.host will contain that specific domain and prepend a leading
* dot to it.
*
* We thus generate all possible subdomains for a given domain and prepend a
* leading dot to them as that is the value that was used as the map key when
* the cookie was set.
*/
function* getPossibleSubdomainVariants(host) {
// Try given domain with a leading dot (.www.example.com).
yield "." + host;
// Stop if there are only two parts left (e.g. example.com was given).
let parts = host.split(".");
if (parts.length < 3) {
return;
}
// Remove the first subdomain (www.example.com -> example.com).
let rest = parts.slice(1).join(".");
// Try possible parent subdomains.
yield* getPossibleSubdomainVariants(rest);
}
/**
* The internal cookie storage that keeps track of every active session cookie.
* These are stored using maps per host, path, and cookie name.
*/
var CookieStore = {
/**
* The internal structure holding all known cookies.
*
* Host =>
* Path =>
* Name => {path: "/", name: "sessionid", secure: true}
*
* Maps are used for storage but the data structure is equivalent to this:
*
* this._hosts = {
* "www.mozilla.org": {
* "/": {
* "username": {name: "username", value: "my_name_is", etc...},
* "sessionid": {name: "sessionid", value: "1fdb3a", etc...}
* }
* },
* "tbpl.mozilla.org": {
* "/path": {
* "cookiename": {name: "cookiename", value: "value", etc...}
* }
* },
* ".example.com": {
* "/path": {
* "cookiename": {name: "cookiename", value: "value", etc...}
* }
* }
* };
*/
_hosts: new Map(),
/**
* Returns the list of stored session cookies for a given host.
*
* @param host
* A string containing the host name we want to get cookies for.
*/
getCookiesForHost: function (host) {
let cookies = [];
let appendCookiesForHost = host => {
if (!this._hosts.has(host)) {
return;
}
for (let pathToNamesMap of this._hosts.get(host).values()) {
for (let nameToCookiesMap of pathToNamesMap.values()) {
cookies.push(...nameToCookiesMap.values());
}
}
}
// Try to find cookies for the given host, e.g. <www.example.com>.
// The full hostname will be in the map if the Set-Cookie header did not
// have a domain= attribute, i.e. the cookie will only be stored for the
// request domain. Also, try to find cookies for subdomains, e.g.
// <.example.com>. We will find those variants with a leading dot in the
// map if the Set-Cookie header had a domain= attribute, i.e. the cookie
// will be stored for a parent domain and we send it for any subdomain.
for (let variant of [host, ...getPossibleSubdomainVariants(host)]) {
appendCookiesForHost(variant);
}
return cookies;
},
/**
* Stores a given cookie.
*
* @param cookie
* The nsICookie2 object to add to the storage.
*/
set: function (cookie) {
let jscookie = {host: cookie.host, value: cookie.value};
// Only add properties with non-default values to save a few bytes.
if (cookie.path) {
jscookie.path = cookie.path;
}
if (cookie.name) {
jscookie.name = cookie.name;
}
if (cookie.isSecure) {
jscookie.secure = true;
}
if (cookie.isHttpOnly) {
jscookie.httponly = true;
}
if (cookie.expiry < MAX_EXPIRY) {
jscookie.expiry = cookie.expiry;
}
if (cookie.originAttributes) {
jscookie.originAttributes = cookie.originAttributes;
}
this._ensureMap(cookie).set(cookie.name, jscookie);
},
/**
* Removes a given cookie.
*
* @param cookie
* The nsICookie2 object to be removed from storage.
*/
delete: function (cookie) {
this._ensureMap(cookie).delete(cookie.name);
},
/**
* Removes all cookies.
*/
clear: function () {
this._hosts.clear();
},
/**
* Creates all maps necessary to store a given cookie.
*
* @param cookie
* The nsICookie2 object to create maps for.
*
* @return The newly created Map instance mapping cookie names to
* internal jscookies, in the given path of the given host.
*/
_ensureMap: function (cookie) {
if (!this._hosts.has(cookie.host)) {
this._hosts.set(cookie.host, new Map());
}
let originAttributesMap = this._hosts.get(cookie.host);
// If cookie.originAttributes is null, originAttributes will be an empty string.
let originAttributes = ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
if (!originAttributesMap.has(originAttributes)) {
originAttributesMap.set(originAttributes, new Map());
}
let pathToNamesMap = originAttributesMap.get(originAttributes);
if (!pathToNamesMap.has(cookie.path)) {
pathToNamesMap.set(cookie.path, new Map());
}
return pathToNamesMap.get(cookie.path);
}
};

View file

@ -0,0 +1,399 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionFile"];
/**
* Implementation of all the disk I/O required by the session store.
* This is a private API, meant to be used only by the session store.
* It will change. Do not use it for any other purpose.
*
* Note that this module implicitly depends on one of two things:
* 1. either the asynchronous file I/O system enqueues its requests
* and never attempts to simultaneously execute two I/O requests on
* the files used by this module from two distinct threads; or
* 2. the clients of this API are well-behaved and do not place
* concurrent requests to the files used by this module.
*
* Otherwise, we could encounter bugs, especially under Windows,
* e.g. if a request attempts to write sessionstore.js while
* another attempts to copy that file.
*
* This implementation uses OS.File, which guarantees property 1.
*/
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RunState",
"resource:///modules/sessionstore/RunState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
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");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
const PREF_MAX_UPGRADE_BACKUPS = "browser.sessionstore.upgradeBackup.maxUpgradeBackups";
const PREF_MAX_SERIALIZE_BACK = "browser.sessionstore.max_serialize_back";
const PREF_MAX_SERIALIZE_FWD = "browser.sessionstore.max_serialize_forward";
this.SessionFile = {
/**
* Read the contents of the session file, asynchronously.
*/
read: function () {
return SessionFileInternal.read();
},
/**
* Write the contents of the session file, asynchronously.
*/
write: function (aData) {
return SessionFileInternal.write(aData);
},
/**
* Wipe the contents of the session file, asynchronously.
*/
wipe: function () {
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);
var Path = OS.Path;
var profileDir = OS.Constants.Path.profileDir;
var 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;
},
}),
// Number of attempted calls to `write`.
// Note that we may have _attempts > _successes + _failures,
// if attempts never complete.
// Used for error reporting.
_attempts: 0,
// Number of successful calls to `write`.
// Used for error reporting.
_successes: 0,
// Number of failed calls to `write`.
// Used for error reporting.
_failures: 0,
// Resolved once initialization is complete.
// The promise never rejects.
_deferredInitialized: PromiseUtils.defer(),
// `true` once we have started initialization, i.e. once something
// has been scheduled that will eventually resolve `_deferredInitialized`.
_initializationStarted: false,
// 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;
}
},
// Find the correct session file, read it and setup the worker.
read: Task.async(function* () {
this._initializationStarted = true;
let result;
let noFilesFound = true;
// 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);
if (!SessionStore.isFormatVersionCompatible(parsed.version || ["sessionrestore", 0] /*fallback for old versions*/)) {
// Skip sessionstore files that we don't understand.
Cu.reportError("Cannot extract data from Session Restore file " + path + ". Wrong format/version: " + JSON.stringify(parsed.version) + ".");
continue;
}
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 OS.File.Error) {
// The file might be inaccessible due to wrong permissions
// or similar failures. We'll just count it as "corrupted".
console.error("Could not read session file ", ex, ex.stack);
corrupted = true;
} catch (ex if ex instanceof SyntaxError) {
console.error("Corrupt session file (invalid JSON found) ", ex, ex.stack);
// File is corrupted, try next file
corrupted = true;
} finally {
if (exists) {
noFilesFound = false;
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
add(corrupted);
}
}
}
// All files are corrupted if files found but none could deliver a result.
let allCorrupt = !noFilesFound && !result;
Telemetry.getHistogramById("FX_SESSION_RESTORE_ALL_FILES_CORRUPT").
add(allCorrupt);
if (!result) {
// If everything fails, start with an empty session.
result = {
origin: "empty",
source: "",
parsed: null
};
}
result.noFilesFound = noFilesFound;
// Initialize the worker (in the background) to let it handle backups and also
// as a workaround for bug 964531.
let promiseInitialized = SessionWorker.post("init", [result.origin, this.Paths, {
maxUpgradeBackups: Preferences.get(PREF_MAX_UPGRADE_BACKUPS, 3),
maxSerializeBack: Preferences.get(PREF_MAX_SERIALIZE_BACK, 10),
maxSerializeForward: Preferences.get(PREF_MAX_SERIALIZE_FWD, -1)
}]);
promiseInitialized.catch(err => {
// Ensure that we report errors but that they do not stop us.
Promise.reject(err);
}).then(() => this._deferredInitialized.resolve());
return result;
}),
// Post a message to the worker, making sure that it has been initialized
// first.
_postToWorker: Task.async(function*(...args) {
if (!this._initializationStarted) {
// Initializing the worker is somewhat complex, as proper handling of
// backups requires us to first read and check the session. Consequently,
// the only way to initialize the worker is to first call `this.read()`.
// The call to `this.read()` causes background initialization of the worker.
// Initialization will be complete once `this._deferredInitialized.promise`
// resolves.
this.read();
}
yield this._deferredInitialized.promise;
return SessionWorker.post(...args)
}),
write: function (aData) {
if (RunState.isClosed) {
return Promise.reject(new Error("SessionFile is closed"));
}
let isFinalWrite = false;
if (RunState.isClosing) {
// If shutdown has started, we will want to stop receiving
// write instructions.
isFinalWrite = true;
RunState.setClosed();
}
let performShutdownCleanup = isFinalWrite &&
!sessionStartup.isAutomaticRestoreEnabled();
this._attempts++;
let options = {isFinalWrite, performShutdownCleanup};
let promise = this._postToWorker("write", [aData, options]);
// Wait until the write is done.
promise = promise.then(msg => {
// Record how long the write took.
this._recordTelemetry(msg.telemetry);
this._successes++;
if (msg.result.upgradeBackup) {
// We have just completed a backup-on-upgrade, store the information
// in preferences.
Services.prefs.setCharPref(PREF_UPGRADE_BACKUP,
Services.appinfo.platformBuildID);
}
}, err => {
// Catch and report any errors.
console.error("Could not write session state file ", err, err.stack);
this._failures++;
// By not doing anything special here we ensure that |promise| cannot
// be rejected anymore. The shutdown/cleanup code at the end of the
// function will thus always be executed.
});
// Ensure that we can write sessionstore.js cleanly before the profile
// becomes unaccessible.
AsyncShutdown.profileBeforeChange.addBlocker(
"SessionFile: Finish writing Session Restore data",
promise,
{
fetchState: () => ({
options,
attempts: this._attempts,
successes: this._successes,
failures: this._failures,
})
});
// This code will always be executed because |promise| can't fail anymore.
// We ensured that by having a reject handler that reports the failure but
// doesn't forward the rejection.
return promise.then(() => {
// Remove the blocker, no matter if writing failed or not.
AsyncShutdown.profileBeforeChange.removeBlocker(promise);
if (isFinalWrite) {
Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
}
});
},
wipe: function () {
return this._postToWorker("wipe");
},
_recordTelemetry: function(telemetry) {
for (let id of Object.keys(telemetry)){
let value = telemetry[id];
let samples = [];
if (Array.isArray(value)) {
samples.push(...value);
} else {
samples.push(value);
}
let histogram = Telemetry.getHistogramById(id);
for (let sample of samples) {
histogram.add(sample);
}
}
}
};

View file

@ -0,0 +1,428 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionHistory"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
function debug(msg) {
Services.console.logStringMessage("SessionHistory: " + msg);
}
/**
* The external API exported by this module.
*/
this.SessionHistory = Object.freeze({
isEmpty: function (docShell) {
return SessionHistoryInternal.isEmpty(docShell);
},
collect: function (docShell) {
return SessionHistoryInternal.collect(docShell);
},
restore: function (docShell, tabData) {
SessionHistoryInternal.restore(docShell, tabData);
}
});
/**
* The internal API for the SessionHistory module.
*/
var SessionHistoryInternal = {
/**
* Returns whether the given docShell's session history is empty.
*
* @param docShell
* The docShell that owns the session history.
*/
isEmpty: function (docShell) {
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory;
if (!webNavigation.currentURI) {
return true;
}
let uri = webNavigation.currentURI.spec;
return uri == "about:blank" && history.count == 0;
},
/**
* Collects session history data for a given docShell.
*
* @param docShell
* The docShell that owns the session history.
*/
collect: function (docShell) {
let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
let data = {entries: [], userContextId: loadContext.originAttributes.userContextId };
if (history && history.count > 0) {
// Loop over the transaction linked list directly so we can get the
// persist property for each transaction.
for (let txn = history.rootTransaction; txn; txn = txn.next) {
let entry = this.serializeEntry(txn.sHEntry);
entry.persist = txn.persist;
data.entries.push(entry);
}
// Ensure the index isn't out of bounds if an exception was thrown above.
data.index = Math.min(history.index + 1, data.entries.length);
}
// If either the session history isn't available yet or doesn't have any
// valid entries, make sure we at least include the current page.
if (data.entries.length == 0) {
let uri = webNavigation.currentURI.spec;
let body = webNavigation.document.body;
// We landed here because the history is inaccessible or there are no
// history entries. In that case we should at least record the docShell's
// current URL as a single history entry. If the URL is not about:blank
// or it's a blank tab that was modified (like a custom newtab page),
// record it. For about:blank we explicitly want an empty array without
// an 'index' property to denote that there are no history entries.
if (uri != "about:blank" || (body && body.hasChildNodes())) {
data.entries.push({ url: uri });
data.index = 1;
}
}
return data;
},
/**
* Get an object that is a serialized representation of a History entry.
*
* @param shEntry
* nsISHEntry instance
* @return object
*/
serializeEntry: function (shEntry) {
let entry = { url: shEntry.URI.spec };
// Save some bytes and don't include the title property
// if that's identical to the current entry's URL.
if (shEntry.title && shEntry.title != entry.url) {
entry.title = shEntry.title;
}
if (shEntry.isSubFrame) {
entry.subframe = true;
}
entry.charset = shEntry.URI.originCharset;
let cacheKey = shEntry.cacheKey;
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
cacheKey.data != 0) {
// XXXbz would be better to have cache keys implement
// nsISerializable or something.
entry.cacheKey = cacheKey.data;
}
entry.ID = shEntry.ID;
entry.docshellID = shEntry.docshellID;
// We will include the property only if it's truthy to save a couple of
// bytes when the resulting object is stringified and saved to disk.
if (shEntry.referrerURI) {
entry.referrer = shEntry.referrerURI.spec;
entry.referrerPolicy = shEntry.referrerPolicy;
}
if (shEntry.originalURI) {
entry.originalURI = shEntry.originalURI.spec;
}
if (shEntry.loadReplace) {
entry.loadReplace = shEntry.loadReplace;
}
if (shEntry.srcdocData)
entry.srcdocData = shEntry.srcdocData;
if (shEntry.isSrcdocEntry)
entry.isSrcdocEntry = shEntry.isSrcdocEntry;
if (shEntry.baseURI)
entry.baseURI = shEntry.baseURI.spec;
if (shEntry.contentType)
entry.contentType = shEntry.contentType;
if (shEntry.scrollRestorationIsManual) {
entry.scrollRestorationIsManual = true;
} else {
let x = {}, y = {};
shEntry.getScrollPosition(x, y);
if (x.value != 0 || y.value != 0)
entry.scroll = x.value + "," + y.value;
}
// Collect triggeringPrincipal data for the current history entry.
// Please note that before Bug 1297338 there was no concept of a
// principalToInherit. To remain backward/forward compatible we
// serialize the principalToInherit as triggeringPrincipal_b64.
// Once principalToInherit is well established (within FF55)
// we can update this code, remove triggeringPrincipal_b64 and
// just keep triggeringPrincipal_base64 as well as
// principalToInherit_base64; see Bug 1301666.
if (shEntry.principalToInherit) {
try {
let principalToInherit = Utils.serializePrincipal(shEntry.principalToInherit);
if (principalToInherit) {
entry.triggeringPrincipal_b64 = principalToInherit;
entry.principalToInherit_base64 = principalToInherit;
}
} catch (e) {
debug(e);
}
}
if (shEntry.triggeringPrincipal) {
try {
let triggeringPrincipal = Utils.serializePrincipal(shEntry.triggeringPrincipal);
if (triggeringPrincipal) {
entry.triggeringPrincipal_base64 = triggeringPrincipal;
}
} catch (e) {
debug(e);
}
}
entry.docIdentifier = shEntry.BFCacheEntry.ID;
if (shEntry.stateData != null) {
entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
entry.structuredCloneVersion = shEntry.stateData.formatVersion;
}
if (!(shEntry instanceof Ci.nsISHContainer)) {
return entry;
}
if (shEntry.childCount > 0 && !shEntry.hasDynamicallyAddedChild()) {
let children = [];
for (let i = 0; i < shEntry.childCount; i++) {
let child = shEntry.GetChildAt(i);
if (child) {
// Don't try to restore framesets containing wyciwyg URLs.
// (cf. bug 424689 and bug 450595)
if (child.URI.schemeIs("wyciwyg")) {
children.length = 0;
break;
}
children.push(this.serializeEntry(child));
}
}
if (children.length) {
entry.children = children;
}
}
return entry;
},
/**
* Restores session history data for a given docShell.
*
* @param docShell
* The docShell that owns the session history.
* @param tabData
* The tabdata including all history entries.
*/
restore: function (docShell, tabData) {
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory;
if (history.count > 0) {
history.PurgeHistory(history.count);
}
history.QueryInterface(Ci.nsISHistoryInternal);
let idMap = { used: {} };
let docIdentMap = {};
for (let i = 0; i < tabData.entries.length; i++) {
let entry = tabData.entries[i];
//XXXzpao Wallpaper patch for bug 514751
if (!entry.url)
continue;
let persist = "persist" in entry ? entry.persist : true;
history.addEntry(this.deserializeEntry(entry, idMap, docIdentMap), persist);
}
// Select the right history entry.
let index = tabData.index - 1;
if (index < history.count && history.index != index) {
history.getEntryAtIndex(index, true);
}
},
/**
* Expands serialized history data into a session-history-entry instance.
*
* @param entry
* Object containing serialized history data for a URL
* @param idMap
* Hash for ensuring unique frame IDs
* @param docIdentMap
* Hash to ensure reuse of BFCache entries
* @returns nsISHEntry
*/
deserializeEntry: function (entry, idMap, docIdentMap) {
var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
createInstance(Ci.nsISHEntry);
shEntry.setURI(Utils.makeURI(entry.url, entry.charset));
shEntry.setTitle(entry.title || entry.url);
if (entry.subframe)
shEntry.setIsSubFrame(entry.subframe || false);
shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
if (entry.contentType)
shEntry.contentType = entry.contentType;
if (entry.referrer) {
shEntry.referrerURI = Utils.makeURI(entry.referrer);
shEntry.referrerPolicy = entry.referrerPolicy;
}
if (entry.originalURI) {
shEntry.originalURI = Utils.makeURI(entry.originalURI);
}
if (entry.loadReplace) {
shEntry.loadReplace = entry.loadReplace;
}
if (entry.isSrcdocEntry)
shEntry.srcdocData = entry.srcdocData;
if (entry.baseURI)
shEntry.baseURI = Utils.makeURI(entry.baseURI);
if (entry.cacheKey) {
var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
createInstance(Ci.nsISupportsPRUint32);
cacheKey.data = entry.cacheKey;
shEntry.cacheKey = cacheKey;
}
if (entry.ID) {
// get a new unique ID for this frame (since the one from the last
// start might already be in use)
var id = idMap[entry.ID] || 0;
if (!id) {
for (id = Date.now(); id in idMap.used; id++);
idMap[entry.ID] = id;
idMap.used[id] = true;
}
shEntry.ID = id;
}
if (entry.docshellID)
shEntry.docshellID = entry.docshellID;
if (entry.structuredCloneState && entry.structuredCloneVersion) {
shEntry.stateData =
Cc["@mozilla.org/docshell/structured-clone-container;1"].
createInstance(Ci.nsIStructuredCloneContainer);
shEntry.stateData.initFromBase64(entry.structuredCloneState,
entry.structuredCloneVersion);
}
if (entry.scrollRestorationIsManual) {
shEntry.scrollRestorationIsManual = true;
} else if (entry.scroll) {
var scrollPos = (entry.scroll || "0,0").split(",");
scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
}
let childDocIdents = {};
if (entry.docIdentifier) {
// If we have a serialized document identifier, try to find an SHEntry
// which matches that doc identifier and adopt that SHEntry's
// BFCacheEntry. If we don't find a match, insert shEntry as the match
// for the document identifier.
let matchingEntry = docIdentMap[entry.docIdentifier];
if (!matchingEntry) {
matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
docIdentMap[entry.docIdentifier] = matchingEntry;
}
else {
shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
childDocIdents = matchingEntry.childDocIdents;
}
}
// The field entry.owner_b64 got renamed to entry.triggeringPricipal_b64 in
// Bug 1286472. To remain backward compatible we still have to support that
// field for a few cycles before we can remove it within Bug 1289785.
if (entry.owner_b64) {
entry.triggeringPricipal_b64 = entry.owner_b64;
delete entry.owner_b64;
}
// Before introducing the concept of principalToInherit we only had
// a triggeringPrincipal within every entry which basically is the
// equivalent of the new principalToInherit. To avoid compatibility
// issues, we first check if the entry has entries for
// triggeringPrincipal_base64 and principalToInherit_base64. If not
// we fall back to using the principalToInherit (which is stored
// as triggeringPrincipal_b64) as the triggeringPrincipal and
// the principalToInherit.
// FF55 will remove the triggeringPrincipal_b64, see Bug 1301666.
if (entry.triggeringPrincipal_base64 || entry.principalToInherit_base64) {
if (entry.triggeringPrincipal_base64) {
shEntry.triggeringPrincipal =
Utils.deserializePrincipal(entry.triggeringPrincipal_base64);
}
if (entry.principalToInherit_base64) {
shEntry.principalToInherit =
Utils.deserializePrincipal(entry.principalToInherit_base64);
}
} else if (entry.triggeringPrincipal_b64) {
shEntry.triggeringPrincipal = Utils.deserializePrincipal(entry.triggeringPrincipal_b64);
shEntry.principalToInherit = shEntry.triggeringPrincipal;
}
if (entry.children && shEntry instanceof Ci.nsISHContainer) {
for (var i = 0; i < entry.children.length; i++) {
//XXXzpao Wallpaper patch for bug 514751
if (!entry.children[i].url)
continue;
// We're getting sessionrestore.js files with a cycle in the
// doc-identifier graph, likely due to bug 698656. (That is, we have
// an entry where doc identifier A is an ancestor of doc identifier B,
// and another entry where doc identifier B is an ancestor of A.)
//
// If we were to respect these doc identifiers, we'd create a cycle in
// the SHEntries themselves, which causes the docshell to loop forever
// when it looks for the root SHEntry.
//
// So as a hack to fix this, we restrict the scope of a doc identifier
// to be a node's siblings and cousins, and pass childDocIdents, not
// aDocIdents, to _deserializeHistoryEntry. That is, we say that two
// SHEntries with the same doc identifier have the same document iff
// they have the same parent or their parents have the same document.
shEntry.AddChild(this.deserializeEntry(entry.children[i], idMap,
childDocIdents), i);
}
}
return shEntry;
},
};

View file

@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionMigration"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
// An encoder to UTF-8.
XPCOMUtils.defineLazyGetter(this, "gEncoder", function () {
return new TextEncoder();
});
// A decoder.
XPCOMUtils.defineLazyGetter(this, "gDecoder", function () {
return new TextDecoder();
});
var SessionMigrationInternal = {
/**
* Convert the original session restore state into a minimal state. It will
* only contain:
* - open windows
* - with tabs
* - with history entries with only title, url
* - with pinned state
* - with tab group info (hidden + group id)
* - with selected tab info
* - with selected window info
*
* The complete state is then wrapped into the "about:welcomeback" page as
* form field info to be restored when restoring the state.
*/
convertState: function(aStateObj) {
let state = {
selectedWindow: aStateObj.selectedWindow,
_closedWindows: []
};
state.windows = aStateObj.windows.map(function(oldWin) {
var win = {extData: {}};
win.tabs = oldWin.tabs.map(function(oldTab) {
var tab = {};
// Keep only titles and urls for history entries
tab.entries = oldTab.entries.map(function(entry) {
return {url: entry.url, title: entry.title};
});
tab.index = oldTab.index;
tab.hidden = oldTab.hidden;
tab.pinned = oldTab.pinned;
return tab;
});
win.selected = oldWin.selected;
win._closedTabs = [];
return win;
});
let url = "about:welcomeback";
let formdata = {id: {sessionData: state}, url};
return {windows: [{tabs: [{entries: [{url}], formdata}]}]};
},
/**
* Asynchronously read session restore state (JSON) from a path
*/
readState: function(aPath) {
return Task.spawn(function() {
let bytes = yield OS.File.read(aPath);
let text = gDecoder.decode(bytes);
let state = JSON.parse(text);
throw new Task.Result(state);
});
},
/**
* Asynchronously write session restore state as JSON to a path
*/
writeState: function(aPath, aState) {
let bytes = gEncoder.encode(JSON.stringify(aState));
return OS.File.writeAtomic(aPath, bytes, {tmpPath: aPath + ".tmp"});
}
}
var SessionMigration = {
/**
* Migrate a limited set of session data from one path to another.
*/
migrate: function(aFromPath, aToPath) {
return Task.spawn(function() {
let inState = yield SessionMigrationInternal.readState(aFromPath);
let outState = SessionMigrationInternal.convertState(inState);
// Unfortunately, we can't use SessionStore's own SessionFile to
// write out the data because it has a dependency on the profile dir
// being known. When the migration runs, there is no guarantee that
// that's true.
yield SessionMigrationInternal.writeState(aToPath, outState);
});
}
};

View file

@ -0,0 +1,264 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionSaver"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Timer.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
"resource:///modules/sessionstore/PrivacyFilter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RunState",
"resource:///modules/sessionstore/RunState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
"resource:///modules/sessionstore/SessionFile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
// Minimal interval between two save operations (in milliseconds).
XPCOMUtils.defineLazyGetter(this, "gInterval", function () {
const PREF = "browser.sessionstore.interval";
// Observer that updates the cached value when the preference changes.
Services.prefs.addObserver(PREF, () => {
this.gInterval = Services.prefs.getIntPref(PREF);
// Cancel any pending runs and call runDelayed() with
// zero to apply the newly configured interval.
SessionSaverInternal.cancel();
SessionSaverInternal.runDelayed(0);
}, false);
return Services.prefs.getIntPref(PREF);
});
// Notify observers about a given topic with a given subject.
function notify(subject, topic) {
Services.obs.notifyObservers(subject, topic, "");
}
// TelemetryStopwatch helper functions.
function stopWatch(method) {
return function (...histograms) {
for (let hist of histograms) {
TelemetryStopwatch[method]("FX_SESSION_RESTORE_" + hist);
}
};
}
var stopWatchStart = stopWatch("start");
var stopWatchCancel = stopWatch("cancel");
var stopWatchFinish = stopWatch("finish");
/**
* The external API implemented by the SessionSaver module.
*/
this.SessionSaver = Object.freeze({
/**
* Immediately saves the current session to disk.
*/
run: function () {
return SessionSaverInternal.run();
},
/**
* Saves the current session to disk delayed by a given amount of time. Should
* another delayed run be scheduled already, we will ignore the given delay
* and state saving may occur a little earlier.
*/
runDelayed: function () {
SessionSaverInternal.runDelayed();
},
/**
* Sets the last save time to the current time. This will cause us to wait for
* at least the configured interval when runDelayed() is called next.
*/
updateLastSaveTime: function () {
SessionSaverInternal.updateLastSaveTime();
},
/**
* Cancels all pending session saves.
*/
cancel: function () {
SessionSaverInternal.cancel();
}
});
/**
* The internal API.
*/
var SessionSaverInternal = {
/**
* The timeout ID referencing an active timer for a delayed save. When no
* save is pending, this is null.
*/
_timeoutID: null,
/**
* A timestamp that keeps track of when we saved the session last. We will
* this to determine the correct interval between delayed saves to not deceed
* the configured session write interval.
*/
_lastSaveTime: 0,
/**
* Immediately saves the current session to disk.
*/
run: function () {
return this._saveState(true /* force-update all windows */);
},
/**
* Saves the current session to disk delayed by a given amount of time. Should
* another delayed run be scheduled already, we will ignore the given delay
* and state saving may occur a little earlier.
*
* @param delay (optional)
* The minimum delay in milliseconds to wait for until we collect and
* save the current session.
*/
runDelayed: function (delay = 2000) {
// Bail out if there's a pending run.
if (this._timeoutID) {
return;
}
// Interval until the next disk operation is allowed.
delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0);
// Schedule a state save.
this._timeoutID = setTimeout(() => this._saveStateAsync(), delay);
},
/**
* Sets the last save time to the current time. This will cause us to wait for
* at least the configured interval when runDelayed() is called next.
*/
updateLastSaveTime: function () {
this._lastSaveTime = Date.now();
},
/**
* Cancels all pending session saves.
*/
cancel: function () {
clearTimeout(this._timeoutID);
this._timeoutID = null;
},
/**
* Saves the current session state. Collects data and writes to disk.
*
* @param forceUpdateAllWindows (optional)
* Forces us to recollect data for all windows and will bypass and
* update the corresponding caches.
*/
_saveState: function (forceUpdateAllWindows = false) {
// Cancel any pending timeouts.
this.cancel();
if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
// Don't save (or even collect) anything in permanent private
// browsing mode
this.updateLastSaveTime();
return Promise.resolve();
}
stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
let state = SessionStore.getCurrentState(forceUpdateAllWindows);
PrivacyFilter.filterPrivateWindowsAndTabs(state);
// Make sure that we keep the previous session if we started with a single
// private window and no non-private windows have been opened, yet.
if (state.deferredInitialState) {
state.windows = state.deferredInitialState.windows || [];
delete state.deferredInitialState;
}
if (AppConstants.platform != "macosx") {
// We want to restore closed windows that are marked with _shouldRestore.
// We're doing this here because we want to control this only when saving
// the file.
while (state._closedWindows.length) {
let i = state._closedWindows.length - 1;
if (!state._closedWindows[i]._shouldRestore) {
// We only need to go until _shouldRestore
// is falsy since we're going in reverse.
break;
}
delete state._closedWindows[i]._shouldRestore;
state.windows.unshift(state._closedWindows.pop());
}
}
// Clear all cookies on clean shutdown according to user preferences
if (RunState.isClosing) {
let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
let restart = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
// Don't clear cookies when restarting
if ((expireCookies || sanitizeCookies) && !restart) {
for (let window of state.windows) {
delete window.cookies;
}
}
}
stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
return this._writeState(state);
},
/**
* Saves the current session state. Collects data asynchronously and calls
* _saveState() to collect data again (with a cache hit rate of hopefully
* 100%) and write to disk afterwards.
*/
_saveStateAsync: function () {
// Allow scheduling delayed saves again.
this._timeoutID = null;
// Write to disk.
this._saveState();
},
/**
* Write the given state object to disk.
*/
_writeState: function (state) {
// We update the time stamp before writing so that we don't write again
// too soon, if saving is requested before the write completes. Without
// this update we may save repeatedly if actions cause a runDelayed
// before writing has completed. See Bug 902280
this.updateLastSaveTime();
// Write (atomically) to a session file, using a tmp file. Once the session
// file is successfully updated, save the time stamp of the last save and
// notify the observers.
return SessionFile.write(state).then(() => {
this.updateLastSaveTime();
notify(null, "sessionstore-state-write-complete");
}, console.error);
},
};

View file

@ -0,0 +1,173 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionStorage"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
// Returns the principal for a given |frame| contained in a given |docShell|.
function getPrincipalForFrame(docShell, frame) {
let ssm = Services.scriptSecurityManager;
let uri = frame.document.documentURIObject;
return ssm.getDocShellCodebasePrincipal(uri, docShell);
}
this.SessionStorage = Object.freeze({
/**
* Updates all sessionStorage "super cookies"
* @param docShell
* That tab's docshell (containing the sessionStorage)
* @param frameTree
* The docShell's FrameTree instance.
* @return Returns a nested object that will have hosts as keys and per-host
* session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
collect: function (docShell, frameTree) {
return SessionStorageInternal.collect(docShell, frameTree);
},
/**
* Restores all sessionStorage "super cookies".
* @param aDocShell
* A tab's docshell (containing the sessionStorage)
* @param aStorageData
* A nested object with storage data to be restored that has hosts as
* keys and per-host session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
restore: function (aDocShell, aStorageData) {
SessionStorageInternal.restore(aDocShell, aStorageData);
},
});
var SessionStorageInternal = {
/**
* Reads all session storage data from the given docShell.
* @param docShell
* A tab's docshell (containing the sessionStorage)
* @param frameTree
* The docShell's FrameTree instance.
* @return Returns a nested object that will have hosts as keys and per-host
* session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
collect: function (docShell, frameTree) {
let data = {};
let visitedOrigins = new Set();
frameTree.forEach(frame => {
let principal = getPrincipalForFrame(docShell, frame);
if (!principal) {
return;
}
// Get the origin of the current history entry
// and use that as a key for the per-principal storage data.
let origin = principal.origin;
if (visitedOrigins.has(origin)) {
// Don't read a host twice.
return;
}
// Mark the current origin as visited.
visitedOrigins.add(origin);
let originData = this._readEntry(principal, docShell);
if (Object.keys(originData).length) {
data[origin] = originData;
}
});
return Object.keys(data).length ? data : null;
},
/**
* Writes session storage data to the given tab.
* @param aDocShell
* A tab's docshell (containing the sessionStorage)
* @param aStorageData
* A nested object with storage data to be restored that has hosts as
* keys and per-host session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
restore: function (aDocShell, aStorageData) {
for (let origin of Object.keys(aStorageData)) {
let data = aStorageData[origin];
let principal;
try {
let attrs = aDocShell.getOriginAttributes();
let originURI = Services.io.newURI(origin, null, null);
principal = Services.scriptSecurityManager.createCodebasePrincipal(originURI, attrs);
} catch (e) {
console.error(e);
continue;
}
let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
// There is no need to pass documentURI, it's only used to fill documentURI property of
// domstorage event, which in this case has no consumer. Prevention of events in case
// of missing documentURI will be solved in a followup bug to bug 600307.
let storage = storageManager.createStorage(window, principal, "", aDocShell.usePrivateBrowsing);
for (let key of Object.keys(data)) {
try {
storage.setItem(key, data[key]);
} catch (e) {
// throws e.g. for URIs that can't have sessionStorage
console.error(e);
}
}
}
},
/**
* Reads an entry in the session storage data contained in a tab's history.
* @param aURI
* That history entry uri
* @param aDocShell
* A tab's docshell (containing the sessionStorage)
*/
_readEntry: function (aPrincipal, aDocShell) {
let hostData = {};
let storage;
let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
try {
let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
storage = storageManager.getStorage(window, aPrincipal);
storage.length; // XXX: Bug 1232955 - storage.length can throw, catch that failure
} catch (e) {
// sessionStorage might throw if it's turned off, see bug 458954
storage = null;
}
if (storage && storage.length) {
for (let i = 0; i < storage.length; i++) {
try {
let key = storage.key(i);
hostData[key] = storage.getItem(key);
} catch (e) {
// This currently throws for secured items (cf. bug 442048).
}
}
}
return hostData;
}
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,381 @@
/* 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/. */
/**
* A worker dedicated to handle I/O for Session Store.
*/
"use strict";
importScripts("resource://gre/modules/osfile.jsm");
var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
var File = OS.File;
var Encoder = new TextEncoder();
var Decoder = new TextDecoder();
var worker = new PromiseWorker.AbstractWorker();
worker.dispatch = function(method, args = []) {
return Agent[method](...args);
};
worker.postMessage = function(result, ...transfers) {
self.postMessage(result, ...transfers);
};
worker.close = function() {
self.close();
};
self.addEventListener("message", msg => worker.handleMessage(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";
var Agent = {
// Path to the files used by the SessionWorker
Paths: null,
/**
* 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.
*/
state: null,
/**
* Number of old upgrade backups that are being kept
*/
maxUpgradeBackups: 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.
* @param {object} prefs The preferences the worker needs to known.
*/
init(origin, paths, prefs = {}) {
if (!(origin in paths || origin == STATE_EMPTY)) {
throw new TypeError("Invalid origin: " + origin);
}
// Check that all required preference values were passed.
for (let pref of ["maxUpgradeBackups", "maxSerializeBack", "maxSerializeForward"]) {
if (!prefs.hasOwnProperty(pref)) {
throw new TypeError(`Missing preference value for ${pref}`);
}
}
this.state = origin;
this.Paths = paths;
this.maxUpgradeBackups = prefs.maxUpgradeBackups;
this.maxSerializeBack = prefs.maxSerializeBack;
this.maxSerializeForward = prefs.maxSerializeForward;
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 {object} state 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 (state, options = {}) {
let exn;
let telemetry = {};
// Cap the number of backward and forward shistory entries on shutdown.
if (options.isFinalWrite) {
for (let window of state.windows) {
for (let tab of window.tabs) {
let lower = 0;
let upper = tab.entries.length;
if (this.maxSerializeBack > -1) {
lower = Math.max(lower, tab.index - this.maxSerializeBack - 1);
}
if (this.maxSerializeForward > -1) {
upper = Math.min(upper, tab.index + this.maxSerializeForward);
}
tab.entries = tab.entries.slice(lower, upper);
tab.index -= lower;
}
}
}
let stateString = JSON.stringify(state);
let data = Encoder.encode(stateString);
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);
}
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);
}
let 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"
});
}
telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startWriteMs;
telemetry.FX_SESSION_RESTORE_FILE_SIZE_BYTES = data.byteLength;
} catch (ex) {
// Don't throw immediately
exn = exn || ex;
}
// 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;
}
// Find all backups
let iterator;
let backups = []; // array that will contain the paths to all upgrade backup
let upgradeBackupPrefix = this.Paths.upgradeBackupPrefix; // access for forEach callback
try {
iterator = new File.DirectoryIterator(this.Paths.backups);
iterator.forEach(function (file) {
if (file.path.startsWith(upgradeBackupPrefix)) {
backups.push(file.path);
}
}, this);
} catch (ex) {
// Don't throw immediately
exn = exn || ex;
} finally {
if (iterator) {
iterator.close();
}
}
// If too many backups exist, delete them
if (backups.length > this.maxUpgradeBackups) {
// Use alphanumerical sort since dates are in YYYYMMDDHHMMSS format
backups.sort().forEach((file, i) => {
// remove backup file if it is among the first (n-maxUpgradeBackups) files
if (i < backups.length - this.maxUpgradeBackups) {
File.remove(file);
}
});
}
}
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 {
result: {
upgradeBackup: upgradeBackupComplete
},
telemetry: telemetry,
};
},
/**
* Wipes all files holding session data from disk.
*/
wipe: function () {
// Don't stop immediately in case of error.
let exn = null;
// Erase main session state file
try {
File.remove(this.Paths.clean);
} catch (ex) {
// Don't stop immediately.
exn = exn || ex;
}
// 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);
try {
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
exn = exn || ex;
}
}
}
if (exn) {
throw exn;
}
} finally {
iterator.close();
}
},
};
function isNoSuchFileEx(aReason) {
return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile;
}
/**
* Estimate the number of bytes that a data structure will use on disk
* once serialized.
*/
function getByteLength(str) {
return Encoder.encode(JSON.stringify(str)).byteLength;
}

View file

@ -0,0 +1,25 @@
/* 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/PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
this.EXPORTED_SYMBOLS = ["SessionWorker"];
this.SessionWorker = new BasePromiseWorker("resource:///modules/sessionstore/SessionWorker.js");
// As the Session Worker performs I/O, we can receive instances of
// OS.File.Error, so we need to install a decoder.
this.SessionWorker.ExceptionHandlers["OS.File.Error"] = OS.File.Error.fromMsg;

View file

@ -0,0 +1,234 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["StartupPerformance"];
const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
const COLLECT_RESULTS_AFTER_MS = 10000;
const OBSERVED_TOPICS = ["sessionstore-restoring-on-startup", "sessionstore-initiating-manual-restore"];
this.StartupPerformance = {
/**
* Once we have finished restoring initial tabs, we broadcast on this topic.
*/
RESTORED_TOPIC: "sessionstore-finished-restoring-initial-tabs",
// Instant at which we have started restoration (notification "sessionstore-restoring-on-startup")
_startTimeStamp: null,
// Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored")
_latestRestoredTimeStamp: null,
// A promise resolved once we have finished restoring all the startup tabs.
_promiseFinished: null,
// Function `resolve()` for `_promiseFinished`.
_resolveFinished: null,
// A timer
_deadlineTimer: null,
// `true` once the timer has fired
_hasFired: false,
// `true` once we are restored
_isRestored: false,
// Statistics on the session we need to restore.
_totalNumberOfEagerTabs: 0,
_totalNumberOfTabs: 0,
_totalNumberOfWindows: 0,
init: function() {
for (let topic of OBSERVED_TOPICS) {
Services.obs.addObserver(this, topic, false);
}
},
/**
* Return the timestamp at which we finished restoring the latest tab.
*
* This information is not really interesting until we have finished restoring
* tabs.
*/
get latestRestoredTimeStamp() {
return this._latestRestoredTimeStamp;
},
/**
* `true` once we have finished restoring startup tabs.
*/
get isRestored() {
return this._isRestored;
},
// Called when restoration starts.
// Record the start timestamp, setup the timer and `this._promiseFinished`.
// Behavior is unspecified if there was already an ongoing measure.
_onRestorationStarts: function(isAutoRestore) {
this._latestRestoredTimeStamp = this._startTimeStamp = Date.now();
this._totalNumberOfEagerTabs = 0;
this._totalNumberOfTabs = 0;
this._totalNumberOfWindows = 0;
// While we may restore several sessions in a single run of the browser,
// that's a very unusual case, and not really worth measuring, so let's
// stop listening for further restorations.
for (let topic of OBSERVED_TOPICS) {
Services.obs.removeObserver(this, topic);
}
Services.obs.addObserver(this, "sessionstore-single-window-restored", false);
this._promiseFinished = new Promise(resolve => {
this._resolveFinished = resolve;
});
this._promiseFinished.then(() => {
try {
this._isRestored = true;
Services.obs.notifyObservers(null, this.RESTORED_TOPIC, "");
if (this._latestRestoredTimeStamp == this._startTimeStamp) {
// Apparently, we haven't restored any tab.
return;
}
// Once we are done restoring tabs, update Telemetry.
let histogramName = isAutoRestore ?
"FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS" :
"FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS";
let histogram = Services.telemetry.getHistogramById(histogramName);
let delta = this._latestRestoredTimeStamp - this._startTimeStamp;
histogram.add(delta);
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED").add(this._totalNumberOfEagerTabs);
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED").add(this._totalNumberOfTabs);
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED").add(this._totalNumberOfWindows);
// Reset
this._startTimeStamp = null;
} catch (ex) {
console.error("StartupPerformance: error after resolving promise", ex);
}
});
},
_startTimer: function() {
if (this._hasFired) {
return;
}
if (this._deadlineTimer) {
clearTimeout(this._deadlineTimer);
}
this._deadlineTimer = setTimeout(() => {
try {
this._resolveFinished();
} catch (ex) {
console.error("StartupPerformance: Error in timeout handler", ex);
} finally {
// Clean up.
this._deadlineTimer = null;
this._hasFired = true;
this._resolveFinished = null;
Services.obs.removeObserver(this, "sessionstore-single-window-restored");
}
}, COLLECT_RESULTS_AFTER_MS);
},
observe: function(subject, topic, details) {
try {
switch (topic) {
case "sessionstore-restoring-on-startup":
this._onRestorationStarts(true);
break;
case "sessionstore-initiating-manual-restore":
this._onRestorationStarts(false);
break;
case "sessionstore-single-window-restored": {
// Session Restore has just opened a window with (initially empty) tabs.
// Some of these tabs will be restored eagerly, while others will be
// restored on demand. The process becomes usable only when all windows
// have finished restored their eager tabs.
//
// While it would be possible to track the restoration of each tab
// from within SessionRestore to determine exactly when the process
// becomes usable, experience shows that this is too invasive. Rather,
// we employ the following heuristic:
// - we maintain a timer of `COLLECT_RESULTS_AFTER_MS` that we expect
// will be triggered only once all tabs have been restored;
// - whenever we restore a new window (hence a bunch of eager tabs),
// we postpone the timer to ensure that the new eager tabs have
// `COLLECT_RESULTS_AFTER_MS` to be restored;
// - whenever a tab is restored, we update
// `this._latestRestoredTimeStamp`;
// - after `COLLECT_RESULTS_AFTER_MS`, we collect the final version
// of `this._latestRestoredTimeStamp`, and use it to determine the
// entire duration of the collection.
//
// Note that this heuristic may be inaccurate if a user clicks
// immediately on a restore-on-demand tab before the end of
// `COLLECT_RESULTS_AFTER_MS`. We assume that this will not
// affect too much the results.
//
// Reset the delay, to give the tabs a little (more) time to restore.
this._startTimer();
this._totalNumberOfWindows += 1;
// Observe the restoration of all tabs. We assume that all tabs of this
// window will have been restored before `COLLECT_RESULTS_AFTER_MS`.
// The last call to `observer` will let us determine how long it took
// to reach that point.
let win = subject;
let observer = (event) => {
// We don't care about tab restorations that are due to
// a browser flipping from out-of-main-process to in-main-process
// or vice-versa. We only care about restorations that are due
// to the user switching to a lazily restored tab, or for tabs
// that are restoring eagerly.
if (!event.detail.isRemotenessUpdate) {
this._latestRestoredTimeStamp = Date.now();
this._totalNumberOfEagerTabs += 1;
}
};
win.gBrowser.tabContainer.addEventListener("SSTabRestored", observer);
this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount;
// Once we have finished collecting the results, clean up the observers.
this._promiseFinished.then(() => {
if (!win.gBrowser.tabContainer) {
// May be undefined during shutdown and/or some tests.
return;
}
win.gBrowser.tabContainer.removeEventListener("SSTabRestored", observer);
});
}
break;
default:
throw new Error(`Unexpected topic ${topic}`);
}
} catch (ex) {
console.error("StartupPerformance error", ex, ex.stack);
throw ex;
}
}
};

View file

@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["TabAttributes"];
// We never want to directly read or write these attributes.
// 'image' should not be accessed directly but handled by using the
// gBrowser.getIcon()/setIcon() methods.
// 'muted' should not be accessed directly but handled by using the
// tab.linkedBrowser.audioMuted/toggleMuteAudio methods.
// 'pending' is used internal by sessionstore and managed accordingly.
// 'iconLoadingPrincipal' is same as 'image' that it should be handled by
// using the gBrowser.getIcon()/setIcon() methods.
const ATTRIBUTES_TO_SKIP = new Set(["image", "muted", "pending", "iconLoadingPrincipal"]);
// A set of tab attributes to persist. We will read a given list of tab
// attributes when collecting tab data and will re-set those attributes when
// the given tab data is restored to a new tab.
this.TabAttributes = Object.freeze({
persist: function (name) {
return TabAttributesInternal.persist(name);
},
get: function (tab) {
return TabAttributesInternal.get(tab);
},
set: function (tab, data = {}) {
TabAttributesInternal.set(tab, data);
}
});
var TabAttributesInternal = {
_attrs: new Set(),
persist: function (name) {
if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.has(name)) {
return false;
}
this._attrs.add(name);
return true;
},
get: function (tab) {
let data = {};
for (let name of this._attrs) {
if (tab.hasAttribute(name)) {
data[name] = tab.getAttribute(name);
}
}
return data;
},
set: function (tab, data = {}) {
// Clear attributes.
for (let name of this._attrs) {
tab.removeAttribute(name);
}
// Set attributes.
for (let name in data) {
if (!ATTRIBUTES_TO_SKIP.has(name)) {
tab.setAttribute(name, data[name]);
}
}
}
};

View file

@ -0,0 +1,196 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["TabState"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
"resource:///modules/sessionstore/PrivacyFilter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
"resource:///modules/sessionstore/TabStateCache.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
"resource:///modules/sessionstore/TabAttributes.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
/**
* Module that contains tab state collection methods.
*/
this.TabState = Object.freeze({
update: function (browser, data) {
TabStateInternal.update(browser, data);
},
collect: function (tab) {
return TabStateInternal.collect(tab);
},
clone: function (tab) {
return TabStateInternal.clone(tab);
},
copyFromCache(browser, tabData, options) {
TabStateInternal.copyFromCache(browser, tabData, options);
},
});
var TabStateInternal = {
/**
* Processes a data update sent by the content script.
*/
update: function (browser, {data}) {
TabStateCache.update(browser, data);
},
/**
* Collect data related to a single tab, synchronously.
*
* @param tab
* tabbrowser tab
*
* @returns {TabData} An object with the data for this tab. If the
* tab has not been invalidated since the last call to
* collect(aTab), the same object is returned.
*/
collect: function (tab) {
return this._collectBaseTabData(tab);
},
/**
* Collect data related to a single tab, including private data.
* Use with caution.
*
* @param tab
* tabbrowser tab
*
* @returns {object} An object with the data for this tab. This data is never
* cached, it will always be read from the tab and thus be
* up-to-date.
*/
clone: function (tab) {
return this._collectBaseTabData(tab, {includePrivateData: true});
},
/**
* Collects basic tab data for a given tab.
*
* @param tab
* tabbrowser tab
* @param options (object)
* {includePrivateData: true} to always include private data
*
* @returns {object} An object with the basic data for this tab.
*/
_collectBaseTabData: function (tab, options) {
let tabData = { entries: [], lastAccessed: tab.lastAccessed };
let browser = tab.linkedBrowser;
if (tab.pinned) {
tabData.pinned = true;
}
tabData.hidden = tab.hidden;
if (browser.audioMuted) {
tabData.muted = true;
tabData.muteReason = tab.muteReason;
}
// Save tab attributes.
tabData.attributes = TabAttributes.get(tab);
if (tab.__SS_extdata) {
tabData.extData = tab.__SS_extdata;
}
// Copy data from the tab state cache only if the tab has fully finished
// restoring. We don't want to overwrite data contained in __SS_data.
this.copyFromCache(browser, tabData, options);
// After copyFromCache() was called we check for properties that are kept
// in the cache only while the tab is pending or restoring. Once that
// happened those properties will be removed from the cache and will
// be read from the tab/browser every time we collect data.
// Store the tab icon.
if (!("image" in tabData)) {
let tabbrowser = tab.ownerGlobal.gBrowser;
tabData.image = tabbrowser.getIcon(tab);
}
// Store the serialized contentPrincipal of this tab to use for the icon.
if (!("iconLoadingPrincipal" in tabData)) {
tabData.iconLoadingPrincipal = Utils.serializePrincipal(browser.contentPrincipal);
}
// If there is a userTypedValue set, then either the user has typed something
// in the URL bar, or a new tab was opened with a URI to load.
// If so, we also track whether we were still in the process of loading something.
if (!("userTypedValue" in tabData) && browser.userTypedValue) {
tabData.userTypedValue = browser.userTypedValue;
// We always used to keep track of the loading state as an integer, where
// '0' indicated the user had typed since the last load (or no load was
// ongoing), and any positive value indicated we had started a load since
// the last time the user typed in the URL bar. Mimic this to keep the
// session store representation in sync, even though we now represent this
// more explicitly:
tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0;
}
return tabData;
},
/**
* Copy data for the given |browser| from the cache to |tabData|.
*
* @param browser (xul:browser)
* The browser belonging to the given |tabData| object.
* @param tabData (object)
* The tab data belonging to the given |tab|.
* @param options (object)
* {includePrivateData: true} to always include private data
*/
copyFromCache(browser, tabData, options = {}) {
let data = TabStateCache.get(browser);
if (!data) {
return;
}
// The caller may explicitly request to omit privacy checks.
let includePrivateData = options && options.includePrivateData;
let isPinned = !!tabData.pinned;
for (let key of Object.keys(data)) {
let value = data[key];
// Filter sensitive data according to the current privacy level.
if (!includePrivateData) {
if (key === "storage") {
value = PrivacyFilter.filterSessionStorageData(value);
} else if (key === "formdata") {
value = PrivacyFilter.filterFormData(value);
}
}
if (key === "history") {
tabData.entries = value.entries;
if (value.hasOwnProperty("userContextId")) {
tabData.userContextId = value.userContextId;
}
if (value.hasOwnProperty("index")) {
tabData.index = value.index;
}
} else {
tabData[key] = value;
}
}
}
};

View file

@ -0,0 +1,163 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["TabStateCache"];
/**
* A cache for tabs data.
*
* This cache implements a weak map from tabs (as XUL elements)
* to tab data (as objects).
*
* Note that we should never cache private data, as:
* - that data is used very seldom by SessionStore;
* - caching private data in addition to public data is memory consuming.
*/
this.TabStateCache = Object.freeze({
/**
* Retrieves cached data for a given |tab| or associated |browser|.
*
* @param browserOrTab (xul:tab or xul:browser)
* The tab or browser to retrieve cached data for.
* @return (object)
* The cached data stored for the given |tab|
* or associated |browser|.
*/
get: function (browserOrTab) {
return TabStateCacheInternal.get(browserOrTab);
},
/**
* Updates cached data for a given |tab| or associated |browser|.
*
* @param browserOrTab (xul:tab or xul:browser)
* The tab or browser belonging to the given tab data.
* @param newData (object)
* The new data to be stored for the given |tab|
* or associated |browser|.
*/
update: function (browserOrTab, newData) {
TabStateCacheInternal.update(browserOrTab, newData);
}
});
var TabStateCacheInternal = {
_data: new WeakMap(),
/**
* Retrieves cached data for a given |tab| or associated |browser|.
*
* @param browserOrTab (xul:tab or xul:browser)
* The tab or browser to retrieve cached data for.
* @return (object)
* The cached data stored for the given |tab|
* or associated |browser|.
*/
get: function (browserOrTab) {
return this._data.get(browserOrTab.permanentKey);
},
/**
* Helper function used by update (see below). For message size
* optimization sometimes we don't update the whole session storage
* only the values that have been changed.
*
* @param data (object)
* The cached data where we want to update the changes.
* @param change (object)
* The actual changed values per domain.
*/
updatePartialStorageChange: function (data, change) {
if (!data.storage) {
data.storage = {};
}
let storage = data.storage;
for (let domain of Object.keys(change)) {
for (let key of Object.keys(change[domain])) {
let value = change[domain][key];
if (value === null) {
if (storage[domain] && storage[domain][key]) {
delete storage[domain][key];
}
} else {
if (!storage[domain]) {
storage[domain] = {};
}
storage[domain][key] = value;
}
}
}
},
/**
* Helper function used by update (see below). For message size
* optimization sometimes we don't update the whole browser history
* only the current index and the tail of the history from a certain
* index (specified by change.fromIdx)
*
* @param data (object)
* The cached data where we want to update the changes.
* @param change (object)
* Object containing the tail of the history array, and
* some additional metadata.
*/
updatePartialHistoryChange: function (data, change) {
const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
if (!data.history) {
data.history = { entries: [] };
}
let history = data.history;
for (let key of Object.keys(change)) {
if (key == "entries") {
if (change.fromIdx != kLastIndex) {
history.entries.splice(change.fromIdx + 1);
while (change.entries.length) {
history.entries.push(change.entries.shift());
}
}
} else if (key != "fromIndex") {
history[key] = change[key];
}
}
},
/**
* Updates cached data for a given |tab| or associated |browser|.
*
* @param browserOrTab (xul:tab or xul:browser)
* The tab or browser belonging to the given tab data.
* @param newData (object)
* The new data to be stored for the given |tab|
* or associated |browser|.
*/
update: function (browserOrTab, newData) {
let data = this._data.get(browserOrTab.permanentKey) || {};
for (let key of Object.keys(newData)) {
if (key == "storagechange") {
this.updatePartialStorageChange(data, newData.storagechange);
continue;
}
if (key == "historychange") {
this.updatePartialHistoryChange(data, newData.historychange);
continue;
}
let value = newData[key];
if (value === null) {
delete data[key];
} else {
data[key] = value;
}
}
this._data.set(browserOrTab.permanentKey, data);
}
};

View file

@ -0,0 +1,184 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["TabStateFlusher"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Promise.jsm", this);
/**
* A module that enables async flushes. Updates from frame scripts are
* throttled to be sent only once per second. If an action wants a tab's latest
* state without waiting for a second then it can request an async flush and
* wait until the frame scripts reported back. At this point the parent has the
* latest data and the action can continue.
*/
this.TabStateFlusher = Object.freeze({
/**
* Requests an async flush for the given browser. Returns a promise that will
* resolve when we heard back from the content process and the parent has
* all the latest data.
*/
flush(browser) {
return TabStateFlusherInternal.flush(browser);
},
/**
* Requests an async flush for all browsers of a given window. Returns a Promise
* that will resolve when we've heard back from all browsers.
*/
flushWindow(window) {
return TabStateFlusherInternal.flushWindow(window);
},
/**
* Resolves the flush request with the given flush ID.
*
* @param browser (<xul:browser>)
* The browser for which the flush is being resolved.
* @param flushID (int)
* The ID of the flush that was sent to the browser.
* @param success (bool, optional)
* Whether or not the flush succeeded.
* @param message (string, optional)
* An error message that will be sent to the Console in the
* event that a flush failed.
*/
resolve(browser, flushID, success=true, message="") {
TabStateFlusherInternal.resolve(browser, flushID, success, message);
},
/**
* Resolves all active flush requests for a given browser. This should be
* used when the content process crashed or the final update message was
* seen. In those cases we can't guarantee to ever hear back from the frame
* script so we just resolve all requests instead of discarding them.
*
* @param browser (<xul:browser>)
* The browser for which all flushes are being resolved.
* @param success (bool, optional)
* Whether or not the flushes succeeded.
* @param message (string, optional)
* An error message that will be sent to the Console in the
* event that the flushes failed.
*/
resolveAll(browser, success=true, message="") {
TabStateFlusherInternal.resolveAll(browser, success, message);
}
});
var TabStateFlusherInternal = {
// Stores the last request ID.
_lastRequestID: 0,
// A map storing all active requests per browser.
_requests: new WeakMap(),
/**
* Requests an async flush for the given browser. Returns a promise that will
* resolve when we heard back from the content process and the parent has
* all the latest data.
*/
flush(browser) {
let id = ++this._lastRequestID;
let mm = browser.messageManager;
mm.sendAsyncMessage("SessionStore:flush", {id});
// Retrieve active requests for given browser.
let permanentKey = browser.permanentKey;
let perBrowserRequests = this._requests.get(permanentKey) || new Map();
return new Promise(resolve => {
// Store resolve() so that we can resolve the promise later.
perBrowserRequests.set(id, resolve);
// Update the flush requests stored per browser.
this._requests.set(permanentKey, perBrowserRequests);
});
},
/**
* Requests an async flush for all browsers of a given window. Returns a Promise
* that will resolve when we've heard back from all browsers.
*/
flushWindow(window) {
let browsers = window.gBrowser.browsers;
let promises = browsers.map((browser) => this.flush(browser));
return Promise.all(promises);
},
/**
* Resolves the flush request with the given flush ID.
*
* @param browser (<xul:browser>)
* The browser for which the flush is being resolved.
* @param flushID (int)
* The ID of the flush that was sent to the browser.
* @param success (bool, optional)
* Whether or not the flush succeeded.
* @param message (string, optional)
* An error message that will be sent to the Console in the
* event that a flush failed.
*/
resolve(browser, flushID, success=true, message="") {
// Nothing to do if there are no pending flushes for the given browser.
if (!this._requests.has(browser.permanentKey)) {
return;
}
// Retrieve active requests for given browser.
let perBrowserRequests = this._requests.get(browser.permanentKey);
if (!perBrowserRequests.has(flushID)) {
return;
}
if (!success) {
Cu.reportError("Failed to flush browser: " + message);
}
// Resolve the request with the given id.
let resolve = perBrowserRequests.get(flushID);
perBrowserRequests.delete(flushID);
resolve(success);
},
/**
* Resolves all active flush requests for a given browser. This should be
* used when the content process crashed or the final update message was
* seen. In those cases we can't guarantee to ever hear back from the frame
* script so we just resolve all requests instead of discarding them.
*
* @param browser (<xul:browser>)
* The browser for which all flushes are being resolved.
* @param success (bool, optional)
* Whether or not the flushes succeeded.
* @param message (string, optional)
* An error message that will be sent to the Console in the
* event that the flushes failed.
*/
resolveAll(browser, success=true, message="") {
// Nothing to do if there are no pending flushes for the given browser.
if (!this._requests.has(browser.permanentKey)) {
return;
}
// Retrieve active requests for given browser.
let perBrowserRequests = this._requests.get(browser.permanentKey);
if (!success) {
Cu.reportError("Failed to flush browser: " + message);
}
// Resolve all requests.
for (let resolve of perBrowserRequests.values()) {
resolve(success);
}
// Clear active requests.
perBrowserRequests.clear();
}
};

View file

@ -0,0 +1,362 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
var gStateObject;
var gTreeData;
// Page initialization
window.onload = function() {
// pages used by this script may have a link that needs to be updated to
// the in-product link.
let anchor = document.getElementById("linkMoreTroubleshooting");
if (anchor) {
let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
anchor.setAttribute("href", baseURL + "troubleshooting");
}
// wire up click handlers for the radio buttons if they exist.
for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) {
let button = document.getElementById(radioId);
if (button) {
button.addEventListener("click", updateTabListVisibility);
}
}
// the crashed session state is kept inside a textbox so that SessionStore picks it up
// (for when the tab is closed or the session crashes right again)
var sessionData = document.getElementById("sessionData");
if (!sessionData.value) {
document.getElementById("errorTryAgain").disabled = true;
return;
}
gStateObject = JSON.parse(sessionData.value);
// make sure the data is tracked to be restored in case of a subsequent crash
var event = document.createEvent("UIEvents");
event.initUIEvent("input", true, true, window, 0);
sessionData.dispatchEvent(event);
initTreeView();
document.getElementById("errorTryAgain").focus();
};
function isTreeViewVisible() {
let tabList = document.querySelector(".tree-container");
return tabList.hasAttribute("available");
}
function initTreeView() {
// If we aren't visible we initialize as we are made visible (and it's OK
// to initialize multiple times)
if (!isTreeViewVisible()) {
return;
}
var tabList = document.getElementById("tabList");
var winLabel = tabList.getAttribute("_window_label");
gTreeData = [];
gStateObject.windows.forEach(function(aWinData, aIx) {
var winState = {
label: winLabel.replace("%S", (aIx + 1)),
open: true,
checked: true,
ix: aIx
};
winState.tabs = aWinData.tabs.map(function(aTabData) {
var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank" };
var iconURL = aTabData.image || null;
// don't initiate a connection just to fetch a favicon (see bug 462863)
if (/^https?:/.test(iconURL))
iconURL = "moz-anno:favicon:" + iconURL;
return {
label: entry.title || entry.url,
checked: true,
src: iconURL,
parent: winState
};
});
gTreeData.push(winState);
for (let tab of winState.tabs)
gTreeData.push(tab);
}, this);
tabList.view = treeView;
tabList.view.selection.select(0);
}
// User actions
function updateTabListVisibility() {
let tabList = document.querySelector(".tree-container");
let container = document.querySelector(".container");
if (document.getElementById("radioRestoreChoose").checked) {
tabList.setAttribute("available", "true");
container.classList.add("restore-chosen");
} else {
tabList.removeAttribute("available");
container.classList.remove("restore-chosen");
}
initTreeView();
}
function restoreSession() {
Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore", "");
document.getElementById("errorTryAgain").disabled = true;
if (isTreeViewVisible()) {
if (!gTreeData.some(aItem => aItem.checked)) {
// This should only be possible when we have no "cancel" button, and thus
// the "Restore session" button always remains enabled. In that case and
// when nothing is selected, we just want a new session.
startNewSession();
return;
}
// remove all unselected tabs from the state before restoring it
var ix = gStateObject.windows.length - 1;
for (var t = gTreeData.length - 1; t >= 0; t--) {
if (treeView.isContainer(t)) {
if (gTreeData[t].checked === 0)
// this window will be restored partially
gStateObject.windows[ix].tabs =
gStateObject.windows[ix].tabs.filter((aTabData, aIx) =>
gTreeData[t].tabs[aIx].checked);
else if (!gTreeData[t].checked)
// this window won't be restored at all
gStateObject.windows.splice(ix, 1);
ix--;
}
}
}
var stateString = JSON.stringify(gStateObject);
var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
var top = getBrowserWindow();
// if there's only this page open, reuse the window for restoring the session
if (top.gBrowser.tabs.length == 1) {
ss.setWindowState(top, stateString, true);
return;
}
// restore the session into a new window and close the current tab
var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all");
var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
obs.addObserver(function observe(win, topic) {
if (win != newWindow) {
return;
}
obs.removeObserver(observe, topic);
ss.setWindowState(newWindow, stateString, true);
var tabbrowser = top.gBrowser;
var tabIndex = tabbrowser.getBrowserIndexForDocument(document);
tabbrowser.removeTab(tabbrowser.tabs[tabIndex]);
}, "browser-delayed-startup-finished", false);
}
function startNewSession() {
var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
if (prefBranch.getIntPref("browser.startup.page") == 0)
getBrowserWindow().gBrowser.loadURI("about:blank");
else
getBrowserWindow().BrowserHome();
}
function onListClick(aEvent) {
// don't react to right-clicks
if (aEvent.button == 2)
return;
var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY);
if (cell.col) {
// Restore this specific tab in the same window for middle/double/accel clicking
// on a tab's title.
let accelKey = AppConstants.platform == "macosx" ?
aEvent.metaKey :
aEvent.ctrlKey;
if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) &&
cell.col.id == "title" &&
!treeView.isContainer(cell.row)) {
restoreSingleTab(cell.row, aEvent.shiftKey);
aEvent.stopPropagation();
}
else if (cell.col.id == "restore")
toggleRowChecked(cell.row);
}
}
function onListKeyDown(aEvent) {
switch (aEvent.keyCode)
{
case KeyEvent.DOM_VK_SPACE:
toggleRowChecked(document.getElementById("tabList").currentIndex);
// Prevent page from scrolling on the space key.
aEvent.preventDefault();
break;
case KeyEvent.DOM_VK_RETURN:
var ix = document.getElementById("tabList").currentIndex;
if (aEvent.ctrlKey && !treeView.isContainer(ix))
restoreSingleTab(ix, aEvent.shiftKey);
break;
}
}
// Helper functions
function getBrowserWindow() {
return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
}
function toggleRowChecked(aIx) {
function isChecked(aItem) {
return aItem.checked;
}
var item = gTreeData[aIx];
item.checked = !item.checked;
treeView.treeBox.invalidateRow(aIx);
if (treeView.isContainer(aIx)) {
// (un)check all tabs of this window as well
for (let tab of item.tabs) {
tab.checked = item.checked;
treeView.treeBox.invalidateRow(gTreeData.indexOf(tab));
}
}
else {
// update the window's checkmark as well (0 means "partially checked")
item.parent.checked = item.parent.tabs.every(isChecked) ? true :
item.parent.tabs.some(isChecked) ? 0 : false;
treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent));
}
// we only disable the button when there's no cancel button.
if (document.getElementById("errorCancel")) {
document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked);
}
}
function restoreSingleTab(aIx, aShifted) {
var tabbrowser = getBrowserWindow().gBrowser;
var newTab = tabbrowser.addTab();
var item = gTreeData[aIx];
var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
var tabState = gStateObject.windows[item.parent.ix]
.tabs[aIx - gTreeData.indexOf(item.parent) - 1];
// ensure tab would be visible on the tabstrip.
tabState.hidden = false;
ss.setTabState(newTab, JSON.stringify(tabState));
// respect the preference as to whether to select the tab (the Shift key inverses)
var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
if (prefBranch.getBoolPref("browser.tabs.loadInBackground") != !aShifted)
tabbrowser.selectedTab = newTab;
}
// Tree controller
var treeView = {
treeBox: null,
selection: null,
get rowCount() { return gTreeData.length; },
setTree: function(treeBox) { this.treeBox = treeBox; },
getCellText: function(idx, column) { return gTreeData[idx].label; },
isContainer: function(idx) { return "open" in gTreeData[idx]; },
getCellValue: function(idx, column){ return gTreeData[idx].checked; },
isContainerOpen: function(idx) { return gTreeData[idx].open; },
isContainerEmpty: function(idx) { return false; },
isSeparator: function(idx) { return false; },
isSorted: function() { return false; },
isEditable: function(idx, column) { return false; },
canDrop: function(idx, orientation, dt) { return false; },
getLevel: function(idx) { return this.isContainer(idx) ? 0 : 1; },
getParentIndex: function(idx) {
if (!this.isContainer(idx))
for (var t = idx - 1; t >= 0 ; t--)
if (this.isContainer(t))
return t;
return -1;
},
hasNextSibling: function(idx, after) {
var thisLevel = this.getLevel(idx);
for (var t = after + 1; t < gTreeData.length; t++)
if (this.getLevel(t) <= thisLevel)
return this.getLevel(t) == thisLevel;
return false;
},
toggleOpenState: function(idx) {
if (!this.isContainer(idx))
return;
var item = gTreeData[idx];
if (item.open) {
// remove this window's tab rows from the view
var thisLevel = this.getLevel(idx);
for (var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++);
var deletecount = t - idx - 1;
gTreeData.splice(idx + 1, deletecount);
this.treeBox.rowCountChanged(idx + 1, -deletecount);
}
else {
// add this window's tab rows to the view
var toinsert = gTreeData[idx].tabs;
for (var i = 0; i < toinsert.length; i++)
gTreeData.splice(idx + i + 1, 0, toinsert[i]);
this.treeBox.rowCountChanged(idx + 1, toinsert.length);
}
item.open = !item.open;
this.treeBox.invalidateRow(idx);
},
getCellProperties: function(idx, column) {
if (column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0)
return "partial";
if (column.id == "title")
return this.getImageSrc(idx, column) ? "icon" : "noicon";
return "";
},
getRowProperties: function(idx) {
var winState = gTreeData[idx].parent || gTreeData[idx];
if (winState.ix % 2 != 0)
return "alternate";
return "";
},
getImageSrc: function(idx, column) {
if (column.id == "title")
return gTreeData[idx].src || null;
return null;
},
getProgressMode : function(idx, column) { },
cycleHeader: function(column) { },
cycleCell: function(idx, column) { },
selectionChanged: function() { },
performAction: function(action) { },
performActionOnCell: function(action, index, column) { },
getColumnProperties: function(column) { return ""; }
};

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<!DOCTYPE html [
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
%htmlDTD;
<!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
%netErrorDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd">
%restorepageDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<head>
<title>&restorepage.tabtitle;</title>
<link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/>
<link rel="icon" type="image/png" href="chrome://global/skin/icons/warning-16.png"/>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/>
</head>
<body dir="&locale.dir;">
<div class="container restore-chosen">
<div class="title">
<h1 class="title-text">&restorepage.errorTitle;</h1>
</div>
<div class="description">
<p>&restorepage.problemDesc;</p>
<div id="errorLongDesc">
<p>&restorepage.tryThis;</p>
<ul>
<li>&restorepage.restoreSome;</li>
<li>&restorepage.startNew;</li>
</ul>
</div>
</div>
<div class="tree-container" available="true">
<xul:tree id="tabList" seltype="single" hidecolumnpicker="true"
onclick="onListClick(event);" onkeydown="onListKeyDown(event);"
_window_label="&restorepage.windowLabel;">
<xul:treecols>
<xul:treecol cycler="true" id="restore" type="checkbox" label="&restorepage.restoreHeader;"/>
<xul:splitter class="tree-splitter"/>
<xul:treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/>
</xul:treecols>
<xul:treechildren flex="1"/>
</xul:tree>
</div>
<div class="button-container">
#ifdef XP_UNIX
<xul:button id="errorCancel"
label="&restorepage.closeButton;"
accesskey="&restorepage.close.access;"
oncommand="startNewSession();"/>
<xul:button class="primary"
id="errorTryAgain"
label="&restorepage.tryagainButton;"
accesskey="&restorepage.restore.access;"
oncommand="restoreSession();"/>
#else
<xul:button class="primary"
id="errorTryAgain"
label="&restorepage.tryagainButton;"
accesskey="&restorepage.restore.access;"
oncommand="restoreSession();"/>
<xul:button id="errorCancel"
label="&restorepage.closeButton;"
accesskey="&restorepage.close.access;"
oncommand="startNewSession();"/>
#endif
</div>
<!-- holds the session data for when the tab is closed -->
<input type="text" id="sessionData" style="display: none;"/>
</div>
</body>
</html>

View file

@ -0,0 +1,897 @@
/* 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";
function debug(msg) {
Services.console.logStringMessage("SessionStoreContent: " + msg);
}
var Cu = Components.utils;
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "FormData",
"resource://gre/modules/FormData.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
"resource:///modules/sessionstore/PageStyle.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource://gre/modules/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
var gFrameTree = new FrameTree(this);
Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
() => { return new ContentRestore(this) });
// The current epoch.
var gCurrentEpoch = 0;
// A bound to the size of data to store for DOM Storage.
const DOM_STORAGE_MAX_CHARS = 10000000; // 10M characters
// This pref controls whether or not we send updates to the parent on a timeout
// or not, and should only be used for tests or debugging.
const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates";
const kNoIndex = Number.MAX_SAFE_INTEGER;
const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
/**
* Returns a lazy function that will evaluate the given
* function |fn| only once and cache its return value.
*/
function createLazy(fn) {
let cached = false;
let cachedValue = null;
return function lazy() {
if (!cached) {
cachedValue = fn();
cached = true;
}
return cachedValue;
};
}
/**
* Listens for and handles content events that we need for the
* session store service to be notified of state changes in content.
*/
var EventListener = {
init: function () {
addEventListener("load", this, true);
},
handleEvent: function (event) {
// Ignore load events from subframes.
if (event.target != content.document) {
return;
}
if (content.document.documentURI.startsWith("about:reader")) {
if (event.type == "load" &&
!content.document.body.classList.contains("loaded")) {
// Don't restore the scroll position of an about:reader page at this
// point; listen for the custom event dispatched from AboutReader.jsm.
content.addEventListener("AboutReaderContentReady", this);
return;
}
content.removeEventListener("AboutReaderContentReady", this);
}
// Restore the form data and scroll position. If we're not currently
// restoring a tab state then this call will simply be a noop.
gContentRestore.restoreDocument();
}
};
/**
* Listens for and handles messages sent by the session store service.
*/
var MessageListener = {
MESSAGES: [
"SessionStore:restoreHistory",
"SessionStore:restoreTabContent",
"SessionStore:resetRestore",
"SessionStore:flush",
],
init: function () {
this.MESSAGES.forEach(m => addMessageListener(m, this));
},
receiveMessage: function ({name, data}) {
// The docShell might be gone. Don't process messages,
// that will just lead to errors anyway.
if (!docShell) {
return;
}
// A fresh tab always starts with epoch=0. The parent has the ability to
// override that to signal a new era in this tab's life. This enables it
// to ignore async messages that were already sent but not yet received
// and would otherwise confuse the internal tab state.
if (data.epoch && data.epoch != gCurrentEpoch) {
gCurrentEpoch = data.epoch;
}
switch (name) {
case "SessionStore:restoreHistory":
this.restoreHistory(data);
break;
case "SessionStore:restoreTabContent":
this.restoreTabContent(data);
break;
case "SessionStore:resetRestore":
gContentRestore.resetRestore();
break;
case "SessionStore:flush":
this.flush(data);
break;
default:
debug("received unknown message '" + name + "'");
break;
}
},
restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) {
gContentRestore.restoreHistory(tabData, loadArguments, {
// Note: The callbacks passed here will only be used when a load starts
// that was not initiated by sessionstore itself. This can happen when
// some code calls browser.loadURI() or browser.reload() on a pending
// browser/tab.
onLoadStarted() {
// Notify the parent that the tab is no longer pending.
sendSyncMessage("SessionStore:restoreTabContentStarted", {epoch});
},
onLoadFinished() {
// Tell SessionStore.jsm that it may want to restore some more tabs,
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch});
}
});
// When restoreHistory finishes, we send a synchronous message to
// SessionStore.jsm so that it can run SSTabRestoring. Users of
// SSTabRestoring seem to get confused if chrome and content are out of
// sync about the state of the restore (particularly regarding
// docShell.currentURI). Using a synchronous message is the easiest way
// to temporarily synchronize them.
sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate});
},
restoreTabContent({loadArguments, isRemotenessUpdate}) {
let epoch = gCurrentEpoch;
// We need to pass the value of didStartLoad back to SessionStore.jsm.
let didStartLoad = gContentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => {
// Tell SessionStore.jsm that it may want to restore some more tabs,
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
});
sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch, isRemotenessUpdate});
if (!didStartLoad) {
// Pretend that the load succeeded so that event handlers fire correctly.
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
}
},
flush({id}) {
// Flush the message queue, send the latest updates.
MessageQueue.send({flushID: id});
}
};
/**
* Listens for changes to the session history. Whenever the user navigates
* we will collect URLs and everything belonging to session history.
*
* Causes a SessionStore:update message to be sent that contains the current
* session history.
*
* Example:
* {entries: [{url: "about:mozilla", ...}, ...], index: 1}
*/
var SessionHistoryListener = {
init: function () {
// The frame tree observer is needed to handle initial subframe loads.
// It will redundantly invalidate with the SHistoryListener in some cases
// but these invalidations are very cheap.
gFrameTree.addObserver(this);
// By adding the SHistoryListener immediately, we will unfortunately be
// notified of every history entry as the tab is restored. We don't bother
// waiting to add the listener later because these notifications are cheap.
// We will likely only collect once since we are batching collection on
// a delay.
docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.
addSHistoryListener(this);
// Collect data if we start with a non-empty shistory.
if (!SessionHistory.isEmpty(docShell)) {
this.collect();
// When a tab is detached from the window, for the new window there is a
// new SessionHistoryListener created. Normally it is empty at this point
// but in a test env. the initial about:blank might have a children in which
// case we fire off a history message here with about:blank in it. If we
// don't do it ASAP then there is going to be a browser swap and the parent
// will be all confused by that message.
MessageQueue.send();
}
// Listen for page title changes.
addEventListener("DOMTitleChanged", this);
},
uninit: function () {
let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
if (sessionHistory) {
sessionHistory.removeSHistoryListener(this);
}
},
collect: function () {
this._fromIdx = kNoIndex;
if (docShell) {
MessageQueue.push("history", () => SessionHistory.collect(docShell));
}
},
_fromIdx: kNoIndex,
// History can grow relatively big with the nested elements, so if we don't have to, we
// don't want to send the entire history all the time. For a simple optimization
// we keep track of the smallest index from after any change has occured and we just send
// the elements from that index. If something more complicated happens we just clear it
// and send the entire history. We always send the additional info like the current selected
// index (so for going back and forth between history entries we set the index to kLastIndex
// if nothing else changed send an empty array and the additonal info like the selected index)
collectFrom: function (idx) {
if (this._fromIdx <= idx) {
// If we already know that we need to update history fromn index N we can ignore any changes
// tha happened with an element with index larger than N.
// Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything
// here, and in case of navigation in the history back and forth we use kLastIndex which ignores
// only the subsequent navigations, but not any new elements added.
return;
}
this._fromIdx = idx;
MessageQueue.push("historychange", () => {
if (this._fromIdx === kNoIndex) {
return null;
}
let history = SessionHistory.collect(docShell);
if (kLastIndex == idx) {
history.entries = [];
} else {
history.entries.splice(0, this._fromIdx + 1);
}
history.fromIdx = this._fromIdx;
this._fromIdx = kNoIndex;
return history;
});
},
handleEvent(event) {
this.collect();
},
onFrameTreeCollected: function () {
this.collect();
},
onFrameTreeReset: function () {
this.collect();
},
OnHistoryNewEntry: function (newURI, oldIndex) {
this.collectFrom(oldIndex);
},
OnHistoryGoBack: function (backURI) {
this.collectFrom(kLastIndex);
return true;
},
OnHistoryGoForward: function (forwardURI) {
this.collectFrom(kLastIndex);
return true;
},
OnHistoryGotoIndex: function (index, gotoURI) {
this.collectFrom(kLastIndex);
return true;
},
OnHistoryPurge: function (numEntries) {
this.collect();
return true;
},
OnHistoryReload: function (reloadURI, reloadFlags) {
this.collect();
return true;
},
OnHistoryReplaceEntry: function (index) {
this.collect();
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsISHistoryListener,
Ci.nsISupportsWeakReference
])
};
/**
* Listens for scroll position changes. Whenever the user scrolls the top-most
* frame we update the scroll position and will restore it when requested.
*
* Causes a SessionStore:update message to be sent that contains the current
* scroll positions as a tree of strings. If no frame of the whole frame tree
* is scrolled this will return null so that we don't tack a property onto
* the tabData object in the parent process.
*
* Example:
* {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
*/
var ScrollPositionListener = {
init: function () {
addEventListener("scroll", this);
gFrameTree.addObserver(this);
},
handleEvent: function (event) {
let frame = event.target.defaultView;
// Don't collect scroll data for frames created at or after the load event
// as SessionStore can't restore scroll data for those.
if (gFrameTree.contains(frame)) {
MessageQueue.push("scroll", () => this.collect());
}
},
onFrameTreeCollected: function () {
MessageQueue.push("scroll", () => this.collect());
},
onFrameTreeReset: function () {
MessageQueue.push("scroll", () => null);
},
collect: function () {
return gFrameTree.map(ScrollPosition.collect);
}
};
/**
* Listens for changes to input elements. Whenever the value of an input
* element changes we will re-collect data for the current frame tree and send
* a message to the parent process.
*
* Causes a SessionStore:update message to be sent that contains the form data
* for all reachable frames.
*
* Example:
* {
* formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
* children: [
* null,
* {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
* ]
* }
*/
var FormDataListener = {
init: function () {
addEventListener("input", this, true);
addEventListener("change", this, true);
gFrameTree.addObserver(this);
},
handleEvent: function (event) {
let frame = event.target.ownerGlobal;
// Don't collect form data for frames created at or after the load event
// as SessionStore can't restore form data for those.
if (gFrameTree.contains(frame)) {
MessageQueue.push("formdata", () => this.collect());
}
},
onFrameTreeReset: function () {
MessageQueue.push("formdata", () => null);
},
collect: function () {
return gFrameTree.map(FormData.collect);
}
};
/**
* Listens for changes to the page style. Whenever a different page style is
* selected or author styles are enabled/disabled we send a message with the
* currently applied style to the chrome process.
*
* Causes a SessionStore:update message to be sent that contains the currently
* selected pageStyle for all reachable frames.
*
* Example:
* {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]}
*/
var PageStyleListener = {
init: function () {
Services.obs.addObserver(this, "author-style-disabled-changed", false);
Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false);
gFrameTree.addObserver(this);
},
uninit: function () {
Services.obs.removeObserver(this, "author-style-disabled-changed");
Services.obs.removeObserver(this, "style-sheet-applicable-state-changed");
},
observe: function (subject, topic) {
let frame = subject.defaultView;
if (frame && gFrameTree.contains(frame)) {
MessageQueue.push("pageStyle", () => this.collect());
}
},
collect: function () {
return PageStyle.collect(docShell, gFrameTree);
},
onFrameTreeCollected: function () {
MessageQueue.push("pageStyle", () => this.collect());
},
onFrameTreeReset: function () {
MessageQueue.push("pageStyle", () => null);
}
};
/**
* Listens for changes to docShell capabilities. Whenever a new load is started
* we need to re-check the list of capabilities and send message when it has
* changed.
*
* Causes a SessionStore:update message to be sent that contains the currently
* disabled docShell capabilities (all nsIDocShell.allow* properties set to
* false) as a string - i.e. capability names separate by commas.
*/
var DocShellCapabilitiesListener = {
/**
* This field is used to compare the last docShell capabilities to the ones
* that have just been collected. If nothing changed we won't send a message.
*/
_latestCapabilities: "",
init: function () {
gFrameTree.addObserver(this);
},
/**
* onFrameTreeReset() is called as soon as we start loading a page.
*/
onFrameTreeReset: function() {
// The order of docShell capabilities cannot change while we're running
// so calling join() without sorting before is totally sufficient.
let caps = DocShellCapabilities.collect(docShell).join(",");
// Send new data only when the capability list changes.
if (caps != this._latestCapabilities) {
this._latestCapabilities = caps;
MessageQueue.push("disallow", () => caps || null);
}
}
};
/**
* Listens for changes to the DOMSessionStorage. Whenever new keys are added,
* existing ones removed or changed, or the storage is cleared we will send a
* message to the parent process containing up-to-date sessionStorage data.
*
* Causes a SessionStore:update message to be sent that contains the current
* DOMSessionStorage contents. The data is a nested object using host names
* as keys and per-host DOMSessionStorage data as values.
*/
var SessionStorageListener = {
init: function () {
addEventListener("MozSessionStorageChanged", this, true);
Services.obs.addObserver(this, "browser:purge-domain-data", false);
gFrameTree.addObserver(this);
},
uninit: function () {
Services.obs.removeObserver(this, "browser:purge-domain-data");
},
handleEvent: function (event) {
if (gFrameTree.contains(event.target)) {
this.collectFromEvent(event);
}
},
observe: function () {
// Collect data on the next tick so that any other observer
// that needs to purge data can do its work first.
setTimeout(() => this.collect(), 0);
},
// Before DOM Storage can be written to disk, it needs to be serialized
// for sending across frames/processes, then again to be sent across
// threads, then again to be put in a buffer for the disk. Each of these
// serializations is an opportunity to OOM and (depending on the site of
// the OOM), either crash, lose all data for the frame or lose all data
// for the application.
//
// In order to avoid this, compute an estimate of the size of the
// object, and block SessionStorage items that are too large. As
// we also don't want to cause an OOM here, we use a quick and memory-
// efficient approximation: we compute the total sum of string lengths
// involved in this object.
estimateStorageSize: function(collected) {
if (!collected) {
return 0;
}
let size = 0;
for (let host of Object.keys(collected)) {
size += host.length;
let perHost = collected[host];
for (let key of Object.keys(perHost)) {
size += key.length;
let perKey = perHost[key];
size += perKey.length;
}
}
return size;
},
// We don't want to send all the session storage data for all the frames
// for every change. So if only a few value changed we send them over as
// a "storagechange" event. If however for some reason before we send these
// changes we have to send over the entire sessions storage data, we just
// reset these changes.
_changes: undefined,
resetChanges: function () {
this._changes = undefined;
},
collectFromEvent: function (event) {
// TODO: we should take browser.sessionstore.dom_storage_limit into an account here.
if (docShell) {
let {url, key, newValue} = event;
let uri = Services.io.newURI(url, null, null);
let domain = uri.prePath;
if (!this._changes) {
this._changes = {};
}
if (!this._changes[domain]) {
this._changes[domain] = {};
}
this._changes[domain][key] = newValue;
MessageQueue.push("storagechange", () => {
let tmp = this._changes;
// If there were multiple changes we send them merged.
// First one will collect all the changes the rest of
// these messages will be ignored.
this.resetChanges();
return tmp;
});
}
},
collect: function () {
if (docShell) {
// We need the entire session storage, let's reset the pending individual change
// messages.
this.resetChanges();
MessageQueue.push("storage", () => {
let collected = SessionStorage.collect(docShell, gFrameTree);
if (collected == null) {
return collected;
}
let size = this.estimateStorageSize(collected);
MessageQueue.push("telemetry", () => ({ FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS: size }));
if (size > Preferences.get("browser.sessionstore.dom_storage_limit", DOM_STORAGE_MAX_CHARS)) {
// Rather than keeping the old storage, which wouldn't match the rest
// of the state of the page, empty the storage. DOM storage will be
// recollected the next time and stored if it is now small enough.
return {};
}
return collected;
});
}
},
onFrameTreeCollected: function () {
this.collect();
},
onFrameTreeReset: function () {
this.collect();
}
};
/**
* Listen for changes to the privacy status of the tab.
* By definition, tabs start in non-private mode.
*
* Causes a SessionStore:update message to be sent for
* field "isPrivate". This message contains
* |true| if the tab is now private
* |null| if the tab is now public - the field is therefore
* not saved.
*/
var PrivacyListener = {
init: function() {
docShell.addWeakPrivacyTransitionObserver(this);
// Check that value at startup as it might have
// been set before the frame script was loaded.
if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) {
MessageQueue.push("isPrivate", () => true);
}
},
// Ci.nsIPrivacyTransitionObserver
privateModeChanged: function(enabled) {
MessageQueue.push("isPrivate", () => enabled || null);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver,
Ci.nsISupportsWeakReference])
};
/**
* A message queue that takes collected data and will take care of sending it
* to the chrome process. It allows flushing using synchronous messages and
* takes care of any race conditions that might occur because of that. Changes
* will be batched if they're pushed in quick succession to avoid a message
* flood.
*/
var MessageQueue = {
/**
* A map (string -> lazy fn) holding lazy closures of all queued data
* collection routines. These functions will return data collected from the
* docShell.
*/
_data: new Map(),
/**
* The delay (in ms) used to delay sending changes after data has been
* invalidated.
*/
BATCH_DELAY_MS: 1000,
/**
* The current timeout ID, null if there is no queue data. We use timeouts
* to damp a flood of data changes and send lots of changes as one batch.
*/
_timeout: null,
/**
* Whether or not sending batched messages on a timer is disabled. This should
* only be used for debugging or testing. If you need to access this value,
* you should probably use the timeoutDisabled getter.
*/
_timeoutDisabled: false,
/**
* True if batched messages are not being fired on a timer. This should only
* ever be true when debugging or during tests.
*/
get timeoutDisabled() {
return this._timeoutDisabled;
},
/**
* Disables sending batched messages on a timer. Also cancels any pending
* timers.
*/
set timeoutDisabled(val) {
this._timeoutDisabled = val;
if (val && this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
return val;
},
init() {
this.timeoutDisabled =
Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this, false);
},
uninit() {
Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this);
},
observe(subject, topic, data) {
if (topic == "nsPref:changed" && data == TIMEOUT_DISABLED_PREF) {
this.timeoutDisabled =
Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
}
},
/**
* Pushes a given |value| onto the queue. The given |key| represents the type
* of data that is stored and can override data that has been queued before
* but has not been sent to the parent process, yet.
*
* @param key (string)
* A unique identifier specific to the type of data this is passed.
* @param fn (function)
* A function that returns the value that will be sent to the parent
* process.
*/
push: function (key, fn) {
this._data.set(key, createLazy(fn));
if (!this._timeout && !this._timeoutDisabled) {
// Wait a little before sending the message to batch multiple changes.
this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS);
}
},
/**
* Sends queued data to the chrome process.
*
* @param options (object)
* {flushID: 123} to specify that this is a flush
* {isFinal: true} to signal this is the final message sent on unload
*/
send: function (options = {}) {
// Looks like we have been called off a timeout after the tab has been
// closed. The docShell is gone now and we can just return here as there
// is nothing to do.
if (!docShell) {
return;
}
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
let flushID = (options && options.flushID) || 0;
let durationMs = Date.now();
let data = {};
let telemetry = {};
for (let [key, func] of this._data) {
let value = func();
if (key == "telemetry") {
for (let histogramId of Object.keys(value)) {
telemetry[histogramId] = value[histogramId];
}
} else if (value || (key != "storagechange" && key != "historychange")) {
data[key] = value;
}
}
this._data.clear();
durationMs = Date.now() - durationMs;
telemetry.FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS = durationMs;
try {
// Send all data to the parent process.
sendAsyncMessage("SessionStore:update", {
data, telemetry, flushID,
isFinal: options.isFinal || false,
epoch: gCurrentEpoch
});
} catch (ex if ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
let telemetry = {
FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM: 1
};
sendAsyncMessage("SessionStore:error", {
telemetry
});
}
},
};
EventListener.init();
MessageListener.init();
FormDataListener.init();
PageStyleListener.init();
SessionHistoryListener.init();
SessionStorageListener.init();
ScrollPositionListener.init();
DocShellCapabilitiesListener.init();
PrivacyListener.init();
MessageQueue.init();
function handleRevivedTab() {
if (!content) {
removeEventListener("pagehide", handleRevivedTab);
return;
}
if (content.document.documentURI.startsWith("about:tabcrashed")) {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
// Sanity check - we'd better be loading this in a non-remote browser.
throw new Error("We seem to be navigating away from about:tabcrashed in " +
"a non-remote browser. This should really never happen.");
}
removeEventListener("pagehide", handleRevivedTab);
// Notify the parent.
sendAsyncMessage("SessionStore:crashedTabRevived");
}
}
// If we're browsing from the tab crashed UI to a blacklisted URI that keeps
// this browser non-remote, we'll handle that in a pagehide event.
addEventListener("pagehide", handleRevivedTab);
addEventListener("unload", () => {
// Upon frameLoader destruction, send a final update message to
// the parent and flush all data currently held in the child.
MessageQueue.send({isFinal: true});
// If we're browsing from the tab crashed UI to a URI that causes the tab
// to go remote again, we catch this in the unload event handler, because
// swapping out the non-remote browser for a remote one in
// tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide
// event to be fired.
handleRevivedTab();
// Remove all registered nsIObservers.
PageStyleListener.uninit();
SessionStorageListener.uninit();
SessionHistoryListener.uninit();
MessageQueue.uninit();
// Remove progress listeners.
gContentRestore.resetRestore();
// We don't need to take care of any gFrameTree observers as the gFrameTree
// will die with the content script. The same goes for the privacy transition
// observer that will die with the docShell when the tab is closed.
});

View file

@ -0,0 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js)
content/browser/content-sessionStore.js (content/content-sessionStore.js)

View file

@ -0,0 +1,52 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
JAR_MANIFESTS += ['jar.mn']
XPIDL_SOURCES += [
'nsISessionStartup.idl',
'nsISessionStore.idl',
]
XPIDL_MODULE = 'sessionstore'
EXTRA_COMPONENTS += [
'nsSessionStartup.js',
'nsSessionStore.js',
'nsSessionStore.manifest',
]
EXTRA_JS_MODULES.sessionstore = [
'ContentRestore.jsm',
'DocShellCapabilities.jsm',
'FrameTree.jsm',
'GlobalState.jsm',
'PageStyle.jsm',
'PrivacyFilter.jsm',
'PrivacyLevel.jsm',
'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
'RunState.jsm',
'SessionCookies.jsm',
'SessionFile.jsm',
'SessionHistory.jsm',
'SessionMigration.jsm',
'SessionSaver.jsm',
'SessionStorage.jsm',
'SessionStore.jsm',
'SessionWorker.js',
'SessionWorker.jsm',
'StartupPerformance.jsm',
'TabAttributes.jsm',
'TabState.jsm',
'TabStateCache.jsm',
'TabStateFlusher.jsm',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Session Restore')

View file

@ -0,0 +1,66 @@
/* 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"
/**
* nsISessionStore keeps track of the current browsing state - i.e.
* tab history, cookies, scroll state, form data, and window features
* - and allows to restore everything into one window.
*/
[scriptable, uuid(934697e4-3807-47f8-b6c9-6caa8d83ccd1)]
interface nsISessionStartup: nsISupports
{
/**
* Return a promise that is resolved once initialization
* is complete.
*/
readonly attribute jsval onceInitialized;
// Get session state
readonly attribute jsval state;
/**
* Determines whether there is a pending session restore. Should only be
* called after initialization has completed.
*/
boolean doRestore();
/**
* Determines whether automatic session restoration is enabled for this
* launch of the browser. This does not include crash restoration, and will
* return false if restoration will only be caused by a crash.
*/
boolean isAutomaticRestoreEnabled();
/**
* Returns whether we will restore a session that ends up replacing the
* homepage. The browser uses this to not start loading the homepage if
* we're going to stop its load anyway shortly after.
*
* This is meant to be an optimization for the average case that loading the
* session file finishes before we may want to start loading the default
* homepage. Should this be called before the session file has been read it
* will just return false.
*/
readonly attribute bool willOverrideHomepage;
/**
* What type of session we're restoring.
* NO_SESSION There is no data available from the previous session
* RECOVER_SESSION The last session crashed. It will either be restored or
* about:sessionrestore will be shown.
* RESUME_SESSION The previous session should be restored at startup
* DEFER_SESSION The previous session is fine, but it shouldn't be restored
* without explicit action (with the exception of pinned tabs)
*/
const unsigned long NO_SESSION = 0;
const unsigned long RECOVER_SESSION = 1;
const unsigned long RESUME_SESSION = 2;
const unsigned long DEFER_SESSION = 3;
readonly attribute unsigned long sessionType;
readonly attribute bool previousSessionCrashed;
};

View file

@ -0,0 +1,220 @@
/* 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 nsIDOMWindow;
interface nsIDOMNode;
/**
* nsISessionStore keeps track of the current browsing state - i.e.
* tab history, cookies, scroll state, form data, and window features
* - and allows to restore everything into one browser window.
*
* The nsISessionStore API operates mostly on browser windows and the tabbrowser
* tabs contained in them:
*
* * "Browser windows" are those DOM windows having loaded
* chrome://browser/content/browser.xul . From overlays you can just pass the
* global |window| object to the API, though (or |top| from a sidebar).
* From elsewhere you can get browser windows through the nsIWindowMediator
* by looking for "navigator:browser" windows.
*
* * "Tabbrowser tabs" are all the child nodes of a browser window's
* |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|.
*/
[scriptable, uuid(4580f5eb-693d-423d-b0ce-2cb20a962e4d)]
interface nsISessionStore : nsISupports
{
/**
* Is it possible to restore the previous session. Will always be false when
* in Private Browsing mode.
*/
attribute boolean canRestoreLastSession;
/**
* Restore the previous session if possible. This will not overwrite the
* current session. Instead the previous session will be merged into the
* current session. Current windows will be reused if they were windows that
* pinned tabs were previously restored into. New windows will be opened as
* needed.
*
* Note: This will throw if there is no previous state to restore. Check with
* canRestoreLastSession first to avoid thrown errors.
*/
void restoreLastSession();
/**
* Get the current browsing state.
* @returns a JSON string representing the session state.
*/
AString getBrowserState();
/**
* Set the browsing state.
* This will immediately restore the state of the whole application to the state
* passed in, *replacing* the current session.
*
* @param aState is a JSON string representing the session state.
*/
void setBrowserState(in AString aState);
/**
* @param aWindow is the browser window whose state is to be returned.
*
* @returns a JSON string representing a session state with only one window.
*/
AString getWindowState(in nsIDOMWindow aWindow);
/**
* @param aWindow is the browser window whose state is to be set.
* @param aState is a JSON string representing a session state.
* @param aOverwrite boolean overwrite existing tabs
*/
void setWindowState(in nsIDOMWindow aWindow, in AString aState, in boolean aOverwrite);
/**
* @param aTab is the tabbrowser tab whose state is to be returned.
*
* @returns a JSON string representing the state of the tab
* (note: doesn't contain cookies - if you need them, use getWindowState instead).
*/
AString getTabState(in nsIDOMNode aTab);
/**
* @param aTab is the tabbrowser tab whose state is to be set.
* @param aState is a JSON string representing a session state.
*/
void setTabState(in nsIDOMNode aTab, in AString aState);
/**
* Duplicates a given tab as thoroughly as possible.
*
* @param aWindow is the browser window into which the tab will be duplicated.
* @param aTab is the tabbrowser tab to duplicate (can be from a different window).
* @param aDelta is the offset to the history entry to load in the duplicated tab.
* @returns a reference to the newly created tab.
*/
nsIDOMNode duplicateTab(in nsIDOMWindow aWindow, in nsIDOMNode aTab,
[optional] in long aDelta);
/**
* Get the number of restore-able tabs for a browser window
*/
unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
/**
* Get closed tab data
*
* @param aWindow is the browser window for which to get closed tab data
* @returns a JSON string representing the list of closed tabs.
*/
AString getClosedTabData(in nsIDOMWindow aWindow);
/**
* @param aWindow is the browser window to reopen a closed tab in.
* @param aIndex is the index of the tab to be restored (FIFO ordered).
* @returns a reference to the reopened tab.
*/
nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
/**
* @param aWindow is the browser window associated with the closed tab.
* @param aIndex is the index of the closed tab to be removed (FIFO ordered).
*/
nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
/**
* Get the number of restore-able windows
*/
unsigned long getClosedWindowCount();
/**
* Get closed windows data
*
* @returns a JSON string representing the list of closed windows.
*/
AString getClosedWindowData();
/**
* @param aIndex is the index of the windows to be restored (FIFO ordered).
* @returns the nsIDOMWindow object of the reopened window
*/
nsIDOMWindow undoCloseWindow(in unsigned long aIndex);
/**
* @param aIndex is the index of the closed window to be removed (FIFO ordered).
*
* @throws NS_ERROR_INVALID_ARG
* when aIndex does not map to a closed window
*/
nsIDOMNode forgetClosedWindow(in unsigned long aIndex);
/**
* @param aWindow is the window to get the value for.
* @param aKey is the value's name.
*
* @returns A string value or an empty string if none is set.
*/
AString getWindowValue(in nsIDOMWindow aWindow, in AString aKey);
/**
* @param aWindow is the browser window to set the value for.
* @param aKey is the value's name.
* @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
*/
void setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in jsval aStringValue);
/**
* @param aWindow is the browser window to get the value for.
* @param aKey is the value's name.
*/
void deleteWindowValue(in nsIDOMWindow aWindow, in AString aKey);
/**
* @param aTab is the tabbrowser tab to get the value for.
* @param aKey is the value's name.
*
* @returns A string value or an empty string if none is set.
*/
AString getTabValue(in nsIDOMNode aTab, in AString aKey);
/**
* @param aTab is the tabbrowser tab to set the value for.
* @param aKey is the value's name.
* @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
*/
void setTabValue(in nsIDOMNode aTab, in AString aKey, in jsval aStringValue);
/**
* @param aTab is the tabbrowser tab to get the value for.
* @param aKey is the value's name.
*/
void deleteTabValue(in nsIDOMNode aTab, in AString aKey);
/**
* @param aKey is the value's name.
*
* @returns A string value or an empty string if none is set.
*/
AString getGlobalValue(in AString aKey);
/**
* @param aKey is the value's name.
* @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
*/
void setGlobalValue(in AString aKey, in jsval aStringValue);
/**
* @param aTab is the browser tab to get the value for.
* @param aKey is the value's name.
*/
void deleteGlobalValue(in AString aKey);
/**
* @param aName is the name of the attribute to save/restore for all tabbrowser tabs.
*/
void persistTabAttribute(in AString aName);
};

View file

@ -0,0 +1,353 @@
/* 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";
/**
* Session Storage and Restoration
*
* Overview
* This service reads user's session file at startup, and makes a determination
* as to whether the session should be restored. It will restore the session
* under the circumstances described below. If the auto-start Private Browsing
* mode is active, however, the session is never restored.
*
* Crash Detection
* The CrashMonitor is used to check if the final session state was successfully
* written at shutdown of the last session. If we did not reach
* 'sessionstore-final-state-write-complete', then it's assumed that the browser
* has previously crashed and we should restore the session.
*
* Forced Restarts
* In the event that a restart is required due to application update or extension
* installation, set the browser.sessionstore.resume_session_once pref to true,
* and the session will be restored the next time the browser starts.
*
* Always Resume
* This service will always resume the session if the integer pref
* browser.startup.page is set to 3.
*/
/* :::::::: Constants and Helpers ::::::::::::::: */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
"resource:///modules/sessionstore/SessionFile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance",
"resource:///modules/sessionstore/StartupPerformance.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
"resource://gre/modules/CrashMonitor.jsm");
const STATE_RUNNING_STR = "running";
// 'browser.startup.page' preference value to resume the previous session.
const BROWSER_STARTUP_RESUME_SESSION = 3;
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);
}
var gOnceInitializedDeferred = (function () {
let deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
})();
/* :::::::: The Service ::::::::::::::: */
function SessionStartup() {
}
SessionStartup.prototype = {
// the state to restore at startup
_initialState: null,
_sessionType: Ci.nsISessionStartup.NO_SESSION,
_initialized: false,
// Stores whether the previous session crashed.
_previousSessionCrashed: null,
/* ........ Global Event Handlers .............. */
/**
* Initialize the component
*/
init: function sss_init() {
Services.obs.notifyObservers(null, "sessionstore-init-started", null);
StartupPerformance.init();
// do not need to initialize anything in auto-started private browsing sessions
if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
this._initialized = true;
gOnceInitializedDeferred.resolve();
return;
}
SessionFile.read().then(
this._onSessionFileRead.bind(this),
console.error
);
},
// Wrap a string as a nsISupports
_createSupportsString: function ssfi_createSupportsString(aData) {
let string = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
string.data = aData;
return string;
},
/**
* Complete initialization once the Session File has been read
*
* @param source The Session State string read from disk.
* @param parsed The object obtained by parsing |source| as JSON.
*/
_onSessionFileRead: function ({source, parsed, noFilesFound}) {
this._initialized = true;
// Let observers modify the state before it is used
let supportsStateString = this._createSupportsString(source);
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
let stateString = supportsStateString.data;
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;
}
let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
let shouldResumeSession = shouldResumeSessionOnce ||
Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
// If this is a normal restore then throw away any previous session
if (!shouldResumeSessionOnce && this._initialState) {
delete this._initialState.lastSessionState;
}
let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
CrashMonitor.previousCheckpoints.then(checkpoints => {
if (checkpoints) {
// If the previous session finished writing the final state, we'll
// assume there was no crash.
this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
} else {
// If the Crash Monitor could not load a checkpoints file it will
// provide null. This could occur on the first run after updating to
// a version including the Crash Monitor, or if the checkpoints file
// was removed, or on first startup with this profile, or after Firefox Reset.
if (noFilesFound) {
// There was no checkpoints file and no sessionstore.js or its backups
// so we will assume that this was a fresh profile.
this._previousSessionCrashed = false;
} else {
// If this is the first run after an update, sessionstore.js should
// still contain the session.state flag to indicate if the session
// crashed. If it is not present, we will assume this was not the first
// run after update and the checkpoints file was somehow corrupted or
// removed by a crash.
//
// If the session.state flag is present, we will fallback to using it
// for crash detection - If the last write of sessionstore.js had it
// set to "running", we crashed.
let stateFlagPresent = (this._initialState.session &&
this._initialState.session.state);
this._previousSessionCrashed = !stateFlagPresent ||
(this._initialState.session.state == STATE_RUNNING_STR);
}
}
// Report shutdown success via telemetry. Shortcoming here are
// being-killed-by-OS-shutdown-logic, shutdown freezing after
// session restore was written, etc.
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed);
// set the startup type
if (this._previousSessionCrashed && resumeFromCrash)
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
else if (!this._previousSessionCrashed && shouldResumeSession)
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
else if (this._initialState)
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
else
this._initialState = null; // reset the state
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
Services.obs.addObserver(this, "browser:purge-session-history", true);
// We're ready. Notify everyone else.
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
gOnceInitializedDeferred.resolve();
});
},
/**
* Handle notifications
*/
observe: function sss_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "app-startup":
Services.obs.addObserver(this, "final-ui-startup", true);
Services.obs.addObserver(this, "quit-application", true);
break;
case "final-ui-startup":
Services.obs.removeObserver(this, "final-ui-startup");
Services.obs.removeObserver(this, "quit-application");
this.init();
break;
case "quit-application":
// no reason for initializing at this point (cf. bug 409115)
Services.obs.removeObserver(this, "final-ui-startup");
Services.obs.removeObserver(this, "quit-application");
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
Services.obs.removeObserver(this, "browser:purge-session-history");
break;
case "sessionstore-windows-restored":
Services.obs.removeObserver(this, "sessionstore-windows-restored");
// free _initialState after nsSessionStore is done with it
this._initialState = null;
break;
case "browser:purge-session-history":
Services.obs.removeObserver(this, "browser:purge-session-history");
// reset all state on sanitization
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
break;
}
},
/* ........ Public API ................*/
get onceInitialized() {
return gOnceInitializedDeferred.promise;
},
/**
* Get the session state as a jsval
*/
get state() {
return this._initialState;
},
/**
* Determines whether there is a pending session restore. Should only be
* called after initialization has completed.
* @returns bool
*/
doRestore: function sss_doRestore() {
return this._willRestore();
},
/**
* Determines whether automatic session restoration is enabled for this
* launch of the browser. This does not include crash restoration. In
* particular, if session restore is configured to restore only in case of
* crash, this method returns false.
* @returns bool
*/
isAutomaticRestoreEnabled: function () {
return Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
},
/**
* Determines whether there is a pending session restore.
* @returns bool
*/
_willRestore: function () {
return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
},
/**
* Returns whether we will restore a session that ends up replacing the
* homepage. The browser uses this to not start loading the homepage if
* we're going to stop its load anyway shortly after.
*
* This is meant to be an optimization for the average case that loading the
* session file finishes before we may want to start loading the default
* homepage. Should this be called before the session file has been read it
* will just return false.
*
* @returns bool
*/
get willOverrideHomepage() {
if (this._initialState && this._willRestore()) {
let windows = this._initialState.windows || null;
// If there are valid windows with not only pinned tabs, signal that we
// will override the default homepage by restoring a session.
return windows && windows.some(w => w.tabs.some(t => !t.pinned));
}
return false;
},
/**
* Get the type of pending session store, if any.
*/
get sessionType() {
return this._sessionType;
},
/**
* Get whether the previous session crashed.
*/
get previousSessionCrashed() {
return this._previousSessionCrashed;
},
/* ........ QueryInterface .............. */
QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference,
Ci.nsISessionStartup]),
classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}")
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]);

View file

@ -0,0 +1,39 @@
/* 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";
/**
* Session Storage and Restoration
*
* Overview
* This service keeps track of a user's session, storing the various bits
* required to return the browser to its current state. The relevant data is
* stored in memory, and is periodically saved to disk in a file in the
* profile directory. The service is started at first window load, in
* delayedStartup, and will restore the session from the data received from
* the nsSessionStartup service.
*/
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
function SessionStoreService() {}
// The SessionStore module's object is frozen. We need to modify our prototype
// and add some properties so let's just copy the SessionStore object.
Object.keys(SessionStore).forEach(function (aName) {
let desc = Object.getOwnPropertyDescriptor(SessionStore, aName);
Object.defineProperty(SessionStoreService.prototype, aName, desc);
});
SessionStoreService.prototype.classID =
Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}");
SessionStoreService.prototype.QueryInterface =
XPCOMUtils.generateQI([Ci.nsISessionStore]);
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]);

View file

@ -0,0 +1,15 @@
# This component must restrict its registration for the app-startup category
# to the specific list of apps that use it so it doesn't get loaded in xpcshell.
# Thus we restrict it to these apps:
#
# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61}
# browser: {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110}
# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66}
# graphene: {d1bfe7d9-c01e-4237-998b-7b5f960a4314}
component {5280606b-2510-4fe0-97ef-9b5a22eafe6b} nsSessionStore.js
contract @mozilla.org/browser/sessionstore;1 {5280606b-2510-4fe0-97ef-9b5a22eafe6b}
component {ec7a6c20-e081-11da-8ad9-0800200c9a66} nsSessionStartup.js
contract @mozilla.org/browser/sessionstartup;1 {ec7a6c20-e081-11da-8ad9-0800200c9a66}
category app-startup nsSessionStartup service,@mozilla.org/browser/sessionstartup;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314}

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"../../../../testing/mochitest/browser.eslintrc.js"
]
};

View file

@ -0,0 +1,242 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# browser_506482.js is disabled because of frequent failures (bug 538672)
# browser_526613.js is disabled because of frequent failures (bug 534489)
# browser_589246.js is disabled for leaking browser windows (bug 752467)
# browser_580512.js is disabled for leaking browser windows (bug 752467)
[DEFAULT]
support-files =
head.js
content.js
content-forms.js
browser_cookies.sjs
browser_formdata_sample.html
browser_formdata_xpath_sample.html
browser_frametree_sample.html
browser_frametree_sample_frameset.html
browser_frame_history_index.html
browser_frame_history_index2.html
browser_frame_history_index_blank.html
browser_frame_history_a.html
browser_frame_history_b.html
browser_frame_history_c.html
browser_frame_history_c1.html
browser_frame_history_c2.html
browser_form_restore_events_sample.html
browser_formdata_format_sample.html
browser_pageStyle_sample.html
browser_pageStyle_sample_nested.html
browser_sessionHistory_slow.sjs
browser_scrollPositions_sample.html
browser_scrollPositions_sample_frameset.html
browser_scrollPositions_readerModeArticle.html
browser_sessionStorage.html
browser_248970_b_sample.html
browser_339445_sample.html
browser_423132_sample.html
browser_447951_sample.html
browser_454908_sample.html
browser_456342_sample.xhtml
browser_463205_sample.html
browser_463206_sample.html
browser_466937_sample.html
browser_485482_sample.html
browser_637020_slow.sjs
browser_662743_sample.html
browser_739531_sample.html
browser_911547_sample.html
browser_911547_sample.html^headers^
restore_redirect_http.html
restore_redirect_http.html^headers^
restore_redirect_js.html
restore_redirect_target.html
browser_1234021_page.html
#NB: the following are disabled
# browser_464620_a.html
# browser_464620_b.html
# browser_464620_xd.html
#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
[browser_aboutPrivateBrowsing.js]
[browser_aboutSessionRestore.js]
[browser_async_duplicate_tab.js]
[browser_async_flushes.js]
run-if = e10s && crashreporter
skip-if = debug # bug 1167933
[browser_async_remove_tab.js]
run-if = e10s
skip-if = debug # bug 1211084
[browser_attributes.js]
[browser_backup_recovery.js]
[browser_broadcast.js]
[browser_capabilities.js]
[browser_cleaner.js]
[browser_cookies.js]
[browser_crashedTabs.js]
skip-if = !e10s || !crashreporter
[browser_unrestored_crashedTabs.js]
skip-if = !e10s || !crashreporter
[browser_revive_crashed_bg_tabs.js]
skip-if = !e10s || !crashreporter
[browser_dying_cache.js]
[browser_dynamic_frames.js]
[browser_form_restore_events.js]
[browser_formdata.js]
[browser_formdata_cc.js]
[browser_formdata_format.js]
[browser_formdata_xpath.js]
[browser_frametree.js]
[browser_frame_history.js]
[browser_global_store.js]
[browser_history_persist.js]
[browser_label_and_icon.js]
[browser_merge_closed_tabs.js]
[browser_page_title.js]
[browser_pageStyle.js]
[browser_pending_tabs.js]
[browser_privatetabs.js]
[browser_purge_shistory.js]
skip-if = e10s # Bug 1271024
[browser_replace_load.js]
[browser_restore_redirect.js]
[browser_restore_cookies_noOriginAttributes.js]
[browser_scrollPositions.js]
[browser_scrollPositionsReaderMode.js]
[browser_sessionHistory.js]
[browser_sessionStorage.js]
[browser_sessionStorage_size.js]
[browser_swapDocShells.js]
[browser_switch_remoteness.js]
run-if = e10s
[browser_upgrade_backup.js]
[browser_windowRestore_perwindowpb.js]
[browser_248970_b_perwindowpb.js]
# Disabled because of leaks.
# Re-enabling and rewriting this test is tracked in bug 936919.
skip-if = true
[browser_339445.js]
[browser_345898.js]
[browser_350525.js]
[browser_354894_perwindowpb.js]
[browser_367052.js]
[browser_393716.js]
[browser_394759_basic.js]
# Disabled for intermittent failures, bug 944372.
skip-if = true
[browser_394759_behavior.js]
[browser_394759_perwindowpb.js]
[browser_394759_purge.js]
[browser_423132.js]
[browser_447951.js]
[browser_454908.js]
[browser_456342.js]
[browser_461634.js]
[browser_463205.js]
[browser_463206.js]
[browser_464199.js]
[browser_465215.js]
[browser_465223.js]
[browser_466937.js]
[browser_467409-backslashplosion.js]
[browser_477657.js]
[browser_480893.js]
[browser_485482.js]
[browser_485563.js]
[browser_490040.js]
[browser_491168.js]
[browser_491577.js]
[browser_495495.js]
[browser_500328.js]
[browser_514751.js]
[browser_522375.js]
[browser_522545.js]
[browser_524745.js]
[browser_528776.js]
[browser_579868.js]
[browser_579879.js]
skip-if = (os == 'linux' && e10s && (debug||asan)) # Bug 1234404
[browser_581937.js]
[browser_586147.js]
[browser_586068-apptabs.js]
[browser_586068-apptabs_ondemand.js]
[browser_586068-browser_state_interrupted.js]
[browser_586068-cascade.js]
[browser_586068-multi_window.js]
[browser_586068-reload.js]
[browser_586068-select.js]
[browser_586068-window_state.js]
[browser_586068-window_state_override.js]
[browser_588426.js]
[browser_590268.js]
[browser_590563.js]
[browser_595601-restore_hidden.js]
[browser_597071.js]
skip-if = true # Needs to be rewritten as Marionette test, bug 995916
[browser_599909.js]
[browser_600545.js]
[browser_601955.js]
[browser_607016.js]
[browser_615394-SSWindowState_events.js]
[browser_618151.js]
[browser_623779.js]
[browser_624727.js]
[browser_628270.js]
[browser_635418.js]
[browser_636279.js]
[browser_637020.js]
[browser_644409-scratchpads.js]
[browser_645428.js]
[browser_659591.js]
[browser_662743.js]
[browser_662812.js]
[browser_665702-state_session.js]
[browser_682507.js]
[browser_687710.js]
[browser_687710_2.js]
[browser_694378.js]
[browser_701377.js]
[browser_705597.js]
[browser_707862.js]
[browser_739531.js]
[browser_739805.js]
[browser_819510_perwindowpb.js]
skip-if = (os == 'win' && bits == 64) # Bug 1284312
# Disabled for frequent intermittent failures
[browser_464620_a.js]
skip-if = true
[browser_464620_b.js]
skip-if = true
# Disabled on OS X:
[browser_625016.js]
skip-if = os == "mac"
[browser_911547.js]
[browser_send_async_message_oom.js]
[browser_multiple_navigateAndRestore.js]
run-if = e10s
[browser_async_window_flushing.js]
[browser_forget_async_closings.js]
[browser_newtab_userTypedValue.js]
[browser_parentProcessRestoreHash.js]
run-if = e10s
[browser_sessionStoreContainer.js]
[browser_windowStateContainer.js]
[browser_1234021.js]
[browser_remoteness_flip_on_restore.js]
run-if = e10s
[browser_background_tab_crash.js]
run-if = e10s && crashreporter
# Disabled on debug for frequent intermittent failures:
[browser_undoCloseById.js]
skip-if = debug

View file

@ -0,0 +1,18 @@
"use strict";
const PREF = 'network.cookie.cookieBehavior';
const PAGE_URL = 'http://mochi.test:8888/browser/' +
'browser/components/sessionstore/test/browser_1234021_page.html';
const BEHAVIOR_REJECT = 2;
add_task(function* test() {
yield pushPrefs([PREF, BEHAVIOR_REJECT]);
yield BrowserTestUtils.withNewTab({
gBrowser: gBrowser,
url: PAGE_URL
}, function* handler(aBrowser) {
yield TabStateFlusher.flush(aBrowser);
ok(true, "Flush didn't time out");
});
});

View file

@ -0,0 +1,6 @@
<!doctype html>
<html>
<script>
sessionStorage
</script>
</html>

View file

@ -0,0 +1,166 @@
/* 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/. */
function test() {
/** Test (B) for Bug 248970 **/
waitForExplicitFinish();
let windowsToClose = [];
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
let filePath = file.path;
let fieldList = {
"//input[@name='input']": Date.now().toString(),
"//input[@name='spaced 1']": Math.random().toString(),
"//input[3]": "three",
"//input[@type='checkbox']": true,
"//input[@name='uncheck']": false,
"//input[@type='radio'][1]": false,
"//input[@type='radio'][2]": true,
"//input[@type='radio'][3]": false,
"//select": 2,
"//select[@multiple]": [1, 3],
"//textarea[1]": "",
"//textarea[2]": "Some text... " + Math.random(),
"//textarea[3]": "Some more text\n" + new Date(),
"//input[@type='file']": filePath
};
registerCleanupFunction(function* () {
for (let win of windowsToClose) {
yield BrowserTestUtils.closeWindow(win);
}
});
function test(aLambda) {
try {
return aLambda() || true;
} catch(ex) { }
return false;
}
function getElementByXPath(aTab, aQuery) {
let doc = aTab.linkedBrowser.contentDocument;
let xptype = Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE;
return doc.evaluate(aQuery, doc, null, xptype, null).singleNodeValue;
}
function setFormValue(aTab, aQuery, aValue) {
let node = getElementByXPath(aTab, aQuery);
if (typeof aValue == "string")
node.value = aValue;
else if (typeof aValue == "boolean")
node.checked = aValue;
else if (typeof aValue == "number")
node.selectedIndex = aValue;
else
Array.forEach(node.options, (aOpt, aIx) =>
(aOpt.selected = aValue.indexOf(aIx) > -1));
}
function compareFormValue(aTab, aQuery, aValue) {
let node = getElementByXPath(aTab, aQuery);
if (!node)
return false;
if (node instanceof Ci.nsIDOMHTMLInputElement)
return aValue == (node.type == "checkbox" || node.type == "radio" ?
node.checked : node.value);
if (node instanceof Ci.nsIDOMHTMLTextAreaElement)
return aValue == node.value;
if (!node.multiple)
return aValue == node.selectedIndex;
return Array.every(node.options, (aOpt, aIx) =>
(aValue.indexOf(aIx) > -1) == aOpt.selected);
}
//////////////////////////////////////////////////////////////////
// Test (B) : Session data restoration between windows //
//////////////////////////////////////////////////////////////////
let rootDir = getRootDirectory(gTestPath);
const testURL = rootDir + "browser_248970_b_sample.html";
const testURL2 = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_248970_b_sample.html";
whenNewWindowLoaded({ private: false }, function(aWin) {
windowsToClose.push(aWin);
// get closed tab count
let count = ss.getClosedTabCount(aWin);
let max_tabs_undo =
Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
ok(0 <= count && count <= max_tabs_undo,
"getClosedTabCount should return zero or at most max_tabs_undo");
// setup a state for tab (A) so we can check later that is restored
let key = "key";
let value = "Value " + Math.random();
let state = { entries: [{ url: testURL }], extData: { key: value } };
// public session, add new tab: (A)
let tab_A = aWin.gBrowser.addTab(testURL);
ss.setTabState(tab_A, JSON.stringify(state));
promiseBrowserLoaded(tab_A.linkedBrowser).then(() => {
// make sure that the next closed tab will increase getClosedTabCount
Services.prefs.setIntPref(
"browser.sessionstore.max_tabs_undo", max_tabs_undo + 1)
// populate tab_A with form data
for (let i in fieldList)
setFormValue(tab_A, i, fieldList[i]);
// public session, close tab: (A)
aWin.gBrowser.removeTab(tab_A);
// verify that closedTabCount increased
ok(ss.getClosedTabCount(aWin) > count,
"getClosedTabCount has increased after closing a tab");
// verify tab: (A), in undo list
let tab_A_restored = test(() => ss.undoCloseTab(aWin, 0));
ok(tab_A_restored, "a tab is in undo list");
promiseTabRestored(tab_A_restored).then(() => {
is(testURL, tab_A_restored.linkedBrowser.currentURI.spec,
"it's the same tab that we expect");
aWin.gBrowser.removeTab(tab_A_restored);
whenNewWindowLoaded({ private: true }, function(aWin) {
windowsToClose.push(aWin);
// setup a state for tab (B) so we can check that its duplicated
// properly
let key1 = "key1";
let value1 = "Value " + Math.random();
let state1 = {
entries: [{ url: testURL2 }], extData: { key1: value1 }
};
let tab_B = aWin.gBrowser.addTab(testURL2);
promiseTabState(tab_B, state1).then(() => {
// populate tab: (B) with different form data
for (let item in fieldList)
setFormValue(tab_B, item, fieldList[item]);
// duplicate tab: (B)
let tab_C = aWin.gBrowser.duplicateTab(tab_B);
promiseTabRestored(tab_C).then(() => {
// verify the correctness of the duplicated tab
is(ss.getTabValue(tab_C, key1), value1,
"tab successfully duplicated - correct state");
for (let item in fieldList)
ok(compareFormValue(tab_C, item, fieldList[item]),
"The value for \"" + item + "\" was correctly duplicated");
// private browsing session, close tab: (C) and (B)
aWin.gBrowser.removeTab(tab_C);
aWin.gBrowser.removeTab(tab_B);
finish();
});
});
});
});
});
});
}

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<meta charset="utf-8">
<title>Test for bug 248970</title>
<h3>Text Fields</h3>
<input type="text" name="input">
<input type="text" name="spaced 1">
<input>
<h3>Checkboxes and Radio buttons</h3>
<input type="checkbox" name="check"> Check 1
<input type="checkbox" name="uncheck" checked> Check 2
<p>
<input type="radio" name="group" value="1"> Radio 1
<input type="radio" name="group" value="some"> Radio 2
<input type="radio" name="group" checked> Radio 3
<h3>Selects</h3>
<select name="any">
<option value="1"> Select 1
<option value="some"> Select 2
<option>Select 3
</select>
<select multiple="multiple">
<option value=1> Multi-select 1
<option value=2> Multi-select 2
<option value=3> Multi-select 3
<option value=4> Multi-select 4
</select>
<h3>Text Areas</h3>
<textarea name="testarea"></textarea>
<textarea name="sized one" rows="5" cols="25"></textarea>
<textarea></textarea>
<h3>File Selector</h3>
<input type="file">

View file

@ -0,0 +1,32 @@
/* 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/. */
add_task(function* test() {
/** Test for Bug 339445 **/
let testURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_339445_sample.html";
let tab = gBrowser.addTab(testURL);
yield promiseBrowserLoaded(tab.linkedBrowser);
yield ContentTask.spawn(tab.linkedBrowser, null, function() {
let doc = content.document;
is(doc.getElementById("storageTestItem").textContent, "PENDING",
"sessionStorage value has been set");
});
let tab2 = gBrowser.duplicateTab(tab);
yield promiseTabRestored(tab2);
yield ContentTask.spawn(tab2.linkedBrowser, null, function() {
let doc2 = content.document;
is(doc2.getElementById("storageTestItem").textContent, "SUCCESS",
"sessionStorage value has been duplicated");
});
// clean up
yield Promise.all([ BrowserTestUtils.removeTab(tab2),
BrowserTestUtils.removeTab(tab) ]);
});

View file

@ -0,0 +1,18 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<meta charset="utf-8">
<title>Test for bug 339445</title>
storageTestItem = <span id="storageTestItem">FAIL</span>
<!--
storageTestItem's textContent will be one of the following:
* FAIL : sessionStorage wasn't available
* PENDING : the test value has been initialized on first load
* SUCCESS : the test value was correctly retrieved
-->
<script type="application/javascript">
document.getElementById("storageTestItem").textContent =
sessionStorage["storageTestItem"] || "PENDING";
sessionStorage["storageTestItem"] = "SUCCESS";
</script>

View file

@ -0,0 +1,44 @@
/* 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/. */
function test() {
/** Test for Bug 345898 **/
function test(aLambda) {
try {
aLambda();
return false;
}
catch (ex) {
return ex.name == "NS_ERROR_ILLEGAL_VALUE" ||
ex.name == "NS_ERROR_FAILURE";
}
}
// all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
ok(test(() => ss.getWindowState({})),
"Invalid window for getWindowState throws");
ok(test(() => ss.setWindowState({}, "", false)),
"Invalid window for setWindowState throws");
ok(test(() => ss.getTabState({})),
"Invalid tab for getTabState throws");
ok(test(() => ss.setTabState({}, "{}")),
"Invalid tab state for setTabState throws");
ok(test(() => ss.setTabState({}, JSON.stringify({ entries: [] }))),
"Invalid tab for setTabState throws");
ok(test(() => ss.duplicateTab({}, {})),
"Invalid tab for duplicateTab throws");
ok(test(() => ss.duplicateTab({}, gBrowser.selectedTab)),
"Invalid window for duplicateTab throws");
ok(test(() => ss.getClosedTabData({})),
"Invalid window for getClosedTabData throws");
ok(test(() => ss.undoCloseTab({}, 0)),
"Invalid window for undoCloseTab throws");
ok(test(() => ss.undoCloseTab(window, -1)),
"Invalid index for undoCloseTab throws");
ok(test(() => ss.getWindowValue({}, "")),
"Invalid window for getWindowValue throws");
ok(test(() => ss.setWindowValue({}, "", "")),
"Invalid window for setWindowValue throws");
}

View file

@ -0,0 +1,102 @@
"use strict";
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [["dom.ipc.processCount", 1]]
});
})
add_task(function* () {
/** Test for Bug 350525 **/
function test(aLambda) {
try {
return aLambda() || true;
}
catch (ex) { }
return false;
}
////////////////////////////
// setWindowValue, et al. //
////////////////////////////
let key = "Unique name: " + Date.now();
let value = "Unique value: " + Math.random();
// test adding
ok(test(() => ss.setWindowValue(window, key, value)), "set a window value");
// test retrieving
is(ss.getWindowValue(window, key), value, "stored window value matches original");
// test deleting
ok(test(() => ss.deleteWindowValue(window, key)), "delete the window value");
// value should not exist post-delete
is(ss.getWindowValue(window, key), "", "window value was deleted");
// test deleting a non-existent value
ok(test(() => ss.deleteWindowValue(window, key)), "delete non-existent window value");
/////////////////////////
// setTabValue, et al. //
/////////////////////////
key = "Unique name: " + Math.random();
value = "Unique value: " + Date.now();
let tab = gBrowser.addTab();
tab.linkedBrowser.stop();
// test adding
ok(test(() => ss.setTabValue(tab, key, value)), "store a tab value");
// test retrieving
is(ss.getTabValue(tab, key), value, "stored tab value match original");
// test deleting
ok(test(() => ss.deleteTabValue(tab, key)), "delete the tab value");
// value should not exist post-delete
is(ss.getTabValue(tab, key), "", "tab value was deleted");
// test deleting a non-existent value
ok(test(() => ss.deleteTabValue(tab, key)), "delete non-existent tab value");
// clean up
yield promiseRemoveTab(tab);
/////////////////////////////////////
// getClosedTabCount, undoCloseTab //
/////////////////////////////////////
// get closed tab count
let count = ss.getClosedTabCount(window);
let max_tabs_undo = gPrefService.getIntPref("browser.sessionstore.max_tabs_undo");
ok(0 <= count && count <= max_tabs_undo,
"getClosedTabCount returns zero or at most max_tabs_undo");
// create a new tab
let testURL = "about:";
tab = gBrowser.addTab(testURL);
yield promiseBrowserLoaded(tab.linkedBrowser);
// make sure that the next closed tab will increase getClosedTabCount
gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
registerCleanupFunction(() => gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"));
// remove tab
yield promiseRemoveTab(tab);
// getClosedTabCount
let newcount = ss.getClosedTabCount(window);
ok(newcount > count, "after closing a tab, getClosedTabCount has been incremented");
// undoCloseTab
tab = test(() => ss.undoCloseTab(window, 0));
ok(tab, "undoCloseTab doesn't throw")
yield promiseTabRestored(tab);
is(tab.linkedBrowser.currentURI.spec, testURL, "correct tab was reopened");
// clean up
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,474 @@
/* 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/. */
/**
* Checks that restoring the last browser window in session is actually
* working.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=354894
* @note It is implicitly tested that restoring the last window works when
* non-browser windows are around. The "Run Tests" window as well as the main
* browser window (wherein the test code gets executed) won't be considered
* browser windows. To achiveve this said main browser window has it's windowtype
* attribute modified so that it's not considered a browser window any longer.
* This is crucial, because otherwise there would be two browser windows around,
* said main test window and the one opened by the tests, and hence the new
* logic wouldn't be executed at all.
* @note Mac only tests the new notifications, as restoring the last window is
* not enabled on that platform (platform shim; the application is kept running
* although there are no windows left)
* @note There is a difference when closing a browser window with
* BrowserTryToCloseWindow() as opposed to close(). The former will make
* nsSessionStore restore a window next time it gets a chance and will post
* notifications. The latter won't.
*/
// Some urls that might be opened in tabs and/or popups
// Do not use about:blank:
// That one is reserved for special purposes in the tests
const TEST_URLS = ["about:mozilla", "about:buildconfig"];
// Number of -request notifications to except
// remember to adjust when adding new tests
const NOTIFICATIONS_EXPECTED = 6;
// Window features of popup windows
const POPUP_FEATURES = "toolbar=no,resizable=no,status=no";
// Window features of browser windows
const CHROME_FEATURES = "chrome,all,dialog=no";
const IS_MAC = navigator.platform.match(/Mac/);
/**
* Returns an Object with two properties:
* open (int):
* A count of how many non-closed navigator:browser windows there are.
* winstates (int):
* A count of how many windows there are in the SessionStore state.
*/
function getBrowserWindowsCount() {
let open = 0;
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
if (!e.getNext().closed)
++open;
}
let winstates = JSON.parse(ss.getBrowserState()).windows.length;
return { open, winstates };
}
add_task(function* setup() {
// Make sure we've only got one browser window to start with
let { open, winstates } = getBrowserWindowsCount();
is(open, 1, "Should only be one open window");
is(winstates, 1, "Should only be one window state in SessionStore");
// This test takes some time to run, and it could timeout randomly.
// So we require a longer timeout. See bug 528219.
requestLongerTimeout(3);
// Make the main test window not count as a browser window any longer
let oldWinType = document.documentElement.getAttribute("windowtype");
document.documentElement.setAttribute("windowtype", "navigator:testrunner");
registerCleanupFunction(() => {
document.documentElement.setAttribute("windowtype", "navigator:browser");
});
});
/**
* Sets up one of our tests by setting the right preferences, and
* then opening up a browser window preloaded with some tabs.
*
* @param options (Object)
* An object that can contain the following properties:
*
* private:
* Whether or not the opened window should be private.
*
* denyFirst:
* Whether or not the first window that attempts to close
* via closeWindowForRestoration should be denied.
*
* @param testFunction (Function*)
* A generator function that yields Promises to be run
* once the test has been set up.
*
* @returns Promise
* Resolves once the test has been cleaned up.
*/
let setupTest = Task.async(function*(options, testFunction) {
yield pushPrefs(["browser.startup.page", 3],
["browser.tabs.warnOnClose", false]);
// Observe these, and also use to count the number of hits
let observing = {
"browser-lastwindow-close-requested": 0,
"browser-lastwindow-close-granted": 0
};
/**
* Helper: Will observe and handle the notifications for us
*/
let hitCount = 0;
function observer(aCancel, aTopic, aData) {
// count so that we later may compare
observing[aTopic]++;
// handle some tests
if (options.denyFirst && ++hitCount == 1) {
aCancel.QueryInterface(Ci.nsISupportsPRBool).data = true;
}
}
for (let o in observing) {
Services.obs.addObserver(observer, o, false);
}
let private = options.private || false;
let newWin = yield promiseNewWindowLoaded({ private });
injectTestTabs(newWin);
yield testFunction(newWin, observing);
let count = getBrowserWindowsCount();
is(count.open, 0, "Got right number of open windows");
is(count.winstates, 1, "Got right number of stored window states");
for (let o in observing) {
Services.obs.removeObserver(observer, o);
}
yield popPrefs();
});
/**
* Loads a TEST_URLS into a browser window.
*
* @param win (Window)
* The browser window to load the tabs in
*/
function injectTestTabs(win) {
TEST_URLS.forEach(function (url) {
win.gBrowser.addTab(url);
});
}
/**
* Attempts to close a window via BrowserTryToCloseWindow so that
* we get the browser-lastwindow-close-requested and
* browser-lastwindow-close-granted observer notifications.
*
* @param win (Window)
* The window to try to close
* @returns Promise
* Resolves to true if the window closed, or false if the window
* was denied the ability to close.
*/
function closeWindowForRestoration(win) {
return new Promise((resolve) => {
let closePromise = BrowserTestUtils.windowClosed(win);
win.BrowserTryToCloseWindow();
if (!win.closed) {
resolve(false);
return;
}
closePromise.then(() => {
resolve(true);
});
});
}
/**
* Normal in-session restore
*
* @note: Non-Mac only
*
* Should do the following:
* 1. Open a new browser window
* 2. Add some tabs
* 3. Close that window
* 4. Opening another window
* 5. Checks that state is restored
*/
add_task(function* test_open_close_normal() {
if (IS_MAC) {
return;
}
yield setupTest({ denyFirst: true }, function*(newWin, obs) {
let closed = yield closeWindowForRestoration(newWin);
ok(!closed, "First close request should have been denied");
closed = yield closeWindowForRestoration(newWin);
ok(closed, "Second close request should be accepted");
newWin = yield promiseNewWindowLoaded();
is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
"Restored window in-session with otherpopup windows around");
// Note that this will not result in the the browser-lastwindow-close
// notifications firing for this other newWin.
yield BrowserTestUtils.closeWindow(newWin);
// setupTest gave us a window which was denied for closing once, and then
// closed.
is(obs["browser-lastwindow-close-requested"], 2,
"Got expected browser-lastwindow-close-requested notifications");
is(obs["browser-lastwindow-close-granted"], 1,
"Got expected browser-lastwindow-close-granted notifications");
});
});
/**
* PrivateBrowsing in-session restore
*
* @note: Non-Mac only
*
* Should do the following:
* 1. Open a new browser window A
* 2. Add some tabs
* 3. Close the window A as the last window
* 4. Open a private browsing window B
* 5. Make sure that B didn't restore the tabs from A
* 6. Close private browsing window B
* 7. Open a new window C
* 8. Make sure that new window C has restored tabs from A
*/
add_task(function* test_open_close_private_browsing() {
if (IS_MAC) {
return;
}
yield setupTest({}, function*(newWin, obs) {
let closed = yield closeWindowForRestoration(newWin);
ok(closed, "Should be able to close the window");
newWin = yield promiseNewWindowLoaded({private: true});
is(newWin.gBrowser.browsers.length, 1,
"Did not restore in private browing mode");
closed = yield closeWindowForRestoration(newWin);
ok(closed, "Should be able to close the window");
newWin = yield promiseNewWindowLoaded();
is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
"Restored tabs in a new non-private window");
// Note that this will not result in the the browser-lastwindow-close
// notifications firing for this other newWin.
yield BrowserTestUtils.closeWindow(newWin);
// We closed two windows with closeWindowForRestoration, and both
// should have been successful.
is(obs["browser-lastwindow-close-requested"], 2,
"Got expected browser-lastwindow-close-requested notifications");
is(obs["browser-lastwindow-close-granted"], 2,
"Got expected browser-lastwindow-close-granted notifications");
});
});
/**
* Open some popup windows to check those aren't restored, but the browser
* window is.
*
* @note: Non-Mac only
*
* Should do the following:
* 1. Open a new browser window
* 2. Add some tabs
* 3. Open some popups
* 4. Add another tab to one popup (so that it gets stored) and close it again
* 5. Close the browser window
* 6. Open another browser window
* 7. Make sure that the tabs of the closed browser window, but not the popup,
* are restored
*/
add_task(function* test_open_close_window_and_popup() {
if (IS_MAC) {
return;
}
yield setupTest({}, function*(newWin, obs) {
let popupPromise = BrowserTestUtils.waitForNewWindow();
openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[0]);
let popup = yield popupPromise;
let popup2Promise = BrowserTestUtils.waitForNewWindow();
openDialog(location, "popup2", POPUP_FEATURES, TEST_URLS[1]);
let popup2 = yield popup2Promise;
popup2.gBrowser.addTab(TEST_URLS[0]);
let closed = yield closeWindowForRestoration(newWin);
ok(closed, "Should be able to close the window");
yield BrowserTestUtils.closeWindow(popup2);
newWin = yield promiseNewWindowLoaded();
is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
"Restored window and associated tabs in session");
yield BrowserTestUtils.closeWindow(popup);
yield BrowserTestUtils.closeWindow(newWin);
// We closed one window with closeWindowForRestoration, and it should
// have been successful.
is(obs["browser-lastwindow-close-requested"], 1,
"Got expected browser-lastwindow-close-requested notifications");
is(obs["browser-lastwindow-close-granted"], 1,
"Got expected browser-lastwindow-close-granted notifications");
});
});
/**
* Open some popup window to check it isn't restored. Instead nothing at all
* should be restored
*
* @note: Non-Mac only
*
* Should do the following:
* 1. Open a popup
* 2. Add another tab to the popup (so that it gets stored) and close it again
* 3. Open a window
* 4. Check that nothing at all is restored
* 5. Open two browser windows and close them again
* 6. undoCloseWindow() one
* 7. Open another browser window
* 8. Check that nothing at all is restored
*/
add_task(function* test_open_close_only_popup() {
if (IS_MAC) {
return;
}
yield setupTest({}, function*(newWin, obs) {
// We actually don't care about the initial window in this test.
yield BrowserTestUtils.closeWindow(newWin);
// This will cause nsSessionStore to restore a window the next time it
// gets a chance.
let popupPromise = BrowserTestUtils.waitForNewWindow();
openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
let popup = yield popupPromise;
is(popup.gBrowser.browsers.length, 1,
"Did not restore the popup window (1)");
let closed = yield closeWindowForRestoration(popup);
ok(closed, "Should be able to close the window");
popupPromise = BrowserTestUtils.waitForNewWindow();
openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
popup = yield popupPromise;
popup.gBrowser.addTab(TEST_URLS[0]);
is(popup.gBrowser.browsers.length, 2,
"Did not restore to the popup window (2)");
yield BrowserTestUtils.closeWindow(popup);
newWin = yield promiseNewWindowLoaded();
isnot(newWin.gBrowser.browsers.length, 2,
"Did not restore the popup window");
is(TEST_URLS.indexOf(newWin.gBrowser.browsers[0].currentURI.spec), -1,
"Did not restore the popup window (2)");
yield BrowserTestUtils.closeWindow(newWin);
// We closed one popup window with closeWindowForRestoration, and popup
// windows should never fire the browser-lastwindow notifications.
is(obs["browser-lastwindow-close-requested"], 0,
"Got expected browser-lastwindow-close-requested notifications");
is(obs["browser-lastwindow-close-granted"], 0,
"Got expected browser-lastwindow-close-granted notifications");
});
});
/**
* Open some windows and do undoCloseWindow. This should prevent any
* restoring later in the test
*
* @note: Non-Mac only
*
* Should do the following:
* 1. Open two browser windows and close them again
* 2. undoCloseWindow() one
* 3. Open another browser window
* 4. Make sure nothing at all is restored
*/
add_task(function* test_open_close_restore_from_popup() {
if (IS_MAC) {
return;
}
yield setupTest({}, function*(newWin, obs) {
let newWin2 = yield promiseNewWindowLoaded();
yield injectTestTabs(newWin2);
let closed = yield closeWindowForRestoration(newWin);
ok(closed, "Should be able to close the window");
closed = yield closeWindowForRestoration(newWin2);
ok(closed, "Should be able to close the window");
let counts = getBrowserWindowsCount();
is(counts.open, 0, "Got right number of open windows");
is(counts.winstates, 1, "Got right number of window states");
newWin = undoCloseWindow(0);
yield BrowserTestUtils.waitForEvent(newWin, "load");
// Make sure we wait until this window is restored.
yield BrowserTestUtils.waitForEvent(newWin.gBrowser.tabContainer,
"SSTabRestored");
newWin2 = yield promiseNewWindowLoaded();
is(newWin2.gBrowser.browsers.length, 1,
"Did not restore, as undoCloseWindow() was last called");
is(TEST_URLS.indexOf(newWin2.gBrowser.browsers[0].currentURI.spec), -1,
"Did not restore, as undoCloseWindow() was last called (2)");
counts = getBrowserWindowsCount();
is(counts.open, 2, "Got right number of open windows");
is(counts.winstates, 3, "Got right number of window states");
yield BrowserTestUtils.closeWindow(newWin);
yield BrowserTestUtils.closeWindow(newWin2);
counts = getBrowserWindowsCount();
is(counts.open, 0, "Got right number of open windows");
is(counts.winstates, 1, "Got right number of window states");
});
});
/**
* Test if closing can be denied on Mac.
* @note: Mac only
*/
add_task(function* test_mac_notifications() {
if (!IS_MAC) {
return;
}
yield setupTest({ denyFirst: true }, function*(newWin, obs) {
let closed = yield closeWindowForRestoration(newWin);
ok(!closed, "First close attempt should be denied");
closed = yield closeWindowForRestoration(newWin);
ok(closed, "Second close attempt should be granted");
// We tried closing once, and got denied. Then we tried again and
// succeeded. That means 2 close requests, and 1 close granted.
is(obs["browser-lastwindow-close-requested"], 2,
"Got expected browser-lastwindow-close-requested notifications");
is(obs["browser-lastwindow-close-granted"], 1,
"Got expected browser-lastwindow-close-granted notifications");
});
});

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/. */
"use strict";
add_task(function* () {
// make sure that the next closed tab will increase getClosedTabCount
let max_tabs_undo = gPrefService.getIntPref("browser.sessionstore.max_tabs_undo");
gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
registerCleanupFunction(() => gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"));
// Empty the list of closed tabs.
while (ss.getClosedTabCount(window)) {
ss.forgetClosedTab(window, 0);
}
// restore a blank tab
let tab = gBrowser.addTab("about:");
yield promiseBrowserLoaded(tab.linkedBrowser);
let count = yield promiseSHistoryCount(tab.linkedBrowser);
ok(count >= 1, "the new tab does have at least one history entry");
yield promiseTabState(tab, {entries: []});
// We may have a different sessionHistory object if the tab
// switched from non-remote to remote.
count = yield promiseSHistoryCount(tab.linkedBrowser);
is(count, 0, "the tab was restored without any history whatsoever");
yield promiseRemoveTab(tab);
is(ss.getClosedTabCount(window), 0,
"The closed blank tab wasn't added to Recently Closed Tabs");
});
function promiseSHistoryCount(browser) {
return ContentTask.spawn(browser, null, function* () {
return docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.count;
});
}

View file

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const URL = "about:config";
/**
* Bug 393716 - Basic tests for getTabState(), setTabState(), and duplicateTab().
*/
add_task(function test_set_tabstate() {
let key = "Unique key: " + Date.now();
let value = "Unique value: " + Math.random();
// create a new tab
let tab = gBrowser.addTab(URL);
ss.setTabValue(tab, key, value);
yield promiseBrowserLoaded(tab.linkedBrowser);
// get the tab's state
yield TabStateFlusher.flush(tab.linkedBrowser);
let state = ss.getTabState(tab);
ok(state, "get the tab's state");
// verify the tab state's integrity
state = JSON.parse(state);
ok(state instanceof Object && state.entries instanceof Array && state.entries.length > 0,
"state object seems valid");
ok(state.entries.length == 1 && state.entries[0].url == URL,
"Got the expected state object (test URL)");
ok(state.extData && state.extData[key] == value,
"Got the expected state object (test manually set tab value)");
// clean up
gBrowser.removeTab(tab);
});
add_task(function test_set_tabstate_and_duplicate() {
let key2 = "key2";
let value2 = "Value " + Math.random();
let value3 = "Another value: " + Date.now();
let state = { entries: [{ url: URL }], extData: { key2: value2 } };
// create a new tab
let tab = gBrowser.addTab();
// set the tab's state
ss.setTabState(tab, JSON.stringify(state));
yield promiseBrowserLoaded(tab.linkedBrowser);
// verify the correctness of the restored tab
ok(ss.getTabValue(tab, key2) == value2 && tab.linkedBrowser.currentURI.spec == URL,
"the tab's state was correctly restored");
// add text data
yield setInputValue(tab.linkedBrowser, {id: "textbox", value: value3});
// duplicate the tab
let tab2 = ss.duplicateTab(window, tab);
yield promiseTabRestored(tab2);
// verify the correctness of the duplicated tab
ok(ss.getTabValue(tab2, key2) == value2 &&
tab2.linkedBrowser.currentURI.spec == URL,
"correctly duplicated the tab's state");
let textbox = yield getInputValue(tab2.linkedBrowser, {id: "textbox"});
is(textbox, value3, "also duplicated text data");
// clean up
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,92 @@
/* 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 TEST_URL = "data:text/html;charset=utf-8,<input%20id=txt>" +
"<input%20type=checkbox%20id=chk>";
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
/**
* This test ensures that closing a window is a reversible action. We will
* close the the window, restore it and check that all data has been restored.
* This includes window-specific data as well as form data for tabs.
*/
function test() {
waitForExplicitFinish();
let uniqueKey = "bug 394759";
let uniqueValue = "unik" + Date.now();
let uniqueText = "pi != " + Math.random();
// Clear the list of closed windows.
forgetClosedWindows();
provideWindow(function onTestURLLoaded(newWin) {
newWin.gBrowser.addTab().linkedBrowser.stop();
// Mark the window with some unique data to be restored later on.
ss.setWindowValue(newWin, uniqueKey, uniqueValue);
let [txt, chk] = newWin.content.document.querySelectorAll("#txt, #chk");
txt.value = uniqueText;
let browser = newWin.gBrowser.selectedBrowser;
setInputChecked(browser, {id: "chk", checked: true}).then(() => {
BrowserTestUtils.closeWindow(newWin).then(() => {
is(ss.getClosedWindowCount(), 1,
"The closed window was added to Recently Closed Windows");
let data = SessionStore.getClosedWindowData(false);
// Verify that non JSON serialized data is the same as JSON serialized data.
is(JSON.stringify(data), ss.getClosedWindowData(),
"Non-serialized data is the same as serialized data")
ok(data[0].title == TEST_URL && JSON.stringify(data[0]).indexOf(uniqueText) > -1,
"The closed window data was stored correctly");
// Reopen the closed window and ensure its integrity.
let newWin2 = ss.undoCloseWindow(0);
ok(newWin2 instanceof ChromeWindow,
"undoCloseWindow actually returned a window");
is(ss.getClosedWindowCount(), 0,
"The reopened window was removed from Recently Closed Windows");
// SSTabRestored will fire more than once, so we need to make sure we count them.
let restoredTabs = 0;
let expectedTabs = data[0].tabs.length;
newWin2.addEventListener("SSTabRestored", function sstabrestoredListener(aEvent) {
++restoredTabs;
info("Restored tab " + restoredTabs + "/" + expectedTabs);
if (restoredTabs < expectedTabs) {
return;
}
is(restoredTabs, expectedTabs, "Correct number of tabs restored");
newWin2.removeEventListener("SSTabRestored", sstabrestoredListener, true);
is(newWin2.gBrowser.tabs.length, 2,
"The window correctly restored 2 tabs");
is(newWin2.gBrowser.currentURI.spec, TEST_URL,
"The window correctly restored the URL");
let [txt, chk] = newWin2.content.document.querySelectorAll("#txt, #chk");
ok(txt.value == uniqueText && chk.checked,
"The window correctly restored the form");
is(ss.getWindowValue(newWin2, uniqueKey), uniqueValue,
"The window correctly restored the data associated with it");
// Clean up.
BrowserTestUtils.closeWindow(newWin2).then(finish);
}, true);
});
});
}, TEST_URL);
}
function setInputChecked(browser, data) {
return sendMessage(browser, "ss-test:setInputChecked", data);
}

View file

@ -0,0 +1,76 @@
/**
* Test helper function that opens a series of windows, closes them
* and then checks the closed window data from SessionStore against
* expected results.
*
* @param windowsToOpen (Array)
* An array of Objects, where each object must define a single
* property "isPopup" for whether or not the opened window should
* be a popup.
* @param expectedResults (Array)
* An Object with two properies: mac and other, where each points
* at yet another Object, with the following properties:
*
* popup (int):
* The number of popup windows we expect to be in the closed window
* data.
* normal (int):
* The number of normal windows we expect to be in the closed window
* data.
* @returns Promise
*/
function testWindows(windowsToOpen, expectedResults) {
return Task.spawn(function*() {
for (let winData of windowsToOpen) {
let features = "chrome,dialog=no," +
(winData.isPopup ? "all=no" : "all");
let url = "http://example.com/?window=" + windowsToOpen.length;
let openWindowPromise = BrowserTestUtils.waitForNewWindow(true, url);
openDialog(getBrowserURL(), "", features, url);
let win = yield openWindowPromise;
yield BrowserTestUtils.closeWindow(win);
}
let closedWindowData = JSON.parse(ss.getClosedWindowData());
let numPopups = closedWindowData.filter(function(el, i, arr) {
return el.isPopup;
}).length;
let numNormal = ss.getClosedWindowCount() - numPopups;
// #ifdef doesn't work in browser-chrome tests, so do a simple regex on platform
let oResults = navigator.platform.match(/Mac/) ? expectedResults.mac
: expectedResults.other;
is(numPopups, oResults.popup,
"There were " + oResults.popup + " popup windows to reopen");
is(numNormal, oResults.normal,
"There were " + oResults.normal + " normal windows to repoen");
});
}
add_task(function* test_closed_window_states() {
// This test takes quite some time, and timeouts frequently, so we require
// more time to run.
// See Bug 518970.
requestLongerTimeout(2);
let windowsToOpen = [{isPopup: false},
{isPopup: false},
{isPopup: true},
{isPopup: true},
{isPopup: true}];
let expectedResults = {mac: {popup: 3, normal: 0},
other: {popup: 3, normal: 1}};
yield testWindows(windowsToOpen, expectedResults);
let windowsToOpen2 = [{isPopup: false},
{isPopup: false},
{isPopup: false},
{isPopup: false},
{isPopup: false}];
let expectedResults2 = {mac: {popup: 0, normal: 3},
other: {popup: 0, normal: 3}};
yield testWindows(windowsToOpen2, expectedResults2);
});

View file

@ -0,0 +1,55 @@
/* 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 TESTS = [
{ url: "about:config",
key: "bug 394759 Non-PB",
value: "uniq" + r() },
{ url: "about:mozilla",
key: "bug 394759 PB",
value: "uniq" + r() },
];
function promiseTestOpenCloseWindow(aIsPrivate, aTest) {
return Task.spawn(function*() {
let win = yield BrowserTestUtils.openNewBrowserWindow({ "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);
yield TabStateFlusher.flushWindow(win);
// Close.
yield BrowserTestUtils.closeWindow(win);
});
}
function promiseTestOnWindow(aIsPrivate, aValue) {
return Task.spawn(function*() {
let win = yield BrowserTestUtils.openNewBrowserWindow({ "private": aIsPrivate });
yield TabStateFlusher.flushWindow(win);
let data = JSON.parse(ss.getClosedWindowData())[0];
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");
registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
});
}
add_task(function* init() {
forgetClosedWindows();
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

@ -0,0 +1,130 @@
/* 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/. */
Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
function waitForClearHistory(aCallback) {
let observer = {
observe: function(aSubject, aTopic, aData) {
Services.obs.removeObserver(this, "browser:purge-domain-data");
setTimeout(aCallback, 0);
}
};
Services.obs.addObserver(observer, "browser:purge-domain-data", false);
}
function test() {
waitForExplicitFinish();
// utility functions
function countClosedTabsByTitle(aClosedTabList, aTitle) {
return aClosedTabList.filter(aData => aData.title == aTitle).length;
}
function countOpenTabsByTitle(aOpenTabList, aTitle) {
return aOpenTabList.filter(aData => aData.entries.some(aEntry => aEntry.title == aTitle)).length;
}
// backup old state
let oldState = ss.getBrowserState();
let oldState_wins = JSON.parse(oldState).windows.length;
if (oldState_wins != 1)
ok(false, "oldState in test_purge has " + oldState_wins + " windows instead of 1");
// create a new state for testing
const REMEMBER = Date.now(), FORGET = Math.random();
let testState = {
windows: [ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 } ],
_closedWindows : [
// _closedWindows[0]
{
tabs: [
{ entries: [{ url: "http://example.com/", title: REMEMBER }] },
{ entries: [{ url: "http://mozilla.org/", title: FORGET }] }
],
selected: 2,
title: "mozilla.org",
_closedTabs: []
},
// _closedWindows[1]
{
tabs: [
{ entries: [{ url: "http://mozilla.org/", title: FORGET }] },
{ entries: [{ url: "http://example.com/", title: REMEMBER }] },
{ entries: [{ url: "http://example.com/", title: REMEMBER }] },
{ entries: [{ url: "http://mozilla.org/", title: FORGET }] },
{ entries: [{ url: "http://example.com/", title: REMEMBER }] }
],
selected: 5,
_closedTabs: []
},
// _closedWindows[2]
{
tabs: [
{ entries: [{ url: "http://example.com/", title: REMEMBER }] }
],
selected: 1,
_closedTabs: [
{
state: {
entries: [
{ url: "http://mozilla.org/", title: FORGET },
{ url: "http://mozilla.org/again", title: "doesn't matter" }
]
},
pos: 1,
title: FORGET
},
{
state: {
entries: [
{ url: "http://example.com", title: REMEMBER }
]
},
title: REMEMBER
}
]
}
]
};
// set browser to test state
ss.setBrowserState(JSON.stringify(testState));
// purge domain & check that we purged correctly for closed windows
ForgetAboutSite.removeDataFromDomain("mozilla.org");
waitForClearHistory(function() {
let closedWindowData = JSON.parse(ss.getClosedWindowData());
// First set of tests for _closedWindows[0] - tests basics
let win = closedWindowData[0];
is(win.tabs.length, 1, "1 tab was removed");
is(countOpenTabsByTitle(win.tabs, FORGET), 0,
"The correct tab was removed");
is(countOpenTabsByTitle(win.tabs, REMEMBER), 1,
"The correct tab was remembered");
is(win.selected, 1, "Selected tab has changed");
is(win.title, REMEMBER, "The window title was correctly updated");
// Test more complicated case
win = closedWindowData[1];
is(win.tabs.length, 3, "2 tabs were removed");
is(countOpenTabsByTitle(win.tabs, FORGET), 0,
"The correct tabs were removed");
is(countOpenTabsByTitle(win.tabs, REMEMBER), 3,
"The correct tabs were remembered");
is(win.selected, 3, "Selected tab has changed");
is(win.title, REMEMBER, "The window title was correctly updated");
// Tests handling of _closedTabs
win = closedWindowData[2];
is(countClosedTabsByTitle(win._closedTabs, REMEMBER), 1,
"The correct number of tabs were removed, and the correct ones");
is(countClosedTabsByTitle(win._closedTabs, FORGET), 0,
"All tabs to be forgotten were indeed removed");
// restore pre-test state
ss.setBrowserState(oldState);
finish();
});
}

View file

@ -0,0 +1,59 @@
"use strict";
/**
* Tests that cookies are stored and restored correctly
* by sessionstore (bug 423132).
*/
add_task(function*() {
const testURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_423132_sample.html";
Services.cookies.removeAll();
// make sure that sessionstore.js can be forced to be created by setting
// the interval pref to 0
yield SpecialPowers.pushPrefEnv({
set: [["browser.sessionstore.interval", 0]]
});
let win = yield BrowserTestUtils.openNewBrowserWindow();
let browser = win.gBrowser.selectedBrowser;
browser.loadURI(testURL);
yield BrowserTestUtils.browserLoaded(browser);
yield TabStateFlusher.flush(browser);
// get the sessionstore state for the window
let state = ss.getWindowState(win);
// verify our cookie got set during pageload
let enumerator = Services.cookies.enumerator;
let cookie;
let i = 0;
while (enumerator.hasMoreElements()) {
cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
i++;
}
Assert.equal(i, 1, "expected one cookie");
// remove the cookie
Services.cookies.removeAll();
// restore the window state
ss.setWindowState(win, state, true);
// at this point, the cookie should be restored...
enumerator = Services.cookies.enumerator;
let cookie2;
while (enumerator.hasMoreElements()) {
cookie2 = enumerator.getNext().QueryInterface(Ci.nsICookie);
if (cookie.name == cookie2.name)
break;
}
is(cookie.name, cookie2.name, "cookie name successfully restored");
is(cookie.value, cookie2.value, "cookie value successfully restored");
is(cookie.path, cookie2.path, "cookie path successfully restored");
// clean up
Services.cookies.removeAll();
yield BrowserTestUtils.closeWindow(win);
});

View file

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
// generate an enormous random number...
var r = Math.floor(Math.random() * Math.pow(2, 62)).toString();
// ... and use it to set a randomly named cookie
document.cookie = r + "=value; path=/ohai";
</script>
<body>
</body>
</html>

View file

@ -0,0 +1,65 @@
/* 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/. */
function test() {
/** Test for Bug 447951 **/
waitForExplicitFinish();
const baseURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_447951_sample.html#";
// Make sure the functionality added in bug 943339 doesn't affect the results
gPrefService.setIntPref("browser.sessionstore.max_serialize_back", -1);
gPrefService.setIntPref("browser.sessionstore.max_serialize_forward", -1);
registerCleanupFunction(function () {
gPrefService.clearUserPref("browser.sessionstore.max_serialize_back");
gPrefService.clearUserPref("browser.sessionstore.max_serialize_forward");
});
let tab = gBrowser.addTab();
promiseBrowserLoaded(tab.linkedBrowser).then(() => {
let tabState = { entries: [] };
let max_entries = gPrefService.getIntPref("browser.sessionhistory.max_entries");
for (let i = 0; i < max_entries; i++)
tabState.entries.push({ url: baseURL + i });
promiseTabState(tab, tabState).then(() => {
return TabStateFlusher.flush(tab.linkedBrowser);
}).then(() => {
tabState = JSON.parse(ss.getTabState(tab));
is(tabState.entries.length, max_entries, "session history filled to the limit");
is(tabState.entries[0].url, baseURL + 0, "... but not more");
// visit yet another anchor (appending it to session history)
ContentTask.spawn(tab.linkedBrowser, null, function() {
content.window.document.querySelector("a").click();
}).then(flushAndCheck);
function flushAndCheck() {
TabStateFlusher.flush(tab.linkedBrowser).then(check);
}
function check() {
tabState = JSON.parse(ss.getTabState(tab));
if (tab.linkedBrowser.currentURI.spec != baseURL + "end") {
// It may take a few passes through the event loop before we
// get the right URL.
executeSoon(flushAndCheck);
return;
}
is(tab.linkedBrowser.currentURI.spec, baseURL + "end",
"the new anchor was loaded");
is(tabState.entries[tabState.entries.length - 1].url, baseURL + "end",
"... and ignored");
is(tabState.entries[0].url, baseURL + 1,
"... and the first item was removed");
// clean up
gBrowser.removeTab(tab);
finish();
}
});
});
}

View file

@ -0,0 +1,5 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Testcase for bug 447951</title>
<a href="#end">click me</a>

View file

@ -0,0 +1,47 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const URL = ROOT + "browser_454908_sample.html";
const PASS = "pwd-" + Math.random();
/**
* Bug 454908 - Don't save/restore values of password fields.
*/
add_task(function* test_dont_save_passwords() {
// Make sure we do save form data.
Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
// Add a tab with a password field.
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Fill in some values.
let usernameValue = "User " + Math.random();
yield setInputValue(browser, {id: "username", value: usernameValue});
yield setInputValue(browser, {id: "passwd", value: PASS});
// Close and restore the tab.
yield promiseRemoveTab(tab);
tab = ss.undoCloseTab(window, 0);
browser = tab.linkedBrowser;
yield promiseTabRestored(tab);
// Check that password fields aren't saved/restored.
let username = yield getInputValue(browser, {id: "username"});
is(username, usernameValue, "username was saved/restored");
let passwd = yield getInputValue(browser, {id: "passwd"});
is(passwd, "", "password wasn't saved/restored");
// Write to disk and read our file.
yield forceSaveState();
yield promiseForEachSessionRestoreFile((state, key) =>
// Ensure that we have not saved our password.
ok(!state.includes(PASS), "password has not been written to file " + key)
);
// Cleanup.
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<title>Test for bug 454908</title>
<h3>Dummy Login</h3>
<form>
<p>Username: <input type="text" id="username">
<p>Password: <input type="password" id="passwd">
</form>

View file

@ -0,0 +1,49 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const URL = ROOT + "browser_456342_sample.xhtml";
/**
* Bug 456342 - Restore values from non-standard input field types.
*/
add_task(function test_restore_nonstandard_input_values() {
// Add tab with various non-standard input field types.
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Fill in form values.
let expectedValue = Math.random();
yield setFormElementValues(browser, {value: expectedValue});
// Remove tab and check collected form data.
yield promiseRemoveTab(tab);
let undoItems = JSON.parse(ss.getClosedTabData(window));
let savedFormData = undoItems[0].state.formdata;
let countGood = 0, countBad = 0;
for (let id of Object.keys(savedFormData.id)) {
if (savedFormData.id[id] == expectedValue) {
countGood++;
} else {
countBad++;
}
}
for (let exp of Object.keys(savedFormData.xpath)) {
if (savedFormData.xpath[exp] == expectedValue) {
countGood++;
} else {
countBad++;
}
}
is(countGood, 4, "Saved text for non-standard input fields");
is(countBad, 0, "Didn't save text for ignored field types");
});
function setFormElementValues(browser, data) {
return sendMessage(browser, "ss-test:setFormElementValues", data);
}

View file

@ -0,0 +1,36 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Test for bug 456342</title></head>
<body>
<form>
<h3>Non-standard &lt;input&gt;s</h3>
<p>Search <input type="search" id="searchTerm"/></p>
<p>Image Search: <input type="image search" /></p>
<p>Autocomplete: <input type="autocomplete" name="fill-in"/></p>
<p>Mistyped: <input type="txet" name="mistyped"/></p>
<h3>Ignored types</h3>
<input type="hidden" name="hideme"/>
<input type="HIDDEN" name="hideme2"/>
<input type="submit" name="submit"/>
<input type="reset" name="reset"/>
<input type="image" name="image"/>
<input type="button" name="button"/>
<input type="password" name="password"/>
<input type="PassWord" name="password2"/>
<input type="PASSWORD" name="password3"/>
<input autocomplete="off" name="auto1"/>
<input type="text" autocomplete="OFF" name="auto2"/>
<textarea autocomplete="off" name="auto3"/>
<select autocomplete="off" name="auto4">
<option value="1" selected="true"/>
<option value="2"/>
<option value="3"/>
</select>
</form>
</body>
</html>

View file

@ -0,0 +1,62 @@
/* 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/. */
function test() {
/** Test for Bug 459906 **/
waitForExplicitFinish();
let testURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_459906_sample.html";
let uniqueValue = "<b>Unique:</b> " + Date.now();
var frameCount = 0;
let tab = gBrowser.addTab(testURL);
tab.linkedBrowser.addEventListener("load", function(aEvent) {
// wait for all frames to load completely
if (frameCount++ < 2)
return;
tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
let iframes = tab.linkedBrowser.contentWindow.frames;
iframes[1].document.body.innerHTML = uniqueValue;
frameCount = 0;
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener("load", function(aEvent) {
// wait for all frames to load (and reload!) completely
if (frameCount++ < 2)
return;
tab2.linkedBrowser.removeEventListener("load", arguments.callee, true);
executeSoon(function() {
let iframes = tab2.linkedBrowser.contentWindow.frames;
if (iframes[1].document.body.innerHTML !== uniqueValue) {
// Poll again the value, since we can't ensure to run
// after SessionStore has injected innerHTML value.
// See bug 521802.
info("Polling for innerHTML value");
setTimeout(arguments.callee, 100);
return;
}
is(iframes[1].document.body.innerHTML, uniqueValue,
"rich textarea's content correctly duplicated");
let innerDomain = null;
try {
innerDomain = iframes[0].document.domain;
}
catch (ex) { /* throws for chrome: documents */ }
is(innerDomain, "mochi.test", "XSS exploit prevented!");
// clean up
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
finish();
});
}, true);
}, true);
}

View file

@ -0,0 +1,3 @@
<title>Cross Domain File for bug 459906</title>
cheers from localhost

View file

@ -0,0 +1,41 @@
<!-- Testcase originally by David Bloom <bloom@google.com> -->
<!DOCTYPE html>
<title>Test for bug 459906</title>
<body>
<iframe src="data:text/html;charset=utf-8,not_on_localhost"></iframe>
<iframe></iframe>
<script type="application/javascript">
var loadCount = 0;
frames[0].addEventListener("DOMContentLoaded", handleLoad, false);
frames[1].addEventListener("DOMContentLoaded", handleLoad, false);
function handleLoad() {
if (++loadCount < 2)
return;
frames[0].removeEventListener("DOMContentLoaded", handleLoad, false);
frames[1].removeEventListener("DOMContentLoaded", handleLoad, false);
frames[0].document.designMode = "on";
frames[0].document.__defineGetter__("designMode", function() {
// inject a cross domain file ...
var documentInjected = false;
document.getElementsByTagName("iframe")[0].onload =
function() { documentInjected = true; };
frames[0].location = "browser_459906_empty.html";
// ... and ensure that it has time to load
for (var c = 0; !documentInjected && c < 20; c++) {
var r = new XMLHttpRequest();
r.open("GET", location.href, false);
r.overrideMimeType("text/plain");
r.send(null);
}
return "on";
});
frames[1].document.designMode = "on";
};
</script>
</body>

View file

@ -0,0 +1,85 @@
/* 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/. */
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
function test() {
/** Test for Bug 461634 **/
waitForExplicitFinish();
const REMEMBER = Date.now(), FORGET = Math.random();
let test_state = { windows: [{ "tabs": [{ "entries": [] }], _closedTabs: [
{ state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
{ state: { entries: [{ url: "http://www.example.net/" }] }, title: REMEMBER },
{ state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
{ state: { entries: [{ url: "http://www.example.net/" }] }, title: REMEMBER },
] }] };
let remember_count = 2;
function countByTitle(aClosedTabList, aTitle) {
return aClosedTabList.filter(aData => aData.title == aTitle).length;
}
function testForError(aFunction) {
try {
aFunction();
return false;
}
catch (ex) {
return ex.name == "NS_ERROR_ILLEGAL_VALUE";
}
}
// Open a window and add the above closed tab list.
let newWin = openDialog(location, "", "chrome,all,dialog=no");
promiseWindowLoaded(newWin).then(() => {
gPrefService.setIntPref("browser.sessionstore.max_tabs_undo",
test_state.windows[0]._closedTabs.length);
ss.setWindowState(newWin, JSON.stringify(test_state), true);
let closedTabs = SessionStore.getClosedTabData(newWin, false);
// Verify that non JSON serialized data is the same as JSON serialized data.
is(JSON.stringify(closedTabs), SessionStore.getClosedTabData(newWin),
"Non-serialized data is the same as serialized data")
is(closedTabs.length, test_state.windows[0]._closedTabs.length,
"Closed tab list has the expected length");
is(countByTitle(closedTabs, FORGET),
test_state.windows[0]._closedTabs.length - remember_count,
"The correct amout of tabs are to be forgotten");
is(countByTitle(closedTabs, REMEMBER), remember_count,
"Everything is set up");
// All of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE.
ok(testForError(() => ss.forgetClosedTab({}, 0)),
"Invalid window for forgetClosedTab throws");
ok(testForError(() => ss.forgetClosedTab(newWin, -1)),
"Invalid tab for forgetClosedTab throws");
ok(testForError(() => ss.forgetClosedTab(newWin, test_state.windows[0]._closedTabs.length + 1)),
"Invalid tab for forgetClosedTab throws");
// Remove third tab, then first tab.
ss.forgetClosedTab(newWin, 2);
ss.forgetClosedTab(newWin, null);
closedTabs = SessionStore.getClosedTabData(newWin, false);
// Verify that non JSON serialized data is the same as JSON serialized data.
is(JSON.stringify(closedTabs), SessionStore.getClosedTabData(newWin),
"Non-serialized data is the same as serialized data")
is(closedTabs.length, remember_count,
"The correct amout of tabs was removed");
is(countByTitle(closedTabs, FORGET), 0,
"All tabs specifically forgotten were indeed removed");
is(countByTitle(closedTabs, REMEMBER), remember_count,
"... and tabs not specifically forgetten weren't");
// Clean up.
gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
BrowserTestUtils.closeWindow(newWin).then(finish);
});
}

View file

@ -0,0 +1,39 @@
/* 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/. */
function test() {
/** Test for Bug 461743 **/
waitForExplicitFinish();
let testURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_461743_sample.html";
let frameCount = 0;
let tab = gBrowser.addTab(testURL);
tab.linkedBrowser.addEventListener("load", function(aEvent) {
// Wait for all frames to load completely.
if (frameCount++ < 2)
return;
tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener("461743", function(aEvent) {
tab2.linkedBrowser.removeEventListener("461743", arguments.callee, true);
is(aEvent.data, "done", "XSS injection was attempted");
executeSoon(function() {
let iframes = tab2.linkedBrowser.contentWindow.frames;
let innerHTML = iframes[1].document.body.innerHTML;
isnot(innerHTML, Components.utils.reportError.toString(),
"chrome access denied!");
// Clean up.
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
finish();
});
}, true, true);
}, true);
}

View file

@ -0,0 +1,56 @@
<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
<!DOCTYPE html>
<title>Test for bug 461743</title>
<body>
<iframe src="data:text/html;charset=utf-8,empty"></iframe>
<iframe></iframe>
<script type="application/javascript">
var chromeUrl = "chrome://global/content/mozilla.xhtml";
var exploitUrl = "javascript:try { document.body.innerHTML = Components.utils.reportError; } catch (ex) { }";
var loadCount = 0;
frames[0].addEventListener("DOMContentLoaded", handleLoad, false);
frames[1].addEventListener("DOMContentLoaded", handleLoad, false);
function handleLoad() {
if (++loadCount < 2)
return;
frames[0].removeEventListener("DOMContentLoaded", handleLoad, false);
frames[1].removeEventListener("DOMContentLoaded", handleLoad, false);
var flip = 0;
MutationEvent.prototype.toString = function() {
return flip++ == 0 ? chromeUrl : exploitUrl;
};
var href = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(frames[1].location), "href").get;
var loadChrome = { handleEvent: href };
var loadExploit = { handleEvent: href };
function delay() {
var xhr = new XMLHttpRequest();
xhr.open("GET", location.href, false);
xhr.send(null);
}
function done() {
var event = new MessageEvent('461743', { bubbles: true, cancelable: false,
data: "done", origin: location.href,
source: window });
document.dispatchEvent(event);
frames[0].document.removeEventListener("DOMNodeInserted", loadChrome, true);
frames[0].document.removeEventListener("DOMNodeInserted", delay, true);
frames[0].document.removeEventListener("DOMNodeInserted", loadExploit, true);
frames[0].document.removeEventListener("DOMNodeInserted", done, true);
}
frames[0].document.addEventListener("DOMNodeInserted", loadChrome, true);
frames[0].document.addEventListener("DOMNodeInserted", delay, true);
frames[0].document.addEventListener("DOMNodeInserted", loadExploit, true);
frames[0].document.addEventListener("DOMNodeInserted", done, true);
frames[0].document.designMode = "on";
};
</script>
</body>

View file

@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const URL = ROOT + "browser_463205_sample.html";
/**
* Bug 463205 - Check URLs before restoring form data to make sure a malicious
* website can't modify frame URLs and make us inject form data into the wrong
* web pages.
*/
add_task(function test_check_urls_before_restoring() {
// Add a blank tab.
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Restore form data with a valid URL.
yield promiseTabState(tab, getState(URL));
let value = yield getInputValue(browser, {id: "text"});
is(value, "foobar", "value was restored");
// Restore form data with an invalid URL.
yield promiseTabState(tab, getState("http://example.com/"));
value = yield getInputValue(browser, {id: "text"});
is(value, "", "value was not restored");
// Cleanup.
gBrowser.removeTab(tab);
});
function getState(url) {
return JSON.stringify({
entries: [{url: URL}],
formdata: {url: url, id: {text: "foobar"}}
});
}

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>bug 463205</title>
<body>
<input type="text" id="text" />
</body>

View file

@ -0,0 +1,53 @@
/* 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 TEST_URL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_463206_sample.html";
add_task(function* () {
// Add a new tab.
let tab = gBrowser.addTab(TEST_URL);
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
// "Type in" some random values.
yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
function typeText(aTextField, aValue) {
aTextField.value = aValue;
let event = aTextField.ownerDocument.createEvent("UIEvents");
event.initUIEvent("input", true, true, aTextField.ownerGlobal, 0);
aTextField.dispatchEvent(event);
}
typeText(content.document.getElementById("out1"), Date.now());
typeText(content.document.getElementsByName("1|#out2")[0], Math.random());
typeText(content.frames[0].frames[1].document.getElementById("in1"), new Date());
});
// Duplicate the tab.
let tab2 = gBrowser.duplicateTab(tab);
yield promiseTabRestored(tab2);
// Query a few values from the top and its child frames.
yield ContentTask.spawn(tab2.linkedBrowser, null, function* () {
Assert.notEqual(content.document.getElementById("out1").value,
content.frames[1].document.getElementById("out1").value,
"text isn't reused for frames");
Assert.notEqual(content.document.getElementsByName("1|#out2")[0].value,
"", "text containing | and # is correctly restored");
Assert.equal(content.frames[1].document.getElementById("out2").value,
"", "id prefixes can't be faked");
// Disabled for now, Bug 588077
//Assert.equal(content.frames[0].frames[1].document.getElementById("in1").value,
// "", "id prefixes aren't mixed up");
Assert.equal(content.frames[1].frames[0].document.getElementById("in1").value,
"", "id prefixes aren't mixed up");
});
// Cleanup.
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,11 @@
<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
<!DOCTYPE html>
<meta charset="utf-8">
<title>Test for bug 463206</title>
<iframe src="data:text/html;charset=utf-8,<iframe></iframe><iframe%20src='data:text/html;charset=utf-8,<input%2520id=%2522in1%2522>'></iframe>"></iframe>
<iframe src="data:text/html;charset=utf-8,<input%20id='out1'><input%20id='out2'><iframe%20src='data:text/html;charset=utf-8,<input%2520id=%2522in1%2522>'>"></iframe>
<input id="out1">
<input name="1|#out2">

View file

@ -0,0 +1,85 @@
/* 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/. */
Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
function waitForClearHistory(aCallback) {
let observer = {
observe: function(aSubject, aTopic, aData) {
Services.obs.removeObserver(this, "browser:purge-domain-data");
setTimeout(aCallback, 0);
}
};
Services.obs.addObserver(observer, "browser:purge-domain-data", false);
}
function test() {
/** Test for Bug 464199 **/
waitForExplicitFinish();
const REMEMBER = Date.now(), FORGET = Math.random();
let test_state = { windows: [{ "tabs": [{ "entries": [] }], _closedTabs: [
{ state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
{ state: { entries: [{ url: "http://www.example.org/" }] }, title: REMEMBER },
{ state: { entries: [{ url: "http://www.example.net/" },
{ url: "http://www.example.org/" }] }, title: FORGET },
{ state: { entries: [{ url: "http://example.net/" }] }, title: FORGET },
{ state: { entries: [{ url: "http://sub.example.net/" }] }, title: FORGET },
{ state: { entries: [{ url: "http://www.example.net:8080/" }] }, title: FORGET },
{ state: { entries: [{ url: "about:license" }] }, title: REMEMBER },
{ state: { entries: [{ url: "http://www.example.org/frameset",
children: [
{ url: "http://www.example.org/frame" },
{ url: "http://www.example.org:8080/frame2" }
] }] }, title: REMEMBER },
{ state: { entries: [{ url: "http://www.example.org/frameset",
children: [
{ url: "http://www.example.org/frame" },
{ url: "http://www.example.net/frame" }
] }] }, title: FORGET },
{ state: { entries: [{ url: "http://www.example.org/form",
formdata: { id: { "url": "http://www.example.net/" } }
}] }, title: REMEMBER },
{ state: { entries: [{ url: "http://www.example.org/form" }],
extData: { "setTabValue": "http://example.net:80" } }, title: REMEMBER }
] }] };
let remember_count = 5;
function countByTitle(aClosedTabList, aTitle) {
return aClosedTabList.filter(aData => aData.title == aTitle).length;
}
// open a window and add the above closed tab list
let newWin = openDialog(location, "", "chrome,all,dialog=no");
promiseWindowLoaded(newWin).then(() => {
gPrefService.setIntPref("browser.sessionstore.max_tabs_undo",
test_state.windows[0]._closedTabs.length);
ss.setWindowState(newWin, JSON.stringify(test_state), true);
let closedTabs = JSON.parse(ss.getClosedTabData(newWin));
is(closedTabs.length, test_state.windows[0]._closedTabs.length,
"Closed tab list has the expected length");
is(countByTitle(closedTabs, FORGET),
test_state.windows[0]._closedTabs.length - remember_count,
"The correct amout of tabs are to be forgotten");
is(countByTitle(closedTabs, REMEMBER), remember_count,
"Everything is set up.");
ForgetAboutSite.removeDataFromDomain("example.net");
waitForClearHistory(function() {
closedTabs = JSON.parse(ss.getClosedTabData(newWin));
is(closedTabs.length, remember_count,
"The correct amout of tabs was removed");
is(countByTitle(closedTabs, FORGET), 0,
"All tabs to be forgotten were indeed removed");
is(countByTitle(closedTabs, REMEMBER), remember_count,
"... and tabs to be remembered weren't.");
// clean up
gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
BrowserTestUtils.closeWindow(newWin).then(finish);
});
});
}

View file

@ -0,0 +1,54 @@
<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
<title>Test for bug 464620 (injection on input)</title>
<iframe></iframe>
<iframe onload="setup()"></iframe>
<script>
var targetUrl = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_464620_xd.html";
var firstPass;
function setup() {
if (firstPass !== undefined)
return;
firstPass = frames[1].location.href == "about:blank";
if (firstPass) {
frames[0].location = 'data:text/html;charset=utf-8,<body onload="if (parent.firstPass) parent.step();"><input id="x" oninput="parent.xss()">XXX</body>';
}
frames[1].location = targetUrl;
}
function step() {
var x = frames[0].document.getElementById("x");
if (x.value == "")
x.value = "ready";
x.style.display = "none";
frames[0].document.designMode = "on";
}
function xss() {
step();
var documentInjected = false;
document.getElementsByTagName("iframe")[0].onload =
function() { documentInjected = true; };
frames[0].location = targetUrl;
for (var c = 0; !documentInjected && c < 20; c++) {
var r = new XMLHttpRequest();
r.open("GET", location.href, false);
r.overrideMimeType("text/plain");
r.send(null);
}
document.getElementById("state").textContent = "done";
var event = new MessageEvent('464620_a', { bubbles: true, cancelable: false,
data: "done", origin: location.href,
source: window });
document.dispatchEvent(event);
}
</script>
<p id="state">pending</p>

View file

@ -0,0 +1,48 @@
/* 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/. */
function test() {
/** Test for Bug 464620 (injection on input) **/
waitForExplicitFinish();
let testURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_464620_a.html";
var frameCount = 0;
let tab = gBrowser.addTab(testURL);
tab.linkedBrowser.addEventListener("load", function(aEvent) {
// wait for all frames to load completely
if (frameCount++ < 4)
return;
this.removeEventListener("load", arguments.callee, true);
executeSoon(function() {
frameCount = 0;
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener("464620_a", function(aEvent) {
tab2.linkedBrowser.removeEventListener("464620_a", arguments.callee, true);
is(aEvent.data, "done", "XSS injection was attempted");
// let form restoration complete and take into account the
// setTimeout(..., 0) in sss_restoreDocument_proxy
executeSoon(function() {
setTimeout(function() {
let win = tab2.linkedBrowser.contentWindow;
isnot(win.frames[0].document.location, testURL,
"cross domain document was loaded");
ok(!/XXX/.test(win.frames[0].document.body.innerHTML),
"no content was injected");
// clean up
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
finish();
}, 0);
});
}, true, true);
});
}, true);
}

View file

@ -0,0 +1,58 @@
<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
<title>Test for bug 464620 (injection on DOM node insertion)</title>
<iframe></iframe>
<iframe></iframe>
<iframe onload="setup()"></iframe>
<script>
var targetUrl = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_464620_xd.html";
var firstPass;
function setup() {
if (firstPass !== undefined)
return;
firstPass = frames[2].location.href == "about:blank";
if (firstPass) {
frames[0].location = 'data:text/html;charset=utf-8,<body onload="parent.step()">a</body>';
frames[1].location = 'data:text/html;charset=utf-8,<body onload="document.designMode=\'on\';">XXX</body>';
}
frames[2].location = targetUrl;
}
function step() {
frames[0].document.designMode = "on";
if (firstPass)
return;
var body = frames[0].document.body;
body.addEventListener("DOMNodeInserted", function() {
body.removeEventListener("DOMNodeInserted", arguments.callee, true);
xss();
}, true);
}
function xss() {
var documentInjected = false;
document.getElementsByTagName("iframe")[1].onload =
function() { documentInjected = true; };
frames[1].location = targetUrl;
for (var c = 0; !documentInjected && c < 20; c++) {
var r = new XMLHttpRequest();
r.open("GET", location.href, false);
r.overrideMimeType("text/plain");
r.send(null);
}
document.getElementById("state").textContent = "done";
var event = new MessageEvent('464620_b', { bubbles: true, cancelable: false,
data: "done", origin: location.href,
source: window });
document.dispatchEvent(event);
}
</script>
<p id="state">pending</p>

View file

@ -0,0 +1,48 @@
/* 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/. */
function test() {
/** Test for Bug 464620 (injection on DOM node insertion) **/
waitForExplicitFinish();
let testURL = "http://mochi.test:8888/browser/" +
"browser/components/sessionstore/test/browser_464620_b.html";
var frameCount = 0;
let tab = gBrowser.addTab(testURL);
tab.linkedBrowser.addEventListener("load", function(aEvent) {
// wait for all frames to load completely
if (frameCount++ < 6)
return;
this.removeEventListener("load", arguments.callee, true);
executeSoon(function() {
frameCount = 0;
let tab2 = gBrowser.duplicateTab(tab);
tab2.linkedBrowser.addEventListener("464620_b", function(aEvent) {
tab2.linkedBrowser.removeEventListener("464620_b", arguments.callee, true);
is(aEvent.data, "done", "XSS injection was attempted");
// let form restoration complete and take into account the
// setTimeout(..., 0) in sss_restoreDocument_proxy
executeSoon(function() {
setTimeout(function() {
let win = tab2.linkedBrowser.contentWindow;
isnot(win.frames[1].document.location, testURL,
"cross domain document was loaded");
ok(!/XXX/.test(win.frames[1].document.body.innerHTML),
"no content was injected");
// clean up
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
finish();
}, 0);
});
}, true, true);
});
}, true);
}

View file

@ -0,0 +1,5 @@
<title>Cross Document File for bug 464620</title>
<body onload="document.designMode='on';" bgcolor="red">
This document is editable.
</body>

View file

@ -0,0 +1,28 @@
"use strict";
var uniqueName = "bug 465215";
var uniqueValue1 = "as good as unique: " + Date.now();
var uniqueValue2 = "as good as unique: " + Math.random();
add_task(function* () {
// set a unique value on a new, blank tab
let tab1 = gBrowser.addTab("about:blank");
yield promiseBrowserLoaded(tab1.linkedBrowser);
ss.setTabValue(tab1, uniqueName, uniqueValue1);
// duplicate the tab with that value
let tab2 = ss.duplicateTab(window, tab1);
yield promiseTabRestored(tab2);
is(ss.getTabValue(tab2, uniqueName), uniqueValue1, "tab value was duplicated");
ss.setTabValue(tab2, uniqueName, uniqueValue2);
isnot(ss.getTabValue(tab1, uniqueName), uniqueValue2, "tab values aren't sync'd");
// overwrite the tab with the value which should remove it
yield promiseTabState(tab1, {entries: []});
is(ss.getTabValue(tab1, uniqueName), "", "tab value was cleared");
// clean up
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab1);
});

View file

@ -0,0 +1,45 @@
/* 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/. */
function test() {
/** Test for Bug 465223 **/
// test setup
waitForExplicitFinish();
let uniqueKey1 = "bug 465223.1";
let uniqueKey2 = "bug 465223.2";
let uniqueValue1 = "unik" + Date.now();
let uniqueValue2 = "pi != " + Math.random();
// open a window and set a value on it
let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
promiseWindowLoaded(newWin).then(() => {
ss.setWindowValue(newWin, uniqueKey1, uniqueValue1);
let newState = { windows: [{ tabs:[{ entries: [] }], extData: {} }] };
newState.windows[0].extData[uniqueKey2] = uniqueValue2;
ss.setWindowState(newWin, JSON.stringify(newState), false);
is(newWin.gBrowser.tabs.length, 2,
"original tab wasn't overwritten");
is(ss.getWindowValue(newWin, uniqueKey1), uniqueValue1,
"window value wasn't overwritten when the tabs weren't");
is(ss.getWindowValue(newWin, uniqueKey2), uniqueValue2,
"new window value was correctly added");
newState.windows[0].extData[uniqueKey2] = uniqueValue1;
ss.setWindowState(newWin, JSON.stringify(newState), true);
is(newWin.gBrowser.tabs.length, 1,
"original tabs were overwritten");
is(ss.getWindowValue(newWin, uniqueKey1), "",
"window value was cleared");
is(ss.getWindowValue(newWin, uniqueKey2), uniqueValue1,
"window value was correctly overwritten");
// clean up
BrowserTestUtils.closeWindow(newWin).then(finish);
});
}

View file

@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const URL = ROOT + "browser_466937_sample.html";
/**
* Bug 466937 - Prevent file stealing with sessionstore.
*/
add_task(function test_prevent_file_stealing() {
// Add a tab with some file input fields.
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Generate a path to a 'secret' file.
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
file.append("466937_test.file");
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
let testPath = file.path;
// Fill in form values.
yield setInputValue(browser, {id: "reverse_thief", value: "/home/user/secret2"});
yield setInputValue(browser, {id: "bystander", value: testPath});
// Duplicate and check form values.
let tab2 = gBrowser.duplicateTab(tab);
let browser2 = tab2.linkedBrowser;
yield promiseTabRestored(tab2);
let thief = yield getInputValue(browser2, {id: "thief"});
is(thief, "", "file path wasn't set to text field value");
let reverse_thief = yield getInputValue(browser2, {id: "reverse_thief"});
is(reverse_thief, "", "text field value wasn't set to full file path");
let bystander = yield getInputValue(browser2, {id: "bystander"});
is(bystander, testPath, "normal case: file path was correctly preserved");
// Cleanup.
gBrowser.removeTab(tab);
gBrowser.removeTab(tab2);
});

View file

@ -0,0 +1,22 @@
<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
<!DOCTYPE html>
<meta charset="utf-8">
<title>Test for bug 466937</title>
<input id="thief" value="/home/user/secret">
<input type="file" id="reverse_thief">
<input type="file" id="bystander">
<script>
window.addEventListener("DOMContentLoaded", function() {
window.removeEventListener("DOMContentLoaded", arguments.callee, false);
if (!document.location.hash) {
document.location.hash = "#ready";
}
else {
document.getElementById("thief").type = "file";
document.getElementById("reverse_thief").type = "text";
}
}, false);
</script>

View file

@ -0,0 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test Summary:
// 1. Open about:sessionrestore where formdata is a JS object, not a string
// 1a. Check that #sessionData on the page is readable after JSON.parse (skipped, checking formdata is sufficient)
// 1b. Check that there are no backslashes in the formdata
// 1c. Check that formdata doesn't require JSON.parse
//
// 2. Use the current state (currently about:sessionrestore with data) and then open that in a new instance of about:sessionrestore
// 2a. Check that there are no backslashes in the formdata
// 2b. Check that formdata doesn't require JSON.parse
//
// 3. [backwards compat] Use a stringified state as formdata when opening about:sessionrestore
// 3a. Make sure there are nodes in the tree on about:sessionrestore (skipped, checking formdata is sufficient)
// 3b. Check that there are no backslashes in the formdata
// 3c. Check that formdata doesn't require JSON.parse
const CRASH_STATE = {windows: [{tabs: [{entries: [{url: "about:mozilla" }]}]}]};
const STATE = createEntries(CRASH_STATE);
const STATE2 = createEntries({windows: [{tabs: [STATE]}]});
const STATE3 = createEntries(JSON.stringify(CRASH_STATE));
function createEntries(sessionData) {
return {
entries: [{url: "about:sessionrestore"}],
formdata: {id: {sessionData: sessionData}, url: "about:sessionrestore"}
};
}
add_task(function test_nested_about_sessionrestore() {
// Prepare a blank tab.
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// test 1
yield promiseTabState(tab, STATE);
yield checkState("test1", tab);
// test 2
yield promiseTabState(tab, STATE2);
yield checkState("test2", tab);
// test 3
yield promiseTabState(tab, STATE3);
yield checkState("test3", tab);
// Cleanup.
gBrowser.removeTab(tab);
});
function* checkState(prefix, tab) {
// Flush and query tab state.
yield TabStateFlusher.flush(tab.linkedBrowser);
let {formdata} = JSON.parse(ss.getTabState(tab));
ok(formdata.id["sessionData"], prefix + ": we have form data for about:sessionrestore");
let sessionData_raw = JSON.stringify(formdata.id["sessionData"]);
ok(!/\\/.test(sessionData_raw), prefix + ": #sessionData contains no backslashes");
info(sessionData_raw);
let gotError = false;
try {
JSON.parse(formdata.id["sessionData"]);
} catch (e) {
info(prefix + ": got error: " + e);
gotError = true;
}
ok(gotError, prefix + ": attempting to JSON.parse form data threw error");
}

View file

@ -0,0 +1,60 @@
/* 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/. */
function test() {
/** Test for Bug 477657 **/
waitForExplicitFinish();
let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
promiseWindowLoaded(newWin).then(() => {
let newState = { windows: [{
tabs: [{ entries: [] }],
_closedTabs: [{
state: { entries: [{ url: "about:" }]},
title: "About:"
}],
sizemode: "maximized"
}] };
let uniqueKey = "bug 477657";
let uniqueValue = "unik" + Date.now();
ss.setWindowValue(newWin, uniqueKey, uniqueValue);
is(ss.getWindowValue(newWin, uniqueKey), uniqueValue,
"window value was set before the window was overwritten");
ss.setWindowState(newWin, JSON.stringify(newState), true);
// use newWin.setTimeout(..., 0) to mirror sss_restoreWindowFeatures
newWin.setTimeout(function() {
is(ss.getWindowValue(newWin, uniqueKey), "",
"window value was implicitly cleared");
is(newWin.windowState, newWin.STATE_MAXIMIZED,
"the window was maximized");
is(JSON.parse(ss.getClosedTabData(newWin)).length, 1,
"the closed tab was added before the window was overwritten");
delete newState.windows[0]._closedTabs;
delete newState.windows[0].sizemode;
ss.setWindowState(newWin, JSON.stringify(newState), true);
newWin.setTimeout(function() {
is(JSON.parse(ss.getClosedTabData(newWin)).length, 0,
"closed tabs were implicitly cleared");
is(newWin.windowState, newWin.STATE_MAXIMIZED,
"the window remains maximized");
newState.windows[0].sizemode = "normal";
ss.setWindowState(newWin, JSON.stringify(newState), true);
newWin.setTimeout(function() {
isnot(newWin.windowState, newWin.STATE_MAXIMIZED,
"the window was explicitly unmaximized");
BrowserTestUtils.closeWindow(newWin).then(finish);
}, 0);
}, 0);
}, 0);
});
}

View file

@ -0,0 +1,47 @@
"use strict";
/**
* Tests that we get sent to the right page when the user clicks
* the "Close" button in about:sessionrestore
*/
add_task(function*() {
yield SpecialPowers.pushPrefEnv({
"set": [
["browser.startup.page", 0],
]
});
let tab = gBrowser.addTab("about:sessionrestore");
gBrowser.selectedTab = tab;
let browser = tab.linkedBrowser;
yield BrowserTestUtils.browserLoaded(browser, false, "about:sessionrestore");
let doc = browser.contentDocument;
// Click on the "Close" button after about:sessionrestore is loaded.
doc.getElementById("errorCancel").click();
yield BrowserTestUtils.browserLoaded(browser, false, "about:blank");
// Test that starting a new session loads the homepage (set to http://mochi.test:8888)
// if Firefox is configured to display a homepage at startup (browser.startup.page = 1)
let homepage = "http://mochi.test:8888/";
yield SpecialPowers.pushPrefEnv({
"set": [
["browser.startup.homepage", homepage],
["browser.startup.page", 1],
]
});
browser.loadURI("about:sessionrestore");
yield BrowserTestUtils.browserLoaded(browser, false, "about:sessionrestore");
doc = browser.contentDocument;
// Click on the "Close" button after about:sessionrestore is loaded.
doc.getElementById("errorCancel").click();
yield BrowserTestUtils.browserLoaded(browser);
is(browser.currentURI.spec, homepage, "loaded page is the homepage");
yield BrowserTestUtils.removeTab(tab);
});

View file

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const URL = ROOT + "browser_485482_sample.html";
/**
* Bug 485482 - Make sure that we produce valid XPath expressions even for very
* weird HTML documents.
*/
add_task(function test_xpath_exp_for_strange_documents() {
// Load a page with weird tag names.
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Fill in some values.
let uniqueValue = Math.random();
yield setInputValue(browser, {selector: "input[type=text]", value: uniqueValue});
yield setInputChecked(browser, {selector: "input[type=checkbox]", checked: true});
// Duplicate the tab.
let tab2 = gBrowser.duplicateTab(tab);
let browser2 = tab2.linkedBrowser;
yield promiseTabRestored(tab2);
// Check that we generated valid XPath expressions to restore form values.
let text = yield getInputValue(browser2, {selector: "input[type=text]"});
is(text, uniqueValue, "generated XPath expression was valid");
let checkbox = yield getInputChecked(browser2, {selector: "input[type=checkbox]"});
ok(checkbox, "generated XPath expression was valid");
// Cleanup.
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<title>Test for bug 485482</title>
<bad=name>
<input type="text">
</bad=name>
<worse=name>
<l0c@l+na~e"'§>
<input type="checkbox" name="check"> Check
</l0c@l+na~e"'§>
</worse=name>

View file

@ -0,0 +1,26 @@
/* 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/. */
function test() {
/** Test for Bug 485563 **/
waitForExplicitFinish();
let uniqueValue = Math.random() + "\u2028Second line\u2029Second paragraph\u2027";
let tab = gBrowser.addTab();
promiseBrowserLoaded(tab.linkedBrowser).then(() => {
ss.setTabValue(tab, "bug485563", uniqueValue);
let tabState = JSON.parse(ss.getTabState(tab));
is(tabState.extData["bug485563"], uniqueValue,
"unicode line separator wasn't over-encoded");
ss.deleteTabValue(tab, "bug485563");
ss.setTabState(tab, JSON.stringify(tabState));
is(ss.getTabValue(tab, "bug485563"), uniqueValue,
"unicode line separator was correctly preserved");
gBrowser.removeTab(tab);
finish();
});
}

View file

@ -0,0 +1,65 @@
/* 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/. */
// Only windows with open tabs are restorable. Windows where a lone tab is
// detached may have _closedTabs, but is left with just an empty tab.
const STATES = [{
shouldBeAdded: true,
windowState: {
windows: [{
tabs: [{ entries: [{ url: "http://example.com", title: "example.com" }] }],
selected: 1,
_closedTabs: []
}]
}
}, {
shouldBeAdded: false,
windowState: {
windows: [{
tabs: [{ entries: [] }],
_closedTabs: []
}]
}
}, {
shouldBeAdded: false,
windowState: {
windows: [{
tabs: [{ entries: [] }],
_closedTabs: [{ state: { entries: [{ url: "http://example.com", index: 1 }] } }]
}]
}
}, {
shouldBeAdded: false,
windowState: {
windows: [{
tabs: [{ entries: [] }],
_closedTabs: [],
extData: { keyname: "pi != " + Math.random() }
}]
}
}];
add_task(function* test_bug_490040() {
for (let state of STATES) {
// Ensure we can store the window if needed.
let startingClosedWindowCount = ss.getClosedWindowCount();
yield pushPrefs(["browser.sessionstore.max_windows_undo",
startingClosedWindowCount + 1]);
let curClosedWindowCount = ss.getClosedWindowCount();
let win = yield BrowserTestUtils.openNewBrowserWindow();
ss.setWindowState(win, JSON.stringify(state.windowState), true);
if (state.windowState.windows[0].tabs.length) {
yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
}
yield BrowserTestUtils.closeWindow(win);
is(ss.getClosedWindowCount(),
curClosedWindowCount + (state.shouldBeAdded ? 1 : 0),
"That window should " + (state.shouldBeAdded ? "" : "not ") +
"be restorable");
}
});

View file

@ -0,0 +1,42 @@
"use strict";
const REFERRER1 = "http://example.org/?" + Date.now();
const REFERRER2 = "http://example.org/?" + Math.random();
add_task(function* () {
function* checkDocumentReferrer(referrer, msg) {
yield ContentTask.spawn(gBrowser.selectedBrowser, { referrer, msg }, function* (args) {
Assert.equal(content.document.referrer, args.referrer, args.msg);
});
}
// Add a new tab.
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Load a new URI with a specific referrer.
let referrerURI = Services.io.newURI(REFERRER1, null, null);
browser.loadURI("http://example.org", referrerURI, null);
yield promiseBrowserLoaded(browser);
yield TabStateFlusher.flush(browser);
let tabState = JSON.parse(ss.getTabState(tab));
is(tabState.entries[0].referrer, REFERRER1,
"Referrer retrieved via getTabState matches referrer set via loadURI.");
tabState.entries[0].referrer = REFERRER2;
yield promiseTabState(tab, tabState);
yield checkDocumentReferrer(REFERRER2,
"document.referrer matches referrer set via setTabState.");
gBrowser.removeCurrentTab();
// Restore the closed tab.
tab = ss.undoCloseTab(window, 0);
yield promiseTabRestored(tab);
yield checkDocumentReferrer(REFERRER2,
"document.referrer is still correct after closing and reopening the tab.");
gBrowser.removeCurrentTab();
});

View file

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
function test() {
/** Test for Bug 491577 **/
// test setup
waitForExplicitFinish();
const REMEMBER = Date.now(), FORGET = Math.random();
let test_state = {
windows: [ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 } ],
_closedWindows : [
// _closedWindows[0]
{
tabs: [
{ entries: [{ url: "http://example.com/", title: "title" }] },
{ entries: [{ url: "http://mozilla.org/", title: "title" }] }
],
selected: 2,
title: FORGET,
_closedTabs: []
},
// _closedWindows[1]
{
tabs: [
{ entries: [{ url: "http://mozilla.org/", title: "title" }] },
{ entries: [{ url: "http://example.com/", title: "title" }] },
{ entries: [{ url: "http://mozilla.org/", title: "title" }] },
],
selected: 3,
title: REMEMBER,
_closedTabs: []
},
// _closedWindows[2]
{
tabs: [
{ entries: [{ url: "http://example.com/", title: "title" }] }
],
selected: 1,
title: FORGET,
_closedTabs: [
{
state: {
entries: [
{ url: "http://mozilla.org/", title: "title" },
{ url: "http://mozilla.org/again", title: "title" }
]
},
pos: 1,
title: "title"
},
{
state: {
entries: [
{ url: "http://example.com", title: "title" }
]
},
title: "title"
}
]
}
]
};
let remember_count = 1;
function countByTitle(aClosedWindowList, aTitle) {
return aClosedWindowList.filter(aData => aData.title == aTitle).length;
}
function testForError(aFunction) {
try {
aFunction();
return false;
}
catch (ex) {
return ex.name == "NS_ERROR_ILLEGAL_VALUE";
}
}
// open a window and add the above closed window list
let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
promiseWindowLoaded(newWin).then(() => {
gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
test_state._closedWindows.length);
ss.setWindowState(newWin, JSON.stringify(test_state), true);
let closedWindows = JSON.parse(ss.getClosedWindowData());
is(closedWindows.length, test_state._closedWindows.length,
"Closed window list has the expected length");
is(countByTitle(closedWindows, FORGET),
test_state._closedWindows.length - remember_count,
"The correct amount of windows are to be forgotten");
is(countByTitle(closedWindows, REMEMBER), remember_count,
"Everything is set up.");
// all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
ok(testForError(() => ss.forgetClosedWindow(-1)),
"Invalid window for forgetClosedWindow throws");
ok(testForError(() => ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
"Invalid window for forgetClosedWindow throws");
// Remove third window, then first window
ss.forgetClosedWindow(2);
ss.forgetClosedWindow(null);
closedWindows = JSON.parse(ss.getClosedWindowData());
is(closedWindows.length, remember_count,
"The correct amount of windows were removed");
is(countByTitle(closedWindows, FORGET), 0,
"All windows specifically forgotten were indeed removed");
is(countByTitle(closedWindows, REMEMBER), remember_count,
"... and windows not specifically forgetten weren't.");
// clean up
gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
BrowserTestUtils.closeWindow(newWin).then(finish);
});
}

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/. */
function test() {
/** Test for Bug 495495 **/
waitForExplicitFinish();
let newWin = openDialog(location, "_blank", "chrome,all,dialog=no,toolbar=yes");
promiseWindowLoaded(newWin).then(() => {
let state1 = ss.getWindowState(newWin);
BrowserTestUtils.closeWindow(newWin).then(() => {
newWin = openDialog(location, "_blank",
"chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar=no,location,personal,directories,dialog=no");
promiseWindowLoaded(newWin).then(() => {
let state2 = ss.getWindowState(newWin);
function testState(state, expected, callback) {
let win = openDialog(location, "_blank", "chrome,all,dialog=no");
promiseWindowLoaded(win).then(() => {
is(win.gURLBar.readOnly, false,
"URL bar should not be read-only before setting the state");
is(win.gURLBar.getAttribute("enablehistory"), "true",
"URL bar autocomplete should be enabled before setting the state");
ss.setWindowState(win, state, true);
is(win.gURLBar.readOnly, expected.readOnly,
"URL bar read-only state should be restored correctly");
is(win.gURLBar.getAttribute("enablehistory"), expected.enablehistory,
"URL bar autocomplete state should be restored correctly");
BrowserTestUtils.closeWindow(win).then(callback);
});
}
BrowserTestUtils.closeWindow(newWin).then(() => {
testState(state1, {readOnly: false, enablehistory: "true"}, function() {
testState(state2, {readOnly: true, enablehistory: "false"}, finish);
});
});
});
});
});
}

View file

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
let checkState = Task.async(function*(browser) {
// Go back and then forward, and make sure that the state objects received
// from the popState event are as we expect them to be.
//
// We also add a node to the document's body when after going back and make
// sure it's still there after we go forward -- this is to test that the two
// history entries correspond to the same document.
let deferred = {};
deferred.promise = new Promise(resolve => deferred.resolve = resolve);
let popStateCount = 0;
browser.addEventListener("popstate", function(aEvent) {
if (popStateCount == 0) {
popStateCount++;
ok(aEvent.state, "Event should have a state property.");
ContentTask.spawn(browser, null, function() {
is(content.testState, "foo",
"testState after going back");
is(JSON.stringify(content.history.state), JSON.stringify({obj1:1}),
"first popstate object.");
// Add a node with id "new-elem" to the document.
let doc = content.document;
ok(!doc.getElementById("new-elem"),
"doc shouldn't contain new-elem before we add it.");
let elem = doc.createElement("div");
elem.id = "new-elem";
doc.body.appendChild(elem);
}).then(() => {
browser.goForward();
});
} else if (popStateCount == 1) {
popStateCount++;
// When content fires a PopStateEvent and we observe it from a chrome event
// listener (as we do here, and, thankfully, nowhere else in the tree), the
// state object will be a cross-compartment wrapper to an object that was
// deserialized in the content scope. And in this case, since RegExps are
// not currently Xrayable (see bug 1014991), trying to pull |obj3| (a RegExp)
// off of an Xrayed Object won't work. So we need to waive.
ContentTask.spawn(browser, aEvent.state, function(state) {
Assert.equal(Cu.waiveXrays(state).obj3.toString(),
"/^a$/", "second popstate object.");
// Make sure that the new-elem node is present in the document. If it's
// not, then this history entry has a different doc identifier than the
// previous entry, which is bad.
let doc = content.document;
let newElem = doc.getElementById("new-elem");
ok(newElem, "doc should contain new-elem.");
newElem.parentNode.removeChild(newElem);
ok(!doc.getElementById("new-elem"), "new-elem should be removed.");
}).then(() => {
browser.removeEventListener("popstate", arguments.callee, true);
deferred.resolve();
});
}
});
// Set some state in the page's window. When we go back(), the page should
// be retrieved from bfcache, and this state should still be there.
yield ContentTask.spawn(browser, null, function() {
content.testState = "foo";
});
// Now go back. This should trigger the popstate event handler above.
browser.goBack();
yield deferred.promise;
});
add_task(function* test() {
// Tests session restore functionality of history.pushState and
// history.replaceState(). (Bug 500328)
// We open a new blank window, let it load, and then load in
// http://example.com. We need to load the blank window first, otherwise the
// docshell gets confused and doesn't have a current history entry.
let state;
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
BrowserTestUtils.loadURI(browser, "http://example.com");
yield BrowserTestUtils.browserLoaded(browser);
// After these push/replaceState calls, the window should have three
// history entries:
// testURL (state object: null) <-- oldest
// testURL (state object: {obj1:1})
// testURL?page2 (state object: {obj3:/^a$/}) <-- newest
function contentTest() {
let history = content.window.history;
history.pushState({obj1:1}, "title-obj1");
history.pushState({obj2:2}, "title-obj2", "?page2");
history.replaceState({obj3:/^a$/}, "title-obj3");
}
yield ContentTask.spawn(browser, null, contentTest);
yield TabStateFlusher.flush(browser);
state = ss.getTabState(gBrowser.getTabForBrowser(browser));
});
// Restore the state into a new tab. Things don't work well when we
// restore into the old tab, but that's not a real use case anyway.
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
let tab2 = gBrowser.getTabForBrowser(browser);
let tabRestoredPromise = promiseTabRestored(tab2);
ss.setTabState(tab2, state, true);
// Run checkState() once the tab finishes loading its restored state.
yield tabRestoredPromise;
yield checkState(browser);
});
});

View file

@ -0,0 +1,73 @@
/* 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/. */
function test() {
/** Test for Bug 506482 **/
// test setup
waitForExplicitFinish();
// read the sessionstore.js mtime (picked from browser_248970_a.js)
let profilePath = Cc["@mozilla.org/file/directory_service;1"].
getService(Ci.nsIProperties).
get("ProfD", Ci.nsIFile);
function getSessionstoreFile() {
let sessionStoreJS = profilePath.clone();
sessionStoreJS.append("sessionstore.js");
return sessionStoreJS;
}
function getSessionstorejsModificationTime() {
let file = getSessionstoreFile();
if (file.exists())
return file.lastModifiedTime;
else
return -1;
}
// delete existing sessionstore.js, to make sure we're not reading
// the mtime of an old one initially.
let sessionStoreJS = getSessionstoreFile();
if (sessionStoreJS.exists())
sessionStoreJS.remove(false);
// test content URL
const TEST_URL = "data:text/html;charset=utf-8,"
+ "<body style='width: 100000px; height: 100000px;'><p>top</p></body>"
// preferences that we use
const PREF_INTERVAL = "browser.sessionstore.interval";
// make sure sessionstore.js is saved ASAP on all events
gPrefService.setIntPref(PREF_INTERVAL, 0);
// get the initial sessionstore.js mtime (-1 if it doesn't exist yet)
let mtime0 = getSessionstorejsModificationTime();
// create and select a first tab
let tab = gBrowser.addTab(TEST_URL);
promiseBrowserLoaded(tab.linkedBrowser).then(() => {
// step1: the above has triggered some saveStateDelayed(), sleep until
// it's done, and get the initial sessionstore.js mtime
setTimeout(function step1(e) {
let mtime1 = getSessionstorejsModificationTime();
isnot(mtime1, mtime0, "initial sessionstore.js update");
// step2: test sessionstore.js is not updated on tab selection
// or content scrolling
gBrowser.selectedTab = tab;
tab.linkedBrowser.contentWindow.scrollTo(1100, 1200);
setTimeout(function step2(e) {
let mtime2 = getSessionstorejsModificationTime();
is(mtime2, mtime1,
"tab selection and scrolling: sessionstore.js not updated");
// ok, done, cleanup and finish
if (gPrefService.prefHasUserValue(PREF_INTERVAL))
gPrefService.clearUserPref(PREF_INTERVAL);
gBrowser.removeTab(tab);
finish();
}, 3500); // end of sleep after tab selection and scrolling
}, 3500); // end of sleep after initial saveStateDelayed()
});
}

View file

@ -0,0 +1,38 @@
/* 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/. */
function test() {
/** Test for Bug 514751 (Wallpaper) **/
waitForExplicitFinish();
let state = {
windows: [{
tabs: [{
entries: [
{ url: "about:mozilla", title: "Mozilla" },
{}
]
}]
}]
};
var theWin = openDialog(location, "", "chrome,all,dialog=no");
theWin.addEventListener("load", function () {
theWin.removeEventListener("load", arguments.callee, false);
executeSoon(function () {
var gotError = false;
try {
ss.setWindowState(theWin, JSON.stringify(state), true);
} catch (e) {
if (/NS_ERROR_MALFORMED_URI/.test(e))
gotError = true;
}
ok(!gotError, "Didn't get a malformed URI error.");
BrowserTestUtils.closeWindow(theWin).then(finish);
});
}, false);
}

View file

@ -0,0 +1,21 @@
function test() {
var startup_info = Components.classes["@mozilla.org/toolkit/app-startup;1"].getService(Components.interfaces.nsIAppStartup).getStartupInfo();
// No .process info on mac
// Check if we encountered a telemetry error for the the process creation
// timestamp and turn the first test into a known failure.
var telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
var snapshot = telemetry.getHistogramById("STARTUP_MEASUREMENT_ERRORS")
.snapshot();
if (snapshot.counts[0] == 0)
ok(startup_info.process <= startup_info.main, "process created before main is run " + uneval(startup_info));
else
todo(false, "An error occurred while recording the process creation timestamp, skipping this test");
// on linux firstPaint can happen after everything is loaded (especially with remote X)
if (startup_info.firstPaint)
ok(startup_info.main <= startup_info.firstPaint, "main ran before first paint " + uneval(startup_info));
ok(startup_info.main < startup_info.sessionRestored, "Session restored after main " + uneval(startup_info));
}

View file

@ -0,0 +1,269 @@
/* 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/. */
function test() {
/** Test for Bug 522545 **/
waitForExplicitFinish();
requestLongerTimeout(4);
// This tests the following use case:
// User opens a new tab which gets focus. The user types something into the
// address bar, then crashes or quits.
function test_newTabFocused() {
let state = {
windows: [{
tabs: [
{ entries: [{ url: "about:mozilla" }] },
{ entries: [], userTypedValue: "example.com", userTypedClear: 0 }
],
selected: 2
}]
};
waitForBrowserState(state, function() {
let browser = gBrowser.selectedBrowser;
is(browser.currentURI.spec, "about:blank",
"No history entries still sets currentURI to about:blank");
is(browser.userTypedValue, "example.com",
"userTypedValue was correctly restored");
ok(!browser.didStartLoadSinceLastUserTyping(),
"We still know that no load is ongoing");
is(gURLBar.value, "example.com",
"Address bar's value correctly restored");
// Change tabs to make sure address bar value gets updated
gBrowser.selectedTab = gBrowser.tabContainer.getItemAtIndex(0);
is(gURLBar.value, "about:mozilla",
"Address bar's value correctly updated");
runNextTest();
});
}
// This tests the following use case:
// User opens a new tab which gets focus. The user types something into the
// address bar, switches back to the first tab, then crashes or quits.
function test_newTabNotFocused() {
let state = {
windows: [{
tabs: [
{ entries: [{ url: "about:mozilla" }] },
{ entries: [], userTypedValue: "example.org", userTypedClear: 0 }
],
selected: 1
}]
};
waitForBrowserState(state, function() {
let browser = gBrowser.getBrowserAtIndex(1);
is(browser.currentURI.spec, "about:blank",
"No history entries still sets currentURI to about:blank");
is(browser.userTypedValue, "example.org",
"userTypedValue was correctly restored");
ok(!browser.didStartLoadSinceLastUserTyping(),
"We still know that no load is ongoing");
is(gURLBar.value, "about:mozilla",
"Address bar's value correctly restored");
// Change tabs to make sure address bar value gets updated
gBrowser.selectedTab = gBrowser.tabContainer.getItemAtIndex(1);
is(gURLBar.value, "example.org",
"Address bar's value correctly updated");
runNextTest();
});
}
// This tests the following use case:
// User is in a tab with session history, then types something in the
// address bar, then crashes or quits.
function test_existingSHEnd_noClear() {
let state = {
windows: [{
tabs: [{
entries: [{ url: "about:mozilla" }, { url: "about:config" }],
index: 2,
userTypedValue: "example.com",
userTypedClear: 0
}]
}]
};
waitForBrowserState(state, function() {
let browser = gBrowser.selectedBrowser;
is(browser.currentURI.spec, "about:config",
"browser.currentURI set to current entry in SH");
is(browser.userTypedValue, "example.com",
"userTypedValue was correctly restored");
ok(!browser.didStartLoadSinceLastUserTyping(),
"We still know that no load is ongoing");
is(gURLBar.value, "example.com",
"Address bar's value correctly restored to userTypedValue");
runNextTest();
});
}
// This tests the following use case:
// User is in a tab with session history, presses back at some point, then
// types something in the address bar, then crashes or quits.
function test_existingSHMiddle_noClear() {
let state = {
windows: [{
tabs: [{
entries: [{ url: "about:mozilla" }, { url: "about:config" }],
index: 1,
userTypedValue: "example.org",
userTypedClear: 0
}]
}]
};
waitForBrowserState(state, function() {
let browser = gBrowser.selectedBrowser;
is(browser.currentURI.spec, "about:mozilla",
"browser.currentURI set to current entry in SH");
is(browser.userTypedValue, "example.org",
"userTypedValue was correctly restored");
ok(!browser.didStartLoadSinceLastUserTyping(),
"We still know that no load is ongoing");
is(gURLBar.value, "example.org",
"Address bar's value correctly restored to userTypedValue");
runNextTest();
});
}
// This test simulates lots of tabs opening at once and then quitting/crashing.
function test_getBrowserState_lotsOfTabsOpening() {
gBrowser.stop();
let uris = [];
for (let i = 0; i < 25; i++)
uris.push("http://example.com/" + i);
// We're waiting for the first location change, which should indicate
// one of the tabs has loaded and the others haven't. So one should
// be in a non-userTypedValue case, while others should still have
// userTypedValue and userTypedClear set.
gBrowser.addTabsProgressListener({
onLocationChange: function (aBrowser) {
if (uris.indexOf(aBrowser.currentURI.spec) > -1) {
gBrowser.removeTabsProgressListener(this);
firstLocationChange();
}
}
});
function firstLocationChange() {
let state = JSON.parse(ss.getBrowserState());
let hasUTV = state.windows[0].tabs.some(function(aTab) {
return aTab.userTypedValue && aTab.userTypedClear && !aTab.entries.length;
});
ok(hasUTV, "At least one tab has a userTypedValue with userTypedClear with no loaded URL");
BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager, "SessionStore:update").then(firstLoad);
}
function firstLoad() {
let state = JSON.parse(ss.getTabState(gBrowser.selectedTab));
let hasSH = !("userTypedValue" in state) && state.entries[0].url;
ok(hasSH, "The selected tab has its entry in SH");
runNextTest();
}
gBrowser.loadTabs(uris);
}
// This simulates setting a userTypedValue and ensures that just typing in the
// URL bar doesn't set userTypedClear as well.
function test_getBrowserState_userTypedValue() {
let state = {
windows: [{
tabs: [{ entries: [] }]
}]
};
waitForBrowserState(state, function() {
let browser = gBrowser.selectedBrowser;
// Make sure this tab isn't loading and state is clear before we test.
is(browser.userTypedValue, null, "userTypedValue is empty to start");
ok(!browser.didStartLoadSinceLastUserTyping(),
"Initially, no load should be ongoing");
let inputText = "example.org";
gURLBar.focus();
gURLBar.value = inputText.slice(0, -1);
EventUtils.synthesizeKey(inputText.slice(-1) , {});
executeSoon(function () {
is(browser.userTypedValue, "example.org",
"userTypedValue was set when changing URLBar value");
ok(!browser.didStartLoadSinceLastUserTyping(),
"No load started since changing URLBar value");
// Now make sure ss gets these values too
let newState = JSON.parse(ss.getBrowserState());
is(newState.windows[0].tabs[0].userTypedValue, "example.org",
"sessionstore got correct userTypedValue");
is(newState.windows[0].tabs[0].userTypedClear, 0,
"sessionstore got correct userTypedClear");
runNextTest();
});
});
}
// test_getBrowserState_lotsOfTabsOpening tested userTypedClear in a few cases,
// but not necessarily any that had legitimate URIs in the state of loading
// (eg, "http://example.com"), so this test will cover that case.
function test_userTypedClearLoadURI() {
let state = {
windows: [{
tabs: [
{ entries: [], userTypedValue: "http://example.com", userTypedClear: 2 }
]
}]
};
waitForBrowserState(state, function() {
let browser = gBrowser.selectedBrowser;
is(browser.currentURI.spec, "http://example.com/",
"userTypedClear=2 caused userTypedValue to be loaded");
is(browser.userTypedValue, null,
"userTypedValue was null after loading a URI");
ok(!browser.didStartLoadSinceLastUserTyping(),
"We should have reset the load state when the tab loaded");
is(gURLBar.textValue, gURLBar.trimValue("http://example.com/"),
"Address bar's value set after loading URI");
runNextTest();
});
}
let tests = [test_newTabFocused, test_newTabNotFocused,
test_existingSHEnd_noClear, test_existingSHMiddle_noClear,
test_getBrowserState_lotsOfTabsOpening,
test_getBrowserState_userTypedValue, test_userTypedClearLoadURI];
let originalState = JSON.parse(ss.getBrowserState());
let state = {
windows: [{
tabs: [{ entries: [{ url: "about:blank" }] }]
}]
};
function runNextTest() {
if (tests.length) {
waitForBrowserState(state, function() {
gBrowser.selectedBrowser.userTypedValue = null;
URLBarSetURI();
(tests.shift())();
});
} else {
waitForBrowserState(originalState, function() {
gBrowser.selectedBrowser.userTypedValue = null;
URLBarSetURI();
finish();
});
}
}
// Run the tests!
runNextTest();
}

View file

@ -0,0 +1,42 @@
/* 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/. */
function test() {
/** Test for Bug 524745 **/
let uniqKey = "bug524745";
let uniqVal = Date.now().toString();
waitForExplicitFinish();
whenNewWindowLoaded({ private: false }, function (window_B) {
waitForFocus(function() {
// Add identifying information to window_B
ss.setWindowValue(window_B, uniqKey, uniqVal);
let state = JSON.parse(ss.getBrowserState());
let selectedWindow = state.windows[state.selectedWindow - 1];
is(selectedWindow.extData && selectedWindow.extData[uniqKey], uniqVal,
"selectedWindow is window_B");
// Now minimize window_B. The selected window shouldn't have the secret data
window_B.minimize();
waitForFocus(function() {
state = JSON.parse(ss.getBrowserState());
selectedWindow = state.windows[state.selectedWindow - 1];
ok(!selectedWindow.extData || !selectedWindow.extData[uniqKey],
"selectedWindow is not window_B after minimizing it");
// Now minimize the last open window (assumes no other tests left windows open)
window.minimize();
state = JSON.parse(ss.getBrowserState());
is(state.selectedWindow, 0,
"selectedWindow should be 0 when all windows are minimized");
// Cleanup
window.restore();
BrowserTestUtils.closeWindow(window_B).then(finish);
});
}, window_B);
});
}

View file

@ -0,0 +1,72 @@
/* 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/. */
function test() {
/** Test for Bug 526613 **/
// test setup
waitForExplicitFinish();
function browserWindowsCount(expected) {
let count = 0;
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
if (!e.getNext().closed)
++count;
}
is(count, expected,
"number of open browser windows according to nsIWindowMediator");
let state = ss.getBrowserState();
info(state);
is(JSON.parse(state).windows.length, expected,
"number of open browser windows according to getBrowserState");
}
browserWindowsCount(1);
// backup old state
let oldState = ss.getBrowserState();
// create a new state for testing
let testState = {
windows: [
{ tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 },
{ tabs: [{ entries: [{ url: "about:mozilla" }] }], selected: 1 },
],
// make sure the first window is focused, otherwise when restoring the
// old state, the first window is closed and the test harness gets unloaded
selectedWindow: 1
};
let pass = 1;
function observer(aSubject, aTopic, aData) {
is(aTopic, "sessionstore-browser-state-restored",
"The sessionstore-browser-state-restored notification was observed");
if (pass++ == 1) {
browserWindowsCount(2);
// let the first window be focused (see above)
function pollMostRecentWindow() {
if (Services.wm.getMostRecentWindow("navigator:browser") == window) {
ss.setBrowserState(oldState);
} else {
info("waiting for the current window to become active");
setTimeout(pollMostRecentWindow, 0);
window.focus(); //XXX Why is this needed?
}
}
pollMostRecentWindow();
}
else {
browserWindowsCount(1);
ok(!window.closed, "Restoring the old state should have left this window open");
Services.obs.removeObserver(observer, "sessionstore-browser-state-restored");
finish();
}
}
Services.obs.addObserver(observer, "sessionstore-browser-state-restored", false);
// set browser to test state
ss.setBrowserState(JSON.stringify(testState));
}

View file

@ -0,0 +1,21 @@
function browserWindowsCount(expected) {
var count = 0;
var e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
if (!e.getNext().closed)
++count;
}
is(count, expected,
"number of open browser windows according to nsIWindowMediator");
is(JSON.parse(ss.getBrowserState()).windows.length, expected,
"number of open browser windows according to getBrowserState");
}
add_task(function() {
browserWindowsCount(1);
let win = yield BrowserTestUtils.openNewBrowserWindow();
browserWindowsCount(2);
yield BrowserTestUtils.closeWindow(win);
browserWindowsCount(1);
});

View file

@ -0,0 +1,30 @@
/* 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/. */
function test() {
waitForExplicitFinish();
let tab1 = gBrowser.addTab("about:rights");
let tab2 = gBrowser.addTab("about:mozilla");
promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
// Tell the session storer that the tab is pinned
let newTabState = '{"entries":[{"url":"about:rights"}],"pinned":true,"userTypedValue":"Hello World!"}';
ss.setTabState(tab1, newTabState);
// Undo pinning
gBrowser.unpinTab(tab1);
// Close and restore tab
gBrowser.removeTab(tab1);
let savedState = JSON.parse(ss.getClosedTabData(window))[0].state;
isnot(savedState.pinned, true, "Pinned should not be true");
tab1 = ss.undoCloseTab(window, 0);
isnot(tab1.pinned, true, "Should not be pinned");
gBrowser.removeTab(tab1);
gBrowser.removeTab(tab2);
finish();
});
}

View file

@ -0,0 +1,20 @@
"use strict";
add_task(function* () {
let tab1 = gBrowser.addTab("data:text/plain;charset=utf-8,foo");
gBrowser.pinTab(tab1);
yield promiseBrowserLoaded(tab1.linkedBrowser);
let tab2 = gBrowser.addTab();
gBrowser.pinTab(tab2);
is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 is at the first position");
yield promiseRemoveTab(tab1);
tab1 = undoCloseTab();
ok(tab1.pinned, "pinned tab 1 has been restored as a pinned tab");
is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 has been restored to the first position");
yield BrowserTestUtils.removeTab(tab1);
yield BrowserTestUtils.removeTab(tab2);
});

View file

@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const URIS_PINNED = ["about:license", "about:about"];
const URIS_NORMAL_A = ["about:mozilla"];
const URIS_NORMAL_B = ["about:buildconfig"];
function test() {
waitForExplicitFinish();
isnot(Services.prefs.getIntPref("browser.startup.page"), 3,
"pref to save session must not be set for this test");
ok(!Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"),
"pref to save session once must not be set for this test");
document.documentElement.setAttribute("windowtype", "navigator:browsertestdummy");
openWinWithCb(closeFirstWin, URIS_PINNED.concat(URIS_NORMAL_A));
}
function closeFirstWin(win) {
win.gBrowser.pinTab(win.gBrowser.tabs[0]);
win.gBrowser.pinTab(win.gBrowser.tabs[1]);
let winClosed = BrowserTestUtils.windowClosed(win);
// We need to call BrowserTryToCloseWindow in order to trigger
// the machinery that chooses whether or not to save the session
// for the last window.
win.BrowserTryToCloseWindow();
ok(win.closed, "window closed");
winClosed.then(() => {
openWinWithCb(checkSecondWin, URIS_NORMAL_B, URIS_PINNED.concat(URIS_NORMAL_B));
});
}
function checkSecondWin(win) {
is(win.gBrowser.browsers[0].currentURI.spec, URIS_PINNED[0], "first pinned tab restored");
is(win.gBrowser.browsers[1].currentURI.spec, URIS_PINNED[1], "second pinned tab restored");
ok(win.gBrowser.tabs[0].pinned, "first pinned tab is still pinned");
ok(win.gBrowser.tabs[1].pinned, "second pinned tab is still pinned");
BrowserTestUtils.closeWindow(win).then(() => {
// cleanup
document.documentElement.setAttribute("windowtype", "navigator:browser");
finish();
});
}
function openWinWithCb(cb, argURIs, expectedURIs) {
if (!expectedURIs)
expectedURIs = argURIs;
var win = openDialog(getBrowserURL(), "_blank",
"chrome,all,dialog=no", argURIs.join("|"));
win.addEventListener("load", function () {
win.removeEventListener("load", arguments.callee, false);
info("the window loaded");
var expectedLoads = expectedURIs.length;
win.gBrowser.addTabsProgressListener({
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aRequest &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
expectedURIs.indexOf(aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec) > -1 &&
--expectedLoads <= 0) {
win.gBrowser.removeTabsProgressListener(this);
info("all tabs loaded");
is(win.gBrowser.tabs.length, expectedURIs.length, "didn't load any unexpected tabs");
executeSoon(function () {
cb(win);
});
}
}
});
}, false);
}

View file

@ -0,0 +1,19 @@
// Tests that an about:blank tab with no history will not be saved into
// session store and thus, it will not show up in Recently Closed Tabs.
"use strict";
add_task(function* () {
let tab = gBrowser.addTab("about:blank");
yield promiseBrowserLoaded(tab.linkedBrowser);
is(tab.linkedBrowser.currentURI.spec, "about:blank",
"we will be removing an about:blank tab");
let r = `rand-${Math.random()}`;
ss.setTabValue(tab, "foobar", r);
yield promiseRemoveTab(tab);
let closedTabData = ss.getClosedTabData(window);
ok(!closedTabData.includes(r), "tab not stored in _closedTabs");
});

Some files were not shown because too many files have changed in this diff Show more