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 358480 0d944258ba3ff8ecab9d0fce7b1c211d1985d52b
parent 358479 442d75009799000429ce0f0b012dbccda63e37d4
child 358481 34579b580023357b5b68ad80689278bec5150195
push id10621
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 16:02:43 +0000
treeherdermozilla-aurora@dca7b42e6c67 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan, mak
bugs1321303
milestone53.0a1
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]