summaryrefslogtreecommitdiffstats
path: root/src/js/contentscripts
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/contentscripts')
-rw-r--r--src/js/contentscripts/clobbercookie.js60
-rw-r--r--src/js/contentscripts/clobberlocalstorage.js94
-rw-r--r--src/js/contentscripts/collapser.js56
-rw-r--r--src/js/contentscripts/dnt.js66
-rw-r--r--src/js/contentscripts/fingerprinting.js367
-rw-r--r--src/js/contentscripts/socialwidgets.js641
-rw-r--r--src/js/contentscripts/supercookie.js151
-rw-r--r--src/js/contentscripts/utils.js53
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();