diff options
author | awy <awy@awy.one> | 2025-08-15 03:01:21 +0300 |
---|---|---|
committer | awy <awy@awy.one> | 2025-08-15 03:01:21 +0300 |
commit | a9370a08517668b3e98cc1d0bd42df407a76c220 (patch) | |
tree | 37e7bdb0e76f5495f798e077e45d377c0c3870c0 /data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js | |
parent | b73acfe395ea849fcd15c9886a7f4631f2b6f82b (diff) |
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js b/data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js new file mode 100644 index 0000000..eb88c27 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js @@ -0,0 +1,188 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { registerScriptlet } from './base.js'; +import { runAt } from './run-at.js'; +import { safeSelf } from './safe-self.js'; +import { urlSkip } from '../urlskip.js'; + +/******************************************************************************/ + +registerScriptlet(urlSkip, { + name: 'urlskip.fn', +}); + +/** + * @scriptlet href-sanitizer + * + * @description + * Set the `href` attribute to a value found in the DOM at, or below the + * targeted `a` element, and optionally with transformation steps. + * + * @param selector + * A plain CSS selector for elements which `href` property must be sanitized. + * + * @param source + * One or more tokens to lookup the source of the `href` property, and + * optionally the transformation steps to perform: + * - `text`: Use the text content of the element as the URL + * - `[name]`: Use the value of the attribute `name` as the URL + * - Transformation steps: see `urlskip` documentation + * + * If `text` or `[name]` is not present, the URL will be the value of `href` + * attribute. + * + * @example + * `example.org##+js(href-sanitizer, a)` + * `example.org##+js(href-sanitizer, a[title], [title])` + * `example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to)` + * `example.org##+js(href-sanitizer, a[href*="/redirect"], ?url ?url -base64)` + * + * */ + +function hrefSanitizer( + selector = '', + source = '' +) { + if ( typeof selector !== 'string' ) { return; } + if ( selector === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source); + if ( source === '' ) { source = 'text'; } + const sanitizeCopycats = (href, text) => { + let elems = []; + try { + elems = document.querySelectorAll(`a[href="${href}"`); + } + catch { + } + for ( const elem of elems ) { + elem.setAttribute('href', text); + } + return elems.length; + }; + const validateURL = text => { + if ( typeof text !== 'string' ) { return ''; } + if ( text === '' ) { return ''; } + if ( /[\x00-\x20\x7f]/.test(text) ) { return ''; } + try { + const url = new URL(text, document.location); + return url.href; + } catch { + } + return ''; + }; + const extractParam = (href, source) => { + if ( Boolean(source) === false ) { return href; } + const recursive = source.includes('?', 1); + const end = recursive ? source.indexOf('?', 1) : source.length; + try { + const url = new URL(href, document.location); + let value = url.searchParams.get(source.slice(1, end)); + if ( value === null ) { return href } + if ( recursive ) { return extractParam(value, source.slice(end)); } + return value; + } catch { + } + return href; + }; + const extractURL = (elem, source) => { + if ( /^\[.*\]$/.test(source) ) { + return elem.getAttribute(source.slice(1,-1).trim()) || ''; + } + if ( source === 'text' ) { + return elem.textContent + .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters + .replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters + ; + } + if ( source.startsWith('?') === false ) { return ''; } + const steps = source.replace(/(\S)\?/g, '\\1?').split(/\s+/); + const url = steps.length === 1 + ? extractParam(elem.href, source) + : urlSkip(elem.href, false, steps); + if ( url === undefined ) { return; } + return url.replace(/ /g, '%20'); + }; + const sanitize = ( ) => { + let elems = []; + try { + elems = document.querySelectorAll(selector); + } + catch { + return false; + } + for ( const elem of elems ) { + if ( elem.localName !== 'a' ) { continue; } + if ( elem.hasAttribute('href') === false ) { continue; } + const href = elem.getAttribute('href'); + const text = extractURL(elem, source); + const hrefAfter = validateURL(text); + if ( hrefAfter === '' ) { continue; } + if ( hrefAfter === href ) { continue; } + elem.setAttribute('href', hrefAfter); + const count = sanitizeCopycats(href, hrefAfter); + safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`); + } + return true; + }; + let observer, timer; + const onDomChanged = mutations => { + if ( timer !== undefined ) { return; } + let shouldSanitize = false; + for ( const mutation of mutations ) { + if ( mutation.addedNodes.length === 0 ) { continue; } + for ( const node of mutation.addedNodes ) { + if ( node.nodeType !== 1 ) { continue; } + shouldSanitize = true; + break; + } + if ( shouldSanitize ) { break; } + } + if ( shouldSanitize === false ) { return; } + timer = safe.onIdle(( ) => { + timer = undefined; + sanitize(); + }); + }; + const start = ( ) => { + if ( sanitize() === false ) { return; } + observer = new MutationObserver(onDomChanged); + observer.observe(document.body, { + subtree: true, + childList: true, + }); + }; + runAt(( ) => { start(); }, 'interactive'); +} +registerScriptlet(hrefSanitizer, { + name: 'href-sanitizer.js', + world: 'ISOLATED', + aliases: [ + 'urlskip.js', + ], + dependencies: [ + runAt, + safeSelf, + urlSkip, + ], +}); |