add_filter( 'wp_get_attachment_url', [ self::$instance, 'get_image_attachment_url' ], - 999, 2 );
add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_uploaded_images' ] );
self::$instance->add_new_actions();
add_action( 'delete_attachment', [ self::$instance, 'delete_attachment_hook' ], 10 );
add_filter( 'handle_bulk_actions-upload', [ self::$instance, 'bulk_action_handler' ], 10, 3 );
add_filter( 'bulk_actions-upload', [ self::$instance, 'register_bulk_media_actions' ] );
add_filter( 'media_row_actions', [ self::$instance, 'add_inline_media_action' ], 10, 2 );
add_filter( 'wp_calculate_image_srcset', [ self::$instance, 'calculate_image_srcset' ], 1, 5 );
add_action( 'post_updated', [ self::$instance, 'update_offload_meta' ], 10, 3 );
// Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
$below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
if ( $below_6_0_0 ) {
add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert_legacy' ], 10, 3 );
} else {
add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 );
}
add_action( 'optml_start_processing_images', [ self::$instance, 'start_processing_images' ], 10, 5 );
add_action(
'optml_start_processing_images_by_id',
[
self::$instance,
'start_processing_images_by_id',
],
10,
4
);
add_action( 'init', [ self::$instance, 'maybe_reschedule' ] );
if ( self::$is_legacy_install === null ) {
self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
}
}
}
return self::$instance;
}
/**
* Reschedule the transfer cron in case is missing or was lost.
*
* @return void
*/
public function maybe_reschedule() {
// If this is in pending, we do nothing.
if ( self::is_scheduled( 'optml_start_processing_images' ) ) {
return;
}
// If there is no transfer in progress, we do nothing.
if ( self::$instance->settings->get( 'transfer_status' ) === 'disabled' ) {
return;
}
$transfer_type = self::$instance->settings->get( 'transfer_status' );
$in_progress = self::$instance->settings->get( 'rollback_images' === $transfer_type ? 'rollback_status' : 'offloading_status' ) !== 'disabled';
// We check if there is an in progress transfer.
if ( ! $in_progress ) {
return;
}
self::$instance->logger->add_log( $transfer_type, 'Cron missed, attempt to reschedule.' );
self::get_image_count( $transfer_type, false );
}
/**
* Function for `update_attached_file` filter-hook.
*
* @param string $file Path to the attached file to update.
* @param int $attachment_id Attachment ID.
*
* @return string
*/
public function wp_update_attached_file_filter( $file, $attachment_id ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'called updated attached' );
}
$info = pathinfo( $file );
$file_name = basename( $file );
$no_ext_file_name = basename( $file, '.' . $info['extension'] );
// if we have current deduplication set and it contains the filename that is updated
// we replace the updated filename with the deduplicated filename
if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) {
$file = str_replace( $file_name, self::$current_file_deduplication, $file );
// we need to store the filename we replaced to check when uploading the image if it was deduplicated
self::$last_deduplicated = $file_name;
self::$current_file_deduplication = false;
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', self::$last_deduplicated );
}
remove_filter( 'update_attached_file', [ self::$instance, 'wp_update_attached_file_filter' ], 10 );
return $file;
}
/**
* Function for `wp_insert_attachment_data` filter-hook.
* Because we remove the images when new images are added the wp deduplication using the files will not work
* To overcome this we hook the attachment data when it's added to the database and we use the post name (slug) which is unique against the database
* For creating a unique quid by replacing the filename with the slug inside the existing guid
* This will ensure the guid is unique and the next step will be to make sure the attached_file meta for the image is also unique
* For this we will hook `update_attached_file` filter which is called after the data is inserted and there we will make sure we replace the filename
* with the deduplicated one which we stored into `$current_file_deduplication` variable
*
* @param array $data An array of slashed, sanitized, and processed attachment post data.
* @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
* @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
* @param bool $update Whether this is an existing attachment post being updated.
*
* @return array
* @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0.
*/
public function insert( $data, $postarr, $unsanitized_postarr, $update ) {
// the post name is unique against the database so not affected by removing the files
// https://developer.wordpress.org/reference/functions/wp_unique_post_slug/
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'data before' );
do_action( 'optml_log', $data );
}
if ( empty( $data['guid'] ) ) {
return $data;
}
$filename = wp_basename( $data['guid'] );
$ext = $this->get_ext( $filename );
// skip if the file is not an image
if ( ! isset( Optml_Config::$all_extensions[ $ext ] ) && ! in_array( $ext, [ 'jpg', 'jpeg', 'jpe' ], true ) ) {
return $data;
}
// on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg`
// we remove that as it is redundant for the file name deduplication we are using it
$sanitized_post_name = str_replace( '-' . $ext, '', $data['post_name'] );
// with the wp deduplication working the post_title is identical to the post_name
// so when they are different it means we need to deduplicate using the post_name
if ( ! empty( $data['post_name'] ) && $data['post_title'] !== $sanitized_post_name ) {
// we append the extension to the post_name to create a filename
// and use it to replace the filename in the guid
$no_ext_filename = str_replace( '.' . $ext, '', $filename );
$no_ext_filename_sanitized = sanitize_title( $no_ext_filename );
// get the deduplication addition from the database post_name
$diff = str_replace( strtolower( $no_ext_filename_sanitized ), '', $sanitized_post_name );
// create the deduplicated filename
$to_replace_with = $no_ext_filename . $diff . '.' . $ext;
$data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] );
// we store the deduplication to be used and add the filter for updating the attached_file meta
self::$current_file_deduplication = $to_replace_with;
add_filter( 'update_attached_file', [ self::$instance, 'wp_update_attached_file_filter' ], 10, 2 );
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'data after' );
do_action( 'optml_log', $data );
}
return $data;
}
/**
* Wrapper for the `insert` method for WP versions < 6.0.0.
*
* @param array $data An array of slashed, sanitized, and processed attachment post data.
* @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
* @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
*
* @return array
*/
public function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
return $this->insert( $data, $postarr, $unsanitized_postarr, false );
}
/**
* Update offload meta when the page is updated.
*
* @param int $post_ID Updated post id.
* @param WP_Post $post_after Post before the update.
* @param WP_Post $post_before Post after the update.
*
* @return void
* @uses action:post_updated
*/
public function update_offload_meta( $post_ID, $post_after, $post_before ) {
if ( self::$offload_update_post === true ) {
return;
}
if ( get_post_type( $post_ID ) === 'attachment' ) {
return;
}
// revisions are skipped inside the function no need to check them before
delete_post_meta( $post_ID, self::POST_ROLLBACK_FLAG );
}
/**
* Get image size name from width and meta.
*
* @param array $sizes Image sizes .
* @param integer $width Size width.
* @param string $filename Image filename.
*
* @return null|string|array
*/
public static function get_image_size_from_width( $sizes, $width, $filename, $just_name = true ) {
foreach ( $sizes as $name => $size ) {
if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
return $just_name ? $name : array_merge( $size, [ 'name' => $name ] );
}
}
return null;
}
/**
* Replace image URLs in the srcset attributes.
*
* @param array $sources Array of image sources.
* @param array $size_array Array of width and height values in pixels (in that order).
* @param string $image_src The 'src' of the image.
* @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
* @param int $attachment_id Image attachment ID.
*
* @return array
*/
public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
if ( ! is_array( $sources ) ) {
return $sources;
}
if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
if ( ! Optml_Media_Offload::is_uploaded_image( $image_src ) || ! isset( $image_meta['file'] ) || ! Optml_Media_Offload::is_uploaded_image( $image_meta['file'] ) ) {
return $sources;
}
foreach ( $sources as $width => $source ) {
$filename = wp_basename( $image_meta['file'] );
$size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename );
$optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
if ( false === $optimized_url ) {
continue;
}
$sources[ $width ]['url'] = $optimized_url[0];
}
return $sources;
}
if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
return $sources;
}
$requested_width = $size_array[0];
$requested_height = $size_array[1];
if ( $requested_height < 1 || $requested_width < 1 ) {
return $sources;
}
$requested_ratio = $requested_width / $requested_height;
$image_sizes = $this->get_all_image_sizes();
$crop = false;
// Loop through image sizes to make sure we're using the right cropping.
foreach ( $image_sizes as $size_name => $args ) {
if ( $args['width'] !== $requested_width && $args['height'] !== $requested_height ) {
continue;
}
if ( isset( $args['crop'] ) ) {
$crop = (bool) $args['crop'];
}
}
foreach ( $sources as $width => $source ) {
$filename = ( $image_meta['file'] );
$size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename, false );
if ( $size === null || ! isset( $size['name'] ) ) {
unset( $sources[ $width ] );
continue;
}
if ( ! isset( $image_sizes[ $size['name'] ] ) || (bool) $image_sizes[ $size['name'] ]['crop'] !== $crop ) {
unset( $sources[ $width ] );
continue;
}
// Some image sizes might have 0 values for width or height.
if ( $size['width'] < 1 || $size['height'] < 1 ) {
unset( $sources[ $width ] );
continue;
}
$size_ratio = $size['width'] / $size['height'];
// We need a srcset with the same aspect ratio.
// Otherwise, we'll display different images on different devices.
if ( $requested_ratio !== $size_ratio ) {
unset( $sources[ $width ] );
continue;
}
$optimized_url = wp_get_attachment_image_src( $attachment_id, $size['name'] );
if ( false === $optimized_url ) {
unset( $sources[ $width ] );
continue;
}
$sources[ $width ]['url'] = $optimized_url[0];
}
// Add the requested size to the srcset.
$sources[ $requested_width ] = [
'url' => $image_src,
'descriptor' => 'w',
'value' => $requested_width,
];
return $sources;
}
/**
* Check if the image is stored on our servers or not.
*
* @param string $src Image src or url.
*
* @return bool Whether image is upload or not.
*/
public static function is_not_processed_image( $src ) {
return strpos( $src, self::KEYS['not_processed_flag'] ) !== false;
}
/**
* Check if the image is stored on our servers or not.
*
* @param string $src Image src or url.
*
* @return bool Whether image is upload or not.
*/
public static function is_uploaded_image( $src ) {
return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false;
}
/**
* Get the attachment ID from the image tag.
*
* @param string $image Image tag.
*
* @return int|false
*/
public function get_id_from_tag( $image ) {
$attachment_id = false;
if ( preg_match( '#class=["|\']?[^"\']*(wp-image-|wp-video-)([\d]+)[^"\']*["|\']?#i', $image, $found ) ) {
$attachment_id = intval( $found[2] );
}
return $attachment_id;
}
/**
* Get attachment id from url
*
* @param string $url The optimized url .
*
* @return false|mixed The attachment id .
*/
public static function get_attachment_id_from_url( $url ) {
preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
return isset( $attachment_id[1] ) ? $attachment_id[1] : false;
}
/**
* Get attachment id from local url
*
* @param string $url The url to look for.
*
* @return array The attachment id and the size from the url.
*/
public function get_local_attachement_id_from_url( $url ) {
$size = 'full';
$found_size = $this->parse_dimensions_from_filename( $url );
$url = $this->add_schema( $url );
if ( $found_size[0] !== false && $found_size[1] !== false ) {
$size = $found_size;
}
$url = $this->add_schema( $url );
$attachment_id = $this->attachment_url_to_post_id( $url );
return [ 'attachment_id' => $attachment_id, 'size' => $size ];
}
/**
* Filter out the urls that are saved to our servers when saving to the DB.
*
* @param array $data The post data array to save.
*
* @return array
* @uses filter:wp_insert_post_data
*/
public function filter_uploaded_images( $data ) {
$content = trim( wp_unslash( $data['post_content'] ) );
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'content to update' );
do_action( 'optml_log', $content );
}
$images = Optml_Manager::instance()->extract_urls_from_content( $content );
if ( ! isset( $images[0] ) ) {
return $data;
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'images to update' );
do_action( 'optml_log', $images );
}
foreach ( $images as $url ) {
$is_original_uploaded = self::is_uploaded_image( $url );
$attachment_id = false;
$size = 'thumbnail';
if ( $is_original_uploaded ) {
$found_size = $this->parse_dimension_from_optimized_url( $url );
if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) {
$size = $found_size;
}
$attachment_id = self::get_attachment_id_from_url( $url );
} else {
$id_and_size = $this->get_local_attachement_id_from_url( $url );
$attachment_id = $id_and_size['attachment_id'];
$size = $id_and_size['size'];
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'image id and found size' );
do_action( 'optml_log', $attachment_id );
do_action( 'optml_log', $size );
}
if ( false === $attachment_id || ! $this->is_legacy_offloaded_attachment( $attachment_id ) || ! wp_attachment_is_image( $attachment_id ) ) {
continue;
}
$optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' image url to replace with ' );
do_action( 'optml_log', $optimized_url );
}
if ( ! isset( $optimized_url[0] ) ) {
continue;
}
if ( $is_original_uploaded === self::is_uploaded_image( $optimized_url[0] ) ) {
continue;
}
$content = str_replace( $url, $optimized_url[0], $content );
}
$data['post_content'] = wp_slash( $content );
return $data;
}
/**
* Get all images that need to be updated from a post.
*
* @param string $post_content The content of the post.
* @param string $job The job name.
*
* @return array An array containing the image ids.
*/
public function get_image_id_from_content( $post_content, $job ) {
$content = trim( wp_unslash( $post_content ) );
$images = Optml_Manager::instance()->extract_urls_from_content( $content );
$found_images = [];
if ( isset( $images[0] ) ) {
foreach ( $images as $url ) {
$is_original_uploaded = self::is_uploaded_image( $url );
$attachment_id = false;
if ( $is_original_uploaded ) {
if ( $job === 'rollback_images' ) {
$attachment_id = self::get_attachment_id_from_url( $url );
}
} else {
if ( $job === 'offload_images' ) {
$id_and_size = $this->get_local_attachement_id_from_url( $url );
$attachment_id = $id_and_size['attachment_id'];
}
}
if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) {
continue;
}
$found_images[] = intval( $attachment_id );
}
}
return apply_filters( 'optml_content_images_to_update', $found_images, $content );
}
/**
* Get the posts ids and the images from them that need sync/rollback.
*
* @param int $page The current page from the query.
* @param string $job The job name rollback_images/offload_images.
* @param int $batch How many posts to query on a page.
* @param array $page_in The pages that need to be updated.
*
* @return array An array containing the page of the query and an array containing the images for every post that need to be updated.
*/
public function update_content( $page, $job, $batch = 1, $page_in = [] ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' updating_content ' );
}
$post_types = array_values(
array_filter(
get_post_types(),
function ( $post_type ) {
if ( $post_type === 'attachment' || $post_type === 'revision' ) {
return false;
}
return true;
}
)
);
$query_args = apply_filters(
'optml_replacement_wp_query_args',
[
'post_type' => $post_types,
'post_status' => 'any',
'fields' => 'ids',
'posts_per_page' => $batch,
'update_post_meta_cache' => true,
'update_post_term_cache' => false,
]
);
$query_args = self::add_page_meta_query_args( $job, $query_args );
if ( ! empty( $page_in ) ) {
$query_args['post__in'] = $page_in;
}
$content = new \WP_Query( $query_args );
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', $page );
}
$images_to_update = [];
if ( $content->have_posts() ) {
while ( $content->have_posts() ) {
$content->the_post();
$content_id = get_the_ID();
if ( get_post_type() !== 'attachment' ) {
$ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job );
if ( count( $ids ) > 0 ) {
$images_to_update[ $content_id ] = $ids;
$duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) {
foreach ( $duplicated_pages as $duplicated_id ) {
$duplicated_ids = $this->get_image_id_from_content( get_post_field( 'post_content', $duplicated_id ), $job );
$images_to_update[ $duplicated_id ] = $duplicated_ids;
}
}
}
if ( $job === 'offload_images' ) {
update_post_meta( $content_id, self::POST_OFFLOADED_FLAG, 'true' );
delete_post_meta( $content_id, self::POST_ROLLBACK_FLAG );
}
if ( $job === 'rollback_images' ) {
update_post_meta( $content_id, self::POST_ROLLBACK_FLAG, 'true' );
delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG );
}
}
}
++$page;
}
$result['page'] = $page;
$result['imagesToUpdate'] = $images_to_update;
return $result;
}
/**
* Add inline action to push to our servers.
*
* @param array $actions All actions.
* @param \WP_Post $post The current post image object.
*
* @return array
*/
public function add_inline_media_action( $actions, $post ) {
$meta = wp_get_attachment_metadata( $post->ID );
if ( ! isset( $meta['file'] ) ) {
return $actions;
}
$file = $meta['file'];
if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) {
return $actions;
}
if ( ! self::is_uploaded_image( $file ) ) {
$upload_action_url = add_query_arg(
[
'page' => 'optimole',
'optimole_action' => 'offload_images',
'0' => $post->ID,
],
'admin.php'
);
$actions['offload_images'] = sprintf(
'%s',
$upload_action_url,
esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
esc_html__( 'Offload to Optimole', 'optimole-wp' )
);
}
if ( self::is_uploaded_image( $file ) ) {
$rollback_action_url = add_query_arg(
[
'page' => 'optimole',
'optimole_action' => 'rollback_images',
'0' => $post->ID,
],
'admin.php'
);
$actions['rollback_images'] = sprintf(
'%s',
$rollback_action_url,
esc_attr__( 'Restore image to media library', 'optimole-wp' ),
esc_html__( 'Restore image to media library', 'optimole-wp' )
);
}
return $actions;
}
/**
* Upload images to our servers and update inside pages.
*
* @param array $image_ids The id of the attachments for the selected images.
*
* @return int The number of successfully processed images.
*/
public function upload_and_update_existing_images( $image_ids ) {
$success_up = 0;
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' images to upload ' );
do_action( 'optml_log', $image_ids );
}
foreach ( $image_ids as $id ) {
if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) {
// if this meta flag below failed at the initial update but the file meta above is updated it will cause an infinite query loop
update_post_meta( $id, self::META_KEYS['offloaded'], 'true' );
update_post_meta( $id, self::OM_OFFLOADED_FLAG, true );
++$success_up;
continue;
}
$meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id );
if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) {
++$success_up;
wp_update_attachment_metadata( $id, $meta );
}
}
if ( $success_up > 0 ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' call post update, succesful images: ' );
do_action( 'optml_log', $success_up );
}
}
return $success_up;
}
/**
* Return the original url of an image attachment.
*
* @param integer $post_id Image attachment id.
*
* @return string|bool The original url of the image.
*/
public static function get_original_url( $post_id ) {
self::$return_original_url = true;
$original_url = wp_get_attachment_url( $post_id );
self::$return_original_url = false;
return $original_url;
}
/**
* Bring images back to media library and update inside pages.
*
* @param array $image_ids The id of the attachments for the selected images.
*
* @return int The number of successfully processed images.
*/
public function rollback_and_update_images( $image_ids ) {
$success_back = 0;
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' images to rollback ' );
do_action( 'optml_log', $image_ids );
}
foreach ( $image_ids as $id ) {
// Skip DAM attachment filtering.
if ( $this->is_dam_imported_image( $id ) ) {
continue;
}
$current_meta = wp_get_attachment_metadata( $id );
if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) {
delete_post_meta( $id, self::META_KEYS['offloaded'] );
delete_post_meta( $id, self::OM_OFFLOADED_FLAG );
++$success_back;
continue;
}
// Account for scaled images.
$source_file = isset( $current_meta['original_image'] ) ? $current_meta['original_image'] : $current_meta['file']; // @phpstan-ignore-line - this exists for scaled images.
$filename = pathinfo( $source_file, PATHINFO_BASENAME );
$image_id = preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $matches ) ? $matches[1] : null;
if ( null === $image_id ) {
continue;
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' image cloud id ' );
do_action( 'optml_log', $image_id );
}
$image_url = Optimole::offload()->getImageUrl( $image_id );
if ( null === $image_url ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' error get url' );
}
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
continue;
}
if ( ! function_exists( 'download_url' ) ) {
include_once ABSPATH . 'wp-admin/includes/file.php';
}
if ( ! function_exists( 'download_url' ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
continue;
}
$timeout_seconds = 60;
$temp_file = download_url( $image_url, $timeout_seconds );
if ( is_wp_error( $temp_file ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' download_url error ' );
}
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error downloading URL.' );
continue;
}
$extension = $this->get_ext( $filename );
if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' image has invalid extension' );
do_action( 'optml_log', $extension );
}
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has invalid extension.' );
continue;
}
$type = Optml_Config::$image_extensions [ $extension ];
$file = [
'name' => $filename,
'type' => $type,
'tmp_name' => $temp_file,
'error' => 0,
'size' => filesize( $temp_file ),
];
$overrides = [
// do not expect the default form data from normal uploads
'test_form' => false,
// Setting this to false lets WordPress allow empty files, not recommended.
'test_size' => true,
// A properly uploaded file will pass this test. There should be no reason to override this one.
'test_upload' => true,
];
if ( ! function_exists( 'wp_handle_sideload' ) ) {
include_once ABSPATH . '/wp-admin/includes/file.php';
}
if ( ! function_exists( 'wp_handle_sideload' ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
continue;
}
// Move the temporary file into the uploads directory.
$results = wp_handle_sideload( $file, $overrides, get_the_date( 'Y/m', $id ) );
if ( ! empty( $results['error'] ) ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' wp_handle_sideload error' );
}
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' faced wp_handle_sideload error.' );
continue;
}
if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
include_once ABSPATH . '/wp-admin/includes/image.php';
}
if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
continue;
}
$original_meta = wp_create_image_subsizes( $results['file'], $id );
if ( $type === 'image/svg+xml' ) {
if ( ! function_exists( 'wp_get_attachment_metadata' ) || ! function_exists( 'wp_update_attachment_metadata' ) ) {
include_once ABSPATH . '/wp-admin/includes/post.php';
}
if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
continue;
}
$meta = wp_get_attachment_metadata( $id );
if ( ! isset( $meta['file'] ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
continue;
}
$meta['file'] = $results['file'];
wp_update_attachment_metadata( $id, $meta );
}
if ( ! function_exists( 'update_attached_file' ) ) {
include_once ABSPATH . '/wp-admin/includes/post.php';
}
if ( ! function_exists( 'update_attached_file' ) ) {
update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
continue;
}
update_attached_file( $id, $results['file'] );
$duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id );
if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
foreach ( $duplicated_images as $duplicated_id ) {
$duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
$duplicated_meta['file'] = $results['file'];
if ( isset( $meta ) ) {
foreach ( $meta['sizes'] as $key => $value ) {
if ( isset( $original_meta['sizes'][ $key ]['file'] ) ) {
$duplicated_meta['sizes'][ $key ]['file'] = $original_meta['sizes'][ $key ]['file'];
}
}
}
wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
delete_post_meta( $duplicated_id, self::META_KEYS['offloaded'] );
delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
}
}
}
++$success_back;
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
$original_url = self::get_original_url( $id );
if ( $original_url === false ) {
continue;
}
$this->delete_attachment_from_server( $original_url, $id, $image_id );
}
if ( $success_back > 0 ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', ' call update post, success rollback' );
do_action( 'optml_log', $success_back );
}
}
return $success_back;
}
/**
* Handle the bulk actions.
*
* @param string $redirect The current url from the media library.
* @param string $doaction The current action selected.
* @param array $image_ids The id of the attachments for the selected images.
*
* @return string The url with the correspondent query args for the executed actions.
*/
public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
if ( empty( $image_ids ) ) {
return $redirect;
}
$image_ids = array_slice( $image_ids, 0, 20, true );
$redirect = 'admin.php';
$redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
$redirect = add_query_arg( 'page', 'optimole', $redirect );
$redirect = add_query_arg( $image_ids, $redirect );
return $redirect;
}
/**
* Register the bulk media actions.
*
* @param array $bulk_array The existing actions array.
*
* @return array The array with the appended actions.
*/
public function register_bulk_media_actions( $bulk_array ) {
$bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
$bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
return $bulk_array;
}
/**
* Send delete request to our servers and update the meta.
*
* @param string $original_url Original url of the image.
* @param integer $post_id Image id inside db.
* @param string $image_id Our cloud id for the image.
*/
public function delete_attachment_from_server( $original_url, $post_id, $image_id ) {
Optimole::offload()->deleteImage( $image_id );
delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
}
/**
* Delete an image from our servers after it is removed from media.
*
* @param int $post_id The deleted post id.
*/
public function delete_attachment_hook( $post_id ) {
$file = wp_get_attachment_metadata( $post_id );
if ( $file === false ) {
return;
}
// Skip if the image was imported from cloud library.
if ( $this->is_dam_imported_image( $post_id ) ) {
return;
}
if ( ! $this->is_new_offloaded_attachment( $post_id ) && ! $this->is_legacy_offloaded_attachment( $post_id ) ) {
return;
}
$file = $file['file'];
if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
$original_url = self::get_original_url( $post_id );
if ( $original_url === false ) {
return;
}
$table_id = [];
preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $file, $table_id );
if ( ! isset( $table_id[1] ) ) {
return;
}
$this->delete_attachment_from_server( $original_url, $post_id, $table_id[1] );
}
}
/**
* Get optimized URL for an attachment image if it is uploaded to our servers.
*
* @param string $url The current url.
* @param int $attachment_id The attachment image id.
*
* @return string Optimole cdn URL.
* @uses filter:wp_get_attachment_url
*/
public function get_image_attachment_url( $url, $attachment_id ) {
if ( self::$return_original_url === true ) {
return $url;
}
if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
$meta = wp_get_attachment_metadata( $attachment_id );
if ( ! isset( $meta['file'] ) ) {
return $url;
}
// Skip DAM attachment filtering.
if ( $this->is_dam_imported_image( $attachment_id ) ) {
return $url;
}
$file = $meta['file'];
if ( self::is_uploaded_image( $file ) ) {
return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
} else {
// this is for the users that already offloaded the images before the other fixes
$local_file = get_attached_file( $attachment_id );
if ( ! file_exists( $local_file ) ) {
$duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
foreach ( $duplicated_images as $id ) {
if ( ! empty( $id ) ) {
$duplicated_meta = wp_get_attachment_metadata( $id );
if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
}
}
}
}
}
}
return $url;
}
if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
return $url;
}
return $this->get_new_offloaded_attachment_url( $url, $attachment_id );
}
/**
* Filter the requested image url.
*
* @param bool|array $image The previous image value (null).
* @param int $attachment_id The ID of the attachment.
* @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
*
* @return bool|array The image sizes and optimized url.
* @uses filter:image_downsize
*/
public function generate_filter_downsize_urls( $image, $attachment_id, $size ) {
if ( $this->is_dam_imported_image( $attachment_id ) ) {
return $image;
}
if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
if ( self::$return_original_url === true ) {
return $image;
}
$sizes2crop = self::size_to_crop();
if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
return $image;
}
$data = image_get_intermediate_size( $attachment_id, $size );
if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) {
return $image;
}
$resize = apply_filters( 'optml_default_crop', [] );
if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
$resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
}
$id_filename = [];
preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename );
if ( ! isset( $id_filename[1] ) ) {
return $image;
}
$url = self::get_original_url( $attachment_id );
return [
str_replace( $url, $id_filename[1], $this->get_optimized_image_url( $url, $data['width'], $data['height'], $resize ) ),
$data['width'],
$data['height'],
true,
];
}
if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
return $image;
}
return $this->alter_attachment_image_src( $image, $attachment_id, $size, false );
}
/**
* Get image extension.
*
* @param string $path Image path.
*
* @return string
*/
private function get_ext( $path ) {
return pathinfo( $path, PATHINFO_EXTENSION );
}
/**
* Mark an image as having a retryable error.
*
* @param int $attachment_id The attachment ID.
* @param string $reason The reason for the error.
*/
public static function mark_retryable_error( $attachment_id, $reason ) {
static $allowed_retries = 5;
$retries = get_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER, true );
$retries = empty( $retries ) ? 0 : (int) $retries;
if ( $retries >= $allowed_retries ) {
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' ' . $reason . '. Reached the maximum number of retries.' );
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
return;
}
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' ' . $reason . '. Marked for retry, retries done: ' . $retries );
update_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER, ( $retries + 1 ) );
}
/**
* Update image meta with optimized cdn path.
*
* @param array $meta Meta information of the image.
* @param int $attachment_id The image attachment ID.
*
* @return array
* @uses filter:wp_generate_attachment_metadata
*/
public function generate_image_meta( $meta, $attachment_id ) {
if ( $this->is_dam_imported_image( $attachment_id ) ) {
return $meta;
}
if ( self::$instance->settings->is_offload_limit_reached() ) {
return $meta;
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'called generate meta' );
}
// No meta, or image was already uploaded.
if ( ! isset( $meta['file'] ) || ! isset( $meta['width'] ) || ! isset( $meta['height'] ) || self::is_uploaded_image( $meta['file'] ) ) {
do_action( 'optml_log', 'invalid meta' );
do_action( 'optml_log', $meta );
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
return $meta;
}
// Skip images based on filters.
if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
do_action( 'optml_log', 'optimization filter' );
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
return $meta;
}
$original_url = self::get_original_url( $attachment_id );
// Could not find original URL.
if ( $original_url === false ) {
do_action( 'optml_log', 'error getting original url' );
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
return $meta;
}
// We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
$original_url = $this->maybe_strip_scaled( $original_url );
$local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
$extension = $this->get_ext( $local_file );
$content_type = Optml_Config::$image_extensions [ $extension ];
$temp = explode( '/', $local_file );
$file_name = end( $temp );
$no_ext_filename = str_replace( '.' . $extension, '', $file_name );
$original_name = $file_name;
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'file before replace' );
do_action( 'optml_log', $local_file );
}
// check if the current filename is the last deduplicated filename
if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
// replace the file with the original before deduplication to get the path where the image is uploaded
$local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
$original_name = self::$last_deduplicated;
self::$last_deduplicated = false;
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'file after replace' );
do_action( 'optml_log', $local_file );
}
if ( ! file_exists( $local_file ) ) {
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
do_action( 'optml_log', 'missing file' );
do_action( 'optml_log', $local_file );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
return $meta;
}
if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
do_action( 'optml_log', 'invalid extension' );
do_action( 'optml_log', $extension );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
return $meta;
}
if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
do_action( 'optml_log', 'extension filter' );
do_action( 'optml_log', $extension );
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
return $meta;
}
$offload_manager = Optimole::offload();
$offload_usage = $offload_manager->getUsage();
$current_run = self::get_process_meta();
$remaining = isset( $current_run['remaining'] ) ? absint( $current_run['remaining'] ) : 0;
if ( $remaining + $offload_usage->getCurrent() >= $offload_usage->getLimit() ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'limit exceeded' );
do_action( 'optml_log', $offload_usage );
}
self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: offloading images would exceed limit.' );
return $meta;
}
try {
$image_id = $offload_manager->uploadImage( $local_file, $original_url );
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'image id' );
do_action( 'optml_log', $image_id );
}
// We clear the retry counter if we reach this point.
delete_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER );
} catch ( InvalidArgumentException $exception ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'invalid argument exception' );
do_action( 'optml_log', $exception );
}
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' file is missing or unreadable.' );
return $meta;
} catch ( InvalidUploadApiResponseException $exception ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'missing table id or upload url' );
do_action( 'optml_log', $exception );
}
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
return $meta;
} catch ( UploadFailedException $exception ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'upload error' );
do_action( 'optml_log', $exception );
}
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
return $meta;
} catch ( UploadLimitException $exception ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'limit exceeded' );
do_action( 'optml_log', $exception );
}
self::$instance->settings->update( 'offload_limit', $exception->getUsage()->getLimit() );
self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: upload limit exceeded' );
return $meta;
} catch ( UploadApiException $exception ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'upload api error' );
do_action( 'optml_log', $exception );
}
self::mark_retryable_error( $attachment_id, 'Image ID: ' . $attachment_id . ' has an error from upload api:' . $exception->getMessage() );
return $meta;
} catch ( RuntimeException $exception ) {
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'runtime exception' );
do_action( 'optml_log', $exception );
}
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has an issue.' );
return $meta;
}
$url_to_append = $original_url;
$url_parts = parse_url( $original_url );
if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
$url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
}
$optimized_url = $this->get_media_optimized_url( $url_to_append, $image_id );
if ( ( new Optml_Api() )->check_optimized_url( $optimized_url ) === false ) {
do_action( 'optml_log', 'optimization error' );
do_action( 'optml_log', $optimized_url );
Optimole::offload()->deleteImage( $image_id );
update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
return $meta;
}
@unlink( $local_file );
update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
$meta['file'] = '/' . self::KEYS['uploaded_flag'] . $image_id . '/' . $url_to_append;
if ( isset( $meta['sizes'] ) ) {
foreach ( $meta['sizes'] as $key => $value ) {
$generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
$meta['sizes'][ $key ]['file'] = $file_name;
}
}
// This is needed for scaled images.
// Otherwise, `-scaled` images will be left behind.
if ( isset( $meta['original_image'] ) ) {
$ext = $this->get_ext( $local_file );
$scaled_path = str_replace( '.' . $ext, '-scaled.' . $ext, $local_file );
file_exists( $scaled_path ) && unlink( $scaled_path );
}
$duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
foreach ( $duplicated_images as $duplicated_id ) {
$duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
if ( isset( $duplicated_meta['file'] ) && ! self::is_uploaded_image( $duplicated_meta['file'] ) ) {
$duplicated_meta['file'] = $meta['file'];
if ( $duplicated_meta['sizes'] ) {
foreach ( $meta['sizes'] as $key => $value ) {
$duplicated_meta['sizes'][ $key ]['file'] = $file_name;
}
}
wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
update_post_meta( $duplicated_id, self::META_KEYS['offloaded'], 'true' );
update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
}
}
}
if ( OPTML_DEBUG_MEDIA ) {
do_action( 'optml_log', 'success offload' );
}
self::decrement_process_meta_remaining();
self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' );
$attachment_page_id = wp_get_post_parent_id( $attachment_id );
if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
self::$offload_update_post = true;
update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
self::$offload_update_post = false;
}
return $meta;
}
/**
* Get the args for wp query according to the scope.
*
* @param int $batch Number of images to get.
* @param string $action The action for which to get the images.
*
* @return array|false The query options array or false if not passed a valid action.
*/
public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
$args = [
'posts_per_page' => $batch,
'fields' => 'ids',
'ignore_sticky_posts' => false,
'no_found_rows' => true,
];
if ( $get_images === true ) {
$args['post_type'] = 'attachment';
$args['post_mime_type'] = 'image';
$args['post_status'] = 'inherit';
// Offload args.
if ( $action === 'offload_images' ) {
$args['meta_query'] = [
'relation' => 'AND',
[
'key' => self::META_KEYS['offloaded'],
'compare' => 'NOT EXISTS',
],
[
'key' => self::META_KEYS['offload_error'],
'compare' => 'NOT EXISTS',
],
];
return $args;
}
// Rollback args.
$args['meta_query'] = [
'relation' => 'AND',
[
'key' => self::META_KEYS['offloaded'],
'value' => 'true',
'compare' => '=',
],
[
'key' => self::META_KEYS['rollback_error'],
'compare' => 'NOT EXISTS',
],
[
'key' => Optml_Dam::OM_DAM_IMPORTED_FLAG,
'compare' => 'NOT EXISTS',
],
];
return $args;
}
$args = self::add_page_meta_query_args( $action, $args );
$post_types = array_filter(
get_post_types(),
function ( $post_type ) {
if ( $post_type === 'attachment' || $post_type === 'revision' ) {
return false;
}
return true;
}
);
$args ['post_type'] = array_values( $post_types );
return $args;
}
/**
* Query the database and upload images to our servers.
*
* @param int $batch Number of images to process in a batch.
*
* @return array Number of found images and number of successfully processed images.
*/
public function upload_images( $batch, $images = [] ) {
self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
if ( empty( $images ) || $images === 'none' ) {
$args = self::get_images_or_pages_query_args( $batch, 'offload_images', true );
$attachments = new \WP_Query( $args );
$ids = $attachments->get_posts();
} else {
$ids = array_slice( $images, 0, $batch );
}
$result = [ 'found_images' => count( $ids ) ];
$result['success_offload'] = $this->upload_and_update_existing_images( $ids );
return $result;
}
/**
* Query the database and bring back image to media library.
*
* @param int $batch Number of images to process in a batch.
*
* @return array Number of found images and number of successfully processed images.
*/
public function rollback_images( $batch, $images = [] ) {
if ( empty( $images ) || $images === 'none' ) {
$args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true );
$attachments = new \WP_Query( $args );
$ids = $attachments->get_posts();
} else {
$ids = array_slice( $images, 0, $batch );
}
$result = [ 'found_images' => count( $ids ) ];
$result['success_rollback'] = $this->rollback_and_update_images( $ids );
return $result;
}
/**
* Update the post with the given id, the images will be updated by the filters we use.
*
* @param int $post_id The post id to update.
*
* @return bool Whether the update was succesful or not.
*/
public function update_page( $post_id ) {
self::$offload_update_post = true;
$post_update = wp_update_post( [ 'ID' => $post_id ] );
self::$offload_update_post = false;
if ( $post_update === 0 ) {
return false;
}
do_action( 'optml_updated_post', $post_id );
return true;
}
/**
* Calculate the number of images in media library and the number of posts/pages.
*
* @param string $action The actions for which to get the number of images.
*
* @return int Number of images.
*/
public static function number_of_images_and_pages( $action ) {
$images_args = self::get_images_or_pages_query_args( - 1, $action, true );
$images = new \WP_Query( $images_args );
// With the new mechanism, when offloading images, we don't need to address pages anymore.
// Bail early with the number of images.
if ( $action === 'offload_images' ) {
return $images->post_count;
}
$pages_args = self::get_images_or_pages_query_args( - 1, $action );
$pages = new \WP_Query( $pages_args );
return $pages->post_count + $images->post_count;
}
/**
* Calculate the number of images in media library and the number of posts/pages by IDs.
*
* @param string $action The actions for which to get the number of images.
*
* @return int Number of images.
*/
public static function number_of_images_by_ids( $action, $ids ) {
$args = self::get_images_or_pages_query_args( - 1, $action, true );
$args['post__in'] = $ids;
$images = new \WP_Query( $args );
return $images->post_count;
}
/**
* Get pages that contain images by IDs.
*
* @param string $action The actions for which to get the number of images.
* @param array $images Image IDs.
* @param int $batch Batch count.
* @param int $page Page number.
*/
public static function get_posts_by_image_ids( $action, $images = [], $batch = 10, $page = 1 ) {
if ( empty( $images ) ) {
return [];
}
$transient_key = 'optml_images_' . md5( serialize( $images ) );
$transient = get_transient( $transient_key );
if ( false !== $transient ) {
return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
}
global $wpdb;
$image_urls = array_map(
function ( $image_id ) {
$meta = wp_get_attachment_metadata( $image_id );
$extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
return str_replace( '.' . $extension, '', $meta['file'] );
},
$images
);
// Sanitize the image URLs for use in the SQL query.
$urls = array_map( 'esc_url_raw', $image_urls );
// Initialize an empty string to hold the query.
$query = '';
// Iterate through the array and add each URL to the query.
foreach ( $urls as $index => $url ) {
// If it's the first item, we don't need to add OR to the beginning.
if ( $index === 0 ) {
$query .= $wpdb->prepare( 'post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
} else {
$query .= $wpdb->prepare( ' OR post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
}
}
// Get all the posts IDs by using LIMIT and offset in a loop.
$ids = [];
$offset = 0;
$limit = $batch;
while ( true ) {
$posts = $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE $query LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$limit,
$offset
)
);
if ( empty( $posts ) ) {
break;
}
$ids = array_merge( $ids, $posts );
$offset += $limit;
}
set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
return array_slice( $ids, ( $page - 1 ) * $batch, $batch );
}
/**
* Record process meta,
*
* @param int $count The number of images to process.
*
* @return void
*/
public static function record_process_meta( $count ) {
$meta = get_option( 'optml_process_meta', [] );
$meta['count'] = $count;
$meta['remaining'] = $count;
$meta['start_time'] = time();
update_option( 'optml_process_meta', $meta );
}
/**
* Update the process meta count.
*
* @return void
*/
public static function decrement_process_meta_remaining() {
$meta = get_option( 'optml_process_meta', [] );
if ( ! isset( $meta['remaining'] ) ) {
return;
}
$meta['remaining'] = $meta['remaining'] - 1;
update_option( 'optml_process_meta', $meta );
}
/**
* Get process meta,
*
* @return array
*/
public static function get_process_meta() {
$res = [];
$meta = get_option( 'optml_process_meta', [] );
$res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
$res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
$res['remaining'] = isset( $meta['remaining'] ) ? $meta['remaining'] : $res['count'];
return $res;
}
/**
* Calculate the number of images in media library and the number of posts/pages.
*
* @param string $action The actions for which to get the number of images.
* @param bool $refresh Whether to refresh the cron or not.
* @param array $images The images to process.
*
* @return array Image count and Cron status.
*/
public static function get_image_count( $action, $refresh, $images = [] ) {
$option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
$count = 0;
$step = 0;
$batch = apply_filters( 'optimole_offload_batch', 20 ); // Reduce this to smaller if we have memory issues during testing.
if ( empty( $images ) ) {
$count = Optml_Media_Offload::number_of_images_and_pages( $action );
} else {
$count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
}
$possible_batch = ceil( $count / 10 );
if ( $possible_batch < $batch ) {
$batch = $possible_batch;
}
// If batch is less than 10, set it to 10.
if ( $batch < 10 ) {
$batch = 10;
}
$in_progress = self::$instance->settings->get( $option ) !== 'disabled';
if ( $count === 0 ) {
$in_progress = false;
}
$type = 'offload_images' === $action ? 'offload' : 'rollback';
// We save the status if this ia multi step transfer.
if ( empty( $images ) ) {
self::$instance->settings->update( 'transfer_status', $action );
}
if ( false === $refresh ) {
// We check also the alternative action to avoid doing both in the same time and disable the running one.
$in_progress_b = self::$instance->settings->get( 'rollback_images' === $action ? 'offloading_status' : 'rollback_status' ) !== 'disabled';
// We do this only if there is a mass action in progress, not individual ones.
if ( $in_progress_b && empty( $images ) ) {
// We stop the oposite action from going any further.
self::$instance->settings->update( 'rollback_images' === $action ? 'offloading_status' : 'rollback_status', 'disabled' );
}
self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
self::record_process_meta( $count );
self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
if ( $in_progress !== true ) {
return [
'count' => $count,
'status' => $in_progress,
'action' => $type,
];
}
if ( empty( $images ) ) {
$total = ceil( $count / $batch );
self::schedule_action(
time(),
'optml_start_processing_images',
[
$action,
$batch,
1,
$total,
$step,
]
);
} else {
self::schedule_action(
time(),
'optml_start_processing_images_by_id',
[
$action,
$batch,
1,
$images,
]
);
}
}
$response = [
'count' => $count,
'action' => $type,
];
if ( $type === 'offload' ) {
$offload_limit_reached = self::$instance->settings->is_offload_limit_reached();
if ( $offload_limit_reached ) {
$in_progress = false;
self::$instance->settings->update( $option, 'disabled' );
}
$response['reached_limit'] = self::$instance->settings->is_offload_limit_reached();
$response['offload_limit'] = self::$instance->settings->get( 'offload_limit' );
}
$response['status'] = $in_progress;
return $response;
}
/**
* Schedule an action.
*
* @param int $time The time to schedule the action.
* @param string $hook The hook to schedule.
* @param array $args The arguments to pass to the hook.
*
* @return mixed
*/
public static function schedule_action( $time, $hook, $args ) {
// We use AS if available to avoid issues with WP Cron.
if ( function_exists( 'as_schedule_single_action' ) ) {
return as_schedule_single_action( $time, $hook, $args );
} else {
return wp_schedule_single_event( $time, $hook, $args );
}
}
/**
* Check if an action hook is scheduled.
*
* @param string $hook The hook to check.
*
* @return bool
*/
public static function is_scheduled( $hook ) {
if ( function_exists( 'as_has_scheduled_action' ) ) {
return as_has_scheduled_action( $hook );
} elseif ( function_exists( 'as_next_scheduled_action' ) ) {
// For older versions of AS.
return as_next_scheduled_action( $hook ) !== false;
} else {
return wp_next_scheduled( $hook ) !== false;
}
}
/**
* Start Processing Images by IDs
*
* @param string $action The action for which to get the number of images.
* @param int $batch The batch of images to process.
* @param int $page The page of images to process.
* @param array $image_ids The images to process.
*
* @return void
*/
public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
$option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
$type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK;
if ( self::$instance->settings->get( $option ) === 'disabled' ) {
return;
}
set_time_limit( 0 );
// Only use the legacy offloaded attachments to query the pages that need to be updated.
// We can be confident that these IDs are already marked as offloaded.
$legacy_offloaded = array_filter(
$image_ids,
function ( $id ) {
return ! $this->is_new_offloaded_attachment( $id );
}
);
// On the new mechanism, we don't update posts anymore when offloading.
$page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
$meta = self::get_process_meta();
self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
self::$instance->settings->update( $option, 'disabled' );
return;
}
try {
// This will be 0 in the case of offloading now.
if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
$to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
if ( isset( $to_update['page'] ) ) {
if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
if ( ! empty( $image_ids ) ) {
$images = array_intersect( $images, $image_ids );
}
if ( empty( $images ) ) {
continue;
}
Optml_Media_Offload::instance()->rollback_and_update_images( $images );
Optml_Media_Offload::instance()->update_page( $post_id );
}
}
}
$page = $page + 1;
} else {
// From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
$images = array_slice( $image_ids, 0, $batch );
$image_ids = array_slice( $image_ids, $batch );
$action === 'rollback_images' ?
Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
Optml_Media_Offload::instance()->upload_images( $batch, $images );
}
self::schedule_action(
time(),
'optml_start_processing_images_by_id',
[
$action,
$batch,
$page,
$image_ids,
]
);
} catch ( Exception $e ) {
// Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
$delay_in_seconds = 10;
self::$instance->logger->add_log( $type, $e->getMessage() );
self::schedule_action(
time() + $delay_in_seconds,
'optml_start_processing_images_by_id',
[
$action,
$batch,
$page,
$image_ids,
]
);
}
}
/**
* Start Processing Images
*
* @param string $action The action for which to get the number of images.
* @param int $batch The batch of images to process.
* @param int $page The page of images to process.
* @param int $total The total number of pages.
* @param int $step The current step.
*
* @return void
*/
public function start_processing_images( $action, $batch, $page, $total, $step ) {
$option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
$type = 'offload_images' === $action ? 'offload' : 'rollback';
if ( self::$instance->settings->get( $option ) === 'disabled' ) {
return;
}
if ( $step > $total || 0 === $total ) {
$meta = self::get_process_meta();
self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
self::$instance->settings->update( $option, 'disabled' );
self::$instance->settings->update( 'show_offload_finish_notice', $type );
return;
}
set_time_limit( 0 );
try {
$posts_to_update = $action === 'offload_images' ? [] : Optml_Media_Offload::instance()->update_content( $page, $action, $batch );
// Kept for backward compatibility with old offloading mechanism where pages were modified.
if ( $action === 'rollback_images' && isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) {
$page = $posts_to_update['page'];
if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
Optml_Media_Offload::instance()->rollback_and_update_images( $images );
Optml_Media_Offload::instance()->update_page( $post_id );
}
}
} else {
$action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch ) : Optml_Media_Offload::instance()->upload_images( $batch );
}
$step = $step + 1;
self::schedule_action(
time(),
'optml_start_processing_images',
[
$action,
$batch,
$page,
$total,
$step,
]
);
} catch ( Exception $e ) {
// Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
$delay_in_seconds = 10;
self::$instance->logger->add_log( $type, $e->getMessage() );
self::schedule_action(
time() + $delay_in_seconds,
'optml_start_processing_images',
[
$action,
$batch,
$page,
$total,
$step,
]
);
}
}
/**
* Alter attachment image src for offloaded images.
*
* @param array|false $image {
* Array of image data.
*
* @type string $0 Image source URL.
* @type int $1 Image width in pixels.
* @type int $2 Image height in pixels.
* @type bool $3 Whether the image is a resized image.
* }
*
* @param int $attachment_id attachment id.
* @param string|int[] $size image size.
* @param bool $icon Whether the image should be treated as an icon.
*
* @return array $image.
*/
public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
return $image;
}
$url = get_post( $attachment_id );
$url = $url->guid;
$image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id );
$metadata = wp_get_attachment_metadata( $attachment_id );
// Use the original size if the requested size is full.
if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
$image_url = $this->get_new_offloaded_attachment_url(
$url,
$attachment_id,
[
'width' => $metadata['width'],
'height' => $metadata['height'],
'attachment_id' => $attachment_id,
]
);
return [
$image_url,
$metadata['width'],
$metadata['height'],
false,
];
}
$crop = false;
// Size can be int [] containing width and height.
if ( is_array( $size ) ) {
$width = $size[0];
$height = $size[1];
$crop = true;
} else {
$sizes = $this->get_all_image_sizes();
if ( ! isset( $sizes[ $size ] ) ) {
return [
$image_url,
$metadata['width'],
$metadata['height'],
false,
];
}
$width = $sizes[ $size ]['width'];
$height = $sizes[ $size ]['height'];
$crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
}
$sizes2crop = self::size_to_crop();
if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
return $image;
}
$resize = apply_filters( 'optml_default_crop', [] );
$data = image_get_intermediate_size( $attachment_id, $size );
if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
$resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
}
}
if ( $crop !== false ) {
$resize = $this->to_optml_crop( $crop );
}
$image_url = $this->get_new_offloaded_attachment_url(
$url,
$attachment_id,
[
'width' => $width,
'height' => $height,
'resize' => $resize,
'attachment_id' => $attachment_id,
]
);
return [
$image_url,
$width,
$height,
$crop,
];
}
/**
* Needed for image sizes inside the editor.
*
* @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js().
* @param WP_Post $attachment Attachment object.
* @param array|false $meta Array of attachment meta data, or false if there is none.
*
* @return array
*/
public function alter_attachment_for_js( $response, $attachment, $meta ) {
if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
return $response;
}
$sizes = $this->get_all_image_sizes();
foreach ( $sizes as $size => $args ) {
if ( isset( $response['sizes'][ $size ] ) ) {
continue;
}
$args = [
'height' => $args['height'],
'width' => $args['width'],
'crop' => true,
];
$response['sizes'][ $size ] = array_merge(
$args,
[
'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
]
);
}
$url_args = [
'height' => $response['height'],
'width' => $response['width'],
'crop' => false,
];
$response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
return $response;
}
/**
* Alter attachment metadata.
*
* @param array $metadata The attachment metadata.
* @param int $id The attachment ID.
*
* @return array
*/
public function alter_attachment_metadata( $metadata, $id ) {
if ( ! $this->is_new_offloaded_attachment( $id ) ) {
return $metadata;
}
return $this->get_altered_metadata_for_remote_images( $metadata, $id );
}
/**
* Get offloaded image attachment URL for new offloads.
*
* @param string $url The initial attachment URL.
* @param int $attachment_id The attachment ID.
* @param array $args The additional arguments.
* - width: The width of the image.
* - height: The height of the image.
* - crop: Whether to crop the image.
*
* @return string
*/
private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [] ) {
$process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
// Image might have already passed through this filter.
if ( strpos( $url, $process_flag ) !== false ) {
return $url;
}
$meta = wp_get_attachment_metadata( $attachment_id );
if ( ! isset( $meta['file'] ) ) {
return $url;
}
$default_args = [
'width' => 'auto',
'height' => 'auto',
'resize' => apply_filters( 'optml_default_crop', [] ),
];
$args = wp_parse_args( $args, $default_args );
// If this is not cropped, we constrain the dimensions to the original image.
if ( empty( $args['resize'] ) && ! in_array( 'auto', [ $args['width'], $args['height'] ], true ) ) {
$dimensions = wp_constrain_dimensions( $meta['width'], $meta['height'], $args['width'], $args['height'] );
$args['width'] = $dimensions[0];
$args['height'] = $dimensions[1];
}
$file = $meta['file'];
if ( self::is_uploaded_image( $file ) ) {
$optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
return strpos( $optimized_url, $process_flag ) === false ? str_replace( '/' . ltrim( $file, '/' ), '/' . $process_flag . $file, $optimized_url ) : $optimized_url;
} else {
// this is for the users that already offloaded the images before the other fixes
$local_file = get_attached_file( $attachment_id );
if ( ! file_exists( $local_file ) ) {
$duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
foreach ( $duplicated_images as $id ) {
if ( ! empty( $id ) ) {
$duplicated_meta = wp_get_attachment_metadata( $id );
if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
}
}
}
}
}
}
return $url;
}
/**
* Replace the URLs in the editor content with the offloaded ones.
*
* @param string $content The incoming content.
*
* @return string
*/
public function replace_urls_in_editor_content( $content ) {
$raw_extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
if ( empty( $raw_extracted ) ) {
return $content;
}
$to_replace = [];
foreach ( $raw_extracted as $url ) {
$attachment = $this->get_local_attachement_id_from_url( $url );
// No local attachment.
if ( $attachment['attachment_id'] === 0 ) {
if ( $this->can_replace_url( $url ) ) {
$to_replace[ $url ] = $this->get_optimized_image_url( $url, 'auto', 'auto' );
}
continue;
}
$attachment_id = $attachment['attachment_id'];
// Not offloaded.
if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
continue;
}
// Get W/H from url.
$size = $this->parse_dimensions_from_filename( $url );
$width = $size[0] !== false ? $size[0] : 'auto';
$height = $size[1] !== false ? $size[1] : 'auto';
// Handle resize.
$sizes2crop = self::size_to_crop();
$resize = apply_filters( 'optml_default_crop', [] );
$sizes = image_get_intermediate_size( $attachment_id, $size );
if ( false !== $sizes ) {
if ( isset( $sizes2crop[ $width . $height ] ) ) {
$resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] );
}
}
// Build the optimized URL.
$optimized_url = $this->get_optimized_image_url( self::KEYS['not_processed_flag'] . $attachment_id . '/' . ltrim( $this->get_offloaded_attachment_url( $attachment_id, $url ), '/' ), $width, $height, $resize );
// Drop any image size from the URL.
$optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
$to_replace[ $url ] = $optimized_url;
}
return str_replace( array_keys( $to_replace ), array_values( $to_replace ), $content );
}
/**
* Replaces the post content URLs to use Offloaded ones on editor fetch.
*
* @param \WP_REST_Response $response The response object.
* @param \WP_Post $post The post object.
* @param \WP_REST_Request $request The request object.
*
* @return \WP_REST_Response
*/
public function pre_filter_rest_content( \WP_REST_Response $response, \WP_Post $post, \WP_REST_Request $request ) {
$context = $request->get_param( 'context' );
if ( $context !== 'edit' ) {
return $response;
}
$data = $response->get_data();
// Actually replace all URLs.
$data['content']['raw'] = $this->replace_urls_in_editor_content( $data['content']['raw'] );
$response->set_data( $data );
return $response;
}
/**
* Legacy function to be used for WordPress versions under 6.0.0.
*
* @param array $post_data Slashed, sanitized, processed post data.
* @param array $postarr Slashed sanitized post data.
* @param array $unsanitized_postarr Un-sanitized post data.
*
* @return array
*/
public function legacy_filter_saved_data( $post_data, $postarr, $unsanitized_postarr ) {
return $this->filter_saved_data( $post_data, $postarr, $unsanitized_postarr, true );
}
/**
* Filter post content to use local attachments when saving offloaded images.
*
* @param array $post_data Slashed, sanitized, processed post data.
* @param array $postarr Slashed sanitized post data.
* @param array $unsanitized_postarr Un-sanitized post data.
* @param bool $update Whether this is an existing post being updated or not.
*
* @return array
*/
public function filter_saved_data( $post_data, $postarr, $unsanitized_postarr, $update ) {
if ( $postarr['post_status'] === 'trash' ) {
return $post_data;
}
$content = $post_data['post_content'];
$extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
$replace = [];
foreach ( $extracted as $idx => $url ) {
$id = self::get_attachment_id_from_url( $url );
if ( $id === false ) {
continue;
}
$id = (int) $id;
if ( $this->is_legacy_offloaded_attachment( $id ) ) {
continue;
}
$original = self::get_original_url( $id );
if ( $original === false ) {
continue;
}
$replace[ $url ] = $original;
$size = $this->parse_dimension_from_optimized_url( $url );
if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
continue;
}
$extension = $this->get_ext( $url );
$metadata = wp_get_attachment_metadata( $id );
// Is this the full URL.
if ( $metadata['width'] === (int) $size[0] && $metadata['height'] === (int) $size[1] ) {
continue;
}
$size_crop_map = self::size_to_crop();
$crop = false;
if ( isset( $size_crop_map[ $size[0] . $size[1] ] ) ) {
$crop = $size_crop_map[ $size[0] . $size[1] ];
}
if ( $crop ) {
$width = $size[0];
$height = $size[1];
} else {
// In case of an image size, we need to calculate the new dimensions for the proper file path.
$constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] );
$width = $constrained[0];
$height = $constrained[1];
}
$replace[ $url ] = $this->maybe_strip_scaled( $replace[ $url ] );
$suffix = sprintf( '-%sx%s.%s', $width, $height, $extension );
$replace[ $url ] = str_replace( '.' . $extension, $suffix, $replace[ $url ] );
}
$post_data['post_content'] = str_replace( array_keys( $replace ), array_values( $replace ), $content );
return $post_data;
}
/**
* Alter the image size for the image widget.
*
* @param string $html the attachment image HTML string.
* @param array $settings Control settings.
* @param string $image_size_key Optional. Settings key for image size.
* Default is `image`.
* @param string $image_key Optional. Settings key for image. Default
* is null. If not defined uses image size key
* as the image key.
*
* @return string
*/
public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) {
if ( ! isset( $settings['image'] ) ) {
return $html;
}
$image = $settings['image'];
if ( ! isset( $image['id'] ) ) {
return $html;
}
if ( ! $this->is_new_offloaded_attachment( $image['id'] ) ) {
return $html;
}
if ( ! isset( $settings['image_size'] ) ) {
return $html;
}
if ( $settings['image_size'] === 'custom' ) {
if ( ! isset( $settings['image_custom_dimension'] ) ) {
return $html;
}
$custom_dimensions = $settings['image_custom_dimension'];
if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) {
return $html;
}
$new_args = [
'width' => $custom_dimensions['width'],
'height' => $custom_dimensions['height'],
'resize' => $this->to_optml_crop( true ),
];
$new_url = $this->get_new_offloaded_attachment_url( $image['url'], $image['id'], $new_args );
return str_replace( $image['url'], $new_url, $html );
}
return $html;
}
/**
* Adds new actions for new offloads.
*
* @return void
*/
public function add_new_actions() {
add_filter( 'wp_prepare_attachment_for_js', [ self::$instance, 'alter_attachment_for_js' ], 999, 3 );
add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
// Needed for rendering beaver builder css properly.
add_filter( 'fl_builder_render_css', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
// Filter saved data on insert to use local attachments.
// Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
$below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
if ( $below_6_0_0 ) {
add_filter( 'wp_insert_post_data', [ self::$instance, 'legacy_filter_saved_data' ], 10, 3 );
} else {
add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
}
// Filter loaded data in the editors to use local attachments.
add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
add_action(
'init',
function () {
$types = get_post_types_by_support( 'editor' );
foreach ( $types as $type ) {
$post_type = get_post_type_object( $type );
if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
add_filter( 'rest_prepare_' . $type, [ self::$instance, 'pre_filter_rest_content' ], 10, 3 );
}
}
},
PHP_INT_MAX
);
add_filter( 'get_attached_file', [ $this, 'alter_attached_file_response' ], 10, 2 );
add_filter(
'elementor/image_size/get_attachment_image_html',
[
$this,
'alter_elementor_image_size',
],
10,
4
);
}
/**
* Elementor checks if the file exists before requesting a specific image size.
*
* Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload.
*
* Also needed because some
*
* @param string $file The file path.
* @param int $id The attachment ID.
*
* @return bool|string
*/
public function alter_attached_file_response( $file, $id ) {
if ( ! $this->is_new_offloaded_attachment( $id ) ) {
return $file;
}
$metadata = wp_get_attachment_metadata( $id );
if ( isset( $metadata['file'] ) ) {
$uploads = wp_get_upload_dir();
return $uploads['basedir'] . '/' . $metadata['file'];
}
return true;
}
/**
* Maybe strip the `-scaled` from the URL.
*
* @param string $url The url.
*
* @return string
*/
public function maybe_strip_scaled( $url ) {
$ext = $this->get_ext( $url );
return str_replace( '-scaled.' . $ext, '.' . $ext, $url );
}
/**
* Is it a PHPUnit test run.
*
* @return bool
*/
public static function is_phpunit_test() {
return defined( 'OPTML_PHPUNIT_TESTING' ) && OPTML_PHPUNIT_TESTING === true;
}
/**
* Get offloaded image attachment URL based on the given attachment ID and URL.
*
* @param mixed $attachment_id The attachment ID.
* @param string $url The attachment URL.
*
* @return string
*/
private function get_offloaded_attachment_url( $attachment_id, $url ) {
if ( ! $this->settings->is_offload_enabled() || ! is_numeric( $attachment_id ) ) {
return $url;
} elseif ( empty( $attachment_id ) && strpos( $url, self::KEYS['not_processed_flag'] ) !== false ) {
$attachment_id = (int) self::get_attachment_id_from_url( $url );
} elseif ( empty( $attachment_id ) ) {
$attachment_id = $this->attachment_url_to_post_id( $url );
}
if ( $attachment_id > 0 || ! empty( get_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true ) ) ) {
$url = wp_get_attachment_metadata( $attachment_id )['file'];
}
return $url;
}
}