Repository: qutebrowser/qutebrowser

Issue:
Private mode data isn't cleared after private windows are closed
<!-- If this is a bug report, please remember to mention your version info from
`:open qute:version` or `qutebrowser --version` -->

Usualy, I open my email in private mode `open -p gmail.com`, so no password will be stored. 

What I expect from a private mode is that all session information (history, commands, cache, passwords, ....) will not be saved OR will be cleared after closing "private windows".  However, I noticed that qutebrowser did not clear passwords and cache of private mode if it is in not totally closed (close all tabs and windows private and non-pivate). If there are non-private windows openend, after closing only private windows, I can get to my mail `open -p gmail.com` without reentring username and password ;( !!!!!!!!!

```
qutebrowser v1.4.1
Git commit: 
Backend: QtWebEngine (Chromium 65.0.3325.230)

CPython: 3.7.0
Qt: 5.11.1
PyQt: 5.11.2

sip: 4.19.12
colorama: 0.3.9
pypeg2: 2.15
jinja2: 2.10
pygments: 2.2.0
yaml: 3.13
cssutils: no
attr: 18.1.0
PyQt5.QtWebEngineWidgets: yes
PyQt5.QtWebKitWidgets: yes
pdf.js: no
sqlite: 3.24.0
QtNetwork SSL: OpenSSL 1.1.0h  27 Mar 2018

Style: Qt5CTProxyStyle
Platform: Linux-4.14.56-1-lts-x86_64-with-glibc2.2.5, 64bit
Linux distribution: Artix Linux (unknown)
Frozen: False
Imported from /usr/lib/python3.7/site-packages/qutebrowser
Using Python from /usr/bin/python3
Qt library executable path: /usr/lib/qt/libexec, data path: /usr/share/qt
OS Version: 

--- /etc/lsb-release ---
LSB_VERSION=1.4
DISTRIB_ID=Artix
DISTRIB_RELEASE=rolling
DISTRIB_DESCRIPTION="Artix"

--- /etc/os-release ---
NAME="Artix"
PRETTY_NAME="Artix Linux"
ID=artix
ID_LIKE=artixlinux

Paths:
cache: /home/hk/.cache/qutebrowser
config: /home/hk/.config/qutebrowser
data: /home/hk/.local/share/qutebrowser
runtime: /run/user/1000/qutebrowser
system data: /usr/share/qutebrowser

Uptime: 19:28:23
```


Discussion:
Looking at Chromium, incognito mode data is shared between multiple incognito windows, but indeed is cleared as soon as all of those are closed.

Not sure yet to what extent we can clear that data - it might be sufficient to use a different `QWebEngineProfile` object for the private profile once all existing windows are closed.

Resolving patch:
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -53,38 +53,39 @@ class _SettingsWrapper:
     For read operations, the default profile value is always used.
     """
 
-    def __init__(self):
-        self._settings = [default_profile.settings()]
+    def _settings(self):
         if private_profile:
-            self._settings.append(private_profile.settings())
+            return (default_profile.settings(), private_profile.settings())
+        else:
+            return (default_profile.settings(),)
 
     def setAttribute(self, *args, **kwargs):
-        for settings in self._settings:
+        for settings in self._settings():
             settings.setAttribute(*args, **kwargs)
 
     def setFontFamily(self, *args, **kwargs):
-        for settings in self._settings:
+        for settings in self._settings():
             settings.setFontFamily(*args, **kwargs)
 
     def setFontSize(self, *args, **kwargs):
-        for settings in self._settings:
+        for settings in self._settings():
             settings.setFontSize(*args, **kwargs)
 
     def setDefaultTextEncoding(self, *args, **kwargs):
-        for settings in self._settings:
+        for settings in self._settings():
             settings.setDefaultTextEncoding(*args, **kwargs)
 
     def testAttribute(self, *args, **kwargs):
-        return self._settings[0].testAttribute(*args, **kwargs)
+        return self._settings()[0].testAttribute(*args, **kwargs)
 
     def fontSize(self, *args, **kwargs):
-        return self._settings[0].fontSize(*args, **kwargs)
+        return self._settings()[0].fontSize(*args, **kwargs)
 
     def fontFamily(self, *args, **kwargs):
-        return self._settings[0].fontFamily(*args, **kwargs)
+        return self._settings()[0].fontFamily(*args, **kwargs)
 
     def defaultTextEncoding(self, *args, **kwargs):
-        return self._settings[0].defaultTextEncoding(*args, **kwargs)
+        return self._settings()[0].defaultTextEncoding(*args, **kwargs)
 
 
 class WebEngineSettings(websettings.AbstractSettings):
@@ -285,9 +286,9 @@ def _update_settings(option):
             private_profile.setter.set_dictionary_language(warn=False)
 
 
-def _init_profiles():
-    """Init the two used QWebEngineProfiles."""
-    global default_profile, private_profile, default_user_agent
+def _init_default_profile():
+    """Init the default QWebEngineProfile."""
+    global default_profile, default_user_agent
 
     default_profile = QWebEngineProfile.defaultProfile()
     default_user_agent = default_profile.httpUserAgent()
@@ -299,6 +300,11 @@ def _init_profiles():
     default_profile.setter.init_profile()
     default_profile.setter.set_persistent_cookie_policy()
 
+
+def init_private_profile():
+    """Init the private QWebEngineProfile."""
+    global private_profile
+
     if not qtutils.is_single_process():
         private_profile = QWebEngineProfile()
         private_profile.setter = ProfileSetter(private_profile)
@@ -315,7 +321,8 @@ def init(args):
     webenginequtescheme.init()
     spell.init()
 
-    _init_profiles()
+    _init_default_profile()
+    init_private_profile()
     config.instance.changed.connect(_update_settings)
 
     global global_settings
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -26,7 +26,7 @@
 from PyQt5.QtGui import QFont
 
 from qutebrowser.config import config, configutils
-from qutebrowser.utils import log, usertypes, urlmatch, qtutils
+from qutebrowser.utils import log, usertypes, urlmatch, qtutils, objreg
 from qutebrowser.misc import objects
 
 UNSET = object()
@@ -188,9 +188,11 @@ def init(args: argparse.Namespace) -> None:
     if objects.backend == usertypes.Backend.QtWebEngine:
         from qutebrowser.browser.webengine import webenginesettings
         webenginesettings.init(args)
-    else:
+    elif objects.backend == usertypes.Backend.QtWebKit:
         from qutebrowser.browser.webkit import webkitsettings
         webkitsettings.init(args)
+    else:
+        raise NotImplementedError
 
     # Make sure special URLs always get JS support
     for pattern in ['file://*', 'chrome://*/*', 'qute://*/*']:
@@ -198,11 +200,24 @@ def init(args: argparse.Namespace) -> None:
                                 pattern=urlmatch.UrlPattern(pattern))
 
 
+def clear_private_data() -> None:
+    """Clear cookies, cache and related data for private browsing sessions."""
+    if objects.backend == usertypes.Backend.QtWebEngine:
+        from qutebrowser.browser.webengine import webenginesettings
+        webenginesettings.init_private_profile()
+    elif objects.backend == usertypes.Backend.QtWebKit:
+        objreg.get('ram-cookie-jar').setAllCookies([])
+    else:
+        raise NotImplementedError
+
+
 def shutdown() -> None:
     """Shut down QWeb(Engine)Settings."""
     if objects.backend == usertypes.Backend.QtWebEngine:
         from qutebrowser.browser.webengine import webenginesettings
         webenginesettings.shutdown()
-    else:
+    elif objects.backend == usertypes.Backend.QtWebKit:
         from qutebrowser.browser.webkit import webkitsettings
         webkitsettings.shutdown()
+    else:
+        raise NotImplementedError
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -30,7 +30,7 @@
 
 from qutebrowser.commands import runners
 from qutebrowser.api import cmdutils
-from qutebrowser.config import config, configfiles
+from qutebrowser.config import config, configfiles, websettings
 from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
                                jinja, debug)
 from qutebrowser.mainwindow import messageview, prompt
@@ -200,7 +200,7 @@ def __init__(self, *, private, geometry=None, parent=None):
             private = True
         else:
             private = bool(private)
-        self._private = private
+        self.private = private
         self.tabbed_browser = tabbedbrowser.TabbedBrowser(win_id=self.win_id,
                                                           private=private,
                                                           parent=self)
@@ -582,6 +582,19 @@ def _do_close(self):
             pass
         objreg.get('session-manager').save_last_window_session()
         self._save_geometry()
+
+        # Wipe private data if we close the last private window, but there are
+        # still other windows
+        if (
+                self.private and
+                len(objreg.window_registry) > 1 and
+                len([window for window in objreg.window_registry.values()
+                     if window.private]) == 1
+        ):
+            log.destroy.debug("Wiping private data before closing last "
+                              "private window")
+            websettings.clear_private_data()
+
         log.destroy.debug("Closing window {}".format(self.win_id))
         self.tabbed_browser.shutdown()
 


Test patch:
diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature
--- a/tests/end2end/features/private.feature
+++ b/tests/end2end/features/private.feature
@@ -42,6 +42,25 @@ Feature: Using private browsing
 
     ## https://github.com/qutebrowser/qutebrowser/issues/1219
 
+    Scenario: Make sure private data is cleared when closing last private window
+        When I open about:blank in a private window
+        And I open cookies/set?cookie-to-delete=1 without waiting in a new tab
+        And I wait until cookies is loaded
+        And I run :close
+        And I open about:blank in a private window
+        And I open cookies
+        Then the cookie cookie-to-delete should not be set
+
+    Scenario: Make sure private data is not cleared when closing a private window but another remains
+        When I open about:blank in a private window
+        And I open about:blank in a private window
+        And I open cookies/set?cookie-to-preserve=1 without waiting in a new tab
+        And I wait until cookies is loaded
+        And I run :close
+        And I open about:blank in a private window
+        And I open cookies
+        Then the cookie cookie-to-preserve should be set to 1
+
     Scenario: Sharing cookies with private browsing
         When I open cookies/set?qute-test=42 without waiting in a private window
         And I wait until cookies is loaded
@@ -153,7 +172,6 @@ Feature: Using private browsing
                 - url: http://localhost:*/data/numbers/1.txt
                 - url: http://localhost:*/data/numbers/2.txt
 
-
   Scenario: Saving a private session with only-active-window
         When I open data/numbers/1.txt
         And I open data/numbers/2.txt in a new tab