diff --git a/src/data/socialwidgets.json b/src/data/socialwidgets.json index bfc9499a54..b1c3a46425 100644 --- a/src/data/socialwidgets.json +++ b/src/data/socialwidgets.json @@ -146,7 +146,9 @@ "Google reCAPTCHA": { "domains": [ "google.com", - "www.google.com" + "www.google.com", + "recaptcha.net", + "www.recaptcha.net" ], "buttonSelectors": [ "div.g-recaptcha", @@ -156,13 +158,17 @@ "scriptSelectors": [ "script[src^='//google.com/recaptcha/api.js']", "script[src^='https://google.com/recaptcha/api.js']", - "script[src^='https://www.google.com/recaptcha/api.js']" + "script[src^='https://www.google.com/recaptcha/api.js']", + "script[src^='//recaptcha.net/recaptcha/api.js']", + "script[src^='https://recaptcha.net/recaptcha/api.js']", + "script[src^='https://www.recaptcha.net/recaptcha/api.js']" ], - "fallbackScriptUrl": "//google.com/recaptcha/api.js", "replacementButton": { "unblockDomains": [ "google.com", - "www.google.com" + "www.google.com", + "recaptcha.net", + "www.recaptcha.net" ], "type": 4 } @@ -203,6 +209,18 @@ "type": 0 } }, + "Rumble Video Player": { + "domain": "rumble.com", + "buttonSelectors": [ + "iframe[src^='https://rumble.com/embed/']" + ], + "replacementButton": { + "unblockDomains": [ + "rumble.com" + ], + "type": 3 + } + }, "SoundCloud": { "domain": "w.soundcloud.com", "buttonSelectors": [ diff --git a/src/data/surrogates.js b/src/data/surrogates.js index d504270a11..91e00edf24 100644 --- a/src/data/surrogates.js +++ b/src/data/surrogates.js @@ -18,6 +18,8 @@ require.scopes.surrogatedb = (function () { const MATCH_SUFFIX = 'suffix', + MATCH_PREFIX = 'prefix', + MATCH_PREFIX_WITH_PARAMS = 'prefix_params', MATCH_ANY = 'any'; /** @@ -86,6 +88,38 @@ const hostnames = { '/apstag.js', ] }, + 'rumble.com': { + match: MATCH_PREFIX, + tokens: [ + '/embedJS/', + ], + widgetName: "Rumble Video Player" + }, + 'www.google.com': { + match: MATCH_PREFIX_WITH_PARAMS, + params: { + onload: true, + //render: "explicit", + render: true, + }, + tokens: [ + '/recaptcha/api.js', + '/recaptcha/enterprise.js', + ], + widgetName: "Google reCAPTCHA" + }, + 'www.recaptcha.net': { + match: MATCH_PREFIX_WITH_PARAMS, + params: { + onload: true, + render: true, + }, + tokens: [ + '/recaptcha/api.js', + '/recaptcha/enterprise.js', + ], + widgetName: "Google reCAPTCHA" + }, }; /** @@ -121,6 +155,11 @@ const surrogates = { '/apstag.js': 'amazon_apstag.js', + '/embedJS/': 'rumble_embedjs.js', + + '/recaptcha/api.js': 'grecaptcha.js', + '/recaptcha/enterprise.js': 'grecaptcha_enterprise.js', + 'noopjs': 'noop.js' }; @@ -132,6 +171,8 @@ Object.keys(surrogates).forEach(key => { const exports = { MATCH_ANY, + MATCH_PREFIX, + MATCH_PREFIX_WITH_PARAMS, MATCH_SUFFIX, hostnames, surrogates, diff --git a/src/data/web_accessible_resources/grecaptcha.js b/src/data/web_accessible_resources/grecaptcha.js new file mode 100644 index 0000000000..75d88a1dd6 --- /dev/null +++ b/src/data/web_accessible_resources/grecaptcha.js @@ -0,0 +1,31 @@ +(function () { + + let script_src = document.currentScript.src; + + window.grecaptcha = { + render: function (container) { + if (Object.prototype.toString.call(container) != "[object String]") { + if (!container.id) { + container.id = "grecaptcha-" + Math.random().toString().replace(".", ""); + } + container = container.id; + } + document.dispatchEvent(new CustomEvent("pbSurrogateMessage", { + detail: { + type: "widgetFromSurrogate", + name: "Google reCAPTCHA", + widgetData: { + domId: container, + scriptUrl: script_src + } + } + })); + } + }; + + let onload = (new URL(script_src)).searchParams.get('onload'); + if (onload && onload in window) { + window[onload](); + } + +}()); diff --git a/src/data/web_accessible_resources/grecaptcha_enterprise.js b/src/data/web_accessible_resources/grecaptcha_enterprise.js new file mode 100644 index 0000000000..b0d870bdbf --- /dev/null +++ b/src/data/web_accessible_resources/grecaptcha_enterprise.js @@ -0,0 +1,37 @@ +(function () { + + let script_src = document.currentScript.src; + + window.grecaptcha = {}; + + window.grecaptcha.enterprise = { + ready: function (cb) { + cb(); + }, + render: function (container) { + if (Object.prototype.toString.call(container) != "[object String]") { + if (!container.id) { + container.id = "grecaptcha-" + Math.random().toString().replace(".", ""); + } + container = container.id; + } + document.dispatchEvent(new CustomEvent("pbSurrogateMessage", { + detail: { + type: "widgetFromSurrogate", + name: "Google reCAPTCHA", + widgetData: { + domId: container, + scriptUrl: script_src + } + } + })); + }, + execute: function () {} + }; + + let onload = (new URL(script_src)).searchParams.get('onload'); + if (onload && onload in window) { + window[onload](); + } + +}()); diff --git a/src/data/web_accessible_resources/rumble_embedjs.js b/src/data/web_accessible_resources/rumble_embedjs.js new file mode 100644 index 0000000000..69ff92a4eb --- /dev/null +++ b/src/data/web_accessible_resources/rumble_embedjs.js @@ -0,0 +1,38 @@ +(function () { + if ("Rumble" in window && "_" in window.Rumble) { + for (let args of window.Rumble._) { + args = [].slice.apply(args); + + let cmd = args[0], + conf = args[1], + script_src = document.currentScript.src, + idx = script_src.indexOf("/embedJS/"), + pub_code; + + if (idx != -1) { + script_src = script_src.slice(idx + "/embedJS/".length); + idx = script_src.indexOf("/"); + if (idx != -1) { + script_src = script_src.slice(0, idx); + idx = script_src.indexOf("."); + if (idx != -1) { + pub_code = script_src.slice(0, idx); + } + } + } + + if (pub_code && cmd == "play" && "div" in conf && "video" in conf) { + document.dispatchEvent(new CustomEvent("pbSurrogateMessage", { + detail: { + type: "widgetFromSurrogate", + name: "Rumble Video Player", + widgetData: { + pubCode: pub_code, + args + } + } + })); + } + } + } +}()); diff --git a/src/js/background.js b/src/js/background.js index a3cec271b8..dae1ed6018 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -233,6 +233,8 @@ Badger.prototype = { : { url: {String} host: {String} + widgetReplacementReady: {Boolean} + widgetQueue: {Array} widget objects warAccessTokens: { : {String} access token ... diff --git a/src/js/contentscripts/socialwidgets.js b/src/js/contentscripts/socialwidgets.js index f9db1da268..1ce9b1e56b 100644 --- a/src/js/contentscripts/socialwidgets.js +++ b/src/js/contentscripts/socialwidgets.js @@ -81,10 +81,17 @@ function init(response) { // set up listener for dynamically created widgets chrome.runtime.onMessage.addListener(function (request) { + // blocked something, see if this is a widget domain that should be replaced if (request.type == "replaceWidget") { if (request.frameId === FRAME_ID) { replaceSubsequentTrackerButtonsHelper(request.trackerDomain); } + + // widget replacement initiated by a surrogate script + } else if (request.type == "replaceWidgetFromSurrogate") { + if (request.frameId === FRAME_ID) { + replaceIndividualButton(request.widget); + } } }); } @@ -197,32 +204,16 @@ function _createButtonReplacement(widget, callback) { function _createWidgetReplacement(widget, trackerElem, callback) { let replacementEl; - // in-place widget type: + // in-place widget types: + // + // type 3: // reinitialize the widget by reinserting its element's HTML - if (widget.replacementButton.type == 3) { - replacementEl = createReplacementWidget( - widget, trackerElem, reinitializeWidgetAndUnblockTracker); - - // in-place widget type: + // + // type 4: // 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); + if ([3, 4].includes(widget.replacementButton.type)) { + replacementEl = createReplacementWidget(widget, trackerElem); } callback(replacementEl); @@ -280,32 +271,30 @@ function replaceButtonWithHtmlCodeAndUnblockTracker(button, widget_name, html) { * Unblocks the given widget and replaces our replacement placeholder * with the original third-party widget element. * - * The teardown to the initialization defined in createReplacementWidget(). + * Reruns scripts defined in scriptSelectors, if any. * - * @param {String} name the name/type of this widget (SoundCloud, Vimeo etc.) + * The teardown to the initialization defined in createReplacementWidget(). */ -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] = []; - }); -} +function restoreWidget(widget) { + let name = widget.name; + + if (widget.scriptSelectors) { + if (widget.scriptSelectors.some(i => i.includes("onload\\=vueRecaptchaApiLoaded"))) { + // we can't do "in-place" activation; reload the page instead + unblockTracker(name, function () { + location.reload(); + }); + return; + } + } -/** - * 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); + if (data.scriptSelectors) { + reloadScripts(data.scriptSelectors); + } }); WIDGET_ELS[name] = []; }); @@ -314,18 +303,9 @@ function replaceWidgetAndReloadScripts(name) { /** * Find and replace script elements with their copies to trigger re-running. */ -function reloadScripts(selectors, fallback_script_url) { +function reloadScripts(selectors) { 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') { @@ -408,7 +388,7 @@ function _make_id(prefix) { return prefix + "-" + Math.random().toString().replace(".", ""); } -function createReplacementWidget(widget, elToReplace, activationFn) { +function createReplacementWidget(widget, elToReplace) { let name = widget.name; let widgetFrame = document.createElement('iframe'); @@ -468,8 +448,10 @@ function createReplacementWidget(widget, elToReplace, activationFn) { // get a direct link to widget content when available let widget_url; - // use the frame URL for framed widgets - if (elToReplace.nodeName.toLowerCase() == 'iframe' && elToReplace.src) { + if (widget.directLinkUrl) { + widget_url = widget.directLinkUrl; + } else if (elToReplace.nodeName.toLowerCase() == 'iframe' && elToReplace.src) { + // use the frame URL for framed widgets widget_url = elToReplace.src; } @@ -568,9 +550,6 @@ function createReplacementWidget(widget, elToReplace, activationFn) { }; if (widget.scriptSelectors) { data.scriptSelectors = widget.scriptSelectors; - if (widget.fallbackScriptUrl) { - data.fallbackScriptUrl = widget.fallbackScriptUrl; - } } WIDGET_ELS[name].push(data); @@ -582,11 +561,13 @@ function createReplacementWidget(widget, elToReplace, activationFn) { onceButton.addEventListener("click", function (e) { if (!e.isTrusted) { return; } e.preventDefault(); - activationFn(name); + restoreWidget(widget); }, { once: true }); siteButton.addEventListener("click", function (e) { - if (!e.isTrusted) { return; } + if (!e.isTrusted) { + return; + } e.preventDefault(); @@ -596,7 +577,7 @@ function createReplacementWidget(widget, elToReplace, activationFn) { type: "allowWidgetOnSite", widgetName: name }, function () { - activationFn(name); + restoreWidget(widget); }); }, { once: true }); @@ -665,6 +646,17 @@ a:hover { * Replaces buttons/widgets in the DOM. */ function replaceIndividualButton(widget) { + // for script type widgets, + // to avoid breaking lazy loaded widgets + // by replacing the DOM element too early + // first check whether a script is actually present + if (widget.replacementButton.type == 4) { + let script_selector = widget.scriptSelectors.join(','); + if (!document.querySelectorAll(script_selector).length) { + return; + } + } + let selector = widget.buttonSelectors.join(','), elsToReplace = document.querySelectorAll(selector); @@ -699,7 +691,12 @@ chrome.runtime.sendMessage({ if (!response) { return; } + init(response); + + chrome.runtime.sendMessage({ + type: "widgetReplacementReady" + }); }); }()); diff --git a/src/js/contentscripts/utils.js b/src/js/contentscripts/utils.js index 67de710b89..247011a9dc 100644 --- a/src/js/contentscripts/utils.js +++ b/src/js/contentscripts/utils.js @@ -15,6 +15,8 @@ * along with Privacy Badger. If not, see . */ +(function () { + /** * Executes a script in the page's JavaScript context. * @@ -47,3 +49,35 @@ function getFrameUrl() { return url; } window.FRAME_URL = getFrameUrl(); + +// 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; +} + +// END FUNCTION DEFINITIONS /////////////////////////////////////////////////// + +// register listener in top-level frames only for now +// NOTE: before removing this restriction, +// investigate implications of third-party scripts in nested frames +// generating pbSurrogateMessage events +if (window.top != window) { + return; +} + +document.addEventListener("pbSurrogateMessage", function (e) { + if (e.detail && e.detail.type == "widgetFromSurrogate") { + chrome.runtime.sendMessage({ + type: "widgetFromSurrogate", + name: e.detail.name, + data: e.detail.widgetData, + frameUrl: window.FRAME_URL + }); + } +}); + +}()); diff --git a/src/js/surrogates.js b/src/js/surrogates.js index 4fb9e11022..ced6db41c8 100644 --- a/src/js/surrogates.js +++ b/src/js/surrogates.js @@ -17,7 +17,21 @@ require.scopes.surrogates = (function () { -const db = require('surrogatedb'); +const db = require('surrogatedb'), + utils = require('utils'); + +const WIDGET_SURROGATES = utils.filter(db.hostnames, item => !!item.widgetName); + +function _match_prefix(url, hostname, tokens) { + let path_onwards = url.slice(url.indexOf(hostname) + hostname.length); + for (const token of tokens) { + if (path_onwards.startsWith(token)) { + return db.surrogates[token]; + } + } + + return false; +} /** * Blocking tracking scripts (trackers) can cause parts of webpages to break. @@ -80,6 +94,39 @@ function getSurrogateUri(script_url, script_hostname) { return false; } + // one or more prefix tokens: + // does the script URL's path component begin with one of these tokens? + case db.MATCH_PREFIX: { + return _match_prefix(script_url, script_hostname, conf.tokens); + } + + // MATCH_PREFIX with querystring parameter matching + case db.MATCH_PREFIX_WITH_PARAMS: { + let surl = _match_prefix(script_url, script_hostname, conf.tokens); + + if (!surl) { + return false; + } + + // check every key/value pair in conf.params against the querystring + let qs = (new URL(script_url)).searchParams; + for (let [key, value] of Object.entries(conf.params)) { + // is the key present? + if (value === true) { + if (!qs.get(key)) { + return false; + } + // is the key present and do the values match? + } else if (utils.isString(value)) { + if (qs.get(key) !== value) { + return false; + } + } + } + + return surl; + } + } return false; @@ -87,7 +134,9 @@ function getSurrogateUri(script_url, script_hostname) { const exports = { getSurrogateUri, + WIDGET_SURROGATES }; return exports; -})(); + +}()); diff --git a/src/js/utils.js b/src/js/utils.js index 32d965c268..4a81a0c0c9 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -575,6 +575,22 @@ function invert(obj) { return result; } +/** + * Array.prototype.filter() for objects. + * + * @param {Object} obj + * @param {Function} cb receives two arguments: current value, current key + */ +function filter(obj, cb) { + let memo = {}; + for (let [key, value] of Object.entries(obj)) { + if (cb(value, key)) { + memo[key] = value; + } + } + return memo; +} + /************************************** exports */ let exports = { arrayBufferToBase64, @@ -583,6 +599,7 @@ let exports = { difference, estimateMaxEntropy, explodeSubdomains, + filter, findCommonSubstrings, firstPartyProtectionsEnabled, getHostFromDomainInput, diff --git a/src/js/webrequest.js b/src/js/webrequest.js index fc043fe4aa..5b9b2a743a 100644 --- a/src/js/webrequest.js +++ b/src/js/webrequest.js @@ -28,8 +28,8 @@ require.scopes.webrequest = (function () { /*********************** webrequest scope **/ let constants = require("constants"), - getSurrogateUri = require("surrogates").getSurrogateUri, incognito = require("incognito"), + surrogates = require("surrogates"), utils = require("utils"); /************ Local Variables *****************/ @@ -102,7 +102,18 @@ function onBeforeRequest(details) { } if (type == 'script') { - let surrogate = getSurrogateUri(url, request_host); + let surrogate; + + if (surrogates.WIDGET_SURROGATES.hasOwnProperty(request_host)) { + let settings = badger.getSettings(); + if (settings.getItem("socialWidgetReplacementEnabled") && !settings.getItem('widgetReplacementExceptions').includes(surrogates.WIDGET_SURROGATES[request_host].widgetName)) { + surrogate = surrogates.getSurrogateUri(url, request_host); + } + + } else { + surrogate = surrogates.getSurrogateUri(url, request_host); + } + if (surrogate) { let secret = getWarSecret(tab_id, frame_id, surrogate); return { @@ -921,6 +932,79 @@ function initAllowedWidgets(tab_id, tab_host) { } } +/** + * Generates widget objects for surrogate-initiated widgets. + * + * @param {String} name UNTRUSTED widget name + * @param {Object} data UNTRUSTED widget-specific data + * @param {String} frame_url containing frame URL, used by some widgets + * + * @returns {Object|false} + */ +function getSurrogateWidget(name, data, frame_url) { + const OK = /^[A-Za-z0-9_-]+$/; + + if (name == "Rumble Video Player") { + // validate + if (!data || !data.args || data.args[0] != "play") { + return false; + } + + let pub_code = data.pubCode, + { video, div } = data.args[1]; + + if (!OK.test(pub_code) || !OK.test(video) || !OK.test(div)) { + return false; + } + + let argsParam = [ "play", { video, div } ]; + + let script_url = `https://rumble.com/embedJS/${encodeURIComponent(pub_code)}.${encodeURIComponent(video)}/?url=${encodeURIComponent(frame_url)}&args=${encodeURIComponent(JSON.stringify(argsParam))}`; + + return { + name, + buttonSelectors: ["div#" + div], + scriptSelectors: [`script[src='${CSS.escape(script_url)}']`], + replacementButton: { + "unblockDomains": ["rumble.com"], + "type": 4 + }, + directLinkUrl: `https://rumble.com/embed/${encodeURIComponent(pub_code)}.${encodeURIComponent(video)}/` + }; + } + + if (name == "Google reCAPTCHA") { + const KNOWN_GRECAPTCHA_SCRIPTS = [ + "https://www.google.com/recaptcha/", + "https://www.recaptcha.net/recaptcha/", + ]; + + // validate + if (!data || !data.domId || !data.scriptUrl) { + return false; + } + + let dom_id = data.domId, + script_url = data.scriptUrl; + + if (!OK.test(dom_id) || !KNOWN_GRECAPTCHA_SCRIPTS.some(s => script_url.startsWith(s))) { + return false; + } + + return { + name, + buttonSelectors: ["#" + dom_id], + scriptSelectors: [`script[src='${CSS.escape(script_url)}']`], + replacementButton: { + "unblockDomains": ["www.google.com"], + "type": 4 + } + }; + } + + return false; +} + // NOTE: sender.tab is available for content script (not popup) messages only function dispatcher(request, sender, sendResponse) { @@ -931,8 +1015,8 @@ function dispatcher(request, sender, sendResponse) { const KNOWN_CONTENT_SCRIPT_MESSAGES = [ "allowWidgetOnSite", "checkDNT", - "checkFloc", "checkEnabled", + "checkFloc", "checkLocation", "checkWidgetReplacementEnabled", "detectFingerprinting", @@ -942,6 +1026,8 @@ function dispatcher(request, sender, sendResponse) { "inspectLocalStorage", "supercookieReport", "unblockWidget", + "widgetFromSurrogate", + "widgetReplacementReady", ]; if (!KNOWN_CONTENT_SCRIPT_MESSAGES.includes(request.type)) { console.error("Rejected unknown message %o from %s", request, sender.url); @@ -1405,8 +1491,9 @@ function dispatcher(request, sender, sendResponse) { break; } + // called from contentscripts/dnt.js + // to check if it should set DNT on Navigator case "checkDNT": { - // called from contentscripts/dnt.js to check if we should enable it sendResponse( badger.isDNTSignalEnabled() && badger.isPrivacyBadgerEnabled( @@ -1416,13 +1503,73 @@ function dispatcher(request, sender, sendResponse) { break; } + // called from contentscripts/floc.js + // to check if we should disable document.interestCohort case "checkFloc": { - // called from contentscripts/floc.js - // to check if we should disable document.interestCohort sendResponse(badger.isFlocOverwriteEnabled()); break; } + // proxies surrogate script-initiated widget replacement messages + // from one content script to another + case "widgetFromSurrogate": { + let tab_host = window.extractHostFromURL(sender.tab.url); + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + break; + } + + // NOTE: request.name and request.data are not to be trusted + // https://github.com/w3c/webextensions/issues/57#issuecomment-914491167 + // https://github.com/w3c/webextensions/issues/78#issuecomment-921058071 + let widget = getSurrogateWidget(request.name, request.data, request.frameUrl); + + if (!widget) { + break; + } + + let frameData = badger.getFrameData(sender.tab.id, sender.frameId); + + if (frameData.widgetReplacementReady) { + // message the content script if it's ready for messages + chrome.tabs.sendMessage(sender.tab.id, { + type: "replaceWidgetFromSurrogate", + frameId: sender.frameId, + widget + }); + } else { + // save the message for later otherwise + if (!frameData.hasOwnProperty("widgetQueue")) { + frameData.widgetQueue = []; + } + frameData.widgetQueue.push(widget); + } + + break; + } + + // marks the widget replacement script in a certain tab/frame + // ready for messages; sends any previously saved messages + case "widgetReplacementReady": { + let frameData = badger.getFrameData(sender.tab.id, sender.frameId); + if (!frameData) { + break; + } + + frameData.widgetReplacementReady = true; + if (frameData.widgetQueue) { + for (let widget of frameData.widgetQueue) { + chrome.tabs.sendMessage(sender.tab.id, { + type: "replaceWidgetFromSurrogate", + frameId: sender.frameId, + widget + }); + } + delete frameData.widgetQueue; + } + + break; + } + } } diff --git a/src/tests/tests/utils.js b/src/tests/tests/utils.js index 7e9df1886e..4e0979efe8 100644 --- a/src/tests/tests/utils.js +++ b/src/tests/tests/utils.js @@ -177,6 +177,176 @@ QUnit.test("getSurrogateUri() suffix tokens", function (assert) { ); }); +QUnit.test("getSurrogateUri() prefix tokens", function (assert) { + const TEST_FQDN = "www.example.com", + TEST_TOKEN = "/foo"; + + const TESTS = [ + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?bar`, + expected: true, + msg: "token at start of path should match" + }, + { + url: `https://${window.getBaseDomain(TEST_FQDN)}${TEST_TOKEN}`, + expected: false, + msg: "should not match (same base domain, but different FQDN)" + }, + { + url: `https://${TEST_FQDN}/bar${TEST_TOKEN}/bar`, + expected: false, + msg: "should not match (token in path but not at start)" + }, + { + url: `https://${TEST_FQDN}/bar${TEST_TOKEN}`, + expected: false, + msg: "should not match (token in path but at end)" + }, + { + url: `https://${TEST_FQDN}/?${TEST_TOKEN}`, + expected: false, + msg: "should not match (token in querystring)" + }, + ]; + + // set up test data for prefix token tests + surrogatedb.hostnames[TEST_FQDN] = { + match: surrogatedb.MATCH_PREFIX, + tokens: [TEST_TOKEN] + }; + surrogatedb.surrogates[TEST_TOKEN] = surrogatedb.surrogates.noopjs; + + for (let test of TESTS) { + let surrogate = getSurrogateUri(test.url, + window.extractHostFromURL(test.url)); + if (test.expected) { + assert.ok(surrogate, test.msg); + if (surrogate) { + assert.equal(surrogate, surrogatedb.surrogates.noopjs, + "got the noop surrogate extension URL"); + } + } else { + assert.notOk(surrogate, test.msg); + } + } +}); + +QUnit.test("getSurrogateUri() prefix tokens with querystring parameters", function (assert) { + const TEST_FQDN = "www.example.com", + TEST_TOKEN = "/foo"; + + const TESTS = [ + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?foo=bar`, + params: { + foo: true + }, + expected: true, + msg: "foo is present" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?another=123`, + params: { + foo: true + }, + expected: false, + msg: "foo is missing" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?foo=baz`, + params: { + foo: true + }, + expected: true, + msg: "foo is present with some other value" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?foo=baz`, + params: { + foo: "baz" + }, + expected: true, + msg: "foo is present with expected value" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?foo=bar`, + params: { + foo: "baz" + }, + expected: false, + msg: "foo is present with unexpected value" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?another=123&foo=bar`, + params: { + another: true, + foo: "bar" + }, + expected: true, + msg: "two parameters match" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?foo=bar&another=123`, + params: { + another: true, + foo: "bar" + }, + expected: true, + msg: "order shouldn't matter" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?another=123&foo=bar`, + params: { + another: true, + foo: "baz" + }, + expected: false, + msg: "two parameters, one fails to match" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?foo=baz`, + params: { + another: true, + foo: "baz" + }, + expected: false, + msg: "two parameters, one is missing" + }, + { + url: `https://${TEST_FQDN}${TEST_TOKEN}?another=123&foo=baz`, + params: { + foo: "baz", + }, + expected: true, + msg: "unspecified parameters are ignored" + }, + ]; + + // set up test data for prefix token tests + surrogatedb.surrogates[TEST_TOKEN] = surrogatedb.surrogates.noopjs; + + for (let test of TESTS) { + // update test data with querystring parameter rules for current test + surrogatedb.hostnames[TEST_FQDN] = { + match: surrogatedb.MATCH_PREFIX_WITH_PARAMS, + params: test.params, + tokens: [TEST_TOKEN] + }; + + let surrogate = getSurrogateUri(test.url, + window.extractHostFromURL(test.url)); + if (test.expected) { + assert.ok(surrogate, test.msg); + if (surrogate) { + assert.equal(surrogate, surrogatedb.surrogates.noopjs, + "got the noop surrogate extension URL"); + } + } else { + assert.notOk(surrogate, test.msg); + } + } +}); + QUnit.test("getSurrogateUri() wildcard tokens", function (assert) { // set up test data for wildcard token tests surrogatedb.hostnames['cdn.example.com'] = {