= $len && substr( $haystack, -$len ) === $needle ); } /** Cuts to at most $max bytes without splitting a UTF-8 character when mbstring is present. */ public static function cut( string $s, int $max ): string { if ( strlen( $s ) <= $max ) { return $s; } if ( function_exists( 'mb_strcut' ) ) { return mb_strcut( $s, 0, $max, 'UTF-8' ); } return substr( $s, 0, $max ); } /** * First marker found (case-insensitive substring), with its byte offset. * * @param string $haystack Bounded text. * @param string[] $markers Markers. * @return array{0:string,1:int}|null */ public static function firstMarker( string $haystack, array $markers ) { $best = null; foreach ( $markers as $m ) { $pos = stripos( $haystack, $m ); if ( false !== $pos && ( null === $best || $pos < $best[1] ) ) { $best = array( $m, $pos ); } } return $best; } /** * Up to Assumptions::EXCERPTS_MAX excerpts around distinct occurrences of the * markers, the first at $first, joined by a newline, at most * Assumptions::EXCERPTS_TOTAL_MAX_CHARS bytes (D-09). An occurrence inside the * previous excerpt does not open a new one. * * @param string $haystack Bounded text. * @param string[] $markers Markers. * @param int $first Offset of the first occurrence. */ public static function excerptsAround( string $haystack, array $markers, int $first ): string { $parts = array(); $pos = $first; $left = Assumptions::EXCERPTS_MAX; while ( null !== $pos && $left > 0 ) { --$left; $parts[] = self::excerptAround( $haystack, $pos ); $end = max( 0, $pos - 60 ) + Assumptions::EXCERPT_MAX_CHARS; $pos = null; foreach ( $markers as $m ) { $p = $end < strlen( $haystack ) ? stripos( $haystack, $m, $end ) : false; if ( false !== $p && ( null === $pos || $p < $pos ) ) { $pos = $p; } } } return self::cut( implode( "\n", $parts ), Assumptions::EXCERPTS_TOTAL_MAX_CHARS ); } /** Excerpt of at most Assumptions::EXCERPT_MAX_CHARS bytes around $pos, newlines flattened. */ public static function excerptAround( string $haystack, int $pos ): string { $start = max( 0, $pos - 60 ); $part = substr( $haystack, $start, Assumptions::EXCERPT_MAX_CHARS + 8 ); $part = str_replace( array( "\r\n", "\r", "\n", "\t" ), ' ', $part ); return self::cut( $part, Assumptions::EXCERPT_MAX_CHARS ); } /** Lowercased file name of a '/' path. */ public static function basename( string $path ): string { $pos = strrpos( $path, '/' ); return false === $pos ? $path : substr( $path, $pos + 1 ); } /** * Extensions of a file name, final last: "x.php.jpg" => array('php','jpg'). * The part before the first dot is never an extension (".htaccess" => array('htaccess')). * * @return string[] lowercased */ public static function extensions( string $path ): array { $parts = explode( '.', strtolower( self::basename( $path ) ) ); array_shift( $parts ); return array_values( array_filter( $parts, 'strlen' ) ); } /** Final extension, lowercased, '' when none. */ public static function finalExtension( string $path ): string { $ext = self::extensions( $path ); return $ext ? (string) end( $ext ) : ''; } /** "Fichier PHP" of the SPEC: final extension in EXEC. */ public static function isPhpFile( string $path ): bool { return in_array( self::finalExtension( $path ), Assumptions::EXEC, true ); } /** Host of an absolute URL (http, https or protocol-relative), lowercased; '' when not absolute. */ public static function hostOf( string $url ): string { $url = trim( $url, " \t\"'" ); if ( self::startsWith( $url, '//' ) ) { $url = 'http:' . $url; } $lower = strtolower( $url ); if ( ! self::startsWith( $lower, 'http://' ) && ! self::startsWith( $lower, 'https://' ) ) { return ''; } $host = parse_url( $url, PHP_URL_HOST ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- pure domain without WordPress functions (PurityTest); wp_parse_url() only works around PHP < 5.4.7, the plugin requires 7.4. return is_string( $host ) ? strtolower( $host ) : ''; } /** * Same host as the site, a "www." variant counting as the same (SPEC * contrôle 6: redirection vers `www.` du même hôte reste muette). * * @param string $host Host to test. * @param string[] $siteHosts Hosts of home and siteurl. */ public static function isSiteHost( string $host, array $siteHosts ): bool { $bare = self::stripWww( strtolower( $host ) ); foreach ( $siteHosts as $site ) { if ( '' !== $site && self::stripWww( strtolower( $site ) ) === $bare ) { return true; } } return false; } private static function stripWww( string $host ): string { return self::startsWith( $host, 'www.' ) ? substr( $host, 4 ) : $host; } /** * Lines of a bounded text, each cut to Assumptions::LINE_MAX_CHARS. * * @return string[] */ public static function lines( string $text ): array { $lines = explode( "\n", str_replace( array( "\r\n", "\r" ), "\n", $text ) ); foreach ( $lines as $i => $line ) { $lines[ $i ] = substr( $line, 0, Assumptions::LINE_MAX_CHARS ); } return $lines; } }