Repository: qutebrowser/qutebrowser
Pull request: Clear private data when closing last private window
This is a work in progress. I still have to:

1. Investigate what happens with webkit backend.
2. Investigate how to clear localStorage data (this doesn't seem to be doing it).
3. Figure out exactly where to put this code. I don't see any other `if objects.backend == usertypes.Backend.QtWebEngine` check in the main window code, so this probably isn't the best place. Let me know what you think.

This fixes #4125.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/qutebrowser/qutebrowser/4147)
<!-- Reviewable:end -->


Problem statement:
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
```

Hints from issue 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.

Commit message: Write a function that creates profiles and sets them up

Patch to review:
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index a334d8100c0..4bd17d75640 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -469,14 +469,14 @@ def _init_modules(args, crash_handler):
     log.init.debug("Initializing sessions...")
     sessions.init(qApp)
 
-    log.init.debug("Initializing websettings...")
-    websettings.init(args)
-
     log.init.debug("Initializing adblock...")
     host_blocker = adblock.HostBlocker()
     host_blocker.read_hosts()
     objreg.register('host-blocker', host_blocker)
 
+    log.init.debug("Initializing websettings...")
+    websettings.init(args)
+
     log.init.debug("Initializing quickmarks...")
     quickmark_manager = urlmarks.QuickmarkManager(qApp)
     objreg.register('quickmark-manager', quickmark_manager)
@@ -504,8 +504,6 @@ def _init_modules(args, crash_handler):
 
     log.init.debug("Misc initialization...")
     macros.init()
-    # Init backend-specific stuff
-    browsertab.init()
 
 
 class Quitter:
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 087834e53d9..066324b68ff 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -65,13 +65,6 @@ def create(win_id, private, parent=None):
                      parent=parent)
 
 
-def init():
-    """Initialize backend-specific modules."""
-    if objects.backend == usertypes.Backend.QtWebEngine:
-        from qutebrowser.browser.webengine import webenginetab
-        webenginetab.init()
-
-
 class WebTabError(Exception):
 
     """Base class for various errors."""
diff --git a/qutebrowser/browser/webengine/cookies.py b/qutebrowser/browser/webengine/cookies.py
index d9d11cd98fb..b2d9ff9e594 100644
--- a/qutebrowser/browser/webengine/cookies.py
+++ b/qutebrowser/browser/webengine/cookies.py
@@ -23,7 +23,7 @@
 from qutebrowser.utils import utils
 
 
-def _accept_cookie(request):
+def accept_cookie(request):
     """Check whether the given cookie should be accepted."""
     accept = config.val.content.cookies.accept
     if accept == 'all':
@@ -34,15 +34,3 @@ def _accept_cookie(request):
         return False
     else:
         raise utils.Unreachable
-
-
-def install_filter(profile):
-    """Install the cookie filter on the given profile.
-
-    On Qt < 5.11, the filter isn't installed.
-    """
-    store = profile.cookieStore()
-    try:
-        store.setCookieFilter(_accept_cookie)
-    except AttributeError:
-        pass
diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py
index 68e1519594b..c036d358899 100644
--- a/qutebrowser/browser/webengine/webenginedownloads.py
+++ b/qutebrowser/browser/webengine/webenginedownloads.py
@@ -203,11 +203,6 @@ def __init__(self, parent=None):
         super().__init__(parent)
         self._mhtml_target = None
 
-    def install(self, profile):
-        """Set up the download manager on a QWebEngineProfile."""
-        profile.downloadRequested.connect(self.handle_download,
-                                          Qt.DirectConnection)
-
     @pyqtSlot(QWebEngineDownloadItem)
     def handle_download(self, qt_item):
         """Start a download coming from a QWebEngineProfile."""
diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py
index 3eb7c7df157..fb379bb1870 100644
--- a/qutebrowser/browser/webengine/webenginequtescheme.py
+++ b/qutebrowser/browser/webengine/webenginequtescheme.py
@@ -31,14 +31,6 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
 
     """Handle qute://* requests on QtWebEngine."""
 
-    def install(self, profile):
-        """Install the handler for qute:// URLs on the given profile."""
-        profile.installUrlSchemeHandler(b'qute', self)
-        if qtutils.version_check('5.11', compiled=False):
-            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
-            profile.installUrlSchemeHandler(b'chrome-error', self)
-            profile.installUrlSchemeHandler(b'chrome-extension', self)
-
     def requestStarted(self, job):
         """Handle a request for a qute: scheme.
 
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index 77f82526a1f..dc2927cdd3c 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -26,14 +26,17 @@
 
 import os
 
+from PyQt5.QtCore import Qt
 from PyQt5.QtGui import QFont
 from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
                                       QWebEnginePage)
+from PyQt5.QtWidgets import QApplication
 
-from qutebrowser.browser.webengine import spell
+from qutebrowser.browser.webengine import (spell, cookies, webenginedownloads,
+                                           interceptor, webenginequtescheme)
 from qutebrowser.config import config, websettings
 from qutebrowser.config.websettings import AttributeInfo as Attr
-from qutebrowser.utils import utils, standarddir, qtutils, message, log
+from qutebrowser.utils import utils, standarddir, qtutils, message, log, objreg
 
 # The default QWebEngineProfile
 default_profile = None
@@ -41,6 +44,12 @@
 private_profile = None
 # The global WebEngineSettings object
 global_settings = None
+# For some reason we need to keep a reference, otherwise the scheme handler
+# won't work...
+# https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
+_qute_scheme_handler = None
+_req_interceptor = None
+_download_manager = None
 
 
 class _SettingsWrapper:
@@ -50,37 +59,41 @@ class _SettingsWrapper:
     For read operations, the default profile value is always used.
     """
 
-    def __init__(self):
-        self._settings = [default_profile.settings(),
-                          private_profile.settings()]
+    @staticmethod
+    def setAttribute(*args, **kwargs):
+        default_profile.settings().setAttribute(*args, **kwargs)
+        private_profile.settings().setAttribute(*args, **kwargs)
 
-    def setAttribute(self, *args, **kwargs):
-        for settings in self._settings:
-            settings.setAttribute(*args, **kwargs)
+    @staticmethod
+    def setFontFamily(*args, **kwargs):
+        default_profile.settings().setFontFamily(*args, **kwargs)
+        private_profile.settings().setFontFamily(*args, **kwargs)
 
-    def setFontFamily(self, *args, **kwargs):
-        for settings in self._settings:
-            settings.setFontFamily(*args, **kwargs)
+    @staticmethod
+    def setFontSize(*args, **kwargs):
+        default_profile.settings().setFontSize(*args, **kwargs)
+        private_profile.settings().setFontSize(*args, **kwargs)
 
-    def setFontSize(self, *args, **kwargs):
-        for settings in self._settings:
-            settings.setFontSize(*args, **kwargs)
+    @staticmethod
+    def setDefaultTextEncoding(*args, **kwargs):
+        default_profile.settings().setDefaultTextEncoding(*args, **kwargs)
+        private_profile.settings().setDefaultTextEncoding(*args, **kwargs)
 
-    def setDefaultTextEncoding(self, *args, **kwargs):
-        for settings in self._settings:
-            settings.setDefaultTextEncoding(*args, **kwargs)
+    @staticmethod
+    def testAttribute(*args, **kwargs):
+        return default_profile.settings().testAttribute(*args, **kwargs)
 
-    def testAttribute(self, *args, **kwargs):
-        return self._settings[0].testAttribute(*args, **kwargs)
+    @staticmethod
+    def fontSize(*args, **kwargs):
+        return default_profile.settings().fontSize(*args, **kwargs)
 
-    def fontSize(self, *args, **kwargs):
-        return self._settings[0].fontSize(*args, **kwargs)
+    @staticmethod
+    def fontFamily(*args, **kwargs):
+        return default_profile.settings().fontFamily(*args, **kwargs)
 
-    def fontFamily(self, *args, **kwargs):
-        return self._settings[0].fontFamily(*args, **kwargs)
-
-    def defaultTextEncoding(self, *args, **kwargs):
-        return self._settings[0].defaultTextEncoding(*args, **kwargs)
+    @staticmethod
+    def defaultTextEncoding(*args, **kwargs):
+        return default_profile.settings().defaultTextEncoding(*args, **kwargs)
 
 
 class WebEngineSettings(websettings.AbstractSettings):
@@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
+    """Create a QWebEngineProfile and install all required extensions on it
 
-    private_profile = QWebEngineProfile()
-    private_profile.setter = ProfileSetter(private_profile)
-    assert private_profile.isOffTheRecord()
-    private_profile.setter.init_profile()
+    Args:
+        storage_name: Name of the persistent storage for this profile. Must be
+                      unique across profiles. This name is ignored for private
+                      profiles and, if omitted but private=False, the default
+                      profile will be returned.
+        private: If True, an off-the-record profile will be created and
+                 storage_name will be ignored.
+    """
+    if private:
+        profile = QWebEngineProfile()
+        assert profile.isOffTheRecord()
+    elif storage_name:
+        profile = QWebEngineProfile(storage_name)
+        # TODO: set custom paths for cache and storage?
+    else:
+        profile = QWebEngineProfile.defaultProfile()
+        profile.setCachePath(
+            os.path.join(standarddir.cache(), 'webengine'))
+        profile.setPersistentStoragePath(
+            os.path.join(standarddir.data(), 'webengine'))
+    profile.setter = ProfileSetter(profile)
+    profile.setter.init_profile()
+
+    # Install qute scheme handler
+    profile.installUrlSchemeHandler(b'qute', _qute_scheme_handler)
+    if qtutils.version_check('5.11', compiled=False):
+        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
+        profile.installUrlSchemeHandler(b'chrome-error', _qute_scheme_handler)
+        profile.installUrlSchemeHandler(b'chrome-extension',
+                                        _qute_scheme_handler)
+
+    # Install request interceptor
+    profile.setRequestInterceptor(_req_interceptor)
+
+    # Install download manager
+    profile.downloadRequested.connect(_download_manager.handle_download,
+                                      Qt.DirectConnection)
+
+    # Install cookie filter
+    # On Qt < 5.11, the cookie filter isn't installed.
+    try:
+        profile.cookieStore().setCookieFilter(cookies.accept_cookie)
+    except AttributeError:
+        pass
+
+    # Clear visited links on web history clear
+    hist = objreg.get('web-history')
+    hist.history_cleared.connect(profile.clearAllVisitedLinks)
+    hist.url_cleared.connect(lambda url, profile=profile:
+                             profile.clearVisitedLinks([url]))
+    return profile
+
+
+def reset_private_profile():
+    """Replace the private profile with a new instance"""
+    global private_profile
+    private_profile = create_profile(private=True)
 
 
 def init(args):
@@ -302,11 +358,31 @@ def init(args):
 
     spell.init()
 
-    _init_profiles()
+    app = QApplication.instance()
+
+    log.init.debug("Initializing qute://* handler...")
+    global _qute_scheme_handler
+    _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
+
+    log.init.debug("Initializing request interceptor...")
+    global _req_interceptor
+    host_blocker = objreg.get('host-blocker')
+    args = objreg.get('args')
+    _req_interceptor = interceptor.RequestInterceptor(host_blocker, args=args,
+                                                      parent=app)
+
+    log.init.debug("Initializing QtWebEngine downloads...")
+    global _download_manager
+    _download_manager = webenginedownloads.DownloadManager(parent=app)
+    objreg.register('webengine-download-manager', _download_manager)
+
+    global default_profile, private_profile
+    default_profile = create_profile()
+    private_profile = create_profile(private=True)
     config.instance.changed.connect(_update_settings)
 
     global global_settings
-    global_settings = WebEngineSettings(_SettingsWrapper())
+    global_settings = WebEngineSettings(_SettingsWrapper)
     global_settings.init_settings()
 
 
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 2c890b8a5dd..7b46451136e 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -35,8 +35,6 @@
 from qutebrowser.config import configdata, config
 from qutebrowser.browser import browsertab, mouse, shared, webelem
 from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
-                                           interceptor, webenginequtescheme,
-                                           cookies, webenginedownloads,
                                            webenginesettings, certificateerror)
 from qutebrowser.misc import miscwidgets
 from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
@@ -44,49 +42,6 @@
 from qutebrowser.qt import sip
 
 
-_qute_scheme_handler = None
-
-
-def init():
-    """Initialize QtWebEngine-specific modules."""
-    # For some reason we need to keep a reference, otherwise the scheme handler
-    # won't work...
-    # https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
-    global _qute_scheme_handler
-
-    app = QApplication.instance()
-    log.init.debug("Initializing qute://* handler...")
-    _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
-    _qute_scheme_handler.install(webenginesettings.default_profile)
-    _qute_scheme_handler.install(webenginesettings.private_profile)
-
-    log.init.debug("Initializing request interceptor...")
-    host_blocker = objreg.get('host-blocker')
-    args = objreg.get('args')
-    req_interceptor = interceptor.RequestInterceptor(
-        host_blocker, args=args, parent=app)
-    req_interceptor.install(webenginesettings.default_profile)
-    req_interceptor.install(webenginesettings.private_profile)
-
-    log.init.debug("Initializing QtWebEngine downloads...")
-    download_manager = webenginedownloads.DownloadManager(parent=app)
-    download_manager.install(webenginesettings.default_profile)
-    download_manager.install(webenginesettings.private_profile)
-    objreg.register('webengine-download-manager', download_manager)
-
-    log.init.debug("Initializing cookie filter...")
-    cookies.install_filter(webenginesettings.default_profile)
-    cookies.install_filter(webenginesettings.private_profile)
-
-    # Clear visited links on web history clear
-    hist = objreg.get('web-history')
-    for p in [webenginesettings.default_profile,
-              webenginesettings.private_profile]:
-        hist.history_cleared.connect(p.clearAllVisitedLinks)
-        hist.url_cleared.connect(lambda url, profile=p:
-                                 profile.clearVisitedLinks([url]))
-
-
 # Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
 _JS_WORLD_MAP = {
     usertypes.JsWorld.main: QWebEngineScript.MainWorld,
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index fb80c543b9f..dd733630d89 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -22,7 +22,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()
@@ -193,6 +193,15 @@ def init(args):
                                 pattern=urlmatch.UrlPattern(pattern))
 
 
+def clear_private_data():
+    """Clear cookies, cache and related data for private browsing sessions"""
+    if objects.backend == usertypes.Backend.QtWebEngine:
+        from qutebrowser.browser.webengine import webenginesettings
+        webenginesettings.reset_private_profile()
+    else:
+        objreg.get('ram-cookie-jar').setAllCookies([])
+
+
 def shutdown():
     """Shut down QWeb(Engine)Settings."""
     if objects.backend == usertypes.Backend.QtWebEngine:
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 3fdad13d1c5..cd4076f826e 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -29,10 +29,11 @@
 from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
 
 from qutebrowser.commands import runners, 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)
 from qutebrowser.mainwindow import messageview, prompt
+from qutebrowser.misc import objects
 from qutebrowser.completion import completionwidget, completer
 from qutebrowser.keyinput import modeman
 from qutebrowser.browser import commands, downloadview, hints, downloads
@@ -186,7 +187,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)
@@ -561,6 +562,15 @@ def _do_close(self):
             pass
         objreg.get('session-manager').save_last_window_session()
         self._save_geometry()
+        if (
+                len(objreg.window_registry) > 1
+                and all(window is self and self.private or not window.private
+                        for window
+                        in objreg.window_registry.values())
+        ):
+            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()
 


Code review comments:
diff_hunk: @@ -469,14 +469,14 @@ def _init_modules(args, crash_handler):
     log.init.debug("Initializing sessions...")
     sessions.init(qApp)
 
-    log.init.debug("Initializing websettings...")
-    websettings.init(args)
-
     log.init.debug("Initializing adblock...")
     host_blocker = adblock.HostBlocker()
     host_blocker.read_hosts()
     objreg.register('host-blocker', host_blocker)
 
+    log.init.debug("Initializing websettings...")
+    websettings.init(args)
+
path: qutebrowser/app.py
text: @user1:
Why is this move needed? I'm always a bit sceptical about moving init things around, though this should be fine.

@author:
Ah yes, I had to move this otherwise I get the following on startup:
```python
Traceback (most recent call last):
  File "./qutebrowser.py", line 29, in <module>
    sys.exit(qutebrowser.qutebrowser.main())
  File "/home/lufte/projects/qutebrowser/qutebrowser/qutebrowser.py", line 194, in main
    return app.run(args)
  File "/home/lufte/projects/qutebrowser/qutebrowser/app.py", line 141, in run
    init(args, crash_handler)
  File "/home/lufte/projects/qutebrowser/qutebrowser/app.py", line 167, in init
    _init_modules(args, crash_handler)
  File "/home/lufte/projects/qutebrowser/qutebrowser/app.py", line 473, in _init_modules
    websettings.init(args)
  File "/home/lufte/projects/qutebrowser/qutebrowser/config/websettings.py", line 185, in init
    webenginesettings.init(args)
  File "/home/lufte/projects/qutebrowser/qutebrowser/browser/webengine/webenginesettings.py", line 369, in init
    host_blocker = objreg.get('host-blocker')
  File "/home/lufte/projects/qutebrowser/qutebrowser/utils/objreg.py", line 229, in get
    return reg[name]
  File "/usr/lib/python3.5/collections/__init__.py", line 989, in __getitem__
    raise KeyError(key)
KeyError: 'host-blocker'
```

diff_hunk: @@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
+    """Create a QWebEngineProfile and install all required extensions on it
 
-    private_profile = QWebEngineProfile()
-    private_profile.setter = ProfileSetter(private_profile)
-    assert private_profile.isOffTheRecord()
-    private_profile.setter.init_profile()
+    Args:
+        storage_name: Name of the persistent storage for this profile. Must be
+                      unique across profiles. This name is ignored for private
+                      profiles and, if omitted but private=False, the default
+                      profile will be returned.
+        private: If True, an off-the-record profile will be created and
+                 storage_name will be ignored.
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user1:
What about making it an error instead when `private=True` is used, but a `storage_name` is also set?

diff_hunk: @@ -50,37 +59,41 @@ class _SettingsWrapper:
     For read operations, the default profile value is always used.
     """
 
-    def __init__(self):
-        self._settings = [default_profile.settings(),
-                          private_profile.settings()]
path: qutebrowser/browser/webengine/webenginesettings.py
text: @author:
We can't keep these references to the profiles' settings anymore in case the profiles change. 

@user1:
What do you think about adding a `_settings()` method instead, which returns all settings objects to use? That'd keep the code closer to the old one, and also probably help with #4102 in the future when there are more than two profiles around.

@author:
That sounds good.

diff_hunk: @@ -34,15 +34,3 @@ def _accept_cookie(request):
         return False
     else:
         raise utils.Unreachable
-
-
-def install_filter(profile):
path: qutebrowser/browser/webengine/cookies.py
text: @user1:
Why not keep this? Seems nicer to keep the install-code with the cookie-related code in `cookies.py`.

@author:
Hmm, looking back now it seems I went a little crazy with changing code based exclusively on personal preference :sweat_smile: 

I think my reasoning was "this made me think it was going to install **the profile on something** but was actually the other way around, so why not put this code directly in the profile init code?". Anyway, doesn't make much sense when looking at it currently, I'll change it back.

diff_hunk: @@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
+    """Create a QWebEngineProfile and install all required extensions on it
 
-    private_profile = QWebEngineProfile()
-    private_profile.setter = ProfileSetter(private_profile)
-    assert private_profile.isOffTheRecord()
-    private_profile.setter.init_profile()
+    Args:
+        storage_name: Name of the persistent storage for this profile. Must be
+                      unique across profiles. This name is ignored for private
+                      profiles and, if omitted but private=False, the default
+                      profile will be returned.
+        private: If True, an off-the-record profile will be created and
+                 storage_name will be ignored.
+    """
+    if private:
+        profile = QWebEngineProfile()
+        assert profile.isOffTheRecord()
+    elif storage_name:
+        profile = QWebEngineProfile(storage_name)
+        # TODO: set custom paths for cache and storage?
+    else:
+        profile = QWebEngineProfile.defaultProfile()
+        profile.setCachePath(
+            os.path.join(standarddir.cache(), 'webengine'))
+        profile.setPersistentStoragePath(
+            os.path.join(standarddir.data(), 'webengine'))
+    profile.setter = ProfileSetter(profile)
+    profile.setter.init_profile()
+
+    # Install qute scheme handler
+    profile.installUrlSchemeHandler(b'qute', _qute_scheme_handler)
+    if qtutils.version_check('5.11', compiled=False):
+        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
+        profile.installUrlSchemeHandler(b'chrome-error', _qute_scheme_handler)
+        profile.installUrlSchemeHandler(b'chrome-extension',
+                                        _qute_scheme_handler)
+
+    # Install request interceptor
+    profile.setRequestInterceptor(_req_interceptor)
+
+    # Install download manager
+    profile.downloadRequested.connect(_download_manager.handle_download,
+                                      Qt.DirectConnection)
+
+    # Install cookie filter
+    # On Qt < 5.11, the cookie filter isn't installed.
+    try:
+        profile.cookieStore().setCookieFilter(cookies.accept_cookie)
+    except AttributeError:
+        pass
+
+    # Clear visited links on web history clear
+    hist = objreg.get('web-history')
+    hist.history_cleared.connect(profile.clearAllVisitedLinks)
+    hist.url_cleared.connect(lambda url, profile=profile:
+                             profile.clearVisitedLinks([url]))
path: qutebrowser/browser/webengine/webenginesettings.py
text: @author:
Should I call `global_settings.init_settings()` here so settings that could've been applied to older profiles are copied to the new one?

@user1:
Hmm, I'm not sure whether it's a good idea to re-apply all settings on existing profiles whenever a new profile is added - but those should definitely be consistent across profiles.

Maybe you could create a temporary `WebEngineSettings` object which only applies the settings to the newly created profile, and after that, `global_settings will take care on updating it for any future changes?

@author:
I'm not really familiar with all the "settings" part of the code but I think I get what you mean. I'll take care of it in the next commits.

diff_hunk: @@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
+    """Create a QWebEngineProfile and install all required extensions on it
 
-    private_profile = QWebEngineProfile()
-    private_profile.setter = ProfileSetter(private_profile)
-    assert private_profile.isOffTheRecord()
-    private_profile.setter.init_profile()
+    Args:
+        storage_name: Name of the persistent storage for this profile. Must be
+                      unique across profiles. This name is ignored for private
+                      profiles and, if omitted but private=False, the default
+                      profile will be returned.
+        private: If True, an off-the-record profile will be created and
+                 storage_name will be ignored.
+    """
+    if private:
+        profile = QWebEngineProfile()
+        assert profile.isOffTheRecord()
+    elif storage_name:
+        profile = QWebEngineProfile(storage_name)
+        # TODO: set custom paths for cache and storage?
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user1:
Yeah, this should probably be set to something custom to be consistent with the paths already in use. Not sure what a good place would be, maybe `os.path.join(standarddir.cache(), 'webengine-profiles', storage_name)` or so (or `.data()` respectively).

diff_hunk: @@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
+    """Create a QWebEngineProfile and install all required extensions on it
 
-    private_profile = QWebEngineProfile()
-    private_profile.setter = ProfileSetter(private_profile)
-    assert private_profile.isOffTheRecord()
-    private_profile.setter.init_profile()
+    Args:
+        storage_name: Name of the persistent storage for this profile. Must be
+                      unique across profiles. This name is ignored for private
+                      profiles and, if omitted but private=False, the default
+                      profile will be returned.
+        private: If True, an off-the-record profile will be created and
+                 storage_name will be ignored.
+    """
+    if private:
+        profile = QWebEngineProfile()
+        assert profile.isOffTheRecord()
+    elif storage_name:
+        profile = QWebEngineProfile(storage_name)
+        # TODO: set custom paths for cache and storage?
+    else:
+        profile = QWebEngineProfile.defaultProfile()
+        profile.setCachePath(
+            os.path.join(standarddir.cache(), 'webengine'))
+        profile.setPersistentStoragePath(
+            os.path.join(standarddir.data(), 'webengine'))
+    profile.setter = ProfileSetter(profile)
+    profile.setter.init_profile()
+
+    # Install qute scheme handler
+    profile.installUrlSchemeHandler(b'qute', _qute_scheme_handler)
+    if qtutils.version_check('5.11', compiled=False):
+        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
+        profile.installUrlSchemeHandler(b'chrome-error', _qute_scheme_handler)
+        profile.installUrlSchemeHandler(b'chrome-extension',
+                                        _qute_scheme_handler)
+
+    # Install request interceptor
+    profile.setRequestInterceptor(_req_interceptor)
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user1:
Looks like you didn't remove `RequestInterceptor.install` - but like with other things above, I'd rather keep the idea that objects are responsible themselves for installing them on a profile.

diff_hunk: @@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
+    """Create a QWebEngineProfile and install all required extensions on it
 
-    private_profile = QWebEngineProfile()
-    private_profile.setter = ProfileSetter(private_profile)
-    assert private_profile.isOffTheRecord()
-    private_profile.setter.init_profile()
+    Args:
+        storage_name: Name of the persistent storage for this profile. Must be
+                      unique across profiles. This name is ignored for private
+                      profiles and, if omitted but private=False, the default
+                      profile will be returned.
+        private: If True, an off-the-record profile will be created and
+                 storage_name will be ignored.
+    """
+    if private:
+        profile = QWebEngineProfile()
+        assert profile.isOffTheRecord()
+    elif storage_name:
+        profile = QWebEngineProfile(storage_name)
+        # TODO: set custom paths for cache and storage?
+    else:
+        profile = QWebEngineProfile.defaultProfile()
+        profile.setCachePath(
+            os.path.join(standarddir.cache(), 'webengine'))
+        profile.setPersistentStoragePath(
+            os.path.join(standarddir.data(), 'webengine'))
+    profile.setter = ProfileSetter(profile)
+    profile.setter.init_profile()
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user1:
Looks like the `profile.setter.set_persistent_cookie_policy()` call got lost here - note that the call should only be done if it isn't a private profile. Maybe that should just be added to `init_profile()` instead, guarded with a `if not profile.isOffTheRecord():` or so.

diff_hunk: @@ -275,23 +288,66 @@ 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_profile = QWebEngineProfile.defaultProfile()
-    default_profile.setter = ProfileSetter(default_profile)
-    default_profile.setCachePath(
-        os.path.join(standarddir.cache(), 'webengine'))
-    default_profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-    default_profile.setter.init_profile()
-    default_profile.setter.set_persistent_cookie_policy()
+def create_profile(storage_name=None, private=False):
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user1:
Hmm, we start to have more and more stuff in here which isn't really related to settings - maybe we should try moving that out to a `webengineprofile.py` - what do you think?

diff_hunk: @@ -302,11 +358,31 @@ def init(args):
 
     spell.init()
 
-    _init_profiles()
+    app = QApplication.instance()
+
+    log.init.debug("Initializing qute://* handler...")
+    global _qute_scheme_handler
+    _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
+
+    log.init.debug("Initializing request interceptor...")
+    global _req_interceptor
+    host_blocker = objreg.get('host-blocker')
+    args = objreg.get('args')
+    _req_interceptor = interceptor.RequestInterceptor(host_blocker, args=args,
+                                                      parent=app)
+
+    log.init.debug("Initializing QtWebEngine downloads...")
+    global _download_manager
+    _download_manager = webenginedownloads.DownloadManager(parent=app)
+    objreg.register('webengine-download-manager', _download_manager)
+
+    global default_profile, private_profile
+    default_profile = create_profile()
+    private_profile = create_profile(private=True)
     config.instance.changed.connect(_update_settings)
 
     global global_settings
-    global_settings = WebEngineSettings(_SettingsWrapper())
+    global_settings = WebEngineSettings(_SettingsWrapper)
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user2:
with the `@user1` changes above, this would be an instance rather than a class again.

diff_hunk: @@ -561,6 +562,15 @@ def _do_close(self):
             pass
         objreg.get('session-manager').save_last_window_session()
         self._save_geometry()
+        if (
+                len(objreg.window_registry) > 1
+                and all(window is self and self.private or not window.private
path: qutebrowser/mainwindow/mainwindow.py
text: @user1:
I found this a bit hard to follow. Couldn't you just check `self.private and len(window for window in objreg.window_registry.values() if window.private) == 1` or so?

@author:
My idea here was to include an extra case: if I'm closing the last private window **but** it is also the last window of any kind, then don't run the cleanup routine. It's just a small improvement to not run unnecesary stuff when shutting down.

Doing `len` instead of `all` would end up in something like:
```
self.private 
and len(objreg.window_registry) > 1
and len(window for window in objreg.window_registry.values() if window.private) == 1
```

which isn't really much shorter, but if you find this clearer I have no problem changing it.

@user1:
Yeah, I was mainly confused by the `all(window is self and self.private or not window.private ...)` part. Maybe adding an extra set of parentheses would make it a bit clearer, but I would indeed prefer the `len(...)` variant (unless you disagree and find the `all(...)` one much clearer).

Maybe also add a comment like `# Wipe private data if we close the last private window, but there are still other windows` or so - I'm usually not a fan of comments duplicating the code's logic, but here I think it'd be worth it.

diff_hunk: @@ -50,37 +59,41 @@ class _SettingsWrapper:
     For read operations, the default profile value is always used.
     """
 
-    def __init__(self):
-        self._settings = [default_profile.settings(),
-                          private_profile.settings()]
+    @staticmethod
path: qutebrowser/browser/webengine/webenginesettings.py
text: @user2:
I don't really use `@user1` in qutebrowser - just make them normal method and add a `self` (even if it's unused, that's fine IMHO).

@author:
Alright, but are you sure you don't like them? I think they keep the code cleaner and also they are definitely faster, even when there is no `__init__` method:
```python
Python 3.5.3 (default, Sep 27 2018, 17:25:39)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class MyClass:
...  def normal(self):
...   return
...  @user1
...  def static():
...   return
...
>>> import timeit
>>> timeit.timeit('MyClass().normal()', globals=globals())
0.24852409300365252
>>> timeit.timeit('MyClass.static()', globals=globals())
0.15611451499717077
```