Bug 1363004 - Implement browsingData.removeFormData webExtension API method on android. r=bsilverberg, grisha draft
authorTushar Saini (:shatur) <tushar.saini1285@gmail.com>
Mon, 19 Jun 2017 20:53:00 +0530
changeset 654234 20b536c99f5289635ad78808913889451bad489e
parent 653594 beb27d607b89aabfaa1ecf857a57031d8ccb939d
child 728511 229334b00eaa664aa50c147ac73afac28917ec75
push id76514
push userbmo:tushar.saini1285@gmail.com
push dateMon, 28 Aug 2017 12:38:28 +0000
reviewersbsilverberg, grisha
bugs1363004
milestone57.0a1
Bug 1363004 - Implement browsingData.removeFormData webExtension API method on android. r=bsilverberg, grisha MozReview-Commit-ID: F0hqTCOCA23
mobile/android/components/extensions/ext-browsingData.js
mobile/android/components/extensions/schemas/browsing_data.json
mobile/android/components/extensions/test/mochitest/chrome.ini
mobile/android/components/extensions/test/mochitest/test_ext_browsingData_formdata.html
mobile/android/modules/Sanitizer.jsm
--- a/mobile/android/components/extensions/ext-browsingData.js
+++ b/mobile/android/components/extensions/ext-browsingData.js
@@ -92,12 +92,15 @@ this.browsingData = class extends Extens
           return clearCookies(options);
         },
         removeCache(options) {
           return Sanitizer.clearItem("cache");
         },
         async removeDownloads(options) {
           return Sanitizer.clearItem("downloadHistory", options.since);
         },
+        removeFormData(options) {
+          return Sanitizer.clearItem("formdata", options.since);
+        },
       },
     };
   }
 };
--- a/mobile/android/components/extensions/schemas/browsing_data.json
+++ b/mobile/android/components/extensions/schemas/browsing_data.json
@@ -279,17 +279,16 @@
           }
         ]
       },
       {
         "name": "removeFormData",
         "description": "Clears the browser's stored form data (autofill).",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
--- a/mobile/android/components/extensions/test/mochitest/chrome.ini
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -3,13 +3,14 @@ support-files =
   head.js
   ../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js
 tags = webextensions
 
 [test_ext_browserAction_getTitle_setTitle.html]
 [test_ext_browserAction_onClicked.html]
 [test_ext_browsingData_cookies_cache.html]
 [test_ext_browsingData_downloads.html]
+[test_ext_browsingData_formdata.html]
 [test_ext_browsingData_settings.html]
 [test_ext_options_ui.html]
 [test_ext_pageAction_show_hide.html]
 [test_ext_pageAction_getPopup_setPopup.html]
 skip-if = os == 'android' # bug 1373170
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_browsingData_formdata.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>BrowsingData FormData test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var {FormHistory} = Cu.import("resource://gre/modules/FormHistory.jsm", {});
+
+const REFERENCE_DATE = Date.now();
+
+function countEntries(fieldname, message, expected) {
+  return new Promise((resolve, reject) => {
+    let callback = {
+      handleResult: result => {
+        is(result, expected, message);
+        resolve();
+      },
+      handleError: reject,
+    };
+
+    FormHistory.count({fieldname}, callback);
+  });
+}
+
+async function setupFormHistory() {
+  function searchEntries(terms, params) {
+    return new Promise((resolve, reject) => {
+      let callback = {
+        handleResult: resolve,
+        handleError: reject,
+      };
+
+      FormHistory.search(terms, params, callback);
+    });
+  }
+
+  function update(changes) {
+    return new Promise((resolve, reject) => {
+      let callback = {
+        handleError: reject,
+        handleCompletion: resolve,
+      };
+      FormHistory.update(changes, callback);
+    });
+  }
+
+  // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+  await update([
+    {op: "remove"},
+    {
+      op: "add",
+      fieldname: "reference",
+      value: "reference",
+    }, {
+      op: "add",
+      fieldname: "10secondsAgo",
+      value: "10s",
+    }, {
+      op: "add",
+      fieldname: "10minutesAgo",
+      value: "10m",
+    }]);
+
+  // Age the entries to the proper vintage.
+  let timestamp = REFERENCE_DATE * 1000;
+  let result = await searchEntries(["guid"], {fieldname: "reference"});
+  await update({op: "update", firstUsed: timestamp, guid: result.guid});
+
+  timestamp = (REFERENCE_DATE - 10000) * 1000;
+  result = await searchEntries(["guid"], {fieldname: "10secondsAgo"});
+  await update({op: "update", firstUsed: timestamp, guid: result.guid});
+
+  timestamp = (REFERENCE_DATE - 10000 * 60) * 1000;
+  result = await searchEntries(["guid"], {fieldname: "10minutesAgo"});
+  await update({op: "update", firstUsed: timestamp, guid: result.guid});
+
+  // Sanity check.
+  await countEntries("reference", "Checking for 10minutes form history entry creation", 1);
+  await countEntries("10secondsAgo", "Checking for 1hour form history entry creation", 1);
+  await countEntries("10minutesAgo", "Checking for 1hour10minutes form history entry creation", 1);
+}
+
+add_task(async function testFormData() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      await browser.browsingData.removeFormData(options);
+      browser.test.sendMessage("formDataRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear form data with no since value.
+    await setupFormHistory();
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("formDataRemoved");
+
+    await countEntries("reference", "reference form entry should be deleted.", 0);
+    await countEntries("10secondsAgo", "10secondsAgo form entry should be deleted.", 0);
+    await countEntries("10minutesAgo", "10minutesAgo form entry should be deleted.", 0);
+
+    // Clear form data with recent since value.
+    await setupFormHistory();
+    extension.sendMessage(method, {since: REFERENCE_DATE});
+    await extension.awaitMessage("formDataRemoved");
+
+    await countEntries("reference", "reference form entry should be deleted.", 0);
+    await countEntries("10secondsAgo", "10secondsAgo form entry should still exist.", 1);
+    await countEntries("10minutesAgo", "10minutesAgo form entry should still exist.", 1);
+
+    // Clear form data with old since value.
+    await setupFormHistory();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 1000000});
+    await extension.awaitMessage("formDataRemoved");
+
+    await countEntries("reference", "reference form entry should be deleted.", 0);
+    await countEntries("10secondsAgo", "10secondsAgo form entry should be deleted.", 0);
+    await countEntries("10minutesAgo", "10minutesAgo form entry should be deleted.", 0);
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeFormData");
+
+  await extension.unload();
+});
+
+</script>
+</body>
+</html>
--- a/mobile/android/modules/Sanitizer.jsm
+++ b/mobile/android/modules/Sanitizer.jsm
@@ -38,16 +38,19 @@ Sanitizer.prototype = {
     // Those who do not will be rejected with error message.
     if (typeof startTime != "undefined") {
       switch (aItemName) {
         // Normal call to DownloadFiles remove actual data from storage, but our web-extension consumer
         // deletes only download history. So, for this reason we are passing a flag 'deleteFiles'.
         case "downloadHistory":
           this._clear("downloadFiles", { startTime, deleteFiles: false });
           break;
+        case "formdata":
+          this._clear(aItemName, { startTime });
+          break;
         default:
           return Promise.reject({message: `Invalid argument: ${aItemName} does not support startTime argument.`});
       }
     } else {
       this._clear(aItemName);
     }
   },
 
@@ -222,22 +225,27 @@ Sanitizer.prototype = {
       },
 
       get canClear() {
         return true;
       }
     },
 
     formdata: {
-      clear: function() {
+      clear: function({ startTime = 0 } = {}) {
         return new Promise(function(resolve, reject) {
           let refObj = {};
           TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj);
 
-          FormHistory.update({ op: "remove" });
+          // Conver time to microseconds
+          let time = startTime * 1000;
+          FormHistory.update({
+            op: "remove",
+            firstUsedStart: time
+          });
 
           TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj);
           resolve();
         });
       },
 
       canClear: function(aCallback) {
         let count = 0;