Skip to content

Instantly share code, notes, and snippets.

@vic4key
Created October 2, 2025 17:17
Show Gist options
  • Select an option

  • Save vic4key/099cdd2a9b3c2ec46ccc88abc7f83161 to your computer and use it in GitHub Desktop.

Select an option

Save vic4key/099cdd2a9b3c2ec46ccc88abc7f83161 to your computer and use it in GitHub Desktop.
URL Safety Confirmation - Secure your click by monitoring and validating every link before clicking in specified websites
// ==UserScript==
// @name URL Safety Confirmation
// @namespace http://tampermonkey.net/
// @version 1.0
// @description URL Safety Confirmation - Secure your click by monitoring and validating every link before clicking in specified websites
// @author Vic P. @ https://vic.onl/
// @match https://outlook.live.com/*
// @match https://outlook.office.com/*
// @match https://mail.google.com/*
// @require https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css');
/* Auto-resize textarea styles using CSS-only approach */
.auto-resize-textarea {
width: 100% !important;
min-width: 200px !important;
max-width: calc(100% - 40px) !important;
min-height: 40px !important;
max-height: 200px !important;
resize: none !important;
overflow: hidden !important;
box-sizing: border-box !important;
display: block !important;
}
.auto-resize-textarea:focus {
outline: none !important;
border-color: #007bff !important;
}
/* Simple auto-resize textarea */
.auto-resize-textarea {
width: 100% !important;
max-width: 100% !important;
max-height: 200px !important;
resize: none !important;
overflow-y: auto !important;
border: 2px solid #ccc !important;
border-radius: 4px !important;
background-color: #f8f8f8 !important;
font-size: 16px !important;
box-sizing: border-box !important;
padding: 8px !important;
margin: 0 !important;
display: block !important;
}
`);
const domains = {
"blacklist_domains": [
"bit.ly",
"tinyurl.com",
"suspicious-domain.com",
],
"whitelist_domains": [
"office.com",
"coursera.org",
"sharepoint.com",
"microsoft.com",
"protection.outlook.com",
"cloud.microsoft",
],
"ignoredlist_domains": [
"google.com",
"bing.com",
]
};
// Load domains from storage and merge with embedded config
let blacklist_domains = [...domains.blacklist_domains, ...(GM_getValue('blacklist_domains', []))];
let whitelist_domains = [...domains.whitelist_domains, ...(GM_getValue('whitelist_domains', []))];
let ignoredlist_domains = [...domains.ignoredlist_domains, ...(GM_getValue('ignoredlist_domains', []))];
// Remove duplicates
blacklist_domains = [...new Set(blacklist_domains)];
whitelist_domains = [...new Set(whitelist_domains)];
ignoredlist_domains = [...new Set(ignoredlist_domains)];
// console.log('Domains loaded:', { blacklist_domains, whitelist_domains, ignoredlist_domains });
// Function to add domain to whitelist dynamically
function add_to_whitelist(domain) {
if (!whitelist_domains.includes(domain)) {
whitelist_domains.push(domain);
// Save to storage (only user-added domains, not embedded ones)
const stored_whitelist = GM_getValue('whitelist_domains', []);
if (!stored_whitelist.includes(domain)) {
stored_whitelist.push(domain);
GM_setValue('whitelist_domains', stored_whitelist);
}
console.log(`Added ${domain} to whitelist and saved to storage`);
}
}
// Function to add domain to blacklist dynamically
function add_to_blacklist(domain) {
if (!blacklist_domains.includes(domain)) {
blacklist_domains.push(domain);
// Save to storage (only user-added domains, not embedded ones)
const stored_blacklist = GM_getValue('blacklist_domains', []);
if (!stored_blacklist.includes(domain)) {
stored_blacklist.push(domain);
GM_setValue('blacklist_domains', stored_blacklist);
}
console.log(`Added ${domain} to blacklist and saved to storage`);
}
}
// Function to remove domain from whitelist
function remove_from_whitelist(domain) {
const index = whitelist_domains.indexOf(domain);
if (index > -1) {
whitelist_domains.splice(index, 1);
const stored = GM_getValue('whitelist_domains', []);
const stored_index = stored.indexOf(domain);
if (stored_index > -1) {
stored.splice(stored_index, 1);
GM_setValue('whitelist_domains', stored);
}
}
}
// Function to remove domain from blacklist
function remove_from_blacklist(domain) {
const index = blacklist_domains.indexOf(domain);
if (index > -1) {
blacklist_domains.splice(index, 1);
const stored = GM_getValue('blacklist_domains', []);
const stored_index = stored.indexOf(domain);
if (stored_index > -1) {
stored.splice(stored_index, 1);
GM_setValue('blacklist_domains', stored);
}
}
}
// Function to add domain to ignored list dynamically
function add_to_ignoredlist(domain) {
if (!ignoredlist_domains.includes(domain)) {
ignoredlist_domains.push(domain);
// Save to storage (only user-added domains, not embedded ones)
const stored_ignoredlist = GM_getValue('ignoredlist_domains', []);
if (!stored_ignoredlist.includes(domain)) {
stored_ignoredlist.push(domain);
GM_setValue('ignoredlist_domains', stored_ignoredlist);
}
console.log(`Added ${domain} to ignored list and saved to storage`);
}
}
// Function to remove domain from ignored list
function remove_from_ignoredlist(domain) {
const index = ignoredlist_domains.indexOf(domain);
if (index > -1) {
ignoredlist_domains.splice(index, 1);
const stored = GM_getValue('ignoredlist_domains', []);
const stored_index = stored.indexOf(domain);
if (stored_index > -1) {
stored.splice(stored_index, 1);
GM_setValue('ignoredlist_domains', stored);
}
}
}
// Function to clear all user-added domains
function clear_user_whitelist() {
GM_setValue('whitelist_domains', []);
whitelist_domains = [...domains.whitelist_domains];
}
// Expose functions globally for console access
window.domainManager = {
whitelist: {
add: add_to_whitelist,
remove: remove_from_whitelist,
clear: clear_user_whitelist,
getCurrentList: () => whitelist_domains
},
blacklist: {
add: add_to_blacklist,
remove: remove_from_blacklist,
getCurrentList: () => blacklist_domains
},
ignoredlist: {
add: add_to_ignoredlist,
remove: remove_from_ignoredlist,
getCurrentList: () => ignoredlist_domains
}
};
unsafeWindow.confirm = function() { return true; };
unsafeWindow.alert = function() {};
GM_addStyle(`
.modal, .dialog, #dialog {
display: none !important;
}
p { user-select: text; }
b { user-select: text; }
i { user-select: text; }
u { user-select: text; }
div { user-select: text; }
textarea {
box-sizing: border-box;
border: 2px solid #ccc;
border-radius: 4px;
background-color: #f8f8f8;
font-size: 16px;
resize: none;
}
`);
function truncate_url(url, max_len = 60) {
// if (url.length >= max_len) {
// return url.substring(0, max_len - 3) + '...';
// }
return url;
}
function extract_url(url, follow_target = false) {
let new_url = url;
// console.log(`url = ${url}`);
try {
new_url = decodeURIComponent(url);
// console.log(`new_url = ${new_url}`);
let parsed_url = new URL(new_url);
// console.log(`parsed_url = `, parsed_url);
new_url = `${parsed_url.protocol}//${parsed_url.host}/`;
// console.log(`new_url = ${new_url}`);
if (follow_target && new_url?.includes("protection.outlook.com"))
{
let temp_url = parsed_url?.searchParams?.get('url');
// let temp_url = parsed_url?.searchParams?.entries()?.toArray()?.find(([key]) => key === 'url')?.[1];
// console.log(`temp_url = ${temp_url}`);
if (temp_url)
{
let parsed_temp_url = new URL(temp_url);
new_url = `${parsed_temp_url.protocol}//${parsed_temp_url.host}/`;
}
}
}
catch (error) {
console.error('Invalid URL:', error);
}
return new_url;
}
function is_domain_ignored(url) {
try {
const parsed_url = new URL(url);
return ignoredlist_domains.some(domain => parsed_url.hostname.includes(domain));
} catch (error) {
console.error('Invalid URL:', error);
return false; // Handle invalid URLs
}
}
function is_blacklisted(url) {
try {
const parsed_url = new URL(url);
return blacklist_domains.some(domain =>
parsed_url.hostname.includes(domain)
);
} catch (error) {
return false;
}
}
function is_whitelisted(url) {
try {
const parsed_url = new URL(url);
return whitelist_domains.some(domain => parsed_url.hostname.includes(domain));
} catch (error) {
console.error('Invalid URL:', error);
return false;
}
}
function show_custom_dialog(options) {
const existing_dialogs = document.querySelectorAll('#custom-dialog, #dialog-overlay');
existing_dialogs.forEach(el => el.remove());
const dialog_html = `
<div id="custom-dialog" style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 9999;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
">
<h2 style="margin-top: 0;">${options.title}</h2>
${options.message}
<div style="display: flex; justify-content: flex-end; margin-top: 20px;">
${options.buttons.map((btn, index) => `
<button id="dialog-btn-${index}" style="
margin-left: 10px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
${['Cancel', 'No'].some(str => btn.text.includes(str)) ? 'background-color: #28a745; color: white;' : 'background-color: #007bff; color: white;'}
">${btn.text}</button>
`).join('')}
</div>
</div>
<div id="dialog-overlay" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 9998;
"></div>
`;
const temp_div = document.createElement('div');
temp_div.innerHTML = dialog_html;
document.body.appendChild(temp_div);
// Simple auto-resize for textarea
setTimeout(() => {
const textarea = document.querySelector('.auto-resize-textarea');
if (textarea) {
// Ensure width fits dialog
const dialog = document.getElementById('custom-dialog');
if (dialog) {
const dialogWidth = dialog.offsetWidth - 40; // Account for padding
textarea.style.width = dialogWidth + 'px';
textarea.style.maxWidth = dialogWidth + 'px';
}
// Auto-resize height
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
}
}, 0);
options.buttons.forEach((btn, index) => {
const button = document.getElementById(`dialog-btn-${index}`);
button.addEventListener('click', () => {
document.getElementById('custom-dialog')?.remove();
document.getElementById('dialog-overlay')?.remove();
btn.onclick && btn.onclick();
});
if (btn.focus) {
button.focus();
}
});
}
document.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href || !href.startsWith('http')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
let href_url = extract_url(href, true); // follow_target = true or false
if (is_domain_ignored(href_url)) {
window.open(href, '_blank');
return;
}
let href_icon = "&#160;";
let href_color = "black";
if (is_whitelisted(href_url))
{
href_color = "green";
href_icon += `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708"/>
</svg>`;
// href_icon += "&#160;"
}
else if (is_blacklisted(href_url))
{
href_color = "red";
href_icon += `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-patch-exclamation-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM8 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
</svg>`;
// href_icon += "&#160;"
}
else
{
href_color = "#D08B00";
href_icon += `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-patch-question" viewBox="0 0 16 16">
<path d="M8.05 9.6c.336 0 .504-.24.554-.627.04-.534.198-.815.847-1.26.673-.475 1.049-1.09 1.049-1.986 0-1.325-.92-2.227-2.262-2.227-1.02 0-1.792.492-2.1 1.29A1.7 1.7 0 0 0 6 5.48c0 .393.203.64.545.64.272 0 .455-.147.564-.51.158-.592.525-.915 1.074-.915.61 0 1.03.446 1.03 1.084 0 .563-.208.885-.822 1.325-.619.433-.926.914-.926 1.64v.111c0 .428.208.745.585.745"/>
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
<path d="M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0"/>
</svg>`;
// href_icon += "&#160;"
}
let href_url_decoded = decodeURIComponent(href);
let href_truncated = truncate_url(href_url_decoded);
let href_url_domain = new URL(href_url).hostname;
let unknow_domain = !is_whitelisted(href_url) && !is_blacklisted(href_url);
let manage_domain = !unknow_domain ? "" : `<div style="margin: 3px 0; text-align: center;"><button id="trust-btn" style="margin-right: 10px; padding: 5px 10px; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">Trust</button><button id="block-btn" style="margin-right: 10px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">Suspicious</button><button id="ignore-btn" style="padding: 5px 10px; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">Ignore</button></div>`;
show_custom_dialog({
title: 'URL Safety Confirmation',
message: `<br><b>Base URL</b><p style='color: ${href_color}'><b>${href_url}${href_icon}</b><br>${manage_domain}</p><b>URL</b><br><textarea readonly class="auto-resize-textarea" rows="3" style='color: ${href_color}'>${href_truncated}</textarea><br><p><b>Continue?</b></p>`,
buttons: [
{
text: `Let's Go`,
onclick: () => {
if (is_blacklisted(href)) {
show_custom_dialog({
title: 'Suspicious URL Warning',
message: 'This link appears to be from a potentially suspicious domain. Are you absolutely sure you want to continue?',
buttons: [
{
text: `I'm Sure`,
onclick: () => {
window.open(href, '_blank');
}
},
{
text: 'Cancel',
onclick: () => {},
focus: true
},
]
});
} else {
window.open(href, '_blank');
}
}
},
{
text: `Cancel`,
onclick: () => {},
focus: true
},
]
});
// Add event listeners for trust/block/ignore buttons
if (unknow_domain) {
setTimeout(() => {
const trustBtn = document.getElementById('trust-btn');
const blockBtn = document.getElementById('block-btn');
const ignoreBtn = document.getElementById('ignore-btn');
if (trustBtn) {
trustBtn.addEventListener('click', () => {
show_custom_dialog({
title: 'Domain Confirmation',
message: `<p style='color: green'>Are you sure to mark the domain <b>${href_url_domain}</b> as <b>TRUSTED</b>?</p><p>Add to your whitelist.</p>`,
buttons: [
{
text: 'Yes, Trusted!',
onclick: () => {
add_to_whitelist(href_url_domain);
show_custom_dialog({
title: 'Domain Trusted',
message: `<p>Domain <b>${href_url_domain}</b> has been added to whitelist.</p>`,
buttons: [
{
text: 'OK',
onclick: () => {}
}
]
});
}
},
{
text: 'No',
onclick: () => {},
focus: true
}
]
});
});
}
if (blockBtn) {
blockBtn.addEventListener('click', () => {
show_custom_dialog({
title: 'Domain Confirmation',
message: `<p style='color: red'>Are you sure to mark the domain <b>${href_url_domain}</b> as <b>SUSPICIOUS</b>?</p><p>Add to your blacklist.</p>`,
buttons: [
{
text: 'Yes, Suspicious!',
onclick: () => {
add_to_blacklist(href_url_domain);
show_custom_dialog({
title: 'Domain Suspicious',
message: `<p>Domain <b>${href_url_domain}</b> has been added to blacklist.</p>`,
buttons: [
{
text: 'OK',
onclick: () => {}
}
]
});
}
},
{
text: 'No',
onclick: () => {},
focus: true
}
]
});
});
}
if (ignoreBtn) {
ignoreBtn.addEventListener('click', () => {
show_custom_dialog({
title: 'Domain Confirmation',
message: `<p style='color: #6c757d'>Are you sure to mark the domain <b>${href_url_domain}</b> as <b>IGNORED</b>?</p><p>Add to your ignored list. Links from this domain will be opened directly without confirmation.</p>`,
buttons: [
{
text: 'Yes, Ignore!',
onclick: () => {
add_to_ignoredlist(href_url_domain);
show_custom_dialog({
title: 'Domain Ignored',
message: `<p>Domain <b>${href_url_domain}</b> has been added to ignored list.</p><p>Links from this domain will be opened directly without confirmation.</p>`,
buttons: [
{
text: 'OK',
onclick: () => {}
}
]
});
}
},
{
text: 'No',
onclick: () => {},
focus: true
}
]
});
});
}
}, 100);
}
return false;
}, true);
window.addEventListener('beforeunload', function(e) {
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment