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,98 @@
<?php
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
use DeliciousBrains\WP_Offload_Media\Pro\Integrations\Media_Library_Pro;
/** @var Amazon_S3_And_CloudFront|Amazon_S3_And_CloudFront_Pro $this */
/** @var array|bool $provider_object */
/** @var WP_Post $post */
/** @var array $available_actions */
/** @var bool $local_file_exists */
/** @var string $sendback */
$provider_name = empty( $provider_object['provider'] ) ? '' : $this->get_provider_service_name( $provider_object['provider'] );
$is_current_provider = ! empty( $provider_object['provider'] ) && $this->get_storage_provider()->get_provider_key_name() === $provider_object['provider'] ? true : false;
$provider_class = $is_current_provider ? '' : ' error';
$is_removable = $is_current_provider && in_array( 'remove', $available_actions );
$is_copyable = $local_file_exists && in_array( 'copy', $available_actions ) && ( $is_current_provider || empty( $provider_object ) );
$is_downloadable = ! $local_file_exists && in_array( 'download', $available_actions ) && $is_current_provider;
$is_local_removable = $is_current_provider && $local_file_exists && in_array( 'remove_local', $available_actions );
/** @var Media_Library|Media_Library_Pro $media_integration */
$media_integration = $this->get_integration_manager()->get_integration( 'mlib' );
?>
<div class="s3-details">
<?php if ( ! $provider_object ) : ?>
<div class="misc-pub-section">
<em class="not-copied"><?php _e( 'This item has not been offloaded yet.', 'amazon-s3-and-cloudfront' ); ?></em>
</div>
<?php else : ?>
<div class="misc-pub-section">
<div class="s3-key"><?php echo $media_integration->get_media_action_strings( 'provider' ); ?>:</div>
<input type="text" id="as3cf-provider" class="widefat<?php echo $provider_class; ?>" readonly="readonly" value="<?php echo $provider_name; ?>">
</div>
<div class="misc-pub-section">
<div class="s3-key"><?php echo $media_integration->get_media_action_strings( 'bucket' ); ?>:</div>
<input type="text" id="as3cf-bucket" class="widefat" readonly="readonly" value="<?php echo $provider_object['bucket']; ?>">
</div>
<div class="misc-pub-section">
<div class="s3-key"><?php echo $media_integration->get_media_action_strings( 'key' ); ?>:</div>
<input type="text" id="as3cf-key" class="widefat" readonly="readonly" value="<?php echo $provider_object['key']; ?>">
</div>
<?php if ( isset( $provider_object['region'] ) && $provider_object['region'] ) : ?>
<div class="misc-pub-section">
<div class="s3-key"><?php echo $media_integration->get_media_action_strings( 'region' ); ?>:</div>
<div id="as3cf-region" class="s3-value"><?php echo $provider_object['region']; ?></div>
</div>
<?php endif; ?>
<div class="misc-pub-section">
<div class="s3-key"><?php echo $media_integration->get_media_action_strings( 'acl' ); ?>:</div>
<div id="as3cf-acl" class="s3-value">
<?php echo $media_integration->get_acl_value_string( $provider_object['acl'], $post->ID ); ?>
</div>
</div>
<?php if ( isset( $provider_object['is_verified'] ) && empty( $provider_object['is_verified'] ) ) : ?>
<div class="misc-pub-section">
<div class="s3-key"><?php echo $media_integration->get_media_action_strings( 'is_verified' ); ?>:</div>
<div id="as3cf-is-verified" class="s3-value"><?php echo $media_integration->get_media_action_strings( 'not_verified' ); ?></div>
</div>
<?php endif; ?>
<?php if ( $is_downloadable ) : ?>
<div class="misc-pub-section">
<div class="not-copied"><?php _e( 'File does not exist on server', 'amazon-s3-and-cloudfront' ); ?></div>
<a id="as3cf-download-action" href="<?php echo $media_integration->get_media_action_url( 'download', $post->ID, $sendback ); ?>">
<?php echo $media_integration->get_media_action_strings( 'download' ); ?>
</a>
</div>
<?php endif; ?>
<?php if ( $is_local_removable ) : ?>
<div class="misc-pub-section">
<div class="not-copied"><?php _e( 'File exists on server', 'amazon-s3-and-cloudfront' ); ?></div>
<a id="as3cf-remove-local-action" href="<?php echo $media_integration->get_media_action_url( 'remove_local', $post->ID, $sendback ); ?>">
<?php echo $media_integration->get_media_action_strings( 'remove_local' ); ?>
</a>
</div>
<?php endif; ?>
<?php endif; ?>
<div class="clear"></div>
</div>
<?php if ( $is_removable || $is_copyable || $is_local_removable ) : ?>
<div class="s3-actions">
<?php if ( $is_removable ) : ?>
<div class="remove-action">
<a id="as3cf-remove-action" href="<?php echo $media_integration->get_media_action_url( 'remove', $post->ID, $sendback ); ?>">
<?php echo $media_integration->get_media_action_strings( 'remove' ); ?>
</a>
</div>
<?php endif; ?>
<?php if ( $is_copyable ) : ?>
<div class="copy-action">
<a id="as3cf-copy-action" href="<?php echo $media_integration->get_media_action_url( 'copy', $post->ID, $sendback ); ?>" class="button button-secondary">
<?php echo $media_integration->get_media_action_strings( 'copy' ); ?>
</a>
</div>
<?php endif; ?>
<div class="clear"></div>
</div>
<?php endif; ?>

32
view/notice.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited, WordPress.Security.EscapeOutput.DeprecatedWhitelistCommentFound
/** @var string $message */
$type = ( isset( $type ) ) ? $type : 'notice-info';
$dismissible = ( isset( $dismissible ) ) ? $dismissible : false;
$inline = ( isset( $inline ) ) ? $inline : false;
$id = ( isset( $id ) ) ? 'id="' . $id . '"' : '';
$style = ( isset( $style ) ) ? $style : '';
$auto_p = ( isset( $auto_p ) ) ? $auto_p : 'true';
$class = ( isset( $class ) ) ? $class : '';
$show_callback = ( isset( $show_callback ) && false !== $show_callback ) ? array( $GLOBALS[ $show_callback[0] ], $show_callback[1] ) : false;
$callback_args = ( isset( $callback_args ) ) ? $callback_args : array();
?>
<div <?php echo $id; ?> class="notice <?php echo $type; ?><?php echo ( $dismissible ) ? ' is-dismissible' : ''; ?> as3cf-notice <?php echo ( $inline ) ? ' inline' : ''; ?> <?php echo empty( $class ) ? '' : ' ' . $class; ?>" style="<?php echo $style; ?>">
<?php if ( $auto_p ) : ?>
<p>
<?php endif; ?>
<?php echo $message; // xss ok ?>
<?php if ( false !== $show_callback && is_callable( $show_callback ) ) : ?>
<a href="#" class="as3cf-notice-toggle" data-hide="<?php _e( 'Hide', 'amazon-s3-and-cloudfront' ); ?>"><?php _e( 'Show', 'amazon-s3-and-cloudfront' ); ?></a>
<?php endif; ?>
<?php if ( $auto_p ) : ?>
</p>
<?php endif; ?>
<?php if ( false !== $show_callback && is_callable( $show_callback ) ) : ?>
<div class="as3cf-notice-toggle-content" style="display: none;">
<?php call_user_func_array( $show_callback, $callback_args ); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,10 @@
<div class="as3cf-deactivate-plugin-container" style="display: none;">
<h3><?php _e( 'WP Offload Media Uninstall', 'amazon-s3-and-cloudfront' ); ?></h3>
<p><?php _e( 'You have the "Remove Local Media" option ON, which means some media files are likely missing from your local server and deactivating WP Offload Media could result in broken images and file links on your site.', 'amazon-s3-and-cloudfront' ); ?></p>
<p><?php _e( 'We recommend you copy all the media files back to your server from the bucket before deactivating. Would you like to do this now?', 'amazon-s3-and-cloudfront' ); ?></p>
<p class="actions select">
<button type="submit" class="button button-primary right" data-download-tool="1"><?php _e( 'Yes', 'amazon-s3-and-cloudfront' ); ?></button>
<button type="submit" class="button right" data-download-tool="0"><?php _e( 'No', 'amazon-s3-and-cloudfront' ); ?></button>
<span class="spinner right"></span>
</p>
</div>

View File

@@ -0,0 +1,36 @@
<?php
/** @var array $plugins_not_installed */
?>
<div class="notice-info notice as3cf-pro-installer">
<p>
<strong>
<?php printf( __( 'Finish Installing %s', 'amazon-s3-and-cloudfront' ), $this->plugin_name ); ?>
</strong>
</p>
<p>
<?php printf( __( 'The %s plugin requires the following plugins to be installed:', 'amazon-s3-and-cloudfront' ), $this->plugin_name ); ?>
</p>
<ul style="list-style-type: disc; padding: 0 0 0 30px; margin: 5px 0;">
<?php foreach ( $plugins_not_installed as $slug => $plugin_name ) : ?>
<li style="margin: 0;">
<a class="thickbox" style="text-decoration: none;" href="<?php echo $this->get_plugin_info_url( $slug ); ?>">
<?php echo $plugin_name; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<p>
<a href="#" class="button button-primary button-large install-plugins" data-process="installer"><?php _e( 'Install & Activate Now' ); ?></a>
</p>
<p>
<em>
<?php
$deactivate_url = $this->get_plugin_action_url( 'deactivate' );
$deactivate_link = sprintf( '<a id="' . $this->plugin_slug . '-install-notice-deactivate" style="text-decoration:none;" href="%s">%s</a>', $deactivate_url, __( 'deactivating', 'amazon-s3-and-cloudfront' ) );
printf( __( 'You can also remove this message by %s the %s plugin.', 'amazon-s3-and-cloudfront' ), $deactivate_link, $this->plugin_name );
?>
</em>
</p>
</div>

View File

@@ -0,0 +1,38 @@
<?php
/** @var string $dashboard */
/** @var string $id */
/** @var string $heading */
/** @var string $type */
/** @var string $message */
/** @var string $extra */
/** @var bool $dismissible */
/** @var string $dismiss_url */
/** @var array $links */
?>
<div id="<?php echo "{$id}"; ?>" class="as3cf-pro-licence-notice notice <?php echo $type; ?> important <?php echo $dismissible ? 'is-dismissible' : ''; ?>">
<p>
<strong><?php echo $heading; ?></strong> &mdash; <?php echo $message; ?>
</p>
<?php if ( $extra ) : ?>
<p>
<?php echo $extra; ?>
</p>
<?php endif; ?>
<?php if ( $links ) : ?>
<p class="notice-links">
<?php echo join( ' | ', $links ); ?>
</p>
<?php endif; ?>
<?php if ( $dismissible ) : // use inline script to omit need to enqueue a script dashboard-wide ?>
<script>
(function( $ ) {
$( '#<?php echo "{$id}.is-dismissible"; ?>' ).on( 'click', '.notice-dismiss', function() {
$.get( '<?php echo $dismiss_url; ?>' );
} );
})( jQuery );
</script>
<?php endif ?>
</div>

11
view/pro/settings.php Normal file
View File

@@ -0,0 +1,11 @@
<div id="as3cf-settings" class="wpome wpomepro">
</div>
<script>
new AS3CFPro_Settings( {
target: document.getElementById( 'as3cf-settings' ),
props: {
init: as3cfpro_settings
}
} );
</script>

11
view/settings.php Normal file
View File

@@ -0,0 +1,11 @@
<div id="as3cf-settings" class="wpome">
</div>
<script>
new AS3CF_Settings( {
target: document.getElementById( 'as3cf-settings' ),
props: {
init: as3cf_settings
}
} );
</script>