def _parse_yaml_backends_dict(name, node): """Parse a dict definition for backends. Example: backends: QtWebKit: true QtWebEngine: Qt 5.9 """ str_to_backend = { 'QtWebKit': usertypes.Backend.QtWebKit, 'QtWebEngine': usertypes.Backend.QtWebEngine, } if node.keys() != str_to_backend.keys(): _raise_invalid_node(name, 'backends', node) backends = [] # The value associated to the key, and whether we should add that backend # or not. conditionals = { True: True, False: False, 'Qt 5.8': qtutils.version_check('5.8'), 'Qt 5.9': qtutils.version_check('5.9'), } for key in sorted(node.keys()): if conditionals[node[key]]: backends.append(str_to_backend[key]) return backends
def _qtwebengine_args() -> typing.Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): # WORKAROUND equivalent to # https://codereview.qt-project.org/#/c/217932/ # Needed for Qt < 5.9.5 and < 5.10.1 yield '--disable-shared-workers' settings = { '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, 'never': '--no-referrers', 'same-domain': '--reduced-referrer-granularity', } } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]] if not qtutils.version_check('5.11'): # On Qt 5.11, we can control this via QWebEngineSettings settings['content.autoplay'] = { True: None, False: '--autoplay-policy=user-gesture-required', } for setting, args in sorted(settings.items()): arg = args[config.instance.get(setting)] if arg is not None: yield arg
def _on_load_started(self): """Clear search when a new load is started if needed.""" if (qtutils.version_check('5.9') and not qtutils.version_check('5.9.2')): # WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-61506 self.search.clear() super()._on_load_started()
def install(self, profile): """Install the handler for qute:// URLs on the given profile.""" if QWebEngineUrlScheme is not None: assert QWebEngineUrlScheme.schemeByName(b'qute') is not None profile.installUrlSchemeHandler(b'qute', self) if (qtutils.version_check('5.11', compiled=False) and not qtutils.version_check('5.12', compiled=False)): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378 profile.installUrlSchemeHandler(b'chrome-error', self) profile.installUrlSchemeHandler(b'chrome-extension', self)
def check_qt_version(args): """Check if the Qt version is recent enough.""" from PyQt5.QtCore import qVersion from qutebrowser.utils import qtutils if qtutils.version_check('5.2.0', operator.lt): text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but {} is " "installed.".format(qVersion())) _die(text) elif args.backend == 'webengine' and qtutils.version_check('5.6.0', operator.lt): text = ("Fatal error: Qt and PyQt >= 5.6.0 are required for " "QtWebEngine support, but {} is installed.".format(qVersion())) _die(text)
def _apply_platform_markers(config, item): """Apply a skip marker to a given item.""" markers = [ ('posix', not utils.is_posix, "Requires a POSIX os"), ('windows', not utils.is_windows, "Requires Windows"), ('linux', not utils.is_linux, "Requires Linux"), ('mac', not utils.is_mac, "Requires macOS"), ('not_mac', utils.is_mac, "Skipped on macOS"), ('not_frozen', getattr(sys, 'frozen', False), "Can't be run when frozen"), ('frozen', not getattr(sys, 'frozen', False), "Can only run when frozen"), ('ci', not ON_CI, "Only runs on CI."), ('no_ci', ON_CI, "Skipped on CI."), ('issue2478', utils.is_windows and config.webengine, "Broken with QtWebEngine on Windows"), ('issue3572', (qtutils.version_check('5.10', compiled=False, exact=True) or qtutils.version_check('5.10.1', compiled=False, exact=True)) and config.webengine and 'TRAVIS' in os.environ, "Broken with QtWebEngine with Qt 5.10 on Travis"), ('qtbug60673', qtutils.version_check('5.8') and not qtutils.version_check('5.10') and config.webengine, "Broken on webengine due to " "https://bugreports.qt.io/browse/QTBUG-60673"), ('unicode_locale', sys.getfilesystemencoding() == 'ascii', "Skipped because of ASCII locale"), ('qtwebkit6021_skip', version.qWebKitVersion and version.qWebKitVersion() == '602.1', "Broken on WebKit 602.1") ] for searched_marker, condition, default_reason in markers: marker = item.get_closest_marker(searched_marker) if not marker or not condition: continue if 'reason' in marker.kwargs: reason = '{}: {}'.format(default_reason, marker.kwargs['reason']) del marker.kwargs['reason'] else: reason = default_reason + '.' skipif_marker = pytest.mark.skipif(condition, *marker.args, reason=reason, **marker.kwargs) item.add_marker(skipif_marker)
def get_fatal_crash_dialog(debug, data): """Get a fatal crash dialog based on a crash log. If the crash is a segfault in qt_mainloop and we're on an old Qt version this is a simple error dialog which lets the user know they should upgrade if possible. If it's anything else, it's a normal FatalCrashDialog with the possibility to report the crash. Args: debug: Whether the debug flag (--debug) was given. data: The crash log data. """ errtype, frame = parse_fatal_stacktrace(data) if (qtutils.version_check('5.4') or errtype != 'Segmentation fault' or frame != 'qt_mainloop'): return FatalCrashDialog(debug, data) else: title = "qutebrowser was restarted after a fatal crash!" text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>" "Unfortunately, this crash occurred in Qt (the library " "qutebrowser uses), and your version ({}) is outdated - " "Qt 5.4 or later is recommended. Unfortuntately Debian and " "Ubuntu don't ship a newer version (yet?)...".format( qVersion())) return QMessageBox(QMessageBox.Critical, title, text, QMessageBox.Ok)
def pytest_collection_modifyitems(config, items): """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE.""" vercheck = qtutils.version_check qtbug_54419_fixed = ((vercheck('5.6.2') and not vercheck('5.7.0')) or qtutils.version_check('5.7.1') or os.environ.get('QUTE_QTBUG54419_PATCHED', '')) markers = [ ('qtwebengine_createWindow', 'Skipped because of QTBUG-54419', pytest.mark.skipif, not qtbug_54419_fixed and config.webengine), ('qtwebengine_todo', 'QtWebEngine TODO', pytest.mark.xfail, config.webengine), ('qtwebengine_skip', 'Skipped with QtWebEngine', pytest.mark.skipif, config.webengine), ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, not config.webengine), ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, config.webengine), ('qtwebengine_osx_xfail', 'Fails on OS X with QtWebEngine', pytest.mark.xfail, config.webengine and sys.platform == 'darwin'), ] for item in items: for name, prefix, pytest_mark, condition in markers: marker = item.get_marker(name) if marker and condition: if marker.args: text = '{}: {}'.format(prefix, marker.args[0]) else: text = prefix item.add_marker(pytest_mark(condition, reason=text, **marker.kwargs))
def _get_suggested_filename(path): """Convert a path we got from chromium to a suggested filename. Chromium thinks we want to download stuff to ~/Download, so even if we don't, we get downloads with a suffix like (1) for files existing there. We simply strip the suffix off via regex. See https://bugreports.qt.io/browse/QTBUG-56978 """ filename = os.path.basename(path) suffix_re = re.compile(r""" \ ? # Optional space between filename and suffix ( # Numerical suffix \([0-9]+\) | # ISO-8601 suffix # https://cs.chromium.org/chromium/src/base/time/time_to_iso8601.cc \ -\ \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z ) (?=\.|$) # Begin of extension, or filename without extension """, re.VERBOSE) filename = suffix_re.sub('', filename) if not qtutils.version_check('5.9', compiled=False): # https://bugreports.qt.io/browse/QTBUG-58155 filename = urllib.parse.unquote(filename) # Doing basename a *second* time because there could be a %2F in # there... filename = os.path.basename(filename) return filename
def install(self, profile): """Install the handler for qute:// URLs on the given profile.""" profile.installUrlSchemeHandler(b'qute', self) if qtutils.version_check('5.11', compiled=False): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378 profile.installUrlSchemeHandler(b'chrome-error', self) profile.installUrlSchemeHandler(b'chrome-extension', self)
def normalize_line(line): line = line.rstrip('\n') line = re.sub('boundary="-+(=_qute|MultipartBoundary)-[0-9a-zA-Z-]+"', 'boundary="---=_qute-UUID"', line) line = re.sub('^-+(=_qute|MultipartBoundary)-[0-9a-zA-Z-]+$', '-----=_qute-UUID', line) line = re.sub(r'localhost:\d{1,5}', 'localhost:(port)', line) if line.startswith('Date: '): line = 'Date: today' if line.startswith('Content-ID: '): line = 'Content-ID: 42' # Depending on Python's mimetypes module/the system's mime files, .js # files could be either identified as x-javascript or just javascript line = line.replace('Content-Type: application/x-javascript', 'Content-Type: application/javascript') # With QtWebKit and newer Werkzeug versions, we also get an encoding # specified. line = line.replace('javascript; charset=utf-8', 'javascript') # Added with Qt 5.11 if (line.startswith('Snapshot-Content-Location: ') and not qtutils.version_check('5.11', compiled=False)): line = None return line
def eventFilter(self, obj, event): """Act on ChildAdded events.""" if event.type() == QEvent.ChildAdded: child = event.child() log.mouse.debug("{} got new child {}, installing filter".format( obj, child)) assert obj is self._widget child.installEventFilter(self._filter) if qtutils.version_check('5.11', compiled=False, exact=True): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076 pass_modes = [usertypes.KeyMode.command, usertypes.KeyMode.prompt, usertypes.KeyMode.yesno] if modeman.instance(self._win_id).mode not in pass_modes: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) current_index = tabbed_browser.widget.currentIndex() try: widget_index = tabbed_browser.widget.indexOf( self._widget.parent()) except RuntimeError: widget_index = -1 if current_index == widget_index: QTimer.singleShot(0, self._widget.setFocus) elif event.type() == QEvent.ChildRemoved: child = event.child() log.mouse.debug("{}: removed child {}".format(obj, child)) return False
def actute_warning(): """Display a warning about the dead_actute issue if needed.""" # WORKAROUND (remove this when we bump the requirements to 5.3.0) # Non Linux OS' aren't affected if not sys.platform.startswith('linux'): return # If no compose file exists for some reason, we're not affected if not os.path.exists('/usr/share/X11/locale/en_US.UTF-8/Compose'): return # Qt >= 5.3 doesn't seem to be affected try: if qtutils.version_check('5.3.0'): return except ValueError: pass try: with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r', encoding='utf-8') as f: for line in f: if '<dead_actute>' in line: if sys.stdout is not None: sys.stdout.flush() print("Note: If you got a 'dead_actute' warning above, " "that is not a bug in qutebrowser! See " "https://bugs.freedesktop.org/show_bug.cgi?id=69476 " "for details.") break except OSError: log.init.exception("Failed to read Compose file")
def __init__(self, win_id, tab_id, tab, parent=None): super().__init__(parent) if sys.platform == 'darwin' and qtutils.version_check('5.4'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # See https://github.com/The-Compiler/qutebrowser/issues/462 self.setStyle(QStyleFactory.create('Fusion')) self.tab = tab self.win_id = win_id self._check_insertmode = False self.scroll_pos = (-1, -1) self._old_scroll_pos = (-1, -1) self._ignore_wheel_event = False self._set_bg_color() self._tab_id = tab_id page = self._init_page() hintmanager = hints.HintManager(win_id, self._tab_id, self) hintmanager.mouse_event.connect(self.on_mouse_event) hintmanager.start_hinting.connect(page.on_start_hinting) hintmanager.stop_hinting.connect(page.on_stop_hinting) objreg.register('hintmanager', hintmanager, scope='tab', window=win_id, tab=tab_id) mode_manager = objreg.get('mode-manager', scope='window', window=win_id) mode_manager.entered.connect(self.on_mode_entered) mode_manager.left.connect(self.on_mode_left) if config.get('input', 'rocker-gestures'): self.setContextMenuPolicy(Qt.PreventContextMenu) objreg.get('config').changed.connect(self.on_config_changed)
def init(): """Disable insecure SSL ciphers on old Qt versions.""" if not qtutils.version_check("5.3.0"): # Disable weak SSL ciphers. # See https://codereview.qt-project.org/#/c/75943/ good_ciphers = [c for c in QSslSocket.supportedCiphers() if c.usedBits() >= 128] QSslSocket.setDefaultCiphers(good_ciphers)
def _check_initiator(self, job): """Check whether the initiator of the job should be allowed. Only the browser itself or qute:// pages should access any of those URLs. The request interceptor further locks down qute://settings/set. Args: job: QWebEngineUrlRequestJob Return: True if the initiator is allowed, False if it was blocked. """ try: initiator = job.initiator() except AttributeError: # Added in Qt 5.11 return True if initiator == QUrl('null') and not qtutils.version_check('5.12'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421 return True if initiator.isValid() and initiator.scheme() != 'qute': log.misc.warning("Blocking malicious request from {} to {}".format( initiator.toDisplayString(), job.requestUrl().toDisplayString())) job.fail(QWebEngineUrlRequestJob.RequestDenied) return False return True
def dictionary_dir(old=False): """Return the path (str) to the QtWebEngine's dictionaries directory.""" if qtutils.version_check('5.10', compiled=False) and not old: datapath = standarddir.data() else: datapath = QLibraryInfo.location(QLibraryInfo.DataPath) return os.path.join(datapath, 'qtwebengine_dictionaries')
def inject_userscripts(): """Register user JavaScript files with the global profiles.""" # The Greasemonkey metadata block support in QtWebEngine only starts at # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response # to urlChanged. if not qtutils.version_check('5.8'): return # Since we are inserting scripts into profile.scripts they won't # just get replaced by new gm scripts like if we were injecting them # ourselves so we need to remove all gm scripts, while not removing # any other stuff that might have been added. Like the one for # stylesheets. greasemonkey = objreg.get('greasemonkey') for profile in [default_profile, private_profile]: scripts = profile.scripts() for script in scripts.toList(): if script.name().startswith("GM-"): log.greasemonkey.debug('Removing script: {}' .format(script.name())) removed = scripts.remove(script) assert removed, script.name() # Then add the new scripts. for script in greasemonkey.all_scripts(): # @run-at (and @include/@exclude/@match) is parsed by # QWebEngineScript. new_script = QWebEngineScript() new_script.setWorldId(QWebEngineScript.MainWorld) new_script.setSourceCode(script.code()) new_script.setName("GM-{}".format(script.name)) new_script.setRunsOnSubFrames(script.runs_on_sub_frames) log.greasemonkey.debug('adding script: {}' .format(new_script.name())) scripts.insert(new_script)
def __init__(self, socketname, parent=None): """Start the IPC server and listen to commands. Args: socketname: The socketname to use. parent: The parent to be used. """ super().__init__(parent) self.ignored = False self._socketname = socketname self._timer = usertypes.Timer(self, 'ipc-timeout') self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) if os.name == 'nt': # pragma: no coverage self._atime_timer = None else: self._atime_timer = usertypes.Timer(self, 'ipc-atime') self._atime_timer.setInterval(ATIME_INTERVAL) self._atime_timer.timeout.connect(self.update_atime) self._atime_timer.setTimerType(Qt.VeryCoarseTimer) self._server = QLocalServer(self) self._server.newConnection.connect(self.handle_connection) self._socket = None self._socketopts_ok = os.name == 'nt' or qtutils.version_check('5.4') if self._socketopts_ok: # pragma: no cover # If we use setSocketOptions on Unix with Qt < 5.4, we get a # NameError while listening... log.ipc.debug("Calling setSocketOptions") self._server.setSocketOptions(QLocalServer.UserAccessOption) else: # pragma: no cover log.ipc.debug("Not calling setSocketOptions")
def __init__(self, *, win_id, private, parent=None): if private: assert not qtutils.is_single_process() super().__init__(parent) self.widget = tabwidget.TabWidget(win_id, parent=self) self._win_id = win_id self._tab_insert_idx_left = 0 self._tab_insert_idx_right = -1 self.shutting_down = False self.widget.tabCloseRequested.connect(self.on_tab_close_requested) self.widget.new_tab_requested.connect(self.tabopen) self.widget.currentChanged.connect(self.on_current_changed) self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide) self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223 if qtutils.version_check('5.10', compiled=False): self.cur_load_finished.connect(self._leave_modes_on_load) else: self.cur_load_started.connect(self._leave_modes_on_load) self._undo_stack = [] self._filter = signalfilter.SignalFilter(win_id, self) self._now_focused = None self.search_text = None self.search_options = {} self._local_marks = {} self._global_marks = {} self.default_window_icon = self.widget.window().windowIcon() self.is_private = private config.instance.changed.connect(self._on_config_changed)
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 with status {}".format(code), browsertab.TerminationStatus.crashed: "Renderer process crashed", browsertab.TerminationStatus.killed: "Renderer process was killed", browsertab.TerminationStatus.unknown: "Renderer process did not start", } msg = messages[status] def show_error_page(html): tab.set_html(html) log.webview.error(msg) if qtutils.version_check('5.9', compiled=False): 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)) else: # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 message.error(msg) self._remove_tab(tab, crashed=True) if self.count() == 0: self.tabopen(QUrl('about:blank'))
def init(args): """Initialize the global QWebSettings.""" if args.enable_webengine_inspector: os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) # Workaround for a black screen with some setups # https://github.com/spyder-ide/spyder/issues/3226 if not os.environ.get('QUTE_NO_OPENGL_WORKAROUND'): # Hide "No OpenGL_accelerate module loaded: ..." message logging.getLogger('OpenGL.acceleratesupport').propagate = False try: from OpenGL import GL # pylint: disable=unused-variable except ImportError: pass else: log.misc.debug("Imported PyOpenGL as workaround") _init_profiles() # We need to do this here as a WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58650 if not qtutils.version_check('5.9'): PersistentCookiePolicy().set(config.get('content', 'cookies-store')) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) websettings.init_mappings(MAPPINGS) objreg.get('config').changed.connect(update_settings)
def __init__(self, *, win_id, tab_id, tab, private, parent=None): super().__init__(parent) if sys.platform == 'darwin' and qtutils.version_check('5.4'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # See https://github.com/qutebrowser/qutebrowser/issues/462 self.setStyle(QStyleFactory.create('Fusion')) # FIXME:qtwebengine this is only used to set the zoom factor from # the QWebPage - we should get rid of it somehow (signals?) self.tab = tab self._tabdata = tab.data self.win_id = win_id self.scroll_pos = (-1, -1) self._old_scroll_pos = (-1, -1) self._set_bg_color() self._tab_id = tab_id page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id, tabdata=tab.data, private=private, parent=self) try: page.setVisibilityState( QWebPage.VisibilityStateVisible if self.isVisible() else QWebPage.VisibilityStateHidden) except AttributeError: pass self.setPage(page) mode_manager = objreg.get('mode-manager', scope='window', window=win_id) mode_manager.entered.connect(self.on_mode_entered) mode_manager.left.connect(self.on_mode_left) objreg.get('config').changed.connect(self._set_bg_color)
def check_qt_version(): """Check if the Qt version is recent enough.""" from PyQt5.QtCore import qVersion from qutebrowser.utils import qtutils if qtutils.version_check("5.2.0", operator.lt): text = "Fatal error: Qt and PyQt >= 5.2.0 are required, but {} is " "installed.".format(qVersion()) _die(text)
def check_qt_version(backend): """Check if the Qt version is recent enough.""" from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR from qutebrowser.utils import qtutils, version if (not qtutils.version_check('5.2.0', strict=True) or PYQT_VERSION < 0x050200): text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but Qt {} / " "PyQt {} is installed.".format(version.qt_version(), PYQT_VERSION_STR)) _die(text) elif (backend == 'webengine' and ( not qtutils.version_check('5.7.1', strict=True) or PYQT_VERSION < 0x050700)): text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required for " "QtWebEngine support, but Qt {} / PyQt {} is installed." .format(version.qt_version(), PYQT_VERSION_STR)) _die(text)
def shutdown(self): self.shutting_down.emit() self.action.exit_fullscreen() if qtutils.version_check('5.8', exact=True): # WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58563 self.search.clear() self._widget.shutdown()
def _check_initiator(self, job): """Check whether the initiator of the job should be allowed. Only the browser itself or qute:// pages should access any of those URLs. The request interceptor further locks down qute://settings/set. Args: job: QWebEngineUrlRequestJob Return: True if the initiator is allowed, False if it was blocked. """ try: initiator = job.initiator() request_url = job.requestUrl() except AttributeError: # Added in Qt 5.11 return True # https://codereview.qt-project.org/#/c/234849/ is_opaque = initiator == QUrl('null') target = request_url.scheme(), request_url.host() if is_opaque and not qtutils.version_check('5.12'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421 # When we don't register the qute:// scheme, all requests are # flagged as opaque. return True if (target == ('qute', 'testdata') and is_opaque and qtutils.version_check('5.12')): # Allow requests to qute://testdata, as this is needed in Qt 5.12 # for all tests to work properly. No qute://testdata handler is # installed outside of tests. return True if initiator.isValid() and initiator.scheme() != 'qute': log.misc.warning("Blocking malicious request from {} to {}".format( initiator.toDisplayString(), request_url.toDisplayString())) job.fail(QWebEngineUrlRequestJob.RequestDenied) return False return True
def _set_cache_size(self): """Set the cache size based on the config.""" size = config.val.content.cache.size if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 if not qtutils.version_check('5.9', compiled=False): size = 0 # pragma: no cover self.setMaximumCacheSize(size)
def expose(self, web_tab): """Expose the web view if needed. On QtWebKit, or Qt < 5.11 on QtWebEngine, we need to show the tab for selections to work properly. """ if (web_tab.backend == usertypes.Backend.QtWebKit or not qtutils.version_check('5.11', compiled=False)): web_tab.container.expose()
def __init__(self, win_id, parent=None): super().__init__(parent) if sys.platform == 'darwin' and qtutils.version_check('5.4'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # See https://github.com/The-Compiler/qutebrowser/issues/462 self.setStyle(QStyleFactory.create('Fusion')) self.win_id = win_id self.load_status = LoadStatus.none self._check_insertmode = False self.inspector = None self.scroll_pos = (-1, -1) self.statusbar_message = '' self._old_scroll_pos = (-1, -1) self._zoom = None self._has_ssl_errors = False self._ignore_wheel_event = False self.keep_icon = False self.search_text = None self.search_flags = 0 self.selection_enabled = False self.init_neighborlist() self._set_bg_color() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) # For some reason, this signal doesn't get disconnected automatically # when the WebView is destroyed on older PyQt versions. # See https://github.com/The-Compiler/qutebrowser/issues/390 self.destroyed.connect(functools.partial( cfg.changed.disconnect, self.init_neighborlist)) self._cur_url = None self.cur_url = QUrl() self.progress = 0 self.registry = objreg.ObjectRegistry() self.tab_id = next(tab_id_gen) tab_registry = objreg.get('tab-registry', scope='window', window=win_id) tab_registry[self.tab_id] = self objreg.register('webview', self, registry=self.registry) page = self._init_page() hintmanager = hints.HintManager(win_id, self.tab_id, self) hintmanager.mouse_event.connect(self.on_mouse_event) hintmanager.start_hinting.connect(page.on_start_hinting) hintmanager.stop_hinting.connect(page.on_stop_hinting) objreg.register('hintmanager', hintmanager, registry=self.registry) mode_manager = objreg.get('mode-manager', scope='window', window=win_id) mode_manager.entered.connect(self.on_mode_entered) mode_manager.left.connect(self.on_mode_left) self.viewing_source = False self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100) self._default_zoom_changed = False if config.get('input', 'rocker-gestures'): self.setContextMenuPolicy(Qt.PreventContextMenu) self.urlChanged.connect(self.on_url_changed) self.loadProgress.connect(lambda p: setattr(self, 'progress', p)) objreg.get('config').changed.connect(self.on_config_changed)
'fonts.web.family.cursive': FontFamilySetter(QWebEngineSettings.CursiveFont), 'fonts.web.family.fantasy': FontFamilySetter(QWebEngineSettings.FantasyFont), 'fonts.web.size.minimum': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.MinimumFontSize]), 'fonts.web.size.minimum_logical': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.MinimumLogicalFontSize]), 'fonts.web.size.default': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.DefaultFontSize]), 'fonts.web.size.default_fixed': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.DefaultFixedFontSize]), 'scrolling.smooth': Attribute(QWebEngineSettings.ScrollAnimatorEnabled), } try: MAPPINGS['content.print_element_backgrounds'] = Attribute( QWebEngineSettings.PrintElementBackgrounds) except AttributeError: # Added in Qt 5.8 pass if qtutils.version_check('5.9'): # https://bugreports.qt.io/browse/QTBUG-58650 MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()
def pytest_collection_modifyitems(config, items): """Handle custom markers. pytest hook called after collection has been performed. Adds a marker named "gui" which can be used to filter gui tests from the command line. For example: pytest -m "not gui" # run all tests except gui tests pytest -m "gui" # run only gui tests It also handles the platform specific markers by translating them to skipif markers. Args: items: list of _pytest.main.Node items, where each item represents a python test that will be executed. Reference: http://pytest.org/latest/plugins.html """ remaining_items = [] deselected_items = [] for item in items: deselected = False if 'qapp' in getattr(item, 'fixturenames', ()): item.add_marker('gui') if hasattr(item, 'module'): module_path = os.path.relpath( item.module.__file__, os.path.commonprefix([__file__, item.module.__file__])) module_root_dir = module_path.split(os.sep)[0] assert module_root_dir in ['end2end', 'unit', 'helpers', 'test_conftest.py'] if module_root_dir == 'end2end': item.add_marker(pytest.mark.end2end) elif os.environ.get('QUTE_BDD_WEBENGINE', ''): deselected = True _apply_platform_markers(item) if item.get_marker('xfail_norun'): item.add_marker(pytest.mark.xfail(run=False)) if item.get_marker('js_prompt'): if config.webengine: js_prompt_pyqt_version = 0x050700 else: js_prompt_pyqt_version = 0x050300 item.add_marker(pytest.mark.skipif( PYQT_VERSION <= js_prompt_pyqt_version, reason='JS prompts are not supported with this PyQt version')) if item.get_marker('issue2183'): item.add_marker(pytest.mark.xfail( config.webengine and qtutils.version_check('5.7.1'), reason='https://github.com/The-Compiler/qutebrowser/issues/' '2183')) if deselected: deselected_items.append(item) else: remaining_items.append(item) config.hook.pytest_deselected(items=deselected_items) items[:] = remaining_items
class RequestInterceptor(QWebEngineUrlRequestInterceptor): """Handle ad blocking and custom headers.""" def __init__(self, parent=None): super().__init__(parent) # This dict should be from QWebEngine Resource Types to qutebrowser # extension ResourceTypes. If a ResourceType is added to Qt, this table # should be updated too. self._resource_types = { QWebEngineUrlRequestInfo.ResourceTypeMainFrame: interceptors.ResourceType.main_frame, QWebEngineUrlRequestInfo.ResourceTypeSubFrame: interceptors.ResourceType.sub_frame, QWebEngineUrlRequestInfo.ResourceTypeStylesheet: interceptors.ResourceType.stylesheet, QWebEngineUrlRequestInfo.ResourceTypeScript: interceptors.ResourceType.script, QWebEngineUrlRequestInfo.ResourceTypeImage: interceptors.ResourceType.image, QWebEngineUrlRequestInfo.ResourceTypeFontResource: interceptors.ResourceType.font_resource, QWebEngineUrlRequestInfo.ResourceTypeSubResource: interceptors.ResourceType.sub_resource, QWebEngineUrlRequestInfo.ResourceTypeObject: interceptors.ResourceType.object, QWebEngineUrlRequestInfo.ResourceTypeMedia: interceptors.ResourceType.media, QWebEngineUrlRequestInfo.ResourceTypeWorker: interceptors.ResourceType.worker, QWebEngineUrlRequestInfo.ResourceTypeSharedWorker: interceptors.ResourceType.shared_worker, QWebEngineUrlRequestInfo.ResourceTypePrefetch: interceptors.ResourceType.prefetch, QWebEngineUrlRequestInfo.ResourceTypeFavicon: interceptors.ResourceType.favicon, QWebEngineUrlRequestInfo.ResourceTypeXhr: interceptors.ResourceType.xhr, QWebEngineUrlRequestInfo.ResourceTypePing: interceptors.ResourceType.ping, QWebEngineUrlRequestInfo.ResourceTypeServiceWorker: interceptors.ResourceType.service_worker, QWebEngineUrlRequestInfo.ResourceTypeCspReport: interceptors.ResourceType.csp_report, QWebEngineUrlRequestInfo.ResourceTypePluginResource: interceptors.ResourceType.plugin_resource, QWebEngineUrlRequestInfo.ResourceTypeUnknown: interceptors.ResourceType.unknown, } try: preload_main_frame = (QWebEngineUrlRequestInfo. ResourceTypeNavigationPreloadMainFrame) preload_sub_frame = (QWebEngineUrlRequestInfo. ResourceTypeNavigationPreloadSubFrame) except AttributeError: # Added in Qt 5.14 pass else: self._resource_types[preload_main_frame] = ( interceptors.ResourceType.preload_main_frame) self._resource_types[preload_sub_frame] = ( interceptors.ResourceType.preload_sub_frame) def install(self, profile): """Install the interceptor on the given QWebEngineProfile.""" try: # Qt >= 5.13, GUI thread profile.setUrlRequestInterceptor(self) except AttributeError: # Qt 5.12, IO thread profile.setRequestInterceptor(self) # Gets called in the IO thread -> showing crash window will fail @utils.prevent_exceptions(None, not qtutils.version_check('5.13')) def interceptRequest(self, info): """Handle the given request. Reimplementing this virtual function and setting the interceptor on a profile makes it possible to intercept URL requests. On Qt < 5.13, this function is executed on the IO thread, and therefore running long tasks here will block networking. info contains the information about the URL request and will track internally whether its members have been altered. Args: info: QWebEngineUrlRequestInfo &info """ if 'log-requests' in objects.debug_flags: resource_type_str = debug.qenum_key(QWebEngineUrlRequestInfo, info.resourceType()) navigation_type_str = debug.qenum_key(QWebEngineUrlRequestInfo, info.navigationType()) log.network.debug("{} {}, first-party {}, resource {}, " "navigation {}".format( bytes(info.requestMethod()).decode('ascii'), info.requestUrl().toDisplayString(), info.firstPartyUrl().toDisplayString(), resource_type_str, navigation_type_str)) url = info.requestUrl() first_party = info.firstPartyUrl() if not url.isValid(): log.network.debug("Ignoring invalid intercepted URL: {}".format( url.errorString())) return # Per QWebEngineUrlRequestInfo::ResourceType documentation, if we fail # our lookup, we should fall back to ResourceTypeUnknown try: resource_type = self._resource_types[info.resourceType()] except KeyError: log.network.warning( "Resource type {} not found in RequestInterceptor dict." .format(debug.qenum_key(QWebEngineUrlRequestInfo, info.resourceType()))) resource_type = interceptors.ResourceType.unknown if ((url.scheme(), url.host(), url.path()) == ('qute', 'settings', '/set')): if (first_party != QUrl('qute://settings/') or info.resourceType() != QWebEngineUrlRequestInfo.ResourceTypeXhr): log.network.warning("Blocking malicious request from {} to {}" .format(first_party.toDisplayString(), url.toDisplayString())) info.block(True) return # FIXME:qtwebengine only block ads for NavigationTypeOther? request = WebEngineRequest( first_party_url=first_party, request_url=url, resource_type=resource_type, webengine_info=info) interceptors.run(request) if request.is_blocked: info.block(True) for header, value in shared.custom_headers(url=url): info.setHttpHeader(header, value) user_agent = websettings.user_agent(url) info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
def _apply_platform_markers(config, item): """Apply a skip marker to a given item.""" markers = [ ('posix', pytest.mark.skipif, not utils.is_posix, "Requires a POSIX os"), ('windows', pytest.mark.skipif, not utils.is_windows, "Requires Windows"), ('linux', pytest.mark.skipif, not utils.is_linux, "Requires Linux"), ('mac', pytest.mark.skipif, not utils.is_mac, "Requires macOS"), ('not_mac', pytest.mark.skipif, utils.is_mac, "Skipped on macOS"), ('not_frozen', pytest.mark.skipif, getattr(sys, 'frozen', False), "Can't be run when frozen"), ('frozen', pytest.mark.skipif, not getattr(sys, 'frozen', False), "Can only run when frozen"), ('ci', pytest.mark.skipif, not testutils.ON_CI, "Only runs on CI."), ('no_ci', pytest.mark.skipif, testutils.ON_CI, "Skipped on CI."), ('unicode_locale', pytest.mark.skipif, sys.getfilesystemencoding() == 'ascii', "Skipped because of ASCII locale"), ('qtbug60673', pytest.mark.xfail, qtutils.version_check('5.8') and not qtutils.version_check('5.10') and config.webengine, "Broken on webengine due to " "https://bugreports.qt.io/browse/QTBUG-60673"), ('qtwebkit6021_xfail', pytest.mark.xfail, version.qWebKitVersion and # type: ignore[unreachable] version.qWebKitVersion() == '602.1', "Broken on WebKit 602.1") ] for searched_marker, new_marker_kind, condition, default_reason in markers: marker = item.get_closest_marker(searched_marker) if not marker or not condition: continue if 'reason' in marker.kwargs: reason = '{}: {}'.format(default_reason, marker.kwargs['reason']) del marker.kwargs['reason'] else: reason = default_reason + '.' new_marker = new_marker_kind(condition, *marker.args, reason=reason, **marker.kwargs) item.add_marker(new_marker)
# but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. import pytest from PyQt5.QtCore import QUrl, QDateTime from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.browser.webkit import cache from qutebrowser.utils import qtutils pytestmark = pytest.mark.skipif( qtutils.version_check('5.7.1', compiled=False) and not qtutils.version_check('5.9', compiled=False), reason="QNetworkDiskCache is broken on Qt 5.7.1 and 5.8") @pytest.fixture def disk_cache(tmpdir, config_stub): return cache.DiskCache(str(tmpdir)) def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): metadata = QNetworkCacheMetaData() metadata.setUrl(QUrl(url)) assert metadata.isValid() device = cache.prepare(metadata) assert device is not None
Backend: {backend} PYTHON IMPLEMENTATION: PYTHON VERSION Qt: {qt} PyQt: PYQT VERSION MODULE VERSION 1 MODULE VERSION 2 pdf.js: PDFJS VERSION QtNetwork SSL: {ssl} {style} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen} Imported from {import_path} Qt library executable path: QT PATH, data path: QT PATH {osinfo} Paths: PATH DESC: PATH NAME """.lstrip('\n')) expected = template.rstrip('\n').format(**substitutions) assert version.version() == expected @pytest.mark.skipif(not qtutils.version_check('5.4'), reason="Needs Qt >= 5.4.") def test_opengl_vendor(): """Simply call version.opengl_vendor() and see if it doesn't crash.""" pytest.importorskip("PyQt5.QtOpenGL") return version.opengl_vendor()
def _qtwebengine_features( feature_flags: Sequence[str], ) -> Tuple[Sequence[str], Sequence[str]]: """Get a tuple of --enable-features/--disable-features flags for QtWebEngine. Args: feature_flags: Existing flags passed via the commandline. """ enabled_features = [] disabled_features = [] for flag in feature_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(',') else: raise utils.Unreachable(flag) if qtutils.version_check('5.15', compiled=False) 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. Checking for 5.15 here to pick up Archlinux' patched package # as well. # # 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 (qtutils.version_check('5.14', compiled=False) 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 (Qt 5.15.x or 6.x): # https://chromium-review.googlesource.com/c/chromium/src/+/2545444 enabled_features.append('ReducedReferrerGranularity') if qtutils.version_check('5.15.2', compiled=False, exact=True): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89740 # FIXME Not needed anymore with QtWebEngne 5.15.3 (or Qt 6), but we'll probably # have no way to detect that... disabled_features.append('InstalledApp') return (enabled_features, disabled_features)
class TestSendOrListen: @attr.s class Args: no_err_windows = attr.ib() basedir = attr.ib() command = attr.ib() target = attr.ib() @pytest.fixture def args(self): return self.Args(no_err_windows=True, basedir='/basedir/for/testing', command=['test'], target=None) @pytest.fixture def qlocalserver_mock(self, mocker): m = mocker.patch('qutebrowser.misc.ipc.QLocalServer', autospec=True) m().errorString.return_value = "Error string" m().newConnection = stubs.FakeSignal() return m @pytest.fixture def qlocalsocket_mock(self, mocker): m = mocker.patch('qutebrowser.misc.ipc.QLocalSocket', autospec=True) m().errorString.return_value = "Error string" for name in [ 'UnknownSocketError', 'UnconnectedState', 'ConnectionRefusedError', 'ServerNotFoundError', 'PeerClosedError' ]: setattr(m, name, getattr(QLocalSocket, name)) return m @pytest.mark.linux(reason="Flaky on Windows and macOS") def test_normal_connection(self, caplog, qtbot, args): ret_server = ipc.send_or_listen(args) assert isinstance(ret_server, ipc.IPCServer) assert "Starting IPC server..." in caplog.messages assert ret_server is ipc.server with qtbot.waitSignal(ret_server.got_args): ret_client = ipc.send_or_listen(args) assert ret_client is None @pytest.mark.posix(reason="Unneeded on Windows") @pytest.mark.xfail(qtutils.version_check('5.12', compiled=False) and utils.is_mac, reason="Broken, see #4471") def test_correct_socket_name(self, args): server = ipc.send_or_listen(args) expected_dir = ipc._get_socketname(args.basedir) assert '/' in expected_dir assert server._socketname == expected_dir def test_address_in_use_ok(self, qlocalserver_mock, qlocalsocket_mock, stubs, caplog, args): """Test the following scenario. - First call to send_to_running_instance: -> could not connect (server not found) - Trying to set up a server and listen -> AddressInUseError - Second call to send_to_running_instance: -> success """ qlocalserver_mock().listen.return_value = False err = QAbstractSocket.AddressInUseError qlocalserver_mock().serverError.return_value = err qlocalsocket_mock().waitForConnected.side_effect = [False, True] qlocalsocket_mock().error.side_effect = [ QLocalSocket.ServerNotFoundError, QLocalSocket.UnknownSocketError, QLocalSocket.UnknownSocketError, # error() gets called twice ] ret = ipc.send_or_listen(args) assert ret is None assert "Got AddressInUseError, trying again." in caplog.messages @pytest.mark.parametrize('has_error, exc_name, exc_msg', [ (True, 'SocketError', 'Error while writing to running instance: Error string (error 0)'), (False, 'AddressInUseError', 'Error while listening to IPC server: Error string (error 8)'), ]) def test_address_in_use_error(self, qlocalserver_mock, qlocalsocket_mock, stubs, caplog, args, has_error, exc_name, exc_msg): """Test the following scenario. - First call to send_to_running_instance: -> could not connect (server not found) - Trying to set up a server and listen -> AddressInUseError - Second call to send_to_running_instance: -> not sent / error """ qlocalserver_mock().listen.return_value = False err = QAbstractSocket.AddressInUseError qlocalserver_mock().serverError.return_value = err # If the second connection succeeds, we will have an error later. # If it fails, that's the "not sent" case above. qlocalsocket_mock().waitForConnected.side_effect = [False, has_error] qlocalsocket_mock().error.side_effect = [ QLocalSocket.ServerNotFoundError, QLocalSocket.ServerNotFoundError, QLocalSocket.ConnectionRefusedError, QLocalSocket.ConnectionRefusedError, # error() gets called twice ] with caplog.at_level(logging.ERROR): with pytest.raises(ipc.Error): ipc.send_or_listen(args) error_msgs = [ 'Handling fatal misc.ipc.{} with --no-err-windows!'.format( exc_name), '', 'title: Error while connecting to running instance!', 'pre_text: ', 'post_text: Maybe another instance is running but frozen?', 'exception text: {}'.format(exc_msg), ] assert caplog.messages == ['\n'.join(error_msgs)] @pytest.mark.posix(reason="Flaky on Windows") def test_error_while_listening(self, qlocalserver_mock, caplog, args): """Test an error with the first listen call.""" qlocalserver_mock().listen.return_value = False err = QAbstractSocket.SocketResourceError qlocalserver_mock().serverError.return_value = err with caplog.at_level(logging.ERROR): with pytest.raises(ipc.Error): ipc.send_or_listen(args) error_msgs = [ 'Handling fatal misc.ipc.ListenError with --no-err-windows!', '', 'title: Error while connecting to running instance!', 'pre_text: ', 'post_text: Maybe another instance is running but frozen?', ('exception text: Error while listening to IPC server: Error ' 'string (error 4)'), ] assert caplog.messages[-1] == '\n'.join(error_msgs)
args=[QWebEngineSettings.MinimumFontSize]), 'fonts.web.size.minimum_logical': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.MinimumLogicalFontSize]), 'fonts.web.size.default': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.DefaultFontSize]), 'fonts.web.size.default_fixed': Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.DefaultFixedFontSize]), 'scrolling.smooth': Attribute(QWebEngineSettings.ScrollAnimatorEnabled), } try: MAPPINGS['content.print_element_backgrounds'] = Attribute( QWebEngineSettings.PrintElementBackgrounds) except AttributeError: # Added in Qt 5.8 pass if qtutils.version_check('5.8'): MAPPINGS['spellcheck.languages'] = DictionaryLanguageSetter() if qtutils.version_check('5.9', compiled=False): # https://bugreports.qt.io/browse/QTBUG-58650 MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()
"""Partial comparison of dicts/lists.""" import re import pprint import os.path import contextlib import pytest from qutebrowser.utils import qtutils, log qt58 = pytest.mark.skipif( qtutils.version_check('5.9'), reason="Needs Qt 5.8 or earlier") qt59 = pytest.mark.skipif( not qtutils.version_check('5.9'), reason="Needs Qt 5.9 or newer") qt510 = pytest.mark.skipif( not qtutils.version_check('5.10'), reason="Needs Qt 5.10 or newer") skip_qt511 = pytest.mark.skipif( qtutils.version_check('5.11'), reason="Needs Qt 5.10 or earlier") class PartialCompareOutcome: """Storage for a partial_compare error. Evaluates to False if an error was found. Attributes:
def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): # WORKAROUND equivalent to # https://codereview.qt-project.org/#/c/217932/ # Needed for Qt < 5.9.5 and < 5.10.1 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 qtutils.version_check('5.12.3', compiled=False): 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' if 'chromium' in namespace.debug_flags: yield '--enable-logging' yield '--v=1' settings = { '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, 'never': '--no-referrers', 'same-domain': '--reduced-referrer-granularity', } } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]] if not qtutils.version_check('5.11'): # On Qt 5.11, we can control this via QWebEngineSettings settings['content.autoplay'] = { True: None, False: '--autoplay-policy=user-gesture-required', } for setting, args in sorted(settings.items()): arg = args[config.instance.get(setting)] if arg is not None: yield arg
def _init_modules(args, crash_handler): """Initialize all 'modules' which need to be initialized. Args: args: The argparse namespace. crash_handler: The CrashHandler instance. """ # pylint: disable=too-many-statements log.init.debug("Initializing prompts...") prompt.init() log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(qApp) objreg.register('save-manager', save_manager) save_manager.add_saveable('version', _save_version) log.init.debug("Initializing network...") networkmanager.init() if qtutils.version_check('5.8'): # Otherwise we can only initialize it for QtWebKit because of crashes log.init.debug("Initializing proxy...") proxy.init() log.init.debug("Initializing readline-bridge...") readline_bridge = readline.ReadlineBridge() objreg.register('readline-bridge', readline_bridge) log.init.debug("Initializing config...") config.init(qApp) save_manager.init_autosave() log.init.debug("Initializing web history...") history.init(qApp) log.init.debug("Initializing crashlog...") if not args.no_err_windows: crash_handler.handle_segfault() log.init.debug("Initializing sessions...") sessions.init(qApp) log.init.debug("Initializing websettings...") websettings.init(args) log.init.debug("Initializing adblock...") host_blocker = adblock.HostBlocker() host_blocker.read_hosts() objreg.register('host-blocker', host_blocker) log.init.debug("Initializing quickmarks...") quickmark_manager = urlmarks.QuickmarkManager(qApp) objreg.register('quickmark-manager', quickmark_manager) log.init.debug("Initializing bookmarks...") bookmark_manager = urlmarks.BookmarkManager(qApp) objreg.register('bookmark-manager', bookmark_manager) log.init.debug("Initializing cookies...") cookie_jar = cookies.CookieJar(qApp) ram_cookie_jar = cookies.RAMCookieJar(qApp) objreg.register('cookie-jar', cookie_jar) objreg.register('ram-cookie-jar', ram_cookie_jar) log.init.debug("Initializing cache...") diskcache = cache.DiskCache(standarddir.cache(), parent=qApp) objreg.register('cache', diskcache) log.init.debug("Initializing completions...") completionmodels.init() log.init.debug("Misc initialization...") if config.get('ui', 'hide-wayland-decoration'): os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' else: os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) macros.init() # Init backend-specific stuff browsertab.init()
"""Various utilities used inside tests.""" import io import re import gzip import pprint import os.path import contextlib import pytest from qutebrowser.utils import qtutils, log ON_CI = 'CI' in os.environ qt58 = pytest.mark.skipif(qtutils.version_check('5.9'), reason="Needs Qt 5.8 or earlier") qt59 = pytest.mark.skipif(not qtutils.version_check('5.9'), reason="Needs Qt 5.9 or newer") qt510 = pytest.mark.skipif(not qtutils.version_check('5.10'), reason="Needs Qt 5.10 or newer") qt514 = pytest.mark.skipif(not qtutils.version_check('5.14'), reason="Needs Qt 5.14 or newer") skip_qt511 = pytest.mark.skipif(qtutils.version_check('5.11'), reason="Needs Qt 5.10 or earlier") class PartialCompareOutcome: """Storage for a partial_compare error. Evaluates to False if an error was found.
import importlib.util import importlib.machinery import pytest from PyQt5.QtCore import qVersion try: from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR except ImportError: PYQT_WEBENGINE_VERSION_STR = None from qutebrowser.utils import qtutils, log ON_CI = 'CI' in os.environ qt514 = pytest.mark.skipif(not qtutils.version_check('5.14'), reason="Needs Qt 5.14 or newer") class PartialCompareOutcome: """Storage for a partial_compare error. Evaluates to False if an error was found. Attributes: error: A string describing an error or None. """ def __init__(self, error=None): self.error = error def __bool__(self):
(False, False, False, ''), (False, True, False, 'onlyscheme:'), (False, True, False, 'http:foo:0'), # Not URLs (False, True, False, 'foo bar'), # no DNS because of space (False, True, False, 'localhost test'), # no DNS because of space (False, True, False, 'another . test'), # no DNS because of space (False, True, True, 'foo'), (False, True, False, 'this is: not a URL'), # no DNS because of space (False, True, False, '23.42'), # no DNS because bogus-IP (False, True, False, '1337'), # no DNS because bogus-IP (False, True, True, 'deadbeef'), (False, True, True, 'hello.'), (False, True, False, 'site:cookies.com oatmeal raisin'), # no DNS because bogus-IP pytest.mark.xfail(qtutils.version_check('5.6.1'), reason='Qt behavior changed') (False, True, False, '31c3'), (False, True, False, 'foo::bar'), # no DNS because of no host # Valid search term with autosearch (False, False, False, 'test foo'), # autosearch = False (False, True, False, 'This is a URL without autosearch'), ]) @pytest.mark.parametrize('auto_search', ['dns', 'naive', False]) def test_is_url(urlutils_config_stub, fake_dns, is_url, is_url_no_autosearch, uses_dns, url, auto_search): """Test is_url(). Args: is_url: Whether the given string is a URL with auto-search dns/naive.
def _qtwebengine_settings_args() -> 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, } } if (qtutils.version_check('5.14', compiled=False) and not qtutils.version_check('5.15.2', compiled=False)): # 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.prefers_color_scheme_dark'] = { True: '--force-dark-mode', False: None, } referrer_setting = settings['content.headers.referer'] if qtutils.version_check('5.14', compiled=False): # Starting with Qt 5.14, this is handled via --enable-features referrer_setting['same-domain'] = None else: referrer_setting['same-domain'] = '--reduced-referrer-granularity' can_override_referer = ( qtutils.version_check('5.12.4', compiled=False) and not qtutils.version_check('5.13.0', compiled=False, exact=True)) # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60203 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
def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]: """Get necessary blink settings to configure dark mode for QtWebEngine.""" if not config.val.colors.webpage.darkmode.enabled: return # Mapping from a colors.webpage.darkmode.algorithm setting value to # Chromium's DarkModeInversionAlgorithm enum values. algorithms = { # 0: kOff (not exposed) # 1: kSimpleInvertForTesting (not exposed) 'brightness-rgb': 2, # kInvertBrightness 'lightness-hsl': 3, # kInvertLightness 'lightness-cielab': 4, # kInvertLightnessLAB } # Mapping from a colors.webpage.darkmode.policy.images setting value to # Chromium's DarkModeImagePolicy enum values. image_policies = { 'always': 0, # kFilterAll 'never': 1, # kFilterNone 'smart': 2, # kFilterSmart } # Mapping from a colors.webpage.darkmode.policy.page setting value to # Chromium's DarkModePagePolicy enum values. page_policies = { 'always': 0, # kFilterAll 'smart': 1, # kFilterByBackground } bools = { True: 'true', False: 'false', } _setting_description_type = typing.Tuple[ str, # qutebrowser option name str, # darkmode setting name # Mapping from the config value to a string (or something convertable # to a string) which gets passed to Chromium. typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]], ] if qtutils.version_check('5.15', compiled=False): settings = [ ('enabled', 'Enabled', bools), ('algorithm', 'InversionAlgorithm', algorithms), ] # type: typing.List[_setting_description_type] mandatory_setting = 'enabled' else: settings = [ ('algorithm', '', algorithms), ] mandatory_setting = 'algorithm' settings += [ ('contrast', 'Contrast', None), ('policy.images', 'ImagePolicy', image_policies), ('policy.page', 'PagePolicy', page_policies), ('threshold.text', 'TextBrightnessThreshold', None), ('threshold.background', 'BackgroundBrightnessThreshold', None), ('grayscale.all', 'Grayscale', bools), ('grayscale.images', 'ImageGrayscale', None), ] for setting, key, mapping in settings: # To avoid blowing up the commandline length, we only pass modified # settings to Chromium, as our defaults line up with Chromium's. # However, we always pass enabled/algorithm to make sure dark mode gets # actually turned on. value = config.instance.get('colors.webpage.darkmode.' + setting, fallback=setting == mandatory_setting) if isinstance(value, usertypes.Unset): continue if mapping is not None: value = mapping[value] # FIXME: This is "forceDarkMode" starting with Chromium 83 prefix = 'darkMode' yield prefix + key, str(value)
def install(self, profile): """Install the handler for qute:// URLs on the given profile.""" profile.installUrlSchemeHandler(b'qute', self) if qtutils.version_check('5.11', compiled=False): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378 profile.installUrlSchemeHandler(b'chrome-error', self)
def normalize_whole(s, webengine): if qtutils.version_check('5.12', compiled=False) and webengine: s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID') return s
def _needs_recreate(self) -> bool: """Recreate the inspector when detaching to a window. WORKAROUND for what's likely an unknown Qt bug. """ return qtutils.version_check('5.12')
# but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. import pytest from PyQt5.QtCore import QUrl, QDateTime from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.browser.webkit import cache from qutebrowser.utils import qtutils pytestmark = pytest.mark.skipif( qtutils.version_check('5.7.1') and not qtutils.version_check('5.9'), reason="QNetworkDiskCache is broken on Qt 5.7.1 and 5.8") @pytest.fixture def disk_cache(tmpdir, config_stub): return cache.DiskCache(str(tmpdir)) def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): metadata = QNetworkCacheMetaData() metadata.setUrl(QUrl(url)) assert metadata.isValid() device = cache.prepare(metadata) assert device is not None device.write(content)
def test_version_check_compiled_and_exact(): with pytest.raises(ValueError): qtutils.version_check('1.2.3', exact=True, compiled=True)
def _open_special_pages(args): """Open special notification pages which are only shown once. Args: args: The argparse namespace. """ if args.basedir is not None: # With --basedir given, don't open anything. return general_sect = configfiles.state['general'] tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') pages = [ # state, condition, URL ('quickstart-done', True, 'https://www.qutebrowser.org/quickstart.html' ), ('config-migration-shown', os.path.exists(os.path.join(standarddir.config(), 'qutebrowser.conf')), 'qute://help/configuring.html'), ('webkit-warning-shown', objects.backend == usertypes.Backend.QtWebKit, 'qute://warning/webkit'), ('session-warning-shown', qtutils.version_check('5.15', compiled=False), 'qute://warning/sessions'), ] if 'quickstart-done' not in general_sect: # New users aren't going to be affected by the Qt 5.15 session change much, as # they aren't used to qutebrowser saving the full back/forward history in # sessions. general_sect['session-warning-shown'] = '1' for state, condition, url in pages: if general_sect.get(state) != '1' and condition: tabbed_browser.tabopen(QUrl(url), background=False) general_sect[state] = '1' # Show changelog on new releases change = configfiles.state.qutebrowser_version_changed if change == configfiles.VersionChange.equal: return setting = config.val.changelog_after_upgrade if not change.matches_filter(setting): log.init.debug( f"Showing changelog is disabled (setting {setting}, change {change})" ) return try: changelog = resources.read_file('html/doc/changelog.html') except OSError as e: log.init.warning(f"Not showing changelog due to {e}") return qbversion = qutebrowser.__version__ if f'id="v{qbversion}"' not in changelog: log.init.warning("Not showing changelog (anchor not found)") return message.info( f"Showing changelog after upgrade to qutebrowser v{qbversion}.") changelog_url = f'qute://help/changelog.html#v{qbversion}' tabbed_browser.tabopen(QUrl(changelog_url), background=False)
class TestStandardDir: @pytest.mark.parametrize('func, init_func, varname', [ (standarddir.data, standarddir._init_data, 'XDG_DATA_HOME'), (standarddir.config, standarddir._init_config, 'XDG_CONFIG_HOME'), (lambda: standarddir.config(auto=True), standarddir._init_config, 'XDG_CONFIG_HOME'), (standarddir.cache, standarddir._init_cache, 'XDG_CACHE_HOME'), (standarddir.runtime, standarddir._init_runtime, 'XDG_RUNTIME_DIR'), ]) @pytest.mark.linux def test_linux_explicit(self, monkeypatch, tmpdir, func, init_func, varname): """Test dirs with XDG environment variables explicitly set. Args: func: The function to test. init_func: The initialization function to call. varname: The environment variable which should be set. """ monkeypatch.setenv(varname, str(tmpdir)) if varname == 'XDG_RUNTIME_DIR': tmpdir.chmod(0o0700) init_func(args=None) assert func() == str(tmpdir / APPNAME) @pytest.mark.parametrize('func, subdirs', [ (standarddir.data, ['.local', 'share', APPNAME]), (standarddir.config, ['.config', APPNAME]), (lambda: standarddir.config(auto=True), ['.config', APPNAME]), (standarddir.cache, ['.cache', APPNAME]), (standarddir.download, ['Downloads']), ]) @pytest.mark.linux def test_linux_normal(self, monkeypatch, tmpdir, func, subdirs): """Test dirs with XDG_*_HOME not set.""" monkeypatch.setenv('HOME', str(tmpdir)) for var in ['DATA', 'CONFIG', 'CACHE']: monkeypatch.delenv('XDG_{}_HOME'.format(var), raising=False) standarddir._init_dirs() assert func() == str(tmpdir.join(*subdirs)) @pytest.mark.linux @pytest.mark.qt_log_ignore(r'^QStandardPaths: ') @pytest.mark.skipif( qtutils.version_check('5.14', compiled=False), reason="Qt 5.14 automatically creates missing runtime dirs") def test_linux_invalid_runtimedir(self, monkeypatch, tmpdir): """With invalid XDG_RUNTIME_DIR, fall back to TempLocation.""" tmpdir_env = tmpdir / 'temp' tmpdir_env.ensure(dir=True) monkeypatch.setenv('XDG_RUNTIME_DIR', str(tmpdir / 'does-not-exist')) monkeypatch.setenv('TMPDIR', str(tmpdir_env)) standarddir._init_runtime(args=None) assert standarddir.runtime() == str(tmpdir_env / APPNAME) @pytest.mark.fake_os('windows') def test_runtimedir_empty_tempdir(self, monkeypatch, tmpdir): """With an empty tempdir on non-Linux, we should raise.""" monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', lambda typ: '') with pytest.raises(standarddir.EmptyValueError): standarddir._init_runtime(args=None) @pytest.mark.parametrize('func, elems, expected', [ (standarddir.data, 2, [APPNAME, 'data']), (standarddir.config, 2, [APPNAME, 'config']), (lambda: standarddir.config(auto=True), 2, [APPNAME, 'config']), (standarddir.cache, 2, [APPNAME, 'cache']), (standarddir.download, 1, ['Downloads']), ]) @pytest.mark.windows def test_windows(self, func, elems, expected): standarddir._init_dirs() assert func().split(os.sep)[-elems:] == expected @pytest.mark.parametrize('func, elems, expected', [ (standarddir.data, 2, ['Application Support', APPNAME]), (lambda: standarddir.config(auto=True), 1, [APPNAME]), (standarddir.config, 0, os.path.expanduser('~').split(os.sep) + ['.qute_test']), (standarddir.cache, 2, ['Caches', APPNAME]), (standarddir.download, 1, ['Downloads']), ]) @pytest.mark.mac def test_mac(self, func, elems, expected): standarddir._init_dirs() assert func().split(os.sep)[-elems:] == expected
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. """Partial comparison of dicts/lists.""" import re import pprint import os.path import pytest from qutebrowser.utils import qtutils qt58 = pytest.mark.skipif(qtutils.version_check('5.9'), reason="Needs Qt 5.8 or earlier") qt59 = pytest.mark.skipif(not qtutils.version_check('5.9'), reason="Needs Qt 5.9 or newer") class PartialCompareOutcome: """Storage for a partial_compare error. Evaluates to False if an error was found. Attributes: error: A string describing an error or None. """ def __init__(self, error=None): self.error = error