Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
339 lines
9.1 KiB
PHP
339 lines
9.1 KiB
PHP
<?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' )
|
|
);
|
|
}
|
|
}
|
|
}
|