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
1016 lines
31 KiB
PHP
1016 lines
31 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Compatibility
|
|
*
|
|
* @package amazon-s3-and-cloudfront
|
|
* @subpackage Classes/Plugin-Compatibility
|
|
* @copyright Copyright (c) 2015, Delicious Brains
|
|
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
|
|
* @since 0.8.3
|
|
*/
|
|
|
|
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Download_Handler;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Item;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Remove_Provider_Handler;
|
|
use DeliciousBrains\WP_Offload_Media\Providers\Storage\Storage_Provider;
|
|
|
|
// Exit if accessed directly
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* AS3CF_Plugin_Compatibility Class
|
|
*
|
|
* This class handles compatibility code for third party plugins used in conjunction with AS3CF
|
|
*
|
|
* @since 0.8.3
|
|
*/
|
|
class AS3CF_Plugin_Compatibility {
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected static $stream_wrappers = array();
|
|
|
|
/**
|
|
* @var Amazon_S3_And_CloudFront
|
|
*/
|
|
protected $as3cf;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $wait_for_generate_attachment_metadata = false;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $removed_files = array();
|
|
|
|
/**
|
|
* @param Amazon_S3_And_CloudFront $as3cf
|
|
*/
|
|
function __construct( $as3cf ) {
|
|
$this->as3cf = $as3cf;
|
|
|
|
$this->compatibility_init();
|
|
}
|
|
|
|
/**
|
|
* Register the compatibility hooks for the plugin.
|
|
*/
|
|
function compatibility_init() {
|
|
/*
|
|
* WP_Customize_Control
|
|
* /wp-includes/class-wp-customize_control.php
|
|
*/
|
|
add_filter( 'attachment_url_to_postid', array( $this, 'customizer_background_image' ), 10, 2 );
|
|
|
|
/*
|
|
* Responsive Images WP 4.4
|
|
*/
|
|
add_filter( 'wp_get_attachment_metadata', array( $this, 'wp_get_attachment_metadata' ), 10, 2 );
|
|
add_filter( 'wp_calculate_image_srcset', array( $this, 'wp_calculate_image_srcset' ), 10, 5 );
|
|
add_filter( 'wp_calculate_image_srcset_meta', array( $this, 'wp_calculate_image_srcset_meta' ), 10, 4 );
|
|
|
|
// Maybe warn about PHP version if in admin screens.
|
|
add_action( 'admin_init', array( $this, 'maybe_warn_about_php_version' ) );
|
|
|
|
// WordPress MU Domain Mapping plugin compatibility.
|
|
add_filter( 'as3cf_get_orig_siteurl', array( $this, 'get_orig_siteurl' ) );
|
|
|
|
if ( $this->as3cf->is_plugin_setup( true ) ) {
|
|
$this->compatibility_init_if_setup();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register the compatibility hooks as long as the plugin is setup.
|
|
*/
|
|
function compatibility_init_if_setup() {
|
|
// Turn on stream wrapper S3 file
|
|
add_filter( 'as3cf_get_attached_file', array( $this, 'get_stream_wrapper_file' ), 20, 4 );
|
|
|
|
/*
|
|
* Legacy filter
|
|
* 'as3cf_get_attached_file_copy_back_to_local'
|
|
*/
|
|
add_filter( 'as3cf_get_attached_file', array( $this, 'legacy_copy_back_to_local' ), 10, 4 );
|
|
|
|
/*
|
|
* WP_Image_Editor
|
|
* /wp-includes/class-wp-image-editor.php
|
|
*/
|
|
add_filter( 'as3cf_pre_update_attachment_metadata', array( $this, 'image_editor_remove_files' ), 10, 4 );
|
|
add_filter( 'as3cf_get_attached_file_noop', array( $this, 'image_editor_download_file' ), 10, 4 );
|
|
add_filter( 'as3cf_get_attached_file', array( $this, 'image_editor_download_file' ), 10, 4 );
|
|
add_filter( 'as3cf_remove_local_files', array( $this, 'image_editor_remove_original_image' ), 10, 3 );
|
|
add_filter( 'as3cf_get_attached_file', array( $this, 'customizer_crop_download_file' ), 10, 4 );
|
|
add_filter( 'as3cf_remove_local_files', array( $this, 'customizer_crop_remove_original_image' ), 10, 3 );
|
|
add_filter( 'wp_unique_filename', array( $this, 'customizer_crop_unique_filename' ), 10, 3 );
|
|
|
|
/*
|
|
* Regenerate Thumbnails (before v3)
|
|
* https://wordpress.org/plugins/regenerate-thumbnails/
|
|
*/
|
|
add_filter( 'as3cf_get_attached_file', array( $this, 'regenerate_thumbnails_download_file' ), 10, 4 );
|
|
|
|
/**
|
|
* Regenerate Thumbnails v3+ and other REST-API using plugins that need a local file.
|
|
*/
|
|
add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request_copy_back_to_local' ), 10, 4 );
|
|
add_filter( 'as3cf_wait_for_generate_attachment_metadata', array( $this, 'wait_for_generate_attachment_metadata' ) );
|
|
|
|
/*
|
|
* WP-CLI Compatibility
|
|
*/
|
|
if ( defined( 'WP_CLI' ) && class_exists( 'WP_CLI' ) ) {
|
|
WP_CLI::add_hook( 'before_invoke:media regenerate', array( $this, 'enable_copy_back_and_wait_for_generate_metadata' ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allow any process to trigger the copy back to local with
|
|
* the filter 'as3cf_get_attached_file_copy_back_to_local'
|
|
*
|
|
* @param string $url
|
|
* @param string $file
|
|
* @param int $attachment_id
|
|
* @param Media_Library_Item $as3cf_item
|
|
*
|
|
* @return string
|
|
*/
|
|
function legacy_copy_back_to_local( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
|
|
$copy_back_to_local = apply_filters( 'as3cf_get_attached_file_copy_back_to_local', false, $file, $attachment_id, $as3cf_item );
|
|
if ( false === $copy_back_to_local ) {
|
|
// Not copying back file
|
|
return $url;
|
|
}
|
|
|
|
if ( ( $file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) ) {
|
|
// Return the file if successfully downloaded from S3
|
|
return $file;
|
|
}
|
|
|
|
// Return S3 URL as a fallback
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Enable copying back attachments from provider
|
|
* and waiting for their metadata to be regenerated
|
|
* before re-offloading.
|
|
*
|
|
* @handles WP_CLI:before_invoke:media regenerate
|
|
*/
|
|
public function enable_copy_back_and_wait_for_generate_metadata() {
|
|
$this->enable_get_attached_file_copy_back_to_local();
|
|
$this->wait_for_generate_attachment_metadata = true;
|
|
}
|
|
|
|
/**
|
|
* Enables copying missing local files back to the server when `get_attached_file` filter is called.
|
|
*/
|
|
public function enable_get_attached_file_copy_back_to_local() {
|
|
add_filter( 'as3cf_get_attached_file_copy_back_to_local', '__return_true' );
|
|
|
|
// Monitor any files that are subsequently removed.
|
|
add_filter( 'as3cf_upload_attachment_local_files_to_remove', array(
|
|
$this,
|
|
'monitor_local_files_to_remove',
|
|
), 10, 3 );
|
|
|
|
// Prevent subsequent attempts to copy back after upload and remove.
|
|
add_filter( 'as3cf_get_attached_file_copy_back_to_local', array(
|
|
$this,
|
|
'prevent_copy_back_to_local_after_remove',
|
|
), 10, 4 );
|
|
|
|
add_filter( 'wp_generate_attachment_metadata', array( $this, 'wp_generate_attachment_metadata' ) );
|
|
}
|
|
|
|
/**
|
|
* Keeps track of local files that are removed after upload.
|
|
*
|
|
* @param array $files_to_remove
|
|
* @param integer $post_id
|
|
* @param string $file_path
|
|
*
|
|
* @return array
|
|
*/
|
|
public function monitor_local_files_to_remove( $files_to_remove, $post_id, $file_path ) {
|
|
$this->removed_files = array_merge( $this->removed_files, $files_to_remove );
|
|
|
|
return $files_to_remove;
|
|
}
|
|
|
|
/**
|
|
* Prevent subsequent attempts to copy back after upload and remove.
|
|
*
|
|
* @param bool $copy_back_to_local
|
|
* @param string $file
|
|
* @param integer $attachment_id
|
|
* @param Media_Library_Item $as3cf_item
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function prevent_copy_back_to_local_after_remove( $copy_back_to_local, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
|
|
if ( $copy_back_to_local && in_array( $file, $this->removed_files ) ) {
|
|
$copy_back_to_local = false;
|
|
}
|
|
|
|
return $copy_back_to_local;
|
|
}
|
|
|
|
/**
|
|
* Handler for wp_generate_attachment_metadata. Updates class
|
|
* member variable when the filter has fired.
|
|
*
|
|
* @handles wp_generate_attachment_metadata
|
|
*
|
|
* @param mixed $metadata
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function wp_generate_attachment_metadata( $metadata ) {
|
|
$this->wait_for_generate_attachment_metadata = false;
|
|
|
|
return $metadata;
|
|
}
|
|
|
|
/**
|
|
* Are we waiting for the wp_generate_attachment_metadata filter and
|
|
* if so, has it run yet?
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* Check the current request is a specific one based on action and
|
|
* optional context
|
|
*
|
|
* @param string $action_key
|
|
* @param bool $ajax
|
|
* @param null|string|array $context_key
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function maybe_process_on_action( $action_key, $ajax, $context_key = null ) {
|
|
if ( $ajax !== AS3CF_Utils::is_ajax() ) {
|
|
return false;
|
|
}
|
|
|
|
$var_type = 'GET';
|
|
|
|
if ( isset( $_GET['action'] ) ) {
|
|
$action = $this->as3cf->filter_input( 'action' );
|
|
} elseif ( isset( $_POST['action'] ) ) {
|
|
$var_type = 'POST';
|
|
$action = $this->as3cf->filter_input( 'action', INPUT_POST );
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
$context_check = true;
|
|
if ( ! is_null( $context_key ) ) {
|
|
$global = constant( 'INPUT_' . $var_type );
|
|
$context = $this->as3cf->filter_input( 'context', $global );
|
|
|
|
if ( is_array( $context_key ) ) {
|
|
$context_check = in_array( $context, $context_key );
|
|
} else {
|
|
$context_check = ( $context_key === $context );
|
|
}
|
|
}
|
|
|
|
return ( $action_key === sanitize_key( $action ) && $context_check );
|
|
}
|
|
|
|
/**
|
|
* Generic method for copying back an S3 file to the server on a specific AJAX action
|
|
*
|
|
* @param string $action_key Action that must be in process
|
|
* @param bool $ajax Must the process be an AJAX one?
|
|
* @param string $url S3 URL
|
|
* @param string $file Local file path of image
|
|
* @param Media_Library_Item $as3cf_item S3 meta data
|
|
*
|
|
* @return string
|
|
*/
|
|
function copy_image_to_server_on_action( $action_key, $ajax, $url, $file, Media_Library_Item $as3cf_item ) {
|
|
if ( false === $this->maybe_process_on_action( $action_key, $ajax ) ) {
|
|
return $url;
|
|
}
|
|
|
|
if ( ( $file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) ) {
|
|
// Return the file if successfully downloaded from S3
|
|
return $file;
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get the file path of the primary image file if it exists.
|
|
*
|
|
* This helper function looks at the current metadata for the Media Library item.
|
|
* In various scenarios this is useful when an item's offloaded objects
|
|
* and the attachment's metadata are not yet in sync.
|
|
*
|
|
* @param Item $as3cf_item
|
|
*
|
|
* @return bool|string
|
|
*/
|
|
private function get_original_image_file( Item $as3cf_item ) {
|
|
if ( Media_Library_Item::source_type() !== $as3cf_item->source_type() ) {
|
|
return false;
|
|
}
|
|
|
|
$meta = get_post_meta( $as3cf_item->source_id(), '_wp_attachment_metadata', true );
|
|
$original_file = trailingslashit( dirname( $as3cf_item->full_source_path() ) ) . wp_basename( $meta['file'] );
|
|
|
|
if ( file_exists( $original_file ) ) {
|
|
return $original_file;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Allow the WordPress Image Editor to remove edited version of images
|
|
* if the original image is being restored and 'IMAGE_EDIT_OVERWRITE' is set
|
|
*
|
|
* @param bool $cancel True if the upload should be cancelled
|
|
* @param array $data Array describing the object being uploaded
|
|
* @param int $post_id Attachment's ID
|
|
* @param Media_Library_Item $as3cf_item The Media Library Item object if previously offloaded
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function image_editor_remove_files( $cancel, $data, $post_id, $as3cf_item ) {
|
|
if ( ! isset( $_POST['do'] ) || 'restore' !== $_POST['do'] ) {
|
|
return $cancel;
|
|
}
|
|
|
|
if ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) {
|
|
return $cancel;
|
|
}
|
|
|
|
if ( empty( $as3cf_item ) ) {
|
|
return $cancel;
|
|
}
|
|
|
|
$keys_to_remove = array();
|
|
$pattern = '/-e[0-9]{13}(?:-[0-9]{1,4}x[0-9]{1,4})?\./';
|
|
$objects = $as3cf_item->objects();
|
|
foreach ( $objects as $object_key => $object ) {
|
|
if ( preg_match( $pattern, $object['source_file'] ) ) {
|
|
$keys_to_remove[] = $object_key;
|
|
unset( $objects[ $object_key ] );
|
|
}
|
|
}
|
|
|
|
$remove_provider_handler = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() );
|
|
$remove_provider_handler->handle( $as3cf_item, array( 'object_keys' => $keys_to_remove ) );
|
|
// TODO: Check these following statements are required.
|
|
$as3cf_item->set_objects( $objects );
|
|
$as3cf_item->save();
|
|
|
|
return $cancel;
|
|
}
|
|
|
|
/**
|
|
* Allow the WordPress Image Editor to edit files that have been copied to provider
|
|
* but removed from the local server, by copying them back temporarily
|
|
*
|
|
* @param string $url
|
|
* @param string $file
|
|
* @param int $attachment_id
|
|
* @param Media_Library_Item $as3cf_item
|
|
*
|
|
* @return string
|
|
*/
|
|
public function image_editor_download_file( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
|
|
if ( ! AS3CF_Utils::is_ajax() ) {
|
|
return $url;
|
|
}
|
|
|
|
// When the image-editor restores the original it requests the edited image,
|
|
// but we actually need to copy back the original image at this point
|
|
// for the restore to be successful and edited images to be deleted from the bucket
|
|
// via image_editor_remove_files()
|
|
if ( isset( $_POST['do'] ) && 'restore' == $_POST['do'] ) {
|
|
$objects = $as3cf_item->objects();
|
|
if ( isset( $objects['full-orig'] ) ) {
|
|
// Copy the original file back to the server for the restore process
|
|
$this->copy_provider_file_to_server( $as3cf_item, $objects['full-orig']['source_file'] );
|
|
}
|
|
|
|
// Copy the edited file back to the server as well, it will be cleaned up later
|
|
if ( $provider_file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) {
|
|
// Return the file if successfully downloaded from bucket.
|
|
return $provider_file;
|
|
}
|
|
}
|
|
|
|
$action = filter_input( INPUT_GET, 'action' );
|
|
$action = $action ? $action : filter_input( INPUT_POST, 'action' );
|
|
|
|
if ( in_array( $action, array( 'image-editor', 'imgedit-preview' ) ) ) { // input var okay
|
|
foreach ( debug_backtrace() as $caller ) { // phpcs:ignore
|
|
if ( isset( $caller['function'] ) && '_load_image_to_edit_path' == $caller['function'] ) {
|
|
// check this has been called by '_load_image_to_edit_path' so as only to copy back once
|
|
if ( $provider_file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) {
|
|
// Return the file if successfully downloaded from bucket.
|
|
return $provider_file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Allow the WordPress Image Editor to remove the main image file after it has been copied
|
|
* back from the bucket after it has done the edit.
|
|
*
|
|
* @param array $files_to_remove
|
|
* @param Item $as3cf_item
|
|
* @param array $item_source
|
|
*
|
|
* @return array
|
|
*/
|
|
public function image_editor_remove_original_image( $files_to_remove, $as3cf_item, $item_source ) {
|
|
if ( ! AS3CF_Utils::is_ajax() ) {
|
|
return $files_to_remove;
|
|
}
|
|
|
|
if ( isset( $_POST['action'] ) && 'image-editor' === sanitize_key( $_POST['action'] ) ) { // input var okay
|
|
if ( ( $original_file = $this->get_original_image_file( $as3cf_item ) ) ) {
|
|
$files_to_remove[] = $original_file;
|
|
}
|
|
}
|
|
|
|
return $files_to_remove;
|
|
}
|
|
|
|
/**
|
|
* Generic check for Customizer crop actions
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_customizer_crop_action() {
|
|
$header_crop = $this->maybe_process_on_action( 'custom-header-crop', true );
|
|
|
|
$context = array( 'site-icon', 'custom_logo' );
|
|
$image_crop = $this->maybe_process_on_action( 'crop-image', true, $context );
|
|
|
|
if ( ! $header_crop && ! $image_crop ) {
|
|
// Not doing a Customizer action
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Allow the WordPress Customizer to crop images that have been copied to bucket
|
|
* but removed from the local server, by copying them back temporarily.
|
|
*
|
|
* @param string $url
|
|
* @param string $file
|
|
* @param int $attachment_id
|
|
* @param Media_Library_Item $as3cf_item
|
|
*
|
|
* @return string
|
|
*/
|
|
public function customizer_crop_download_file( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
|
|
if ( false === $this->is_customizer_crop_action() ) {
|
|
return $url;
|
|
}
|
|
|
|
/** @var Media_Library $media_library */
|
|
$media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
|
|
if ( $media_library->item_just_uploaded( $attachment_id ) ) {
|
|
return $url;
|
|
}
|
|
|
|
if ( ( $file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) ) {
|
|
// Return the file if successfully downloaded from bucket.
|
|
return $file;
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Allow the WordPress Image Editor to remove the main image file after it has been copied
|
|
* back from the bucket after it has done the edit.
|
|
*
|
|
* @param array $files_to_remove
|
|
* @param Item $as3cf_item
|
|
* @param array $item_source
|
|
*
|
|
* @return array
|
|
*/
|
|
function customizer_crop_remove_original_image( $files_to_remove, $as3cf_item, $item_source ) {
|
|
if ( false === $this->is_customizer_crop_action() ) {
|
|
return $files_to_remove;
|
|
}
|
|
|
|
if ( ( $original_file = $this->get_original_image_file( $as3cf_item ) ) ) {
|
|
$files_to_remove[] = $original_file;
|
|
}
|
|
|
|
return $files_to_remove;
|
|
}
|
|
|
|
/**
|
|
* Show the correct background image in the customizer
|
|
*
|
|
* @param int|null $post_id
|
|
* @param string $url
|
|
*
|
|
* @return int|null
|
|
*/
|
|
function customizer_background_image( $post_id, $url ) {
|
|
if ( ! is_null( $post_id ) ) {
|
|
return $post_id;
|
|
}
|
|
|
|
// There seems to be a bug in the WP Customizer whereby sometimes it puts the attachment ID on the URL.
|
|
if ( is_numeric( $url ) ) {
|
|
$as3cf_item = Media_Library_Item::get_by_source_id( $url );
|
|
|
|
// If we found an offloaded Media Library item for that ID, job's a good'n'.
|
|
if ( $as3cf_item ) {
|
|
$post_id = $url;
|
|
}
|
|
} else {
|
|
/** @var Media_Library $media_library */
|
|
$media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
|
|
$post_id = $media_library->get_attachment_id_from_provider_url( $url );
|
|
}
|
|
|
|
// Must return null if not found.
|
|
if ( empty( $post_id ) ) {
|
|
return null;
|
|
} else {
|
|
return $post_id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allow the Regenerate Thumbnails plugin to copy the bucket 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
|
|
*/
|
|
function regenerate_thumbnails_download_file( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
|
|
return $this->copy_image_to_server_on_action( 'regeneratethumbnail', true, $url, $file, $as3cf_item );
|
|
}
|
|
|
|
/**
|
|
* Download a file from bucket if the file does not exist locally and places it where
|
|
* the attachment's file should be.
|
|
*
|
|
* @param Media_Library_Item $as3cf_item
|
|
* @param string $file
|
|
*
|
|
* @return string|bool File if downloaded, false on failure
|
|
*/
|
|
public function copy_provider_file_to_server( Media_Library_Item $as3cf_item, $file ) {
|
|
/** @var Download_Handler $download_handler */
|
|
$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 false;
|
|
}
|
|
|
|
return $file;
|
|
}
|
|
|
|
/**
|
|
* Register stream wrappers per region
|
|
*
|
|
* @param string $region
|
|
*
|
|
* @return Storage_Provider|null
|
|
* @throws Exception
|
|
*/
|
|
protected function register_stream_wrapper( $region ) {
|
|
$stored_region = ( '' === $region ) ? $this->as3cf->get_default_region() : $region;
|
|
|
|
if ( ! empty( self::$stream_wrappers[ $stored_region ] ) ) {
|
|
return self::$stream_wrappers[ $stored_region ];
|
|
}
|
|
|
|
$client = $this->as3cf->get_provider_client( $region, true );
|
|
|
|
if ( ! empty( $client ) && $client->register_stream_wrapper( $region ) ) {
|
|
self::$stream_wrappers[ $stored_region ] = $client;
|
|
|
|
return $client;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Allow access to the remote file via the stream wrapper.
|
|
* This is useful for compatibility with plugins when attachments are removed from the local server after upload.
|
|
*
|
|
* @param string $url
|
|
* @param string $file
|
|
* @param int $attachment_id
|
|
* @param Media_Library_Item $as3cf_item
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function get_stream_wrapper_file( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
|
|
if ( $url === $file ) {
|
|
// Abort if an earlier hook to get the file has been called and it has been copied back.
|
|
return $file;
|
|
}
|
|
|
|
// Make sure the region stream wrapper is registered.
|
|
$client = $this->register_stream_wrapper( $as3cf_item->region() );
|
|
|
|
if ( ! empty( $client ) ) {
|
|
return $client->prepare_stream_wrapper_file( $as3cf_item->region(), $as3cf_item->bucket(), $as3cf_item->key() );
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Fixes comparison of attachment metadata to already urlencoded content during 'the_content' filter.
|
|
*
|
|
* @param array $data
|
|
* @param int $attachment_id
|
|
*
|
|
* @return array
|
|
*/
|
|
public function wp_get_attachment_metadata( $data, $attachment_id ) {
|
|
global $wp_current_filter;
|
|
|
|
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
|
|
if (
|
|
is_array( $wp_current_filter ) &&
|
|
! empty( $wp_current_filter[0] ) &&
|
|
'the_content' === $wp_current_filter[0] &&
|
|
! empty( $as3cf_item ) &&
|
|
$as3cf_item->served_by_provider( $attachment_id )
|
|
) {
|
|
// Ensure each filename is encoded the same way as URL, slightly fixed up for wp_basename() manipulation compatibility.
|
|
if ( ! empty( $data['file'] ) ) {
|
|
$data['file'] = AS3CF_Utils::encode_filename_in_path( $data['file'] );
|
|
}
|
|
|
|
if ( ! empty( $data['sizes'] ) ) {
|
|
$data['sizes'] = array_map( function ( $size ) {
|
|
$size['file'] = AS3CF_Utils::encode_filename_in_path( $size['file'] );
|
|
|
|
return $size;
|
|
}, $data['sizes'] );
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Adds 'srcset' and 'sizes' attributes to an existing S3 'img' element.
|
|
*
|
|
* @param string $image An HTML 'img' element to be filtered.
|
|
* @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
|
|
* @param int $attachment_id Image attachment ID.
|
|
*
|
|
* @return string Converted 'img' element with 'srcset' and 'sizes' attributes added.
|
|
*/
|
|
public function wp_image_add_srcset_and_sizes( $image, $image_meta, $attachment_id ) {
|
|
// Ensure the image meta exists.
|
|
if ( empty( $image_meta['sizes'] ) ) {
|
|
return $image;
|
|
}
|
|
|
|
if ( ! is_string( $image ) ) {
|
|
return $image;
|
|
}
|
|
|
|
$image_src = preg_match( '/src="([^"]+)"/', $image, $match_src ) ? $match_src[1] : '';
|
|
|
|
list( $image_src ) = explode( '?', $image_src );
|
|
|
|
// Return early if we couldn't get the image source.
|
|
if ( ! $image_src ) {
|
|
return $image;
|
|
}
|
|
|
|
// Bail early if an image has been inserted and later edited.
|
|
if ( preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) && strpos( wp_basename( $image_src ), $img_edit_hash[0] ) === false ) {
|
|
|
|
return $image;
|
|
}
|
|
|
|
$width = preg_match( '/ width="([0-9]+)"/', $image, $match_width ) ? (int) $match_width[1] : 0;
|
|
$height = preg_match( '/ height="([0-9]+)"/', $image, $match_height ) ? (int) $match_height[1] : 0;
|
|
|
|
if ( ! $width || ! $height ) {
|
|
/*
|
|
* If attempts to parse the size value failed, attempt to use the image meta data to match
|
|
* the image file name from 'src' against the available sizes for an attachment.
|
|
*/
|
|
$image_filename = wp_basename( $image_src );
|
|
|
|
if ( $image_filename === wp_basename( $image_meta['file'] ) ) {
|
|
$width = (int) $image_meta['width'];
|
|
$height = (int) $image_meta['height'];
|
|
} else {
|
|
foreach ( $image_meta['sizes'] as $image_size_data ) {
|
|
if ( $image_filename === $image_size_data['file'] ) {
|
|
$width = (int) $image_size_data['width'];
|
|
$height = (int) $image_size_data['height'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! $width || ! $height ) {
|
|
return $image;
|
|
}
|
|
|
|
$size_array = array( $width, $height );
|
|
$srcset = wp_calculate_image_srcset( $size_array, $image_src, $image_meta, $attachment_id );
|
|
$sizes = false;
|
|
|
|
if ( $srcset ) {
|
|
// Check if there is already a 'sizes' attribute.
|
|
$sizes = strpos( $image, ' sizes=' );
|
|
|
|
if ( ! $sizes ) {
|
|
$sizes = wp_calculate_image_sizes( $size_array, $image_src, $image_meta, $attachment_id );
|
|
}
|
|
}
|
|
|
|
if ( $srcset && $sizes ) {
|
|
// Format the 'srcset' and 'sizes' string and escape attributes.
|
|
$attr = sprintf( ' srcset="%s"', esc_attr( $srcset ) );
|
|
|
|
if ( is_string( $sizes ) ) {
|
|
$attr .= sprintf( ' sizes="%s"', esc_attr( $sizes ) );
|
|
}
|
|
|
|
// Add 'srcset' and 'sizes' attributes to the image markup.
|
|
$image = preg_replace( '/<img ([^>]+?)[\/ ]*>/', '<img $1' . $attr . ' />', $image );
|
|
}
|
|
|
|
return $image;
|
|
}
|
|
|
|
/**
|
|
* Alter the image meta data to add srcset support for object versioned provider URLs.
|
|
*
|
|
* @param array $image_meta
|
|
* @param array $size_array
|
|
* @param string $image_src
|
|
* @param int $attachment_id
|
|
*
|
|
* @return array
|
|
*/
|
|
public function wp_calculate_image_srcset_meta( $image_meta, $size_array, $image_src, $attachment_id ) {
|
|
if ( empty( $image_meta['file'] ) ) {
|
|
// Corrupt `_wp_attachment_metadata`
|
|
return $image_meta;
|
|
}
|
|
|
|
if ( false !== strpos( $image_src, $image_meta['file'] ) ) {
|
|
// Path matches URL, no need to change
|
|
return $image_meta;
|
|
}
|
|
|
|
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
|
|
if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) {
|
|
// Attachment not uploaded to S3, abort
|
|
return $image_meta;
|
|
}
|
|
|
|
$image_basename = AS3CF_Utils::encode_filename_in_path( wp_basename( $image_meta['file'] ) );
|
|
|
|
if ( false === strpos( AS3CF_Utils::encode_filename_in_path( $as3cf_item->path() ), $image_basename ) ) {
|
|
// Not the correct attachment, abort
|
|
return $image_meta;
|
|
}
|
|
|
|
// Strip the meta file prefix so that just the filename will always match
|
|
// the S3 URL regardless of different prefixes for the offloaded file.
|
|
// Also ensure filename is encoded the same way as URL.
|
|
$image_meta['file'] = $image_basename;
|
|
|
|
// Ensure each size filename is encoded the same way as URL.
|
|
if ( ! empty( $image_meta['sizes'] ) ) {
|
|
$image_meta['sizes'] = array_map( function ( $size ) {
|
|
$size['file'] = AS3CF_Utils::encode_filename_in_path( $size['file'] );
|
|
|
|
return $size;
|
|
}, $image_meta['sizes'] );
|
|
}
|
|
|
|
return $image_meta;
|
|
}
|
|
|
|
/**
|
|
* Replace local URLs with provider ones for srcset image sources.
|
|
*
|
|
* @param array $sources
|
|
* @param array $size_array
|
|
* @param string $image_src
|
|
* @param array $image_meta
|
|
* @param int $attachment_id
|
|
*
|
|
* @return array
|
|
*/
|
|
public function wp_calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
|
|
if ( ! is_array( $sources ) ) {
|
|
// Sources corrupt
|
|
return $sources;
|
|
}
|
|
|
|
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
|
|
if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) {
|
|
// Attachment not uploaded to S3, abort
|
|
return $sources;
|
|
}
|
|
|
|
foreach ( $sources as $width => $source ) {
|
|
$filename = wp_basename( $source['url'] );
|
|
$size = $this->find_image_size_from_width( $image_meta['sizes'], $width, $filename );
|
|
$provider_url = $as3cf_item->get_provider_url( $size );
|
|
|
|
if ( false === $provider_url || is_wp_error( $provider_url ) ) {
|
|
// Skip URLs not offloaded to S3
|
|
continue;
|
|
}
|
|
|
|
$sources[ $width ]['url'] = $provider_url;
|
|
}
|
|
|
|
return $sources;
|
|
}
|
|
|
|
/**
|
|
* Helper function to find size name from width and filename
|
|
*
|
|
* @param array $sizes
|
|
* @param string $width
|
|
* @param string $filename
|
|
*
|
|
* @return null|string
|
|
*/
|
|
protected function find_image_size_from_width( $sizes, $width, $filename ) {
|
|
foreach ( $sizes as $name => $size ) {
|
|
if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
|
|
return $name;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Filters the result when generating a unique file name for a customizer crop.
|
|
*
|
|
* @param string $filename Unique file name.
|
|
* @param string $ext File extension, eg. ".png".
|
|
* @param string $dir Directory path.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function customizer_crop_unique_filename( $filename, $ext, $dir ) {
|
|
if ( false === $this->is_customizer_crop_action() ) {
|
|
return $filename;
|
|
}
|
|
|
|
// Get parent Post ID for cropped image.
|
|
$post_id = filter_input( INPUT_POST, 'id', FILTER_VALIDATE_INT );
|
|
|
|
/** @var Media_Library $media_library */
|
|
$media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
|
|
$filename = $media_library->filter_unique_filename( $filename, $ext, $dir, $post_id );
|
|
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* Display an admin message if PHP version is soon to be unsupported by plugin.
|
|
*
|
|
* NOTE: This is not added to AWS SDK compatibility checks as it is remaining compatible with earlier PHP versions.
|
|
*
|
|
* This function should be reworked when the minimum supported version of PHP
|
|
* is planned to be increased in a subsequent release.
|
|
*/
|
|
public function maybe_warn_about_php_version() {
|
|
$key_base = 'php-version-72';
|
|
|
|
if ( version_compare( PHP_VERSION, '7.2', '<' ) ) {
|
|
$message = sprintf(
|
|
__( '<strong>Warning:</strong> This site is using PHP %1$s, in a future update WP Offload Media will require PHP %2$s or later. %3$s', 'amazon-s3-and-cloudfront' ),
|
|
PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
|
|
'7.2',
|
|
$this->as3cf->more_info_link( '/wp-offload-media/doc/php-version-requirements/', 'upgrade-php-version' )
|
|
);
|
|
|
|
// Show notice to user if not already dismissed.
|
|
$args = array(
|
|
'custom_id' => $key_base . '-site',
|
|
'type' => 'notice-warning',
|
|
'flash' => false,
|
|
'only_show_to_user' => false,
|
|
);
|
|
|
|
if ( ! in_array( $args['custom_id'], $this->as3cf->notices->get_dismissed_notices() ) ) {
|
|
$this->as3cf->notices->add_notice( $message, $args );
|
|
} else {
|
|
// If user has dismissed site-wide notice but we're in settings pages, show notice.
|
|
$args['custom_id'] = $key_base . '-settings';
|
|
$args['dismissible'] = false;
|
|
$args['only_show_in_settings'] = true;
|
|
|
|
$this->as3cf->notices->add_notice( $message, $args );
|
|
}
|
|
} else {
|
|
// If PHP version (now) OK, make sure notices not shown.
|
|
$this->as3cf->notices->remove_notice_by_id( $key_base . '-site' );
|
|
$this->as3cf->notices->remove_notice_by_id( $key_base . '-settings' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filters the REST dispatch request to determine whether route needs compatibility actions.
|
|
*
|
|
* @param bool $dispatch_result Dispatch result, will be used if not empty.
|
|
* @param WP_REST_Request $request Request used to generate the response.
|
|
* @param string $route Route matched for the request.
|
|
* @param array $handler Route handler used for the request.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function rest_dispatch_request_copy_back_to_local( $dispatch_result, $request, $route, $handler ) {
|
|
$routes = array(
|
|
'/regenerate-thumbnails/v\d+/regenerate/',
|
|
);
|
|
|
|
$routes = apply_filters( 'as3cf_rest_api_enable_get_attached_file_copy_back_to_local', $routes );
|
|
$routes = is_array( $routes ) ? $routes : (array) $routes;
|
|
|
|
if ( ! empty( $routes ) ) {
|
|
foreach ( $routes as $match_route ) {
|
|
if ( preg_match( '@' . $match_route . '@i', $route ) ) {
|
|
$this->enable_copy_back_and_wait_for_generate_metadata();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $dispatch_result;
|
|
}
|
|
|
|
/**
|
|
* Domain Mapping may have overridden the original siteurl that is needed for search/replace.
|
|
*
|
|
* @param string $siteurl
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function get_orig_siteurl( $siteurl ) {
|
|
if ( defined( 'DOMAIN_MAPPING' ) && function_exists( 'get_original_url' ) ) {
|
|
$siteurl = get_original_url( 'siteurl' );
|
|
}
|
|
|
|
return $siteurl;
|
|
}
|
|
}
|