From 2c43ab2f6429257d8489aac13a99a31239342d7b Mon Sep 17 00:00:00 2001 From: Nikhil Marathe Date: Wed, 29 Jul 2015 11:33:48 -0700 Subject: [PATCH] Bug 1188686 - Clear push subscriptions when forgetting about site. r=kitcambridge --- .../push/nsIPushNotificationService.idl | 11 +++- dom/push/PushDB.jsm | 30 +++++++++ dom/push/PushNotificationService.js | 4 ++ dom/push/PushService.jsm | 42 +++++++++++- dom/push/test/xpcshell/head.js | 2 +- toolkit/forgetaboutsite/ForgetAboutSite.jsm | 10 +++ .../test/unit/head_forgetaboutsite.js | 6 +- .../test/unit/test_removeDataFromDomain.js | 64 +++++++++++++++++++ .../forgetaboutsite/test/unit/xpcshell.ini | 2 +- 9 files changed, 162 insertions(+), 9 deletions(-) diff --git a/dom/interfaces/push/nsIPushNotificationService.idl b/dom/interfaces/push/nsIPushNotificationService.idl index 7dd1ac51b0e..da947aaf1b7 100644 --- a/dom/interfaces/push/nsIPushNotificationService.idl +++ b/dom/interfaces/push/nsIPushNotificationService.idl @@ -11,7 +11,7 @@ * uses service workers to notify applications. This interface exists to allow * privileged code to receive messages without migrating to service workers. */ -[scriptable, uuid(abde228b-7d14-4cab-b1f9-9f87750ede0f)] +[scriptable, uuid(74586476-d73f-4867-bece-87c1dea35750)] interface nsIPushNotificationService : nsISupports { /** @@ -48,7 +48,12 @@ interface nsIPushNotificationService : nsISupports jsval registration(in string scope, in jsval originAttributes); /** - * Clear all subscriptions + * Clear all subscriptions. */ - jsval clearAll(); + jsval clearAll(); + + /** + * Clear subscriptions for a domain. + */ + jsval clearForDomain(in string domain); }; diff --git a/dom/push/PushDB.jsm b/dom/push/PushDB.jsm index 613d2187802..60a4f336219 100644 --- a/dom/push/PushDB.jsm +++ b/dom/push/PushDB.jsm @@ -154,6 +154,36 @@ this.PushDB.prototype = { ); }, + // testFn(record) is called with a database record and should return true if + // that record should be deleted. + clearIf: function(testFn) { + debug("clearIf()"); + return new Promise((resolve, reject) => + this.newTxn( + "readwrite", + this._dbStoreName, + (aTxn, aStore) => { + aTxn.result = undefined; + + aStore.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + if (testFn(this.toPushRecord(cursor.value))) { + let deleteRequest = cursor.delete(); + deleteRequest.onerror = e => { + debug("Failed to delete entry even when test succeeded!"); + } + } + cursor.continue(); + } + } + }, + resolve, + reject + ) + ); + }, + getByPushEndpoint: function(aPushEndpoint) { debug("getByPushEndpoint()"); diff --git a/dom/push/PushNotificationService.js b/dom/push/PushNotificationService.js index 4ce0189017d..9ecc8f671db 100644 --- a/dom/push/PushNotificationService.js +++ b/dom/push/PushNotificationService.js @@ -58,6 +58,10 @@ PushNotificationService.prototype = { return PushService._clearAll(); }, + clearForDomain: function(domain) { + return PushService._clearForDomain(domain); + }, + observe: function observe(subject, topic, data) { switch (topic) { case "app-startup": diff --git a/dom/push/PushService.jsm b/dom/push/PushService.jsm index 88ba57a7d2f..471a08fa5e4 100644 --- a/dom/push/PushService.jsm +++ b/dom/push/PushService.jsm @@ -1027,7 +1027,47 @@ this.PushService = { _clearAll: function _clearAll() { return this._checkActivated() .then(_ => this._db.clearAll()) - .catch(_ => { + .catch(_ => Promise.resolve()); + }, + + _clearForDomain: function(domain) { + /** + * Copied from ForgetAboutSite.jsm. + * + * Returns true if the string passed in is part of the root domain of the + * current string. For example, if this is "www.mozilla.org", and we pass in + * "mozilla.org", this will return true. It would return false the other way + * around. + */ + function hasRootDomain(str, aDomain) + { + let index = str.indexOf(aDomain); + // If aDomain is not found, we know we do not have it as a root domain. + if (index == -1) + return false; + + // If the strings are the same, we obviously have a match. + if (str == aDomain) + return true; + + // Otherwise, we have aDomain as our root domain iff the index of aDomain is + // aDomain.length subtracted from our length and (since we do not have an + // exact match) the character before the index is a dot or slash. + let prevChar = str[index - 1]; + return (index == (str.length - aDomain.length)) && + (prevChar == "." || prevChar == "/"); + } + + let clear = (db, domain) => { + db.clearIf(record => { + return hasRootDomain(record.origin, domain); + }); + } + + return this._checkActivated() + .then(_ => clear(this._db, domain)) + .catch(e => { + debug("Error forgetting about domain! " + e); return Promise.resolve(); }); }, diff --git a/dom/push/test/xpcshell/head.js b/dom/push/test/xpcshell/head.js index 327812ca58b..aa1ce9b2c75 100644 --- a/dom/push/test/xpcshell/head.js +++ b/dom/push/test/xpcshell/head.js @@ -3,7 +3,7 @@ 'use strict'; -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import('resource://gre/modules/XPCOMUtils.jsm'); Cu.import('resource://gre/modules/Services.jsm'); diff --git a/toolkit/forgetaboutsite/ForgetAboutSite.jsm b/toolkit/forgetaboutsite/ForgetAboutSite.jsm index ad9f5a46370..bbff491c40c 100644 --- a/toolkit/forgetaboutsite/ForgetAboutSite.jsm +++ b/toolkit/forgetaboutsite/ForgetAboutSite.jsm @@ -181,6 +181,16 @@ this.ForgetAboutSite = { let np = Cc["@mozilla.org/network/predictor;1"]. getService(Ci.nsINetworkPredictor); np.reset(); + + // Push notifications. + try { + var push = Cc["@mozilla.org/push/NotificationService;1"] + .getService(Ci.nsIPushNotificationService); + push.clearForDomain(aDomain); + } catch (e) { + dump("Web Push may not be available.\n"); + } + return Promise.all(promises); } }; diff --git a/toolkit/forgetaboutsite/test/unit/head_forgetaboutsite.js b/toolkit/forgetaboutsite/test/unit/head_forgetaboutsite.js index 8e7a9ea4c42..ba72a264f8a 100644 --- a/toolkit/forgetaboutsite/test/unit/head_forgetaboutsite.js +++ b/toolkit/forgetaboutsite/test/unit/head_forgetaboutsite.js @@ -2,9 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); var profileDir = do_get_profile(); diff --git a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js b/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js index d9a40aa9815..a6793091c70 100644 --- a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js +++ b/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js @@ -462,6 +462,67 @@ function test_content_preferences_not_cleared_with_uri_contains_domain() do_check_false(yield preference_exists(TEST_URI)); } +// Push +function test_push_cleared() +{ + let ps; + try { + ps = Cc["@mozilla.org/push/NotificationService;1"]. + getService(Ci.nsIPushNotificationService); + } catch(e) { + // No push service, skip test. + return; + } + + do_get_profile(); + setPrefs(); + const {PushDB, PushService, PushServiceWebSocket} = serviceExports; + const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f'; + const channelID = '0ef2ad4a-6c49-41ad-af6e-95d2425276bf'; + + let db = PushServiceWebSocket.newPushDB(); + do_register_cleanup(() => {return db.drop().then(_ => db.close());}); + + PushService.init({ + serverURI: "wss://push.example.org/", + networkInfo: new MockDesktopNetworkInfo(), + db, + makeWebSocket(uri) { + return new MockWebSocket(uri, { + onHello(request) { + this.serverSendMsg(JSON.stringify({ + messageType: 'hello', + status: 200, + uaid: userAgentID, + })); + }, + }); + } + }); + + function push_registration_exists(aURL, ps) + { + return ps.registration(aURL, ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })) + .then(record => !!record) + .catch(_ => false); + } + + const TEST_URL = "https://www.mozilla.org/scope/"; + do_check_false(yield push_registration_exists(TEST_URL, ps)); + yield db.put({ + channelID, + pushEndpoint: 'https://example.org/update/clear-success', + scope: TEST_URL, + version: 1, + originAttributes: '', + quota: Infinity, + }); + do_check_true(yield push_registration_exists(TEST_URL, ps)); + ForgetAboutSite.removeDataFromDomain("mozilla.org"); + yield waitForPurgeNotification(); + do_check_false(yield push_registration_exists(TEST_URL, ps)); +} + // Cache function test_cache_cleared() { @@ -555,6 +616,9 @@ let tests = [ test_content_preferences_cleared_with_subdomain, test_content_preferences_not_cleared_with_uri_contains_domain, + // Push + test_push_cleared, + // Storage test_storage_cleared, diff --git a/toolkit/forgetaboutsite/test/unit/xpcshell.ini b/toolkit/forgetaboutsite/test/unit/xpcshell.ini index a4e800e2446..0d1abb25f43 100644 --- a/toolkit/forgetaboutsite/test/unit/xpcshell.ini +++ b/toolkit/forgetaboutsite/test/unit/xpcshell.ini @@ -1,5 +1,5 @@ [DEFAULT] -head = head_forgetaboutsite.js +head = head_forgetaboutsite.js ../../../../dom/push/test/xpcshell/head.js tail = skip-if = toolkit == 'android' || toolkit == 'gonk'