= strlen( $blocked ) ? $number : $blocked; $short = $long === $number ? $blocked : $number; if ( $long === $short || ( strlen( $long ) - strlen( $short ) <= 3 && str_ends_with( $long, $short ) && strlen( $short ) >= 9 ) ) { return $pattern; } } return ''; } /** * Phone digits without an international prefix (00) or national trunk 0. * * Stricter than DuplicateCustomers\Similarity::phone() on purpose: a block needs the exact * national number, while duplicate finding uses looser heuristics to suggest likely matches. * * @param string $phone Phone. * @return string */ private static function digits( $phone ) { $digits = preg_replace( '/\D+/', '', (string) $phone ); $digits = str_starts_with( $digits, '00' ) ? substr( $digits, 2 ) : $digits; return ltrim( $digits, '0' ); } /** * An IPv4-mapped IPv6 address (::ffff:1.2.3.4) as plain IPv4, so both forms match the same list. * * @param string|false $bin Packed address. * @return string|false */ private static function unmap( $bin ) { if ( is_string( $bin ) && 16 === strlen( $bin ) && str_starts_with( $bin, str_repeat( "\0", 10 ) . "\xff\xff" ) ) { return substr( $bin, 12 ); } return $bin; } /** * Parse an IP or CIDR pattern. A range needs a whole-number prefix within the address size; * anything else is invalid (never read as /0, which would match everyone). * * @since 1.0.0 * @param string $pattern Pattern. * @return array{0: string, 1: int}|null [ packed network, prefix bits ] or null. */ public static function network( $pattern ) { $parts = explode( '/', trim( (string) $pattern ), 2 ); $raw = @inet_pton( $parts[0] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- invalid input returns false. if ( false === $raw ) { return null; } $max = strlen( $raw ) * 8; if ( ! isset( $parts[1] ) ) { $bits = $max; } elseif ( preg_match( '/^\d{1,3}$/', $parts[1] ) && (int) $parts[1] <= $max && (int) $parts[1] >= ( 4 === strlen( $raw ) ? 8 : 16 ) ) { $bits = (int) $parts[1]; } else { return null; // Also refuses huge ranges (/0–/7) that would block whole regions by mistake. } $net = self::unmap( $raw ); if ( strlen( $net ) !== strlen( $raw ) ) { if ( $bits < 104 ) { return null; } $bits -= 96; // ::ffff:a.b.c.d/120 is a.b.c.d/24. } return array( $net, $bits ); } /** * The first IP pattern that matches: exact IPv4/IPv6 or a CIDR range. * * @since 1.0.0 * @param string $ip IP. * @param string[] $patterns Patterns. * @return string */ public static function ip( $ip, array $patterns ) { $bin = self::unmap( @inet_pton( trim( (string) $ip ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- invalid input returns false. if ( false === $bin ) { return ''; } foreach ( $patterns as $pattern ) { $parsed = self::network( $pattern ); if ( ! $parsed || strlen( $parsed[0] ) !== strlen( $bin ) ) { continue; } list( $net, $bits ) = $parsed; $full = intdiv( $bits, 8 ); $rest = $bits % 8; if ( substr( $bin, 0, $full ) !== substr( $net, 0, $full ) ) { continue; } if ( $rest ) { $mask = chr( ( 0xff << ( 8 - $rest ) ) & 0xff ); if ( ( $bin[ $full ] & $mask ) !== ( $net[ $full ] & $mask ) ) { continue; } } return $pattern; } return ''; } }