/* * This file is part of Privacy Badger * Copyright (C) 2014 Electronic Frontier Foundation * Derived from ShareMeNot * Copyright (C) 2011-2014 University of Washington * * Privacy Badger is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * Privacy Badger is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Privacy Badger. If not, see . */ /* * ShareMeNot is licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * Copyright (c) 2011-2014 University of Washington * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // widget data let widgetList; // cached chrome.i18n.getMessage() results const TRANSLATIONS = {}; // references to widget page elements const WIDGET_ELS = {}; /** * @param {Object} response response to checkWidgetReplacementEnabled */ function initialize(response) { for (const key in response.translations) { TRANSLATIONS[key] = response.translations[key]; } widgetList = response.widgetList; // check for widgets blocked before we got here replaceInitialTrackerButtonsHelper(response.widgetsToReplace); // set up listener for dynamically created widgets chrome.runtime.onMessage.addListener(function (request) { if (request.replaceWidget) { replaceSubsequentTrackerButtonsHelper(request.trackerDomain); } }); } /** * Creates a replacement placeholder element for the given widget. * * @param {Object} widget the SocialWidget object * @param {Element} trackerElem the button/widget element we are replacing * @param {Function} callback called with the replacement element */ function createReplacementElement(widget, trackerElem, callback) { let buttonData = widget.replacementButton; // no image data to fetch if (!buttonData.hasOwnProperty('imagePath')) { return setTimeout(function () { _createReplacementElementCallback(widget, trackerElem, callback); }, 0); } // already have replacement button image URI cached if (buttonData.buttonUrl) { return setTimeout(function () { _createReplacementElementCallback(widget, trackerElem, callback); }, 0); } // already messaged for but haven't yet received the image data if (buttonData.loading) { // check back in 10 ms return setTimeout(function () { createReplacementElement(widget, trackerElem, callback); }, 10); } // don't have image data cached yet, get it from the background page buttonData.loading = true; chrome.runtime.sendMessage({ type: "getReplacementButton", widgetName: widget.name }, function (response) { if (response) { buttonData.buttonUrl = response; // cache image data _createReplacementElementCallback(widget, trackerElem, callback); } }); } function _createReplacementElementCallback(widget, trackerElem, callback) { if (widget.replacementButton.buttonUrl) { _createButtonReplacement(widget, callback); } else { _createWidgetReplacement(widget, trackerElem, callback); } } function _createButtonReplacement(widget, callback) { let buttonData = widget.replacementButton, button_type = buttonData.type; let button = document.createElement("img"); button.setAttribute("src", buttonData.buttonUrl); // TODO use custom tooltip to support RTL locales? button.setAttribute( "title", TRANSLATIONS.social_tooltip_pb_has_replaced.replace("XXX", widget.name) ); let styleAttrs = [ "border: none", "cursor: pointer", "height: auto", "width: auto", ]; button.setAttribute("style", styleAttrs.join(" !important;") + " !important"); // normal button type; just open a new window when clicked if (button_type === 0) { let popup_url = buttonData.details + encodeURIComponent(window.location.href); button.addEventListener("click", function () { window.open(popup_url); }); // in place button type; replace the existing button // with an iframe when clicked } else if (button_type == 1) { let iframe_url = buttonData.details + encodeURIComponent(window.location.href); button.addEventListener("click", function () { replaceButtonWithIframeAndUnblockTracker(button, widget.name, iframe_url); }, { once: true }); // in place button type; replace the existing button with code // specified in the widgets JSON } else if (button_type == 2) { button.addEventListener("click", function () { replaceButtonWithHtmlCodeAndUnblockTracker(button, widget.name, buttonData.details); }, { once: true }); } callback(button); } function _createWidgetReplacement(widget, trackerElem, callback) { let replacementEl; // in-place widget type: // reinitialize the widget by reinserting its element's HTML if (widget.replacementButton.type == 3) { replacementEl = createReplacementWidget( widget, trackerElem, reinitializeWidgetAndUnblockTracker); // in-place widget type: // reinitialize the widget by reinserting its element's HTML // and activating associated scripts } else if (widget.replacementButton.type == 4) { let activationFn = replaceWidgetAndReloadScripts; // if there are no matching script elements if (!document.querySelectorAll(widget.scriptSelectors.join(',')).length) { // and we don't have a fallback script URL if (!widget.fallbackScriptUrl) { // we can't do "in-place" activation; reload the page instead activationFn = function () { unblockTracker(widget.name, function () { location.reload(); }); }; } } replacementEl = createReplacementWidget(widget, trackerElem, activationFn); } callback(replacementEl); } /** * Unblocks the given widget and replaces the given button with an iframe * pointing to the given URL. * * @param {Element} button the DOM element of the button to replace * @param {String} widget_name the name of the replacement widget * @param {String} iframeUrl the URL of the iframe to replace the button */ function replaceButtonWithIframeAndUnblockTracker(button, widget_name, iframeUrl) { unblockTracker(widget_name, function () { // check is needed as for an unknown reason this callback function is // executed for buttons that have already been removed; we are trying // to prevent replacing an already removed button if (button.parentNode !== null) { let iframe = document.createElement("iframe"); iframe.setAttribute("src", iframeUrl); iframe.setAttribute("style", "border: none !important; height: 1.5em !important;"); button.parentNode.replaceChild(iframe, button); } }); } /** * Unblocks the given widget and replaces the given button with the * HTML code defined in the provided SocialWidget object. * * @param {Element} button the DOM element of the button to replace * @param {String} widget_name the name of the replacement widget * @param {String} html the HTML string that should replace the button */ function replaceButtonWithHtmlCodeAndUnblockTracker(button, widget_name, html) { unblockTracker(widget_name, function () { // check is needed as for an unknown reason this callback function is // executed for buttons that have already been removed; we are trying // to prevent replacing an already removed button if (button.parentNode !== null) { let codeContainer = document.createElement("div"); codeContainer.innerHTML = html; button.parentNode.replaceChild(codeContainer, button); replaceScriptsRecurse(codeContainer); } }); } /** * Unblocks the given widget and replaces our replacement placeholder * with the original third-party widget element. * * The teardown to the initialization defined in createReplacementWidget(). * * @param {String} name the name/type of this widget (SoundCloud, Vimeo etc.) */ function reinitializeWidgetAndUnblockTracker(name) { unblockTracker(name, function () { // restore all widgets of this type WIDGET_ELS[name].forEach(data => { data.parent.replaceChild(data.widget, data.replacement); }); WIDGET_ELS[name] = []; }); } /** * Similar to reinitializeWidgetAndUnblockTracker() above, * but also reruns scripts defined in scriptSelectors. * * @param {String} name the name/type of this widget (Disqus, Google reCAPTCHA) */ function replaceWidgetAndReloadScripts(name) { unblockTracker(name, function () { // restore all widgets of this type WIDGET_ELS[name].forEach(data => { data.parent.replaceChild(data.widget, data.replacement); reloadScripts(data.scriptSelectors, data.fallbackScriptUrl); }); WIDGET_ELS[name] = []; }); } /** * Find and replace script elements with their copies to trigger re-running. */ function reloadScripts(selectors, fallback_script_url) { let scripts = document.querySelectorAll(selectors.join(',')); // if there are no matches, try a known script URL if (!scripts.length && fallback_script_url) { let parent = document.documentElement, replacement = document.createElement("script"); replacement.src = fallback_script_url; parent.insertBefore(replacement, parent.firstChild); return; } for (let scriptEl of scripts) { // reinsert script elements only if (!scriptEl.nodeName || scriptEl.nodeName.toLowerCase() != 'script') { continue; } let replacement = document.createElement("script"); for (let attr of scriptEl.attributes) { replacement.setAttribute(attr.nodeName, attr.value); } scriptEl.parentNode.replaceChild(replacement, scriptEl); // reinsert one script and quit break; } } /** * Dumping scripts into innerHTML won't execute them, so replace them * with executable scripts. */ function replaceScriptsRecurse(node) { if (node.nodeName && node.nodeName.toLowerCase() == 'script' && node.getAttribute && node.getAttribute("type") == "text/javascript") { let script = document.createElement("script"); script.text = node.innerHTML; script.src = node.src; node.parentNode.replaceChild(script, node); } else { let i = 0, children = node.childNodes; while (i < children.length) { replaceScriptsRecurse(children[i]); i++; } } return node; } /** * Replaces all tracker buttons on the current web page with the internal * replacement buttons, respecting the user's blocking settings. * * @param {Array} widgetsToReplace a list of widget names to replace */ function replaceInitialTrackerButtonsHelper(widgetsToReplace) { widgetList.forEach(function (widget) { if (widgetsToReplace.hasOwnProperty(widget.name)) { replaceIndividualButton(widget); } }); } /** * Individually replaces tracker buttons blocked after initial check. */ function replaceSubsequentTrackerButtonsHelper(tracker_domain) { if (!widgetList) { return; } widgetList.forEach(function (widget) { let replace = widget.domains.some(domain => { if (domain == tracker_domain) { return true; // leading wildcard } else if (domain[0] == "*") { if (tracker_domain.endsWith(domain.slice(1))) { return true; } } return false; }); if (replace) { replaceIndividualButton(widget); } }); } function _make_id(prefix) { return prefix + "-" + Math.random().toString().replace(".", ""); } function createReplacementWidget(widget, elToReplace, activationFn) { let name = widget.name; let widgetFrame = document.createElement('iframe'); // widget replacement frame styles let border_width = 1; let styleAttrs = [ "background-color: #fff", "border: " + border_width + "px solid #ec9329", "min-width: 220px", "min-height: 210px", "max-height: 600px", "z-index: 2147483647", ]; if (elToReplace.offsetWidth > 0) { styleAttrs.push(`width: ${elToReplace.offsetWidth - 2*border_width}px`); } if (elToReplace.offsetHeight > 0) { styleAttrs.push(`height: ${elToReplace.offsetHeight - 2*border_width}px`); } widgetFrame.style = styleAttrs.join(" !important;") + " !important"; let widgetDiv = document.createElement('div'); // parent div styles styleAttrs = [ "display: flex", "flex-direction: column", "align-items: center", "justify-content: center", "width: 100%", "height: 100%", ]; if (TRANSLATIONS.rtl) { styleAttrs.push("direction: rtl"); } widgetDiv.style = styleAttrs.join(" !important;") + " !important"; // child div styles styleAttrs = [ "color: #303030", "font-family: helvetica, arial, sans-serif", "font-size: 16px", "display: flex", "flex-wrap: wrap", "justify-content: center", "text-align: center", "margin: 10px", ]; let textDiv = document.createElement('div'); textDiv.style = styleAttrs.join(" !important;") + " !important"; textDiv.appendChild(document.createTextNode( TRANSLATIONS.widget_placeholder_pb_has_replaced.replace("XXX", name))); let infoIcon = document.createElement('a'), info_icon_id = _make_id("ico-help"); infoIcon.id = info_icon_id; infoIcon.href = "https://privacybadger.org/#How-does-Privacy-Badger-handle-social-media-widgets"; infoIcon.rel = "noreferrer"; infoIcon.target = "_blank"; textDiv.appendChild(infoIcon); widgetDiv.appendChild(textDiv); let buttonDiv = document.createElement('div'); styleAttrs.push("width: 100%"); buttonDiv.style = styleAttrs.join(" !important;") + " !important"; // allow once button let button = document.createElement('button'), button_id = _make_id("btn-once"); button.id = button_id; styleAttrs = [ "transition: background-color 0.25s ease-out, border-color 0.25s ease-out, color 0.25s ease-out", "border-radius: 3px", "cursor: pointer", "font-family: 'Lucida Grande', 'Segoe UI', Tahoma, 'DejaVu Sans', Arial, sans-serif", "font-size: 12px", "font-weight: bold", "line-height: 16px", "padding: 10px", "margin: 4px", "text-align: center", "width: 70%", "max-width: 280px", ]; button.style = styleAttrs.join(" !important;") + " !important"; // allow on this site button let site_button = document.createElement('button'), site_button_id = _make_id("btn-site"); site_button.id = site_button_id; site_button.style = styleAttrs.join(" !important;") + " !important"; button.appendChild(document.createTextNode(TRANSLATIONS.allow_once)); site_button.appendChild(document.createTextNode(TRANSLATIONS.allow_on_site)); buttonDiv.appendChild(button); buttonDiv.appendChild(site_button); widgetDiv.appendChild(buttonDiv); // save refs. to elements for use in teardown if (!WIDGET_ELS.hasOwnProperty(name)) { WIDGET_ELS[name] = []; } let data = { parent: elToReplace.parentNode, widget: elToReplace, replacement: widgetFrame }; if (widget.scriptSelectors) { data.scriptSelectors = widget.scriptSelectors; if (widget.fallbackScriptUrl) { data.fallbackScriptUrl = widget.fallbackScriptUrl; } } WIDGET_ELS[name].push(data); // set up click handler widgetFrame.addEventListener('load', function () { let onceButton = widgetFrame.contentDocument.getElementById(button_id), siteButton = widgetFrame.contentDocument.getElementById(site_button_id); onceButton.addEventListener("click", function (e) { e.preventDefault(); activationFn(name); }, { once: true }); siteButton.addEventListener("click", function (e) { e.preventDefault(); // first message the background page to record that // this widget should always be allowed on this site chrome.runtime.sendMessage({ type: "allowWidgetOnSite", widgetName: name }, function () { activationFn(name); }); }, { once: true }); }, false); // end of click handler let head_styles = ` html, body { height: 100% !important; overflow: hidden !important; } #${button_id} { border: 2px solid #f06a0a !important; background-color: #f06a0a !important; color: #fefefe !important; } #${site_button_id} { border: 2px solid #333 !important; background-color: #fefefe !important; color: #333 !important; } #${button_id}:hover { background-color: #fefefe !important; color: #333 !important; } #${site_button_id}:hover { background-color: #fefefe !important; border: 2px solid #f06a0a !important; } #${info_icon_id} { position: absolute; ${TRANSLATIONS.rtl ? "left" : "right"}: 4px; top: 4px; line-height: 12px; text-decoration: none; } #${info_icon_id}:before { border: 2px solid; border-radius: 50%; display: inline-block; color: #555; content: '?'; font-size: 12px; font-weight: bold; padding: 1px; height: 1em; width: 1em; } #${info_icon_id}:hover:before { color: #ec9329; } `.trim(); widgetFrame.srcdoc = '' + widgetDiv.outerHTML + ''; return widgetFrame; } /** * Replaces buttons/widgets in the DOM. */ function replaceIndividualButton(widget) { let selector = widget.buttonSelectors.join(','), elsToReplace = document.querySelectorAll(selector); elsToReplace.forEach(function (el) { createReplacementElement(widget, el, function (replacementEl) { el.parentNode.replaceChild(replacementEl, el); }); }); } /** * Messages the background page to temporarily allow domains associated with a * given replacement widget. * Calls the provided callback function upon response. * * @param {String} name the name of the replacement widget * @param {Function} callback the callback function */ function unblockTracker(name, callback) { let request = { type: "unblockWidget", widgetName: name }; chrome.runtime.sendMessage(request, callback); } // END FUNCTION DEFINITIONS /////////////////////////////////////////////////// (function () { // don't inject into non-HTML documents (such as XML documents) // but do inject into XHTML documents if (document instanceof HTMLDocument === false && ( document instanceof XMLDocument === false || document.createElement('div') instanceof HTMLDivElement === false )) { return; } chrome.runtime.sendMessage({ type: "checkWidgetReplacementEnabled" }, function (response) { if (!response) { return; } initialize(response); }); }());