|
430 | 430 | /** |
431 | 431 | * @typedef {object} Platform |
432 | 432 | * @property {'ios' | 'macos' | 'extension' | 'android' | 'windows'} name |
433 | | - * @property {string} [version] |
| 433 | + * @property {string | number } [version] |
434 | 434 | */ |
435 | 435 |
|
436 | 436 | /** |
437 | 437 | * @typedef {object} UserPreferences |
438 | 438 | * @property {Platform} platform |
439 | 439 | * @property {boolean} [debug] |
440 | 440 | * @property {boolean} [globalPrivacyControl] |
| 441 | + * @property {number} [versionNumber] - Android version number only |
| 442 | + * @property {string} [versionString] - Non Android version string |
441 | 443 | * @property {string} sessionKey |
442 | 444 | */ |
443 | 445 |
|
444 | 446 | /** |
445 | | - * @param {{ features: Record<string, { state: string; settings: any; exceptions: string[] }>; unprotectedTemporary: string; }} data |
| 447 | + * Expansion point to add platform specific versioning logic |
| 448 | + * @param {UserPreferences} preferences |
| 449 | + * @returns {string | number | undefined} |
| 450 | + */ |
| 451 | + function getPlatformVersion (preferences) { |
| 452 | + if (preferences.versionNumber) { |
| 453 | + return preferences.versionNumber |
| 454 | + } |
| 455 | + if (preferences.versionString) { |
| 456 | + return preferences.versionString |
| 457 | + } |
| 458 | + return undefined |
| 459 | + } |
| 460 | + |
| 461 | + function parseVersionString (versionString) { |
| 462 | + const [major = 0, minor = 0, patch = 0] = versionString.split('.').map(Number); |
| 463 | + return { |
| 464 | + major, |
| 465 | + minor, |
| 466 | + patch |
| 467 | + } |
| 468 | + } |
| 469 | + |
| 470 | + /** |
| 471 | + * @param {string} minVersionString |
| 472 | + * @param {string} applicationVersionString |
| 473 | + * @returns {boolean} |
| 474 | + */ |
| 475 | + function satisfiesMinVersion (minVersionString, applicationVersionString) { |
| 476 | + const { major: minMajor, minor: minMinor, patch: minPatch } = parseVersionString(minVersionString); |
| 477 | + const { major, minor, patch } = parseVersionString(applicationVersionString); |
| 478 | + |
| 479 | + return (major > minMajor || |
| 480 | + (major >= minMajor && minor > minMinor) || |
| 481 | + (major >= minMajor && minor >= minMinor && patch >= minPatch)) |
| 482 | + } |
| 483 | + |
| 484 | + /** |
| 485 | + * @param {string | number | undefined} minSupportedVersion |
| 486 | + * @param {string | number | undefined} currentVersion |
| 487 | + * @returns {boolean} |
| 488 | + */ |
| 489 | + function isSupportedVersion (minSupportedVersion, currentVersion) { |
| 490 | + if (typeof currentVersion === 'string' && typeof minSupportedVersion === 'string') { |
| 491 | + if (satisfiesMinVersion(minSupportedVersion, currentVersion)) { |
| 492 | + return true |
| 493 | + } |
| 494 | + } else if (typeof currentVersion === 'number' && typeof minSupportedVersion === 'number') { |
| 495 | + if (minSupportedVersion <= currentVersion) { |
| 496 | + return true |
| 497 | + } |
| 498 | + } |
| 499 | + return false |
| 500 | + } |
| 501 | + |
| 502 | + /** |
| 503 | + * @param {{ features: Record<string, { state: string; settings: any; exceptions: string[], minSupportedVersion?: string|number }>; unprotectedTemporary: string[]; }} data |
446 | 504 | * @param {string[]} userList |
447 | 505 | * @param {UserPreferences} preferences |
448 | 506 | * @param {string[]} platformSpecificFeatures |
|
452 | 510 | const allowlisted = userList.filter(domain => domain === topLevelHostname).length > 0; |
453 | 511 | const remoteFeatureNames = Object.keys(data.features); |
454 | 512 | const platformSpecificFeaturesNotInRemoteConfig = platformSpecificFeatures.filter((featureName) => !remoteFeatureNames.includes(featureName)); |
| 513 | + /** @type {Record<string, any>} */ |
| 514 | + const output = { ...preferences }; |
| 515 | + if (output.platform) { |
| 516 | + const version = getPlatformVersion(preferences); |
| 517 | + if (version) { |
| 518 | + output.platform.version = version; |
| 519 | + } |
| 520 | + } |
455 | 521 | const enabledFeatures = remoteFeatureNames.filter((featureName) => { |
456 | 522 | const feature = data.features[featureName]; |
| 523 | + // Check that the platform supports minSupportedVersion checks and that the feature has a minSupportedVersion |
| 524 | + if (feature.minSupportedVersion && preferences.platform?.version) { |
| 525 | + if (!isSupportedVersion(feature.minSupportedVersion, preferences.platform.version)) { |
| 526 | + return false |
| 527 | + } |
| 528 | + } |
457 | 529 | return feature.state === 'enabled' && !isUnprotectedDomain(topLevelHostname, feature.exceptions) |
458 | 530 | }).concat(platformSpecificFeaturesNotInRemoteConfig); // only disable platform specific features if it's explicitly disabled in remote config |
459 | 531 | const isBroken = isUnprotectedDomain(topLevelHostname, data.unprotectedTemporary); |
460 | | - /** @type {Record<string, any>} */ |
461 | | - const output = { ...preferences }; |
462 | 532 | output.site = { |
463 | 533 | domain: topLevelHostname, |
464 | 534 | isBroken, |
|
518 | 588 | 'fingerprintingTemporaryStorage', |
519 | 589 | 'navigatorInterface', |
520 | 590 | 'clickToLoad', |
521 | | - 'elementHiding' |
| 591 | + 'elementHiding', |
| 592 | + 'exceptionHandler' |
522 | 593 | ]; |
523 | 594 |
|
524 | 595 | /** |
|
577 | 648 | case './features/click-to-play.js': return Promise.resolve().then(function () { return clickToPlay; }); |
578 | 649 | case './features/cookie.js': return Promise.resolve().then(function () { return cookie; }); |
579 | 650 | case './features/element-hiding.js': return Promise.resolve().then(function () { return elementHiding; }); |
| 651 | + case './features/exception-handler.js': return Promise.resolve().then(function () { return exceptionHandler; }); |
580 | 652 | case './features/fingerprinting-audio.js': return Promise.resolve().then(function () { return fingerprintingAudio; }); |
581 | 653 | case './features/fingerprinting-battery.js': return Promise.resolve().then(function () { return fingerprintingBattery; }); |
582 | 654 | case './features/fingerprinting-canvas.js': return Promise.resolve().then(function () { return fingerprintingCanvas; }); |
|
624 | 696 | /** |
625 | 697 | * @param {LoadArgs} args |
626 | 698 | */ |
627 | | - async function load (args) { |
| 699 | + function load (args) { |
628 | 700 | const mark = performanceMonitor.mark('load'); |
629 | 701 | if (!shouldRun()) { |
630 | 702 | return |
631 | 703 | } |
632 | 704 |
|
633 | 705 | for (const featureName of featureNames) { |
634 | 706 | const filename = featureName.replace(/([a-zA-Z])(?=[A-Z0-9])/g, '$1-').toLowerCase(); |
| 707 | + // eslint-disable-next-line promise/prefer-await-to-then |
635 | 708 | const feature = __variableDynamicImportRuntime0__(`./features/${filename}.js`).then((exported) => { |
636 | 709 | const ContentFeature = exported.default; |
637 | 710 | const featureInstance = new ContentFeature(featureName); |
|
2273 | 2346 | }) |
2274 | 2347 | } |
2275 | 2348 |
|
| 2349 | + // eslint-disable-next-line @typescript-eslint/no-empty-function |
2276 | 2350 | init (args) { |
2277 | 2351 | } |
2278 | 2352 |
|
|
2285 | 2359 | this.measure(); |
2286 | 2360 | } |
2287 | 2361 |
|
| 2362 | + // eslint-disable-next-line @typescript-eslint/no-empty-function |
2288 | 2363 | load (args) { |
2289 | 2364 | } |
2290 | 2365 |
|
|
2302 | 2377 | } |
2303 | 2378 | } |
2304 | 2379 |
|
| 2380 | + // eslint-disable-next-line @typescript-eslint/no-empty-function |
2305 | 2381 | update () { |
2306 | 2382 | } |
2307 | 2383 | } |
2308 | 2384 |
|
2309 | | - // @ts-nocheck |
| 2385 | + // TODO - Remove these comments to enable full linting. |
2310 | 2386 |
|
2311 | 2387 | let devMode$1 = false; |
2312 | 2388 | let isYoutubePreviewsEnabled$1 = false; |
|
2530 | 2606 | } |
2531 | 2607 |
|
2532 | 2608 | /* |
2533 | | - * Fades out the given element. Returns a promise that resolves when the fade is complete. |
2534 | | - * @param {Element} element - the element to fade in or out |
2535 | | - * @param {int} interval - frequency of opacity updates (ms) |
2536 | | - * @param {bool} fadeIn - true if the element should fade in instead of out |
2537 | | - */ |
| 2609 | + * Fades out the given element. Returns a promise that resolves when the fade is complete. |
| 2610 | + * @param {Element} element - the element to fade in or out |
| 2611 | + * @param {int} interval - frequency of opacity updates (ms) |
| 2612 | + * @param {boolean} fadeIn - true if the element should fade in instead of out |
| 2613 | + */ |
2538 | 2614 | fadeElement (element, interval, fadeIn) { |
2539 | | - return new Promise((resolve, reject) => { |
| 2615 | + return new Promise(resolve => { |
2540 | 2616 | let opacity = fadeIn ? 0 : 1; |
2541 | 2617 | const originStyle = element.style.cssText; |
2542 | 2618 | const fadeOut = setInterval(function () { |
|
2560 | 2636 |
|
2561 | 2637 | clickFunction (originalElement, replacementElement) { |
2562 | 2638 | let clicked = false; |
2563 | | - const handleClick = async function handleClick (e) { |
| 2639 | + const handleClick = function handleClick (e) { |
2564 | 2640 | // Ensure that the click is created by a user event & prevent double clicks from adding more animations |
2565 | 2641 | if (e.isTrusted && !clicked) { |
2566 | 2642 | this.isUnblocked = true; |
|
2633 | 2709 | parent.replaceChild(fbContainer, replacementElement); |
2634 | 2710 | fbContainer.appendChild(replacementElement); |
2635 | 2711 | fadeIn.appendChild(fbElement); |
2636 | | - fbElement.addEventListener('load', () => { |
2637 | | - this.fadeOutElement(replacementElement) |
2638 | | - .then(v => { |
2639 | | - fbContainer.replaceWith(fbElement); |
2640 | | - this.dispatchEvent(fbElement, 'ddg-ctp-placeholder-clicked'); |
2641 | | - this.fadeInElement(fadeIn).then(() => { |
2642 | | - fbElement.focus(); // focus on new element for screen readers |
2643 | | - }); |
2644 | | - }); |
| 2712 | + fbElement.addEventListener('load', async () => { |
| 2713 | + await this.fadeOutElement(replacementElement); |
| 2714 | + fbContainer.replaceWith(fbElement); |
| 2715 | + this.dispatchEvent(fbElement, 'ddg-ctp-placeholder-clicked'); |
| 2716 | + await this.fadeInElement(fadeIn); |
| 2717 | + // Focus on new element for screen readers. |
| 2718 | + fbElement.focus(); |
2645 | 2719 | }, { once: true }); |
2646 | 2720 | // Note: This event only fires on Firefox, on Chrome the frame's |
2647 | 2721 | // load event will always fire. |
|
2840 | 2914 | * @typedef unblockClickToLoadContentRequest |
2841 | 2915 | * @property {string} entity |
2842 | 2916 | * The entity to unblock requests for (e.g. "Facebook, Inc."). |
2843 | | - * @property {bool} [isLogin=false] |
| 2917 | + * @property {boolean} [isLogin=false] |
2844 | 2918 | * True if we should "allow social login", defaults to false. |
2845 | 2919 | * @property {string} action |
2846 | 2920 | * The Click to Load blocklist rule action (e.g. "block-ctl-fb") that should |
|
2853 | 2927 | * Send a message to the background to unblock requests for the given entity for |
2854 | 2928 | * the page. |
2855 | 2929 | * @param {unblockClickToLoadContentRequest} message |
2856 | | - * @see {@event ddg-ctp-unblockClickToLoadContent-complete} for the response handler. |
| 2930 | + * @see {@link ddg-ctp-unblockClickToLoadContent-complete} for the response handler. |
2857 | 2931 | */ |
2858 | 2932 | function unblockClickToLoadContent$1 (message) { |
2859 | 2933 | sendMessage('unblockClickToLoadContent', message); |
|
6185 | 6259 | } |
6186 | 6260 |
|
6187 | 6261 | getExpiry () { |
6188 | | - // @ts-ignore |
| 6262 | + // @ts-expect-error expires is not defined in the type definition |
6189 | 6263 | if (!this.maxAge && !this.expires) { |
6190 | 6264 | return NaN |
6191 | 6265 | } |
6192 | 6266 | const expiry = this.maxAge |
6193 | 6267 | ? parseInt(this.maxAge) |
6194 | | - // @ts-ignore |
| 6268 | + // @ts-expect-error expires is not defined in the type definition |
6195 | 6269 | : (new Date(this.expires) - new Date()) / 1000; |
6196 | 6270 | return expiry |
6197 | 6271 | } |
|
6745 | 6819 | default: ElementHiding |
6746 | 6820 | }); |
6747 | 6821 |
|
| 6822 | + class ExceptionHandler extends ContentFeature { |
| 6823 | + init () { |
| 6824 | + // Report to the debugger panel if an uncaught exception occurs |
| 6825 | + function handleUncaughtException (e) { |
| 6826 | + postDebugMessage('jsException', { |
| 6827 | + documentUrl: document.location.href, |
| 6828 | + message: e.message, |
| 6829 | + filename: e.filename, |
| 6830 | + lineno: e.lineno, |
| 6831 | + colno: e.colno, |
| 6832 | + stack: e.error.stack |
| 6833 | + }); |
| 6834 | + } |
| 6835 | + globalThis.addEventListener('error', handleUncaughtException); |
| 6836 | + } |
| 6837 | + } |
| 6838 | + |
| 6839 | + var exceptionHandler = /*#__PURE__*/Object.freeze({ |
| 6840 | + __proto__: null, |
| 6841 | + default: ExceptionHandler |
| 6842 | + }); |
| 6843 | + |
6748 | 6844 | // @ts-nocheck |
6749 | 6845 | const sjcl = (() => { |
6750 | 6846 | /*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */ |
|
8968 | 9064 | try { |
8969 | 9065 | defineProperty(globalThis, property, { |
8970 | 9066 | get: () => value, |
| 9067 | + // eslint-disable-next-line @typescript-eslint/no-empty-function |
8971 | 9068 | set: () => {}, |
8972 | 9069 | configurable: true |
8973 | 9070 | }); |
|
10279 | 10376 | return Promise.reject(new DOMException('Pan-tilt-zoom is not supported')) |
10280 | 10377 | } |
10281 | 10378 |
|
| 10379 | + // eslint-disable-next-line promise/prefer-await-to-then |
10282 | 10380 | return DDGReflect.apply(target, thisArg, args).then(function (stream) { |
10283 | 10381 | console.debug(`User stream ${stream.id} has been acquired`); |
10284 | 10382 | userMediaStreams.add(stream); |
|
0 commit comments