Bug 899276 - Don't collect/save private tabs. r=ttaubert
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Wed, 18 Dec 2013 12:39:53 -0500
changeset 161092 7289c4384fc7c34ba594a0149b949ec07898d142
parent 161091 40978aa5238d1de302fc1f5782466f4d47ad19f2
child 161093 e6750e3b3b66c46af5252c1646dbe496dd8219ed
push id25867
push userryanvm@gmail.com
push dateThu, 19 Dec 2013 02:19:33 +0000
treeherdermozilla-central@04a70c8908de [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersttaubert
bugs899276
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 899276 - Don't collect/save private tabs. r=ttaubert
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/src/SessionSaver.jsm
browser/components/sessionstore/src/SessionStore.jsm
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_privatetabs.js
browser/components/sessionstore/test/content.js
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -304,16 +304,40 @@ let SessionStorageListener = {
     MessageQueue.push("storage", () => SessionStorage.collect(docShell));
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
 };
 
 /**
+ * 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.
+ */
+let PrivacyListener = {
+  init: function() {
+    docShell.addWeakPrivacyTransitionObserver(this);
+  },
+
+  // 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.
  */
 let MessageQueue = {
   /**
@@ -461,8 +485,9 @@ let MessageQueue = {
 
 EventListener.init();
 MessageListener.init();
 SyncHandler.init();
 ProgressListener.init();
 PageStyleListener.init();
 SessionStorageListener.init();
 DocShellCapabilitiesListener.init();
+PrivacyListener.init();
--- a/browser/components/sessionstore/src/SessionSaver.jsm
+++ b/browser/components/sessionstore/src/SessionSaver.jsm
@@ -67,17 +67,17 @@ let stopWatchFinish = stopWatch("finish"
 /**
  * The external API implemented by the SessionSaver module.
  */
 this.SessionSaver = Object.freeze({
   /**
    * Immediately saves the current session to disk.
    */
   run: function () {
-    SessionSaverInternal.run();
+    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 () {
@@ -124,17 +124,17 @@ let SessionSaverInternal = {
    * the configured session write interval.
    */
   _lastSaveTime: 0,
 
   /**
    * Immediately saves the current session to disk.
    */
   run: function () {
-    this._saveState(true /* force-update all windows */);
+    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)
@@ -187,33 +187,49 @@ let SessionSaverInternal = {
    */
   _saveState: function (forceUpdateAllWindows = false) {
     // Cancel any pending timeouts.
     this.cancel();
 
     stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
     let state = SessionStore.getCurrentState(forceUpdateAllWindows);
 
-    // Forget about private windows.
+    // Forget about private windows and tabs.
     for (let i = state.windows.length - 1; i >= 0; i--) {
-      if (state.windows[i].isPrivate) {
-        state.windows.splice(i, 1);
-        if (state.selectedWindow >= i) {
-          state.selectedWindow--;
+      let win = state.windows[i];
+      if (win.isPrivate || false) { // The whole window is private, remove it
+         state.windows.splice(i, 1);
+         if (state.selectedWindow >= i) {
+           state.selectedWindow--;
+         }
+        continue;
+      }
+      // The window is not private, but its tabs still might
+      for (let j = win.tabs.length - 1; j >= 0 ; --j) {
+        let tab = win.tabs[j];
+        if (tab.isPrivate || false) {
+          win.tabs.splice(j, 1);
+          if (win.selected >= j) {
+            win.selected--;
+          }
         }
       }
     }
 
     // Remove private windows from the list of closed windows.
     for (let i = state._closedWindows.length - 1; i >= 0; i--) {
       if (state._closedWindows[i].isPrivate) {
         state._closedWindows.splice(i, 1);
       }
     }
 
+    // Note that closed private tabs are never stored (see
+    // SessionStoreInternal.onTabClose), so we do not need to remove
+    // them.
+
     // 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;
     }
 
 #ifndef XP_MACOSX
@@ -230,17 +246,17 @@ let SessionSaverInternal = {
       }
 
       delete state._closedWindows[i]._shouldRestore;
       state.windows.unshift(state._closedWindows.pop());
     }
 #endif
 
     stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
-    this._writeState(state);
+    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 () {
@@ -273,29 +289,29 @@ let SessionSaverInternal = {
     let data = JSON.stringify(state);
     stopWatchFinish("SERIALIZE_DATA_MS", "SERIALIZE_DATA_LONGEST_OP_MS");
 
     // Give observers a chance to modify session data.
     data = this._notifyObserversBeforeStateWrite(data);
 
     // Don't touch the file if an observer has deleted all state data.
     if (!data) {
-      return;
+      return Promise.resolve();
     }
 
     // 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.
-    SessionFile.write(data).then(() => {
+    return SessionFile.write(data).then(() => {
       this.updateLastSaveTime();
       notify(null, "sessionstore-state-write-complete");
     }, Cu.reportError);
   },
 
   /**
    * Notify sessionstore-state-write observer and give them a
    * chance to modify session data before we'll write it to disk.
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -1306,16 +1306,21 @@ let SessionStoreInternal = {
     }
 
     // Flush all data queued in the content script before the tab is gone.
     TabState.flush(aTab.linkedBrowser);
 
     // Get the latest data for this tab (generally, from the cache)
     let tabState = TabState.collectSync(aTab);
 
+    // Don't save private tabs
+    if (tabState.isPrivate || false) {
+      return;
+    }
+
     // store closed-tab data for undo
     if (this._shouldSaveTabState(tabState)) {
       let tabTitle = aTab.label;
       let tabbrowser = aWindow.gBrowser;
       tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
 
       this._windows[aWindow.__SSi]._closedTabs.unshift({
         state: tabState,
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -54,16 +54,17 @@ support-files =
 [browser_dying_cache.js]
 [browser_form_restore_events.js]
 [browser_formdata_format.js]
 [browser_global_store.js]
 [browser_input.js]
 [browser_merge_closed_tabs.js]
 [browser_pageshow.js]
 [browser_pageStyle.js]
+[browser_privatetabs.js]
 [browser_sessionStorage.js]
 [browser_swapDocShells.js]
 [browser_tabStateCache.js]
 [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.
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_privatetabs.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Imports = {};
+Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", Imports);
+let {Promise, Task, SessionSaver} = Imports;
+
+add_task(function cleanup() {
+  info("Forgetting closed tabs");
+  while (ss.getClosedTabCount(window)) {
+    ss.forgetClosedTab(window, 0);
+  }
+});
+
+add_task(function() {
+  let URL_PUBLIC = "http://example.com/public/" + Math.random();
+  let URL_PRIVATE = "http://example.com/private/" + Math.random();
+  let tab1, tab2;
+  try {
+    // Setup a public tab and a private tab
+    info("Setting up public tab");
+    tab1 = gBrowser.addTab(URL_PUBLIC);
+    yield promiseBrowserLoaded(tab1.linkedBrowser);
+
+    info("Setting up private tab");
+    tab2 = gBrowser.addTab();
+    yield promiseBrowserLoaded(tab2.linkedBrowser);
+    yield setUsePrivateBrowsing(tab2.linkedBrowser, true);
+    tab2.linkedBrowser.loadURI(URL_PRIVATE);
+    yield promiseBrowserLoaded(tab2.linkedBrowser);
+
+    info("Flush to make sure chrome received all data.");
+    SyncHandlers.get(tab2.linkedBrowser).flush();
+
+    info("Checking out state");
+    yield SessionSaver.run();
+    let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+    let data = yield OS.File.read(path);
+    let state = new TextDecoder().decode(data);
+    info("State: " + state);
+    // Ensure that sessionstore.js only knows about the public tab
+    ok(state.indexOf(URL_PUBLIC) != -1, "State contains public tab");
+    ok(state.indexOf(URL_PRIVATE) == -1, "State does not contain private tab");
+
+    // Ensure that we can close and undo close the public tab but not the private tab
+    gBrowser.removeTab(tab2);
+    tab2 = null;
+
+    gBrowser.removeTab(tab1);
+    tab1 = null;
+
+    tab1 = ss.undoCloseTab(window, 0);
+    ok(true, "Public tab supports undo close");
+
+    is(ss.getClosedTabCount(window), 0, "Private tab does not support undo close");
+
+  } finally {
+    if (tab1) {
+      gBrowser.removeTab(tab1);
+    }
+    if (tab2) {
+      gBrowser.removeTab(tab2);
+    }
+  }
+});
+
+function setUsePrivateBrowsing(browser, val) {
+  return sendMessage(browser, "ss-test:setUsePrivateBrowsing", val);
+}
+
--- a/browser/components/sessionstore/test/content.js
+++ b/browser/components/sessionstore/test/content.js
@@ -47,8 +47,15 @@ addMessageListener("ss-test:getAuthorSty
 });
 
 addMessageListener("ss-test:setAuthorStyleDisabled", function (msg) {
   let markupDocumentViewer =
     docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
   markupDocumentViewer.authorStyleDisabled = msg.data;
   sendSyncMessage("ss-test:setAuthorStyleDisabled");
 });
+
+addMessageListener("ss-test:setUsePrivateBrowsing", function (msg) {
+  let loadContext =
+    docShell.QueryInterface(Ci.nsILoadContext);
+  loadContext.usePrivateBrowsing = msg.data;
+  sendAsyncMessage("ss-test:setUsePrivateBrowsing");
+});