Bug 1321303 - Part 1: Implement browsingData.remove and removeCookies, r=aswan,mak
authorBob Silverberg <bsilverberg@mozilla.com>
Wed, 30 Nov 2016 10:18:02 -0500
changeset 377740 0d944258ba3ff8ecab9d0fce7b1c211d1985d52b
parent 377739 442d75009799000429ce0f0b012dbccda63e37d4
child 377741 34579b580023357b5b68ad80689278bec5150195
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan, mak
bugs1321303
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1321303 - Part 1: Implement browsingData.remove and removeCookies, r=aswan,mak MozReview-Commit-ID: DawjN9bGcmL
browser/components/extensions/ext-browsingData.js
browser/components/extensions/schemas/browsing_data.json
browser/components/extensions/test/xpcshell/head.js
browser/components/extensions/test/xpcshell/test_ext_browsingData.js
browser/components/extensions/test/xpcshell/test_ext_browsingData_cookies.js
browser/components/extensions/test/xpcshell/xpcshell.ini
--- 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]