feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)
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
This commit is contained in:
300
classes/settings/delivery-check.php
Normal file
300
classes/settings/delivery-check.php
Normal file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings;
|
||||
|
||||
use DeliciousBrains\WP_Offload_Media\Items\Provider_Test_Item;
|
||||
use DeliciousBrains\WP_Offload_Media\Items\Upload_Handler;
|
||||
use DeliciousBrains\WP_Offload_Media\Items\Remove_Provider_Handler;
|
||||
use Amazon_S3_And_CloudFront;
|
||||
use AS3CF_Utils;
|
||||
use Exception;
|
||||
use WP_Error as AS3CF_Result;
|
||||
|
||||
class Delivery_Check extends Domain_Check {
|
||||
/**
|
||||
* @var Amazon_S3_And_CloudFront
|
||||
*/
|
||||
private $as3cf;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $local_file_path = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $test_file_name = '';
|
||||
|
||||
/**
|
||||
* @var Provider_Test_Item|null
|
||||
*/
|
||||
private $as3cf_item;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Amazon_S3_And_CloudFront $as3cf
|
||||
*/
|
||||
public function __construct( Amazon_S3_And_CloudFront $as3cf ) {
|
||||
parent::__construct( '' );
|
||||
$this->as3cf = $as3cf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class destructor.
|
||||
*/
|
||||
public function __destruct() {
|
||||
$this->remove_test_files();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test file & to upload to the storage provider.
|
||||
*
|
||||
* @param bool $is_private
|
||||
*
|
||||
* @return AS3CF_Result
|
||||
*/
|
||||
public function setup_test_file( bool $is_private ): AS3CF_Result {
|
||||
if ( empty( $this->as3cf_item ) || $this->as3cf_item->is_private() !== (bool) $is_private ) {
|
||||
$mode = $is_private ? __( 'Private', 'amazon-s3-and-cloudfront' ) : __( 'Public', 'amazon-s3-and-cloudfront' );
|
||||
|
||||
if ( ! $this->create_local_file( $is_private ) ) {
|
||||
return new AS3CF_Result(
|
||||
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
|
||||
sprintf(
|
||||
_x(
|
||||
'Delivery provider status cannot be determined. An error was encountered while attempting to create a temporary file for %1$s delivery.',
|
||||
'amazon-s3-and-cloudfront'
|
||||
),
|
||||
$mode
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $this->upload_file( $is_private ) ) {
|
||||
$this->remove_test_files();
|
||||
|
||||
return new AS3CF_Result(
|
||||
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
|
||||
sprintf(
|
||||
_x(
|
||||
'Delivery provider status cannot be determined. An error was encountered while attempting to offload a temporary file for %1$s delivery.',
|
||||
'amazon-s3-and-cloudfront'
|
||||
),
|
||||
$mode
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a public file.
|
||||
*
|
||||
* @return AS3CF_Result
|
||||
*/
|
||||
public function test_public_file_access(): AS3CF_Result {
|
||||
// Protect against improper use of this method.
|
||||
if ( empty( $this->as3cf_item ) || false !== $this->as3cf_item->is_private() ) {
|
||||
return new AS3CF_Result(
|
||||
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
|
||||
__( 'Internal error', 'amazon-s3-and-cloudfront' )
|
||||
);
|
||||
}
|
||||
|
||||
$url = $this->as3cf_item->get_provider_url();
|
||||
|
||||
if ( is_wp_error( $url ) ) {
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR, $url->get_error_message() );
|
||||
}
|
||||
|
||||
$this->domain = AS3CF_Utils::parse_url( $url, PHP_URL_HOST );
|
||||
|
||||
try {
|
||||
$this->check_dns_resolution();
|
||||
$response = $this->dispatch_request( $url );
|
||||
$this->check_response_code( wp_remote_retrieve_response_code( $response ) );
|
||||
} catch ( Exception $e ) {
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR, $e->getMessage() );
|
||||
}
|
||||
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a private file.
|
||||
*
|
||||
* @return AS3CF_Result
|
||||
*/
|
||||
public function test_private_file_access(): AS3CF_Result {
|
||||
// Protect against improper use of this method.
|
||||
if ( empty( $this->as3cf_item ) || false === $this->as3cf_item->is_private() ) {
|
||||
return new AS3CF_Result(
|
||||
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
|
||||
__( 'Internal error', 'amazon-s3-and-cloudfront' )
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to access the file with the standard (signed) URL.
|
||||
$url = $this->as3cf_item->get_provider_url();
|
||||
|
||||
if ( is_wp_error( $url ) ) {
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR, $url->get_error_message() );
|
||||
}
|
||||
|
||||
$this->domain = AS3CF_Utils::parse_url( $url, PHP_URL_HOST );
|
||||
|
||||
try {
|
||||
$this->check_dns_resolution();
|
||||
$response = $this->dispatch_request( $url );
|
||||
$this->check_response_code( (int) wp_remote_retrieve_response_code( $response ) );
|
||||
} catch ( Exception $e ) {
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR, $e->getMessage() );
|
||||
}
|
||||
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a private file using an unsigned URL.
|
||||
*
|
||||
* @return AS3CF_Result
|
||||
*/
|
||||
public function test_private_file_access_unsigned(): AS3CF_Result {
|
||||
// Protect against improper use of this method.
|
||||
if ( empty( $this->as3cf_item ) || false === $this->as3cf_item->is_private() ) {
|
||||
return new AS3CF_Result(
|
||||
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
|
||||
__( 'Internal error', 'amazon-s3-and-cloudfront' )
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the signing parameters from the URL.
|
||||
$url = $this->as3cf->maybe_remove_query_string( $this->as3cf_item->get_provider_url() );
|
||||
|
||||
if ( is_wp_error( $url ) ) {
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR, $url->get_error_message() );
|
||||
}
|
||||
|
||||
try {
|
||||
$this->check_dns_resolution();
|
||||
$response = $this->dispatch_request( $url );
|
||||
|
||||
// This should throw in an exception.
|
||||
$this->check_response_code( (int) wp_remote_retrieve_response_code( $response ) );
|
||||
|
||||
// If we're still here it's no good.
|
||||
return new AS3CF_Result(
|
||||
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
|
||||
__( 'Private file accessible using an unsigned URL.', 'amazon-s3-and-cloudfront' )
|
||||
);
|
||||
} catch ( Exception $e ) {
|
||||
// Accessing a private file using an unsigned URL failed, this is actually success.
|
||||
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to the storage provider.
|
||||
*
|
||||
* @param bool $is_private
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function upload_file( bool $is_private ): bool {
|
||||
// Use a bucket key with no dynamic parts.
|
||||
$bucket_path = $this->as3cf->get_object_prefix() . $this->test_file_name;
|
||||
|
||||
$extra_info = array(
|
||||
'objects' => array(
|
||||
Provider_Test_Item::primary_object_key() => array(
|
||||
'source_file' => basename( $this->local_file_path ),
|
||||
'is_private' => $is_private,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->as3cf_item = new Provider_Test_Item(
|
||||
$this->as3cf->get_storage_provider()->get_provider_key_name(),
|
||||
$this->as3cf->get_setting( 'region' ),
|
||||
$this->as3cf->get_setting( 'bucket' ),
|
||||
$bucket_path,
|
||||
$is_private,
|
||||
0,
|
||||
$this->local_file_path,
|
||||
null,
|
||||
$extra_info
|
||||
);
|
||||
|
||||
$upload_handler = new Upload_Handler( $this->as3cf );
|
||||
|
||||
add_filter( 'upload_mimes', array( $this, 'allow_txt_offload' ) );
|
||||
$upload_result = $upload_handler->handle( $this->as3cf_item );
|
||||
remove_filter( 'upload_mimes', array( $this, 'allow_txt_offload' ) );
|
||||
|
||||
if ( true !== $upload_result ) {
|
||||
$this->remove_test_files();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure txt files can be offloaded.
|
||||
*
|
||||
* @handles upload_mimes
|
||||
*
|
||||
* @param array $mime_types
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function allow_txt_offload( array $mime_types ): array {
|
||||
if ( empty( $mime_types['txt'] ) ) {
|
||||
$mime_types['txt'] = 'text/plain';
|
||||
}
|
||||
|
||||
return $mime_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove created test files both locally and on the storage provider.
|
||||
*/
|
||||
public function remove_test_files() {
|
||||
if ( file_exists( $this->local_file_path ) ) {
|
||||
unlink( $this->local_file_path );
|
||||
}
|
||||
|
||||
if ( ! is_null( $this->as3cf_item ) ) {
|
||||
$item_remover = new Remove_Provider_Handler( $this->as3cf );
|
||||
$item_remover->handle( $this->as3cf_item );
|
||||
$this->as3cf_item = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create local file with unique file name.
|
||||
*
|
||||
* @param bool $is_private
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function create_local_file( bool $is_private ): bool {
|
||||
if ( ! empty( $this->local_file_path ) ) {
|
||||
$this->remove_test_files();
|
||||
}
|
||||
|
||||
$uploads_dir = wp_get_upload_dir();
|
||||
$visibility = $is_private ? 'private' : 'public';
|
||||
|
||||
$this->test_file_name = "as3cf-delivery-check-$visibility-" . time() . '.txt';
|
||||
$this->local_file_path = $uploads_dir['basedir'] . '/' . $this->test_file_name;
|
||||
$file_contents = __( 'This is a test file to check delivery. Delete me if found.', 'amazon-s3-and-cloudfront' );
|
||||
|
||||
return (bool) file_put_contents( $this->local_file_path, $file_contents );
|
||||
}
|
||||
}
|
||||
338
classes/settings/domain-check.php
Normal file
338
classes/settings/domain-check.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?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' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
classes/settings/exceptions/domain-check-exception.php
Normal file
32
classes/settings/exceptions/domain-check-exception.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
|
||||
class Domain_Check_Exception extends Exception {
|
||||
|
||||
/**
|
||||
* @var string Relative path for dbrains link
|
||||
*/
|
||||
protected $more_info = '/wp-offload-media/doc/assets-pull-domain-check-errors/';
|
||||
|
||||
/**
|
||||
* Get the exception name in key form.
|
||||
*/
|
||||
public function get_key(): string {
|
||||
$class = new ReflectionClass( $this );
|
||||
|
||||
return strtolower( $class->getShortName() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative URL to a help document for this exception.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function more_info(): string {
|
||||
return $this->more_info;
|
||||
}
|
||||
}
|
||||
6
classes/settings/exceptions/http-response-exception.php
Normal file
6
classes/settings/exceptions/http-response-exception.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class HTTP_Response_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Invalid_Response_Code_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Invalid_Response_Type_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Malformed_Query_String_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Malformed_Response_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class S3_Bucket_Origin_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Signature_Verification_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
6
classes/settings/exceptions/ssl-connection-exception.php
Normal file
6
classes/settings/exceptions/ssl-connection-exception.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Ssl_Connection_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings\Exceptions;
|
||||
|
||||
class Unresolveable_Hostname_Exception extends Domain_Check_Exception {
|
||||
}
|
||||
242
classes/settings/validation-manager.php
Normal file
242
classes/settings/validation-manager.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings;
|
||||
|
||||
use Amazon_S3_And_CloudFront;
|
||||
use WP_Error;
|
||||
|
||||
class Validation_Manager {
|
||||
/**
|
||||
* @var Amazon_S3_And_CloudFront
|
||||
*/
|
||||
protected $as3cf;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $validation_result = array();
|
||||
|
||||
/**
|
||||
* The threshold in seconds for the relative time to be considered "just now".
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected static $threshold_just_now = 60;
|
||||
|
||||
/**
|
||||
* @var Validator_Interface[]
|
||||
*/
|
||||
private $settings_validators = array();
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $base_last_validation_result_key = 'as3cf_last_settings_validation_result';
|
||||
|
||||
/**
|
||||
* @param Amazon_S3_And_CloudFront $as3cf
|
||||
*/
|
||||
public function __construct( Amazon_S3_And_CloudFront $as3cf ) {
|
||||
$this->as3cf = $as3cf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a validator class for a section key.
|
||||
*
|
||||
* @param string $section
|
||||
* @param Validator_Interface $settings_validator
|
||||
*/
|
||||
public function register_validator( string $section, Validator_Interface $settings_validator ) {
|
||||
// Only one instance can be responsible for a section, so we just overwrite if called twice.
|
||||
$this->settings_validators[ $section ] = $settings_validator;
|
||||
|
||||
// The validator may know about one or more actions that are fired when its settings are saved.
|
||||
foreach ( $settings_validator->post_save_settings_actions() as $action ) {
|
||||
add_action( $action, array( $this, 'action_post_save_settings' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all registered validator classes and return the result in an array. If the
|
||||
* force flag is set to true, the validators may run checks that are time-consuming
|
||||
* or affects the global plugin state (notices).
|
||||
*
|
||||
* @param bool $force
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate( bool $force = false ): array {
|
||||
// Sort the validators by priority.
|
||||
uasort(
|
||||
$this->settings_validators,
|
||||
function ( Validator_Interface $a, Validator_Interface $b ) {
|
||||
return $a->get_validator_priority() <=> $b->get_validator_priority();
|
||||
}
|
||||
);
|
||||
|
||||
$validation_start = time();
|
||||
|
||||
// Run all registered settings validators.
|
||||
foreach ( $this->settings_validators as $section => $settings_validator ) {
|
||||
$transient_key = $this->last_validation_result_key( $section );
|
||||
$this->validation_result[ $section ] = get_site_transient( $transient_key );
|
||||
|
||||
if ( ! $force && is_array( $this->validation_result[ $section ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure only one instance of a validator is processing at once unless forced.
|
||||
$this->validation_result[ $section ] = array(
|
||||
'type' => Validator_Interface::AS3CF_STATUS_MESSAGE_INFO,
|
||||
'message' => array( _x( 'Processing…', 'Validation in progress', 'amazon-s3-and-cloudfront' ) ),
|
||||
'timestamp' => $validation_start,
|
||||
);
|
||||
set_site_transient( $transient_key, $this->validation_result[ $section ], MINUTE_IN_SECONDS );
|
||||
|
||||
$result = $settings_validator->validate_settings( $force );
|
||||
|
||||
$this->validation_result[ $section ]['type'] = $result->get_error_code();
|
||||
$this->validation_result[ $section ]['message'] = array( $result->get_error_message() );
|
||||
|
||||
$timeout = Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR === $result->get_error_code() ? 5 * MINUTE_IN_SECONDS : DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Adjust the transient timeout for the cache of a setting section's validation results.
|
||||
*
|
||||
* Min: 3 seconds
|
||||
* Max: 7 days
|
||||
*
|
||||
* The default is 1 day unless the result is an error, in which case it is then 5 minutes.
|
||||
*
|
||||
* @param int $timeout Time until transient expires in seconds.
|
||||
* @param string $section The setting section, e.g. storage, delivery or assets.
|
||||
* @param WP_Error $result The result to be cached.
|
||||
*/
|
||||
$timeout = min( max( 3, (int) apply_filters( $this->base_last_validation_result_key . '_timeout', $timeout, $section, $result ) ), 7 * DAY_IN_SECONDS );
|
||||
|
||||
set_site_transient( $transient_key, $this->validation_result[ $section ], $timeout );
|
||||
}
|
||||
|
||||
$this->update_relative_time();
|
||||
|
||||
return $this->validation_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific or all registered sections and status messages.
|
||||
*
|
||||
* If all sections to be returned, they're in an associative array
|
||||
* with the sections as the keys.
|
||||
*
|
||||
* @param string $section Optional specific section's result only.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_validation_result( string $section = '' ) {
|
||||
if ( empty( $this->validation_result ) ) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
if ( empty( $section ) ) {
|
||||
return $this->validation_result;
|
||||
} elseif ( ! empty( $this->validation_result[ $section ] ) ) {
|
||||
return $this->validation_result[ $section ];
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the validation status for a section.
|
||||
*
|
||||
* @param string $section
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_validation_status( string $section ): string {
|
||||
$result = $this->get_validation_result( $section );
|
||||
|
||||
if ( ! isset( $result['type'] ) ) {
|
||||
return Validator_Interface::AS3CF_STATUS_MESSAGE_UNKNOWN;
|
||||
}
|
||||
|
||||
return $result['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the last validation timestamp to force validation on next check.
|
||||
*
|
||||
* @param bool $saved Whether settings were successfully saved or not
|
||||
*/
|
||||
public function action_post_save_settings( bool $saved ) {
|
||||
if ( empty( $this->settings_validators ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
static $deleted = array();
|
||||
|
||||
foreach ( $this->settings_validators as $section => $validator ) {
|
||||
if ( in_array( $section, $deleted ) || ! in_array( current_action(), $validator->post_save_settings_actions() ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete_site_transient( $this->last_validation_result_key( $section ) );
|
||||
$deleted[] = $section;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given section currently have a validation error?
|
||||
*
|
||||
* @param string $section
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function section_has_error( string $section ): bool {
|
||||
if ( empty( $section ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR === $this->get_validation_status( $section );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a correctly formatted validation result transient key.
|
||||
*
|
||||
* @param string $section
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function last_validation_result_key( string $section ): string {
|
||||
return $this->base_last_validation_result_key . '_' . $section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the relative time for each section's last validation.
|
||||
*/
|
||||
protected function update_relative_time() {
|
||||
foreach ( array_keys( $this->validation_result ) as $key ) {
|
||||
if ( ! isset( $this->validation_result[ $key ]['timestamp'] ) ) {
|
||||
$this->validation_result[ $key ]['last_update'] = _x( 'Unknown', 'Relative time in settings notice', 'amazon-s3-and-cloudfront' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->validation_result[ $key ]['last_update'] = $this->get_relative_time( $this->validation_result[ $key ]['timestamp'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative time display string.
|
||||
*
|
||||
* @param int $timestamp
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_relative_time( int $timestamp ): string {
|
||||
if ( time() - $timestamp <= static::$threshold_just_now ) {
|
||||
return _x( 'Just now', 'Relative time in settings notice', 'amazon-s3-and-cloudfront' );
|
||||
}
|
||||
|
||||
return sprintf( __( '%s ago', 'amazon-s3-and-cloudfront' ), human_time_diff( $timestamp ) );
|
||||
}
|
||||
}
|
||||
39
classes/settings/validator-interface.php
Normal file
39
classes/settings/validator-interface.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Settings;
|
||||
|
||||
use WP_Error as AS3CF_Result;
|
||||
|
||||
interface Validator_Interface {
|
||||
const AS3CF_STATUS_MESSAGE_ERROR = 'error';
|
||||
const AS3CF_STATUS_MESSAGE_WARNING = 'warning';
|
||||
const AS3CF_STATUS_MESSAGE_INFO = 'info';
|
||||
const AS3CF_STATUS_MESSAGE_SUCCESS = 'success';
|
||||
const AS3CF_STATUS_MESSAGE_UNKNOWN = 'unknown';
|
||||
const AS3CF_STATUS_MESSAGE_SKIPPED = 'skipped';
|
||||
|
||||
/**
|
||||
* Validate settings. Set the force flag to true to allow the validators to run
|
||||
* checks that are time-consuming or affects the global state of the plugin.
|
||||
*
|
||||
* @param bool $force A potentially time resource consuming tests to run.
|
||||
*
|
||||
* @return AS3CF_Result
|
||||
*/
|
||||
public function validate_settings( bool $force = false ): AS3CF_Result;
|
||||
|
||||
/**
|
||||
* Get the name of the actions that are fired when the settings that the validator
|
||||
* is responsible for are saved.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function post_save_settings_actions(): array;
|
||||
|
||||
/**
|
||||
* Get the validator priority.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_validator_priority(): int;
|
||||
}
|
||||
Reference in New Issue
Block a user