Bug 614220 - Port Deferred Session restore - Bug 588482. r+=Neil
authorMisak Khachatryan <misak.bugzilla@gmail.com>
Thu, 09 Dec 2010 20:38:33 +0400
changeset 6807 73479070b3a1
parent 6806 f0c903a93fc6
child 6808 61c697891f6c
push id5220
push usermisak.bugzilla@gmail.com
push dateThu, 09 Dec 2010 16:47:55 +0000
treeherdercomm-central@73479070b3a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs614220, 588482
Bug 614220 - Port Deferred Session restore - Bug 588482. r+=Neil
suite/browser/browser-prefs.js
suite/browser/navigator.js
suite/browser/navigatorOverlay.xul
suite/common/public/nsISessionStartup.idl
suite/common/public/nsISessionStore.idl
suite/common/src/nsSessionStartup.js
suite/common/src/nsSessionStore.js
suite/locales/en-US/chrome/browser/navigator.dtd
--- a/suite/browser/browser-prefs.js
+++ b/suite/browser/browser-prefs.js
@@ -393,16 +393,18 @@ pref("browser.sessionstore.resume_sessio
 // minimal interval between two save operations in milliseconds
 pref("browser.sessionstore.interval", 15000);
 // maximum amount of POSTDATA to be saved in bytes per history entry (-1 = all of it)
 // (NB: POSTDATA will be saved either entirely or not at all)
 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", 0);
+// the same as browser.sessionstore.privacy_level, but for saving deferred session data
+pref("browser.sessionstore.privacy_level_deferred", 2);
 // number of crashes that can occur before the about:sessionrestore page is displayed
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("browser.sessionstore.max_resumed_crashes", 1);
 // 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
 // pref may be ignored when dealing with pop-up windows to ensure proper startup
 pref("browser.sessionstore.max_windows_undo", 3);
--- a/suite/browser/navigator.js
+++ b/suite/browser/navigator.js
@@ -729,16 +729,22 @@ function UpdateNavBar()
 }
 
 function InitSessionStoreCallback()
 {
   try {
     var ss = Components.classes["@mozilla.org/suite/sessionstore;1"]
                        .getService(Components.interfaces.nsISessionStore);
     ss.init(window);
+
+    //Check if we have "Deferred Session Restore"
+    let restoreItem = document.getElementById("historyRestoreLastSession");
+
+    if (ss.canRestoreLastSession)
+      restoreItem.removeAttribute("disabled");
   } catch(ex) {
     dump("nsSessionStore could not be initialized: " + ex + "\n");
   }
 }
 
 function WindowFocusTimerCallback(element)
 {
   // This function is a redo of the fix for jag bug 91884.
@@ -1430,16 +1436,22 @@ function updateRecentWindows(menupopup)
 function undoCloseWindow(aIndex)
 {
   var ss = Components.classes["@mozilla.org/suite/sessionstore;1"]
                      .getService(Components.interfaces.nsISessionStore);
 
   return ss.undoCloseWindow(aIndex);
 }
 
+function restoreLastSession() {
+  let ss = Components.classes["@mozilla.org/suite/sessionstore;1"]
+                     .getService(Components.interfaces.nsISessionStore);
+  ss.restoreLastSession();
+}
+
 /*
  * Determines if a tab is "empty" using isBrowserEmpty from utilityOverlay.js
  */
 function isTabEmpty(aTab)
 {
   return isBrowserEmpty(aTab.linkedBrowser);
 }
 
--- a/suite/browser/navigatorOverlay.xul
+++ b/suite/browser/navigatorOverlay.xul
@@ -412,16 +412,21 @@
         <menuseparator/>
         <menuitem id="menu_showAllHistory"
                   label="&historyCmd.label;"
                   accesskey="&historyCmd.accesskey;"
                   oncommand="toHistory()"
                   key="key_gotoHistory"/>
         <menuseparator id="startHistorySeparator" hidden="true"/>
         <menuseparator id="endHistorySeparator" hidden="true"/>
+        <menuitem id="historyRestoreLastSession"
+                  label="&historyRestoreLastSession.label;"
+                  accesskey="&historyRestoreLastSessionCmd.accesskey;"
+                  oncommand="restoreLastSession();"
+                  disabled="true"/>
       </menupopup>
     </menu>
 
     <menu id="bookmarksMenu"
           label="&bookmarksMenu.label;"
           accesskey="&bookmarksMenu.accesskey;"
           ondragenter="PlacesMenuDNDHandler.onDragEnter(event);"
           ondragover="PlacesMenuDNDHandler.onDragOver(event);"
--- a/suite/common/public/nsISessionStartup.idl
+++ b/suite/common/public/nsISessionStartup.idl
@@ -38,30 +38,35 @@
 #include "nsISupports.idl"
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
  * tab history, cookies, scroll state, form data, POSTDATA and window features
  * - and allows to restore everything into one window.
  */
 
-[scriptable, uuid(34b4eded-1b07-4424-9012-33eea529d6ab)]
+[scriptable, uuid(f9d1763e-e4a6-4446-8fe1-5bc2cd3201c8)]
 interface nsISessionStartup: nsISupports
 {
   // Get session state as string
   readonly attribute AString state;
 
   /**
    * Determine if session should be restored
    */
   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;
 };
--- a/suite/common/public/nsISessionStore.idl
+++ b/suite/common/public/nsISessionStore.idl
@@ -42,25 +42,43 @@ interface nsIDOMWindow;
 interface nsIDOMNode;
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
  * tab history, cookies, scroll state, form data, POSTDATA and window features
  * - and allows to restore everything into one window.
  */
 
-[scriptable, uuid(e63e4931-ff9f-4840-a203-b787223e057d)]
+[scriptable, uuid(a2f14785-c4d4-4bac-b048-a849d2e74513)]
 interface nsISessionStore : nsISupports
 {
   /**
    * Initialize the service
    */
   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.
    * @return 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
--- a/suite/common/src/nsSessionStartup.js
+++ b/suite/common/src/nsSessionStartup.js
@@ -127,20 +127,22 @@ SessionStartup.prototype = {
       initialState && initialState.session && initialState.session.state &&
       initialState.session.state == STATE_RUNNING_STR;
 
     // set the startup type
     if (lastSessionCrashed && resumeFromCrash)
       this._sessionType = Components.interfaces.nsISessionStartup.RECOVER_SESSION;
     else if (!lastSessionCrashed && doResumeSession)
       this._sessionType = Components.interfaces.nsISessionStartup.RESUME_SESSION;
+    else if (initialState)
+      this._sessionType = Components.interfaces.nsISessionStartup.DEFER_SESSION;
     else
       this._iniString = null; // reset the state string
 
-    if (this._sessionType != Components.interfaces.nsISessionStartup.NO_SESSION) {
+    if (this.doRestore()) {
       // wait for the first browser window to open
       Services.obs.addObserver(this, "browser:purge-session-history", true);
     }
   },
 
   /**
    * Handle notifications
    */
@@ -179,17 +181,18 @@ SessionStartup.prototype = {
     return this._iniString;
   },
 
   /**
    * Determine whether there is a pending session restore.
    * @returns bool
    */
   doRestore: function sss_doRestore() {
-    return this._sessionType != Components.interfaces.nsISessionStartup.NO_SESSION;
+    return this._sessionType == Components.interfaces.nsISessionStartup.RECOVER_SESSION ||
+           this._sessionType == Components.interfaces.nsISessionStartup.RESUME_SESSION;
   },
 
   /**
    * Get the type of pending session store, if any.
    */
   get sessionType() {
     return this._sessionType;
   },
--- a/suite/common/src/nsSessionStore.js
+++ b/suite/common/src/nsSessionStore.js
@@ -185,16 +185,32 @@ SessionStoreService.prototype = {
   _statesToRestore: {},
 
   // counts the number of crashes since the last clean start
   _recentCrashes: 0,
 
   // 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;
+  },
+
+  set canRestoreLastSession(val) {
+    // Cheat a bit; only allow false.
+    if (!val)
+      this._lastSessionState = null;
+  },
+
 /* ........ Global Event Handlers .............. */
 
   /**
    * Initialize the component
    */
   init: function sss_init(aWindow) {
     if (!aWindow || this._loadState == STATE_RUNNING) {
       // make sure that all browser windows which try to initialize
@@ -229,54 +245,65 @@ SessionStoreService.prototype = {
     // get file references
     this._sessionFile = Services.dirsvc.get("ProfD", Components.interfaces.nsILocalFile);
     this._sessionFileBackup = this._sessionFile.clone();
     this._sessionFile.append("sessionstore.json");
     this._sessionFileBackup.append("sessionstore.bak");
 
     // get string containing session state
     var iniString;
+    var ss = Components.classes["@mozilla.org/suite/sessionstartup;1"]
+                       .getService(Components.interfaces.nsISessionStartup);
     try {
-      var ss = Components.classes["@mozilla.org/suite/sessionstartup;1"]
-                         .getService(Components.interfaces.nsISessionStartup);
-      if (ss.doRestore())
+      if (ss._sessionType != Components.interfaces.nsISessionStartup.NO_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);
+        // If we're doing a DEFERRED session, then we want to pull pinned tabs
+        // out so they can be restored.
+        if (ss.sessionType == Components.interfaces.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;
+        }
       }
       catch (ex) { debug("The session file is invalid: " + ex); }
-
-      let lastSessionCrashed =
-        this._initialState && this._initialState.session && this._initialState.session.state &&
-        this._initialState.session.state == STATE_RUNNING_STR;
-       // if last session crashed, backup the session
-       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
-      if (this._initialState.windows[0]) {
-        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;
-      }
     }
 
     // remove the session data files if crash recovery is disabled
     if (!this._resume_from_crash)
       this._clearDisk();
     else { // create a backup if the session data file exists
       try {
         if (this._sessionFileBackup.exists())
@@ -297,22 +324,19 @@ SessionStoreService.prototype = {
     this.onLoad(aWindow);
   },
 
   /**
    * Called on application shutdown, after notifications:
    * quit-application-granted, quit-application
    */
   _uninit: function sss_uninit() {
-    if (this._doResumeSession()) { // save all data for session resuming
-      this.saveState(true);
-    }
-    else { // discard all session related data
-      this._clearDisk();
-    }
+    // save all data for session resuming
+    this.saveState(true);
+
     // Make sure to break our cycle with the save timer
     if (this._saveTimer) {
       this._saveTimer.cancel();
       this._saveTimer = null;
     }
   },
 
   /**
@@ -1021,16 +1045,88 @@ SessionStoreService.prototype = {
       if (!state && !aWinState.isPopup) {
         state = aWinState;
       }
     });
     return (this._restoreLastWindow && state &&
             this._doResumeSession());
   },
 
+  /**
+   * 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 = Components.results.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 = Components.results.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 .............. */
 
   /**
    * Store all session data for a window
    * @param aWindow
    *        Window reference
    */
   _saveWindowHistory: function sss_saveWindowHistory(aWindow) {
@@ -1073,19 +1169,21 @@ SessionStoreService.prototype = {
     //           data even when we shouldn't (e.g. Back, different anchor)
     if (history && browser.__SS_data &&
         browser.__SS_data.entries[history.index] &&
         history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
       tabData = browser.__SS_data;
       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, false);
+        tabData.entries.push(entry);
+      }
       tabData.index = history.index + 1;
 
       // make sure not to cache privacy sensitive data which shouldn't get out
       if (!aFullData)
         browser.__SS_data = tabData;
     }
     else if (browser.currentURI.spec != "about:blank" ||
              browser.contentDocument.body.hasChildNodes()) {
@@ -1120,31 +1218,35 @@ SessionStoreService.prototype = {
     }
 
     if (aTab.__SS_extdata)
       tabData.extData = aTab.__SS_extdata;
     else if (tabData.extData)
       delete tabData.extData;
 
     if (history && browser.docShell instanceof Components.interfaces.nsIDocShell)
-      this._serializeSessionStorage(tabData, history, browser.docShell, aFullData);
+      this._serializeSessionStorage(tabData, history, browser.docShell, aFullData,
+                                    false);
 
     return tabData;
   },
 
   /**
    * Get an object that is a serialized representation of a History entry
    * Used for data storage
    * @param aEntry
    *        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) {
       entry.title = aEntry.title;
     }
     if (aEntry.isSubFrame) {
       entry.subframe = true;
     }
@@ -1169,18 +1271,18 @@ SessionStoreService.prototype = {
 
     var x = {}, y = {};
     aEntry.getScrollPosition(x, y);
     if (x.value != 0 || y.value != 0)
       entry.scroll = x.value + "," + y.value;
 
     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(Components.interfaces.nsISeekableStream)
                        .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0);
         var stream = Components.classes["@mozilla.org/binaryinputstream;1"]
                                .createInstance(Components.interfaces.nsIBinaryInputStream);
         stream.setInputStream(aEntry.postData);
         var postBytes = stream.readByteArray(stream.available());
         var postdata = String.fromCharCode.apply(null, postBytes);
         if (aFullData || prefPostdata == -1 ||
@@ -1233,17 +1335,18 @@ SessionStoreService.prototype = {
       return entry;
     }
 
     if (aEntry.childCount > 0) {
       entry.children = [];
       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" });
         }
         // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
         if (/^wyciwyg:\/\//.test(entry.children[i].url)) {
           delete entry.children;
           break;
@@ -1259,32 +1362,35 @@ SessionStoreService.prototype = {
    * @param aTabData
    *        The data object for a specific tab
    * @param aHistory
    *        That tab's session history
    * @param aDocShell
    *        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;
 
     for (let i = 0; i < aHistory.count; i++) {
       let uri = aHistory.getEntryAtIndex(i, false).URI;
       // sessionStorage is saved per origin (cf. nsDocShell::GetSessionStorageForURI)
       let domain = uri.spec;
       try {
         if (uri.host)
           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;
       try {
         var principal = SecMan.getCodebasePrincipal(uri);
 
         // Using getSessionStorageForPrincipal instead of getSessionStorageForURI
         // just to be able to pass aCreate = false, that avoids creation of the
@@ -1358,17 +1464,18 @@ SessionStoreService.prototype = {
                             this._getSelectedPageStyle(aBrowser.contentWindow);
     if (selectedPageStyle)
       aTabData.pageStyle = selectedPageStyle;
     else if (aTabData.pageStyle)
       delete aTabData.pageStyle;
 
     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 = {
         "#textbox": aBrowser.contentDocument.getElementById("textbox").value
       };
   },
 
   /**
@@ -1379,28 +1486,31 @@ SessionStoreService.prototype = {
    * @param aContent
    *        frame reference
    * @param aData
    *        part of a tabData object to add the information to
    * @param aUpdateFormData
    *        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);
         if (formData)
           aData.formdata = formData;
         else if (aData.formdata)
           delete aData.formdata;
       }
@@ -1522,41 +1632,64 @@ SessionStoreService.prototype = {
       }
 
     } while ((node = formNodes.iterateNext()));
 
     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
    *        Window reference
    */
   _updateCookieHosts: function sss_updateCookieHosts(aWindow) {
     var hosts = this._windows[aWindow.__SSi]._hosts = {};
 
-    // get the host for each URL
-    var _this = this;
-    function extractHosts(aEntry) {
-      var match = /^https?:\/\/(?:[^@\/\s]+@)?([\w.-]+)/.exec(aEntry.url);
-      if (match) {
-        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);
-      }
-    }
-
-    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);
   },
 
   /**
    * Serialize cookie data
    * @param aWindows
    *        array of Window references
    */
   _updateCookies: function sss_updateCookies(aWindows) {
@@ -1564,21 +1697,26 @@ SessionStoreService.prototype = {
     for (var i = 0; i < aWindows.length; i++)
       aWindows[i].cookies = [];
 
     var jscookies = {};
     var _this = this;
     // 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 = cm.getCookiesFromHost(host);
         while (list.hasMoreElements()) {
           var cookie = list.getNext().QueryInterface(Components.interfaces.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
 
             // lazily build up a 3-dimensional hash, with
             // host, path, and name as keys
             if (!jscookies[cookie.host])
               jscookies[cookie.host] = {};
             if (!jscookies[cookie.host][cookie.path])
@@ -1732,16 +1870,22 @@ SessionStoreService.prototype = {
       return;
 
     // update the internal state data for this window
     this._saveWindowHistory(aWindow);
     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;
   },
 
 /* ........ Restoring Functionality .............. */
 
   /**
    * restore features to a single window
    * @param aWindow
@@ -1811,16 +1955,23 @@ SessionStoreService.prototype = {
     for (var t = 0; t < newTabCount; t++) {
       tabs.push(t < openTabCount ? tabbrowser.tabs[t] : tabbrowser.addTab());
       // when resuming at startup: add additionally requested pages to the end
       if (!aOverwriteTabs && root._firstTabs) {
         tabbrowser.moveTabTo(tabs[t], t);
       }
     }
 
+    // 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
     for (t = openTabCount - 1; t >= newTabCount; t--) {
       tabbrowser.removeTab(tabbrowser.tabs[t]);
     }
 
     if (aOverwriteTabs) {
       this.restoreWindowFeatures(aWindow, winData);
       delete this._windows[aWindow.__SSi].extData;
@@ -2399,16 +2550,20 @@ SessionStoreService.prototype = {
    * @param aUpdateAll
    *        Bool update all windows
    */
   saveState: function sss_saveState(aUpdateAll) {
     // if crash recovery is disabled, only save session resuming information
     if (!this._resume_from_crash && this._loadState == STATE_RUNNING)
       return;
 
+    // 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);
     oState.session = {
       state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
       lastUpdate: Date.now()
     };
     if (this._recentCrashes)
       oState.session.recentCrashes = this._recentCrashes;
 
@@ -2578,20 +2733,28 @@ SessionStoreService.prototype = {
       aWindow.arguments[0] == "about:blank";
   },
 
   /**
    * don't save sensitive data if the user doesn't want to
    * (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);
   },
 
   /**
    * on popup windows, the XULWindow's attributes seem not to be set correctly
    * we use thus JSDOMWindow attributes for sizemode and normal window attributes
    * (and hope for reasonable values when maximized/minimized - since then
    * outerWidth/outerHeight aren't the dimensions of the restored window)
    * @param aWindow
@@ -2681,25 +2844,159 @@ SessionStoreService.prototype = {
                      (Date.now() - aState.session.lastUpdate);
 
     return max_resumed_crashes != -1 &&
            (aRecentCrashes > max_resumed_crashes ||
             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).
    *
    * The inverse operation consists of JSON.parse(JSON_string).
    *
    * @param aJSObject is the object to be converted
    * @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) ? undefined : value;
     }
     return JSON.stringify(aJSObject, exclude);
   },
 
   _sendRestoreCompletedNotifications: function sss_sendRestoreCompletedNotifications() {
--- a/suite/locales/en-US/chrome/browser/navigator.dtd
+++ b/suite/locales/en-US/chrome/browser/navigator.dtd
@@ -103,16 +103,18 @@
 
 <!ENTITY clearPrivateDataCmd.label "Clear Private Data…">
 <!ENTITY clearPrivateDataCmd.accesskey "e">
 
 <!ENTITY goMenu.label "Go">
 <!ENTITY goMenu.accesskey "g">
 <!ENTITY goHomeCmd.label "Home">
 <!ENTITY goHomeCmd.accesskey "h">
+<!ENTITY historyRestoreLastSession.label "Restore Previous Session">
+<!ENTITY historyRestoreLastSessionCmd.accesskey "R">
 
 <!ENTITY bookmarksMenu.label "Bookmarks">
 <!ENTITY bookmarksMenu.accesskey "B">
 <!ENTITY addCurPageCmd.label "Bookmark This Page">
 <!ENTITY addCurPageCmd.accesskey "B">
 <!ENTITY addCurPageAsCmd.label "File Bookmark…">
 <!ENTITY addCurPageAsCmd.accesskey "F">
 <!ENTITY addCurPageAsCmd.commandkey "d">