diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js index 2f9aea9645ae..962ebc026aca 100644 --- a/browser/base/content/browser-siteProtections.js +++ b/browser/base/content/browser-siteProtections.js @@ -1397,6 +1397,11 @@ var gProtectionsHandler = { shimId: "TiktokEmbed", displayName: "Tiktok", }, + { + sites: ["https://platform.twitter.com"], + shimId: "TwitterEmbed", + displayName: "X", + }, ], /** diff --git a/browser/extensions/webcompat/data/shims.js b/browser/extensions/webcompat/data/shims.js index cf4f3229bf6e..5c92ad281198 100644 --- a/browser/extensions/webcompat/data/shims.js +++ b/browser/extensions/webcompat/data/shims.js @@ -1025,6 +1025,33 @@ const AVAILABLE_SHIMS = [ ["*://steam.tv/*", "*://checkout.steampowered.com/*"], ], }, + { + id: "TwitterEmbed", + platform: "desktop", + name: "Twitter embed placeholder", + bug: "1901602", + runFirst: "twitter-embed.js", + // Blank stub file just so we run the script above when the matched script + // files get blocked. + file: "empty-script.js", + matches: ["https://platform.twitter.com/widgets.js"], + logos: ["x-logo.svg"], + needsShimHelpers: [ + "embedClicked", + "smartblockEmbedReplaced", + "smartblockGetFluentString", + ], + isSmartblockEmbedShim: true, + onlyIfBlockedByETP: true, + unblocksOnOptIn: [ + "*://platform.twitter.com/*", + "*://syndication.twitter.com/*", + "*://cdn.syndication.twimg.com/*", + "*://pbs.twimg.com/*", + "*://abs.twimg.com/*", + "*://abs-0.twimg.com/*", + ], + }, ]; if (typeof module !== "undefined") { diff --git a/browser/extensions/webcompat/manifest.json b/browser/extensions/webcompat/manifest.json index 4bdb5800862f..12942d38970c 100644 --- a/browser/extensions/webcompat/manifest.json +++ b/browser/extensions/webcompat/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Web Compatibility Interventions", "description": "Urgent post-release fixes for web compatibility.", - "version": "138.2.0", + "version": "138.3.0", "browser_specific_settings": { "gecko": { "id": "webcompat@mozilla.org", @@ -164,10 +164,12 @@ "shims/tiktok.svg", "shims/tracking-pixel.png", "shims/tsn-ca.js", + "shims/twitter-embed.js", "shims/vast2.xml", "shims/vast3.xml", "shims/vidible.js", "shims/vmad.xml", - "shims/webtrends.js" + "shims/webtrends.js", + "shims/x-logo.svg" ] } diff --git a/browser/extensions/webcompat/shims/twitter-embed.js b/browser/extensions/webcompat/shims/twitter-embed.js new file mode 100644 index 000000000000..ed47ceb80c21 --- /dev/null +++ b/browser/extensions/webcompat/shims/twitter-embed.js @@ -0,0 +1,237 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +/* globals browser */ + +if (!window.smartblockTwitterShimInitialized) { + // Guard against this script running multiple times + window.smartblockTwitterShimInitialized = true; + + const SHIM_ID = "TwitterEmbed"; + + const SHIM_EMBED_CLASSES = ["twitter-tweet", "twitter-timeline"]; + const SHIM_CLASS_SELECTORS = SHIM_EMBED_CLASSES.map( + className => `.${className}` + ).join(","); + + // Original URL of the embed script. + const ORIGINAL_URL = "https://platform.twitter.com/widgets.js"; + const LOGO_URL = "https://smartblock.firefox.etp/x-logo.svg"; + + // Timeout for observing new changes to the page + const OBSERVER_TIMEOUT_MS = 10000; + let observerTimeout; + let newEmbedObserver; + + let originalEmbedContainers = []; + let embedPlaceholders = []; + + function sendMessageToAddon(message) { + return browser.runtime.sendMessage({ message, shimId: SHIM_ID }); + } + + function addonMessageHandler(message) { + let { topic, shimId } = message; + // Only react to messages which are targeting this shim. + if (shimId != SHIM_ID) { + return; + } + + if (topic === "smartblock:unblock-embed") { + if (newEmbedObserver) { + newEmbedObserver.disconnect(); + newEmbedObserver = null; + } + + if (observerTimeout) { + clearTimeout(observerTimeout); + } + + // remove embed placeholders + embedPlaceholders.forEach((p, idx) => { + p.replaceWith(originalEmbedContainers[idx]); + }); + + // recreate scripts + let scriptElement = document.createElement("script"); + + // Set the script element's src with the website's principal instead of + // the content script principal to ensure the tracker script is not loaded + // via the content script's expanded principal. + scriptElement.wrappedJSObject.src = ORIGINAL_URL; + document.body.appendChild(scriptElement); + } + } + + /** + * Replaces embeds with a SmartBlock Embed placeholder. Optionally takes a list + * of embeds to replace, otherwise will search for all embeds on the page. + * + * @param {HTMLElement[]} embedContainers - Array of elements to replace with placeholders. + * If the array is empty, this function will search + * for and replace all embeds on the page. + */ + async function createShimPlaceholders(embedContainers = []) { + const [titleString, descriptionString, buttonString] = + await sendMessageToAddon("smartblockGetFluentString"); + + if (!embedContainers.length) { + // No containers were passed in, do own search for containers + embedContainers = document.querySelectorAll(SHIM_CLASS_SELECTORS); + } + + embedContainers.forEach(originalContainer => { + // this string has to be defined within this function to avoid linting errors + // see: https://github.com/mozilla/eslint-plugin-no-unsanitized/issues/259 + const SMARTBLOCK_PLACEHOLDER_HTML_STRING = ` + +
+ +

+

+ +
`; + + // Create the placeholder inside a shadow dom + const placeholderDiv = document.createElement("div"); + + const shadowRoot = placeholderDiv.attachShadow({ mode: "closed" }); + + shadowRoot.innerHTML = SMARTBLOCK_PLACEHOLDER_HTML_STRING; + shadowRoot.getElementById("smartblock-placeholder-image").src = LOGO_URL; + shadowRoot.getElementById("smartblock-placeholder-title").textContent = + titleString; + shadowRoot.getElementById("smartblock-placeholder-desc").textContent = + descriptionString; + shadowRoot.getElementById("smartblock-placeholder-button").textContent = + buttonString; + + // Wait for user to opt-in. + shadowRoot + .getElementById("smartblock-placeholder-button") + .addEventListener("click", ({ isTrusted }) => { + if (!isTrusted) { + return; + } + // Send a message to the addon to allow loading tracking resources + // needed by the embed. + sendMessageToAddon("embedClicked"); + }); + + // Save the original embed element and the newly created placeholder + embedPlaceholders.push(placeholderDiv); + originalEmbedContainers.push(originalContainer); + + // Replace the embed with the placeholder + originalContainer.replaceWith(placeholderDiv); + + sendMessageToAddon("smartblockEmbedReplaced"); + }); + } + + // Listen for messages from the background script. + browser.runtime.onMessage.addListener(request => { + addonMessageHandler(request); + }); + + // Monitor for new embeds being added after page load so we can replace them + // with placeholders. + newEmbedObserver = new MutationObserver(mutations => { + for (let { addedNodes, target, type } of mutations) { + const nodes = type === "attributes" ? [target] : addedNodes; + for (const node of nodes) { + if ( + SHIM_EMBED_CLASSES.some(className => + node.classList?.contains(className) + ) + ) { + // If node is an embed, replace with placeholder + createShimPlaceholders([node]); + } else { + // If node is not an embed, check if any children are + // and replace if needed + let maybeEmbedNodeList = + node.querySelectorAll?.(SHIM_CLASS_SELECTORS); + if (maybeEmbedNodeList) { + createShimPlaceholders(maybeEmbedNodeList); + } + } + } + } + }); + + newEmbedObserver.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["class"], + }); + + // Disconnect the mutation observer after a fixed (long) timeout to conserve resources. + observerTimeout = setTimeout( + () => newEmbedObserver.disconnect(), + OBSERVER_TIMEOUT_MS + ); + + createShimPlaceholders(); +} diff --git a/browser/extensions/webcompat/shims/x-logo.svg b/browser/extensions/webcompat/shims/x-logo.svg new file mode 100644 index 000000000000..5d9652d1dee1 --- /dev/null +++ b/browser/extensions/webcompat/shims/x-logo.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file