diff --git a/.gitignore b/.gitignore index 92671c596ab2ea98a5d070078d481df9e85048ed..0206ae2789aca52a9945beec5156b6818f44daad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *~ \#* .#* -safelinks-cleaner-thunderbird.xpi +build/ +*.xpi diff --git a/firefox/content.js b/firefox/content.js index 5e9a958aa5ee49a2c83fc3a550219266a03375fe..3fe7f87d02be8f47f4abf4354bed7a9ea51534ff 100644 --- a/firefox/content.js +++ b/firefox/content.js @@ -22,161 +22,6 @@ // 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. @@ -187,22 +32,5 @@ function addLinkPopup(link) { } -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 deleted file mode 100644 index dc540fd6026284266c72dc1d835e2060eb62c583..0000000000000000000000000000000000000000 --- a/firefox/icon.svg +++ /dev/null @@ -1,22 +0,0 @@ -<?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 deleted file mode 100644 index 56c82fb4b95114d5c0a6d085037787bdc319a2a7..0000000000000000000000000000000000000000 --- a/firefox/manifest.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "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/firefox/manifest.part.json b/firefox/manifest.part.json new file mode 100644 index 0000000000000000000000000000000000000000..1c47e9897ba6395981526680445419f0d71565c4 --- /dev/null +++ b/firefox/manifest.part.json @@ -0,0 +1,24 @@ +{ + "background": { + "scripts": [ + "menu.js" + ] + }, + "content_scripts": [ + { + "matches": ["*://outlook.office.com/*"], + "css": [ + "style.css" + ], + "js": [ + "links.js", + "popup.js", + "mutation.js", + "content.js" + ] + } + ], + "permissions": [ + "activeTab" + ] +} diff --git a/firefox/mutation.js b/firefox/mutation.js new file mode 100644 index 0000000000000000000000000000000000000000..746dbb7c40bcc9a324e62a630e4e80f755ea0cab --- /dev/null +++ b/firefox/mutation.js @@ -0,0 +1,34 @@ +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(); + } + } +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..5b5adfb8a3c0da1f7f764b2578e3fb6691d4e3ee --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,20 @@ +#! /bin/bash + + +TARGETS=(thunderbird firefox) + +BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +BUILDDIR="$BASEDIR/build" +SHAREDDIR="$BASEDIR/shared" + +for target in "${TARGETS[@]}" ; do + targetdir="$BUILDDIR/$target" + sourcedir="$basedir/$target" + outputfile="$BUILDDIR/safelinks-cleaner-$target.xpi" + + [ -d "$targetdir" ] && rm -r "$targetdir" + mkdir -p "$targetdir" + cp -r "$SHAREDDIR"/* "$sourcedir"/* "$targetdir" + cd "$targetdir" + zip -r ../"$outputfile" * +done diff --git a/scripts/makemanifest.py b/scripts/makemanifest.py new file mode 100644 index 0000000000000000000000000000000000000000..4b4a2945804e726eb620cc81f7cc42244f9245af --- /dev/null +++ b/scripts/makemanifest.py @@ -0,0 +1,59 @@ +#! /usr/bin/python3 + +import argparse +import json +import sys + + +def deepmerge_dict(data_a, data_b, *, path): + result ={} + for key in data_a: + if key not in data_b: + result[key] = data_a[key] + else: + result[key] = deepmerge(data_a[key], data_b[key], path=path + "." + key) + for key in data_b: + if key not in data_a: + result[key] = data_b[key] + return result + + +def deepmerge(data_a, data_b, *, path=""): + if type(data_a) is not type(data_b): + raise TypeError('mismatched types at %s' % path) + if isinstance(data_a, dict): + return deepmerge_dict(data_a, data_b, path=path) + if isinstance(data_a, list): + return data_a + data_b + if data_a != data_b: + raise ValueError('mismatched values at %s' % path) + + + +def main(): + """Main function.""" + parser = argparse.ArgumentParser("Merge fragments of manifest.json files.") +# parser.add_argument('--output', required=True, help='path to output file') + parser.add_argument('files', nargs='+', help='input files') + opts = parser.parse_args() + + result = {} + errors = False + for path in opts.files: + with open(path, 'r') as fp: + try: + data = json.load(fp) + except json.decoder.JSONDecodeError as exc: + print('%s: %s' % (path, str(exc)), file=sys.stderr) + errors = True + if not errors: + result = deepmerge(result, data) + + if errors: + sys.exit(1) + + print(json.dumps(result, indent=4)) + + +if __name__ == '__main__': + main() diff --git a/firefox/_locales/en/messages.json b/shared/_locales/en/messages.json similarity index 100% rename from firefox/_locales/en/messages.json rename to shared/_locales/en/messages.json diff --git a/firefox/_locales/sv/messages.json b/shared/_locales/sv/messages.json similarity index 100% rename from firefox/_locales/sv/messages.json rename to shared/_locales/sv/messages.json diff --git a/thunderbird/icon.svg b/shared/icon.svg similarity index 100% rename from thunderbird/icon.svg rename to shared/icon.svg diff --git a/firefox/common.js b/shared/links.js similarity index 89% rename from firefox/common.js rename to shared/links.js index 32f296f6c6e89933f2a48d2d379b33ba638d0e55..27ffc309ee29aceead251a81938944737ce962a2 100644 --- a/firefox/common.js +++ b/shared/links.js @@ -97,3 +97,21 @@ function getTextNodes(elem) { } return result; } + + +/** + * Fix all the links in the document. + */ +function 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); + } + } +} diff --git a/shared/manifest.part.json b/shared/manifest.part.json new file mode 100644 index 0000000000000000000000000000000000000000..764aa8ac7dfbdd7e0d8524fc883c25185ad63dd0 --- /dev/null +++ b/shared/manifest.part.json @@ -0,0 +1,19 @@ +{ + "manifest_version": 2, + "name": "ATP Safe Links Cleaner", + "description": "__MSG_extensionDescription__", + "version": "1.3", + "author": "David Byers", + "homepage_url": "https://gitlab.liu.se/safelinks/safelinks-cleaner/", + "default_locale": "en", + "icons": { + "48": "icon.svg", + "96": "icon.svg", + "144": "icon.svg", + "192": "icon.svg" + }, + "permissions": [ + "clipboardWrite", + "menus" + ] +} diff --git a/firefox/background.js b/shared/menu.js similarity index 94% rename from firefox/background.js rename to shared/menu.js index a0d1cf2bf28bddb1f74ece44f963b29e054e1f41..3f774163410c47fcabfe62467e5fdcb694609870 100644 --- a/firefox/background.js +++ b/shared/menu.js @@ -19,11 +19,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// Background scripts +// Context menu -console.log('enter background'); - browser.menus.create({ id: "liu-safelinks-copy", title: browser.i18n.getMessage("copyLinkMenuTitle"), @@ -37,6 +35,3 @@ browser.menus.onClicked.addListener((info, tab) => { navigator.clipboard.writeText(untangleLink(info.linkUrl)); } }); - - -console.log('exit background'); diff --git a/shared/popup.js b/shared/popup.js new file mode 100644 index 0000000000000000000000000000000000000000..d1a6aec4ffe8b3ba51ec776abd0de535b97a7528 --- /dev/null +++ b/shared/popup.js @@ -0,0 +1,142 @@ +// 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. + +// Popups + + +let currentPopupTarget = null; +let hidePopupTimeout = null; + + +/** + * 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) { + 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); + } +} diff --git a/firefox/style.css b/shared/style.css similarity index 100% rename from firefox/style.css rename to shared/style.css diff --git a/thunderbird/_locales/en/messages.json b/thunderbird/_locales/en/messages.json deleted file mode 100644 index b4f672fca009ab18519391db8a3a7ba525a11a4e..0000000000000000000000000000000000000000 --- a/thunderbird/_locales/en/messages.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index b5572a0e00c2da2e9d6ce09b244ad6de80763f7e..0000000000000000000000000000000000000000 --- a/thunderbird/_locales/sv/messages.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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/thunderbird/background.js b/thunderbird/background.js index 9d9957cfdd949a06179f89db9b7d88e4060232d7..e85a04292a55b30cee03f7e4935b066ee9e8b03c 100644 --- a/thunderbird/background.js +++ b/thunderbird/background.js @@ -23,8 +23,12 @@ browser.composeScripts.register({ + css: [ + {file: "/style.css"} + ], js: [ - {file: "common.js"}, + {file: "links.js"}, + {file: "popup.js"}, {file: "compose.js"} ], }); @@ -34,22 +38,8 @@ browser.messageDisplayScripts.register({ {file: "/style.css"} ], js: [ - {file: "/common.js"}, + {file: "/links.js"}, + {file: "/popup.js"}, {file: "/display.js"} ], }); - -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)); - } -}); - diff --git a/thunderbird/common.js b/thunderbird/common.js deleted file mode 100644 index 32f296f6c6e89933f2a48d2d379b33ba638d0e55..0000000000000000000000000000000000000000 --- a/thunderbird/common.js +++ /dev/null @@ -1,99 +0,0 @@ -// 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/thunderbird/display.js b/thunderbird/display.js index 9ecc2593c41301e748f21af10f9c380c2859bd57..2f8061b9aa5cab2d7009caf9ef67bfc4091b26df 100644 --- a/thunderbird/display.js +++ b/thunderbird/display.js @@ -22,122 +22,6 @@ // Display script -let currentPopupTarget = null; -let hidePopupTimeout = null; - - -/** - * 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(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) { - 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; - popup.style.top = event.clientY; - popup.classList.add(safelinksPopupVisibleClass); - //clampElementToDocument(popup); - } -} - /** * Add event handlers to a link so it will show the original url. * @param {Element} link - The link to add the popup to. @@ -148,14 +32,4 @@ function addLinkPopup(link) { } -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); - } -} +fixAllTheLinks(); diff --git a/thunderbird/manifest.json b/thunderbird/manifest.json deleted file mode 100644 index b2db40d25edf4456a51a7678f29cbc755bc3f3e8..0000000000000000000000000000000000000000 --- a/thunderbird/manifest.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "manifest_version": 2, - "name": "Microsoft ATP Safe Links Cleaner", - "description": "__MSG_extensionDescription__", - "version": "1.3", - "author": "David Byers", - "homepage_url": "https://gitlab.liu.se/safelinks/safelinks-cleaner-thunderbird/", - "default_locale": "en", - "icons": { - "48": "icon.svg", - "96": "icon.svg", - "144": "icon.svg", - "192": "icon.svg" - }, - "applications": { - "gecko": { - "id": "safelinks-cleaner-thunderbird@it.liu.se", - "strict_min_version": "78.4.0" - } - }, - "background": { - "scripts": [ - "common.js", - "background.js" - ] - }, - "permissions": [ - "messagesModify", - "clipboardWrite", - "menus", - "compose" - ] -} diff --git a/thunderbird/manifest.part.json b/thunderbird/manifest.part.json new file mode 100644 index 0000000000000000000000000000000000000000..4c21da9b0949118330e5b5187d3aa15aea4972dd --- /dev/null +++ b/thunderbird/manifest.part.json @@ -0,0 +1,17 @@ +{ + "applications": { + "gecko": { + "id": "safelinks-cleaner-thunderbird@it.liu.se", + "strict_min_version": "78.4.0" + } + }, + "background": { + "scripts": [ + "background.js" + ] + }, + "permissions": [ + "messagesModify", + "compose" + ] +} diff --git a/thunderbird/style.css b/thunderbird/style.css deleted file mode 100644 index 95f271cbd0983e90529959308997a5bc6a2ef07a..0000000000000000000000000000000000000000 --- a/thunderbird/style.css +++ /dev/null @@ -1,41 +0,0 @@ -/* -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; -}