PageSourceSearch

https://www.bonusfinder.com/wp-content/themes/bonusfinder/js/algolia-search.js?ver=53196.537777778

js bonusfinder.com collected 2026-09-25 18:46:19 UTC 19,758 bytes, 644 lines download raw bytes

1(function () {
2	'use strict';
3
4	let searchInitialized = false;
5	let index = null;
6	let indexName = '';
7	let searchTimeout = null;
8	let keyRefreshInFlight = false;
9
10	const HIGHLIGHT_PRE = '__bf-hl__';
11	const HIGHLIGHT_POST = '__/bf-hl__';
12
13	const dataEl = document.getElementById( 'algolia-search-data' );
14	const suggestionTexts = dataEl ? JSON.parse( dataEl.getAttribute( 'data-suggestions' ) ) : [];
15	const noResultsText = dataEl ? dataEl.getAttribute( 'data-no-results' ) : '';
16	const aiEnabled = dataEl ? dataEl.getAttribute( 'data-ai-enabled' ) === '1' : false;
17	const aiAssistantId = dataEl ? dataEl.getAttribute( 'data-ai-assistant-id' ) : '';
18	const aiAgentStudio = dataEl ? dataEl.getAttribute( 'data-ai-agent-studio' ) === '1' : true;
19
20	// Algolia SiteSearch Ask AI CDN bundle. Lazy-loaded only when the user clicks
21	// the AI mode button to keep the header free of unused JS for visitors who
22	// never engage with AI search.
23	// Version is pinned (not @latest) to avoid supply-chain risk from a
24	// compromised npm publish landing instantly on all visitors. Bump
25	// intentionally after reviewing the changelog.
26	const ASKAI_VERSION = '1.0.14';
27	const ASKAI_CSS = 'https://unpkg.com/@algolia/sitesearch@' + ASKAI_VERSION + '/dist/search-askai.min.css';
28	const ASKAI_JS  = 'https://unpkg.com/@algolia/sitesearch@' + ASKAI_VERSION + '/dist/search-askai.min.js';
29	let askAiLoader = null;
30	let askAiContainerId = null;
31	// Guard: track whether init() has run so we can destroy-then-reinit on
32	// subsequent clicks rather than stacking duplicate React trees. We always
33	// reinit (rather than skip init) because the Algolia bundle accumulates
34	// conversation history in React state; a fresh init resets that state,
35	// preventing the "data-suggestions" schema error on the second search.
36	let askAiInitialized = false;
37
38	const SEARCH_ICON_SVG =
39		'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">' +
40		'<path fill-rule="evenodd" clip-rule="evenodd" d="M6.33 0.583a5.747 5.747 0 1 0 0 11.494 ' +
41		'5.747 5.747 0 0 0 0-11.494ZM1.75 6.33a4.58 4.58 0 1 1 9.16 0 4.58 4.58 0 0 1-9.16 0Z" fill="currentColor"/>' +
42		'<path fill-rule="evenodd" clip-rule="evenodd" d="M10.39 10.39a.583.583 0 0 1 .825 0l2.202 ' +
43		'2.202a.583.583 0 1 1-.825.825l-2.202-2.202a.583.583 0 0 1 0-.825Z" fill="currentColor"/></svg>';
44
45	/**
46	 * Maximum allowed search query length. Legitimate casino/brand names
47	 * rarely exceed 60 characters; injection payloads are typically much
48	 * longer. This caps queries before they reach Algolia.
49	 */
50	const MAX_QUERY_LENGTH = 100;
51
52	/**
53	 * Patterns that indicate bot/injection attacks rather than genuine
54	 * search queries. Each regex is case-insensitive. When a query matches
55	 * any pattern it is silently dropped — no Algolia API call is made.
56	 *
57	 * Covers the attack signatures catalogued in BCR-401 / BCR-434:
58	 *   - SQL injection (MySQL comment bypass, UNION SELECT, CAST, etc.)
59	 *   - RCE probes (ThinkPHP invokefunction)
60	 *   - CGI scanner fingerprints (search.cgi, show.pl, etc.)
61	 *   - XSS probes (<script>, onerror=, javascript:, etc.)
62	 *   - PHP shell / path traversal patterns
63	 */
64	const MALICIOUS_PATTERNS = [
65		// SQL injection.
66		/\/\*[!*]/,
67		/union\s+select/i,
68		/\bcast\s*\(/i,
69		/floor\s*\(\s*rand/i,
70		/\bselect\s+.*\bfrom\b/i,
71		/\binsert\s+into\b/i,
72		/\bdrop\s+table\b/i,
73		/\bor\s+1\s*=\s*1/i,
74		/\band\s+1\s*=\s*1/i,
75		/\bwaitfor\s+delay\b/i,
76		/\bbenchmark\s*\(/i,
77		/\bsleep\s*\(/i,
78		/0x[0-9a-f]{8,}/i,
79
80		// RCE probes (ThinkPHP and similar).
81		/invokefunction/i,
82		/\\think\\/i,
83		/thinkphp/i,
84
85		// CGI scanner fingerprints.
86		/search\.cgi/i,
87		/show\.pl\b/i,
88		/results\.stm/i,
89		/webglimpse\.cgi/i,
90		/\.asp\b/i,
91		/\.aspx\b/i,
92		/\.jsp\b/i,
93
94		// XSS probes.
95		/<\s*script/i,
96		/onerror\s*=/i,
97		/onload\s*=/i,
98		/javascript\s*:/i,
99		/'\s*><\s*/,
100		/\balert\s*\(/i,
101		/\bprompt\s*\(/i,
102		/\bconfirm\s*\(/i,
103		/\bdocument\s*\.\s*cookie/i,
104
105		// PHP shell / path traversal / external domain noise.
106		/php-shell/i,
107		/\/etc\/passwd/i,
108		/\.\.\//,
109		/\beval\s*\(/i,
110		/\bexec\s*\(/i,
111		/\bsystem\s*\(/i,
112		/\bpassthru\s*\(/i,
113		/\.php\b/i,
114	];
115
116	/**
117	 * Returns true when the query looks like a bot/injection probe.
118	 *
119	 * @param {string} query Trimmed search input.
120	 * @return {boolean}
121	 */
122	function isMaliciousQuery(query) {
123		if (query.length > MAX_QUERY_LENGTH) {
124			return true;
125		}
126		for (let i = 0; i < MALICIOUS_PATTERNS.length; i++) {
127			if (MALICIOUS_PATTERNS[i].test(query)) {
128				return true;
129			}
130		}
131		return false;
132	}
133
134	function escapeHtml(str) {
135		const el = document.createElement('span');
136		el.appendChild(document.createTextNode(str));
137		return el.innerHTML;
138	}
139
140	function initSearch() {
141		if (searchInitialized) {
142			return;
143		}
144		if (
145			typeof algoliasearch === 'undefined' ||
146			typeof algolia === 'undefined'
147		) {
148			return;
149		}
150
151		const client = algoliasearch(
152			algolia.application_id,
153			algolia.search_api_key
154		);
155		indexName = '';
156
157		if (
158			algolia.autocomplete &&
159			algolia.autocomplete.sources &&
160			algolia.autocomplete.sources.length > 0
161		) {
162			indexName = algolia.autocomplete.sources[0].index_name;
163		} else if (algolia.indices && algolia.indices.searchable_posts) {
164			indexName = algolia.indices.searchable_posts.name;
165		}
166
167		if (!indexName) {
168			return;
169		}
170
171		index = client.initIndex(indexName);
172		searchInitialized = true;
173
174		setupSearchBehavior();
175	}
176
177	function renderSuggestionsEarly() {
178		const suggestionsList = document.getElementById('search-suggestions');
179		const searchInput = document.getElementById('search-input');
180		if (suggestionsList && searchInput) {
181			renderSuggestions(suggestionsList, searchInput);
182		}
183	}
184
185	function loadAskAiBundle() {
186		if ( askAiLoader ) {
187			return askAiLoader;
188		}
189		askAiLoader = new Promise( function ( resolve, reject ) {
190			if ( typeof window.algolia === 'undefined' ) {
191				reject( new Error( 'Algolia config (window.algolia) is missing.' ) );
192				return;
193			}
194			if ( ! document.querySelector( 'link[data-bf-askai="1"]' ) ) {
195				const link = document.createElement( 'link' );
196				link.rel  = 'stylesheet';
197				link.href = ASKAI_CSS;
198				link.setAttribute( 'data-bf-askai', '1' );
199				document.head.appendChild( link );
200			}
201			if ( typeof window.SiteSearchAskAI !== 'undefined' ) {
202				resolve();
203				return;
204			}
205			const script = document.createElement( 'script' );
206			script.src   = ASKAI_JS;
207			script.async = true;
208			script.setAttribute( 'data-bf-askai', '1' );
209			script.onload  = function () { resolve(); };
210			script.onerror = function () { reject( new Error( 'Failed to load Algolia Ask AI bundle.' ) ); };
211			document.head.appendChild( script );
212		} );
213		return askAiLoader;
214	}
215
216	function ensureAskAiContainer() {
217		if ( askAiContainerId && document.getElementById( askAiContainerId ) ) {
218			return askAiContainerId;
219		}
220		askAiContainerId = 'bf-askai-container';
221		if ( ! document.getElementById( askAiContainerId ) ) {
222			const div = document.createElement( 'div' );
223			div.id = askAiContainerId;
224			// The bundle renders an "Open search" trigger button inside the
225			// container that we click programmatically — keep it visually
226			// hidden so visitors only see our own header button.
227			div.style.cssText = 'position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;';
228			document.body.appendChild( div );
229		}
230		return askAiContainerId;
231	}
232
233	// Poll the DOM for an element, resolving with it or null after the timeout.
234	function waitForElement( selector, timeoutMs ) {
235		return new Promise( function ( resolve ) {
236			const deadline = Date.now() + ( timeoutMs || 2000 );
237			const tick = function () {
238				const el = document.querySelector( selector );
239				if ( el ) { resolve( el ); return; }
240				if ( Date.now() > deadline ) { resolve( null ); return; }
241				requestAnimationFrame( tick );
242			};
243			tick();
244		} );
245	}
246
247	// Drive the SiteSearchAskAI modal into chat mode. The bundle gates chat on
248	// a non-empty trimmed query (see setShowChat call site), so the path that
249	// actually works is: focus the input, set a value, dispatch Enter.
250	function enterChatMode( initialQuery ) {
251		const selector = '.ss-searchbox-form input, .ss-searchbox-form [contenteditable="true"]';
252		waitForElement( selector, 2500 ).then( function ( firstInput ) {
253			if ( ! firstInput ) { return; }
254			firstInput.focus();
255			const query = ( initialQuery || '' ).trim();
256			if ( ! query ) {
257				// Without a query we cannot programmatically enter chat mode —
258				// the bundle refuses. Leave the input focused so the user can
259				// type and the very next Enter puts them straight into chat.
260				return;
261			}
262
263			const proto  = window.HTMLInputElement && window.HTMLInputElement.prototype;
264			const setter = proto && Object.getOwnPropertyDescriptor( proto, 'value' );
265
266			// Set the value via the native setter so React's controlled input
267			// picks up the change (a plain assignment leaves React state empty).
268			function injectValue( el ) {
269				try {
270					if ( setter && setter.set && 'INPUT' === el.tagName ) {
271						setter.set.call( el, query );
272					} else {
273						el.value = query;
274					}
275					el.dispatchEvent( new Event( 'input', { bubbles: true } ) );
276				} catch ( e ) {
277					// Any Algolia SDK change to the input/React handler could
278					// break this — degrade to a focused, user-typable modal.
279					console.warn( '[bf] Ask AI value injection failed:', e );
280				}
281			}
282
283			// The bundle's keydown handler reads the current trimmed value.
284			function submitEnter( el ) {
285				try {
286					el.dispatchEvent( new KeyboardEvent( 'keydown', {
287						key: 'Enter',
288						code: 'Enter',
289						keyCode: 13,
290						which: 13,
291						bubbles: true
292					} ) );
293				} catch ( e ) {
294					// Synthetic Enter failed — the input is pre-filled; the
295					// user can press Enter manually to submit.
296					console.warn( '[bf] Ask AI chat-mode Enter dispatch failed:', e );
297				}
298			}
299
300			// On the FIRST open the searchbox is freshly mounted and React's
301			// change handler may not be wired yet, so a single injection is
302			// silently reset to empty — the query is lost and chat opens blank
303			// (works on the 2nd, warm open). Retry until the value sticks, then
304			// submit. Re-query each attempt in case the node was re-rendered.
305			let attempts = 0;
306			const maxAttempts = 15;
307			function attempt() {
308				const el = document.querySelector( selector ) || firstInput;
309				if ( 'INPUT' !== el.tagName ) {
310					// contenteditable / unknown — best-effort single shot.
311					injectValue( el );
312					submitEnter( el );
313					return;
314				}
315				el.focus();
316				injectValue( el );
317				setTimeout( function () {
318					const current = document.querySelector( selector ) || el;
319					if ( current.value !== query && attempts < maxAttempts ) {
320						attempts++;
321						attempt();
322						return;
323					}
324					submitEnter( current );
325				}, 60 );
326			}
327			attempt();
328		} );
329	}
330
331	function openAskAi( opts ) {
332		opts = opts || {};
333		const assistantId = opts.assistantId || aiAssistantId;
334		const agentStudio = typeof opts.agentStudio === 'boolean' ? opts.agentStudio : aiAgentStudio;
335		if ( ! assistantId ) {
336			console.warn( '[bf] No Algolia Ask AI Assistant ID configured.' );
337			return;
338		}
339		if ( typeof window.algolia === 'undefined' ) {
340			console.warn( '[bf] window.algolia is not available; cannot open Ask AI.' );
341			return;
342		}
343
344		let indexName = '';
345		if ( window.algolia.autocomplete && window.algolia.autocomplete.sources && window.algolia.autocomplete.sources.length > 0 ) {
346			indexName = window.algolia.autocomplete.sources[ 0 ].index_name;
347		} else if ( window.algolia.indices && window.algolia.indices.searchable_posts ) {
348			indexName = window.algolia.indices.searchable_posts.name;
349		}
350
351		loadAskAiBundle().then( function () {
352			if ( typeof window.SiteSearchAskAI === 'undefined' ) {
353				return;
354			}
355			const containerId = ensureAskAiContainer();
356
357			// Destroy the previous React tree before reinitialising. This is
358			// intentional: the Algolia bundle accumulates conversation history
359			// in React state and re-sends it on subsequent turns. The server's
360			// schema validator rejects the "data-suggestions" part type,
361			// causing a 422 on the second search (see bundle analysis). A
362			// fresh init resets the state tree cleanly. The bundle's exported
363			// destroy() takes the container selector or element.
364			if ( askAiInitialized && typeof window.SiteSearchAskAI.destroy === 'function' ) {
365				try { window.SiteSearchAskAI.destroy( '#' + containerId ); } catch ( e ) { /* ignore */ }
366			}
367			// Reset the loader promise so the next click re-mounts correctly
368			// (the bundle is already in memory; we only reset the Preact tree).
369			askAiInitialized = false;
370
371			window.SiteSearchAskAI.init( {
372				container: '#' + containerId,
373				applicationId: window.algolia.application_id,
374				apiKey: window.algolia.search_api_key,
375				indexName: indexName,
376				assistantId: assistantId,
377				agentStudio: agentStudio,
378				attributes: {
379					primaryText: 'post_title',
380					tertiaryText: 'post_author.display_name',
381					url: 'permalink',
382					image: 'images.thumbnail.url'
383				},
384				buttonText: 'AI mode'
385			} );
386			askAiInitialized = true;
387
388			// Click the bundle's own trigger button to open the modal. We wait
389			// for it to mount because init() renders asynchronously.
390			waitForElement( '#' + containerId + ' [aria-label="Open search"]', 2000 ).then( function ( trigger ) {
391				if ( trigger ) { trigger.click(); }
392				enterChatMode( opts.initialQuery );
393			} );
394		} ).catch( function ( err ) {
395			console.error( '[bf] Failed to open Algolia Ask AI:', err );
396		} );
397	}
398
399	// Expose the opener so it can be invoked from other entry points (e.g. the
400	// InstantSearch results page) without duplicating the loader logic.
401	window.bfOpenAlgoliaAskAi = openAskAi;
402
403	function setupAiToggle() {
404		if ( ! aiEnabled ) {
405			return;
406		}
407		const aiToggle = document.getElementById( 'search-ai-mode-toggle' );
408		if ( ! aiToggle || aiToggle.dataset.bfBound === '1' ) {
409			return;
410		}
411		aiToggle.dataset.bfBound = '1';
412		aiToggle.addEventListener( 'click', function ( e ) {
413			e.preventDefault();
414			const searchInput = document.getElementById( 'search-input' );
415			openAskAi( { initialQuery: searchInput ? searchInput.value : '' } );
416		} );
417	}
418
419	function setupSearchBehavior() {
420		const searchInput = document.getElementById('search-input');
421		const emptyState = document.getElementById('search-empty-state');
422		const resultsState = document.getElementById('search-results-state');
423		const resultsList = document.getElementById('search-results-list');
424		const resultsCount = document.getElementById('search-results-count');
425		const searchForm = document.querySelector('.menu-search-form');
426
427		if (!searchInput || !searchForm) {
428			return;
429		}
430
431		searchInput.addEventListener('input', function () {
432			const query = this.value.trim();
433
434			clearTimeout(searchTimeout);
435
436			if (query.length === 0) {
437				showEmptyState(emptyState, resultsState);
438				searchForm.classList.add('dropdown-open');
439				return;
440			}
441
442			if (query.length < 2) {
443				return;
444			}
445
446			searchTimeout = setTimeout(function () {
447				performSearch(
448					query,
449					resultsList,
450					resultsCount,
451					emptyState,
452					resultsState,
453					searchForm
454				);
455			}, 200);
456		});
457	}
458
459	function performSearch(
460		query,
461		resultsList,
462		resultsCount,
463		emptyState,
464		resultsState,
465		searchForm
466	) {
467		if (!index) {
468			return;
469		}
470
471		if (isMaliciousQuery(query)) {
472			return;
473		}
474
475		index
476			.search(query, {
477				hitsPerPage: 20,
478				attributesToSnippet: ['content:10'],
479				highlightPreTag: HIGHLIGHT_PRE,
480				highlightPostTag: HIGHLIGHT_POST,
481			})
482			.then(function (response) {
483				emptyState.style.display = 'none';
484				resultsState.style.display = 'block';
485				const separator = document.querySelector(
486					'.search-dropdown-separator'
487				);
488				if (separator) {
489					separator.style.display = 'none';
490				}
491
492				resultsCount.textContent = response.nbHits;
493				resultsList.innerHTML = '';
494
495				if (response.hits.length === 0) {
496					const emptyLi = document.createElement('li');
497					emptyLi.className = 'search-no-results';
498					emptyLi.textContent =
499						noResultsText &&
500						noResultsText !== 'HEA_SEARCH_NO_RESULTS'
501							? noResultsText
502							: 'No results found';
503					resultsList.appendChild(emptyLi);
504					searchForm.classList.add('dropdown-open');
505					return;
506				}
507
508				response.hits.forEach(function (hit) {
509					let title = '';
510					if (
511						hit._highlightResult &&
512						hit._highlightResult.post_title
513					) {
514						title = escapeHtml(
515							hit._highlightResult.post_title.value
516						);
517						title = title
518							.replace(/__bf-hl__/g, '<strong>')
519							.replace(/__\/bf-hl__/g, '</strong>');
520					} else {
521						title = escapeHtml(hit.post_title || hit.name || '');
522					}
523
524					const li = document.createElement('li');
525					const a = document.createElement('a');
526					a.href = hit.permalink || hit.posts_url || '#';
527					a.innerHTML = title;
528					li.appendChild(a);
529					resultsList.appendChild(li);
530				});
531
532				searchForm.classList.add('dropdown-open');
533			})
534			.catch(function (error) {
535				// Algolia returns a specific message when the secured key
536				// has expired (page served from cache after TTL elapsed).
537				// Fetch a fresh key from the server and retry silently.
538				if (
539					error &&
540					error.message &&
541					error.message.indexOf('validUntil') !== -1 &&
542					!keyRefreshInFlight &&
543					algolia.refresh_key_url
544				) {
545					keyRefreshInFlight = true;
546					fetch(algolia.refresh_key_url)
547						.then(function (resp) {
548							return resp.json();
549						})
550						.then(function (json) {
551							if (
552								json.success &&
553								json.data &&
554								json.data.search_api_key
555							) {
556								algolia.search_api_key =
557									json.data.search_api_key;
558								var newClient = algoliasearch(
559									algolia.application_id,
560									algolia.search_api_key
561								);
562								index = newClient.initIndex(
563									indexName ||
564										(algolia.autocomplete &&
565											algolia.autocomplete.sources &&
566											algolia.autocomplete.sources[0] &&
567											algolia.autocomplete.sources[0]
568												.index_name) ||
569										''
570								);
571								// Retry the original search with the fresh key.
572								performSearch(
573									query,
574									resultsList,
575									resultsCount,
576									emptyState,
577									resultsState,
578									searchForm
579								);
580							}
581						})
582						.catch(function () {
583							// Refresh failed — nothing more we can do.
584						})
585						.finally(function () {
586							keyRefreshInFlight = false;
587						});
588					return;
589				}
590				console.error('Algolia search error:', error);
591			});
592	}
593
594	function showEmptyState(emptyState, resultsState) {
595		const separator = document.querySelector('.search-dropdown-separator');
596		if (emptyState) {
597			emptyState.style.display = '';
598		}
599		if (resultsState) {
600			resultsState.style.display = 'none';
601		}
602		if (separator) {
603			separator.style.display = '';
604		}
605	}
606
607	function renderSuggestions(container, searchInput) {
608		if (!container) {
609			return;
610		}
611		container.innerHTML = '';
612
613		suggestionTexts.forEach(function (text) {
614			if (!text) {
615				return;
616			}
617			const li = document.createElement('li');
618			li.className = 'search-suggestion-item';
619			li.innerHTML =
620				SEARCH_ICON_SVG + '<span>' + escapeHtml(text) + '</span>';
621
622			li.addEventListener('mousedown', function (e) {
623				e.preventDefault();
624				searchInput.value = text;
625				searchInput.dispatchEvent(new Event('input'));
626			});
627
628			container.appendChild(li);
629		});
630	}
631
632	renderSuggestionsEarly();
633	setupAiToggle();
634
635	window.addEventListener('load', function () {
636		initSearch();
637		setupAiToggle();
638	} );
639
640	if (document.readyState === 'complete') {
641		initSearch();
642		setupAiToggle();
643	}
644})();

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.