"{}|\\\\^`]/', $host ) ) { return null; } $port = ''; if ( isset( $p['port'] ) ) { $default = ( 'http' === $scheme && 80 === (int) $p['port'] ) || ( 'https' === $scheme && 443 === (int) $p['port'] ); $port = $default ? '' : ':' . (int) $p['port']; } $userinfo = ''; if ( isset( $p['user'] ) ) { $userinfo = $p['user'] . ( isset( $p['pass'] ) ? ':' . $p['pass'] : '' ) . '@'; } $path = $p['path'] ?? '/'; $path = '' === $path ? '/' : self::remove_dot_segments( $path ); $path = str_replace( ' ', '%20', $path ); $query = isset( $p['query'] ) ? '?' . str_replace( ' ', '%20', $p['query'] ) : ''; return $scheme . '://' . $userinfo . $host . $port . $path . $query; } /** * Remove "." and ".." segments (RFC 3986 section 5.2.4). * * @param string $path URL path. */ public static function remove_dot_segments( string $path ): string { if ( false === strpos( $path, '.' ) ) { return $path; } $segments = explode( '/', $path ); $output = array(); $count = count( $segments ); foreach ( $segments as $i => $segment ) { if ( '.' === $segment ) { if ( $i === $count - 1 ) { $output[] = ''; } continue; } if ( '..' === $segment ) { if ( count( $output ) > 1 ) { array_pop( $output ); } if ( $i === $count - 1 ) { $output[] = ''; } continue; } $output[] = $segment; } $result = implode( '/', $output ); if ( '' === $result || '/' !== $result[0] ) { $result = '/' . ltrim( $result, '/' ); } return $result; } /** * Hash used for the UNIQUE index. * * @param string $normalized Normalized URL. */ public static function hash( string $normalized ): string { return sha1( $normalized ); } /** * Lowercase host of a URL ('' if none). * * @param string $url URL. */ public static function host( string $url ): string { $host = wp_parse_url( $url, PHP_URL_HOST ); return is_string( $host ) ? strtolower( $host ) : ''; } /** * Number of host buckets. Divisible by 1, 2, 3 and 4 so any lane count maps buckets evenly. */ public const HOST_BUCKETS = 12; /** * Stable bucket (0-11) for a host; check lanes own a fixed subset of buckets, so a host is * only ever checked by one lane at a time. * * @param string $host Lowercase host. */ public static function host_bucket( string $host ): int { return (int) ( sprintf( '%u', crc32( $host ) ) % self::HOST_BUCKETS ); } /** * Whether a URL points at this site (same host as home_url(), ignoring a "www." prefix). * * @param string $url Normalized URL. */ public static function is_internal( string $url ): bool { $home = self::bare_host( self::host( home_url( '/' ) ) ); return '' !== $home && self::bare_host( self::host( $url ) ) === $home; } /** * Admin/auth endpoints that only make sense for a logged-in user. They are never extracted or * checked: without cookies they just redirect to the login screen (a false "Redirect"). */ private const ADMIN_FILES = array( 'wp-login.php', 'admin-ajax.php', 'admin-post.php' ); /** * Whether a URL is a WordPress admin or auth URL on this site: /wp-admin/*, wp-login.php, * admin-ajax.php, admin-post.php, or any URL carrying a _wpnonce parameter. * * @param string $url Normalized absolute URL. */ public static function is_wp_admin( string $url ): bool { $host = self::bare_host( self::host( $url ) ); $own = array_filter( array( self::bare_host( self::host( home_url( '/' ) ) ), self::bare_host( self::host( site_url( '/' ) ) ), self::bare_host( self::host( admin_url( '/' ) ) ), ) ); if ( '' === $host || ! in_array( $host, $own, true ) ) { return false; } $path = strtolower( rawurldecode( (string) wp_parse_url( $url, PHP_URL_PATH ) ) ); $segments = array_values( array_filter( explode( '/', $path ), static function ( string $segment ): bool { return '' !== $segment; } ) ); // Segment match also covers WordPress installed in a subdirectory (/wp/wp-admin/). if ( in_array( 'wp-admin', $segments, true ) || in_array( (string) end( $segments ), self::ADMIN_FILES, true ) ) { return true; } $query = (string) wp_parse_url( $url, PHP_URL_QUERY ); if ( '' !== $query ) { foreach ( explode( '&', $query ) as $pair ) { $name = strtolower( rawurldecode( (string) strtok( $pair, '=' ) ) ); if ( '_wpnonce' === $name ) { return true; } } } return false; } /** * Host without a leading "www.". * * @param string $host Host. */ private static function bare_host( string $host ): string { return 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : $host; } /** * Whether a host matches a pattern list. Patterns: "linkedin.com" (also matches subdomains), * "amazon.*" (any public suffix), "*.instagram.com". * * @param string $host Lowercase host. * @param string[] $patterns Patterns. */ public static function host_matches( string $host, array $patterns ): bool { $host = strtolower( $host ); foreach ( $patterns as $pattern ) { $pattern = strtolower( trim( $pattern ) ); if ( '' === $pattern ) { continue; } if ( 0 === strpos( $pattern, '*.' ) ) { $pattern = substr( $pattern, 2 ); } if ( '.*' === substr( $pattern, -2 ) ) { // "amazon.*" => amazon.com, amazon.co.uk, www.amazon.de, smile.amazon.com ... $label = preg_quote( substr( $pattern, 0, -2 ), '/' ); if ( preg_match( '/(^|\.)' . $label . '\.[a-z]{2,}(\.[a-z]{2,})?$/', $host ) ) { return true; } continue; } if ( $host === $pattern || substr( $host, -strlen( '.' . $pattern ) ) === '.' . $pattern ) { return true; } } return false; } }