mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-16 12:17:34 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
431
browser/components/sessionstore/ContentRestore.jsm
Normal file
431
browser/components/sessionstore/ContentRestore.jsm
Normal 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() {},
|
||||
};
|
||||
50
browser/components/sessionstore/DocShellCapabilities.jsm
Normal file
50
browser/components/sessionstore/DocShellCapabilities.jsm
Normal 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);
|
||||
},
|
||||
};
|
||||
254
browser/components/sessionstore/FrameTree.jsm
Normal file
254
browser/components/sessionstore/FrameTree.jsm
Normal 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])
|
||||
};
|
||||
84
browser/components/sessionstore/GlobalState.jsm
Normal file
84
browser/components/sessionstore/GlobalState.jsm
Normal 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) || {};
|
||||
}
|
||||
};
|
||||
100
browser/components/sessionstore/PageStyle.jsm
Normal file
100
browser/components/sessionstore/PageStyle.jsm
Normal 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);
|
||||
}
|
||||
};
|
||||
135
browser/components/sessionstore/PrivacyFilter.jsm
Normal file
135
browser/components/sessionstore/PrivacyFilter.jsm
Normal 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.
|
||||
}
|
||||
});
|
||||
64
browser/components/sessionstore/PrivacyLevel.jsm
Normal file
64
browser/components/sessionstore/PrivacyLevel.jsm
Normal 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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
96
browser/components/sessionstore/RunState.jsm
Normal file
96
browser/components/sessionstore/RunState.jsm
Normal 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
476
browser/components/sessionstore/SessionCookies.jsm
Normal file
476
browser/components/sessionstore/SessionCookies.jsm
Normal 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);
|
||||
}
|
||||
};
|
||||
399
browser/components/sessionstore/SessionFile.jsm
Normal file
399
browser/components/sessionstore/SessionFile.jsm
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
428
browser/components/sessionstore/SessionHistory.jsm
Normal file
428
browser/components/sessionstore/SessionHistory.jsm
Normal 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;
|
||||
},
|
||||
|
||||
};
|
||||
100
browser/components/sessionstore/SessionMigration.jsm
Normal file
100
browser/components/sessionstore/SessionMigration.jsm
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
264
browser/components/sessionstore/SessionSaver.jsm
Normal file
264
browser/components/sessionstore/SessionSaver.jsm
Normal 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);
|
||||
},
|
||||
};
|
||||
173
browser/components/sessionstore/SessionStorage.jsm
Normal file
173
browser/components/sessionstore/SessionStorage.jsm
Normal 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;
|
||||
}
|
||||
};
|
||||
4719
browser/components/sessionstore/SessionStore.jsm
Normal file
4719
browser/components/sessionstore/SessionStore.jsm
Normal file
File diff suppressed because it is too large
Load diff
381
browser/components/sessionstore/SessionWorker.js
Normal file
381
browser/components/sessionstore/SessionWorker.js
Normal 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;
|
||||
}
|
||||
25
browser/components/sessionstore/SessionWorker.jsm
Normal file
25
browser/components/sessionstore/SessionWorker.jsm
Normal 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;
|
||||
|
||||
234
browser/components/sessionstore/StartupPerformance.jsm
Normal file
234
browser/components/sessionstore/StartupPerformance.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
74
browser/components/sessionstore/TabAttributes.jsm
Normal file
74
browser/components/sessionstore/TabAttributes.jsm
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
196
browser/components/sessionstore/TabState.jsm
Normal file
196
browser/components/sessionstore/TabState.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
163
browser/components/sessionstore/TabStateCache.jsm
Normal file
163
browser/components/sessionstore/TabStateCache.jsm
Normal 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);
|
||||
}
|
||||
};
|
||||
184
browser/components/sessionstore/TabStateFlusher.jsm
Normal file
184
browser/components/sessionstore/TabStateFlusher.jsm
Normal 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();
|
||||
}
|
||||
};
|
||||
362
browser/components/sessionstore/content/aboutSessionRestore.js
Normal file
362
browser/components/sessionstore/content/aboutSessionRestore.js
Normal 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 ""; }
|
||||
};
|
||||
|
|
@ -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>
|
||||
897
browser/components/sessionstore/content/content-sessionStore.js
Normal file
897
browser/components/sessionstore/content/content-sessionStore.js
Normal 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.
|
||||
});
|
||||
8
browser/components/sessionstore/jar.mn
Normal file
8
browser/components/sessionstore/jar.mn
Normal 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)
|
||||
52
browser/components/sessionstore/moz.build
Normal file
52
browser/components/sessionstore/moz.build
Normal 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')
|
||||
66
browser/components/sessionstore/nsISessionStartup.idl
Normal file
66
browser/components/sessionstore/nsISessionStartup.idl
Normal 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;
|
||||
};
|
||||
220
browser/components/sessionstore/nsISessionStore.idl
Normal file
220
browser/components/sessionstore/nsISessionStore.idl
Normal 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);
|
||||
};
|
||||
353
browser/components/sessionstore/nsSessionStartup.js
Normal file
353
browser/components/sessionstore/nsSessionStartup.js
Normal 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]);
|
||||
39
browser/components/sessionstore/nsSessionStore.js
Normal file
39
browser/components/sessionstore/nsSessionStore.js
Normal 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]);
|
||||
15
browser/components/sessionstore/nsSessionStore.manifest
Normal file
15
browser/components/sessionstore/nsSessionStore.manifest
Normal 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}
|
||||
7
browser/components/sessionstore/test/.eslintrc.js
Normal file
7
browser/components/sessionstore/test/.eslintrc.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc.js"
|
||||
]
|
||||
};
|
||||
242
browser/components/sessionstore/test/browser.ini
Normal file
242
browser/components/sessionstore/test/browser.ini
Normal 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
|
||||
18
browser/components/sessionstore/test/browser_1234021.js
Normal file
18
browser/components/sessionstore/test/browser_1234021.js
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<script>
|
||||
sessionStorage
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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">
|
||||
32
browser/components/sessionstore/test/browser_339445.js
Normal file
32
browser/components/sessionstore/test/browser_339445.js
Normal 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) ]);
|
||||
});
|
||||
|
|
@ -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>
|
||||
44
browser/components/sessionstore/test/browser_345898.js
Normal file
44
browser/components/sessionstore/test/browser_345898.js
Normal 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");
|
||||
}
|
||||
102
browser/components/sessionstore/test/browser_350525.js
Normal file
102
browser/components/sessionstore/test/browser_350525.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
41
browser/components/sessionstore/test/browser_367052.js
Normal file
41
browser/components/sessionstore/test/browser_367052.js
Normal 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;
|
||||
});
|
||||
}
|
||||
71
browser/components/sessionstore/test/browser_393716.js
Normal file
71
browser/components/sessionstore/test/browser_393716.js
Normal 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);
|
||||
});
|
||||
92
browser/components/sessionstore/test/browser_394759_basic.js
Normal file
92
browser/components/sessionstore/test/browser_394759_basic.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
130
browser/components/sessionstore/test/browser_394759_purge.js
Normal file
130
browser/components/sessionstore/test/browser_394759_purge.js
Normal 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();
|
||||
});
|
||||
}
|
||||
59
browser/components/sessionstore/test/browser_423132.js
Normal file
59
browser/components/sessionstore/test/browser_423132.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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>
|
||||
65
browser/components/sessionstore/test/browser_447951.js
Normal file
65
browser/components/sessionstore/test/browser_447951.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Testcase for bug 447951</title>
|
||||
|
||||
<a href="#end">click me</a>
|
||||
47
browser/components/sessionstore/test/browser_454908.js
Normal file
47
browser/components/sessionstore/test/browser_454908.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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>
|
||||
49
browser/components/sessionstore/test/browser_456342.js
Normal file
49
browser/components/sessionstore/test/browser_456342.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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 <input>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>
|
||||
62
browser/components/sessionstore/test/browser_459906.js
Normal file
62
browser/components/sessionstore/test/browser_459906.js
Normal 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);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<title>Cross Domain File for bug 459906</title>
|
||||
|
||||
cheers from localhost
|
||||
|
|
@ -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>
|
||||
85
browser/components/sessionstore/test/browser_461634.js
Normal file
85
browser/components/sessionstore/test/browser_461634.js
Normal 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);
|
||||
});
|
||||
}
|
||||
39
browser/components/sessionstore/test/browser_461743.js
Normal file
39
browser/components/sessionstore/test/browser_461743.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
40
browser/components/sessionstore/test/browser_463205.js
Normal file
40
browser/components/sessionstore/test/browser_463205.js
Normal 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"}}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>bug 463205</title>
|
||||
|
||||
<body>
|
||||
<input type="text" id="text" />
|
||||
</body>
|
||||
53
browser/components/sessionstore/test/browser_463206.js
Normal file
53
browser/components/sessionstore/test/browser_463206.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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">
|
||||
85
browser/components/sessionstore/test/browser_464199.js
Normal file
85
browser/components/sessionstore/test/browser_464199.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
54
browser/components/sessionstore/test/browser_464620_a.html
Normal file
54
browser/components/sessionstore/test/browser_464620_a.html
Normal 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>
|
||||
48
browser/components/sessionstore/test/browser_464620_a.js
Normal file
48
browser/components/sessionstore/test/browser_464620_a.js
Normal 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);
|
||||
}
|
||||
58
browser/components/sessionstore/test/browser_464620_b.html
Normal file
58
browser/components/sessionstore/test/browser_464620_b.html
Normal 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>
|
||||
48
browser/components/sessionstore/test/browser_464620_b.js
Normal file
48
browser/components/sessionstore/test/browser_464620_b.js
Normal 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);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<title>Cross Document File for bug 464620</title>
|
||||
|
||||
<body onload="document.designMode='on';" bgcolor="red">
|
||||
This document is editable.
|
||||
</body>
|
||||
28
browser/components/sessionstore/test/browser_465215.js
Normal file
28
browser/components/sessionstore/test/browser_465215.js
Normal 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);
|
||||
});
|
||||
45
browser/components/sessionstore/test/browser_465223.js
Normal file
45
browser/components/sessionstore/test/browser_465223.js
Normal 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);
|
||||
});
|
||||
}
|
||||
42
browser/components/sessionstore/test/browser_466937.js
Normal file
42
browser/components/sessionstore/test/browser_466937.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
60
browser/components/sessionstore/test/browser_477657.js
Normal file
60
browser/components/sessionstore/test/browser_477657.js
Normal 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);
|
||||
});
|
||||
}
|
||||
47
browser/components/sessionstore/test/browser_480893.js
Normal file
47
browser/components/sessionstore/test/browser_480893.js
Normal 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);
|
||||
});
|
||||
37
browser/components/sessionstore/test/browser_485482.js
Normal file
37
browser/components/sessionstore/test/browser_485482.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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>
|
||||
26
browser/components/sessionstore/test/browser_485563.js
Normal file
26
browser/components/sessionstore/test/browser_485563.js
Normal 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();
|
||||
});
|
||||
}
|
||||
65
browser/components/sessionstore/test/browser_490040.js
Normal file
65
browser/components/sessionstore/test/browser_490040.js
Normal 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");
|
||||
}
|
||||
});
|
||||
42
browser/components/sessionstore/test/browser_491168.js
Normal file
42
browser/components/sessionstore/test/browser_491168.js
Normal 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();
|
||||
});
|
||||
120
browser/components/sessionstore/test/browser_491577.js
Normal file
120
browser/components/sessionstore/test/browser_491577.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
46
browser/components/sessionstore/test/browser_495495.js
Normal file
46
browser/components/sessionstore/test/browser_495495.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
120
browser/components/sessionstore/test/browser_500328.js
Normal file
120
browser/components/sessionstore/test/browser_500328.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
73
browser/components/sessionstore/test/browser_506482.js
Normal file
73
browser/components/sessionstore/test/browser_506482.js
Normal 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()
|
||||
});
|
||||
}
|
||||
38
browser/components/sessionstore/test/browser_514751.js
Normal file
38
browser/components/sessionstore/test/browser_514751.js
Normal 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);
|
||||
}
|
||||
|
||||
21
browser/components/sessionstore/test/browser_522375.js
Normal file
21
browser/components/sessionstore/test/browser_522375.js
Normal 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));
|
||||
}
|
||||
269
browser/components/sessionstore/test/browser_522545.js
Normal file
269
browser/components/sessionstore/test/browser_522545.js
Normal 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();
|
||||
}
|
||||
42
browser/components/sessionstore/test/browser_524745.js
Normal file
42
browser/components/sessionstore/test/browser_524745.js
Normal 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);
|
||||
});
|
||||
}
|
||||
72
browser/components/sessionstore/test/browser_526613.js
Normal file
72
browser/components/sessionstore/test/browser_526613.js
Normal 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));
|
||||
}
|
||||
21
browser/components/sessionstore/test/browser_528776.js
Normal file
21
browser/components/sessionstore/test/browser_528776.js
Normal 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);
|
||||
});
|
||||
30
browser/components/sessionstore/test/browser_579868.js
Normal file
30
browser/components/sessionstore/test/browser_579868.js
Normal 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();
|
||||
});
|
||||
}
|
||||
20
browser/components/sessionstore/test/browser_579879.js
Normal file
20
browser/components/sessionstore/test/browser_579879.js
Normal 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);
|
||||
});
|
||||
81
browser/components/sessionstore/test/browser_580512.js
Normal file
81
browser/components/sessionstore/test/browser_580512.js
Normal 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);
|
||||
}
|
||||
19
browser/components/sessionstore/test/browser_581937.js
Normal file
19
browser/components/sessionstore/test/browser_581937.js
Normal 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
Loading…
Add table
Add a link
Reference in a new issue