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
281 lines
7.9 KiB
PHP
281 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace DeliciousBrains\WP_Offload_Media\Integrations;
|
|
|
|
use AS3CF_Error;
|
|
use AS3CF_Utils;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Remove_Local_Handler;
|
|
use Exception;
|
|
use WP_Error;
|
|
use WP_Post;
|
|
|
|
class Advanced_Custom_Fields extends Integration {
|
|
/**
|
|
* Is installed?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_installed(): bool {
|
|
if ( class_exists( 'acf' ) ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Init integration.
|
|
*/
|
|
public function init() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function setup() {
|
|
/*
|
|
* Content Filtering
|
|
*/
|
|
add_filter( 'acf/load_value/type=text', array( $this->as3cf->filter_local, 'filter_post' ) );
|
|
add_filter( 'acf/load_value/type=textarea', array( $this->as3cf->filter_local, 'filter_post' ) );
|
|
add_filter( 'acf/load_value/type=wysiwyg', array( $this->as3cf->filter_local, 'filter_post' ) );
|
|
add_filter( 'acf/load_value/type=url', array( $this->as3cf->filter_local, 'filter_post' ) );
|
|
add_filter( 'acf/load_value/type=link', array( $this, 'filter_link_local' ) );
|
|
add_filter( 'acf/update_value/type=text', array( $this->as3cf->filter_provider, 'filter_post' ) );
|
|
add_filter( 'acf/update_value/type=textarea', array( $this->as3cf->filter_provider, 'filter_post' ) );
|
|
add_filter( 'acf/update_value/type=wysiwyg', array( $this->as3cf->filter_provider, 'filter_post' ) );
|
|
add_filter( 'acf/update_value/type=url', array( $this->as3cf->filter_provider, 'filter_post' ) );
|
|
add_filter( 'acf/update_value/type=link', array( $this, 'filter_link_provider' ) );
|
|
|
|
/*
|
|
* Image Crop Add-on
|
|
* https://en-gb.wordpress.org/plugins/acf-image-crop-add-on/
|
|
*/
|
|
if ( class_exists( 'acf_field_image_crop' ) ) {
|
|
add_filter( 'wp_get_attachment_metadata', array( $this, 'download_image' ), 10, 2 );
|
|
add_filter( 'sanitize_file_name', array( $this, 'remove_original_after_download' ) );
|
|
}
|
|
|
|
/*
|
|
* Rewrite URLs in field and field group config.
|
|
*/
|
|
add_filter( 'acf/load_fields', array( $this, 'acf_load_config' ) );
|
|
add_filter( 'acf/load_field_group', array( $this, 'acf_load_config' ) );
|
|
|
|
/*
|
|
* Supply missing data.
|
|
*/
|
|
add_filter( 'acf/filesize', array( $this, 'acf_filesize' ), 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* Copy back the S3 image for cropping
|
|
*
|
|
* @param array $data
|
|
* @param int $post_id
|
|
*
|
|
* @return array
|
|
*/
|
|
public function download_image( $data, $post_id ) {
|
|
$this->maybe_download_image( $post_id );
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Copy back the S3 image
|
|
*
|
|
* @param int $post_id
|
|
*
|
|
* @return bool|WP_Error
|
|
*/
|
|
public function maybe_download_image( $post_id ) {
|
|
if ( false === $this->as3cf->plugin_compat->maybe_process_on_action( 'acf_image_crop_perform_crop', true ) ) {
|
|
return $this->as3cf->_throw_error( 1, 'Not ACF crop process' );
|
|
}
|
|
|
|
$file = get_attached_file( $post_id, true );
|
|
|
|
if ( file_exists( $file ) ) {
|
|
return $this->as3cf->_throw_error( 2, 'File already exists' );
|
|
}
|
|
|
|
$as3cf_item = Media_Library_Item::get_by_source_id( $post_id );
|
|
|
|
if ( ! $as3cf_item ) {
|
|
return $this->as3cf->_throw_error( 3, 'Attachment not offloaded' );
|
|
}
|
|
|
|
$callers = debug_backtrace(); // phpcs:ignore
|
|
foreach ( $callers as $caller ) {
|
|
if ( isset( $caller['function'] ) && 'image_downsize' === $caller['function'] ) {
|
|
// Don't copy when downsizing the image, which would result in bringing back
|
|
// the newly cropped image from S3.
|
|
return $this->as3cf->_throw_error( 4, 'Copying back cropped file' );
|
|
}
|
|
}
|
|
|
|
// Copy back the original file for cropping
|
|
$result = $this->as3cf->plugin_compat->copy_provider_file_to_server( $as3cf_item, $file );
|
|
|
|
if ( false === $result ) {
|
|
return $this->as3cf->_throw_error( 5, 'Copy back failed' );
|
|
}
|
|
|
|
// Mark the attachment so we know to remove it later after the crop
|
|
update_post_meta( $post_id, 'as3cf_acf_cropped_to_remove', true );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove the original image downloaded for the cropping after it has been processed
|
|
*
|
|
* @param string $filename
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function remove_original_after_download( $filename ) {
|
|
$this->maybe_remove_original_after_download();
|
|
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* Remove the original image from the server
|
|
*
|
|
* @return bool|WP_Error
|
|
*/
|
|
public function maybe_remove_original_after_download() {
|
|
if ( false === $this->as3cf->plugin_compat->maybe_process_on_action( 'acf_image_crop_perform_crop', true ) ) {
|
|
return $this->as3cf->_throw_error( 1, 'Not ACF crop process' );
|
|
}
|
|
|
|
$original_attachment_id = $this->as3cf->filter_input( 'id', INPUT_POST, FILTER_VALIDATE_INT );
|
|
|
|
if ( ! isset( $original_attachment_id ) ) {
|
|
// Can't find the original attachment id
|
|
return $this->as3cf->_throw_error( 6, 'Attachment ID not available' );
|
|
}
|
|
|
|
$as3cf_item = Media_Library_Item::get_by_source_id( $original_attachment_id );
|
|
|
|
if ( ! $as3cf_item ) {
|
|
// Original attachment not on S3
|
|
return $this->as3cf->_throw_error( 3, 'Attachment not offloaded' );
|
|
}
|
|
|
|
if ( ! get_post_meta( $original_attachment_id, 'as3cf_acf_cropped_to_remove', true ) ) {
|
|
// Original attachment should exist locally, no need to delete
|
|
return $this->as3cf->_throw_error( 7, 'Attachment not to be removed from server' );
|
|
}
|
|
|
|
// Remove the original file from the server
|
|
$original_file = get_attached_file( $original_attachment_id, true );
|
|
|
|
$remove_local_handler = $this->as3cf->get_item_handler( Remove_Local_Handler::get_item_handler_key_name() );
|
|
$remove_local_handler->handle( $as3cf_item, array( 'files_to_remove' => array( $original_file ) ) );
|
|
|
|
// Remove marker
|
|
delete_post_meta( $original_attachment_id, 'as3cf_acf_cropped_to_remove' );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Rewrites URLs from local to remote inside ACF field and field group config. If the
|
|
* rewriting process fails, it will return the original config.
|
|
*
|
|
* @handles acf/load_fields
|
|
* @handles acf/load_field_group
|
|
*
|
|
* @param array $config
|
|
*
|
|
* @return array
|
|
*/
|
|
public function acf_load_config( array $config ): array {
|
|
try {
|
|
$filtered_config = AS3CF_Utils::maybe_unserialize( $this->as3cf->filter_local->filter_post( serialize( $config ) ) );
|
|
} catch ( Exception $e ) {
|
|
AS3CF_Error::log( __METHOD__ . ' ' . $e->getMessage() );
|
|
|
|
return $config;
|
|
}
|
|
|
|
return is_array( $filtered_config ) ? $filtered_config : $config;
|
|
}
|
|
|
|
/**
|
|
* Shortcut ACF's `filesize` call to prevent remote stream wrapper call
|
|
* if attachment offloaded and removed from local and filesize metadata missing.
|
|
*
|
|
* ACF doesn't really use result, so if attachment offloaded and removed
|
|
* but for some reason we too do not have the filesize, returning true
|
|
* satisfies ACF's requirements.
|
|
*
|
|
* @param int|null $filesize The default filesize.
|
|
* @param WP_Post $attachment The attachment post object we're looking for the filesize for.
|
|
*
|
|
* @return int|true
|
|
*/
|
|
public function acf_filesize( $filesize, $attachment ) {
|
|
if ( ! empty( $filesize ) || ! is_a( $attachment, 'WP_Post' ) ) {
|
|
return $filesize;
|
|
}
|
|
|
|
$item = Media_Library_Item::get_by_source_id( $attachment->ID );
|
|
|
|
if ( empty( $item ) ) {
|
|
return $filesize;
|
|
}
|
|
|
|
$filesize = $item->get_filesize();
|
|
|
|
if ( empty( $filesize ) ) {
|
|
return true;
|
|
}
|
|
|
|
return $filesize;
|
|
}
|
|
|
|
/**
|
|
* Filter a link field's URL from local to provider.
|
|
*
|
|
* @param array $link
|
|
*
|
|
* @return array
|
|
*/
|
|
public function filter_link_local( $link ) {
|
|
if ( is_array( $link ) && ! empty( $link['url'] ) ) {
|
|
$url = $this->as3cf->filter_local->filter_post( $link['url'] );
|
|
|
|
if ( ! empty( $url ) ) {
|
|
$link['url'] = $url;
|
|
}
|
|
}
|
|
|
|
return $link;
|
|
}
|
|
|
|
/**
|
|
* Filter a link field's URL from provider to local.
|
|
*
|
|
* @param array $link
|
|
*
|
|
* @return array
|
|
*/
|
|
public function filter_link_provider( $link ) {
|
|
if ( is_array( $link ) && ! empty( $link['url'] ) ) {
|
|
$url = $this->as3cf->filter_provider->filter_post( $link['url'] );
|
|
|
|
if ( ! empty( $url ) ) {
|
|
$link['url'] = $url;
|
|
}
|
|
}
|
|
|
|
return $link;
|
|
}
|
|
}
|