Bug 1522120 - Remove permission prompts when entering full-screen and leave full-screen when a permission prompt is shown. r=johannh

Differential Revision: https://phabricator.services.mozilla.com/D36932

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Paul Zuehlcke 2019-07-24 16:17:54 +00:00
parent a908700415
commit 39687fe7bb
9 changed files with 274 additions and 23 deletions

View File

@ -403,6 +403,8 @@ pref("permissions.desktop-notification.postPrompt.enabled", true);
pref("permissions.desktop-notification.postPrompt.enabled", false);
#endif
pref("permissions.fullscreen.allowed", false);
pref("permissions.postPrompt.animate", true);
// This is primarily meant to be enabled for studies.

View File

@ -443,7 +443,7 @@ var gXPInstallObserver = {
PopupNotifications.getNotification(id, browser)
).filter(notification => notification != null);
PopupNotifications.remove(notifications);
PopupNotifications.remove(notifications, true);
},
observe(aSubject, aTopic, aData) {

View File

@ -6,6 +6,8 @@
// This file is loaded into the browser window scope.
/* eslint-env mozilla/browser-window */
ChromeUtils.import("resource:///modules/PermissionUI.jsm", this);
var PointerlockFsWarning = {
_element: null,
_origin: null,
@ -246,7 +248,19 @@ var FullScreen = {
"DOMFullscreen:Painted",
],
_permissionNotificationIDs: Object.values(PermissionUI)
.filter(value => value.prototype && value.prototype.notificationID)
.map(value => value.prototype.notificationID)
// Additionally include webRTC permission prompt which does not use PermissionUI
.concat(["webRTC-shareDevices"]),
init() {
XPCOMUtils.defineLazyPreferenceGetter(
this,
"permissionsFullScreenAllowed",
"permissions.fullscreen.allowed"
);
// called when we go into full screen, even if initiated by a web page script
window.addEventListener("fullscreen", this, true);
window.addEventListener("willenterfullscreen", this, true);
@ -385,11 +399,6 @@ var FullScreen = {
browser = event.target.ownerGlobal.docShell.chromeEventHandler;
}
// Addon installation should be cancelled when entering fullscreen for security and usability reasons.
// Installation prompts in fullscreen can trick the user into installing unwanted addons.
// In fullscreen the notification box does not have a clear visual association with its parent anymore.
gXPInstallObserver.removeAllNotifications(browser);
TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
this.enterDomFullscreen(browser);
break;
@ -401,6 +410,18 @@ var FullScreen = {
}
},
_handlePermPromptShow() {
if (
!FullScreen.permissionsFullScreenAllowed &&
window.fullScreen &&
PopupNotifications.getNotification(
this._permissionNotificationIDs
).filter(n => !n.dismissed).length > 0
) {
this.exitDomFullScreen();
}
},
receiveMessage(aMessage) {
let browser = aMessage.target;
switch (aMessage.name) {
@ -465,6 +486,14 @@ var FullScreen = {
return;
}
// Remove permission prompts when entering full-screen.
if (!FullScreen.permissionsFullScreenAllowed) {
let notifications = PopupNotifications.getNotification(
this._permissionNotificationIDs
).filter(n => !n.dismissed);
PopupNotifications.remove(notifications, true);
}
document.documentElement.setAttribute("inDOMFullscreen", true);
if (gFindBarInitialized) {
@ -478,6 +507,17 @@ var FullScreen = {
// If a fullscreen window loses focus, we show a warning when the
// fullscreen window is refocused.
window.addEventListener("activate", this);
// Addon installation should be cancelled when entering fullscreen for security and usability reasons.
// Installation prompts in fullscreen can trick the user into installing unwanted addons.
// In fullscreen the notification box does not have a clear visual association with its parent anymore.
gXPInstallObserver.removeAllNotifications(aBrowser);
PopupNotifications.panel.addEventListener(
"popupshowing",
() => this._handlePermPromptShow(),
true
);
},
cleanup() {
@ -490,6 +530,11 @@ var FullScreen = {
},
cleanupDomFullscreen() {
PopupNotifications.panel.removeEventListener(
"popupshowing",
() => this._handlePermPromptShow(),
true
);
window.messageManager.broadcastAsyncMessage("DOMFullscreen:CleanUp");
PointerlockFsWarning.close();

View File

@ -3,3 +3,5 @@ support-files =
head.js
[browser_bug1557041.js]
skip-if = os == 'linux' # Bug 1561973
[browser_fullscreen_permissions_prompt.js]
skip-if = debug && os == 'macos' # Bug 1568570

View File

@ -0,0 +1,156 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
SimpleTest.requestCompleteLog();
async function requestNotificationPermission(browser) {
return ContentTask.spawn(browser, null, () => {
return content.Notification.requestPermission();
});
}
async function requestCameraPermission(browser) {
return ContentTask.spawn(browser, null, () => {
return new Promise(resolve => {
content.navigator.mediaDevices
.getUserMedia({ video: true, fake: true })
.catch(resolve(false))
.then(resolve(true));
});
});
}
add_task(async function test_fullscreen_closes_permissionui_prompt() {
await SpecialPowers.pushPrefEnv({
set: [
["dom.webnotifications.requireuserinteraction", false],
["permissions.fullscreen.allowed", false],
],
});
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com"
);
let browser = tab.linkedBrowser;
let popupShown, requestResult, popupHidden;
popupShown = BrowserTestUtils.waitForEvent(
window.PopupNotifications.panel,
"popupshown"
);
info("Requesting notification permission");
requestResult = requestNotificationPermission(browser);
await popupShown;
info("Entering DOM full-screen");
popupHidden = BrowserTestUtils.waitForEvent(
window.PopupNotifications.panel,
"popuphidden"
);
await changeFullscreen(browser, true);
await popupHidden;
is(
await requestResult,
"default",
"Expect permission request to be cancelled"
);
await changeFullscreen(browser, false);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_fullscreen_closes_webrtc_permission_prompt() {
await SpecialPowers.pushPrefEnv({
set: [
["media.navigator.permission.fake", true],
["media.navigator.permission.force", true],
["permissions.fullscreen.allowed", false],
],
});
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com"
);
let browser = tab.linkedBrowser;
let popupShown, requestResult, popupHidden;
popupShown = BrowserTestUtils.waitForEvent(
window.PopupNotifications.panel,
"popupshown"
);
info("Requesting camera permission");
requestResult = requestCameraPermission(browser);
await popupShown;
info("Entering DOM full-screen");
popupHidden = BrowserTestUtils.waitForEvent(
window.PopupNotifications.panel,
"popuphidden"
);
await changeFullscreen(browser, true);
await popupHidden;
is(
await requestResult,
false,
"Expect webrtc permission request to be cancelled"
);
await changeFullscreen(browser, false);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_permission_prompt_closes_fullscreen() {
await SpecialPowers.pushPrefEnv({
set: [
["dom.webnotifications.requireuserinteraction", false],
["permissions.fullscreen.allowed", false],
],
});
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com"
);
let browser = tab.linkedBrowser;
info("Entering DOM full-screen");
await changeFullscreen(browser, true);
let popupShown = BrowserTestUtils.waitForEvent(
window.PopupNotifications.panel,
"popupshown"
);
let fullScreenExit = waitForFullScreenState(browser, false);
info("Requesting notification permission");
requestNotificationPermission(browser);
await popupShown;
info("Waiting for full-screen exit");
await fullScreenExit;
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});

View File

@ -6,10 +6,29 @@ const { ContentTask } = ChromeUtils.import(
);
function waitForFullScreenState(browser, state) {
let eventName = state
? "MozDOMFullscreen:Entered"
: "MozDOMFullscreen:Exited";
return BrowserTestUtils.waitForEvent(browser.ownerGlobal, eventName);
return new Promise(resolve => {
let eventReceived = false;
window.messageManager.addMessageListener(
"DOMFullscreen:Painted",
function listener() {
if (!eventReceived) {
return;
}
window.messageManager.removeMessageListener(
"DOMFullscreen:Painted",
listener
);
resolve();
}
);
window.addEventListener(
`MozDOMFullscreen:${state ? "Entered" : "Exited"}`,
() => {
eventReceived = true;
},
{ once: true }
);
});
}
/**
@ -18,9 +37,17 @@ function waitForFullScreenState(browser, state) {
* @param {Boolean} fullscreenState - true to enter fullscreen, false to leave
* @returns {Promise} - Resolves once fullscreen change is applied
*/
function changeFullscreen(browser, fullScreenState) {
async function changeFullscreen(browser, fullScreenState) {
await new Promise(resolve =>
SimpleTest.waitForFocus(resolve, browser.ownerGlobal)
);
let fullScreenChange = waitForFullScreenState(browser, fullScreenState);
ContentTask.spawn(browser, fullScreenState, state => {
ContentTask.spawn(browser, fullScreenState, async state => {
// Wait for document focus before requesting full-screen
await ContentTaskUtils.waitForCondition(
() => docShell.isActive && content.document.hasFocus(),
"Waiting for document focus"
);
if (state) {
content.document.body.requestFullscreen();
} else {

View File

@ -625,7 +625,7 @@ var PermissionPromptPrototype = {
options.hideClose = true;
}
options.eventCallback = (topic, nextRemovalReason) => {
options.eventCallback = (topic, nextRemovalReason, isCancel) => {
// When the docshell of the browser is aboout to be swapped to another one,
// the "swapping" event is called. Returning true causes the notification
// to be moved to the new browser.
@ -653,6 +653,9 @@ var PermissionPromptPrototype = {
nextRemovalReason
);
}
if (isCancel) {
this.cancel();
}
this.onAfterShow();
}
return false;

View File

@ -572,7 +572,7 @@ function prompt(aBrowser, aRequest) {
name: getHostOrExtensionName(principal.URI),
persistent: true,
hideClose: true,
eventCallback(aTopic, aNewBrowser) {
eventCallback(aTopic, aNewBrowser, isCancel) {
if (aTopic == "swapping") {
return true;
}
@ -602,6 +602,11 @@ function prompt(aBrowser, aRequest) {
}
}
// If the notification has been cancelled (e.g. due to entering full-screen), also cancel the webRTC request
if (aTopic == "removed" && notification && isCancel) {
denyRequest(notification.browser, aRequest);
}
if (aTopic != "showing") {
return false;
}

View File

@ -346,20 +346,24 @@ PopupNotifications.prototype = {
},
/**
* Retrieve a Notification object associated with the browser/ID pair.
* @param id
* The Notification ID to search for.
* @param browser
* Retrieve one or many Notification object/s associated with the browser/ID pair.
* @param {string|string[]} id
* The Notification ID or an array of IDs to search for.
* @param [browser]
* The browser whose notifications should be searched. If null, the
* currently selected browser's notifications will be searched.
*
* @returns the corresponding Notification object, or null if no such
* @returns {Notification|Notification[]|null} If passed a single id, returns the corresponding Notification object, or null if no such
* notification exists.
* If passed an id array, returns an array of Notification objects which match the ids.
*/
getNotification: function PopupNotifications_getNotification(id, browser) {
let notifications = this._getNotificationsForBrowser(
browser || this.tabbrowser.selectedBrowser
);
if (Array.isArray(id)) {
return notifications.filter(x => id.includes(x.id));
}
return notifications.find(x => x.id == id) || null;
},
@ -732,15 +736,18 @@ PopupNotifications.prototype = {
/**
* Removes one or many Notifications.
* @param {Notification|Notification[]} notification - The Notification object/s to remove.
* @param {Boolean} [isCancel] - Whether to signal, in the notification event, that removal
* should be treated as cancel. This is currently used to cancel permission requests
* when their Notifications are removed.
*/
remove: function PopupNotifications_remove(notification) {
remove: function PopupNotifications_remove(notification, isCancel = false) {
let notificationArray = Array.isArray(notification)
? notification
: [notification];
let activeBrowser;
notificationArray.forEach(n => {
this._remove(n);
this._remove(n, isCancel);
if (!activeBrowser && this._isActiveBrowser(n.browser)) {
activeBrowser = n.browser;
}
@ -796,7 +803,10 @@ PopupNotifications.prototype = {
: [];
},
_remove: function PopupNotifications_removeHelper(notification) {
_remove: function PopupNotifications_removeHelper(
notification,
isCancel = false
) {
// This notification may already be removed, in which case let's just fail
// silently.
let notifications = this._getNotificationsForBrowser(notification.browser);
@ -818,7 +828,8 @@ PopupNotifications.prototype = {
this._fireCallback(
notification,
NOTIFICATION_EVENT_REMOVED,
this.nextRemovalReason
this.nextRemovalReason,
isCancel
);
},