Created
October 2, 2025 17:17
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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 = " "; | |
| 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 += " " | |
| } | |
| 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 += " " | |
| } | |
| 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 += " " | |
| } | |
| 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