diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /browser/extensions/webcompat/shims | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/extensions/webcompat/shims')
57 files changed, 6021 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/shims/addthis-angular.js b/browser/extensions/webcompat/shims/addthis-angular.js new file mode 100644 index 0000000000..0f0cdd5029 --- /dev/null +++ b/browser/extensions/webcompat/shims/addthis-angular.js @@ -0,0 +1,16 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713694 - Shim AddThis Angular module + * + * Sites using Angular with AddThis can break entirely if the module is + * blocked. This shim mitigates that breakage by loading an empty module. + */ + +if (!window.addthisModule) { + window.addthisModule = window?.angular?.module("addthis", ["ng"]); +} diff --git a/browser/extensions/webcompat/shims/adform.js b/browser/extensions/webcompat/shims/adform.js new file mode 100644 index 0000000000..d6727d500e --- /dev/null +++ b/browser/extensions/webcompat/shims/adform.js @@ -0,0 +1,30 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713695 - Shim Adform tracking + * + * Sites such as m.tim.it may gate content behind AdForm's trackpoint, + * breaking download links and such if blocked. This shim stubs out the + * script and its related tracking pixel, so the content still works. + */ + +if (!window.Adform) { + window.Adform = { + Opt: { + disableRedirect() {}, + getStatus(clientID, callback) { + callback({ + clientID, + errorMessage: undefined, + optIn() {}, + optOut() {}, + status: "nocookie", + }); + }, + }, + }; +} diff --git a/browser/extensions/webcompat/shims/adnexus-ast.js b/browser/extensions/webcompat/shims/adnexus-ast.js new file mode 100644 index 0000000000..ae07fa6a03 --- /dev/null +++ b/browser/extensions/webcompat/shims/adnexus-ast.js @@ -0,0 +1,210 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1734130 - Shim AdNexus AST + * + * Some sites expect AST to successfully load, or they break. + * This shim mitigates that breakage. + */ + +if (!window.apntag?.loaded) { + const anq = window.apntag?.anq || []; + + const gTags = new Map(); + const gAds = new Map(); + const gEventHandlers = {}; + + const Ad = class { + adType = "banner"; + auctionId = "-"; + banner = { + width: 1, + height: 1, + content: "", + trackers: { + impression_urls: [], + video_events: {}, + }, + }; + brandCategoryId = 0; + buyerMemberId = 0; + cpm = 0.1; + cpm_publisher_currency = 0.1; + creativeId = 0; + dealId = undefined; + height = 1; + mediaSubtypeId = 1; + mediaTypeId = 1; + publisher_currency_code = "US"; + source = "-"; + tagId = -1; + targetId = ""; + width = 1; + + constructor(tagId, targetId) { + this.tagId = tagId; + this.targetId = targetId; + } + }; + + const fireAdEvent = (type, adObj) => { + const { targetId } = adObj; + const handlers = gEventHandlers[type]?.[targetId]; + if (!handlers) { + return Promise.resolve(); + } + const evt = { adObj, type }; + return new Promise(done => { + setTimeout(() => { + for (const cb of handlers) { + try { + cb(evt); + } catch (e) { + console.error(e); + } + } + done(); + }, 1); + }); + }; + + const refreshTag = targetId => { + const tag = gTags.get(targetId); + if (!tag) { + return; + } + if (!gAds.has(targetId)) { + gAds.set(targetId, new Ad(tag.tagId, targetId)); + } + const adObj = gAds.get(targetId); + fireAdEvent("adRequested", adObj).then(() => { + // TODO: do some sites expect adAvailable+adLoaded instead of adNoBid? + fireAdEvent("adNoBid", adObj); + }); + }; + + const off = (type, targetId, cb) => { + gEventHandlers[type]?.[targetId]?.delete(cb); + }; + + const on = (type, targetId, cb) => { + gEventHandlers[type] = gEventHandlers[type] || {}; + gEventHandlers[type][targetId] = + gEventHandlers[type][targetId] || new Set(); + gEventHandlers[type][targetId].add(cb); + }; + + const Tag = class { + static #nextId = 0; + debug = undefined; + displayed = false; + initialHeight = 1; + initialWidth = 1; + keywords = {}; + member = 0; + showTagCalled = false; + sizes = []; + targetId = ""; + utCalled = true; + utDivId = ""; + utiframeId = ""; + uuid = ""; + + constructor(raw) { + const { keywords, sizes, targetId } = raw; + this.tagId = Tag.#nextId++; + this.keywords = keywords || {}; + this.sizes = sizes || []; + this.targetId = targetId || ""; + } + modifyTag() {} + off(type, cb) { + off(type, this.targetId, cb); + } + on(type, cb) { + on(type, this.targetId, cb); + } + setKeywords(kw) { + this.keywords = kw; + } + }; + + window.apntag = { + anq, + attachClickTrackers() {}, + checkAdAvailable() {}, + clearPageTargeting() {}, + clearRequest() {}, + collapseAd() {}, + debug: false, + defineTag(dfn) { + const { targetId } = dfn; + if (!targetId) { + return; + } + gTags.set(targetId, new Tag(dfn)); + }, + disableDebug() {}, + dongle: undefined, + emitEvent(adObj, type) { + fireAdEvent(type, adObj); + }, + enableCookieSet() {}, + enableDebug() {}, + fireImpressionTrackers() {}, + getAdMarkup: () => "", + getAdWrap() {}, + getAstVersion: () => "0.49.0", + getPageTargeting() {}, + getTag(targetId) { + return gTags.get(targetId); + }, + handleCb() {}, + handleMediationBid() {}, + highlightAd() {}, + loaded: true, + loadTags() { + for (const tagName of gTags.keys()) { + refreshTag(tagName); + } + }, + modifyTag() {}, + notify() {}, + offEvent(type, target, cb) { + off(type, target, cb); + }, + onEvent(type, target, cb) { + on(type, target, cb); + }, + recordErrorEvent() {}, + refresh() {}, + registerRenderer() {}, + requests: {}, + resizeAd() {}, + setEndpoint() {}, + setKeywords() {}, + setPageOpts() {}, + setPageTargeting() {}, + setSafeFrameConfig() {}, + setSizes() {}, + showTag() {}, + }; + + const push = function (fn) { + if (typeof fn === "function") { + try { + fn(); + } catch (e) { + console.trace(e); + } + } + }; + + anq.push = push; + + anq.forEach(push); +} diff --git a/browser/extensions/webcompat/shims/adnexus-prebid.js b/browser/extensions/webcompat/shims/adnexus-prebid.js new file mode 100644 index 0000000000..f0f810f0e9 --- /dev/null +++ b/browser/extensions/webcompat/shims/adnexus-prebid.js @@ -0,0 +1,68 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1694401 - Shim Prebid.js + * + * Some sites rely on prebid.js to place content, perhaps in conjunction with + * other services like Google Publisher Tags and Amazon TAM. This shim prevents + * site breakage like image galleries breaking as the user browsers them, by + * allowing the content placement to succeed. + */ + +if (!window.pbjs?.requestBids) { + const que = window.pbjs?.que || []; + const cmd = window.pbjs?.cmd || []; + const adUnits = window.pbjs?.adUnits || []; + + window.pbjs = { + adUnits, + addAdUnits(arr) { + if (!Array.isArray(arr)) { + arr = [arr]; + } + adUnits.push(arr); + }, + cmd, + offEvent() {}, + que, + refreshAds() {}, + removeAdUnit(codes) { + if (!Array.isArray(codes)) { + codes = [codes]; + } + for (const code of codes) { + for (let i = adUnits.length - 1; i >= 0; i--) { + if (adUnits[i].code === code) { + adUnits.splice(i, 1); + } + } + } + }, + renderAd() {}, + requestBids(params) { + params?.bidsBackHandler?.(); + }, + setConfig() {}, + setTargetingForGPTAsync() {}, + }; + + const push = function (fn) { + if (typeof fn === "function") { + try { + fn(); + } catch (e) { + console.trace(e); + } + } + }; + + que.push = push; + cmd.push = push; + + que.forEach(push); + cmd.forEach(push); +} diff --git a/browser/extensions/webcompat/shims/adsafeprotected-ima.js b/browser/extensions/webcompat/shims/adsafeprotected-ima.js new file mode 100644 index 0000000000..93cd8e1eab --- /dev/null +++ b/browser/extensions/webcompat/shims/adsafeprotected-ima.js @@ -0,0 +1,19 @@ +/* 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/. */ + +"use strict"; + +/** + * + * Sites relying on Ad Safe Protected's adapter for Google IMA may + * have broken videos when the script is blocked. This shim stubs + * out the API to help mitigate major breakage. + */ + +if (!window.googleImaVansAdapter) { + window.googleImaVansAdapter = { + init() {}, + dispose() {}, + }; +} diff --git a/browser/extensions/webcompat/shims/apstag.js b/browser/extensions/webcompat/shims/apstag.js new file mode 100644 index 0000000000..55be05916b --- /dev/null +++ b/browser/extensions/webcompat/shims/apstag.js @@ -0,0 +1,73 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713698 - Shim Amazon Transparent Ad Marketplace's apstag.js + * + * Some sites such as politico.com rely on Amazon TAM tracker to serve ads, + * breaking functionality like galleries if it is blocked. This shim helps + * mitigate major breakage in that case. + */ + +if (!window.apstag?._getSlotIdToNameMapping) { + const _Q = window.apstag?._Q || []; + + const newBid = config => { + return { + amznbid: "", + amzniid: "", + amznp: "", + amznsz: "0x0", + size: "0x0", + slotID: config.slotID, + }; + }; + + window.apstag = { + _Q, + _getSlotIdToNameMapping() {}, + bids() {}, + debug() {}, + deleteId() {}, + fetchBids(cfg, cb) { + if (!Array.isArray(cfg?.slots)) { + return; + } + setTimeout(() => { + cb(cfg.slots.map(s => newBid(s))); + }, 1); + }, + init() {}, + punt() {}, + renderImp() {}, + renewId() {}, + setDisplayBids() {}, + targetingKeys: () => [], + thirdPartyData: {}, + updateId() {}, + }; + + window.apstagLOADED = true; + + _Q.push = function (prefix, args) { + try { + switch (prefix) { + case "f": + window.apstag.fetchBids(...args); + break; + case "i": + window.apstag.init(...args); + break; + } + } catch (e) { + console.trace(e); + } + }; + + for (const cmd of _Q) { + _Q.push(cmd); + } +} diff --git a/browser/extensions/webcompat/shims/blogger.js b/browser/extensions/webcompat/shims/blogger.js new file mode 100644 index 0000000000..a474b3c5e9 --- /dev/null +++ b/browser/extensions/webcompat/shims/blogger.js @@ -0,0 +1,39 @@ +/* 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/. */ + +/* globals exportFunction */ + +"use strict"; + +/** + * Blogger powered blogs rely on storage access to https://blogger.com to enable + * oauth with Google. For dFPI, sites need to use the Storage Access API to gain + * first party storage access. This shim calls requestStorageAccess on behalf of + * the site when a user wants to log in via oauth. + */ + +console.warn( + `When using oauth, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1776869 for details.` +); + +const GOOGLE_OAUTH_PATH_PREFIX = "https://accounts.google.com/ServiceLogin"; + +// Overwrite the window.open method so we can detect oauth related popups. +const origOpen = window.wrappedJSObject.open; +Object.defineProperty(window.wrappedJSObject, "open", { + value: exportFunction((url, ...args) => { + // Filter oauth popups. + if (!url.startsWith(GOOGLE_OAUTH_PATH_PREFIX)) { + return origOpen(url, ...args); + } + // Request storage access for the Blogger iframe. + document.requestStorageAccess().then(() => { + origOpen(url, ...args); + }); + // We don't have the window object yet which window.open returns, since the + // sign-in flow is dependent on the async storage access request. This isn't + // a problem as long as the website does not consume it. + return null; + }, window), +}); diff --git a/browser/extensions/webcompat/shims/bloggerAccount.js b/browser/extensions/webcompat/shims/bloggerAccount.js new file mode 100644 index 0000000000..19e80dbfbe --- /dev/null +++ b/browser/extensions/webcompat/shims/bloggerAccount.js @@ -0,0 +1,68 @@ +/* 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/. */ + +/* globals exportFunction */ + +"use strict"; + +/** + * Blogger uses Google as the auth provider. The account panel uses a + * third-party iframe of https://ogs.google.com, which requires first-party + * storage access to authenticate. This shim calls requestStorageAccess on + * behalf of the site when the user opens the account panel. + */ + +console.warn( + `When logging in with Google, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1777690 for details.` +); + +const STORAGE_ACCESS_ORIGIN = "https://ogs.google.com"; + +document.documentElement.addEventListener( + "click", + e => { + const { target, isTrusted } = e; + if (!isTrusted) { + return; + } + + const anchorEl = target.closest("a"); + if (!anchorEl) { + return; + } + + if ( + !anchorEl.href.startsWith("https://accounts.google.com/SignOutOptions") + ) { + return; + } + + // The storage access request below runs async so the panel won't open + // immediately. Mitigate this UX issue by updating the clicked element's + // style so the user gets some immediate feedback. + anchorEl.style.opacity = 0.5; + e.stopPropagation(); + e.preventDefault(); + + document + .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN) + .then(() => { + // Reload all iframes of ogs.google.com so the first-party cookies are + // sent to the server. + // The reload mechanism here is a bit of a hack, since we don't have + // access to the content window of a cross-origin iframe. + document + .querySelectorAll("iframe[src^='https://ogs.google.com/']") + .forEach(frame => (frame.src += "")); + }) + // Show the panel in both success and error state. When the user denies + // the storage access prompt they will see an error message in the account + // panel. + .finally(() => { + anchorEl.style.opacity = 1.0; + target.click(); + }); + }, + true +); diff --git a/browser/extensions/webcompat/shims/bmauth.js b/browser/extensions/webcompat/shims/bmauth.js new file mode 100644 index 0000000000..944f2100d6 --- /dev/null +++ b/browser/extensions/webcompat/shims/bmauth.js @@ -0,0 +1,21 @@ +/* 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/. */ + +"use strict"; + +if (!window.BmAuth) { + window.BmAuth = { + init: () => new Promise(() => {}), + handleSignIn: () => { + // TODO: handle this properly! + }, + isAuthenticated: () => Promise.resolve(false), + addListener: () => {}, + api: { + event: { + addListener: () => {}, + }, + }, + }; +} diff --git a/browser/extensions/webcompat/shims/branch.js b/browser/extensions/webcompat/shims/branch.js new file mode 100644 index 0000000000..31e8f4eeec --- /dev/null +++ b/browser/extensions/webcompat/shims/branch.js @@ -0,0 +1,84 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1716220 - Shim Branch Web SDK + * + * Sites such as TataPlay may not load properly if Branch Web SDK is + * blocked. This shim stubs out its script so the page still loads. + */ + +if (!window?.branch?.b) { + const queue = window?.branch?._q || []; + window.branch = new (class { + V = {}; + g = 0; + X = "web2.62.0"; + b = { + A: {}, + clear() {}, + get() {}, + getAll() {}, + isEnabled: () => true, + remove() {}, + set() {}, + ca() {}, + g: [], + l: 0, + o: 0, + s: null, + }; + addListener() {} + applyCode() {} + autoAppIndex() {} + banner() {} + c() {} + closeBanner() {} + closeJourney() {} + constructor() {} + creditHistory() {} + credits() {} + crossPlatformIds() {} + data() {} + deepview() {} + deepviewCta() {} + disableTracking() {} + first() {} + getBrowserFingerprintId() {} + getCode() {} + init(key, ...args) { + const cb = args.pop(); + if (typeof cb === "function") { + cb(undefined, {}); + } + } + lastAttributedTouchData() {} + link() {} + logEvent() {} + logout() {} + qrCode() {} + redeem() {} + referrals() {} + removeListener() {} + renderFinalize() {} + renderQueue() {} + sendSMS() {} + setAPIResponseCallback() {} + setBranchViewData() {} + setIdentity() {} + track() {} + trackCommerceEvent() {} + validateCode() {} + })(); + const push = ([fn, ...args]) => { + try { + window.branch[fn].apply(window.branch, args); + } catch (e) { + console.error(e); + } + }; + queue.forEach(push); +} diff --git a/browser/extensions/webcompat/shims/chartbeat.js b/browser/extensions/webcompat/shims/chartbeat.js new file mode 100644 index 0000000000..0e57fc6da1 --- /dev/null +++ b/browser/extensions/webcompat/shims/chartbeat.js @@ -0,0 +1,18 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713699 - Shim ChartBeat tracking + * + * Sites may rely on chartbeat's tracking as they might with Google Analytics, + * expecting it to be present for interactive site content to function. This + * shim mitigates related breakage. + */ + +window.pSUPERFLY = { + activity() {}, + virtualPage() {}, +}; diff --git a/browser/extensions/webcompat/shims/crave-ca.js b/browser/extensions/webcompat/shims/crave-ca.js new file mode 100644 index 0000000000..b4d93ccdfa --- /dev/null +++ b/browser/extensions/webcompat/shims/crave-ca.js @@ -0,0 +1,56 @@ +/* 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/. */ + +"use strict"; + +/* + * Bug 1746439 - crave.ca login broken with dFPI enabled + * + * Crave.ca relies upon a login page that is out-of-origin. That login page + * sets a cookie for https://www.crave.ca, which is then used as an proof of + * authentication on redirect back to the main site. This shim adds a request + * for storage access for https://www.crave.ca when the user tries to log in. + */ + +console.warn( + `When logging in, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1746439 for details.` +); + +// Third-party origin we need to request storage access for. +const STORAGE_ACCESS_ORIGIN = "https://www.crave.ca"; + +document.documentElement.addEventListener( + "click", + e => { + const { target, isTrusted } = e; + if (!isTrusted) { + return; + } + const button = target.closest("button"); + if (!button) { + return; + } + const form = target.closest(".login-form"); + if (!form) { + return; + } + + console.warn( + "Calling the Storage Access API on behalf of " + STORAGE_ACCESS_ORIGIN + ); + button.disabled = true; + e.stopPropagation(); + e.preventDefault(); + document + .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN) + .then(() => { + button.disabled = false; + target.click(); + }) + .catch(() => { + button.disabled = false; + }); + }, + true +); diff --git a/browser/extensions/webcompat/shims/criteo.js b/browser/extensions/webcompat/shims/criteo.js new file mode 100644 index 0000000000..afdc00b888 --- /dev/null +++ b/browser/extensions/webcompat/shims/criteo.js @@ -0,0 +1,64 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713720 - Shim Criteo + * + * Sites relying on window.Criteo to be loaded can experience + * breakage if it is blocked. Stubbing out the API in a shim can + * mitigate this breakage. + */ + +if (window.Criteo?.CallRTA === undefined) { + window.Criteo = { + CallRTA() {}, + ComputeStandaloneDFPTargeting() {}, + DisplayAcceptableAdIfAdblocked() {}, + DisplayAd() {}, + GetBids() {}, + GetBidsForAdUnit() {}, + Passback: { + RequestBids() {}, + RenderAd() {}, + }, + PubTag: { + Adapters: { + AMP() {}, + Prebid() {}, + }, + Context: { + GetIdfs() {}, + SetIdfs() {}, + }, + DirectBidding: { + DirectBiddingEvent() {}, + DirectBiddingSlot() {}, + DirectBiddingUrlBuilder() {}, + Size() {}, + }, + RTA: { + DefaultCrtgContentName: "crtg_content", + DefaultCrtgRtaCookieName: "crtg_rta", + }, + }, + RenderAd() {}, + RequestBids() {}, + RequestBidsOnGoogleTagSlots() {}, + SetCCPAExplicitOptOut() {}, + SetCeh() {}, + SetDFPKeyValueTargeting() {}, + SetLineItemRanges() {}, + SetPublisherExt() {}, + SetSlotsExt() {}, + SetTargeting() {}, + SetUserExt() {}, + events: { + push() {}, + }, + passbackEvents: [], + usePrebidEvents: true, + }; +} diff --git a/browser/extensions/webcompat/shims/cxense.js b/browser/extensions/webcompat/shims/cxense.js new file mode 100644 index 0000000000..55862f4fb5 --- /dev/null +++ b/browser/extensions/webcompat/shims/cxense.js @@ -0,0 +1,593 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713721 - Shim Cxense + * + * Sites relying on window.cX can experience breakage if it is blocked. + * Stubbing out the API in a shim can mitigate this breakage. There are + * two versions of the API, one including window.cX.CCE, but both appear + * to be very similar so we use one shim for both. + */ + +if (window.cX?.getUserSegmentIds === undefined) { + const callQueue = window.cX?.callQueue || []; + const callQueueCCE = window.cX?.CCE?.callQueue || []; + + function getRandomString(l = 16) { + const v = crypto.getRandomValues(new Uint8Array(l)); + const s = Array.from(v, c => c.toString(16)).join(""); + return s.slice(0, l); + } + + const call = (cb, ...args) => { + if (typeof cb !== "function") { + return; + } + try { + cb(...args); + } catch (e) { + console.error(e); + } + }; + + const invokeOn = lib => { + return (fn, ...args) => { + try { + lib[fn](...args); + } catch (e) { + console.error(e); + } + }; + }; + + const userId = getRandomString(); + const cxUserId = `cx:${getRandomString(25)}:${getRandomString(12)}`; + const topLeft = { left: 0, top: 0 }; + const margins = { left: 0, top: 0, right: 0, bottom: 0 }; + const ccePushUrl = + "https://comcluster.cxense.com/cce/push?callback={{callback}}"; + const displayWidget = (divId, a, ctx, callback) => call(callback, ctx, divId); + const getUserSegmentIds = a => call(a?.callback, a?.defaultValue || []); + const init = (a, b, c, d, callback) => call(callback); + const render = (a, data, ctx, callback) => call(callback, data, ctx); + const run = (params, ctx, callback) => call(callback, params, ctx); + const runCtrlVersion = (a, b, callback) => call(callback); + const runCxVersion = (a, data, b, ctx, callback) => call(callback, data, ctx); + const runTest = (a, divId, b, c, ctx, callback) => call(callback, divId, ctx); + const sendConversionEvent = (a, options) => call(options?.callback, {}); + const sendEvent = (a, b, args) => call(args?.callback, {}); + + const getDivId = className => { + const e = document.querySelector(`.${className}`); + if (e) { + return `${className}-01`; + } + return null; + }; + + const getDocumentSize = () => { + const width = document.body.clientWidth; + const height = document.body.clientHeight; + return { width, height }; + }; + + const getNowSeconds = () => { + return Math.round(new Date().getTime() / 1000); + }; + + const getPageContext = () => { + return { + location: location.href, + pageViewRandom: "", + userId, + }; + }; + + const getWindowSize = () => { + const width = window.innerWidth; + const height = window.innerHeight; + return { width, height }; + }; + + const isObject = i => { + return typeof i === "object" && i !== null && !Array.isArray(i); + }; + + const runMulti = widgets => { + widgets?.forEach(({ widgetParams, widgetContext, widgetCallback }) => { + call(widgetCallback, widgetParams, widgetContext); + }); + }; + + let testGroup = -1; + let snapPoints = []; + const startTime = new Date(); + + const library = { + addCustomerScript() {}, + addEventListener() {}, + addExternalId() {}, + afterInitializePage() {}, + allUserConsents() {}, + backends: { + production: { + baseAdDeliveryUrl: "http://adserver.cxad.cxense.com/adserver/search", + secureBaseAdDeliveryUrl: + "https://s-adserver.cxad.cxense.com/adserver/search", + }, + sandbox: { + baseAdDeliveryUrl: + "http://adserver.sandbox.cxad.cxense.com/adserver/search", + secureBaseAdDeliveryUrl: + "https://s-adserver.sandbox.cxad.cxense.com/adserver/search", + }, + }, + calculateAdSpaceSize(adCount, adUnitSize, marginA, marginB) { + return adCount * (adUnitSize + marginA + marginB); + }, + cdn: { + template: { + direct: { + http: "http://cdn.cxpublic.com/", + https: "https://cdn.cxpublic.com/", + }, + mapped: { + http: "http://cdn-templates.cxpublic.com/", + https: "https://cdn-templates.cxpublic.com/", + }, + }, + }, + cint() {}, + cleanUpGlobalIds: [], + clearBaseUrl: "https://scdn.cxense.com/sclear.html", + clearCustomParameters() {}, + clearIdUrl: "https://scomcluster.cxense.com/public/clearid", + clearIds() {}, + clickTracker: (a, b, callback) => call(callback), + clientStorageUrl: "https://clientstorage.cxense.com", + combineArgs: () => Object.create(), + combineKeywordsIntoArray: () => [], + consentClasses: ["pv", "segment", "ad", "recs"], + consentClassesV2: ["geo", "device"], + cookieSyncRUrl: "csyn-r.cxense.com", + createDelegate() {}, + csdUrls: { + domainScriptUrl: "//csd.cxpublic.com/d/", + customerScriptUrl: "//csd.cxpublic.com/t/", + }, + cxenseGlobalIdIframeUrl: "https://scdn.cxense.com/sglobal.html", + cxenseUserIdUrl: "https://id.cxense.com/public/user/id", + decodeUrlEncodedNameValuePairs: () => Object.create(), + defaultAdRenderer: () => "", + deleteCookie() {}, + denyWithoutConsent: { + addExternalId: "pv", + getUserSegmentIds: "segment", + insertAdSpace: "ad", + insertMultipleAdSpaces: "ad", + sendEvent: "pv", + sendPageViewEvent: "pv", + sync: "ad", + }, + dmpPushUrl: "https://comcluster.cxense.com/dmp/push?callback={{callback}}", + emptyWidgetUrl: "https://scdn.cxense.com/empty.html", + eventReceiverBaseUrl: "https://scomcluster.cxense.com/Repo/rep.html", + eventReceiverBaseUrlGif: "https://scomcluster.cxense.com/Repo/rep.gif", + getAllText: () => "", + getClientStorageVariable() {}, + getCookie: () => null, + getCxenseUserId: () => cxUserId, + getDocumentSize, + getElementPosition: () => topLeft, + getHashFragment: () => location.hash.substr(1), + getLocalStats: () => Object.create(), + getNodeValue: n => n.nodeValue, + getNowSeconds, + getPageContext, + getRandomString, + getScrollPos: () => topLeft, + getSessionId: () => "", + getSiteId: () => "", + getTimezoneOffset: () => new Date().getTimezoneOffset(), + getTopLevelDomain: () => location.hostname, + getUserId: () => userId, + getUserSegmentIds, + getWindowSize, + hasConsent: () => true, + hasHistory: () => true, + hasLocalStorage: () => true, + hasPassiveEventListeners: () => true, + hasPostMessage: () => true, + hasSessionStorage() {}, + initializePage() {}, + insertAdSpace() {}, + insertMultipleAdSpaces() {}, + insertWidget() {}, + invoke: invokeOn(library), + isAmpIFrame() {}, + isArray() {}, + isCompatModeActive() {}, + isConsentRequired() {}, + isEdge: () => false, + isFirefox: () => true, + isIE6Or7: () => false, + isObject, + isRecsDestination: () => false, + isSafari: () => false, + isTextNode: n => n?.nodeType === 3, + isTopWindow: () => window === top, + jsonpRequest: () => false, + loadScript() {}, + m_accountId: "0", + m_activityEvents: false, + m_activityState: { + activeTime: startTime, + currScrollLeft: 0, + currScrollTop: 0, + exitLink: "", + hadHIDActivity: false, + maxViewLeft: 1, + maxViewTop: 1, + parentMetrics: undefined, + prevActivityTime: startTime + 2, + prevScreenX: 0, + prevScreenY: 0, + prevScrollLeft: 0, + prevScrollTop: 0, + prevTime: startTime + 1, + prevWindowHeight: 1, + prevWindowWidth: 1, + scrollDepthPercentage: 0, + scrollDepthPixels: 0, + }, + m_atfr: null, + m_c1xTpWait: 0, + m_clientStorage: { + iframeEl: null, + iframeIsLoaded: false, + iframeOrigin: "https://clientstorage.cxense.com", + iframePath: "/clientstorage_v2.html", + messageContexts: {}, + messageQueue: [], + }, + m_compatMode: {}, + m_compatModeActive: false, + m_compatPvSent: false, + m_consentVersion: 1, + m_customParameters: [], + m_documentSizeRequestedFromChild: false, + m_externalUserIds: [], + m_globalIdLoading: { + globalIdIFrameEl: null, + globalIdIFrameElLoaded: false, + }, + m_isSpaRecsDestination: false, + m_knownMessageSources: [], + m_p1Complete: false, + m_prevLocationHash: "", + m_previousPageViewReport: null, + m_rawCustomParameters: {}, + m_rnd: getRandomString(), + m_scriptStartTime: startTime, + m_siteId: "0", + m_spaRecsClickUrl: null, + m_thirdPartyIds: true, + m_usesConsent: false, + m_usesIabConsent: false, + m_usesSecureCookies: true, + m_usesTcf20Consent: false, + m_widgetSpecs: {}, + Object, + onClearIds() {}, + onFFP1() {}, + onP1() {}, + p1BaseUrl: "https://scdn.cxense.com/sp1.html", + p1JsUrl: "https://p1cluster.cxense.com/p1.js", + parseHashArgs: () => Object.create(), + parseMargins: () => margins, + parseUrlArgs: () => Object.create(), + postMessageToParent() {}, + publicWidgetDataUrl: "https://api.cxense.com/public/widget/data", + removeClientStorageVariable() {}, + removeEventListener() {}, + renderContainedImage: () => "<div/>", + renderTemplate: () => "<div/>", + reportActivity() {}, + requireActivityEvents() {}, + requireConsent() {}, + requireOnlyFirstPartyIds() {}, + requireSecureCookies() {}, + requireTcf20() {}, + sendEvent, + sendSpaRecsClick: (a, callback) => call(callback), + setAccountId() {}, + setAllConsentsTo() {}, + setClientStorageVariable() {}, + setCompatMode() {}, + setConsent() {}, + setCookie() {}, + setCustomParameters() {}, + setEventAttributes() {}, + setGeoPosition() {}, + setNodeValue() {}, + setRandomId() {}, + setRestrictionsToConsentClasses() {}, + setRetargetingParameters() {}, + setSiteId() {}, + setUserProfileParameters() {}, + setupIabCmp() {}, + setupTcfApi() {}, + shouldPollActivity() {}, + startLocalStats() {}, + startSessionAnnotation() {}, + stopAllSessionAnnotations() {}, + stopSessionAnnotation() {}, + sync() {}, + trackAmpIFrame() {}, + trackElement() {}, + trim: s => s.trim(), + tsridUrl: "https://tsrid.cxense.com/lookup?callback={{callback}}", + userSegmentUrl: + "https://api.cxense.com/profile/user/segment?callback={{callback}}", + }; + + const libraryCCE = { + "__cx-toolkit__": { + isShown: true, + data: [], + }, + activeSnapPoint: null, + activeWidgets: [], + ccePushUrl, + clickTracker: () => "", + displayResult() {}, + displayWidget, + getDivId, + getTestGroup: () => testGroup, + init, + insertMaster() {}, + instrumentClickLinks() {}, + invoke: invokeOn(libraryCCE), + noCache: false, + offerProductId: null, + persistedQueryId: null, + prefix: null, + previewCampaign: null, + previewDiv: null, + previewId: null, + previewTestId: null, + processCxResult() {}, + render, + reportTestImpression() {}, + run, + runCtrlVersion, + runCxVersion, + runMulti, + runTest, + sendConversionEvent, + sendPageViewEvent: (a, b, c, callback) => call(callback), + setSnapPoints(x) { + snapPoints = x; + }, + setTestGroup(x) { + testGroup = x; + }, + setVisibilityField() {}, + get snapPoints() { + return snapPoints; + }, + startTime, + get testGroup() { + return testGroup; + }, + testVariant: null, + trackTime: 0.5, + trackVisibility() {}, + updateRecsClickUrls() {}, + utmParams: [], + version: "2.42", + visibilityField: "timeHalf", + }; + + const CCE = { + activeSnapPoint: null, + activeWidgets: [], + callQueue: callQueueCCE, + ccePushUrl, + clickTracker: () => "", + displayResult() {}, + displayWidget, + getDivId, + getTestGroup: () => testGroup, + init, + insertMaster() {}, + instrumentClickLinks() {}, + invoke: invokeOn(libraryCCE), + library: libraryCCE, + noCache: false, + offerProductId: null, + persistedQueryId: null, + prefix: null, + previewCampaign: null, + previewDiv: null, + previewId: null, + previewTestId: null, + processCxResult() {}, + render, + reportTestImpression() {}, + run, + runCtrlVersion, + runCxVersion, + runMulti, + runTest, + sendConversionEvent, + sendPageViewEvent: (a, b, c, callback) => call(callback), + setSnapPoints(x) { + snapPoints = x; + }, + setTestGroup(x) { + testGroup = x; + }, + setVisibilityField() {}, + get snapPoints() { + return snapPoints; + }, + startTime, + get testGroup() { + return testGroup; + }, + testVariant: null, + trackTime: 0.5, + trackVisibility() {}, + updateRecsClickUrls() {}, + utmParams: [], + version: "2.42", + visibilityField: "timeHalf", + }; + + window.cX = { + addCustomerScript() {}, + addEventListener() {}, + addExternalId() {}, + afterInitializePage() {}, + allUserConsents: () => undefined, + Array, + calculateAdSpaceSize: () => 0, + callQueue, + CCE, + cint: () => undefined, + clearCustomParameters() {}, + clearIds() {}, + clickTracker: () => "", + combineArgs: () => Object.create(), + combineKeywordsIntoArray: () => [], + createDelegate() {}, + decodeUrlEncodedNameValuePairs: () => Object.create(), + defaultAdRenderer: () => "", + deleteCookie() {}, + getAllText: () => "", + getClientStorageVariable() {}, + getCookie: () => null, + getCxenseUserId: () => cxUserId, + getDocumentSize, + getElementPosition: () => topLeft, + getHashFragment: () => location.hash.substr(1), + getLocalStats: () => Object.create(), + getNodeValue: n => n.nodeValue, + getNowSeconds, + getPageContext, + getRandomString, + getScrollPos: () => topLeft, + getSessionId: () => "", + getSiteId: () => "", + getTimezoneOffset: () => new Date().getTimezoneOffset(), + getTopLevelDomain: () => location.hostname, + getUserId: () => userId, + getUserSegmentIds, + getWindowSize, + hasConsent: () => true, + hasHistory: () => true, + hasLocalStorage: () => true, + hasPassiveEventListeners: () => true, + hasPostMessage: () => true, + hasSessionStorage() {}, + initializePage() {}, + insertAdSpace() {}, + insertMultipleAdSpaces() {}, + insertWidget() {}, + invoke: invokeOn(library), + isAmpIFrame() {}, + isArray() {}, + isCompatModeActive() {}, + isConsentRequired() {}, + isEdge: () => false, + isFirefox: () => true, + isIE6Or7: () => false, + isObject, + isRecsDestination: () => false, + isSafari: () => false, + isTextNode: n => n?.nodeType === 3, + isTopWindow: () => window === top, + JSON, + jsonpRequest: () => false, + library, + loadScript() {}, + Object, + onClearIds() {}, + onFFP1() {}, + onP1() {}, + parseHashArgs: () => Object.create(), + parseMargins: () => margins, + parseUrlArgs: () => Object.create(), + postMessageToParent() {}, + removeClientStorageVariable() {}, + removeEventListener() {}, + renderContainedImage: () => "<div/>", + renderTemplate: () => "<div/>", + reportActivity() {}, + requireActivityEvents() {}, + requireConsent() {}, + requireOnlyFirstPartyIds() {}, + requireSecureCookies() {}, + requireTcf20() {}, + sendEvent, + sendPageViewEvent: (a, callback) => call(callback, {}), + sendSpaRecsClick() {}, + setAccountId() {}, + setAllConsentsTo() {}, + setClientStorageVariable() {}, + setCompatMode() {}, + setConsent() {}, + setCookie() {}, + setCustomParameters() {}, + setEventAttributes() {}, + setGeoPosition() {}, + setNodeValue() {}, + setRandomId() {}, + setRestrictionsToConsentClasses() {}, + setRetargetingParameters() {}, + setSiteId() {}, + setUserProfileParameters() {}, + setupIabCmp() {}, + setupTcfApi() {}, + shouldPollActivity() {}, + startLocalStats() {}, + startSessionAnnotation() {}, + stopAllSessionAnnotations() {}, + stopSessionAnnotation() {}, + sync() {}, + trackAmpIFrame() {}, + trackElement() {}, + trim: s => s.trim(), + }; + + window.cxTest = window.cX; + + window.cx_pollActiveTime = () => undefined; + window.cx_pollActivity = () => undefined; + window.cx_pollFragmentMessage = () => undefined; + + const execQueue = (lib, queue) => { + return () => { + const invoke = invokeOn(lib); + setTimeout(() => { + queue.push = cmd => { + setTimeout(() => invoke(...cmd), 1); + }; + for (const cmd of queue) { + invoke(...cmd); + } + }, 25); + }; + }; + + window.cx_callQueueExecute = execQueue(library, callQueue); + window.cxCCE_callQueueExecute = execQueue(libraryCCE, callQueueCCE); + + window.cx_callQueueExecute(); + window.cxCCE_callQueueExecute(); +} diff --git a/browser/extensions/webcompat/shims/doubleverify.js b/browser/extensions/webcompat/shims/doubleverify.js new file mode 100644 index 0000000000..7eaf945d77 --- /dev/null +++ b/browser/extensions/webcompat/shims/doubleverify.js @@ -0,0 +1,36 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1771557 - Shim DoubleVerify analytics + * + * Some sites such as Sports Illustrated expect DoubleVerify's + * analytics script to load, otherwise odd breakage may occur. + * This shim helps mitigate such breakage. + */ + +if (!window?.PQ?.loaded) { + const cmd = []; + cmd.push = function (c) { + try { + c?.(); + } catch (_) {} + }; + + window.apntag = { + anq: [], + }; + + window.PQ = { + cmd, + loaded: true, + getTargeting: (_, cb) => cb?.([]), + init: () => {}, + loadSignals: (_, cb) => cb?.(), + loadSignalsForSlots: (_, cb) => cb?.(), + PTS: {}, + }; +} diff --git a/browser/extensions/webcompat/shims/eluminate.js b/browser/extensions/webcompat/shims/eluminate.js new file mode 100644 index 0000000000..3fa65c048c --- /dev/null +++ b/browser/extensions/webcompat/shims/eluminate.js @@ -0,0 +1,95 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1606448 - Shim CoreMetrics Eluminate analytics + * + * Sites may rely on eluminate.js tracking in ways which cause breakage, + * which has been seen on shopping sites such as Vans.com, where the + * search filtering UX is broken. This shim mitigates such breakage. + */ + +if (!window.CM_DDX) { + window.CM_DDX = { + domReadyFired: false, + headScripts: true, + dispatcherLoadRequested: false, + firstPassFunctionBinding: false, + BAD_PAGE_ID_ELAPSED_TIMEOUT: 5000, + version: -1, + standalone: false, + test: { + syndicate: true, + testCounter: "", + doTest: false, + newWin: false, + process: () => {}, + }, + partner: {}, + invokeFunctionWhenAvailable: a => { + a(); + }, + gup: d => "", + privacy: { + isDoNotTrackEnabled: () => false, + setDoNotTrack: () => {}, + getDoNotTrack: () => false, + }, + setSubCookie: () => {}, + }; + const noopfn = () => {}; + const w = window; + w.cmAddShared = noopfn; + w.cmCalcSKUString = noopfn; + w.cmCreateManualImpressionTag = noopfn; + w.cmCreateManualLinkClickTag = noopfn; + w.cmCreateManualPageviewTag = noopfn; + w.cmCreateOrderTag = noopfn; + w.cmCreatePageviewTag = noopfn; + w.cmExecuteTagQueue = noopfn; + w.cmRetrieveUserID = noopfn; + w.cmSetClientID = noopfn; + w.cmSetCurrencyCode = noopfn; + w.cmSetFirstPartyIDs = noopfn; + w.cmSetSubCookie = noopfn; + w.cmSetupCookieMigration = noopfn; + w.cmSetupNormalization = noopfn; + w.cmSetupOther = noopfn; + w.cmStartTagSet = noopfn; + w.cmCreateConversionEventTag = noopfn; + w.cmCreateDefaultPageviewTag = noopfn; + w.cmCreateElementTag = noopfn; + w.cmCreateManualImpressionTag = noopfn; + w.cmCreateManualLinkClickTag = noopfn; + w.cmCreateManualPageviewTag = noopfn; + w.cmCreatePageElementTag = noopfn; + w.cmCreatePageviewTag = noopfn; + w.cmCreateProductElementTag = noopfn; + w.cmCreateProductviewTag = noopfn; + w.cmCreateTechPropsTag = noopfn; + w.cmLoadIOConfig = noopfn; + w.cmSetClientID = noopfn; + w.cmSetCurrencyCode = noopfn; + w.cmSetFirstPartyIDs = noopfn; + w.cmSetupCookieMigration = noopfn; + w.cmSetupNormalization = noopfn; + + w.cmSetupOther = b => { + for (const a in b) { + window[a] = b[a]; + } + }; + + const techProps = {}; + + w.coremetrics = { + cmLastReferencedPageID: "", + cmLoad: noopfn, + cmUpdateConfig: noopfn, + getTechProps: () => techProps, + isDef: c => typeof c !== "undefined" && c, + }; +} diff --git a/browser/extensions/webcompat/shims/empty-script.js b/browser/extensions/webcompat/shims/empty-script.js new file mode 100644 index 0000000000..d01f2ab537 --- /dev/null +++ b/browser/extensions/webcompat/shims/empty-script.js @@ -0,0 +1,5 @@ +/* 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/. */ + +/* This script is intentionally empty */ diff --git a/browser/extensions/webcompat/shims/empty-shim.txt b/browser/extensions/webcompat/shims/empty-shim.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/browser/extensions/webcompat/shims/empty-shim.txt diff --git a/browser/extensions/webcompat/shims/everest.js b/browser/extensions/webcompat/shims/everest.js new file mode 100644 index 0000000000..259ab9033e --- /dev/null +++ b/browser/extensions/webcompat/shims/everest.js @@ -0,0 +1,171 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1728114 - Shim Adobe EverestJS + * + * Sites assuming EverestJS will load can break if it is blocked. + * This shim mitigates that breakage. + */ + +if (!window.__ql) { + window.__ql = {}; +} + +if (!window.EF) { + const AdCloudLocalStorage = { + get: (_, cb) => cb(), + isInitDone: true, + isInitSuccess: true, + }; + + const emptyObj = {}; + + const nullSrc = { + getHosts: () => [undefined], + getProtocols: () => [undefined], + hash: {}, + hashParamsOrder: [], + host: undefined, + path: [], + port: undefined, + query: {}, + queryDelimiter: "&", + queryParamsOrder: [], + queryPrefix: "?", + queryWithoutEncode: {}, + respectEmptyQueryParamValue: undefined, + scheme: undefined, + text: "//", + userInfo: undefined, + }; + + const pixelDetailsEvent = { + addToDom() {}, + canAddToDom: () => false, + fire() {}, + getDomElement() {}, + initializeUri() {}, + pixelDetailsReceiver() {}, + scheme: "https:", + uri: nullSrc, + userid: 0, + }; + + window.EF = { + AdCloudLocalStorage, + accessTopUrl: 0, + acquireCookieMatchingSlot() {}, + addListener() {}, + addPixelDetailsReadyListener() {}, + addToDom() {}, + allow3rdPartyPixels: 1, + appData: "", + appendDictionary() {}, + checkGlobalSid() {}, + checkUrlParams() {}, + cmHost: "cm.everesttech.net", + context: { + isFbApp: () => 0, + isPageview: () => false, + isSegmentation: () => false, + isTransaction: () => false, + }, + conversionData: "", + cookieMatchingSlots: 1, + debug: 0, + deserializeUrlParams: () => emptyObj, + doCookieMatching() {}, + ef_itp_ls: false, + eventType: "", + executeAfterLoad() {}, + executeOnloadCallbacks() {}, + expectedTrackingParams: ["ev_cl", "ev_sid"], + fbIsApp: 0, + fbsCM: 0, + fbsPixelId: 0, + filterList: () => [], + getArrayIndex: -1, + getConversionData: () => "", + getConversionDataFromLocalStorage: cb => cb(), + getDisplayClickUri: () => "", + getEpochFromEfUniq: () => 0, + getFirstLevelObjectCopy: () => emptyObj, + getInvisibleIframeElement() {}, + getInvisibleImageElement() {}, + getMacroSubstitutedText: () => "", + getPixelDetails: cb => cb({}), + getScriptElement() {}, + getScriptSrc: () => "", + getServerParams: () => emptyObj, + getSortedAttributes: () => [], + getTrackingParams: () => emptyObj, + getTransactionParams: () => emptyObj, + handleConversionData() {}, + impressionProperties: "", + impressionTypes: ["impression", "impression_served"], + inFloodlight: 0, + init(config) { + try { + const { userId } = config; + window.EF.userId = userId; + pixelDetailsEvent.userId = userId; + } catch (_) {} + }, + initializeEFVariables() {}, + isArray: a => Array.isArray(a), + isEmptyDictionary: () => true, + isITPEnabled: () => false, + isPermanentCookieSet: () => false, + isSearchClick: () => 0, + isXSSReady() {}, + jsHost: "www.everestjs.net", + jsTagAdded: 0, + location: nullSrc, + locationHref: nullSrc, + locationSkipBang: nullSrc, + log() {}, + main() {}, + main2() {}, + newCookieMatchingEvent: () => emptyObj, + newFbsCookieMatching: () => emptyObj, + newImpression: () => emptyObj, + newPageview: () => emptyObj, + newPixelDetails: () => emptyObj, + newPixelEvent: () => emptyObj, + newPixelServerDisplayClickRedirectUri: () => emptyObj, + newPixelServerGenericRedirectUri: () => emptyObj, + newPixelServerUri: () => emptyObj, + newProductSegment: () => emptyObj, + newSegmentJavascript: () => emptyObj, + newTransaction: () => emptyObj, + newUri: () => emptyObj, + onloadCallbacks: [], + pageViewProperties: "", + pageviewProperties: "", + pixelDetails: {}, + pixelDetailsAdded: 1, + pixelDetailsEvent, + pixelDetailsParams: [], + pixelDetailsReadyCallbackFns: [], + pixelDetailsRecieverCalled: 1, + pixelHost: "pixel.everesttech.net", + protocol: document?.location?.protocol || "", + referrer: nullSrc, + removeListener() {}, + searchSegment: "", + segment: "", + serverParamsListener() {}, + sid: 0, + sku: "", + throttleCookie: "", + trackingJavascriptSrc: nullSrc, + transactionObjectList: [], + transactionProperties: "", + userServerParams: {}, + userid: 0, + }; +} diff --git a/browser/extensions/webcompat/shims/facebook-sdk.js b/browser/extensions/webcompat/shims/facebook-sdk.js new file mode 100644 index 0000000000..1e995ff047 --- /dev/null +++ b/browser/extensions/webcompat/shims/facebook-sdk.js @@ -0,0 +1,554 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1226498 - Shim Facebook SDK + * + * This shim provides functionality to enable Facebook's authenticator on third + * party sites ("continue/log in with Facebook" buttons). This includes rendering + * the button as the SDK would, if sites require it. This way, if users wish to + * opt into the Facebook login process regardless of the tracking consequences, + * they only need to click the button as usual. + * + * In addition, the shim also attempts to provide placeholders for Facebook + * videos, which users may click to opt into seeing the video (also despite + * the increased tracking risks). This is an experimental feature enabled + * that is only currently enabled on nightly builds. + * + * Finally, this shim also stubs out as much of the SDK as possible to prevent + * breaking on sites which expect that it will always successfully load. + */ + +if (!window.FB) { + const FacebookLogoURL = "https://smartblock.firefox.etp/facebook.svg"; + const PlayIconURL = "https://smartblock.firefox.etp/play.svg"; + + const originalUrl = document.currentScript.src; + + let haveUnshimmed; + let initInfo; + let activeOnloginAttribute; + const placeholdersToRemoveOnUnshim = new Set(); + const loggedGraphApiCalls = []; + const eventHandlers = new Map(); + + function getGUID() { + const v = crypto.getRandomValues(new Uint8Array(20)); + return Array.from(v, c => c.toString(16)).join(""); + } + + const sendMessageToAddon = (function () { + const shimId = "FacebookSDK"; + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function (message) { + const messageId = getGUID(); + return new Promise(resolve => { + const payload = { message, messageId, shimId }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + const isNightly = sendMessageToAddon("getOptions").then(opts => { + return opts.releaseBranch === "nightly"; + }); + + function makeLoginPlaceholder(target) { + // Sites may provide their own login buttons, or rely on the Facebook SDK + // to render one for them. For the latter case, we provide placeholders + // which try to match the examples and documentation here: + // https://developers.facebook.com/docs/facebook-login/web/login-button/ + + if (target.textContent || target.hasAttribute("fb-xfbml-state")) { + return; + } + target.setAttribute("fb-xfbml-state", ""); + + const size = target.getAttribute("data-size") || "large"; + + let font, margin, minWidth, maxWidth, height, iconHeight; + if (size === "small") { + font = 11; + margin = 8; + minWidth = maxWidth = 200; + height = 20; + iconHeight = 12; + } else if (size === "medium") { + font = 13; + margin = 8; + minWidth = 200; + maxWidth = 320; + height = 28; + iconHeight = 16; + } else { + font = 16; + minWidth = 240; + maxWidth = 400; + margin = 12; + height = 40; + iconHeight = 24; + } + + const wattr = target.getAttribute("data-width") || ""; + const width = + wattr === "100%" ? wattr : `${parseFloat(wattr) || minWidth}px`; + + const round = target.getAttribute("data-layout") === "rounded" ? 20 : 4; + + const text = + target.getAttribute("data-button-type") === "continue_with" + ? "Continue with Facebook" + : "Log in with Facebook"; + + const button = document.createElement("div"); + button.style = ` + display: flex; + align-items: center; + justify-content: center; + padding-left: ${margin + iconHeight}px; + ${width}; + min-width: ${minWidth}px; + max-width: ${maxWidth}px; + height: ${height}px; + border-radius: ${round}px; + -moz-text-size-adjust: none; + -moz-user-select: none; + color: #fff; + font-size: ${font}px; + font-weight: bold; + font-family: Helvetica, Arial, sans-serif; + letter-spacing: .25px; + background-color: #1877f2; + background-repeat: no-repeat; + background-position: ${margin}px 50%; + background-size: ${iconHeight}px ${iconHeight}px; + background-image: url(${FacebookLogoURL}); + `; + button.textContent = text; + target.appendChild(button); + target.addEventListener("click", () => { + activeOnloginAttribute = target.getAttribute("onlogin"); + }); + } + + async function makeVideoPlaceholder(target) { + // For videos, we provide a more generic placeholder of roughly the + // expected size with a play button, as well as a Facebook logo. + if (!(await isNightly) || target.hasAttribute("fb-xfbml-state")) { + return; + } + target.setAttribute("fb-xfbml-state", ""); + + let width = parseInt(target.getAttribute("data-width")); + let height = parseInt(target.getAttribute("data-height")); + if (height) { + height = `${width * 0.6}px`; + } else { + height = `100%; min-height:${width * 0.75}px`; + } + if (width) { + width = `${width}px`; + } else { + width = `100%; min-width:200px`; + } + + const placeholder = document.createElement("div"); + placeholdersToRemoveOnUnshim.add(placeholder); + placeholder.style = ` + width: ${width}; + height: ${height}; + top: 0px; + left: 0px; + background: #000; + color: #fff; + text-align: center; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + background-image: url(${FacebookLogoURL}), url(${PlayIconURL}); + background-position: calc(100% - 24px) 24px, 50% 47.5%; + background-repeat: no-repeat, no-repeat; + background-size: 43px 42px, 25% 25%; + -moz-text-size-adjust: none; + -moz-user-select: none; + color: #fff; + align-items: center; + padding-top: 200px; + font-size: 14pt; + `; + placeholder.textContent = "Click to allow blocked Facebook content"; + placeholder.addEventListener("click", evt => { + if (!evt.isTrusted) { + return; + } + allowFacebookSDK(() => { + placeholdersToRemoveOnUnshim.forEach(p => p.remove()); + }); + }); + + target.innerHTML = ""; + target.appendChild(placeholder); + } + + // We monitor for XFBML objects as Facebook SDK does, so we + // can provide placeholders for dynamically-added ones. + const xfbmlObserver = new MutationObserver(mutations => { + for (let { addedNodes, target, type } of mutations) { + const nodes = type === "attributes" ? [target] : addedNodes; + for (const node of nodes) { + if (node?.classList?.contains("fb-login-button")) { + makeLoginPlaceholder(node); + } + if (node?.classList?.contains("fb-video")) { + makeVideoPlaceholder(node); + } + } + } + }); + + xfbmlObserver.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["class"], + }); + + const needPopup = + !/app_runner/.test(window.name) && !/iframe_canvas/.test(window.name); + const popupName = getGUID(); + let activePopup; + + if (needPopup) { + const oldWindowOpen = window.open; + window.open = function (href, name, params) { + try { + const url = new URL(href, window.location.href); + if ( + url.protocol === "https:" && + (url.hostname === "m.facebook.com" || + url.hostname === "www.facebook.com") && + url.pathname.endsWith("/oauth") + ) { + name = popupName; + } + } catch (e) { + console.error(e); + } + return oldWindowOpen.call(window, href, name, params); + }; + } + + let allowingFacebookPromise; + + async function allowFacebookSDK(postInitCallback) { + if (allowingFacebookPromise) { + return allowingFacebookPromise; + } + + let resolve, reject; + allowingFacebookPromise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + await sendMessageToAddon("optIn"); + + xfbmlObserver.disconnect(); + + const shim = window.FB; + window.FB = undefined; + + // We need to pass the site's initialization info to the real + // SDK as it loads, so we use the fbAsyncInit mechanism to + // do so, also ensuring our own post-init callbacks are called. + const oldInit = window.fbAsyncInit; + window.fbAsyncInit = () => { + try { + if (typeof initInfo !== "undefined") { + window.FB.init(initInfo); + } else if (oldInit) { + oldInit(); + } + } catch (e) { + console.error(e); + } + + // Also re-subscribe any SDK event listeners as early as possible. + for (const [name, fns] of eventHandlers.entries()) { + for (const fn of fns) { + window.FB.Event.subscribe(name, fn); + } + } + + // Allow the shim to do any post-init work early as well, while the + // SDK script finishes loading and we ask it to re-parse XFBML etc. + postInitCallback?.(); + }; + + const script = document.createElement("script"); + script.src = originalUrl; + + script.addEventListener("error", () => { + allowingFacebookPromise = null; + script.remove(); + activePopup?.close(); + window.FB = shim; + reject(); + alert("Failed to load Facebook SDK; please try again"); + }); + + script.addEventListener("load", () => { + haveUnshimmed = true; + + // After the real SDK has fully loaded we re-issue any Graph API + // calls the page is waiting on, as well as requesting for it to + // re-parse any XBFML elements (including ones with placeholders). + + for (const args of loggedGraphApiCalls) { + try { + window.FB.api.apply(window.FB, args); + } catch (e) { + console.error(e); + } + } + + window.FB.XFBML.parse(document.body, resolve); + }); + + document.head.appendChild(script); + + return allowingFacebookPromise; + } + + function buildPopupParams() { + // We try to match Facebook's popup size reasonably closely. + const { outerWidth, outerHeight, screenX, screenY } = window; + const { width, height } = window.screen; + const w = Math.min(width, 400); + const h = Math.min(height, 400); + const ua = navigator.userAgent; + const isMobile = ua.includes("Mobile") || ua.includes("Tablet"); + const left = screenX + (screenX < 0 ? width : 0) + (outerWidth - w) / 2; + const top = screenY + (screenY < 0 ? height : 0) + (outerHeight - h) / 2.5; + let params = `left=${left},top=${top},width=${w},height=${h},scrollbars=1,toolbar=0,location=1`; + if (!isMobile) { + params = `${params},width=${w},height=${h}`; + } + return params; + } + + // If a page stores the window.FB reference of the shim, then we + // want to have it proxy calls to the real SDK once we've unshimmed. + function ensureProxiedToUnshimmed(obj) { + const shim = {}; + for (const key in obj) { + const value = obj[key]; + if (typeof value === "function") { + shim[key] = function () { + if (haveUnshimmed) { + return window.FB[key].apply(window.FB, arguments); + } + return value.apply(this, arguments); + }; + } else if (typeof value !== "object" || value === null) { + shim[key] = value; + } else { + shim[key] = ensureProxiedToUnshimmed(value); + } + } + return new Proxy(shim, { + get: (shimmed, key) => (haveUnshimmed ? window.FB : shimmed)[key], + }); + } + + window.FB = ensureProxiedToUnshimmed({ + api() { + loggedGraphApiCalls.push(arguments); + }, + AppEvents: { + activateApp() {}, + clearAppVersion() {}, + clearUserID() {}, + EventNames: { + ACHIEVED_LEVEL: "fb_mobile_level_achieved", + ADDED_PAYMENT_INFO: "fb_mobile_add_payment_info", + ADDED_TO_CART: "fb_mobile_add_to_cart", + ADDED_TO_WISHLIST: "fb_mobile_add_to_wishlist", + COMPLETED_REGISTRATION: "fb_mobile_complete_registration", + COMPLETED_TUTORIAL: "fb_mobile_tutorial_completion", + INITIATED_CHECKOUT: "fb_mobile_initiated_checkout", + PAGE_VIEW: "fb_page_view", + RATED: "fb_mobile_rate", + SEARCHED: "fb_mobile_search", + SPENT_CREDITS: "fb_mobile_spent_credits", + UNLOCKED_ACHIEVEMENT: "fb_mobile_achievement_unlocked", + VIEWED_CONTENT: "fb_mobile_content_view", + }, + getAppVersion: () => "", + getUserID: () => "", + logEvent() {}, + logPageView() {}, + logPurchase() {}, + ParameterNames: { + APP_USER_ID: "_app_user_id", + APP_VERSION: "_appVersion", + CONTENT_ID: "fb_content_id", + CONTENT_TYPE: "fb_content_type", + CURRENCY: "fb_currency", + DESCRIPTION: "fb_description", + LEVEL: "fb_level", + MAX_RATING_VALUE: "fb_max_rating_value", + NUM_ITEMS: "fb_num_items", + PAYMENT_INFO_AVAILABLE: "fb_payment_info_available", + REGISTRATION_METHOD: "fb_registration_method", + SEARCH_STRING: "fb_search_string", + SUCCESS: "fb_success", + }, + setAppVersion() {}, + setUserID() {}, + updateUserProperties() {}, + }, + Canvas: { + getHash: () => "", + getPageInfo(cb) { + cb?.call(this, { + clientHeight: 1, + clientWidth: 1, + offsetLeft: 0, + offsetTop: 0, + scrollLeft: 0, + scrollTop: 0, + }); + }, + Plugin: { + hidePluginElement() {}, + showPluginElement() {}, + }, + Prefetcher: { + COLLECT_AUTOMATIC: 0, + COLLECT_MANUAL: 1, + addStaticResource() {}, + setCollectionMode() {}, + }, + scrollTo() {}, + setAutoGrow() {}, + setDoneLoading() {}, + setHash() {}, + setSize() {}, + setUrlHandler() {}, + startTimer() {}, + stopTimer() {}, + }, + Event: { + subscribe(e, f) { + if (!eventHandlers.has(e)) { + eventHandlers.set(e, new Set()); + } + eventHandlers.get(e).add(f); + }, + unsubscribe(e, f) { + eventHandlers.get(e)?.delete(f); + }, + }, + frictionless: { + init() {}, + isAllowed: () => false, + }, + gamingservices: { + friendFinder() {}, + uploadImageToMediaLibrary() {}, + }, + getAccessToken: () => null, + getAuthResponse() { + return { status: "" }; + }, + getLoginStatus(cb) { + cb?.call(this, { status: "unknown" }); + }, + getUserID() {}, + init(_initInfo) { + initInfo = _initInfo; // in case the site is not using fbAsyncInit + }, + login(cb, opts) { + // We have to load Facebook's script, and then wait for it to call + // window.open. By that time, the popup blocker will likely trigger. + // So we open a popup now with about:blank, and then make sure FB + // will re-use that same popup later. + if (needPopup) { + activePopup = window.open("about:blank", popupName, buildPopupParams()); + } + allowFacebookSDK(() => { + activePopup = undefined; + function runPostLoginCallbacks() { + try { + cb?.apply(this, arguments); + } catch (e) { + console.error(e); + } + if (activeOnloginAttribute) { + setTimeout(activeOnloginAttribute, 1); + activeOnloginAttribute = undefined; + } + } + window.FB.login(runPostLoginCallbacks, opts); + }).catch(() => { + activePopup = undefined; + activeOnloginAttribute = undefined; + try { + cb?.({}); + } catch (e) { + console.error(e); + } + }); + }, + logout(cb) { + cb?.call(this); + }, + ui(params, fn) { + if (params.method === "permissions.oauth") { + window.FB.login(fn, params); + } + }, + XFBML: { + parse(node, cb) { + node = node || document; + node.querySelectorAll(".fb-login-button").forEach(makeLoginPlaceholder); + node.querySelectorAll(".fb-video").forEach(makeVideoPlaceholder); + try { + cb?.call(this); + } catch (e) { + console.error(e); + } + }, + }, + }); + + window.FB.XFBML.parse(); + + window?.fbAsyncInit?.(); +} diff --git a/browser/extensions/webcompat/shims/facebook.svg b/browser/extensions/webcompat/shims/facebook.svg new file mode 100644 index 0000000000..df63700a9e --- /dev/null +++ b/browser/extensions/webcompat/shims/facebook.svg @@ -0,0 +1,3 @@ +<!-- copyright is dedicated to the Public Domain. + https://en.wikipedia.org/wiki/File:Facebook_f_logo_(2019).svg --> +<svg xmlns="http://www.w3.org/2000/svg" width="1365.12" height="1365.12" viewBox="0 0 14222 14222"><circle cx="7111" cy="7112" r="7111" fill="#fff"/><path d="M9879 9168l315-2056H8222V5778c0-562 275-1111 1159-1111h897V2917s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9168z" fill="#1977f3"/></svg> diff --git a/browser/extensions/webcompat/shims/fastclick.js b/browser/extensions/webcompat/shims/fastclick.js new file mode 100644 index 0000000000..ad6814c995 --- /dev/null +++ b/browser/extensions/webcompat/shims/fastclick.js @@ -0,0 +1,75 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1738220 - Shim Conversant FastClick + * + * Sites assuming FastClick will load can break if it is blocked. + * This shim mitigates that breakage. + */ + +// FastClick bundles nodeJS packages/core-js/internals/dom-iterables.js +// which is known to be needed by at least one site. +if (!HTMLCollection.prototype.forEach) { + const DOMIterables = [ + "CSSRuleList", + "CSSStyleDeclaration", + "CSSValueList", + "ClientRectList", + "DOMRectList", + "DOMStringList", + "DOMTokenList", + "DataTransferItemList", + "FileList", + "HTMLAllCollection", + "HTMLCollection", + "HTMLFormElement", + "HTMLSelectElement", + "MediaList", + "MimeTypeArray", + "NamedNodeMap", + "NodeList", + "PaintRequestList", + "Plugin", + "PluginArray", + "SVGLengthList", + "SVGNumberList", + "SVGPathSegList", + "SVGPointList", + "SVGStringList", + "SVGTransformList", + "SourceBufferList", + "StyleSheetList", + "TextTrackCueList", + "TextTrackList", + "TouchList", + ]; + + const forEach = Array.prototype.forEach; + + const handlePrototype = proto => { + if (!proto || proto.forEach === forEach) { + return; + } + try { + Object.defineProperty(proto, "forEach", { + enumerable: false, + get: () => forEach, + }); + } catch (_) { + proto.forEach = forEach; + } + }; + + for (const name of DOMIterables) { + handlePrototype(window[name]?.prototype); + } +} + +if (!window.conversant?.launch) { + const c = (window.conversant = window.conversant || {}); + c.launch = () => {}; +} diff --git a/browser/extensions/webcompat/shims/firebase.js b/browser/extensions/webcompat/shims/firebase.js new file mode 100644 index 0000000000..8ac049c5e4 --- /dev/null +++ b/browser/extensions/webcompat/shims/firebase.js @@ -0,0 +1,95 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1767407 - Shim Firebase + * + * Sites relying on firebase-messaging.js will break in Private + * browsing mode because it assumes that they require service + * workers and indexedDB, when they generally do not. + */ + +/* globals cloneInto */ + +(function () { + const win = window.wrappedJSObject; + const emptyObj = new win.Object(); + const emptyArr = new win.Array(); + const emptyMsg = cloneInto({ message: "" }, window); + const noOpFn = cloneInto(function () {}, window, { cloneFunctions: true }); + + if (!win.indexedDB) { + const idb = { + open: () => win.Promise.reject(emptyMsg), + }; + + Object.defineProperty(win, "indexedDB", { + value: cloneInto(idb, window, { cloneFunctions: true }), + }); + } + + // bug 1778993 + for (const name of [ + "IDBCursor", + "IDBDatabase", + "IDBIndex", + "IDBOpenDBRequest", + "IDBRequest", + "IDBTransaction", + ]) { + if (!win[name]) { + Object.defineProperty(win, name, { value: emptyObj }); + } + } + + if (!win.serviceWorker) { + const sw = { + addEventListener() {}, + getRegistrations: () => win.Promise.resolve(emptyArr), + register: () => win.Promise.reject(emptyMsg), + }; + + Object.defineProperty(navigator.wrappedJSObject, "serviceWorker", { + value: cloneInto(sw, window, { cloneFunctions: true }), + }); + + // bug 1779536 + Object.defineProperty(navigator.wrappedJSObject.serviceWorker, "ready", { + value: new win.Promise(noOpFn), + }); + } + + // bug 1750699 + if (!win.PushManager) { + Object.defineProperty(win, "PushManager", { value: emptyObj }); + } + + // bug 1750699 + if (!win.PushSubscription) { + const ps = { + prototype: { + getKey() {}, + }, + }; + + Object.defineProperty(win, "PushSubscription", { + value: cloneInto(ps, window, { cloneFunctions: true }), + }); + } + + // bug 1750699 + if (!win.ServiceWorkerRegistration) { + const swr = { + prototype: { + showNotification() {}, + }, + }; + + Object.defineProperty(win, "ServiceWorkerRegistration", { + value: cloneInto(swr, window, { cloneFunctions: true }), + }); + } +})(); diff --git a/browser/extensions/webcompat/shims/google-ads.js b/browser/extensions/webcompat/shims/google-ads.js new file mode 100644 index 0000000000..a432186f43 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-ads.js @@ -0,0 +1,77 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713726 - Shim Ads by Google + * + * Sites relying on window.adsbygoogle may encounter breakage if it is blocked. + * This shim provides a stub for that API to mitigate that breakage. + */ + +if (window.adsbygoogle?.loaded === undefined) { + window.adsbygoogle = { + loaded: true, + push() {}, + }; +} + +if (window.gapi?._pl === undefined) { + const stub = { + go() {}, + render: () => "", + }; + window.gapi = { + _pl: true, + additnow: stub, + autocomplete: stub, + backdrop: stub, + blogger: stub, + commentcount: stub, + comments: stub, + community: stub, + donation: stub, + family_creation: stub, + follow: stub, + hangout: stub, + health: stub, + interactivepost: stub, + load() {}, + logutil: { + enableDebugLogging() {}, + }, + page: stub, + partnersbadge: stub, + person: stub, + platform: { + go() {}, + }, + playemm: stub, + playreview: stub, + plus: stub, + plusone: stub, + post: stub, + profile: stub, + ratingbadge: stub, + recobar: stub, + savetoandroidpay: stub, + savetodrive: stub, + savetowallet: stub, + share: stub, + sharetoclassroom: stub, + shortlists: stub, + signin: stub, + signin2: stub, + surveyoptin: stub, + visibility: stub, + youtube: stub, + ytsubscribe: stub, + zoomableimage: stub, + }; +} + +for (const e of document.querySelectorAll("ins.adsbygoogle")) { + e.style.maxWidth = "0px"; +} diff --git a/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js b/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js new file mode 100644 index 0000000000..8809fca8ec --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js @@ -0,0 +1,187 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713687 - Shim Google Analytics and Tag Manager + * + * Sites often rely on the Google Analytics window object and will + * break if it fails to load or is blocked. This shim works around + * such breakage. + * + * Sites also often use the Google Optimizer (asynchide) code snippet, + * only for it to cause multi-second delays if Google Analytics does + * not load. This shim also avoids such delays. + * + * They also rely on Google Tag Manager, which often goes hand-in- + * hand with Analytics, but is not always blocked by anti-tracking + * lists. Handling both in the same shim handles both cases. + */ + +if (window[window.GoogleAnalyticsObject || "ga"]?.loaded === undefined) { + const DEFAULT_TRACKER_NAME = "t0"; + + const trackers = new Map(); + + const run = function (fn, ...args) { + if (typeof fn === "function") { + try { + fn(...args); + } catch (e) { + console.error(e); + } + } + }; + + const create = (id, cookie, name, opts) => { + id = id || opts?.trackerId; + if (!id) { + return undefined; + } + cookie = cookie || opts?.cookieDomain || "_ga"; + name = name || opts?.name || DEFAULT_TRACKER_NAME; + if (!trackers.has(name)) { + let props; + try { + props = new Map(Object.entries(opts)); + } catch (_) { + props = new Map(); + } + trackers.set(name, { + get(p) { + if (p === "name") { + return name; + } else if (p === "trackingId") { + return id; + } else if (p === "cookieDomain") { + return cookie; + } + return props.get(p); + }, + ma() {}, + requireSync() {}, + send() {}, + set(p, v) { + if (typeof p !== "object") { + p = Object.fromEntries([[p, v]]); + } + for (const k in p) { + props.set(k, p[k]); + if (k === "hitCallback") { + run(p[k]); + } + } + }, + }); + } + return trackers.get(name); + }; + + const cmdRE = /((?<name>.*?)\.)?((?<plugin>.*?):)?(?<method>.*)/; + + function ga(cmd, ...args) { + if (arguments.length === 1 && typeof cmd === "function") { + run(cmd, trackers.get(DEFAULT_TRACKER_NAME)); + return undefined; + } + + if (typeof cmd !== "string") { + return undefined; + } + + const groups = cmdRE.exec(cmd)?.groups; + if (!groups) { + console.error("Could not parse GA command", cmd); + return undefined; + } + + let { name, plugin, method } = groups; + + if (plugin) { + return undefined; + } + + if (cmd === "set") { + trackers.get(name)?.set(args[0], args[1]); + } + + if (method === "remove") { + trackers.delete(name); + return undefined; + } + + if (cmd === "send") { + run(args.at(-1)?.hitCallback); + return undefined; + } + + if (method === "create") { + let id, cookie, fields; + for (const param of args.slice(0, 4)) { + if (typeof param === "object") { + fields = param; + break; + } + if (id === undefined) { + id = param; + } else if (cookie === undefined) { + cookie = param; + } else { + name = param; + } + } + return create(id, cookie, name, fields); + } + + return undefined; + } + + Object.assign(ga, { + create: (a, b, c, d) => ga("create", a, b, c, d), + getAll: () => Array.from(trackers.values()), + getByName: name => trackers.get(name), + loaded: true, + remove: t => ga("remove", t), + }); + + // Process any GA command queue the site pre-declares (bug 1736850) + const q = window[window.GoogleAnalyticsObject || "ga"]?.q; + window[window.GoogleAnalyticsObject || "ga"] = ga; + + if (Array.isArray(q)) { + const push = o => { + ga(...o); + return true; + }; + q.push = push; + q.forEach(o => push(o)); + } + + // Also process the Google Tag Manager dataLayer (bug 1713688) + const dl = window.dataLayer; + + if (Array.isArray(dl) && !dl.find(e => e["gtm.start"])) { + const push = function (o) { + setTimeout(() => run(o?.eventCallback), 1); + return true; + }; + dl.push = push; + dl.forEach(o => push(o)); + } + + // Run dataLayer.hide.end to handle asynchide (bug 1628151) + run(window.dataLayer?.hide?.end); +} + +if (!window?.gaplugins?.Linker) { + window.gaplugins = window.gaplugins || {}; + window.gaplugins.Linker = class { + autoLink() {} + decorate(url) { + return url; + } + passthrough() {} + }; +} diff --git a/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js b/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js new file mode 100644 index 0000000000..60b49df120 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js @@ -0,0 +1,13 @@ +/* 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/. */ + +"use strict"; + +if (!window.gaplugins) { + window.gaplugins = {}; +} + +if (!window.gaplugins.EC) { + window.gaplugins.EC = () => {}; +} diff --git a/browser/extensions/webcompat/shims/google-analytics-legacy.js b/browser/extensions/webcompat/shims/google-analytics-legacy.js new file mode 100644 index 0000000000..da1a638e12 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics-legacy.js @@ -0,0 +1,137 @@ +/* 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/. */ + +// based on https://github.com/gorhill/uBlock/blob/6f49e079db0262e669b70f4169924f796ac8db7c/src/web_accessible_resources/google-analytics_ga.js + +"use strict"; + +if (!window._gaq) { + function noopfn() {} + + const gaq = { + Na: noopfn, + O: noopfn, + Sa: noopfn, + Ta: noopfn, + Va: noopfn, + _createAsyncTracker: noopfn, + _getAsyncTracker: noopfn, + _getPlugin: noopfn, + push: a => { + if (typeof a === "function") { + a(); + return; + } + if (!Array.isArray(a)) { + return; + } + if ( + typeof a[0] === "string" && + /(^|\.)_link$/.test(a[0]) && + typeof a[1] === "string" + ) { + window.location.assign(a[1]); + } + if ( + a[0] === "_set" && + a[1] === "hitCallback" && + typeof a[2] === "function" + ) { + a[2](); + } + }, + }; + + const tracker = { + _addIgnoredOrganic: noopfn, + _addIgnoredRef: noopfn, + _addItem: noopfn, + _addOrganic: noopfn, + _addTrans: noopfn, + _clearIgnoredOrganic: noopfn, + _clearIgnoredRef: noopfn, + _clearOrganic: noopfn, + _cookiePathCopy: noopfn, + _deleteCustomVar: noopfn, + _getName: noopfn, + _setAccount: noopfn, + _getAccount: noopfn, + _getClientInfo: noopfn, + _getDetectFlash: noopfn, + _getDetectTitle: noopfn, + _getLinkerUrl: a => a, + _getLocalGifPath: noopfn, + _getServiceMode: noopfn, + _getVersion: noopfn, + _getVisitorCustomVar: noopfn, + _initData: noopfn, + _link: noopfn, + _linkByPost: noopfn, + _setAllowAnchor: noopfn, + _setAllowHash: noopfn, + _setAllowLinker: noopfn, + _setCampContentKey: noopfn, + _setCampMediumKey: noopfn, + _setCampNameKey: noopfn, + _setCampNOKey: noopfn, + _setCampSourceKey: noopfn, + _setCampTermKey: noopfn, + _setCampaignCookieTimeout: noopfn, + _setCampaignTrack: noopfn, + _setClientInfo: noopfn, + _setCookiePath: noopfn, + _setCookiePersistence: noopfn, + _setCookieTimeout: noopfn, + _setCustomVar: noopfn, + _setDetectFlash: noopfn, + _setDetectTitle: noopfn, + _setDomainName: noopfn, + _setLocalGifPath: noopfn, + _setLocalRemoteServerMode: noopfn, + _setLocalServerMode: noopfn, + _setReferrerOverride: noopfn, + _setRemoteServerMode: noopfn, + _setSampleRate: noopfn, + _setSessionTimeout: noopfn, + _setSiteSpeedSampleRate: noopfn, + _setSessionCookieTimeout: noopfn, + _setVar: noopfn, + _setVisitorCookieTimeout: noopfn, + _trackEvent: noopfn, + _trackPageLoadTime: noopfn, + _trackPageview: noopfn, + _trackSocial: noopfn, + _trackTiming: noopfn, + _trackTrans: noopfn, + _visitCode: noopfn, + }; + + const gat = { + _anonymizeIP: noopfn, + _createTracker: noopfn, + _forceSSL: noopfn, + _getPlugin: noopfn, + _getTracker: () => tracker, + _getTrackerByName: () => tracker, + _getTrackers: noopfn, + aa: noopfn, + ab: noopfn, + hb: noopfn, + la: noopfn, + oa: noopfn, + pa: noopfn, + u: noopfn, + }; + + window._gat = gat; + + const aa = window._gaq || []; + if (Array.isArray(aa)) { + while (aa[0]) { + gaq.push(aa.shift()); + } + } + + window._gaq = gaq.qf = gaq; +} diff --git a/browser/extensions/webcompat/shims/google-ima.js b/browser/extensions/webcompat/shims/google-ima.js new file mode 100644 index 0000000000..1f5e56239d --- /dev/null +++ b/browser/extensions/webcompat/shims/google-ima.js @@ -0,0 +1,620 @@ +/* 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?.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 = undefined; + + 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(); + }; + })(); + + let ima = {}; + + class AdDisplayContainer { + destroy() {} + initialize() {} + } + + class ImaSdkSettings { + #c = true; + #f = {}; + #i = false; + #l = ""; + #p = ""; + #r = 0; + #t = ""; + #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) {} + } + ImaSdkSettings.CompanionBackfillMode = { + ALWAYS: "always", + ON_MASTER_AD: "on_master_ad", + }; + ImaSdkSettings.VpaidMode = { + DISABLED: 0, + ENABLED: 1, + INSECURE: 2, + }; + + class EventHandler { + #listeners = new Map(); + + _dispatch(e) { + const listeners = this.#listeners.get(e.type) || []; + for (const listener of Array.from(listeners)) { + try { + listener(e); + } catch (r) { + console.error(r); + } + } + } + + addEventListener(t, c) { + if (!this.#listeners.has(t)) { + this.#listeners.set(t, new Set()); + } + this.#listeners.get(t).add(c); + } + + removeEventListener(t, c) { + this.#listeners.get(t)?.delete(c); + } + } + + class AdsLoader extends EventHandler { + #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 { + #volume = 1; + 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) {} + 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_RESUME_REQUESTED, + AdEvent.Type.AD_BUFFERING, + AdEvent.Type.FIRST_QUARTILE, + AdEvent.Type.MIDPOINT, + AdEvent.Type.THIRD_QUARTILE, + AdEvent.Type.COMPLETE, + AdEvent.Type.ALL_ADS_COMPLETED, + ]) { + 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 { + _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 [""]; + } + 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 { + #errorCode = -1; + #message = ""; + #type = ""; + #vastErrorCode = -1; + constructor(type, code, vast, message) { + this.#errorCode = code; + this.#message = message; + this.#type = type; + this.#vastErrorCode = vast; + } + getErrorCode() { + return this.#errorCode; + } + getInnerError() {} + 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())) { + if (ctx.getPlayer()?.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 { + type = "adError"; + #error = ""; + constructor(error) { + 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() { + 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 ""; + } + getAdIsValue() { + 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", + }, + 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; +} diff --git a/browser/extensions/webcompat/shims/google-page-ad.js b/browser/extensions/webcompat/shims/google-page-ad.js new file mode 100644 index 0000000000..42f3a0fca5 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-page-ad.js @@ -0,0 +1,17 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713692 - Shim Google Page Ad conversion tracker + * + * This shim stubs out the simple API for converstion tracking with + * Google Page Ad, mitigating major breakage on pages which presume + * the API will always successfully load. + */ + +if (!window.google_trackConversion) { + window.google_trackConversion = () => {}; +} diff --git a/browser/extensions/webcompat/shims/google-publisher-tags.js b/browser/extensions/webcompat/shims/google-publisher-tags.js new file mode 100644 index 0000000000..e5174d3244 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-publisher-tags.js @@ -0,0 +1,509 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713685 - Shim Google Publisher Tags + * + * Many sites rely on googletag to place content or drive ad bidding, + * and will experience major breakage if it is blocked. This shim provides + * enough of the API's frame To mitigate much of that breakage. + */ + +if (window.googletag?.apiReady === undefined) { + const version = "2021050601"; + + const noopthisfn = function () { + return this; + }; + + const slots = new Map(); + const slotsById = new Map(); + const slotsPerPath = new Map(); + const slotCreatives = new Map(); + const usedCreatives = new Map(); + const fetchedSlots = new Set(); + const eventCallbacks = new Map(); + + const fireSlotEvent = (name, slot) => { + return new Promise(resolve => { + requestAnimationFrame(() => { + const size = [0, 0]; + for (const cb of eventCallbacks.get(name) || []) { + cb({ isEmpty: true, size, slot }); + } + resolve(); + }); + }); + }; + + const recreateIframeForSlot = slot => { + const eid = `google_ads_iframe_${slot.getId()}`; + document.getElementById(eid)?.remove(); + const node = document.getElementById(slot.getSlotElementId()); + if (node) { + const f = document.createElement("iframe"); + f.id = eid; + f.srcdoc = "<body></body>"; + f.style = + "position:absolute; width:0; height:0; left:0; right:0; z-index:-1; border:0"; + f.setAttribute("width", 0); + f.setAttribute("height", 0); + node.appendChild(f); + } + }; + + const emptySlotElement = slot => { + const node = document.getElementById(slot.getSlotElementId()); + while (node?.lastChild) { + node.lastChild.remove(); + } + }; + + const SizeMapping = class extends Array { + getCreatives() { + const { clientWidth, clientHeight } = document.documentElement; + for (const [size, creatives] of this) { + if (clientWidth >= size[0] && clientHeight >= size[1]) { + return creatives; + } + } + return []; + } + }; + + const fetchSlot = slot => { + if (!slot) { + return; + } + + const id = slot.getSlotElementId(); + + const node = document.getElementById(id); + if (!node) { + return; + } + + let creatives = slotCreatives.get(id); + if (creatives instanceof SizeMapping) { + creatives = creatives.getCreatives(); + } + + if (!creatives?.length) { + return; + } + + for (const creative of creatives) { + if (usedCreatives.has(creative)) { + return; + } + } + + const creative = creatives[0]; + usedCreatives.set(creative, slot); + fetchedSlots.add(id); + }; + + const displaySlot = async slot => { + if (!slot) { + return; + } + + const id = slot.getSlotElementId(); + if (!document.getElementById(id)) { + return; + } + + if (!fetchedSlots.has(id)) { + fetchSlot(slot); + } + + const parent = document.getElementById(id); + if (parent) { + parent.appendChild(document.createElement("div")); + } + + emptySlotElement(slot); + recreateIframeForSlot(slot); + await fireSlotEvent("slotRenderEnded", slot); + await fireSlotEvent("slotRequested", slot); + await fireSlotEvent("slotResponseReceived", slot); + await fireSlotEvent("slotOnload", slot); + await fireSlotEvent("impressionViewable", slot); + }; + + const addEventListener = function (name, listener) { + if (!eventCallbacks.has(name)) { + eventCallbacks.set(name, new Set()); + } + eventCallbacks.get(name).add(listener); + return this; + }; + + const removeEventListener = function (name, listener) { + if (eventCallbacks.has(name)) { + return eventCallbacks.get(name).delete(listener); + } + return false; + }; + + const companionAdsService = { + addEventListener, + enable() {}, + fillSlot() {}, + getAttributeKeys: () => [], + getDisplayAdsCorrelator: () => "", + getName: () => "companion_ads", + getSlotIdMap: () => { + return {}; + }, + getSlots: () => [], + getVideoStreamCorrelator() {}, + isRoadblockingSupported: () => false, + isSlotAPersistentRoadblock: () => false, + notifyUnfilledSlots() {}, + onImplementationLoaded() {}, + refreshAllSlots() { + for (const slot of slotsById.values()) { + fetchSlot(slot); + displaySlot(slot); + } + }, + removeEventListener, + set() {}, + setRefreshUnfilledSlots() {}, + setVideoSession() {}, + slotRenderEnded() {}, + }; + + const contentService = { + addEventListener, + setContent() {}, + removeEventListener, + }; + + const getTargetingValue = v => { + if (typeof v === "string") { + return [v]; + } + try { + return [Array.prototype.flat.call(v)[0]]; + } catch (_) {} + return []; + }; + + const updateTargeting = (targeting, map) => { + if (typeof map === "object") { + const entries = Object.entries(map || {}); + for (const [k, v] of entries) { + targeting.set(k, getTargetingValue(v)); + } + } + }; + + const defineSlot = (adUnitPath, creatives, opt_div) => { + if (slotsById.has(opt_div)) { + document.getElementById(opt_div)?.remove(); + return slotsById.get(opt_div); + } + const attributes = new Map(); + const targeting = new Map(); + const exclusions = new Set(); + const response = { + advertiserId: undefined, + campaignId: undefined, + creativeId: undefined, + creativeTemplateId: undefined, + lineItemId: undefined, + }; + const sizes = [ + { + getHeight: () => 2, + getWidth: () => 2, + }, + ]; + const num = (slotsPerPath.get(adUnitPath) || 0) + 1; + slotsPerPath.set(adUnitPath, num); + const id = `${adUnitPath}_${num}`; + let clickUrl = ""; + let collapseEmptyDiv = null; + let services = new Set(); + const slot = { + addService(e) { + services.add(e); + return slot; + }, + clearCategoryExclusions: noopthisfn, + clearTargeting(k) { + if (k === undefined) { + targeting.clear(); + } else { + targeting.delete(k); + } + }, + defineSizeMapping(mapping) { + slotCreatives.set(opt_div, mapping); + return this; + }, + get: k => attributes.get(k), + getAdUnitPath: () => adUnitPath, + getAttributeKeys: () => Array.from(attributes.keys()), + getCategoryExclusions: () => Array.from(exclusions), + getClickUrl: () => clickUrl, + getCollapseEmptyDiv: () => collapseEmptyDiv, + getContentUrl: () => "", + getDivStartsCollapsed: () => null, + getDomId: () => opt_div, + getEscapedQemQueryId: () => "", + getFirstLook: () => 0, + getId: () => id, + getHtml: () => "", + getName: () => id, + getOutOfPage: () => false, + getResponseInformation: () => response, + getServices: () => Array.from(services), + getSizes: () => sizes, + getSlotElementId: () => opt_div, + getSlotId: () => slot, + getTargeting: k => targeting.get(k) || gTargeting.get(k) || [], + getTargetingKeys: () => + Array.from( + new Set(Array.of(...gTargeting.keys(), ...targeting.keys())) + ), + getTargetingMap: () => + Object.assign( + Object.fromEntries(gTargeting.entries()), + Object.fromEntries(targeting.entries()) + ), + set(k, v) { + attributes.set(k, v); + return slot; + }, + setCategoryExclusion(e) { + exclusions.add(e); + return slot; + }, + setClickUrl(u) { + clickUrl = u; + return slot; + }, + setCollapseEmptyDiv(v) { + collapseEmptyDiv = !!v; + return slot; + }, + setSafeFrameConfig: noopthisfn, + setTagForChildDirectedTreatment: noopthisfn, + setTargeting(k, v) { + targeting.set(k, getTargetingValue(v)); + return slot; + }, + toString: () => id, + updateTargetingFromMap(map) { + updateTargeting(targeting, map); + return slot; + }, + }; + slots.set(adUnitPath, slot); + slotsById.set(opt_div, slot); + slotCreatives.set(opt_div, creatives); + return slot; + }; + + let initialLoadDisabled = false; + + const gTargeting = new Map(); + const gAttributes = new Map(); + + let imaContent = { vid: "", cmsid: "" }; + let videoContent = { vid: "", cmsid: "" }; + + const pubadsService = { + addEventListener, + clear() {}, + clearCategoryExclusions: noopthisfn, + clearTagForChildDirectedTreatment: noopthisfn, + clearTargeting(k) { + if (k === undefined) { + gTargeting.clear(); + } else { + gTargeting.delete(k); + } + }, + collapseEmptyDivs() {}, + defineOutOfPagePassback: (a, o) => defineSlot(a, 0, o), + definePassback: (a, s, o) => defineSlot(a, s, o), + disableInitialLoad() { + initialLoadDisabled = true; + return this; + }, + display(adUnitPath, sizes, opt_div) { + const slot = defineSlot(adUnitPath, sizes, opt_div); + displaySlot(slot); + }, + enable() {}, + enableAsyncRendering() {}, + enableLazyLoad() {}, + enableSingleRequest() {}, + enableSyncRendering() {}, + enableVideoAds() {}, + forceExperiment() {}, + get: k => gAttributes.get(k), + getAttributeKeys: () => Array.from(gAttributes.keys()), + getCorrelator() {}, + getImaContent: () => imaContent, + getName: () => "publisher_ads", + getSlots: () => Array.from(slots.values()), + getSlotIdMap() { + const map = {}; + slots.values().forEach(s => { + map[s.getId()] = s; + }); + return map; + }, + getTagSessionCorrelator() {}, + getTargeting: k => gTargeting.get(k) || [], + getTargetingKeys: () => Array.from(gTargeting.keys()), + getTargetingMap: () => Object.fromEntries(gTargeting.entries()), + getVersion: () => version, + getVideoContent: () => videoContent, + isInitialLoadDisabled: () => initialLoadDisabled, + isSRA: () => false, + markAsAmp() {}, + refresh(slts) { + if (!slts) { + slts = slots.values(); + } else if (!Array.isArray(slts)) { + slts = [slts]; + } + for (const slot of slts) { + if (slot) { + try { + fetchSlot(slot); + displaySlot(slot); + } catch (e) { + console.error(e); + } + } + } + }, + removeEventListener, + set(k, v) { + gAttributes[k] = v; + return this; + }, + setCategoryExclusion: noopthisfn, + setCentering() {}, + setCookieOptions: noopthisfn, + setCorrelator: noopthisfn, + setForceSafeFrame: noopthisfn, + setImaContent(vid, cmsid) { + imaContent = { vid, cmsid }; + return this; + }, + setLocation: noopthisfn, + setPrivacySettings: noopthisfn, + setPublisherProvidedId: noopthisfn, + setRequestNonPersonalizedAds: noopthisfn, + setSafeFrameConfig: noopthisfn, + setTagForChildDirectedTreatment: noopthisfn, + setTagForUnderAgeOfConsent: noopthisfn, + setTargeting(k, v) { + gTargeting.set(k, getTargetingValue(v)); + return this; + }, + setVideoContent(vid, cmsid) { + videoContent = { vid, cmsid }; + return this; + }, + updateCorrelator() {}, + updateTargetingFromMap(map) { + updateTargeting(gTargeting, map); + return this; + }, + }; + + const SizeMappingBuilder = class { + #mapping; + constructor() { + this.#mapping = new SizeMapping(); + } + addSize(size, creatives) { + if ( + size !== "fluid" && + (!Array.isArray(size) || isNaN(size[0]) || isNaN(size[1])) + ) { + this.#mapping = null; + } else { + this.#mapping?.push([size, creatives]); + } + return this; + } + build() { + return this.#mapping; + } + }; + + let gt = window.googletag; + if (!gt) { + gt = window.googletag = {}; + } + + Object.assign(gt, { + apiReady: true, + companionAds: () => companionAdsService, + content: () => contentService, + defineOutOfPageSlot: (a, o) => defineSlot(a, 0, o), + defineSlot: (a, s, o) => defineSlot(a, s, o), + destroySlots() { + slots.clear(); + slotsById.clear(); + }, + disablePublisherConsole() {}, + display(arg) { + let id; + if (arg?.getSlotElementId) { + id = arg.getSlotElementId(); + } else if (arg?.nodeType) { + id = arg.id; + } else { + id = String(arg); + } + displaySlot(slotsById.get(id)); + }, + enableServices() {}, + enums: { + OutOfPageFormat: { + BOTTOM_ANCHOR: 3, + INTERSTITIAL: 5, + REWARDED: 4, + TOP_ANCHOR: 2, + }, + }, + getVersion: () => version, + pubads: () => pubadsService, + pubadsReady: true, + setAdIframeTitle() {}, + sizeMapping: () => new SizeMappingBuilder(), + }); + + const run = function (fn) { + if (typeof fn === "function") { + try { + fn.call(window); + } catch (e) { + console.error(e); + } + } + }; + + const cmds = gt.cmd || []; + const newCmd = []; + newCmd.push = run; + gt.cmd = newCmd; + + for (const cmd of cmds) { + run(cmd); + } +} diff --git a/browser/extensions/webcompat/shims/google-safeframe.html b/browser/extensions/webcompat/shims/google-safeframe.html new file mode 100644 index 0000000000..34bc1d242f --- /dev/null +++ b/browser/extensions/webcompat/shims/google-safeframe.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<!-- 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 1713691 - Shim Google SafeFrame + + Some sites will break if they cannot load a Google SafeFrame. This + shim provides a stand-in for the frame to mitigate that breakage. +--> +<html> + <head> + <meta charset="UTF-8" /> + <title>SafeFrame Container</title> + <script> + try { + const F = /^([^;]+);(\d+);([\s\S]*)$/.exec(window.name); + window.name = ""; + const P = window.document; + P.open("text/html", "replace"); + P.write(F[3].substr(0, +F[2])); + P.close(); + } catch (e) { + console.error(e); + } + </script> + </head> + <body></body> +</html> diff --git a/browser/extensions/webcompat/shims/history.js b/browser/extensions/webcompat/shims/history.js new file mode 100644 index 0000000000..6fbd1fdedb --- /dev/null +++ b/browser/extensions/webcompat/shims/history.js @@ -0,0 +1,54 @@ +/* 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/. */ + +"use strict"; + +/* + * Bug 1624853 - Shim Storage Access API on history.com + * + * history.com uses Adobe as a necessary third party to authenticating + * with a TV provider. In order to accomodate this, we grant storage access + * to the Adobe domain via the Storage Access API when the login or logout + * buttons are clicked, then forward the click to continue as normal. + */ + +console.warn( + `When using oauth, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1624853 for details.` +); + +// Third-party origin we need to request storage access for. +const STORAGE_ACCESS_ORIGIN = "https://sp.auth.adobe.com"; + +document.documentElement.addEventListener( + "click", + e => { + const { target, isTrusted } = e; + if (!isTrusted) { + return; + } + + const button = target.closest("a"); + if (!button) { + return; + } + + const buttonLink = button.href; + if (buttonLink?.startsWith("https://www.history.com/mvpd-auth")) { + button.disabled = true; + button.style.opacity = 0.5; + e.stopPropagation(); + e.preventDefault(); + document + .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN) + .then(() => { + target.click(); + }) + .catch(() => { + button.disabled = false; + button.style.opacity = 1.0; + }); + } + }, + true +); diff --git a/browser/extensions/webcompat/shims/iam.js b/browser/extensions/webcompat/shims/iam.js new file mode 100644 index 0000000000..84dee0e484 --- /dev/null +++ b/browser/extensions/webcompat/shims/iam.js @@ -0,0 +1,39 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1761774 - Shim INFOnline IAM tracker + * + * Sites using IAM can break if it is blocked. This shim mitigates that + * breakage by loading a stand-in module. + */ + +if (!window.iom?.c) { + window.iom = { + c: () => {}, + consent: () => {}, + count: () => {}, + ct: () => {}, + deloptout: () => {}, + doo: () => {}, + e: () => {}, + event: () => {}, + getInvitation: () => {}, + getPlus: () => {}, + gi: () => {}, + gp: () => {}, + h: () => {}, + hybrid: () => {}, + i: () => {}, + init: () => {}, + oi: () => {}, + optin: () => {}, + setMultiIdentifier: () => {}, + setoptout: () => {}, + smi: () => {}, + soo: () => {}, + }; +} diff --git a/browser/extensions/webcompat/shims/iaspet.js b/browser/extensions/webcompat/shims/iaspet.js new file mode 100644 index 0000000000..7e19dd52ad --- /dev/null +++ b/browser/extensions/webcompat/shims/iaspet.js @@ -0,0 +1,45 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713701 - Shim Integral Ad Science iaspet.js + * + * Some sites use iaspet to place content, often together with Google Publisher + * Tags. This shim prevents breakage when the script is blocked. + */ + +if (!window.__iasPET?.VERSION) { + let queue = window?.__iasPET?.queue; + if (!Array.isArray(queue)) { + queue = []; + } + + const response = JSON.stringify({ + brandSafety: {}, + slots: {}, + }); + + function run(cmd) { + try { + cmd?.dataHandler?.(response); + } catch (_) {} + } + + queue.push = run; + + window.__iasPET = { + VERSION: "1.16.18", + queue, + sessionId: "", + setTargetingForAppNexus() {}, + setTargetingForGPT() {}, + start() {}, + }; + + while (queue.length) { + run(queue.shift()); + } +} diff --git a/browser/extensions/webcompat/shims/instagram.js b/browser/extensions/webcompat/shims/instagram.js new file mode 100644 index 0000000000..5bf5014fdc --- /dev/null +++ b/browser/extensions/webcompat/shims/instagram.js @@ -0,0 +1,55 @@ +/* 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/. */ + +"use strict"; + +/* + * Bug 1804445 - instagram login broken with dFPI enabled + * + * Instagram login with Facebook account requires Facebook to have the storage + * access under Instagram. This shim adds a request for storage access for + * Facebook when the user tries to log in with a Facebook account. + */ + +console.warn( + `When logging in, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1804445 for details.` +); + +// Third-party origin we need to request storage access for. +const STORAGE_ACCESS_ORIGIN = "https://www.facebook.com"; + +document.documentElement.addEventListener( + "click", + e => { + const { target, isTrusted } = e; + if (!isTrusted) { + return; + } + const button = target.closest("button[type=button]"); + if (!button) { + return; + } + const form = target.closest("#loginForm"); + if (!form) { + return; + } + + console.warn( + "Calling the Storage Access API on behalf of " + STORAGE_ACCESS_ORIGIN + ); + button.disabled = true; + e.stopPropagation(); + e.preventDefault(); + document + .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN) + .then(() => { + button.disabled = false; + target.click(); + }) + .catch(() => { + button.disabled = false; + }); + }, + true +); diff --git a/browser/extensions/webcompat/shims/kinja.js b/browser/extensions/webcompat/shims/kinja.js new file mode 100644 index 0000000000..d30425b42d --- /dev/null +++ b/browser/extensions/webcompat/shims/kinja.js @@ -0,0 +1,44 @@ +/* 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/. */ + +/* globals exportFunction */ + +"use strict"; + +/** + * Kinja powered blogs rely on storage access to https://kinja.com to enable + * oauth with external providers. For dFPI, sites need to use the Storage Access + * API to gain first party storage access. This shim calls requestStorageAccess + * on behalf of the site when a user wants to log in via oauth. + */ + +// Third-party origin we need to request storage access for. +const STORAGE_ACCESS_ORIGIN = "https://kinja.com"; + +// Prefix of the path opened in a new window when users click the oauth login +// buttons. +const OAUTH_PATH_PREFIX = "/oauthlogin?provider="; + +console.warn( + `When using oauth, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1656171 for details.` +); + +// Overwrite the window.open method so we can detect oauth related popups. +const origOpen = window.wrappedJSObject.open; +Object.defineProperty(window.wrappedJSObject, "open", { + value: exportFunction((url, ...args) => { + // Filter oauth popups. + if (!url.startsWith(OAUTH_PATH_PREFIX)) { + return origOpen(url, ...args); + } + // Request storage access for Kinja. + document.requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN).then(() => { + origOpen(url, ...args); + }); + // We don't have the window object yet which window.open returns, since the + // sign-in flow is dependent on the async storage access request. This isn't + // a problem as long as the website does not consume it. + return null; + }, window), +}); diff --git a/browser/extensions/webcompat/shims/live-test-shim.js b/browser/extensions/webcompat/shims/live-test-shim.js new file mode 100644 index 0000000000..45ab9ba48b --- /dev/null +++ b/browser/extensions/webcompat/shims/live-test-shim.js @@ -0,0 +1,82 @@ +/* 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/. */ + +"use strict"; + +/* globals browser */ + +if (!window.LiveTestShimPromise) { + const originalUrl = document.currentScript.src; + + const shimId = "LiveTestShim"; + + const sendMessageToAddon = (function () { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function (message) { + const messageId = + Math.random().toString(36).substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + async function go(options) { + try { + const o = document.getElementById("shims"); + const cl = o.classList; + cl.remove("red"); + cl.add("green"); + o.innerText = JSON.stringify(options || ""); + } catch (_) {} + + if (window !== top) { + return; + } + + await sendMessageToAddon("optIn"); + + const s = document.createElement("script"); + s.src = originalUrl; + document.head.appendChild(s); + } + + window[`${shimId}Promise`] = sendMessageToAddon("getOptions").then( + options => { + if (document.readyState !== "loading") { + go(options); + } else { + window.addEventListener("DOMContentLoaded", () => { + go(options); + }); + } + } + ); +} diff --git a/browser/extensions/webcompat/shims/maxmind-geoip.js b/browser/extensions/webcompat/shims/maxmind-geoip.js new file mode 100644 index 0000000000..e5eb1e45a3 --- /dev/null +++ b/browser/extensions/webcompat/shims/maxmind-geoip.js @@ -0,0 +1,69 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1754389 - Shim Maxmind GeoIP library + * + * Some sites rely on Maxmind's GeoIP library which gets blocked by ETP's + * fingerprinter blocking. With the library window global not being defined + * functionality may break or the site does not render at all. This shim + * has it return the United States as the location for all users. + */ + +if (!window.geoip2) { + const continent = { + code: "NA", + geoname_id: 6255149, + names: { + de: "Nordamerika", + en: "North America", + es: "Norteamérica", + fr: "Amérique du Nord", + ja: "北アメリカ", + "pt-BR": "América do Norte", + ru: "Северная Америка", + "zh-CN": "北美洲", + }, + }; + + const country = { + geoname_id: 6252001, + iso_code: "US", + names: { + de: "USA", + en: "United States", + es: "Estados Unidos", + fr: "États-Unis", + ja: "アメリカ合衆国", + "pt-BR": "Estados Unidos", + ru: "США", + "zh-CN": "美国", + }, + }; + + const city = { + names: { + en: "", + }, + }; + + const callback = onSuccess => { + requestAnimationFrame(() => { + onSuccess({ + city, + continent, + country, + registered_country: country, + }); + }); + }; + + window.geoip2 = { + country: callback, + city: callback, + insights: callback, + }; +} diff --git a/browser/extensions/webcompat/shims/microsoftLogin.js b/browser/extensions/webcompat/shims/microsoftLogin.js new file mode 100644 index 0000000000..ebbfb2fbff --- /dev/null +++ b/browser/extensions/webcompat/shims/microsoftLogin.js @@ -0,0 +1,29 @@ +/* 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/. */ + +const SANDBOX_ATTR = "allow-storage-access-by-user-activation"; + +console.warn( + "Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1638383 for details." +); + +// Watches for MS auth iframes and adds missing sandbox attribute. The attribute +// is required so the third-party iframe can gain access to its first party +// storage via the Storage Access API. +function init() { + const observer = new MutationObserver(() => { + document.body + .querySelectorAll("iframe[id^='msalRenewFrame'][sandbox]") + .forEach(frame => { + frame.sandbox.add(SANDBOX_ATTR); + }); + }); + + observer.observe(document.body, { + attributes: true, + subtree: false, + childList: true, + }); +} +window.addEventListener("DOMContentLoaded", init); diff --git a/browser/extensions/webcompat/shims/microsoftVirtualAssistant.js b/browser/extensions/webcompat/shims/microsoftVirtualAssistant.js new file mode 100644 index 0000000000..4b4493750c --- /dev/null +++ b/browser/extensions/webcompat/shims/microsoftVirtualAssistant.js @@ -0,0 +1,46 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1801277 - Shim Microsoft virtual assistant. + * + * The microsoft virtual assistant will break when accessing the indexedDB that + * will throw a security error because the virtual assistant is under a + * third-party tracking domain 'liveperson.net'. The shim replaces the indexedDB + * with a fake interface that won't throw an error. + */ + +/* globals cloneInto */ + +(function () { + const win = window.wrappedJSObject; + + try { + // We only replace the indexedDB when liveperson.net is loaded in a + // third-party context. Note that this is not strictly correct because + // this is a cross-origin check but not a third-party check. + if (win.parent == win || win.location.origin == win.top.location.origin) { + return; + } + } catch (e) { + // If we get a security error when accessing the top-level origin, this + // shows that the window is in a cross-origin context. In this case, we can + // proceed to apply the shim. + if (e.name != "SecurityError") { + throw e; + } + } + + const emptyMsg = cloneInto({ message: "" }, window); + + const idb = { + open: () => win.Promise.reject(emptyMsg), + }; + + Object.defineProperty(win, "indexedDB", { + value: cloneInto(idb, window, { cloneFunctions: true }), + }); +})(); diff --git a/browser/extensions/webcompat/shims/moat.js b/browser/extensions/webcompat/shims/moat.js new file mode 100644 index 0000000000..9957492684 --- /dev/null +++ b/browser/extensions/webcompat/shims/moat.js @@ -0,0 +1,46 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713704 - Shim Moat ad tracker + * + * Sites such as Forbes may gate content behind Moat ads, resulting in + * breakage like black boxes where videos should be placed. This shim + * helps mitigate that breakage by allowing the placement to succeed. + */ + +if (!window.moatPrebidAPI?.__A) { + const targeting = new Map(); + + const slotConfig = { + m_categories: ["moat_safe"], + m_data: "0", + m_safety: "safe", + }; + + window.moatPrebidApi = { + __A() {}, + disableLogging() {}, + enableLogging() {}, + getMoatTargetingForPage: () => slotConfig, + getMoatTargetingForSlot(slot) { + return targeting.get(slot?.getSlotElementId()); + }, + pageDataAvailable: () => true, + safetyDataAvailable: () => true, + setMoatTargetingForAllSlots() { + for (const slot of window.googletag.pubads().getSlots() || []) { + targeting.set(slot.getSlotElementId(), slot.getTargeting()); + } + }, + setMoatTargetingForSlot(slot) { + targeting.set(slot?.getSlotElementId(), slotConfig); + }, + slotDataAvailable() { + return window.googletag?.pubads().getSlots().length > 0; + }, + }; +} diff --git a/browser/extensions/webcompat/shims/mochitest-shim-1.js b/browser/extensions/webcompat/shims/mochitest-shim-1.js new file mode 100644 index 0000000000..d95059cf7a --- /dev/null +++ b/browser/extensions/webcompat/shims/mochitest-shim-1.js @@ -0,0 +1,87 @@ +/* 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/. */ + +"use strict"; + +/* globals browser */ + +if (!window.MochitestShimPromise) { + const originalUrl = document.currentScript.src; + + const shimId = "MochitestShim"; + + const sendMessageToAddon = (function () { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function (message) { + const messageId = + Math.random().toString(36).substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + async function go(options) { + try { + const o = document.getElementById("shims"); + const cl = o.classList; + cl.remove("red"); + cl.add("green"); + o.innerText = JSON.stringify(options || ""); + } catch (_) {} + + window.shimPromiseResolve("shimmed"); + + if (window !== top) { + window.optInPromiseResolve(false); + return; + } + + await sendMessageToAddon("optIn"); + + window.doingOptIn = true; + const s = document.createElement("script"); + s.src = originalUrl; + s.onerror = () => window.optInPromiseResolve("error"); + document.head.appendChild(s); + } + + window[`${shimId}Promise`] = new Promise(resolve => { + sendMessageToAddon("getOptions").then(options => { + if (document.readyState !== "loading") { + resolve(go(options)); + } else { + window.addEventListener("DOMContentLoaded", () => { + resolve(go(options)); + }); + } + }); + }); +} diff --git a/browser/extensions/webcompat/shims/mochitest-shim-2.js b/browser/extensions/webcompat/shims/mochitest-shim-2.js new file mode 100644 index 0000000000..bc5536405e --- /dev/null +++ b/browser/extensions/webcompat/shims/mochitest-shim-2.js @@ -0,0 +1,85 @@ +/* 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/. */ + +"use strict"; + +/* globals browser */ + +if (!window.testPromise) { + const originalUrl = document.currentScript.src; + + const shimId = "MochitestShim2"; + + const sendMessageToAddon = (function () { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function (message) { + const messageId = + Math.random().toString(36).substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + async function go(options) { + try { + const o = document.getElementById("shims"); + const cl = o.classList; + cl.remove("red"); + cl.add("green"); + o.innerText = JSON.stringify(options || ""); + } catch (_) {} + + window.shimPromiseResolve("shimmed"); + + if (window !== top) { + window.optInPromiseResolve(false); + return; + } + + await sendMessageToAddon("optIn"); + + window.doingOptIn = true; + const s = document.createElement("script"); + s.src = originalUrl; + s.onerror = () => window.optInPromiseResolve("error"); + document.head.appendChild(s); + } + + sendMessageToAddon("getOptions").then(options => { + if (document.readyState !== "loading") { + go(options); + } else { + window.addEventListener("DOMContentLoaded", () => { + go(options); + }); + } + }); +} diff --git a/browser/extensions/webcompat/shims/mochitest-shim-3.js b/browser/extensions/webcompat/shims/mochitest-shim-3.js new file mode 100644 index 0000000000..dc0a8005f5 --- /dev/null +++ b/browser/extensions/webcompat/shims/mochitest-shim-3.js @@ -0,0 +1,7 @@ +/* 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/. */ + +"use strict"; + +window.shimPromiseResolve("shimmed"); diff --git a/browser/extensions/webcompat/shims/nielsen.js b/browser/extensions/webcompat/shims/nielsen.js new file mode 100644 index 0000000000..d34528a7c1 --- /dev/null +++ b/browser/extensions/webcompat/shims/nielsen.js @@ -0,0 +1,111 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1760754 - Shim Nielsen tracker + * + * Sites expecting the Nielsen tracker to load properly can break if it + * is blocked. This shim mitigates that breakage by loading a stand-in. + */ + +if (!window.nol_t) { + const cid = ""; + + let domain = ""; + let schemeHost = ""; + let scriptName = ""; + try { + const url = document?.currentScript?.src; + const { pathname, protocol, host } = new URL(url); + domain = host.split(".").slice(0, -2).join("."); + schemeHost = `${protocol}//${host}/`; + scriptName = pathname.split("/").pop(); + } catch (_) {} + + const NolTracker = class { + CONST = { + max_tags: 20, + }; + feat = {}; + globals = { + cid, + content: "0", + defaultApidFile: "config250", + defaultErrorParams: { + nol_vcid: "c00", + nol_clientid: "", + }, + domain, + fpidSfCodeList: [""], + init() {}, + tagCurrRetry: -1, + tagMaxRetry: 3, + wlCurrRetry: -1, + wlMaxRetry: 3, + }; + pmap = []; + pvar = { + cid, + content: "0", + cookies_enabled: "n", + server: domain, + }; + scriptName = [scriptName]; + version = "6.0.107"; + + addScript() {} + catchLinkOverlay() {} + clickEvent() {} + clickTrack() {} + do_sample() {} + downloadEvent() {} + eventTrack() {} + filter() {} + fireToUrl() {} + getSchemeHost() { + return schemeHost; + } + getVersion() {} + iframe() {} + in_sample() { + return true; + } + injectBsdk() {} + invite() {} + linkTrack() {} + mergeFeatures() {} + pageEvent() {} + pause() {} + populateWhitelist() {} + post() {} + postClickTrack() {} + postData() {} + postEvent() {} + postEventTrack() {} + postLinkTrack() {} + prefix() { + return ""; + } + processDdrsSvc() {} + random() {} + record() { + return this; + } + regLinkOverlay() {} + regListen() {} + retrieveCiFileViaCors() {} + sectionEvent() {} + sendALink() {} + sendForm() {} + sendIt() {} + slideEvent() {} + whitelistAssigned() {} + }; + + window.nol_t = () => { + return new NolTracker(); + }; +} diff --git a/browser/extensions/webcompat/shims/optimizely.js b/browser/extensions/webcompat/shims/optimizely.js new file mode 100644 index 0000000000..dcda87421d --- /dev/null +++ b/browser/extensions/webcompat/shims/optimizely.js @@ -0,0 +1,205 @@ +/* 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 1714431 - Shim Optimizely + * + * This shim stubs out window.optimizely for those sites which + * break when it is not successfully loaded. + */ + +if (!window.optimizely?.state) { + const behavior = { + query: () => [], + }; + + const dcp = { + getAttributeValue() {}, + waitForAttributeValue: () => Promise.resolve(), + }; + + const data = { + accountId: "", + audiences: {}, + campaigns: {}, + clientName: "js", + clientVersion: "0.166.0", + dcpServiceId: null, + events: {}, + experiments: {}, + groups: {}, + pages: {}, + projectId: "", + revision: "", + snippetId: null, + variations: {}, + }; + + const activationId = String(Date.now()); + + const state = { + getActivationId() { + return activationId; + }, + getActiveExperimentIds() { + return []; + }, + getCampaignStateLists() { + return {}; + }, + getCampaignStates() { + return {}; + }, + getDecisionObject() { + return null; + }, + getDecisionString() { + return null; + }, + getExperimentStates() { + return {}; + }, + getPageStates() { + return {}; + }, + getRedirectInfo() { + return null; + }, + getVariationMap() { + return {}; + }, + isGlobalHoldback() { + return false; + }, + }; + + const poll = (fn, to) => { + setInterval(() => { + try { + fn(); + } catch (_) {} + }, to); + }; + + const waitUntil = test => { + let interval, resolve; + function check() { + try { + if (test()) { + clearInterval(interval); + resolve?.(); + return true; + } + } catch (_) {} + return false; + } + return new Promise(r => { + resolve = r; + if (check()) { + resolve(); + return; + } + interval = setInterval(check, 250); + }); + }; + + const waitForElement = sel => { + return waitUntil(() => { + document.querySelector(sel); + }); + }; + + const observeSelector = (sel, fn, opts) => { + let interval; + const observed = new Set(); + function check() { + try { + for (const e of document.querySelectorAll(sel)) { + if (observed.has(e)) { + continue; + } + observed.add(e); + try { + fn(e); + } catch (_) {} + if (opts.once) { + clearInterval(interval); + } + } + } catch (_) {} + } + interval = setInterval(check, 250); + const timeout = { opts }; + if (timeout) { + setTimeout(() => { + clearInterval(interval); + }, timeout); + } + }; + + const utils = { + Promise: window.Promise, + observeSelector, + poll, + waitForElement, + waitUntil, + }; + + const visitorId = { + randomId: "", + }; + + let browserVersion = ""; + try { + browserVersion = navigator.userAgent.match(/rv:(.*)\)/)[1]; + } catch (_) {} + + const visitor = { + browserId: "ff", + browserVersion, + currentTimestamp: Date.now(), + custom: {}, + customBehavior: {}, + device: "desktop", + device_type: "desktop_laptop", + events: [], + first_session: true, + offset: 240, + referrer: null, + source_type: "direct", + visitorId, + }; + + window.optimizely = { + data: { + note: "Obsolete, use optimizely.get('data') instead", + }, + get(e) { + switch (e) { + case "behavior": + return behavior; + case "data": + return data; + case "dcp": + return dcp; + case "jquery": + throw new Error("jQuery not implemented"); + case "session": + return undefined; + case "state": + return state; + case "utils": + return utils; + case "visitor": + return visitor; + case "visitor_id": + return visitorId; + } + return undefined; + }, + initialized: true, + push() {}, + state: {}, + }; +} diff --git a/browser/extensions/webcompat/shims/play.svg b/browser/extensions/webcompat/shims/play.svg new file mode 100644 index 0000000000..df5bbcb4f1 --- /dev/null +++ b/browser/extensions/webcompat/shims/play.svg @@ -0,0 +1,7 @@ +<!-- 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/. + source: https://searchfox.org/mozilla-central/source/devtools/client/themes/images/play.svg --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <path fill="#fff" d="M20.436 11.37L5.904 2.116c-.23-.147-.523-.158-.762-.024-.24.132-.39.384-.39.657v18.5c0 .273.15.525.39.657.112.063.236.093.36.093.14 0 .28-.04.402-.117l14.53-9.248c.218-.138.35-.376.35-.633 0-.256-.132-.495-.348-.633z"/> +</svg> diff --git a/browser/extensions/webcompat/shims/private-browsing-web-api-fixes.js b/browser/extensions/webcompat/shims/private-browsing-web-api-fixes.js new file mode 100644 index 0000000000..bc45aeda26 --- /dev/null +++ b/browser/extensions/webcompat/shims/private-browsing-web-api-fixes.js @@ -0,0 +1,17 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1714354 - Fix for site issues with web APIs in private browsing + * + * Some sites expect specific DOM APIs to work in specific ways, which + * is not always true, such as in private browsing mode. We work around + * related breakage by undefining those APIs entirely in private + * browsing mode for those sites. + */ + +delete window.wrappedJSObject.caches; +delete window.wrappedJSObject.indexedDB; diff --git a/browser/extensions/webcompat/shims/rambler-authenticator.js b/browser/extensions/webcompat/shims/rambler-authenticator.js new file mode 100644 index 0000000000..1fe074b660 --- /dev/null +++ b/browser/extensions/webcompat/shims/rambler-authenticator.js @@ -0,0 +1,84 @@ +/* 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/. */ + +"use strict"; + +if (!window.ramblerIdHelper) { + const originalScript = document.currentScript.src; + + const sendMessageToAddon = (function () { + const shimId = "Rambler"; + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function (message) { + const messageId = + Math.random().toString(36).substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + const ramblerIdHelper = { + getProfileInfo: (successCallback, errorCallback) => { + successCallback({}); + }, + openAuth: () => { + sendMessageToAddon("optIn").then(function () { + const openAuthArgs = arguments; + window.ramblerIdHelper = undefined; + const s = document.createElement("script"); + s.src = originalScript; + document.head.appendChild(s); + s.addEventListener("load", () => { + const helper = window.ramblerIdHelper; + for (const { fn, args } of callLog) { + helper[fn].apply(helper, args); + } + helper.openAuth.apply(helper, openAuthArgs); + }); + }); + }, + }; + + const callLog = []; + function addLoggedCall(fn) { + ramblerIdHelper[fn] = () => { + callLog.push({ fn, args: arguments }); + }; + } + + addLoggedCall("registerOnFrameCloseCallback"); + addLoggedCall("registerOnFrameRedirect"); + addLoggedCall("registerOnPossibleLoginCallback"); + addLoggedCall("registerOnPossibleLogoutCallback"); + addLoggedCall("registerOnPossibleOauthLoginCallback"); + + window.ramblerIdHelper = ramblerIdHelper; +} diff --git a/browser/extensions/webcompat/shims/rich-relevance.js b/browser/extensions/webcompat/shims/rich-relevance.js new file mode 100644 index 0000000000..aea85c030a --- /dev/null +++ b/browser/extensions/webcompat/shims/rich-relevance.js @@ -0,0 +1,288 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713725 - Shim Rich Relevance personalized shopping + * + * Sites may expect the Rich Relevance personalized shopping API to load, + * breaking if it is blocked. This shim attempts to limit breakage on those + * site to just the personalized shopping aspects, by stubbing out the APIs. + */ + +if (!window.r3_common) { + const jsonCallback = window.RR?.jsonCallback; + const defaultCallback = window.RR?.defaultCallback; + + const getRandomString = (l = 66) => { + const v = crypto.getRandomValues(new Uint8Array(l)); + const s = Array.from(v, c => c.toString(16)).join(""); + return s.slice(0, l); + }; + + const call = (fn, ...args) => { + if (typeof fn === "function") { + try { + fn(...args); + } catch (e) { + console.error(e); + } + } + }; + + class r3_generic { + type = "GENERIC"; + createScript() {} + destroy() {} + } + + class r3_addtocart extends r3_generic { + type = "ADDTOCART"; + addItemIdToCart() {} + } + + class r3_addtoregistry extends r3_generic { + type = "ADDTOREGISTRY"; + addItemIdCentsQuantity() {} + } + + class r3_brand extends r3_generic { + type = "BRAND"; + } + + class r3_cart extends r3_generic { + type = "CART"; + addItemId() {} + addItemIdCentsQuantity() {} + addItemIdDollarsAndCentsQuantity() {} + addItemIdPriceQuantity() {} + } + + class r3_category extends r3_generic { + type = "CATEGORY"; + addItemId() {} + setId() {} + setName() {} + setParentId() {} + setTopName() {} + } + + class r3_common extends r3_generic { + type = "COMMON"; + baseUrl = "https://recs.richrelevance.com/rrserver/"; + devFlags = {}; + jsFileName = "p13n_generated.js"; + RICHSORT = { + paginate() {}, + filterPrice() {}, + filterAttribute() {}, + }; + addCategoryHintId() {} + addClickthruParams() {} + addContext() {} + addFilter() {} + addFilterBrand() {} + addFilterCategory() {} + addItemId() {} + addItemIdToCart() {} + addPlacementType() {} + addRefinement() {} + addSearchTerm() {} + addSegment() {} + blockItemId() {} + enableCfrad() {} + enableRad() {} + forceDebugMode() {} + forceDevMode() {} + forceDisplayMode() {} + forceLocale() {} + initFromParams() {} + setApiKey() {} + setBaseUrl() {} + setCartValue() {} + setChannel() {} + setClickthruServer() {} + setCurrency() {} + setDeviceId() {} + setFilterBrandsIncludeMatchingElements() {} + setForcedTreatment() {} + setImageServer() {} + setLanguage() {} + setMVTForcedTreatment() {} + setNoCookieMode() {} + setPageBrand() {} + setPrivateMode() {} + setRefinementFallback() {} + setRegionId() {} + setRegistryId() {} + setRegistryType() {} + setSessionId() {} + setUserId() {} + useDummyData() {} + } + + class r3_error extends r3_generic { + type = "ERROR"; + } + + class r3_home extends r3_generic { + type = "HOME"; + } + + class r3_item extends r3_generic { + type = "ITEM"; + addAttribute() {} + addCategory() {} + addCategoryId() {} + setBrand() {} + setEndDate() {} + setId() {} + setImageId() {} + setLinkId() {} + setName() {} + setPrice() {} + setRating() {} + setRecommendable() {} + setReleaseDate() {} + setSalePrice() {} + } + + class r3_personal extends r3_generic { + type = "PERSONAL"; + } + + class r3_purchased extends r3_generic { + type = "PURCHASED"; + addItemId() {} + addItemIdCentsQuantity() {} + addItemIdDollarsAndCentsQuantity() {} + addItemIdPriceQuantity() {} + setOrderNumber() {} + setPromotionCode() {} + setShippingCost() {} + setTaxes() {} + setTotalPrice() {} + } + + class r3_search extends r3_generic { + type = "SEARCH"; + addItemId() {} + setTerms() {} + } + + class r3_wishlist extends r3_generic { + type = "WISHLIST"; + addItemId() {} + } + + const RR = { + add() {}, + addItemId() {}, + addItemIdCentsQuantity() {}, + addItemIdDollarsAndCentsQuantity() {}, + addItemIdPriceQuantity() {}, + addItemIdToCart() {}, + addObject() {}, + addSearchTerm() {}, + c() {}, + charset: "UTF-8", + checkParamCookieValue: () => null, + d: document, + data: { + JSON: { + placements: [], + }, + }, + debugWindow() {}, + set defaultCallback(fn) { + call(fn); + }, + fixName: n => n, + genericAddItemPriceQuantity() {}, + get() {}, + getDomElement(a) { + return typeof a === "string" && a ? document.querySelector(a) : null; + }, + id() {}, + insert() {}, + insertDynamicPlacement() {}, + isArray: a => Array.isArray(a), + js() {}, + set jsonCallback(fn) { + call(fn, {}); + }, + l: document.location.href, + lc() {}, + noCookieMode: false, + ol() {}, + onloadCalled: true, + pq() {}, + rcsCookieDefaultDuration: 364, + registerPageType() {}, + registeredPageTypes: { + ADDTOCART: r3_addtocart, + ADDTOREGISTRY: r3_addtoregistry, + BRAND: r3_brand, + CART: r3_cart, + CATEGORY: r3_category, + COMMON: r3_common, + ERROR: r3_error, + GENERIC: r3_generic, + HOME: r3_home, + ITEM: r3_item, + PERSONAL: r3_personal, + PURCHASED: r3_purchased, + SEARCH: r3_search, + WISHLIST: r3_wishlist, + }, + renderDynamicPlacements() {}, + set() {}, + setCharset() {}, + U: "undefined", + unregisterAllPageType() {}, + unregisterPageType() {}, + }; + + Object.assign(window, { + r3() {}, + r3_addtocart, + r3_addtoregistry, + r3_brand, + r3_cart, + r3_category, + r3_common, + r3_error, + r3_generic, + r3_home, + r3_item, + r3_personal, + r3_placement() {}, + r3_purchased, + r3_search, + r3_wishlist, + RR, + rr_addLoadEvent() {}, + rr_annotations_array: [undefined], + rr_call_after_flush() {}, + rr_create_script() {}, + rr_dynamic: { + placements: [], + }, + rr_flush() {}, + rr_flush_onload() {}, + rr_insert_placement() {}, + rr_onload_called: true, + rr_placement_place_holders: [], + rr_placements: [], + rr_recs: { + placements: [], + }, + rr_remote_data: getRandomString(), + rr_v: "1.2.6.20210212", + }); + + call(jsonCallback); + call(defaultCallback, {}); +} diff --git a/browser/extensions/webcompat/shims/spotify-embed.js b/browser/extensions/webcompat/shims/spotify-embed.js new file mode 100644 index 0000000000..62ad05b725 --- /dev/null +++ b/browser/extensions/webcompat/shims/spotify-embed.js @@ -0,0 +1,133 @@ +/* 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/. */ + +/* globals exportFunction */ + +"use strict"; + +/** + * Spotify embeds default to "track preview mode". They require first-party + * storage access in order to detect the login status and allow the user to play + * the whole song or add it to their library. + * Upon clicking the "play" button in the preview view this shim attempts to get + * storage access and on success, reloads the frame and plays the full track. + * This only works if the user is already logged in to Spotify in the + * first-party context. + */ + +const AUTOPLAY_FLAG = "shimPlayAfterStorageAccess"; +const SELECTOR_PREVIEW_PLAY = 'div[data-testid="preview-play-pause"] > button'; +const SELECTOR_FULL_PLAY = 'button[data-testid="play-pause-button"]'; + +/** + * Promise-wrapper around DOMContentLoaded event. + */ +function waitForDOMContentLoaded() { + return new Promise(resolve => { + window.addEventListener("DOMContentLoaded", resolve, { once: true }); + }); +} + +/** + * Listener for the preview playback button which requests storage access and + * reloads the page. + */ +function previewPlayButtonListener(event) { + const { target, isTrusted } = event; + if (!isTrusted) { + return; + } + + const button = target.closest("button"); + if (!button) { + return; + } + + // Filter for the preview playback button. This won't match the full + // playback button that is shown when the user is logged in. + if (!button.matches(SELECTOR_PREVIEW_PLAY)) { + return; + } + + // The storage access request below runs async so playback won't start + // immediately. Mitigate this UX issue by updating the clicked element's + // style so the user gets some immediate feedback. + button.style.opacity = 0.5; + event.stopPropagation(); + event.preventDefault(); + + console.debug("Requesting storage access.", location.origin); + document + .requestStorageAccess() + // When storage access is granted, reload the frame for the embedded + // player to detect the login state and give us full playback + // capabilities. + .then(() => { + // Use a flag to indicate that we want to click play after reload. + // This is so the user does not have to click play twice. + sessionStorage.setItem(AUTOPLAY_FLAG, "true"); + console.debug("Reloading after storage access grant."); + location.reload(); + }) + // If the user denies the storage access prompt we can't use the login + // state. Attempt start preview playback instead. + .catch(() => { + button.click(); + }) + // Reset button style for both success and error case. + .finally(() => { + button.style.opacity = 1.0; + }); +} + +/** + * Attempt to start (full) playback. Waits for the play button to appear and + * become ready. + */ +async function startFullPlayback() { + // Wait for DOMContentLoaded before looking for the playback button. + await waitForDOMContentLoaded(); + + let numTries = 0; + let intervalId = setInterval(() => { + try { + document.querySelector(SELECTOR_FULL_PLAY).click(); + clearInterval(intervalId); + console.debug("Clicked play after storage access grant."); + } catch (e) {} + numTries++; + + if (numTries >= 50) { + console.debug("Can not start playback. Giving up."); + clearInterval(intervalId); + } + }, 200); +} + +(async () => { + // Only run the shim for embedded iframes. + if (window.top == window) { + return; + } + + console.warn( + `When using the Spotify embedded player, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1792395 for details.` + ); + + // Already requested storage access before the reload, trigger playback. + if (sessionStorage.getItem(AUTOPLAY_FLAG) == "true") { + sessionStorage.removeItem(AUTOPLAY_FLAG); + + await startFullPlayback(); + return; + } + + // Wait for the user to click the preview play button. If the player has + // already loaded the full version, this method will do nothing. + document.documentElement.addEventListener( + "click", + previewPlayButtonListener, + { capture: true } + ); +})(); diff --git a/browser/extensions/webcompat/shims/tracking-pixel.png b/browser/extensions/webcompat/shims/tracking-pixel.png Binary files differnew file mode 100644 index 0000000000..52c591798e --- /dev/null +++ b/browser/extensions/webcompat/shims/tracking-pixel.png diff --git a/browser/extensions/webcompat/shims/vast2.xml b/browser/extensions/webcompat/shims/vast2.xml new file mode 100644 index 0000000000..3536ccfc0f --- /dev/null +++ b/browser/extensions/webcompat/shims/vast2.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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 1713693 - Shim Doubleclick + + Some sites rely on an XML VAST Ad response from Doubleclick, or will + break (showing black boxes instead of videos, etc). This shim mitigates + such breakage. +--> +<VAST version="2.0"></VAST> diff --git a/browser/extensions/webcompat/shims/vast3.xml b/browser/extensions/webcompat/shims/vast3.xml new file mode 100644 index 0000000000..ae03f0dc14 --- /dev/null +++ b/browser/extensions/webcompat/shims/vast3.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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 1713693 - Shim Doubleclick + + Some sites rely on an XML VAST Ad response from Doubleclick, or will + break (showing black boxes instead of videos, etc). This shim mitigates + such breakage. +--> +<VAST version="3.0"></VAST> diff --git a/browser/extensions/webcompat/shims/vidible.js b/browser/extensions/webcompat/shims/vidible.js new file mode 100644 index 0000000000..1d45bc0f7e --- /dev/null +++ b/browser/extensions/webcompat/shims/vidible.js @@ -0,0 +1,424 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1713710 - Shim Vidible video player + * + * Sites relying on Vidible's video player may experience broken videos if that + * script is blocked. This shim allows users to opt into viewing those videos + * regardless of any tracking consequences, by providing placeholders for each. + */ + +if (!window.vidible?.version) { + const PlayIconURL = "https://smartblock.firefox.etp/play.svg"; + + const originalScript = document.currentScript.src; + + const getGUID = () => { + const v = crypto.getRandomValues(new Uint8Array(20)); + return Array.from(v, c => c.toString(16)).join(""); + }; + + const sendMessageToAddon = (function () { + const shimId = "Vidible"; + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function (message) { + const messageId = getGUID(); + return new Promise(resolve => { + const payload = { message, messageId, shimId }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + const Shimmer = (function () { + // If a page might store references to an object before we replace it, + // ensure that it only receives proxies to that object created by + // `Shimmer.proxy(obj)`. Later when the unshimmed object is created, + // call `Shimmer.unshim(proxy, unshimmed)`. This way the references + // will automatically "become" the unshimmed object when appropriate. + + const shimmedObjects = new WeakMap(); + const unshimmedObjects = new Map(); + + function proxy(shim) { + if (shimmedObjects.has(shim)) { + return shimmedObjects.get(shim); + } + + const prox = new Proxy(shim, { + get: (target, k) => { + if (unshimmedObjects.has(prox)) { + return unshimmedObjects.get(prox)[k]; + } + return target[k]; + }, + apply: (target, thisArg, args) => { + if (unshimmedObjects.has(prox)) { + return unshimmedObjects.get(prox)(...args); + } + return target.apply(thisArg, args); + }, + construct: (target, args) => { + if (unshimmedObjects.has(prox)) { + return new unshimmedObjects.get(prox)(...args); + } + return new target(...args); + }, + }); + shimmedObjects.set(shim, prox); + shimmedObjects.set(prox, prox); + + for (const key in shim) { + const value = shim[key]; + if (typeof value === "function") { + shim[key] = function () { + const unshimmed = unshimmedObjects.get(prox); + if (unshimmed) { + return unshimmed[key].apply(unshimmed, arguments); + } + return value.apply(this, arguments); + }; + } else if (typeof value !== "object" || value === null) { + shim[key] = value; + } else { + shim[key] = Shimmer.proxy(value); + } + } + + return prox; + } + + function unshim(shim, unshimmed) { + unshimmedObjects.set(shim, unshimmed); + + for (const prop in shim) { + if (prop in unshimmed) { + const un = unshimmed[prop]; + if (typeof un === "object" && un !== null) { + unshim(shim[prop], un); + } + } else { + unshimmedObjects.set(shim[prop], undefined); + } + } + } + + return { proxy, unshim }; + })(); + + const extras = []; + const playersByNode = new WeakMap(); + const playerData = new Map(); + + const getJSONPVideoPlacements = () => { + return document.querySelectorAll( + `script[src*="delivery.vidible.tv/jsonp"]` + ); + }; + + const allowVidible = () => { + if (allowVidible.promise) { + return allowVidible.promise; + } + + const shim = window.vidible; + window.vidible = undefined; + + allowVidible.promise = sendMessageToAddon("optIn") + .then(() => { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = originalScript; + script.addEventListener("load", () => { + Shimmer.unshim(shim, window.vidible); + + for (const args of extras) { + window.visible.registerExtra(...args); + } + + for (const jsonp of getJSONPVideoPlacements()) { + const { src } = jsonp; + const jscript = document.createElement("script"); + jscript.onload = resolve; + jscript.src = src; + jsonp.replaceWith(jscript); + } + + for (const [playerShim, data] of playerData.entries()) { + const { loadCalled, on, parent, placeholder, setup } = data; + + placeholder?.remove(); + + const player = window.vidible.player(parent); + Shimmer.unshim(playerShim, player); + + for (const [type, fns] of on.entries()) { + for (const fn of fns) { + try { + player.on(type, fn); + } catch (e) { + console.error(e); + } + } + } + + if (setup) { + player.setup(setup); + } + + if (loadCalled) { + player.load(); + } + } + + resolve(); + }); + + script.addEventListener("error", () => { + script.remove(); + reject(); + }); + + document.head.appendChild(script); + }); + }) + .catch(() => { + window.vidible = shim; + delete allowVidible.promise; + }); + + return allowVidible.promise; + }; + + const createVideoPlaceholder = (service, callback) => { + const placeholder = document.createElement("div"); + placeholder.style = ` + position: absolute; + width: 100%; + height: 100%; + min-width: 160px; + min-height: 100px; + top: 0px; + left: 0px; + background: #000; + color: #fff; + text-align: center; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + background-image: url(${PlayIconURL}); + background-position: 50% 47.5%; + background-repeat: no-repeat; + background-size: 25% 25%; + -moz-text-size-adjust: none; + -moz-user-select: none; + color: #fff; + align-items: center; + padding-top: 200px; + font-size: 14pt; + `; + placeholder.textContent = `Click to allow blocked ${service} content`; + placeholder.addEventListener("click", evt => { + evt.isTrusted && callback(); + }); + return placeholder; + }; + + const Player = function (parent) { + const existing = playersByNode.get(parent); + if (existing) { + return existing; + } + + const player = Shimmer.proxy(this); + playersByNode.set(parent, player); + + const placeholder = createVideoPlaceholder("Vidible", allowVidible); + parent.parentNode.insertBefore(placeholder, parent); + + playerData.set(player, { + on: new Map(), + parent, + placeholder, + }); + return player; + }; + + const changeData = function (fn) { + const data = playerData.get(this); + if (data) { + fn(data); + playerData.set(this, data); + } + }; + + Player.prototype = { + addEventListener() {}, + destroy() { + const { placeholder } = playerData.get(this); + placeholder?.remove(); + playerData.delete(this); + }, + dispatchEvent() {}, + getAdsPassedTime() {}, + getAllMacros() {}, + getCurrentTime() {}, + getDuration() {}, + getHeight() {}, + getPixelsLog() {}, + getPlayerContainer() {}, + getPlayerInfo() {}, + getPlayerStatus() {}, + getRequestsLog() {}, + getStripUrl() {}, + getVolume() {}, + getWidth() {}, + hidePlayReplayControls() {}, + isMuted() {}, + isPlaying() {}, + load() { + changeData(data => (data.loadCalled = true)); + }, + mute() {}, + on(type, fn) { + changeData(({ on }) => { + if (!on.has(type)) { + on.set(type, new Set()); + } + on.get(type).add(fn); + }); + }, + off(type, fn) { + changeData(({ on }) => { + on.get(type)?.delete(fn); + }); + }, + overrideMacro() {}, + pause() {}, + play() {}, + playVideoByIndex() {}, + removeEventListener() {}, + seekTo() {}, + sendBirthDate() {}, + sendKey() {}, + setup(s) { + changeData(data => (data.setup = s)); + return this; + }, + setVideosToPlay() {}, + setVolume() {}, + showPlayReplayControls() {}, + toggleFullscreen() {}, + toggleMute() {}, + togglePlay() {}, + updateBid() {}, + version() {}, + volume() {}, + }; + + const vidible = { + ADVERT_CLOSED: "advertClosed", + AD_END: "adend", + AD_META: "admeta", + AD_PAUSED: "adpaused", + AD_PLAY: "adplay", + AD_START: "adstart", + AD_TIMEUPDATE: "adtimeupdate", + AD_WAITING: "adwaiting", + AGE_GATE_DISPLAYED: "agegatedisplayed", + BID_UPDATED: "BidUpdated", + CAROUSEL_CLICK: "CarouselClick", + CONTEXT_ENDED: "contextended", + CONTEXT_STARTED: "contextstarted", + ENTER_FULLSCREEN: "playerenterfullscreen", + EXIT_FULLSCREEN: "playerexitfullscreen", + FALLBACK: "fallback", + FLOAT_END_ACTION: "floatended", + FLOAT_START_ACTION: "floatstarted", + HIDE_PLAY_REPLAY_BUTTON: "hideplayreplaybutton", + LIGHTBOX_ACTIVATED: "lightboxactivated", + LIGHTBOX_DEACTIVATED: "lightboxdeactivated", + MUTE: "Mute", + PLAYER_CONTROLS_STATE_CHANGE: "playercontrolsstatechaned", + PLAYER_DOCKED: "playerDocked", + PLAYER_ERROR: "playererror", + PLAYER_FLOATING: "playerFloating", + PLAYER_READY: "playerready", + PLAYER_RESIZE: "playerresize", + PLAYLIST_END: "playlistend", + SEEK_END: "SeekEnd", + SEEK_START: "SeekStart", + SHARE_SCREEN_CLOSED: "sharescreenclosed", + SHARE_SCREEN_OPENED: "sharescreenopened", + SHOW_PLAY_REPLAY_BUTTON: "showplayreplaybutton", + SUBTITLES_DISABLED: "subtitlesdisabled", + SUBTITLES_ENABLED: "subtitlesenabled", + SUBTITLES_READY: "subtitlesready", + UNMUTE: "Unmute", + VIDEO_DATA_LOADED: "videodataloaded", + VIDEO_END: "videoend", + VIDEO_META: "videometadata", + VIDEO_MODULE_CREATED: "videomodulecreated", + VIDEO_PAUSE: "videopause", + VIDEO_PLAY: "videoplay", + VIDEO_SEEKEND: "videoseekend", + VIDEO_SELECTED: "videoselected", + VIDEO_START: "videostart", + VIDEO_TIMEUPDATE: "videotimeupdate", + VIDEO_VOLUME_CHANGED: "videovolumechanged", + VOLUME: "Volume", + _getContexts: () => [], + "content.CLICK": "content.click", + "content.IMPRESSION": "content.impression", + "content.QUARTILE": "content.quartile", + "content.VIEW": "content.view", + createPlayer: parent => new Player(parent), + createPlayerAsync: parent => new Player(parent), + createVPAIDPlayer: parent => new Player(parent), + destroyAll() {}, + extension() {}, + getContext() {}, + player: parent => new Player(parent), + playerInceptionTime() { + return { undefined: 1620149827713 }; + }, + registerExtra(a, b, c) { + extras.push([a, b, c]); + }, + version: () => "21.1.313", + }; + + window.vidible = Shimmer.proxy(vidible); + + for (const jsonp of getJSONPVideoPlacements()) { + const player = new Player(jsonp); + const { placeholder } = playerData.get(player); + jsonp.parentNode.insertBefore(placeholder, jsonp); + } +} diff --git a/browser/extensions/webcompat/shims/vmad.xml b/browser/extensions/webcompat/shims/vmad.xml new file mode 100644 index 0000000000..5bb9a5a5d5 --- /dev/null +++ b/browser/extensions/webcompat/shims/vmad.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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 1713693 - Shim Doubleclick + + Some sites rely on an XML VMAD Ad response from Doubleclick, or will + break (showing black boxes instead of videos, etc). This shim mitigates + such breakage. +--> +<vmap:AdBreak></vmap:AdBreak> diff --git a/browser/extensions/webcompat/shims/webtrends.js b/browser/extensions/webcompat/shims/webtrends.js new file mode 100644 index 0000000000..c7ef0069da --- /dev/null +++ b/browser/extensions/webcompat/shims/webtrends.js @@ -0,0 +1,46 @@ +/* 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/. */ + +"use strict"; + +/** + * Bug 1766414 - Shim WebTrends Core Tag and Advanced Link Tracking + * + * Sites using WebTrends Core Tag or Link Tracking can break if they are + * are blocked. This shim mitigates that breakage by loading an empty module. + */ + +if (!window.dcsMultiTrack) { + window.dcsMultiTrack = o => { + o?.callback?.({}); + }; +} + +if (!window.WebTrends) { + class dcs { + addSelector() { + return this; + } + addTransform() { + return this; + } + DCSext = {}; + init(obj) { + return this; + } + track() { + return this; + } + } + + window.Webtrends = window.WebTrends = { + dcs, + multiTrack: window.dcsMultiTrack, + }; + + window.requestAnimationFrame(() => { + window.webtrendsAsyncLoad?.(dcs); + window.webtrendsAsyncInit?.(); + }); +} |