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