-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathremove-url-tracker.user.js
More file actions
301 lines (275 loc) · 11.9 KB
/
Copy pathremove-url-tracker.user.js
File metadata and controls
301 lines (275 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// ==UserScript==
// @name 移除 URL 追蹤
// @namespace https://chris.taipei
// @version 0.4.4
// @description 自動移除 URL 中的追蹤參數,保護您的隱私(部分規則引用自 ClearURLs Project)
// @author chris1004tw
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect rules1.clearurls.xyz
// @run-at document-start
// @updateURL https://github.com/chris1004tw/userscripts/raw/main/remove-url-tracker.user.js
// @downloadURL https://github.com/chris1004tw/userscripts/raw/main/remove-url-tracker.user.js
// ==/UserScript==
// Co-authored with Claude Opus 4.6 Thinking
// Co-authored with ChatGPT 5.6 Sol Ultra
// 維護索引:README.md「維護索引」
(function () {
'use strict';
/**
* @typedef {Object} SiteRule
* @property {RegExp} hostnamePattern 精確比對解析後 hostname 的規則。
* @property {Set<string>} params 僅限該站點移除的查詢參數。
* @property {RegExp[]} [regexParams] 僅限該站點套用的參數名稱規則。
* @property {RegExp[]} [except] 不應套用規則的完整 URL 例外。
*/
/**
* @typedef {Object} RemoteRule
* @property {RegExp} pattern ClearURLs provider 的完整 URL 規則。
* @property {RegExp[]} exceptions provider 的完整 URL 例外。
* @property {Set<string>} stringRules 完全相符的參數名稱。
* @property {RegExp[]} regexRules 以正規表示式比對的參數名稱。
*/
const RULE_URL = 'https://rules1.clearurls.xyz/data.minify.json';
const ONE_HOUR = 3600000;
const SKIP_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '']);
const SKIP_PROTOCOLS = new Set(['file:', 'data:', 'javascript:']);
// 特定網域規則:所有 hostnamePattern 都錨定結尾,避免 lookalike hostname 誤套規則。
/** @type {SiteRule[]} */
const SITE_RULES = [
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*google\.(?:(?:com|co)\.)?[a-z]{2}$|^(?:[a-z0-9-]+\.)*google\.com$/i,
params: new Set(["source", "ved", "ei", "gs_l", "gs_lcp", "sclient", "sxsrf", "rlz", "ICID"]),
except: [/^https?:\/\/mail\.google\.com\//i]
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*facebook\.com$/i,
params: new Set([
"action_history", "action_type_map", "action_ref_map", "share_url", "type",
"ref", "ref_url", "referrer", "origin_source", "rdid", "idorvanity",
"hoisted_section_header_type", "referral_story_type"
]),
regexParams: [/^fb_/]
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*(?:twitter\.com|x\.com)$/i,
params: new Set(["s", "t", "ref_src"])
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*youtube\.com$/i,
params: new Set(["feature", "kw", "pp", "si"]),
except: [/^https?:\/\/(?:[a-z0-9-]+\.)*?youtube\.com\/redirect/i]
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*amazon\.(?:(?:com|co)\.)?[a-z]{2}$|^(?:[a-z0-9-]+\.)*amazon\.com$/i,
params: new Set(["tag", "ascsubtag", "ref_", "psc", "linkCode", "linkId", "camp", "creative"]),
regexParams: [/^pf_rd_/, /^pd_rd_/]
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*(?:taobao\.com|tmall\.com|tmall\.hk)$/i,
params: new Set([
"ns", "source", "sku_properties", "priceTId", "abbucket", "xxc", "mi_id",
"initiative_id", "clientPreloadId", "preLoadOrigin", "pisk", "rn", "sourceId",
"ssid", "suggest_query", "wq", "spm", "scm", "xmt", "slof"
]),
regexParams: [/^ali_/, /^mm_/]
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*shopee\.(?:(?:com|co)\.)?[a-z]{2}$|^(?:[a-z0-9-]+\.)*shopee\.(?:sg|ph|vn|tw|cl|pl)$/i,
params: new Set(["seoName", "d_id", "uls_trackid"])
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*trip\.com$/i,
params: new Set(["masterhotelid_tracelogid", "trip_sub1", "hasAidInUrl"])
},
{
hostnamePattern: /^(?:[a-z0-9-]+\.)*linkedin\.com$/i,
params: new Set(["trk", "trkCampaign"])
}
];
// 通用規則只保留跨站唯一性高的追蹤名稱;短名稱與平台名稱一律限制在 SITE_RULES。
const GENERAL_STRING_PARAMS = new Set([
// 跨站 Click ID
"yclid", "fbclid", "gclid", "dclid", "msclkid", "twclid", "igshid", "mibextid", "srsltid",
// Email 行銷追蹤
"mc_cid", "mc_eid", "mkt_tok", "nr_email_referer", "vero_conv", "vero_id",
// 名稱具跨站唯一性的分析識別碼
"oly_anon_id", "oly_enc_id", "wickedid"
]);
const GENERAL_REGEX_PARAMS = [
/^utm_/, /^ga_/
];
/** @type {RemoteRule[]} */
let remoteRules = [];
/**
* 判斷解析後 URL 是否符合一條本地站點規則。
*
* @param {URL} urlObj 已完成解析的網址物件。
* @param {SiteRule} rule 待驗證的站點規則。
* @returns {boolean} hostname 符合且未命中例外時為 true。
*/
function matchesSiteRule(urlObj, rule) {
if (!rule.hostnamePattern.test(urlObj.hostname)) return false;
return !rule.except?.some(ex => ex.test(urlObj.href));
}
/**
* 清除網址中的本地與遠端追蹤參數。
*
* @param {string} url 要清理的絕對網址。
* @returns {string | null} 有變更時回傳清理後網址,無變更或解析失敗時回傳 null。
*/
function cleanURL(url) {
try {
const urlObj = new URL(url);
if (SKIP_HOSTS.has(urlObj.hostname) || SKIP_PROTOCOLS.has(urlObj.protocol)) {
return null;
}
const params = urlObj.searchParams;
const keys = [...params.keys()];
if (keys.length === 0) return null;
// 預先收集匹配此 URL 的特定網域參數
const matchedSiteParams = new Set();
const matchedSiteRegexes = [];
for (const rule of SITE_RULES) {
if (!matchesSiteRule(urlObj, rule)) continue;
for (const p of rule.params) matchedSiteParams.add(p);
matchedSiteRegexes.push(...(rule.regexParams || []));
}
// 預先收集匹配此 URL 的遠端規則
const matchedRemoteStrings = new Set();
const matchedRemoteRegexes = [];
for (const provider of remoteRules) {
if (!provider.pattern.test(urlObj.href)) continue;
if (provider.exceptions?.some(ex => ex.test(urlObj.href))) continue;
for (const s of provider.stringRules) matchedRemoteStrings.add(s);
matchedRemoteRegexes.push(...provider.regexRules);
}
let changed = false;
for (const key of keys) {
if (
GENERAL_STRING_PARAMS.has(key) ||
matchedSiteParams.has(key) ||
matchedSiteRegexes.some(r => r.test(key)) ||
matchedRemoteStrings.has(key) ||
GENERAL_REGEX_PARAMS.some(r => r.test(key)) ||
matchedRemoteRegexes.some(r => r.test(key))
) {
params.delete(key);
changed = true;
}
}
// 直接使用 URL.href,避免手動重組時遺失 username/password 等合法 URL 元件。
return changed ? urlObj.href : null;
} catch {
return null;
}
}
/**
* 將 ClearURLs JSON provider 預編譯為可重複使用的正規表示式與 Set。
*
* @param {Record<string, unknown>} data ClearURLs provider 集合或含 providers 的回應物件。
* @returns {RemoteRule[]} 可安全套用的有效遠端規則;無效 provider 會被略過。
*/
function compileRemoteRules(data) {
const providers = data.providers || data;
return Object.values(providers).filter(p => p.urlPattern).map(provider => {
try {
return {
pattern: new RegExp(provider.urlPattern, 'i'),
exceptions: provider.exceptions?.map(ex => new RegExp(ex, 'i')) || [],
stringRules: new Set(provider.rules?.filter(r => !/[\\^$.*+?()[\]{}|]/.test(r)) || []),
regexRules: provider.rules?.filter(r => /[\\^$.*+?()[\]{}|]/.test(r)).map(r => new RegExp('^' + r + '$', 'i')) || []
};
} catch {
return null;
}
}).filter(Boolean);
}
/**
* 背景下載並快取 ClearURLs 規則,成功後重新清理目前頁面。
*
* @returns {void}
* @sideeffect 發出 GM_xmlhttpRequest、寫入 GM 儲存空間,並可能改寫目前網址。
*/
function loadRemoteRules() {
GM_xmlhttpRequest({
method: 'GET',
url: RULE_URL,
timeout: 10000,
onload: (res) => {
if (res.status === 200) {
try {
const data = JSON.parse(res.responseText);
remoteRules = compileRemoteRules(data);
GM_setValue('cachedRules', res.responseText);
GM_setValue('cacheTime', Date.now());
// 新規則載入後重新清理當前 URL
cleanCurrentURL();
} catch { }
}
},
onerror: () => { },
ontimeout: () => { }
});
}
// 載入快取的遠端規則
const cached = GM_getValue('cachedRules', null);
const cacheTime = GM_getValue('cacheTime', 0);
if (cached) {
try {
remoteRules = compileRemoteRules(JSON.parse(cached));
} catch { }
}
// 超過一小時則背景更新
if (!cached || Date.now() - cacheTime >= ONE_HOUR) {
loadRemoteRules();
}
// 攔截 pushState/replaceState 支援 SPA
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
let cleaning = false;
/**
* 清理目前頁面網址,並隔離 userscript 自己 replaceState 失敗造成的例外。
*
* @returns {void}
* @sideeffect 需要清理時透過原生 replaceState 更新網址;finally 保證解除重入鎖。
*/
function cleanCurrentURL() {
if (cleaning) return;
const cleaned = cleanURL(location.href);
if (cleaned) {
cleaning = true;
try {
originalReplaceState.call(history, history.state, '', cleaned);
} catch { }
finally {
cleaning = false;
}
}
}
/** 攔截頁面的 pushState,保留原生例外與回傳值,成功後再執行追蹤清理。 */
history.pushState = function (state, title, url) {
const result = originalPushState.call(this, state, title, url);
cleanCurrentURL();
return result;
};
/** 攔截頁面的 replaceState,保留原生例外與回傳值,成功後再執行追蹤清理。 */
history.replaceState = function (state, title, url) {
const result = originalReplaceState.call(this, state, title, url);
cleanCurrentURL();
return result;
};
// popstate 事件(上一頁/下一頁)
window.addEventListener('popstate', cleanCurrentURL);
// bfcache 恢復時重新清理 URL
window.addEventListener('pageshow', (e) => {
if (e.persisted) cleanCurrentURL();
});
// 清理當前頁面(多時機觸發,防止 MV3 延遲注入)
cleanCurrentURL();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', cleanCurrentURL, { once: true });
}
})();