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
301 lines
8.1 KiB
PHP
301 lines
8.1 KiB
PHP
<?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 );
|
|
}
|
|
}
|