: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use Smush\Core\Controller;
use Smush\Core\File_System;
use Smush\Core\Media\Media_Item;
use Smush\Core\Media\Media_Item_Cache;
class S3_Controller extends Controller {
const AS3CF_GET_ATTACHED_FILE_PRIORITY = - 10;
private $media_item_cache;
* @var WP_Offload_Media_Api
private $wp_offload_media;
public function __construct() {
$this->media_item_cache = Media_Item_Cache::get_instance();
$this->wp_offload_media = new WP_Offload_Media_Api();
$this->logger = Helper::logger()->integrations();
$this->settings = Settings::get_instance();
$this->fs = new File_System();
$this->register_action( 'init', array( $this, 'maybe_initialize' ), - 10 );
public function maybe_initialize() {
$wp_offload_media_active = $this->wp_offload_media_active();
if ( ! $wp_offload_media_active || ! $this->settings->is_s3_active() ) {
// TODO: PNG2Jpg file names should not exist on the server?
// TODO: check whether we need to check is_plugin_setup, remove-local-file and copy-to-s3 settings from wp-offload
$this->support_s3_image_optimization();
$this->support_s3_backup_and_restore();
add_filter( 'wp_smush_media_item_size', array( $this, 'initialize_s3_size' ), 10, 4 );
public function before_restore( $callback, $priority ) {
add_action( 'wp_smush_before_restore_backup', $callback, $priority, 2 );
public function before_restore_attempt( $callback, $priority ) {
add_action( 'wp_smush_before_restore_backup_attempt', $callback, $priority, 1 );
public function after_restore( $callback, $priority ) {
add_action( 'wp_smush_after_restore_backup', $callback, $priority, 3 );
public function disable_s3_auto_download() {
add_filter( 'as3cf_get_attached_file_copy_back_to_local', array( $this, 'return_false' ) );
public function enable_back_s3_auto_download() {
add_filter( 'as3cf_get_attached_file_copy_back_to_local', array( $this, 'return_false' ) );
public function download_all_sizes( $attachment_id ) {
$media_item = $this->media_item_cache->get( $attachment_id );
if ( ! $this->is_media_item_valid( $media_item ) ) {
foreach ( $media_item->get_sizes() as $size ) {
if ( ! is_a( $size, '\Smush\Core\S3\S3_Media_Item_Size' ) ) {
$this->log_error( 'Something went wrong while trying to download the images for Smush.' );
if ( ! $this->fs->file_exists( $size->get_local_path() ) ) {
$this->download_remote_file( $attachment_id, $size->get_local_path() );
public function download_backup_file( $file_path, $attachment_id ) {
if ( ! $this->fs->file_exists( $file_path ) ) {
$this->download_remote_file( $attachment_id, $file_path );
private function download_remote_file( $attachment_id, $file_path ) {
$s3_library_item = $this->get_s3_media_item( $attachment_id );
if ( $s3_library_item ) {
$this->wp_offload_media->copy_provider_file_to_server( $s3_library_item, $file_path );
if ( ! $this->fs->file_exists( $file_path ) ) {
$this->log_error( "Failed to download remote file $attachment_id." );
public function disable_s3_update_attachment( $data ) {
add_filter( 'as3cf_pre_update_attachment_metadata', array( $this, 'return_true' ) );
public function enable_back_s3_update_attachment() {
remove_filter( 'as3cf_pre_update_attachment_metadata', array( $this, 'return_true' ) );
public function return_true() {
public function return_false() {
private function wp_offload_media_active() {
return function_exists( 'as3cf_init' ) || function_exists( 'as3cf_pro_init' );
* @param Media_Item $media_item
private function is_media_item_valid( $media_item ) {
$invalid = ! $media_item || empty( $media_item->get_wp_metadata() );
$this->log_error( 'Media item is not valid.' );
public function disable_s3_get_attached_file_filters() {
// Make sure smush always gets local paths
$this->disable_stream_wrapper_file();
// S3 auto downloads an image when get_attached_file is called, we want to disable this, because we will explicitly download all media item sizes.
$this->disable_s3_auto_download();
// Reset media items, so they have to fetch the new values
$this->media_item_cache->reset_all();
public function enable_back_s3_get_attached_file_filters() {
$this->enable_back_stream_wrapper_file();
$this->enable_back_s3_auto_download();
$this->media_item_cache->reset_all();
private function disable_stream_wrapper_file() {
'as3cf_get_attached_file',
array( $this, 'return_local_file_path' ),
self::AS3CF_GET_ATTACHED_FILE_PRIORITY, // Our callback needs to run before the s3 callback get_stream_wrapper_file
private function enable_back_stream_wrapper_file() {
remove_filter( 'as3cf_get_attached_file', array(
'return_local_file_path',
), self::AS3CF_GET_ATTACHED_FILE_PRIORITY );
public function return_local_file_path( $url, $file_path ) {
* @param callable $callback
private function before_smush( $callback, $priority ) {
add_action( 'wp_smush_before_smush_file', $callback, $priority );
private function before_smush_attempt( $callback, $priority ) {
add_action( 'wp_smush_before_smush_attempt', $callback, $priority );
* @param callable $callback
private function after_smush( $callback, $priority ) {
add_action( 'wp_smush_after_smush_file', $callback, $priority );
public function trigger_update_attachment_metadata( $attachment_id ) {
$media_item = $this->media_item_cache->get( $attachment_id );
if ( ! $this->is_media_item_valid( $media_item ) ) {
wp_update_attachment_metadata( $attachment_id, $media_item->get_wp_metadata() );
* @param $media_item Media_Item
* @return S3_Media_Item_Size
public function initialize_s3_size( $size, $key, $metadata, $media_item ) {
return new S3_Media_Item_Size(
$media_item->get_base_url(),
* @return Media_Library_Item|null
private function get_s3_media_item( $attachment_id ) {
return $this->wp_offload_media->is_attachment_served_by_provider( $attachment_id, true );
private function support_s3_image_optimization() {
* Prevent frequent offloading attempts
// During the optimization we might call wp_update_attachment_metadata multiple times. Prevent any offload attempts while smushing is in progress.
$this->before_smush( array( $this, 'disable_s3_update_attachment' ), 10 );
* Ensure smush has access to local files during optimization
// Download any of the sizes that don't exist locally
$this->before_smush( array( $this, 'download_all_sizes' ), 20 );
* Delete remote version before uploading optimized
// When all optimizations are completed, the new files will be uploaded.
// Note that this is especially important for Png2Jpg optimization for getting rid of the old files from the servers. The new files are nothing like the old ones.
add_action( 'wp_smush_png_jpg_converted', array( $this, 'delete_old_png_files_after_convert' ), 10, 4 );
* Trigger offloading after smush is done
// Turn offloading back on
$this->after_smush( array( $this, 'enable_back_s3_update_attachment' ), 20 );
$this->after_smush( array( $this, 'trigger_update_attachment_metadata' ), 30 );
* Delay offloading on new media upload when auto smush is on
$auto_smush_on = $this->settings->get( 'auto' );
* We need to prevent {@see Media_Library::wp_update_attachment_metadata()} from getting called
// New media upload triggers wp_update_attachment_metadata which triggers offloading. Make sure offloading is postponed until smush is done.
add_filter( 'add_attachment', array( $this, 'disable_s3_update_attachment' ) );
$priority = 100; // This has to be higher than other methods attached to this hook because $media_item->is_skipped() depends on those other methods
$this->after_attachment_upload( array( $this, 'offload_if_media_item_not_optimizable' ), $priority );
$this->before_smush_attempt( array( $this, 'offload_if_media_item_not_optimizable' ), $priority );
private function support_s3_backup_and_restore() {
* Disable remote file filters during the restore process
$this->before_restore_attempt( array( $this, 'disable_s3_get_attached_file_filters' ), 10 );
* Ensure smush has access to local files during restoration
$this->before_restore( array( $this, 'download_backup_file' ), 10 );
$this->before_restore( array( $this, 'disable_s3_update_attachment' ), 20 );
* Delete remote version before uploading restored
// When the restoration is completed, the new files will be uploaded. Again, this is especially important for Png2Jpg
add_action( 'wp_smush_after_restore_png_jpg', array( $this, 'delete_old_jpg_files_after_restore' ), 10, 2 );
* Trigger offloading after restore is done
$this->after_restore( array( $this, 'enable_back_s3_update_attachment' ), 20 );
$this->after_restore( array( $this, 'enable_back_s3_get_attached_file_filters' ), 30 );
$this->after_restore( function ( $restored, $backup_file_path, $attachment_id ) {
$this->wp_offload_media->delete_remote_files( $backup_file_path, $attachment_id );
$this->trigger_update_attachment_metadata( $attachment_id );
private function log_error( $error ) {
$this->logger->error( "Smush S3 Integration: $error" );
private function is_media_item_optimizable( Media_Item $media_item ) {
return ! $this->is_media_item_not_optimizable( $media_item );
* @param Media_Item $media_item
private function is_media_item_not_optimizable( Media_Item $media_item ) {
return ! $media_item->is_valid() || $media_item->has_errors() || $media_item->is_skipped();
public function offload_if_media_item_not_optimizable( $attachment_id ) {
$media_item = $this->media_item_cache->get( $attachment_id );
if ( $this->is_media_item_not_optimizable( $media_item ) ) {
// If there is an error we want the image to be offloaded explicitly
$this->enable_back_s3_update_attachment();
$this->trigger_update_attachment_metadata( $attachment_id );
// We have already added hooks for enabling back and triggering offloading after successful smush.
private function after_attachment_upload( $callback, $priority ) {
add_action( 'wp_smush_after_attachment_upload', $callback, $priority );
public function delete_old_png_files_after_convert( $attachment_id, $image_metadata, $media_item_stats, $png_file_paths ) {
if ( empty( $png_file_paths ) ) {
$this->after_smush( function () use ( $png_file_paths, $attachment_id ) {
$this->wp_offload_media->delete_remote_files( $png_file_paths, $attachment_id );
public function delete_old_jpg_files_after_restore( $media_item, $jpg_file_paths ) {
if ( empty( $jpg_file_paths ) ) {
$attachment_id = $media_item->get_id();
$this->after_restore( function ( $restored ) use ( $jpg_file_paths, $attachment_id ) {
$this->wp_offload_media->delete_remote_files( $jpg_file_paths, $attachment_id );