Bug 1376991 - Extend browsingData to restrict removing cookies to a give list of hostnames; r?mixedpuppy
MozReview-Commit-ID: 4Tfneh5s1Q8
***
Fixes for try run failures
MozReview-Commit-ID: 2BAT1GUcvH3
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -43,23 +43,24 @@ function clearCache() {
return sanitizer.items.cache.clear();
}
let clearCookies = async function(options) {
let cookieMgr = Services.cookies;
// This code has been borrowed from sanitize.js.
let yieldCounter = 0;
- if (options.since) {
+ if (options.since || options.hostnames) {
// Iterate through the cookies and delete any created after our cutoff.
let cookiesEnum = cookieMgr.enumerator;
while (cookiesEnum.hasMoreElements()) {
let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
- if (cookie.creationTime >= PlacesUtils.toPRTime(options.since)) {
+ if ((!options.since || cookie.creationTime >= PlacesUtils.toPRTime(options.since)) &&
+ (!options.hostnames || options.hostnames.includes(cookie.host.replace(/^\./, "")))) {
// This cookie was created after our cutoff, clear it.
cookieMgr.remove(cookie.host, cookie.name, cookie.path,
false, cookie.originAttributes);
if (++yieldCounter % YIELD_PERIOD == 0) {
await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
}
}
--- a/browser/components/extensions/schemas/browsing_data.json
+++ b/browser/components/extensions/schemas/browsing_data.json
@@ -27,16 +27,22 @@
"type": "object",
"description": "Options that determine exactly what data will be removed.",
"properties": {
"since": {
"$ref": "extensionTypes.Date",
"optional": true,
"description": "Remove data accumulated on or after this date, represented in milliseconds since the epoch (accessible via the <code>getTime</code> method of the JavaScript <code>Date</code> object). If absent, defaults to 0 (which would remove all browsing data)."
},
+ "hostnames": {
+ "type": "array",
+ "items": {"type": "string", "format": "hostname"},
+ "optional": true,
+ "description": "Only remove data associated with these hostnames (only applies to cookies)."
+ },
"originTypes": {
"type": "object",
"optional": true,
"description": "An object whose properties specify which origin types ought to be cleared. If this object isn't specified, it defaults to clearing only \"unprotected\" origins. Please ensure that you <em>really</em> want to remove application data before adding 'protectedWeb' or 'extensions'.",
"properties": {
"unprotectedWeb": {
"type": "boolean",
"optional": true,
--- a/browser/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js
@@ -5,33 +5,49 @@
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
const COOKIE = {
host: "example.com",
name: "test_cookie",
path: "/",
};
+const COOKIE_NET = {
+ host: "example.net",
+ name: "test_cookie",
+ path: "/",
+};
+const COOKIE_ORG = {
+ host: "example.org",
+ name: "test_cookie",
+ path: "/",
+};
let since, oldCookie;
function addCookie(cookie) {
Services.cookies.add(cookie.host, cookie.path, cookie.name, "test", false, false, false, Date.now() / 1000 + 10000);
ok(Services.cookies.cookieExists(cookie), `Cookie ${cookie.name} was created.`);
}
async function setUpCookies() {
+ Services.cookies.removeAll();
+
// Add a cookie which will end up with an older creationTime.
oldCookie = Object.assign({}, COOKIE, {name: Date.now()});
addCookie(oldCookie);
await new Promise(resolve => setTimeout(resolve, 10));
since = Date.now();
await new Promise(resolve => setTimeout(resolve, 10));
// Add a cookie which will end up with a more recent creationTime.
addCookie(COOKIE);
+
+ // Add cookies for different domains.
+ addCookie(COOKIE_NET);
+ addCookie(COOKIE_ORG);
}
add_task(async function testCache() {
function background() {
browser.test.onMessage.addListener(async msg => {
if (msg == "removeCache") {
await browser.browsingData.removeCache({});
} else {
@@ -153,20 +169,56 @@ add_task(async function testCacheAndCook
awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
extension.sendMessage({since: since - 100000});
await awaitNotification;
await extension.awaitMessage("cacheAndCookiesRemoved");
ok(!Services.cookies.cookieExists(oldCookie), "Old cookie was removed.");
ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
- // Clear cache and cookies with no since value.
+ // Clear cache and cookies with hostnames value.
+ await setUpCookies();
+ awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+ extension.sendMessage({hostnames: ["example.net", "example.org", "unknown.com"]});
+ await awaitNotification;
+ await extension.awaitMessage("cacheAndCookiesRemoved");
+
+ ok(Services.cookies.cookieExists(COOKIE), `Cookie ${COOKIE.name} was not removed.`);
+ ok(!Services.cookies.cookieExists(COOKIE_NET), `Cookie ${COOKIE_NET.name} was removed.`);
+ ok(!Services.cookies.cookieExists(COOKIE_ORG), `Cookie ${COOKIE_ORG.name} was removed.`);
+
+ // Clear cache and cookies with (empty) hostnames value.
+ await setUpCookies();
+ awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+ extension.sendMessage({hostnames: []});
+ await awaitNotification;
+ await extension.awaitMessage("cacheAndCookiesRemoved");
+
+ ok(Services.cookies.cookieExists(COOKIE), `Cookie ${COOKIE.name} was not removed.`);
+ ok(Services.cookies.cookieExists(COOKIE_NET), `Cookie ${COOKIE_NET.name} was not removed.`);
+ ok(Services.cookies.cookieExists(COOKIE_ORG), `Cookie ${COOKIE_ORG.name} was not removed.`);
+
+ // Clear cache and cookies with both hostnames and since values.
+ await setUpCookies();
+ awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+ extension.sendMessage({hostnames: ["example.com"], since});
+ await awaitNotification;
+ await extension.awaitMessage("cacheAndCookiesRemoved");
+
+ ok(Services.cookies.cookieExists(oldCookie), "Old cookie was not removed.");
+ ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
+ ok(Services.cookies.cookieExists(COOKIE_NET), "Cookie with different hostname was not removed");
+ ok(Services.cookies.cookieExists(COOKIE_ORG), "Cookie with different hostname was not removed");
+
+ // Clear cache and cookies with no since or hostnames value.
await setUpCookies();
awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
extension.sendMessage({});
await awaitNotification;
await extension.awaitMessage("cacheAndCookiesRemoved");
ok(!Services.cookies.cookieExists(COOKIE), `Cookie ${COOKIE.name} was removed.`);
ok(!Services.cookies.cookieExists(oldCookie), `Cookie ${oldCookie.name} was removed.`);
+ ok(!Services.cookies.cookieExists(COOKIE_NET), `Cookie ${COOKIE_NET.name} was removed.`);
+ ok(!Services.cookies.cookieExists(COOKIE_ORG), `Cookie ${COOKIE_ORG.name} was removed.`);
await extension.unload();
});
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -806,16 +806,32 @@ class InjectionContext extends Context {
* The methods in this singleton represent the "format" specifier for
* JSON Schema string types.
*
* Each method either returns a normalized version of the original
* value, or throws an error if the value is not valid for the given
* format.
*/
const FORMATS = {
+ hostname(string, context) {
+ let valid = true;
+
+ try {
+ valid = new URL(`http://${string}`).host === string;
+ } catch (e) {
+ valid = false;
+ }
+
+ if (!valid) {
+ throw new Error(`Invalid hostname ${string}`);
+ }
+
+ return string;
+ },
+
url(string, context) {
let url = new URL(string).href;
if (!context.checkLoadURL(url)) {
throw new Error(`Access denied for URL ${url}`);
}
return url;
},
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -208,16 +208,17 @@ let json = [
{
name: "format",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
+ hostname: {type: "string", "format": "hostname", "optional": true},
url: {type: "string", "format": "url", "optional": true},
relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true},
},
},
],
},
@@ -617,25 +618,40 @@ add_task(async function() {
root.testing.pattern("DEADbeef");
verify("call", "testing", "pattern", ["DEADbeef"]);
tallied = null;
Assert.throws(() => root.testing.pattern("DEADcow"),
/String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
"should throw for non-match");
+ root.testing.format({hostname: "foo"});
+ verify("call", "testing", "format", [{hostname: "foo",
+ url: null,
+ relativeUrl: null,
+ strictRelativeUrl: null}]);
+ tallied = null;
+
+ for (let invalid of ["", " ", "http://foo", "foo/bar", "foo.com/", "foo?"]) {
+ Assert.throws(() => root.testing.format({hostname: invalid}),
+ /Invalid hostname/,
+ "should throw for invalid hostname");
+ }
+
root.testing.format({url: "http://foo/bar",
relativeUrl: "http://foo/bar"});
- verify("call", "testing", "format", [{url: "http://foo/bar",
+ verify("call", "testing", "format", [{hostname: null,
+ url: "http://foo/bar",
relativeUrl: "http://foo/bar",
strictRelativeUrl: null}]);
tallied = null;
root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"});
- verify("call", "testing", "format", [{url: null,
+ verify("call", "testing", "format", [{hostname: null,
+ url: null,
relativeUrl: `${wrapper.url}foo.html`,
strictRelativeUrl: `${wrapper.url}foo.html`}]);
tallied = null;
for (let format of ["url", "relativeUrl"]) {
Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}),
/Access denied/,
"should throw for access denied");