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-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|
||||||
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
||||||
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
|<<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-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-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.
|
|<<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]+
|
Default: +pass:[ask]+
|
||||||
|
|
||||||
This setting is only available with the QtWebKit backend.
|
|
||||||
|
|
||||||
[[network-dns-prefetch]]
|
[[network-dns-prefetch]]
|
||||||
=== dns-prefetch
|
=== dns-prefetch
|
||||||
Whether to try to pre-fetch DNS entries to speed up browsing.
|
Whether to try to pre-fetch DNS entries to speed up browsing.
|
||||||
@ -1452,6 +1451,20 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[ask]+
|
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]]
|
[[content-javascript-can-open-windows-automatically]]
|
||||||
=== javascript-can-open-windows-automatically
|
=== javascript-can-open-windows-automatically
|
||||||
Whether JavaScript programs can open new windows without user interaction.
|
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
|
end2end: End to end tests which run qutebrowser as subprocess
|
||||||
xfail_norun: xfail the test with out running it
|
xfail_norun: xfail the test with out running it
|
||||||
ci: Tests which should only run on CI.
|
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_todo: Features still missing with QtWebEngine
|
||||||
qtwebengine_skip: Tests not applicable with QtWebEngine
|
qtwebengine_skip: Tests not applicable with QtWebEngine
|
||||||
qtwebkit_skip: Tests not applicable with QtWebKit
|
qtwebkit_skip: Tests not applicable with QtWebKit
|
||||||
qtwebengine_createWindow: Tests using createWindow with QtWebEngine (QTBUG-54419)
|
qtwebengine_createWindow: Tests using createWindow with QtWebEngine (QTBUG-54419)
|
||||||
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
|
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
|
||||||
qtwebengine_osx_xfail: Tests which fail on OS X 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
|
this: Used to mark tests during development
|
||||||
qt_log_level_fail = WARNING
|
qt_log_level_fail = WARNING
|
||||||
qt_log_ignore =
|
qt_log_ignore =
|
||||||
|
@ -1991,7 +1991,7 @@ class CommandDispatcher:
|
|||||||
tab.send_event(release_event)
|
tab.send_event(release_event)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
debug=True)
|
debug=True, backend=usertypes.Backend.QtWebKit)
|
||||||
def debug_clear_ssl_errors(self):
|
def debug_clear_ssl_errors(self):
|
||||||
"""Clear remembered SSL error answers."""
|
"""Clear remembered SSL error answers."""
|
||||||
self._current_widget().clear_ssl_errors()
|
self._current_widget().clear_ssl_errors()
|
||||||
|
@ -19,7 +19,17 @@
|
|||||||
|
|
||||||
"""Various utilities shared between webpage/webview subclasses."""
|
"""Various utilities shared between webpage/webview subclasses."""
|
||||||
|
|
||||||
|
import html
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
|
||||||
from qutebrowser.config import config
|
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():
|
def custom_headers():
|
||||||
@ -39,3 +49,149 @@ def custom_headers():
|
|||||||
headers[b'Accept-Language'] = accept_language.encode('ascii')
|
headers[b'Accept-Language'] = accept_language.encode('ascii')
|
||||||
|
|
||||||
return sorted(headers.items())
|
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)
|
QWebEngineProfile)
|
||||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
# 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,
|
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||||
interceptor, webenginequtescheme,
|
interceptor, webenginequtescheme,
|
||||||
webenginedownloads)
|
webenginedownloads)
|
||||||
@ -544,7 +544,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
self._widget.page().runJavaScript(code, callback)
|
self._widget.page().runJavaScript(code, callback)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
log.stub()
|
self.shutting_down.emit()
|
||||||
|
self._widget.shutdown()
|
||||||
|
|
||||||
def reload(self, *, force=False):
|
def reload(self, *, force=False):
|
||||||
if force:
|
if force:
|
||||||
@ -575,7 +576,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
self._widget.setHtml(html, base_url)
|
self._widget.setHtml(html, base_url)
|
||||||
|
|
||||||
def clear_ssl_errors(self):
|
def clear_ssl_errors(self):
|
||||||
log.stub()
|
raise browsertab.UnsupportedOperationError
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _on_history_trigger(self):
|
def _on_history_trigger(self):
|
||||||
@ -596,6 +597,13 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
|
|
||||||
self.add_history_item.emit(url, requested_url, title)
|
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):
|
def _connect_signals(self):
|
||||||
view = self._widget
|
view = self._widget
|
||||||
page = view.page()
|
page = view.page()
|
||||||
@ -609,6 +617,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
page.loadFinished.connect(self._on_load_finished)
|
page.loadFinished.connect(self._on_load_finished)
|
||||||
page.certificate_error.connect(self._on_ssl_errors)
|
page.certificate_error.connect(self._on_ssl_errors)
|
||||||
page.link_clicked.connect(self._on_link_clicked)
|
page.link_clicked.connect(self._on_link_clicked)
|
||||||
|
page.authenticationRequired.connect(self._on_authentication_required)
|
||||||
try:
|
try:
|
||||||
view.iconChanged.connect(self.icon_changed)
|
view.iconChanged.connect(self.icon_changed)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -20,14 +20,17 @@
|
|||||||
"""The main browser widget for QtWebEngine."""
|
"""The main browser widget for QtWebEngine."""
|
||||||
|
|
||||||
import os
|
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
|
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
||||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
# 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.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):
|
class WebEngineView(QWebEngineView):
|
||||||
@ -39,6 +42,9 @@ class WebEngineView(QWebEngineView):
|
|||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
self.setPage(WebEnginePage(tabdata, parent=self))
|
self.setPage(WebEnginePage(tabdata, parent=self))
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.page().shutdown()
|
||||||
|
|
||||||
def createWindow(self, wintype):
|
def createWindow(self, wintype):
|
||||||
"""Called by Qt when a page wants to create a new window.
|
"""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.
|
"""Custom QWebEnginePage subclass with qutebrowser-specific features.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_is_shutting_down: Whether the page is currently shutting down.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
certificate_error: FIXME:qtwebengine
|
certificate_error: Emitted on certificate errors.
|
||||||
link_clicked: Emitted when a link was clicked on a page.
|
link_clicked: Emitted when a link was clicked on a page.
|
||||||
|
shutting_down: Emitted when the page is shutting down.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
certificate_error = pyqtSignal()
|
certificate_error = pyqtSignal()
|
||||||
link_clicked = pyqtSignal(QUrl)
|
link_clicked = pyqtSignal(QUrl)
|
||||||
|
shutting_down = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, tabdata, parent=None):
|
def __init__(self, tabdata, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._tabdata = tabdata
|
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):
|
def certificateError(self, error):
|
||||||
|
"""Handle certificate errors coming from Qt."""
|
||||||
self.certificate_error.emit()
|
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):
|
def javaScriptConsoleMessage(self, level, msg, line, source):
|
||||||
"""Log javascript messages to qutebrowser's log."""
|
"""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 netrc
|
||||||
import html
|
import html
|
||||||
|
|
||||||
import jinja2
|
|
||||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||||
QUrl, QByteArray)
|
QUrl, QByteArray)
|
||||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
|
||||||
QSslSocket)
|
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||||
urlutils, debug)
|
urlutils)
|
||||||
from qutebrowser.browser import shared
|
from qutebrowser.browser import shared
|
||||||
from qutebrowser.browser.webkit.network import webkitqutescheme, networkreply
|
from qutebrowser.browser.webkit import certificateerror
|
||||||
from qutebrowser.browser.webkit.network import filescheme
|
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
|
||||||
|
filescheme)
|
||||||
|
|
||||||
|
|
||||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||||
@ -112,24 +111,6 @@ def init():
|
|||||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
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):
|
class NetworkManager(QNetworkAccessManager):
|
||||||
|
|
||||||
"""Our own QNetworkAccessManager.
|
"""Our own QNetworkAccessManager.
|
||||||
@ -203,32 +184,18 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
self.setCache(cache)
|
self.setCache(cache)
|
||||||
cache.setParent(app)
|
cache.setParent(app)
|
||||||
|
|
||||||
def _ask(self, title, text, mode, owner=None, default=None):
|
def _get_abort_signals(self, owner=None):
|
||||||
"""Ask a blocking question in the statusbar.
|
"""Get a list of signals which should abort a question."""
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
abort_on = [self.shutting_down]
|
abort_on = [self.shutting_down]
|
||||||
if owner is not None:
|
if owner is not None:
|
||||||
abort_on.append(owner.destroyed)
|
abort_on.append(owner.destroyed)
|
||||||
|
|
||||||
# This might be a generic network manager, e.g. one belonging to a
|
# This might be a generic network manager, e.g. one belonging to a
|
||||||
# DownloadManager. In this case, just skip the webview thing.
|
# DownloadManager. In this case, just skip the webview thing.
|
||||||
if self._tab_id is not None:
|
if self._tab_id is not None:
|
||||||
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
||||||
tab=self._tab_id)
|
tab=self._tab_id)
|
||||||
abort_on.append(tab.load_started)
|
abort_on.append(tab.load_started)
|
||||||
|
return abort_on
|
||||||
return message.ask(title=title, text=text, mode=mode,
|
|
||||||
abort_on=abort_on, default=default)
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Abort all running requests."""
|
"""Abort all running requests."""
|
||||||
@ -248,11 +215,9 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
reply: The QNetworkReply that is encountering the errors.
|
reply: The QNetworkReply that is encountering the errors.
|
||||||
errors: A list of errors.
|
errors: A list of errors.
|
||||||
"""
|
"""
|
||||||
errors = [SslError(e) for e in errors]
|
errors = [certificateerror.CertificateErrorWrapper(e) for e in errors]
|
||||||
ssl_strict = config.get('network', 'ssl-strict')
|
log.webview.debug("Certificate errors: {!r}".format(
|
||||||
log.webview.debug("SSL errors {!r}, strict {}".format(
|
' / '.join(str(err) for err in errors)))
|
||||||
errors, ssl_strict))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
host_tpl = urlutils.host_tuple(reply.url())
|
host_tpl = urlutils.host_tuple(reply.url())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -268,42 +233,22 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
log.webview.debug("Already accepted: {} / "
|
log.webview.debug("Already accepted: {} / "
|
||||||
"rejected {}".format(is_accepted, is_rejected))
|
"rejected {}".format(is_accepted, is_rejected))
|
||||||
|
|
||||||
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
|
if is_rejected:
|
||||||
return
|
return
|
||||||
elif is_accepted:
|
elif is_accepted:
|
||||||
reply.ignoreSslErrors()
|
reply.ignoreSslErrors()
|
||||||
return
|
return
|
||||||
|
|
||||||
if ssl_strict == 'ask':
|
abort_on = self._get_abort_signals(reply)
|
||||||
err_template = jinja2.Template("""
|
ignore = shared.ignore_certificate_errors(reply.url(), errors,
|
||||||
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
|
abort_on=abort_on)
|
||||||
<ul>
|
if ignore:
|
||||||
{% 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()
|
reply.ignoreSslErrors()
|
||||||
err_dict = self._accepted_ssl_errors
|
err_dict = self._accepted_ssl_errors
|
||||||
else:
|
else:
|
||||||
err_dict = self._rejected_ssl_errors
|
err_dict = self._rejected_ssl_errors
|
||||||
if host_tpl is not None:
|
if host_tpl is not None:
|
||||||
err_dict[host_tpl] += errors
|
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()))
|
|
||||||
reply.ignoreSslErrors()
|
|
||||||
self._accepted_ssl_errors[host_tpl] += errors
|
|
||||||
|
|
||||||
def clear_all_ssl_errors(self):
|
def clear_all_ssl_errors(self):
|
||||||
"""Clear all remembered SSL errors."""
|
"""Clear all remembered SSL errors."""
|
||||||
@ -343,19 +288,13 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
except netrc.NetrcParseError:
|
except netrc.NetrcParseError:
|
||||||
log.misc.exception("Error when parsing the netrc file")
|
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:
|
if user is not None:
|
||||||
authenticator.setUser(user)
|
authenticator.setUser(user)
|
||||||
authenticator.setPassword(password)
|
authenticator.setPassword(password)
|
||||||
|
else:
|
||||||
|
abort_on = self._get_abort_signals(reply)
|
||||||
|
shared.authentication_required(reply.url(), authenticator,
|
||||||
|
abort_on=abort_on)
|
||||||
|
|
||||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
|
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
|
||||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||||
@ -369,9 +308,10 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
msg = '<b>{}</b> says:<br/>{}'.format(
|
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||||
html.escape(proxy.hostName()),
|
html.escape(proxy.hostName()),
|
||||||
html.escape(authenticator.realm()))
|
html.escape(authenticator.realm()))
|
||||||
answer = self._ask(
|
abort_on = self._get_abort_signals()
|
||||||
"Proxy authentication required", msg,
|
answer = message.ask(
|
||||||
mode=usertypes.PromptMode.user_pwd)
|
title="Proxy authentication required", text=msg,
|
||||||
|
mode=usertypes.PromptMode.user_pwd, abort_on=abort_on)
|
||||||
if answer is not None:
|
if answer is not None:
|
||||||
authenticator.setUser(answer.user)
|
authenticator.setUser(answer.user)
|
||||||
authenticator.setPassword(answer.password)
|
authenticator.setPassword(answer.password)
|
||||||
|
@ -30,7 +30,7 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
|||||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||||
|
|
||||||
from qutebrowser.config import config
|
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 import http
|
||||||
from qutebrowser.browser.webkit.network import networkmanager
|
from qutebrowser.browser.webkit.network import networkmanager
|
||||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
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.unsupportedContent.connect(self.on_unsupported_content)
|
||||||
self.loadStarted.connect(self.on_load_started)
|
self.loadStarted.connect(self.on_load_started)
|
||||||
self.featurePermissionRequested.connect(
|
self.featurePermissionRequested.connect(
|
||||||
self.on_feature_permission_requested)
|
self._on_feature_permission_requested)
|
||||||
self.saveFrameStateRequested.connect(
|
self.saveFrameStateRequested.connect(
|
||||||
self.on_save_frame_state_requested)
|
self.on_save_frame_state_requested)
|
||||||
self.restoreFrameStateRequested.connect(
|
self.restoreFrameStateRequested.connect(
|
||||||
@ -94,23 +94,16 @@ class BrowserPage(QWebPage):
|
|||||||
# of a bug in PyQt.
|
# of a bug in PyQt.
|
||||||
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
|
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
|
||||||
|
|
||||||
def javaScriptPrompt(self, _frame, js_msg, default):
|
def javaScriptPrompt(self, frame, js_msg, default):
|
||||||
"""Override javaScriptPrompt to use the statusbar."""
|
"""Override javaScriptPrompt to use qutebrowser prompts."""
|
||||||
if (self._is_shutting_down or
|
if self._is_shutting_down:
|
||||||
config.get('content', 'ignore-javascript-prompt')):
|
|
||||||
return (False, "")
|
return (False, "")
|
||||||
msg = '<b>{}</b> asks:<br/>{}'.format(
|
try:
|
||||||
html.escape(self.mainFrame().url().toDisplayString()),
|
return shared.javascript_prompt(frame.url(), js_msg, default,
|
||||||
html.escape(js_msg))
|
|
||||||
answer = message.ask('Javascript prompt', msg,
|
|
||||||
mode=usertypes.PromptMode.text,
|
|
||||||
default=default,
|
|
||||||
abort_on=[self.loadStarted,
|
abort_on=[self.loadStarted,
|
||||||
self.shutting_down])
|
self.shutting_down])
|
||||||
if answer is None:
|
except shared.CallSuper:
|
||||||
return (False, "")
|
return super().javaScriptPrompt(frame, js_msg, default)
|
||||||
else:
|
|
||||||
return (True, answer)
|
|
||||||
|
|
||||||
def _handle_errorpage(self, info, errpage):
|
def _handle_errorpage(self, info, errpage):
|
||||||
"""Display an error page if needed.
|
"""Display an error page if needed.
|
||||||
@ -296,7 +289,7 @@ class BrowserPage(QWebPage):
|
|||||||
self.error_occurred = False
|
self.error_occurred = False
|
||||||
|
|
||||||
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
|
@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."""
|
"""Ask the user for approval for geolocation/notifications."""
|
||||||
if not isinstance(frame, QWebFrame): # pragma: no cover
|
if not isinstance(frame, QWebFrame): # pragma: no cover
|
||||||
# This makes no sense whatsoever, but someone reported this being
|
# This makes no sense whatsoever, but someone reported this being
|
||||||
@ -309,20 +302,10 @@ class BrowserPage(QWebPage):
|
|||||||
QWebPage.Notifications: ('content', 'notifications'),
|
QWebPage.Notifications: ('content', 'notifications'),
|
||||||
QWebPage.Geolocation: ('content', 'geolocation'),
|
QWebPage.Geolocation: ('content', 'geolocation'),
|
||||||
}
|
}
|
||||||
config_val = config.get(*options[feature])
|
messages = {
|
||||||
if config_val == 'ask':
|
|
||||||
msgs = {
|
|
||||||
QWebPage.Notifications: 'show notifications',
|
QWebPage.Notifications: 'show notifications',
|
||||||
QWebPage.Geolocation: 'access your location',
|
QWebPage.Geolocation: 'access your location',
|
||||||
}
|
}
|
||||||
|
|
||||||
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])
|
|
||||||
|
|
||||||
yes_action = functools.partial(
|
yes_action = functools.partial(
|
||||||
self.setFeaturePermission, frame, feature,
|
self.setFeaturePermission, frame, feature,
|
||||||
QWebPage.PermissionGrantedByUser)
|
QWebPage.PermissionGrantedByUser)
|
||||||
@ -330,24 +313,18 @@ class BrowserPage(QWebPage):
|
|||||||
self.setFeaturePermission, frame, feature,
|
self.setFeaturePermission, frame, feature,
|
||||||
QWebPage.PermissionDeniedByUser)
|
QWebPage.PermissionDeniedByUser)
|
||||||
|
|
||||||
question = message.confirm_async(yes_action=yes_action,
|
question = shared.feature_permission(
|
||||||
no_action=no_action,
|
url=frame.url(),
|
||||||
cancel_action=no_action,
|
option=options[feature], msg=messages[feature],
|
||||||
abort_on=[self.shutting_down,
|
yes_action=yes_action, no_action=no_action,
|
||||||
self.loadStarted],
|
abort_on=[self.shutting_down, self.loadStarted])
|
||||||
title='Permission request',
|
|
||||||
text=text)
|
|
||||||
self.featurePermissionRequestCanceled.connect(
|
|
||||||
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,
|
if question is not None:
|
||||||
|
self.featurePermissionRequestCanceled.connect(
|
||||||
|
functools.partial(self._on_feature_permission_cancelled,
|
||||||
|
question, frame, feature))
|
||||||
|
|
||||||
|
def _on_feature_permission_cancelled(self, question, frame, feature,
|
||||||
cancelled_frame, cancelled_feature):
|
cancelled_frame, cancelled_feature):
|
||||||
"""Slot invoked when a feature permission request was cancelled.
|
"""Slot invoked when a feature permission request was cancelled.
|
||||||
|
|
||||||
@ -441,37 +418,26 @@ class BrowserPage(QWebPage):
|
|||||||
return handler(opt, out)
|
return handler(opt, out)
|
||||||
|
|
||||||
def javaScriptAlert(self, frame, js_msg):
|
def javaScriptAlert(self, frame, js_msg):
|
||||||
"""Override javaScriptAlert to use the statusbar."""
|
"""Override javaScriptAlert to use qutebrowser prompts."""
|
||||||
log.js.debug("alert: {}".format(js_msg))
|
if self._is_shutting_down:
|
||||||
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')):
|
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
msg = 'From <b>{}</b>:<br/>{}'.format(
|
shared.javascript_alert(frame.url(), js_msg,
|
||||||
html.escape(self.mainFrame().url().toDisplayString()),
|
abort_on=[self.loadStarted,
|
||||||
html.escape(js_msg))
|
self.shutting_down])
|
||||||
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
|
except shared.CallSuper:
|
||||||
abort_on=[self.loadStarted, self.shutting_down])
|
super().javaScriptAlert(frame, js_msg)
|
||||||
|
|
||||||
def javaScriptConfirm(self, frame, js_msg):
|
def javaScriptConfirm(self, frame, js_msg):
|
||||||
"""Override javaScriptConfirm to use the statusbar."""
|
"""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:
|
if self._is_shutting_down:
|
||||||
return False
|
return False
|
||||||
|
try:
|
||||||
msg = 'From <b>{}</b>:<br/>{}'.format(
|
return shared.javascript_confirm(frame.url(), js_msg,
|
||||||
html.escape(self.mainFrame().url().toDisplayString()),
|
abort_on=[self.loadStarted,
|
||||||
html.escape(js_msg))
|
self.shutting_down])
|
||||||
ans = message.ask('Javascript confirm', msg,
|
except shared.CallSuper:
|
||||||
mode=usertypes.PromptMode.yesno,
|
return super().javaScriptConfirm(frame, js_msg)
|
||||||
abort_on=[self.loadStarted, self.shutting_down])
|
|
||||||
return bool(ans)
|
|
||||||
|
|
||||||
def javaScriptConsoleMessage(self, msg, line, source):
|
def javaScriptConsoleMessage(self, msg, line, source):
|
||||||
"""Override javaScriptConsoleMessage to use debug log."""
|
"""Override javaScriptConsoleMessage to use debug log."""
|
||||||
|
@ -430,8 +430,7 @@ def data(readonly=False):
|
|||||||
"Whether to send DNS requests over the configured proxy."),
|
"Whether to send DNS requests over the configured proxy."),
|
||||||
|
|
||||||
('ssl-strict',
|
('ssl-strict',
|
||||||
SettingValue(typ.BoolAsk(), 'ask',
|
SettingValue(typ.BoolAsk(), 'ask'),
|
||||||
backends=[usertypes.Backend.QtWebKit]),
|
|
||||||
"Whether to validate SSL handshakes."),
|
"Whether to validate SSL handshakes."),
|
||||||
|
|
||||||
('dns-prefetch',
|
('dns-prefetch',
|
||||||
@ -822,6 +821,11 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.BoolAsk(), 'ask'),
|
SettingValue(typ.BoolAsk(), 'ask'),
|
||||||
"Allow websites to show notifications."),
|
"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',
|
('javascript-can-open-windows-automatically',
|
||||||
SettingValue(typ.Bool(), 'false'),
|
SettingValue(typ.Bool(), 'false'),
|
||||||
"Whether JavaScript programs can open new windows without user "
|
"Whether JavaScript programs can open new windows without user "
|
||||||
|
@ -402,3 +402,20 @@ class Timer(QTimer):
|
|||||||
super().start(msec)
|
super().start(msec)
|
||||||
else:
|
else:
|
||||||
super().start()
|
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',
|
('tests/unit/browser/test_signalfilter.py',
|
||||||
'qutebrowser/browser/signalfilter.py'),
|
'qutebrowser/browser/signalfilter.py'),
|
||||||
('tests/unit/browser/test_shared.py',
|
(None,
|
||||||
'qutebrowser/browser/shared.py'),
|
'qutebrowser/browser/webkit/certificateerror.py'),
|
||||||
# ('tests/unit/browser/test_tab.py',
|
# ('tests/unit/browser/test_tab.py',
|
||||||
# 'qutebrowser/browser/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.config.' + attr
|
||||||
|
|
||||||
yield 'scripts.dev.pylint_checkers.modeline.process_module'
|
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',
|
for attr in ['_get_default_metavar_for_optional',
|
||||||
'_get_default_metavar_for_positional', '_metavar_formatter']:
|
'_get_default_metavar_for_positional', '_metavar_formatter']:
|
||||||
|
@ -27,6 +27,7 @@ import warnings
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import hypothesis
|
import hypothesis
|
||||||
|
from PyQt5.QtCore import PYQT_VERSION
|
||||||
|
|
||||||
pytest.register_assert_rewrite('helpers')
|
pytest.register_assert_rewrite('helpers')
|
||||||
|
|
||||||
@ -120,8 +121,14 @@ def pytest_collection_modifyitems(config, items):
|
|||||||
_apply_platform_markers(item)
|
_apply_platform_markers(item)
|
||||||
if item.get_marker('xfail_norun'):
|
if item.get_marker('xfail_norun'):
|
||||||
item.add_marker(pytest.mark.xfail(run=False))
|
item.add_marker(pytest.mark.xfail(run=False))
|
||||||
if item.get_marker('flaky_once'):
|
if item.get_marker('js_prompt'):
|
||||||
item.add_marker(pytest.mark.flaky())
|
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:
|
if deselected:
|
||||||
deselected_items.append(item)
|
deselected_items.append(item)
|
||||||
|
@ -352,6 +352,15 @@ def javascript_message_when(quteproc, message):
|
|||||||
quteproc.wait_for_js(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
|
## Then
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,9 +75,8 @@ Feature: Downloading things from a website.
|
|||||||
And I run :leave-mode
|
And I run :leave-mode
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
@qtwebengine_todo: ssl-strict is not implemented yet
|
|
||||||
Scenario: Downloading with SSL errors (issue 1413)
|
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 set network -> ssl-strict to ask
|
||||||
And I download an SSL page
|
And I download an SSL page
|
||||||
And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
|
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:
|
Then the history file should contain:
|
||||||
http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli
|
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
|
Scenario: History with an error
|
||||||
When I run :open file:///does/not/exist
|
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
|
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>"
|
And I press the key "<Ctrl-C>"
|
||||||
Then no crash should happen
|
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)
|
Scenario: Focusing download widget via Tab (original issue)
|
||||||
When I open data/prompt/jsprompt.html
|
When I open data/prompt/jsprompt.html
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
|
@ -40,7 +40,7 @@ Feature: Prompts
|
|||||||
And I run :leave-mode
|
And I run :leave-mode
|
||||||
Then the javascript message "confirm reply: false" should be logged
|
Then the javascript message "confirm reply: false" should be logged
|
||||||
|
|
||||||
@pyqt>=5.3.1
|
@js_prompt
|
||||||
Scenario: Javascript prompt
|
Scenario: Javascript prompt
|
||||||
When I open data/prompt/jsprompt.html
|
When I open data/prompt/jsprompt.html
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
@ -49,7 +49,7 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
Then the javascript message "Prompt reply: prompt test" should be logged
|
Then the javascript message "Prompt reply: prompt test" should be logged
|
||||||
|
|
||||||
@pyqt>=5.3.1
|
@js_prompt
|
||||||
Scenario: Javascript prompt with default
|
Scenario: Javascript prompt with default
|
||||||
When I open data/prompt/jsprompt.html
|
When I open data/prompt/jsprompt.html
|
||||||
And I run :click-element id button-default
|
And I run :click-element id button-default
|
||||||
@ -57,7 +57,7 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
Then the javascript message "Prompt reply: default" should be logged
|
Then the javascript message "Prompt reply: default" should be logged
|
||||||
|
|
||||||
@pyqt>=5.3.1
|
@js_prompt
|
||||||
Scenario: Rejected javascript prompt
|
Scenario: Rejected javascript prompt
|
||||||
When I open data/prompt/jsprompt.html
|
When I open data/prompt/jsprompt.html
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
@ -68,6 +68,7 @@ Feature: Prompts
|
|||||||
|
|
||||||
# Multiple prompts
|
# Multiple prompts
|
||||||
|
|
||||||
|
@qtwebengine_skip: QtWebEngine refuses to load anything with a JS question
|
||||||
Scenario: Blocking question interrupted by blocking one
|
Scenario: Blocking question interrupted by blocking one
|
||||||
When I set content -> ignore-javascript-alert to false
|
When I set content -> ignore-javascript-alert to false
|
||||||
And I open data/prompt/jsalert.html
|
And I open data/prompt/jsalert.html
|
||||||
@ -83,6 +84,7 @@ Feature: Prompts
|
|||||||
Then the javascript message "confirm reply: true" should be logged
|
Then the javascript message "confirm reply: true" should be logged
|
||||||
And the javascript message "Alert done" 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
|
Scenario: Blocking question interrupted by async one
|
||||||
When I set content -> ignore-javascript-alert to false
|
When I set content -> ignore-javascript-alert to false
|
||||||
And I set content -> notifications to ask
|
And I set content -> notifications to ask
|
||||||
@ -99,6 +101,7 @@ Feature: Prompts
|
|||||||
Then the javascript message "Alert done" should be logged
|
Then the javascript message "Alert done" should be logged
|
||||||
And the javascript message "notification permission granted" 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
|
Scenario: Async question interrupted by async one
|
||||||
When I set content -> notifications to ask
|
When I set content -> notifications to ask
|
||||||
And I open data/prompt/notifications.html in a new tab
|
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
|
Then the javascript message "notification permission granted" should be logged
|
||||||
And "Added quickmark test for *" 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
|
Scenario: Async question interrupted by blocking one
|
||||||
When I set content -> notifications to ask
|
When I set content -> notifications to ask
|
||||||
And I set content -> ignore-javascript-alert to false
|
And I set content -> ignore-javascript-alert to false
|
||||||
@ -131,7 +135,7 @@ Feature: Prompts
|
|||||||
|
|
||||||
# Shift-Insert with prompt (issue 1299)
|
# Shift-Insert with prompt (issue 1299)
|
||||||
|
|
||||||
@pyqt>=5.3.1
|
@js_prompt
|
||||||
Scenario: Pasting via shift-insert in prompt mode
|
Scenario: Pasting via shift-insert in prompt mode
|
||||||
When selection is supported
|
When selection is supported
|
||||||
And I put "insert test" into the primary selection
|
And I put "insert test" into the primary selection
|
||||||
@ -142,7 +146,7 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
Then the javascript message "Prompt reply: insert test" should be logged
|
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
|
Scenario: Pasting via shift-insert without it being supported
|
||||||
When selection is not supported
|
When selection is not supported
|
||||||
And I put "insert test" into the primary selection
|
And I put "insert test" into the primary selection
|
||||||
@ -153,7 +157,7 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
Then the javascript message "Prompt reply: " should be logged
|
Then the javascript message "Prompt reply: " should be logged
|
||||||
|
|
||||||
@pyqt>=5.3.1
|
@js_prompt
|
||||||
Scenario: Using content -> ignore-javascript-prompt
|
Scenario: Using content -> ignore-javascript-prompt
|
||||||
When I set content -> ignore-javascript-prompt to true
|
When I set content -> ignore-javascript-prompt to true
|
||||||
And I open data/prompt/jsprompt.html
|
And I open data/prompt/jsprompt.html
|
||||||
@ -163,22 +167,21 @@ Feature: Prompts
|
|||||||
# SSL
|
# SSL
|
||||||
|
|
||||||
Scenario: SSL error with ssl-strict = false
|
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 set network -> ssl-strict to false
|
||||||
And I load an SSL page
|
And I load an SSL page
|
||||||
And I wait until the SSL page finished loading
|
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!"
|
And the page should contain the plaintext "Hello World via SSL!"
|
||||||
|
|
||||||
Scenario: SSL error with ssl-strict = true
|
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 set network -> ssl-strict to true
|
||||||
And I load an SSL page
|
And I load an SSL page
|
||||||
Then "Error while loading *: SSL handshake failed" should be logged
|
Then a SSL error page should be shown
|
||||||
And the page should contain the plaintext "Unable to load page"
|
|
||||||
|
|
||||||
Scenario: SSL error with ssl-strict = ask -> yes
|
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 set network -> ssl-strict to ask
|
||||||
And I load an SSL page
|
And I load an SSL page
|
||||||
And I wait for a prompt
|
And I wait for a prompt
|
||||||
@ -187,13 +190,12 @@ Feature: Prompts
|
|||||||
Then the page should contain the plaintext "Hello World via SSL!"
|
Then the page should contain the plaintext "Hello World via SSL!"
|
||||||
|
|
||||||
Scenario: SSL error with ssl-strict = ask -> no
|
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 set network -> ssl-strict to ask
|
||||||
And I load an SSL page
|
And I load an SSL page
|
||||||
And I wait for a prompt
|
And I wait for a prompt
|
||||||
And I run :prompt-accept no
|
And I run :prompt-accept no
|
||||||
Then "Error while loading *: SSL handshake failed" should be logged
|
Then a SSL error page should be shown
|
||||||
And the page should contain the plaintext "Unable to load page"
|
|
||||||
|
|
||||||
# Geolocation
|
# Geolocation
|
||||||
|
|
||||||
@ -237,18 +239,21 @@ Feature: Prompts
|
|||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
|
|
||||||
|
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||||
Scenario: Always rejecting notifications
|
Scenario: Always rejecting notifications
|
||||||
When I set content -> notifications to false
|
When I set content -> notifications to false
|
||||||
And I open data/prompt/notifications.html in a new tab
|
And I open data/prompt/notifications.html in a new tab
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
Then the javascript message "notification permission denied" should be logged
|
Then the javascript message "notification permission denied" should be logged
|
||||||
|
|
||||||
|
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||||
Scenario: Always accepting notifications
|
Scenario: Always accepting notifications
|
||||||
When I set content -> notifications to true
|
When I set content -> notifications to true
|
||||||
And I open data/prompt/notifications.html in a new tab
|
And I open data/prompt/notifications.html in a new tab
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
Then the javascript message "notification permission granted" should be logged
|
Then the javascript message "notification permission granted" should be logged
|
||||||
|
|
||||||
|
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||||
Scenario: notifications with ask -> false
|
Scenario: notifications with ask -> false
|
||||||
When I set content -> notifications to ask
|
When I set content -> notifications to ask
|
||||||
And I open data/prompt/notifications.html in a new tab
|
And I open data/prompt/notifications.html in a new tab
|
||||||
@ -257,6 +262,7 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept no
|
And I run :prompt-accept no
|
||||||
Then the javascript message "notification permission denied" should be logged
|
Then the javascript message "notification permission denied" should be logged
|
||||||
|
|
||||||
|
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||||
Scenario: notifications with ask -> true
|
Scenario: notifications with ask -> true
|
||||||
When I set content -> notifications to ask
|
When I set content -> notifications to ask
|
||||||
And I open data/prompt/notifications.html in a new tab
|
And I open data/prompt/notifications.html in a new tab
|
||||||
@ -275,6 +281,7 @@ Feature: Prompts
|
|||||||
And I run :leave-mode
|
And I run :leave-mode
|
||||||
Then the javascript message "notification permission aborted" should be logged
|
Then the javascript message "notification permission aborted" should be logged
|
||||||
|
|
||||||
|
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||||
Scenario: answering notification after closing tab
|
Scenario: answering notification after closing tab
|
||||||
When I set content -> notifications to ask
|
When I set content -> notifications to ask
|
||||||
And I open data/prompt/notifications.html in a new tab
|
And I open data/prompt/notifications.html in a new tab
|
||||||
@ -287,55 +294,55 @@ Feature: Prompts
|
|||||||
# Page authentication
|
# Page authentication
|
||||||
|
|
||||||
Scenario: Successful webpage authentification
|
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 wait for a prompt
|
||||||
And I press the keys "user"
|
And I press the keys "user1"
|
||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
And I press the keys "password"
|
And I press the keys "password1"
|
||||||
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/user1/password1 is loaded
|
||||||
Then the json on the page should be:
|
Then the json on the page should be:
|
||||||
{
|
{
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"user": "user"
|
"user": "user1"
|
||||||
}
|
}
|
||||||
|
|
||||||
Scenario: Authentication with :prompt-accept value
|
Scenario: Authentication with :prompt-accept value
|
||||||
When I open about:blank in a new tab
|
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 wait for a prompt
|
||||||
And I run :prompt-accept user:password
|
And I run :prompt-accept user2:password2
|
||||||
And I wait until basic-auth/user/password is loaded
|
And I wait until basic-auth/user2/password2 is loaded
|
||||||
Then the json on the page should be:
|
Then the json on the page should be:
|
||||||
{
|
{
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"user": "user"
|
"user": "user2"
|
||||||
}
|
}
|
||||||
|
|
||||||
Scenario: Authentication with invalid :prompt-accept value
|
Scenario: Authentication with invalid :prompt-accept value
|
||||||
When I open about:blank in a new tab
|
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 wait for a prompt
|
||||||
And I run :prompt-accept foo
|
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
|
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
|
Scenario: Tabbing between username and password
|
||||||
When I open about:blank in a new tab
|
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 wait for a prompt
|
||||||
And I press the keys "us"
|
And I press the keys "us"
|
||||||
And I run :prompt-item-focus next
|
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 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 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:
|
Then the json on the page should be:
|
||||||
{
|
{
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"user": "user"
|
"user": "user4"
|
||||||
}
|
}
|
||||||
|
|
||||||
# :prompt-accept with value argument
|
# :prompt-accept with value argument
|
||||||
@ -350,7 +357,7 @@ Feature: Prompts
|
|||||||
Then the javascript message "Alert done" should be logged
|
Then the javascript message "Alert done" should be logged
|
||||||
And the error "No value is permitted with alert prompts!" should be shown
|
And the error "No value is permitted with alert prompts!" should be shown
|
||||||
|
|
||||||
@pyqt>=5.3.1
|
@js_prompt
|
||||||
Scenario: Javascript prompt with value
|
Scenario: Javascript prompt with value
|
||||||
When I set content -> ignore-javascript-prompt to false
|
When I set content -> ignore-javascript-prompt to false
|
||||||
And I open data/prompt/jsprompt.html
|
And I open data/prompt/jsprompt.html
|
||||||
@ -396,6 +403,7 @@ Feature: Prompts
|
|||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
|
||||||
|
@qtwebengine_skip
|
||||||
Scenario: Shutting down with a question
|
Scenario: Shutting down with a question
|
||||||
When I open data/prompt/jsconfirm.html
|
When I open data/prompt/jsconfirm.html
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
@ -429,32 +437,34 @@ Feature: Prompts
|
|||||||
Then "Added quickmark prompt-in-command-mode for *" should be logged
|
Then "Added quickmark prompt-in-command-mode for *" should be logged
|
||||||
|
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/1093
|
# 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
|
Scenario: Keyboard focus with multiple auth prompts
|
||||||
When I open basic-auth/user1/password1 without waiting
|
When I open basic-auth/user5/password5 without waiting
|
||||||
And I open basic-auth/user2/password2 in a new tab 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
|
||||||
And I wait for a prompt
|
And I wait for a prompt
|
||||||
# Second prompt (showed first)
|
# 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 key "<Enter>"
|
||||||
And I press the keys "password2"
|
And I press the keys "password6"
|
||||||
And I press the key "<Enter>"
|
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
|
# First prompt
|
||||||
And I press the keys "user1"
|
And I press the keys "user5"
|
||||||
And I press the key "<Enter>"
|
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 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
|
# We're on the second page
|
||||||
Then the json on the page should be:
|
Then the json on the page should be:
|
||||||
{
|
{
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"user": "user2"
|
"user": "user6"
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
|
# https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
|
||||||
# https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544
|
# 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
|
Scenario: Interrupting SSL prompt during a notification prompt
|
||||||
When I set content -> notifications to ask
|
When I set content -> notifications to ask
|
||||||
And I set network -> ssl-strict 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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import pytest
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
bdd.scenarios('prompts.feature')
|
bdd.scenarios('prompts.feature')
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.qtwebengine_todo("Prompts are not implemented")
|
|
||||||
|
|
||||||
|
|
||||||
@bdd.when("I load an SSL page")
|
@bdd.when("I load an SSL page")
|
||||||
def load_ssl_page(quteproc, ssl_server):
|
def load_ssl_page(quteproc, ssl_server):
|
||||||
# We don't wait here as we can get an SSL question.
|
# 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):
|
def no_prompt_shown(quteproc):
|
||||||
quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: '
|
quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: '
|
||||||
'question asked)')
|
'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
|
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):
|
class LogLine(testprocess.Line):
|
||||||
|
|
||||||
"""A parsed line from the qutebrowser log output.
|
"""A parsed line from the qutebrowser log output.
|
||||||
@ -222,14 +235,8 @@ class QuteProc(testprocess.Process):
|
|||||||
except testprocess.InvalidLine:
|
except testprocess.InvalidLine:
|
||||||
if not line.strip():
|
if not line.strip():
|
||||||
return None
|
return None
|
||||||
elif 'Running without the SUID sandbox!' in line:
|
elif (is_ignored_qt_message(line) or
|
||||||
# QtWebEngine error
|
is_ignored_lowlevel_message(line)):
|
||||||
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):
|
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
@ -81,7 +81,7 @@ class Request(testprocess.Line):
|
|||||||
http.client.FOUND]
|
http.client.FOUND]
|
||||||
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
|
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
|
||||||
http.client.FOUND]
|
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)
|
key = '/basic-auth/user{}/password{}'.format(suffix, suffix)
|
||||||
path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK]
|
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