uBlock/src/web_accessible_resources/google-ima.js
Dave Vandyke c1d8f5908d
More improvements to the google-ima shim script (#3908)
We have enabled the google-ima shim script again in the DuckDuckGo
Privacy Essentials browser extension, and found a couple more issues:
- Some websites set the enablePreloading[1] option, which should
  cause[2] the AdsManager.init() method to trigger the LOADED AdEvent
  to fire. If the event doesn't fire, those websites can get stuck
  waiting for the event forever.
- When AdsManager.start() method is called, a bunch of events are
  dispatched in order, to simulate ads loading, playing and
  finishing. There was a mistake in that logic though. The
  CONTENT_PAUSE_REQUESTED and CONTENT_RESUME_REQUESTED events[3]
  should fire as the ads start and finish respectively. By firing the
  latter early, and skipping the former, some websites got confused
  and tried to display ad overlays at the same time as playing their
  content, or didn't display they content at all.

1 - https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsRenderingSettings#enablePreloading
2 - https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/preload#timing
3 - https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdEvent
2023-11-16 09:41:07 -05:00

855 lines
18 KiB
JavaScript

/**
*
* Source below is based on Mozilla source code:
* https://searchfox.org/mozilla-central/rev/d317e93d9a59c9e4c06ada85fbff9f6a1ceaaad1/browser/extensions/webcompat/shims/google-ima.js
*
* Modifications to the original code below this comment:
* - Avoid JS syntax not supported by older browser versions
* - Add missing shim event
* - Modified to avoid jshint warnings as per uBO's config
* - Added `OmidVerificationVendor` to `ima`
* - Have `AdError.getInnerError()` return `null`
* - Have `AdDisplayContainer` constructor add DIV element to container
* - Added missing event dispatcher functionality
* - Corrected return type of `Ad.getUniversalAdIds()`
* - Corrected typo in `UniversalAdIdInfo.getAdIdValue()` method name
* - Corrected dispatch of LOAD event when preloading is enabled
* - Corrected dispatch of CONTENT_PAUSE/RESUME_REQUESTED events
*
* Related issue:
* - https://github.com/uBlockOrigin/uBlock-issues/issues/2158
*
**/
'use strict';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Bug 1713690 - Shim Google Interactive Media Ads ima3.js
*
* Many sites use ima3.js for ad bidding and placement, often in conjunction
* with Google Publisher Tags, Prebid.js and/or other scripts. This shim
* provides a stubbed-out version of the API which helps work around related
* site breakage, such as black bxoes where videos ought to be placed.
*/
if (!window.google || !window.google.ima || !window.google.ima.VERSION) {
const VERSION = "3.517.2";
const CheckCanAutoplay = (function() {
// Sourced from: https://searchfox.org/mozilla-central/source/dom/media/gtest/negative_duration.mp4
const TEST_VIDEO = new Blob(
[
new Uint32Array([
469762048,
1887007846,
1752392036,
0,
913273705,
1717987696,
828601953,
-1878917120,
1987014509,
1811939328,
1684567661,
0,
0,
0,
-402456576,
0,
256,
1,
0,
0,
256,
0,
0,
0,
256,
0,
0,
0,
64,
0,
0,
0,
0,
0,
0,
33554432,
-201261056,
1801548404,
1744830464,
1684564852,
251658241,
0,
0,
0,
0,
16777216,
0,
-1,
-1,
0,
0,
0,
0,
256,
0,
0,
0,
256,
0,
0,
0,
64,
5,
53250,
-2080309248,
1634296941,
738197504,
1684563053,
1,
0,
0,
0,
0,
-2137614336,
-1,
-1,
50261,
754974720,
1919706216,
0,
0,
1701079414,
0,
0,
0,
1701079382,
1851869295,
1919249508,
16777216,
1852402979,
102,
1752004116,
100,
1,
0,
0,
1852400676,
102,
1701995548,
102,
0,
1,
1819440396,
32,
1,
1651799011,
108,
1937011607,
100,
0,
1,
1668702599,
49,
0,
1,
0,
0,
0,
33555712,
4718800,
4718592,
0,
65536,
0,
0,
0,
0,
0,
0,
0,
0,
16776984,
1630601216,
21193590,
-14745500,
1729626337,
-1407254428,
89161945,
1049019,
9453056,
-251611125,
27269507,
-379058688,
-1329024392,
268435456,
1937011827,
0,
0,
268435456,
1668510835,
0,
0,
335544320,
2054386803,
0,
0,
0,
268435456,
1868788851,
0,
0,
671088640,
2019915373,
536870912,
2019914356,
0,
16777216,
16777216,
0,
0,
0,
]),
],
{ type: "video/mp4" }
);
let testVideo;
return function() {
if (!testVideo) {
testVideo = document.createElement("video");
testVideo.style =
"position:absolute; width:0; height:0; left:0; right:0; z-index:-1; border:0";
testVideo.setAttribute("muted", "muted");
testVideo.setAttribute("playsinline", "playsinline");
testVideo.src = URL.createObjectURL(TEST_VIDEO);
document.body.appendChild(testVideo);
}
return testVideo.play();
};
})();
const ima = {};
class AdDisplayContainer {
constructor(containerElement) {
const divElement = document.createElement("div");
divElement.style.setProperty("display", "none", "important");
divElement.style.setProperty("visibility", "collapse", "important");
containerElement.appendChild(divElement);
}
destroy() {}
initialize() {}
}
class ImaSdkSettings {
constructor() {
this.c = true;
this.f = {};
this.i = false;
this.l = "";
this.p = "";
this.r = 0;
this.t = "";
this.v = "";
}
getCompanionBackfill() {}
getDisableCustomPlaybackForIOS10Plus() {
return this.i;
}
getFeatureFlags() {
return this.f;
}
getLocale() {
return this.l;
}
getNumRedirects() {
return this.r;
}
getPlayerType() {
return this.t;
}
getPlayerVersion() {
return this.v;
}
getPpid() {
return this.p;
}
isCookiesEnabled() {
return this.c;
}
setAutoPlayAdBreaks() {}
setCompanionBackfill() {}
setCookiesEnabled(c) {
this.c = !!c;
}
setDisableCustomPlaybackForIOS10Plus(i) {
this.i = !!i;
}
setFeatureFlags(f) {
this.f = f;
}
setLocale(l) {
this.l = l;
}
setNumRedirects(r) {
this.r = r;
}
setPlayerType(t) {
this.t = t;
}
setPlayerVersion(v) {
this.v = v;
}
setPpid(p) {
this.p = p;
}
setSessionId(/*s*/) {}
setVpaidAllowed(/*a*/) {}
setVpaidMode(/*m*/) {}
// https://github.com/uBlockOrigin/uBlock-issues/issues/2265#issuecomment-1637094149
getDisableFlashAds() {
}
setDisableFlashAds() {
}
}
ImaSdkSettings.CompanionBackfillMode = {
ALWAYS: "always",
ON_MASTER_AD: "on_master_ad",
};
ImaSdkSettings.VpaidMode = {
DISABLED: 0,
ENABLED: 1,
INSECURE: 2,
};
class EventHandler {
constructor() {
this.listeners = new Map();
}
_dispatch(e) {
let listeners = this.listeners.get(e.type);
listeners = listeners ? Array.from(listeners.values()) : [];
for (const listener of listeners) {
try {
listener(e);
} catch (r) {
console.error(r);
}
}
}
addEventListener(types, c, options, context) {
if (!Array.isArray(types)) {
types = [types];
}
for (const t of types) {
if (!this.listeners.has(t)) {
this.listeners.set(t, new Map());
}
this.listeners.get(t).set(c, c.bind(context || this));
}
}
removeEventListener(types, c) {
if (!Array.isArray(types)) {
types = [types];
}
for (const t of types) {
const typeSet = this.listeners.get(t);
if (typeSet) {
typeSet.delete(c);
}
}
}
}
class AdsLoader extends EventHandler {
constructor() {
super();
this.settings = new ImaSdkSettings();
}
contentComplete() {}
destroy() {}
getSettings() {
return this.settings;
}
getVersion() {
return VERSION;
}
requestAds(/*r, c*/) {
// If autoplay is disabled and the page is trying to autoplay a tracking
// ad, then IMA fails with an error, and the page is expected to request
// ads again later when the user clicks to play.
CheckCanAutoplay().then(
() => {
const { ADS_MANAGER_LOADED } = AdsManagerLoadedEvent.Type;
this._dispatch(new ima.AdsManagerLoadedEvent(ADS_MANAGER_LOADED));
},
() => {
const e = new ima.AdError(
"adPlayError",
1205,
1205,
"The browser prevented playback initiated without user interaction."
);
this._dispatch(new ima.AdErrorEvent(e));
}
);
}
}
class AdsManager extends EventHandler {
constructor() {
super();
this.volume = 1;
this._enablePreloading = false;
}
collapse() {}
configureAdsManager() {}
destroy() {}
discardAdBreak() {}
expand() {}
focus() {}
getAdSkippableState() {
return false;
}
getCuePoints() {
return [0];
}
getCurrentAd() {
return currentAd;
}
getCurrentAdCuePoints() {
return [];
}
getRemainingTime() {
return 0;
}
getVolume() {
return this.volume;
}
init(/*w, h, m, e*/) {
if (this._enablePreloading) {
this._dispatch(new ima.AdEvent(AdEvent.Type.LOADED));
}
}
isCustomClickTrackingUsed() {
return false;
}
isCustomPlaybackUsed() {
return false;
}
pause() {}
requestNextAdBreak() {}
resize(/*w, h, m*/) {}
resume() {}
setVolume(v) {
this.volume = v;
}
skip() {}
start() {
requestAnimationFrame(() => {
for (const type of [
AdEvent.Type.LOADED,
AdEvent.Type.STARTED,
AdEvent.Type.CONTENT_PAUSE_REQUESTED,
AdEvent.Type.AD_BUFFERING,
AdEvent.Type.FIRST_QUARTILE,
AdEvent.Type.MIDPOINT,
AdEvent.Type.THIRD_QUARTILE,
AdEvent.Type.COMPLETE,
AdEvent.Type.ALL_ADS_COMPLETED,
AdEvent.Type.CONTENT_RESUME_REQUESTED,
]) {
try {
this._dispatch(new ima.AdEvent(type));
} catch (e) {
console.error(e);
}
}
});
}
stop() {}
updateAdsRenderingSettings(/*s*/) {}
}
class AdsRenderingSettings {}
class AdsRequest {
setAdWillAutoPlay() {}
setAdWillPlayMuted() {}
setContinuousPlayback() {}
}
class AdPodInfo {
getAdPosition() {
return 1;
}
getIsBumper() {
return false;
}
getMaxDuration() {
return -1;
}
getPodIndex() {
return 1;
}
getTimeOffset() {
return 0;
}
getTotalAds() {
return 1;
}
}
class Ad {
constructor() {
this._pi = new AdPodInfo();
}
getAdId() {
return "";
}
getAdPodInfo() {
return this._pi;
}
getAdSystem() {
return "";
}
getAdvertiserName() {
return "";
}
getApiFramework() {
return null;
}
getCompanionAds() {
return [];
}
getContentType() {
return "";
}
getCreativeAdId() {
return "";
}
getCreativeId() {
return "";
}
getDealId() {
return "";
}
getDescription() {
return "";
}
getDuration() {
return 8.5;
}
getHeight() {
return 0;
}
getMediaUrl() {
return null;
}
getMinSuggestedDuration() {
return -2;
}
getSkipTimeOffset() {
return -1;
}
getSurveyUrl() {
return null;
}
getTitle() {
return "";
}
getTraffickingParameters() {
return {};
}
getTraffickingParametersString() {
return "";
}
getUiElements() {
return [""];
}
getUniversalAdIdRegistry() {
return "unknown";
}
getUniversalAdIds() {
return [new UniversalAdIdInfo()];
}
getUniversalAdIdValue() {
return "unknown";
}
getVastMediaBitrate() {
return 0;
}
getVastMediaHeight() {
return 0;
}
getVastMediaWidth() {
return 0;
}
getWidth() {
return 0;
}
getWrapperAdIds() {
return [""];
}
getWrapperAdSystems() {
return [""];
}
getWrapperCreativeIds() {
return [""];
}
isLinear() {
return true;
}
isSkippable() {
return true;
}
}
class CompanionAd {
getAdSlotId() {
return "";
}
getContent() {
return "";
}
getContentType() {
return "";
}
getHeight() {
return 1;
}
getWidth() {
return 1;
}
}
class AdError {
constructor(type, code, vast, message) {
this.errorCode = code;
this.message = message;
this.type = type;
this.vastErrorCode = vast;
}
getErrorCode() {
return this.errorCode;
}
getInnerError() {
return null;
}
getMessage() {
return this.message;
}
getType() {
return this.type;
}
getVastErrorCode() {
return this.vastErrorCode;
}
toString() {
return `AdError ${this.errorCode}: ${this.message}`;
}
}
AdError.ErrorCode = {};
AdError.Type = {};
const isEngadget = () => {
try {
for (const ctx of Object.values(window.vidible._getContexts())) {
const player = ctx.getPlayer();
if (!player) { continue;}
const div = player.div;
if (!div) { continue; }
if (div.innerHTML.includes("www.engadget.com")) {
return true;
}
}
} catch (_) {}
return false;
};
const currentAd = isEngadget() ? undefined : new Ad();
class AdEvent {
constructor(type) {
this.type = type;
}
getAd() {
return currentAd;
}
getAdData() {
return {};
}
}
AdEvent.Type = {
AD_BREAK_READY: "adBreakReady",
AD_BUFFERING: "adBuffering",
AD_CAN_PLAY: "adCanPlay",
AD_METADATA: "adMetadata",
AD_PROGRESS: "adProgress",
ALL_ADS_COMPLETED: "allAdsCompleted",
CLICK: "click",
COMPLETE: "complete",
CONTENT_PAUSE_REQUESTED: "contentPauseRequested",
CONTENT_RESUME_REQUESTED: "contentResumeRequested",
DURATION_CHANGE: "durationChange",
EXPANDED_CHANGED: "expandedChanged",
FIRST_QUARTILE: "firstQuartile",
IMPRESSION: "impression",
INTERACTION: "interaction",
LINEAR_CHANGE: "linearChange",
LINEAR_CHANGED: "linearChanged",
LOADED: "loaded",
LOG: "log",
MIDPOINT: "midpoint",
PAUSED: "pause",
RESUMED: "resume",
SKIPPABLE_STATE_CHANGED: "skippableStateChanged",
SKIPPED: "skip",
STARTED: "start",
THIRD_QUARTILE: "thirdQuartile",
USER_CLOSE: "userClose",
VIDEO_CLICKED: "videoClicked",
VIDEO_ICON_CLICKED: "videoIconClicked",
VIEWABLE_IMPRESSION: "viewable_impression",
VOLUME_CHANGED: "volumeChange",
VOLUME_MUTED: "mute",
};
class AdErrorEvent {
constructor(error) {
this.type = "adError";
this.error = error;
}
getError() {
return this.error;
}
getUserRequestContext() {
return {};
}
}
AdErrorEvent.Type = {
AD_ERROR: "adError",
};
const manager = new AdsManager();
class AdsManagerLoadedEvent {
constructor(type) {
this.type = type;
}
getAdsManager(c, settings) {
if (settings && settings.enablePreloading) {
manager._enablePreloading = true;
}
return manager;
}
getUserRequestContext() {
return {};
}
}
AdsManagerLoadedEvent.Type = {
ADS_MANAGER_LOADED: "adsManagerLoaded",
};
class CustomContentLoadedEvent {}
CustomContentLoadedEvent.Type = {
CUSTOM_CONTENT_LOADED: "deprecated-event",
};
class CompanionAdSelectionSettings {}
CompanionAdSelectionSettings.CreativeType = {
ALL: "All",
FLASH: "Flash",
IMAGE: "Image",
};
CompanionAdSelectionSettings.ResourceType = {
ALL: "All",
HTML: "Html",
IFRAME: "IFrame",
STATIC: "Static",
};
CompanionAdSelectionSettings.SizeCriteria = {
IGNORE: "IgnoreSize",
SELECT_EXACT_MATCH: "SelectExactMatch",
SELECT_NEAR_MATCH: "SelectNearMatch",
};
class AdCuePoints {
getCuePoints() {
return [];
}
}
class AdProgressData {}
class UniversalAdIdInfo {
getAdIdRegistry() {
return "";
}
getAdIdValue() {
return "";
}
}
Object.assign(ima, {
AdCuePoints,
AdDisplayContainer,
AdError,
AdErrorEvent,
AdEvent,
AdPodInfo,
AdProgressData,
AdsLoader,
AdsManager: manager,
AdsManagerLoadedEvent,
AdsRenderingSettings,
AdsRequest,
CompanionAd,
CompanionAdSelectionSettings,
CustomContentLoadedEvent,
gptProxyInstance: {},
ImaSdkSettings,
OmidAccessMode: {
DOMAIN: "domain",
FULL: "full",
LIMITED: "limited",
},
OmidVerificationVendor: {
1: "OTHER",
2: "GOOGLE",
GOOGLE: 2,
OTHER: 1
},
settings: new ImaSdkSettings(),
UiElements: {
AD_ATTRIBUTION: "adAttribution",
COUNTDOWN: "countdown",
},
UniversalAdIdInfo,
VERSION,
ViewMode: {
FULLSCREEN: "fullscreen",
NORMAL: "normal",
},
});
if (!window.google) {
window.google = {};
}
window.google.ima = ima;
}