diff options
Diffstat (limited to 'src/js/contentscripts')
-rw-r--r-- | src/js/contentscripts/clobbercookie.js | 60 | ||||
-rw-r--r-- | src/js/contentscripts/clobberlocalstorage.js | 94 | ||||
-rw-r--r-- | src/js/contentscripts/collapser.js | 56 | ||||
-rw-r--r-- | src/js/contentscripts/dnt.js | 66 | ||||
-rw-r--r-- | src/js/contentscripts/fingerprinting.js | 367 | ||||
-rw-r--r-- | src/js/contentscripts/socialwidgets.js | 641 | ||||
-rw-r--r-- | src/js/contentscripts/supercookie.js | 151 | ||||
-rw-r--r-- | src/js/contentscripts/utils.js | 53 |
8 files changed, 1488 insertions, 0 deletions
diff --git a/src/js/contentscripts/clobbercookie.js b/src/js/contentscripts/clobbercookie.js new file mode 100644 index 0000000..402d00e --- /dev/null +++ b/src/js/contentscripts/clobbercookie.js @@ -0,0 +1,60 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2014 Electronic Frontier Foundation + * + * 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 <http://www.gnu.org/licenses/>. + */ + +(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; +} + +// don't bother asking to run when trivially in first-party context +if (window.top == window) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkLocation", + frameUrl: window.FRAME_URL +}, function (blocked) { + if (blocked) { + var code = '('+ function() { + document.__defineSetter__("cookie", function(/*value*/) { }); + document.__defineGetter__("cookie", function() { return ""; }); + + // trim referrer down to origin + let referrer = document.referrer; + if (referrer) { + referrer = referrer.slice( + 0, + referrer.indexOf('/', referrer.indexOf('://') + 3) + ) + '/'; + } + document.__defineGetter__("referrer", function () { return referrer; }); + } +')();'; + + window.injectScript(code); + } + return true; +}); + +}()); diff --git a/src/js/contentscripts/clobberlocalstorage.js b/src/js/contentscripts/clobberlocalstorage.js new file mode 100644 index 0000000..7ff3528 --- /dev/null +++ b/src/js/contentscripts/clobberlocalstorage.js @@ -0,0 +1,94 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2014 Electronic Frontier Foundation + * + * 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 <http://www.gnu.org/licenses/>. + */ + +(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; +} + +// don't bother asking to run when trivially in first-party context +if (window.top == window) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkLocation", + frameUrl: window.FRAME_URL +}, function (blocked) { + if (blocked) { + // https://stackoverflow.com/questions/49092423/how-to-break-on-localstorage-changes + var code = + '('+ function() { + + /* + * If localStorage is inaccessible, such as when "Block third-party cookies" + * in enabled in Chrome or when `dom.storage.enabled` is set to `false` in + * Firefox, do not go any further. + */ + try { + // No localStorage raises an Exception in Chromium-based browsers, while + // it's equal to `null` in Firefox. + if (null === localStorage) { + throw false; + } + } catch (ex) { + return; + } + + let lsProxy = new Proxy(localStorage, { + set: function (/*ls, prop, value*/) { + return true; + }, + get: function (ls, prop) { + if (typeof ls[prop] == 'function') { + let fn = function () {}; + if (prop == 'getItem' || prop == 'key') { + fn = function () { return null; }; + } + return fn.bind(ls); + } else { + if (prop == 'length') { + return 0; + } else if (prop == '__proto__') { + return lsProxy; + } + return; + } + } + }); + + Object.defineProperty(window, 'localStorage', { + configurable: true, + enumerable: true, + value: lsProxy + }); + + } +')()'; + + window.injectScript(code); + } + return true; +}); + +}()); diff --git a/src/js/contentscripts/collapser.js b/src/js/contentscripts/collapser.js new file mode 100644 index 0000000..968905c --- /dev/null +++ b/src/js/contentscripts/collapser.js @@ -0,0 +1,56 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2020 Electronic Frontier Foundation + * + * 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 <http://www.gnu.org/licenses/>. + */ + +(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; +} + +function hideFrame(url) { + let sel = "iframe[src='" + CSS.escape(url) + "']"; + let el = document.querySelector(sel); + if (el) { // el could have gotten replaced since the lookup + el.style.setProperty("display", "none", "important"); + } +} + +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + if (request.hideFrame) { + hideFrame(request.url); + sendResponse(true); + } +}); + +// check the page for any frames that were blocked before we got here +chrome.runtime.sendMessage({ + type: "getBlockedFrameUrls" +}, function (frameUrls) { + if (!frameUrls) { + return; + } + for (let url of frameUrls) { + hideFrame(url); + } +}); + +}()); diff --git a/src/js/contentscripts/dnt.js b/src/js/contentscripts/dnt.js new file mode 100644 index 0000000..0584748 --- /dev/null +++ b/src/js/contentscripts/dnt.js @@ -0,0 +1,66 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2018 Electronic Frontier Foundation + * + * 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 <http://www.gnu.org/licenses/>. + */ + +function getPageScript() { + + // code below is not a content script: no chrome.* APIs ///////////////////// + + // return a string + return "(" + function (NAVIGATOR, OBJECT) { + + OBJECT.defineProperty(OBJECT.getPrototypeOf(NAVIGATOR), "doNotTrack", { + get: function doNotTrack() { + return "1"; + } + }); + + OBJECT.defineProperty(OBJECT.getPrototypeOf(NAVIGATOR), "globalPrivacyControl", { + get: function globalPrivacyControl() { + return "1"; + } + }); + + // save locally to keep from getting overwritten by site code + } + "(window.navigator, Object));"; + + // code above is not a content script: no chrome.* APIs ///////////////////// + +} + +// 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; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkDNT" +}, function (enabled) { + if (enabled) { + window.injectScript(getPageScript()); + } +}); + +}()); diff --git a/src/js/contentscripts/fingerprinting.js b/src/js/contentscripts/fingerprinting.js new file mode 100644 index 0000000..0891896 --- /dev/null +++ b/src/js/contentscripts/fingerprinting.js @@ -0,0 +1,367 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2015 Electronic Frontier Foundation + * + * Derived from Chameleon <https://github.com/ghostwords/chameleon> + * Copyright (C) 2015 ghostwords + * + * 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 <http://www.gnu.org/licenses/>. + */ + +function getFpPageScript() { + + // code below is not a content script: no chrome.* APIs ///////////////////// + + // return a string + return "(" + function (DOCUMENT, dispatchEvent, CUSTOM_EVENT, ERROR, DATE, setTimeout, OBJECT) { + + const V8_STACK_TRACE_API = !!(ERROR && ERROR.captureStackTrace); + + if (V8_STACK_TRACE_API) { + ERROR.stackTraceLimit = Infinity; // collect all frames + } else { + // from https://github.com/csnover/TraceKit/blob/b76ad786f84ed0c94701c83d8963458a8da54d57/tracekit.js#L641 + var geckoCallSiteRe = /^\s*(.*?)(?:\((.*?)\))?@?((?:file|https?|chrome):.*?):(\d+)(?::(\d+))?\s*$/i; + } + + var event_id = DOCUMENT.currentScript.getAttribute('data-event-id'); + + // from Underscore v1.6.0 + function debounce(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function () { + var last = DATE.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function () { + context = this; // eslint-disable-line consistent-this + args = arguments; + timestamp = DATE.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + } + + // messages the injected script + var send = (function () { + var messages = []; + + // debounce sending queued messages + var _send = debounce(function () { + dispatchEvent.call(DOCUMENT, new CUSTOM_EVENT(event_id, { + detail: messages + })); + + // clear the queue + messages = []; + }, 100); + + return function (msg) { + // queue the message + messages.push(msg); + + _send(); + }; + }()); + + /** + * Gets the stack trace by throwing and catching an exception. + * @returns {*} Returns the stack trace + */ + function getStackTraceFirefox() { + let stack; + + try { + throw new ERROR(); + } catch (err) { + stack = err.stack; + } + + return stack.split('\n'); + } + + /** + * Gets the stack trace using the V8 stack trace API: + * https://github.com/v8/v8/wiki/Stack-Trace-API + * @returns {*} Returns the stack trace + */ + function getStackTrace() { + let err = {}, + origFormatter, + stack; + + origFormatter = ERROR.prepareStackTrace; + ERROR.prepareStackTrace = function (_, structuredStackTrace) { + return structuredStackTrace; + }; + + ERROR.captureStackTrace(err, getStackTrace); + stack = err.stack; + + ERROR.prepareStackTrace = origFormatter; + + return stack; + } + + /** + * Strip away the line and column number (from stack trace urls) + * @param script_url The stack trace url to strip + * @returns {String} the pure URL + */ + function stripLineAndColumnNumbers(script_url) { + return script_url.replace(/:\d+:\d+$/, ''); + } + + /** + * Parses the stack trace for the originating script URL + * without using the V8 stack trace API. + * @returns {String} The URL of the originating script + */ + function getOriginatingScriptUrlFirefox() { + let trace = getStackTraceFirefox(); + + if (trace.length < 4) { + return ''; + } + + // this script is at 0, 1 and 2 + let callSite = trace[3]; + + let scriptUrlMatches = callSite.match(geckoCallSiteRe); + return scriptUrlMatches && scriptUrlMatches[3] || ''; + } + + /** + * Parses the stack trace for the originating script URL. + * @returns {String} The URL of the originating script + */ + function getOriginatingScriptUrl() { + let trace = getStackTrace(); + + if (OBJECT.prototype.toString.call(trace) == '[object String]') { + // we failed to get a structured stack trace + trace = trace.split('\n'); + // this script is at 0, 1, 2 and 3 + let script_url_matches = trace[4].match(/\((http.*:\d+:\d+)/); + // TODO do we need stripLineAndColumnNumbers (in both places) here? + return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(trace[4]); + } + + if (trace.length < 2) { + return ''; + } + + // this script is at 0 and 1 + let callSite = trace[2]; + + if (callSite.isEval()) { + // argh, getEvalOrigin returns a string ... + let eval_origin = callSite.getEvalOrigin(), + script_url_matches = eval_origin.match(/\((http.*:\d+:\d+)/); + + // TODO do we need stripLineAndColumnNumbers (in both places) here? + return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(eval_origin); + } else { + return callSite.getFileName(); + } + } + + /** + * Monitor the writes in a canvas instance + * @param item special item objects + */ + function trapInstanceMethod(item) { + var is_canvas_write = ( + item.propName == 'fillText' || item.propName == 'strokeText' + ); + + item.obj[item.propName] = (function (orig) { + // set to true after the first write, if the method is not + // restorable. Happens if another library also overwrites + // this method. + var skip_monitoring = false; + + function wrapped() { + var args = arguments; + + if (is_canvas_write) { + // to avoid false positives, + // bail if the text being written is too short, + // of if we've already sent a monitoring payload + if (skip_monitoring || !args[0] || args[0].length < 5) { + return orig.apply(this, args); + } + } + + var script_url = ( + V8_STACK_TRACE_API ? + getOriginatingScriptUrl() : + getOriginatingScriptUrlFirefox() + ), + msg = { + obj: item.objName, + prop: item.propName, + scriptUrl: script_url + }; + + if (item.hasOwnProperty('extra')) { + msg.extra = item.extra.apply(this, args); + } + + send(msg); + + if (is_canvas_write) { + // optimization: one canvas write is enough, + // restore original write method + // to this CanvasRenderingContext2D object instance + // Careful! Only restorable if we haven't already been replaced + // by another lib, such as the hidpi polyfill + if (this[item.propName] === wrapped) { + this[item.propName] = orig; + } else { + skip_monitoring = true; + } + } + + return orig.apply(this, args); + } + + OBJECT.defineProperty(wrapped, "name", { value: orig.name }); + OBJECT.defineProperty(wrapped, "length", { value: orig.length }); + OBJECT.defineProperty(wrapped, "toString", { value: orig.toString.bind(orig) }); + + return wrapped; + + }(item.obj[item.propName])); + } + + var methods = []; + + ['getImageData', 'fillText', 'strokeText'].forEach(function (method) { + var item = { + objName: 'CanvasRenderingContext2D.prototype', + propName: method, + obj: CanvasRenderingContext2D.prototype, + extra: function () { + return { + canvas: true + }; + } + }; + + if (method == 'getImageData') { + item.extra = function () { + var args = arguments, + width = args[2], + height = args[3]; + + // "this" is a CanvasRenderingContext2D object + if (width === undefined) { + width = this.canvas.width; + } + if (height === undefined) { + height = this.canvas.height; + } + + return { + canvas: true, + width: width, + height: height + }; + }; + } + + methods.push(item); + }); + + methods.push({ + objName: 'HTMLCanvasElement.prototype', + propName: 'toDataURL', + obj: HTMLCanvasElement.prototype, + extra: function () { + // "this" is a canvas element + return { + canvas: true, + width: this.width, + height: this.height + }; + } + }); + + methods.forEach(trapInstanceMethod); + + // save locally to keep from getting overwritten by site code + } + "(document, document.dispatchEvent, CustomEvent, Error, Date, setTimeout, Object));"; + + // code above is not a content script: no chrome.* APIs ///////////////////// + +} + +// 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; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "detectFingerprinting" +}, function (enabled) { + if (!enabled) { + return; + } + /** + * Communicating to webrequest.js + */ + var event_id = Math.random(); + + // listen for messages from the script we are about to insert + document.addEventListener(event_id, function (e) { + // pass these on to the background page + chrome.runtime.sendMessage({ + type: "fpReport", + data: e.detail + }); + }); + + window.injectScript(getFpPageScript(), { + event_id: event_id + }); +}); + +}()); diff --git a/src/js/contentscripts/socialwidgets.js b/src/js/contentscripts/socialwidgets.js new file mode 100644 index 0000000..14ae2b3 --- /dev/null +++ b/src/js/contentscripts/socialwidgets.js @@ -0,0 +1,641 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * 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 <http://www.gnu.org/licenses/>. + */ + +/* + * 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 = '<html><head><style>' + head_styles + '</style></head><body style="margin:0">' + widgetDiv.outerHTML + '</body></html>'; + + 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); +}); + +}()); diff --git a/src/js/contentscripts/supercookie.js b/src/js/contentscripts/supercookie.js new file mode 100644 index 0000000..0b15211 --- /dev/null +++ b/src/js/contentscripts/supercookie.js @@ -0,0 +1,151 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2015 Electronic Frontier Foundation + * + * Derived from Chameleon <https://github.com/ghostwords/chameleon> + * Copyright (C) 2015 ghostwords + * + * 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 <http://www.gnu.org/licenses/>. + */ + +/** + * Generate script to inject into the page + * + * @returns {string} + */ +function getScPageScript() { + // code below is not a content script: no chrome.* APIs ///////////////////// + + // return a string + return "(" + function () { + + /* + * If localStorage is inaccessible, such as when "Block third-party cookies" + * in enabled in Chrome or when `dom.storage.enabled` is set to `false` in + * Firefox, do not go any further. + */ + try { + // No localStorage raises an Exception in Chromium-based browsers, while + // it's equal to `null` in Firefox. + if (null === localStorage) { + throw false; + } + } catch (ex) { + return; + } + + (function (DOCUMENT, dispatchEvent, CUSTOM_EVENT, LOCAL_STORAGE, OBJECT, keys) { + + var event_id = DOCUMENT.currentScript.getAttribute('data-event-id-super-cookie'); + + /** + * send message to the content script + * + * @param {*} message + */ + var send = function (message) { + dispatchEvent.call(DOCUMENT, new CUSTOM_EVENT(event_id, { + detail: message + })); + }; + + /** + * Read HTML5 local storage and return contents + * @returns {Object} + */ + let getLocalStorageItems = function () { + let lsItems = {}; + for (let i = 0; i < LOCAL_STORAGE.length; i++) { + let key = LOCAL_STORAGE.key(i); + lsItems[key] = LOCAL_STORAGE.getItem(key); + } + return lsItems; + }; + + if (event_id) { // inserted script may run before the event_id is available + let localStorageItems = getLocalStorageItems(); + if (keys.call(OBJECT, localStorageItems).length) { + // send to content script + send({ localStorageItems }); + } + } + + // save locally to keep from getting overwritten by site code + } (document, document.dispatchEvent, CustomEvent, localStorage, Object, Object.keys)); + + } + "());"; + + // code above is not a content script: no chrome.* APIs ///////////////////// + +} + +// 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; +} + +// don't bother asking to run when trivially in first-party context +if (window.top == window) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 + +// TODO here we could also be injected too quickly +// and miss localStorage setting upon initial page load +// +// we should eventually switch injection back to document_start +// (reverting https://github.com/EFForg/privacybadger/pull/1522), +// and fix localstorage detection +// (such as by delaying it or peforming it periodically) +// +// could then remove test workarounds like +// https://github.com/EFForg/privacybadger/commit/39d5d0899e22d1c451d429e44553c5f9cad7fc46 + +// TODO sometimes contentscripts/utils.js isn't here?! +// TODO window.FRAME_URL / window.injectScript are undefined ... +chrome.runtime.sendMessage({ + type: "inspectLocalStorage", + frameUrl: window.FRAME_URL +}, function (enabledAndThirdParty) { + if (!enabledAndThirdParty) { + return; + } + + var event_id_super_cookie = Math.random(); + + // listen for messages from the script we are about to insert + document.addEventListener(event_id_super_cookie, function (e) { + // pass these on to the background page (handled by webrequest.js) + chrome.runtime.sendMessage({ + type: "supercookieReport", + data: e.detail, + frameUrl: window.FRAME_URL + }); + }); + + window.injectScript(getScPageScript(), { + event_id_super_cookie: event_id_super_cookie + }); + +}); + +}()); diff --git a/src/js/contentscripts/utils.js b/src/js/contentscripts/utils.js new file mode 100644 index 0000000..9f0a0fa --- /dev/null +++ b/src/js/contentscripts/utils.js @@ -0,0 +1,53 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2018 Electronic Frontier Foundation + * + * 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 <http://www.gnu.org/licenses/>. + */ + +/** + * Executes a script in the page's JavaScript context. + * + * @param {String} text The content of the script to insert. + * @param {Object} data Data attributes to set on the inserted script tag. + */ +window.injectScript = function (text, data) { + var parent = document.documentElement, + script = document.createElement('script'); + + script.text = text; + script.async = false; + + for (var key in data) { + script.setAttribute('data-' + key.replace(/_/g, '-'), data[key]); + } + + parent.insertBefore(script, parent.firstChild); + parent.removeChild(script); +}; + +function getFrameUrl() { + let url = document.location.href, + parentFrame = (document != window.top) && window.parent; + while (parentFrame && url && !url.startsWith("http")) { + try { + url = parentFrame.document.location.href; + } catch (ex) { + // ignore 'Blocked a frame with origin "..." + // from accessing a cross-origin frame.' exceptions + } + parentFrame = (parentFrame != window.top) && parentFrame.parent; + } + return url; +} +window.FRAME_URL = getFrameUrl(); |