def _qtwebengine_args( namespace: argparse.Namespace, special_flags: Sequence[str], ) -> Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" versions = version.qtwebengine_versions(avoid_init=True) qt_514_ver = utils.VersionNumber(5, 14) qt_515_ver = utils.VersionNumber(5, 15) if qt_514_ver <= versions.webengine < qt_515_ver: # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-82105 yield '--disable-shared-workers' # WORKAROUND equivalent to # https://codereview.qt-project.org/c/qt/qtwebengine/+/256786 # also see: # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265753 if versions.webengine >= utils.VersionNumber(5, 12, 3): if 'stack' in namespace.debug_flags: # Only actually available in Qt 5.12.5, but let's save another # check, as passing the option won't hurt. yield '--enable-in-process-stack-traces' else: if 'stack' not in namespace.debug_flags: yield '--disable-in-process-stack-traces' lang_override = _get_lang_override( webengine_version=versions.webengine, locale_name=QLocale().bcp47Name(), ) if lang_override is not None: yield f'--lang={lang_override}' if 'chromium' in namespace.debug_flags: yield '--enable-logging' yield '--v=1' if 'wait-renderer-process' in namespace.debug_flags: yield '--renderer-startup-dialog' from qutebrowser.browser.webengine import darkmode darkmode_settings = darkmode.settings( versions=versions, special_flags=special_flags, ) for switch_name, values in darkmode_settings.items(): # If we need to use other switches (say, --enable-features), we might need to # refactor this so values still get combined with existing ones. assert switch_name in ['dark-mode-settings', 'blink-settings'], switch_name yield f'--{switch_name}=' + ','.join(f'{k}={v}' for k, v in values) enabled_features, disabled_features = _qtwebengine_features( versions, special_flags) if enabled_features: yield _ENABLE_FEATURES + ','.join(enabled_features) if disabled_features: yield _DISABLE_FEATURES + ','.join(disabled_features) yield from _qtwebengine_settings_args(versions)
def test_preferred_colorscheme_with_dark_mode(request, quteproc_new, webengine_versions): """Test interaction between preferred-color-scheme and dark mode.""" if not request.config.webengine: pytest.skip("Skipped with QtWebKit") args = _base_args(request.config) + [ '--temp-basedir', '-s', 'colors.webpage.preferred_color_scheme', 'dark', '-s', 'colors.webpage.darkmode.enabled', 'true', '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb', ] quteproc_new.start(args) quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') content = quteproc_new.get_content() qtwe_version = webengine_versions.webengine xfail = None if utils.VersionNumber(5, 15, 3) <= qtwe_version <= utils.VersionNumber(6): # https://bugs.chromium.org/p/chromium/issues/detail?id=1177973 # No workaround known. expected_text = 'Light preference detected.' # light website color, inverted by darkmode expected_color = (testutils.Color(123, 125, 123) if IS_ARM else testutils.Color(127, 127, 127)) xfail = "Chromium bug 1177973" elif qtwe_version == utils.VersionNumber(5, 15, 2): # Our workaround breaks when dark mode is enabled... # Also, for some reason, dark mode doesn't work on that page either! expected_text = 'No preference detected.' expected_color = testutils.Color(0, 170, 0) # green xfail = "QTBUG-89753" else: # Qt 5.14 and 5.15.0/.1 work correctly. # Hopefully, so does Qt 6.x in the future? expected_text = 'Dark preference detected.' expected_color = (testutils.Color(33, 32, 33) if IS_ARM else testutils.Color(34, 34, 34)) # dark website color xfail = False pos = QPoint(0, 0) img = quteproc_new.get_screenshot(probe_pos=pos, probe_color=expected_color) color = testutils.Color(img.pixelColor(pos)) assert content == expected_text assert color == expected_color if xfail: # We still do some checks, but we want to mark the test outcome as xfail. pytest.xfail(xfail)
def _get_lang_override( webengine_version: utils.VersionNumber, locale_name: str ) -> Optional[str]: """Get a --lang switch to override Qt's locale handling. This is needed as a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715 There is no fix yet, but we assume it'll be fixed with QtWebEngine 5.15.4. """ if not config.val.qt.workarounds.locale: return None if webengine_version != utils.VersionNumber(5, 15, 3) or not utils.is_linux: return None locales_path = pathlib.Path( QLibraryInfo.location(QLibraryInfo.TranslationsPath)) / 'qtwebengine_locales' if not locales_path.exists(): log.init.debug(f"{locales_path} not found, skipping workaround!") return None pak_path = _get_locale_pak_path(locales_path, locale_name) if pak_path.exists(): log.init.debug(f"Found {pak_path}, skipping workaround") return None pak_name = _get_pak_name(locale_name) pak_path = _get_locale_pak_path(locales_path, pak_name) if pak_path.exists(): log.init.debug(f"Found {pak_path}, applying workaround") return pak_name log.init.debug(f"Can't find pak in {locales_path} for {locale_name} or {pak_name}") return 'en-US'
def gentoo_version_patch(monkeypatch): versions = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='faked', ) monkeypatch.setattr(version, 'qtwebengine_versions', lambda avoid_init: versions)
def init(parent=None): """Initialize sessions. Args: parent: The parent to use for the SessionManager. """ base_path = pathlib.Path(standarddir.data()) / 'sessions' # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/5359 backup_path = base_path / 'before-qt-515' if objects.backend == usertypes.Backend.QtWebEngine: webengine_version = version.qtwebengine_versions().webengine do_backup = webengine_version >= utils.VersionNumber(5, 15) else: do_backup = False if base_path.exists() and not backup_path.exists() and do_backup: backup_path.mkdir() for path in base_path.glob('*.yml'): shutil.copy(path, backup_path) base_path.mkdir(exist_ok=True) global session_manager session_manager = SessionManager(str(base_path), parent)
def test_from_pyqt(self): expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='PyQt', ) assert version.WebEngineVersions.from_pyqt('5.15.2') == expected
def test_from_elf(self): elf_version = elf.Versions(webengine='5.15.2', chromium='83.0.4103.122') expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='ELF', ) assert version.WebEngineVersions.from_elf(elf_version) == expected
def handle_download(self, qt_item): """Start a download coming from a QWebEngineProfile.""" qt_filename = os.path.basename(qt_item.path()) # FIXME use 5.14 API mime_type = qt_item.mimeType() url = qt_item.url() # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-90355 if version.qtwebengine_versions().webengine >= utils.VersionNumber( 5, 15, 3): needs_workaround = False elif url.scheme().lower() == 'data': if '/' in url.path().split(',')[-1]: # e.g. a slash in base64 wrong_filename = url.path().split('/')[-1] else: wrong_filename = mime_type.split('/')[1] needs_workaround = qt_filename == wrong_filename else: needs_workaround = False if needs_workaround: suggested_filename = urlutils.filename_from_url( url, fallback='qutebrowser-download') else: suggested_filename = _strip_suffix(qt_filename) use_pdfjs = pdfjs.should_use_pdfjs(mime_type, url) download = DownloadItem(qt_item, manager=self) self._init_item(download, auto_remove=use_pdfjs, suggested_filename=suggested_filename) if self._mhtml_target is not None: download.set_target(self._mhtml_target) self._mhtml_target = None return if use_pdfjs: download.set_target(downloads.PDFJSDownloadTarget()) return filename = downloads.immediate_download_path() if filename is not None: # User doesn't want to be asked, so just use the download_dir target = downloads.FileDownloadTarget(filename) download.set_target(target) return if download.cancel_for_origin(): return # Ask the user for a filename - needs to be blocking! question = downloads.get_filename_question( suggested_filename=suggested_filename, url=qt_item.url(), parent=self) self._init_filename_question(question, download) message.global_bridge.ask(question, blocking=True)
def _variant(versions: version.WebEngineVersions) -> Variant: """Get the dark mode variant based on the underlying Qt version.""" env_var = os.environ.get('QUTE_DARKMODE_VARIANT') if env_var is not None: try: return Variant[env_var] except KeyError: log.init.warning( f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}") if (versions.webengine == utils.VersionNumber(5, 15, 2) and versions.chromium_major == 87): # WORKAROUND for Gentoo packaging something newer as 5.15.2... return Variant.qt_515_3 elif versions.webengine >= utils.VersionNumber(5, 15, 3): return Variant.qt_515_3 elif versions.webengine >= utils.VersionNumber(5, 15, 2): return Variant.qt_515_2 elif versions.webengine == utils.VersionNumber(5, 15, 1): return Variant.qt_515_1 elif versions.webengine == utils.VersionNumber(5, 15): return Variant.qt_515_0 elif versions.webengine >= utils.VersionNumber(5, 14): return Variant.qt_514 elif versions.webengine >= utils.VersionNumber(5, 11): return Variant.qt_511_to_513 raise utils.Unreachable(versions.webengine)
def from_pyqt(cls, pyqt_webengine_version: str) -> 'WebEngineVersions': """Get the versions based on the PyQtWebEngine version. This is the "last resort" if we don't want to fully initialize QtWebEngine (so from_ua isn't possible), we're not on Linux (or ELF parsing failed), and PyQtWebEngine-Qt{5,} isn't available from PyPI. Here, we assume that the PyQtWebEngine version is the same as the QtWebEngine version, and infer the Chromium version from that. This assumption isn't generally true, but good enough for some scenarios, especially the prebuilt Windows/macOS releases. """ parsed = utils.VersionNumber.parse(pyqt_webengine_version) if utils.VersionNumber(5, 15, 3) <= parsed < utils.VersionNumber(6): # If we land here, we're in a tricky situation where we are forced to guess: # # PyQt 5.15.3 and 5.15.4 from PyPI come with QtWebEngine 5.15.2 (Chromium # 83), not 5.15.3 (Chromium 87). Given that there was no binary release of # QtWebEngine 5.15.3, this is unlikely to change before Qt 6. # # However, at this point: # # - ELF parsing failed # (so we're likely on macOS or Windows, but not definitely) # # - Getting infos from a PyPI-installed PyQtWebEngine failed # (so we're either in a PyInstaller-deployed qutebrowser, or a self-built # or distribution-installed Qt) # # PyQt 5.15.3 and 5.15.4 come with QtWebEngine 5.15.2 (83-based), but if # someone lands here with the last Qt/PyQt installed from source, they might # be using QtWebEngine 5.15.3 (87-based). For now, we play it safe, and only # do this kind of "downgrade" when we know we're using PyInstaller. frozen = hasattr(sys, 'frozen') log.misc.debug(f"PyQt5 >= 5.15.3, frozen {frozen}") if frozen: parsed = utils.VersionNumber(5, 15, 2) return cls( webengine=parsed, chromium=cls._infer_chromium_version(parsed), source='PyQt', )
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) if chromium_version is not None: return chromium_version # 5.15 patch versions change their QtWebEngine version, but no changes are # expected after 5.15.3. v5_15_3 = utils.VersionNumber(5, 15, 3) if v5_15_3 <= pyqt_webengine_version < utils.VersionNumber(6): minor_version = v5_15_3 else: # e.g. 5.14.2 -> 5.14 minor_version = pyqt_webengine_version.strip_patch() return cls._CHROMIUM_VERSIONS.get(minor_version)
def test_from_ua(self): ua = websettings.UserAgent( os_info='X11; Linux x86_64', webkit_version='537.36', upstream_browser_key='Chrome', upstream_browser_version='83.0.4103.122', qt_key='QtWebEngine', qt_version='5.15.2', ) expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='UA', ) assert version.WebEngineVersions.from_ua(ua) == expected
def _on_renderer_process_terminated(self, tab, status, code): """Show an error when a renderer process terminated.""" if status == browsertab.TerminationStatus.normal: return messages = { browsertab.TerminationStatus.abnormal: "Renderer process exited", browsertab.TerminationStatus.crashed: "Renderer process crashed", browsertab.TerminationStatus.killed: "Renderer process was killed", browsertab.TerminationStatus.unknown: "Renderer process did not start", } msg = messages[status] + f" (status {code})" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715 versions = version.qtwebengine_versions() is_qtbug_91715 = (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: log.webview.error(msg) log.webview.error('') log.webview.error( 'NOTE: If you see this and "Network service crashed, restarting ' 'service.", please see:') log.webview.error( 'https://github.com/qutebrowser/qutebrowser/issues/6235') log.webview.error( 'You can set the "qt.workarounds.locale" setting in qutebrowser to ' 'work around the issue.') log.webview.error( '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))
def expected_names(self, webengine_versions, pdf_bytes): """Get the expected filenames before/after the workaround. With QtWebEngine 5.15.3, this is handled correctly inside QtWebEngine and we get a qwe_download.pdf instead. """ if webengine_versions.webengine >= utils.VersionNumber(5, 15, 3): return _ExpectedNames(before='qwe_download.pdf', after='qwe_download.pdf') with_slash = b'% ?' in pdf_bytes base64_data = base64.b64encode(pdf_bytes).decode('ascii') if with_slash: assert '/' in base64_data before = base64_data.split('/')[1] else: assert '/' not in base64_data before = 'pdf' # from the mimetype return _ExpectedNames(before=before, after='download.pdf')
class WebEngineVersions: """Version numbers for QtWebEngine and the underlying Chromium.""" webengine: utils.VersionNumber chromium: Optional[str] source: str chromium_major: Optional[int] = dataclasses.field(init=False) _CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, str]] = { # Qt 5.12: Chromium 69 # (LTS) 69.0.3497.128 (~2018-09-11) # 5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24) # 5.12.1: Security fixes up to 71.0.3578.94 (2018-12-12) # 5.12.2: Security fixes up to 72.0.3626.121 (2019-03-01) # 5.12.3: Security fixes up to 73.0.3683.75 (2019-03-12) # 5.12.4: Security fixes up to 74.0.3729.157 (2019-05-14) # 5.12.5: Security fixes up to 76.0.3809.87 (2019-07-30) # 5.12.6: Security fixes up to 77.0.3865.120 (~2019-09-10) # 5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16) # 5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18) # 5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03) # 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06) utils.VersionNumber(5, 12): '69.0.3497.128', # Qt 5.13: Chromium 73 # 73.0.3683.105 (~2019-02-28) # 5.13.0: Security fixes up to 74.0.3729.157 (2019-05-14) # 5.13.1: Security fixes up to 76.0.3809.87 (2019-07-30) # 5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10) utils.VersionNumber(5, 13): '73.0.3683.105', # Qt 5.14: Chromium 77 # 77.0.3865.129 (~2019-10-10) # 5.14.0: Security fixes up to 77.0.3865.129 (~2019-09-10) # 5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07) # 5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03) utils.VersionNumber(5, 14): '77.0.3865.129', # Qt 5.15: Chromium 80 # 80.0.3987.163 (2020-04-02) # 5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05) # 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25) # 5.15.2: Updated to 83.0.4103.122 (~2020-06-24) # Security fixes up to 86.0.4240.183 (2020-11-02) # 5.15.3: Updated to 87.0.4280.144 (~2020-12-02) # Security fixes up to 88.0.4324.150 (2021-02-04) utils.VersionNumber(5, 15): '80.0.3987.163', utils.VersionNumber(5, 15, 2): '83.0.4103.122', utils.VersionNumber(5, 15, 3): '87.0.4280.144', } def __post_init__(self) -> None: """Set the major Chromium version.""" if self.chromium is None: self.chromium_major = None else: self.chromium_major = int(self.chromium.split('.')[0]) def __str__(self) -> str: s = 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 @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. """ assert ua.qt_version is not None, ua return cls( webengine=utils.VersionNumber.parse(ua.qt_version), chromium=ua.upstream_browser_version, source='UA', ) @classmethod def from_elf(cls, versions: elf.Versions) -> 'WebEngineVersions': """Get the versions based on an ELF file. This only works on Linux, and even there, depends on various assumption on how QtWebEngine is built (e.g. that the version string is in the .rodata section). On Windows/macOS, we instead rely on from_pyqt, but especially on Linux, people sometimes mix and match Qt/QtWebEngine versions, so this is a more reliable (though hackish) way to get a more accurate result. """ return cls( webengine=utils.VersionNumber.parse(versions.webengine), chromium=versions.chromium, source='ELF', ) @classmethod 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) if chromium_version is not None: return chromium_version # 5.15 patch versions change their QtWebEngine version, but no changes are # expected after 5.15.3. v5_15_3 = utils.VersionNumber(5, 15, 3) if v5_15_3 <= pyqt_webengine_version < utils.VersionNumber(6): minor_version = v5_15_3 else: # e.g. 5.14.2 -> 5.14 minor_version = pyqt_webengine_version.strip_patch() return cls._CHROMIUM_VERSIONS.get(minor_version) @classmethod def from_importlib(cls, pyqt_webengine_qt_version: str) -> 'WebEngineVersions': """Get the versions based on the PyQtWebEngine version. This is called if we don't want to fully initialize QtWebEngine (so from_ua isn't possible), we're not on Linux (or ELF parsing failed), but we have a PyQtWebEngine-Qt{,5} package from PyPI, so we could query its exact version. """ parsed = utils.VersionNumber.parse(pyqt_webengine_qt_version) return cls( webengine=parsed, chromium=cls._infer_chromium_version(parsed), source='importlib', ) @classmethod def from_pyqt(cls, pyqt_webengine_version: str) -> 'WebEngineVersions': """Get the versions based on the PyQtWebEngine version. This is the "last resort" if we don't want to fully initialize QtWebEngine (so from_ua isn't possible), we're not on Linux (or ELF parsing failed), and PyQtWebEngine-Qt{5,} isn't available from PyPI. Here, we assume that the PyQtWebEngine version is the same as the QtWebEngine version, and infer the Chromium version from that. This assumption isn't generally true, but good enough for some scenarios, especially the prebuilt Windows/macOS releases. """ parsed = utils.VersionNumber.parse(pyqt_webengine_version) if utils.VersionNumber(5, 15, 3) <= parsed < utils.VersionNumber(6): # If we land here, we're in a tricky situation where we are forced to guess: # # PyQt 5.15.3 and 5.15.4 from PyPI come with QtWebEngine 5.15.2 (Chromium # 83), not 5.15.3 (Chromium 87). Given that there was no binary release of # QtWebEngine 5.15.3, this is unlikely to change before Qt 6. # # However, at this point: # # - ELF parsing failed # (so we're likely on macOS or Windows, but not definitely) # # - Getting infos from a PyPI-installed PyQtWebEngine failed # (so we're either in a PyInstaller-deployed qutebrowser, or a self-built # or distribution-installed Qt) # # PyQt 5.15.3 and 5.15.4 come with QtWebEngine 5.15.2 (83-based), but if # someone lands here with the last Qt/PyQt installed from source, they might # be using QtWebEngine 5.15.3 (87-based). For now, we play it safe, and only # do this kind of "downgrade" when we know we're using PyInstaller. frozen = hasattr(sys, 'frozen') log.misc.debug(f"PyQt5 >= 5.15.3, frozen {frozen}") if frozen: parsed = utils.VersionNumber(5, 15, 2) return cls( webengine=parsed, chromium=cls._infer_chromium_version(parsed), source='PyQt', ) @classmethod def from_qt(cls, qt_version: str, *, source: str = 'Qt') -> 'WebEngineVersions': """Get the versions based on the Qt version. This is called if we don't have PYQT_WEBENGINE_VERSION, i.e. with PyQt 5.12. """ parsed = utils.VersionNumber.parse(qt_version) return cls( webengine=parsed, chromium=cls._infer_chromium_version(parsed), source=source, )
def test_version_info(params, stubs, monkeypatch, config_stub): """Test version.version_info().""" config.instance.config_py_loaded = params.config_py_loaded import_path = pathlib.Path('/IMPORTPATH').resolve() patches = { 'qutebrowser.__file__': str(import_path / '__init__.py'), 'qutebrowser.__version__': 'VERSION', '_git_str': lambda: ('GIT COMMIT' if params.git_commit else None), 'platform.python_implementation': lambda: 'PYTHON IMPLEMENTATION', 'platform.python_version': lambda: 'PYTHON VERSION', 'sys.executable': 'EXECUTABLE PATH', 'PYQT_VERSION_STR': 'PYQT VERSION', 'earlyinit.qt_version': lambda: 'QT VERSION', '_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'], '_pdfjs_version': lambda: 'PDFJS VERSION', 'QSslSocket': FakeQSslSocket('SSL VERSION', params.ssl_support), 'platform.platform': lambda: 'PLATFORM', 'platform.architecture': lambda: ('ARCHITECTURE', ''), '_os_info': lambda: ['OS INFO 1', 'OS INFO 2'], '_path_info': lambda: {'PATH DESC': 'PATH NAME'}, 'objects.qapp': (stubs.FakeQApplication(style='STYLE', platform_name='PLATFORM') if params.qapp else None), 'QLibraryInfo.location': (lambda _loc: 'QT PATH'), 'sql.version': lambda: 'SQLITE VERSION', '_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45), 'config.instance.yaml_loaded': params.autoconfig_loaded, } version.opengl_info.cache_clear() monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION') substitutions = { 'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '', 'style': '\nStyle: STYLE' if params.qapp else '', 'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp else ''), 'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '', 'qt': 'QT VERSION', 'frozen': str(params.frozen), 'import_path': import_path, 'python_path': 'EXECUTABLE PATH', 'uptime': "1:23:45", 'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no", } patches['qtwebengine_versions'] = ( lambda avoid_init: version.WebEngineVersions( webengine=utils.VersionNumber(1, 2, 3), chromium=None, source='faked', ) ) if params.config_py_loaded: substitutions["config_py_loaded"] = "{} has been loaded".format( standarddir.config_py()) else: substitutions["config_py_loaded"] = "no config.py was loaded" if params.with_webkit: patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION' patches['objects.backend'] = usertypes.Backend.QtWebKit substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)' else: monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) patches['objects.backend'] = usertypes.Backend.QtWebEngine substitutions['backend'] = 'QtWebEngine 1.2.3 (from faked)' if params.known_distribution: patches['distribution'] = lambda: version.DistributionInfo( parsed=version.Distribution.arch, version=None, pretty='LINUX DISTRIBUTION', id='arch') substitutions['linuxdist'] = ('\nLinux distribution: ' 'LINUX DISTRIBUTION (arch)') substitutions['osinfo'] = '' else: patches['distribution'] = lambda: None substitutions['linuxdist'] = '' substitutions['osinfo'] = 'OS INFO 1\nOS INFO 2\n' substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no' for name, val in patches.items(): monkeypatch.setattr(f'qutebrowser.utils.version.{name}', val) if params.frozen: monkeypatch.setattr(sys, 'frozen', True, raising=False) else: monkeypatch.delattr(sys, 'frozen', raising=False) template = version._LOGO.lstrip('\n') + textwrap.dedent(""" qutebrowser vVERSION{git_commit} Backend: {backend} Qt: {qt} PYTHON IMPLEMENTATION: PYTHON VERSION PyQt: PYQT VERSION MODULE VERSION 1 MODULE VERSION 2 pdf.js: PDFJS VERSION sqlite: SQLITE VERSION QtNetwork SSL: {ssl} {style}{platform_plugin}{opengl} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen} Imported from {import_path} Using Python from {python_path} Qt library executable path: QT PATH, data path: QT PATH {osinfo} Paths: PATH DESC: PATH NAME Autoconfig loaded: {autoconfig_loaded} Config.py: {config_py_loaded} Uptime: {uptime} """.lstrip('\n')) expected = template.rstrip('\n').format(**substitutions) assert version.version_info() == expected
version.DistributionInfo(id='arch', parsed=version.Distribution.arch, version=None, pretty='Arch Linux')), # Ubuntu 14.04 (""" NAME="Ubuntu" VERSION="14.04.5 LTS, Trusty Tahr" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 14.04.5 LTS" VERSION_ID="14.04" """, version.DistributionInfo(id='ubuntu', parsed=version.Distribution.ubuntu, version=utils.VersionNumber(14, 4, 5), pretty='Ubuntu 14.04.5 LTS')), # Ubuntu 17.04 (""" NAME="Ubuntu" VERSION="17.04 (Zesty Zapus)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 17.04" VERSION_ID="17.04" """, version.DistributionInfo(id='ubuntu', parsed=version.Distribution.ubuntu, version=utils.VersionNumber(17, 4), pretty='Ubuntu 17.04')), # Debian Jessie
class WebEngineVersions: """Version numbers for QtWebEngine and the underlying Chromium.""" webengine: utils.VersionNumber chromium: Optional[str] source: str chromium_major: Optional[int] = dataclasses.field(init=False) _CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, str]] = { # Qt 5.12: Chromium 69 # (LTS) 69.0.3497.128 (~2018-09-11) # 5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24) # 5.12.1: Security fixes up to 71.0.3578.94 (2018-12-12) # 5.12.2: Security fixes up to 72.0.3626.121 (2019-03-01) # 5.12.3: Security fixes up to 73.0.3683.75 (2019-03-12) # 5.12.4: Security fixes up to 74.0.3729.157 (2019-05-14) # 5.12.5: Security fixes up to 76.0.3809.87 (2019-07-30) # 5.12.6: Security fixes up to 77.0.3865.120 (~2019-09-10) # 5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16) # 5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18) # 5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03) # 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06) utils.VersionNumber(5, 12): '69.0.3497.128', # Qt 5.13: Chromium 73 # 73.0.3683.105 (~2019-02-28) # 5.13.0: Security fixes up to 74.0.3729.157 (2019-05-14) # 5.13.1: Security fixes up to 76.0.3809.87 (2019-07-30) # 5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10) utils.VersionNumber(5, 13): '73.0.3683.105', # Qt 5.14: Chromium 77 # 77.0.3865.129 (~2019-10-10) # 5.14.0: Security fixes up to 77.0.3865.129 (~2019-09-10) # 5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07) # 5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03) utils.VersionNumber(5, 14): '77.0.3865.129', # Qt 5.15: Chromium 80 # 80.0.3987.163 (2020-04-02) # 5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05) # 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25) # 5.15.2: Updated to 83.0.4103.122 (~2020-06-24) # Security fixes up to 86.0.4240.183 (2020-11-02) # 5.15.3: Updated to 87.0.4280.144 (~2020-12-02) # Security fixes up to 88.0.4324.150 (2021-02-04) utils.VersionNumber(5, 15): '80.0.3987.163', utils.VersionNumber(5, 15, 2): '83.0.4103.122', utils.VersionNumber(5, 15, 3): '87.0.4280.144', } def __post_init__(self) -> None: """Set the major Chromium version.""" if self.chromium is None: self.chromium_major = None else: self.chromium_major = int(self.chromium.split('.')[0]) def __str__(self) -> str: s = f'QtWebEngine {self.webengine}' if self.chromium is not None: s += f', Chromium {self.chromium}' if self.source != 'UA': s += f' (from {self.source})' return s @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. """ assert ua.qt_version is not None, ua return cls( webengine=utils.VersionNumber.parse(ua.qt_version), chromium=ua.upstream_browser_version, source='UA', ) @classmethod def from_elf(cls, versions: elf.Versions) -> 'WebEngineVersions': """Get the versions based on an ELF file. This only works on Linux, and even there, depends on various assumption on how QtWebEngine is built (e.g. that the version string is in the .rodata section). On Windows/macOS, we instead rely on from_pyqt, but especially on Linux, people sometimes mix and match Qt/QtWebEngine versions, so this is a more reliable (though hackish) way to get a more accurate result. """ return cls( webengine=utils.VersionNumber.parse(versions.webengine), chromium=versions.chromium, source='ELF', ) @classmethod 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) if chromium_version is not None: return chromium_version # 5.15 patch versions change their QtWebEngine version, but no changes are # expected after 5.15.3. v5_15_3 = utils.VersionNumber(5, 15, 3) if v5_15_3 <= pyqt_webengine_version < utils.VersionNumber(6): minor_version = v5_15_3 else: # e.g. 5.14.2 -> 5.14 minor_version = pyqt_webengine_version.strip_patch() return cls._CHROMIUM_VERSIONS.get(minor_version) @classmethod def from_pyqt( cls, pyqt_webengine_version: str, source: str = 'PyQt', ) -> 'WebEngineVersions': """Get the versions based on the PyQtWebEngine version. This is the "last resort" if we don't want to fully initialize QtWebEngine (so from_ua isn't possible) and we're not on Linux (or ELF parsing failed). Here, we assume that the PyQtWebEngine version is the same as the QtWebEngine version, and infer the Chromium version from that. This assumption isn't generally true, but good enough for some scenarios, especially the prebuilt Windows/macOS releases. Note that we only can get the PyQtWebEngine version with PyQt 5.13 or newer. With Qt 5.12, we instead rely on qVersion(). """ parsed = utils.VersionNumber.parse(pyqt_webengine_version) return cls( webengine=parsed, chromium=cls._infer_chromium_version(parsed), source=source, )
def qtwe_version(): """A version number needing the workaround.""" return utils.VersionNumber(5, 15, 3)
locale_name=locale_name, ) locales_path = qtargs._webengine_locales_path() original_path = qtargs._get_locale_pak_path(locales_path, locale_name) if override is None: assert original_path.exists() else: assert override == expected assert not original_path.exists() assert qtargs._get_locale_pak_path(locales_path, override).exists() @pytest.mark.parametrize('version', [ utils.VersionNumber(5, 14, 2), utils.VersionNumber(5, 15, 2), utils.VersionNumber(5, 15, 4), utils.VersionNumber(6), ]) @pytest.mark.fake_os('linux') def test_different_qt_version(version): assert qtargs._get_lang_override(version, "de-CH") is None @pytest.mark.fake_os('windows') def test_non_linux(qtwe_version): assert qtargs._get_lang_override(qtwe_version, "de-CH") is None @pytest.mark.fake_os('linux')
def _find_quirks( # noqa: C901 ("too complex" self, name: str, vendor: str, ver: str, ) -> Optional[_ServerQuirks]: """Find quirks to use based on the server information.""" if (name, vendor) == ("notify-osd", "Canonical Ltd"): # Shows a dialog box instead of a notification bubble as soon as a # notification has an action (even if only a default one). Dialog boxes are # buggy and return a notification with ID 0. # https://wiki.ubuntu.com/NotificationDevelopmentGuidelines#Avoiding_actions return _ServerQuirks(avoid_actions=True, spec_version="1.1") elif (name, vendor) == ("Notification Daemon", "MATE"): # Still in active development but doesn't implement spec 1.2: # https://github.com/mate-desktop/mate-notification-daemon/issues/132 quirks = _ServerQuirks(spec_version="1.1") if utils.VersionNumber.parse(ver) <= utils.VersionNumber(1, 24): # https://github.com/mate-desktop/mate-notification-daemon/issues/118 quirks.avoid_body_hyperlinks = True return quirks elif (name, vendor) == ("naughty", "awesome") and ver != "devel": # Still in active development but spec 1.0/1.2 support isn't # released yet: # https://github.com/awesomeWM/awesome/commit/e076bc664e0764a3d3a0164dabd9b58d334355f4 parsed_version = utils.VersionNumber.parse(ver.lstrip('v')) if parsed_version <= utils.VersionNumber(4, 3): return _ServerQuirks(spec_version="1.0") elif (name, vendor) == ("twmnd", "twmnd"): # https://github.com/sboli/twmn/pull/96 return _ServerQuirks(spec_version="0") elif (name, vendor) == ("tiramisu", "Sweets"): if utils.VersionNumber.parse(ver) < utils.VersionNumber(2, 0): # https://github.com/Sweets/tiramisu/issues/20 return _ServerQuirks(skip_capabilities=True) elif (name, vendor) == ("lxqt-notificationd", "lxqt.org"): quirks = _ServerQuirks() parsed_version = utils.VersionNumber.parse(ver) if parsed_version <= utils.VersionNumber(0, 16): # https://github.com/lxqt/lxqt-notificationd/issues/253 quirks.escape_title = True if parsed_version < utils.VersionNumber(0, 16): # https://github.com/lxqt/lxqt-notificationd/commit/c23e254a63c39837fb69d5c59c5e2bc91e83df8c quirks.icon_key = 'image_data' return quirks elif (name, vendor) == ("haskell-notification-daemon", "abc"): # aka "deadd" return _ServerQuirks( # https://github.com/phuhl/linux_notification_center/issues/160 spec_version="1.0", # https://github.com/phuhl/linux_notification_center/issues/161 wrong_replaces_id=True, ) elif (name, vendor) == ("ninomiya", "deifactor"): return _ServerQuirks( no_padded_images=True, wrong_replaces_id=True, ) elif (name, vendor) == ("Raven", "Budgie Desktop Developers"): # Before refactor return _ServerQuirks( # https://github.com/solus-project/budgie-desktop/issues/2114 escape_title=True, # https://github.com/solus-project/budgie-desktop/issues/2115 wrong_replaces_id=True, ) elif (name, vendor) == ("Budgie Notification Server", "Budgie Desktop Developers"): # After refactor: https://github.com/BuddiesOfBudgie/budgie-desktop/pull/36 if utils.VersionNumber.parse(ver) < utils.VersionNumber(10, 6, 2): return _ServerQuirks( # https://github.com/BuddiesOfBudgie/budgie-desktop/issues/118 wrong_closes_type=True, ) return None
def _notifications_supported() -> bool: """Check whether the current QtWebEngine version has notification support.""" versions = version.qtwebengine_versions(avoid_init=True) return versions.webengine >= utils.VersionNumber(5, 14)
""", version.DistributionInfo( id='arch', parsed=version.Distribution.arch, version=None, pretty='Arch Linux')), # Ubuntu 14.04 (""" NAME="Ubuntu" VERSION="14.04.5 LTS, Trusty Tahr" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 14.04.5 LTS" VERSION_ID="14.04" """, version.DistributionInfo( id='ubuntu', parsed=version.Distribution.ubuntu, version=utils.VersionNumber(14, 4, 5), pretty='Ubuntu 14.04.5 LTS')), # Ubuntu 17.04 (""" NAME="Ubuntu" VERSION="17.04 (Zesty Zapus)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 17.04" VERSION_ID="17.04" """, version.DistributionInfo( id='ubuntu', parsed=version.Distribution.ubuntu, version=utils.VersionNumber(17, 4), pretty='Ubuntu 17.04')), # Debian Jessie
class TestChromiumVersion: @pytest.fixture(autouse=True) def clear_parsed_ua(self, monkeypatch): pytest.importorskip('PyQt5.QtWebEngineWidgets') if webenginesettings is not None: # Not available with QtWebKit monkeypatch.setattr(webenginesettings, 'parsed_user_agent', None) def test_fake_ua(self, monkeypatch, caplog): ver = '77.0.3865.98' webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format(ver)) assert version.qtwebengine_versions().chromium == ver def test_prefers_saved_user_agent(self, monkeypatch): webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87')) class FakeProfile: def defaultProfile(self): raise AssertionError("Should not be called") monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile()) version.qtwebengine_versions() def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub): assert version.qtwebengine_versions().chromium is not None def test_avoided(self, monkeypatch): versions = version.qtwebengine_versions(avoid_init=True) assert versions.source in ['ELF', 'importlib', 'PyQt', 'Qt'] @pytest.fixture def patch_elf_fail(self, monkeypatch): """Simulate parsing the version from ELF to fail.""" monkeypatch.setattr(elf, 'parse_webenginecore', lambda: None) @pytest.fixture def patch_old_pyqt(self, monkeypatch): """Simulate an old PyQt without PYQT_WEBENGINE_VERSION_STR.""" monkeypatch.setattr(version, 'PYQT_WEBENGINE_VERSION_STR', None) @pytest.fixture def patch_no_importlib(self, monkeypatch, stubs): """Simulate missing importlib modules.""" import_fake = stubs.ImportFake({ 'importlib_metadata': False, 'importlib.metadata': False, }, monkeypatch) import_fake.patch() @pytest.fixture def importlib_patcher(self, monkeypatch): """Patch the importlib module.""" def _patch(*, qt, qt5): try: import importlib.metadata as importlib_metadata except ImportError: importlib_metadata = pytest.importorskip("importlib_metadata") def _fake_version(name): if name == 'PyQtWebEngine-Qt': outcome = qt elif name == 'PyQtWebEngine-Qt5': outcome = qt5 else: raise utils.Unreachable(outcome) if outcome is None: raise importlib_metadata.PackageNotFoundError(name) return outcome monkeypatch.setattr(importlib_metadata, 'version', _fake_version) return _patch @pytest.fixture def patch_importlib_no_package(self, importlib_patcher): """Simulate importlib not finding PyQtWebEngine-Qt[5].""" importlib_patcher(qt=None, qt5=None) @pytest.mark.parametrize('patches, sources', [ (['elf_fail'], ['importlib', 'PyQt', 'Qt']), (['elf_fail', 'old_pyqt'], ['importlib', 'Qt']), (['elf_fail', 'no_importlib'], ['PyQt', 'Qt']), (['elf_fail', 'no_importlib', 'old_pyqt'], ['Qt']), (['elf_fail', 'importlib_no_package'], ['PyQt', 'Qt']), (['elf_fail', 'importlib_no_package', 'old_pyqt'], ['Qt']), ], ids=','.join) def test_simulated(self, request, patches, sources): """Test various simulated error conditions. This dynamically gets a list of fixtures (above) to do the patching. It then checks whether the version it got is from one of the expected sources. Depending on the environment this test is run in, some sources might fail "naturally", i.e. without any patching related to them. """ for patch in patches: request.getfixturevalue(f'patch_{patch}') versions = version.qtwebengine_versions(avoid_init=True) assert versions.source in sources @pytest.mark.parametrize('qt, qt5, expected', [ (None, '5.15.4', utils.VersionNumber(5, 15, 4)), ('5.15.3', None, utils.VersionNumber(5, 15, 3)), ('5.15.3', '5.15.4', utils.VersionNumber(5, 15, 4)), # -Qt5 takes precedence ]) def test_importlib(self, qt, qt5, expected, patch_elf_fail, importlib_patcher): """Test the importlib version logic with different Qt packages. With PyQtWebEngine 5.15.4, PyQtWebEngine-Qt was renamed to PyQtWebEngine-Qt5. """ importlib_patcher(qt=qt, qt5=qt5) versions = version.qtwebengine_versions(avoid_init=True) assert versions.source == 'importlib' assert versions.webengine == expected
def _qtwebengine_settings_args(versions: version.WebEngineVersions) -> Iterator[str]: settings: Dict[str, Dict[Any, Optional[str]]] = { 'qt.force_software_rendering': { 'software-opengl': None, 'qt-quick': None, 'chromium': '--disable-gpu', 'none': None, }, 'content.canvas_reading': { True: None, False: '--disable-reading-from-canvas', }, 'content.webrtc_ip_handling_policy': { 'all-interfaces': None, 'default-public-and-private-interfaces': '--force-webrtc-ip-handling-policy=' 'default_public_and_private_interfaces', 'default-public-interface-only': '--force-webrtc-ip-handling-policy=' 'default_public_interface_only', 'disable-non-proxied-udp': '--force-webrtc-ip-handling-policy=' 'disable_non_proxied_udp', }, 'qt.process_model': { 'process-per-site-instance': None, 'process-per-site': '--process-per-site', 'single-process': '--single-process', }, 'qt.low_end_device_mode': { 'auto': None, 'always': '--enable-low-end-device-mode', 'never': '--disable-low-end-device-mode', }, 'content.headers.referer': { 'always': None, } } qt_514_ver = utils.VersionNumber(5, 14) if qt_514_ver <= versions.webengine < utils.VersionNumber(5, 15, 2): # In Qt 5.14 to 5.15.1, `--force-dark-mode` is used to set the # preferred colorscheme. In Qt 5.15.2, this is handled by a # blink-setting in browser/webengine/darkmode.py instead. settings['colors.webpage.preferred_color_scheme'] = { 'dark': '--force-dark-mode', 'light': None, 'auto': None, } referrer_setting = settings['content.headers.referer'] if versions.webengine >= qt_514_ver: # Starting with Qt 5.14, this is handled via --enable-features referrer_setting['same-domain'] = None else: referrer_setting['same-domain'] = '--reduced-referrer-granularity' # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60203 can_override_referer = ( versions.webengine >= utils.VersionNumber(5, 12, 4) and versions.webengine != utils.VersionNumber(5, 13) ) referrer_setting['never'] = None if can_override_referer else '--no-referrers' for setting, args in sorted(settings.items()): arg = args[config.instance.get(setting)] if arg is not None: yield arg
class TestWebEngineVersions: @pytest.mark.parametrize('version, expected', [ ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium=None, source='UA'), "QtWebEngine 5.15.2", ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='UA'), "QtWebEngine 5.15.2, Chromium 87.0.4280.144", ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='faked'), "QtWebEngine 5.15.2, Chromium 87.0.4280.144 (from faked)", ), ]) def test_str(self, version, expected): assert str(version) == expected @pytest.mark.parametrize('version, expected', [ ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium=None, source='test'), None, ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='test'), 87, ), ]) def test_chromium_major(self, version, expected): assert version.chromium_major == expected def test_from_ua(self): ua = websettings.UserAgent( os_info='X11; Linux x86_64', webkit_version='537.36', upstream_browser_key='Chrome', upstream_browser_version='83.0.4103.122', qt_key='QtWebEngine', qt_version='5.15.2', ) expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='UA', ) assert version.WebEngineVersions.from_ua(ua) == expected def test_from_elf(self): elf_version = elf.Versions(webengine='5.15.2', chromium='83.0.4103.122') expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='ELF', ) assert version.WebEngineVersions.from_elf(elf_version) == expected @pytest.mark.parametrize('pyqt_version, chromium_version', [ ('5.12.10', '69.0.3497.128'), ('5.14.2', '77.0.3865.129'), ('5.15.1', '80.0.3987.163'), ('5.15.2', '83.0.4103.122'), ('5.15.3', '87.0.4280.144'), ('5.15.4', '87.0.4280.144'), ('5.15.5', '87.0.4280.144'), ]) def test_from_pyqt(self, freezer, pyqt_version, chromium_version): if freezer and pyqt_version in ['5.15.3', '5.15.4', '5.15.5']: chromium_version = '83.0.4103.122' expected_pyqt_version = '5.15.2' else: expected_pyqt_version = pyqt_version expected = version.WebEngineVersions( webengine=utils.VersionNumber.parse(expected_pyqt_version), chromium=chromium_version, source='PyQt', ) assert version.WebEngineVersions.from_pyqt(pyqt_version) == expected def test_real_chromium_version(self, qapp): """Compare the inferred Chromium version with the real one.""" if '.dev' in PYQT_VERSION_STR: pytest.skip("dev version of PyQt5") try: from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR except ImportError as e: # QtWebKit or QtWebEngine < 5.1'../3 pytest.skip(str(e)) pyqt_webengine_version = version._get_pyqt_webengine_qt_version() if pyqt_webengine_version is None: pyqt_webengine_version = PYQT_WEBENGINE_VERSION_STR versions = version.WebEngineVersions.from_pyqt(pyqt_webengine_version) webenginesettings.init_user_agent() expected = webenginesettings.parsed_user_agent.upstream_browser_version assert versions.chromium == expected
def _qtwebengine_features( versions: version.WebEngineVersions, special_flags: Sequence[str], ) -> Tuple[Sequence[str], Sequence[str]]: """Get a tuple of --enable-features/--disable-features flags for QtWebEngine. Args: versions: The WebEngineVersions to get flags for. special_flags: Existing flags passed via the commandline. """ enabled_features = [] disabled_features = [] for flag in special_flags: if flag.startswith(_ENABLE_FEATURES): flag = flag[len(_ENABLE_FEATURES):] enabled_features += flag.split(',') elif flag.startswith(_DISABLE_FEATURES): flag = flag[len(_DISABLE_FEATURES):] disabled_features += flag.split(',') elif flag.startswith(_BLINK_SETTINGS): pass else: raise utils.Unreachable(flag) if versions.webengine >= utils.VersionNumber(5, 15, 1) and utils.is_linux: # Enable WebRTC PipeWire for screen capturing on Wayland. # # This is disabled in Chromium by default because of the "dialog hell": # https://bugs.chromium.org/p/chromium/issues/detail?id=682122#c50 # https://github.com/flatpak/xdg-desktop-portal-gtk/issues/204 # # However, we don't have Chromium's confirmation dialog in qutebrowser, # so we should only get qutebrowser's permission dialog. # # In theory this would be supported with Qt 5.13 already, but # QtWebEngine only started picking up PipeWire correctly with Qt # 5.15.1. # # This only should be enabled on Wayland, but it's too early to check # that, as we don't have a QApplication available at this point. Thus, # just turn it on unconditionally on Linux, which shouldn't hurt. enabled_features.append('WebRTCPipeWireCapturer') if not utils.is_mac: # Enable overlay scrollbars. # # There are two additional flags in Chromium: # # - OverlayScrollbarFlashAfterAnyScrollUpdate # - OverlayScrollbarFlashWhenMouseEnter # # We don't expose/activate those, but the changes they introduce are # quite subtle: The former seems to show the scrollbar handle even if # there was a 0px scroll (though no idea how that can happen...). The # latter flashes *all* scrollbars when a scrollable area was entered, # which doesn't seem to make much sense. if config.val.scrolling.bar == 'overlay': enabled_features.append('OverlayScrollbar') if (versions.webengine >= utils.VersionNumber(5, 14) and config.val.content.headers.referer == 'same-domain'): # Handling of reduced-referrer-granularity in Chromium 76+ # https://chromium-review.googlesource.com/c/chromium/src/+/1572699 # # Note that this is removed entirely (and apparently the default) starting with # Chromium 89 (presumably arriving with Qt 6.2): # https://chromium-review.googlesource.com/c/chromium/src/+/2545444 enabled_features.append('ReducedReferrerGranularity') if versions.webengine == utils.VersionNumber(5, 15, 2): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89740 disabled_features.append('InstalledApp') if not config.val.input.media_keys: disabled_features.append('HardwareMediaKeyHandling') return (enabled_features, disabled_features)
def gentoo_versions(): return version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='faked', )
class TestWebEngineVersions: @pytest.mark.parametrize('version, expected', [ ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium=None, source='UA'), "QtWebEngine 5.15.2", ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='UA'), "QtWebEngine 5.15.2, Chromium 87.0.4280.144", ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='faked'), "QtWebEngine 5.15.2, Chromium 87.0.4280.144 (from faked)", ), ]) def test_str(self, version, expected): assert str(version) == expected def test_from_ua(self): ua = websettings.UserAgent( os_info='X11; Linux x86_64', webkit_version='537.36', upstream_browser_key='Chrome', upstream_browser_version='83.0.4103.122', qt_key='QtWebEngine', qt_version='5.15.2', ) expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='UA', ) assert version.WebEngineVersions.from_ua(ua) == expected def test_from_elf(self): elf_version = elf.Versions(webengine='5.15.2', chromium='83.0.4103.122') expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='ELF', ) assert version.WebEngineVersions.from_elf(elf_version) == expected def test_from_pyqt(self): expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', source='PyQt', ) assert version.WebEngineVersions.from_pyqt('5.15.2') == expected def test_real_chromium_version(self, qapp): """Compare the inferred Chromium version with the real one.""" try: from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR except ImportError as e: # QtWebKit or QtWebEngine < 5.13 pytest.skip(str(e)) from qutebrowser.browser.webengine import webenginesettings webenginesettings.init_user_agent() expected = webenginesettings.parsed_user_agent.upstream_browser_version versions = version.WebEngineVersions.from_pyqt(PYQT_WEBENGINE_VERSION_STR) assert versions.chromium == expected