webengine: Initial support for authentication and javascript prompts

This commit is contained in:
Florian Bruhin 2016-11-09 21:49:36 +01:00
parent 6d72bce4b6
commit 65625a9dea
8 changed files with 215 additions and 91 deletions

View File

@ -19,7 +19,15 @@
"""Various utilities shared between webpage/webview subclasses."""
import html
from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log
class CallSuper(Exception):
"""Raised when the caller should call the superclass instead."""
def custom_headers():
@ -39,3 +47,66 @@ 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."""
msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(url.toDisplayString()),
html.escape(authenticator.realm()))
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)

View File

@ -22,6 +22,7 @@
"""Wrapper over a QWebEngineView."""
import html
import functools
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer
@ -32,12 +33,12 @@ 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)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg)
objreg, message)
_qute_scheme_handler = None
@ -538,7 +539,7 @@ class WebEngineTab(browsertab.AbstractTab):
self._widget.page().runJavaScript(code, callback)
def shutdown(self):
log.stub()
self._widget.shutdown()
def reload(self, *, force=False):
if force:
@ -590,6 +591,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()
@ -603,6 +611,7 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors)
page.link_clicked.connect(self._on_link_clicked)
page.authenticationRequired.connect(self._on_authentication_required)
try:
view.iconChanged.connect(self.icon_changed)
except AttributeError:

View File

@ -26,6 +26,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import shared
from qutebrowser.config import config
from qutebrowser.utils import log, debug, usertypes, objreg, qtutils
@ -39,6 +40,9 @@ class WebEngineView(QWebEngineView):
self._win_id = win_id
self.setPage(WebEnginePage(tabdata, parent=self))
def shutdown(self):
self.page().shutdown()
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.
@ -99,22 +103,65 @@ 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.
shutting_down: Emitted when the page is shutting down.
"""
certificate_error = pyqtSignal()
link_clicked = pyqtSignal(QUrl)
shutting_down = pyqtSignal()
def __init__(self, tabdata, parent=None):
super().__init__(parent)
self._tabdata = tabdata
self._is_shutting_down = False
def shutdown(self):
self._is_shutting_down = True
self.shutting_down.emit()
def certificateError(self, error):
self.certificate_error.emit()
return super().certificateError(error)
def javaScriptConfirm(self, url, js_msg):
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)
# Can't override javaScriptPrompt currently
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-November/038293.html
# def javaScriptPrompt(self, url, js_msg, default, result):
# 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 the statusbar."""
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."""
# FIXME:qtwebengine maybe unify this in the tab api somehow?

View File

@ -343,19 +343,18 @@ 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.shutting_down, reply.destroyed]
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)
shared.authentication_required(reply.url(), authenticator,
abort_on=abort_on)
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
def on_proxy_authentication_required(self, proxy, authenticator):

View File

@ -30,7 +30,7 @@ from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from qutebrowser.config import config
from qutebrowser.browser import pdfjs
from qutebrowser.browser import pdfjs, shared
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
@ -94,23 +94,16 @@ class BrowserPage(QWebPage):
# of a bug in PyQt.
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
def javaScriptPrompt(self, _frame, js_msg, default):
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')):
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.
@ -442,36 +435,25 @@ class BrowserPage(QWebPage):
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')):
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."""

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
@pyqt>=5.3.1 @qtwebengine_skip
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
@pyqt>=5.3.1 @qtwebengine_skip
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
@pyqt>=5.3.1 @qtwebengine_skip
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: Permissions are not implemented yet
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: Permissions are not implemented yet
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
@pyqt>=5.3.1 @qtwebengine_skip
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
@pyqt>=5.3.1 @qtwebengine_skip
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
@pyqt>=5.3.1 @qtwebengine_skip
Scenario: Using content -> ignore-javascript-prompt
When I set content -> ignore-javascript-prompt to true
And I open data/prompt/jsprompt.html
@ -162,6 +166,7 @@ Feature: Prompts
# SSL
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = false
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to false
@ -170,6 +175,7 @@ Feature: Prompts
Then the error "SSL error: *" should be shown
And the page should contain the plaintext "Hello World via SSL!"
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = true
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to true
@ -177,6 +183,7 @@ Feature: Prompts
Then "Error while loading *: SSL handshake failed" should be logged
And the page should contain the plaintext "Unable to load page"
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = ask -> yes
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
@ -186,6 +193,7 @@ Feature: Prompts
And I wait until the SSL page finished loading
Then the page should contain the plaintext "Hello World via SSL!"
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = ask -> no
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
@ -197,20 +205,21 @@ Feature: Prompts
# Geolocation
@qtwebengine_todo: Permissions are not implemented yet
Scenario: Always rejecting geolocation
When I set content -> geolocation to false
And I open data/prompt/geolocation.html in a new tab
And I run :click-element id button
Then the javascript message "geolocation permission denied" should be logged
@ci @not_osx
@ci @not_osx @qtwebengine_todo: Permissions are not implemented yet
Scenario: Always accepting geolocation
When I set content -> geolocation to true
And I open data/prompt/geolocation.html in a new tab
And I run :click-element id button
Then the javascript message "geolocation permission denied" should not be logged
@ci @not_osx
@ci @not_osx @qtwebengine_todo: Permissions are not implemented yet
Scenario: geolocation with ask -> true
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
@ -219,6 +228,7 @@ Feature: Prompts
And I run :prompt-accept yes
Then the javascript message "geolocation permission denied" should not be logged
@qtwebengine_todo: Permissions are not implemented yet
Scenario: geolocation with ask -> false
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
@ -227,6 +237,7 @@ Feature: Prompts
And I run :prompt-accept no
Then the javascript message "geolocation permission denied" should be logged
@qtwebengine_todo: Permissions are not implemented yet
Scenario: geolocation with ask -> abort
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
@ -237,18 +248,21 @@ Feature: Prompts
# Notifications
@qtwebengine_todo: Permissions are not implemented yet
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: Permissions are not implemented yet
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: Permissions are not implemented yet
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 +271,7 @@ Feature: Prompts
And I run :prompt-accept no
Then the javascript message "notification permission denied" should be logged
@qtwebengine_todo: Permissions are not implemented yet
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 +290,7 @@ Feature: Prompts
And I run :leave-mode
Then the javascript message "notification permission aborted" should be logged
@qtwebengine_todo: Permissions are not implemented yet
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 +303,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 +366,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
@pyqt>=5.3.1 @qtwebengine_skip
Scenario: Javascript prompt with value
When I set content -> ignore-javascript-prompt to false
And I open data/prompt/jsprompt.html
@ -396,6 +412,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 +446,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: SSL errors are not implemented yet
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

@ -22,9 +22,6 @@ 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.

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]