Bug 1170115 - Use `clear-origin-data` to remove Push records. r=allstars.chh
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 13 Nov 2015 15:18:10 -0800
changeset 273013 a43c962a140f1f83fe7ceba5ff4e31346ecc7e82
parent 272973 f3eb66d21d1ca1e9b140a3052dda40717358562b
child 273014 fe34d01fb2ecb2dd4cda82e788cf7b541d5cbdb4
push id29693
push usercbook@mozilla.com
push dateWed, 18 Nov 2015 13:50:33 +0000
treeherdermozilla-central@1d6155d7e6c9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersallstars.chh
bugs1170115
milestone45.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 1170115 - Use `clear-origin-data` to remove Push records. r=allstars.chh
dom/push/PushRecord.jsm
dom/push/PushService.jsm
dom/push/test/xpcshell/head.js
dom/push/test/xpcshell/test_clear_origin_data.js
dom/push/test/xpcshell/test_webapps_cleardata.js
dom/push/test/xpcshell/xpcshell.ini
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -204,16 +204,21 @@ PushRecord.prototype = {
   quotaApplies() {
     return Number.isFinite(this.quota);
   },
 
   isExpired() {
     return this.quota === 0;
   },
 
+  matchesOriginAttributes(pattern) {
+    return ChromeUtils.originAttributesMatchPattern(
+      this.principal.originAttributes, pattern);
+  },
+
   toSubscription() {
     return {
       pushEndpoint: this.pushEndpoint,
       lastPush: this.lastPush,
       pushCount: this.pushCount,
       p256dhKey: this.p256dhPublicKey,
     };
   },
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -272,47 +272,41 @@ this.PushService = {
 
       case "perm-changed":
         this._onPermissionChange(aSubject, aData).catch(error => {
           console.error("onPermissionChange: Error updating registrations:",
             error);
         })
         break;
 
-      case "webapps-clear-data":
-        console.debug("webapps-clear-data");
-
-        let data = aSubject
-                     .QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
-        if (!data) {
-          console.error("webapps-clear-data: Failed to get information " +
-            "about application");
-          return;
-        }
-
-        var originAttributes =
-          ChromeUtils.originAttributesToSuffix({ appId: data.appId,
-                                                 inBrowser: data.browserOnly });
-        this._db.getAllByOriginAttributes(originAttributes)
-          .then(records => Promise.all(records.map(record =>
-            this._db.delete(record.keyID)
-              .catch(err => {
-                console.error("webapps-clear-data: Error removing record",
-                  record, err);
-                // This is the record we were unable to delete.
-                return record;
-              })
-              .then(maybeDeleted => this._backgroundUnregister(maybeDeleted))
-            )
-          ));
-
+      case "clear-origin-data":
+        this._clearOriginData(data).catch(error => {
+          console.error("clearOriginData: Error clearing origin data:", error);
+        });
         break;
     }
   },
 
+  _clearOriginData: function(data) {
+    console.log("clearOriginData()");
+
+    if (!data) {
+      return Promise.resolve();
+    }
+
+    let pattern = JSON.parse(data);
+    return this._db.clearIf(record => {
+      if (!record.matchesOriginAttributes(pattern)) {
+        return false;
+      }
+      this._backgroundUnregister(record);
+      return true;
+    });
+  },
+
   /**
    * Sends an unregister request to the server in the background. If the
    * service is not connected, this function is a no-op.
    *
    * @param {PushRecord} record The record to unregister.
    */
   _backgroundUnregister: function(record) {
     console.debug("backgroundUnregister()");
@@ -482,17 +476,17 @@ this.PushService = {
 
   _startObservers: function() {
     console.debug("startObservers()");
 
     if (this._state != PUSH_SERVICE_ACTIVATING) {
       return;
     }
 
-    Services.obs.addObserver(this, "webapps-clear-data", false);
+    Services.obs.addObserver(this, "clear-origin-data", false);
 
     // On B2G the NetworkManager interface fires a network-active-changed
     // event.
     //
     // The "active network" is based on priority - i.e. Wi-Fi has higher
     // priority than data. The PushService should just use the preferred
     // network, and not care about all interface changes.
     // network-active-changed is not fired when the network goes offline, but
@@ -609,17 +603,17 @@ this.PushService = {
 
     if (this._state < PUSH_SERVICE_ACTIVATING) {
       return;
     }
 
     prefs.ignore("connection.enabled", this);
 
     Services.obs.removeObserver(this, this._networkStateChangeEventName);
-    Services.obs.removeObserver(this, "webapps-clear-data");
+    Services.obs.removeObserver(this, "clear-origin-data");
     Services.obs.removeObserver(this, "idle-daily");
     Services.obs.removeObserver(this, "perm-changed");
   },
 
   uninit: function() {
     console.debug("uninit()");
 
     this._childListeners = [];
--- a/dom/push/test/xpcshell/head.js
+++ b/dom/push/test/xpcshell/head.js
@@ -7,16 +7,17 @@ var {classes: Cc, interfaces: Ci, utils:
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/Task.jsm');
 Cu.import('resource://gre/modules/Timer.jsm');
 Cu.import('resource://gre/modules/Promise.jsm');
 Cu.import('resource://gre/modules/Preferences.jsm');
 Cu.import('resource://gre/modules/PlacesUtils.jsm');
+Cu.import('resource://gre/modules/ObjectUtils.jsm');
 
 const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {});
 const servicePrefs = new Preferences('dom.push.');
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "PushNotificationService",
   "@mozilla.org/push/NotificationService;1",
new file mode 100644
--- /dev/null
+++ b/dom/push/test/xpcshell/test_clear_origin_data.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f';
+
+let clearForPattern = Task.async(function* (testRecords, pattern) {
+  let patternString = JSON.stringify(pattern);
+  yield PushService._clearOriginData(patternString);
+
+  for (let length = testRecords.length; length--;) {
+    let test = testRecords[length];
+    let originSuffix = ChromeUtils.originAttributesToSuffix(
+      test.originAttributes);
+
+    let registration = yield PushNotificationService.registration(
+      test.scope,
+      originSuffix
+    );
+
+    let url = test.scope + originSuffix;
+
+    if (ObjectUtils.deepEqual(test.clearIf, pattern)) {
+      ok(!registration, 'Should clear registration ' + url +
+        ' for pattern ' + patternString);
+      testRecords.splice(length, 1);
+    } else {
+      ok(registration, 'Should not clear registration ' + url +
+        ' for pattern ' + patternString);
+    }
+  }
+});
+
+function run_test() {
+  do_get_profile();
+  setPrefs({
+    userAgentID,
+    requestTimeout: 1000,
+    retryBaseInterval: 150
+  });
+  disableServiceWorkerEvents(
+    'https://example.org/1'
+  );
+  run_next_test();
+}
+
+add_task(function* test_webapps_cleardata() {
+  let db = PushServiceWebSocket.newPushDB();
+  do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+  let testRecords = [{
+    scope: 'https://example.org/1',
+    originAttributes: { appId: 1 },
+    clearIf: { appId: 1, inBrowser: false },
+  }, {
+    scope: 'https://example.org/1',
+    originAttributes: { appId: 1, inBrowser: true },
+    clearIf: { appId: 1 },
+  }, {
+    scope: 'https://example.org/1',
+    originAttributes: { appId: 2, inBrowser: true },
+    clearIf: { appId: 2, inBrowser: true },
+  }, {
+    scope: 'https://example.org/2',
+    originAttributes: { appId: 1 },
+    clearIf: { appId: 1, inBrowser: false },
+  }, {
+    scope: 'https://example.org/2',
+    originAttributes: { appId: 2, inBrowser: true },
+    clearIf: { appId: 2, inBrowser: true },
+  }, {
+    scope: 'https://example.org/3',
+    originAttributes: { appId: 3, inBrowser: true },
+    clearIf: { inBrowser: true },
+  }, {
+    scope: 'https://example.org/3',
+    originAttributes: { appId: 4, inBrowser: true },
+    clearIf: { inBrowser: true },
+  }];
+
+  let unregisterDone;
+  let unregisterPromise = new Promise(resolve =>
+    unregisterDone = after(testRecords.length, resolve));
+
+  PushService.init({
+    serverURI: "wss://push.example.org",
+    networkInfo: new MockDesktopNetworkInfo(),
+    db,
+    makeWebSocket(uri) {
+      return new MockWebSocket(uri, {
+        onHello(data) {
+          equal(data.messageType, 'hello', 'Handshake: wrong message type');
+          equal(data.uaid, userAgentID, 'Handshake: wrong device ID');
+          this.serverSendMsg(JSON.stringify({
+            messageType: 'hello',
+            status: 200,
+            uaid: userAgentID
+          }));
+        },
+        onRegister(data) {
+          equal(data.messageType, 'register', 'Register: wrong message type');
+          this.serverSendMsg(JSON.stringify({
+            messageType: 'register',
+            status: 200,
+            channelID: data.channelID,
+            uaid: userAgentID,
+            pushEndpoint: 'https://example.com/update/' + Math.random(),
+          }));
+        },
+        onUnregister(data) {
+          unregisterDone();
+        },
+      });
+    }
+  });
+
+  yield Promise.all(testRecords.map(test =>
+    PushNotificationService.register(
+      test.scope,
+      ChromeUtils.originAttributesToSuffix(test.originAttributes)
+    )
+  ));
+
+  // Removes records for all scopes with the same app ID. Excludes records
+  // where `inBrowser` is true.
+  yield clearForPattern(testRecords, { appId: 1, inBrowser: false });
+
+  // Removes the remaining record for app ID 1, where `inBrowser` is true.
+  yield clearForPattern(testRecords, { appId: 1 });
+
+  // Removes all records for all scopes with the same app ID, where
+  // `inBrowser` is true.
+  yield clearForPattern(testRecords, { appId: 2, inBrowser: true });
+
+  // Removes all records where `inBrowser` is true.
+  yield clearForPattern(testRecords, { inBrowser: true });
+
+  equal(testRecords.length, 0, 'Should remove all test records');
+  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
+    'Timed out waiting for unregister');
+});
deleted file mode 100644
--- a/dom/push/test/xpcshell/test_webapps_cleardata.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-'use strict';
-
-const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
-
-const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f';
-
-function run_test() {
-  do_get_profile();
-  setPrefs({
-    userAgentID,
-    requestTimeout: 1000,
-    retryBaseInterval: 150
-  });
-  disableServiceWorkerEvents(
-    'https://example.org/1'
-  );
-  run_next_test();
-}
-
-add_task(function* test_webapps_cleardata() {
-  let db = PushServiceWebSocket.newPushDB();
-  do_register_cleanup(() => {return db.drop().then(_ => db.close());});
-
-  let unregisterDone;
-  let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
-
-  PushService.init({
-    serverURI: "wss://push.example.org",
-    networkInfo: new MockDesktopNetworkInfo(),
-    db,
-    makeWebSocket(uri) {
-      return new MockWebSocket(uri, {
-        onHello(data) {
-          equal(data.messageType, 'hello', 'Handshake: wrong message type');
-          equal(data.uaid, userAgentID, 'Handshake: wrong device ID');
-          this.serverSendMsg(JSON.stringify({
-            messageType: 'hello',
-            status: 200,
-            uaid: userAgentID
-          }));
-        },
-        onRegister(data) {
-          equal(data.messageType, 'register', 'Register: wrong message type');
-          this.serverSendMsg(JSON.stringify({
-            messageType: 'register',
-            status: 200,
-            channelID: data.channelID,
-            uaid: userAgentID,
-            pushEndpoint: 'https://example.com/update/' + Math.random(),
-          }));
-        },
-        onUnregister(data) {
-          unregisterDone();
-        },
-      });
-    }
-  });
-
-  let registers = yield Promise.all([
-    PushNotificationService.register(
-      'https://example.org/1',
-      ChromeUtils.originAttributesToSuffix({ appId: 1, inBrowser: false })),
-    PushNotificationService.register(
-      'https://example.org/1',
-      ChromeUtils.originAttributesToSuffix({ appId: 1, inBrowser: true })),
-  ]);
-
-  Services.obs.notifyObservers(
-      { appId: 1, browserOnly: false,
-        QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])},
-      "webapps-clear-data", "");
-
-  let waitAWhile = new Promise(function(res) {
-    setTimeout(res, 2000);
-  });
-  yield waitAWhile;
-
-  let registration;
-  registration = yield PushNotificationService.registration(
-    'https://example.org/1',
-    ChromeUtils.originAttributesToSuffix({ appId: 1, inBrowser: false }));
-  ok(!registration, 'Registration for { 1, false } should not exist.');
-
-  registration = yield PushNotificationService.registration(
-    'https://example.org/1',
-    ChromeUtils.originAttributesToSuffix({ appId: 1, inBrowser: true }));
-  ok(registration, 'Registration for { 1, true } should still exist.');
-
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister');
-});
-
--- a/dom/push/test/xpcshell/xpcshell.ini
+++ b/dom/push/test/xpcshell/xpcshell.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 head = head.js head-http2.js
 tail =
 # Push notifications and alarms are currently disabled on Android.
 skip-if = toolkit == 'android'
 
+[test_clear_origin_data.js]
 [test_drop_expired.js]
 [test_notification_ack.js]
 [test_notification_data.js]
 [test_notification_duplicate.js]
 [test_notification_error.js]
 [test_notification_incomplete.js]
 [test_notification_version_string.js]
 
@@ -34,17 +35,16 @@ run-sequentially = This will delete all 
 [test_registration_missing_scope.js]
 [test_registration_none.js]
 [test_registration_success.js]
 [test_unregister_empty_scope.js]
 [test_unregister_error.js]
 [test_unregister_invalid_json.js]
 [test_unregister_not_found.js]
 [test_unregister_success.js]
-[test_webapps_cleardata.js]
 [test_updateRecordNoEncryptionKeys_ws.js]
 [test_reconnect_retry.js]
 [test_retry_ws.js]
 #http2 test
 [test_resubscribe_4xxCode_http2.js]
 [test_resubscribe_5xxCode_http2.js]
 [test_resubscribe_listening_for_msg_error_http2.js]
 [test_register_5xxCode_http2.js]