tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

smartblock_embeds_helper.js (10886B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* globals browser exportFunction */
      6 
      7 "use strict";
      8 
      9 const SMARTBLOCK_EMBED_OBSERVER_TIMEOUT_MS = 10000;
     10 
     11 /**
     12 * Helper library to create shims for Smartblock Embeds
     13 *
     14 */
     15 const embedHelperLib = (() => {
     16  let prevRanShims = new Set();
     17  let originalEmbedContainers = [];
     18  let embedPlaceholders = [];
     19  let observerTimeout;
     20  let newEmbedObserver;
     21 
     22  function sendMessageToAddon(message, shimId) {
     23    return browser.runtime.sendMessage({ message, shimId });
     24  }
     25 
     26  function addonMessageHandler(message, SHIM_INFO) {
     27    const { topic, shimId: sendingShimId } = message;
     28    const { shimId: handlingShimId, scriptURL } = SHIM_INFO;
     29 
     30    // Only react to messages which are targeting this shim.
     31    if (sendingShimId != handlingShimId) {
     32      return;
     33    }
     34 
     35    if (topic === "smartblock:unblock-embed") {
     36      if (newEmbedObserver) {
     37        newEmbedObserver.disconnect();
     38        newEmbedObserver = null;
     39      }
     40 
     41      if (observerTimeout) {
     42        clearTimeout(observerTimeout);
     43        observerTimeout = null;
     44      }
     45 
     46      // remove embed placeholders
     47      embedPlaceholders.forEach((p, idx) => {
     48        p.replaceWith(originalEmbedContainers[idx]);
     49      });
     50 
     51      // recreate scripts
     52      let scriptElement = document.createElement("script");
     53 
     54      // Set the script element's src with the website's principal instead of
     55      // the content script principal to ensure the tracker script is not loaded
     56      // via the content script's expanded principal.
     57      scriptElement.wrappedJSObject.src = scriptURL;
     58      document.body.appendChild(scriptElement);
     59    }
     60  }
     61 
     62  /**
     63   * Replaces embeds with a SmartBlock Embed placeholder. Optionally takes a list
     64   * of embeds to replace, otherwise will search for all embeds on the page.
     65   *
     66   * @param {HTMLElement[]} embedContainers - Array of elements to replace with placeholders.
     67   *                                  If the array is empty, this function will search
     68   *                                  for and replace all embeds on the page.
     69   *
     70   * @param {object} SHIM_INFO - Information about the shim wrapped in an object.
     71   */
     72  async function createShimPlaceholders(embedContainers, SHIM_INFO) {
     73    const { shimId, embedSelector, embedLogoURL, isTestShim } = SHIM_INFO;
     74 
     75    const [titleString, descriptionString, buttonString] =
     76      await sendMessageToAddon("smartblockGetFluentString", shimId);
     77 
     78    if (!embedContainers.length) {
     79      // No containers were passed in, do own search for containers
     80      embedContainers = document.querySelectorAll(embedSelector);
     81    }
     82 
     83    embedContainers.forEach(originalContainer => {
     84      // this string has to be defined within this function to avoid linting errors
     85      // see: https://github.com/mozilla/eslint-plugin-no-unsanitized/issues/259
     86      const SMARTBLOCK_PLACEHOLDER_HTML_STRING = `
     87        <style>
     88          #smartblock-placeholder-wrapper {
     89            min-height: 137px;
     90            min-width: 150px;
     91            max-height: 225px;
     92            max-width: 400px;
     93            padding: 32px 24px;
     94  
     95            display: block;
     96            align-content: center;
     97            text-align: center;
     98  
     99            background-color: light-dark(rgb(255, 255, 255), rgb(28, 27, 34));
    100            color: light-dark(rgb(43, 42, 51), rgb(251, 251, 254));
    101  
    102            border-radius: 8px;
    103            border: 2px dashed #0250bb;
    104  
    105            font-size: 14px;
    106            line-height: 1.2;
    107            font-family: system-ui;
    108          }
    109  
    110          #smartblock-placeholder-button {
    111            min-height: 32px;
    112            padding: 8px 14px;
    113  
    114            border-radius: 4px;
    115            font-weight: 600;
    116            border: 0;
    117  
    118            /* Colours match light/dark theme from
    119              https://searchfox.org/mozilla-central/source/browser/themes/addons/light/manifest.json
    120              https://searchfox.org/mozilla-central/source/browser/themes/addons/dark/manifest.json */
    121            background-color: light-dark(rgb(0, 97, 224), rgb(0, 221, 255));
    122            color: light-dark(rgb(251, 251, 254), rgb(43, 42, 51));
    123          }
    124  
    125          #smartblock-placeholder-button:hover {
    126            /* Colours match light/dark theme from
    127              https://searchfox.org/mozilla-central/source/browser/themes/addons/light/manifest.json
    128              https://searchfox.org/mozilla-central/source/browser/themes/addons/dark/manifest.json */
    129            background-color: light-dark(rgb(2, 80, 187), rgb(128, 235, 255));
    130          }
    131 
    132          #smartblock-placeholder-button:hover:active {
    133            /* Colours match light/dark theme from
    134              https://searchfox.org/mozilla-central/source/browser/themes/addons/light/manifest.json
    135              https://searchfox.org/mozilla-central/source/browser/themes/addons/dark/manifest.json */
    136            background-color: light-dark(rgb(5, 62, 148), rgb(170, 242, 255));
    137          }
    138  
    139          #smartblock-placeholder-title {
    140            margin-block: 14px;
    141            font-size: 16px;
    142            font-weight: bold;
    143          }
    144  
    145          #smartblock-placeholder-desc {
    146            margin-block: 14px;
    147          }
    148        </style>
    149        <div id="smartblock-placeholder-wrapper">
    150          <img id="smartblock-placeholder-image" width="24" height="24" />
    151          <p id="smartblock-placeholder-title"></p>
    152          <p id="smartblock-placeholder-desc"></p>
    153          <button id="smartblock-placeholder-button"></button>
    154        </div>`;
    155 
    156      // Create the placeholder inside a shadow dom
    157      const placeholderDiv = document.createElement("div");
    158 
    159      // Workaround to make sure clicks reach our placeholder button if the site
    160      // uses pointer capture. See Bug 1966696 for an example.
    161      disableSetPointerCaptureFor(placeholderDiv);
    162 
    163      if (isTestShim) {
    164        // Tag the div with a class to make it easily detectable FOR THE TEST SHIM ONLY
    165        placeholderDiv.classList.add("shimmed-embedded-content");
    166      }
    167 
    168      const shadowRoot = placeholderDiv.attachShadow({ mode: "closed" });
    169 
    170      shadowRoot.innerHTML = SMARTBLOCK_PLACEHOLDER_HTML_STRING;
    171      shadowRoot.getElementById("smartblock-placeholder-image").src =
    172        embedLogoURL;
    173      shadowRoot.getElementById("smartblock-placeholder-title").textContent =
    174        titleString;
    175      shadowRoot.getElementById("smartblock-placeholder-desc").textContent =
    176        descriptionString;
    177      shadowRoot.getElementById("smartblock-placeholder-button").textContent =
    178        buttonString;
    179 
    180      // Wait for user to opt-in.
    181      shadowRoot
    182        .getElementById("smartblock-placeholder-button")
    183        .addEventListener("click", ({ isTrusted }) => {
    184          if (!isTrusted) {
    185            return;
    186          }
    187          // Send a message to the addon to allow loading tracking resources
    188          // needed by the embed.
    189          sendMessageToAddon("embedClicked", shimId);
    190        });
    191 
    192      // Save the original embed element and the newly created placeholder
    193      embedPlaceholders.push(placeholderDiv);
    194      originalEmbedContainers.push(originalContainer);
    195 
    196      // Replace the embed with the placeholder
    197      originalContainer.replaceWith(placeholderDiv);
    198 
    199      sendMessageToAddon("smartblockEmbedReplaced", shimId);
    200    });
    201 
    202    if (isTestShim) {
    203      // Dispatch event to signal that the script is done replacing FOR TEST SHIM ONLY
    204      const finishedEvent = new CustomEvent("smartblockEmbedScriptFinished", {
    205        bubbles: true,
    206        composed: true,
    207      });
    208      window.dispatchEvent(finishedEvent);
    209    }
    210  }
    211 
    212  /**
    213   * Creates a mutation observer to observe new changes after page load to monitor for
    214   * new embeds.
    215   *
    216   * @param {object} SHIM_INFO - Information about the shim wrapped in an object.
    217   */
    218  function createEmbedMutationObserver(SHIM_INFO) {
    219    const { embedSelector } = SHIM_INFO;
    220 
    221    // Monitor for new embeds being added after page load so we can replace them
    222    // with placeholders.
    223    newEmbedObserver = new MutationObserver(mutations => {
    224      for (let { addedNodes, target, type } of mutations) {
    225        const nodes = type === "attributes" ? [target] : addedNodes;
    226        for (const node of nodes) {
    227          if (node.nodeType !== Node.ELEMENT_NODE) {
    228            // node is not an element, skip
    229            continue;
    230          }
    231          if (node.matches(embedSelector)) {
    232            // If element is an embed, replace with placeholder
    233            createShimPlaceholders([node], SHIM_INFO);
    234          } else {
    235            // If element is not an embed, check if any children are
    236            // and replace if needed
    237            let maybeEmbedNodeList = node.querySelectorAll?.(embedSelector);
    238            if (maybeEmbedNodeList) {
    239              createShimPlaceholders(maybeEmbedNodeList, SHIM_INFO);
    240            }
    241          }
    242        }
    243      }
    244    });
    245 
    246    newEmbedObserver.observe(document.documentElement, {
    247      childList: true,
    248      subtree: true,
    249      attributes: true,
    250      attributeFilter: ["id", "class"],
    251    });
    252 
    253    // Disconnect the mutation observer after a fixed (long) timeout to conserve resources.
    254    observerTimeout = setTimeout(() => {
    255      if (newEmbedObserver) {
    256        newEmbedObserver.disconnect();
    257      }
    258    }, SMARTBLOCK_EMBED_OBSERVER_TIMEOUT_MS);
    259  }
    260 
    261  /**
    262   * Disables the setPointerCapture method for a given element to prevent
    263   * pointer capture issues.
    264   *
    265   * @param {HTMLElement} el - The element to disable setPointerCapture for.
    266   */
    267  function disableSetPointerCaptureFor(el) {
    268    const pageEl = el.wrappedJSObject;
    269 
    270    Object.defineProperty(pageEl, "setPointerCapture", {
    271      configurable: true,
    272      writable: true,
    273      enumerable: false,
    274      // no-op ONLY for this element
    275      value: exportFunction(function (_pointerId) {
    276        console.warn(
    277          "Blocked setPointerCapture on SmartBlock embed placeholder.",
    278          this,
    279          _pointerId
    280        );
    281        // swallow
    282      }, window),
    283    });
    284  }
    285 
    286  /**
    287   * Initializes a smartblock embeds shim on the page.
    288   *
    289   * @param {object} SHIM_INFO - Information about the shim wrapped in an object.
    290   */
    291  function initEmbedShim(SHIM_INFO) {
    292    let { shimId } = SHIM_INFO;
    293 
    294    if (prevRanShims.has(shimId)) {
    295      // we should not init shims twice
    296      return;
    297    }
    298 
    299    prevRanShims.add(shimId);
    300 
    301    // Listen for messages from the background script.
    302    browser.runtime.onMessage.addListener(request => {
    303      addonMessageHandler(request, SHIM_INFO);
    304    });
    305 
    306    // Listen for page changes in case of new embeds
    307    createEmbedMutationObserver(SHIM_INFO);
    308 
    309    // Run placeholder creation
    310    createShimPlaceholders([], SHIM_INFO);
    311  }
    312 
    313  return {
    314    initEmbedShim,
    315  };
    316 })();