using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Clipman { internal static class UrlTrackingCleaner { private static readonly Regex HtmlUrlAttributeRegex = new Regex( @"(?\b(?:href|src|action)\s*=\s*[""'])(?https?://[^""']+)(?[""'])", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex PlainUrlRegex = new Regex( @"https?://[^\s<>'""]+", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly HashSet TrackingParameters = new HashSet(StringComparer.OrdinalIgnoreCase) { "fbclid", "gclid", "dclid", "msclkid", "gbraid", "wbraid", "igshid", "mc_cid", "mc_eid", "mkt_tok", "vero_id", "_hsenc", "_hsmi", "yclid", "twclid", "li_fat_id", "sc_cid", "oly_anon_id", "oly_enc_id", "rb_clickid", "spm", "ref", "ref_src" }; public static string CleanText(string text) { return CleanText(text, false); } public static string CleanForSharing(string text) { return CleanText(text, true); } private static string CleanText(string text, bool cleanShareState) { if (string.IsNullOrEmpty(text)) return string.Empty; var cleaned = HtmlUrlAttributeRegex.Replace(text, match => { var url = match.Groups["url"].Value; var cleanedUrl = CleanUrl(url, true, cleanShareState); return match.Groups["prefix"].Value + cleanedUrl + match.Groups["suffix"].Value; }); cleaned = PlainUrlRegex.Replace(cleaned, match => { var value = match.Value; var trailing = string.Empty; while (value.Length > 0 && ".,);]!?".IndexOf(value[value.Length - 1]) >= 0) { trailing = value[value.Length - 1] + trailing; value = value.Substring(0, value.Length - 1); } return CleanUrl(value, false, cleanShareState) + trailing; }); return cleaned; } private static string CleanUrl(string url, bool htmlAttribute, bool cleanShareState) { if (string.IsNullOrWhiteSpace(url)) return url ?? string.Empty; var parseUrl = htmlAttribute ? DecodeHtmlAmpersands(url) : url; Uri uri; if (!Uri.TryCreate(parseUrl, UriKind.Absolute, out uri)) { return url; } if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { return url; } var query = uri.Query; if (string.IsNullOrEmpty(query) || query == "?") return url; var parts = query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries); var kept = new List(); var changed = false; foreach (var part in parts) { var equals = part.IndexOf('='); var name = equals >= 0 ? part.Substring(0, equals) : part; if (ShouldRemoveParameter(name) || (cleanShareState && ShouldRemoveShareParameter(uri, name))) { changed = true; continue; } kept.Add(part); } if (!changed) return url; var builder = new UriBuilder(uri) { Query = kept.Count == 0 ? string.Empty : string.Join("&", kept.ToArray()) }; var result = builder.Uri.ToString(); return htmlAttribute ? EncodeHtmlAmpersands(result) : result; } private static bool ShouldRemoveParameter(string name) { if (string.IsNullOrWhiteSpace(name)) return false; var decoded = Uri.UnescapeDataString(name).Trim(); if (decoded.StartsWith("utm_", StringComparison.OrdinalIgnoreCase)) return true; if (decoded.StartsWith("hsa_", StringComparison.OrdinalIgnoreCase)) return true; return TrackingParameters.Contains(decoded); } private static bool ShouldRemoveShareParameter(Uri uri, string name) { if (uri == null || string.IsNullOrWhiteSpace(name)) return false; var host = uri.Host ?? string.Empty; var decoded = Uri.UnescapeDataString(name).Trim(); if (IsYouTubeHost(host)) { return decoded.Equals("t", StringComparison.OrdinalIgnoreCase) || decoded.Equals("time_continue", StringComparison.OrdinalIgnoreCase) || decoded.Equals("start", StringComparison.OrdinalIgnoreCase) || decoded.Equals("pp", StringComparison.OrdinalIgnoreCase) || decoded.Equals("si", StringComparison.OrdinalIgnoreCase) || decoded.Equals("feature", StringComparison.OrdinalIgnoreCase); } return decoded.Equals("si", StringComparison.OrdinalIgnoreCase); } private static bool IsYouTubeHost(string host) { if (string.IsNullOrWhiteSpace(host)) return false; return host.Equals("youtu.be", StringComparison.OrdinalIgnoreCase) || host.Equals("youtube.com", StringComparison.OrdinalIgnoreCase) || host.EndsWith(".youtube.com", StringComparison.OrdinalIgnoreCase); } private static string DecodeHtmlAmpersands(string value) { return value.Replace("&", "&").Replace("&", "&").Replace("&", "&").Replace("&", "&"); } private static string EncodeHtmlAmpersands(string value) { return value.Replace("&", "&"); } } }