remove google tracking UWAA

remove google tracking

当前为 2020-04-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @namespace      jp.sceneq.rgtuwaaa
// @name           remove google tracking UWAA
// @description    remove google tracking
// @homepageURL    https://github.com/sceneq/RemoveGoogleTracking
// @version        0.20
// @include        https://www.google.*/*
// @grant          none
// @run-at         document-body
// @author         sceneq
// @license        MIT
// ==/UserScript==

const yes = () => true;
const doNothing = () => {};
const $ = (s, n = document) => n.querySelector(s);
const $$ = (s, n = document) => [...n.querySelectorAll(s)];
const sleep = millis => new Promise(resolve => setTimeout(resolve, millis));
const zip = rows => rows[0].map((_, c) => rows.map(row => row[c]));

const rewriteProperties = props => {
	for (const p of props) {
		Object.defineProperty(p[0] || {}, p[1], {
			value: p[2],
			writable: false,
		});
	}
};

const waitUntilDeclare = ({ obj, property, interval }) => {
	console.debug('waitUntilDeclare', obj.toString(), property);
	return new Promise(async (resolve, reject) => {
		const propertyNames = property.split('.');
		let currObj = obj;
		for (const propertyName of propertyNames) {
			while (!(propertyName in currObj) || currObj[propertyName] === null) {
				await sleep(interval);
			}
			currObj = currObj[propertyName];
		}
		console.debug('waitUntilDeclare', obj.toString(), property, 'Done');
		resolve(currObj);
	});
};

const untrackBuilder = arg => {
	const r = arg.badParamsRegex;
	return a => {
		const href = a.getAttribute('href');
		if(href === null) return;
		if (href.startsWith('/url?')) {
			a.href = new URLSearchParams(href.slice(5)).get('q');
		} else {
			a.removeAttribute('ping');
			a.href = a.href.replace(r, '');
		}
	};
};

const Types = {
	search: Symbol('search'),
	isch: Symbol('image'),
	shop: Symbol('shop'),
	nws: Symbol('news'),
	vid: Symbol('video'),
	bks: Symbol('book'),
	maps: Symbol('maps'),
	fin: Symbol('finance'),
	toppage: Symbol('toppage'),
	// flights: Symbol('flights'),
};

const tbmToType = tbm => {
	if (tbm === null) {
		return Types.search;
	}
	const t = {
		isch: Types.isch,
		shop: Types.shop,
		nws: Types.nws,
		vid: Types.vid,
		bks: Types.bks,
		fin: Types.fin,
	}[tbm];
	if (t === undefined) {
		return null;
	} else {
		return t;
	}
};

const BadParamsBase = [
	'biw', // offsetWidth
	'bih', // offsetHeight
	'ei',
	'sa',
	'ved',
	'source',
	'prds',
	'bvm',
	'bav',
	'psi',
	'stick',
	'dq',
	'ech',
	'gs_gbg',
	'gs_rn',
	'cp',
	'ictx',
	'cshid',
	'gs_lcp',
];

const BadParams = (() => {
	const o = {};
	o[Types.search] = [
		'pbx',
		'dpr',
		'pf',
		'gs_rn',
		'gs_mss',
		'pq',
		'cp',
		'oq',
		'sclient',
		'gs_l',
		'aqs',
	];
	o[Types.isch] = [
		'scroll',
		'vet',
		'yv',
		'iact',
		'forward',
		'ndsp',
		'csi',
		'tbnid',
		'sclient',
		'oq',
	];
	o[Types.shop] = ['oq'];
	o[Types.nws] = ['oq'];
	o[Types.vid] = ['oq'];
	o[Types.bks] = ['oq'];
	o[Types.fin] = ['oq'];
	o[Types.toppage] = ['oq', 'sclient', 'uact'];
	o[Types.maps] = ['psi'];
	return o;
})();

const searchFormUriuri = async arg => {
	let form = null;
	if (arg.pageType.mobileOld) {
		form = $('#sf');
	} else if (arg.pageType.ty === Types.isch) {
		form = await waitUntilDeclare({
			obj: window,
			property: 'sf',
			interval: 30,
		});
	} else {
		form = await waitUntilDeclare({
			obj: window,
			property: 'tsf',
			interval: 30,
		});
	}

	if (form === null) {
		console.warn('form === null');
		return;
	}

	for (const i of form) {
		if (i.tagName !== 'INPUT') continue;
		if (arg.badParams.includes(i.name)) {
			i.parentElement.removeChild(i);
		}
	}
	const orig = form.appendChild.bind(form);
	form.appendChild = e => {
		if (!arg.badParams.includes(e.name)) {
			orig(e);
		}
	};
};

const untrackAnchors = (untrack, arg) => {
	return waitUntilDeclare({
		obj: window,
		property: arg.pageType.mobile
			? 'topstuff'
			: arg.pageType.mobileOld
			? 'rmenu'
			: 'search',
		interval: 30,
	}).then(_ => {
		for (const a of $$('a')) {
			untrack(a);
		}
	});

};

const gcommon = async arg => {
	const untrack = untrackBuilder(arg);
	const p1 = waitUntilDeclare({
		obj: window,
		property: 'google',
		interval: 30,
	}).then(google => {
		rewriteProperties([
			[google, 'log', yes],
			[google, 'logUrl', doNothing],
			[google, 'getLEI', yes],
			[google, 'ctpacw', yes],
			[google, 'csiReport', yes],
			[google, 'report', yes],
			[google, 'aft', yes],
			//[google, 'kEI', '0'],
			//[google, 'getEI', yes], or ()=>"0"
		]);
	});
	rewriteProperties([
		[window, 'rwt', doNothing],
		[window.gbar_, 'Rm', doNothing],
	]);

	const p2 = untrackAnchors(untrack, arg);
	const p3 = searchFormUriuri(arg);
	await Promise.all([p1, p2, p3]);

	if (arg.pageType.mobile) {
		const sel =
			arg.pageType.ty === Types.search
				? '#bres + h1 + div > div + div'
				: '#bres + div > div + div';
		new MutationObserver(mutations => {
			const nodes = [];
			for (const m of mutations) {
				nodes.push(...m.addedNodes);
			}
			for (const n of nodes) {
				new MutationObserver((_, obs) => {
					console.debug('untrack', n);
					for (const a of $$('a', n)) {
						untrack(a);
					}
					obs.disconnect();
				}).observe(n, { childList: true });
			}
		}).observe($(sel), {
			childList: true,
		});
	}
};

const fun = {};

// TODO mobile, mobileOld
fun[Types.toppage] = searchFormUriuri;

fun[Types.search] = gcommon;
fun[Types.vid] = gcommon;
fun[Types.nws] = gcommon;
fun[Types.bks] = gcommon;
fun[Types.fin] = gcommon;

fun[Types.isch] = async arg => {
	// TODO desktop, mobile, mobileOld
	const untrack = untrackBuilder(arg);
	const p1 = waitUntilDeclare({
		obj: window,
		property: 'islmp',
		interval: 30,
	}).then(() => {
		for (const a of $$('a')) {
			untrack(a);
		}
	});
	const p2 = searchFormUriuri(arg);
	await Promise.all([p1, p2]);
};

fun[Types.shop] = async arg => {
	// TODO mobile, mobileOld
	const untrack = untrackBuilder(arg);
	const p1 = waitUntilDeclare({
		obj: window,
		property: 'google.pmc.spop.r',
		interval: 30,
	}).then(shopObj => {
		// Rewrite to original link
		const tmp = $$("div[class$='__content'] a[jsaction='spop.c']");
		const [anchors, thumbs] = [0, 1].map(m =>
			tmp.filter((_, i) => i % 2 === m)
		);
		const shops = Object.values(shopObj);
		const links = shops.map(a => a[34][6]);
		if (anchors.length === links.length) {
			for (const [anchor, link, thumb, shop] of zip([
				anchors,
				links,
				thumbs,
				shops,
			])) {
				anchors.href = thumb.href = link;
				shop[3][0][1] = link;
				shop[14][1] = link;
				shop[89][16] = link;
				shop[89][18][0] = link;
				shop[85][3] = link;
			}
		} else {
			console.warn('length does not match', anchors.length, links.length);
		}
	});

	const p2 = untrackAnchors(untrack, arg);
	const p3 = searchFormUriuri(arg);
	await Promise.all([p1, p2, p3]);
};
// TODO fun[Types.maps] = async arg => {}

(async () => {
	'use strict';
	console.debug('rgt: init');
	console.time('rgt');

	const ty = (() => {
		if (location.pathname.startsWith('/maps')) {
			return Types.maps;
		}
		if (location.pathname === '/' || location.pathname === '/webhp') {
			return Types.toppage;
		}
		const tbm = new URLSearchParams(location.search).get('tbm');
		if (location.pathname === '/search') {
			return tbmToType(tbm);
		}
		return null;
	})();

	if (ty === null) {
		console.debug('ty === null');
		return;
	}

	const badParams = (() => {
		return [...BadParamsBase, ...BadParams[ty]];
	})();
	const badParamsRegex = new RegExp(
		'&(?:' + badParams.join('|') + ')=.*?(?=(&|$))',
		'g'
	);

	const badImageSrcRegex = /\/(?:(?:gen(?:erate)?|client|fp)_|log)204|(?:metric|csi)\.gstatic\.|(?:adservice)\.(google)/;
	Object.defineProperty(window.Image.prototype, 'src', {
		set: function(url) {
			if (!badImageSrcRegex.test(url)) {
				this.setAttribute('src', url);
			}
		},
	});

	Object.defineProperty(window.HTMLScriptElement.prototype, 'src', {
		set: function(url) {
			this.setAttribute('src', url.replace(badParamsRegex, ''));
		},
	});

	const badPaths = [
		'imgevent',
		'shopping\\/product\\/.*?\\/popout',
		'async/ecr',
		'async/bgasy',
	];
	const regBadPaths = new RegExp('^/(?:' + badPaths.join('|') + ')');
	const origOpen = XMLHttpRequest.prototype.open;
	window.XMLHttpRequest.prototype.open = function(act, path) {
		if (path === undefined) return;
		if (path.startsWith('https://aa.google.com/')) return;
		if (path.startsWith('https://play.google.com/log')) return;
		if (path.startsWith('https://www.google.com/log')) return;
		if (regBadPaths.test(path)) return;
		origOpen.apply(this, [act, path.replace(badParamsRegex, '')]);
	};

	if ('navigator' in window) {
		const origSendBeacon = navigator.sendBeacon.bind(navigator);
		navigator.sendBeacon = (path, data) => {
			if (path.startsWith('https://play.google.com/log')) return;
			if (badImageSrcRegex.test(path)) return;
			origSendBeacon(path, data);
		};
	}

	if (ty in fun) {
		const mobileOld = $("html[itemtype]") === null; // &liteui=2
		const mobile = !mobileOld && $('meta[name=viewport]') !== null;
		const arg = {
			pageType: {
				ty,
				mobile,
				mobileOld,
			},
			badParams,
			badParamsRegex,
		};
		console.debug('arg', arg);
		await fun[ty](arg);
	} else {
		console.warn(`key not found in fun: ${ty.toString()}`);
	}
	console.timeEnd('rgt');
})();