// ==UserScript==
// @name TrackingParameterRemover
// @namespace https://htsign.hateblo.jp
// @version 20240915-rev0
// @description remove tracking parameters
// @author htsign
// @match *://*/*
// @run-at document-start
// @grant GM_registerMenuCommand
// ==/UserScript==
const createStyle = params => {
const toKebab = s => s.replace(/(?<=[a-z])[A-Z]/g, m => `-${m.toLowerCase()}`);
return Object.entries(params)
.reduce((acc, [key, val]) => ({ ...acc, [toKebab(key)]: val }), {});
};
GM_registerMenuCommand('open settings', ev => {
const PADDING = 20;
const wrapper = Object.assign(
document.createElement('div'),
{
style: createStyle({
position: 'absolute',
left: PADDING,
top: PADDING,
}),
},
);
const shadowRoot = wrapper.shadowRoot ?? wrapper.attachShadow({ mode: 'open' });
document.body.append(wrapper);
});
(function() {
'use strict';
const TRACKING_TAGS = [
'#?utm_campaign',
'#?utm_content',
'#?utm_int',
'#?utm_medium',
'#?utm_source',
'_hsmi',
'_openstat',
'action_object_map',
'action_ref_map',
'action_type_map',
'fb_action_ids',
'fb_action_types',
'fb_ref',
'fb_source',
'ga_campaign',
'ga_content',
'ga_medium',
'ga_place',
'ga_source',
'ga_term',
'gs_l',
'guccounter',
'guce_referrer',
'guce_referrer_sig',
'gws_rd',
'hmb_campaign',
'hmb_medium',
'hmb_source',
'ref_src',
'ref_url',
'utm_campaign',
'utm_cid',
'utm_content',
'utm_int',
'utm_language',
'utm_medium',
'utm_name',
'utm_place',
'utm_pubreferrer',
'utm_reader',
'utm_source',
'utm_swu',
'utm_term',
'utm_userid',
'utm_viz_id',
'yclid',
'[email protected]',
'_encoding@amazon.*',
'linkCode@amazon.*',
'linkId@amazon.*',
'pd_rd_*@amazon.*',
'psc@amazon.*',
'qid@amazon.*',
'sbo@amazon.*',
'sprefix@amazon.*',
'sr@amazon.*',
'tag@amazon.*',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'bi?@google.*',
'client@google.*',
'dpr@google.*',
'ei@google.*',
'gws_rd@google.*',
'oq@google.*',
'sa@google.*',
'sei@google.*',
'source@google.*',
'ved@google.*',
'[email protected]',
'[email protected]',
'[email protected]',
'ref@*.nicovideo.jp',
'ss_id@*.nicovideo.jp',
'ss_pos@*.nicovideo.jp',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]/watch',
];
/**
* @param {Location} loc
* @returns {{ url: string, locationChanged: boolean }}
*/
const removeTracking = loc => {
let locationChanged = false;
const escapables = Object.freeze({
'.': '\.',
});
const wildcardCharacters = Object.freeze({
'*': '.*',
'?': '.',
});
const wildcardKeys = Object.keys(wildcardCharacters);
/**
* @param {string} pattern
* @returns {RegExp}
*/
const toRegExp = pattern => {
/**
*
* @param {Record<string, string>} table
* @param {string} s
* @returns {string}
*/
const replace = (table, s) => Object.entries(table).reduce((acc, [f, t]) => acc.split(f).join(t), s);
const sanitized = replace(escapables, pattern);
const inner = replace(wildcardCharacters, sanitized);
return new RegExp('^' + inner + '$', 'i');
};
/**
*
* @param {string} pattern
* @param {string} s
* @returns {boolean}
*/
const match = (pattern, s) => {
if (wildcardKeys.some(c => pattern.includes(c))) {
return toRegExp(pattern).test(s);
}
return pattern === s;
};
/**
* @param {string} domain
* @param {URLSearchParams} params
* @param {string} pattern
* @returns {string}
*/
const deleteKeys = (domain, params, pattern) => {
if (!domain || loc.hostname.split('.').some((_, i, arr) => match(domain, arr.slice(i).join('.')))) {
for (const [key] of params) {
if (match(pattern, key)) {
params.delete(key);
locationChanged = true;
}
}
}
return params.toString().split('%25').join('%');
};
/**
* @param {string} search
* @param {() => boolean} condition
* @param {(arg: URLSearchParams) => any} callback
* @returns {void}
*/
const proc = (search, condition, callback) => {
const params = new URLSearchParams(search);
if (params.size === 0) return;
if (!condition()) return;
callback(params);
};
const url = new URL(loc);
url.search = url.search.split('%25').join('\0'); // avoid to escape of original '%25'
TRACKING_TAGS.forEach(tag => {
const [t, domain, pathname] = tag.split(/[@\/]/);
if (t.startsWith('#?')) {
proc(
url.hash.slice(1),
() => pathname == null || url.pathname === `/${pathname}`,
params => {
url.hash = deleteKeys(domain, params, t.slice(2));
},
);
}
else {
if (!url.search) return;
proc(
url.search.slice(1).replace(/%(?!25)/g, '%25'),
() => pathname == null || url.pathname === `/${pathname}`,
params => {
url.search = deleteKeys(domain, params, t);
},
);
}
});
url.search = url.search.split('%00').join('%25'); // restore original '%25'
return { url: url.href, locationChanged };
};
const { url, locationChanged } = removeTracking(location);
if (locationChanged) {
console.info(`TrackingParameterRemover: {${location.href}} => {${url}}`);
location.replace(url);
}
}());