Merge branch 'master' of https://github.com/The-Compiler/qutebrowser into pintab
This commit is contained in:
commit
abe3c19646
@ -160,6 +160,7 @@
|
||||
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|
||||
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
||||
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
||||
|<<content-media-capture,media-capture>>|Allow websites to record audio/video.
|
||||
|<<content-javascript-can-open-windows-automatically,javascript-can-open-windows-automatically>>|Whether JavaScript programs can open new windows without user interaction.
|
||||
|<<content-javascript-can-close-windows,javascript-can-close-windows>>|Whether JavaScript programs can close windows.
|
||||
|<<content-javascript-can-access-clipboard,javascript-can-access-clipboard>>|Whether JavaScript programs can read or write to the clipboard.
|
||||
@ -797,8 +798,6 @@ Valid values:
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[network-dns-prefetch]]
|
||||
=== dns-prefetch
|
||||
Whether to try to pre-fetch DNS entries to speed up browsing.
|
||||
@ -1452,6 +1451,20 @@ Valid values:
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
[[content-media-capture]]
|
||||
=== media-capture
|
||||
Allow websites to record audio/video.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[content-javascript-can-open-windows-automatically]]
|
||||
=== javascript-can-open-windows-automatically
|
||||
Whether JavaScript programs can open new windows without user interaction.
|
||||
|
@ -14,13 +14,13 @@ markers =
|
||||
end2end: End to end tests which run qutebrowser as subprocess
|
||||
xfail_norun: xfail the test with out running it
|
||||
ci: Tests which should only run on CI.
|
||||
flaky_once: Try to rerun this test once if it fails
|
||||
qtwebengine_todo: Features still missing with QtWebEngine
|
||||
qtwebengine_skip: Tests not applicable with QtWebEngine
|
||||
qtwebkit_skip: Tests not applicable with QtWebKit
|
||||
qtwebengine_createWindow: Tests using createWindow with QtWebEngine (QTBUG-54419)
|
||||
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
|
||||
qtwebengine_osx_xfail: Tests which fail on OS X with QtWebEngine
|
||||
js_prompt: Tests needing to display a javascript prompt
|
||||
this: Used to mark tests during development
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
43
qutebrowser/browser/webengine/certificateerror.py
Normal file
43
qutebrowser/browser/webengine/certificateerror.py
Normal file
@ -0,0 +1,43 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrapper over a QWebEngineCertificateError."""
|
||||
|
||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.utils import usertypes, utils, debug
|
||||
|
||||
|
||||
class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
|
||||
|
||||
"""A wrapper over a QWebEngineCertificateError."""
|
||||
|
||||
def __str__(self):
|
||||
return self._error.errorDescription()
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, error=debug.qenum_key(QWebEngineCertificateError,
|
||||
self._error.error()),
|
||||
string=str(self))
|
||||
|
||||
def is_overridable(self):
|
||||
return self._error.isOverridable()
|
@ -32,7 +32,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
|
||||
QWebEngineProfile)
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.browser import browsertab, mouse
|
||||
from qutebrowser.browser import browsertab, mouse, shared
|
||||
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||
interceptor, webenginequtescheme,
|
||||
webenginedownloads)
|
||||
@ -544,7 +544,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._widget.page().runJavaScript(code, callback)
|
||||
|
||||
def shutdown(self):
|
||||
log.stub()
|
||||
self.shutting_down.emit()
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
if force:
|
||||
@ -575,7 +576,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._widget.setHtml(html, base_url)
|
||||
|
||||
def clear_ssl_errors(self):
|
||||
log.stub()
|
||||
raise browsertab.UnsupportedOperationError
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_history_trigger(self):
|
||||
@ -596,6 +597,13 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
self.add_history_item.emit(url, requested_url, title)
|
||||
|
||||
@pyqtSlot(QUrl, 'QAuthenticator*')
|
||||
def _on_authentication_required(self, url, authenticator):
|
||||
# FIXME:qtwebengine support .netrc
|
||||
shared.authentication_required(url, authenticator,
|
||||
abort_on=[self.shutting_down,
|
||||
self.load_started])
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
@ -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:
|
||||
|
@ -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
|
||||
|
52
qutebrowser/browser/webkit/certificateerror.py
Normal file
52
qutebrowser/browser/webkit/certificateerror.py
Normal file
@ -0,0 +1,52 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrapper over a QSslError."""
|
||||
|
||||
|
||||
from PyQt5.QtNetwork import QSslError
|
||||
|
||||
from qutebrowser.utils import usertypes, utils, debug
|
||||
|
||||
|
||||
class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
|
||||
|
||||
"""A wrapper over a QSslError."""
|
||||
|
||||
def __str__(self):
|
||||
return self._error.errorString()
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, error=debug.qenum_key(QSslError, self._error.error()),
|
||||
string=str(self))
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
# Qt >= 5.4
|
||||
return hash(self._error)
|
||||
except TypeError:
|
||||
return hash((self._error.certificate().toDer(),
|
||||
self._error.error()))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._error == other._error # pylint: disable=protected-access
|
||||
|
||||
def is_overridable(self):
|
||||
return True
|
@ -24,18 +24,17 @@ import collections
|
||||
import netrc
|
||||
import html
|
||||
|
||||
import jinja2
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||
QUrl, QByteArray)
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
QSslSocket)
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils, debug)
|
||||
urlutils)
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webkit.network import webkitqutescheme, networkreply
|
||||
from qutebrowser.browser.webkit.network import filescheme
|
||||
from qutebrowser.browser.webkit import certificateerror
|
||||
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
|
||||
filescheme)
|
||||
|
||||
|
||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||
@ -112,24 +111,6 @@ def init():
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
|
||||
|
||||
class SslError(QSslError):
|
||||
|
||||
"""A QSslError subclass which provides __hash__ on Qt < 5.4."""
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
# Qt >= 5.4
|
||||
# pylint: disable=not-callable,useless-suppression
|
||||
return super().__hash__()
|
||||
except TypeError:
|
||||
return hash((self.certificate().toDer(), self.error()))
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, error=debug.qenum_key(QSslError, self.error()),
|
||||
string=self.errorString())
|
||||
|
||||
|
||||
class NetworkManager(QNetworkAccessManager):
|
||||
|
||||
"""Our own QNetworkAccessManager.
|
||||
@ -203,32 +184,18 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.setCache(cache)
|
||||
cache.setParent(app)
|
||||
|
||||
def _ask(self, title, text, mode, owner=None, default=None):
|
||||
"""Ask a blocking question in the statusbar.
|
||||
|
||||
Args:
|
||||
title: The title to display to the user.
|
||||
text: The text to display to the user.
|
||||
mode: A PromptMode.
|
||||
owner: An object which will abort the question if destroyed, or
|
||||
None.
|
||||
|
||||
Return:
|
||||
The answer the user gave or None if the prompt was cancelled.
|
||||
"""
|
||||
def _get_abort_signals(self, owner=None):
|
||||
"""Get a list of signals which should abort a question."""
|
||||
abort_on = [self.shutting_down]
|
||||
if owner is not None:
|
||||
abort_on.append(owner.destroyed)
|
||||
|
||||
# This might be a generic network manager, e.g. one belonging to a
|
||||
# DownloadManager. In this case, just skip the webview thing.
|
||||
if self._tab_id is not None:
|
||||
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
abort_on.append(tab.load_started)
|
||||
|
||||
return message.ask(title=title, text=text, mode=mode,
|
||||
abort_on=abort_on, default=default)
|
||||
return abort_on
|
||||
|
||||
def shutdown(self):
|
||||
"""Abort all running requests."""
|
||||
@ -248,11 +215,9 @@ class NetworkManager(QNetworkAccessManager):
|
||||
reply: The QNetworkReply that is encountering the errors.
|
||||
errors: A list of errors.
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
log.webview.debug("SSL errors {!r}, strict {}".format(
|
||||
errors, ssl_strict))
|
||||
|
||||
errors = [certificateerror.CertificateErrorWrapper(e) for e in errors]
|
||||
log.webview.debug("Certificate errors: {!r}".format(
|
||||
' / '.join(str(err) for err in errors)))
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
except ValueError:
|
||||
@ -268,42 +233,22 @@ class NetworkManager(QNetworkAccessManager):
|
||||
log.webview.debug("Already accepted: {} / "
|
||||
"rejected {}".format(is_accepted, is_rejected))
|
||||
|
||||
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
|
||||
if is_rejected:
|
||||
return
|
||||
elif is_accepted:
|
||||
reply.ignoreSslErrors()
|
||||
return
|
||||
|
||||
if ssl_strict == 'ask':
|
||||
err_template = jinja2.Template("""
|
||||
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
|
||||
<ul>
|
||||
{% for err in errors %}
|
||||
<li>{{err.errorString()}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
""".strip())
|
||||
msg = err_template.render(url=reply.url(), errors=errors)
|
||||
|
||||
answer = self._ask('SSL errors - continue?', msg,
|
||||
mode=usertypes.PromptMode.yesno, owner=reply,
|
||||
default=False)
|
||||
log.webview.debug("Asked for SSL errors, answer {}".format(answer))
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
err_dict = self._accepted_ssl_errors
|
||||
else:
|
||||
err_dict = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
err_dict[host_tpl] += errors
|
||||
else:
|
||||
log.webview.debug("ssl-strict is False, only warning about errors")
|
||||
for err in errors:
|
||||
# FIXME we might want to use warn here (non-fatal error)
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/114
|
||||
message.error('SSL error: {}'.format(err.errorString()))
|
||||
abort_on = self._get_abort_signals(reply)
|
||||
ignore = shared.ignore_certificate_errors(reply.url(), errors,
|
||||
abort_on=abort_on)
|
||||
if ignore:
|
||||
reply.ignoreSslErrors()
|
||||
self._accepted_ssl_errors[host_tpl] += errors
|
||||
err_dict = self._accepted_ssl_errors
|
||||
else:
|
||||
err_dict = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
err_dict[host_tpl] += errors
|
||||
|
||||
def clear_all_ssl_errors(self):
|
||||
"""Clear all remembered SSL errors."""
|
||||
@ -343,19 +288,13 @@ class NetworkManager(QNetworkAccessManager):
|
||||
except netrc.NetrcParseError:
|
||||
log.misc.exception("Error when parsing the netrc file")
|
||||
|
||||
if user is None:
|
||||
# netrc check failed
|
||||
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||
html.escape(reply.url().toDisplayString()),
|
||||
html.escape(authenticator.realm()))
|
||||
answer = self._ask("Authentication required",
|
||||
text=msg, mode=usertypes.PromptMode.user_pwd,
|
||||
owner=reply)
|
||||
if answer is not None:
|
||||
user, password = answer.user, answer.password
|
||||
if user is not None:
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
else:
|
||||
abort_on = self._get_abort_signals(reply)
|
||||
shared.authentication_required(reply.url(), authenticator,
|
||||
abort_on=abort_on)
|
||||
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
|
||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||
@ -369,9 +308,10 @@ class NetworkManager(QNetworkAccessManager):
|
||||
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||
html.escape(proxy.hostName()),
|
||||
html.escape(authenticator.realm()))
|
||||
answer = self._ask(
|
||||
"Proxy authentication required", msg,
|
||||
mode=usertypes.PromptMode.user_pwd)
|
||||
abort_on = self._get_abort_signals()
|
||||
answer = message.ask(
|
||||
title="Proxy authentication required", text=msg,
|
||||
mode=usertypes.PromptMode.user_pwd, abort_on=abort_on)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 "
|
||||
|
@ -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)
|
||||
|
@ -402,3 +402,20 @@ class Timer(QTimer):
|
||||
super().start(msec)
|
||||
else:
|
||||
super().start()
|
||||
|
||||
|
||||
class AbstractCertificateErrorWrapper:
|
||||
|
||||
"""A wrapper over an SSL/certificate error."""
|
||||
|
||||
def __init__(self, error):
|
||||
self._error = error
|
||||
|
||||
def __str__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_overridable(self):
|
||||
raise NotImplementedError
|
||||
|
@ -74,8 +74,8 @@ PERFECT_FILES = [
|
||||
|
||||
('tests/unit/browser/test_signalfilter.py',
|
||||
'qutebrowser/browser/signalfilter.py'),
|
||||
('tests/unit/browser/test_shared.py',
|
||||
'qutebrowser/browser/shared.py'),
|
||||
(None,
|
||||
'qutebrowser/browser/webkit/certificateerror.py'),
|
||||
# ('tests/unit/browser/test_tab.py',
|
||||
# 'qutebrowser/browser/tab.py'),
|
||||
|
||||
|
@ -98,6 +98,7 @@ def whitelist_generator():
|
||||
yield 'scripts.dev.pylint_checkers.config.' + attr
|
||||
|
||||
yield 'scripts.dev.pylint_checkers.modeline.process_module'
|
||||
yield 'scripts.dev.pylint_checkers.qute_pylint.config.msgs'
|
||||
|
||||
for attr in ['_get_default_metavar_for_optional',
|
||||
'_get_default_metavar_for_positional', '_metavar_formatter']:
|
||||
|
@ -27,6 +27,7 @@ import warnings
|
||||
|
||||
import pytest
|
||||
import hypothesis
|
||||
from PyQt5.QtCore import PYQT_VERSION
|
||||
|
||||
pytest.register_assert_rewrite('helpers')
|
||||
|
||||
@ -120,8 +121,14 @@ def pytest_collection_modifyitems(config, items):
|
||||
_apply_platform_markers(item)
|
||||
if item.get_marker('xfail_norun'):
|
||||
item.add_marker(pytest.mark.xfail(run=False))
|
||||
if item.get_marker('flaky_once'):
|
||||
item.add_marker(pytest.mark.flaky())
|
||||
if item.get_marker('js_prompt'):
|
||||
if config.webengine:
|
||||
js_prompt_pyqt_version = 0x050700
|
||||
else:
|
||||
js_prompt_pyqt_version = 0x050300
|
||||
item.add_marker(pytest.mark.skipif(
|
||||
PYQT_VERSION <= js_prompt_pyqt_version,
|
||||
reason='JS prompts are not supported with this PyQt version'))
|
||||
|
||||
if deselected:
|
||||
deselected_items.append(item)
|
||||
|
12
tests/end2end/data/hints/rapid.html
Normal file
12
tests/end2end/data/hints/rapid.html
Normal 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>
|
@ -352,6 +352,15 @@ def javascript_message_when(quteproc, message):
|
||||
quteproc.wait_for_js(message)
|
||||
|
||||
|
||||
@bdd.when("I clear SSL errors")
|
||||
def clear_ssl_errors(request, quteproc):
|
||||
if request.config.webengine:
|
||||
quteproc.terminate()
|
||||
quteproc.start()
|
||||
else:
|
||||
quteproc.send_cmd(':debug-clear-ssl-errors')
|
||||
|
||||
|
||||
## Then
|
||||
|
||||
|
||||
|
@ -75,9 +75,8 @@ Feature: Downloading things from a website.
|
||||
And I run :leave-mode
|
||||
Then no crash should happen
|
||||
|
||||
@qtwebengine_todo: ssl-strict is not implemented yet
|
||||
Scenario: Downloading with SSL errors (issue 1413)
|
||||
When I run :debug-clear-ssl-errors
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to ask
|
||||
And I download an SSL page
|
||||
And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
|
||||
|
@ -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:
|
||||
|
@ -34,7 +34,7 @@ Feature: Page history
|
||||
Then the history file should contain:
|
||||
http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli
|
||||
|
||||
@flaky_once @qtwebengine_todo: Error page message is not implemented
|
||||
@flaky @qtwebengine_todo: Error page message is not implemented
|
||||
Scenario: History with an error
|
||||
When I run :open file:///does/not/exist
|
||||
And I wait for "Error while loading file:///does/not/exist: Error opening /does/not/exist: *" in the log
|
||||
|
@ -382,7 +382,7 @@ Feature: Various utility commands.
|
||||
And I press the key "<Ctrl-C>"
|
||||
Then no crash should happen
|
||||
|
||||
@pyqt>=5.3.1 @qtwebengine_skip: JS prompt is not implemented yet
|
||||
@js_prompt
|
||||
Scenario: Focusing download widget via Tab (original issue)
|
||||
When I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button
|
||||
|
@ -40,7 +40,7 @@ Feature: Prompts
|
||||
And I run :leave-mode
|
||||
Then the javascript message "confirm reply: false" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Javascript prompt
|
||||
When I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button
|
||||
@ -49,7 +49,7 @@ Feature: Prompts
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: prompt test" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Javascript prompt with default
|
||||
When I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button-default
|
||||
@ -57,7 +57,7 @@ Feature: Prompts
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: default" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Rejected javascript prompt
|
||||
When I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button
|
||||
@ -68,6 +68,7 @@ Feature: Prompts
|
||||
|
||||
# Multiple prompts
|
||||
|
||||
@qtwebengine_skip: QtWebEngine refuses to load anything with a JS question
|
||||
Scenario: Blocking question interrupted by blocking one
|
||||
When I set content -> ignore-javascript-alert to false
|
||||
And I open data/prompt/jsalert.html
|
||||
@ -83,6 +84,7 @@ Feature: Prompts
|
||||
Then the javascript message "confirm reply: true" should be logged
|
||||
And the javascript message "Alert done" should be logged
|
||||
|
||||
@qtwebengine_skip: QtWebEngine refuses to load anything with a JS question
|
||||
Scenario: Blocking question interrupted by async one
|
||||
When I set content -> ignore-javascript-alert to false
|
||||
And I set content -> notifications to ask
|
||||
@ -99,6 +101,7 @@ Feature: Prompts
|
||||
Then the javascript message "Alert done" should be logged
|
||||
And the javascript message "notification permission granted" should be logged
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: Async question interrupted by async one
|
||||
When I set content -> notifications to ask
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
@ -113,6 +116,7 @@ Feature: Prompts
|
||||
Then the javascript message "notification permission granted" should be logged
|
||||
And "Added quickmark test for *" should be logged
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: Async question interrupted by blocking one
|
||||
When I set content -> notifications to ask
|
||||
And I set content -> ignore-javascript-alert to false
|
||||
@ -131,7 +135,7 @@ Feature: Prompts
|
||||
|
||||
# Shift-Insert with prompt (issue 1299)
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Pasting via shift-insert in prompt mode
|
||||
When selection is supported
|
||||
And I put "insert test" into the primary selection
|
||||
@ -142,7 +146,7 @@ Feature: Prompts
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: insert test" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Pasting via shift-insert without it being supported
|
||||
When selection is not supported
|
||||
And I put "insert test" into the primary selection
|
||||
@ -153,7 +157,7 @@ Feature: Prompts
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: " should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Using content -> ignore-javascript-prompt
|
||||
When I set content -> ignore-javascript-prompt to true
|
||||
And I open data/prompt/jsprompt.html
|
||||
@ -163,22 +167,21 @@ Feature: Prompts
|
||||
# SSL
|
||||
|
||||
Scenario: SSL error with ssl-strict = false
|
||||
When I run :debug-clear-ssl-errors
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to false
|
||||
And I load an SSL page
|
||||
And I wait until the SSL page finished loading
|
||||
Then the error "SSL error: *" should be shown
|
||||
Then the error "Certificate error: *" should be shown
|
||||
And the page should contain the plaintext "Hello World via SSL!"
|
||||
|
||||
Scenario: SSL error with ssl-strict = true
|
||||
When I run :debug-clear-ssl-errors
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to true
|
||||
And I load an SSL page
|
||||
Then "Error while loading *: SSL handshake failed" should be logged
|
||||
And the page should contain the plaintext "Unable to load page"
|
||||
Then a SSL error page should be shown
|
||||
|
||||
Scenario: SSL error with ssl-strict = ask -> yes
|
||||
When I run :debug-clear-ssl-errors
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to ask
|
||||
And I load an SSL page
|
||||
And I wait for a prompt
|
||||
@ -187,13 +190,12 @@ Feature: Prompts
|
||||
Then the page should contain the plaintext "Hello World via SSL!"
|
||||
|
||||
Scenario: SSL error with ssl-strict = ask -> no
|
||||
When I run :debug-clear-ssl-errors
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to ask
|
||||
And I load an SSL page
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept no
|
||||
Then "Error while loading *: SSL handshake failed" should be logged
|
||||
And the page should contain the plaintext "Unable to load page"
|
||||
Then a SSL error page should be shown
|
||||
|
||||
# Geolocation
|
||||
|
||||
@ -237,18 +239,21 @@ Feature: Prompts
|
||||
|
||||
# Notifications
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: Always rejecting notifications
|
||||
When I set content -> notifications to false
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
And I run :click-element id button
|
||||
Then the javascript message "notification permission denied" should be logged
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: Always accepting notifications
|
||||
When I set content -> notifications to true
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
And I run :click-element id button
|
||||
Then the javascript message "notification permission granted" should be logged
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: notifications with ask -> false
|
||||
When I set content -> notifications to ask
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
@ -257,6 +262,7 @@ Feature: Prompts
|
||||
And I run :prompt-accept no
|
||||
Then the javascript message "notification permission denied" should be logged
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: notifications with ask -> true
|
||||
When I set content -> notifications to ask
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
@ -275,6 +281,7 @@ Feature: Prompts
|
||||
And I run :leave-mode
|
||||
Then the javascript message "notification permission aborted" should be logged
|
||||
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: answering notification after closing tab
|
||||
When I set content -> notifications to ask
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
@ -287,55 +294,55 @@ Feature: Prompts
|
||||
# Page authentication
|
||||
|
||||
Scenario: Successful webpage authentification
|
||||
When I open basic-auth/user/password without waiting
|
||||
When I open basic-auth/user1/password1 without waiting
|
||||
And I wait for a prompt
|
||||
And I press the keys "user"
|
||||
And I press the keys "user1"
|
||||
And I run :prompt-accept
|
||||
And I press the keys "password"
|
||||
And I press the keys "password1"
|
||||
And I run :prompt-accept
|
||||
And I wait until basic-auth/user/password is loaded
|
||||
And I wait until basic-auth/user1/password1 is loaded
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user"
|
||||
"user": "user1"
|
||||
}
|
||||
|
||||
Scenario: Authentication with :prompt-accept value
|
||||
When I open about:blank in a new tab
|
||||
And I open basic-auth/user/password without waiting
|
||||
And I open basic-auth/user2/password2 without waiting
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept user:password
|
||||
And I wait until basic-auth/user/password is loaded
|
||||
And I run :prompt-accept user2:password2
|
||||
And I wait until basic-auth/user2/password2 is loaded
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user"
|
||||
"user": "user2"
|
||||
}
|
||||
|
||||
Scenario: Authentication with invalid :prompt-accept value
|
||||
When I open about:blank in a new tab
|
||||
And I open basic-auth/user/password without waiting
|
||||
And I open basic-auth/user3/password3 without waiting
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept foo
|
||||
And I run :prompt-accept user:password
|
||||
And I run :prompt-accept user3:password3
|
||||
Then the error "Value needs to be in the format username:password, but foo was given" should be shown
|
||||
|
||||
Scenario: Tabbing between username and password
|
||||
When I open about:blank in a new tab
|
||||
And I open basic-auth/user/password without waiting
|
||||
And I open basic-auth/user4/password4 without waiting
|
||||
And I wait for a prompt
|
||||
And I press the keys "us"
|
||||
And I run :prompt-item-focus next
|
||||
And I press the keys "password"
|
||||
And I press the keys "password4"
|
||||
And I run :prompt-item-focus prev
|
||||
And I press the keys "er"
|
||||
And I press the keys "er4"
|
||||
And I run :prompt-accept
|
||||
And I run :prompt-accept
|
||||
And I wait until basic-auth/user/password is loaded
|
||||
And I wait until basic-auth/user4/password4 is loaded
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user"
|
||||
"user": "user4"
|
||||
}
|
||||
|
||||
# :prompt-accept with value argument
|
||||
@ -350,7 +357,7 @@ Feature: Prompts
|
||||
Then the javascript message "Alert done" should be logged
|
||||
And the error "No value is permitted with alert prompts!" should be shown
|
||||
|
||||
@pyqt>=5.3.1
|
||||
@js_prompt
|
||||
Scenario: Javascript prompt with value
|
||||
When I set content -> ignore-javascript-prompt to false
|
||||
And I open data/prompt/jsprompt.html
|
||||
@ -396,6 +403,7 @@ Feature: Prompts
|
||||
|
||||
# Other
|
||||
|
||||
@qtwebengine_skip
|
||||
Scenario: Shutting down with a question
|
||||
When I open data/prompt/jsconfirm.html
|
||||
And I run :click-element id button
|
||||
@ -429,32 +437,34 @@ Feature: Prompts
|
||||
Then "Added quickmark prompt-in-command-mode for *" should be logged
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1093
|
||||
@qtwebengine_skip: QtWebEngine doesn't open the second page/prompt
|
||||
Scenario: Keyboard focus with multiple auth prompts
|
||||
When I open basic-auth/user1/password1 without waiting
|
||||
And I open basic-auth/user2/password2 in a new tab without waiting
|
||||
When I open basic-auth/user5/password5 without waiting
|
||||
And I open basic-auth/user6/password6 in a new tab without waiting
|
||||
And I wait for a prompt
|
||||
And I wait for a prompt
|
||||
# Second prompt (showed first)
|
||||
And I press the keys "user2"
|
||||
And I press the keys "user6"
|
||||
And I press the key "<Enter>"
|
||||
And I press the keys "password2"
|
||||
And I press the keys "password6"
|
||||
And I press the key "<Enter>"
|
||||
And I wait until basic-auth/user2/password2 is loaded
|
||||
And I wait until basic-auth/user6/password6 is loaded
|
||||
# First prompt
|
||||
And I press the keys "user1"
|
||||
And I press the keys "user5"
|
||||
And I press the key "<Enter>"
|
||||
And I press the keys "password1"
|
||||
And I press the keys "password5"
|
||||
And I press the key "<Enter>"
|
||||
And I wait until basic-auth/user1/password1 is loaded
|
||||
And I wait until basic-auth/user5/password5 is loaded
|
||||
# We're on the second page
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user2"
|
||||
"user": "user6"
|
||||
}
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
|
||||
# https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544
|
||||
@qtwebengine_todo: Notifications are not implemented in QtWebEngine
|
||||
Scenario: Interrupting SSL prompt during a notification prompt
|
||||
When I set content -> notifications to ask
|
||||
And I set network -> ssl-strict to ask
|
||||
|
@ -17,14 +17,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('prompts.feature')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.qtwebengine_todo("Prompts are not implemented")
|
||||
|
||||
|
||||
@bdd.when("I load an SSL page")
|
||||
def load_ssl_page(quteproc, ssl_server):
|
||||
# We don't wait here as we can get an SSL question.
|
||||
@ -46,3 +42,32 @@ def wait_for_prompt(quteproc):
|
||||
def no_prompt_shown(quteproc):
|
||||
quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: '
|
||||
'question asked)')
|
||||
|
||||
|
||||
@bdd.then("a SSL error page should be shown")
|
||||
def ssl_error_page(request, quteproc):
|
||||
if not request.config.webengine:
|
||||
line = quteproc.wait_for(message='Error while loading *: SSL '
|
||||
'handshake failed')
|
||||
line.expected = True
|
||||
quteproc.wait_for(message="Changing title for idx * to 'Error "
|
||||
"loading page: *'")
|
||||
content = quteproc.get_content().strip()
|
||||
assert "Unable to load page" in content
|
||||
|
||||
|
||||
class AbstractCertificateErrorWrapper:
|
||||
|
||||
"""A wrapper over an SSL/certificate error."""
|
||||
|
||||
def __init__(self, error):
|
||||
self._error = error
|
||||
|
||||
def __str__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_overridable(self):
|
||||
raise NotImplementedError
|
||||
|
@ -53,6 +53,19 @@ def is_ignored_qt_message(message):
|
||||
return False
|
||||
|
||||
|
||||
def is_ignored_lowlevel_message(message):
|
||||
"""Check if we want to ignore a lowlevel process output."""
|
||||
if 'Running without the SUID sandbox!' in message:
|
||||
return True
|
||||
elif message.startswith('Xlib: sequence lost'):
|
||||
# https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720
|
||||
# ???
|
||||
return True
|
||||
elif 'CERT_PKIXVerifyCert for localhost failed' in message:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class LogLine(testprocess.Line):
|
||||
|
||||
"""A parsed line from the qutebrowser log output.
|
||||
@ -222,14 +235,8 @@ class QuteProc(testprocess.Process):
|
||||
except testprocess.InvalidLine:
|
||||
if not line.strip():
|
||||
return None
|
||||
elif 'Running without the SUID sandbox!' in line:
|
||||
# QtWebEngine error
|
||||
return None
|
||||
elif line.startswith('Xlib: sequence lost'):
|
||||
# https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720
|
||||
# ???
|
||||
return None
|
||||
elif is_ignored_qt_message(line):
|
||||
elif (is_ignored_qt_message(line) or
|
||||
is_ignored_lowlevel_message(line)):
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
@ -81,7 +81,7 @@ class Request(testprocess.Line):
|
||||
http.client.FOUND]
|
||||
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
|
||||
http.client.FOUND]
|
||||
for suffix in ['', '1', '2']:
|
||||
for suffix in ['', '1', '2', '3', '4', '5', '6']:
|
||||
key = '/basic-auth/user{}/password{}'.format(suffix, suffix)
|
||||
path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK]
|
||||
|
||||
|
27
tests/unit/utils/usertypes/test_misc.py
Normal file
27
tests/unit/utils/usertypes/test_misc.py
Normal file
@ -0,0 +1,27 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
def test_abstract_certificate_error_wrapper():
|
||||
err = object()
|
||||
wrapper = usertypes.AbstractCertificateErrorWrapper(err)
|
||||
assert wrapper._error is err
|
Loading…
Reference in New Issue
Block a user