domain = $domain; } /** * Get the domain that was checked. * * @return string */ public function domain(): string { return $this->domain; } /** * Validate the domain looks like a real domain. * * @throws InvalidArgumentException */ public function validate() { if ( ! is_string( $this->domain ) ) { throw new InvalidArgumentException( sprintf( __( 'Domain must be a string, [%s] given.', 'amazon-s3-and-cloudfront' ), gettype( $this->domain ) ) ); } if ( ! trim( $this->domain ) ) { throw new InvalidArgumentException( __( 'Domain cannot be blank.', 'amazon-s3-and-cloudfront' ) ); } if ( preg_match( '/[^a-z0-9-\.]/i', $this->domain ) ) { throw new InvalidArgumentException( __( 'Domain can only contain letters, numbers, hyphens (-), and periods (.)', 'amazon-s3-and-cloudfront' ) ); } if ( $this->domain === parse_url( network_home_url(), PHP_URL_HOST ) ) { throw new InvalidArgumentException( sprintf( __( '%s must not be the same as the site domain. Use a subdomain instead.', 'amazon-s3-and-cloudfront' ), $this->domain ) ); } } /** * Validate the domain looks like a real domain and return a description of * any issue found or an empty string if no issue was found. * * Convenience function for validate() and check_dns_resolution() that doesn't throw exceptions. * * @return string */ public function get_validation_issue(): string { try { $this->validate(); $this->check_dns_resolution(); } catch ( Exception $e ) { return $e->getMessage(); } return ''; } /** * Check if the given domain passes all validation. * * @param string $domain * * @return bool */ public static function is_valid( string $domain ): bool { if ( isset( self::$validated[ $domain ] ) ) { return self::$validated[ $domain ]; } $check = new static( $domain ); try { $check->validate(); self::$validated[ $domain ] = true; } catch ( Exception $e ) { self::$validated[ $domain ] = false; } return self::$validated[ $domain ]; } /** * Test that the given URL works and returns a response that looks like * a REST response. * * @param string $url * * @return array * * @throws Domain_Check_Exception */ public function test_rest_endpoint( string $url ): array { $this->validate(); $this->check_dns_resolution(); $response = $this->dispatch_request( $url ); $this->check_response_headers( wp_remote_retrieve_headers( $response ) ); $this->check_response_code( wp_remote_retrieve_response_code( $response ) ); $this->check_response_type( wp_remote_retrieve_header( $response, 'content-type' ) ); $this->check_response_body( wp_remote_retrieve_body( $response ) ); return $response; } /** * Rewrite the given URL to use the configured domain. * * @param string $url * * @return string */ protected function prepare_url( string $url ): string { if ( empty( $this->domain() ) ) { return $url; } $pull_hostname = AS3CF_Utils::parse_url( $url, PHP_URL_HOST ); // Force the given domain in the rewritten URL if hostnames do not match. if ( $this->domain !== $pull_hostname ) { $url = str_replace( "//$pull_hostname/", "//$this->domain/", $url ); } return $url; } /** * Check that the domain is resolvable. * * @throws Unresolveable_Hostname_Exception */ protected function check_dns_resolution() { if ( $this->dns_checked ) { return; } if ( ! WP_Http::is_ip_address( $this->domain ) && $this->domain === gethostbyname( $this->domain ) ) { throw new Unresolveable_Hostname_Exception( sprintf( __( '%s did not resolve to an IP address.', 'amazon-s3-and-cloudfront' ), $this->domain ) ); } $this->dns_checked = true; } /** * Convert a WP_Error to the appropriate exception. * * @param WP_Error $error * * @return Domain_Check_Exception */ protected function get_exception_for_wp_error( WP_Error $error ) { if ( preg_match( '/SSL (certificate problem|operation failed)/i', $error->get_error_message() ) ) { return new Ssl_Connection_Exception( sprintf( __( 'An HTTPS connection error was encountered: %s.', 'amazon-s3-and-cloudfront' ), $error->get_error_message() ) ); } return new HTTP_Response_Exception( sprintf( __( 'An HTTP connection error was encountered: %s.', 'amazon-s3-and-cloudfront' ), $error->get_error_message() ) ); } /** * Check that the given response code is within the acceptable range. * * @param int $response_code * * @throws Invalid_Response_Code_Exception */ protected function check_response_code( int $response_code ) { if ( $response_code < 200 || $response_code > 399 ) { throw new Invalid_Response_Code_Exception( sprintf( __( 'An error was encountered while testing the domain: Received %d from endpoint.', 'amazon-s3-and-cloudfront' ), $response_code ) ); } } /** * Check that the response type is the correct type. * * @param array|string $content_type * * @throws Invalid_Response_Type_Exception */ protected function check_response_type( $content_type ) { if ( is_array( $content_type ) || ! preg_match( '/^application\/json/i', $content_type ) ) { throw new Invalid_Response_Type_Exception( sprintf( __( 'An error was encountered while testing the domain: Invalid response type: %s.', 'amazon-s3-and-cloudfront' ), join( ', ', (array) $content_type ) ) ); } } /** * Send a request to the given URL. * * @param string $url * * @return array Response data * @throws Domain_Check_Exception */ protected function dispatch_request( string $url ): array { $request_url = $this->prepare_url( $url ); $response = wp_remote_get( $request_url, array( /** * CloudFront origin timeout is configurable in Origin settings. * * @param int $seconds */ 'timeout' => apply_filters( 'as3cf_assets_pull_test_endpoint_timeout', 15 ), /** * Verify SSL certificates by default. * * @param bool $verify * @param string $domain */ 'sslverify' => apply_filters( 'as3cf_assets_pull_test_endpoint_sslverify', true, $this->domain ), // Make sure headers work for various services. 'headers' => array( 'accept' => '*/*', 'user-agent' => 'wp-offload-media', 'referer' => home_url(), ), ) ); if ( is_wp_error( $response ) ) { throw $this->get_exception_for_wp_error( $response ); } return $response; } /** * Make assertions about the pull request based on the response body. * * @param string $response_body * * @throws Malformed_Query_String_Exception * @throws Malformed_Response_Exception */ protected function check_response_body( string $response_body ) { $raw_body = json_decode( $response_body, true ); if ( null === $raw_body ) { throw new Malformed_Response_Exception( __( 'An error was encountered while testing the domain: Malformed response from endpoint.', 'amazon-s3-and-cloudfront' ) ); } if ( empty( $raw_body['ver'] ) ) { throw new Malformed_Query_String_Exception( __( 'An error was encountered while testing the domain: Query string missing "ver" parameter.', 'amazon-s3-and-cloudfront' ) ); } } /** * Checks response headers for possible errors. * * @param array|Requests_Utility_CaseInsensitiveDictionary $response_headers * * @throws S3_Bucket_Origin_Exception */ public static function check_response_headers( $response_headers ) { if ( ! empty( $response_headers['server'] ) && 'AmazonS3' === $response_headers['server'] ) { throw new S3_Bucket_Origin_Exception( __( 'An error was encountered while testing the domain: S3 bucket set as CDN origin.', 'amazon-s3-and-cloudfront' ) ); } } }