|
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