normalized source name. */ public const AI_SOURCES = array( 'chatgpt.com' => 'chatgpt', 'openai.com' => 'chatgpt', 'perplexity.ai' => 'perplexity', 'gemini.google.com' => 'gemini', 'copilot.microsoft.com' => 'copilot', 'bing.com/chat' => 'copilot', 'claude.ai' => 'claude', 'you.com' => 'you', 'meta.ai' => 'meta-ai', 'deepseek.com' => 'deepseek', 'grok.com' => 'grok', ); /** * Run a callable and swallow any error so a failure never takes the site down. * * @param callable $fn Work. * @param mixed $fallback Returned on failure. * @return mixed */ public static function safe( callable $fn, $fallback = null ) { try { return $fn(); } catch ( \Throwable $e ) { self::log( $e->getMessage() . ' @ ' . basename( $e->getFile() ) . ':' . $e->getLine() ); return $fallback; } } /** Wrap a hook callback in safe(). */ public static function guard( callable $fn ): \Closure { return static function ( ...$args ) use ( $fn ) { $first = $args[0] ?? null; try { return $fn( ...$args ); } catch ( \Throwable $e ) { self::log( $e->getMessage() . ' @ ' . basename( $e->getFile() ) . ':' . $e->getLine() ); return $first; // For filters, pass the value through unchanged. } }; } public static function log( string $msg ): void { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'flareOps: ' . $msg ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions } } /* ---------- request headers ($_SERVER): sanitized and validated in one place ---------- */ /** A $_SERVER value as a single-line string (tags, control characters and line breaks removed). */ public static function server_text( string $key, int $max = 512 ): string { if ( ! isset( $_SERVER[ $key ] ) || ! is_string( $_SERVER[ $key ] ) ) { return ''; } $v = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) ); return mb_substr( $v, 0, $max ); } /** A $_SERVER value that must be an IP address ('' if it is not one). */ public static function server_ip( string $key ): string { $ip = self::server_text( $key, 64 ); if ( 'HTTP_X_FORWARDED_FOR' === $key ) { $ip = trim( explode( ',', $ip )[0] ); // The first entry is the client. } $ip = filter_var( $ip, FILTER_VALIDATE_IP ); return false === $ip ? '' : $ip; } /** The request path (no query string or fragment), starting with "/". */ public static function request_path(): string { if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! is_string( $_SERVER['REQUEST_URI'] ) ) { return '/'; } // esc_url_raw, not sanitize_text_field: that would strip the %XX of non-ASCII paths (e.g. Hebrew URLs). $uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ), array( 'http', 'https' ) ); $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); $path = (string) preg_replace( '#[^A-Za-z0-9/%._~!$&\'()*+,;=:@-]#', '', $path ); return ( '' === $path || '/' !== $path[0] ) ? '/' : mb_substr( $path, 0, 255 ); } public static function woo_active(): bool { return class_exists( '\WooCommerce' ) && function_exists( 'wc_get_product' ); } public static function iso( $ts = null ): string { return gmdate( 'Y-m-d\TH:i:s\Z', null === $ts ? time() : (int) $ts ); } /** Convert stored UTC MySQL datetime to ISO 8601. */ public static function mysql_to_iso( ?string $dt ): ?string { if ( ! $dt ) { return null; } $ts = strtotime( $dt . ' UTC' ); return false === $ts ? null : self::iso( $ts ); } /** HTML -> plain text, decoded, whitespace collapsed. */ public static function plain_text( $html ): string { $html = (string) $html; if ( function_exists( 'strip_shortcodes' ) ) { $html = strip_shortcodes( $html ); } // Leftover shortcodes of inactive plugins / page builders ([vc_row], [et_pb_text admin_label="x"], [/foo]). // Plain bracketed words such as "[New]" or "[2 pack]" are kept. $html = (string) preg_replace( '#\[/?[a-z][a-z0-9]*[_\-][a-z0-9_\-]*(?:\s[^\]]*)?\]|\[/[a-z][a-z0-9_\-]*\]|\[[a-z][a-z0-9_\-]*\s+[^\]]*=[^\]]*\]#i', ' ', $html ); $html = preg_replace( '#<(script|style)[^>]*>.*?#is', ' ', $html ); $html = preg_replace( '#<(br|/p|/div|/li|/h[1-6])[^>]*>#i', "\n", (string) $html ); $text = wp_strip_all_tags( (string) $html ); $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $text = preg_replace( "/[ \t\x{00A0}]+/u", ' ', $text ); $text = preg_replace( "/\s*\n\s*/", "\n", (string) $text ); return trim( (string) $text ); } /** scheme://host[:port]/path only: query strings and fragments (tokens, emails, chat IDs) are dropped. */ public static function strip_url_query( string $url ): string { $url = trim( $url ); if ( '' === $url ) { return ''; } $p = wp_parse_url( $url ); if ( ! is_array( $p ) || empty( $p['host'] ) ) { return (string) preg_replace( '/[?#].*$/s', '', $url ); } $scheme = isset( $p['scheme'] ) ? strtolower( $p['scheme'] ) . '://' : '//'; $port = isset( $p['port'] ) ? ':' . (int) $p['port'] : ''; return $scheme . strtolower( $p['host'] ) . $port . ( $p['path'] ?? '' ); } /** Salted SHA-256 of an order ID (salt: random per-site option). The API never returns raw order IDs. */ public static function order_ref( int $order_id ): string { $salt = (string) get_option( 'flareops_order_salt', '' ); if ( '' === $salt ) { $salt = bin2hex( random_bytes( 32 ) ); update_option( 'flareops_order_salt', $salt, false ); } return hash( 'sha256', $salt . '|' . $order_id ); } /** * Validate a GTIN (GTIN-8/12/13/14) including the GS1 check digit. * * @param string $gtin Digits only. */ public static function is_valid_gtin( string $gtin ): bool { if ( ! preg_match( '/^(\d{8}|\d{12}|\d{13}|\d{14})$/', $gtin ) ) { return false; } $digits = str_split( $gtin ); $check = (int) array_pop( $digits ); $digits = array_reverse( $digits ); $sum = 0; foreach ( $digits as $i => $d ) { $sum += (int) $d * ( 0 === $i % 2 ? 3 : 1 ); } return ( ( 10 - ( $sum % 10 ) ) % 10 ) === $check; } /** Strip spaces and hyphens from a GTIN candidate. */ public static function normalize_gtin( string $gtin ): string { return preg_replace( '/[\s\-]/', '', trim( $gtin ) ); } /** * Classify a referrer and/or utm_source as an AI app. * * @return string|null Normalized source name or null. */ public static function ai_source( string $referrer, string $utm = '' ): ?string { $candidates = array(); if ( '' !== $referrer ) { $parts = wp_parse_url( $referrer ); if ( is_array( $parts ) && ! empty( $parts['host'] ) ) { $host = strtolower( preg_replace( '/^www\./i', '', $parts['host'] ) ); $candidates[] = $host . ( $parts['path'] ?? '' ); } } if ( '' !== $utm ) { $candidates[] = strtolower( $utm ); } foreach ( $candidates as $c ) { foreach ( self::AI_SOURCES as $needle => $name ) { if ( str_contains( $needle, '/' ) ) { if ( str_starts_with( $c, $needle ) ) { return $name; } continue; } // Host match: exact or subdomain. utm_source may be "chatgpt.com" or "chatgpt". if ( $c === $needle || str_ends_with( explode( '/', $c )[0], '.' . $needle ) || str_starts_with( $c, $needle . '/' ) ) { return $name; } } // Bare utm names such as "chatgpt", "perplexity". if ( in_array( $c, array_unique( array_values( self::AI_SOURCES ) ), true ) ) { return $c; } } return null; } }