diff --git a/.flake8 b/.flake8 index 7a783a4b0..04c491bf2 100644 --- a/.flake8 +++ b/.flake8 @@ -46,12 +46,11 @@ ignore = min-version = 3.4.0 max-complexity = 12 per-file-ignores = - /qutebrowser/api/hook.py : N801 - /tests/**/*.py : D100,D101,D401 - /tests/unit/browser/test_history.py : N806 - /tests/helpers/fixtures.py : N806 - /tests/unit/browser/webkit/http/test_content_disposition.py : D400 - /scripts/dev/ci/appveyor_install.py : FI53 + qutebrowser/api/hook.py : N801 + tests/* : D100,D101 + tests/unit/browser/test_history.py : D100,D101,N806 + tests/helpers/fixtures.py : D100,D101,N806 + tests/unit/browser/webkit/http/test_content_disposition.py : D100,D101,D400 copyright-check = True copyright-regexp = # Copyright [\d-]+ .* copyright-min-file-size = 110 diff --git a/.travis.yml b/.travis.yml index dfa566671..061810ee8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,51 +5,84 @@ python: 3.6 os: linux matrix: + fast_finish: true include: + ### Archlinux QtWebKit - env: DOCKER=archlinux services: docker + + ### Archlinux QtWebEngine - env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true services: docker - - env: TESTENV=py36-pyqt571 + + ### PyQt 5.7.1 (Python 3.5) - python: 3.5 env: TESTENV=py35-pyqt571 + ### PyQt 5.7.1 + - env: TESTENV=py36-pyqt571 + + ### PyQt 5.9 - env: TESTENV=py36-pyqt59 + + ### PyQt 5.10 - env: TESTENV=py36-pyqt510 addons: apt: packages: - xfonts-base + + ### PyQt 5.11 (with coverage) - env: TESTENV=py36-pyqt511-cov + ### PyQt 5.11 (Python 3.7) - python: 3.7 env: TESTENV=py37-pyqt511 + + ### PyQt 5.12 + - env: TESTENV=py36-pyqt512 + addons: + apt: + packages: + - libxkbcommon-x11-0 + + ### macOS sierra - os: osx - env: TESTENV=py37 OSX=sierra + env: TESTENV=py37-pyqt511 OSX=sierra osx_image: xcode9.2 language: generic + ### macOS yosemite # https://github.com/qutebrowser/qutebrowser/issues/2013 # - os: osx # env: TESTENV=py35 OSX=yosemite # osx_image: xcode6.4 + + ### pylint/flake8/mypy - env: TESTENV=pylint - env: TESTENV=flake8 - env: TESTENV=mypy + + ### docs - env: TESTENV=docs addons: apt: packages: - asciidoc + + ### vulture/misc/pyroma/check-manifest - env: TESTENV=vulture - env: TESTENV=misc - env: TESTENV=pyroma - env: TESTENV=check-manifest + + ### eslint - env: TESTENV=eslint language: node_js python: null node_js: "lts/*" + + ### shellcheck - language: generic env: TESTENV=shellcheck services: docker - fast_finish: true cache: directories: diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 6b2bc1d71..bf07b4416 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -35,6 +35,9 @@ Added are used for hints, and also allows adding custom hint groups. - New `:yank markdown` feature which yanks the current URL and title in markdown format. +- Basic support for client certificates with Qt 5.12. Selecting the certificate + to show when there are multiple matching certificates isn't implemented yet. +- Support for DNS prefetching (`content.dns_prefetch`) with QtWebEngine on 5.12. Changed ~~~~~~~ @@ -59,6 +62,9 @@ Changed `org.qutebrowser.qutebrowser.appdata.xml`. - The `qute-pass` userscript now understands domains in gpg filenames in addition to directory names. +- macOS: The IPC socket path used to communicate with existing instances + changed due to changes in Qt 5.12. Please make sure to quit qutebrowser + before upgrading. Fixed ~~~~~ @@ -79,6 +85,11 @@ Fixed - When `scrolling.bar = True` was set in versions before v1.5.0, this now correctly gets migrated to `always` instead of `when-searching`. - Completion highlighting now works again on Qt 5.11.3 and 5.12.1. +- The outdated header `X-Do-Not-Track` is no longer sent. +- A javascript error on page load when using Qt 5.12 was fixed. +- `window.print()` works with Qt 5.12 now. +- PAC proxies were never correctly supported with QtWebEngine, but are now + explicitly disallowed. v1.5.2 ------ @@ -378,11 +389,11 @@ v1.3.3 Security ~~~~~~~~ -- An XSS vulnerability on the `qute://history` page allowed websites to inject - HTML into the page via a crafted title tag. This could allow them to steal - your browsing history. If you're currently unable to upgrade, avoid using - `:history`. A CVE request for this issue is pending, see - https://github.com/qutebrowser/qutebrowser/issues/4011[#4011] for updates. +- CVE-2018-1000559: An XSS vulnerability on the `qute://history` page allowed + websites to inject HTML into the page via a crafted title tag. This could + allow them to steal your browsing history. If you're currently unable to + upgrade, avoid using `:history`. See the related GitHub issue for details: + https://github.com/qutebrowser/qutebrowser/issues/4011. Fixed ~~~~~ diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index dc52dd9a0..a9e266a1d 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -710,6 +710,7 @@ qutebrowser release * Update changelog (remove *(unreleased)*). * Adjust `__version_info__` in `qutebrowser/__init__.py`. +* Consider updating the completions for `content.headers.user_agent` in `configdata.yml`. * Commit. * Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`). diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index eb3907cce..eab51a16b 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1647,7 +1647,7 @@ Type: <> Default: +pass:[true]+ -This setting is only available with the QtWebKit backend. +On QtWebEngine, this setting requires Qt 5.12 or newer. [[content.frame_flattening]] === content.frame_flattening diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 42255f825..6cdfac747 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -1,27 +1,25 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py attrs==18.2.0 -flake8==3.6.0 +entrypoints==0.3 +flake8==3.7.5 flake8-bugbear==18.8.0 flake8-builtins==1.4.1 -flake8-comprehensions==1.4.1 +flake8-comprehensions==2.0.0 flake8-copyright==0.2.2 flake8-debugger==3.1.0 flake8-deprecated==1.3 flake8-docstrings==1.3.0 flake8-future-import==0.4.5 flake8-mock==0.3 -flake8-per-file-ignores==0.7 flake8-polyfill==1.0.2 flake8-string-format==0.2.3 -flake8-tidy-imports==1.1.0 +flake8-tidy-imports==2.0.0 flake8-tuple==0.2.13 mccabe==0.6.1 -pathmatch==0.2.1 -pep8-naming==0.7.0 -pycodestyle==2.4.0 +pep8-naming==0.8.2 +pycodestyle==2.5.0 pydocstyle==3.0.0 -pyflakes==2.0.0 +pyflakes==2.1.0 six==1.12.0 snowballstemmer==1.2.1 -typing==3.6.6 diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw index 1f30b83ae..1bdca6974 100644 --- a/misc/requirements/requirements-flake8.txt-raw +++ b/misc/requirements/requirements-flake8.txt-raw @@ -8,7 +8,6 @@ flake8-deprecated flake8-docstrings flake8-future-import flake8-mock -flake8-per-file-ignores flake8-string-format flake8-tidy-imports flake8-tuple diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 6b8c63e97..8cac2edcd 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,8 +1,8 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -mypy==0.650 +mypy==0.670 mypy-extensions==0.4.1 -PyQt5==5.11.3 -PyQt5-sip==4.19.13 +# PyQt5==5.11.3 +# PyQt5-sip==4.19.14 -e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs -typed-ast==1.1.1 +typed-ast==1.3.1 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw index 636ad43a4..92a35ab74 100644 --- a/misc/requirements/requirements-mypy.txt-raw +++ b/misc/requirements/requirements-mypy.txt-raw @@ -3,3 +3,4 @@ mypy # remove @commit-id for scm installs #@ replace: @.*# @wip# +#@ ignore: PyQt5, PyQt5-sip diff --git a/misc/requirements/requirements-optional.txt b/misc/requirements/requirements-optional.txt index aafa38e46..52d067f69 100644 --- a/misc/requirements/requirements-optional.txt +++ b/misc/requirements/requirements-optional.txt @@ -2,6 +2,5 @@ colorama==0.4.1 cssutils==1.0.2 -hunter==2.1.0 +hunter==2.2.1 Pympler==0.6 -six==1.12.0 diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index f15a3a3e1..909fcc8c6 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -1,8 +1,8 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py appdirs==1.4.3 -packaging==18.0 -pyparsing==2.3.0 -setuptools==40.6.3 +packaging==19.0 +pyparsing==2.3.1 +setuptools==40.8.0 six==1.12.0 wheel==0.32.3 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index b3ecdaf70..3c0c51e60 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -5,8 +5,8 @@ astroid==2.1.0 certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 -cryptography==2.4.2 -github3.py==1.2.0 +cryptography==2.5 +github3.py==1.3.0 idna==2.8 isort==4.3.4 jwcrypto==0.6.0 @@ -14,10 +14,10 @@ lazy-object-proxy==1.3.1 mccabe==0.6.1 pycparser==2.19 pylint==2.2.2 -python-dateutil==2.7.5 +python-dateutil==2.8.0 ./scripts/dev/pylint_checkers requests==2.21.0 six==1.12.0 uritemplate==3.0.0 urllib3==1.24.1 -wrapt==1.10.11 +wrapt==1.11.1 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 32aee87a9..63722a679 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,4 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.11.3 -PyQt5-sip==4.19.13 +PyQt5==5.12 +PyQt5-sip==4.19.14 +PyQtWebEngine==5.12 diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw index 37a69c45a..9c6afbf16 100644 --- a/misc/requirements/requirements-pyqt.txt-raw +++ b/misc/requirements/requirements-pyqt.txt-raw @@ -1 +1,2 @@ -PyQt5 \ No newline at end of file +PyQt5 +PyQtWebEngine diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index c089895d1..14c247043 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -9,13 +9,13 @@ idna==2.8 imagesize==1.1.0 Jinja2==2.10 MarkupSafe==1.1.0 -packaging==18.0 +packaging==19.0 Pygments==2.3.1 -pyparsing==2.3.0 -pytz==2018.7 +pyparsing==2.3.1 +pytz==2018.9 requests==2.21.0 six==1.12.0 snowballstemmer==1.2.1 -Sphinx==1.8.3 +Sphinx==1.8.4 sphinxcontrib-websupport==1.1.0 urllib3==1.24.1 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 228045f8c..da28f08a0 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,41 +1,42 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -atomicwrites==1.2.1 +atomicwrites==1.3.0 attrs==18.2.0 backports.functools-lru-cache==1.5 -beautifulsoup4==4.7.0 -cheroot==6.5.3 +beautifulsoup4==4.7.1 +cheroot==6.5.4 Click==7.0 # colorama==0.4.1 coverage==4.5.2 EasyProcess==0.2.5 Flask==1.0.2 glob2==0.6 -hunter==2.1.0 -hypothesis==3.85.2 +hunter==2.2.1 +hypothesis==4.5.6 itsdangerous==1.1.0 # Jinja2==2.10 Mako==1.0.7 # MarkupSafe==1.1.0 more-itertools==5.0.0 -parse==1.9.0 +parse==1.11.1 parse-type==0.4.2 -pluggy==0.8.0 +pluggy==0.8.1 py==1.7.0 py-cpuinfo==4.0.0 -pytest==4.0.2 +pytest==4.2.0 pytest-bdd==3.0.1 -pytest-benchmark==3.1.1 -pytest-cov==2.6.0 +pytest-benchmark==3.2.2 +pytest-cov==2.6.1 pytest-faulthandler==1.5.0 pytest-instafail==0.4.0 -pytest-mock==1.10.0 +pytest-mock==1.10.1 pytest-qt==3.2.2 pytest-repeat==0.7.0 -pytest-rerunfailures==5.0 +pytest-rerunfailures==6.0 pytest-travis-fold==1.3.0 -pytest-xvfb==1.1.0 +pytest-xvfb==1.2.0 PyVirtualDisplay==0.2.1 six==1.12.0 +soupsieve==1.7.3 vulture==1.0 Werkzeug==0.14.1 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index ed0db2870..47ea7a4e3 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -1,9 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py filelock==3.0.10 -pluggy==0.8.0 +pluggy==0.8.1 py==1.7.0 six==1.12.0 toml==0.10.0 -tox==3.6.1 -virtualenv==16.1.0 +tox==3.7.0 +virtualenv==16.4.0 diff --git a/misc/userscripts/format_json b/misc/userscripts/format_json index 0d476b327..610cefbac 100755 --- a/misc/userscripts/format_json +++ b/misc/userscripts/format_json @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -euo pipefail # # Behavior: diff --git a/pytest.ini b/pytest.ini index c278b0591..15ce2993e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -65,10 +65,9 @@ qt_log_ignore = ^Icon theme ".*" not found ^Error receiving trust for a CA certificate ^QBackingStore::endPaint\(\) called with active painter on backingstore paint device + ^QPaintDevice: Cannot destroy paint device that is being painted xfail_strict = true filterwarnings = error - # This happens in many qutebrowser dependencies... - ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning - # WORKAROUND for https://github.com/ionelmc/pytest-benchmark/issues/124 - ignore:Node\.warn\(code, message\) form has been deprecated, use Node\.warn\(warning_instance\) instead:pytest.PytestDeprecationWarning + # WORKAROUND for https://github.com/pytest-dev/pytest-bdd/pull/288 + ignore:the `pytest\.config` global is deprecated\. Please use `request.config` or `pytest_configure` \(if you're a pytest plugin\) instead\. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 2b6896b76..ca0bc06a7 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -83,7 +83,7 @@ from qutebrowser.misc import utilcmds # pylint: enable=unused-import -qApp = None +q_app = None def run(args): @@ -101,25 +101,25 @@ def run(args): log.init.debug("Initializing config...") configinit.early_init(args) - global qApp - qApp = Application(args) - qApp.setOrganizationName("qutebrowser") - qApp.setApplicationName("qutebrowser") - qApp.setDesktopFileName("qutebrowser") - qApp.setApplicationVersion(qutebrowser.__version__) - qApp.lastWindowClosed.connect(quitter.on_last_window_closed) + global q_app + q_app = Application(args) + q_app.setOrganizationName("qutebrowser") + q_app.setApplicationName("qutebrowser") + q_app.setDesktopFileName("qutebrowser") + q_app.setApplicationVersion(qutebrowser.__version__) + q_app.lastWindowClosed.connect(quitter.on_last_window_closed) if args.version: print(version.version()) sys.exit(usertypes.Exit.ok) crash_handler = crashsignal.CrashHandler( - app=qApp, quitter=quitter, args=args, parent=qApp) + app=q_app, quitter=quitter, args=args, parent=q_app) crash_handler.activate() objreg.register('crash-handler', crash_handler) - signal_handler = crashsignal.SignalHandler(app=qApp, quitter=quitter, - parent=qApp) + signal_handler = crashsignal.SignalHandler(app=q_app, quitter=quitter, + parent=q_app) signal_handler.activate() objreg.register('signal-handler', signal_handler) @@ -151,7 +151,7 @@ def qt_mainloop(): WARNING: misc/crashdialog.py checks the stacktrace for this function name, so if this is changed, it should be changed there as well! """ - return qApp.exec_() + return q_app.exec_() def init(args, crash_handler): @@ -162,7 +162,7 @@ def init(args, crash_handler): crash_handler: The CrashHandler instance. """ log.init.debug("Starting init...") - qApp.setQuitOnLastWindowClosed(False) + q_app.setQuitOnLastWindowClosed(False) _init_icon() loader.init() @@ -175,12 +175,12 @@ def init(args, crash_handler): sys.exit(usertypes.Exit.err_init) log.init.debug("Initializing eventfilter...") - event_filter = EventFilter(qApp) - qApp.installEventFilter(event_filter) + event_filter = EventFilter(q_app) + q_app.installEventFilter(event_filter) objreg.register('event-filter', event_filter) log.init.debug("Connecting signals...") - qApp.focusChanged.connect(on_focus_changed) + q_app.focusChanged.connect(on_focus_changed) _process_args(args) @@ -207,7 +207,7 @@ def _init_icon(): if icon.isNull(): log.init.warning("Failed to load icon") else: - qApp.setWindowIcon(icon) + q_app.setWindowIcon(icon) def _process_args(args): @@ -220,7 +220,7 @@ def _process_args(args): window = mainwindow.MainWindow(private=None) if not args.nowindow: window.show() - qApp.setActiveWindow(window) + q_app.setActiveWindow(window) process_pos_args(args.command) _open_startpage() @@ -425,7 +425,7 @@ def _init_modules(args, crash_handler): crash_handler: The CrashHandler instance. """ log.init.debug("Initializing save manager...") - save_manager = savemanager.SaveManager(qApp) + save_manager = savemanager.SaveManager(q_app) objreg.register('save-manager', save_manager) configinit.late_init(save_manager) @@ -450,7 +450,7 @@ def _init_modules(args, crash_handler): sql.init(os.path.join(standarddir.data(), 'history.sqlite')) log.init.debug("Initializing web history...") - history.init(qApp) + history.init(q_app) except sql.SqlEnvironmentError as e: error.handle_fatal_exc(e, args, 'Error initializing SQL', pre_text='Error initializing SQL') @@ -464,31 +464,31 @@ def _init_modules(args, crash_handler): crash_handler.handle_segfault() log.init.debug("Initializing sessions...") - sessions.init(qApp) + sessions.init(q_app) log.init.debug("Initializing websettings...") websettings.init(args) log.init.debug("Initializing quickmarks...") - quickmark_manager = urlmarks.QuickmarkManager(qApp) + quickmark_manager = urlmarks.QuickmarkManager(q_app) objreg.register('quickmark-manager', quickmark_manager) log.init.debug("Initializing bookmarks...") - bookmark_manager = urlmarks.BookmarkManager(qApp) + bookmark_manager = urlmarks.BookmarkManager(q_app) objreg.register('bookmark-manager', bookmark_manager) log.init.debug("Initializing cookies...") - cookie_jar = cookies.CookieJar(qApp) - ram_cookie_jar = cookies.RAMCookieJar(qApp) + cookie_jar = cookies.CookieJar(q_app) + ram_cookie_jar = cookies.RAMCookieJar(q_app) objreg.register('cookie-jar', cookie_jar) objreg.register('ram-cookie-jar', ram_cookie_jar) log.init.debug("Initializing cache...") - diskcache = cache.DiskCache(standarddir.cache(), parent=qApp) + diskcache = cache.DiskCache(standarddir.cache(), parent=q_app) objreg.register('cache', diskcache) log.init.debug("Initializing downloads...") - download_manager = qtnetworkdownloads.DownloadManager(parent=qApp) + download_manager = qtnetworkdownloads.DownloadManager(parent=q_app) objreg.register('qtnetwork-download-manager', download_manager) log.init.debug("Initializing Greasemonkey...") @@ -735,7 +735,7 @@ class Quitter: def _shutdown(self, status, restart): # noqa """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") - if qApp is None: + if q_app is None: # No QApplication exists yet, so quit hard. sys.exit(status) # Remove eventfilter @@ -743,7 +743,7 @@ class Quitter: log.destroy.debug("Removing eventfilter...") event_filter = objreg.get('event-filter', None) if event_filter is not None: - qApp.removeEventFilter(event_filter) + q_app.removeEventFilter(event_filter) except AttributeError: pass # Close all windows @@ -792,7 +792,7 @@ class Quitter: session_manager.delete_autosave() # We use a singleshot timer to exit here to minimize the likelihood of # segfaults. - QTimer.singleShot(0, functools.partial(qApp.exit, status)) + QTimer.singleShot(0, functools.partial(q_app.exit, status)) class Application(QApplication): @@ -893,7 +893,7 @@ class EventFilter(QObject): Return: True if the event should be filtered, False if it's passed through. """ - if qApp.activeWindow() not in objreg.window_registry.values(): + if q_app.activeWindow() not in objreg.window_registry.values(): # Some other window (print dialog, etc.) is focused so we pass the # event through. return False diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index bd060820b..47bb1e537 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -208,6 +208,8 @@ class PACResolver: Return: A list of QNetworkProxy objects in order of preference. """ + qtutils.ensure_valid(query.url()) + if from_file: string_flags = QUrl.PrettyDecoded else: diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index d3e25c23c..f5685ea25 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -20,10 +20,12 @@ """Handling of proxies.""" +from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory from qutebrowser.config import config, configtypes -from qutebrowser.utils import objreg +from qutebrowser.utils import objreg, message, usertypes, urlutils +from qutebrowser.misc import objects from qutebrowser.browser.network import pac @@ -33,6 +35,18 @@ def init(): objreg.register('proxy-factory', proxy_factory) QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory) + config.instance.changed.connect(_warn_for_pac) + _warn_for_pac() + + +@config.change_filter('content.proxy', function=True) +def _warn_for_pac(): + """Show a warning if PAC is used with QtWebEngine.""" + proxy = config.val.content.proxy + if (isinstance(proxy, pac.PACFetcher) and + objects.backend == usertypes.Backend.QtWebEngine): + message.error("PAC support isn't implemented for QtWebEngine yet!") + def shutdown(): QNetworkProxyFactory.setApplicationProxyFactory(None) @@ -70,7 +84,11 @@ class ProxyFactory(QNetworkProxyFactory): # ref. http://doc.qt.io/qt-5/qnetworkproxyfactory.html#systemProxyForQuery proxies = QNetworkProxyFactory.systemProxyForQuery(query) elif isinstance(proxy, pac.PACFetcher): - proxies = proxy.resolve(query) + if objects.backend == usertypes.Backend.QtWebEngine: + # Looks like query.url() is always invalid on QtWebEngine... + proxies = [urlutils.proxy_from_url(QUrl('direct://'))] + else: + proxies = proxy.resolve(query) else: proxies = [proxy] for p in proxies: diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 14c43ad1e..6545dc4c0 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -340,19 +340,11 @@ def qute_gpl(_url): def _asciidoc_fallback_path(html_path): """Fall back to plaintext asciidoc if the HTML is unavailable.""" - asciidoc_path = html_path.replace('.html', '.asciidoc') - asciidoc_paths = [asciidoc_path] - if asciidoc_path.startswith('html/doc/'): - asciidoc_paths += [asciidoc_path.replace('html/doc/', '../doc/help/'), - asciidoc_path.replace('html/doc/', '../doc/')] - - for path in asciidoc_paths: - try: - return utils.read_file(path) - except OSError: - pass - - return None + path = html_path.replace('.html', '.asciidoc') + try: + return utils.read_file(path) + except OSError: + return None @add_handler('help') diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 0bf3301f9..92130be65 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -42,7 +42,6 @@ def custom_headers(url): if dnt_config is not None: dnt = b'1' if dnt_config else b'0' headers[b'DNT'] = dnt - headers[b'X-Do-Not-Track'] = dnt conf_headers = config.instance.get('content.headers.custom', url=url) for header, value in conf_headers.items(): diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 6dde42070..077ea775b 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -180,7 +180,21 @@ def _get_suggested_filename(path): See https://bugreports.qt.io/browse/QTBUG-56978 """ filename = os.path.basename(path) - filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename) + + suffix_re = re.compile(r""" + \ ? # Optional space between filename and suffix + ( + # Numerical suffix + \([0-9]+\) + | + # ISO-8601 suffix + # https://cs.chromium.org/chromium/src/base/time/time_to_iso8601.cc + \ -\ \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z + ) + (?=\.|$) # Begin of extension, or filename without extension + """, re.VERBOSE) + + filename = suffix_re.sub('', filename) if not qtutils.version_check('5.9', compiled=False): # https://bugreports.qt.io/browse/QTBUG-58155 filename = urllib.parse.unquote(filename) diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 816589514..821fc49dc 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -62,18 +62,33 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): """ try: initiator = job.initiator() + request_url = job.requestUrl() except AttributeError: # Added in Qt 5.11 return True - if initiator == QUrl('null') and not qtutils.version_check('5.12'): + # https://codereview.qt-project.org/#/c/234849/ + is_opaque = initiator == QUrl('null') + target = request_url.scheme(), request_url.host() + + if is_opaque and not qtutils.version_check('5.12'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421 + # When we don't register the qute:// scheme, all requests are + # flagged as opaque. + return True + + if (target == ('qute', 'testdata') and + is_opaque and + qtutils.version_check('5.12')): + # Allow requests to qute://testdata, as this is needed in Qt 5.12 + # for all tests to work properly. No qute://testdata handler is + # installed outside of tests. return True if initiator.isValid() and initiator.scheme() != 'qute': log.misc.warning("Blocking malicious request from {} to {}".format( initiator.toDisplayString(), - job.requestUrl().toDisplayString())) + request_url.toDisplayString())) job.fail(QWebEngineUrlRequestJob.RequestDenied) return False diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 10c4d4e6b..69b9d0319 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -25,6 +25,7 @@ Module attributes: """ import os +import operator from PyQt5.QtGui import QFont from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, @@ -163,9 +164,14 @@ class WebEngineSettings(websettings.AbstractSettings): # Qt 5.8 'content.print_element_backgrounds': ('PrintElementBackgrounds', None), + # Qt 5.11 'content.autoplay': - ('PlaybackRequiresUserGesture', lambda val: not val), + ('PlaybackRequiresUserGesture', operator.not_), + + # Qt 5.12 + 'content.dns_prefetch': + ('DnsPrefetchEnabled', None), } for name, (attribute, converter) in new_attributes.items(): try: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 22380cb1f..8f84779c4 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -715,8 +715,6 @@ class _WebEnginePermissions(QObject): """Handling of various permission-related signals.""" - _abort_questions = pyqtSignal() - def __init__(self, tab, parent=None): super().__init__(parent) self._tab = tab @@ -736,9 +734,6 @@ class _WebEnginePermissions(QObject): page.registerProtocolHandlerRequested.connect( self._on_register_protocol_handler_requested) - self._tab.shutting_down.connect(self._abort_questions) - self._tab.load_started.connect(self._abort_questions) - @pyqtSlot('QWebEngineFullScreenRequest') def _on_fullscreen_requested(self, request): request.accept() @@ -816,7 +811,7 @@ class _WebEnginePermissions(QObject): question = shared.feature_permission( url=url, option=options[feature], msg=messages[feature], yes_action=yes_action, no_action=no_action, - abort_on=[self._abort_questions]) + abort_on=[self._tab.abort_questions]) if question is not None: page.featurePermissionRequestCanceled.connect( @@ -844,7 +839,7 @@ class _WebEnginePermissions(QObject): option='content.persistent_storage', msg='use {} of persistent storage'.format(size), yes_action=request.accept, no_action=request.reject, - abort_on=[self._abort_questions], + abort_on=[self._tab.abort_questions], blocking=True) def _on_register_protocol_handler_requested(self, request): @@ -853,7 +848,7 @@ class _WebEnginePermissions(QObject): option='content.register_protocol_handler', msg='open all {} links'.format(request.scheme()), yes_action=request.accept, no_action=request.reject, - abort_on=[self._abort_questions], + abort_on=[self._tab.abort_questions], blocking=True) @@ -927,10 +922,14 @@ class _WebEngineScripts(QObject): utils.read_file('javascript/webelem.js'), utils.read_file('javascript/caret.js'), ) - self._inject_early_js('js', - utils.read_file('javascript/print.js'), - subframes=True, - world=QWebEngineScript.MainWorld) + if not qtutils.version_check('5.12'): + # WORKAROUND for Qt versions < 5.12 not exposing window.print(). + # Qt 5.12 has a printRequested() signal so we don't need this hack + # anymore. + self._inject_early_js('js', + utils.read_file('javascript/print.js'), + subframes=True, + world=QWebEngineScript.MainWorld) # FIXME:qtwebengine what about subframes=True? self._inject_early_js('js', js_code, subframes=True) self._init_stylesheet() @@ -1076,10 +1075,13 @@ class WebEngineTab(browsertab.AbstractTab): Signals: _load_finished_fake: Used in place of unreliable loadFinished + abort_questions: Emitted when a new load started or we're shutting + down. """ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223 _load_finished_fake = pyqtSignal(bool) + abort_questions = pyqtSignal() def __init__(self, *, win_id, mode_manager, private, parent=None): super().__init__(win_id=win_id, private=private, parent=parent) @@ -1252,15 +1254,13 @@ class WebEngineTab(browsertab.AbstractTab): answer = message.ask( title="Proxy authentication required", text=msg, mode=usertypes.PromptMode.user_pwd, - abort_on=[self.shutting_down, self.load_started], url=urlstr) + abort_on=[self.abort_questions], url=urlstr) if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) else: try: - # pylint: disable=no-member, useless-suppression sip.assign(authenticator, QAuthenticator()) - # pylint: enable=no-member, useless-suppression except AttributeError: self._show_error_page(url, "Proxy authentication required") @@ -1276,15 +1276,12 @@ class WebEngineTab(browsertab.AbstractTab): if not netrc_success: log.network.debug("Asking for credentials") - abort_on = [self.shutting_down, self.load_started] - answer = shared.authentication_required(url, authenticator, - abort_on) + answer = shared.authentication_required( + url, authenticator, abort_on=[self.abort_questions]) if not netrc_success and answer is None: log.network.debug("Aborting auth") try: - # pylint: disable=no-member, useless-suppression sip.assign(authenticator, QAuthenticator()) - # pylint: enable=no-member, useless-suppression except AttributeError: # WORKAROUND for # https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html @@ -1389,7 +1386,7 @@ class WebEngineTab(browsertab.AbstractTab): if error.is_overridable(): error.ignore = shared.ignore_certificate_errors( - url, [error], abort_on=[self.shutting_down, self.load_started]) + url, [error], abort_on=[self.abort_questions]) else: log.webview.error("Non-overridable certificate error: " "{}".format(error)) @@ -1418,15 +1415,20 @@ class WebEngineTab(browsertab.AbstractTab): if not qtutils.version_check('5.11.1', compiled=False): self.settings.update_for_url(url) + @pyqtSlot() + def _on_print_requested(self): + """Slot for window.print() in JS.""" + try: + self.printing.show_dialog() + except browsertab.WebTabError as e: + message.error(str(e)) + @pyqtSlot(usertypes.NavigationRequest) def _on_navigation_request(self, navigation): super()._on_navigation_request(navigation) if navigation.url == QUrl('qute://print'): - try: - self.printing.show_dialog() - except browsertab.WebTabError as e: - message.error(str(e)) + self._on_print_requested() navigation.accepted = False if not navigation.accepted or not navigation.is_main_frame: @@ -1458,6 +1460,37 @@ class WebEngineTab(browsertab.AbstractTab): if reload_needed: self._reload_url = navigation.url + def _on_select_client_certificate(self, selection): + """Handle client certificates. + + Currently, we simply pick the first available certificate and show an + additional note if there are multiple matches. + """ + certificate = selection.certificates()[0] + text = ('Subject: {subj}
' + 'Issuer: {issuer}
' + 'Serial: {serial}'.format( + subj=html_utils.escape(certificate.subjectDisplayName()), + issuer=html_utils.escape(certificate.issuerDisplayName()), + serial=bytes(certificate.serialNumber()).decode('ascii'))) + if len(selection.certificates()) > 1: + text += ('

Note: Multiple matching certificates ' + 'were found, but certificate selection is not ' + 'implemented yet!') + urlstr = selection.host().host() + + present = message.ask( + title='Present client certificate to {}?'.format(urlstr), + text=text, + mode=usertypes.PromptMode.yesno, + abort_on=[self.abort_questions], + url=urlstr) + + if present: + selection.select(certificate) + else: + selection.selectNone() + def _connect_signals(self): view = self._widget page = view.page() @@ -1473,6 +1506,11 @@ class WebEngineTab(browsertab.AbstractTab): page.contentsSizeChanged.connect(self.contents_size_changed) page.navigation_request.connect(self._on_navigation_request) + if qtutils.version_check('5.12'): + page.printRequested.connect(self._on_print_requested) + page.selectClientCertificate.connect( + self._on_select_client_certificate) + view.titleChanged.connect(self.title_changed) view.urlChanged.connect(self._on_url_changed) view.renderProcessTerminated.connect( @@ -1493,6 +1531,8 @@ class WebEngineTab(browsertab.AbstractTab): page.loadFinished.connect(self._on_load_finished) self.before_load_started.connect(self._on_before_load_started) + self.shutting_down.connect(self.abort_questions) + self.load_started.connect(self.abort_questions) # pylint: disable=protected-access self.audio._connect_signals() diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index e70226f30..9023bf037 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -64,6 +64,8 @@ class WebEngineView(QWebEngineView): Normally, this would always be the focusProxy(). However, it sometimes isn't, so we use this as a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68727 + + This got introduced in Qt 5.11.0 and fixed in 5.12.0. """ if 'lost-focusproxy' not in objreg.get('args').debug_flags: proxy = self.focusProxy() diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index c56fe2a9b..139f8b462 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -34,8 +34,7 @@ class FixedDataNetworkReply(QNetworkReply): """QNetworkReply subclass for fixed data.""" - def __init__(self, request, fileData, mimeType, # noqa: N803 - parent=None): + def __init__(self, request, fileData, mimeType, parent=None): # noqa: N803 """Constructor. Args: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 61e35fd53..e3f2ce397 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -91,15 +91,15 @@ def _parse_yaml_type( ) -> configtypes.BaseType: if isinstance(node, str): # e.g: - # type: Bool + # > type: Bool # -> create the type object without any arguments type_name = node kwargs = {} # type: typing.MutableMapping[str, typing.Any] elif isinstance(node, dict): # e.g: - # type: - # name: String - # none_ok: true + # > type: + # > name: String + # > none_ok: true # -> create the type object and pass arguments type_name = node.pop('name') kwargs = node @@ -164,6 +164,7 @@ def _parse_yaml_backends_dict( 'Qt 5.9.2': qtutils.version_check('5.9.2'), 'Qt 5.10': qtutils.version_check('5.10'), 'Qt 5.11': qtutils.version_check('5.11'), + 'Qt 5.12': qtutils.version_check('5.12'), } for key in sorted(node.keys()): if conditionals[node[key]]: diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 40f8c8b2a..e2d443280 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -379,7 +379,9 @@ content.developer_extras: content.dns_prefetch: default: true type: Bool - backend: QtWebKit + backend: + QtWebKit: true + QtWebEngine: Qt 5.12 supports_pattern: true desc: Try to pre-fetch DNS entries to speed up browsing. @@ -476,46 +478,14 @@ content.headers.user_agent: # 'ua_fetch.py' # Vim-protip: Place your cursor below this comment and run # :r!python scripts/dev/ua_fetch.py - - - "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:53.0) Gecko/20100101 - Firefox/53.0" - - Firefox 53.0 Win8.1 - - - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 - Firefox/53.0" - - Firefox 53.0 Linux - - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) - Gecko/20100101 Firefox/53.0" - - Firefox 53.0 MacOSX - - - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 - (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4" - - Safari Generic MacOSX - - - "Mozilla/5.0 (iPad; CPU OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 - (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1" - - Mobile Safari 10.0 iOS - - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/58.0.3029.110 Safari/537.36" - - Chrome Generic Win10 - - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 - (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" - - Chrome Generic MacOSX + like Gecko) Chrome/71.0.3578.98 Safari/537.36" + - Chrome 71.0 Win10 - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like - Gecko) Chrome/58.0.3029.110 Safari/537.36" - - Chrome Generic Linux - - - - "Mozilla/5.0 (compatible; Googlebot/2.1; - +http://www.google.com/bot.html" - - Google Bot - - - "Wget/1.16.1 (linux-gnu)" - - wget 1.16.1 - - - "curl/7.40.0" - - curl 7.40.0 - - - "Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 (KHTML, - like Gecko) Version/4.0 Mobile Safari/534.30" - - Mobile Generic Android - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like - Gecko" - - IE 11.0 for Desktop Win7 64-bit + Gecko) Chrome/71.0.3578.98 Safari/537.36" + - Chrome 71.0 Linux + - - "" + - Use default QtWebKit/QtWebEngine User-Agent supports_pattern: true desc: >- diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index b1cdeb26e..ce1cc167d 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -40,6 +40,11 @@ window._qutebrowser.stylesheet = (function() { // then move the stylesheet to the end. Partially inspired by Stylus: // https://github.com/openstyles/stylus/blob/1.1.4.2/content/apply.js#L235-L355 function watch_root() { + if (!document.documentElement) { + root_observer.observe(document, {"childList": true}); + return; + } + if (root_elem !== document.documentElement) { root_elem = document.documentElement; root_observer.disconnect(); @@ -53,7 +58,8 @@ window._qutebrowser.stylesheet = (function() { function create_style() { let ns = xhtml_ns; - if (document.documentElement.namespaceURI === svg_ns) { + if (document.documentElement && + document.documentElement.namespaceURI === svg_ns) { ns = svg_ns; } style_elem = document.createElementNS(ns, "style"); diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index cf8e13810..641ccc5f8 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -31,7 +31,7 @@ except ImportError: # pragma: no cover try: # Python2 from Tkinter import Tk # type: ignore - import tkMessageBox as messagebox # type: ignore + import tkMessageBox as messagebox # type: ignore # noqa: N813 except ImportError: # Some Python without Tk Tk = None # type: ignore diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 661c7b805..46ce8becd 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -162,7 +162,7 @@ class ConsoleWidget(QWidget): namespace = { '__name__': '__console__', '__doc__': None, - 'qApp': QApplication.instance(), + 'q_app': QApplication.instance(), # We use parent as self here because the user "feels" the whole # console, not just the line edit. 'self': parent, diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 690ede60f..61ae1dbfc 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -173,8 +173,10 @@ def check_qt_version(): PYQT_VERSION_STR) from pkg_resources import parse_version from qutebrowser.utils import log + parsed_qversion = parse_version(qVersion()) + if (QT_VERSION < 0x050701 or PYQT_VERSION < 0x050700 or - parse_version(qVersion()) < parse_version('5.7.1')): + parsed_qversion < parse_version('5.7.1')): text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, " "but Qt {} / PyQt {} is installed.".format(qt_version(), PYQT_VERSION_STR)) @@ -184,6 +186,12 @@ def check_qt_version(): log.init.warning("Running qutebrowser with Qt 5.8 is untested and " "unsupported!") + if (parsed_qversion >= parse_version('5.12') and + (PYQT_VERSION < 0x050c00 or QT_VERSION < 0x050c00)): + log.init.warning("Combining PyQt {} with Qt {} is unsupported! Ensure " + "all versions are newer than 5.12 to avoid potential " + "issues.".format(PYQT_VERSION_STR, qt_version())) + def check_ssl_support(): """Check if SSL support is available.""" @@ -199,19 +207,11 @@ def _check_modules(modules): for name, text in modules.items(): try: - # https://github.com/pallets/jinja/pull/628 - # https://bitbucket.org/birkenfeld/pygments-main/issues/1314/ - # https://github.com/pallets/jinja/issues/646 # https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e - messages = ['invalid escape sequence', - 'Flags not at the start of the expression'] # pylint: disable=bad-continuation with log.ignore_py_warnings( category=DeprecationWarning, - message=r'({})'.format('|'.join(messages)) - ), log.ignore_py_warnings( - category=PendingDeprecationWarning, - module='imp' + message=r'invalid escape sequence' ), log.ignore_py_warnings( category=ImportWarning, message=r'Not importing directory .*: missing __init__' diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 3c718de7b..ef913f94b 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -65,11 +65,9 @@ def _get_socketname(basedir): data_to_hash = '-'.join(parts_to_hash).encode('utf-8') md5 = hashlib.md5(data_to_hash).hexdigest() - target_dir = standarddir.runtime() - - parts = ['ipc'] - parts.append(md5) - return os.path.join(target_dir, '-'.join(parts)) + prefix = 'i-' if utils.is_mac else 'ipc-' + filename = '{}{}'.format(prefix, md5) + return os.path.join(standarddir.runtime(), filename) class Error(Exception): diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 5373e76aa..520ced252 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -40,7 +40,7 @@ from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover - qWebKitVersion = None # type: ignore + qWebKitVersion = None # type: ignore # noqa: N816 MAXVALS = { diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 2d517043a..8c2d733ae 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -30,6 +30,7 @@ import datetime import traceback import functools import contextlib +import posixpath import socket import shlex import glob @@ -165,6 +166,9 @@ def read_file(filename, binary=False): Return: The file contents as string. """ + assert not posixpath.isabs(filename), filename + assert os.path.pardir not in filename.split(posixpath.sep), filename + if not binary and filename in _resource_cache: return _resource_cache[filename] @@ -655,7 +659,15 @@ def expand_windows_drive(path): def yaml_load(f): """Wrapper over yaml.load using the C loader if possible.""" start = datetime.datetime.now() - data = yaml.load(f, Loader=YamlLoader) + + # WORKAROUND for https://github.com/yaml/pyyaml/pull/181 + with log.ignore_py_warnings( + category=DeprecationWarning, + message=r"Using or importing the ABCs from 'collections' instead " + r"of from 'collections\.abc' is deprecated, and in 3\.8 it will " + r"stop working"): + data = yaml.load(f, Loader=YamlLoader) + end = datetime.datetime.now() delta = (end - start).total_seconds() diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index a52e31ed8..962e328d6 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import QApplication try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover - qWebKitVersion = None # type: ignore + qWebKitVersion = None # type: ignore # noqa: N816 try: from PyQt5.QtWebEngineWidgets import QWebEngineProfile @@ -324,7 +324,7 @@ def _chromium_version(): Qt 5.9: Chromium 56 (LTS) 56.0.2924.122 (2017-01-25) - 5.9.6: Security fixes up to 66.0.3359.170 (2018-05-10) + 5.9.7: Security fixes up to 69.0.3497.113 (2018-09-27) Qt 5.10: Chromium 61 61.0.3163.140 (2017-09-05) @@ -332,11 +332,14 @@ def _chromium_version(): Qt 5.11: Chromium 65 65.0.3325.151 (.1: .230) (2018-03-06) - 5.11.2: Security fixes up to 68.0.3440.75 (2018-07-24) + 5.11.3: Security fixes up to 70.0.3538.102 (2018-11-09) Qt 5.12: Chromium 69 - 69.0.3497.128 (~2018-09-17) - 5.12.0: Security fixes up to 70.0.3538.67 (2018-10-16) + (LTS) 69.0.3497.113 (2018-09-27) + 5.12.1: Security fixes up to 71.0.3578.94 (2018-12-14) + 5.12.2: Security fixes up to 72.0.3626.96 (2019-02-06) + + Qt 5.13: (in development) Chromium 71 merged, 73 in review. Also see https://www.chromium.org/developers/calendar and https://chromereleases.googleblog.com/ diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh index c736a01d3..064f4098a 100644 --- a/scripts/dev/ci/travis_install.sh +++ b/scripts/dev/ci/travis_install.sh @@ -53,43 +53,13 @@ npm_install() { travis_retry npm install -g "$@" } -check_pyqt() { - python3 < And I open headers Then the header Dnt should be set to - And the header X-Do-Not-Track should be set to Scenario: Accept-Language header When I set content.headers.accept_language to en,de diff --git a/tests/end2end/features/navigate.feature b/tests/end2end/features/navigate.feature index 07bd56c69..2596f3ef1 100644 --- a/tests/end2end/features/navigate.feature +++ b/tests/end2end/features/navigate.feature @@ -24,7 +24,8 @@ Feature: Using :navigate Then data/navigate should be loaded Scenario: Navigating up in qute://help/ - When I open qute://help/commands.html + When the documentation is up to date + And I open qute://help/commands.html And I run :navigate up Then qute://help/ should be loaded diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index 0f5954e19..35c110dc5 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -8,7 +8,8 @@ Feature: Special qute:// pages # :help Scenario: :help without topic - When I run :tab-only + When the documentation is up to date + And I run :tab-only And I run :help And I wait until qute://help/index.html is loaded Then the following tabs should be open: @@ -39,7 +40,8 @@ Feature: Special qute:// pages - qute://help/settings.html#editor.command (active) Scenario: :help with -t - When I run :tab-only + When the documentation is up to date + And I run :tab-only And I run :help -t And I wait until qute://help/index.html is loaded Then the following tabs should be open: @@ -140,29 +142,25 @@ Feature: Special qute:// pages And I press the key "" Then "Invalid value 'foo' *" should be logged - @qtwebkit_skip - Scenario: qute://settings CSRF via img (webengine) + Scenario: qute://settings CSRF via img When I open data/misc/qutescheme_csrf.html And I run :click-element id via-img - Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged + Then the img request should be blocked - @qtwebkit_skip - Scenario: qute://settings CSRF via link (webengine) + Scenario: qute://settings CSRF via link When I open data/misc/qutescheme_csrf.html And I run :click-element id via-link - Then "Blocking malicious request from qute://settings/set?* to qute://settings/set?*" should be logged + Then the link request should be blocked - @qtwebkit_skip - Scenario: qute://settings CSRF via redirect (webengine) + Scenario: qute://settings CSRF via redirect When I open data/misc/qutescheme_csrf.html And I run :click-element id via-redirect - Then "Blocking malicious request from qute://settings/set?* to qute://settings/set?*" should be logged + Then the redirect request should be blocked - @qtwebkit_skip - Scenario: qute://settings CSRF via form (webengine) + Scenario: qute://settings CSRF via form When I open data/misc/qutescheme_csrf.html And I run :click-element id via-form - Then "Blocking malicious request from qute://settings/set?* to qute://settings/set?*" should be logged + Then the form request should be blocked @qtwebkit_skip Scenario: qute://settings CSRF token (webengine) @@ -171,32 +169,6 @@ Feature: Special qute:// pages Then "RequestDeniedError while handling qute://* URL" should be logged And the error "Invalid CSRF token for qute://settings!" should be shown - @qtwebengine_skip - Scenario: qute://settings CSRF via img (webkit) - When I open data/misc/qutescheme_csrf.html - And I run :click-element id via-img - Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged - - @qtwebengine_skip - Scenario: qute://settings CSRF via link (webkit) - When I open data/misc/qutescheme_csrf.html - And I run :click-element id via-link - Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged - And "Error while loading qute://settings/set?*: Invalid qute://settings request" should be logged - - @qtwebengine_skip - Scenario: qute://settings CSRF via redirect (webkit) - When I open data/misc/qutescheme_csrf.html - And I run :click-element id via-redirect - Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged - And "Error while loading qute://settings/set?*: Invalid qute://settings request" should be logged - - @qtwebengine_skip - Scenario: qute://settings CSRF via form (webkit) - When I open data/misc/qutescheme_csrf.html - And I run :click-element id via-form - Then "Error while loading qute://settings/set?*: Unsupported request type" should be logged - # pdfjs support Scenario: pdfjs is used for pdf files diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 626a88ba8..494feb0ba 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -228,7 +228,7 @@ Feature: Saving and loading sessions url: http://localhost:*/data/hello.txt # Seems like that bug is fixed upstream in QtWebEngine - @qtwebkit_skip @flaky + @skip # Too flaky Scenario: Saving a session with a page using history.replaceState() and navigating away When I open data/sessions/history_replace_state.html without waiting And I wait for "* Called history.replaceState" in the log diff --git a/tests/end2end/features/test_qutescheme_bdd.py b/tests/end2end/features/test_qutescheme_bdd.py index 8706a1a9c..ae66e24d0 100644 --- a/tests/end2end/features/test_qutescheme_bdd.py +++ b/tests/end2end/features/test_qutescheme_bdd.py @@ -17,40 +17,58 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import sys -import os.path -import subprocess - -import pytest import pytest_bdd as bdd -import qutebrowser -from qutebrowser.utils import docutils +from qutebrowser.utils import qtutils + bdd.scenarios('qutescheme.feature') -@bdd.when("the documentation is up to date") -def update_documentation(): - """Update the docs before testing :help.""" - base_path = os.path.dirname(os.path.abspath(qutebrowser.__file__)) - doc_path = os.path.join(base_path, 'html', 'doc') - script_path = os.path.join(base_path, '..', 'scripts') +@bdd.then(bdd.parsers.parse("the {kind} request should be blocked")) +def request_blocked(request, quteproc, kind): + blocking_set_msg = ( + "Blocking malicious request from qute://settings/set?* to " + "qute://settings/set?*") + blocking_csrf_msg = ( + "Blocking malicious request from " + "http://localhost:*/data/misc/qutescheme_csrf.html to " + "qute://settings/set?*") + blocking_js_msg = ( + "[http://localhost:*/data/misc/qutescheme_csrf.html:0] Not allowed to " + "load local resource: qute://settings/set?*" + ) - try: - os.mkdir(doc_path) - except FileExistsError: - pass + webkit_error_invalid = ( + "Error while loading qute://settings/set?*: Invalid qute://settings " + "request") + webkit_error_unsupported = ( + "Error while loading qute://settings/set?*: Unsupported request type") - files = os.listdir(doc_path) - if files and all(docutils.docs_up_to_date(p) for p in files): - return + if request.config.webengine and qtutils.version_check('5.12'): + # On Qt 5.12, we mark qute:// as a local scheme, causing most requests + # being blocked by Chromium internally (logging to the JS console). + expected_messages = { + 'img': [blocking_js_msg], + 'link': [blocking_js_msg], + 'redirect': [blocking_set_msg], + 'form': [blocking_js_msg], + } + elif request.config.webengine: + expected_messages = { + 'img': [blocking_csrf_msg], + 'link': [blocking_set_msg], + 'redirect': [blocking_set_msg], + 'form': [blocking_set_msg], + } + else: # QtWebKit + expected_messages = { + 'img': [blocking_csrf_msg], + 'link': [blocking_csrf_msg, webkit_error_invalid], + 'redirect': [blocking_csrf_msg, webkit_error_invalid], + 'form': [webkit_error_unsupported], + } - try: - subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) - except OSError: - pytest.skip("Docs outdated and asciidoc unavailable!") - - update_script = os.path.join(script_path, 'asciidoc2html.py') - subprocess.run([sys.executable, update_script]) + for pattern in expected_messages[kind]: + msg = quteproc.wait_for(message=pattern) + msg.expected = True diff --git a/tests/unit/browser/test_shared.py b/tests/unit/browser/test_shared.py index 78302d8c1..b8da41a02 100644 --- a/tests/unit/browser/test_shared.py +++ b/tests/unit/browser/test_shared.py @@ -26,18 +26,15 @@ from qutebrowser.browser import shared @pytest.mark.parametrize('dnt, accept_language, custom_headers, expected', [ # DNT - (True, None, {}, {b'DNT': b'1', b'X-Do-Not-Track': b'1'}), - (False, None, {}, {b'DNT': b'0', b'X-Do-Not-Track': b'0'}), + (True, None, {}, {b'DNT': b'1'}), + (False, None, {}, {b'DNT': b'0'}), (None, None, {}, {}), # Accept-Language - (False, 'de, en', {}, {b'DNT': b'0', b'X-Do-Not-Track': b'0', - b'Accept-Language': b'de, en'}), + (False, 'de, en', {}, {b'DNT': b'0', b'Accept-Language': b'de, en'}), # Custom headers - (False, None, {'X-Qute': 'yes'}, {b'DNT': b'0', b'X-Do-Not-Track': b'0', - b'X-Qute': b'yes'}), + (False, None, {'X-Qute': 'yes'}, {b'DNT': b'0', b'X-Qute': b'yes'}), # Mixed (False, 'de, en', {'X-Qute': 'yes'}, {b'DNT': b'0', - b'X-Do-Not-Track': b'0', b'Accept-Language': b'de, en', b'X-Qute': b'yes'}), ]) diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py index a34962522..4ca447f56 100644 --- a/tests/unit/browser/webengine/test_webenginedownloads.py +++ b/tests/unit/browser/webengine/test_webenginedownloads.py @@ -30,6 +30,8 @@ from helpers import utils @pytest.mark.parametrize('path, expected', [ (os.path.join('subfolder', 'foo'), 'foo'), ('foo(1)', 'foo'), + ('foo (1)', 'foo'), + ('foo - 1970-01-01T00:00:00.000Z', 'foo'), ('foo(a)', 'foo(a)'), ('foo1', 'foo1'), pytest.param('foo%20bar', 'foo bar', marks=utils.qt58), diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py index 24f5bdf0d..719a51016 100644 --- a/tests/unit/completion/test_completionmodel.py +++ b/tests/unit/completion/test_completionmodel.py @@ -41,7 +41,7 @@ def test_first_last_item(counts): cat = mock.Mock(spec=['layoutChanged', 'layoutAboutToBeChanged']) cat.rowCount = mock.Mock(return_value=c, spec=[]) model.add_category(cat) - data = [i for i, rowCount in enumerate(counts) if rowCount > 0] + data = [i for i, row_count in enumerate(counts) if row_count > 0] if not data: # with no items, first and last should be an invalid index assert not model.first_item().isValid() diff --git a/tests/unit/components/test_adblock.py b/tests/unit/components/test_adblock.py index f37b57962..d63c802c8 100644 --- a/tests/unit/components/test_adblock.py +++ b/tests/unit/components/test_adblock.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.components import adblock from qutebrowser.utils import urlmatch -from tests.helpers import utils +from helpers import utils pytestmark = pytest.mark.usefixtures('qapp') diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index b0b85d997..78116be20 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -38,7 +38,7 @@ from qutebrowser.config import configtypes, configexc, configutils from qutebrowser.utils import debug, utils, qtutils, urlmatch from qutebrowser.browser.network import pac from qutebrowser.keyinput import keyutils -from tests.helpers import utils as testutils +from helpers import utils as testutils class Font(QFont): diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index d6e7bce34..f69d90446 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import Qt, QEvent, pyqtSignal from PyQt5.QtGui import QKeyEvent, QKeySequence from PyQt5.QtWidgets import QWidget -from tests.unit.keyinput import key_data +from unit.keyinput import key_data from qutebrowser.keyinput import keyutils from qutebrowser.utils import utils diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 29ca0ff9d..eb12f78e3 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -34,7 +34,7 @@ from PyQt5.QtTest import QSignalSpy import qutebrowser from qutebrowser.misc import ipc -from qutebrowser.utils import standarddir, utils, qtutils +from qutebrowser.utils import standarddir, utils from helpers import stubs @@ -98,7 +98,7 @@ class FakeSocket(QObject): _connect_successful: The value returned for waitForConnected(). """ - readyRead = pyqtSignal() + readyRead = pyqtSignal() # noqa: N815 disconnected = pyqtSignal() def __init__(self, *, error=QLocalSocket.UnknownSocketError, state=None, @@ -177,11 +177,6 @@ def md5(inp): class TestSocketName: - POSIX_TESTS = [ - (None, 'ipc-{}'.format(md5('testusername'))), - ('/x', 'ipc-{}'.format(md5('testusername-/x'))), - ] - WINDOWS_TESTS = [ (None, 'qutebrowser-testusername'), ('/x', 'qutebrowser-testusername-{}'.format(md5('/x'))), @@ -203,7 +198,10 @@ class TestSocketName: assert socketname == expected @pytest.mark.mac - @pytest.mark.parametrize('basedir, expected', POSIX_TESTS) + @pytest.mark.parametrize('basedir, expected', [ + (None, 'i-{}'.format(md5('testusername'))), + ('/x', 'i-{}'.format(md5('testusername-/x'))), + ]) def test_mac(self, basedir, expected): socketname = ipc._get_socketname(basedir) parts = socketname.split(os.sep) @@ -211,7 +209,10 @@ class TestSocketName: assert parts[-1] == expected @pytest.mark.linux - @pytest.mark.parametrize('basedir, expected', POSIX_TESTS) + @pytest.mark.parametrize('basedir, expected', [ + (None, 'ipc-{}'.format(md5('testusername'))), + ('/x', 'ipc-{}'.format(md5('testusername-/x'))), + ]) def test_linux(self, basedir, fake_runtime_dir, expected): socketname = ipc._get_socketname(basedir) expected_path = str(fake_runtime_dir / 'qutebrowser' / expected) @@ -630,8 +631,6 @@ class TestSendOrListen: assert ret_client is None @pytest.mark.posix(reason="Unneeded on Windows") - @pytest.mark.xfail(qtutils.version_check('5.12', compiled=False) and - utils.is_mac, reason="Broken, see #4471") def test_correct_socket_name(self, args): server = ipc.send_or_listen(args) expected_dir = ipc._get_socketname(args.basedir) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 3d7555bc4..9b2916ba8 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -30,7 +30,7 @@ def test_on_focus_changed_issue1484(monkeypatch, qapp, caplog): For some reason, Qt sometimes calls on_focus_changed() with a QBuffer as argument. Let's make sure we handle that gracefully. """ - monkeypatch.setattr(app, 'qApp', qapp) + monkeypatch.setattr(app, 'q_app', qapp) buf = QBuffer() app.on_focus_changed(buf, buf) diff --git a/tox.ini b/tox.ini index 75a00961e..1dfc5ac5a 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,8 @@ skipsdist = true setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms PYTEST_QT_API=pyqt5 - pyqt{,56,571,59,510,511}: LINK_PYQT_SKIP=true - pyqt{,56,571,59,510,511}: QUTE_BDD_WEBENGINE=true + pyqt{,56,571,59,510,511,512}: LINK_PYQT_SKIP=true + pyqt{,56,571,59,510,511,512}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER QT_QUICK_BACKEND basepython = @@ -29,6 +29,7 @@ deps = pyqt59: PyQt5==5.9.2 pyqt510: PyQt5==5.10.1 pyqt511: PyQt5==5.11.3 + pyqt512: PyQtWebEngine==5.12 commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -bb -m pytest {posargs:tests}