Bug 1321303 - Part 1: Implement browsingData.remove and removeCookies, r=aswan,mak
MozReview-Commit-ID: DawjN9bGcmL
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -1,16 +1,96 @@
"use strict";
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
"resource:///modules/Sanitizer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cookieMgr",
+ "@mozilla.org/cookiemanager;1",
+ "nsICookieManager");
+
+/**
+* A number of iterations after which to yield time back
+* to the system.
+*/
+const YIELD_PERIOD = 10;
+
+const PREF_DOMAIN = "privacy.cpd.";
+
+XPCOMUtils.defineLazyGetter(this, "sanitizer", () => {
+ let sanitizer = new Sanitizer();
+ sanitizer.prefDomain = PREF_DOMAIN;
+ return sanitizer;
+});
+
+let clearCookies = Task.async(function* (options) {
+ // This code has been borrowed from sanitize.js.
+ let yieldCounter = 0;
+
+ if (options.since) {
+ // 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)) {
+ // 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) {
+ yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+ }
+ }
+ }
+ } else {
+ // Remove everything.
+ cookieMgr.removeAll();
+ }
+});
+
+function doRemoval(options, dataToRemove, extension) {
+ if (options.originTypes &&
+ (options.originTypes.protectedWeb || options.originTypes.extension)) {
+ return Promise.reject(
+ {message: "Firefox does not support protectedWeb or extension as originTypes."});
+ }
+
+ let removalPromises = [];
+ let invalidDataTypes = [];
+ for (let dataType in dataToRemove) {
+ if (dataToRemove[dataType]) {
+ switch (dataType) {
+ case "cookies":
+ removalPromises.push(clearCookies(options));
+ break;
+ default:
+ invalidDataTypes.push(dataType);
+ }
+ }
+ }
+ if (extension && invalidDataTypes.length) {
+ extension.logger.warn(
+ `Firefox does not support dataTypes: ${invalidDataTypes.toString()}.`);
+ }
+ return Promise.all(removalPromises);
+}
extensions.registerSchemaAPI("browsingData", "addon_parent", context => {
+ let {extension} = context;
return {
browsingData: {
settings() {
const PREF_DOMAIN = "privacy.cpd.";
// The following prefs are the only ones in Firefox that match corresponding
// values used by Chrome when rerturning settings.
const PREF_LIST = ["cache", "cookies", "history", "formdata", "downloads"];
@@ -29,11 +109,17 @@ extensions.registerSchemaAPI("browsingDa
dataRemovalPermitted[item] = true;
}
// formData has a different case than the pref formdata.
dataToRemove.formData = Preferences.get(`${PREF_DOMAIN}formdata`);
dataRemovalPermitted.formData = true;
return Promise.resolve({options, dataToRemove, dataRemovalPermitted});
},
+ remove(options, dataToRemove) {
+ return doRemoval(options, dataToRemove, extension);
+ },
+ removeCookies(options) {
+ return doRemoval(options, {cookies: true});
+ },
},
};
});
--- a/browser/components/extensions/schemas/browsing_data.json
+++ b/browser/components/extensions/schemas/browsing_data.json
@@ -151,17 +151,16 @@
}
]
},
{
"name": "remove",
"description": "Clears various types of browsing data stored in a user's profile.",
"type": "function",
"async": "callback",
- "unsupported": true,
"parameters": [
{
"$ref": "RemovalOptions",
"name": "options"
},
{
"name": "dataToRemove",
"$ref": "DataTypeSet",
@@ -216,17 +215,16 @@
}
]
},
{
"name": "removeCookies",
"description": "Clears the browser's cookies and server-bound certificates modified within a particular timeframe.",
"type": "function",
"async": "callback",
- "unsupported": true,
"parameters": [
{
"$ref": "RemovalOptions",
"name": "options"
},
{
"name": "callback",
"type": "function",
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -1,14 +1,15 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-/* exported createHttpServer */
+/* exported createHttpServer, promiseConsoleOutput */
+Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
"resource://gre/modules/Extension.jsm");
@@ -48,8 +49,37 @@ function createHttpServer(port = -1) {
do_register_cleanup(() => {
return new Promise(resolve => {
server.stop(resolve);
});
});
return server;
}
+
+var promiseConsoleOutput = Task.async(function* (task) {
+ const DONE = `=== console listener ${Math.random()} done ===`;
+
+ let listener;
+ let messages = [];
+ let awaitListener = new Promise(resolve => {
+ listener = msg => {
+ if (msg == DONE) {
+ resolve();
+ } else {
+ void (msg instanceof Ci.nsIConsoleMessage);
+ messages.push(msg);
+ }
+ };
+ });
+
+ Services.console.registerListener(listener);
+ try {
+ let result = yield task();
+
+ Services.console.logStringMessage(DONE);
+ yield awaitListener;
+
+ return {messages, result};
+ } finally {
+ Services.console.unregisterListener(listener);
+ }
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData.js
@@ -0,0 +1,66 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testInvalidArguments() {
+ async function background() {
+ const UNSUPPORTED_DATA_TYPES = ["appcache", "fileSystems", "webSQL"];
+
+ await browser.test.assertRejects(
+ browser.browsingData.remove({originTypes: {protectedWeb: true}}, {cookies: true}),
+ "Firefox does not support protectedWeb or extension as originTypes.",
+ "Expected error received when using protectedWeb originType.");
+
+ await browser.test.assertRejects(
+ browser.browsingData.removeCookies({originTypes: {extension: true}}),
+ "Firefox does not support protectedWeb or extension as originTypes.",
+ "Expected error received when using extension originType.");
+
+ for (let dataType of UNSUPPORTED_DATA_TYPES) {
+ let dataTypes = {};
+ dataTypes[dataType] = true;
+ browser.test.assertThrows(
+ () => browser.browsingData.remove({}, dataTypes),
+ /Type error for parameter dataToRemove/,
+ `Expected error received when using ${dataType} dataType.`
+ );
+ }
+
+ browser.test.notifyPass("invalidArguments");
+ }
+
+ let extensionData = {
+ background: background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ await extension.awaitFinish("invalidArguments");
+ await extension.unload();
+});
+
+add_task(async function testUnimplementedDataType() {
+ function background() {
+ browser.browsingData.remove({}, {localStorage: true});
+ browser.test.sendMessage("finished");
+ }
+
+ let {messages} = await promiseConsoleOutput(async function() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished");
+ await extension.unload();
+ });
+
+ let warningObserved = messages.find(line => /Firefox does not support dataTypes: localStorage/.test(line));
+ ok(warningObserved, "Warning issued when calling remove with an unimplemented dataType.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_cookies.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+const COOKIE = {
+ host: "example.com",
+ name: "test_cookie",
+ path: "/",
+};
+
+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.`);
+}
+
+add_task(async function testCookies() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeCookies") {
+ await browser.browsingData.removeCookies(options);
+ } else {
+ await browser.browsingData.remove(options, {cookies: true});
+ }
+ browser.test.sendMessage("cookiesRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Add a cookie which will end up with an older creationTime.
+ let oldCookie = Object.assign({}, COOKIE, {name: Date.now()});
+ addCookie(oldCookie);
+ await new Promise(resolve => setTimeout(resolve, 10));
+ let since = Date.now();
+
+ // Add a cookie which will end up with a more recent creationTime.
+ addCookie(COOKIE);
+
+ // Clear cookies with a since value.
+ extension.sendMessage(method, {since});
+ await extension.awaitMessage("cookiesRemoved");
+
+ ok(Services.cookies.cookieExists(oldCookie), "Old cookie was not removed.");
+ ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
+
+ // Clear cookies with no since value and valid originTypes.
+ addCookie(COOKIE);
+ extension.sendMessage(
+ method,
+ {originTypes: {unprotectedWeb: true, protectedWeb: false}});
+ await extension.awaitMessage("cookiesRemoved");
+
+ ok(!Services.cookies.cookieExists(COOKIE), `Cookie ${COOKIE.name} was removed.`);
+ ok(!Services.cookies.cookieExists(oldCookie), `Cookie ${oldCookie.name} was removed.`);
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removeCookies");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,11 +1,13 @@
[DEFAULT]
head = head.js
firefox-appdir = browser
tags = webextensions
[test_ext_bookmarks.js]
+[test_ext_browsingData.js]
+[test_ext_browsingData_cookies.js]
[test_ext_browsingData_settings.js]
[test_ext_history.js]
[test_ext_manifest_commands.js]
[test_ext_manifest_omnibox.js]
[test_ext_manifest_permissions.js]