From 56147009fcfd96d88428a21697d8dd352696c0bc Mon Sep 17 00:00:00 2001 From: David Byers <david.byers@liu.se> Date: Tue, 2 Feb 2021 18:03:08 +0100 Subject: [PATCH] First version of Firefox extension. --- .../_locales/en/messages.json | 0 .../_locales/sv/messages.json | 0 firefox/background.js | 42 ++++ {extension => firefox}/common.js | 0 firefox/content.js | 208 ++++++++++++++++++ firefox/icon.svg | 22 ++ firefox/manifest.json | 38 ++++ {extension => firefox}/style.css | 0 thunderbird/_locales/en/messages.json | 11 + thunderbird/_locales/sv/messages.json | 11 + {extension => thunderbird}/background.js | 0 thunderbird/common.js | 99 +++++++++ {extension => thunderbird}/compose.js | 0 {extension => thunderbird}/display.js | 0 {extension => thunderbird}/icon.svg | 0 {extension => thunderbird}/manifest.json | 2 +- thunderbird/style.css | 41 ++++ 17 files changed, 473 insertions(+), 1 deletion(-) rename {extension => firefox}/_locales/en/messages.json (100%) rename {extension => firefox}/_locales/sv/messages.json (100%) create mode 100644 firefox/background.js rename {extension => firefox}/common.js (100%) create mode 100644 firefox/content.js create mode 100644 firefox/icon.svg create mode 100644 firefox/manifest.json rename {extension => firefox}/style.css (100%) create mode 100644 thunderbird/_locales/en/messages.json create mode 100644 thunderbird/_locales/sv/messages.json rename {extension => thunderbird}/background.js (100%) create mode 100644 thunderbird/common.js rename {extension => thunderbird}/compose.js (100%) rename {extension => thunderbird}/display.js (100%) rename {extension => thunderbird}/icon.svg (100%) rename {extension => thunderbird}/manifest.json (88%) create mode 100644 thunderbird/style.css diff --git a/extension/_locales/en/messages.json b/firefox/_locales/en/messages.json similarity index 100% rename from extension/_locales/en/messages.json rename to firefox/_locales/en/messages.json diff --git a/extension/_locales/sv/messages.json b/firefox/_locales/sv/messages.json similarity index 100% rename from extension/_locales/sv/messages.json rename to firefox/_locales/sv/messages.json diff --git a/firefox/background.js b/firefox/background.js new file mode 100644 index 0000000..a0d1cf2 --- /dev/null +++ b/firefox/background.js @@ -0,0 +1,42 @@ +// Microsoft ATP Safe Links Cleaner +// Copyright 2021 David Byers <david.byers@liu.se> +// +// 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. + +// Background scripts + + +console.log('enter background'); + +browser.menus.create({ + id: "liu-safelinks-copy", + title: browser.i18n.getMessage("copyLinkMenuTitle"), + contexts: ["link"], + visible: true, + targetUrlPatterns: ["*://*.safelinks.protection.outlook.com/*"], +}); + +browser.menus.onClicked.addListener((info, tab) => { + if (info.menuItemId == "liu-safelinks-copy") { + navigator.clipboard.writeText(untangleLink(info.linkUrl)); + } +}); + + +console.log('exit background'); diff --git a/extension/common.js b/firefox/common.js similarity index 100% rename from extension/common.js rename to firefox/common.js diff --git a/firefox/content.js b/firefox/content.js new file mode 100644 index 0000000..5e9a958 --- /dev/null +++ b/firefox/content.js @@ -0,0 +1,208 @@ +// Microsoft ATP Safe Links Cleaner +// Copyright 2021 David Byers <david.byers@liu.se> +// +// 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. + +// Display script + + +let currentPopupTarget = null; +let hidePopupTimeout = null; +let mutationObserver = null; + + +function enableMutationObserver() { + console.log('enter enableMutationObserver'); + if (!mutationObserver) { + mutationObserver = new MutationObserver(withoutMutationObserver(fixAllTheLinks)); + } + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + console.log('exit enableMutationObserver'); +} + +function disableMutationObserver() { + console.log('enter disableMutationObserver'); + if (mutationObserver) { + mutationObserver.disconnect(); + } + console.log('exit disableMutationObserver'); +} + +function withoutMutationObserver(func) { + return (...args) => { + try { + disableMutationObserver(); + func(...args); + } + finally { + enableMutationObserver(); + } + } +} + + + +/** + * Return the popup div element, creating it if necessary. + * @returns {Element} The popup element. + */ +function getPopup() { + let popup = document.getElementById(safelinksPopupId); + if (!popup) { + popupElementLocked = false; + popup = document.createElement('div'); + popup.id = safelinksPopupId; + popup.addEventListener('mouseenter', cancelHidePopup, {passive: true}); + popup.addEventListener('mouseleave', scheduleHidePopup, {passive: true}); + document.body.appendChild(popup); + } + return popup; +} + + +/** + * Cancel hiding the popup (if it has been scheduled) and set + * hidePopupTimeout to null. + */ +function cancelHidePopup() { + if (hidePopupTimeout) { + clearTimeout(hidePopupTimeout); + hidePopupTimeout = null; + } +} + + +/** + * Hide the current popup. If there is no popup, one will be created. + */ +function hidePopup() { + cancelHidePopup(); + getPopup().classList.remove(safelinksPopupVisibleClass); + currentPopupTarget = undefined; +} + + +/** + * Schedule hiding the current popup. + */ +function scheduleHidePopup() { + if (!hidePopupTimeout) { + hidePopupTimeout = setTimeout(withoutMutationObserver(hidePopup), 100); + } +} + + +/** + * Get the absolute bounds of an element. + * @param {Element} elem - The element for which to return bounds. + * @returns {{top: number, left: number, right: number, bottom: + * number}} The top, left, right, and bottom coordinates of the + * element. + */ +function getAbsoluteBoundingRect(elem) { + let rect = elem.getBoundingClientRect(); + let scrollLeft = window.scrollX; + let scrollTop = window.scrollY; + return { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX, + bottom: rect.bottom + window.scrollY, + right: rect.right + window.scrollX, + } +} + +/** + * Attempt to ensure that at least part of an element is visible. If + * the element's right-hand coordinate is off-screen, move it + * on-screen without moving the left-hand side off-screen. If the + * bottom of the element is off-screen, move it on-screen. + * @param {Element} elem - The element to show. + */ +function clampElementToDocument(elem) { + let elemBounds = getAbsoluteBoundingRect(elem); + + if (elemBounds.bottom > document.documentElement.scrollHeight) { + elem.style.removeProperty('top'); + elem.style.bottom = 0; + } + + if (elemBounds.right > document.documentElement.scrollWidth) { + elem.style.removeProperty('left'); + elem.style.right = 0; + if (getAbsoluteBoundingRect(elem).left < 0) { + elem.style.left = 0; + } + } +} + +/** + * Show the original URL of a link. + * @param {MouseEvent} event - The event triggering this handler. + */ +function showOriginalUrl(event) { + console.log('enter showOriginalUrl'); + let popup = getPopup(); + cancelHidePopup(); + if (event.target != currentPopupTarget + || !popup.classList.contains(safelinksPopupVisibleClass)) { + currentPopupTarget = event.target; + popup.textContent = untangleLink(event.target.href); + popup.style.removeProperty('bottom'); + popup.style.removeProperty('right'); + popup.style.left = event.clientX + 'px'; + popup.style.top = event.clientY + 'px'; + popup.classList.add(safelinksPopupVisibleClass); + clampElementToDocument(popup); + } + console.log('exit showOriginalUrl'); +} + + +/** + * Add event handlers to a link so it will show the original url. + * @param {Element} link - The link to add the popup to. + */ +function addLinkPopup(link) { + link.addEventListener('mouseenter', withoutMutationObserver(showOriginalUrl), {passive: true}); + link.addEventListener('mouseleave', scheduleHidePopup, {passive: true}); +} + + +function fixAllTheLinks() { + console.log('enter fixAllTheLinks'); + for (const link of document.links) { + // Untangle link text + for (const node of getTextNodes(link)) { + node.textContent = untangleLink(node.textContent); + } + + // Create popup event handlers + if (isTangledLink(link.href)) { + addLinkPopup(link); + } + } + console.log('leave fixAllTheLinks'); +} + + +fixAllTheLinks(); +enableMutationObserver(); diff --git a/firefox/icon.svg b/firefox/icon.svg new file mode 100644 index 0000000..dc540fd --- /dev/null +++ b/firefox/icon.svg @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 283.46 283.46" style="enable-background:new 0 0 283.46 283.46;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#231F20;} + .st1{fill:none;stroke:#231F20;stroke-width:36;stroke-linecap:round;stroke-miterlimit:10;} + .st2{fill:#ED1C24;} + .st3{fill:#FFFFFF;} +</style> +<g> + <path class="st0" d="M209.46,74v135.46H74V74H209.46 M283.46,0H0v283.46h283.46V0L283.46,0z"/> +</g> +<path class="st1" d="M15.14,199.56"/> +<path class="st1" d="M15.14,216.86"/> +<g> + <polygon class="st2" points="97.71,88.66 150.06,36.3 99.24,-14.5 297.96,-14.5 297.96,184.2 246.66,132.89 194.3,185.25 "/> + <path class="st3" d="M283.46,0v149.2l-36.81-36.81l-52.36,52.36l-76.09-76.09l52.36-52.36L134.26,0H283.46 M312.46-29h-29H134.26 + H64.23l49.52,49.51l15.8,15.79L97.71,68.15L77.2,88.66l20.51,20.51l76.09,76.09l20.51,20.51l20.51-20.51l31.85-31.85l16.3,16.3 + l49.51,49.51V149.2V0V-29L312.46-29z"/> +</g> +</svg> diff --git a/firefox/manifest.json b/firefox/manifest.json new file mode 100644 index 0000000..56c82fb --- /dev/null +++ b/firefox/manifest.json @@ -0,0 +1,38 @@ +{ + "manifest_version": 2, + "name": "Safe Links Cleaner", + "description": "__MSG_extensionDescription__", + "version": "1.0", + "author": "David Byers", + "homepage_url": "https://safelinks.gitlab-pages.liu.se/safelinks-cleaner-firefox/", + "default_locale": "en", + "icons": { + "48": "icon.svg", + "96": "icon.svg", + "144": "icon.svg", + "192": "icon.svg" + }, + "background": { + "scripts": [ + "common.js", + "background.js" + ] + }, + "content_scripts": [ + { + "matches": ["*://outlook.office.com/*"], + "css": [ + "/style.css" + ], + "js": [ + "common.js", + "content.js" + ] + } + ], + "permissions": [ + "activeTab", + "clipboardWrite", + "menus" + ] +} diff --git a/extension/style.css b/firefox/style.css similarity index 100% rename from extension/style.css rename to firefox/style.css diff --git a/thunderbird/_locales/en/messages.json b/thunderbird/_locales/en/messages.json new file mode 100644 index 0000000..b4f672f --- /dev/null +++ b/thunderbird/_locales/en/messages.json @@ -0,0 +1,11 @@ +{ + "extensionDescription": { + "message": "Clean up display of links rewritten by Microsoft Defender for Office 365 Safe Links, so it is easy to see and copy the original link.", + "description": "Description of the extension." + }, + + "copyLinkMenuTitle": { + "message": "Copy original link", + "description": "Title of the copy original link menu item." + } +} diff --git a/thunderbird/_locales/sv/messages.json b/thunderbird/_locales/sv/messages.json new file mode 100644 index 0000000..b5572a0 --- /dev/null +++ b/thunderbird/_locales/sv/messages.json @@ -0,0 +1,11 @@ +{ + "extensionDescription": { + "message": "Ändra visning av länkar omskrivna av Microsoft Defender för Office 365 Safe Links, så det är enkelt att se och kopiera den ursrpungliga länken.", + "description": "Description of the extension." + }, + + "copyLinkMenuTitle": { + "message": "Kopiera ursprunglig länk", + "description": "Title of the copy original link menu item." + } +} diff --git a/extension/background.js b/thunderbird/background.js similarity index 100% rename from extension/background.js rename to thunderbird/background.js diff --git a/thunderbird/common.js b/thunderbird/common.js new file mode 100644 index 0000000..32f296f --- /dev/null +++ b/thunderbird/common.js @@ -0,0 +1,99 @@ +// Microsoft ATP Safe Links Cleaner +// Copyright 2021 David Byers <david.byers@liu.se> +// +// 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. + +// Shared code + + +/** + * Regexp that matches safe links. The original URL must be collected + * in match group 1. + */ +const safelinksRegexp = new RegExp( + 'https?://[^.]+[.]safelinks[.]protection[.]outlook[.]com/[?]url=([^&]+)&.*', + 'gi' +); + + +/** + * The ID for the popup element that is added to the HTML document. + */ +const safelinksPopupId = 'safelinks-cleaner-thunderbird-popup'; + + +/** + * The class that is added to the popup when visible. + */ +const safelinksPopupVisibleClass = 'safelinks-cleaner-thunderbird-popup-visible'; + + +/** + * Return the original URL for a safe link. + * @param {string} link - The safe link. + * @returns {string} The original link or the safe link if there was an error. + */ +function untangleLink(link) { + return link.replaceAll( + safelinksRegexp, (match, url) => { + try { + return decodeURIComponent(url); + } + catch (e) { + console.log(e); + return url; + } + }); +} + + +/** + * Check if a link is a safe link. + * @param {string} link - The URL to check. + * @returns {boolean} Returns true if the link is a safe link. + */ +function isTangledLink(link) { + return link.match(safelinksRegexp); +} + + +/** + * Return the text nodes under a DOM element. + * @param {Element} elem - The element to return text nodes for. + * @returns {Element[]} The text elements under elem. + */ +function getTextNodes(elem) { + var result = []; + if (elem) { + for (var nodes = elem.childNodes, i = nodes.length; i--;) { + let node = nodes[i]; + let nodeType = node.nodeType; + + if (nodeType == Node.TEXT_NODE) { + result.push(node); + } + else if (nodeType == Node.ELEMENT_NODE + || nodeType == Node.DOCUMENT_NODE + || nodeType == Node.DOCUMENT_FRAGMENT_NODE) { + result = result.concat(getTextNodes(node)); + } + } + } + return result; +} diff --git a/extension/compose.js b/thunderbird/compose.js similarity index 100% rename from extension/compose.js rename to thunderbird/compose.js diff --git a/extension/display.js b/thunderbird/display.js similarity index 100% rename from extension/display.js rename to thunderbird/display.js diff --git a/extension/icon.svg b/thunderbird/icon.svg similarity index 100% rename from extension/icon.svg rename to thunderbird/icon.svg diff --git a/extension/manifest.json b/thunderbird/manifest.json similarity index 88% rename from extension/manifest.json rename to thunderbird/manifest.json index 9b0767c..b2db40d 100644 --- a/extension/manifest.json +++ b/thunderbird/manifest.json @@ -4,7 +4,7 @@ "description": "__MSG_extensionDescription__", "version": "1.3", "author": "David Byers", - "homepage_url": "https://gitlab.liu.se/safelinks-cleaner-thunderbird/", + "homepage_url": "https://gitlab.liu.se/safelinks/safelinks-cleaner-thunderbird/", "default_locale": "en", "icons": { "48": "icon.svg", diff --git a/thunderbird/style.css b/thunderbird/style.css new file mode 100644 index 0000000..95f271c --- /dev/null +++ b/thunderbird/style.css @@ -0,0 +1,41 @@ +/* +Microsoft ATP Safe Links Cleaner +Copyright 2021 David Byers <david.byers@liu.se> + +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. +*/ + + +#safelinks-cleaner-thunderbird-popup { + display: none; + background: #fffff8; + color: black; + padding: 3px 3px 4px 3px; + position: fixed; + z-index: 1000; + border: 1px solid black; + -webkit-box-shadow: 0px 0px 6px 1px rgba(0,0,0,0.5); + -moz-box-shadow: 0px 0px 6px 1px rgba(0,0,0,0.5); + box-shadow: 0px 0px 6px 1px rgba(0,0,0,0.5); + font: 14px sans-serif; +} + +#safelinks-cleaner-thunderbird-popup.safelinks-cleaner-thunderbird-popup-visible { + display: block; +} -- GitLab