Files
WPS3Media/classes/pro/integrations/enable-media-replace.php
Malin 3248cbb029 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
2026-03-03 12:30:18 +01:00

209 lines
6.1 KiB
PHP

<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Pro\Items\Remove_Provider_Handler;
use Exception;
class Enable_Media_Replace extends Integration {
/**
* @var Media_Library
*/
private $media_library;
/**
* @var bool
*/
private $wait_for_generate_attachment_metadata = false;
/**
* @var string
*/
private $downloaded_original;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'EnableMediaReplace\EnableMediaReplacePlugin' ) || function_exists( 'enable_media_replace_init' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
$this->media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
}
/**
* @inheritDoc
*/
public function setup() {
// Make sure EMR allows OME to filter get_attached_file.
add_filter( 'emr_unfiltered_get_attached_file', '__return_false' );
// Download the files and return their path so EMR doesn't get tripped up.
add_filter( 'as3cf_get_attached_file', array( $this, 'download_file' ), 10, 4 );
// Although EMR uses wp_unique_filename, it discards that potentially new filename for plain replace, but does then use the following filter.
add_filter( 'emr_unique_filename', array( $this, 'ensure_unique_filename' ), 10, 3 );
// Remove objects before offload happens, but don't re-offload just yet.
add_filter( 'as3cf_update_attached_file', array( $this, 'remove_existing_provider_files_during_replace' ), 10, 2 );
if ( $this->is_replacing_media() ) {
$this->wait_for_generate_attachment_metadata = true;
// Let the media library integration know it should wait for all attachment metadata.
add_filter( 'as3cf_wait_for_generate_attachment_metadata', array( $this, 'wait_for_generate_attachment_metadata' ) );
// Wait for WordPress core to tell us it has finished generating thumbnails.
add_filter( 'wp_generate_attachment_metadata', array( $this, 'generate_attachment_metadata_done' ) );
// Add our potentially downloaded primary file to the list files to remove.
add_filter( 'as3cf_remove_local_files', array( $this, 'filter_remove_local_files' ) );
}
}
/**
* Are we waiting for the wp_generate_attachment_metadata filter to fire?
*
* @handles as3cf_wait_for_generate_attachment_metadata
*
* @param bool $wait
*
* @return bool
*/
public function wait_for_generate_attachment_metadata( $wait ) {
if ( $this->wait_for_generate_attachment_metadata ) {
return true;
}
return $wait;
}
/**
* Update internal state for waiting for attachment_metadata.
*
* @handles wp_generate_attachment_metadata
*
* @param array $metadata
*
* @return array
*/
public function generate_attachment_metadata_done( $metadata ) {
$this->wait_for_generate_attachment_metadata = false;
return $metadata;
}
/**
* If we've downloaded an existing primary file from the provider we add it to the
* files_to_remove array when the Remove_Local handler runs.
*
* @handles as3cf_remove_local_files
*
* @param array $files_to_remove
*
* @return array
*/
public function filter_remove_local_files( $files_to_remove ) {
if ( ! empty( $this->downloaded_original ) && file_exists( $this->downloaded_original ) && ! in_array( $this->downloaded_original, $files_to_remove ) ) {
$files_to_remove[] = $this->downloaded_original;
}
return $files_to_remove;
}
/**
* Allow the Enable Media Replace plugin to copy the provider file back to the local
* server when the file is missing on the server via get_attached_file().
*
* @param string $url
* @param string $file
* @param int $attachment_id
* @param Media_Library_Item $as3cf_item
*
* @return string
*/
public function download_file( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
$this->downloaded_original = $this->as3cf->plugin_compat->copy_image_to_server_on_action( 'media_replace_upload', false, $url, $file, $as3cf_item );
return $this->downloaded_original;
}
/**
* EMR deletes the original files before replace, then updates metadata etc.
* So we should remove associated offloaded files too, and let normal (re)offload happen afterwards.
*
* @param string $file
* @param int $attachment_id
*
* @return string
* @throws Exception
*/
public function remove_existing_provider_files_during_replace( $file, $attachment_id ) {
if ( ! $this->is_replacing_media() ) {
return $file;
}
if ( ! $this->as3cf->is_plugin_setup( true ) ) {
return $file;
}
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( ! empty( $as3cf_item ) ) {
$remove_provider = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() );
$remove_provider->handle( $as3cf_item, array( 'verify_exists_on_local' => false ) );
// By deleting the item here, a new one will be created by when EMR generates the thumbnails and our ML integration
// picks it up. Ensuring that the object versioning string and object list are generated fresh.
$as3cf_item->delete();
}
return $file;
}
/**
* Are we doing a media replacement?
*
* @return bool
*/
public function is_replacing_media() {
$action = filter_input( INPUT_GET, 'action' );
if ( empty( $action ) ) {
return false;
}
return ( 'media_replace_upload' === sanitize_key( $action ) );
}
/**
* Ensure the generated filename for an image replaced with a new image is unique.
*
* @param string $filename File name that should be unique.
* @param string $path Absolute path to where the file will go.
* @param int $id Attachment ID.
*
* @return string
*/
public function ensure_unique_filename( $filename, $path, $id ) {
// Get extension.
$ext = pathinfo( $filename, PATHINFO_EXTENSION );
$ext = $ext ? ".$ext" : '';
return $this->media_library->filter_unique_filename( $filename, $ext, $path, $id );
}
}