*/ private const RIAL_FACTORS = [ 'IRR' => 1, 'IRT' => 10, 'IRHR' => 1000, 'IRHT' => 10000, ]; /** * The server address and site id in a tracking code Matomo hands out. * * Reads the JavaScript code (`var u="//host/"` and `setSiteId`) and the image * code (`matomo.php?idsite=`). The code itself is never kept. * * @param string $code Pasted tracking code. * * @return array{url: string, site: int}|null */ public static function snippet( string $code ): ?array { $url = null; $site = 0; if ( 1 === preg_match( '/var\s+u\s*=\s*["\']([^"\']+)["\']/', $code, $m ) ) { $url = $m[1]; } elseif ( 1 === preg_match( '#(?:https?:)?//[^"\'\s<>]+?/(?:matomo|piwik)\.(?:php|js)#i', $code, $m ) ) { $url = $m[0]; } if ( 1 === preg_match( '/setSiteId["\']\s*,\s*["\']?(\d+)/', $code, $m ) || 1 === preg_match( '/[?&](?:amp;)?idsite=(\d+)/i', $code, $m ) ) { $site = (int) $m[1]; } $url = null === $url ? null : self::serverUrl( $url ); return null === $url || $site < 1 ? null : [ 'url' => $url, 'site' => $site, ]; } /** * A Matomo server address as the base the tracker paths hang from. * * `//host/`, `https://host/matomo.php` and `https://host/sub/` all become * `https://host/…/`. Only HTTPS, except for a local host. * * @param string $url Address as typed or found in a tracking code. */ public static function serverUrl( string $url ): ?string { $url = trim( $url ); if ( str_starts_with( $url, '//' ) ) { $url = 'https:' . $url; } $parts = parse_url( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Pure helper, tested without WordPress. if ( ! is_array( $parts ) || ! isset( $parts['scheme'], $parts['host'] ) ) { return null; } $scheme = strtolower( $parts['scheme'] ); $host = strtolower( $parts['host'] ); if ( 'https' !== $scheme && ! ( 'http' === $scheme && self::isLocal( $host ) ) ) { return null; } $path = (string) ( $parts['path'] ?? '' ); $path = (string) preg_replace( '#/(?:matomo|piwik)\.(?:php|js)$|/index\.php$|/js/?$#i', '/', $path ); $path = rtrim( $path, '/' ) . '/'; $port = isset( $parts['port'] ) ? ':' . $parts['port'] : ''; return $scheme . '://' . $host . $port . $path; } /** * A host that may be reached over plain HTTP, as for RAHKAR_HUB_URL. * * @param string $host Lower-case host. */ public static function isLocal( string $host ): bool { return in_array( $host, [ 'localhost', '127.0.0.1', '::1' ], true ) || str_ends_with( $host, '.test' ) || str_ends_with( $host, '.local' ); } /** * The Matomo visitor id of this browser for a site, from its `_pk_id` cookie. * * The cookie is `_pk_id..` with the id first in its value. PHP * turns the dots of a cookie name into underscores, so both are accepted. * * @param array $cookies Cookies, as in `$_COOKIE`. * @param int $site Matomo site id. */ public static function visitorId( array $cookies, int $site ): ?string { foreach ( $cookies as $name => $value ) { if ( ! is_string( $value ) || 1 !== preg_match( '/^_pk_id[._]' . $site . '[._][0-9a-f]+$/', (string) $name ) ) { continue; } if ( 1 === preg_match( '/^([0-9a-f]{16})\./', $value, $m ) ) { return $m[1]; } } return null; } /** * Whether a search term looks like contact or identity details. * * A search box is now and then taken for the place to type a mobile number or * an email. Such a term must not reach Matomo: an email, or ten digits or more * in a row — a mobile, a national id — in Persian, Arabic or Latin digits, with * spaces and dashes between them. * * @param string $term Search term. */ public static function looksPersonal( string $term ): bool { $term = strtr( $term, array_combine( [ '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩' ], [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ] ) ); if ( 1 === preg_match( '/[^\s@]+@[^\s@]+\.[^\s@]+/u', $term ) ) { return true; } $compact = (string) preg_replace( '/(?<=\d)[\s\-.()]+(?=\d)/u', '', $term ); return 1 === preg_match( '/\d{10,}/', $compact ); } /** * An amount in the store's currency as whole rials, or null for a currency * the hub does not convert. * * @param mixed $amount Amount as WooCommerce gives it. * @param string $currency Store currency code. */ public static function rials( mixed $amount, string $currency ): ?int { $factor = self::RIAL_FACTORS[ strtoupper( $currency ) ] ?? null; if ( null === $factor || ! is_numeric( $amount ) ) { return null; } return (int) round( (float) $amount * $factor ); } /** * An amount in rials when the currency converts, else as it is. * * @param mixed $amount Amount. * @param string $currency Store currency code. */ public static function money( mixed $amount, string $currency ): float { $rials = self::rials( $amount, $currency ); return null === $rials ? round( (float) ( is_numeric( $amount ) ? $amount : 0 ), 2 ) : (float) $rials; } }