ThirdPartyCookieBlockingExceptionListService.sys.mjs (7095B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 9 }); 10 11 // Name of the RemoteSettings collection containing the records. 12 const COLLECTION_NAME = "third-party-cookie-blocking-exempt-urls"; 13 const PREF_NAME = "network.cookie.cookieBehavior.optInPartitioning.skip_list"; 14 15 export class ThirdPartyCookieBlockingExceptionListService { 16 classId = Components.ID("{1ee0cc18-c968-4105-a895-bdea08e187eb}"); 17 QueryInterface = ChromeUtils.generateQI([ 18 "nsIThirdPartyCookieBlockingExceptionListService", 19 ]); 20 21 #rs = null; 22 #onSyncCallback = null; 23 24 // Sets to keep track of the exceptions in the pref. It uses the string in the 25 // format "firstPartySite,thirdPartySite" as the key. 26 #prefValueSet = null; 27 // Set to keep track of exceptions from RemoteSettings. It uses the same 28 // keying as above. 29 #rsValueSet = null; 30 31 constructor() { 32 this.#rs = lazy.RemoteSettings(COLLECTION_NAME); 33 } 34 35 async init() { 36 await this.importAllExceptions(); 37 38 Services.prefs.addObserver(PREF_NAME, this); 39 40 if (!this.#onSyncCallback) { 41 this.#onSyncCallback = this.onSync.bind(this); 42 this.#rs.on("sync", this.#onSyncCallback); 43 } 44 45 // Import for initial pref state. 46 this.onPrefChange(); 47 } 48 49 shutdown() { 50 Services.prefs.removeObserver(PREF_NAME, this); 51 52 if (this.#onSyncCallback) { 53 this.#rs.off("sync", this.#onSyncCallback); 54 this.#onSyncCallback = null; 55 } 56 } 57 58 #handleExceptionChange(created = [], deleted = []) { 59 if (created.length) { 60 Services.cookies.addThirdPartyCookieBlockingExceptions(created); 61 } 62 if (deleted.length) { 63 Services.cookies.removeThirdPartyCookieBlockingExceptions(deleted); 64 } 65 } 66 67 onSync({ data: { created = [], updated = [], deleted = [] } }) { 68 // Convert the RemoteSettings records to exception entries. 69 created = created.map(ex => 70 ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord(ex) 71 ); 72 deleted = deleted.map(ex => 73 ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord(ex) 74 ); 75 76 updated.forEach(ex => { 77 let newEntry = ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord( 78 ex.new 79 ); 80 let oldEntry = ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord( 81 ex.old 82 ); 83 84 // We only care about changes in the sites. 85 if (newEntry.equals(oldEntry)) { 86 return; 87 } 88 created.push(newEntry); 89 deleted.push(oldEntry); 90 }); 91 92 this.#rsValueSet ??= new Set(); 93 94 // Remove items in sitesToRemove 95 for (const site of deleted) { 96 this.#rsValueSet.delete(site.serialize()); 97 } 98 99 // Add items from sitesToAdd 100 for (const site of created) { 101 this.#rsValueSet.add(site.serialize()); 102 } 103 104 this.#handleExceptionChange(created, deleted); 105 } 106 107 onPrefChange() { 108 let newExceptions = Services.prefs.getStringPref(PREF_NAME, "").split(";"); 109 110 // Convert the exception strings to exception entries. 111 newExceptions = newExceptions 112 .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex)) 113 .filter(Boolean); 114 115 // If this is the first time we're initializing from pref, we can directly 116 // call handleExceptionChange to create the exceptions. 117 if (!this.#prefValueSet) { 118 this.#handleExceptionChange({ 119 data: { created: newExceptions }, 120 prefUpdate: true, 121 }); 122 // Serialize the exception entries to the string format and store in the 123 // pref set. 124 this.#prefValueSet = new Set(newExceptions.map(ex => ex.serialize())); 125 return; 126 } 127 128 // Otherwise, we need to check for changes in the pref. 129 130 // Find added items 131 let created = [...newExceptions].filter( 132 ex => !this.#prefValueSet.has(ex.serialize()) 133 ); 134 135 // Convert the new exceptions to the string format to check against the pref 136 // set. 137 let newExceptionStringSet = new Set( 138 newExceptions.map(ex => ex.serialize()) 139 ); 140 141 // Find removed items 142 let deleted = Array.from(this.#prefValueSet) 143 .filter(item => !newExceptionStringSet.has(item)) 144 .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex)); 145 146 // We shouldn't remove the exceptions in the remote settings list. 147 if (this.#rsValueSet) { 148 deleted = deleted.filter(ex => !this.#rsValueSet.has(ex.serialize())); 149 } 150 151 this.#prefValueSet = newExceptionStringSet; 152 153 // Calling handleExceptionChange to handle the changes. 154 this.#handleExceptionChange(created, deleted); 155 } 156 157 observe(subject, topic, data) { 158 if (topic != "nsPref:changed" || data != PREF_NAME) { 159 throw new Error(`Unexpected event ${topic} with ${data}`); 160 } 161 162 this.onPrefChange(); 163 } 164 165 async importAllExceptions() { 166 try { 167 let exceptions = await this.#rs.get(); 168 if (!exceptions.length) { 169 return; 170 } 171 this.onSync({ data: { created: exceptions } }); 172 } catch (error) { 173 console.error( 174 "Error while importing 3pcb exceptions from RemoteSettings", 175 error 176 ); 177 } 178 } 179 } 180 181 export class ThirdPartyCookieExceptionEntry { 182 classId = Components.ID("{8200e12c-416c-42eb-8af5-db9745d2e527}"); 183 QueryInterface = ChromeUtils.generateQI([ 184 "nsIThirdPartyCookieExceptionEntry", 185 ]); 186 187 constructor(fpSite, tpSite) { 188 this.firstPartySite = fpSite; 189 this.thirdPartySite = tpSite; 190 } 191 192 // Serialize the exception entry into a string. This is used for keying the 193 // exception in the pref and RemoteSettings set. 194 serialize() { 195 return `${this.firstPartySite},${this.thirdPartySite}`; 196 } 197 198 equals(other) { 199 return ( 200 this.firstPartySite === other.firstPartySite && 201 this.thirdPartySite === other.thirdPartySite 202 ); 203 } 204 205 static fromString(exStr) { 206 if (!exStr) { 207 return null; 208 } 209 210 let [fpSite, tpSite] = exStr.split(","); 211 try { 212 fpSite = this.#sanitizeSite(fpSite, true); 213 tpSite = this.#sanitizeSite(tpSite); 214 215 return new ThirdPartyCookieExceptionEntry(fpSite, tpSite); 216 } catch (e) { 217 console.error( 218 `Error while constructing 3pcd exception entry from string`, 219 exStr 220 ); 221 return null; 222 } 223 } 224 225 static fromRemoteSettingsRecord(record) { 226 try { 227 let fpSite = this.#sanitizeSite(record.fpSite, true); 228 let tpSite = this.#sanitizeSite(record.tpSite); 229 230 return new ThirdPartyCookieExceptionEntry(fpSite, tpSite); 231 } catch (e) { 232 console.error( 233 `Error while constructing 3pcd exception entry from RemoteSettings record`, 234 record 235 ); 236 return null; 237 } 238 } 239 240 // A helper function to sanitize the site using the eTLD service. 241 static #sanitizeSite(site, acceptWildcard = false) { 242 if (acceptWildcard && site === "*") { 243 return "*"; 244 } 245 246 let uri = Services.io.newURI(site); 247 return Services.eTLD.getSite(uri); 248 } 249 }