Skip to content
Snippets Groups Projects
Commit 2a862454 authored by David Byers's avatar David Byers
Browse files

Merge branch '1-tooltips-break-some-links' into 'master'

Resolve "Tooltips break some links"

Closes #1

See merge request safelinks/safelinks-cleaner-thunderbird!2
parents 9f43faa9 82dcfff6
No related branches found
No related tags found
1 merge request!2Resolve "Tooltips break some links"
Pipeline #32604 passed
......@@ -20,9 +20,7 @@ in such a way that the original link is accessible.
* Since only the *display* is changed, links that have the URL as the
link text will now have link text that differs from the link target.
Thunderbird picks up on this when you click a link and asks if you
want to visit the original link or the safe link. Due to the way the
tooltip is constructed, visiting the original link will most likely
fail. This is not by design and may change in the future.
want to visit the original link or the safe link.
* When composing a message the original links are restored. This
process could potentially change text that isn't meant to be
......
......@@ -22,11 +22,33 @@
// 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) => {
......@@ -40,10 +62,22 @@ function untangleLink(link) {
});
}
/**
* 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) {
......
......@@ -22,19 +22,140 @@
// 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.
*/
function addLinkPopup(link) {
link.addEventListener('mouseenter', showOriginalUrl, {passive: true});
link.addEventListener('mouseleave', scheduleHidePopup, {passive: true});
}
for (const link of document.links) {
// Mangle the link text
// Untangle link text
for (const node of getTextNodes(link)) {
node.textContent = untangleLink(node.textContent);
}
// Generate the tooltip and set link class
// Create popup event handlers
if (isTangledLink(link.href)) {
let tooltiptext = untangleLink(link.href);
let tooltip = document.createElement('span');
tooltip.classList.add('liu_safelinks_tooltip');
tooltip.textContent = tooltiptext;
link.classList.add('liu_safelinks_link');
link.prepend(tooltip);
addLinkPopup(link);
}
}
......@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Microsoft ATP Safe Links Cleaner",
"description": "__MSG_extensionDescription__",
"version": "1.1",
"version": "1.2",
"author": "David Byers",
"homepage_url": "https://safelinks.gitlab-pages.liu.se/safelinks-cleaner-thunderbird/",
"default_locale": "en",
......
......@@ -21,16 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
a:hover .liu_safelinks_tooltip {
display: block;
}
.liu_safelinks_tooltip {
#safelinks-cleaner-thunderbird-popup {
display: none;
background: #fffff8;
color: black;
padding: 3px 3px 4px 3px;
position: absolute;
position: fixed;
z-index: 1000;
border: 1px solid black;
-webkit-box-shadow: 0px 0px 6px 1px rgba(0,0,0,0.5);
......@@ -38,3 +35,7 @@ a:hover .liu_safelinks_tooltip {
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;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment