HEX
Server: Apache/2.4.65 (Debian)
System: Linux wordpress-7cb4c6b6f6-qgbk2 5.15.0-131-generic #141-Ubuntu SMP Fri Jan 10 21:18:28 UTC 2025 x86_64
User: www-data (33)
PHP: 8.3.27
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/woocommerce/assets/js/frontend/a8c-address-autocomplete-service.js
( function () {
	const permanentlyDisabledServices = [];
	const baseUrl =
		'https://public-api.wordpress.com/wpcom/v2/woo/address-autocomplete';
	const searchUrl = `${ baseUrl }/search`;
	const selectUrl = `${ baseUrl }/select`;

	const MAX_SERVICE_ERROR_RETRIES = 3;

	/**
	 * Generate a unique session ID using crypto.randomUUID if available, otherwise fallback to Math.random
	 * @returns {string} A unique session ID
	 */
	function generateSessionId() {
		return crypto && crypto.randomUUID
			? crypto.randomUUID()
			: Math.random().toString( 36 ).substring( 2 );
	}

	/**
	 * Debounce function from lodash, modified to return a promise.
	 */
	function debounce( func, wait, options ) {
		var lastArgs,
			lastThis,
			maxWait,
			result,
			timerId,
			lastCallTime,
			lastInvokeTime = 0,
			leading = false,
			maxing = false,
			trailing = true;

		if ( typeof func != 'function' ) {
			throw new TypeError( 'Expected a function' );
		}

		if ( typeof options === 'object' ) {
			leading = !! options.leading;
			maxing = 'maxWait' in options;
			maxWait = maxing ? Math.max( options.maxWait || 0, wait ) : maxWait;
			trailing = 'trailing' in options ? !! options.trailing : trailing;
		}

		function invokeFunc( time ) {
			var args = lastArgs,
				thisArg = lastThis,
				resolve = args._resolve;

			lastArgs = lastThis = undefined;
			lastInvokeTime = time;
			result = func.apply( thisArg, args );

			// If there's a resolve function, call it with the result
			if ( resolve ) {
				resolve( result );
			}

			return result;
		}

		function leadingEdge( time ) {
			// Reset any `maxWait` timer.
			lastInvokeTime = time;
			// Start the timer for the trailing edge.
			timerId = setTimeout( timerExpired, wait );
			// Invoke the leading edge.
			return leading
				? invokeFunc( time )
				: new Promise( ( resolve ) => {
						// Store the resolve function to be called when the function executes
						lastArgs._resolve = resolve;
				  } );
		}

		function remainingWait( time ) {
			var timeSinceLastCall = time - lastCallTime,
				timeSinceLastInvoke = time - lastInvokeTime,
				timeWaiting = wait - timeSinceLastCall;

			return maxing
				? Math.min( timeWaiting, maxWait - timeSinceLastInvoke )
				: timeWaiting;
		}

		function shouldInvoke( time ) {
			var timeSinceLastCall = time - lastCallTime,
				timeSinceLastInvoke = time - lastInvokeTime;

			// Either this is the first call, activity has stopped and we're at the
			// trailing edge, the system time has gone backwards and we're treating
			// it as the trailing edge, or we've hit the `maxWait` limit.
			return (
				lastCallTime === undefined ||
				timeSinceLastCall >= wait ||
				timeSinceLastCall < 0 ||
				( maxing && timeSinceLastInvoke >= maxWait )
			);
		}

		function timerExpired() {
			var time = Date.now();
			if ( shouldInvoke( time ) ) {
				return trailingEdge( time );
			}
			// Restart the timer.
			timerId = setTimeout( timerExpired, remainingWait( time ) );
		}

		function trailingEdge( time ) {
			timerId = undefined;

			// Only invoke if we have `lastArgs` which means `func` has been
			// debounced at least once.
			if ( trailing && lastArgs ) {
				return invokeFunc( time );
			}
			lastArgs = lastThis = undefined;
			return result;
		}

		function cancel() {
			if ( timerId !== undefined ) {
				clearTimeout( timerId );
			}
			// Reject any pending promise
			if ( lastArgs && lastArgs._resolve ) {
				lastArgs._resolve( [] ); // Resolve with empty array for cancelled requests
			}
			lastInvokeTime = 0;
			lastArgs = lastCallTime = lastThis = timerId = undefined;
		}

		function flush() {
			return timerId === undefined ? result : trailingEdge( Date.now() );
		}

		function debounced() {
			var time = Date.now(),
				isInvoking = shouldInvoke( time );

			lastArgs = arguments;
			lastThis = this;
			lastCallTime = time;

			if ( isInvoking ) {
				if ( timerId === undefined ) {
					return leadingEdge( lastCallTime );
				}
				if ( maxing ) {
					// Handle invocations in a tight loop.
					clearTimeout( timerId );
					timerId = setTimeout( timerExpired, wait );
					return invokeFunc( lastCallTime );
				}
			}
			if ( timerId === undefined ) {
				timerId = setTimeout( timerExpired, wait );
			}
			// Return a promise that will resolve when the function is eventually called
			return new Promise( ( resolve ) => {
				// Store the resolve function to be called when the function executes
				lastArgs._resolve = resolve;
			} );
		}
		debounced.cancel = cancel;
		debounced.flush = flush;
		return debounced;
	}

	Object.entries( a8cAddressAutocompleteServiceKeys ).forEach(
		( [ key, value ] ) => {
			let sessionId = generateSessionId();
			let requestDurations = [];
			let serviceErrorRetries = 0;
			// LRU Cache for search results - key: `${inputValue}:${country}`, value: data
			class LRUCache {
				constructor( maxSize = 100 ) {
					this.maxSize = maxSize;
					this.cache = new Map();
				}

				get( key ) {
					if ( this.cache.has( key ) ) {
						// Move to end (most recently used)
						const value = this.cache.get( key );
						this.cache.delete( key );
						this.cache.set( key, value );
						return value;
					}
					return null;
				}

				set( key, value ) {
					if ( this.cache.has( key ) ) {
						// Remove existing entry to move to end
						this.cache.delete( key );
					} else if ( this.cache.size >= this.maxSize ) {
						// Remove least recently used (first entry)
						const firstKey = this.cache.keys().next().value;
						this.cache.delete( firstKey );
					}
					this.cache.set( key, value );
				}

				clear() {
					this.cache.clear();
				}

				get size() {
					return this.cache.size;
				}
			}

			const searchCache = new LRUCache( 100 );

			// Helper function to check cache
			const getCachedResult = ( inputValue, country ) => {
				const cacheKey = `${ inputValue }:${ country }`;
				return searchCache.get( cacheKey );
			};

			// Helper function to store result in cache
			const cacheResult = ( inputValue, country, data ) => {
				const cacheKey = `${ inputValue }:${ country }`;
				searchCache.set( cacheKey, data );
			};

			// Shared error handling function
			const handleApiError = ( data, response ) => {
				if ( ! data.code && ! data.error ) {
					return; // No error to handle
				}

				const errorCode = data.code || data.error;

				switch ( errorCode ) {
					case 'expired_jwt_token':
					case 'malformed_jwt_token':
					case 'invalid_jwt_token':
					case 'invalid_issuer':
					case 'invalid_service':
					case 'missing_jwt_token':
						permanentlyDisabledServices.push( key );
						console.error(
							`Automattic Address Suggestion (${ key }) has been disabled due to invalid JWT token`
						);
						return;
					case 'rate_limit_exceeded':
						permanentlyDisabledServices.push( key );
						setTimeout( () => {
							const index =
								permanentlyDisabledServices.indexOf( key );
							if ( index !== -1 ) {
								permanentlyDisabledServices.splice( index, 1 );
							}
						}, ( Number( response.headers.get( 'RateLimit-Retry-After' ) ) || 60 ) * 1000 );
						console.error(
							`Automattic Address Suggestion (${ key }) has been disabled due to rate limit exceeded`
						);
						return;
					case 'missing_query':
						return;
					case 'no_suggestions':
						return;
					case 'missing_address_id':
						console.error(
							`Automattic Address Suggestion (${ key }) has been disabled due to missing address ID`
						);
						return;
					case 'no_place':
						console.error(
							`Automattic Address Suggestion (${ key }) has been disabled due to no place found`
						);
						return;
					case 'missing_session_id':
						sessionId = generateSessionId();
						return;
					case 'woo_address_suggestion_internal_error':
					case 'woo_address_suggestion_service_error':
					case 'woo_address_suggestion_server_error':
						serviceErrorRetries++;
						if (
							serviceErrorRetries >= MAX_SERVICE_ERROR_RETRIES
						) {
							permanentlyDisabledServices.push( key );
							console.error(
								`Automattic Address Suggestion (${ key }) has been disabled due to internal service error`
							);
						}
						return;
					default:
						return;
				}
			};

			const debouncedSearch = debounce(
				async ( inputValue, country ) => {
					const params = new URLSearchParams( {
						query: inputValue,
						country,
						lang: document.documentElement.lang || navigator.lang,
						session_id: sessionId,
						token: value.key,
					} );

					try {
						const startTime = performance.now();
						const response = await fetch(
							`${ searchUrl }?${ params.toString() }`
						);
						const endTime = performance.now();
						requestDurations.push( endTime - startTime );
						let data = await response.json();

						// Handle errors using shared function
						handleApiError( data, response );

						if ( Array.isArray( data ) ) {
							data = data.map( ( item ) => ( {
								id: item.id,
								label: item.label,
								matchedSubstrings: item.matched_substrings,
							} ) );
							// Cache the successful result.
							// An empty result is still a valid result and is cached.
							cacheResult( inputValue, country, data );
							return data;
						}
					} catch ( e ) {
						if ( e.name === 'AbortError' ) {
							// Ignore abort errors from cancelled requests
							return [];
						}
						console.error(
							`Error fetching address suggestions for ${ key }:`,
							e
						);
						return [];
					}
				},
				300,
				{ leading: false, trailing: true }
			);
			window.wc.addressAutocomplete.registerAddressAutocompleteProvider( {
				id: key,
				canSearch: () => {
					try {
						if ( permanentlyDisabledServices.includes( key ) ) {
							return false;
						}

						// Split JWT into parts
						const [ , payload ] = value.key.split( '.' );
						if ( ! payload ) {
							permanentlyDisabledServices.push( key );
							return false;
						}

						// Decode payload
						const decodedPayload = JSON.parse( atob( payload ) );

						// Check expiration
						const currentTime = Math.floor( Date.now() / 1000 );
						if (
							! decodedPayload.exp ||
							decodedPayload.exp < currentTime
						) {
							permanentlyDisabledServices.push( key );
							return false;
						}

						return true;
					} catch ( e ) {
						permanentlyDisabledServices.push( key );
						return false;
					}
				},
				search: async ( inputValue, country, type ) => {
					// We need to return early here because canSearch is not always called from search.
					if ( permanentlyDisabledServices.includes( key ) ) {
						return [];
					}

					inputValue = inputValue.trim();

					// Check cache first - bypass debounce for cached results
					const cachedResult = getCachedResult( inputValue, country );
					if ( cachedResult !== null ) {
						return cachedResult;
					}

					return await debouncedSearch( inputValue, country );
				},
				async select( addressId ) {
					const params = new URLSearchParams( {
						address_id: addressId,
						session_id: sessionId,
						lang: document.documentElement.lang,
						token: value.key,
					} );

					const response = await fetch(
						`${ selectUrl }?${ params.toString() }`
					);

					let data = await response.json();
					// Reset session ID after successful select
					sessionId = generateSessionId();
					try {
						dispatchEvent(
							new CustomEvent(
								'wc-address-autocomplete-service-request-durations',
								{
									detail: {
										requestDurations,
										provider: key,
									},
								}
							)
						);
					} catch ( e ) {
						console.error( e );
					}
					requestDurations = [];

					// Handle errors using shared function
					handleApiError( data, response );

					return data;
				},
			} );

			window.addEventListener(
				'wc-address-autocomplete-service-request-durations',
				( e ) => {
					if ( ! value.canTelemetry || e.detail.provider !== key ) {
						return;
					}
					// Send request durations to statsd, to keep track of the average request duration.
					new Image().src = createStatsdURL( 'a8c-ac-service', {
						name: 'request-durations',
						value: e.detail.requestDurations,
						type: 'timing',
					} );
				}
			);
		}
	);
} )();

function createBeacon( section, { name, value, type } ) {
	const event = name.replace( '-', '_' );

	// A counting event defaults to incrementing by one.
	if ( type === 'counting' ) {
		value = value === undefined ? 1 : value;
	}
	value = Array.isArray( value ) ? value : [ value ];
	return value.map(
		( v ) =>
			`a8c.${ section }.${ event }:${ v }|${
				type === 'timing' ? 'ms' : 'c'
			}`
	);
}

function createStatsdURL( sectionName, events ) {
	if ( ! Array.isArray( events ) ) {
		events = [ events ]; // Only a single event was passed to process.
	}

	const sanitizedSection = sectionName.replace( /[.:-]/g, '_' );
	const json = JSON.stringify( {
		beacons: events
			.map( ( event ) => createBeacon( sanitizedSection, event ) )
			.flat(),
	} );

	const encodedJson = encodeURIComponent( json );

	return `https://pixel.wp.com/boom.gif?json=${ encodedJson }`;
}