summaryrefslogtreecommitdiff
path: root/qutebrowser
diff options
context:
space:
mode:
Diffstat (limited to 'qutebrowser')
-rw-r--r--qutebrowser/__init__.py5
-rw-r--r--qutebrowser/app.py3
-rw-r--r--qutebrowser/browser/commands.py2
-rw-r--r--qutebrowser/browser/pdfjs.py27
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py2
-rw-r--r--qutebrowser/browser/webelem.py10
-rw-r--r--qutebrowser/browser/webengine/darkmode.py47
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py20
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py10
-rw-r--r--qutebrowser/commands/userscripts.py1
-rw-r--r--qutebrowser/completion/completer.py6
-rw-r--r--qutebrowser/completion/completionwidget.py2
-rw-r--r--qutebrowser/config/configdata.py5
-rw-r--r--qutebrowser/config/configdata.yml5
-rw-r--r--qutebrowser/html/version.html2
-rw-r--r--qutebrowser/keyinput/keyutils.py2
-rw-r--r--qutebrowser/mainwindow/messageview.py4
-rw-r--r--qutebrowser/mainwindow/statusbar/clock.py5
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py28
-rw-r--r--qutebrowser/mainwindow/tabwidget.py2
-rw-r--r--qutebrowser/misc/elf.py2
-rw-r--r--qutebrowser/misc/httpclient.py4
-rw-r--r--qutebrowser/misc/ipc.py5
-rw-r--r--qutebrowser/misc/pakjoy.py6
-rw-r--r--qutebrowser/qt/machinery.py2
-rw-r--r--qutebrowser/qt/webenginewidgets.py1
-rw-r--r--qutebrowser/qt/widgets.py1
-rw-r--r--qutebrowser/utils/qtutils.py17
-rw-r--r--qutebrowser/utils/resources.py8
-rw-r--r--qutebrowser/utils/usertypes.py38
-rw-r--r--qutebrowser/utils/utils.py8
-rw-r--r--qutebrowser/utils/version.py235
32 files changed, 345 insertions, 170 deletions
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index 5df8d7a31..ff2f94675 100644
--- a/qutebrowser/__init__.py
+++ b/qutebrowser/__init__.py
@@ -5,9 +5,12 @@
"""A keyboard-driven, vim-like browser based on Python and Qt."""
import os.path
+import datetime
+
+_year = datetime.date.today().year
__author__ = "Florian Bruhin"
-__copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)"
+__copyright__ = "Copyright 2013-{} Florian Bruhin (The Compiler)".format(_year)
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 015715eef..51603a2b9 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -132,6 +132,9 @@ def init(*, args: argparse.Namespace) -> None:
crashsignal.crash_handler.init_faulthandler()
objects.qapp.setQuitOnLastWindowClosed(False)
+ # WORKAROUND for KDE file dialogs / QEventLoopLocker quitting:
+ # https://bugreports.qt.io/browse/QTBUG-124386
+ objects.qapp.setQuitLockEnabled(False)
quitter.instance.shutting_down.connect(QApplication.closeAllWindows)
_init_icon()
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 83a846b85..06298a8ca 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -948,6 +948,8 @@ class CommandDispatcher:
"No window specified and couldn't find active window!")
assert isinstance(active_win, mainwindow.MainWindow), active_win
win_id = active_win.win_id
+ else:
+ raise utils.Unreachable(index_parts)
if win_id not in objreg.window_registry:
raise cmdutils.CommandError(
diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py
index 841285deb..7062febb1 100644
--- a/qutebrowser/browser/pdfjs.py
+++ b/qutebrowser/browser/pdfjs.py
@@ -69,6 +69,21 @@ def generate_pdfjs_page(filename, url):
return html
+def _generate_polyfills():
+ return """
+ if (typeof Promise.withResolvers === 'undefined') {
+ Promise.withResolvers = function () {
+ let resolve, reject
+ const promise = new Promise((res, rej) => {
+ resolve = res
+ reject = rej
+ })
+ return { promise, resolve, reject }
+ }
+ }
+ """
+
+
def _generate_pdfjs_script(filename):
"""Generate the script that shows the pdf with pdf.js.
@@ -83,6 +98,8 @@ def _generate_pdfjs_script(filename):
js_url = javascript.to_js(url.toString(urlutils.FormatOption.ENCODED))
return jinja.js_environment.from_string("""
+ {{ polyfills }}
+
document.addEventListener("DOMContentLoaded", function() {
if (typeof window.PDFJS !== 'undefined') {
// v1.x
@@ -104,7 +121,7 @@ def _generate_pdfjs_script(filename):
});
}
});
- """).render(url=js_url)
+ """).render(url=js_url, polyfills=_generate_polyfills())
def get_pdfjs_res_and_path(path):
@@ -148,6 +165,14 @@ def get_pdfjs_res_and_path(path):
log.misc.warning("OSError while reading PDF.js file: {}".format(e))
raise PDFJSNotFound(path) from None
+ if path == "build/pdf.worker.mjs":
+ content = b"\n".join(
+ [
+ _generate_polyfills().encode("ascii"),
+ content,
+ ]
+ )
+
return content, file_path
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 0360eed66..63122208f 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -124,7 +124,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
log.downloads.exception("Error while closing file object")
if pos == 0:
- # Emtpy remaining file
+ # Empty remaining file
filename = self._get_open_filename()
log.downloads.debug(f"Removing empty file at {filename}")
try:
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index 2356ad086..721ab83df 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -355,10 +355,14 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
QMouseEvent(QEvent.Type.MouseButtonRelease, pos, button, Qt.MouseButton.NoButton, modifiers),
]
- for evt in events:
- self._tab.send_event(evt)
+ def _send_events_after_delay() -> None:
+ """Delay clicks to workaround timing issue in e2e tests on 6.7."""
+ for evt in events:
+ self._tab.send_event(evt)
- QTimer.singleShot(0, self._move_text_cursor)
+ QTimer.singleShot(0, self._move_text_cursor)
+
+ QTimer.singleShot(10, _send_events_after_delay)
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click on an editable input field."""
diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py
index b1b81c61e..8f1908547 100644
--- a/qutebrowser/browser/webengine/darkmode.py
+++ b/qutebrowser/browser/webengine/darkmode.py
@@ -113,6 +113,11 @@ Qt 6.6
- New alternative image classifier:
https://chromium-review.googlesource.com/c/chromium/src/+/3987823
+
+Qt 6.7
+------
+
+Enabling dark mode can now be done at runtime via QWebEngineSettings.
"""
import os
@@ -126,6 +131,10 @@ from typing import (Any, Iterator, Mapping, MutableMapping, Optional, Set, Tuple
from qutebrowser.config import config
from qutebrowser.utils import usertypes, utils, log, version
+# Note: We *cannot* initialize QtWebEngine (even implicitly) in here, but checking for
+# the enum attribute seems to be okay.
+from qutebrowser.qt.webenginecore import QWebEngineSettings
+
_BLINK_SETTINGS = 'blink-settings'
@@ -138,6 +147,7 @@ class Variant(enum.Enum):
qt_515_3 = enum.auto()
qt_64 = enum.auto()
qt_66 = enum.auto()
+ qt_67 = enum.auto()
# Mapping from a colors.webpage.darkmode.algorithm setting value to
@@ -187,11 +197,6 @@ _BOOLS = {
False: 'false',
}
-_INT_BOOLS = {
- True: '1',
- False: '0',
-}
-
@dataclasses.dataclass
class _Setting:
@@ -260,26 +265,25 @@ class _Definition:
switch = self._switch_names.get(setting.option, self._switch_names[None])
yield switch, setting.with_prefix(self.prefix)
- def copy_with(self, attr: str, value: Any) -> '_Definition':
- """Get a new _Definition object with a changed attribute.
-
- NOTE: This does *not* copy the settings list. Both objects will reference the
- same (immutable) tuple.
- """
- new = copy.copy(self)
- setattr(new, attr, value)
- return new
-
def copy_add_setting(self, setting: _Setting) -> '_Definition':
"""Get a new _Definition object with an additional setting."""
new = copy.copy(self)
new._settings = self._settings + (setting,) # pylint: disable=protected-access
return new
+ def copy_remove_setting(self, name: str) -> '_Definition':
+ """Get a new _Definition object with a setting removed."""
+ new = copy.copy(self)
+ filtered_settings = tuple(s for s in self._settings if s.option != name)
+ if len(filtered_settings) == len(self._settings):
+ raise ValueError(f"Setting {name} not found in {self}")
+ new._settings = filtered_settings # pylint: disable=protected-access
+ return new
+
def copy_replace_setting(self, option: str, chromium_key: str) -> '_Definition':
"""Get a new _Definition object with `old` replaced by `new`.
- If `old` is not in the settings list, return the old _Definition object.
+ If `old` is not in the settings list, raise ValueError.
"""
new = copy.deepcopy(self)
@@ -332,6 +336,8 @@ _DEFINITIONS[Variant.qt_64] = _DEFINITIONS[Variant.qt_515_3].copy_replace_settin
_DEFINITIONS[Variant.qt_66] = _DEFINITIONS[Variant.qt_64].copy_add_setting(
_Setting('policy.images', 'ImageClassifierPolicy', _IMAGE_CLASSIFIERS),
)
+# Qt 6.7: Enabled is now handled dynamically via QWebEngineSettings
+_DEFINITIONS[Variant.qt_67] = _DEFINITIONS[Variant.qt_66].copy_remove_setting('enabled')
_SettingValType = Union[str, usertypes.Unset]
@@ -367,7 +373,14 @@ def _variant(versions: version.WebEngineVersions) -> Variant:
except KeyError:
log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
- if versions.webengine >= utils.VersionNumber(6, 6):
+ if (
+ # We need a PyQt 6.7 as well with the API available, otherwise we can't turn on
+ # dark mode later in webenginesettings.py.
+ versions.webengine >= utils.VersionNumber(6, 7) and
+ hasattr(QWebEngineSettings.WebAttribute, 'ForceDarkMode')
+ ):
+ return Variant.qt_67
+ elif versions.webengine >= utils.VersionNumber(6, 6):
return Variant.qt_66
elif versions.webengine >= utils.VersionNumber(6, 4):
return Variant.qt_64
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index 78a4946ad..fd0d8c8de 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -148,12 +148,20 @@ class WebEngineSettings(websettings.AbstractSettings):
Attr(QWebEngineSettings.WebAttribute.AutoLoadIconsForPage,
converter=lambda val: val != 'never'),
}
- try:
- _ATTRIBUTES['content.canvas_reading'] = Attr(
- QWebEngineSettings.WebAttribute.ReadingFromCanvasEnabled) # type: ignore[attr-defined,unused-ignore]
- except AttributeError:
- # Added in QtWebEngine 6.6
- pass
+
+ if machinery.IS_QT6:
+ try:
+ _ATTRIBUTES['content.canvas_reading'] = Attr(
+ QWebEngineSettings.WebAttribute.ReadingFromCanvasEnabled)
+ except AttributeError:
+ # Added in QtWebEngine 6.6
+ pass
+ try:
+ _ATTRIBUTES['colors.webpage.darkmode.enabled'] = Attr(
+ QWebEngineSettings.WebAttribute.ForceDarkMode)
+ except AttributeError:
+ # Added in QtWebEngine 6.7
+ pass
_FONT_SIZES = {
'fonts.web.size.minimum':
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 02d912a50..48ae7ea50 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -12,7 +12,7 @@ import re
import html as html_utils
from typing import cast, Union, Optional
-from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
+from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QObject, QByteArray)
from qutebrowser.qt.network import QAuthenticator
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
@@ -816,7 +816,7 @@ class WebEngineAudio(browsertab.AbstractAudio):
# Implements the intended two-second delay specified at
# https://doc.qt.io/archives/qt-5.14/qwebenginepage.html#recentlyAudibleChanged
delay_ms = 2000
- self._silence_timer = QTimer(self)
+ self._silence_timer = usertypes.Timer(self)
self._silence_timer.setSingleShot(True)
self._silence_timer.setInterval(delay_ms)
@@ -1477,9 +1477,9 @@ class WebEngineTab(browsertab.AbstractTab):
log.network.debug("Asking for credentials")
answer = shared.authentication_required(
url, authenticator, abort_on=[self.abort_questions])
- if not netrc_success and answer is None:
- log.network.debug("Aborting auth")
- sip.assign(authenticator, QAuthenticator())
+ if answer is None:
+ log.network.debug("Aborting auth")
+ sip.assign(authenticator, QAuthenticator())
@pyqtSlot()
def _on_load_started(self):
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
index f4ddd2bf4..01710a63c 100644
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -330,7 +330,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
self._filepath = handle.name
except OSError as e:
message.error("Error while creating tempfile: {}".format(e))
- return
class Error(Exception):
diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
index 49a97c9cb..846fa7c22 100644
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -7,12 +7,12 @@
import dataclasses
from typing import TYPE_CHECKING
-from qutebrowser.qt.core import pyqtSlot, QObject, QTimer
+from qutebrowser.qt.core import pyqtSlot, QObject
from qutebrowser.config import config
from qutebrowser.commands import parser, cmdexc
from qutebrowser.misc import objects, split
-from qutebrowser.utils import log, utils, debug, objreg
+from qutebrowser.utils import log, utils, debug, objreg, usertypes
from qutebrowser.completion.models import miscmodels
from qutebrowser.completion import completionwidget
if TYPE_CHECKING:
@@ -49,7 +49,7 @@ class Completer(QObject):
super().__init__(parent)
self._cmd = cmd
self._win_id = win_id
- self._timer = QTimer()
+ self._timer = usertypes.Timer()
self._timer.setSingleShot(True)
self._timer.setInterval(0)
self._timer.timeout.connect(self._update_completion)
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 0f5dc0de9..27e631662 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -414,7 +414,7 @@ class CompletionView(QTreeView):
def on_clear_completion_selection(self):
"""Clear the selection model when an item is activated."""
self.hide()
- selmod = self._selection_model()
+ selmod = self.selectionModel()
if selmod is not None:
selmod.clearSelection()
selmod.clearCurrentIndex()
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
index 9535dd727..2377841ef 100644
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -10,7 +10,7 @@ DATA: A dict of Option objects after init() has been called.
"""
from typing import (Any, Dict, Iterable, List, Mapping, MutableMapping, Optional,
- Sequence, Tuple, Union, cast)
+ Sequence, Tuple, Union, NoReturn, cast)
import functools
import dataclasses
@@ -57,7 +57,7 @@ class Migrations:
deleted: List[str] = dataclasses.field(default_factory=list)
-def _raise_invalid_node(name: str, what: str, node: Any) -> None:
+def _raise_invalid_node(name: str, what: str, node: Any) -> NoReturn:
"""Raise an exception for an invalid configdata YAML node.
Args:
@@ -94,6 +94,7 @@ def _parse_yaml_type(
_raise_invalid_node(name, 'type', node)
try:
+ # pylint: disable=possibly-used-before-assignment
typ = getattr(configtypes, type_name)
except AttributeError:
raise AttributeError("Did not find type {} for {}".format(
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index ca92f96c1..322f88f6c 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -3272,6 +3272,9 @@ colors.webpage.darkmode.enabled:
desc: >-
Render all web contents using a dark theme.
+ On QtWebEngine < 6.7, this setting requires a restart and does not support
+ URL patterns, only the global setting is applied.
+
Example configurations from Chromium's `chrome://flags`:
- "With simple HSL/CIELAB/RGB-based inversion": Set
@@ -3279,7 +3282,7 @@ colors.webpage.darkmode.enabled:
set `colors.webpage.darkmode.policy.images` to `never`.
- "With selective image inversion": qutebrowser default settings.
- restart: true
+ supports_pattern: true
backend: QtWebEngine
colors.webpage.darkmode.algorithm:
diff --git a/qutebrowser/html/version.html b/qutebrowser/html/version.html
index 643929088..666414b26 100644
--- a/qutebrowser/html/version.html
+++ b/qutebrowser/html/version.html
@@ -19,8 +19,8 @@ html { margin-left: 10px; }
{% block content %}
{{ super() }}
<h1>Version info</h1>
-<pre>{{ version }}</pre>
<button onclick="paste_version()">Yank pastebin URL for version info</button>
+<pre>{{ version }}</pre>
<h1>Copyright info</h1>
<p>{{ copyright }}</p>
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index 54b6e88b1..18730c74b 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -359,6 +359,8 @@ class KeyInfo:
modifier_classes = (Qt.KeyboardModifier, Qt.KeyboardModifiers)
elif machinery.IS_QT6:
modifier_classes = Qt.KeyboardModifier
+ else:
+ raise utils.Unreachable()
assert isinstance(self.key, Qt.Key), self.key
assert isinstance(self.modifiers, modifier_classes), self.modifiers
diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py
index 38d2a2f9e..95bbed724 100644
--- a/qutebrowser/mainwindow/messageview.py
+++ b/qutebrowser/mainwindow/messageview.py
@@ -6,7 +6,7 @@
from typing import MutableSequence, Optional
-from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QTimer, Qt
+from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt
from qutebrowser.qt.widgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from qutebrowser.config import config, stylesheet
@@ -101,7 +101,7 @@ class MessageView(QWidget):
self._vbox.setSpacing(0)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- self._clear_timer = QTimer()
+ self._clear_timer = usertypes.Timer()
self._clear_timer.timeout.connect(self.clear_messages)
config.instance.changed.connect(self._set_clear_timer_interval)
diff --git a/qutebrowser/mainwindow/statusbar/clock.py b/qutebrowser/mainwindow/statusbar/clock.py
index aa2afe8a0..604243935 100644
--- a/qutebrowser/mainwindow/statusbar/clock.py
+++ b/qutebrowser/mainwindow/statusbar/clock.py
@@ -5,9 +5,10 @@
"""Clock displayed in the statusbar."""
from datetime import datetime
-from qutebrowser.qt.core import Qt, QTimer
+from qutebrowser.qt.core import Qt
from qutebrowser.mainwindow.statusbar import textbase
+from qutebrowser.utils import usertypes
class Clock(textbase.TextBase):
@@ -20,7 +21,7 @@ class Clock(textbase.TextBase):
super().__init__(parent, elidemode=Qt.TextElideMode.ElideNone)
self.format = ""
- self.timer = QTimer(self)
+ self.timer = usertypes.Timer(self)
self.timer.timeout.connect(self._show_time)
def _show_time(self):
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 28f32c4fd..47d8dc680 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -1011,16 +1011,11 @@ class TabbedBrowser(QWidget):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715
versions = version.qtwebengine_versions()
- is_qtbug_91715 = (
+ if (
status == browsertab.TerminationStatus.unknown and
code == 1002 and
- versions.webengine == utils.VersionNumber(5, 15, 3))
-
- def show_error_page(html):
- tab.set_html(html)
- log.webview.error(msg)
-
- if is_qtbug_91715:
+ versions.webengine == utils.VersionNumber(5, 15, 3)
+ ):
log.webview.error(msg)
log.webview.error('')
log.webview.error(
@@ -1034,12 +1029,17 @@ class TabbedBrowser(QWidget):
'A proper fix is likely available in QtWebEngine soon (which is why '
'the workaround is disabled by default).')
log.webview.error('')
- else:
- url_string = tab.url(requested=True).toDisplayString()
- error_page = jinja.render(
- 'error.html', title="Error loading {}".format(url_string),
- url=url_string, error=msg)
- QTimer.singleShot(100, lambda: show_error_page(error_page))
+ return
+
+ def show_error_page(html):
+ tab.set_html(html)
+ log.webview.error(msg)
+
+ url_string = tab.url(requested=True).toDisplayString()
+ error_page = jinja.render(
+ 'error.html', title="Error loading {}".format(url_string),
+ url=url_string, error=msg)
+ QTimer.singleShot(100, lambda: show_error_page(error_page))
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index 42c31c97e..afbfa0a95 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -398,7 +398,7 @@ class TabBar(QTabBar):
self.setStyle(self._our_style)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.vertical = False
- self._auto_hide_timer = QTimer()
+ self._auto_hide_timer = usertypes.Timer()
self._auto_hide_timer.setSingleShot(True)
self._auto_hide_timer.timeout.connect(self.maybe_hide)
self._on_show_switching_delay_changed()
diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py
index 35af5af28..e44d8b573 100644
--- a/qutebrowser/misc/elf.py
+++ b/qutebrowser/misc/elf.py
@@ -235,7 +235,7 @@ def _find_versions(data: bytes) -> Versions:
# Here it gets even more crazy: Sometimes, we don't have the full UA in one piece
# in the string table somehow (?!). However, Qt 6.2 added a separate
# qWebEngineChromiumVersion(), with PyQt wrappers following later. And *that*
- # apperently stores the full version in the string table separately from the UA.
+ # apparently stores the full version in the string table separately from the UA.
# As we clearly didn't have enough crazy heuristics yet so far, let's hunt for it!
# We first get the partial Chromium version from the UA:
diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py
index 6e1a0f577..19186ffb7 100644
--- a/qutebrowser/misc/httpclient.py
+++ b/qutebrowser/misc/httpclient.py
@@ -12,7 +12,7 @@ from qutebrowser.qt.core import pyqtSignal, QObject, QTimer
from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
-from qutebrowser.utils import qtlog
+from qutebrowser.utils import qtlog, usertypes
class HTTPRequest(QNetworkRequest):
@@ -85,7 +85,7 @@ class HTTPClient(QObject):
if reply.isFinished():
self.on_reply_finished(reply)
else:
- timer = QTimer(self)
+ timer = usertypes.Timer(self)
timer.setInterval(10000)
timer.timeout.connect(reply.abort)
timer.start()
diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py
index 21a3352d6..eefa2e3f3 100644
--- a/qutebrowser/misc/ipc.py
+++ b/qutebrowser/misc/ipc.py
@@ -391,6 +391,11 @@ class IPCServer(QObject):
def on_timeout(self):
"""Cancel the current connection if it was idle for too long."""
assert self._socket is not None
+ if not self._timer.check_timeout_validity():
+ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-124496
+ log.ipc.debug("Ignoring early on_timeout call")
+ return
+
log.ipc.error("IPC connection timed out "
"(socket 0x{:x}).".format(id(self._socket)))
self._socket.disconnectFromServer()
diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py
index 2bcde7ce9..c0e6b4d0c 100644
--- a/qutebrowser/misc/pakjoy.py
+++ b/qutebrowser/misc/pakjoy.py
@@ -168,7 +168,7 @@ def _find_webengine_resources() -> pathlib.Path:
# I'm not sure how to arrive at this path without hardcoding it
# ourselves. importlib_resources("PyQt6.Qt6") can serve as a
# replacement for the qtutils bit but it doesn't seem to help find the
- # actuall Resources folder.
+ # actual Resources folder.
candidates.append(
qt_data_path / "lib" / "QtWebEngineCore.framework" / "Resources"
)
@@ -184,7 +184,9 @@ def _find_webengine_resources() -> pathlib.Path:
if (candidate / PAK_FILENAME).exists():
return candidate
- raise binparsing.ParseError("Couldn't find webengine resources dir")
+ candidates_str = "\n".join(f" {p}" for p in candidates)
+ raise FileNotFoundError(
+ f"Couldn't find webengine resources dir, candidates:\n{candidates_str}")
def copy_webengine_resources() -> Optional[pathlib.Path]:
diff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py
index 9f45dd6ce..45a1f6598 100644
--- a/qutebrowser/qt/machinery.py
+++ b/qutebrowser/qt/machinery.py
@@ -48,7 +48,7 @@ class Error(Exception):
"""Base class for all exceptions in this module."""
-class Unavailable(Error, ImportError):
+class Unavailable(Error, ModuleNotFoundError):
"""Raised when a module is unavailable with the given wrapper."""
diff --git a/qutebrowser/qt/webenginewidgets.py b/qutebrowser/qt/webenginewidgets.py
index b8833e9c8..88758cf23 100644
--- a/qutebrowser/qt/webenginewidgets.py
+++ b/qutebrowser/qt/webenginewidgets.py
@@ -27,6 +27,7 @@ else:
if machinery.IS_QT5:
+ # pylint: disable=undefined-variable
# moved to WebEngineCore in Qt 6
del QWebEngineSettings
del QWebEngineProfile
diff --git a/qutebrowser/qt/widgets.py b/qutebrowser/qt/widgets.py
index eac8cafbb..f82ec2e3b 100644
--- a/qutebrowser/qt/widgets.py
+++ b/qutebrowser/qt/widgets.py
@@ -26,4 +26,5 @@ else:
raise machinery.UnknownWrapper()
if machinery.IS_QT5:
+ # pylint: disable=undefined-variable
del QFileSystemModel # moved to QtGui in Qt 6
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index 21f3b8478..c1f05b78d 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -193,14 +193,15 @@ def check_qdatastream(stream: QDataStream) -> None:
QDataStream.Status.WriteFailed: ("The data stream cannot write to the "
"underlying device."),
}
- try:
- status_to_str[QDataStream.Status.SizeLimitExceeded] = ( # type: ignore[attr-defined]
- "The data stream cannot read or write the data because its size is larger "
- "than supported by the current platform."
- )
- except AttributeError:
- # Added in Qt 6.7
- pass
+ if machinery.IS_QT6:
+ try:
+ status_to_str[QDataStream.Status.SizeLimitExceeded] = (
+ "The data stream cannot read or write the data because its size is larger "
+ "than supported by the current platform."
+ )
+ except AttributeError:
+ # Added in Qt 6.7
+ pass
if stream.status() != QDataStream.Status.Ok:
raise OSError(status_to_str[stream.status()])
diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py
index a40f9d2bd..a97a2e994 100644
--- a/qutebrowser/utils/resources.py
+++ b/qutebrowser/utils/resources.py
@@ -26,6 +26,7 @@ else: # pragma: no cover
import qutebrowser
_cache: Dict[str, str] = {}
+_bin_cache: Dict[str, bytes] = {}
_ResourceType = Union[Traversable, pathlib.Path]
@@ -88,6 +89,10 @@ def preload() -> None:
for name in _glob(resource_path, subdir, ext):
_cache[name] = read_file(name)
+ for name in _glob(resource_path, 'img', '.png'):
+ # e.g. broken_qutebrowser_logo.png
+ _bin_cache[name] = read_file_binary(name)
+
def read_file(filename: str) -> str:
"""Get the contents of a file contained with qutebrowser.
@@ -115,6 +120,9 @@ def read_file_binary(filename: str) -> bytes:
Return:
The file contents as a bytes object.
"""
+ if filename in _bin_cache:
+ return _bin_cache[filename]
+
path = _path(filename)
with _keyerror_workaround():
return path.read_bytes()
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 95b1f3a01..d61d4aba7 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -7,7 +7,9 @@
import html
import operator
import enum
+import time
import dataclasses
+import logging
from typing import Optional, Sequence, TypeVar, Union
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QTimer
@@ -443,6 +445,8 @@ class Timer(QTimer):
def __init__(self, parent: QObject = None, name: str = None) -> None:
super().__init__(parent)
+ self._start_time: Optional[float] = None
+ self.timeout.connect(self._validity_check_handler)
if name is None:
self._name = "unnamed"
else:
@@ -452,6 +456,39 @@ class Timer(QTimer):
def __repr__(self) -> str:
return utils.get_repr(self, name=self._name)
+ @pyqtSlot()
+ def _validity_check_handler(self) -> None:
+ if not self.check_timeout_validity() and self._start_time is not None:
+ elapsed = time.monotonic() - self._start_time
+ level = logging.WARNING
+ if utils.is_windows and self._name == "ipc-timeout":
+ level = logging.DEBUG
+ log.misc.log(
+ level,
+ (
+ f"Timer {self._name} (id {self.timerId()}) triggered too early: "
+ f"interval {self.interval()} but only {elapsed:.3f}s passed"
+ )
+ )
+
+ def check_timeout_validity(self) -> bool:
+ """Check to see if the timeout signal was fired at the expected time.
+
+ WORKAROUND for https://bugreports.qt.io/browse/QTBUG-124496
+ """
+ if self._start_time is None:
+ # manual emission?
+ return True
+
+ elapsed = time.monotonic() - self._start_time
+ # Checking for half the interval is pretty arbitrary. In the bug case
+ # the timer typically fires immediately since the expiry event is
+ # already pending when it is created.
+ if elapsed < self.interval() / 1000 / 2:
+ return False
+
+ return True
+
def setInterval(self, msec: int) -> None:
"""Extend setInterval to check for overflows."""
qtutils.check_overflow(msec, 'int')
@@ -459,6 +496,7 @@ class Timer(QTimer):
def start(self, msec: int = None) -> None:
"""Extend start to check for overflows."""
+ self._start_time = time.monotonic()
if msec is not None:
qtutils.check_overflow(msec, 'int')
super().start(msec)
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 11c160c9e..13ccf5ca2 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -17,6 +17,7 @@ import traceback
import functools
import contextlib
import shlex
+import sysconfig
import mimetypes
from typing import (Any, Callable, IO, Iterator,
Optional, Sequence, Tuple, List, Type, Union,
@@ -636,7 +637,7 @@ def expand_windows_drive(path: str) -> str:
path: The path to expand.
"""
# Usually, "E:" on Windows refers to the current working directory on drive
- # E:\. The correct way to specifify drive E: is "E:\", but most users
+ # E:\. The correct way to specify drive E: is "E:\", but most users
# probably don't use the "multiple working directories" feature and expect
# "E:" and "E:\" to be equal.
if re.fullmatch(r'[A-Z]:', path, re.IGNORECASE):
@@ -666,7 +667,10 @@ def yaml_load(f: Union[str, IO[str]]) -> Any:
end = datetime.datetime.now()
delta = (end - start).total_seconds()
- deadline = 10 if 'CI' in os.environ else 2
+ if "CI" in os.environ or sysconfig.get_config_var("Py_DEBUG"):
+ deadline = 10
+ else:
+ deadline = 2
if delta > deadline: # pragma: no cover
log.misc.warning(
"YAML load took unusually long, please report this at "
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index a4bb893f6..2bb39fea0 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -486,7 +486,7 @@ def _pdfjs_version() -> str:
else:
pdfjs_file = pdfjs_file.decode('utf-8')
version_re = re.compile(
- r"^ *(PDFJS\.version|(var|const) pdfjsVersion) = '(?P<version>[^']+)';$",
+ r"""^ *(PDFJS\.version|(var|const) pdfjsVersion) = ['"](?P<version>[^'"]+)['"];$""",
re.MULTILINE)
match = version_re.search(pdfjs_file)
@@ -535,9 +535,22 @@ class WebEngineVersions:
webengine: utils.VersionNumber
chromium: Optional[str]
source: str
+ chromium_security: Optional[str] = None
chromium_major: Optional[int] = dataclasses.field(init=False)
- _CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, str]] = {
+ _BASES: ClassVar[Dict[int, str]] = {
+ 83: '83.0.4103.122', # ~2020-06-24
+ 87: '87.0.4280.144', # ~2020-12-02
+ 90: '90.0.4430.228', # 2021-06-22
+ 94: '94.0.4606.126', # 2021-11-17
+ 102: '102.0.5005.177', # ~2022-05-24
+ # (.220 claimed by code, .181 claimed by CHROMIUM_VERSION)
+ 108: '108.0.5359.220', # ~2022-12-23
+ 112: '112.0.5615.213', # ~2023-04-18
+ 118: '118.0.5993.220', # ~2023-10-24
+ }
+
+ _CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, Tuple[str, Optional[str]]]] = {
# ====== UNSUPPORTED =====
# Qt 5.12: Chromium 69
@@ -558,73 +571,61 @@ class WebEngineVersions:
# 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25)
# ====== SUPPORTED =====
-
- # Qt 5.15.2: Chromium 83
- # 83.0.4103.122 (~2020-06-24)
- # 5.15.2: Security fixes up to 86.0.4240.183 (2020-11-02)
- utils.VersionNumber(5, 15, 2): '83.0.4103.122',
-
- # Qt 5.15.3: Chromium 87
- # 87.0.4280.144 (~2020-12-02)
- # 5.15.3: Security fixes up to 88.0.4324.150 (2021-02-04)
- # 5.15.4: Security fixes up to ???
- # 5.15.5: Security fixes up to ???
- # 5.15.6: Security fixes up to ???
- # 5.15.7: Security fixes up to 94.0.4606.61 (2021-09-24)
- # 5.15.8: Security fixes up to 96.0.4664.110 (2021-12-13)
- # 5.15.9: Security fixes up to 98.0.4758.102 (2022-02-14)
- # 5.15.10: Security fixes up to ???
- # 5.15.11: Security fixes up to ???
- utils.VersionNumber(5, 15): '87.0.4280.144', # >= 5.15.3
-
- # Qt 6.2: Chromium 90
- # 90.0.4430.228 (2021-06-22)
- # 6.2.0: Security fixes up to 93.0.4577.63 (2021-08-31)
- # 6.2.1: Security fixes up to 94.0.4606.61 (2021-09-24)
- # 6.2.2: Security fixes up to 96.0.4664.45 (2021-11-15)
- # 6.2.3: Security fixes up to 96.0.4664.45 (2021-11-15)
- # 6.2.4: Security fixes up to 98.0.4758.102 (2022-02-14)
- # 6.2.5: Security fixes up to ???
- # 6.2.6: Security fixes up to ???
- # 6.2.7: Security fixes up to ???
- utils.VersionNumber(6, 2): '90.0.4430.228',
-
- # Qt 6.3: Chromium 94
- # 94.0.4606.126 (2021-11-17)
- # 6.3.0: Security fixes up to 99.0.4844.84 (2022-03-25)
- # 6.3.1: Security fixes up to 101.0.4951.64 (2022-05-10)
- # 6.3.2: Security fixes up to 104.0.5112.81 (2022-08-01)
- utils.VersionNumber(6, 3): '94.0.4606.126',
-
- # Qt 6.4: Chromium 102
- # 102.0.5005.177 (~2022-05-24)
- # 6.4.0: Security fixes up to 104.0.5112.102 (2022-08-16)
- # 6.4.1: Security fixes up to 107.0.5304.88 (2022-10-27)
- # 6.4.2: Security fixes up to 108.0.5359.94 (2022-12-02)
- # 6.4.3: Security fixes up to 110.0.5481.78 (2023-02-07)
- utils.VersionNumber(6, 4): '102.0.5005.177',
-
- # Qt 6.5: Chromium 108
- # 108.0.5359.220 (~2022-12-23)
- # (.220 claimed by code, .181 claimed by CHROMIUM_VERSION)
- # 6.5.0: Security fixes up to 110.0.5481.104 (2023-02-16)
- # 6.5.1: Security fixes up to 112.0.5615.138 (2023-04-18)
- # 6.5.2: Security fixes up to 114.0.5735.133 (2023-06-13)
- # 6.5.3: Security fixes up to 117.0.5938.63 (2023-09-12)
- utils.VersionNumber(6, 5): '108.0.5359.220',
-
- # Qt 6.6: Chromium 112
- # 112.0.5615.213 (~2023-04-18)
- # 6.6.0: Security fixes up to 117.0.5938.63 (2023-09-12)
- # 6.6.1: Security fixes up to 119.0.6045.123 (2023-11-07)
- # 6.6.2: Security fixes up to 121.0.6167.160 (2024-02-06)
- # 6.6.3: Security fixes up to 122.0.6261.128 (2024-03-12)
- utils.VersionNumber(6, 6): '112.0.5615.213',
-
- # Qt 6.7: Chromium 118
- # 118.0.5993.220 (~2023-10-24)
- # 6.6.0: Security fixes up to 122.0.6261.128 (?) (2024-03-12)
- utils.VersionNumber(6, 7): '118.0.5993.220',
+ # base security
+ ## Qt 5.15
+ utils.VersionNumber(5, 15, 2): (_BASES[83], '86.0.4240.183'), # 2020-11-02
+ utils.VersionNumber(5, 15): (_BASES[87], None), # >= 5.15.3
+ utils.VersionNumber(5, 15, 3): (_BASES[87], '88.0.4324.150'), # 2021-02-04
+ # 5.15.4 to 5.15.6: unknown security fixes
+ utils.VersionNumber(5, 15, 7): (_BASES[87], '94.0.4606.61'), # 2021-09-24
+ utils.VersionNumber(5, 15, 8): (_BASES[87], '96.0.4664.110'), # 2021-12-13
+ utils.VersionNumber(5, 15, 9): (_BASES[87], '98.0.4758.102'), # 2022-02-14
+ utils.VersionNumber(5, 15, 10): (_BASES[87], '98.0.4758.102'), # (?) 2022-02-14
+ utils.VersionNumber(5, 15, 11): (_BASES[87], '98.0.4758.102'), # (?) 2022-02-14
+ utils.VersionNumber(5, 15, 12): (_BASES[87], '98.0.4758.102'), # (?) 2022-02-14
+ utils.VersionNumber(5, 15, 13): (_BASES[87], '108.0.5359.124'), # 2022-12-13
+ utils.VersionNumber(5, 15, 14): (_BASES[87], '113.0.5672.64'), # 2023-05-02
+ # 5.15.15: unknown security fixes
+ utils.VersionNumber(5, 15, 16): (_BASES[87], '119.0.6045.123'), # 2023-11-07
+ utils.VersionNumber(5, 15, 17): (_BASES[87], '123.0.6312.58'), # 2024-03-19
+
+
+ ## Qt 6.2
+ utils.VersionNumber(6, 2): (_BASES[90], '93.0.4577.63'), # 2021-08-31
+ utils.VersionNumber(6, 2, 1): (_BASES[90], '94.0.4606.61'), # 2021-09-24
+ utils.VersionNumber(6, 2, 2): (_BASES[90], '96.0.4664.45'), # 2021-11-15
+ utils.VersionNumber(6, 2, 3): (_BASES[90], '96.0.4664.45'), # 2021-11-15
+ utils.VersionNumber(6, 2, 4): (_BASES[90], '98.0.4758.102'), # 2022-02-14
+ # 6.2.5 / 6.2.6: unknown security fixes
+ utils.VersionNumber(6, 2, 7): (_BASES[90], '107.0.5304.110'), # 2022-11-08
+ utils.VersionNumber(6, 2, 8): (_BASES[90], '111.0.5563.110'), # 2023-03-21
+
+ ## Qt 6.3
+ utils.VersionNumber(6, 3): (_BASES[94], '99.0.4844.84'), # 2022-03-25
+ utils.VersionNumber(6, 3, 1): (_BASES[94], '101.0.4951.64'), # 2022-05-10
+ utils.VersionNumber(6, 3, 2): (_BASES[94], '104.0.5112.81'), # 2022-08-01
+
+ ## Qt 6.4
+ utils.VersionNumber(6, 4): (_BASES[102], '104.0.5112.102'), # 2022-08-16
+ utils.VersionNumber(6, 4, 1): (_BASES[102], '107.0.5304.88'), # 2022-10-27
+ utils.VersionNumber(6, 4, 2): (_BASES[102], '108.0.5359.94'), # 2022-12-02
+ utils.VersionNumber(6, 4, 3): (_BASES[102], '110.0.5481.78'), # 2023-02-07
+
+ ## Qt 6.5
+ utils.VersionNumber(6, 5): (_BASES[108], '110.0.5481.104'), # 2023-02-16
+ utils.VersionNumber(6, 5, 1): (_BASES[108], '112.0.5615.138'), # 2023-04-18
+ utils.VersionNumber(6, 5, 2): (_BASES[108], '114.0.5735.133'), # 2023-06-13
+ utils.VersionNumber(6, 5, 3): (_BASES[108], '117.0.5938.63'), # 2023-09-12
+
+ ## Qt 6.6
+ utils.VersionNumber(6, 6): (_BASES[112], '117.0.5938.63'), # 2023-09-12
+ utils.VersionNumber(6, 6, 1): (_BASES[112], '119.0.6045.123'), # 2023-11-07
+ utils.VersionNumber(6, 6, 2): (_BASES[112], '121.0.6167.160'), # 2024-02-06
+ utils.VersionNumber(6, 6, 3): (_BASES[112], '122.0.6261.128'), # 2024-03-12
+
+ ## Qt 6.7
+ utils.VersionNumber(6, 7): (_BASES[118], '122.0.6261.128'), # 2024-03-12
+ utils.VersionNumber(6, 7, 1): (_BASES[118], '124.0.6367.202'), # ~2024-05-09
}
def __post_init__(self) -> None:
@@ -635,25 +636,37 @@ class WebEngineVersions:
self.chromium_major = int(self.chromium.split('.')[0])
def __str__(self) -> str:
- s = f'QtWebEngine {self.webengine}'
+ lines = [f'QtWebEngine {self.webengine}']
if self.chromium is not None:
- s += f', based on Chromium {self.chromium}'
- if self.source != 'UA':
- s += f' (from {self.source})'
- return s
+ lines.append(f' based on Chromium {self.chromium}')
+ if self.chromium_security is not None:
+ lines.append(f' with security patches up to {self.chromium_security} (plus any distribution patches)')
+ lines.append(f' (source: {self.source})')
+ return "\n".join(lines)
@classmethod
def from_ua(cls, ua: 'websettings.UserAgent') -> 'WebEngineVersions':
"""Get the versions parsed from a user agent.
- This is the most reliable and "default" way to get this information (at least
- until QtWebEngine adds an API for it). However, it needs a fully initialized
- QtWebEngine, and we sometimes need this information before that is available.
+ This is the most reliable and "default" way to get this information for
+ older Qt versions that don't provide an API for it. However, it needs a
+ fully initialized QtWebEngine, and we sometimes need this information
+ before that is available.
"""
assert ua.qt_version is not None, ua
+ webengine = utils.VersionNumber.parse(ua.qt_version)
+ chromium_inferred, chromium_security = cls._infer_chromium_version(webengine)
+ if ua.upstream_browser_version != chromium_inferred: # pragma: no cover
+ # should never happen, but let's play it safe
+ log.misc.debug(
+ f"Chromium version mismatch: {ua.upstream_browser_version} (UA) != "
+ f"{chromium_inferred} (inferred)")
+ chromium_security = None
+
return cls(
- webengine=utils.VersionNumber.parse(ua.qt_version),
+ webengine=webengine,
chromium=ua.upstream_browser_version,
+ chromium_security=chromium_security,
source='UA',
)
@@ -668,9 +681,19 @@ class WebEngineVersions:
sometimes mix and match Qt/QtWebEngine versions, so this is a more reliable
(though hackish) way to get a more accurate result.
"""
+ webengine = utils.VersionNumber.parse(versions.webengine)
+ chromium_inferred, chromium_security = cls._infer_chromium_version(webengine)
+ if versions.chromium != chromium_inferred: # pragma: no cover
+ # should never happen, but let's play it safe
+ log.misc.debug(
+ f"Chromium version mismatch: {versions.chromium} (ELF) != "
+ f"{chromium_inferred} (inferred)")
+ chromium_security = None
+
return cls(
- webengine=utils.VersionNumber.parse(versions.webengine),
+ webengine=webengine,
chromium=versions.chromium,
+ chromium_security=chromium_security,
source='ELF',
)
@@ -678,24 +701,37 @@ class WebEngineVersions:
def _infer_chromium_version(
cls,
pyqt_webengine_version: utils.VersionNumber,
- ) -> Optional[str]:
- """Infer the Chromium version based on the PyQtWebEngine version."""
- chromium_version = cls._CHROMIUM_VERSIONS.get(pyqt_webengine_version)
+ ) -> Tuple[Optional[str], Optional[str]]:
+ """Infer the Chromium version based on the PyQtWebEngine version.
+
+ Returns:
+ A tuple of the Chromium version and the security patch version.
+ """
+ chromium_version, security_version = cls._CHROMIUM_VERSIONS.get(
+ pyqt_webengine_version, (None, None))
if chromium_version is not None:
- return chromium_version
+ return chromium_version, security_version
# 5.15 patch versions change their QtWebEngine version, but no changes are
# expected after 5.15.3 and 5.15.[01] are unsupported.
- if pyqt_webengine_version == utils.VersionNumber(5, 15, 2):
- minor_version = pyqt_webengine_version
- else:
- # e.g. 5.14.2 -> 5.14
- minor_version = pyqt_webengine_version.strip_patch()
+ assert pyqt_webengine_version != utils.VersionNumber(5, 15, 2)
+
+ # e.g. 5.15.4 -> 5.15
+ # we ignore the security version as that one will have changed from .0
+ # and is thus unknown.
+ minor_version = pyqt_webengine_version.strip_patch()
+ chromium_ver, _security_ver = cls._CHROMIUM_VERSIONS.get(
+ minor_version, (None, None))
- return cls._CHROMIUM_VERSIONS.get(minor_version)
+ return chromium_ver, None
@classmethod
- def from_api(cls, qtwe_version: str, chromium_version: Optional[str]) -> 'WebEngineVersions':
+ def from_api(
+ cls,
+ qtwe_version: str,
+ chromium_version: Optional[str],
+ chromium_security: Optional[str] = None,
+ ) -> 'WebEngineVersions':
"""Get the versions based on the exact versions.
This is called if we have proper APIs to get the versions easily
@@ -705,6 +741,7 @@ class WebEngineVersions:
return cls(
webengine=parsed,
chromium=chromium_version,
+ chromium_security=chromium_security,
source='api',
)
@@ -721,9 +758,11 @@ class WebEngineVersions:
a PyQtWebEngine-Qt{,5} package from PyPI, so we could query its exact version.
"""
parsed = utils.VersionNumber.parse(pyqt_webengine_qt_version)
+ chromium, chromium_security = cls._infer_chromium_version(parsed)
return cls(
webengine=parsed,
- chromium=cls._infer_chromium_version(parsed),
+ chromium=chromium,
+ chromium_security=chromium_security,
source=source,
)
@@ -766,9 +805,12 @@ class WebEngineVersions:
if frozen:
parsed = utils.VersionNumber(5, 15, 2)
+ chromium, chromium_security = cls._infer_chromium_version(parsed)
+
return cls(
webengine=parsed,
- chromium=cls._infer_chromium_version(parsed),
+ chromium=chromium,
+ chromium_security=chromium_security,
source=source,
)
@@ -805,11 +847,20 @@ def qtwebengine_versions(*, avoid_init: bool = False) -> WebEngineVersions:
except ImportError:
pass # Needs QtWebEngine 6.2+ with PyQtWebEngine 6.3.1+
else:
+ try:
+ from qutebrowser.qt.webenginecore import (
+ qWebEngineChromiumSecurityPatchVersion,
+ )
+ chromium_security = qWebEngineChromiumSecurityPatchVersion()
+ except ImportError:
+ chromium_security = None # Needs QtWebEngine 6.3+
+
qtwe_version = qWebEngineVersion()
assert qtwe_version is not None
return WebEngineVersions.from_api(
qtwe_version=qtwe_version,
chromium_version=qWebEngineChromiumVersion(),
+ chromium_security=chromium_security,
)
from qutebrowser.browser.webengine import webenginesettings