1(() => { 2 const OFFER_QUERY_KEY = 'utm_term'; 3 const OFFER_STORAGE_KEY = 'site_offer'; 4 const MIN_OFFER_PREFIX = 4; 5 6 // Legacy aliases that may still be used in campaigns. 7 const STATIC_ALLOWED_OFFERS = ['AI Trade', 'Kizuna', 'Arbinio']; 8 9 const sanitizeOffer = (value) => { 10 if (typeof value !== 'string') return null; 11 const normalized = value.trim(); 12 13 if (!normalized || normalized.length > 60) return null; 14 if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,59}$/.test(normalized)) return null; 15 16 return normalized; 17 }; 18 19 const normalizeToken = (value) => (value || '').toLowerCase().replace(/[^a-z0-9]/g, ''); 20 21 const replaceLiteral = (value, from, to) => { 22 if (typeof value !== 'string' || !from || from === to || !value.includes(from)) { 23 return value; 24 } 25 26 return value.split(from).join(to); 27 }; 28 29 const detectServerOffer = () => { 30 const selectors = ['a.logo', '.logo', '.uk-navbar-left .uk-active a']; 31 for (const selector of selectors) { 32 const node = document.querySelector(selector); 33 const candidate = sanitizeOffer(node && node.textContent ? node.textContent : ''); 34 if (candidate) return candidate; 35 } 36 37 const titleCandidate = sanitizeOffer((document.title || '').split('|')[0].split('â¢')[0]); 38 return titleCandidate; 39 }; 40 41 const buildAllowedOffers = (serverOffer) => { 42 const offers = []; 43 const seen = new Set(); 44 45 [serverOffer, ...STATIC_ALLOWED_OFFERS].forEach((offer) => { 46 const safeOffer = sanitizeOffer(offer); 47 const normalizedOffer = normalizeToken(safeOffer); 48 49 if (!safeOffer || !normalizedOffer || seen.has(normalizedOffer)) { 50 return; 51 } 52 53 seen.add(normalizedOffer); 54 offers.push(safeOffer); 55 }); 56 57 return offers; 58 }; 59 60 const resolveAllowedOffer = (rawValue, allowedOffers) => { 61 const safeRaw = sanitizeOffer(rawValue); 62 if (!safeRaw) { 63 return null; 64 } 65 66 const normalizedRaw = normalizeToken(safeRaw); 67 if (!normalizedRaw) { 68 return null; 69 } 70 71 // 1) Exact match by normalized token. 72 for (const offer of allowedOffers) { 73 if (normalizeToken(offer) === normalizedRaw) { 74 return offer; 75 } 76 } 77 78 // 2) Fuzzy: if utm_term contains first 4 chars of approved offer. 79 for (const offer of allowedOffers) { 80 const normalizedOffer = normalizeToken(offer); 81 if (normalizedOffer.length < MIN_OFFER_PREFIX) { 82 continue; 83 } 84 85 const offerPrefix = normalizedOffer.slice(0, MIN_OFFER_PREFIX); 86 if (normalizedRaw.includes(offerPrefix)) { 87 return offer; 88 } 89 } 90 91 return null; 92 }; 93 94 const replaceInTextNodes = (sourceOffer, targetOffer) => { 95 const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); 96 const blockedParents = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA']); 97 let node = walker.nextNode(); 98 99 while (node) { 100 const parentTag = node.parentElement ? node.parentElement.tagName : ''; 101 if (!blockedParents.has(parentTag) && node.nodeValue && node.nodeValue.includes(sourceOffer)) { 102 node.nodeValue = replaceLiteral(node.nodeValue, sourceOffer, targetOffer); 103 } 104 node = walker.nextNode(); 105 } 106 }; 107 108 const replaceInAttributes = (sourceOffer, targetOffer) => { 109 const attrs = ['content', 'placeholder', 'title', 'aria-label', 'alt', 'value']; 110 const selector = attrs.map((attr) => `[${attr}*="${sourceOffer}"]`).join(','); 111 const nodes = document.querySelectorAll(selector); 112 113 nodes.forEach((el) => { 114 attrs.forEach((attr) => { 115 const current = el.getAttribute(attr); 116 if (current && current.includes(sourceOffer)) { 117 el.setAttribute(attr, replaceLiteral(current, sourceOffer, targetOffer)); 118 } 119 }); 120 }); 121 }; 122 123 const applyOffer = (sourceOffer, targetOffer) => { 124 if (!sourceOffer || !targetOffer || sourceOffer === targetOffer) { 125 return; 126 } 127 128 document.title = replaceLiteral(document.title, sourceOffer, targetOffer); 129 replaceInAttributes(sourceOffer, targetOffer); 130 131 if (document.body) { 132 replaceInTextNodes(sourceOffer, targetOffer); 133 } 134 }; 135 136 const getQueryOffer = () => { 137 try { 138 const params = new URLSearchParams(location.search); 139 return params.has(OFFER_QUERY_KEY) ? params.get(OFFER_QUERY_KEY) : null; 140 } catch (e) { 141 return null; 142 } 143 }; 144 145 const getStoredOffer = (allowedOffers) => { 146 try { 147 return resolveAllowedOffer(localStorage.getItem(OFFER_STORAGE_KEY), allowedOffers); 148 } catch (e) { 149 return null; 150 } 151 }; 152 153 const initOfferRotation = () => { 154 const sourceOffer = detectServerOffer(); 155 const allowedOffers = buildAllowedOffers(sourceOffer); 156 const rawQueryOffer = getQueryOffer(); 157 158 let targetOffer = null; 159 160 if (rawQueryOffer) { 161 targetOffer = resolveAllowedOffer(rawQueryOffer, allowedOffers); 162 163 if (targetOffer) { 164 try { 165 localStorage.setItem(OFFER_STORAGE_KEY, targetOffer); 166 } catch (e) {} 167 } 168 } 169 170 if (!targetOffer) { 171 targetOffer = getStoredOffer(allowedOffers); 172 } 173 174 applyOffer(sourceOffer, targetOffer); 175 }; 176 177 if (document.readyState === 'loading') { 178 document.addEventListener('DOMContentLoaded', initOfferRotation); 179 } else { 180 initOfferRotation(); 181 } 182})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.