|
| 1 | +/******************************************************************************* |
| 2 | +
|
| 3 | + uBlock Origin - a comprehensive, efficient content blocker |
| 4 | + Copyright (C) 2019-present Raymond Hill |
| 5 | +
|
| 6 | + This program is free software: you can redistribute it and/or modify |
| 7 | + it under the terms of the GNU General Public License as published by |
| 8 | + the Free Software Foundation, either version 3 of the License, or |
| 9 | + (at your option) any later version. |
| 10 | +
|
| 11 | + This program is distributed in the hope that it will be useful, |
| 12 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | + GNU General Public License for more details. |
| 15 | +
|
| 16 | + You should have received a copy of the GNU General Public License |
| 17 | + along with this program. If not, see {http://www.gnu.org/licenses/}. |
| 18 | +
|
| 19 | + Home: https://github.com/gorhill/uBlock |
| 20 | +
|
| 21 | +*/ |
| 22 | + |
| 23 | +import { registerScriptlet } from './base.js'; |
| 24 | +import { runAt } from './run-at.js'; |
| 25 | +import { safeSelf } from './safe-self.js'; |
| 26 | +import { urlSkip } from '../urlskip.js'; |
| 27 | + |
| 28 | +/******************************************************************************/ |
| 29 | + |
| 30 | +registerScriptlet(urlSkip, { |
| 31 | + name: 'urlskip.fn', |
| 32 | +}); |
| 33 | + |
| 34 | +/** |
| 35 | + * @scriptlet href-sanitizer |
| 36 | + * |
| 37 | + * @description |
| 38 | + * Set the `href` attribute to a value found in the DOM at, or below the |
| 39 | + * targeted `a` element, and optionally with transformation steps. |
| 40 | + * |
| 41 | + * @param selector |
| 42 | + * A plain CSS selector for elements which `href` property must be sanitized. |
| 43 | + * |
| 44 | + * @param source |
| 45 | + * One or more tokens to lookup the source of the `href` property, and |
| 46 | + * optionally the transformation steps to perform: |
| 47 | + * - `text`: Use the text content of the element as the URL |
| 48 | + * - `[name]`: Use the value of the attribute `name` as the URL |
| 49 | + * - Transformation steps: see `urlskip` documentation |
| 50 | + * |
| 51 | + * If `text` or `[name]` is not present, the URL will be the value of `href` |
| 52 | + * attribute. |
| 53 | + * |
| 54 | + * @example |
| 55 | + * `example.org##+js(href-sanitizer, a)` |
| 56 | + * `example.org##+js(href-sanitizer, a[title], [title])` |
| 57 | + * `example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to)` |
| 58 | + * `example.org##+js(href-sanitizer, a[href*="/redirect"], ?url ?url -base64)` |
| 59 | + * |
| 60 | + * */ |
| 61 | + |
| 62 | +function hrefSanitizer( |
| 63 | + selector = '', |
| 64 | + source = '' |
| 65 | +) { |
| 66 | + if ( typeof selector !== 'string' ) { return; } |
| 67 | + if ( selector === '' ) { return; } |
| 68 | + const safe = safeSelf(); |
| 69 | + const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source); |
| 70 | + if ( source === '' ) { source = 'text'; } |
| 71 | + const sanitizeCopycats = (href, text) => { |
| 72 | + let elems = []; |
| 73 | + try { |
| 74 | + elems = document.querySelectorAll(`a[href="${href}"`); |
| 75 | + } |
| 76 | + catch(ex) { |
| 77 | + } |
| 78 | + for ( const elem of elems ) { |
| 79 | + elem.setAttribute('href', text); |
| 80 | + } |
| 81 | + return elems.length; |
| 82 | + }; |
| 83 | + const validateURL = text => { |
| 84 | + if ( typeof text !== 'string' ) { return ''; } |
| 85 | + if ( text === '' ) { return ''; } |
| 86 | + if ( /[\x00-\x20\x7f]/.test(text) ) { return ''; } |
| 87 | + try { |
| 88 | + const url = new URL(text, document.location); |
| 89 | + return url.href; |
| 90 | + } catch(ex) { |
| 91 | + } |
| 92 | + return ''; |
| 93 | + }; |
| 94 | + const extractURL = (elem, source) => { |
| 95 | + if ( /^\[.*\]$/.test(source) ) { |
| 96 | + return elem.getAttribute(source.slice(1,-1).trim()) || ''; |
| 97 | + } |
| 98 | + if ( source === 'text' ) { |
| 99 | + return elem.textContent |
| 100 | + .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters |
| 101 | + .replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters |
| 102 | + ; |
| 103 | + } |
| 104 | + if ( source.startsWith('?') ) { |
| 105 | + const steps = source.replace(/(\S)\?/g, '\\1?').split(/\s+/); |
| 106 | + const url = urlSkip(elem.href, false, steps); |
| 107 | + if ( url === undefined ) { return; } |
| 108 | + return url.replace(/ /g, '%20'); |
| 109 | + } |
| 110 | + return ''; |
| 111 | + }; |
| 112 | + const sanitize = ( ) => { |
| 113 | + let elems = []; |
| 114 | + try { |
| 115 | + elems = document.querySelectorAll(selector); |
| 116 | + } |
| 117 | + catch(ex) { |
| 118 | + return false; |
| 119 | + } |
| 120 | + for ( const elem of elems ) { |
| 121 | + if ( elem.localName !== 'a' ) { continue; } |
| 122 | + if ( elem.hasAttribute('href') === false ) { continue; } |
| 123 | + const href = elem.getAttribute('href'); |
| 124 | + const text = extractURL(elem, source); |
| 125 | + const hrefAfter = validateURL(text); |
| 126 | + if ( hrefAfter === '' ) { continue; } |
| 127 | + if ( hrefAfter === href ) { continue; } |
| 128 | + elem.setAttribute('href', hrefAfter); |
| 129 | + const count = sanitizeCopycats(href, hrefAfter); |
| 130 | + safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`); |
| 131 | + } |
| 132 | + return true; |
| 133 | + }; |
| 134 | + let observer, timer; |
| 135 | + const onDomChanged = mutations => { |
| 136 | + if ( timer !== undefined ) { return; } |
| 137 | + let shouldSanitize = false; |
| 138 | + for ( const mutation of mutations ) { |
| 139 | + if ( mutation.addedNodes.length === 0 ) { continue; } |
| 140 | + for ( const node of mutation.addedNodes ) { |
| 141 | + if ( node.nodeType !== 1 ) { continue; } |
| 142 | + shouldSanitize = true; |
| 143 | + break; |
| 144 | + } |
| 145 | + if ( shouldSanitize ) { break; } |
| 146 | + } |
| 147 | + if ( shouldSanitize === false ) { return; } |
| 148 | + timer = safe.onIdle(( ) => { |
| 149 | + timer = undefined; |
| 150 | + sanitize(); |
| 151 | + }); |
| 152 | + }; |
| 153 | + const start = ( ) => { |
| 154 | + if ( sanitize() === false ) { return; } |
| 155 | + observer = new MutationObserver(onDomChanged); |
| 156 | + observer.observe(document.body, { |
| 157 | + subtree: true, |
| 158 | + childList: true, |
| 159 | + }); |
| 160 | + }; |
| 161 | + runAt(( ) => { start(); }, 'interactive'); |
| 162 | +} |
| 163 | +registerScriptlet(hrefSanitizer, { |
| 164 | + name: 'href-sanitizer.js', |
| 165 | + world: 'ISOLATED', |
| 166 | + aliases: [ |
| 167 | + 'urlskip.js', |
| 168 | + ], |
| 169 | + dependencies: [ |
| 170 | + runAt, |
| 171 | + safeSelf, |
| 172 | + urlSkip, |
| 173 | + ], |
| 174 | +}); |
0 commit comments