Handle feature permissions with QtWebEngine

This commit is contained in:
Florian Bruhin 2016-11-10 18:37:36 +01:00
parent 8f55725555
commit bbcbb24cb5
6 changed files with 156 additions and 51 deletions

View File

@ -160,6 +160,8 @@
|<<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-mouse-lock,mouse-lock>>|Allow websites to lock the mouse pointer.
|<<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.
@ -1450,6 +1452,34 @@ 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-mouse-lock]]
=== mouse-lock
Allow websites to lock the mouse pointer.
Valid values:
* +true+
* +false+
* +ask+
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content-javascript-can-open-windows-automatically]]
=== javascript-can-open-windows-automatically
Whether JavaScript programs can open new windows without user interaction.

View File

@ -157,3 +157,37 @@ def ignore_certificate_errors(url, errors, abort_on):
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

View File

@ -20,8 +20,9 @@
"""The main browser widget for QtWebEngine."""
import os
import functools
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl
# 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
@ -121,6 +122,55 @@ class WebEnginePage(QWebEnginePage):
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'),
QWebEnginePage.MouseLock: ('content', 'mouse-lock'),
}
messages = {
QWebEnginePage.Geolocation: 'access your location',
QWebEnginePage.MediaAudioCapture: 'record audio',
QWebEnginePage.MediaVideoCapture: 'record video',
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
}
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

View File

@ -82,7 +82,7 @@ class BrowserPage(QWebPage):
self.unsupportedContent.connect(self.on_unsupported_content)
self.loadStarted.connect(self.on_load_started)
self.featurePermissionRequested.connect(
self.on_feature_permission_requested)
self._on_feature_permission_requested)
self.saveFrameStateRequested.connect(
self.on_save_frame_state_requested)
self.restoreFrameStateRequested.connect(
@ -289,7 +289,7 @@ class BrowserPage(QWebPage):
self.error_occurred = False
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
def on_feature_permission_requested(self, frame, feature):
def _on_feature_permission_requested(self, frame, feature):
"""Ask the user for approval for geolocation/notifications."""
if not isinstance(frame, QWebFrame): # pragma: no cover
# This makes no sense whatsoever, but someone reported this being
@ -302,46 +302,30 @@ class BrowserPage(QWebPage):
QWebPage.Notifications: ('content', 'notifications'),
QWebPage.Geolocation: ('content', 'geolocation'),
}
config_val = config.get(*options[feature])
if config_val == 'ask':
msgs = {
QWebPage.Notifications: 'show notifications',
QWebPage.Geolocation: 'access your location',
}
messages = {
QWebPage.Notifications: 'show notifications',
QWebPage.Geolocation: 'access your location',
}
yes_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionGrantedByUser)
no_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
host = frame.url().host()
if host:
text = "Allow the website at <b>{}</b> to {}?".format(
html.escape(frame.url().toDisplayString()), msgs[feature])
else:
text = "Allow the website to {}?".format(msgs[feature])
question = shared.feature_permission(
url=frame.url(),
option=options[feature], msg=messages[feature],
yes_action=yes_action, no_action=no_action,
abort_on=[self.shutting_down, self.loadStarted])
yes_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionGrantedByUser)
no_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
question = message.confirm_async(yes_action=yes_action,
no_action=no_action,
cancel_action=no_action,
abort_on=[self.shutting_down,
self.loadStarted],
title='Permission request',
text=text)
if question is not None:
self.featurePermissionRequestCanceled.connect(
functools.partial(self.on_feature_permission_cancelled,
functools.partial(self._on_feature_permission_cancelled,
question, frame, feature))
elif config_val:
self.setFeaturePermission(frame, feature,
QWebPage.PermissionGrantedByUser)
else:
self.setFeaturePermission(frame, feature,
QWebPage.PermissionDeniedByUser)
def on_feature_permission_cancelled(self, question, frame, feature,
cancelled_frame, cancelled_feature):
def _on_feature_permission_cancelled(self, question, frame, feature,
cancelled_frame, cancelled_feature):
"""Slot invoked when a feature permission request was cancelled.
To be used with functools.partial.

View File

@ -821,6 +821,16 @@ 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."),
('mouse-lock',
SettingValue(typ.BoolAsk(), 'ask',
backends=[usertypes.Backend.QtWebEngine]),
"Allow websites to lock the mouse pointer."),
('javascript-can-open-windows-automatically',
SettingValue(typ.Bool(), 'false'),
"Whether JavaScript programs can open new windows without user "

View File

@ -101,7 +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
@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
@ -116,7 +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
@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
@ -199,21 +199,20 @@ 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 @qtwebengine_todo: Permissions are not implemented yet
@ci @not_osx
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 @qtwebengine_todo: Permissions are not implemented yet
@ci @not_osx
Scenario: geolocation with ask -> true
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
@ -222,7 +221,6 @@ 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
@ -231,7 +229,6 @@ 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
@ -242,21 +239,21 @@ Feature: Prompts
# Notifications
@qtwebengine_todo: Permissions are not implemented yet
@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: Permissions are not implemented yet
@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: Permissions are not implemented yet
@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
@ -265,7 +262,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
@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
@ -284,7 +281,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
@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
@ -467,7 +464,7 @@ Feature: Prompts
# https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
# https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544
@qtwebengine_todo: Permissions are not implemented yet
@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