trackingProtection.js (10871B)
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 "use strict"; 6 7 /* global ExtensionAPI, ExtensionCommon, ExtensionParent, Services, XPCOMUtils */ 8 9 // eslint-disable-next-line mozilla/reject-importGlobalProperties 10 XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "ChannelWrapper"]); 11 12 class AllowList { 13 constructor(id) { 14 this._id = id; 15 } 16 17 setShims(patterns, notHosts) { 18 this._shimPatterns = patterns; 19 this._shimMatcher = new MatchPatternSet(patterns || []); 20 this._shimNotHosts = notHosts || []; 21 return this; 22 } 23 24 setAllows(patterns, hosts) { 25 this._allowPatterns = patterns; 26 this._allowMatcher = new MatchPatternSet(patterns || []); 27 this._allowHosts = hosts || []; 28 return this; 29 } 30 31 shims(url, topHost) { 32 return ( 33 this._shimMatcher?.matches(url) && !this._shimNotHosts?.includes(topHost) 34 ); 35 } 36 37 allows(url, topHost) { 38 return ( 39 this._allowMatcher?.matches(url) && this._allowHosts?.includes(topHost) 40 ); 41 } 42 } 43 44 class Manager { 45 constructor() { 46 this._allowLists = new Map(); 47 this._PBModeAllowLists = new Map(); 48 } 49 50 _getAllowList(id, isPrivateMode) { 51 const activeAllowLists = isPrivateMode 52 ? this._PBModeAllowLists 53 : this._allowLists; 54 55 if (!activeAllowLists.has(id)) { 56 activeAllowLists.set(id, new AllowList(id)); 57 } 58 return activeAllowLists.get(id); 59 } 60 61 _ensureStarted() { 62 if (this._classifierObserver) { 63 return; 64 } 65 66 this._unblockedChannelIds = new Set(); 67 this._PBModeUnblockedChannelIds = new Set(); 68 this._channelClassifier = Cc[ 69 "@mozilla.org/url-classifier/channel-classifier-service;1" 70 ].getService(Ci.nsIChannelClassifierService); 71 this._classifierObserver = {}; 72 this._classifierObserver.observe = (subject, topic) => { 73 switch (topic) { 74 case "http-on-stop-request": { 75 const { channelId } = subject.QueryInterface(Ci.nsIIdentChannel); 76 const isPrivateMode = 77 subject.loadInfo.browsingContext?.originAttributes 78 ?.privateBrowsingId; 79 if (isPrivateMode) { 80 this._PBModeUnblockedChannelIds.delete(channelId); 81 } else { 82 this._unblockedChannelIds.delete(channelId); 83 } 84 break; 85 } 86 case "urlclassifier-before-block-channel": { 87 const channel = subject.QueryInterface( 88 Ci.nsIUrlClassifierBlockedChannel 89 ); 90 const isPrivateMode = subject.isPrivateBrowsing; 91 const { channelId, url } = channel; 92 let topHost; 93 try { 94 topHost = new URL(channel.topLevelUrl).hostname; 95 } catch (_) { 96 return; 97 } 98 const activeAllowLists = isPrivateMode 99 ? this._PBModeAllowLists 100 : this._allowLists; 101 const activeUnblockedChannelIds = isPrivateMode 102 ? this._PBModeUnblockedChannelIds 103 : this._unblockedChannelIds; 104 // If anti-tracking webcompat is disabled, we only permit replacing 105 // channels, not fully unblocking them. 106 if (Manager.ENABLE_WEBCOMPAT) { 107 // if any allowlist unblocks the request entirely, we allow it 108 for (const allowList of activeAllowLists.values()) { 109 if (allowList.allows(url, topHost)) { 110 activeUnblockedChannelIds.add(channelId); 111 channel.allow(); 112 return; 113 } 114 } 115 } 116 // otherwise, if any allowlist shims the request we say it's replaced 117 for (const allowList of activeAllowLists.values()) { 118 if (allowList.shims(url, topHost)) { 119 activeUnblockedChannelIds.add(channelId); 120 channel.replace(); 121 return; 122 } 123 } 124 break; 125 } 126 } 127 }; 128 Services.obs.addObserver(this._classifierObserver, "http-on-stop-request"); 129 this._channelClassifier.addListener(this._classifierObserver); 130 } 131 132 stop() { 133 if (!this._classifierObserver) { 134 return; 135 } 136 137 Services.obs.removeObserver( 138 this._classifierObserver, 139 "http-on-stop-request" 140 ); 141 this._channelClassifier.removeListener(this._classifierObserver); 142 delete this._channelClassifier; 143 delete this._classifierObserver; 144 } 145 146 wasChannelIdUnblocked(channelId, isPrivateMode) { 147 const activeUnblockedChannelIds = isPrivateMode 148 ? this._PBModeUnblockedChannelIds 149 : this._unblockedChannelIds; 150 return activeUnblockedChannelIds?.has(channelId); 151 } 152 153 allow(allowListId, patterns, isPrivateMode, hosts) { 154 this._ensureStarted(); 155 this._getAllowList(allowListId, isPrivateMode).setAllows(patterns, hosts); 156 } 157 158 shim(allowListId, patterns, isPrivateMode, notHosts) { 159 this._ensureStarted(); 160 this._getAllowList(allowListId, isPrivateMode).setShims(patterns, notHosts); 161 } 162 163 revoke(allowListId) { 164 this._allowLists.delete(allowListId); 165 this._PBModeAllowLists.delete(allowListId); 166 } 167 } 168 var manager = new Manager(); 169 170 function getChannelId(context, requestId) { 171 const wrapper = ChannelWrapper.getRegisteredChannel( 172 requestId, 173 context.extension.policy, 174 context.xulBrowser.frameLoader.remoteTab 175 ); 176 return wrapper?.channel?.QueryInterface(Ci.nsIIdentChannel)?.channelId; 177 } 178 179 var dFPIPrefName = "network.cookie.cookieBehavior"; 180 var dFPIPbPrefName = "network.cookie.cookieBehavior.pbmode"; 181 var dFPIStatus; 182 function updateDFPIStatus() { 183 dFPIStatus = { 184 nonPbMode: 5 == Services.prefs.getIntPref(dFPIPrefName), 185 pbMode: 5 == Services.prefs.getIntPref(dFPIPbPrefName), 186 }; 187 } 188 189 this.trackingProtection = class extends ExtensionAPI { 190 onShutdown() { 191 if (manager) { 192 manager.stop(); 193 } 194 Services.prefs.removeObserver(dFPIPrefName, updateDFPIStatus); 195 Services.prefs.removeObserver(dFPIPbPrefName, updateDFPIStatus); 196 } 197 198 getAPI(context) { 199 const { 200 extension: { tabManager }, 201 } = this; 202 const EventManager = ExtensionCommon.EventManager; 203 Services.prefs.addObserver(dFPIPrefName, updateDFPIStatus); 204 Services.prefs.addObserver(dFPIPbPrefName, updateDFPIStatus); 205 updateDFPIStatus(); 206 207 return { 208 trackingProtection: { 209 onSmartBlockEmbedUnblock: new EventManager({ 210 context, 211 name: "trackingProtection.onSmartBlockEmbedUnblock", 212 register: fire => { 213 const callback = (subject, topic, data) => { 214 // chrome tab id needs to be converted to extension tab id 215 let hostname = subject.linkedBrowser.currentURI.host; 216 let tabId = tabManager.convert(subject).id; 217 fire.sync(tabId, data, hostname); 218 }; 219 Services.obs.addObserver(callback, "smartblock:unblock-embed"); 220 return () => { 221 Services.obs.removeObserver(callback, "smartblock:unblock-embed"); 222 }; 223 }, 224 }).api(), 225 onSmartBlockEmbedReblock: new EventManager({ 226 context, 227 name: "trackingProtection.onSmartBlockEmbedReblock", 228 register: fire => { 229 const callback = (subject, _topic, data) => { 230 // chrome tab id needs to be converted to extension tab id 231 let hostname = subject.linkedBrowser.currentURI.host; 232 let tabId = tabManager.convert(subject).id; 233 fire.sync(tabId, data, hostname); 234 }; 235 Services.obs.addObserver(callback, "smartblock:reblock-embed"); 236 return () => { 237 Services.obs.removeObserver(callback, "smartblock:reblock-embed"); 238 }; 239 }, 240 }).api(), 241 onPrivateSessionEnd: new EventManager({ 242 context, 243 name: "trackingProtection.onPrivateSessionEnd", 244 register: fire => { 245 const callback = (_subject, _topic) => { 246 fire.sync(); 247 }; 248 Services.obs.addObserver(callback, "last-pb-context-exited"); 249 return () => { 250 Services.obs.removeObserver(callback, "last-pb-context-exited"); 251 }; 252 }, 253 }).api(), 254 async shim(allowListId, patterns, notHosts) { 255 // shim for both PB and non-PB modes 256 manager.shim(allowListId, patterns, true, notHosts); 257 manager.shim(allowListId, patterns, false, notHosts); 258 }, 259 async allow(allowListId, patterns, isPrivate, hosts) { 260 manager.allow(allowListId, patterns, isPrivate, hosts); 261 }, 262 async revoke(allowListId) { 263 manager.revoke(allowListId); 264 }, 265 async clearResourceCache() { 266 ChromeUtils.clearResourceCache({ target: "content" }); 267 }, 268 async wasRequestUnblocked(requestId, isPrivate) { 269 if (!manager) { 270 return false; 271 } 272 const channelId = getChannelId(context, requestId); 273 if (!channelId) { 274 return false; 275 } 276 return manager.wasChannelIdUnblocked(channelId, isPrivate); 277 }, 278 async isDFPIActive(isPrivate) { 279 if (isPrivate) { 280 return dFPIStatus.pbMode; 281 } 282 return dFPIStatus.nonPbMode; 283 }, 284 openProtectionsPanel(tabId) { 285 let tab = tabManager.get(tabId); 286 if (!tab?.active) { 287 // break if tab is not the active tab 288 return; 289 } 290 291 let win = tab?.window; 292 Services.obs.notifyObservers( 293 win.gBrowser.selectedBrowser.browsingContext, 294 "smartblock:open-protections-panel" 295 ); 296 }, 297 incrementSmartblockEmbedShownTelemetry() { 298 Glean.securityUiProtectionspopup.smartblockembedsShown.add(); 299 }, 300 async getSmartBlockEmbedFluentString(tabId, shimId, websiteHost) { 301 let win = tabManager.get(tabId).window; 302 let document = win.document; 303 304 let { gProtectionsHandler } = win.gBrowser.ownerGlobal; 305 let { displayName } = gProtectionsHandler.smartblockEmbedInfo.find( 306 element => element.shimId == shimId 307 ); 308 309 let fluentArgs = [ 310 { 311 id: "smartblock-placeholder-title", 312 args: { 313 trackername: displayName, 314 }, 315 }, 316 { 317 id: "smartblock-placeholder-desc", 318 }, 319 { 320 id: "smartblock-placeholder-button-text", 321 args: { websitehost: websiteHost }, 322 }, 323 ]; 324 325 return document.l10n.formatValues(fluentArgs); 326 }, 327 }, 328 }; 329 } 330 }; 331 332 XPCOMUtils.defineLazyPreferenceGetter( 333 Manager, 334 "ENABLE_WEBCOMPAT", 335 "privacy.antitracking.enableWebcompat", 336 false 337 );