/******************************************************************************* 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(ex) { } 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(ex) { } 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(x) { } 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(ex) { 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, ], });