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:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

View 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 ) );
}
}