Censors information related to personal details on speedtest.net, so if you're recording, live-streaming, or even screensharing, it is automatically censored for you.
// ==UserScript==
// @name Speed-Test Information Censor
// @namespace http://tampermonkey.net/
// @version 1.6
// @description Censors information related to personal details on speedtest.net, so if you're recording, live-streaming, or even screensharing, it is automatically censored for you.
// @match https://www.speedtest.net/*
// @author EnterpriseExperience
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const xpath_table = [
'//*[@id="container"]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[2]/div/div/div/div[3]',
'/html/body/div[3]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[2]/div/div/div/div[3]',
'//*[@id="container"]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[3]/div/div/div/div[3]/span',
'/html/body/div[3]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[3]/div/div/div/div[3]/span',
'//*[@id="container"]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[2]/div/div/div/div[2]',
'/html/body/div[3]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[2]/div/div/div/div[2]',
'//*[@id="container"]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[3]/div/div/div/div[2]/a',
'/html/body/div[3]/div[1]/div[4]/div/div/div/div[2]/div[2]/div/div[4]/div/div[4]/div[1]/div[3]/div/div/div/div[2]/a',
'//*[@id="container"]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[1]',
'/html/body/div[3]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[1]',
'//*[@id="container"]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[2]',
'/html/body/div[3]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[2]',
'//*[@id="container"]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/h4',
'/html/body/div[3]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/h4',
'//*[@id="container"]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[3]/dl/dd[1]',
'/html/body/div[3]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[3]/dl/dd[1]',
'//*[@id="container"]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[3]/dl/dd[2]',
'/html/body/div[3]/div[3]/div/div/div/div/div/div[2]/div[2]/form/fieldset/div[3]/dl/dd[2]'
];
const class_selectors = [
'.js-data-isp',
'.js-data-ip',
'.result-label.js-data-isp',
'.result-data.js-data-ip'
];
const censor_node = (node) => {
console.log('[censor] censoring:', node);
node.textContent = '************';
node.removeAttribute('title');
node.removeAttribute('href');
};
const resolve_xpath = (xpath) => document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
const censor_by_class = () => {
for (const sel of class_selectors) {
const node = document.querySelector(sel);
if (node && node.textContent !== '************') {
console.log('[censor] hit by class selector:', sel, node);
censor_node(node);
}
}
};
const build_observer = (table, timeout_ms) => {
const pending = new Set(table);
const shutdown = () => {
obs.disconnect();
clearTimeout(deadline);
};
const try_censor = () => {
censor_by_class();
for (const xpath of [...pending]) {
const node = resolve_xpath(xpath);
if (node) {
console.log('[censor] trying xpath:', xpath, '-> found:', node);
censor_node(node);
pending.delete(xpath);
}
}
if (pending.size === 0) shutdown();
};
const obs = new MutationObserver(try_censor);
const deadline = setTimeout(() => {
obs.disconnect();
censor_by_class();
for (const xpath of pending) {
const node = resolve_xpath(xpath);
if (node) censor_node(node);
}
}, timeout_ms);
obs.observe(document.body, { childList: true, subtree: true });
};
build_observer(xpath_table, 60000);
})();