diff options
Diffstat (limited to 'src/js/firstparties')
-rw-r--r-- | src/js/firstparties/facebook.js | 56 | ||||
-rw-r--r-- | src/js/firstparties/google-search.js | 39 | ||||
-rw-r--r-- | src/js/firstparties/google-static.js | 41 | ||||
-rw-r--r-- | src/js/firstparties/lib/utils.js | 62 |
4 files changed, 198 insertions, 0 deletions
diff --git a/src/js/firstparties/facebook.js b/src/js/firstparties/facebook.js new file mode 100644 index 0000000..2c5d299 --- /dev/null +++ b/src/js/firstparties/facebook.js @@ -0,0 +1,56 @@ +/* globals findInAllFrames:false, observeMutations:false */ +// Adapted from https://github.com/mgziminsky/FacebookTrackingRemoval +// this should only run on facebook.com, messenger.com, and +// facebookcorewwwi.onion +let fb_wrapped_link = `a[href*='${document.domain.split(".").slice(-2).join(".")}/l.php?']:not([aria-label])`; + +// remove all attributes from a link except for class and ARIA attributes +function cleanAttrs(elem) { + for (let i = elem.attributes.length - 1; i >= 0; --i) { + const attr = elem.attributes[i]; + if (attr.name !== 'class' && !attr.name.startsWith('aria-')) { + elem.removeAttribute(attr.name); + } + } +} + +// Remove excessive attributes and event listeners from link a and replace +// its destination with href +function cleanLink(a) { + let href = new URL(a.href).searchParams.get('u'); + + // If we can't extract a good URL, abort without breaking the links + if (!window.isURL(href)) { + return; + } + + let href_url = new URL(href); + href_url.searchParams.delete('fbclid'); + href = href_url.toString(); + + cleanAttrs(a); + a.href = href; + a.rel = "noreferrer"; + a.target = "_blank"; + a.addEventListener("click", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mousedown", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mouseup", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mouseover", function (e) { e.stopImmediatePropagation(); }, true); +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkEnabled" +}, function (enabled) { + if (!enabled) { + return; + } + + // unwrap wrapped links in the original page + findInAllFrames(fb_wrapped_link).forEach((link) => { + cleanLink(link); + }); + + // Execute redirect unwrapping each time new content is added to the page + observeMutations(fb_wrapped_link, cleanLink); +}); diff --git a/src/js/firstparties/google-search.js b/src/js/firstparties/google-search.js new file mode 100644 index 0000000..d5eea9a --- /dev/null +++ b/src/js/firstparties/google-search.js @@ -0,0 +1,39 @@ +/* globals findInAllFrames:false */ +// In Firefox, outbound google links have the `rwt(...)` mousedown trigger. +// In Chrome, they just have a `ping` attribute. +let trap_link = "a[onmousedown^='return rwt(this,'], a[ping]"; + +// Remove excessive attributes and event listeners from link a +function cleanLink(a) { + // remove all attributes except for href, + // target (to support "Open each selected result in a new browser window"), + // class, style and ARIA attributes + for (let i = a.attributes.length - 1; i >= 0; --i) { + const attr = a.attributes[i]; + if (attr.name !== 'href' && attr.name !== 'target' && + attr.name !== 'class' && attr.name !== 'style' && + !attr.name.startsWith('aria-')) { + a.removeAttribute(attr.name); + } + } + a.rel = "noreferrer noopener"; + + // block event listeners on the link + a.addEventListener("click", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mousedown", function (e) { e.stopImmediatePropagation(); }, true); +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkEnabled" +}, function (enabled) { + if (!enabled) { + return; + } + + // since the page is rendered all at once, no need to set up a + // mutationObserver or setInterval + findInAllFrames(trap_link).forEach((link) => { + cleanLink(link); + }); +}); diff --git a/src/js/firstparties/google-static.js b/src/js/firstparties/google-static.js new file mode 100644 index 0000000..cf73004 --- /dev/null +++ b/src/js/firstparties/google-static.js @@ -0,0 +1,41 @@ +let g_wrapped_link = "a[href^='https://www.google.com/url?']"; + +// Unwrap a Hangouts tracking link +function unwrapLink(a) { + let href = new URL(a.href).searchParams.get('q'); + if (!window.isURL(href)) { + return; + } + + // remove all attributes except for target, class, style and aria-* + // attributes. This should prevent the script from breaking styles and + // features for people with disabilities. + for (let i = a.attributes.length - 1; i >= 0; --i) { + const attr = a.attributes[i]; + if (attr.name !== 'target' && attr.name !== 'class' && + attr.name !== 'style' && !attr.name.startsWith('aria-')) { + a.removeAttribute(attr.name); + } + } + + a.rel = "noreferrer"; + a.href = href; +} + +// Scan the page for all wrapped links +function unwrapAll() { + document.querySelectorAll(g_wrapped_link).forEach((a) => { + unwrapLink(a); + }); +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkEnabled" +}, function (enabled) { + if (!enabled) { + return; + } + unwrapAll(); + setInterval(unwrapAll, 2000); +}); diff --git a/src/js/firstparties/lib/utils.js b/src/js/firstparties/lib/utils.js new file mode 100644 index 0000000..3edb0c8 --- /dev/null +++ b/src/js/firstparties/lib/utils.js @@ -0,0 +1,62 @@ +window.isURL = function(url) { + // ensure the URL starts with HTTP or HTTPS; otherwise we might be vulnerable + // to XSS attacks + return (url && (url.startsWith("https://") || url.startsWith("http://"))); +}; + +/** + * Search a window and all IFrames within it for a query selector, then return a + * list of all the elements in any frame that match. + */ +window.findInAllFrames = function(query) { + let out = []; + document.querySelectorAll(query).forEach((node) => { + out.push(node); + }); + Array.from(document.getElementsByTagName('iframe')).forEach((iframe) => { + try { + iframe.contentDocument.querySelectorAll(query).forEach((node) => { + out.push(node); + }); + } catch (e) { + // pass on cross origin iframe errors + } + }); + return out; +}; + +/** + * Listen for mutations on a page. If any nodes that match `selector` are added + * to the page, execute the function `callback` on them. + * Used by first-party scripts to listen for new links being added to the page + * and strip them of tracking code immediately. + */ +window.observeMutations = function(selector, callback) { + // Check all new nodes added by a mutation for tracking links and unwrap them + function onMutation(mutation) { + if (!mutation.addedNodes.length) { + return; + } + for (let node of mutation.addedNodes) { + // Only act on element nodes, otherwise querySelectorAll won't work + if (node.nodeType != Node.ELEMENT_NODE) { + continue; + } + + // check all child nodes against the selector first + node.querySelectorAll(selector).forEach((element) => { + callback(element); + }); + + // then check the node itself + if (node.matches(selector)) { + callback(node); + } + } + } + + // Set up a mutation observer with the constructed callback + new MutationObserver(function(mutations) { + mutations.forEach(onMutation); + }).observe(document, {childList: true, subtree: true, attributes: false, characterData: false}); +}; |