If you support users of Microsoft security products, you may need to clean so-called SafeLinks. While they are functional, they actually cause problems for some users, and they are just ugly. I had previously created a Safelink decoder (based on somebody else’s efforts), and today I received a request to add a WordPress utility to identify when they are used. After thinking about this for a while, I turned to ChatGPT and started our 2-hour discussion on how to implement the idea. The final result is an accessible solution that enables editors to identify and clean the safelink with just a couple of clicks.
The biggest challenge was navigating the block editor to save the change.
The code
<?php
/**
* WPCodeBox: SafeLinks — A11y Flag (#D2196F, #FFF, 14px) + aria-describedby + Clean (React) + Apply + Persist
*
* - Flags *.safelinks.protection.outlook.com links in the editor canvas with accessible styling.
* - Adds a screen-reader note via aria-describedby.
* - Adds "Clean SafeLink" + "Apply cleaned URL" in the Link panel.
* - Uses native value setter + InputEvent so React LinkControl updates properly.
* - Patches selected block attributes AFTER native save completes to prevent overwrites.
* - Cleans up aria-describedby references after cleaning.
*/
add_action('enqueue_block_editor_assets', function () {
// ---- Accessible styles (contrast-safe) ----
$css = <<<CSS
a.fw-safelink-flagged{
outline:1px solid #D2196F !important; /* Accessible magenta */
outline-offset:2px;
position:relative;
border-radius:1px 0 1px 1px;
}
a.fw-safelink-flagged::after{
content:"Has Safelink";
font:13px/1.2 system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif; /* 14px for legibility */
background:#D2196F;
color:#FFF !important;
padding:2px 6px;
border-radius:.25rem .25rem 0 0;
position:absolute;
top:-1.3rem;
right:-3px;
pointer-events:none;
}
.fw-safelink-toolbar{
margin-top:.5rem; display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;
}
.fw-safelink-toolbar .fw-btn{
border:1px solid #2271b1; background:#2271b1; color:#fff; border-radius:4px;
padding:.35rem .6rem; font-size:12px; line-height:1.4; cursor:pointer;
}
.fw-safelink-toolbar .fw-btn[disabled]{ opacity:.5; cursor:not-allowed; }
.fw-safelink-toolbar .fw-note{ font-size:12px; color:#1e1e1e; }
.fw-safelink-toolbar .fw-note.ok{ color:#1a7f37; }
.fw-safelink-toolbar .fw-note.error{ color:#d63638; }
CSS;
wp_register_style('fw-safelink-editor', false, [], null);
wp_enqueue_style('fw-safelink-editor');
wp_add_inline_style('fw-safelink-editor', $css);
// Need wp-data + wp-block-editor for persistence
wp_register_script('fw-safelink-editor', false, ['wp-data','wp-block-editor','wp-dom-ready'], null, true);
$js = <<<JS
(function(){
// ---------------- Constants & helpers ----------------
const SAFE = /https:\\/\\/[a-z0-9.-]*safelinks\\.protection\\.outlook\\.com\\//i;
function isSafeLink(u){ return typeof u==='string' && SAFE.test(u); }
function decodeSafeLink(u){
if(!isSafeLink(u)) return null;
const normalized = u.replace(/&/g,'&');
const q = (normalized.split('?')[1]||'');
let urlParam = null;
q.split('&').forEach(kv=>{
const p = kv.split('=');
if(p.length===2 && decodeURIComponent(p[0])==='url'){ urlParam = p[1]; }
});
if(urlParam==null) return null;
let prev=null, cur=urlParam;
for(let i=0;i<3;i++){ prev=cur; try{cur=decodeURIComponent(cur);}catch(e){} if(cur===prev) break; }
return /^https?:\\/\\//i.test(cur) ? cur : null;
}
// React-controlled input helpers
function setNativeInputValue(el, value){
const proto = Object.getPrototypeOf(el);
const valueSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
const ownSetter = Object.getOwnPropertyDescriptor(el, 'value')?.set;
if (ownSetter && valueSetter && ownSetter !== valueSetter) valueSetter.call(el, value);
else if (valueSetter) valueSetter.call(el, value);
else el.value = value;
}
function fireReactInput(el){
try {
el.dispatchEvent(new InputEvent('input', { bubbles:true, composed:true, inputType:'insertReplacementText' }));
} catch(e) {
el.dispatchEvent(new Event('input', { bubbles:true }));
}
el.dispatchEvent(new Event('change', { bubbles:true }));
}
// Replace URLs in strings / HTML (for attribute patching)
function replaceSafeLinksInHtml(html){
if(typeof html!=='string' || !html) return html;
if(!html.includes('safelinks.protection.outlook.com')) return html;
const div=document.createElement('div'); div.innerHTML=html;
div.querySelectorAll('a[href]').forEach(a=>{
const href=(a.getAttribute('href')||'').replace(/&/g,'&');
if(isSafeLink(href)){
const dec=decodeSafeLink(href);
if(dec){
a.setAttribute('href', dec);
const t=(a.textContent||'').trim();
if(t && isSafeLink(t)) a.textContent=dec;
}
}
});
return div.innerHTML;
}
function replaceSafeLinksInString(val){
if(typeof val!=='string' || !val) return val;
if(isSafeLink(val)){ const dec=decodeSafeLink(val); return dec?dec:val; }
if(!val.includes('safelinks.protection.outlook.com')) return val;
return val.replace(/https?:\\/\\/[a-z0-9.-]*safelinks\\.protection\\.outlook\\.com\\/\\?[^\\s"']+/gi, m=>{
const dec=decodeSafeLink(m); return dec?dec:m;
});
}
// ---------------- Accessibility: aria-describedby management ----------------
function ensureSrContainer(){
let c = document.querySelector('#fw-safelink-sr-notes');
if(!c){
c = document.createElement('div');
c.id = 'fw-safelink-sr-notes';
c.className = 'screen-reader-text';
c.setAttribute('aria-hidden','false');
document.body.appendChild(c);
}
return c;
}
function addSrNote(text){
const container = ensureSrContainer();
const id = 'fw-safelink-note-' + Math.random().toString(36).slice(2);
const div = document.createElement('div');
div.id = id;
div.textContent = text;
container.appendChild(div);
return id;
}
function describeSafeLink(anchor){
if(anchor.hasAttribute('data-fw-safelink-described')) return;
const id = addSrNote('Microsoft SafeLink detected. Use the "Clean SafeLink" button in the link panel to restore the original URL.');
const existing = (anchor.getAttribute('aria-describedby')||'').trim();
anchor.setAttribute('aria-describedby', (existing ? existing+' ' : '') + id);
anchor.setAttribute('data-fw-safelink-described','1');
anchor.setAttribute('data-fw-safelink-sr-id', id); // Store for cleanup
}
function removeSrDescription(anchor){
const id = anchor.getAttribute('data-fw-safelink-sr-id');
if(id){
const note = document.getElementById(id);
if(note) note.remove();
const described = (anchor.getAttribute('aria-describedby')||'').split(' ').filter(x => x !== id).join(' ');
if(described) anchor.setAttribute('aria-describedby', described);
else anchor.removeAttribute('aria-describedby');
anchor.removeAttribute('data-fw-safelink-sr-id');
anchor.removeAttribute('data-fw-safelink-described');
}
}
// ---------------- Persist to selected block ----------------
function persistCleanSelectedBlock(){
const select = wp.data.select('core/block-editor');
const dispatch = wp.data.dispatch('core/block-editor');
const clientId = select.getSelectedBlockClientId?.();
if(!clientId) return false;
const block = select.getBlock(clientId);
if(!block) return false;
const attrs = { ...block.attributes };
const patch = {};
let changed = false;
// Obvious URL slots
['url','href','linkUrl','textLink'].forEach(key=>{
if(typeof attrs[key]==='string' && isSafeLink(attrs[key])){
const next = replaceSafeLinksInString(attrs[key]);
if(next !== attrs[key]){ patch[key]=next; changed=true; }
}
});
// HTML-bearing
['content','value','caption','description'].forEach(key=>{
if(typeof attrs[key]==='string' && attrs[key].includes('safelinks.protection.outlook.com')){
const hasHtml = /<\\w+[^>]*>/.test(attrs[key]);
const next = hasHtml ? replaceSafeLinksInHtml(attrs[key]) : replaceSafeLinksInString(attrs[key]);
if(next !== attrs[key]){ patch[key]=next; changed=true; }
}
});
// Safety sweep
for(const key in attrs){
if(patch[key]!==undefined) continue;
const v = attrs[key];
if(typeof v==='string' && v.includes('safelinks.protection.outlook.com')){
const hasHtml = /<\\w+[^>]*>/.test(v);
const next = hasHtml ? replaceSafeLinksInHtml(v) : replaceSafeLinksInString(v);
if(next !== v){ patch[key]=next; changed=true; }
}
}
if(changed) dispatch.updateBlockAttributes(clientId, patch);
return changed;
}
// ---------------- Link panel controls ----------------
function findUrlField(panel){
let el = panel.querySelector('.block-editor-link-control__search-input input');
if(el) return el;
const ctrls = panel.querySelectorAll('.components-base-control');
for(let i=0;i<ctrls.length;i++){
const lab=ctrls[i].querySelector('.components-base-control__label');
if(lab){
const t=(lab.textContent||'').trim().toLowerCase();
if(t==='link' || t.includes('url') || (t.includes('link') && !t.includes('text'))){
const input = ctrls[i].querySelector('input,textarea'); if(input) return input;
}
}
}
const cand = panel.querySelectorAll('input,textarea');
for(let j=0;j<cand.length;j++){ const v=(cand[j].value||'').trim(); if(isSafeLink(v)) return cand[j]; }
const all = panel.querySelectorAll('.components-text-control__input');
return all.length ? all[all.length-1] : null;
}
function findApplyButton(panel){
let btn = panel.querySelector('button.components-button.is-primary, button[aria-label="Apply"], button[aria-label="Save"]');
if(btn) return btn;
const all = panel.querySelectorAll('button');
for(let i=0;i<all.length;i++){
const t=(all[i].textContent||'').trim().toLowerCase();
if(t==='save' || t==='apply') return all[i];
}
return null;
}
function simulateEnter(el){
['keydown','keypress','keyup'].forEach(evt=>{
const ev = new KeyboardEvent(evt, {key:'Enter', code:'Enter', keyCode:13, which:13, bubbles:true, cancelable:true});
el.dispatchEvent(ev);
});
const form = el.closest('form');
if(form) form.dispatchEvent(new Event('submit', {bubbles:true, cancelable:true}));
}
function injectToolbar(panel){
if(panel.querySelector('.fw-safelink-toolbar')) return;
const urlField = findUrlField(panel);
if(!urlField) return;
const bar = document.createElement('div');
bar.className = 'fw-safelink-toolbar';
const cleanBtn = document.createElement('button');
cleanBtn.type = 'button';
cleanBtn.className = 'fw-btn';
cleanBtn.textContent = 'Clean SafeLink';
const applyBtn = document.createElement('button');
applyBtn.type = 'button';
applyBtn.className = 'fw-btn';
applyBtn.textContent = 'Apply cleaned URL';
applyBtn.disabled = true;
const note = document.createElement('span');
note.className = 'fw-note';
note.textContent = 'Enter or paste a SafeLink to enable cleaning.';
bar.append(cleanBtn, applyBtn, note);
const target = urlField.closest('.components-base-control') || urlField.parentElement || panel;
target.insertAdjacentElement('afterend', bar);
function refresh(){
const val = (urlField.value||'').trim();
const canClean = isSafeLink(val);
cleanBtn.disabled = !canClean;
note.classList.toggle('ok', canClean);
note.classList.remove('error');
note.textContent = canClean ? 'SafeLink detected. Click "Clean SafeLink".'
: 'Enter or paste a SafeLink to enable cleaning.';
}
refresh();
urlField.addEventListener('input', refresh);
// Keep panel open on our clicks
['pointerdown','mousedown'].forEach(evt=>{
[cleanBtn, applyBtn].forEach(btn=>{
btn.addEventListener(evt, e=>{
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation?.();
requestAnimationFrame(()=>{ try{ urlField.focus(); }catch(_){} });
}, {capture:true});
});
});
// CLEAN: write to controlled input via native setter (don't persist yet)
cleanBtn.addEventListener('click', e=>{
e.preventDefault(); e.stopPropagation();
const raw = (urlField.value||'').trim();
const cleaned = decodeSafeLink(raw);
if(!cleaned || cleaned.trim() === ''){
alert('Could not parse original URL from this SafeLink.');
return;
}
// UI: native setter so React state updates (enables Apply)
setNativeInputValue(urlField, cleaned);
fireReactInput(urlField);
applyBtn.disabled = false;
note.textContent = '✅ Cleaned field. Review and click "Apply cleaned URL".';
note.classList.add('ok');
note.classList.remove('error');
requestAnimationFrame(()=>{
try{
urlField.focus();
const len=urlField.value.length;
urlField.setSelectionRange?.(len, len);
}catch(_){}
});
});
// APPLY: native Apply/Save first, THEN persist block attributes
applyBtn.addEventListener('click', e=>{
e.preventDefault(); e.stopPropagation();
const saveBtn = findApplyButton(panel);
// Helper to persist after save completes
const cleanAfterSave = () => {
setTimeout(() => {
let persisted = false;
let error = false;
try {
persisted = persistCleanSelectedBlock();
} catch(err) {
console.error('Failed to persist block attributes:', err);
error = true;
}
if(error){
note.textContent = '⚠️ Applied to field but attribute update failed.';
note.classList.remove('ok');
note.classList.add('error');
} else {
note.textContent = persisted
? '✅ Applied & cleaned all block attributes.'
: '✅ Applied. (No SafeLinks found in block attributes)';
note.classList.add('ok');
note.classList.remove('error');
}
// Clean up canvas flags and aria-describedby
requestAnimationFrame(()=>{
const docs = getEditorDocs();
docs.forEach(d=>{
d.querySelectorAll('a.fw-safelink-flagged').forEach(a=>{
const href = (a.getAttribute('href')||'').trim();
if(!isSafeLink(href)){
a.classList.remove('fw-safelink-flagged');
a.removeAttribute('title');
removeSrDescription(a);
}
});
});
});
}, 150); // Allow native save to complete
};
if(saveBtn){
saveBtn.removeAttribute('disabled');
saveBtn.removeAttribute('aria-disabled');
saveBtn.click();
cleanAfterSave();
} else {
simulateEnter(urlField);
cleanAfterSave();
}
applyBtn.disabled = true;
requestAnimationFrame(()=>{ try{ urlField.focus(); }catch(_){ } });
});
}
// ---------------- Canvas flagging (with aria-describedby) ----------------
function injectFlagCssInto(doc){
if(!doc || !doc.head) return;
if(doc.querySelector('style[data-fw-safelink-css]')) return;
const style = doc.createElement('style');
style.type = 'text/css';
style.setAttribute('data-fw-safelink-css','1');
style.textContent =
'a.fw-safelink-flagged{outline:2px solid #D2196F !important;outline-offset:2px;position:relative}'+
'a.fw-safelink-flagged::after{content:"SafeLink";font:14px/1.6 system-ui,-apple-system,\\'Segoe UI\\',Roboto,Arial,sans-serif;background:#D2196F;color:#FFF !important;padding:0 6px;border-radius:999px;position:absolute;top:-0.9rem;right:-0.3rem;pointer-events:none}';
doc.head.appendChild(style);
}
function getEditorDocs(){
const docs=[document];
document.querySelectorAll('iframe.editor-canvas__iframe, iframe.block-editor-iframe__frame, iframe.components-iframe-editor__frame, iframe[name="editor-canvas"]').forEach(ifr=>{
try{
const d = ifr.contentDocument || ifr.contentWindow?.document;
if(d) docs.push(d);
}catch(_){}
});
return Array.from(new Set(docs));
}
function flagAndDescribe(doc){
const root = doc.querySelector('.editor-styles-wrapper') || doc;
root.querySelectorAll('a[href*="safelinks.protection.outlook.com"]').forEach(a=>{
if(!a.classList.contains('fw-safelink-flagged')){
a.classList.add('fw-safelink-flagged');
a.title = 'Microsoft SafeLink detected.';
// Add aria-describedby for AT users
describeSafeLink(a);
}
});
}
// ---------------- Scan & observe ----------------
function scan(){
// Inject toolbar into any open Link panels
document.querySelectorAll(
'.components-popover__content .block-editor-link-control,' +
'.components-modal__frame .block-editor-link-control'
).forEach(injectToolbar);
// Canvas flags + a11y notes (top + iframes)
const docs = getEditorDocs();
docs.forEach(d=>{
if(d!==document) injectFlagCssInto(d);
flagAndDescribe(d);
});
}
new MutationObserver(()=>requestAnimationFrame(scan))
.observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:['href','class','style']});
function watchIframes(){
document.querySelectorAll('iframe').forEach(ifr=>{
if(ifr.__fw_watch) return;
ifr.__fw_watch = true;
ifr.addEventListener('load', ()=>setTimeout(scan, 50));
});
}
new MutationObserver(watchIframes).observe(document.body, {childList:true, subtree:true});
watchIframes();
wp.domReady(scan);
})();
JS;
wp_enqueue_script('fw-safelink-editor');
wp_add_inline_script('fw-safelink-editor', $js);
});


