Bug 1476555 - Show notification when autoplay blocked globally. r=cpearce, r=johannh
MozReview-Commit-ID: EI0GiaoBNqX
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -1010,17 +1010,19 @@ var gIdentityHandler = {
let nameLabel = document.createXULElement("label");
nameLabel.setAttribute("flex", "1");
nameLabel.setAttribute("class", "identity-popup-permission-label");
nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id);
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");
let menupopup = document.createXULElement("menupopup");
let block = document.createXULElement("vbox");
block.setAttribute("id", "identity-popup-popup-container");
menulist.setAttribute("sizetopopup", "none");
menulist.setAttribute("class", "identity-popup-popup-menulist");
@@ -1078,18 +1080,18 @@ var gIdentityHandler = {
stateLabel.textContent = SitePermissions.getCurrentStateLabel(state, aPermission.id, scope);
container.appendChild(img);
container.appendChild(nameLabel);
container.appendChild(stateLabel);
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;
}
let button = document.createXULElement("button");
button.setAttribute("class", "identity-popup-permission-remove-button");
let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
button.setAttribute("tooltiptext", tooltiptext);
--- a/browser/base/content/pageinfo/permissions.js
+++ b/browser/base/content/pageinfo/permissions.js
@@ -92,17 +92,17 @@ function initRow(aPartId) {
if (state != defaultState) {
checkbox.checked = false;
command.removeAttribute("disabled");
} else {
checkbox.checked = true;
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");
}
setRadioState(aPartId, state);
}
function createRow(aPartId) {
--- a/browser/base/content/test/permissions/browser.ini
+++ b/browser/base/content/test/permissions/browser.ini
@@ -5,11 +5,15 @@ support-files=
[browser_canvas_fingerprinting_resistance.js]
[browser_permissions.js]
[browser_reservedkey.js]
[browser_temporary_permissions.js]
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]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js
@@ -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");
+});
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -274,16 +274,29 @@ var PermissionPromptPrototype = {
// If we're reading and setting permissions, then we need
// to check to see if we already have a permission setting
// for this particular principal.
let {state} = SitePermissions.get(requestingURI,
this.permissionKey,
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;
}
if (state == SitePermissions.ALLOW) {
this.allow();
return;
}
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -124,16 +124,81 @@ const TemporaryBlockedPermissions = {
copy(browser, newBrowser) {
let entry = this._stateByBrowser.get(browser);
if (entry) {
this._stateByBrowser.set(newBrowser, entry);
}
},
};
+// 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.
*
* Some methods have the side effect of dispatching a "PermissionStateChange"
* event on changes to temporary permissions, as mentioned in the respective docs.
*/
var SitePermissions = {
@@ -148,16 +213,17 @@ var SitePermissions = {
PROMPT_HIDE: Ci.nsIObjectLoadingContent.PLUGIN_PERMISSION_PROMPT_ACTION_QUIET,
// Permission scopes.
SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}",
SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}",
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."),
/**
* Gets all custom permissions for a given URI.
* Install addon permission is excluded, check bug 1303108.
*
* @return {Array} a list of objects with the keys:
@@ -226,16 +292,20 @@ var SitePermissions = {
getAllForBrowser(browser) {
let permissions = {};
for (let permission of TemporaryBlockedPermissions.getAll(browser)) {
permission.scope = this.SCOPE_TEMPORARY;
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;
}
return Object.values(permissions);
},
/**
@@ -326,16 +396,33 @@ var SitePermissions = {
gPermissionObject[permissionID].getDefault)
return gPermissionObject[permissionID].getDefault();
// Otherwise try to get the default preference for that permission.
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.
*
* This method will NOT dispatch a "PermissionStateChange" event on the specified
* browser if a temporary permission was removed because it has expired.
*
* @param {nsIURI} uri
* The URI to check.
* @param {String} permissionID
@@ -399,16 +486,23 @@ var SitePermissions = {
* The state of the permission.
* @param {SitePermissions scope} scope (optional)
* The scope of the permission. Defaults to SCOPE_PERSISTENT.
* @param {Browser} browser (optional)
* The browser object to set temporary permissions on.
* 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
// allow the user to add exceptions to their cookie rules without removing them.
if (permissionID != "cookie") {
this.remove(uri, permissionID, browser);
return;
}
@@ -602,23 +696,24 @@ var gPermissionObject = {
* Array of permission states to be exposed to the user.
* Defaults to ALLOW, BLOCK and the default state (see getDefault).
* The PROMPT_HIDE state is deliberately excluded from "plugin:flash" since we
* don't want to expose a "Hide Prompt" button to the user through pageinfo.
*/
"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;
},
labelID: "autoplay-media"
},
"image": {
states: [ SitePermissions.ALLOW, SitePermissions.BLOCK ],
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -2005,17 +2005,17 @@ HTMLMediaElement::Load()
"ownerDoc=%p (%s) ownerDocUserActivated=%d "
"muted=%d volume=%f",
this,
!!mSrcAttrStream,
HasAttr(kNameSpaceID_None, nsGkAtoms::src),
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,
mMuted,
mVolume));
if (mIsRunningLoadMethod) {
return;
@@ -2524,26 +2524,26 @@ HTMLMediaElement::ResumeLoad(PreloadActi
LoadFromSourceChildren();
}
}
}
bool
HTMLMediaElement::AllowedToPlay() const
{
- return AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED;
+ return AutoplayPolicy::IsAllowedToPlay(*this);
}
void
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;
} else {
// Find the appropriate preload action by looking at the attribute.
const nsAttrValue* val =
mAttrs.GetAttr(nsGkAtoms::preload, kNameSpaceID_None);
// MSE doesn't work if preload is none, so it ignores the pref when src is
@@ -3066,17 +3066,17 @@ HTMLMediaElement::SetMutedInternal(uint3
}
void
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);
OwnerDoc()->SetDocTreeHadPlayRevoked();
}
}
void
@@ -4098,37 +4098,24 @@ HTMLMediaElement::Play(ErrorResult& aRv)
DispatchAsyncEvent(NS_LITERAL_STRING("blocked"));
}
return promise.forget();
}
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();
}
void
HTMLMediaElement::EnsureAutoplayRequested(bool aHandlingUserInput)
{
if (mAutoplayPermissionRequest.Exists()) {
@@ -6146,18 +6133,17 @@ HTMLMediaElement::ChangeReadyState(nsMed
DispatchAsyncEvent(NS_LITERAL_STRING("loadeddata"));
mLoadedDataFired = true;
}
if (oldState < HAVE_FUTURE_DATA && mReadyState >= HAVE_FUTURE_DATA) {
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();
}
}
CheckAutoplayDataReady();
@@ -6259,24 +6245,19 @@ HTMLMediaElement::CanActivateAutoplay()
void
HTMLMediaElement::CheckAutoplayDataReady()
{
if (!CanActivateAutoplay()) {
return;
}
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;
// We changed mPaused which can affect AddRemoveSelfReference
AddRemoveSelfReference();
UpdateSrcMediaStreamPlaying();
UpdateAudioChannelPlayingState();
--- a/dom/media/AutoplayPolicy.cpp
+++ b/dom/media/AutoplayPolicy.cpp
@@ -151,35 +151,39 @@ IsMediaElementAllowedToPlay(const HTMLMe
}
/* static */ bool
AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(const HTMLMediaElement& aElement)
{
return IsMediaElementAllowedToPlay(aElement);
}
-/* static */ uint32_t
+/* static */ bool
AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
{
const uint32_t autoplayDefault = DefaultAutoplayBehaviour();
// TODO : this old way would be removed when user-gestures-needed becomes
// as a default option to block autoplay.
if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false)) {
// 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;
}
/* static */ bool
AutoplayPolicy::IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext)
{
if (!Preferences::GetBool("media.autoplay.block-webaudio", false)) {
return true;
--- a/dom/media/AutoplayPolicy.h
+++ b/dom/media/AutoplayPolicy.h
@@ -31,17 +31,17 @@ class AudioContext;
* We restrict user gestures to "mouse click", "keyboard press" and "touch".
* 2) Muted media content or video without audio content.
* 3) Document's origin has the "autoplay-media" permission.
*/
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
// either block or ask for permission.
// Note: this is for telemetry purposes, and doesn't check the prefs
// which enable/disable block autoplay. Do not use for blocking logic!
static bool WouldBeAllowedToPlayIfAutoplayDisabled(const HTMLMediaElement& aElement);