diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 9341676f5..c7da3e82e 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -160,6 +160,7 @@ |<>|Enable or disable hyperlink auditing (). |<>|Allow websites to request geolocations. |<>|Allow websites to show notifications. +|<>|Allow websites to record audio/video. |<>|Whether JavaScript programs can open new windows without user interaction. |<>|Whether JavaScript programs can close windows. |<>|Whether JavaScript programs can read or write to the clipboard. @@ -797,8 +798,6 @@ Valid values: Default: +pass:[ask]+ -This setting is only available with the QtWebKit backend. - [[network-dns-prefetch]] === dns-prefetch Whether to try to pre-fetch DNS entries to speed up browsing. @@ -1452,6 +1451,20 @@ Valid values: Default: +pass:[ask]+ +[[content-media-capture]] +=== media-capture +Allow websites to record audio/video. + +Valid values: + + * +true+ + * +false+ + * +ask+ + +Default: +pass:[ask]+ + +This setting is only available with the QtWebEngine backend. + [[content-javascript-can-open-windows-automatically]] === javascript-can-open-windows-automatically Whether JavaScript programs can open new windows without user interaction. diff --git a/pytest.ini b/pytest.ini index 0c2d00f21..109e30fde 100644 --- a/pytest.ini +++ b/pytest.ini @@ -14,13 +14,13 @@ markers = end2end: End to end tests which run qutebrowser as subprocess xfail_norun: xfail the test with out running it ci: Tests which should only run on CI. - flaky_once: Try to rerun this test once if it fails qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine qtwebkit_skip: Tests not applicable with QtWebKit qtwebengine_createWindow: Tests using createWindow with QtWebEngine (QTBUG-54419) qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_osx_xfail: Tests which fail on OS X with QtWebEngine + js_prompt: Tests needing to display a javascript prompt this: Used to mark tests during development qt_log_level_fail = WARNING qt_log_ignore = diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index b326a1356..6820cb3dc 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1991,7 +1991,7 @@ class CommandDispatcher: tab.send_event(release_event) @cmdutils.register(instance='command-dispatcher', scope='window', - debug=True) + debug=True, backend=usertypes.Backend.QtWebKit) def debug_clear_ssl_errors(self): """Clear remembered SSL error answers.""" self._current_widget().clear_ssl_errors() diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index ddc49e02e..b58ffade5 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -19,7 +19,17 @@ """Various utilities shared between webpage/webview subclasses.""" +import html + +import jinja2 + from qutebrowser.config import config +from qutebrowser.utils import usertypes, message, log + + +class CallSuper(Exception): + + """Raised when the caller should call the superclass instead.""" def custom_headers(): @@ -39,3 +49,149 @@ def custom_headers(): headers[b'Accept-Language'] = accept_language.encode('ascii') return sorted(headers.items()) + + +def authentication_required(url, authenticator, abort_on): + """Ask a prompt for an authentication question.""" + realm = authenticator.realm() + if realm: + msg = '{} says:
{}'.format( + html.escape(url.toDisplayString()), html.escape(realm)) + else: + msg = '{} needs authentication'.format( + html.escape(url.toDisplayString())) + answer = message.ask(title="Authentication required", text=msg, + mode=usertypes.PromptMode.user_pwd, + abort_on=abort_on) + if answer is not None: + authenticator.setUser(answer.user) + authenticator.setPassword(answer.password) + + +def javascript_confirm(url, js_msg, abort_on): + """Display a javascript confirm prompt.""" + log.js.debug("confirm: {}".format(js_msg)) + if config.get('ui', 'modal-js-dialog'): + raise CallSuper + + msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), + html.escape(js_msg)) + ans = message.ask('Javascript confirm', msg, + mode=usertypes.PromptMode.yesno, + abort_on=abort_on) + return bool(ans) + + +def javascript_prompt(url, js_msg, default, abort_on): + """Display a javascript prompt.""" + log.js.debug("prompt: {}".format(js_msg)) + if config.get('ui', 'modal-js-dialog'): + raise CallSuper + if config.get('content', 'ignore-javascript-prompt'): + return (False, "") + + msg = '{} asks:
{}'.format(html.escape(url.toDisplayString()), + html.escape(js_msg)) + answer = message.ask('Javascript prompt', msg, + mode=usertypes.PromptMode.text, + default=default, + abort_on=abort_on) + + if answer is None: + return (False, "") + else: + return (True, answer) + + +def javascript_alert(url, js_msg, abort_on): + """Display a javascript alert.""" + log.js.debug("alert: {}".format(js_msg)) + if config.get('ui', 'modal-js-dialog'): + raise CallSuper + + if config.get('content', 'ignore-javascript-alert'): + return + + msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), + html.escape(js_msg)) + message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, + abort_on=abort_on) + + +def ignore_certificate_errors(url, errors, abort_on): + """Display a certificate error question. + + Args: + url: The URL the errors happened in + errors: A list of QSslErrors or QWebEngineCertificateErrors + + Return: + True if the error should be ignored, False otherwise. + """ + ssl_strict = config.get('network', 'ssl-strict') + log.webview.debug("Certificate errors {!r}, strict {}".format( + errors, ssl_strict)) + + for error in errors: + assert error.is_overridable(), repr(error) + + if ssl_strict == 'ask': + err_template = jinja2.Template(""" + Errors while loading {{url.toDisplayString()}}:
+
    + {% for err in errors %} +
  • {{err}}
  • + {% endfor %} +
+ """.strip()) + msg = err_template.render(url=url, errors=errors) + + return message.ask(title="Certificate errors - continue?", text=msg, + mode=usertypes.PromptMode.yesno, default=False, + abort_on=abort_on) + elif ssl_strict is False: + log.webview.debug("ssl-strict is False, only warning about errors") + for err in errors: + # FIXME we might want to use warn here (non-fatal error) + # https://github.com/The-Compiler/qutebrowser/issues/114 + message.error('Certificate error: {}'.format(err)) + return True + elif ssl_strict is True: + return False + else: + raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict)) + raise AssertionError("Not reached") + + +def feature_permission(url, option, msg, yes_action, no_action, abort_on): + """Handle a feature permission request. + + Args: + url: The URL the request was done for. + option: A (section, option) tuple for the option to check. + msg: A string like "show notifications" + yes_action: A callable to call if the request was approved + no_action: A callable to call if the request was denied + abort_on: A list of signals which interrupt the question. + + Return: + The Question object if a question was asked, None otherwise. + """ + config_val = config.get(*option) + if config_val == 'ask': + if url.isValid(): + text = "Allow the website at {} to {}?".format( + html.escape(url.toDisplayString()), msg) + else: + text = "Allow the website to {}?".format(msg) + + return message.confirm_async( + yes_action=yes_action, no_action=no_action, + cancel_action=no_action, abort_on=abort_on, + title='Permission request', text=text) + elif config_val: + yes_action() + return None + else: + no_action() + return None diff --git a/qutebrowser/browser/webengine/certificateerror.py b/qutebrowser/browser/webengine/certificateerror.py new file mode 100644 index 000000000..19b59c522 --- /dev/null +++ b/qutebrowser/browser/webengine/certificateerror.py @@ -0,0 +1,43 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# 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 . + +"""Wrapper over a QWebEngineCertificateError.""" + +# pylint: disable=no-name-in-module,import-error,useless-suppression +from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError +# pylint: enable=no-name-in-module,import-error,useless-suppression + +from qutebrowser.utils import usertypes, utils, debug + + +class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper): + + """A wrapper over a QWebEngineCertificateError.""" + + def __str__(self): + return self._error.errorDescription() + + def __repr__(self): + return utils.get_repr( + self, error=debug.qenum_key(QWebEngineCertificateError, + self._error.error()), + string=str(self)) + + def is_overridable(self): + return self._error.isOverridable() diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index acef4e5ef..afb687c86 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -32,7 +32,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript, QWebEngineProfile) # pylint: enable=no-name-in-module,import-error,useless-suppression -from qutebrowser.browser import browsertab, mouse +from qutebrowser.browser import browsertab, mouse, shared from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, interceptor, webenginequtescheme, webenginedownloads) @@ -544,7 +544,8 @@ class WebEngineTab(browsertab.AbstractTab): self._widget.page().runJavaScript(code, callback) def shutdown(self): - log.stub() + self.shutting_down.emit() + self._widget.shutdown() def reload(self, *, force=False): if force: @@ -575,7 +576,7 @@ class WebEngineTab(browsertab.AbstractTab): self._widget.setHtml(html, base_url) def clear_ssl_errors(self): - log.stub() + raise browsertab.UnsupportedOperationError @pyqtSlot() def _on_history_trigger(self): @@ -596,6 +597,13 @@ class WebEngineTab(browsertab.AbstractTab): self.add_history_item.emit(url, requested_url, title) + @pyqtSlot(QUrl, 'QAuthenticator*') + def _on_authentication_required(self, url, authenticator): + # FIXME:qtwebengine support .netrc + shared.authentication_required(url, authenticator, + abort_on=[self.shutting_down, + self.load_started]) + def _connect_signals(self): view = self._widget page = view.page() @@ -609,6 +617,7 @@ class WebEngineTab(browsertab.AbstractTab): page.loadFinished.connect(self._on_load_finished) page.certificate_error.connect(self._on_ssl_errors) page.link_clicked.connect(self._on_link_clicked) + page.authenticationRequired.connect(self._on_authentication_required) try: view.iconChanged.connect(self.icon_changed) except AttributeError: diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 46a99db24..fbb47b23e 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -20,14 +20,17 @@ """The main browser widget for QtWebEngine.""" import os +import functools -from PyQt5.QtCore import pyqtSignal, QUrl +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION # pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage # pylint: enable=no-name-in-module,import-error,useless-suppression +from qutebrowser.browser import shared +from qutebrowser.browser.webengine import certificateerror from qutebrowser.config import config -from qutebrowser.utils import log, debug, usertypes, objreg, qtutils +from qutebrowser.utils import log, debug, usertypes, objreg, qtutils, jinja class WebEngineView(QWebEngineView): @@ -39,6 +42,9 @@ class WebEngineView(QWebEngineView): self._win_id = win_id self.setPage(WebEnginePage(tabdata, parent=self)) + def shutdown(self): + self.page().shutdown() + def createWindow(self, wintype): """Called by Qt when a page wants to create a new window. @@ -99,21 +105,155 @@ class WebEnginePage(QWebEnginePage): """Custom QWebEnginePage subclass with qutebrowser-specific features. + Attributes: + _is_shutting_down: Whether the page is currently shutting down. + Signals: - certificate_error: FIXME:qtwebengine + certificate_error: Emitted on certificate errors. link_clicked: Emitted when a link was clicked on a page. + shutting_down: Emitted when the page is shutting down. """ certificate_error = pyqtSignal() link_clicked = pyqtSignal(QUrl) + shutting_down = pyqtSignal() def __init__(self, tabdata, parent=None): super().__init__(parent) self._tabdata = tabdata + self._is_shutting_down = False + self.featurePermissionRequested.connect( + self._on_feature_permission_requested) + + @pyqtSlot(QUrl, 'QWebEnginePage::Feature') + def _on_feature_permission_requested(self, url, feature): + """Ask the user for approval for geolocation/media/etc..""" + options = { + QWebEnginePage.Geolocation: ('content', 'geolocation'), + QWebEnginePage.MediaAudioCapture: ('content', 'media-capture'), + QWebEnginePage.MediaVideoCapture: ('content', 'media-capture'), + QWebEnginePage.MediaAudioVideoCapture: + ('content', 'media-capture'), + } + messages = { + QWebEnginePage.Geolocation: 'access your location', + QWebEnginePage.MediaAudioCapture: 'record audio', + QWebEnginePage.MediaVideoCapture: 'record video', + QWebEnginePage.MediaAudioVideoCapture: 'record audio/video', + } + assert options.keys() == messages.keys() + + if feature not in options: + log.webview.error("Unhandled feature permission {}".format( + debug.qenum_key(QWebEnginePage, feature))) + self.setFeaturePermission(url, feature, + QWebEnginePage.PermissionDeniedByUser) + return + + yes_action = functools.partial( + self.setFeaturePermission, url, feature, + QWebEnginePage.PermissionGrantedByUser) + no_action = functools.partial( + self.setFeaturePermission, url, feature, + QWebEnginePage.PermissionDeniedByUser) + + question = shared.feature_permission( + url=url, option=options[feature], msg=messages[feature], + yes_action=yes_action, no_action=no_action, + abort_on=[self.shutting_down, self.loadStarted]) + + if question is not None: + self.featurePermissionRequestCanceled.connect( + functools.partial(self._on_feature_permission_cancelled, + question, url, feature)) + + def _on_feature_permission_cancelled(self, question, url, feature, + cancelled_url, cancelled_feature): + """Slot invoked when a feature permission request was cancelled. + + To be used with functools.partial. + """ + if url == cancelled_url and feature == cancelled_feature: + try: + question.abort() + except RuntimeError: + # The question could already be deleted, e.g. because it was + # aborted after a loadStarted signal. + pass + + def shutdown(self): + self._is_shutting_down = True + self.shutting_down.emit() def certificateError(self, error): + """Handle certificate errors coming from Qt.""" self.certificate_error.emit() - return super().certificateError(error) + url = error.url() + error = certificateerror.CertificateErrorWrapper(error) + log.webview.debug("Certificate error: {}".format(error)) + + url_string = url.toDisplayString() + error_page = jinja.render( + 'error.html', title="Error loading page: {}".format(url_string), + url=url_string, error=str(error), icon='') + + if error.is_overridable(): + ignore = shared.ignore_certificate_errors( + url, [error], abort_on=[self.loadStarted, self.shutting_down]) + else: + log.webview.error("Non-overridable certificate error: " + "{}".format(error)) + ignore = False + + # We can't really know when to show an error page, as the error might + # have happened when loading some resource. + # However, self.url() is not available yet and self.requestedUrl() + # might not match the URL we get from the error - so we just apply a + # heuristic here. + # See https://bugreports.qt.io/browse/QTBUG-56207 + log.webview.debug("ignore {}, URL {}, requested {}".format( + ignore, url, self.requestedUrl())) + if not ignore and url.matches(self.requestedUrl(), QUrl.RemoveScheme): + self.setHtml(error_page) + + return ignore + + def javaScriptConfirm(self, url, js_msg): + """Override javaScriptConfirm to use qutebrowser prompts.""" + if self._is_shutting_down: + return False + try: + return shared.javascript_confirm(url, js_msg, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + return super().javaScriptConfirm(url, js_msg) + + if PYQT_VERSION > 0x050700: + # WORKAROUND + # Can't override javaScriptPrompt with older PyQt versions + # https://www.riverbankcomputing.com/pipermail/pyqt/2016-November/038293.html + def javaScriptPrompt(self, url, js_msg, default): + """Override javaScriptPrompt to use qutebrowser prompts.""" + if self._is_shutting_down: + return (False, "") + try: + return shared.javascript_prompt(url, js_msg, default, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + return super().javaScriptPrompt(url, js_msg, default) + + def javaScriptAlert(self, url, js_msg): + """Override javaScriptAlert to use qutebrowser prompts.""" + if self._is_shutting_down: + return + try: + shared.javascript_alert(url, js_msg, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + super().javaScriptAlert(url, js_msg) def javaScriptConsoleMessage(self, level, msg, line, source): """Log javascript messages to qutebrowser's log.""" diff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py new file mode 100644 index 000000000..41cf2866f --- /dev/null +++ b/qutebrowser/browser/webkit/certificateerror.py @@ -0,0 +1,52 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# 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 . + +"""Wrapper over a QSslError.""" + + +from PyQt5.QtNetwork import QSslError + +from qutebrowser.utils import usertypes, utils, debug + + +class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper): + + """A wrapper over a QSslError.""" + + def __str__(self): + return self._error.errorString() + + def __repr__(self): + return utils.get_repr( + self, error=debug.qenum_key(QSslError, self._error.error()), + string=str(self)) + + def __hash__(self): + try: + # Qt >= 5.4 + return hash(self._error) + except TypeError: + return hash((self._error.certificate().toDer(), + self._error.error())) + + def __eq__(self, other): + return self._error == other._error # pylint: disable=protected-access + + def is_overridable(self): + return True diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 81ce0eb79..100db9ab0 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -24,18 +24,17 @@ import collections import netrc import html -import jinja2 from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, QUrl, QByteArray) -from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError, - QSslSocket) +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from qutebrowser.config import config from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, - urlutils, debug) + urlutils) from qutebrowser.browser import shared -from qutebrowser.browser.webkit.network import webkitqutescheme, networkreply -from qutebrowser.browser.webkit.network import filescheme +from qutebrowser.browser.webkit import certificateerror +from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, + filescheme) HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' @@ -112,24 +111,6 @@ def init(): QSslSocket.setDefaultCiphers(good_ciphers) -class SslError(QSslError): - - """A QSslError subclass which provides __hash__ on Qt < 5.4.""" - - def __hash__(self): - try: - # Qt >= 5.4 - # pylint: disable=not-callable,useless-suppression - return super().__hash__() - except TypeError: - return hash((self.certificate().toDer(), self.error())) - - def __repr__(self): - return utils.get_repr( - self, error=debug.qenum_key(QSslError, self.error()), - string=self.errorString()) - - class NetworkManager(QNetworkAccessManager): """Our own QNetworkAccessManager. @@ -203,32 +184,18 @@ class NetworkManager(QNetworkAccessManager): self.setCache(cache) cache.setParent(app) - def _ask(self, title, text, mode, owner=None, default=None): - """Ask a blocking question in the statusbar. - - Args: - title: The title to display to the user. - text: The text to display to the user. - mode: A PromptMode. - owner: An object which will abort the question if destroyed, or - None. - - Return: - The answer the user gave or None if the prompt was cancelled. - """ + def _get_abort_signals(self, owner=None): + """Get a list of signals which should abort a question.""" abort_on = [self.shutting_down] if owner is not None: abort_on.append(owner.destroyed) - # This might be a generic network manager, e.g. one belonging to a # DownloadManager. In this case, just skip the webview thing. if self._tab_id is not None: tab = objreg.get('tab', scope='tab', window=self._win_id, tab=self._tab_id) abort_on.append(tab.load_started) - - return message.ask(title=title, text=text, mode=mode, - abort_on=abort_on, default=default) + return abort_on def shutdown(self): """Abort all running requests.""" @@ -248,11 +215,9 @@ class NetworkManager(QNetworkAccessManager): reply: The QNetworkReply that is encountering the errors. errors: A list of errors. """ - errors = [SslError(e) for e in errors] - ssl_strict = config.get('network', 'ssl-strict') - log.webview.debug("SSL errors {!r}, strict {}".format( - errors, ssl_strict)) - + errors = [certificateerror.CertificateErrorWrapper(e) for e in errors] + log.webview.debug("Certificate errors: {!r}".format( + ' / '.join(str(err) for err in errors))) try: host_tpl = urlutils.host_tuple(reply.url()) except ValueError: @@ -268,42 +233,22 @@ class NetworkManager(QNetworkAccessManager): log.webview.debug("Already accepted: {} / " "rejected {}".format(is_accepted, is_rejected)) - if (ssl_strict and ssl_strict != 'ask') or is_rejected: + if is_rejected: return elif is_accepted: reply.ignoreSslErrors() return - if ssl_strict == 'ask': - err_template = jinja2.Template(""" - Errors while loading {{url.toDisplayString()}}:
-
    - {% for err in errors %} -
  • {{err.errorString()}}
  • - {% endfor %} -
- """.strip()) - msg = err_template.render(url=reply.url(), errors=errors) - - answer = self._ask('SSL errors - continue?', msg, - mode=usertypes.PromptMode.yesno, owner=reply, - default=False) - log.webview.debug("Asked for SSL errors, answer {}".format(answer)) - if answer: - reply.ignoreSslErrors() - err_dict = self._accepted_ssl_errors - else: - err_dict = self._rejected_ssl_errors - if host_tpl is not None: - err_dict[host_tpl] += errors - else: - log.webview.debug("ssl-strict is False, only warning about errors") - for err in errors: - # FIXME we might want to use warn here (non-fatal error) - # https://github.com/The-Compiler/qutebrowser/issues/114 - message.error('SSL error: {}'.format(err.errorString())) + abort_on = self._get_abort_signals(reply) + ignore = shared.ignore_certificate_errors(reply.url(), errors, + abort_on=abort_on) + if ignore: reply.ignoreSslErrors() - self._accepted_ssl_errors[host_tpl] += errors + err_dict = self._accepted_ssl_errors + else: + err_dict = self._rejected_ssl_errors + if host_tpl is not None: + err_dict[host_tpl] += errors def clear_all_ssl_errors(self): """Clear all remembered SSL errors.""" @@ -343,19 +288,13 @@ class NetworkManager(QNetworkAccessManager): except netrc.NetrcParseError: log.misc.exception("Error when parsing the netrc file") - if user is None: - # netrc check failed - msg = '{} says:
{}'.format( - html.escape(reply.url().toDisplayString()), - html.escape(authenticator.realm())) - answer = self._ask("Authentication required", - text=msg, mode=usertypes.PromptMode.user_pwd, - owner=reply) - if answer is not None: - user, password = answer.user, answer.password if user is not None: authenticator.setUser(user) authenticator.setPassword(password) + else: + abort_on = self._get_abort_signals(reply) + shared.authentication_required(reply.url(), authenticator, + abort_on=abort_on) @pyqtSlot('QNetworkProxy', 'QAuthenticator*') def on_proxy_authentication_required(self, proxy, authenticator): @@ -369,9 +308,10 @@ class NetworkManager(QNetworkAccessManager): msg = '{} says:
{}'.format( html.escape(proxy.hostName()), html.escape(authenticator.realm())) - answer = self._ask( - "Proxy authentication required", msg, - mode=usertypes.PromptMode.user_pwd) + abort_on = self._get_abort_signals() + answer = message.ask( + title="Proxy authentication required", text=msg, + mode=usertypes.PromptMode.user_pwd, abort_on=abort_on) if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 09feed3d5..bf41ec10a 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -30,7 +30,7 @@ from PyQt5.QtPrintSupport import QPrintDialog from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from qutebrowser.config import config -from qutebrowser.browser import pdfjs +from qutebrowser.browser import pdfjs, shared from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils, @@ -82,7 +82,7 @@ class BrowserPage(QWebPage): self.unsupportedContent.connect(self.on_unsupported_content) self.loadStarted.connect(self.on_load_started) self.featurePermissionRequested.connect( - self.on_feature_permission_requested) + self._on_feature_permission_requested) self.saveFrameStateRequested.connect( self.on_save_frame_state_requested) self.restoreFrameStateRequested.connect( @@ -94,23 +94,16 @@ class BrowserPage(QWebPage): # of a bug in PyQt. # See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html - def javaScriptPrompt(self, _frame, js_msg, default): - """Override javaScriptPrompt to use the statusbar.""" - if (self._is_shutting_down or - config.get('content', 'ignore-javascript-prompt')): + def javaScriptPrompt(self, frame, js_msg, default): + """Override javaScriptPrompt to use qutebrowser prompts.""" + if self._is_shutting_down: return (False, "") - msg = '{} asks:
{}'.format( - html.escape(self.mainFrame().url().toDisplayString()), - html.escape(js_msg)) - answer = message.ask('Javascript prompt', msg, - mode=usertypes.PromptMode.text, - default=default, - abort_on=[self.loadStarted, - self.shutting_down]) - if answer is None: - return (False, "") - else: - return (True, answer) + try: + return shared.javascript_prompt(frame.url(), js_msg, default, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + return super().javaScriptPrompt(frame, js_msg, default) def _handle_errorpage(self, info, errpage): """Display an error page if needed. @@ -296,7 +289,7 @@ class BrowserPage(QWebPage): self.error_occurred = False @pyqtSlot('QWebFrame*', 'QWebPage::Feature') - def on_feature_permission_requested(self, frame, feature): + def _on_feature_permission_requested(self, frame, feature): """Ask the user for approval for geolocation/notifications.""" if not isinstance(frame, QWebFrame): # pragma: no cover # This makes no sense whatsoever, but someone reported this being @@ -309,46 +302,30 @@ class BrowserPage(QWebPage): QWebPage.Notifications: ('content', 'notifications'), QWebPage.Geolocation: ('content', 'geolocation'), } - config_val = config.get(*options[feature]) - if config_val == 'ask': - msgs = { - QWebPage.Notifications: 'show notifications', - QWebPage.Geolocation: 'access your location', - } + messages = { + QWebPage.Notifications: 'show notifications', + QWebPage.Geolocation: 'access your location', + } + yes_action = functools.partial( + self.setFeaturePermission, frame, feature, + QWebPage.PermissionGrantedByUser) + no_action = functools.partial( + self.setFeaturePermission, frame, feature, + QWebPage.PermissionDeniedByUser) - host = frame.url().host() - if host: - text = "Allow the website at {} to {}?".format( - html.escape(frame.url().toDisplayString()), msgs[feature]) - else: - text = "Allow the website to {}?".format(msgs[feature]) + question = shared.feature_permission( + url=frame.url(), + option=options[feature], msg=messages[feature], + yes_action=yes_action, no_action=no_action, + abort_on=[self.shutting_down, self.loadStarted]) - yes_action = functools.partial( - self.setFeaturePermission, frame, feature, - QWebPage.PermissionGrantedByUser) - no_action = functools.partial( - self.setFeaturePermission, frame, feature, - QWebPage.PermissionDeniedByUser) - - question = message.confirm_async(yes_action=yes_action, - no_action=no_action, - cancel_action=no_action, - abort_on=[self.shutting_down, - self.loadStarted], - title='Permission request', - text=text) + if question is not None: self.featurePermissionRequestCanceled.connect( - functools.partial(self.on_feature_permission_cancelled, + functools.partial(self._on_feature_permission_cancelled, question, frame, feature)) - elif config_val: - self.setFeaturePermission(frame, feature, - QWebPage.PermissionGrantedByUser) - else: - self.setFeaturePermission(frame, feature, - QWebPage.PermissionDeniedByUser) - def on_feature_permission_cancelled(self, question, frame, feature, - cancelled_frame, cancelled_feature): + def _on_feature_permission_cancelled(self, question, frame, feature, + cancelled_frame, cancelled_feature): """Slot invoked when a feature permission request was cancelled. To be used with functools.partial. @@ -441,37 +418,26 @@ class BrowserPage(QWebPage): return handler(opt, out) def javaScriptAlert(self, frame, js_msg): - """Override javaScriptAlert to use the statusbar.""" - log.js.debug("alert: {}".format(js_msg)) - if config.get('ui', 'modal-js-dialog'): - return super().javaScriptAlert(frame, js_msg) - - if (self._is_shutting_down or - config.get('content', 'ignore-javascript-alert')): + """Override javaScriptAlert to use qutebrowser prompts.""" + if self._is_shutting_down: return - - msg = 'From {}:
{}'.format( - html.escape(self.mainFrame().url().toDisplayString()), - html.escape(js_msg)) - message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, - abort_on=[self.loadStarted, self.shutting_down]) + try: + shared.javascript_alert(frame.url(), js_msg, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + super().javaScriptAlert(frame, js_msg) def javaScriptConfirm(self, frame, js_msg): """Override javaScriptConfirm to use the statusbar.""" - log.js.debug("confirm: {}".format(js_msg)) - if config.get('ui', 'modal-js-dialog'): - return super().javaScriptConfirm(frame, js_msg) - if self._is_shutting_down: return False - - msg = 'From {}:
{}'.format( - html.escape(self.mainFrame().url().toDisplayString()), - html.escape(js_msg)) - ans = message.ask('Javascript confirm', msg, - mode=usertypes.PromptMode.yesno, - abort_on=[self.loadStarted, self.shutting_down]) - return bool(ans) + try: + return shared.javascript_confirm(frame.url(), js_msg, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + return super().javaScriptConfirm(frame, js_msg) def javaScriptConsoleMessage(self, msg, line, source): """Override javaScriptConsoleMessage to use debug log.""" diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index d55830380..e90ac15b3 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -430,8 +430,7 @@ def data(readonly=False): "Whether to send DNS requests over the configured proxy."), ('ssl-strict', - SettingValue(typ.BoolAsk(), 'ask', - backends=[usertypes.Backend.QtWebKit]), + SettingValue(typ.BoolAsk(), 'ask'), "Whether to validate SSL handshakes."), ('dns-prefetch', @@ -822,6 +821,11 @@ def data(readonly=False): SettingValue(typ.BoolAsk(), 'ask'), "Allow websites to show notifications."), + ('media-capture', + SettingValue(typ.BoolAsk(), 'ask', + backends=[usertypes.Backend.QtWebEngine]), + "Allow websites to record audio/video."), + ('javascript-can-open-windows-automatically', SettingValue(typ.Bool(), 'false'), "Whether JavaScript programs can open new windows without user " diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 1d2788ba1..7ad89b3a2 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -402,3 +402,20 @@ class Timer(QTimer): super().start(msec) else: super().start() + + +class AbstractCertificateErrorWrapper: + + """A wrapper over an SSL/certificate error.""" + + def __init__(self, error): + self._error = error + + def __str__(self): + raise NotImplementedError + + def __repr__(self): + raise NotImplementedError + + def is_overridable(self): + raise NotImplementedError diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index eeda04287..895993bae 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -74,8 +74,8 @@ PERFECT_FILES = [ ('tests/unit/browser/test_signalfilter.py', 'qutebrowser/browser/signalfilter.py'), - ('tests/unit/browser/test_shared.py', - 'qutebrowser/browser/shared.py'), + (None, + 'qutebrowser/browser/webkit/certificateerror.py'), # ('tests/unit/browser/test_tab.py', # 'qutebrowser/browser/tab.py'), diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 3d8fa7e17..ade05ac94 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -98,6 +98,7 @@ def whitelist_generator(): yield 'scripts.dev.pylint_checkers.config.' + attr yield 'scripts.dev.pylint_checkers.modeline.process_module' + yield 'scripts.dev.pylint_checkers.qute_pylint.config.msgs' for attr in ['_get_default_metavar_for_optional', '_get_default_metavar_for_positional', '_metavar_formatter']: diff --git a/tests/conftest.py b/tests/conftest.py index 907656b80..7fa94ffc3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ import warnings import pytest import hypothesis +from PyQt5.QtCore import PYQT_VERSION pytest.register_assert_rewrite('helpers') @@ -120,8 +121,14 @@ def pytest_collection_modifyitems(config, items): _apply_platform_markers(item) if item.get_marker('xfail_norun'): item.add_marker(pytest.mark.xfail(run=False)) - if item.get_marker('flaky_once'): - item.add_marker(pytest.mark.flaky()) + 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 deselected: deselected_items.append(item) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 9b7a9e2d5..dea0e9a83 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -352,6 +352,15 @@ def javascript_message_when(quteproc, message): quteproc.wait_for_js(message) +@bdd.when("I clear SSL errors") +def clear_ssl_errors(request, quteproc): + if request.config.webengine: + quteproc.terminate() + quteproc.start() + else: + quteproc.send_cmd(':debug-clear-ssl-errors') + + ## Then diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index f187a7336..1e1e59b34 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -75,9 +75,8 @@ Feature: Downloading things from a website. And I run :leave-mode Then no crash should happen - @qtwebengine_todo: ssl-strict is not implemented yet Scenario: Downloading with SSL errors (issue 1413) - When I run :debug-clear-ssl-errors + When I clear SSL errors And I set network -> ssl-strict to ask And I download an SSL page And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index 165bdd792..529cced46 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -34,7 +34,7 @@ Feature: Page history Then the history file should contain: http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli - @flaky_once @qtwebengine_todo: Error page message is not implemented + @flaky @qtwebengine_todo: Error page message is not implemented Scenario: History with an error When I run :open file:///does/not/exist And I wait for "Error while loading file:///does/not/exist: Error opening /does/not/exist: *" in the log diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 46e4dc287..40f17d1a0 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -382,7 +382,7 @@ Feature: Various utility commands. And I press the key "" Then no crash should happen - @pyqt>=5.3.1 @qtwebengine_skip: JS prompt is not implemented yet + @js_prompt Scenario: Focusing download widget via Tab (original issue) When I open data/prompt/jsprompt.html And I run :click-element id button diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index fe39a90be..978e79406 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -40,7 +40,7 @@ Feature: Prompts And I run :leave-mode Then the javascript message "confirm reply: false" should be logged - @pyqt>=5.3.1 + @js_prompt Scenario: Javascript prompt When I open data/prompt/jsprompt.html And I run :click-element id button @@ -49,7 +49,7 @@ Feature: Prompts And I run :prompt-accept Then the javascript message "Prompt reply: prompt test" should be logged - @pyqt>=5.3.1 + @js_prompt Scenario: Javascript prompt with default When I open data/prompt/jsprompt.html And I run :click-element id button-default @@ -57,7 +57,7 @@ Feature: Prompts And I run :prompt-accept Then the javascript message "Prompt reply: default" should be logged - @pyqt>=5.3.1 + @js_prompt Scenario: Rejected javascript prompt When I open data/prompt/jsprompt.html And I run :click-element id button @@ -68,6 +68,7 @@ Feature: Prompts # Multiple prompts + @qtwebengine_skip: QtWebEngine refuses to load anything with a JS question Scenario: Blocking question interrupted by blocking one When I set content -> ignore-javascript-alert to false And I open data/prompt/jsalert.html @@ -83,6 +84,7 @@ Feature: Prompts Then the javascript message "confirm reply: true" should be logged And the javascript message "Alert done" should be logged + @qtwebengine_skip: QtWebEngine refuses to load anything with a JS question Scenario: Blocking question interrupted by async one When I set content -> ignore-javascript-alert to false And I set content -> notifications to ask @@ -99,6 +101,7 @@ Feature: Prompts Then the javascript message "Alert done" should be logged And the javascript message "notification permission granted" should be logged + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: Async question interrupted by async one When I set content -> notifications to ask And I open data/prompt/notifications.html in a new tab @@ -113,6 +116,7 @@ Feature: Prompts Then the javascript message "notification permission granted" should be logged And "Added quickmark test for *" should be logged + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: Async question interrupted by blocking one When I set content -> notifications to ask And I set content -> ignore-javascript-alert to false @@ -131,7 +135,7 @@ Feature: Prompts # Shift-Insert with prompt (issue 1299) - @pyqt>=5.3.1 + @js_prompt Scenario: Pasting via shift-insert in prompt mode When selection is supported And I put "insert test" into the primary selection @@ -142,7 +146,7 @@ Feature: Prompts And I run :prompt-accept Then the javascript message "Prompt reply: insert test" should be logged - @pyqt>=5.3.1 + @js_prompt Scenario: Pasting via shift-insert without it being supported When selection is not supported And I put "insert test" into the primary selection @@ -153,7 +157,7 @@ Feature: Prompts And I run :prompt-accept Then the javascript message "Prompt reply: " should be logged - @pyqt>=5.3.1 + @js_prompt Scenario: Using content -> ignore-javascript-prompt When I set content -> ignore-javascript-prompt to true And I open data/prompt/jsprompt.html @@ -163,22 +167,21 @@ Feature: Prompts # SSL Scenario: SSL error with ssl-strict = false - When I run :debug-clear-ssl-errors + When I clear SSL errors And I set network -> ssl-strict to false And I load an SSL page And I wait until the SSL page finished loading - Then the error "SSL error: *" should be shown + Then the error "Certificate error: *" should be shown And the page should contain the plaintext "Hello World via SSL!" Scenario: SSL error with ssl-strict = true - When I run :debug-clear-ssl-errors + When I clear SSL errors And I set network -> ssl-strict to true And I load an SSL page - Then "Error while loading *: SSL handshake failed" should be logged - And the page should contain the plaintext "Unable to load page" + Then a SSL error page should be shown Scenario: SSL error with ssl-strict = ask -> yes - When I run :debug-clear-ssl-errors + When I clear SSL errors And I set network -> ssl-strict to ask And I load an SSL page And I wait for a prompt @@ -187,13 +190,12 @@ Feature: Prompts Then the page should contain the plaintext "Hello World via SSL!" Scenario: SSL error with ssl-strict = ask -> no - When I run :debug-clear-ssl-errors + When I clear SSL errors And I set network -> ssl-strict to ask And I load an SSL page And I wait for a prompt And I run :prompt-accept no - Then "Error while loading *: SSL handshake failed" should be logged - And the page should contain the plaintext "Unable to load page" + Then a SSL error page should be shown # Geolocation @@ -237,18 +239,21 @@ Feature: Prompts # Notifications + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: Always rejecting notifications When I set content -> notifications to false And I open data/prompt/notifications.html in a new tab And I run :click-element id button Then the javascript message "notification permission denied" should be logged + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: Always accepting notifications When I set content -> notifications to true And I open data/prompt/notifications.html in a new tab And I run :click-element id button Then the javascript message "notification permission granted" should be logged + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: notifications with ask -> false When I set content -> notifications to ask And I open data/prompt/notifications.html in a new tab @@ -257,6 +262,7 @@ Feature: Prompts And I run :prompt-accept no Then the javascript message "notification permission denied" should be logged + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: notifications with ask -> true When I set content -> notifications to ask And I open data/prompt/notifications.html in a new tab @@ -275,6 +281,7 @@ Feature: Prompts And I run :leave-mode Then the javascript message "notification permission aborted" should be logged + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: answering notification after closing tab When I set content -> notifications to ask And I open data/prompt/notifications.html in a new tab @@ -287,55 +294,55 @@ Feature: Prompts # Page authentication Scenario: Successful webpage authentification - When I open basic-auth/user/password without waiting + When I open basic-auth/user1/password1 without waiting And I wait for a prompt - And I press the keys "user" + And I press the keys "user1" And I run :prompt-accept - And I press the keys "password" + And I press the keys "password1" And I run :prompt-accept - And I wait until basic-auth/user/password is loaded + And I wait until basic-auth/user1/password1 is loaded Then the json on the page should be: { "authenticated": true, - "user": "user" + "user": "user1" } Scenario: Authentication with :prompt-accept value When I open about:blank in a new tab - And I open basic-auth/user/password without waiting + And I open basic-auth/user2/password2 without waiting And I wait for a prompt - And I run :prompt-accept user:password - And I wait until basic-auth/user/password is loaded + And I run :prompt-accept user2:password2 + And I wait until basic-auth/user2/password2 is loaded Then the json on the page should be: { "authenticated": true, - "user": "user" + "user": "user2" } Scenario: Authentication with invalid :prompt-accept value When I open about:blank in a new tab - And I open basic-auth/user/password without waiting + And I open basic-auth/user3/password3 without waiting And I wait for a prompt And I run :prompt-accept foo - And I run :prompt-accept user:password + And I run :prompt-accept user3:password3 Then the error "Value needs to be in the format username:password, but foo was given" should be shown Scenario: Tabbing between username and password When I open about:blank in a new tab - And I open basic-auth/user/password without waiting + And I open basic-auth/user4/password4 without waiting And I wait for a prompt And I press the keys "us" And I run :prompt-item-focus next - And I press the keys "password" + And I press the keys "password4" And I run :prompt-item-focus prev - And I press the keys "er" + And I press the keys "er4" And I run :prompt-accept And I run :prompt-accept - And I wait until basic-auth/user/password is loaded + And I wait until basic-auth/user4/password4 is loaded Then the json on the page should be: { "authenticated": true, - "user": "user" + "user": "user4" } # :prompt-accept with value argument @@ -350,7 +357,7 @@ Feature: Prompts Then the javascript message "Alert done" should be logged And the error "No value is permitted with alert prompts!" should be shown - @pyqt>=5.3.1 + @js_prompt Scenario: Javascript prompt with value When I set content -> ignore-javascript-prompt to false And I open data/prompt/jsprompt.html @@ -396,6 +403,7 @@ Feature: Prompts # Other + @qtwebengine_skip Scenario: Shutting down with a question When I open data/prompt/jsconfirm.html And I run :click-element id button @@ -429,32 +437,34 @@ Feature: Prompts Then "Added quickmark prompt-in-command-mode for *" should be logged # https://github.com/The-Compiler/qutebrowser/issues/1093 + @qtwebengine_skip: QtWebEngine doesn't open the second page/prompt Scenario: Keyboard focus with multiple auth prompts - When I open basic-auth/user1/password1 without waiting - And I open basic-auth/user2/password2 in a new tab without waiting + When I open basic-auth/user5/password5 without waiting + And I open basic-auth/user6/password6 in a new tab without waiting And I wait for a prompt And I wait for a prompt # Second prompt (showed first) - And I press the keys "user2" + And I press the keys "user6" And I press the key "" - And I press the keys "password2" + And I press the keys "password6" And I press the key "" - And I wait until basic-auth/user2/password2 is loaded + And I wait until basic-auth/user6/password6 is loaded # First prompt - And I press the keys "user1" + And I press the keys "user5" And I press the key "" - And I press the keys "password1" + And I press the keys "password5" And I press the key "" - And I wait until basic-auth/user1/password1 is loaded + And I wait until basic-auth/user5/password5 is loaded # We're on the second page Then the json on the page should be: { "authenticated": true, - "user": "user2" + "user": "user6" } # https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531 # https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544 + @qtwebengine_todo: Notifications are not implemented in QtWebEngine Scenario: Interrupting SSL prompt during a notification prompt When I set content -> notifications to ask And I set network -> ssl-strict to ask diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py index 8a66880b7..3ebd53e8c 100644 --- a/tests/end2end/features/test_prompts_bdd.py +++ b/tests/end2end/features/test_prompts_bdd.py @@ -17,14 +17,10 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import pytest import pytest_bdd as bdd bdd.scenarios('prompts.feature') -pytestmark = pytest.mark.qtwebengine_todo("Prompts are not implemented") - - @bdd.when("I load an SSL page") def load_ssl_page(quteproc, ssl_server): # We don't wait here as we can get an SSL question. @@ -46,3 +42,32 @@ def wait_for_prompt(quteproc): def no_prompt_shown(quteproc): quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: ' 'question asked)') + + +@bdd.then("a SSL error page should be shown") +def ssl_error_page(request, quteproc): + if not request.config.webengine: + line = quteproc.wait_for(message='Error while loading *: SSL ' + 'handshake failed') + line.expected = True + quteproc.wait_for(message="Changing title for idx * to 'Error " + "loading page: *'") + content = quteproc.get_content().strip() + assert "Unable to load page" in content + + +class AbstractCertificateErrorWrapper: + + """A wrapper over an SSL/certificate error.""" + + def __init__(self, error): + self._error = error + + def __str__(self): + raise NotImplementedError + + def __repr__(self): + raise NotImplementedError + + def is_overridable(self): + raise NotImplementedError diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 46a0d2ff7..53f3bee88 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -53,6 +53,19 @@ def is_ignored_qt_message(message): return False +def is_ignored_lowlevel_message(message): + """Check if we want to ignore a lowlevel process output.""" + if 'Running without the SUID sandbox!' in message: + return True + elif message.startswith('Xlib: sequence lost'): + # https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720 + # ??? + return True + elif 'CERT_PKIXVerifyCert for localhost failed' in message: + return True + return False + + class LogLine(testprocess.Line): """A parsed line from the qutebrowser log output. @@ -222,14 +235,8 @@ class QuteProc(testprocess.Process): except testprocess.InvalidLine: if not line.strip(): return None - elif 'Running without the SUID sandbox!' in line: - # QtWebEngine error - return None - elif line.startswith('Xlib: sequence lost'): - # https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720 - # ??? - return None - elif is_ignored_qt_message(line): + elif (is_ignored_qt_message(line) or + is_ignored_lowlevel_message(line)): return None else: raise diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 2c5d23e65..f6e0f8ff2 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -81,7 +81,7 @@ class Request(testprocess.Line): http.client.FOUND] path_to_statuses['/absolute-redirect/{}'.format(i)] = [ http.client.FOUND] - for suffix in ['', '1', '2']: + for suffix in ['', '1', '2', '3', '4', '5', '6']: key = '/basic-auth/user{}/password{}'.format(suffix, suffix) path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK] diff --git a/tests/unit/utils/usertypes/test_misc.py b/tests/unit/utils/usertypes/test_misc.py new file mode 100644 index 000000000..c2c0738e5 --- /dev/null +++ b/tests/unit/utils/usertypes/test_misc.py @@ -0,0 +1,27 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# 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 . + + +from qutebrowser.utils import usertypes + + +def test_abstract_certificate_error_wrapper(): + err = object() + wrapper = usertypes.AbstractCertificateErrorWrapper(err) + assert wrapper._error is err