Bug 1476555 - Show notification when autoplay blocked globally. r=cpearce,johannh

MozReview-Commit-ID: EI0GiaoBNqX

--HG--
extra : rebase_source : 0c6fb98047913fc50423532cc4c433c4627c5b06
This commit is contained in:
Dale Harvey 2018-07-23 16:43:08 +01:00
parent 2456972415
commit 59e904f6cd
10 changed files with 218 additions and 48 deletions

View File

@ -1015,7 +1015,9 @@ var gIdentityHandler = {
let nameLabelId = "identity-popup-permission-label-" + aPermission.id;
nameLabel.setAttribute("id", nameLabelId);
let isPolicyPermission = aPermission.scope == SitePermissions.SCOPE_POLICY;
let isPolicyPermission = [
SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL
].includes(aPermission.scope);
if (aPermission.id == "popup" && !isPolicyPermission) {
let menulist = document.createXULElement("menulist");
@ -1083,8 +1085,8 @@ var gIdentityHandler = {
container.setAttribute("aria-labelledby", nameLabelId + " " + stateLabelId);
/* We return the permission item here without a remove button if the permission is a
SCOPE_POLICY permission. Policy permissions cannot be removed/changed for the duration
of the browser session. */
SCOPE_POLICY or SCOPE_GLOBAL permission. Policy permissions cannot be
removed/changed for the duration of the browser session. */
if (isPolicyPermission) {
return container;
}

View File

@ -97,7 +97,7 @@ function initRow(aPartId) {
command.setAttribute("disabled", "true");
}
if (scope == SitePermissions.SCOPE_POLICY) {
if ([SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL].includes(scope)) {
checkbox.setAttribute("disabled", "true");
command.setAttribute("disabled", "true");
}

View File

@ -10,6 +10,10 @@ support-files=
support-files =
temporary_permissions_subframe.html
../webrtc/get_user_media.html
[browser_autoplay_blocked.js]
support-files =
browser_autoplay_blocked.html
../general/audio.ogg
[browser_temporary_permissions_expiry.js]
[browser_temporary_permissions_navigation.js]
[browser_temporary_permissions_tabs.js]

View File

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
</head>
<body>
<audio autoplay="autoplay" >
<source src="audio.ogg" />
</audio>
</body>
</html>

View File

@ -0,0 +1,57 @@
/*
* Test that a blocked request to autoplay media is shown to the user
*/
const AUTOPLAY_PAGE = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com") + "browser_autoplay_blocked.html";
function openIdentityPopup() {
let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
gIdentityHandler._identityBox.click();
return promise;
}
function closeIdentityPopup() {
let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
gIdentityHandler._identityPopup.hidePopup();
return promise;
}
function autoplayBlockedIcon() {
return document.querySelector("#blocked-permissions-container " +
".blocked-permission-icon.autoplay-media-icon");
}
add_task(async function testMainViewVisible() {
Services.prefs.setIntPref("media.autoplay.default", Ci.nsIAutoplay.ALLOWED);
await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function() {
let permissionsList = document.getElementById("identity-popup-permission-list");
let emptyLabel = permissionsList.nextSibling.nextSibling;
ok(BrowserTestUtils.is_hidden(autoplayBlockedIcon()), "Blocked icon not shown");
await openIdentityPopup();
ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
await closeIdentityPopup();
});
Services.prefs.setIntPref("media.autoplay.default", Ci.nsIAutoplay.BLOCKED);
await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function() {
let permissionsList = document.getElementById("identity-popup-permission-list");
let emptyLabel = permissionsList.nextSibling.nextSibling;
ok(!BrowserTestUtils.is_hidden(autoplayBlockedIcon()), "Blocked icon is shown");
await openIdentityPopup();
ok(BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is not empty");
let labelText = SitePermissions.getPermissionLabel("autoplay-media");
let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
is(labels.length, 1, "One permission visible in main view");
is(labels[0].textContent, labelText, "Correct value");
await closeIdentityPopup();
});
Services.prefs.clearUserPref("media.autoplay.default");
});

View File

@ -279,6 +279,19 @@ var PermissionPromptPrototype = {
this.browser);
if (state == SitePermissions.BLOCK) {
// If the request is blocked by a global setting then we record
// a flag that lasts for the duration of the current page load
// to notify the user that the permission has been blocked.
// Currently only applies to autoplay-media
if (state == SitePermissions.getDefault(this.permissionKey) &&
SitePermissions.showGloballyBlocked(this.permissionKey)) {
SitePermissions.set(this.principal.URI,
this.permissionKey,
state,
SitePermissions.SCOPE_GLOBAL,
this.browser);
}
this.cancel();
return;
}

View File

@ -129,6 +129,71 @@ const TemporaryBlockedPermissions = {
},
};
// This hold a flag per browser to indicate whether we should show the
// user a notification as a permission has been requested that has been
// blocked globally. We only want to notify the user in the case that
// they actually requested the permission within the current page load
// so will clear the flag on navigation.
const GloballyBlockedPermissions = {
_stateByBrowser: new WeakMap(),
set(browser, id) {
if (!this._stateByBrowser.has(browser)) {
this._stateByBrowser.set(browser, {});
}
let entry = this._stateByBrowser.get(browser);
let prePath = browser.currentURI.prePath;
if (!entry[prePath]) {
entry[prePath] = {};
}
entry[prePath][id] = true;
// Listen to any top level navigations, once we see one clear the flag
// and remove the listener.
browser.addProgressListener({
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
if (aWebProgress.isTopLevel) {
GloballyBlockedPermissions.remove(browser, id);
browser.removeProgressListener(this);
}
},
});
},
// Removes a permission with the specified id for the specified browser.
remove(browser, id) {
let entry = this._stateByBrowser.get(browser);
let prePath = browser.currentURI.prePath;
if (entry && entry[prePath]) {
delete entry[prePath][id];
}
},
// Gets all permissions for the specified browser.
// Note that only permissions that apply to the current URI
// of the passed browser element will be returned.
getAll(browser) {
let permissions = [];
let entry = this._stateByBrowser.get(browser);
let prePath = browser.currentURI.prePath;
if (entry && entry[prePath]) {
let timeStamps = entry[prePath];
for (let id of Object.keys(timeStamps)) {
permissions.push({
id,
state: SitePermissions.BLOCK,
scope: SitePermissions.SCOPE_GLOBAL
});
}
}
return permissions;
},
};
/**
* A module to manage permanent and temporary permissions
* by URI and browser.
@ -153,6 +218,7 @@ var SitePermissions = {
SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}",
SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}",
SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}",
_defaultPrefBranch: Services.prefs.getBranch("permissions.default."),
@ -231,6 +297,10 @@ var SitePermissions = {
permissions[permission.id] = permission;
}
for (let permission of GloballyBlockedPermissions.getAll(browser)) {
permissions[permission.id] = permission;
}
for (let permission of this.getAllByURI(browser.currentURI)) {
permissions[permission.id] = permission;
}
@ -330,6 +400,23 @@ var SitePermissions = {
return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN);
},
/**
* Return whether the browser should notify the user if a permission was
* globally blocked due to a preference.
*
* @param {string} permissionID
* The ID to get the state for.
*
* @return boolean Whether to show notification for globally blocked permissions.
*/
showGloballyBlocked(permissionID) {
if (permissionID in gPermissionObject &&
gPermissionObject[permissionID].showGloballyBlocked)
return gPermissionObject[permissionID].showGloballyBlocked;
return false;
},
/**
* Returns the state and scope of a particular permission for a given URI.
*
@ -404,6 +491,13 @@ var SitePermissions = {
* This needs to be provided if the scope is SCOPE_TEMPORARY!
*/
set(uri, permissionID, state, scope = this.SCOPE_PERSISTENT, browser = null) {
if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) {
GloballyBlockedPermissions.set(browser, permissionID);
browser.dispatchEvent(new browser.ownerGlobal.CustomEvent("PermissionStateChange"));
return;
}
if (state == this.UNKNOWN || state == this.getDefault(permissionID)) {
// Because they are controlled by two prefs with many states that do not
// correspond to the classical ALLOW/DENY/PROMPT model, we want to always
@ -607,13 +701,14 @@ var gPermissionObject = {
"autoplay-media": {
exactHostMatch: true,
showGloballyBlocked: true,
getDefault() {
let state = Services.prefs.getIntPref("media.autoplay.default",
Ci.nsIAutoplay.PROMPT);
if (state == Ci.nsIAutoplay.ALLOW) {
if (state == Ci.nsIAutoplay.ALLOWED) {
return SitePermissions.ALLOW;
} if (state == Ci.nsIAutoplay.BLOCK) {
return SitePermissions.DENY;
} if (state == Ci.nsIAutoplay.BLOCKED) {
return SitePermissions.BLOCK;
}
return SitePermissions.UNKNOWN;
},

View File

@ -2010,7 +2010,7 @@ HTMLMediaElement::Load()
HasSourceChildren(this),
EventStateManager::IsHandlingUserInput(),
HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay),
AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED,
AutoplayPolicy::IsAllowedToPlay(*this),
OwnerDoc(),
DocumentOrigin(OwnerDoc()).get(),
OwnerDoc() ? OwnerDoc()->HasBeenUserGestureActivated() : 0,
@ -2529,7 +2529,7 @@ HTMLMediaElement::ResumeLoad(PreloadAction aAction)
bool
HTMLMediaElement::AllowedToPlay() const
{
return AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED;
return AutoplayPolicy::IsAllowedToPlay(*this);
}
void
@ -2538,7 +2538,7 @@ HTMLMediaElement::UpdatePreloadAction()
PreloadAction nextAction = PRELOAD_UNDEFINED;
// If autoplay is set, or we're playing, we should always preload data,
// as we'll need it to play.
if ((AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED &&
if ((AutoplayPolicy::IsAllowedToPlay(*this) &&
HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay)) ||
!mPaused) {
nextAction = HTMLMediaElement::PRELOAD_ENOUGH;
@ -3071,7 +3071,7 @@ HTMLMediaElement::PauseIfShouldNotBePlaying()
if (GetPaused()) {
return;
}
if (AutoplayPolicy::IsAllowedToPlay(*this) != nsIAutoplay::ALLOWED) {
if (!AutoplayPolicy::IsAllowedToPlay(*this)) {
AUTOPLAY_LOG("pause because not allowed to play, element=%p", this);
ErrorResult rv;
Pause(rv);
@ -4103,27 +4103,14 @@ HTMLMediaElement::Play(ErrorResult& aRv)
UpdateHadAudibleAutoplayState();
const bool handlingUserInput = EventStateManager::IsHandlingUserInput();
switch (AutoplayPolicy::IsAllowedToPlay(*this)) {
case nsIAutoplay::ALLOWED: {
mPendingPlayPromises.AppendElement(promise);
PlayInternal(handlingUserInput);
UpdateCustomPolicyAfterPlayed();
break;
}
case nsIAutoplay::BLOCKED: {
AUTOPLAY_LOG("%p play blocked.", this);
promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
if (StaticPrefs::MediaBlockEventEnabled()) {
DispatchAsyncEvent(NS_LITERAL_STRING("blocked"));
}
break;
}
case nsIAutoplay::PROMPT: {
// Prompt the user for permission to play.
mPendingPlayPromises.AppendElement(promise);
EnsureAutoplayRequested(handlingUserInput);
break;
}
if (AutoplayPolicy::IsAllowedToPlay(*this)) {
mPendingPlayPromises.AppendElement(promise);
PlayInternal(handlingUserInput);
UpdateCustomPolicyAfterPlayed();
} else {
// Prompt the user for permission to play.
mPendingPlayPromises.AppendElement(promise);
EnsureAutoplayRequested(handlingUserInput);
}
return promise.forget();
}
@ -6151,8 +6138,7 @@ HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState)
DispatchAsyncEvent(NS_LITERAL_STRING("canplay"));
if (!mPaused) {
if (mDecoder && !mPausedForInactiveDocumentOrChannel) {
MOZ_ASSERT(AutoplayPolicy::IsAllowedToPlay(*this) ==
nsIAutoplay::ALLOWED);
MOZ_ASSERT(AutoplayPolicy::IsAllowedToPlay(*this));
mDecoder->Play();
}
NotifyAboutPlaying();
@ -6264,14 +6250,9 @@ HTMLMediaElement::CheckAutoplayDataReady()
}
UpdateHadAudibleAutoplayState();
switch (AutoplayPolicy::IsAllowedToPlay(*this)) {
case nsIAutoplay::BLOCKED:
return;
case nsIAutoplay::PROMPT:
EnsureAutoplayRequested(false);
return;
case nsIAutoplay::ALLOWED:
break;
if (!AutoplayPolicy::IsAllowedToPlay(*this)) {
EnsureAutoplayRequested(false);
return;
}
mPaused = false;

View File

@ -156,7 +156,7 @@ AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(const HTMLMediaElement& a
return IsMediaElementAllowedToPlay(aElement);
}
/* static */ uint32_t
/* static */ bool
AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
{
const uint32_t autoplayDefault = DefaultAutoplayBehaviour();
@ -166,15 +166,19 @@ AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
// If element is blessed, it would always be allowed to play().
return (autoplayDefault == nsIAutoplay::ALLOWED ||
aElement.IsBlessed() ||
EventStateManager::IsHandlingUserInput())
? nsIAutoplay::ALLOWED : nsIAutoplay::BLOCKED;
EventStateManager::IsHandlingUserInput());
}
const uint32_t result = IsMediaElementAllowedToPlay(aElement) ?
nsIAutoplay::ALLOWED : autoplayDefault;
if (IsMediaElementAllowedToPlay(aElement)) {
return true;
}
const bool result = IsMediaElementAllowedToPlay(aElement) ||
autoplayDefault == nsIAutoplay::ALLOWED;
AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s",
&aElement, AllowAutoplayToStr(result));
return result;
}

View File

@ -36,7 +36,7 @@ class AutoplayPolicy
{
public:
// Returns whether a given media element is allowed to play.
static uint32_t IsAllowedToPlay(const HTMLMediaElement& aElement);
static bool IsAllowedToPlay(const HTMLMediaElement& aElement);
// Returns true if a given media element would be allowed to play
// if block autoplay was enabled. If this returns false, it means we would