= $min && strlen( $value ) <= $max && 1 === preg_match( '/^[A-Za-z0-9_-]+$/', $value ); } /** * @param mixed $value Candidate identifier. * @return bool */ public static function is_identifier( $value ) { return is_string( $value ) && strlen( $value ) >= 3 && strlen( $value ) <= 256 && 1 === preg_match( '/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $value ); } /** * @param string $raw Site URL. * @return array|null */ public static function canonical_site( $raw ) { if ( ! is_string( $raw ) || '' === trim( $raw ) ) { return null; } $parts = wp_parse_url( trim( $raw ) ); if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) { return null; } $scheme = strtolower( $parts['scheme'] ); $host = strtolower( rtrim( $parts['host'], '.' ) ); if ( ! in_array( $scheme, array( 'http', 'https' ), true ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) { return null; } $port = isset( $parts['port'] ) ? (int) $parts['port'] : null; if ( null !== $port && ! ( ( 'http' === $scheme && 80 === $port ) || ( 'https' === $scheme && 443 === $port ) ) ) { return null; } if ( ! self::is_public_hostname( $host ) ) { return null; } $key = 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : $host; return array( 'site_origin' => $scheme . '://' . $host, 'site_key' => $key, 'security_level' => 'https' === $scheme ? 'https' : 'limited_http', ); } /** * @param string $host Hostname. * @return bool */ private static function is_public_hostname( $host ) { if ( false !== filter_var( $host, FILTER_VALIDATE_IP ) || 'localhost' === $host || false === strpos( $host, '.' ) ) { return false; } foreach ( array( '.localhost', '.local', '.internal' ) as $suffix ) { if ( substr( $host, -strlen( $suffix ) ) === $suffix ) { return false; } } $labels = explode( '.', $host ); foreach ( $labels as $label ) { if ( '' === $label || strlen( $label ) > 63 || ! preg_match( '/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/', $label ) ) { return false; } } return true; } /** * @param array $launch Launch payload. * @return string */ public static function launch_fingerprint( $launch ) { return Crypto::sha256_base64url( Crypto::canonical_json( $launch ) ); } /** * @param array $input Completion fields. * @return string */ public static function completion_message( $input ) { return implode( "\n", array( self::COMPLETION_PREFIX, $input['challenge_id'], $input['challenge_hash'], $input['project_id'], $input['site_origin'], $input['callback_uri'], $input['code_challenge'], ) ); } /** * @param string $callback_uri Canonical callback URI. * @param string $body_bytes Exact request bytes. * @param string $timestamp Header timestamp. * @param string $key_id Header key id. * @return string */ public static function callback_signature_payload( $callback_uri, $body_bytes, $timestamp, $key_id ) { return implode( "\n", array( self::CALLBACK_PREFIX, 'POST', $callback_uri, Crypto::sha256_base64url( $body_bytes ), $timestamp, $key_id, ) ); } /** * @param string $value Canonical ISO timestamp. * @return int|null */ public static function parse_canonical_time( $value ) { if ( ! is_string( $value ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/', $value ) ) { return null; } $date = \DateTimeImmutable::createFromFormat( '!Y-m-d\TH:i:s.v\Z', $value, new \DateTimeZone( 'UTC' ) ); $errors = \DateTimeImmutable::getLastErrors(); if ( false === $date || ( is_array( $errors ) && ( $errors['warning_count'] > 0 || $errors['error_count'] > 0 ) ) || $date->format( 'Y-m-d\TH:i:s.v\Z' ) !== $value ) { return null; } return $date->getTimestamp(); } /** @return string */ public static function format_canonical_time( $timestamp ) { return gmdate( 'Y-m-d\TH:i:s.000\Z', (int) $timestamp ); } /** @return string */ public static function php_runtime_version() { return PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; } }