intervention_helpers.js (9835B)
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 /* globals browser, UAHelpers */ 8 9 const GOOGLE_TLDS = [ 10 "com", 11 "ac", 12 "ad", 13 "ae", 14 "com.af", 15 "com.ag", 16 "com.ai", 17 "al", 18 "am", 19 "co.ao", 20 "com.ar", 21 "as", 22 "at", 23 "com.au", 24 "az", 25 "ba", 26 "com.bd", 27 "be", 28 "bf", 29 "bg", 30 "com.bh", 31 "bi", 32 "bj", 33 "com.bn", 34 "com.bo", 35 "com.br", 36 "bs", 37 "bt", 38 "co.bw", 39 "by", 40 "com.bz", 41 "ca", 42 "com.kh", 43 "cc", 44 "cd", 45 "cf", 46 "cat", 47 "cg", 48 "ch", 49 "ci", 50 "co.ck", 51 "cl", 52 "cm", 53 "cn", 54 "com.co", 55 "co.cr", 56 "com.cu", 57 "cv", 58 "com.cy", 59 "cz", 60 "de", 61 "dj", 62 "dk", 63 "dm", 64 "com.do", 65 "dz", 66 "com.ec", 67 "ee", 68 "com.eg", 69 "es", 70 "com.et", 71 "fi", 72 "com.fj", 73 "fm", 74 "fr", 75 "ga", 76 "ge", 77 "gf", 78 "gg", 79 "com.gh", 80 "com.gi", 81 "gl", 82 "gm", 83 "gp", 84 "gr", 85 "com.gt", 86 "gy", 87 "com.hk", 88 "hn", 89 "hr", 90 "ht", 91 "hu", 92 "co.id", 93 "iq", 94 "ie", 95 "co.il", 96 "im", 97 "co.in", 98 "io", 99 "is", 100 "it", 101 "je", 102 "com.jm", 103 "jo", 104 "co.jp", 105 "co.ke", 106 "ki", 107 "kg", 108 "co.kr", 109 "com.kw", 110 "kz", 111 "la", 112 "com.lb", 113 "com.lc", 114 "li", 115 "lk", 116 "co.ls", 117 "lt", 118 "lu", 119 "lv", 120 "com.ly", 121 "co.ma", 122 "md", 123 "me", 124 "mg", 125 "mk", 126 "ml", 127 "com.mm", 128 "mn", 129 "ms", 130 "com.mt", 131 "mu", 132 "mv", 133 "mw", 134 "com.mx", 135 "com.my", 136 "co.mz", 137 "com.na", 138 "ne", 139 "com.nf", 140 "com.ng", 141 "com.ni", 142 "nl", 143 "no", 144 "com.np", 145 "nr", 146 "nu", 147 "co.nz", 148 "com.om", 149 "com.pk", 150 "com.pa", 151 "com.pe", 152 "com.ph", 153 "pl", 154 "com.pg", 155 "pn", 156 "com.pr", 157 "ps", 158 "pt", 159 "com.py", 160 "com.qa", 161 "ro", 162 "rs", 163 "ru", 164 "rw", 165 "com.sa", 166 "com.sb", 167 "sc", 168 "se", 169 "com.sg", 170 "sh", 171 "si", 172 "sk", 173 "com.sl", 174 "sn", 175 "sm", 176 "so", 177 "st", 178 "sr", 179 "com.sv", 180 "td", 181 "tg", 182 "co.th", 183 "com.tj", 184 "tk", 185 "tl", 186 "tm", 187 "to", 188 "tn", 189 "com.tr", 190 "tt", 191 "com.tw", 192 "co.tz", 193 "com.ua", 194 "co.ug", 195 "co.uk", 196 "com", 197 "com.uy", 198 "co.uz", 199 "com.vc", 200 "co.ve", 201 "vg", 202 "co.vi", 203 "com.vn", 204 "vu", 205 "ws", 206 "co.za", 207 "co.zm", 208 "co.zw", 209 ]; 210 211 var InterventionHelpers = { 212 skip_if_functions: { 213 getWeekInfo_defined: () => { 214 return !!Intl?.Locale?.prototype?.getWeekInfo; 215 }, 216 InstallTrigger_defined: () => { 217 return "InstallTrigger" in window; 218 }, 219 InstallTrigger_undefined: () => { 220 return !("InstallTrigger" in window); 221 }, 222 text_event_supported: () => { 223 return !!window.TextEvent; 224 }, 225 }, 226 227 ua_change_functions: { 228 add_Chrome: (ua, config) => { 229 return UAHelpers.addChrome(ua, config.version); 230 }, 231 add_Firefox_as_Gecko: (ua, config) => { 232 return UAHelpers.addGecko(ua, config.version); 233 }, 234 add_Samsung_for_Samsung_devices: ua => { 235 return UAHelpers.addSamsungForSamsungDevices(ua); 236 }, 237 add_Version_segment: ua => { 238 return `${ua} Version/0`; 239 }, 240 cap_Version_to_99: ua => { 241 return UAHelpers.capVersionTo99(ua); 242 }, 243 change_Firefox_to_FireFox: ua => { 244 return UAHelpers.changeFirefoxToFireFox(ua); 245 }, 246 change_Gecko_to_like_Gecko: ua => { 247 return ua.replace("Gecko", "like Gecko"); 248 }, 249 change_OS_to_MacOSX: (ua, config) => { 250 return UAHelpers.getMacOSXUA(ua, config.arch, config.version); 251 }, 252 change_OS_to_Windows: ua => { 253 return UAHelpers.windows(ua); 254 }, 255 Chrome: (ua, config) => { 256 config.ua = ua; 257 config.noFxQuantum = true; 258 return UAHelpers.getDeviceAppropriateChromeUA(config); 259 }, 260 Chrome_with_FxQuantum: (ua, config) => { 261 config.ua = ua; 262 return UAHelpers.getDeviceAppropriateChromeUA(config); 263 }, 264 desktop_not_mobile: () => { 265 return UAHelpers.desktopUA(); 266 }, 267 mimic_Android_Hotspot2_device: ua => { 268 return UAHelpers.androidHotspot2Device(ua); 269 }, 270 replace_colon_in_rv_with_space: ua => { 271 return ua.replace("rv:", "rv "); 272 }, 273 reduce_firefox_version_by_one: ua => { 274 const [head, fx, tail] = ua.split(/(firefox\/)/i); 275 if (!fx || !tail) { 276 return ua; 277 } 278 const major = parseInt(tail); 279 if (!major) { 280 return ua; 281 } 282 return `${head}${fx}${major - 1}${tail.slice(major.toString().length)}`; 283 }, 284 add_Safari: (ua, config) => { 285 config.withFirefox = true; 286 return UAHelpers.safari(config); 287 }, 288 Safari: (ua, config) => { 289 return UAHelpers.safari(config); 290 }, 291 Safari_with_FxQuantum: (ua, config) => { 292 config.withFxQuantum = true; 293 return UAHelpers.safari(config); 294 }, 295 }, 296 297 valid_platforms: [ 298 "all", 299 "android", 300 "desktop", 301 "fenix", 302 "linux", 303 "mac", 304 "windows", 305 ], 306 valid_channels: ["beta", "esr", "nightly", "stable"], 307 308 shouldSkip(intervention, firefoxVersion, firefoxChannel) { 309 const { 310 bug, 311 max_version, 312 min_version, 313 not_channels, 314 only_channels, 315 skip_if, 316 ua_string, 317 } = intervention; 318 if (firefoxChannel) { 319 if (only_channels && !only_channels.includes(firefoxChannel)) { 320 return true; 321 } 322 if (not_channels?.includes(firefoxChannel)) { 323 return true; 324 } 325 } 326 if (min_version && firefoxVersion < min_version) { 327 return true; 328 } 329 if (max_version) { 330 // Make sure to handle the case where only the major version matters, 331 // for instance if we want 138 and the version number is 138.1. 332 if (String(max_version).includes(".")) { 333 if (firefoxVersion > max_version) { 334 return true; 335 } 336 } else if (Math.floor(firefoxVersion) > max_version) { 337 return true; 338 } 339 } 340 if (ua_string) { 341 for (let ua of Array.isArray(ua_string) ? ua_string : [ua_string]) { 342 if (!InterventionHelpers.ua_change_functions[ua.change ?? ua]) { 343 return true; 344 } 345 } 346 } 347 if (skip_if) { 348 try { 349 if ( 350 !this.skip_if_functions[skip_if] || 351 this.skip_if_functions[skip_if]?.() 352 ) { 353 return true; 354 } 355 } catch (e) { 356 console.trace( 357 `Error while checking skip-if condition ${skip_if} for bug ${bug}:`, 358 e 359 ); 360 return true; 361 } 362 } 363 return false; 364 }, 365 366 nonCustomInterventionKeys: Object.freeze( 367 new Set([ 368 "content_scripts", 369 "enabled", 370 "max_version", 371 "min_version", 372 "not_platforms", 373 "platforms", 374 "not_channels", 375 "only_channels", 376 "pref_check", 377 "skip_if", 378 "ua_string", 379 ]) 380 ), 381 382 isMissingCustomFunctions(intervention, customFunctionNames) { 383 for (let key of Object.keys(intervention)) { 384 if ( 385 !InterventionHelpers.nonCustomInterventionKeys.has(key) && 386 !customFunctionNames.has(key) 387 ) { 388 return true; 389 } 390 } 391 return false; 392 }, 393 394 async getOS() { 395 const os = 396 browser.aboutConfigPrefs.getPref("platform_override") ?? 397 (await browser.runtime.getPlatformInfo()).os; 398 if (os === "win") { 399 return "windows"; 400 } 401 return os; 402 }, 403 404 async getPlatformMatches() { 405 if (!InterventionHelpers._platformMatches) { 406 const os = await this.getOS(); 407 InterventionHelpers._platformMatches = [ 408 "all", 409 os, 410 os == "android" ? "android" : "desktop", 411 ]; 412 if (os == "android") { 413 const packageName = await browser.appConstants.getAndroidPackageName(); 414 if (packageName.includes("fenix") || packageName.includes("firefox")) { 415 InterventionHelpers._platformMatches.push("fenix"); 416 } 417 } 418 } 419 return InterventionHelpers._platformMatches; 420 }, 421 422 async checkPlatformMatches(intervention) { 423 let desired = intervention.platforms; 424 let undesired = intervention.not_platforms; 425 if (!desired && !undesired) { 426 return true; 427 } 428 429 const actual = await InterventionHelpers.getPlatformMatches(); 430 if (undesired) { 431 if (!Array.isArray(undesired)) { 432 undesired = [undesired]; 433 } 434 if ( 435 undesired.includes("all") || 436 actual.filter(x => undesired.includes(x)).length 437 ) { 438 return false; 439 } 440 } 441 442 if (!desired) { 443 return true; 444 } 445 if (!Array.isArray(desired)) { 446 desired = [desired]; 447 } 448 return ( 449 desired.includes("all") || 450 !!actual.filter(x => desired.includes(x)).length 451 ); 452 }, 453 454 applyUAChanges(ua, changes) { 455 if (!Array.isArray(changes)) { 456 changes = [changes]; 457 } 458 for (let config of changes) { 459 if (typeof config === "string") { 460 config = { change: config }; 461 } 462 let finalChanges = config.change; 463 if (!Array.isArray(finalChanges)) { 464 finalChanges = [finalChanges]; 465 } 466 for (const change of finalChanges) { 467 try { 468 ua = InterventionHelpers.ua_change_functions[change](ua, config); 469 } catch (e) { 470 console.trace( 471 `Error while calling UA change function ${change} for bug ${config.bug}:`, 472 e 473 ); 474 return ua; 475 } 476 } 477 } 478 return ua; 479 }, 480 481 /** 482 * Useful helper to generate a list of domains with a fixed base domain and 483 * multiple country-TLDs or other cases with various TLDs. 484 * 485 * Example: 486 * matchPatternsForTLDs("*://mozilla.", "/*", ["com", "org"]) 487 * => ["*://mozilla.com/*", "*://mozilla.org/*"] 488 */ 489 matchPatternsForTLDs(base, suffix, tlds) { 490 return tlds.map(tld => base + tld + suffix); 491 }, 492 493 /** 494 * A modified version of matchPatternsForTLDs that always returns the match 495 * list for all known Google country TLDs. 496 */ 497 matchPatternsForGoogle(base, suffix = "/*") { 498 return InterventionHelpers.matchPatternsForTLDs(base, suffix, GOOGLE_TLDS); 499 }, 500 };