Files
WPS3Media/classes/pro/integrations/meta-slider.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

376 lines
10 KiB
PHP

<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
use DeliciousBrains\WP_Offload_Media\Items\Download_Handler;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Items\Upload_Handler;
use Exception;
class Meta_Slider extends Integration {
/**
* @var Media_Library
*/
private $media_library;
/**
* Keep track of get_metadata recursion level
*
* @var int
*/
private $get_postmeta_recursion_level = 0;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'MetaSliderPlugin' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
$this->media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
}
/**
* @inheritDoc
*/
public function setup() {
add_filter( 'metaslider_attachment_url', array( $this, 'metaslider_attachment_url' ), 10, 2 );
add_filter( 'sanitize_post_meta_amazonS3_info', array( $this, 'layer_slide_sanitize_post_meta' ), 10, 3 );
add_filter( 'as3cf_pre_update_attachment_metadata', array( $this, 'layer_slide_abort_upload' ), 10, 4 );
add_filter( 'as3cf_remove_attachment_paths', array( $this, 'layer_slide_remove_attachment_paths' ), 10, 4 );
add_action( 'add_post_meta', array( $this, 'add_post_meta' ), 10, 3 );
add_action( 'update_post_meta', array( $this, 'update_post_meta' ), 10, 4 );
// Maybe download primary image.
add_filter( 'as3cf_get_attached_file', array( $this, 'download_for_resize' ), 10, 4 );
// Filter HTML in layer sliders when they are saved and fetched.
add_filter( 'sanitize_post_meta_ml-slider_html', array( $this, 'sanitize_layer_slider_html' ) );
add_filter( 'get_post_metadata', array( $this, 'filter_get_post_metadata' ), 10, 4 );
}
/**
* Use the Provider URL for a Meta Slider slide image.
*
* @handles metaslider_attachment_url
*
* @param string $url
* @param int $slide_id
*
* @return string
*/
public function metaslider_attachment_url( $url, $slide_id ) {
$provider_url = $this->media_library->wp_get_attachment_url( $url, $slide_id );
if ( ! is_wp_error( $provider_url ) && false !== $provider_url ) {
return $provider_url;
}
return $url;
}
/**
* Layer slide sanitize post meta.
*
* This fixes issues with 'Layer Slides', which uses `get_post_custom` to retrieve
* attachment meta, but does not unserialize the data. This results in the `amazonS3_info`
* key being double serialized when inserted into the database.
*
* @handles sanitize_post_meta_amazonS3_info
*
* @param mixed $meta_value
* @param string $meta_key
* @param string $object_type
*
* @return mixed
*
* Note: Legacy filter, kept for migration purposes.
*/
public function layer_slide_sanitize_post_meta( $meta_value, $meta_key, $object_type ) {
if ( ! $this->is_layer_slide() ) {
return $meta_value;
}
return AS3CF_Utils::maybe_unserialize( $meta_value );
}
/**
* Layer slide abort upload.
*
* 'Layer Slide' duplicates an attachment in the Media Library, but uses the same
* file as the original. This prevents us trying to upload a new version to the bucket.
*
* @handles as3cf_pre_update_attachment_metadata
*
* @param bool $pre
* @param mixed $data
* @param int $post_id
* @param Media_Library_Item|null $as3cf_item
*
* @return bool
*/
public function layer_slide_abort_upload( $pre, $data, $post_id, Media_Library_Item $as3cf_item = null ) {
if ( $this->is_layer_slide() && empty( $as3cf_item ) ) {
$original_id = filter_input( INPUT_POST, 'slide_id' );
if ( empty( $original_id ) ) {
return $pre;
}
$original_item = Media_Library_Item::get_by_source_id( $original_id );
if ( empty( $original_item ) ) {
return $pre;
}
$as3cf_item = Media_Library_Item::create_from_source_id( $post_id );
if ( empty( $as3cf_item ) ) {
return $pre;
}
$as3cf_item->set_path( $original_item->path() );
$as3cf_item->set_original_path( $original_item->original_path() );
$as3cf_item->set_extra_info( $original_item->extra_info() );
$as3cf_item->save();
return true;
}
return $pre;
}
/**
* Layer slide remove attachment paths.
*
* Because 'Layer Slide' duplicates an attachment in the Media Library, but uses the same
* file as the original we don't want to remove them from the bucket. Only the backup sizes
* should be removed.
*
* @handles as3cf_remove_attachment_paths
*
* @param array $paths
* @param int $post_id
* @param Media_Library_Item $item
* @param bool $remove_backup_sizes
*
* @return array
*/
public function layer_slide_remove_attachment_paths( $paths, $post_id, Media_Library_Item $item, $remove_backup_sizes ) {
$slider = get_post_meta( $post_id, 'ml-slider_type', true );
if ( 'html_overlay' !== $slider ) {
// Not a layer slide, return.
return $paths;
}
$meta = get_post_meta( $post_id, '_wp_attachment_metadata', true );
unset( $paths[ Media_Library_Item::primary_object_key() ] );
if ( isset( $meta['sizes'] ) ) {
foreach ( $meta['sizes'] as $size => $details ) {
unset( $paths[ $size ] );
}
}
return $paths;
}
/**
* Is layer slide.
*
* @return bool
*/
private function is_layer_slide() {
if ( 'create_html_overlay_slide' === filter_input( INPUT_POST, 'action' ) ) {
return true;
}
return false;
}
/**
* Add post meta
*
* @handles add_post_meta
*
* @param int $object_id
* @param string $meta_key
* @param mixed $_meta_value
*
* @throws Exception
*/
public function add_post_meta( $object_id, $meta_key, $_meta_value ) {
$this->maybe_upload_attachment_backup_sizes( $object_id, $meta_key, $_meta_value );
}
/**
* Update post meta
*
* @handles update_post_meta
*
* @param int $meta_id
* @param int $object_id
* @param string $meta_key
* @param mixed $_meta_value
*
* @throws Exception
*/
public function update_post_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
$this->maybe_upload_attachment_backup_sizes( $object_id, $meta_key, $_meta_value );
}
/**
* Rewrites remote URLs to local when Meta Slider saves HTML layer slides.
*
* @handles sanitize_post_meta_ml-slider_html
*
* @param string $meta_value
*
* @return string
*/
public function sanitize_layer_slider_html( $meta_value ) {
return $this->as3cf->filter_provider->filter_post( $meta_value );
}
/**
* Rewrites remote URLs to local when Meta Slider gets HTML layer slides.
*
* @handles get_post_metadata
*
* @param mixed $check
* @param int $object_id
* @param string $meta_key
* @param mixed $meta_value
*
* @return string
*/
public function filter_get_post_metadata( $check, $object_id, $meta_key, $meta_value ) {
// Exit early if this is not our key to process.
if ( 'ml-slider_html' !== $meta_key ) {
return $check;
}
// We're calling get_metadata recursively and need to make sure
// we never nest deeper than one level.
if ( 0 === $this->get_postmeta_recursion_level ) {
$this->get_postmeta_recursion_level++;
$new_meta_value = get_metadata( 'post', $object_id, $meta_key, true );
$new_meta_value = $this->as3cf->filter_local->filter_post( $new_meta_value );
// Reset recursion.
$this->get_postmeta_recursion_level = 0;
return $new_meta_value;
}
return $check;
}
/**
* Allow meta slider to resize images that have been removed from local
*
* @handles as3cf_get_attached_file
*
* @param string $url
* @param string $file
* @param int $attachment_id
* @param Media_Library_Item $as3cf_item
*
* @return string
*/
public function download_for_resize( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
$action = filter_input( INPUT_POST, 'action' );
if ( ! in_array( $action, array( 'resize_image_slide', 'create_html_overlay_slide' ) ) ) {
return $url;
}
$download_handler = $this->as3cf->get_item_handler( Download_Handler::get_item_handler_key_name() );
$result = $download_handler->handle( $as3cf_item, array( 'full_source_paths' => array( $file ) ) );
if ( empty( $result ) || is_wp_error( $result ) ) {
return $url;
}
return $file;
}
/**
* Maybe upload attachment backup sizes
*
* @param int $object_id
* @param string $meta_key
* @param mixed $data
*
* @throws Exception
*/
private function maybe_upload_attachment_backup_sizes( $object_id, $meta_key, $data ) {
if ( '_wp_attachment_backup_sizes' !== $meta_key ) {
return;
}
if ( 'resize_image_slide' !== filter_input( INPUT_POST, 'action' ) && ! $this->is_layer_slide() ) {
return;
}
if ( ! $this->as3cf->is_plugin_setup( true ) ) {
return;
}
$item = Media_Library_Item::get_by_source_id( $object_id );
if ( ! $item && ! $this->as3cf->get_setting( 'copy-to-s3' ) ) {
// Abort if not already offloaded to provider and the copy setting is off.
return;
}
$this->upload_attachment_backup_sizes( $object_id, $item, $data );
}
/**
* Upload attachment backup sizes
*
* @param int $object_id
* @param Media_Library_Item $as3cf_item
* @param mixed $data
*
* @throws Exception
*/
private function upload_attachment_backup_sizes( $object_id, Media_Library_Item $as3cf_item, $data ) {
foreach ( $data as $key => $file ) {
if ( ! isset( $file['path'] ) ) {
continue;
}
$objects = $as3cf_item->objects();
if ( ! empty( $objects[ $key ] ) ) {
continue;
}
$options = array(
'offloaded_files' => $as3cf_item->offloaded_files(),
);
$objects[ $key ] = array(
'source_file' => wp_basename( $file['path'] ),
'is_private' => false,
);
$as3cf_item->set_objects( $objects );
$upload_handler = $this->as3cf->get_item_handler( Upload_Handler::get_item_handler_key_name() );
$upload_handler->handle( $as3cf_item, $options );
}
}
}