}); this.#rsValueSet ??= new Set(); // Remove items in sitesToRemove for (const site of deleted) { this.#rsValueSet.delete(site.serialize()); } // Add items from sitesToAdd for (const site of created) { this.#rsValueSet.add(site.serialize()); } this.#handleExceptionChange(created, deleted); } onPrefChange() { let newExceptions = Services.prefs.getStringPref(PREF_NAME, "").split(";"); // Convert the exception strings to exception entries. newExceptions = newExceptions .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex)) .filter(Boolean); // If this is the first time we're initializing from pref, we can directly // call handleExceptionChange to create the exceptions. if (!this.#prefValueSet) { this.#handleExceptionChange({ data: { created: newExceptions }, prefUpdate: true, }); // Serialize the exception entries to the string format and store in the // pref set. this.#prefValueSet = new Set(newExceptions.map(ex => ex.serialize())); return; } // Otherwise, we need to check for changes in the pref. // Find added items let created = [...newExceptions].filter( ex => !this.#prefValueSet.has(ex.serialize()) ); // Convert the new exceptions to the string format to check against the pref // set. let newExceptionStringSet = new Set( newExceptions.map(ex => ex.serialize()) ); // Find removed items let deleted = Array.from(this.#prefValueSet) .filter(item => !newExceptionStringSet.has(item)) .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex)); // We shouldn't remove the exceptions in the remote settings list. if (this.#rsValueSet) { deleted = deleted.filter(ex => !this.#rsValueSet.has(ex.serialize())); } this.#prefValueSet = newExceptionStringSet; // Calling handleExceptionChange to handle the changes. this.#handleExceptionChange(created, deleted); } observe(subject, topic, data) { if (topic != "nsPref:changed" || data != PREF_NAME) { throw new Error(`Unexpected event ${topic} with ${data}`); } this.onPrefChange(); } async importAllExceptions() { try { let exceptions = await this.#rs.get(); if (!exceptions.length) { return; } this.onSync({ data: { created: exceptions } }); } catch (error) { console.error( "Error while importing 3pcb exceptions from RemoteSettings", error ); } } } export class ThirdPartyCookieExceptionEntry { classId = Components.ID("{8200e12c-416c-42eb-8af5-db9745d2e527}"); QueryInterface = ChromeUtils.generateQI([ "nsIThirdPartyCookieExceptionEntry", ]); constructor(fpSite, tpSite) { this.firstPartySite = fpSite; this.thirdPartySite = tpSite; } // Serialize the exception entry into a string. This is used for keying the // exception in the pref and RemoteSettings set. serialize() { return `${this.firstPartySite},${this.thirdPartySite}`; } equals(other) { return ( this.firstPartySite === other.firstPartySite && this.thirdPartySite === other.thirdPartySite ); } static fromString(exStr) { if (!exStr) { return null; } let [fpSite, tpSite] = exStr.split(","); try { fpSite = this.#sanitizeSite(fpSite, true); tpSite = this.#sanitizeSite(tpSite); return new ThirdPartyCookieExceptionEntry(fpSite, tpSite); } catch (e) { console.error( `Error while constructing 3pcd exception entry from string`, exStr ); return null; } } static fromRemoteSettingsRecord(record) { try { let fpSite = this.#sanitizeSite(record.fpSite, true); let tpSite = this.#sanitizeSite(record.tpSite); return new ThirdPartyCookieExceptionEntry(fpSite, tpSite); } catch (e) { console.error( `Error while constructing 3pcd exception entry from RemoteSettings record`, record ); return null; } } // A helper function to sanitize the site using the eTLD service. static #sanitizeSite(site, acceptWildcard = false) { if (acceptWildcard && site === "*") { return "*"; } let uri = Services.io.newURI(site); return Services.eTLD.getSite(uri); } } PK