This commit is contained in:
thuck 2016-11-14 19:01:49 +01:00
commit abe3c19646
32 changed files with 827 additions and 364 deletions

View File

@ -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.

View File

@ -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 =

View File

@ -27,8 +27,7 @@ from PyQt5.QtWidgets import QWidget, QApplication
from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.utils import (utils, objreg, usertypes, message, log, qtutils,
urlutils)
from qutebrowser.utils import utils, objreg, usertypes, log, qtutils
from qutebrowser.misc import miscwidgets
from qutebrowser.browser import mouse, hints
@ -84,7 +83,6 @@ class TabData:
load.
inspector: The QWebInspector used for this webview.
viewing_source: Set if we're currently showing a source view.
open_target: How the next clicked link should be opened.
override_target: Override for open_target for fake clicks (like hints).
pinned: Flag to pin the tab
"""
@ -93,16 +91,9 @@ class TabData:
self.keep_icon = False
self.viewing_source = False
self.inspector = None
self.open_target = usertypes.ClickTarget.normal
self.override_target = None
self.pinned = False
def combined_target(self):
if self.override_target is not None:
return self.override_target
else:
return self.open_target
class AbstractPrinting:
@ -612,44 +603,6 @@ class AbstractTab(QWidget):
evt.posted = True
QApplication.postEvent(recipient, evt)
@pyqtSlot(QUrl)
def _on_link_clicked(self, url):
log.webview.debug("link clicked: url {}, override target {}, "
"open_target {}".format(
url.toDisplayString(),
self.data.override_target,
self.data.open_target))
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(msg)
self.data.open_target = usertypes.ClickTarget.normal
return False
target = self.data.combined_target()
if target == usertypes.ClickTarget.normal:
return
elif target == usertypes.ClickTarget.tab:
win_id = self.win_id
bg_tab = False
elif target == usertypes.ClickTarget.tab_bg:
win_id = self.win_id
bg_tab = True
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
bg_tab = False
else:
raise ValueError("Invalid ClickTarget {}".format(target))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(url, background=bg_tab)
self.data.open_target = usertypes.ClickTarget.normal
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
"""Update title when URL has changed and no title is available."""

View File

@ -2044,7 +2044,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()

View File

@ -96,7 +96,6 @@ class MouseEventFilter(QObject):
return True
self._ignore_wheel_event = True
self._mousepress_opentarget(e)
self._tab.elements.find_at_pos(e.pos(), self._mousepress_insertmode_cb)
return False
@ -197,27 +196,6 @@ class MouseEventFilter(QObject):
else:
message.error("At end of history.")
def _mousepress_opentarget(self, e):
"""Set the open target when something was clicked.
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs')
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
self._tab.data.open_target = target
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
target))
else:
self._tab.data.open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
def eventFilter(self, obj, event):
"""Filter events going to a QWeb(Engine)View."""
evtype = event.type()

View File

@ -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, objreg
class CallSuper(Exception):
"""Raised when the caller should call the superclass instead."""
def custom_headers():
@ -39,3 +49,176 @@ 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
def get_tab(win_id, target):
"""Get a tab widget for the given usertypes.ClickTarget.
Args:
win_id: The window ID to open new tabs in
target: A usertypes.ClickTarget
"""
if target == usertypes.ClickTarget.tab:
win_id = win_id
bg_tab = False
elif target == usertypes.ClickTarget.tab_bg:
win_id = win_id
bg_tab = True
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
bg_tab = False
else:
raise ValueError("Invalid ClickTarget {}".format(target))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
return tabbed_browser.tabopen(url=None, background=bg_tab)

View File

@ -365,10 +365,9 @@ class AbstractWebElement(collections.abc.MutableMapping):
self._tab.send_event(evt)
def after_click():
"""Move cursor to end and reset override_target after clicking."""
"""Move cursor to end after clicking."""
if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document()
self._tab.data.override_target = None
QTimer.singleShot(0, after_click)
def hover(self):

View 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()

View File

@ -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()
@ -608,7 +616,7 @@ class WebEngineTab(browsertab.AbstractTab):
view.urlChanged.connect(self._on_url_changed)
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:

View File

@ -20,14 +20,18 @@
"""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, qtutils, jinja, urlutils,
message)
class WebEngineView(QWebEngineView):
@ -37,7 +41,11 @@ class WebEngineView(QWebEngineView):
def __init__(self, tabdata, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self.setPage(WebEnginePage(tabdata, parent=self))
self._tabdata = tabdata
self.setPage(WebEnginePage(parent=self))
def shutdown(self):
self.page().shutdown()
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.
@ -65,24 +73,41 @@ class WebEngineView(QWebEngineView):
The new QWebEngineView object.
"""
debug_type = debug.qenum_key(QWebEnginePage, wintype)
log.webview.debug("createWindow with type {}".format(debug_type))
background_tabs = config.get('tabs', 'background-tabs')
override_target = self._tabdata.override_target
background = False
if wintype in [QWebEnginePage.WebBrowserWindow,
QWebEnginePage.WebDialog]:
log.webview.debug("createWindow with type {}, background_tabs "
"{}, override_target {}".format(
debug_type, background_tabs, override_target))
if override_target is not None:
target = override_target
self._tabdata.override_target = None
elif wintype == QWebEnginePage.WebBrowserWindow:
log.webview.debug("createWindow with WebBrowserWindow - when does "
"this happen?!")
target = usertypes.ClickTarget.window
elif wintype == QWebEnginePage.WebDialog:
log.webview.warning("{} requested, but we don't support "
"that!".format(debug_type))
target = usertypes.ClickTarget.tab
elif wintype == QWebEnginePage.WebBrowserTab:
pass
# Middle-click / Ctrl-Click with Shift
if background_tabs:
target = usertypes.ClickTarget.tab
else:
target = usertypes.ClickTarget.tab_bg
elif (hasattr(QWebEnginePage, 'WebBrowserBackgroundTab') and
wintype == QWebEnginePage.WebBrowserBackgroundTab):
background = True
# Middle-click / Ctrl-Click
if background_tabs:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
else:
raise ValueError("Invalid wintype {}".format(debug_type))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tab = tabbed_browser.tabopen(background=background)
tab = shared.get_tab(self._win_id, target)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-54419
vercheck = qtutils.version_check
@ -99,21 +124,152 @@ 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
link_clicked: Emitted when a link was clicked on a page.
certificate_error: Emitted on certificate errors.
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):
def __init__(self, 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."""
@ -137,24 +293,16 @@ class WebEnginePage(QWebEnginePage):
is_main_frame: bool):
"""Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound
to linkClicked won't work correctly, because when in a frameset, we
have no idea in which frame the link should be opened.
Checks if it should open it in a tab (middle-click or control) or not,
and then conditionally opens the URL. Opening it in a new tab/window
is handled in the slot connected to link_clicked.
This only show an error on invalid links - everything else is handled
in createWindow.
"""
target = self._tabdata.combined_target()
log.webview.debug("navigation request: url {}, type {}, "
"target {}, is_main_frame {}".format(
url.toDisplayString(),
debug.qenum_key(QWebEnginePage, typ),
target, is_main_frame))
if typ != QWebEnginePage.NavigationTypeLinkClicked:
return True
self.link_clicked.emit(url)
return url.isValid() and target == usertypes.ClickTarget.normal
log.webview.debug("navigation request: url {}, type {}, is_main_frame "
"{}".format(url.toDisplayString(),
debug.qenum_key(QWebEnginePage, typ),
is_main_frame))
if (typ == QWebEnginePage.NavigationTypeLinkClicked and
not url.isValid()):
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(msg)
return False
return True

View 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

View File

@ -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)

View File

@ -329,7 +329,7 @@ class WebKitCaret(browsertab.AbstractCaret):
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
if tab:
self._tab.data.open_target = usertypes.ClickTarget.tab
self._tab.data.override_target = usertypes.ClickTarget.tab
self._tab.run_js_async(
'window.getSelection().anchorNode.parentNode.click()')
else:
@ -714,7 +714,6 @@ class WebKitTab(browsertab.AbstractTab):
page.frameCreated.connect(self._on_frame_created)
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger)
page.link_clicked.connect(self._on_link_clicked)
def _event_target(self):
return self._widget

View File

@ -30,11 +30,11 @@ 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,
objreg, debug)
objreg, debug, urlutils)
class BrowserPage(QWebPage):
@ -54,12 +54,10 @@ class BrowserPage(QWebPage):
shutting_down: Emitted when the page is currently shutting down.
reloading: Emitted before a web page reloads.
arg: The URL which gets reloaded.
link_clicked: Emitted when a link was clicked on a page.
"""
shutting_down = pyqtSignal()
reloading = pyqtSignal(QUrl)
link_clicked = pyqtSignal(QUrl)
def __init__(self, win_id, tab_id, tabdata, parent=None):
super().__init__(parent)
@ -72,6 +70,7 @@ class BrowserPage(QWebPage):
}
self._ignore_load_started = False
self.error_occurred = False
self.open_target = usertypes.ClickTarget.normal
self._networkmanager = networkmanager.NetworkManager(
win_id, tab_id, self)
self.setNetworkAccessManager(self._networkmanager)
@ -82,7 +81,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 +93,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 +288,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 +301,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 +417,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."""
@ -496,16 +461,21 @@ class BrowserPage(QWebPage):
have no idea in which frame the link should be opened.
Checks if it should open it in a tab (middle-click or control) or not,
and then conditionally opens the URL. Opening it in a new tab/window
is handled in the slot connected to link_clicked.
and then conditionally opens the URL here or in another tab/window.
"""
url = request.url()
target = self._tabdata.combined_target()
log.webview.debug("navigation request: url {}, type {}, "
"target {}".format(
"target {} override {}".format(
url.toDisplayString(),
debug.qenum_key(QWebPage, typ),
target))
self.open_target,
self._tabdata.override_target))
if self._tabdata.override_target is not None:
target = self._tabdata.override_target
self._tabdata.override_target = None
else:
target = self.open_target
if typ == QWebPage.NavigationTypeReload:
self.reloading.emit(url)
@ -513,6 +483,16 @@ class BrowserPage(QWebPage):
elif typ != QWebPage.NavigationTypeLinkClicked:
return True
self.link_clicked.emit(url)
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(msg)
self.open_target = usertypes.ClickTarget.normal
return False
return url.isValid() and target == usertypes.ClickTarget.normal
if target == usertypes.ClickTarget.normal:
return True
tab = shared.get_tab(self._win_id, target)
tab.openurl(url)
self.open_target = usertypes.ClickTarget.normal
return False

View File

@ -64,6 +64,7 @@ class WebView(QWebView):
# FIXME:qtwebengine this is only used to set the zoom factor from
# the QWebPage - we should get rid of it somehow (signals?)
self.tab = tab
self._tabdata = tab.data
self.win_id = win_id
self.scroll_pos = (-1, -1)
self._old_scroll_pos = (-1, -1)
@ -280,3 +281,24 @@ class WebView(QWebView):
pass
super().hideEvent(e)
def mousePressEvent(self, e):
"""Set the tabdata ClickTarget on a mousepress.
This is implemented here as we don't need it for QtWebEngine.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs')
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
self.page().open_target = target
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
target))
else:
self.page().open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
super().mousePressEvent(e)

View File

@ -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',
@ -844,6 +843,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 "

View File

@ -370,7 +370,10 @@ class TabbedBrowser(tabwidget.TabWidget):
"""
if url is not None:
qtutils.ensure_valid(url)
log.webview.debug("Creating new tab with URL {}".format(url))
log.webview.debug("Creating new tab with URL {}, background {}, "
"explicit {}, idx {}".format(
url, background, explicit, idx))
if config.get('tabs', 'tabs-are-windows') and self.count() > 0:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
@ -522,13 +525,15 @@ class TabbedBrowser(tabwidget.TabWidget):
# If needed, re-open the tab as a workaround for QTBUG-54419.
# See https://bugreports.qt.io/browse/QTBUG-54419
background = self.currentIndex() != idx
if (tab.backend == usertypes.Backend.QtWebEngine and
tab.needs_qtbug54419_workaround):
log.misc.debug("Doing QTBUG-54419 workaround for {}, "
"url {}".format(tab, url))
self.setUpdatesEnabled(False)
try:
self.tabopen(url)
self.tabopen(url, background=background, idx=idx)
self.close_tab(tab, add_undo=False)
finally:
self.setUpdatesEnabled(True)

View File

@ -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

View File

@ -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'),

View File

@ -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']:

View File

@ -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)

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Two links</title>
</head>
<body>
<a href="/data/hello.txt" id="link">Hello</a>
<a href="/data/hello2.txt" id="link">Hello 2</a>
</body>
</html>

View File

@ -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

View File

@ -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

View File

@ -112,6 +112,27 @@ Feature: Using hints
And I hint with args "links yank-primary" and follow a
Then the clipboard should contain "http://localhost:(port)/data/hello.txt"
Scenario: Rapid hinting
When I open data/hints/rapid.html in a new tab
And I run :tab-only
And I hint with args "all tab-bg --rapid"
And I run :follow-hint a
And I run :follow-hint s
And I run :leave-mode
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
# We should check what the active tab is, but for some reason that makes
# the test flaky
Then the session should look like:
windows:
- tabs:
- history:
- url: http://localhost:*/data/hints/rapid.html
- history:
- url: http://localhost:*/data/hello.txt
- history:
- url: http://localhost:*/data/hello2.txt
Scenario: Using hint --rapid to hit multiple buttons
When I open data/hints/buttons.html
And I hint with args "--rapid"
@ -174,6 +195,7 @@ Feature: Using hints
@qtwebengine_createWindow
Scenario: Opening a link with specific target frame in a new tab
When I open data/hints/iframe_target.html
And I run :tab-only
And I hint with args "links tab" and follow a
And I wait until data/hello.txt is loaded
Then the following tabs should be open:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View 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