Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* globals browser */
"use strict";
const SMARTBLOCK_EMBED_OBSERVER_TIMEOUT_MS = 10000;
/**
* Helper library to create shims for Smartblock Embeds
*
*/
const embedHelperLib = (() => {
let prevRanShims = new Set();
let originalEmbedContainers = [];
let embedPlaceholders = [];
let observerTimeout;
let newEmbedObserver;
function sendMessageToAddon(message, shimId) {
return browser.runtime.sendMessage({ message, shimId });
}
function addonMessageHandler(message, SHIM_INFO) {
const { topic, shimId: sendingShimId } = message;
const { shimId: handlingShimId, scriptURL } = SHIM_INFO;
// Only react to messages which are targeting this shim.
if (sendingShimId != handlingShimId) {
return;
}
if (topic === "smartblock:unblock-embed") {
if (newEmbedObserver) {
newEmbedObserver.disconnect();
newEmbedObserver = null;
}
if (observerTimeout) {
clearTimeout(observerTimeout);
observerTimeout = null;
}
// 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 = scriptURL;
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.
*
* @param {object} SHIM_INFO - Information about the shim wrapped in an object.
*/
async function createShimPlaceholders(embedContainers, SHIM_INFO) {
const { shimId, embedSelector, embedLogoURL, isTestShim } = SHIM_INFO;
const [titleString, descriptionString, buttonString] =
await sendMessageToAddon("smartblockGetFluentString", shimId);
if (!embedContainers.length) {
// No containers were passed in, do own search for containers
embedContainers = document.querySelectorAll(embedSelector);
}
embedContainers.forEach(originalContainer => {
// this string has to be defined within this function to avoid linting errors
const SMARTBLOCK_PLACEHOLDER_HTML_STRING = `
<style>
#smartblock-placeholder-wrapper {
min-height: 137px;
min-width: 216px;
max-height: 225px;
max-width: 400px;
padding: 32px 24px;
display: block;
align-content: center;
text-align: center;
background-color: light-dark(rgb(255, 255, 255), rgb(28, 27, 34));
color: light-dark(rgb(43, 42, 51), rgb(251, 251, 254));
border-radius: 8px;
border: 2px dashed #0250bb;
font-size: 14px;
line-height: 1.2;
font-family: system-ui;
}
#smartblock-placeholder-button {
min-height: 32px;
padding: 8px 14px;
border-radius: 4px;
font-weight: 600;
border: 0;
/* Colours match light/dark theme from
background-color: light-dark(rgb(0, 97, 224), rgb(0, 221, 255));
color: light-dark(rgb(251, 251, 254), rgb(43, 42, 51));
}
#smartblock-placeholder-button:hover {
/* Colours match light/dark theme from
background-color: light-dark(rgb(2, 80, 187), rgb(128, 235, 255));
}
#smartblock-placeholder-button:hover:active {
/* Colours match light/dark theme from
background-color: light-dark(rgb(5, 62, 148), rgb(170, 242, 255));
}
#smartblock-placeholder-title {
margin-block: 14px;
font-size: 16px;
font-weight: bold;
}
#smartblock-placeholder-desc {
margin-block: 14px;
}
</style>
<div id="smartblock-placeholder-wrapper">
<img id="smartblock-placeholder-image" width="24" height="24" />
<p id="smartblock-placeholder-title"></p>
<p id="smartblock-placeholder-desc"></p>
<button id="smartblock-placeholder-button"></button>
</div>`;
// Create the placeholder inside a shadow dom
const placeholderDiv = document.createElement("div");
if (isTestShim) {
// Tag the div with a class to make it easily detectable FOR THE TEST SHIM ONLY
placeholderDiv.classList.add("shimmed-embedded-content");
}
const shadowRoot = placeholderDiv.attachShadow({ mode: "closed" });
shadowRoot.innerHTML = SMARTBLOCK_PLACEHOLDER_HTML_STRING;
shadowRoot.getElementById("smartblock-placeholder-image").src =
embedLogoURL;
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", shimId);
});
// 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", shimId);
});
if (isTestShim) {
// Dispatch event to signal that the script is done replacing FOR TEST SHIM ONLY
const finishedEvent = new CustomEvent("smartblockEmbedScriptFinished", {
bubbles: true,
composed: true,
});
window.dispatchEvent(finishedEvent);
}
}
/**
* Creates a mutation observer to observe new changes after page load to monitor for
* new embeds.
*
* @param {object} SHIM_INFO - Information about the shim wrapped in an object.
*/
function createEmbedMutationObserver(SHIM_INFO) {
const { embedSelector } = SHIM_INFO;
// 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 (node.matches(embedSelector)) {
// If node is an embed, replace with placeholder
createShimPlaceholders([node], SHIM_INFO);
} else {
// If node is not an embed, check if any children are
// and replace if needed
let maybeEmbedNodeList = node.querySelectorAll?.(embedSelector);
if (maybeEmbedNodeList) {
createShimPlaceholders(maybeEmbedNodeList, SHIM_INFO);
}
}
}
}
});
newEmbedObserver.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["id", "class"],
});
// Disconnect the mutation observer after a fixed (long) timeout to conserve resources.
observerTimeout = setTimeout(() => {
if (newEmbedObserver) {
newEmbedObserver.disconnect();
}
}, SMARTBLOCK_EMBED_OBSERVER_TIMEOUT_MS);
}
/**
* Initializes a smartblock embeds shim on the page.
*
* @param {object} SHIM_INFO - Information about the shim wrapped in an object.
*/
function initEmbedShim(SHIM_INFO) {
let { shimId } = SHIM_INFO;
if (prevRanShims.has(shimId)) {
// we should not init shims twice
return;
}
prevRanShims.add(shimId);
// Listen for messages from the background script.
browser.runtime.onMessage.addListener(request => {
addonMessageHandler(request, SHIM_INFO);
});
// Listen for page changes in case of new embeds
createEmbedMutationObserver(SHIM_INFO);
// Run placeholder creation
createShimPlaceholders([], SHIM_INFO);
}
return {
initEmbedShim,
};
})();