bg.js (11181B)
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 /* global browser, ConditionFactory */ 6 7 /** 8 * The main class for the IPP activator add-on. 9 */ 10 class IPPAddonActivator { 11 #initialized = false; 12 13 #tabBaseBreakages; 14 #webrequestBaseBreakages; 15 16 #tabBreakages; 17 #webrequestBreakages; 18 19 #pendingTabs = new Set(); // pending due to tab URL change while inactive 20 #pendingWebRequests = new Map(); // tabId -> Set of pending request URLs 21 #shownDomainByTab = new Map(); // tabId -> baseDomain of currently shown notification 22 23 constructor() { 24 this.tabUpdated = this.#tabUpdated.bind(this); 25 this.tabActivated = this.#tabActivated.bind(this); 26 this.tabRemoved = this.#tabRemoved.bind(this); 27 this.onRequest = this.#onRequest.bind(this); 28 29 browser.ippActivator.isTesting().then(async isTesting => { 30 await this.#loadAndRebuildBreakages(); 31 browser.ippActivator.onDynamicTabBreakagesUpdated.addListener(() => 32 this.#loadAndRebuildBreakages() 33 ); 34 browser.ippActivator.onDynamicWebRequestBreakagesUpdated.addListener(() => 35 this.#loadAndRebuildBreakages() 36 ); 37 38 if (isTesting) { 39 this.#init(); 40 return; 41 } 42 43 // Initialize only when IPP is active, keep in sync with activation. 44 if (await browser.ippActivator.isIPPActive()) { 45 this.#init(); 46 } 47 48 // IPP start event: initialize when service starts. 49 browser.ippActivator.onIPPActivated.addListener(async () => { 50 if (await browser.ippActivator.isIPPActive()) { 51 this.#init(); 52 } else { 53 this.#uninit(); 54 } 55 }); 56 }); 57 } 58 59 async #init() { 60 if (this.#initialized) { 61 return; 62 } 63 64 // Register only the listeners that are needed for existing breakages. 65 this.#registerListeners(); 66 67 this.#initialized = true; 68 } 69 70 async #uninit() { 71 if (!this.#initialized) { 72 return; 73 } 74 75 this.#unregisterListeners(); 76 77 // When IPP is deactivated, mark currently shown banners as consumed 78 const uniqueDomains = new Set(this.#shownDomainByTab.values()); 79 await Promise.allSettled( 80 Array.from(uniqueDomains).map(d => 81 browser.ippActivator.addNotifiedDomain(d) 82 ) 83 ); 84 85 const ids = Array.from(this.#shownDomainByTab.keys()); 86 await Promise.allSettled( 87 ids.map(id => browser.ippActivator.hideMessage(id)) 88 ); 89 90 this.#shownDomainByTab.clear(); 91 92 this.#initialized = false; 93 } 94 95 async #loadAndRebuildBreakages() { 96 if (!this.#tabBaseBreakages) { 97 try { 98 const url = browser.runtime.getURL("breakages/tab.json"); 99 const res = await fetch(url); 100 const base = await res.json(); 101 this.#tabBaseBreakages = Array.isArray(base) ? base : []; 102 } catch (e) { 103 this.#tabBaseBreakages = []; 104 } 105 } 106 107 if (!this.#webrequestBaseBreakages) { 108 try { 109 const url = browser.runtime.getURL("breakages/webrequest.json"); 110 const res = await fetch(url); 111 const base = await res.json(); 112 this.#webrequestBaseBreakages = Array.isArray(base) ? base : []; 113 } catch (e) { 114 this.#webrequestBaseBreakages = []; 115 } 116 } 117 118 let dynamicTab = []; 119 try { 120 const dynT = await browser.ippActivator.getDynamicTabBreakages(); 121 dynamicTab = Array.isArray(dynT) ? dynT : []; 122 } catch (_) { 123 console.warn("Unable to retrieve dynamicTabBreakages"); 124 } 125 126 let dynamicWr = []; 127 try { 128 const dynW = await browser.ippActivator.getDynamicWebRequestBreakages(); 129 dynamicWr = Array.isArray(dynW) ? dynW : []; 130 } catch (_) { 131 console.warn("Unable to retrieve dynamicWebRequestBreakages"); 132 } 133 134 this.#tabBreakages = [...(this.#tabBaseBreakages || []), ...dynamicTab]; 135 this.#webrequestBreakages = [ 136 ...(this.#webrequestBaseBreakages || []), 137 ...dynamicWr, 138 ]; 139 140 // Adjust listeners if we've already initialized. 141 if (this.#initialized) { 142 this.#registerListeners(); 143 } 144 } 145 146 #registerListeners() { 147 this.#unregisterListeners(); 148 149 const needTabUpdated = 150 Array.isArray(this.#tabBreakages) && !!this.#tabBreakages.length; 151 const needWebRequest = 152 Array.isArray(this.#webrequestBreakages) && 153 !!this.#webrequestBreakages.length; 154 const needActivation = needTabUpdated || needWebRequest; 155 156 // tabs.onUpdated (only if there are tab breakages) 157 if (needTabUpdated) { 158 browser.tabs.onUpdated.addListener(this.tabUpdated, { 159 properties: ["url", "status"], 160 }); 161 } 162 163 // webRequest.onBeforeRequest (only if there are webRequest breakages) 164 if (needWebRequest) { 165 browser.webRequest.onBeforeRequest.addListener( 166 this.onRequest, 167 { 168 urls: ["<all_urls>"], 169 types: ["media", "sub_frame", "xmlhttprequest"], 170 }, 171 [] 172 ); 173 } 174 175 // tabs.onActivated and tabs.onRemoved are needed when either above is needed 176 if (needActivation) { 177 browser.tabs.onActivated.addListener(this.tabActivated); 178 browser.tabs.onRemoved.addListener(this.tabRemoved); 179 } 180 } 181 182 #unregisterListeners() { 183 if (browser.tabs.onUpdated.hasListener(this.tabUpdated)) { 184 browser.tabs.onUpdated.removeListener(this.tabUpdated); 185 } 186 187 if (browser.tabs.onActivated.hasListener(this.tabActivated)) { 188 browser.tabs.onActivated.removeListener(this.tabActivated); 189 } 190 191 if (browser.tabs.onRemoved.hasListener(this.tabRemoved)) { 192 browser.tabs.onRemoved.removeListener(this.tabRemoved); 193 } 194 195 if (browser.webRequest.onBeforeRequest.hasListener(this.onRequest)) { 196 browser.webRequest.onBeforeRequest.removeListener(this.onRequest); 197 } 198 199 this.#pendingTabs.clear(); 200 this.#pendingWebRequests.clear(); 201 } 202 203 async #tabUpdated(tabId, changeInfo, tab) { 204 // React only to URL changes and to load completion; avoid showing during 'loading' 205 if (!("url" in changeInfo) && changeInfo.status !== "complete") { 206 return; 207 } 208 209 // If the tab URL changed, reset any pending web requests for this tab 210 if ("url" in changeInfo) { 211 try { 212 // If we had a notification for a different base domain, hide it 213 const info = await browser.ippActivator.getBaseDomainFromURL( 214 changeInfo.url || tab?.url || "" 215 ); 216 const shownBase = this.#shownDomainByTab.get(tabId); 217 if ( 218 shownBase && 219 shownBase !== info.baseDomain && 220 shownBase !== info.host 221 ) { 222 await browser.ippActivator.hideMessage(tabId); 223 this.#shownDomainByTab.delete(tabId); 224 } 225 } catch (_) { 226 // ignore lookup issues 227 } 228 this.#pendingWebRequests.delete(tabId); 229 } 230 231 // If we haven't reached load completion yet, wait for later events 232 if (changeInfo.status && changeInfo.status !== "complete") { 233 if (!tab.active) { 234 this.#pendingTabs.add(tabId); 235 } 236 return; 237 } 238 239 // At this point, either the URL changed and load already completed, or 240 // we received the 'complete' status: handle only if tab is active 241 if (!tab.active) { 242 this.#pendingTabs.add(tabId); 243 return; 244 } 245 246 await this.#maybeNotify(tab, this.#tabBreakages, tab.url); 247 } 248 249 async #tabActivated(activeInfo) { 250 const { tabId } = activeInfo || {}; 251 252 const hadTabPending = this.#pendingTabs.has(tabId); 253 const wrSet = this.#pendingWebRequests.get(tabId); 254 const pendingWrUrls = wrSet ? Array.from(wrSet) : []; 255 if (!hadTabPending && pendingWrUrls.length === 0) { 256 return; 257 } 258 259 this.#pendingTabs.delete(tabId); 260 this.#pendingWebRequests.delete(tabId); 261 262 let tab; 263 try { 264 tab = await browser.tabs.get(tabId); 265 if (!tab || !tab.active) { 266 return; 267 } 268 } catch (_) { 269 // Tab might have been closed; ignore. 270 return; 271 } 272 273 if ( 274 hadTabPending && 275 (await this.#maybeNotify(tab, this.#tabBreakages, tab.url)) 276 ) { 277 return; 278 } 279 280 for (const url of pendingWrUrls) { 281 if (await this.#maybeNotify(tab, this.#webrequestBreakages, url)) { 282 return; 283 } 284 } 285 } 286 287 async #maybeNotify(tab, breakages, url) { 288 const info = await browser.ippActivator.getBaseDomainFromURL(url); 289 if (!info.baseDomain && !info.host) { 290 return false; 291 } 292 293 if (await browser.ippActivator.hasExclusion(url)) { 294 return false; 295 } 296 297 let domain = info.baseDomain; 298 let breakage = breakages.find( 299 b => Array.isArray(b.domains) && b.domains.includes(info.baseDomain) 300 ); 301 if (!breakage) { 302 breakage = breakages.find( 303 b => Array.isArray(b.domains) && b.domains.includes(info.host) 304 ); 305 if (!breakage) { 306 return false; 307 } 308 309 domain = info.host; 310 } 311 312 // Do not show the same notification again for the same base domain. 313 const shown = await browser.ippActivator.getNotifiedDomains(); 314 if (Array.isArray(shown) && shown.includes(domain)) { 315 return false; 316 } 317 318 if ( 319 !(await ConditionFactory.run(breakage.condition, { tabId: tab.id, url })) 320 ) { 321 return false; 322 } 323 324 // Track which base domain this tab is showing a notification for 325 this.#shownDomainByTab.set(tab.id, domain); 326 327 // This function returns when the notification is dismissed. We don't want 328 // to wait for that to happen. 329 browser.ippActivator 330 .showMessage(breakage.message, tab.id) 331 .then(async dismissed => { 332 if (!dismissed) { 333 return; 334 } 335 336 await browser.ippActivator.addNotifiedDomain(domain); 337 338 // Close all notifications currently shown for the same base domain 339 // across all tabs and clean up tracking state. 340 const toClose = []; 341 for (const [tid, base] of this.#shownDomainByTab.entries()) { 342 if (base === domain) { 343 toClose.push(tid); 344 } 345 } 346 347 await Promise.allSettled( 348 toClose.map(id => browser.ippActivator.hideMessage(id)) 349 ); 350 351 for (const id of toClose) { 352 this.#shownDomainByTab.delete(id); 353 } 354 }); 355 356 return true; 357 } 358 359 async #onRequest(details) { 360 if ( 361 typeof details.tabId !== "number" || 362 details.tabId < 0 || 363 !details.url 364 ) { 365 return; 366 } 367 368 try { 369 const tab = await browser.tabs.get(details.tabId); 370 if (!tab) { 371 return; 372 } 373 374 if (tab.active) { 375 await this.#maybeNotify(tab, this.#webrequestBreakages, details.url); 376 } else { 377 const set = this.#pendingWebRequests.get(details.tabId) || new Set(); 378 set.add(details.url); 379 380 this.#pendingWebRequests.set(details.tabId, set); 381 } 382 } catch (_) { 383 // tab may not exist 384 } 385 } 386 387 async #tabRemoved(tabId, _removeInfo) { 388 // Clean up any pending state associated with the closed tab 389 this.#pendingTabs.delete(tabId); 390 this.#pendingWebRequests.delete(tabId); 391 this.#shownDomainByTab.delete(tabId); 392 } 393 } 394 395 /* This object is kept alive by listeners */ 396 new IPPAddonActivator();