Bug 588482 - Restore Session on-demand at startup [r=dao, r=dietrich, a=blocking-b6+]

This commit is contained in:
Paul O’Shannessy 2010-09-10 10:57:28 -07:00
parent 824ce1b531
commit f83d4e45c1
10 changed files with 429 additions and 81 deletions

View File

@ -780,6 +780,8 @@ pref("browser.sessionstore.postdata", 0);
// on which sites to save text data, POSTDATA and cookies
// 0 = everywhere, 1 = unencrypted sites, 2 = nowhere
pref("browser.sessionstore.privacy_level", 1);
// the same as browser.sessionstore.privacy_level, but for saving deferred session data
pref("browser.sessionstore.privacy_level_deferred", 2);
// how many tabs can be reopened (per window)
pref("browser.sessionstore.max_tabs_undo", 10);
// how many windows can be reopened (per session) - on non-OS X platforms this

View File

@ -397,6 +397,11 @@
oncommand="BrowserOpenSyncTabs();"
disabled="true"/>
#endif
<menuitem id="historyRestoreLastSession"
class="restoreLastSession"
label="&historyRestoreLastSession.label;"
oncommand="restoreLastSession();"
disabled="true"/>
<menu id="historyUndoMenu"
class="recentlyClosedTabsMenu"
label="&historyUndoMenu.label;"

View File

@ -758,6 +758,15 @@ HistoryMenu.prototype = {
#endif
},
toggleRestoreLastSession: function PHM_toggleRestoreLastSession() {
let restoreItem = this._rootElt.getElementsByClassName("restoreLastSession")[0];
if (this._ss.canRestoreLastSession)
restoreItem.removeAttribute("disabled");
else
restoreItem.setAttribute("disabled", true);
},
_onPopupShowing: function HM__onPopupShowing(aEvent) {
PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
@ -768,6 +777,7 @@ HistoryMenu.prototype = {
this.toggleRecentlyClosedTabs();
this.toggleRecentlyClosedWindows();
this.toggleTabsFromOtherComputers();
this.toggleRestoreLastSession();
},
_onCommand: function HM__onCommand(aEvent) {

View File

@ -7989,6 +7989,12 @@ function switchToTabHavingURI(aURI, aOpenNew, aCallback) {
return false;
}
function restoreLastSession() {
let ss = Cc["@mozilla.org/browser/sessionstore;1"].
getService(Ci.nsISessionStore);
ss.restoreLastSession();
}
var TabContextMenu = {
contextTab: null,
updateContextMenu: function updateContextMenu(aPopupMenu) {

View File

@ -667,6 +667,11 @@
key="key_sanitize"
command="Tools:Sanitize"/>
<menuseparator class="hide-if-empty-places-result"/>
<menuitem id="appmenu_restoreLastSession"
class="restoreLastSession"
label="&historyRestoreLastSession.label;"
oncommand="restoreLastSession();"
disabled="true"/>
<menu id="appmenu_recentlyClosedTabsMenu"
class="recentlyClosedTabsMenu"
label="&historyUndoMenu.label;"

View File

@ -43,7 +43,7 @@
* - and allows to restore everything into one window.
*/
[scriptable, uuid(c0b185e7-0d21-46ac-8eee-7b5065ee7ecd)]
[scriptable, uuid(e7bb7828-0e32-4995-a848-4aa35df603c7)]
interface nsISessionStartup: nsISupports
{
// Get session state as string
@ -55,13 +55,18 @@ interface nsISessionStartup: nsISupports
boolean doRestore();
/**
* What type of session we're restoring. If we have a session, we're
* either restoring state from a crash or restoring state that the user
* requested we save on shutdown.
* 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;
};

View File

@ -59,7 +59,7 @@ interface nsIDOMNode;
* |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|.
*/
[scriptable, uuid(70592a0d-87d3-459c-8db7-dcb8d47af78e)]
[scriptable, uuid(59bfaf00-e3d8-4728-b4f0-cc0b9dfb4806)]
interface nsISessionStore : nsISupports
{
/**
@ -67,6 +67,24 @@ interface nsISessionStore : nsISupports
*/
void init(in nsIDOMWindow aWindow);
/**
* 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.

View File

@ -152,10 +152,12 @@ SessionStartup.prototype = {
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
else if (!lastSessionCrashed && doResumeSession)
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
else if (initialState)
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
else
this._iniString = null; // reset the state string
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) {
if (this.doRestore()) {
// wait for the first browser window to open
// Don't reset the initial window's default args (i.e. the home page(s))
@ -252,7 +254,8 @@ SessionStartup.prototype = {
* @returns bool
*/
doRestore: function sss_doRestore() {
return this._sessionType != Ci.nsISessionStartup.NO_SESSION;
return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
},
/**

View File

@ -201,6 +201,23 @@ SessionStoreService.prototype = {
// whether the last window was closed and should be restored
_restoreLastWindow: false,
// The state from the previous session (after restoring pinned tabs)
_lastSessionState: null,
/* ........ Public Getters .............. */
get canRestoreLastSession() {
// Always disallow restoring the previous session when in private browsing
return this._lastSessionState && !this._inPrivateBrowsing;
},
set canRestoreLastSession(val) {
// Cheat a bit; only allow false.
if (val)
return;
this._lastSessionState = null;
},
/* ........ Global Event Handlers .............. */
/**
@ -250,40 +267,54 @@ SessionStoreService.prototype = {
// get string containing session state
var iniString;
var ss = Cc["@mozilla.org/browser/sessionstartup;1"].
getService(Ci.nsISessionStartup);
try {
var ss = Cc["@mozilla.org/browser/sessionstartup;1"].
getService(Ci.nsISessionStartup);
if (ss.doRestore())
if (ss.doRestore() ||
ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
iniString = ss.state;
}
catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok
if (iniString) {
try {
// parse the session state into JS objects
this._initialState = JSON.parse(iniString);
let lastSessionCrashed =
this._initialState.session && this._initialState.session.state &&
this._initialState.session.state == STATE_RUNNING_STR;
if (lastSessionCrashed) {
this._recentCrashes = (this._initialState.session &&
this._initialState.session.recentCrashes || 0) + 1;
if (this._needsRestorePage(this._initialState, this._recentCrashes)) {
// replace the crashed session with a restore-page-only session
let pageData = {
url: "about:sessionrestore",
formdata: { "#sessionData": iniString }
};
this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] };
}
// If we're doing a DEFERRED session, then we want to pull pinned tabs
// out so they can be restored.
if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
let [iniState, remainingState] = this._prepDataForDeferredRestore(iniString);
// If we have a iniState with windows, that means that we have windows
// with app tabs to restore.
if (iniState.windows.length)
this._initialState = iniState;
if (remainingState.windows.length)
this._lastSessionState = remainingState;
}
else {
// parse the session state into JS objects
this._initialState = JSON.parse(iniString);
let lastSessionCrashed =
this._initialState.session && this._initialState.session.state &&
this._initialState.session.state == STATE_RUNNING_STR;
if (lastSessionCrashed) {
this._recentCrashes = (this._initialState.session &&
this._initialState.session.recentCrashes || 0) + 1;
if (this._needsRestorePage(this._initialState, this._recentCrashes)) {
// replace the crashed session with a restore-page-only session
let pageData = {
url: "about:sessionrestore",
formdata: { "#sessionData": iniString }
};
this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] };
}
}
// make sure that at least the first window doesn't have anything hidden
delete this._initialState.windows[0].hidden;
// Since nothing is hidden in the first window, it cannot be a popup
delete this._initialState.windows[0].isPopup;
}
// make sure that at least the first window doesn't have anything hidden
delete this._initialState.windows[0].hidden;
// Since nothing is hidden in the first window, it cannot be a popup
delete this._initialState.windows[0].isPopup;
}
catch (ex) { debug("The session file is invalid: " + ex); }
}
@ -317,11 +348,6 @@ SessionStoreService.prototype = {
// save all data for session resuming
this.saveState(true);
if (!this._doResumeSession()) {
// discard all session related data
this._clearDisk();
}
// Make sure to break our cycle with the save timer
if (this._saveTimer) {
this._saveTimer.cancel();
@ -1134,6 +1160,78 @@ SessionStoreService.prototype = {
this.saveStateDelayed();
},
/**
* Restores the session state stored in _lastSessionState. This will attempt
* to merge data into the current session. If a window was opened at startup
* with pinned tab(s), then the remaining data from the previous session for
* that window will be opened into that winddow. Otherwise new windows will
* be opened.
*/
restoreLastSession: function sss_restoreLastSession() {
// Use the public getter since it also checks PB mode
if (!this.canRestoreLastSession)
throw (Components.returnCode = Cr.NS_ERROR_FAILURE);
// First collect each window with its id...
let windows = {};
this._forEachBrowserWindow(function(aWindow) {
if (aWindow.__SS_lastSessionWindowID)
windows[aWindow.__SS_lastSessionWindowID] = aWindow;
});
let lastSessionState = this._lastSessionState;
// This shouldn't ever be the case...
if (!lastSessionState.windows.length)
throw (Components.returnCode = Cr.NS_ERROR_UNEXPECTED);
// We're technically doing a restore, so set things up so we send the
// notification when we're done. We want to send "sessionstore-browser-state-restored".
this._restoreCount = lastSessionState.windows.length;
this._browserSetState = true;
// Restore into windows or open new ones as needed.
for (let i = 0; i < lastSessionState.windows.length; i++) {
let winState = lastSessionState.windows[i];
let lastSessionWindowID = winState.__lastSessionWindowID;
// delete lastSessionWindowID so we don't add that to the window again
delete winState.__lastSessionWindowID;
// Look to see if this window is already open...
if (windows[lastSessionWindowID]) {
// Since we're not overwriting existing tabs, we want to merge _closedTabs,
// putting existing ones first. Then make sure we're respecting the max pref.
if (winState._closedTabs && winState._closedTabs.length) {
let curWinState = this._windows[windows[lastSessionWindowID].__SSi];
curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
curWinState._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"));
}
// Restore into that window - pretend it's a followup since we'll already
// have a focused window.
//XXXzpao This is going to merge extData together (taking what was in
// winState over what is in the window already), so this is going
// to have an effect on Tab Candy.
// Bug 588217 should make this go away by merging the group data.
this.restoreWindow(windows[lastSessionWindowID], { windows: [winState] },
false, true);
}
else {
this._openWindowWithState({ windows: [winState] });
}
}
// Merge closed windows from this session with ones from last session
if (lastSessionState._closedWindows) {
this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
this._capClosedWindows();
}
// Set recent crashes
this._recentCrashes = lastSessionState.session &&
lastSessionState.session.recentCrashes || 0;
this._lastSessionState = null;
},
/* ........ Saving Functionality .............. */
/**
@ -1191,9 +1289,11 @@ SessionStoreService.prototype = {
tabData.index = history.index + 1;
}
else if (history && history.count > 0) {
for (var j = 0; j < history.count; j++)
tabData.entries.push(this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
aFullData));
for (var j = 0; j < history.count; j++) {
let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
aFullData, aTab.pinned);
tabData.entries.push(entry);
}
tabData.index = history.index + 1;
// make sure not to cache privacy sensitive data which shouldn't get out
@ -1242,7 +1342,8 @@ SessionStoreService.prototype = {
delete tabData.extData;
if (history && browser.docShell instanceof Ci.nsIDocShell)
this._serializeSessionStorage(tabData, history, browser.docShell, aFullData);
this._serializeSessionStorage(tabData, history, browser.docShell, aFullData,
aTab.pinned);
return tabData;
},
@ -1254,9 +1355,12 @@ SessionStoreService.prototype = {
* nsISHEntry instance
* @param aFullData
* always return privacy sensitive data (use with care)
* @param aIsPinned
* the tab is pinned and should be treated differently for privacy
* @returns object
*/
_serializeHistoryEntry: function sss_serializeHistoryEntry(aEntry, aFullData) {
_serializeHistoryEntry:
function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned) {
var entry = { url: aEntry.URI.spec };
if (aEntry.title && aEntry.title != entry.url) {
@ -1291,8 +1395,8 @@ SessionStoreService.prototype = {
try {
var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata");
if (aEntry.postData && (aFullData ||
prefPostdata && this._checkPrivacyLevel(aEntry.URI.schemeIs("https")))) {
if (aEntry.postData && (aFullData || prefPostdata &&
this._checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) {
aEntry.postData.QueryInterface(Ci.nsISeekableStream).
seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
var stream = Cc["@mozilla.org/binaryinputstream;1"].
@ -1355,7 +1459,8 @@ SessionStoreService.prototype = {
for (var i = 0; i < aEntry.childCount; i++) {
var child = aEntry.GetChildAt(i);
if (child) {
entry.children.push(this._serializeHistoryEntry(child, aFullData));
entry.children.push(this._serializeHistoryEntry(child, aFullData,
aIsPinned));
}
else { // to maintain the correct frame order, insert a dummy entry
entry.children.push({ url: "about:blank" });
@ -1381,9 +1486,11 @@ SessionStoreService.prototype = {
* That tab's docshell (containing the sessionStorage)
* @param aFullData
* always return privacy sensitive data (use with care)
* @param aIsPinned
* the tab is pinned and should be treated differently for privacy
*/
_serializeSessionStorage:
function sss_serializeSessionStorage(aTabData, aHistory, aDocShell, aFullData) {
function sss_serializeSessionStorage(aTabData, aHistory, aDocShell, aFullData, aIsPinned) {
let storageData = {};
let hasContent = false;
@ -1396,7 +1503,8 @@ SessionStoreService.prototype = {
domain = uri.prePath;
}
catch (ex) { /* this throws for host-less URIs (such as about: or jar:) */ }
if (storageData[domain] || !(aFullData || this._checkPrivacyLevel(uri.schemeIs("https"))))
if (storageData[domain] ||
!(aFullData || this._checkPrivacyLevel(uri.schemeIs("https"), aIsPinned)))
continue;
let storage, storageItemCount = 0;
@ -1479,7 +1587,8 @@ SessionStoreService.prototype = {
this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
aTabData.entries[tabIndex],
!aTabData._formDataSaved, aFullData);
!aTabData._formDataSaved, aFullData,
!!aTabData.pinned);
aTabData._formDataSaved = true;
if (aBrowser.currentURI.spec == "about:config")
aTabData.entries[tabIndex].formdata = {
@ -1500,18 +1609,21 @@ SessionStoreService.prototype = {
* update all form data for this tab
* @param aFullData
* always return privacy sensitive data (use with care)
* @param aIsPinned
* the tab is pinned and should be treated differently for privacy
*/
_updateTextAndScrollDataForFrame:
function sss_updateTextAndScrollDataForFrame(aWindow, aContent, aData,
aUpdateFormData, aFullData) {
aUpdateFormData, aFullData, aIsPinned) {
for (var i = 0; i < aContent.frames.length; i++) {
if (aData.children && aData.children[i])
this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
aData.children[i], aUpdateFormData, aFullData);
aData.children[i], aUpdateFormData,
aFullData, aIsPinned);
}
var isHTTPS = this._getURIFromString((aContent.parent || aContent).
document.location.href).schemeIs("https");
if (aFullData || this._checkPrivacyLevel(isHTTPS) ||
if (aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned) ||
aContent.top.document.location.href == "about:sessionrestore") {
if (aFullData || aUpdateFormData) {
let formData = this._collectFormDataForFrame(aContent.document);
@ -1642,6 +1754,42 @@ SessionStoreService.prototype = {
return data;
},
/**
* extract the base domain from a history entry and its children
* @param aEntry
* the history entry, serialized
* @param aHosts
* the hash that will be used to store hosts eg, { hostname: true }
* @param aCheckPrivacy
* should we check the privacy level for https
* @param aIsPinned
* is the entry we're evaluating for a pinned tab; used only if
* aCheckPrivacy
*/
_extractHostsForCookies:
function sss__extractHostsForCookies(aEntry, aHosts, aCheckPrivacy, aIsPinned) {
let match;
if ((match = /^https?:\/\/(?:[^@\/\s]+@)?([\w.-]+)/.exec(aEntry.url)) != null) {
if (!aHosts[match[1]] &&
(!aCheckPrivacy ||
this._checkPrivacyLevel(this._getURIFromString(aEntry.url).schemeIs("https"),
aIsPinned))) {
// By setting this to true or false, we can determine when looking at
// the host in _updateCookies if we should check for privacy.
aHosts[match[1]] = aIsPinned;
}
}
else if ((match = /^file:\/\/([^\/]*)/.exec(aEntry.url)) != null) {
aHosts[match[1]] = true;
}
if (aEntry.children) {
aEntry.children.forEach(function(entry) {
this._extractHostsForCookies(entry, aHosts, aCheckPrivacy, aIsPinned);
}, this);
}
},
/**
* store all hosts for a URL
* @param aWindow
@ -1649,25 +1797,12 @@ SessionStoreService.prototype = {
*/
_updateCookieHosts: function sss_updateCookieHosts(aWindow) {
var hosts = this._windows[aWindow.__SSi]._hosts = {};
// get the domain for each URL
function extractHosts(aEntry) {
var match;
if ((match = /^https?:\/\/(?:[^@\/\s]+@)?([\w.-]+)/.exec(aEntry.url)) != null) {
if (!hosts[match[1]] && _this._checkPrivacyLevel(_this._getURIFromString(aEntry.url).schemeIs("https"))) {
hosts[match[1]] = true;
}
}
else if ((match = /^file:\/\/([^\/]*)/.exec(aEntry.url)) != null) {
hosts[match[1]] = true;
}
if (aEntry.children) {
aEntry.children.forEach(extractHosts);
}
}
var _this = this;
this._windows[aWindow.__SSi].tabs.forEach(function(aTabData) { aTabData.entries.forEach(extractHosts); });
this._windows[aWindow.__SSi].tabs.forEach(function(aTabData) {
aTabData.entries.forEach(function(entry) {
this._extractHostsForCookies(entry, hosts, true, !!aTabData.pinned);
}, this);
}, this);
},
/**
@ -1695,11 +1830,16 @@ SessionStoreService.prototype = {
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
var MAX_EXPIRY = Math.pow(2, 62);
aWindows.forEach(function(aWindow) {
for (var host in aWindow._hosts) {
if (!aWindow._hosts)
return;
for (var [host, isPinned] in Iterator(aWindow._hosts)) {
var list = CookieSvc.getCookiesFromHost(host);
while (list.hasMoreElements()) {
var cookie = list.getNext().QueryInterface(Ci.nsICookie2);
if (cookie.isSession && _this._checkPrivacyLevel(cookie.isSecure)) {
// aWindow._hosts will only have hosts with the right privacy rules,
// so there is no need to do anything special with this call to
// _checkPrivacyLevel.
if (cookie.isSession && _this._checkPrivacyLevel(cookie.isSecure, isPinned)) {
// use the cookie's host, path, and name as keys into a hash,
// to make sure we serialize each cookie only once
if (!(cookie.host in jscookies &&
@ -1870,7 +2010,13 @@ SessionStoreService.prototype = {
this._updateTextAndScrollData(aWindow);
this._updateCookieHosts(aWindow);
this._updateWindowFeatures(aWindow);
// Make sure we keep __SS_lastSessionWindowID around for cases like entering
// or leaving PB mode.
if (aWindow.__SS_lastSessionWindowID)
this._windows[aWindow.__SSi].__lastSessionWindowID =
aWindow.__SS_lastSessionWindowID;
this._dirtyWindows[aWindow.__SSi] = false;
},
@ -1968,6 +2114,13 @@ SessionStoreService.prototype = {
tabs[t].hidden = winData.tabs[t].hidden;
}
// We want to correlate the window with data from the last session, so
// assign another id if we have one. Otherwise clear so we don't do
// anything with it.
delete aWindow.__SS_lastSessionWindowID;
if (winData.__lastSessionWindowID)
aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
// when overwriting tabs, remove all superflous ones
if (aOverwriteTabs && newTabCount < openTabCount) {
Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
@ -2618,11 +2771,9 @@ SessionStoreService.prototype = {
if (this._inPrivateBrowsing)
return;
var pinnedOnly = false;
if (this._loadState == STATE_QUITTING && !this._doResumeSession() ||
/* if crash recovery is disabled, only save session resuming information */
this._loadState == STATE_RUNNING && !this._resume_from_crash)
pinnedOnly = true;
// If crash recovery is disabled, we only want to resume with pinned tabs
// if we crash.
let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash;
var oState = this._getCurrentState(aUpdateAll, pinnedOnly);
if (!oState)
@ -2816,10 +2967,18 @@ SessionStoreService.prototype = {
* (distinguishes between encrypted and non-encrypted sites)
* @param aIsHTTPS
* Bool is encrypted
* @param aUseDefaultPref
* don't do normal check for deferred
* @returns bool
*/
_checkPrivacyLevel: function sss_checkPrivacyLevel(aIsHTTPS) {
return this._prefBranch.getIntPref("sessionstore.privacy_level") < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
_checkPrivacyLevel: function sss_checkPrivacyLevel(aIsHTTPS, aUseDefaultPref) {
let pref = "sessionstore.privacy_level";
// If we're in the process of quitting and we're not autoresuming the session
// then we should treat it as a deferred session. We have a different privacy
// pref for that case.
if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession())
pref = "sessionstore.privacy_level_deferred";
return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
},
/**
@ -2931,6 +3090,133 @@ SessionStoreService.prototype = {
sessionAge && sessionAge >= SIX_HOURS_IN_MS);
},
/**
* This is going to take a state as provided at startup (via
* nsISessionStartup.state) and split it into 2 parts. The first part
* (defaultState) will be a state that should still be restored at startup,
* while the second part (state) is a state that should be saved for later.
* defaultState will be comprised of windows with only pinned tabs, extracted
* from state. It will contain the cookies that go along with the history
* entries in those tabs. It will also contain window position information.
*
* defaultState will be restored at startup. state will be placed into
* this._lastSessionState and will be kept in case the user explicitly wants
* to restore the previous session (publicly exposed as restoreLastSession).
*
* @param stateString
* The state string, presumably from nsISessionStartup.state
* @returns [defaultState, state]
*/
_prepDataForDeferredRestore: function sss__prepDataForDeferredRestore(stateString) {
let state = JSON.parse(stateString);
let defaultState = { windows: [], selectedWindow: 1 };
state.selectedWindow = state.selectedWindow || 1;
// Look at each window, remove pinned tabs, adjust selectedindex,
// remove window if necessary.
for (let wIndex = 0; wIndex < state.windows.length;) {
let window = state.windows[wIndex];
window.selected = window.selected || 1;
// We're going to put the state of the window into this object
let pinnedWindowState = { tabs: [], cookies: []};
for (let tIndex = 0; tIndex < window.tabs.length;) {
if (window.tabs[tIndex].pinned) {
// Adjust window.selected
if (tIndex + 1 < window.selected)
window.selected -= 1;
else if (tIndex + 1 == window.selected)
pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
// + 2 because the tab isn't actually in the array yet
// Now add the pinned tab to our window
pinnedWindowState.tabs =
pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1));
// We don't want to increment tIndex here.
continue;
}
tIndex++;
}
// At this point the window in the state object has been modified (or not)
// We want to build the rest of this new window object if we have pinnedTabs.
if (pinnedWindowState.tabs.length) {
// First get the other attributes off the window
WINDOW_ATTRIBUTES.forEach(function(attr) {
if (attr in window) {
pinnedWindowState[attr] = window[attr];
delete window[attr];
}
});
// We're just copying position data into the pinned window.
// Not copying over:
// - _closedTabs
// - extData
// - isPopup
// - hidden
// Assign a unique ID to correlate the window to be opened with the
// remaining data
window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID
= "" + Date.now() + Math.random();
// Extract the cookies that belong with each pinned tab
this._splitCookiesFromWindow(window, pinnedWindowState);
// Actually add this window to our defaultState
defaultState.windows.push(pinnedWindowState);
// Remove the window from the state if it doesn't have any tabs
if (!window.tabs.length) {
if (wIndex + 1 <= state.selectedWindow)
state.selectedWindow -= 1;
else if (wIndex + 1 == state.selectedWindow)
defaultState.selectedIndex = defaultState.windows.length + 1;
state.windows.splice(wIndex, 1);
// We don't want to increment wIndex here.
continue;
}
}
wIndex++;
}
return [defaultState, state];
},
/**
* Splits out the cookies from aWinState into aTargetWinState based on the
* tabs that are in aTargetWinState.
* This alters the state of aWinState and aTargetWinState.
*/
_splitCookiesFromWindow:
function sss__splitCookiesFromWindow(aWinState, aTargetWinState) {
if (!aWinState.cookies || !aWinState.cookies.length)
return;
// Get the hosts for history entries in aTargetWinState
let cookieHosts = {};
aTargetWinState.tabs.forEach(function(tab) {
tab.entries.forEach(function(entry) {
this._extractHostsForCookies(entry, cookieHosts, false)
}, this);
}, this);
// By creating a regex we reduce overhead and there is only one loop pass
// through either array (cookieHosts and aWinState.cookies).
let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g");
let cookieRegex = new RegExp(".*(" + hosts + ")");
for (let cIndex = 0; cIndex < aWinState.cookies.length;) {
if (cookieRegex.test(aWinState.cookies[cIndex].host)) {
aTargetWinState.cookies =
aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1));
continue;
}
cIndex++;
}
},
/**
* Converts a JavaScript object into a JSON string
* (see http://www.json.org/ for more information).
@ -2941,9 +3227,16 @@ SessionStoreService.prototype = {
* @returns the object's JSON representation
*/
_toJSONString: function sss_toJSONString(aJSObject) {
// We never want to save __lastSessionWindowID across sessions, but we do
// want it exported to consumers when running (eg. Private Browsing).
let internalKeys = INTERNAL_KEYS;
if (this._loadState == STATE_QUITTING) {
internalKeys = internalKeys.slice();
internalKeys.push("__lastSessionWindowID");
}
function exclude(key, value) {
// returning undefined results in the exclusion of that key
return INTERNAL_KEYS.indexOf(key) == -1 ? value : undefined;
return internalKeys.indexOf(key) == -1 ? value : undefined;
}
return JSON.stringify(aJSObject, exclude);
},

View File

@ -265,6 +265,7 @@ can reach it easily. -->
<!ENTITY historyUndoMenu.label "Recently Closed Tabs">
<!-- LOCALIZATION NOTE (historyUndoWindowMenu): see bug 394759 -->
<!ENTITY historyUndoWindowMenu.label "Recently Closed Windows">
<!ENTITY historyRestoreLastSession.label "Restore Previous Session">
<!ENTITY historyHomeCmd.label "Home">
<!ENTITY showAllHistoryCmd2.label "Show All History">