--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -528,16 +528,19 @@ pref("privacy.sanitize.timeSpan", 1);
pref("privacy.sanitize.sanitizeOnShutdown", false);
pref("privacy.sanitize.migrateFx3Prefs", false);
pref("privacy.panicButton.enabled", true);
pref("privacy.firstparty.isolate", false);
+// Time until temporary permissions expire, in ms
+pref("privacy.temporary_permission_expire_time_ms", 3600000);
+
pref("network.proxy.share_proxy_settings", false); // use the same proxy settings for all protocols
// simple gestures support
pref("browser.gesture.swipe.left", "Browser:BackOrBackDuplicate");
pref("browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate");
pref("browser.gesture.swipe.up", "cmd_scrollTop");
pref("browser.gesture.swipe.down", "cmd_scrollBottom");
#ifdef XP_MACOSX
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1122,16 +1122,20 @@ var gBrowserInit = {
// Reset the zoom for the tabcrashed page.
ZoomManager.setZoomForBrowser(browser, 1);
}, false, true);
gBrowser.addEventListener("InsecureLoginFormsStateChange", function() {
gIdentityHandler.refreshForInsecureLoginForms();
});
+ gBrowser.addEventListener("PermissionStateChange", function() {
+ gIdentityHandler.refreshIdentityBlock();
+ });
+
let uriToLoad = this._getUriToLoad();
if (uriToLoad && uriToLoad != "about:blank") {
if (uriToLoad instanceof Ci.nsIArray) {
let count = uriToLoad.length;
let specs = [];
for (let i = 0; i < count; i++) {
let urisstring = uriToLoad.queryElementAt(i, Ci.nsISupportsString);
specs.push(urisstring.data);
@@ -3220,16 +3224,20 @@ function BrowserReloadWithFlags(reloadFl
if (gBrowser.updateBrowserRemotenessByURL(gBrowser.selectedBrowser, url)) {
// If the remoteness has changed, the new browser doesn't have any
// information of what was loaded before, so we need to load the previous
// URL again.
gBrowser.loadURIWithFlags(url, reloadFlags);
return;
}
+ // Reset temporary permissions on the current tab. This is done here
+ // because we only want to reset permissions on user reload.
+ SitePermissions.clearTemporaryPermissions(gBrowser.selectedBrowser);
+
let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
gBrowser.selectedBrowser
.messageManager
.sendAsyncMessage("Browser:Reload",
{ flags: reloadFlags,
handlingUserInput: windowUtils.isHandlingUserInput });
@@ -3322,17 +3330,17 @@ function getPEMString(cert) {
var PrintPreviewListener = {
_printPreviewTab: null,
_tabBeforePrintPreview: null,
_simplifyPageTab: null,
getPrintPreviewBrowser() {
if (!this._printPreviewTab) {
- let browser = gBrowser.selectedTab.linkedBrowser;
+ let browser = gBrowser.selectedBrowser;
let preferredRemoteType = browser.remoteType;
this._tabBeforePrintPreview = gBrowser.selectedTab;
this._printPreviewTab = gBrowser.loadOneTab("about:blank",
{ inBackground: false,
preferredRemoteType,
relatedBrowser: browser });
gBrowser.selectedTab = this._printPreviewTab;
}
@@ -4638,18 +4646,22 @@ var XULBrowserWindow = {
// aRequest will be null always in case 2 and 3, and sometimes in case 1 (for
// instance, there won't be a request when STATE_BLOCKED_TRACKING_CONTENT is observed).
onSecurityChange(aWebProgress, aRequest, aState, aIsSimulated) {
// Don't need to do anything if the data we use to update the UI hasn't
// changed
let uri = gBrowser.currentURI;
let spec = uri.spec;
if (this._state == aState &&
- this._lastLocation == spec)
+ this._lastLocation == spec) {
+ // Switching to a tab of the same URL doesn't change most security
+ // information, but tab specific permissions may be different.
+ gIdentityHandler.refreshIdentityBlock();
return;
+ }
this._state = aState;
this._lastLocation = spec;
if (typeof(aIsSimulated) != "boolean" && typeof(aIsSimulated) != "undefined") {
throw "onSecurityChange: aIsSimulated receieved an unexpected type";
}
// Make sure the "https" part of the URL is striked out or not,
@@ -7016,26 +7028,26 @@ var gIdentityHandler = {
for (let icon of Object.values(permissionAnchors)) {
icon.removeAttribute("showing");
}
// keeps track if we should show an indicator that there are active permissions
let hasGrantedPermissions = false;
// show permission icons
- for (let permission of SitePermissions.getAllByURI(this._uri)) {
- if (permission.state === SitePermissions.BLOCK) {
+ let permissions = SitePermissions.getAllForBrowser(gBrowser.selectedBrowser);
+ for (let permission of permissions) {
+ if (permission.state == SitePermissions.BLOCK) {
let icon = permissionAnchors[permission.id];
if (icon) {
icon.setAttribute("showing", "true");
}
- } else if (permission.state === SitePermissions.ALLOW ||
- permission.state === SitePermissions.SESSION) {
+ } else if (permission.state != SitePermissions.UNKNOWN) {
hasGrantedPermissions = true;
}
}
if (hasGrantedPermissions) {
this._identityBox.classList.add("grantedPermissions");
}
@@ -7360,19 +7372,19 @@ var gIdentityHandler = {
this._permissionEmptyHint.removeAttribute("hidden");
}
},
updateSitePermissions() {
while (this._permissionList.hasChildNodes())
this._permissionList.removeChild(this._permissionList.lastChild);
- let uri = gBrowser.currentURI;
-
- let permissions = SitePermissions.getPermissionDetailsByURI(uri);
+ let permissions =
+ SitePermissions.getAllPermissionDetailsForBrowser(gBrowser.selectedBrowser);
+
if (this._sharingState) {
// If WebRTC device or screen permissions are in use, we need to find
// the associated permission item to set the inUse field to true.
for (let id of ["camera", "microphone", "screen"]) {
if (this._sharingState[id]) {
let found = false;
for (let permission of permissions) {
if (permission.id != id)
@@ -7380,17 +7392,18 @@ var gIdentityHandler = {
found = true;
permission.inUse = true;
break;
}
if (!found) {
// If the permission item we were looking for doesn't exist,
// the user has temporarily allowed sharing and we need to add
// an item in the permissions array to reflect this.
- let permission = SitePermissions.getPermissionItem(id);
+ let permission =
+ SitePermissions.getPermissionDetails(id, SitePermissions.SCOPE_REQUEST);
permission.inUse = true;
permissions.push(permission);
}
}
}
}
for (let permission of permissions) {
let item = this._createPermissionItem(permission);
@@ -7436,67 +7449,83 @@ var gIdentityHandler = {
let nameLabel = document.createElement("label");
nameLabel.setAttribute("flex", "1");
nameLabel.setAttribute("class", "identity-popup-permission-label");
nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id);
let stateLabel = document.createElement("label");
stateLabel.setAttribute("flex", "1");
stateLabel.setAttribute("class", "identity-popup-permission-state-label");
- stateLabel.textContent = SitePermissions.getStateLabel(
- aPermission.id, aPermission.state, aPermission.inUse || false);
+ let {state, scope} = aPermission;
+ // If the user did not permanently allow this device but it is currently
+ // used, set the variables to display a "temporarily allowed" info.
+ if (state != SitePermissions.ALLOW && aPermission.inUse) {
+ state = SitePermissions.ALLOW;
+ scope = SitePermissions.SCOPE_REQUEST;
+ }
+ stateLabel.textContent = SitePermissions.getStateLabel(state, scope);
let button = document.createElement("button");
button.setAttribute("class", "identity-popup-permission-remove-button");
let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
button.setAttribute("tooltiptext", tooltiptext);
button.addEventListener("command", () => {
+ let browser = gBrowser.selectedBrowser;
// Only resize the window if the reload hint was previously hidden.
this._handleHeightChange(() => this._permissionList.removeChild(container),
this._permissionReloadHint.hasAttribute("hidden"));
if (aPermission.inUse &&
["camera", "microphone", "screen"].includes(aPermission.id)) {
let windowId = this._sharingState.windowId;
if (aPermission.id == "screen") {
windowId = "screen:" + windowId;
} else {
// If we set persistent permissions or the sharing has
// started due to existing persistent permissions, we need
// to handle removing these even for frames with different hostnames.
- let uris = gBrowser.selectedBrowser._devicePermissionURIs || [];
+ let uris = browser._devicePermissionURIs || [];
for (let uri of uris) {
// It's not possible to stop sharing one of camera/microphone
// without the other.
for (let id of ["camera", "microphone"]) {
- if (this._sharingState[id] &&
- SitePermissions.get(uri, id) == SitePermissions.ALLOW)
- SitePermissions.remove(uri, id);
+ if (this._sharingState[id]) {
+ let perm = SitePermissions.get(uri, id);
+ if (perm.state == SitePermissions.ALLOW &&
+ perm.scope == SitePermissions.SCOPE_PERSISTENT) {
+ SitePermissions.remove(uri, id);
+ }
+ }
}
}
}
- let mm = gBrowser.selectedBrowser.messageManager;
- mm.sendAsyncMessage("webrtc:StopSharing", windowId);
+ browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
}
- SitePermissions.remove(gBrowser.currentURI, aPermission.id);
+ SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser);
this._permissionReloadHint.removeAttribute("hidden");
// Set telemetry values for clearing a permission
let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
let permissionType = 0;
- if (aPermission.state == SitePermissions.ALLOW) {
+ if (aPermission.state == SitePermissions.ALLOW &&
+ aPermission.scope == SitePermissions.SCOPE_PERSISTENT) {
// 1 : clear permanently allowed permission
permissionType = 1;
- } else if (aPermission.state == SitePermissions.BLOCK) {
+ } else if (aPermission.state == SitePermissions.BLOCK &&
+ aPermission.scope == SitePermissions.SCOPE_PERSISTENT) {
// 2 : clear permanently blocked permission
permissionType = 2;
+ } else if (aPermission.state == SitePermissions.ALLOW) {
+ // 3 : clear temporary allowed permission
+ permissionType = 3;
+ } else if (aPermission.state == SitePermissions.BLOCK) {
+ // 4 : clear temporary blocked permission
+ permissionType = 4;
}
- // 3 : TODO clear temporary allowed permission
- // 4 : TODO clear temporary blocked permission
histogram.add("(all)", permissionType);
histogram.add(aPermission.id, permissionType);
});
container.appendChild(img);
container.appendChild(nameLabel);
container.appendChild(stateLabel);
--- a/browser/base/content/pageinfo/permissions.js
+++ b/browser/base/content/pageinfo/permissions.js
@@ -68,27 +68,27 @@ function initRow(aPartId) {
initPluginsRow();
return;
}
createRow(aPartId);
var checkbox = document.getElementById(aPartId + "Def");
var command = document.getElementById("cmd_" + aPartId + "Toggle");
- var perm = SitePermissions.get(gPermURI, aPartId);
+ var {state} = SitePermissions.get(gPermURI, aPartId);
- if (perm) {
+ if (state != SitePermissions.UNKNOWN) {
checkbox.checked = false;
command.removeAttribute("disabled");
} else {
checkbox.checked = true;
command.setAttribute("disabled", "true");
- perm = SitePermissions.getDefault(aPartId);
+ state = SitePermissions.getDefault(aPartId);
}
- setRadioState(aPartId, perm);
+ setRadioState(aPartId, state);
if (aPartId == "indexedDB") {
initIndexedDBRow();
}
}
function createRow(aPartId) {
let rowId = "perm-" + aPartId + "-row";
@@ -130,17 +130,17 @@ function createRow(aPartId) {
controls.appendChild(spacer);
let radiogroup = document.createElement("radiogroup");
radiogroup.setAttribute("id", radiogroupId);
radiogroup.setAttribute("orient", "horizontal");
for (let state of SitePermissions.getAvailableStates(aPartId)) {
let radio = document.createElement("radio");
radio.setAttribute("id", aPartId + "#" + state);
- radio.setAttribute("label", SitePermissions.getStateLabel(aPartId, state));
+ radio.setAttribute("label", SitePermissions.getStateLabel(state));
radio.setAttribute("command", commandId);
radiogroup.appendChild(radio);
}
controls.appendChild(radiogroup);
row.appendChild(controls);
document.getElementById("permList").appendChild(row);
@@ -309,12 +309,12 @@ function initPluginsRow() {
}
function setPluginsRadioState() {
let box = document.getElementById("perm-plugins-row");
for (let permissionEntry of box.childNodes) {
if (permissionEntry.hasAttribute("permString")) {
let permString = permissionEntry.getAttribute("permString");
let permission = SitePermissions.get(gPermURI, permString);
- setRadioState(permString, permission);
+ setRadioState(permString, permission.state);
}
}
}
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2858,16 +2858,18 @@
}
if (aOtherTab.hasAttribute("sharing")) {
aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
modifiedAttrs.push("sharing");
aOurTab._sharingState = aOtherTab._sharingState;
webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
}
+ SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
+
// If the other tab is pending (i.e. has not been restored, yet)
// then do not switch docShells but retrieve the other tab's state
// and apply it to our tab.
if (isPending) {
SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
// Make sure to unregister any open URIs.
this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -415,16 +415,21 @@ skip-if = os == "linux" || os == "mac" #
skip-if = (os == "mac" && !e10s) # Bug 1237713 - OSX eats keypresses for some reason
[browser_tabopen_reflows.js]
[browser_tabs_close_beforeunload.js]
support-files =
close_beforeunload_opens_second_tab.html
close_beforeunload.html
[browser_tabs_isActive.js]
[browser_tabs_owner.js]
+[browser_temporary_permissions.js]
+support-files =
+ permissions.html
+ temporary_permissions_subframe.html
+[browser_temporary_permissions_navigation.js]
[browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js]
run-if = e10s
[browser_trackingUI_1.js]
tags = trackingprotection
support-files =
trackingPage.html
benignPage.html
[browser_trackingUI_2.js]
--- a/browser/base/content/test/general/browser_permissions.js
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -85,17 +85,17 @@ add_task(function* testIdentityIcon() {
ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
"identity-box doesn't signal granted permissions");
SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
"identity-box doesn't signal granted permissions");
- SitePermissions.set(gBrowser.currentURI, "cookie", SitePermissions.SESSION);
+ SitePermissions.set(gBrowser.currentURI, "cookie", SitePermissions.ALLOW_COOKIES_FOR_SESSION);
ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
"identity-box signals granted permissions");
SitePermissions.remove(gBrowser.currentURI, "geo");
SitePermissions.remove(gBrowser.currentURI, "camera");
SitePermissions.remove(gBrowser.currentURI, "cookie");
});
@@ -174,29 +174,23 @@ add_task(function* testPermissionHints()
add_task(function* testPermissionIcons() {
let {gIdentityHandler} = gBrowser.ownerGlobal;
let tab = gBrowser.selectedTab = gBrowser.addTab();
yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.BLOCK);
- SitePermissions.set(gBrowser.currentURI, "microphone", SitePermissions.SESSION);
let geoIcon = gIdentityHandler._identityBox
.querySelector(".blocked-permission-icon[data-permission-id='geo']");
ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
let cameraIcon = gIdentityHandler._identityBox
.querySelector(".blocked-permission-icon[data-permission-id='camera']");
ok(!cameraIcon.hasAttribute("showing"),
"allowed permission icon is not shown");
- let microphoneIcon = gIdentityHandler._identityBox
- .querySelector(".blocked-permission-icon[data-permission-id='microphone']");
- ok(!microphoneIcon.hasAttribute("showing"),
- "allowed permission icon is not shown");
-
SitePermissions.remove(gBrowser.currentURI, "geo");
ok(!geoIcon.hasAttribute("showing"),
"blocked permission icon is not shown after reset");
});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_temporary_permissions.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource:///modules/SitePermissions.jsm", this);
+Cu.import("resource:///modules/E10SUtils.jsm");
+
+const SUBFRAME_PAGE = "https://example.com/browser/browser/base/content/test/general/temporary_permissions_subframe.html";
+
+// Test that setting temp permissions triggers a change in the identity block.
+add_task(function* testTempPermissionChangeEvents() {
+ let uri = NetUtil.newURI("https://example.com");
+ let id = "geo";
+
+ yield BrowserTestUtils.withNewTab(uri.spec, function*(browser) {
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ let geoIcon = document.querySelector(".blocked-permission-icon[data-permission-id=geo]");
+
+ Assert.notEqual(geoIcon.boxObject.width, 0, "geo anchor should be visible");
+
+ SitePermissions.remove(uri, id, browser);
+
+ Assert.equal(geoIcon.boxObject.width, 0, "geo anchor should not be visible");
+ });
+});
+
+// Test that temp permissions are persisted through moving tabs to new windows.
+add_task(function* testTempPermissionOnTabMove() {
+ let uri = NetUtil.newURI("https://example.com");
+ let id = "geo";
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, tab.linkedBrowser);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, tab.linkedBrowser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(tab);
+ let win = yield promiseWin;
+ tab = win.gBrowser.selectedTab;
+
+ Assert.deepEqual(SitePermissions.get(uri, id, tab.linkedBrowser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ SitePermissions.remove(uri, id, tab.linkedBrowser);
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+// Test that temp permissions don't affect other tabs of the same URI.
+add_task(function* testTempPermissionMultipleTabs() {
+ let uri = NetUtil.newURI("https://example.com");
+ let id = "geo";
+
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, tab2.linkedBrowser);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, tab2.linkedBrowser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ Assert.deepEqual(SitePermissions.get(uri, id, tab1.linkedBrowser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ let geoIcon = document.querySelector(".blocked-permission-icon[data-permission-id=geo]");
+
+ Assert.notEqual(geoIcon.boxObject.width, 0, "geo anchor should be visible");
+
+ yield BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ Assert.equal(geoIcon.boxObject.width, 0, "geo anchor should not be visible");
+
+ SitePermissions.remove(uri, id, tab2.linkedBrowser);
+ yield BrowserTestUtils.removeTab(tab1);
+ yield BrowserTestUtils.removeTab(tab2);
+});
+
+// Test that temp blocked permissions requested by subframes (with a different URI) affect the whole page.
+add_task(function* testTempPermissionSubframes() {
+ let uri = NetUtil.newURI("https://example.com");
+ let id = "geo";
+
+ yield BrowserTestUtils.withNewTab(SUBFRAME_PAGE, function*(browser) {
+ let popupshown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+
+ // Request a permission;
+ yield ContentTask.spawn(browser, uri.host, function(host) {
+ E10SUtils.wrapHandlingUserInput(content, true, function() {
+ let frame = content.document.getElementById("frame");
+ let frameDoc = frame.contentWindow.document;
+
+ // Make sure that the origin of our test page is different.
+ Assert.notEqual(frameDoc.location.host, host);
+
+ frameDoc.getElementById("geo").click();
+ });
+ });
+
+ yield popupshown;
+
+ let popuphidden = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+
+ let notification = PopupNotifications.panel.firstChild;
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+
+ yield popuphidden;
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+ });
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_temporary_permissions_navigation.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource:///modules/SitePermissions.jsm", this);
+
+// Test that temporary permissions are removed on user initiated reload only.
+add_task(function* testTempPermissionOnReload() {
+ let uri = NetUtil.newURI("https://example.com");
+ let id = "geo";
+
+ yield BrowserTestUtils.withNewTab(uri.spec, function*(browser) {
+ let reloadButton = document.getElementById("urlbar-reload-button");
+
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+ let reloaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ // Reload through the page (should not remove the temp permission).
+ yield ContentTask.spawn(browser, {}, () => content.document.location.reload());
+
+ yield reloaded;
+ yield BrowserTestUtils.waitForCondition(() => {
+ return reloadButton.disabled == false;
+ });
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ // Reload as a user (should remove the temp permission).
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {});
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ SitePermissions.remove(uri, id, browser);
+ });
+});
+
+// Test that temporary permissions are persisted through navigation in a tab.
+add_task(function* testTempPermissionOnNavigation() {
+ let uri = NetUtil.newURI("https://example.com/");
+ let id = "geo";
+
+ yield BrowserTestUtils.withNewTab(uri.spec, function*(browser) {
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, "https://example.org/");
+
+ // Navigate to another domain.
+ yield ContentTask.spawn(browser, {}, () => content.document.location = "https://example.org/");
+
+ yield loaded;
+
+ // The temporary permissions for the current URI should be reset.
+ Assert.deepEqual(SitePermissions.get(browser.currentURI, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ loaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+
+ // Navigate to the original domain.
+ yield ContentTask.spawn(browser, {}, () => content.document.location = "https://example.com/");
+
+ yield loaded;
+
+ // The temporary permissions for the original URI should still exist.
+ Assert.deepEqual(SitePermissions.get(browser.currentURI, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ SitePermissions.remove(uri, id, browser);
+ });
+});
+
--- a/browser/base/content/test/general/permissions.html
+++ b/browser/base/content/test/general/permissions.html
@@ -4,10 +4,11 @@
- 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>
<!-- This page could eventually request permissions from content
and make sure that chrome responds appropriately -->
+ <button id="geo" onclick="navigator.geolocation.getCurrentPosition(() => {})">Geolocation</button>
</body>
</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/temporary_permissions_subframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Temporary Permissions Subframe Test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <iframe id="frame" src="https://example.org/browser/browser/base/content/test/general/permissions.html" />
+</body>
+</html>
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -114,16 +114,44 @@ var gTests = [
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
yield checkNotSharing();
+
+ // Verify that we set 'Temporarily blocked' permissions.
+ let browser = gBrowser.selectedBrowser;
+ let blockedPerms = document.getElementById("blocked-permissions-container");
+
+ let {state, scope} = SitePermissions.get(null, "camera", browser);
+ Assert.equal(state, SitePermissions.BLOCK);
+ Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
+ ok(blockedPerms.querySelector(".blocked-permission-icon.camera-icon[showing=true]"),
+ "the blocked camera icon is shown");
+
+ ({state, scope} = SitePermissions.get(null, "microphone", browser));
+ Assert.equal(state, SitePermissions.BLOCK);
+ Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
+ ok(blockedPerms.querySelector(".blocked-permission-icon.microphone-icon[showing=true]"),
+ "the blocked microphone icon is shown");
+
+ info("requesting devices again to check temporarily blocked permissions");
+ promise = promiseMessage(permissionError);
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ yield expectObserverCalled("getUserMedia:response:deny");
+ yield expectObserverCalled("recording-window-ended");
+ yield checkNotSharing();
+
+ SitePermissions.remove(browser.currentURI, "camera", browser);
+ SitePermissions.remove(browser.currentURI, "microphone", browser);
}
},
{
desc: "getUserMedia audio+video: stop sharing",
run: function* checkStopSharing() {
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
@@ -176,17 +204,17 @@ var gTests = [
},
{
desc: "getUserMedia prompt: Always/Never Share",
run: function* checkRememberCheckbox() {
let elt = id => document.getElementById(id);
function* checkPerm(aRequestAudio, aRequestVideo,
- aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
+ aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(aRequestAudio, aRequestVideo);
yield promise;
yield expectObserverCalled("getUserMedia:request");
is(elt("webRTC-selectMicrophone").hidden, !aRequestAudio,
"microphone selector expected to be " + (aRequestAudio ? "visible" : "hidden"));
@@ -275,16 +303,19 @@ var gTests = [
yield expectObserverCalled("getUserMedia:request");
// Deny the request to cleanup...
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.remove(null, "camera", browser);
+ SitePermissions.remove(null, "microphone", browser);
} else {
let expectedMessage = aExpectStream ? "ok" : permissionError;
let promise = promiseMessage(expectedMessage);
yield promiseRequestDevice(aRequestAudio, aRequestVideo);
yield promise;
if (expectedMessage == "ok") {
yield expectObserverCalled("getUserMedia:request");
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -330,16 +330,18 @@ var gTests = [
yield promiseMessage(permissionError, () => {
PopupNotifications.panel.firstChild.button.click();
});
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
yield checkNotSharing();
+ SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
+ SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
}
},
{
desc: "getUserMedia screen, user clicks \"Don't Allow\"",
run: function* checkDontShare() {
let promise = promisePopupNotificationShown("webRTC-shareDevices");
@@ -350,16 +352,18 @@ var gTests = [
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
yield checkNotSharing();
+ SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
+ SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
}
},
{
desc: "getUserMedia audio+video+screen: stop sharing",
run: function* checkStopSharing() {
if (AppConstants.platform == "macosx") {
todo(false, "Bug 1323481 - On Mac on treeherder, but not locally, requesting microphone + screen never makes the permission prompt appear, and so causes the test to timeout");
@@ -494,20 +498,20 @@ var gTests = [
yield closeStream();
}
},
{
desc: "Only persistent block is possible for screen sharing",
run: function* checkPersistentPermissions() {
- let Perms = Services.perms;
- let uri = gBrowser.selectedBrowser.documentURI;
- let devicePerms = Perms.testExactPermission(uri, "screen");
- is(devicePerms, Perms.UNKNOWN_ACTION,
+ let browser = gBrowser.selectedBrowser;
+ let uri = browser.documentURI;
+ let devicePerms = SitePermissions.get(uri, "screen", browser);
+ is(devicePerms.state, SitePermissions.UNKNOWN,
"starting without screen persistent permissions");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(false, true, null, "screen");
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(false, false, true);
document.getElementById("webRTC-selectWindow-menulist")
@@ -528,27 +532,30 @@ var gTests = [
// Click "Don't Allow" to save a persistent block permission.
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
yield checkNotSharing();
- is(Perms.testExactPermission(uri, "screen"), Perms.DENY_ACTION,
+ let permission = SitePermissions.get(uri, "screen", browser);
+ is(permission.state, SitePermissions.BLOCK,
+ "screen sharing is blocked");
+ is(permission.scope, SitePermissions.SCOPE_PERSISTENT,
"screen sharing is persistently blocked");
// Request screensharing again, expect an immediate failure.
promise = promiseMessage(permissionError);
yield promiseRequestDevice(false, true, null, "screen");
yield promise;
yield expectObserverCalled("recording-window-ended");
// Now set the permission to allow and expect a prompt.
- Perms.add(uri, "screen", Perms.ALLOW_ACTION);
+ SitePermissions.set(uri, "screen", SitePermissions.ALLOW);
// Request devices and expect a prompt despite the saved 'Allow' permission.
promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(false, true, null, "screen");
yield promise;
yield expectObserverCalled("getUserMedia:request");
// The 'remember' checkbox shouldn't be checked anymore.
@@ -559,17 +566,17 @@ var gTests = [
ok(!checkbox.checked, "checkbox is not checked");
// Deny the request to cleanup...
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
- Perms.remove(uri, "screen");
+ SitePermissions.remove(uri, "screen", browser);
}
}
];
function test() {
waitForExplicitFinish();
--- a/browser/base/content/test/webrtc/head.js
+++ b/browser/base/content/test/webrtc/head.js
@@ -1,9 +1,10 @@
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource:///modules/SitePermissions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
const CONTENT_SCRIPT_HELPER = getRootDirectory(gTestPath) + "get_user_media_content_script.js";
function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties
+++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties
@@ -1,16 +1,17 @@
# 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/.
allow = Allow
allowForSession = Allow for Session
allowTemporarily = Allow Temporarily
block = Block
+blockTemporarily = Block Temporarily
alwaysAsk = Always Ask
permission.cookie.label = Set Cookies
permission.desktop-notification2.label = Receive Notifications
permission.image.label = Load Images
permission.camera.label = Use the Camera
permission.microphone.label = Use the Microphone
permission.screen.label = Share the Screen
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -60,16 +60,18 @@ this.EXPORTED_SYMBOLS = [
* the caller having called into createPermissionPrompt.
*/
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
+ "resource:///modules/SitePermissions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
return Services.strings
.createBundle('chrome://branding/locale/brand.properties');
});
@@ -209,27 +211,20 @@ this.PermissionPromptPrototype = {
* via a dropdown menu. The first item in this array will be
* the default selection. Each action is an Object with the
* following properties:
*
* label (string):
* The label that will be displayed for this choice.
* accessKey (string):
* The access key character that will be used for this choice.
- * action (Ci.nsIPermissionManager action, optional)
- * The nsIPermissionManager action that will be associated with
- * this choice. For example, Ci.nsIPermissionManager.DENY_ACTION.
+ * action (SitePermissions state)
+ * The action that will be associated with this choice.
+ * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK.
*
- * If omitted, the nsIPermissionManager will not be written to
- * when this choice is chosen.
- * expireType (Ci.nsIPermissionManager expiration policy, optional)
- * The nsIPermissionManager expiration policy that will be associated
- * with this choice. For example, Ci.nsIPermissionManager.EXPIRE_SESSION.
- *
- * If action is not set, expireType will be ignored.
* callback (function, optional)
* A callback function that will fire if the user makes this choice, with
* a single parameter, state. State is an Object that contains the property
* checkboxChecked, which identifies whether the checkbox to remember this
* decision was checked.
*/
get promptActions() {
return [];
@@ -266,62 +261,68 @@ this.PermissionPromptPrototype = {
if (!(requestingURI instanceof Ci.nsIStandardURL)) {
return;
}
if (this.permissionKey) {
// 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 result =
- Services.perms.testExactPermissionFromPrincipal(this.principal,
- this.permissionKey);
+ let {state} = SitePermissions.get(requestingURI,
+ this.permissionKey,
+ this.browser);
- if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+ if (state == SitePermissions.BLOCK) {
this.cancel();
return;
}
- if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ if (state == SitePermissions.ALLOW) {
this.allow();
return;
}
}
// Transform the PermissionPrompt actions into PopupNotification actions.
let popupNotificationActions = [];
for (let promptAction of this.promptActions) {
- // Don't offer action in PB mode if the action remembers permission
- // for more than a session.
- if (PrivateBrowsingUtils.isWindowPrivate(chromeWin) &&
- promptAction.expireType != Ci.nsIPermissionManager.EXPIRE_SESSION &&
- promptAction.action) {
- continue;
- }
-
let action = {
label: promptAction.label,
accessKey: promptAction.accessKey,
callback: state => {
if (promptAction.callback) {
promptAction.callback();
}
if (this.permissionKey) {
- // Remember permissions.
- if (state && state.checkboxChecked && promptAction.action) {
- Services.perms.addFromPrincipal(this.principal,
- this.permissionKey,
- promptAction.action,
- promptAction.expireType);
+
+ // Permanently store permission.
+ if (state && state.checkboxChecked) {
+ let scope = SitePermissions.SCOPE_PERSISTENT;
+ // Only remember permission for session if in PB mode.
+ if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
+ scope = SitePermissions.SCOPE_SESSION;
+ }
+ SitePermissions.set(this.principal.URI,
+ this.permissionKey,
+ promptAction.action,
+ scope);
+ } else if (promptAction.action == SitePermissions.BLOCK) {
+ // Temporarily store BLOCK permissions only.
+ // SitePermissions does not consider subframes when storing temporary
+ // permissions on a tab, thus storing ALLOW could be exploited.
+ SitePermissions.set(this.principal.URI,
+ this.permissionKey,
+ promptAction.action,
+ SitePermissions.SCOPE_TEMPORARY,
+ this.browser);
}
- // Grant permission if action is null or ALLOW_ACTION.
- if (!promptAction.action ||
- promptAction.action == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ // Grant permission if action is ALLOW.
+ if (promptAction.action == SitePermissions.ALLOW) {
this.allow();
} else {
this.cancel();
}
}
},
};
if (promptAction.dismiss) {
@@ -474,33 +475,29 @@ GeolocationPermissionPrompt.prototype =
Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_NEVER_SHARE;
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
return [{
label: gBrowserBundle.GetStringFromName("geolocation.allowLocation"),
accessKey:
gBrowserBundle.GetStringFromName("geolocation.allowLocation.accesskey"),
- action: null,
- expireType: null,
+ action: SitePermissions.ALLOW,
callback(state) {
if (state && state.checkboxChecked) {
secHistogram.add(ALWAYS_SHARE);
} else {
secHistogram.add(SHARE_LOCATION);
}
},
}, {
label: gBrowserBundle.GetStringFromName("geolocation.dontAllowLocation"),
accessKey:
gBrowserBundle.GetStringFromName("geolocation.dontAllowLocation.accesskey"),
- action: Ci.nsIPermissionManager.DENY_ACTION,
- expireType: PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal) ?
- Ci.nsIPermissionManager.EXPIRE_SESSION :
- null,
+ action: SitePermissions.BLOCK,
callback(state) {
if (state && state.checkboxChecked) {
secHistogram.add(NEVER_SHARE);
}
},
}];
},
@@ -574,28 +571,22 @@ DesktopNotificationPermissionPrompt.prot
},
get promptActions() {
return [
{
label: gBrowserBundle.GetStringFromName("webNotifications.allow"),
accessKey:
gBrowserBundle.GetStringFromName("webNotifications.allow.accesskey"),
- action: Ci.nsIPermissionManager.ALLOW_ACTION,
- expireType: PrivateBrowsingUtils.isBrowserPrivate(this.browser) ?
- Ci.nsIPermissionManager.EXPIRE_SESSION :
- null,
+ action: SitePermissions.ALLOW,
},
{
label: gBrowserBundle.GetStringFromName("webNotifications.dontAllow"),
accessKey:
gBrowserBundle.GetStringFromName("webNotifications.dontAllow.accesskey"),
- action: Ci.nsIPermissionManager.DENY_ACTION,
- expireType: PrivateBrowsingUtils.isBrowserPrivate(this.browser) ?
- Ci.nsIPermissionManager.EXPIRE_SESSION :
- null,
+ action: SitePermissions.BLOCK,
},
];
},
};
PermissionUI.DesktopNotificationPermissionPrompt =
DesktopNotificationPermissionPrompt;
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -1,194 +1,517 @@
/* 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/. */
this.EXPORTED_SYMBOLS = [ "SitePermissions" ];
Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
var gStringBundle =
Services.strings.createBundle("chrome://browser/locale/sitePermissions.properties");
+/**
+ * A helper module to manage temporarily blocked permissions.
+ *
+ * Permissions are keyed by browser, so methods take a Browser
+ * element to identify the corresponding permission set.
+ *
+ * This uses a WeakMap to key browsers, so that entries are
+ * automatically cleared once the browser stops existing
+ * (once there are no other references to the browser object);
+ */
+const TemporaryBlockedPermissions = {
+ // This is a three level deep map with the following structure:
+ //
+ // Browser => {
+ // <prePath>: {
+ // <permissionID>: {Number} <timeStamp>
+ // }
+ // }
+ //
+ // Only the top level browser elements are stored via WeakMap. The WeakMap
+ // value is an object with URI prePaths as keys. The keys of that object
+ // are ids that identify permissions that were set for the specific URI.
+ // The final value is an object containing the timestamp of when the permission
+ // was set (in order to invalidate after a certain amount of time has passed).
+ _stateByBrowser: new WeakMap(),
+
+ // Private helper method that bundles some shared behavior for
+ // get() and getAll(), e.g. deleting permissions when they have expired.
+ _get(entry, prePath, id, timeStamp) {
+ if (timeStamp == null) {
+ delete entry[prePath][id];
+ return null;
+ }
+ if (timeStamp + SitePermissions.temporaryPermissionExpireTime < Date.now()) {
+ delete entry[prePath][id];
+ return null;
+ }
+ return {id, state: SitePermissions.BLOCK, scope: SitePermissions.SCOPE_TEMPORARY};
+ },
+
+ // Sets a new permission for the specified browser.
+ set(browser, id) {
+ if (!browser) {
+ return;
+ }
+ 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] = Date.now();
+ },
+
+ // Removes a permission with the specified id for the specified browser.
+ remove(browser, id) {
+ if (!browser) {
+ return;
+ }
+ let entry = this._stateByBrowser.get(browser);
+ let prePath = browser.currentURI.prePath;
+ if (entry && entry[prePath]) {
+ delete entry[prePath][id];
+ }
+ },
+
+ // Gets a permission with the specified id for the specified browser.
+ get(browser, id) {
+ if (!browser || !browser.currentURI) {
+ return null;
+ }
+ let entry = this._stateByBrowser.get(browser);
+ let prePath = browser.currentURI.prePath;
+ if (entry && entry[prePath]) {
+ let permission = entry[prePath][id];
+ return this._get(entry, prePath, id, permission);
+ }
+ return null;
+ },
+
+ // 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)) {
+ let permission = this._get(entry, prePath, id, timeStamps[id]);
+ // _get() returns null when the permission has expired.
+ if (permission) {
+ permissions.push(permission);
+ }
+ }
+ }
+ return permissions;
+ },
+
+ // Clears all permissions for the specified browser.
+ // Unlike other methods, this does NOT clear only for
+ // the currentURI but the whole browser state.
+ clear(browser) {
+ this._stateByBrowser.delete(browser);
+ },
+
+ // Copies the temporary permission state of one browser
+ // into a new entry for the other browser.
+ copy(browser, newBrowser) {
+ let entry = this._stateByBrowser.get(browser);
+ if (entry) {
+ this._stateByBrowser.set(newBrowser, entry);
+ }
+ },
+};
+
this.SitePermissions = {
-
+ // Permission states.
UNKNOWN: Services.perms.UNKNOWN_ACTION,
ALLOW: Services.perms.ALLOW_ACTION,
BLOCK: Services.perms.DENY_ACTION,
- SESSION: Components.interfaces.nsICookiePermission.ACCESS_SESSION,
+ ALLOW_COOKIES_FOR_SESSION: Components.interfaces.nsICookiePermission.ACCESS_SESSION,
- /* Returns all custom permissions for a given URI, the return
- * type is a list of objects with the keys:
- * - id: the permissionId of the permission
- * - state: a constant representing the current permission state
- * (e.g. SitePermissions.ALLOW)
+ // Permission scopes.
+ SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}",
+ SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}",
+ SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}",
+ SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}",
+
+ /**
+ * Gets all custom permissions for a given URI.
+ * Install addon permission is excluded, check bug 1303108.
*
- * To receive a more detailed, albeit less performant listing see
- * SitePermissions.getPermissionDetailsByURI().
- *
- * install addon permission is excluded, check bug 1303108
+ * @return {Array} a list of objects with the keys:
+ * - id: the permissionId of the permission
+ * - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY)
+ * - state: a constant representing the current permission state
+ * (e.g. SitePermissions.ALLOW)
*/
- getAllByURI(aURI) {
+ getAllByURI(uri) {
let result = [];
- if (!this.isSupportedURI(aURI)) {
+ if (!this.isSupportedURI(uri)) {
return result;
}
- let permissions = Services.perms.getAllForURI(aURI);
+ let permissions = Services.perms.getAllForURI(uri);
while (permissions.hasMoreElements()) {
let permission = permissions.getNext();
// filter out unknown permissions
if (gPermissionObject[permission.type]) {
// XXX Bug 1303108 - Control Center should only show non-default permissions
if (permission.type == "install") {
continue;
}
+ let scope = this.SCOPE_PERSISTENT;
+ if (permission.expireType == Services.perms.EXPIRE_SESSION) {
+ scope = this.SCOPE_SESSION;
+ }
result.push({
id: permission.type,
+ scope,
state: permission.capability,
});
}
}
return result;
},
- /* Returns an object representing the aId permission. It contains the
- * following keys:
- * - id: the permissionID of the permission
- * - label: the localized label
- * - state: a constant representing the aState permission state
- * (e.g. SitePermissions.ALLOW), or the default if aState is omitted
- * - availableStates: an array of all available states for that permission,
- * represented as objects with the keys:
- * - id: the state constant
- * - label: the translated label of that state
+ /**
+ * Returns detailed information on the specified permission.
+ *
+ * @param {String} id
+ * The permissionID of the permission.
+ * @param {SitePermissions scope} scope
+ * The current scope of the permission.
+ * @param {SitePermissions state} state (optional)
+ * The current state of the permission.
+ * Will default to the default state if omitted.
+ *
+ * @return {Object} an object with the keys:
+ * - id: the permissionID of the permission
+ * - label: the localized label
+ * - state: the passed in state argument
+ * - scope: the passed in scope argument
+ * - availableStates: an array of all available states for that permission,
+ * represented as objects with the keys:
+ * - id: the state constant
+ * - label: the translated label of that state
*/
- getPermissionItem(aId, aState) {
- let availableStates = this.getAvailableStates(aId).map(state => {
- return { id: state, label: this.getStateLabel(aId, state) };
+ getPermissionDetails(id, scope, state = this.getDefault(id)) {
+ let availableStates = this.getAvailableStates(id).map(val => {
+ return { id: val, label: this.getStateLabel(val) };
});
- if (aState == undefined)
- aState = this.getDefault(aId);
- return {id: aId, label: this.getPermissionLabel(aId),
- state: aState, availableStates};
+ return {id, label: this.getPermissionLabel(id), state, scope, availableStates};
},
- /* Returns a list of objects representing all permissions that are currently
- * set for the given URI. See getPermissionItem for the content of each object.
+ /**
+ * Returns all custom permissions for a given browser.
+ *
+ * To receive a more detailed, albeit less performant listing see
+ * SitePermissions.getAllPermissionDetailsForBrowser().
+ *
+ * @param {Browser} browser
+ * The browser to fetch permission for.
+ *
+ * @return {Array} a list of objects with the keys:
+ * - id: the permissionId of the permission
+ * - state: a constant representing the current permission state
+ * (e.g. SitePermissions.ALLOW)
+ * - scope: a constant representing how long the permission will
+ * be kept.
*/
- getPermissionDetailsByURI(aURI) {
- let permissions = [];
- for (let {state, id} of this.getAllByURI(aURI)) {
- permissions.push(this.getPermissionItem(id, state));
+ getAllForBrowser(browser) {
+ let permissions = {};
+
+ for (let permission of TemporaryBlockedPermissions.getAll(browser)) {
+ permission.scope = this.SCOPE_TEMPORARY;
+ permissions[permission.id] = permission;
+ }
+
+ for (let permission of this.getAllByURI(browser.currentURI)) {
+ permissions[permission.id] = permission;
}
- return permissions;
+ return Object.values(permissions);
},
- /* Checks whether a UI for managing permissions should be exposed for a given
+ /**
+ * Returns a list of objects with detailed information on all permissions
+ * that are currently set for the given browser.
+ *
+ * @param {Browser} browser
+ * The browser to fetch permission for.
+ *
+ * @return {Array} a list of objects. See getPermissionDetails for the content of each object.
+ */
+ getAllPermissionDetailsForBrowser(browser) {
+ return this.getAllForBrowser(browser).map(({id, scope, state}) =>
+ this.getPermissionDetails(id, scope, state));
+ },
+
+ /**
+ * Checks whether a UI for managing permissions should be exposed for a given
* URI. This excludes file URIs, for instance, as they don't have a host,
* even though nsIPermissionManager can still handle them.
+ *
+ * @param {nsIURI} uri
+ * The URI to check.
+ *
+ * @return {boolean} if the URI is supported.
*/
- isSupportedURI(aURI) {
- return aURI.schemeIs("http") || aURI.schemeIs("https");
+ isSupportedURI(uri) {
+ return uri && (uri.schemeIs("http") || uri.schemeIs("https"));
},
- /* Returns an array of all permission IDs.
+ /**
+ * Gets an array of all permission IDs.
+ *
+ * @return {Array<String>} an array of all permission IDs.
*/
listPermissions() {
return Object.keys(gPermissionObject);
},
- /* Returns an array of permission states to be exposed to the user for a
+ /**
+ * Returns an array of permission states to be exposed to the user for a
* permission with the given ID.
+ *
+ * @param {string} permissionID
+ * The ID to get permission states for.
+ *
+ * @return {Array<SitePermissions state>} an array of all permission states.
*/
- getAvailableStates(aPermissionID) {
- if (aPermissionID in gPermissionObject &&
- gPermissionObject[aPermissionID].states)
- return gPermissionObject[aPermissionID].states;
+ getAvailableStates(permissionID) {
+ if (permissionID in gPermissionObject &&
+ gPermissionObject[permissionID].states)
+ return gPermissionObject[permissionID].states;
- if (this.getDefault(aPermissionID) == this.UNKNOWN)
+ if (this.getDefault(permissionID) == this.UNKNOWN)
return [ SitePermissions.UNKNOWN, SitePermissions.ALLOW, SitePermissions.BLOCK ];
return [ SitePermissions.ALLOW, SitePermissions.BLOCK ];
},
- /* Returns the default state of a particular permission.
+ /**
+ * Returns the default state of a particular permission.
+ *
+ * @param {string} permissionID
+ * The ID to get the default for.
+ *
+ * @return {SitePermissions.state} the default state.
*/
- getDefault(aPermissionID) {
- if (aPermissionID in gPermissionObject &&
- gPermissionObject[aPermissionID].getDefault)
- return gPermissionObject[aPermissionID].getDefault();
+ getDefault(permissionID) {
+ if (permissionID in gPermissionObject &&
+ gPermissionObject[permissionID].getDefault)
+ return gPermissionObject[permissionID].getDefault();
return this.UNKNOWN;
},
- /* Returns the state of a particular permission for a given URI.
+ /**
+ * Returns the state and scope of a particular permission for a given URI.
+ *
+ * @param {nsIURI} uri
+ * The URI to check.
+ * @param {String} permissionID
+ * The id of the permission.
+ * @param {Browser} browser (optional)
+ * The browser object to check for temporary permissions.
+ *
+ * @return {Object} an object with the keys:
+ * - state: The current state of the permission
+ * (e.g. SitePermissions.ALLOW)
+ * - scope: The scope of the permission
+ * (e.g. SitePermissions.SCOPE_PERSISTENT)
*/
- get(aURI, aPermissionID) {
- if (!this.isSupportedURI(aURI))
- return this.UNKNOWN;
+ get(uri, permissionID, browser) {
+ let result = { state: this.UNKNOWN, scope: this.SCOPE_PERSISTENT };
+ if (this.isSupportedURI(uri)) {
+ let permission = null;
+ if (permissionID in gPermissionObject &&
+ gPermissionObject[permissionID].exactHostMatch) {
+ permission = Services.perms.getPermissionObjectForURI(uri, permissionID, true);
+ } else {
+ permission = Services.perms.getPermissionObjectForURI(uri, permissionID, false);
+ }
- let state;
- if (aPermissionID in gPermissionObject &&
- gPermissionObject[aPermissionID].exactHostMatch)
- state = Services.perms.testExactPermission(aURI, aPermissionID);
- else
- state = Services.perms.testPermission(aURI, aPermissionID);
- return state;
+ if (permission) {
+ result.state = permission.capability;
+ if (permission.expireType == Services.perms.EXPIRE_SESSION) {
+ result.scope = this.SCOPE_SESSION;
+ }
+ }
+ }
+
+ if (!result.state) {
+ // If there's no persistent permission saved, check if we have something
+ // set temporarily.
+ let value = TemporaryBlockedPermissions.get(browser, permissionID);
+
+ if (value) {
+ result.state = value.state;
+ result.scope = this.SCOPE_TEMPORARY;
+ }
+ }
+
+ return result;
},
- /* Sets the state of a particular permission for a given URI.
+ /**
+ * Sets the state of a particular permission for a given URI or browser.
+ *
+ * @param {nsIURI} uri
+ * The URI to set the permission for.
+ * Note that this will be ignored if the scope is set to SCOPE_TEMPORARY
+ * @param {String} permissionID
+ * The id of the permission.
+ * @param {SitePermissions state} state
+ * 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(aURI, aPermissionID, aState) {
- if (!this.isSupportedURI(aURI))
- return;
-
- if (aState == this.UNKNOWN) {
- this.remove(aURI, aPermissionID);
+ set(uri, permissionID, state, scope = this.SCOPE_PERSISTENT, browser = null) {
+ if (state == this.UNKNOWN) {
+ this.remove(uri, permissionID, browser);
return;
}
- Services.perms.add(aURI, aPermissionID, aState);
+ if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") {
+ throw "ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission";
+ }
+
+ // Save temporary permissions.
+ if (scope == this.SCOPE_TEMPORARY) {
+ // We do not support setting temp ALLOW for security reasons.
+ // In its current state, this permission could be exploited by subframes
+ // on the same page. This is because for BLOCK we ignore the request
+ // URI and only consider the current browser URI, to avoid notification spamming.
+ //
+ // If you ever consider removing this line, you likely want to implement
+ // a more fine-grained TemporaryBlockedPermissions that temporarily blocks for the
+ // entire browser, but temporarily allows only for specific frames.
+ if (state != this.BLOCK) {
+ throw "'Block' is the only permission we can save temporarily on a browser";
+ }
+
+ if (!browser) {
+ throw "TEMPORARY scoped permissions require a browser object";
+ }
+
+ TemporaryBlockedPermissions.set(browser, permissionID);
+
+ browser.dispatchEvent(new browser.ownerGlobal
+ .CustomEvent("PermissionStateChange"));
+ } else if (this.isSupportedURI(uri)) {
+ let perms_scope = Services.perms.EXPIRE_NEVER;
+ if (scope == this.SCOPE_SESSION) {
+ perms_scope = Services.perms.EXPIRE_SESSION;
+ }
+
+ Services.perms.add(uri, permissionID, state, perms_scope);
+ }
},
- /* Removes the saved state of a particular permission for a given URI.
+ /**
+ * Removes the saved state of a particular permission for a given URI and/or browser.
+ *
+ * @param {nsIURI} uri
+ * The URI to remove the permission for.
+ * @param {String} permissionID
+ * The id of the permission.
+ * @param {Browser} browser (optional)
+ * The browser object to remove temporary permissions on.
*/
- remove(aURI, aPermissionID) {
- if (!this.isSupportedURI(aURI))
- return;
+ remove(uri, permissionID, browser) {
+ if (this.isSupportedURI(uri))
+ Services.perms.remove(uri, permissionID);
- Services.perms.remove(aURI, aPermissionID);
+ // TemporaryBlockedPermissions.get() deletes expired permissions automatically,
+ if (TemporaryBlockedPermissions.get(browser, permissionID)) {
+ // If it exists but has not expired, remove it explicitly.
+ TemporaryBlockedPermissions.remove(browser, permissionID);
+ // Send a PermissionStateChange event only if the permission hasn't expired.
+ browser.dispatchEvent(new browser.ownerGlobal
+ .CustomEvent("PermissionStateChange"));
+ }
},
- /* Returns the localized label for the permission with the given ID, to be
- * used in a UI for managing permissions.
+ /**
+ * Clears all permissions that were temporarily saved.
+ *
+ * @param {Browser} browser
+ * The browser object to clear.
*/
- getPermissionLabel(aPermissionID) {
- let labelID = gPermissionObject[aPermissionID].labelID || aPermissionID;
+ clearTemporaryPermissions(browser) {
+ TemporaryBlockedPermissions.clear(browser);
+ },
+
+ /**
+ * Copy all permissions that were temporarily saved on one
+ * browser object to a new browser.
+ *
+ * @param {Browser} browser
+ * The browser object to copy from.
+ * @param {Browser} newBrowser
+ * The browser object to copy to.
+ */
+ copyTemporaryPermissions(browser, newBrowser) {
+ TemporaryBlockedPermissions.copy(browser, newBrowser);
+ },
+
+ /**
+ * Returns the localized label for the permission with the given ID, to be
+ * used in a UI for managing permissions.
+ *
+ * @param {string} permissionID
+ * The permission to get the label for.
+ *
+ * @return {String} the localized label.
+ */
+ getPermissionLabel(permissionID) {
+ let labelID = gPermissionObject[permissionID].labelID || permissionID;
return gStringBundle.GetStringFromName("permission." + labelID + ".label");
},
- /* Returns the localized label for the given permission state, to be used in
+ /**
+ * Returns the localized label for the given permission state, to be used in
* a UI for managing permissions.
+ *
+ * @param {SitePermissions state} state
+ * The state to get the label for.
+ * @param {SitePermissions scope} scope (optional)
+ * The scope to get the label for.
+ *
+ * @return {String} the localized label.
*/
- getStateLabel(aPermissionID, aState, aInUse = false) {
- switch (aState) {
+ getStateLabel(state, scope = null) {
+ switch (state) {
case this.UNKNOWN:
- if (aInUse)
- return gStringBundle.GetStringFromName("allowTemporarily");
return gStringBundle.GetStringFromName("alwaysAsk");
case this.ALLOW:
+ if (scope && scope != this.SCOPE_PERSISTENT)
+ return gStringBundle.GetStringFromName("allowTemporarily");
return gStringBundle.GetStringFromName("allow");
- case this.SESSION:
+ case this.ALLOW_COOKIES_FOR_SESSION:
return gStringBundle.GetStringFromName("allowForSession");
case this.BLOCK:
+ if (scope && scope != this.SCOPE_PERSISTENT)
+ return gStringBundle.GetStringFromName("blockTemporarily");
return gStringBundle.GetStringFromName("block");
default:
return null;
}
}
};
var gPermissionObject = {
@@ -217,23 +540,23 @@ var gPermissionObject = {
"image": {
getDefault() {
return Services.prefs.getIntPref("permissions.default.image") == 2 ?
SitePermissions.BLOCK : SitePermissions.ALLOW;
}
},
"cookie": {
- states: [ SitePermissions.ALLOW, SitePermissions.SESSION, SitePermissions.BLOCK ],
+ states: [ SitePermissions.ALLOW, SitePermissions.ALLOW_COOKIES_FOR_SESSION, SitePermissions.BLOCK ],
getDefault() {
if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == 2)
return SitePermissions.BLOCK;
if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == 2)
- return SitePermissions.SESSION;
+ return SitePermissions.ALLOW_COOKIES_FOR_SESSION;
return SitePermissions.ALLOW;
}
},
"desktop-notification": {
exactHostMatch: true,
labelID: "desktop-notification2",
@@ -261,9 +584,11 @@ var gPermissionObject = {
"geo": {
exactHostMatch: true
},
"indexedDB": {}
};
-const kPermissionIDs = Object.keys(gPermissionObject);
+XPCOMUtils.defineLazyPreferenceGetter(SitePermissions, "temporaryPermissionExpireTime",
+ "privacy.temporary_permission_expire_time_ms", 3600 * 1000);
+
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -19,16 +19,20 @@ support-files =
[browser_NetworkPrioritizer.js]
[browser_PermissionUI.js]
[browser_ProcessHangNotifications.js]
skip-if = !e10s
[browser_SelfSupportBackend.js]
support-files =
../../components/uitour/test/uitour.html
../../components/uitour/UITour-lib.js
+[browser_SitePermissions.js]
+[browser_SitePermissions_combinations.js]
+[browser_SitePermissions_expiry.js]
+[browser_SitePermissions_tab_urls.js]
[browser_taskbar_preview.js]
skip-if = os != "win"
[browser_UnsubmittedCrashHandler.js]
run-if = crashreporter
[browser_UsageTelemetry.js]
[browser_UsageTelemetry_private_and_restore.js]
[browser_UsageTelemetry_urlbar.js]
support-files =
--- a/browser/modules/test/browser_PermissionUI.js
+++ b/browser/modules/test/browser_PermissionUI.js
@@ -3,16 +3,17 @@
* permission prompts to the user. It also tests to ensure that
* add-ons can introduce their own permission prompts.
*/
"use strict";
Cu.import("resource://gre/modules/Integration.jsm", this);
Cu.import("resource:///modules/PermissionUI.jsm", this);
+Cu.import("resource:///modules/SitePermissions.jsm", this);
/**
* Given a <xul:browser> at some non-internal web page,
* return something that resembles an nsIContentPermissionRequest,
* using the browsers currently loaded document to get a principal.
*
* @param browser (<xul:browser>)
* The browser that we'll create a nsIContentPermissionRequest
@@ -216,38 +217,36 @@ add_task(function* test_with_permission_
const kTestNotificationID = "test-notification";
const kTestMessage = "Test message";
const kTestPermissionKey = "test-permission-key";
let allowed = false;
let mainAction = {
label: "Allow",
accessKey: "M",
- action: Ci.nsIPermissionManager.ALLOW_ACTION,
- expiryType: Ci.nsIPermissionManager.EXPIRE_SESSION,
+ action: SitePermissions.ALLOW,
callback() {
allowed = true;
}
};
let denied = false;
let secondaryAction = {
label: "Deny",
accessKey: "D",
- action: Ci.nsIPermissionManager.DENY_ACTION,
- expiryType: Ci.nsIPermissionManager.EXPIRE_SESSION,
+ action: SitePermissions.BLOCK,
callback() {
denied = true;
}
};
let mockRequest = makeMockPermissionRequest(browser);
let principal = mockRequest.principal;
registerCleanupFunction(function() {
- Services.perms.removeFromPrincipal(principal, kTestPermissionKey);
+ SitePermissions.remove(principal.URI, kTestPermissionKey);
});
let TestPrompt = {
__proto__: PermissionUI.PermissionPromptForRequestPrototype,
request: mockRequest,
notificationID: kTestNotificationID,
permissionKey: kTestPermissionKey,
message: kTestMessage,
@@ -264,54 +263,90 @@ add_task(function* test_with_permission_
let shownPromise =
BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
TestPrompt.prompt();
yield shownPromise;
let notification =
PopupNotifications.getNotification(kTestNotificationID, browser);
Assert.ok(notification, "Should have gotten the notification");
- let curPerm =
- Services.perms.testPermissionFromPrincipal(principal,
- kTestPermissionKey);
- Assert.equal(curPerm, Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ let curPerm = SitePermissions.get(principal.URI, kTestPermissionKey, browser);
+ Assert.equal(curPerm.state, SitePermissions.UNKNOWN,
"Should be no permission set to begin with.");
- // First test denying the permission request.
+ // First test denying the permission request without the checkbox checked.
+ let popupNotification = getPopupNotificationNode();
+ popupNotification.checkbox.checked = false;
+
Assert.equal(notification.secondaryActions.length, 1,
"There should only be 1 secondary action");
yield clickSecondaryAction();
- curPerm = Services.perms.testPermissionFromPrincipal(principal,
- kTestPermissionKey);
- Assert.equal(curPerm, Ci.nsIPermissionManager.DENY_ACTION,
- "Should have denied the action");
+ curPerm = SitePermissions.get(principal.URI, kTestPermissionKey, browser);
+ Assert.deepEqual(curPerm, {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }, "Should have denied the action temporarily");
+ // Try getting the permission without passing the browser object (should fail).
+ curPerm = SitePermissions.get(principal.URI, kTestPermissionKey);
+ Assert.deepEqual(curPerm, {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }, "Should have made no permanent permission entry");
Assert.ok(denied, "The secondaryAction callback should have fired");
Assert.ok(!allowed, "The mainAction callback should not have fired");
Assert.ok(mockRequest._cancelled,
"The request should have been cancelled");
Assert.ok(!mockRequest._allowed,
"The request should not have been allowed");
// Clear the permission and pretend we never denied
- Services.perms.removeFromPrincipal(principal, kTestPermissionKey);
+ SitePermissions.remove(principal.URI, kTestPermissionKey, browser);
denied = false;
mockRequest._cancelled = false;
// Bring the PopupNotification back up now...
shownPromise =
BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
TestPrompt.prompt();
yield shownPromise;
- // Next test allowing the permission request.
+ // Test denying the permission request.
+ Assert.equal(notification.secondaryActions.length, 1,
+ "There should only be 1 secondary action");
+ yield clickSecondaryAction();
+ curPerm = SitePermissions.get(principal.URI, kTestPermissionKey);
+ Assert.deepEqual(curPerm, {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT
+ }, "Should have denied the action");
+ Assert.ok(denied, "The secondaryAction callback should have fired");
+ Assert.ok(!allowed, "The mainAction callback should not have fired");
+ Assert.ok(mockRequest._cancelled,
+ "The request should have been cancelled");
+ Assert.ok(!mockRequest._allowed,
+ "The request should not have been allowed");
+
+ // Clear the permission and pretend we never denied
+ SitePermissions.remove(principal.URI, kTestPermissionKey);
+ denied = false;
+ mockRequest._cancelled = false;
+
+ // Bring the PopupNotification back up now...
+ shownPromise =
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ TestPrompt.prompt();
+ yield shownPromise;
+
+ // Test allowing the permission request.
yield clickMainAction();
- curPerm = Services.perms.testPermissionFromPrincipal(principal,
- kTestPermissionKey);
- Assert.equal(curPerm, Ci.nsIPermissionManager.ALLOW_ACTION,
- "Should have allowed the action");
+ curPerm = SitePermissions.get(principal.URI, kTestPermissionKey);
+ Assert.deepEqual(curPerm, {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT
+ }, "Should have allowed the action");
Assert.ok(!denied, "The secondaryAction callback should not have fired");
Assert.ok(allowed, "The mainAction callback should have fired");
Assert.ok(!mockRequest._cancelled,
"The request should not have been cancelled");
Assert.ok(mockRequest._allowed,
"The request should have been allowed");
});
});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_SitePermissions.js
@@ -0,0 +1,101 @@
+/* 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/. */
+
+"use strict";
+
+Cu.import("resource:///modules/SitePermissions.jsm", this);
+
+// This asserts that SitePermissions.set can not save ALLOW permissions
+// temporarily on a tab.
+add_task(function* testTempAllowThrows() {
+ let uri = Services.io.newURI("https://example.com");
+ let id = "notifications";
+
+ yield BrowserTestUtils.withNewTab(uri.spec, function(browser) {
+ Assert.throws(function() {
+ SitePermissions.set(uri, id, SitePermissions.ALLOW, SitePermissions.SCOPE_TEMPORARY, browser);
+ }, "'Block' is the only permission we can save temporarily on a tab");
+ });
+});
+
+// This tests the SitePermissions.getAllPermissionDetailsForBrowser function.
+add_task(function* testGetAllPermissionDetailsForBrowser() {
+ let uri = Services.io.newURI("https://example.com");
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+
+ SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+ SitePermissions.set(uri, "cookie", SitePermissions.ALLOW_COOKIES_FOR_SESSION);
+ SitePermissions.set(uri, "popup", SitePermissions.BLOCK);
+ SitePermissions.set(uri, "geo", SitePermissions.ALLOW, SitePermissions.SCOPE_SESSION);
+
+ let permissions = SitePermissions.getAllPermissionDetailsForBrowser(tab.linkedBrowser);
+
+ let camera = permissions.find(({id}) => id === "camera");
+ Assert.deepEqual(camera, {
+ id: "camera",
+ label: "Use the Camera",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ availableStates: [
+ { id: SitePermissions.UNKNOWN, label: "Always Ask" },
+ { id: SitePermissions.ALLOW, label: "Allow" },
+ { id: SitePermissions.BLOCK, label: "Block" },
+ ]
+ });
+
+ // check that removed permissions (State.UNKNOWN) are skipped
+ SitePermissions.remove(uri, "camera");
+ permissions = SitePermissions.getAllPermissionDetailsForBrowser(tab.linkedBrowser);
+
+ camera = permissions.find(({id}) => id === "camera");
+ Assert.equal(camera, undefined);
+
+ // check that different available state values are represented
+
+ let cookie = permissions.find(({id}) => id === "cookie");
+ Assert.deepEqual(cookie, {
+ id: "cookie",
+ label: "Set Cookies",
+ state: SitePermissions.ALLOW_COOKIES_FOR_SESSION,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ availableStates: [
+ { id: SitePermissions.ALLOW, label: "Allow" },
+ { id: SitePermissions.ALLOW_COOKIES_FOR_SESSION, label: "Allow for Session" },
+ { id: SitePermissions.BLOCK, label: "Block" },
+ ]
+ });
+
+ let popup = permissions.find(({id}) => id === "popup");
+ Assert.deepEqual(popup, {
+ id: "popup",
+ label: "Open Pop-up Windows",
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ availableStates: [
+ { id: SitePermissions.ALLOW, label: "Allow" },
+ { id: SitePermissions.BLOCK, label: "Block" },
+ ]
+ });
+
+ let geo = permissions.find(({id}) => id === "geo");
+ Assert.deepEqual(geo, {
+ id: "geo",
+ label: "Access Your Location",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_SESSION,
+ availableStates: [
+ { id: SitePermissions.UNKNOWN, label: "Always Ask" },
+ { id: SitePermissions.ALLOW, label: "Allow" },
+ { id: SitePermissions.BLOCK, label: "Block" },
+ ]
+ });
+
+ SitePermissions.remove(uri, "cookie");
+ SitePermissions.remove(uri, "popup");
+ SitePermissions.remove(uri, "geo");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_SitePermissions_combinations.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+"use strict";
+
+Cu.import("resource:///modules/SitePermissions.jsm", this);
+
+// This function applies combinations of different permissions and
+// checks how they override each other.
+function* checkPermissionCombinations(combinations) {
+ let uri = Services.io.newURI("https://example.com");
+
+ yield BrowserTestUtils.withNewTab(uri.spec, function(browser) {
+ let id = "geo";
+ for (let {reverse, states, result} of combinations) {
+ let loop = () => {
+ for (let [state, scope] of states) {
+ SitePermissions.set(uri, id, state, scope, browser);
+ }
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), result);
+ SitePermissions.remove(uri, id, browser);
+ };
+
+ loop();
+
+ if (reverse) {
+ states.reverse();
+ loop();
+ }
+ }
+ });
+}
+
+// Test that passing null as scope becomes SCOPE_PERSISTENT.
+add_task(function* testDefaultScope() {
+ yield checkPermissionCombinations([{
+ states: [
+ [SitePermissions.ALLOW, null],
+ ],
+ result: {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ }]);
+});
+
+// Test that "wide" scopes like PERSISTENT always override "narrower" ones like TAB.
+add_task(function* testScopeOverrides() {
+ yield checkPermissionCombinations([
+ {
+ // The behavior of SCOPE_SESSION is not in line with the general behavior
+ // because of the legacy nsIPermissionManager implementation.
+ states: [
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_PERSISTENT],
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_SESSION],
+ ],
+ result: {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_SESSION,
+ },
+ }, {
+ states: [
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_SESSION],
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_PERSISTENT],
+ ],
+ result: {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+
+ }, {
+ reverse: true,
+ states: [
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY],
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_SESSION],
+ ],
+ result: {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_SESSION,
+ },
+ }, {
+ reverse: true,
+ states: [
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY],
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_PERSISTENT],
+ ],
+ result: {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ }
+ ]);
+});
+
+// Test that clearing a temporary permission also removes a
+// persistent permission that was set for the same URL.
+add_task(function* testClearTempPermission() {
+ yield checkPermissionCombinations([{
+ states: [
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY],
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_PERSISTENT],
+ [SitePermissions.UNKNOWN, SitePermissions.SCOPE_TEMPORARY],
+ ],
+ result: {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ }]);
+});
+
+// Test that states override each other when applied with the same scope.
+add_task(function* testStateOverride() {
+ yield checkPermissionCombinations([
+ {
+ states: [
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_PERSISTENT],
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_PERSISTENT],
+ ],
+ result: {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ }, {
+ states: [
+ [SitePermissions.BLOCK, SitePermissions.SCOPE_PERSISTENT],
+ [SitePermissions.ALLOW, SitePermissions.SCOPE_PERSISTENT],
+ ],
+ result: {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ }
+ ]);
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_SitePermissions_expiry.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+"use strict";
+
+Cu.import("resource:///modules/SitePermissions.jsm", this);
+
+// This tests the time delay to expire temporary permission entries.
+add_task(function* testTemporaryPermissionExpiry() {
+ SpecialPowers.pushPrefEnv({set: [
+ ["privacy.temporary_permission_expire_time_ms", 100],
+ ]});
+
+ let uri = Services.io.newURI("https://example.com")
+ let id = "camera";
+
+ yield BrowserTestUtils.withNewTab(uri.spec, function*(browser) {
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ yield new Promise((c) => setTimeout(c, 500));
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+ });
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_SitePermissions_tab_urls.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+"use strict";
+
+Cu.import("resource:///modules/SitePermissions.jsm", this);
+
+// This tests the key used to store the URI -> permission map on a tab.
+add_task(function* testTemporaryPermissionTabURLs() {
+
+ // Prevent showing a dialog for https://name:password@example.com
+ SpecialPowers.pushPrefEnv({set: [
+ ["network.http.phishy-userpass-length", 2048],
+ ]});
+
+ // This usually takes about 60 seconds on 32bit Linux debug,
+ // due to the combinatory nature of the test that is hard to fix.
+ requestLongerTimeout(2);
+
+ let same = [ "https://example.com", "https://example.com/sub/path", "https://example.com:443" ].map(Services.io.newURI);
+ let different = [ "https://example.com", "https://name:password@example.com", "https://test1.example.com", "http://example.com", "http://example.org" ].map(Services.io.newURI);
+
+ let id = "microphone";
+
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ for (let uri of same) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+ browser.loadURI(uri.spec);
+ yield loaded;
+
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+ for (let uri2 of same) {
+ let loaded2 = BrowserTestUtils.browserLoaded(browser, false, uri2.spec);
+ browser.loadURI(uri2.spec);
+ yield loaded2;
+
+ Assert.deepEqual(SitePermissions.get(uri2, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }, `${uri.spec} should share tab permissions with ${uri2.spec}`);
+ }
+
+ SitePermissions.clearTemporaryPermissions(browser);
+ }
+
+ for (let uri of different) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+ browser.loadURI(uri.spec);
+ yield loaded;
+
+ SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+ Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ for (let uri2 of different) {
+ loaded = BrowserTestUtils.browserLoaded(browser, false, uri2.spec);
+ browser.loadURI(uri2.spec);
+ yield loaded;
+
+ if (uri2 != uri) {
+ Assert.deepEqual(SitePermissions.get(uri2, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }, `${uri.spec} should not share tab permissions with ${uri2.spec}`);
+ }
+ }
+
+ SitePermissions.clearTemporaryPermissions(browser);
+ }
+ });
+
+});
+
--- a/browser/modules/test/browser_UsageTelemetry.js
+++ b/browser/modules/test/browser_UsageTelemetry.js
@@ -5,16 +5,19 @@ const TAB_EVENT_COUNT = "browser.engagem
const MAX_CONCURRENT_WINDOWS = "browser.engagement.max_concurrent_window_count";
const WINDOW_OPEN_COUNT = "browser.engagement.window_open_event_count";
const TOTAL_URI_COUNT = "browser.engagement.total_uri_count";
const UNIQUE_DOMAINS_COUNT = "browser.engagement.unique_domains_count";
const UNFILTERED_URI_COUNT = "browser.engagement.unfiltered_uri_count";
const TELEMETRY_SUBSESSION_TOPIC = "internal-telemetry-after-subsession-split";
+// Reset internal URI counter in case URIs were opened by other tests.
+Services.obs.notifyObservers(null, TELEMETRY_SUBSESSION_TOPIC, "");
+
/**
* Waits for the web progress listener associated with this tab to fire an
* onLocationChange for a non-error page.
*
* @param {xul:browser} browser
* A xul:browser.
*
* @return {Promise}
--- a/browser/modules/test/xpcshell/test_SitePermissions.js
+++ b/browser/modules/test/xpcshell/test_SitePermissions.js
@@ -19,97 +19,35 @@ add_task(function* testGetAllByURI() {
let wrongURI = Services.io.newURI("file:///example.js")
Assert.deepEqual(SitePermissions.getAllByURI(wrongURI), []);
let uri = Services.io.newURI("https://example.com")
Assert.deepEqual(SitePermissions.getAllByURI(uri), []);
SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
Assert.deepEqual(SitePermissions.getAllByURI(uri), [
- { id: "camera", state: SitePermissions.ALLOW }
+ { id: "camera", state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_PERSISTENT }
]);
- SitePermissions.set(uri, "microphone", SitePermissions.SESSION);
+ SitePermissions.set(uri, "microphone", SitePermissions.ALLOW, SitePermissions.SCOPE_SESSION);
SitePermissions.set(uri, "desktop-notification", SitePermissions.BLOCK);
Assert.deepEqual(SitePermissions.getAllByURI(uri), [
- { id: "camera", state: SitePermissions.ALLOW },
- { id: "microphone", state: SitePermissions.SESSION },
- { id: "desktop-notification", state: SitePermissions.BLOCK }
+ { id: "camera", state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_PERSISTENT },
+ { id: "microphone", state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_SESSION },
+ { id: "desktop-notification", state: SitePermissions.BLOCK, scope: SitePermissions.SCOPE_PERSISTENT }
]);
SitePermissions.remove(uri, "microphone");
Assert.deepEqual(SitePermissions.getAllByURI(uri), [
- { id: "camera", state: SitePermissions.ALLOW },
- { id: "desktop-notification", state: SitePermissions.BLOCK }
+ { id: "camera", state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_PERSISTENT },
+ { id: "desktop-notification", state: SitePermissions.BLOCK, scope: SitePermissions.SCOPE_PERSISTENT }
]);
SitePermissions.remove(uri, "camera");
SitePermissions.remove(uri, "desktop-notification");
Assert.deepEqual(SitePermissions.getAllByURI(uri), []);
// XXX Bug 1303108 - Control Center should only show non-default permissions
SitePermissions.set(uri, "addon", SitePermissions.BLOCK);
Assert.deepEqual(SitePermissions.getAllByURI(uri), []);
SitePermissions.remove(uri, "addon");
});
-
-add_task(function* testGetPermissionDetailsByURI() {
- // check that it returns an empty array on an invalid URI
- // like a file URI, which doesn't support site permissions
- let wrongURI = Services.io.newURI("file:///example.js")
- Assert.deepEqual(SitePermissions.getPermissionDetailsByURI(wrongURI), []);
-
- let uri = Services.io.newURI("https://example.com")
-
- SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
- SitePermissions.set(uri, "cookie", SitePermissions.SESSION);
- SitePermissions.set(uri, "popup", SitePermissions.BLOCK);
-
- let permissions = SitePermissions.getPermissionDetailsByURI(uri);
-
- let camera = permissions.find(({id}) => id === "camera");
- Assert.deepEqual(camera, {
- id: "camera",
- label: "Use the Camera",
- state: SitePermissions.ALLOW,
- availableStates: [
- { id: SitePermissions.UNKNOWN, label: "Always Ask" },
- { id: SitePermissions.ALLOW, label: "Allow" },
- { id: SitePermissions.BLOCK, label: "Block" },
- ]
- });
-
- // check that removed permissions (State.UNKNOWN) are skipped
- SitePermissions.remove(uri, "camera");
- permissions = SitePermissions.getPermissionDetailsByURI(uri);
-
- camera = permissions.find(({id}) => id === "camera");
- Assert.equal(camera, undefined);
-
- // check that different available state values are represented
-
- let cookie = permissions.find(({id}) => id === "cookie");
- Assert.deepEqual(cookie, {
- id: "cookie",
- label: "Set Cookies",
- state: SitePermissions.SESSION,
- availableStates: [
- { id: SitePermissions.ALLOW, label: "Allow" },
- { id: SitePermissions.SESSION, label: "Allow for Session" },
- { id: SitePermissions.BLOCK, label: "Block" },
- ]
- });
-
- let popup = permissions.find(({id}) => id === "popup");
- Assert.deepEqual(popup, {
- id: "popup",
- label: "Open Pop-up Windows",
- state: SitePermissions.BLOCK,
- availableStates: [
- { id: SitePermissions.ALLOW, label: "Allow" },
- { id: SitePermissions.BLOCK, label: "Block" },
- ]
- });
-
- SitePermissions.remove(uri, "cookie");
- SitePermissions.remove(uri, "popup");
-});
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -15,16 +15,18 @@ Cu.import("resource://gre/modules/Servic
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
+ "resource:///modules/SitePermissions.jsm");
XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
return Services.strings.createBundle("chrome://branding/locale/brand.properties");
});
this.webrtcUI = {
peerConnectionBlockers: new Set(),
emitter: new EventEmitter(),
@@ -331,16 +333,27 @@ function getHost(uri, href) {
}
return host;
}
function prompt(aBrowser, aRequest) {
let {audioDevices: audioDevices, videoDevices: videoDevices,
sharingScreen: sharingScreen, sharingAudio: sharingAudio,
requestTypes: requestTypes} = aRequest;
+
+ // If the user has already denied access once in this tab,
+ // deny again without even showing the notification icon.
+ if ((audioDevices.length && SitePermissions
+ .get(null, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
+ (videoDevices.length && SitePermissions
+ .get(null, sharingScreen ? "screen" : "camera", aBrowser).state == SitePermissions.BLOCK)) {
+ denyRequest(aBrowser, aRequest);
+ return;
+ }
+
let uri = Services.io.newURI(aRequest.documentURI);
let host = getHost(uri);
let chromeDoc = aBrowser.ownerDocument;
let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
let stringId = "getUserMedia.share" + requestTypes.join("And") + "2.message";
let message = stringBundle.getFormattedString(stringId, [host]);
let notification; // Used by action callbacks.
@@ -354,23 +367,26 @@ function prompt(aBrowser, aRequest) {
};
let secondaryActions = [
{
label: stringBundle.getString("getUserMedia.dontAllow.label"),
accessKey: stringBundle.getString("getUserMedia.dontAllow.accesskey"),
callback(aState) {
denyRequest(notification.browser, aRequest);
+ let scope = SitePermissions.SCOPE_TEMPORARY;
if (aState && aState.checkboxChecked) {
- let perms = Services.perms;
- if (audioDevices.length)
- perms.add(uri, "microphone", perms.DENY_ACTION);
- if (videoDevices.length)
- perms.add(uri, sharingScreen ? "screen" : "camera", perms.DENY_ACTION);
+ scope = SitePermissions.SCOPE_PERSISTENT;
}
+ if (audioDevices.length)
+ SitePermissions.set(uri, "microphone",
+ SitePermissions.BLOCK, scope, notification.browser);
+ if (videoDevices.length)
+ SitePermissions.set(uri, sharingScreen ? "screen" : "camera",
+ SitePermissions.BLOCK, scope, notification.browser);
}
}
];
let productName = gBrandBundle.GetStringFromName("brandShortName");
// Disable the permanent 'Allow' action if the connection isn't secure, or for
// screen/audio sharing (because we can't guess which window the user wants to
@@ -417,55 +433,49 @@ function prompt(aBrowser, aRequest) {
menupopup.removeEventListener("command", menupopup._commandEventListener);
menupopup._commandEventListener = null;
}
}
if (aTopic != "showing")
return false;
- // DENY_ACTION is handled immediately by MediaManager, but handling
- // of ALLOW_ACTION is delayed until the popupshowing event
+ // BLOCK is handled immediately by MediaManager if it has been set
+ // persistently in the permission manager. If it has been set on the tab,
+ // it is handled synchronously before we add the notification.
+ // Handling of ALLOW is delayed until the popupshowing event,
// to avoid granting permissions automatically to background tabs.
if (aRequest.secure) {
- let perms = Services.perms;
+ let micAllowed =
+ SitePermissions.get(uri, "microphone").state == SitePermissions.ALLOW;
+ let camAllowed =
+ SitePermissions.get(uri, "camera").state == SitePermissions.ALLOW;
- let micPerm = perms.testExactPermission(uri, "microphone");
- if (micPerm == perms.PROMPT_ACTION)
- micPerm = perms.UNKNOWN_ACTION;
-
- let camPerm = perms.testExactPermission(uri, "camera");
-
+ let perms = Services.perms;
let mediaManagerPerm =
perms.testExactPermission(uri, "MediaManagerVideo");
if (mediaManagerPerm) {
perms.remove(uri, "MediaManagerVideo");
}
- if (camPerm == perms.PROMPT_ACTION)
- camPerm = perms.UNKNOWN_ACTION;
-
// Screen sharing shouldn't follow the camera permissions.
if (videoDevices.length && sharingScreen)
- camPerm = perms.UNKNOWN_ACTION;
+ camAllowed = false;
- // We don't check that permissions are set to ALLOW_ACTION in this
- // test; only that they are set. This is because if audio is allowed
- // and video is denied persistently, we don't want to show the prompt,
- // and will grant audio access immediately.
- if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
+ if ((!audioDevices.length || micAllowed) &&
+ (!videoDevices.length || camAllowed)) {
// All permissions we were about to request are already persistently set.
let allowedDevices = [];
- if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
+ if (videoDevices.length && camAllowed) {
allowedDevices.push(videoDevices[0].deviceIndex);
Services.perms.add(uri, "MediaManagerVideo",
Services.perms.ALLOW_ACTION,
Services.perms.EXPIRE_SESSION);
}
- if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
+ if (audioDevices.length && micAllowed)
allowedDevices.push(audioDevices[0].deviceIndex);
// Remember on which URIs we found persistent permissions so that we
// can remove them if the user clicks 'Stop Sharing'. There's no
// other way for the stop sharing code to know the hostnames of frames
// using devices until bug 1066082 is fixed.
let browser = this.browser;
browser._devicePermissionURIs = browser._devicePermissionURIs || [];
@@ -649,31 +659,34 @@ function prompt(aBrowser, aRequest) {
let videoDeviceIndex = doc.getElementById(listId).value;
let allowCamera = videoDeviceIndex != "-1";
if (allowCamera) {
allowedDevices.push(videoDeviceIndex);
// Session permission will be removed after use
// (it's really one-shot, not for the entire session)
perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
perms.EXPIRE_SESSION);
- }
- if (remember) {
- perms.add(uri, "camera",
- allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
+ if (remember)
+ SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+ } else {
+ let scope = remember ? SitePermissions.SCOPE_PERSISTENT : SitePermissions.SCOPE_TEMPORARY;
+ SitePermissions.set(uri, "camera", SitePermissions.BLOCK, scope, aBrowser);
}
}
if (audioDevices.length) {
if (!sharingAudio) {
let audioDeviceIndex = doc.getElementById("webRTC-selectMicrophone-menulist").value;
let allowMic = audioDeviceIndex != "-1";
- if (allowMic)
+ if (allowMic) {
allowedDevices.push(audioDeviceIndex);
- if (remember) {
- perms.add(uri, "microphone",
- allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
+ if (remember)
+ SitePermissions.set(uri, "microphone", SitePermissions.ALLOW);
+ } else {
+ let scope = remember ? SitePermissions.SCOPE_PERSISTENT : SitePermissions.SCOPE_TEMPORARY;
+ SitePermissions.set(uri, "microphone", SitePermissions.BLOCK, scope, aBrowser);
}
} else {
// Only one device possible for audio capture.
allowedDevices.push(0);
}
}
if (!allowedDevices.length) {
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm
@@ -88,21 +88,22 @@ this.ControlCenter = {
yield loadPage(PERMISSIONS_PAGE);
yield openIdentityPopup();
}),
},
allPermissions: {
applyConfig: Task.async(function* () {
- // there are 3 possible non-default permission states, so we alternate between them
- let states = [SitePermissions.ALLOW, SitePermissions.BLOCK, SitePermissions.SESSION];
+ // TODO: (Bug 1330601) Rewrite this to consider temporary (TAB) permission states.
+ // There are 2 possible non-default permission states, so we alternate between them.
+ let states = [SitePermissions.ALLOW, SitePermissions.BLOCK];
let uri = Services.io.newURI(PERMISSIONS_PAGE)
SitePermissions.listPermissions().forEach(function(permission, index) {
- SitePermissions.set(uri, permission, states[index % 3]);
+ SitePermissions.set(uri, permission, states[index % 2]);
});
yield loadPage(PERMISSIONS_PAGE);
yield openIdentityPopup();
}),
},
mixed: {
--- a/extensions/cookie/nsPermissionManager.cpp
+++ b/extensions/cookie/nsPermissionManager.cpp
@@ -2015,16 +2015,29 @@ NS_IMETHODIMP
nsPermissionManager::TestPermissionFromPrincipal(nsIPrincipal* aPrincipal,
const char* aType,
uint32_t* aPermission)
{
return CommonTestPermission(aPrincipal, aType, aPermission, false, true);
}
NS_IMETHODIMP
+nsPermissionManager::GetPermissionObjectForURI(nsIURI* aURI,
+ const char* aType,
+ bool aExactHostMatch,
+ nsIPermission** aResult)
+{
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetPermissionObject(principal, aType, aExactHostMatch, aResult);
+}
+
+NS_IMETHODIMP
nsPermissionManager::GetPermissionObject(nsIPrincipal* aPrincipal,
const char* aType,
bool aExactHostMatch,
nsIPermission** aResult)
{
NS_ENSURE_ARG_POINTER(aPrincipal);
NS_ENSURE_ARG_POINTER(aType);
--- a/netwerk/base/nsIPermissionManager.idl
+++ b/netwerk/base/nsIPermissionManager.idl
@@ -204,16 +204,33 @@ interface nsIPermissionManager : nsISupp
* @param type a case-sensitive ASCII string, identifying the consumer
* @param return see add(), param permission. returns UNKNOWN_ACTION when
* there is no stored permission for this uri and / or type.
*/
uint32_t testExactPermanentPermission(in nsIPrincipal principal,
in string type);
/**
+ * Get the permission object associated with the given URI and action.
+ * @param uri The URI
+ * @param type A case-sensitive ASCII string identifying the consumer
+ * @param exactHost If true, only the specific host will be matched,
+ * @see testExactPermission. If false, subdomains will
+ * also be searched, @see testPermission.
+ * @returns The matching permission object, or null if no matching object
+ * was found. No matching object is equivalent to UNKNOWN_ACTION.
+ * @note Clients in general should prefer the test* methods unless they
+ * need to know the specific stored details.
+ * @note This method will always return null for the system principal.
+ */
+ nsIPermission getPermissionObjectForURI(in nsIURI uri,
+ in string type,
+ in boolean exactHost);
+
+ /**
* Get the permission object associated with the given principal and action.
* @param principal The principal
* @param type A case-sensitive ASCII string identifying the consumer
* @param exactHost If true, only the specific host will be matched,
* @see testExactPermission. If false, subdomains will
* also be searched, @see testPermission.
* @returns The matching permission object, or null if no matching object
* was found. No matching object is equivalent to UNKNOWN_ACTION.