Files
WPS3Media/classes/settings/domain-check.php

339 lines
9.1 KiB
PHP
Raw Permalink Normal View History

<?php
namespace DeliciousBrains\WP_Offload_Media\Settings;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Domain_Check_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\HTTP_Response_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Invalid_Response_Code_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Invalid_Response_Type_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Malformed_Query_String_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Malformed_Response_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\S3_Bucket_Origin_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Ssl_Connection_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Unresolveable_Hostname_Exception;
use Exception;
use InvalidArgumentException;
use Requests_Utility_CaseInsensitiveDictionary;
use WP_Error;
use WP_Http;
class Domain_Check {
/**
* @var string domain / hostname
*/
protected $domain = '';
/**
* @var bool
*/
private $dns_checked;
/**
* @var array Validated domain cache
*/
private static $validated = array();
/**
* Domain_Check constructor.
*
* @param string $domain
*/
public function __construct( string $domain ) {
$this->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(
__( '<code>%s</code> 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(
__( '<code>%s</code> 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: <code>%s</code>.', 'amazon-s3-and-cloudfront' ), $error->get_error_message() )
);
}
return new HTTP_Response_Exception(
sprintf( __( 'An HTTP connection error was encountered: <code>%s</code>.', '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: <code>Received %d from endpoint</code>.', '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: <code>Invalid response type: %s</code>.', '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: <code>Malformed response from endpoint</code>.', 'amazon-s3-and-cloudfront' )
);
}
if ( empty( $raw_body['ver'] ) ) {
throw new Malformed_Query_String_Exception(
__( 'An error was encountered while testing the domain: <code>Query string missing "ver" parameter</code>.', '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: <code>S3 bucket set as CDN origin</code>.', 'amazon-s3-and-cloudfront' )
);
}
}
}