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_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' ) );
if ( $this->as3cf->is_plugin_setup() ) {
$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_action( 'as3cf_pre_upload_attachment', array( $this, 'image_editor_remove_files' ), 10, 3 );
add_filter( 'as3cf_get_attached_file', array( $this, 'image_editor_download_file' ), 10, 4 );
add_filter( 'as3cf_upload_attachment_local_files_to_remove', 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_upload_attachment_local_files_to_remove', array( $this, 'customizer_crop_remove_original_image' ), 10, 3 );
add_filter( 'wp_unique_filename', array( $this, 'customizer_crop_unique_filename' ), 10, 3 );
/*
* Regenerate Thumbnails
* https://wordpress.org/plugins/regenerate-thumbnails/
*/
add_filter( 'as3cf_get_attached_file', array( $this, 'regenerate_thumbnails_download_file' ), 10, 4 );
/*
* WP-CLI Compatibility
*/
if ( defined( 'WP_CLI' ) && class_exists( 'WP_CLI') ) {
WP_CLI::add_hook( 'before_invoke:media regenerate', array( $this, 'enable_get_attached_file_copy_back_to_local' ) );
}
}
/**
* 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 array $s3_object
*
* @return string
*/
function legacy_copy_back_to_local( $url, $file, $attachment_id, $s3_object ) {
$copy_back_to_local = apply_filters( 'as3cf_get_attached_file_copy_back_to_local', false, $file, $attachment_id, $s3_object );
if ( false === $copy_back_to_local ) {
// Not copying back file
return $url;
}
if ( ( $file = $this->copy_s3_file_to_server( $s3_object, $file ) ) ) {
// Return the file if successfully downloaded from S3
return $file;
};
// Return S3 URL as a fallback
return $url;
}
/**
* 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' );
}
/**
* Is this an AJAX process?
*
* @return bool
*/
function is_ajax() {
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return true;
}
return false;
}
/**
* 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
*/
function maybe_process_on_action( $action_key, $ajax, $context_key = null ) {
if ( $ajax !== $this->is_ajax() ) {
return false;
}
$var_type = 'GET';
if ( isset( $_GET['action'] ) ) {
$action = $this->as3cf->filter_input( 'action' );
} else if ( 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 array $s3_object S3 meta data
*
* @return string
*/
function copy_image_to_server_on_action( $action_key, $ajax, $url, $file, $s3_object ) {
if ( false === $this->maybe_process_on_action( $action_key, $ajax ) ) {
return $url;
}
if ( ( $file = $this->copy_s3_file_to_server( $s3_object, $file ) ) ) {
// Return the file if successfully downloaded from S3
return $file;
};
return $url;
}
/**
* Get the file path of the original image file before an update
*
* @param int $post_id
* @param string $file_path
*
* @return bool|string
*/
function get_original_image_file( $post_id, $file_path ) {
// remove original main image after edit
$meta = get_post_meta( $post_id, '_wp_attachment_metadata', true );
$original_file = trailingslashit( dirname( $file_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 $pre
* @param int $post_id
* @param array $data
*
* @return bool
*/
public function image_editor_remove_files( $pre, $post_id, $data ) {
if ( ! isset( $_POST['do'] ) || 'restore' !== $_POST['do'] ) {
return $pre;
}
if ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) {
return $pre;
}
$s3object = $this->as3cf->get_attachment_s3_info( $post_id );
$this->remove_edited_image_files( $post_id, $s3object );
// Update object key with original filename
$restored_filename = wp_basename( $data['file'] );
$old_filename = wp_basename( $s3object['key'] );
$s3object['key'] = str_replace( $old_filename, $restored_filename, $s3object['key'] );
update_post_meta( $post_id, 'amazonS3_info', $s3object );
return true;
}
/**
* Remove edited image files from S3.
*
* @param int $attachment_id
* @param array $s3object
*/
protected function remove_edited_image_files( $attachment_id, $s3object ) {
$bucket = $s3object['bucket'];
$region = $this->as3cf->get_s3object_region( $s3object );
$keys = AS3CF_Utils::get_attachment_edited_keys( $attachment_id, $s3object );
if ( empty( $keys ) ) {
return;
}
$this->as3cf->delete_s3_objects( $region, $bucket, $keys );
}
/**
* Allow the WordPress Image Editor to edit files that have been copied to S3
* but removed from the local server, by copying them back temporarily
*
* @param string $url
* @param string $file
* @param int $attachment_id
* @param array $s3_object
*
* @return string
*/
function image_editor_download_file( $url, $file, $attachment_id, $s3_object ) {
if ( ! $this->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 S3
// via image_editor_remove_files()
if ( isset( $_POST['do'] ) && 'restore' == $_POST['do'] ) {
$backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
$original_filename = $backup_sizes['full-orig']['file'];
$orig_s3 = $s3_object;
$orig_s3['key'] = dirname( $s3_object['key'] ) . '/' . $original_filename;
$orig_file = dirname( $file ) . '/' . $original_filename;
// Copy the original file back to the server for the restore process
$this->copy_s3_file_to_server( $orig_s3, $orig_file );
// Copy the edited file back to the server as well, it will be cleaned up later
if ( $s3_file = $this->copy_s3_file_to_server( $s3_object, $file ) ) {
// Return the file if successfully downloaded from S3
return $s3_file;
};
}
$action = filter_input( INPUT_GET, 'action' ) ?: filter_input( INPUT_POST, 'action' );
if ( in_array( $action, array( 'image-editor', 'imgedit-preview' ) ) ) { // input var okay
foreach ( debug_backtrace() as $caller ) {
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 ( $s3_file = $this->copy_s3_file_to_server( $s3_object, $file ) ) {
// Return the file if successfully downloaded from S3
return $s3_file;
};
}
}
}
return $url;
}
/**
* Allow the WordPress Image Editor to remove the main image file after it has been copied
* back from S3 after it has done the edit.
*
* @param array $files
* @param int $post_id
* @param string $file_path
*
* @return array
*/
function image_editor_remove_original_image( $files, $post_id, $file_path ) {
if ( ! $this->is_ajax() ) {
return $files;
}
if ( isset( $_POST['action'] ) && 'image-editor' === sanitize_key( $_POST['action'] ) ) { // input var okay
// remove original main image after edit
if ( ( $original_file = $this->get_original_image_file( $post_id, $file_path ) ) ) {
$files[] = $original_file;
}
}
return $files;
}
/**
* Generic check for Customizer crop actions
*
* @return bool
*/
protected 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 S3
* but removed from the local server, by copying them back temporarily
*
* @param string $url
* @param string $file
* @param int $attachment_id
* @param array $s3_object
*
* @return string
*/
public function customizer_crop_download_file( $url, $file, $attachment_id, $s3_object ) {
if ( false === $this->is_customizer_crop_action() ) {
return $url;
}
if ( $this->as3cf->attachment_just_uploaded( $attachment_id ) ) {
return $url;
}
if ( ( $file = $this->copy_s3_file_to_server( $s3_object, $file ) ) ) {
// Return the file if successfully downloaded from S3
return $file;
};
return $url;
}
/**
* Allow the WordPress Image Editor to remove the main image file after it has been copied
* back from S3 after it has done the edit.
*
* @param array $files
* @param int $post_id
* @param string $file_path
*
* @return array
*/
function customizer_crop_remove_original_image( $files, $post_id, $file_path ) {
if ( false === $this->is_customizer_crop_action() ) {
return $files;
}
// remove original main image after edit
if ( ( $original_file = $this->get_original_image_file( $_POST['id'], $file_path ) ) ) {
$files[] = $original_file;
}
return $files;
}
/**
* 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;
}
$url = parse_url( $url );
if ( ! isset( $url['path'] ) ) {
return $post_id; // URL path can't be determined
}
$key1 = ltrim( $url['path'], '/' );
$length1 = strlen( $key1 );
// URLs may contain the bucket name within the path, therefore we must
// also perform the search with the first path segment removed
$parts = explode( '/', $key1 );
unset( $parts[0] );
$key2 = implode( '/', $parts );
$length2 = strlen( $key2 );
global $wpdb;
$sql = "
SELECT `post_id`
FROM `{$wpdb->prefix}postmeta`
WHERE `{$wpdb->prefix}postmeta`.`meta_key` = 'amazonS3_info'
AND ( `{$wpdb->prefix}postmeta`.`meta_value` LIKE '%s:3:\"key\";s:{$length1}:\"{$key1}\";%'
OR `{$wpdb->prefix}postmeta`.`meta_value` LIKE '%s:3:\"key\";s:{$length2}:\"{$key2}\";%' )
";
if ( $id = $wpdb->get_var( $sql ) ) {
return $id;
}
return $post_id; // No attachment found on S3
}
/**
* Allow the Regenerate Thumbnails plugin to copy the S3 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 array $s3_object
*
* @return string
*/
function regenerate_thumbnails_download_file( $url, $file, $attachment_id, $s3_object ) {
return $this->copy_image_to_server_on_action( 'regeneratethumbnail', true, $url, $file, $s3_object );
}
/**
* Download a file from S3 if the file does not exist locally and places it where
* the attachment's file should be.
*
* @param array $s3_object
* @param string $file
*
* @return string|bool File if downloaded, false on failure
*/
public function copy_s3_file_to_server( $s3_object, $file ) {
// Make sure the directory exists
$dir = dirname( $file );
if ( ! wp_mkdir_p( $dir ) ) {
$error_message = sprintf( __( 'The local directory %s does not exist and could not be created.', 'amazon-s3-and-cloudfront' ), $dir );
AS3CF_Error::log( sprintf( __( 'There was an error attempting to download the file %s from S3: %s', 'amazon-s3-and-cloudfront' ), $s3_object['key'], $error_message ) );
return false;
}
try {
$this->as3cf->get_s3client( $s3_object['region'], true )->getObject( array(
'Bucket' => $s3_object['bucket'],
'Key' => $s3_object['key'],
'SaveAs' => $file,
) );
} catch ( Exception $e ) {
AS3CF_Error::log( sprintf( __( 'There was an error attempting to download the file %s from S3: %s', 'amazon-s3-and-cloudfront' ), $s3_object['key'], $e->getMessage() ) );
return false;
}
return $file;
}
/**
* Register stream wrappers per region
*
* @param string $region
*
* @return mixed
*/
protected function register_stream_wrapper( $region ) {
$stored_region = ( '' === $region ) ? Amazon_S3_And_CloudFront::DEFAULT_REGION : $region;
if ( in_array( $stored_region, self::$stream_wrappers ) ) {
return;
}
$client = $this->as3cf->get_s3client( $region, true );
$protocol = $this->get_stream_wrapper_protocol( $region );
// Register the region specific S3 stream wrapper to be used by plugins
AS3CF_Stream_Wrapper::register( $client, $protocol );
self::$stream_wrappers[] = $stored_region;
}
/**
* Generate the stream wrapper protocol
*
* @param string $region
*
* @return string
*/
protected function get_stream_wrapper_protocol( $region ) {
$protocol = 's3';
$protocol .= str_replace( '-', '', $region );
return $protocol;
}
/**
* Generate an S3 stream wrapper compatible URL
*
* @param string $bucket
* @param string $key
*
* @return string
*/
function prepare_stream_wrapper_file( $bucket, $region, $key ) {
$protocol = $this->get_stream_wrapper_protocol( $region );
return $protocol . '://' . $bucket . '/' . $key;
}
/**
* Allow access to the S3 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 array $s3_object
*
* @return string
*/
public function get_stream_wrapper_file( $url, $file, $attachment_id, $s3_object ) {
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
$this->register_stream_wrapper( $s3_object['region'] );
return $this->prepare_stream_wrapper_file( $s3_object['bucket'], $s3_object['region'], $s3_object['key'] );
}
/**
* 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;
}
$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( '/
]+?)[\/ ]*>/', '
', $image );
}
return $image;
}
/**
* Alter the image meta data to add srcset support for object versioned S3 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;
}
if ( ! ( $s3object = $this->as3cf->is_attachment_served_by_s3( $attachment_id ) ) ) {
// Attachment not uploaded to S3, abort
return $image_meta;
}
$image_basename = wp_basename( $image_meta['file'] );
if ( false === strpos( $s3object['key'], $image_basename ) ) {
// Not the correct attachment, abort
return $image_meta;
}
// Strip the meta file prefix so the 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'] = rawurlencode( $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'] = rawurlencode( $size['file'] );
return $size;
}, $image_meta['sizes'] );
}
return $image_meta;
}
/**
* Replace local URLs with S3 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;
}
if ( ! ( $s3object = $this->as3cf->is_attachment_served_by_s3( $attachment_id ) ) ) {
// 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 );
$s3_url = $this->as3cf->get_attachment_s3_url( $attachment_id, $s3object, null, $size, $image_meta );
if ( false === $s3_url || is_wp_error( $s3_url ) ) {
// Skip URLs not offloaded to S3
continue;
}
$sources[ $width ]['url'] = $s3_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 === $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 );
$filename = $this->as3cf->filter_unique_filename( $filename, $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 removed or reworked once PHP 5.5 is required.
*/
public function maybe_warn_about_php_version() {
$key_base = 'php-version-55';
if ( version_compare( PHP_VERSION, '5.5', '<' ) ) {
$message = sprintf(
__( 'Warning: This site is using PHP %1$s, in a future update WP Offload S3 will require PHP %2$s or later. %3$s', 'amazon-s3-and-cloudfront' ),
PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
'5.5',
$this->as3cf->more_info_link( '/wp-offload-s3/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' );
}
}
}