From a478c3086c632a333698d3bca8b93da5b1a2d28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Mon, 19 Feb 2024 20:14:12 +0100 Subject: [PATCH] Convert to TypeScript --- .../{href-sanitizer.js => href-sanitizer.ts} | 67 +++++++++++-------- tests/scriptlets/href-sanitizer.test.js | 39 +++++++++-- 2 files changed, 70 insertions(+), 36 deletions(-) rename src/scriptlets/{href-sanitizer.js => href-sanitizer.ts} (74%) diff --git a/src/scriptlets/href-sanitizer.js b/src/scriptlets/href-sanitizer.ts similarity index 74% rename from src/scriptlets/href-sanitizer.js rename to src/scriptlets/href-sanitizer.ts index 9c5707342..5100f9395 100644 --- a/src/scriptlets/href-sanitizer.js +++ b/src/scriptlets/href-sanitizer.ts @@ -91,7 +91,11 @@ import { * @added unknown. */ -export function hrefSanitizer(source, selector, attribute = 'text') { +export function hrefSanitizer( + source: Source, + selector: string, + attribute = 'text', +) { if (!selector) { logMessage(source, 'Selector is required.'); return; @@ -109,61 +113,66 @@ export function hrefSanitizer(source, selector, attribute = 'text') { /** * Extracts text from an element based on the specified attribute. * - * @param {Element} element - The element from which to extract the text. - * @param {string} attribute - The attribute indicating how to extract the text. + * @param {HTMLAnchorElement} anchor - The element from which to extract the text. + * @param {string} attr - The attribute indicating how to extract the text. * @returns {string} The extracted text. */ - const extractText = (element, attribute) => { - if (attribute === 'text') { - return element.textContent - .replace(regexpNotValidAtStart, '') - .replace(regexpNotValidAtEnd, ''); + const extractNewHref = (anchor: HTMLAnchorElement, attr: string): string => { + if (attr === 'text') { + return anchor.textContent + ? anchor.textContent.replace(regexpNotValidAtStart, '') + .replace(regexpNotValidAtEnd, '') + : ''; } - if (attribute.startsWith('?')) { + if (attr.startsWith('?')) { try { - const url = new URL(element.href, document.location); - return url.searchParams.get(attribute.slice(1)) || ''; + const url = new URL(anchor.href, document.location.href); + return url.searchParams.get(attr.slice(1)) || ''; } catch (ex) { + logMessage( + source, + `Cannot retrieve the parameter '${attr.slice(1)}' from the URL '${anchor.href}`, + ); return ''; } } - if (attribute.startsWith('[') && attribute.endsWith(']')) { - return element.getAttribute(attribute.slice(1, -1)) || ''; + if (attr.startsWith('[') && attr.endsWith(']')) { + return anchor.getAttribute(attr.slice(1, -1)) || ''; } return ''; }; /** * Validates a URL, if valid return URL, - * otherwise return empty string. + * otherwise return null. * * @param {string} text - The URL to be validated - * @returns {string} - URL + * @returns {string} - URL for valid URL, otherwise null. */ - const validateURL = (text) => { + const getValidURL = (text: string): string | null => { if (!text) { - return ''; + return null; } try { - const { href } = new URL(text, document.location); + const { href } = new URL(text, document.location.href); return href; } catch { - return ''; + return null; } }; /** * Sanitizes the href attribute of elements matching the given selector. * - * @param {string} selector - The CSS selector to match the elements. + * @param {string} elem - The CSS selector to match the elements. * @returns {void} */ - const sanitize = (selector) => { + const sanitize = (elem: string) => { let elements; try { - elements = document.querySelectorAll(selector); + elements = document.querySelectorAll(elem); } catch (e) { - logMessage(source, `Failed to find elements matching selector "${selector}"`); + logMessage(source, `Failed to find elements matching selector "${elem}"`); return; } @@ -173,17 +182,17 @@ export function hrefSanitizer(source, selector, attribute = 'text') { for (let index = 0; index < elements.length; index += 1) { try { - const element = elements[index]; + const element = elements[index] as HTMLAnchorElement; if (element.nodeName.toLowerCase() !== 'a' || !element.hasAttribute('href')) { continue; } - const href = element.getAttribute('href'); - const text = extractText(element, attribute); - const validatedHref = validateURL(text); - if (validatedHref === '' || validatedHref === href) { + const newHref = extractNewHref(element, attribute); + const newValidHref = getValidURL(newHref); + if (!newValidHref) { + logMessage(source, `Invalid URL: ${newHref}`); continue; } - element.setAttribute('href', text); + element.setAttribute('href', newValidHref); } catch (ex) { logMessage(source, `Failed to sanitize ${elements[index]}.`); } diff --git a/tests/scriptlets/href-sanitizer.test.js b/tests/scriptlets/href-sanitizer.test.js index 812269964..a34cfda89 100644 --- a/tests/scriptlets/href-sanitizer.test.js +++ b/tests/scriptlets/href-sanitizer.test.js @@ -58,7 +58,7 @@ test('Checking if alias name works', (assert) => { test('Santize href - text content', (assert) => { const expectedHref = 'https://example.org/'; - const elem = createElem('https://tracker.com/foo?redirect=https%3A%2F%2Fexample.org%2F', expectedHref); + const elem = createElem('https://example.com/foo?redirect=https%3A%2F%2Fexample.org%2F', expectedHref); const selector = 'a[href*="?redirect="]'; const scriptletArgs = [selector]; @@ -86,20 +86,20 @@ test('Santize href - text content, create element after running scriptlet', (ass test('Santize href - text content special characters', (assert) => { const expectedHref = 'https://example.com/search?q=łódź'; - const elem = createElem('https://tracker.com/foo', expectedHref); - const selector = 'a[href*="//tracker.com"]'; + const elem = createElem('https://example.org/foo', expectedHref); + const selector = 'a[href*="//example.org"]'; const scriptletArgs = [selector]; runScriptlet(name, scriptletArgs); - assert.strictEqual(elem.getAttribute('href'), expectedHref, 'href has been sanitized'); + assert.strictEqual(decodeURIComponent(elem.getAttribute('href')), expectedHref, 'href has been sanitized'); assert.strictEqual(window.hit, 'FIRED'); }); test('Santize href - text content, Twitter like case', (assert) => { - const elem = createElem('https://tracker.com/foo', 'https://agrd.io/promo_turk_83off…'); // Link from Twitter/X + const elem = createElem('https://example.com/foo', 'https://agrd.io/promo_turk_83off…'); // Link from Twitter/X const expectedHref = 'https://agrd.io/promo_turk_83off'; - const selector = 'a[href*="//tracker.com"]'; + const selector = 'a[href*="//example.com"]'; const scriptletArgs = [selector]; runScriptlet(name, scriptletArgs); @@ -109,7 +109,7 @@ test('Santize href - text content, Twitter like case', (assert) => { }); test('Santize href - query parameter 1', (assert) => { - const elem = createElem('https://tracker.com/foo?redirect=https://example.org/'); + const elem = createElem('https://example.com/foo?redirect=https://example.org/'); const expectedHref = 'https://example.org/'; const selector = 'a[href*="?redirect="]'; const attr = '?redirect'; @@ -146,3 +146,28 @@ test('Santize href - get href from attribute', (assert) => { assert.strictEqual(elem.getAttribute('href'), expectedHref, 'href has been sanitized'); assert.strictEqual(window.hit, 'FIRED'); }); + +test('Santize href - invalid URL', (assert) => { + const expectedHref = 'https://foo.com/bar'; + const elem = createElem(expectedHref, 'https://?'); + const selector = 'a[href="https://foo.com/bar"]'; + + const scriptletArgs = [selector]; + runScriptlet(name, scriptletArgs); + + assert.strictEqual(elem.getAttribute('href'), expectedHref, 'href has not been changed'); + assert.strictEqual(window.hit, 'FIRED'); +}); + +test('Santize href - parameter, invalid URL', (assert) => { + const expectedHref = 'https://?example.com/foo?redirect=https://example.org/'; + const elem = createElem(expectedHref); + const selector = 'a[href*="?redirect="]'; + const attr = '?redirect'; + + const scriptletArgs = [selector, attr]; + runScriptlet(name, scriptletArgs); + + assert.strictEqual(elem.getAttribute('href'), expectedHref, 'href has not been changed'); + assert.strictEqual(window.hit, 'FIRED'); +});