Merge branch 'webengine-prompts'
This commit is contained in:
commit
650b9e465c
@ -160,6 +160,7 @@
|
||||
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|
||||
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
||||
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
||||
|<<content-media-capture,media-capture>>|Allow websites to record audio/video.
|
||||
|<<content-javascript-can-open-windows-automatically,javascript-can-open-windows-automatically>>|Whether JavaScript programs can open new windows without user interaction.
|
||||
|<<content-javascript-can-close-windows,javascript-can-close-windows>>|Whether JavaScript programs can close windows.
|
||||
|<<content-javascript-can-access-clipboard,javascript-can-access-clipboard>>|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.
|
||||
|
@ -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 =
|
||||
|
@ -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()
|
||||
|
@ -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 = '<b>{}</b> says:<br/>{}'.format(
|
||||
html.escape(url.toDisplayString()), html.escape(realm))
|
||||
else:
|
||||
msg = '<b>{}</b> 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 <b>{}</b>:<br/>{}'.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 = '<b>{}</b> asks:<br/>{}'.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 <b>{}</b>:<br/>{}'.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 <b>{{url.toDisplayString()}}</b>:<br/>
|
||||
<ul>
|
||||
{% for err in errors %}
|
||||
<li>{{err}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
""".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 <b>{}</b> 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
|
||||
|
43
qutebrowser/browser/webengine/certificateerror.py
Normal file
43
qutebrowser/browser/webengine/certificateerror.py
Normal file
@ -0,0 +1,43 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""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()
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
52
qutebrowser/browser/webkit/certificateerror.py
Normal file
52
qutebrowser/browser/webkit/certificateerror.py
Normal file
@ -0,0 +1,52 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""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
|
@ -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 <b>{{url.toDisplayString()}}</b>:<br/>
|
||||
<ul>
|
||||
{% for err in errors %}
|
||||
<li>{{err.errorString()}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
""".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 = '<b>{}</b> says:<br/>{}'.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 = '<b>{}</b> says:<br/>{}'.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)
|
||||
|
@ -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 = '<b>{}</b> asks:<br/>{}'.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 <b>{}</b> 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 <b>{}</b>:<br/>{}'.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 <b>{}</b>:<br/>{}'.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."""
|
||||
|
@ -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 "
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
|
||||
|
@ -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']:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -382,7 +382,7 @@ Feature: Various utility commands.
|
||||
And I press the key "<Ctrl-C>"
|
||||
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
|
||||
|
@ -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 "<Enter>"
|
||||
And I press the keys "password2"
|
||||
And I press the keys "password6"
|
||||
And I press the key "<Enter>"
|
||||
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 "<Enter>"
|
||||
And I press the keys "password1"
|
||||
And I press the keys "password5"
|
||||
And I press the key "<Enter>"
|
||||
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
|
||||
|
@ -17,14 +17,10 @@
|
||||
# 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
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
27
tests/unit/utils/usertypes/test_misc.py
Normal file
27
tests/unit/utils/usertypes/test_misc.py
Normal file
@ -0,0 +1,27 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
def test_abstract_certificate_error_wrapper():
|
||||
err = object()
|
||||
wrapper = usertypes.AbstractCertificateErrorWrapper(err)
|
||||
assert wrapper._error is err
|
Loading…
Reference in New Issue
Block a user