PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 3.11.1
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v3.11.1
4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 3.12.1 All 134 releases
optimole-wp / inc / media_offload.php

media_offload.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 3.11.1, at inc/media_offload.php

2,595 lines 83.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Optml_Media_Offload class.
4 *
5 * @package \Optimole\Inc
6 * @author Optimole <friends@optimole.com>
7 */
8
9 /**
10 * Class Optml_Admin
11 */
12 class Optml_Media_Offload extends Optml_App_Replacer {
13 use Optml_Normalizer;
14 use Optml_Dam_Offload_Utils;
15
16
17 /**
18 * Hold the settings object.
19 *
20 * @var Optml_Settings Settings object.
21 */
22 public $settings;
23 /**
24 * Cached object instance.
25 *
26 * @var Optml_Media_Offload
27 */
28 private static $instance = null;
29
30 /**
31 * Hold the logger object.
32 *
33 * @var Optml_Logger
34 */
35 public $logger;
36
37 const KEYS = [
38 'uploaded_flag' => 'id:',
39 'not_processed_flag' => 'process:',
40 ];
41 const META_KEYS = [
42 'offloaded' => 'optimole_offload',
43 'offload_error' => 'optimole_offload_error',
44 'rollback_error' => 'optimole_rollback_error',
45 ];
46 const OM_OFFLOADED_FLAG = 'om_image_offloaded';
47 const POST_OFFLOADED_FLAG = 'optimole_offload_post';
48 const POST_ROLLBACK_FLAG = 'optimole_rollback_post';
49 /**
50 * Flag used inside wp_get_attachment url filter.
51 *
52 * @var bool Whether or not to return the original url of the image.
53 */
54 private static $return_original_url = false;
55 /**
56 * Flag used inside wp_get_attachment url filter.
57 *
58 * @var bool Whether or not to return the original url of the image.
59 */
60 private static $offload_update_post = false;
61
62 /**
63 * Flag used inside wp_unique_filename filter.
64 *
65 * @var bool|string Whether to skip our custom deduplication.
66 */
67 private static $current_file_deduplication = false;
68 /**
69 * Keeps the last deduplicated lower case value.
70 *
71 * @var bool|string Used to check if the current processed image was deduplicated.
72 */
73 private static $last_deduplicated = false;
74 /**
75 * Checks if the plugin was installed before adding POST_OFFLOADED_FLAG.
76 *
77 * @var bool Used when applying the flags for the page query.
78 */
79 private static $is_legacy_install = null;
80
81 /**
82 * Adds page meta query args
83 *
84 * @param string $action The action for which the args are needed.
85 * @param array $args The initial args without the added meta_query args.
86 * @return array The args with the added meta_query args.
87 */
88 public static function add_page_meta_query_args( $action, $args ) {
89 if ( $action === 'offload_images' ) {
90 $args['meta_query'] = [
91 'relation' => 'AND',
92 [
93 'key' => self::POST_OFFLOADED_FLAG,
94 'compare' => 'NOT EXISTS',
95 ],
96 ];
97 }
98 if ( $action === 'rollback_images' ) {
99 $args['meta_query'] = [
100 'relation' => 'AND',
101 [
102 'key' => self::POST_ROLLBACK_FLAG,
103 'compare' => 'NOT EXISTS',
104 ],
105 ];
106 if ( self::$is_legacy_install ) {
107 $args['meta_query'][] = [
108 'key' => self::POST_OFFLOADED_FLAG,
109 'value' => 'true',
110 'compare' => '=',
111 ];
112 }
113 }
114 return $args;
115 }
116
117 /**
118 * Get count of all images from db.
119 *
120 * @return int Number of all images.
121 */
122 public static function number_of_all_images() {
123 $total_images_by_mime = wp_count_attachments( 'image' );
124 return array_sum( (array) $total_images_by_mime );
125 }
126
127 /**
128 * Optml_Media_Offload constructor.
129 */
130 public static function instance() {
131 if ( null === self::$instance ||
132 ( self::$instance->settings !== null && ( ! self::$instance->settings->is_connected()
133 || self::$instance->settings->get( 'offload_media' ) === 'disabled'
134 || self::$instance->settings->get( 'cloud_images' ) === 'disabled' ) ) ) {
135 self::$instance = new self();
136 self::$instance->settings = new Optml_Settings();
137 self::$instance->logger = Optml_Logger::instance();
138
139 if ( self::$instance->settings->is_connected() ) {
140 self::$instance->init();
141 }
142 if ( self::$instance->settings->is_offload_enabled() ) {
143 add_filter( 'image_downsize', [self::$instance, 'generate_filter_downsize_urls'], 10, 3 );
144 add_filter( 'wp_generate_attachment_metadata', [self::$instance, 'generate_image_meta'], 10, 2 );
145 add_filter( 'wp_get_attachment_url', [self::$instance, 'get_image_attachment_url'], -999, 2 );
146 add_filter( 'wp_insert_post_data', [self::$instance, 'filter_uploaded_images'] );
147
148 self::$instance->add_new_actions();
149
150 add_action( 'delete_attachment', [self::$instance, 'delete_attachment_hook'], 10 );
151 add_filter( 'handle_bulk_actions-upload', [self::$instance, 'bulk_action_handler'], 10, 3 );
152 add_filter( 'bulk_actions-upload', [self::$instance, 'register_bulk_media_actions'] );
153 add_filter( 'media_row_actions', [self::$instance, 'add_inline_media_action'], 10, 2 );
154 add_filter( 'wp_calculate_image_srcset', [self::$instance, 'calculate_image_srcset'], 1, 5 );
155 add_action( 'post_updated', [self::$instance, 'update_offload_meta'], 10, 3 );
156
157 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
158 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
159 if ( $below_6_0_0 ) {
160 add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert_legacy'], 10, 3 );
161 } else {
162 add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert'], 10, 4 );
163 }
164
165 add_action( 'optml_start_processing_images', [self::$instance, 'start_processing_images'], 10, 5 );
166 add_action( 'optml_start_processing_images_by_id', [self::$instance, 'start_processing_images_by_id'], 10, 4 );
167
168 if ( self::$is_legacy_install === null ) {
169 self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
170 }
171 }
172 }
173 return self::$instance;
174 }
175
176 /**
177 * Function for `update_attached_file` filter-hook.
178 *
179 * @param string $file Path to the attached file to update.
180 * @param int $attachment_id Attachment ID.
181 *
182 * @return string
183 */
184 function wp_update_attached_file_filter( $file, $attachment_id ) {
185
186 if ( OPTML_DEBUG_MEDIA ) {
187 do_action( 'optml_log', 'called updated attached' );
188 }
189 $info = pathinfo( $file );
190 $file_name = basename( $file );
191 $no_ext_file_name = basename( $file, '.' . $info['extension'] );
192 // if we have current deduplication set and it contains the filename that is updated
193 // we replace the updated filename with the deduplicated filename
194 if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) {
195 $file = str_replace( $file_name, self::$current_file_deduplication, $file );
196 // we need to store the filename we replaced to check when uploading the image if it was deduplicated
197 self::$last_deduplicated = $file_name;
198
199 self::$current_file_deduplication = false;
200 }
201 if ( OPTML_DEBUG_MEDIA ) {
202 do_action( 'optml_log', self::$last_deduplicated );
203 }
204 remove_filter( 'update_attached_file', [self::$instance, 'wp_update_attached_file_filter'], 10 );
205 return $file;
206 }
207
208 /**
209 * Function for `wp_insert_attachment_data` filter-hook.
210 * Because we remove the images when new images are added the wp deduplication using the files will not work
211 * 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
212 * For creating a unique quid by replacing the filename with the slug inside the existing guid
213 * 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
214 * 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
215 * with the deduplicated one which we stored into `$current_file_deduplication` variable
216 *
217 * @param array $data An array of slashed, sanitized, and processed attachment post data.
218 * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
219 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
220 * @param bool $update Whether this is an existing attachment post being updated.
221 *
222 * @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0.
223 *
224 * @return array
225 */
226 function insert( $data, $postarr, $unsanitized_postarr, $update ) {
227
228 // the post name is unique against the database so not affected by removing the files
229 // https://developer.wordpress.org/reference/functions/wp_unique_post_slug/
230 if ( OPTML_DEBUG_MEDIA ) {
231 do_action( 'optml_log', 'data before' );
232 do_action( 'optml_log', $data );
233 }
234 if ( empty( $data['guid'] ) ) {
235 return $data;
236 }
237
238 $filename = wp_basename( $data['guid'] );
239 $ext = $this->get_ext( $filename );
240 // skip if the file is not an image
241 if ( ! isset( Optml_Config::$all_extensions[ $ext ] ) && ! in_array( $ext, ['jpg', 'jpeg', 'jpe'], true ) ) {
242 return $data;
243 }
244
245 // on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg`
246 // we remove that as it is redundant for the file name deduplication we are using it
247 $sanitized_post_name = str_replace( '-' . $ext, '', $data['post_name'] );
248
249 // with the wp deduplication working the post_title is identical to the post_name
250 // so when they are different it means we need to deduplicate using the post_name
251 if ( ! empty( $data['post_name'] ) && $data['post_title'] !== $sanitized_post_name ) {
252 // we append the extension to the post_name to create a filename
253 // and use it to replace the filename in the guid
254 $no_ext_filename = str_replace( '.' . $ext, '', $filename );
255
256 $no_ext_filename_sanitized = sanitize_title( $no_ext_filename );
257
258 // get the deduplication addition from the database post_name
259 $diff = str_replace( strtolower( $no_ext_filename_sanitized ), '', $sanitized_post_name );
260
261 // create the deduplicated filename
262 $to_replace_with = $no_ext_filename . $diff . '.' . $ext;
263
264 $data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] );
265 // we store the deduplication to be used and add the filter for updating the attached_file meta
266 self::$current_file_deduplication = $to_replace_with;
267 add_filter( 'update_attached_file', [self::$instance, 'wp_update_attached_file_filter'], 10, 2 );
268 }
269 if ( OPTML_DEBUG_MEDIA ) {
270 do_action( 'optml_log', 'data after' );
271 do_action( 'optml_log', $data );
272 }
273
274 return $data;
275 }
276
277 /**
278 * Wrapper for the `insert` method for WP versions < 6.0.0.
279 *
280 * @param array $data An array of slashed, sanitized, and processed attachment post data.
281 * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
282 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
283 *
284 * @return array
285 */
286 function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
287 return $this->insert( $data, $postarr, $unsanitized_postarr, false );
288 }
289
290 /**
291 * Update offload meta when the page is updated.
292 *
293 * @param int $post_ID Updated post id.
294 * @param WP_Post $post_after Post before the update.
295 * @param WP_Post $post_before Post after the update.
296 * @uses action:post_updated
297 *
298 * @return void
299 */
300 public function update_offload_meta( $post_ID, $post_after, $post_before ) {
301 if ( self::$offload_update_post === true ) {
302 return;
303 }
304 if ( get_post_type( $post_ID ) === 'attachment' ) {
305 return;
306 }
307
308 // revisions are skipped inside the function no need to check them before
309 delete_post_meta( $post_ID, self::POST_ROLLBACK_FLAG );
310 }
311
312 /**
313 * Get image size name from width and meta.
314 *
315 * @param array $sizes Image sizes .
316 * @param integer $width Size width.
317 * @param string $filename Image filename.
318 *
319 * @return null|string|array
320 */
321 public static function get_image_size_from_width( $sizes, $width, $filename, $just_name = true ) {
322 foreach ( $sizes as $name => $size ) {
323 if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
324 return $just_name ? $name : array_merge( $size, [ 'name' => $name ] );
325 }
326 }
327
328 return null;
329 }
330
331 /**
332 * Replace image URLs in the srcset attributes.
333 *
334 * @param array $sources Array of image sources.
335 * @param array $size_array Array of width and height values in pixels (in that order).
336 * @param string $image_src The 'src' of the image.
337 * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
338 * @param int $attachment_id Image attachment ID.
339 *
340 * @return array
341 */
342 public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
343 if ( ! is_array( $sources ) ) {
344 return $sources;
345 }
346
347 if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
348 if ( ! Optml_Media_Offload::is_uploaded_image( $image_src ) || ! isset( $image_meta['file'] ) || ! Optml_Media_Offload::is_uploaded_image( $image_meta['file'] ) ) {
349 return $sources;
350 }
351 foreach ( $sources as $width => $source ) {
352 $filename = wp_basename( $image_meta['file'] );
353 $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename );
354 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
355
356 if ( false === $optimized_url ) {
357 continue;
358 }
359
360 $sources[ $width ]['url'] = $optimized_url[0];
361 }
362 return $sources;
363 }
364
365 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
366 return $sources;
367 }
368
369 $requested_width = $size_array[0];
370 $requested_height = $size_array[1];
371
372 if ( $requested_height < 1 || $requested_width < 1 ) {
373 return $sources;
374 }
375
376 $requested_ratio = $requested_width / $requested_height;
377
378 $image_sizes = $this->get_all_image_sizes();
379 $crop = false;
380
381 // Loop through image sizes to make sure we're using the right cropping.
382 foreach ( $image_sizes as $size_name => $args ) {
383 if ( $args['width'] !== $requested_width && $args['height'] !== $requested_height ) {
384 continue;
385 }
386
387 if ( isset( $args['crop'] ) ) {
388 $crop = (bool) $args['crop'];
389 }
390 }
391
392 foreach ( $sources as $width => $source ) {
393 $filename = ( $image_meta['file'] );
394 $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename, false );
395
396 if ( $size === null || ! isset( $size['name'] ) ) {
397 unset( $sources[ $width ] );
398
399 continue;
400 }
401
402 if ( ! isset( $image_sizes[ $size['name'] ] ) || (bool) $image_sizes[ $size['name'] ]['crop'] !== $crop ) {
403 unset( $sources[ $width ] );
404
405 continue;
406 }
407
408 // Some image sizes might have 0 values for width or height.
409 if ( $size['width'] < 1 || $size['height'] < 1 ) {
410 unset( $sources[ $width ] );
411
412 continue;
413 }
414
415 $size_ratio = $size['width'] / $size['height'];
416
417 // We need a srcset with the same aspect ratio.
418 // Otherwise, we'll display different images on different devices.
419 if ( $requested_ratio !== $size_ratio ) {
420 unset( $sources[ $width ] );
421
422 continue;
423 }
424
425 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size['name'] );
426
427 if ( false === $optimized_url ) {
428 unset( $sources[ $width ] );
429
430 continue;
431 }
432
433 $sources[ $width ]['url'] = $optimized_url[0];
434 }
435
436 // Add the requested size to the srcset.
437 $sources[ $requested_width ] = [
438 'url' => $image_src,
439 'descriptor' => 'w',
440 'value' => $requested_width,
441 ];
442
443 return $sources;
444 }
445
446 /**
447 * Check if the image is stored on our servers or not.
448 *
449 * @param string $src Image src or url.
450 * @return bool Whether image is upload or not.
451 */
452 public static function is_not_processed_image( $src ) {
453 return strpos( $src, self::KEYS['not_processed_flag'] ) !== false;
454 }
455
456 /**
457 * Check if the image is stored on our servers or not.
458 *
459 * @param string $src Image src or url.
460 * @return bool Whether image is upload or not.
461 */
462 public static function is_uploaded_image( $src ) {
463 return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false;
464 }
465
466 /**
467 * Get the attachment ID from the image tag.
468 *
469 * @param string $image Image tag.
470 *
471 * @return int|false
472 */
473 public function get_id_from_tag( $image ) {
474 $attachment_id = false;
475 if ( preg_match( '#class=["|\']?[^"\']*(wp-image-|wp-video-)([\d]+)[^"\']*["|\']?#i', $image, $found ) ) {
476 $attachment_id = intval( $found[2] );
477 }
478
479 return $attachment_id;
480 }
481
482 /**
483 * Get attachment id from url
484 *
485 * @param string $url The optimized url .
486 * @return false|mixed The attachment id .
487 */
488 public static function get_attachment_id_from_url( $url ) {
489 preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
490 return isset( $attachment_id[1] ) ? $attachment_id[1] : false;
491 }
492
493 /**
494 * Get attachment id from local url
495 *
496 * @param string $url The url to look for.
497 * @return array The attachment id and the size from the url.
498 */
499 public function get_local_attachement_id_from_url( $url ) {
500
501 $size = 'full';
502 $found_size = $this->parse_dimensions_from_filename( $url );
503 $strip_url = $url;
504 $scaled_url = $url;
505 if ( $found_size[0] !== false && $found_size[1] !== false ) {
506 $size = $found_size;
507 $strip_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '', $url );
508 $scaled_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '-scaled', $url );
509 }
510 $strip_url = $this->add_schema( $strip_url );
511
512 $attachment_id = attachment_url_to_postid( $strip_url );
513 if ( $attachment_id === 0 ) {
514 $scaled_url = $this->add_schema( $scaled_url );
515 $attachment_id = attachment_url_to_postid( $scaled_url );
516 }
517
518 return [ 'attachment_id' => $attachment_id, 'size' => $size ];
519 }
520
521 /**
522 * Filter out the urls that are saved to our servers when saving to the DB.
523 *
524 * @param array $data The post data array to save.
525 *
526 * @return array
527 * @uses filter:wp_insert_post_data
528 */
529 public function filter_uploaded_images( $data ) {
530
531 $content = trim( wp_unslash( $data['post_content'] ) );
532 if ( OPTML_DEBUG_MEDIA ) {
533 do_action( 'optml_log', 'content to update' );
534 do_action( 'optml_log', $content );
535 }
536 $images = Optml_Manager::instance()->extract_urls_from_content( $content );
537 if ( ! isset( $images[0] ) ) {
538 return $data;
539 }
540 if ( OPTML_DEBUG_MEDIA ) {
541 do_action( 'optml_log', 'images to update' );
542 do_action( 'optml_log', $images );
543 }
544 foreach ( $images as $url ) {
545 $is_original_uploaded = self::is_uploaded_image( $url );
546 $attachment_id = false;
547 $size = 'thumbnail';
548 if ( $is_original_uploaded ) {
549 $found_size = $this->parse_dimension_from_optimized_url( $url );
550 if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) {
551 $size = $found_size;
552 }
553 $attachment_id = self::get_attachment_id_from_url( $url );
554 } else {
555 $id_and_size = $this->get_local_attachement_id_from_url( $url );
556 $attachment_id = $id_and_size['attachment_id'];
557 $size = $id_and_size['size'];
558 }
559
560 if ( OPTML_DEBUG_MEDIA ) {
561 do_action( 'optml_log', 'image id and found size' );
562 do_action( 'optml_log', $attachment_id );
563 do_action( 'optml_log', $size );
564 }
565 if ( false === $attachment_id || ! $this->is_legacy_offloaded_attachment( $attachment_id ) || ! wp_attachment_is_image( $attachment_id ) ) {
566 continue;
567 }
568 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
569 if ( OPTML_DEBUG_MEDIA ) {
570 do_action( 'optml_log', ' image url to replace with ' );
571 do_action( 'optml_log', $optimized_url );
572 }
573
574 if ( ! isset( $optimized_url[0] ) ) {
575 continue;
576 }
577 if ( $is_original_uploaded === self::is_uploaded_image( $optimized_url[0] ) ) {
578 continue;
579 }
580 $content = str_replace( $url, $optimized_url[0], $content );
581 }
582 $data['post_content'] = wp_slash( $content );
583 return $data;
584 }
585
586 /**
587 * Get all images that need to be updated from a post.
588 *
589 * @param string $post_content The content of the post.
590 * @param string $job The job name.
591 * @return array An array containing the image ids.
592 */
593 public function get_image_id_from_content( $post_content, $job ) {
594 $content = trim( wp_unslash( $post_content ) );
595 $images = Optml_Manager::instance()->extract_urls_from_content( $content );
596 $found_images = [];
597 if ( isset( $images[0] ) ) {
598 foreach ( $images as $url ) {
599 $is_original_uploaded = self::is_uploaded_image( $url );
600 $attachment_id = false;
601 if ( $is_original_uploaded ) {
602 if ( $job === 'rollback_images' ) {
603 $attachment_id = self::get_attachment_id_from_url( $url );
604 }
605 } else {
606 if ( $job === 'offload_images' ) {
607 $id_and_size = $this->get_local_attachement_id_from_url( $url );
608 $attachment_id = $id_and_size['attachment_id'];
609 }
610 }
611 if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) {
612 continue;
613 }
614 $found_images[] = intval( $attachment_id );
615 }
616 }
617 return apply_filters( 'optml_content_images_to_update', $found_images, $content );
618 }
619
620 /**
621 * Get the posts ids and the images from them that need sync/rollback.
622 *
623 * @param int $page The current page from the query.
624 * @param string $job The job name rollback_images/offload_images.
625 * @param int $batch How many posts to query on a page.
626 * @param array $page_in The pages that need to be updated.
627 *
628 * @return array An array containing the page of the query and an array containing the images for every post that need to be updated.
629 */
630 public function update_content( $page, $job, $batch = 1, $page_in = [] ) {
631 if ( OPTML_DEBUG_MEDIA ) {
632 do_action( 'optml_log', ' updating_content ' );
633 }
634 $post_types = array_values(
635 array_filter(
636 get_post_types(),
637 function( $post_type ) {
638 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
639 return false;
640 }
641 return true;
642 }
643 )
644 );
645 $query_args = apply_filters(
646 'optml_replacement_wp_query_args',
647 [
648 'post_type' => $post_types,
649 'post_status' => 'any',
650 'fields' => 'ids',
651 'posts_per_page' => $batch,
652 'update_post_meta_cache' => true,
653 'update_post_term_cache' => false,
654 ]
655 );
656
657 $query_args = self::add_page_meta_query_args( $job, $query_args );
658
659 if ( ! empty( $page_in ) ) {
660 $query_args['post__in'] = $page_in;
661 }
662
663 $content = new \WP_Query( $query_args );
664 if ( OPTML_DEBUG_MEDIA ) {
665 do_action( 'optml_log', $page );
666 }
667 $images_to_update = [];
668 if ( $content->have_posts() ) {
669 while ( $content->have_posts() ) {
670 $content->the_post();
671 $content_id = get_the_ID();
672 if ( get_post_type() !== 'attachment' ) {
673 $ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job );
674 if ( count( $ids ) > 0 ) {
675 $images_to_update[ $content_id ] = $ids;
676 $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
677 if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) {
678 foreach ( $duplicated_pages as $duplicated_id ) {
679 $duplicated_ids = $this->get_image_id_from_content( get_post_field( 'post_content', $duplicated_id ), $job );
680 $images_to_update[ $duplicated_id ] = $duplicated_ids;
681 }
682 }
683 }
684 if ( $job === 'offload_images' ) {
685 update_post_meta( $content_id, self::POST_OFFLOADED_FLAG, 'true' );
686 delete_post_meta( $content_id, self::POST_ROLLBACK_FLAG );
687 }
688 if ( $job === 'rollback_images' ) {
689 update_post_meta( $content_id, self::POST_ROLLBACK_FLAG, 'true' );
690 delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG );
691 }
692 }
693 }
694 $page ++;
695 }
696 $result['page'] = $page;
697 $result['imagesToUpdate'] = $images_to_update;
698 return $result;
699 }
700
701 /**
702 * Add inline action to push to our servers.
703 *
704 * @param array $actions All actions.
705 * @param \WP_Post $post The current post image object.
706 *
707 * @return array
708 */
709 public function add_inline_media_action( $actions, $post ) {
710 $meta = wp_get_attachment_metadata( $post->ID );
711 if ( ! isset( $meta['file'] ) ) {
712 return $actions;
713 }
714 $file = $meta['file'];
715 if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) {
716 return $actions;
717 }
718 if ( ! self::is_uploaded_image( $file ) ) {
719 $upload_action_url = add_query_arg(
720 [
721 'page' => 'optimole',
722 'optimole_action' => 'offload_images',
723 '0' => $post->ID,
724 ],
725 'admin.php'
726 );
727
728 $actions['offload_images'] = sprintf(
729 '<a href="%s" aria-label="%s">%s</a>',
730 $upload_action_url,
731 esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
732 esc_html__( 'Offload to Optimole', 'optimole-wp' )
733 );
734 }
735 if ( self::is_uploaded_image( $file ) ) {
736 $rollback_action_url = add_query_arg(
737 [
738 'page' => 'optimole',
739 'optimole_action' => 'rollback_images',
740 '0' => $post->ID,
741 ],
742 'admin.php'
743 );
744 $actions['rollback_images'] = sprintf(
745 '<a href="%s" aria-label="%s">%s</a>',
746 $rollback_action_url,
747 esc_attr__( 'Restore image to media library', 'optimole-wp' ),
748 esc_html__( 'Restore image to media library', 'optimole-wp' )
749 );
750 }
751 return $actions;
752 }
753
754 /**
755 * Upload images to our servers and update inside pages.
756 *
757 * @param array $image_ids The id of the attachments for the selected images.
758 * @return int The number of successfully processed images.
759 */
760 public function upload_and_update_existing_images( $image_ids ) {
761 $success_up = 0;
762 if ( OPTML_DEBUG_MEDIA ) {
763 do_action( 'optml_log', ' images to upload ' );
764 do_action( 'optml_log', $image_ids );
765 }
766 foreach ( $image_ids as $id ) {
767 if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) {
768 // if this meta flag below failed at the initial update but the file meta above is updated it will cause an infinite query loop
769 update_post_meta( $id, 'optimole_offload', 'true' );
770 $success_up ++;
771 continue;
772 }
773
774 $meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id );
775 if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) {
776 $success_up ++;
777 wp_update_attachment_metadata( $id, $meta );
778 }
779 }
780 if ( $success_up > 0 ) {
781 if ( OPTML_DEBUG_MEDIA ) {
782 do_action( 'optml_log', ' call post update, succesful images: ' );
783 do_action( 'optml_log', $success_up );
784 }
785 }
786 return $success_up;
787 }
788
789 /**
790 * Return the original url of an image attachment.
791 *
792 * @param integer $post_id Image attachment id.
793 * @return string|bool The original url of the image.
794 */
795 public static function get_original_url( $post_id ) {
796 self::$return_original_url = true;
797 $original_url = wp_get_attachment_url( $post_id );
798 self::$return_original_url = false;
799 return $original_url;
800 }
801
802 /**
803 * Bring images back to media library and update inside pages.
804 *
805 * @param array $image_ids The id of the attachments for the selected images.
806 * @return int The number of successfully processed images.
807 */
808 public function rollback_and_update_images( $image_ids ) {
809 $success_back = 0;
810 if ( OPTML_DEBUG_MEDIA ) {
811 do_action( 'optml_log', ' images to rollback ' );
812 do_action( 'optml_log', $image_ids );
813 }
814
815 foreach ( $image_ids as $id ) {
816 // Skip DAM attachment filtering.
817 if ( $this->is_dam_imported_image( $id ) ) {
818 continue;
819 }
820 $current_meta = wp_get_attachment_metadata( $id );
821 if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) {
822 delete_post_meta( $id, self::META_KEYS['offloaded'] );
823 delete_post_meta( $id, self::OM_OFFLOADED_FLAG );
824 $success_back++;
825 continue;
826 }
827 $table_id = [];
828 // Account for scaled images.
829 $source_file = isset( $current_meta['original_image'] ) ? $current_meta['original_image'] : $current_meta['file']; // @phpstan-ignore-line - this exists for scaled images.
830 $filename = pathinfo( $source_file, PATHINFO_BASENAME );
831 preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $table_id );
832 if ( ! isset( $table_id[1] ) ) {
833 continue;
834 }
835 $table_id = $table_id[1];
836 if ( OPTML_DEBUG_MEDIA ) {
837 do_action( 'optml_log', ' image cloud id ' );
838 do_action( 'optml_log', $table_id );
839 }
840 $request = new Optml_Api();
841 $get_response = $request->call_upload_api( '', 'false', $table_id, 'false', 'true' );
842
843 if ( is_wp_error( $get_response ) || wp_remote_retrieve_response_code( $get_response ) !== 200 ) {
844 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
845 if ( OPTML_DEBUG_MEDIA ) {
846 do_action( 'optml_log', ' error get url' );
847 }
848
849 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
850 continue;
851 }
852
853 $get_url = json_decode( $get_response['body'], true )['getUrl'];
854
855 if ( ! function_exists( 'download_url' ) ) {
856 include_once ABSPATH . 'wp-admin/includes/file.php';
857 }
858 if ( ! function_exists( 'download_url' ) ) {
859 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
860 continue;
861 }
862 $timeout_seconds = 60;
863 $temp_file = download_url( $get_url, $timeout_seconds );
864
865 if ( is_wp_error( $temp_file ) ) {
866 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
867 if ( OPTML_DEBUG_MEDIA ) {
868 do_action( 'optml_log', ' download_url error ' );
869 }
870
871 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
872 continue;
873 }
874
875 $extension = $this->get_ext( $filename );
876
877 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
878 if ( OPTML_DEBUG_MEDIA ) {
879 do_action( 'optml_log', ' image has invalid extension' );
880 do_action( 'optml_log', $extension );
881 }
882 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
883
884 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has invalid extension.' );
885 continue;
886 }
887
888 $type = Optml_Config::$image_extensions [ $extension ];
889 $file = [
890 'name' => $filename,
891 'type' => $type,
892 'tmp_name' => $temp_file,
893 'error' => 0,
894 'size' => filesize( $temp_file ),
895 ];
896
897 $overrides = [
898 // do not expect the default form data from normal uploads
899 'test_form' => false,
900
901 // Setting this to false lets WordPress allow empty files, not recommended.
902 'test_size' => true,
903
904 // A properly uploaded file will pass this test. There should be no reason to override this one.
905 'test_upload' => true,
906 ];
907
908 if ( ! function_exists( 'wp_handle_sideload' ) ) {
909 include_once ABSPATH . '/wp-admin/includes/file.php';
910 }
911 if ( ! function_exists( 'wp_handle_sideload' ) ) {
912 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
913 continue;
914 }
915
916 // Move the temporary file into the uploads directory.
917 $upload_date = $this->is_new_offloaded_attachment( $id ) ? get_the_date( 'Y/m', $id ) : null;
918 $results = wp_handle_sideload( $file, $overrides, $upload_date );
919 if ( ! empty( $results['error'] ) ) {
920 if ( OPTML_DEBUG_MEDIA ) {
921 do_action( 'optml_log', ' wp_handle_sideload error' );
922 }
923 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
924
925 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' faced wp_handle_sideload error.' );
926 continue;
927 }
928
929 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
930 include_once ABSPATH . '/wp-admin/includes/image.php';
931 }
932 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
933 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
934 continue;
935 }
936 $original_meta = wp_create_image_subsizes( $results['file'], $id );
937 if ( $type === 'image/svg+xml' ) {
938 if ( ! function_exists( 'wp_get_attachment_metadata' ) || ! function_exists( 'wp_update_attachment_metadata' ) ) {
939 include_once ABSPATH . '/wp-admin/includes/post.php';
940 }
941 if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
942 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
943 continue;
944 }
945 $meta = wp_get_attachment_metadata( $id );
946 if ( ! isset( $meta['file'] ) ) {
947 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
948 continue;
949 }
950 $meta['file'] = $results['file'];
951 wp_update_attachment_metadata( $id, $meta );
952 }
953
954 if ( ! function_exists( 'update_attached_file' ) ) {
955 include_once ABSPATH . '/wp-admin/includes/post.php';
956 }
957 if ( ! function_exists( 'update_attached_file' ) ) {
958 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
959 continue;
960 }
961 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id );
962 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
963 foreach ( $duplicated_images as $duplicated_id ) {
964 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
965 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
966 $duplicated_meta['file'] = $results['file'];
967 if ( isset( $meta ) ) {
968 foreach ( $meta['sizes'] as $key => $value ) {
969 if ( isset( $original_meta['sizes'][ $key ]['file'] ) ) {
970 $duplicated_meta['sizes'][ $key ]['file'] = $original_meta['sizes'][ $key ]['file'];
971 }
972 }
973 }
974 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
975 delete_post_meta( $duplicated_id, self::META_KEYS['offloaded'] );
976 delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
977 }
978 }
979 }
980 $success_back++;
981
982 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
983
984 $original_url = self::get_original_url( $id );
985 if ( $original_url === false ) {
986 continue;
987 }
988 $this->delete_attachment_from_server( $original_url, $id, $table_id );
989 }
990
991 if ( $success_back > 0 ) {
992 if ( OPTML_DEBUG_MEDIA ) {
993 do_action( 'optml_log', ' call update post, success rollback' );
994 do_action( 'optml_log', $success_back );
995 }
996 }
997 return $success_back;
998 }
999
1000 /**
1001 * Handle the bulk actions.
1002 *
1003 * @param string $redirect The current url from the media library.
1004 * @param string $doaction The current action selected.
1005 * @param array $image_ids The id of the attachments for the selected images.
1006 * @return string The url with the correspondent query args for the executed actions.
1007 */
1008 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
1009
1010 if ( empty( $image_ids ) ) {
1011 return $redirect;
1012 }
1013
1014 $image_ids = array_slice( $image_ids, 0, 20, true );
1015 $redirect = 'admin.php';
1016 $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
1017 $redirect = add_query_arg( 'page', 'optimole', $redirect );
1018 $redirect = add_query_arg( $image_ids, $redirect );
1019 return $redirect;
1020
1021 }
1022
1023 /**
1024 * Register the bulk media actions.
1025 *
1026 * @param array $bulk_array The existing actions array.
1027 * @return array The array with the appended actions.
1028 */
1029 public function register_bulk_media_actions( $bulk_array ) {
1030
1031 $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1032 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
1033 return $bulk_array;
1034
1035 }
1036
1037 /**
1038 * Send delete request to our servers and update the meta.
1039 *
1040 * @param string $original_url Original url of the image.
1041 * @param integer $post_id Image id inside db.
1042 * @param string $table_id Our cloud id for the image.
1043 */
1044 public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
1045 $request = new Optml_Api();
1046 $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
1047
1048 delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
1049 delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
1050 if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
1051 // should add some routine to retry delete once if delete fails
1052 }
1053 }
1054
1055 /**
1056 * Delete an image from our servers after it is removed from media.
1057 *
1058 * @param int $post_id The deleted post id.
1059 */
1060 public function delete_attachment_hook( $post_id ) {
1061 $file = wp_get_attachment_metadata( $post_id );
1062 if ( $file === false ) {
1063 return;
1064 }
1065
1066 // Skip if the image was imported from cloud library.
1067 if ( $this->is_dam_imported_image( $post_id ) ) {
1068 return;
1069 }
1070
1071 if ( ! $this->is_new_offloaded_attachment( $post_id ) && ! $this->is_legacy_offloaded_attachment( $post_id ) ) {
1072 return;
1073 }
1074
1075 $file = $file['file'];
1076 if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
1077 $original_url = self::get_original_url( $post_id );
1078 if ( $original_url === false ) {
1079 return;
1080 }
1081 $table_id = [];
1082
1083 preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $file, $table_id );
1084
1085 if ( ! isset( $table_id[1] ) ) {
1086 return;
1087 }
1088 $this->delete_attachment_from_server( $original_url, $post_id, $table_id[1] );
1089 }
1090 }
1091
1092 /**
1093 * Get optimized URL for an attachment image if it is uploaded to our servers.
1094 *
1095 * @param string $url The current url.
1096 * @param int $attachment_id The attachment image id.
1097 * @return string Optimole cdn URL.
1098 * @uses filter:wp_get_attachment_url
1099 */
1100 public function get_image_attachment_url( $url, $attachment_id ) {
1101 if ( self::$return_original_url === true ) {
1102 return $url;
1103 }
1104
1105 if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
1106 $meta = wp_get_attachment_metadata( $attachment_id );
1107 if ( ! isset( $meta['file'] ) ) {
1108 return $url;
1109 }
1110
1111 // Skip DAM attachment filtering.
1112 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1113 return $url;
1114 }
1115
1116 $file = $meta['file'];
1117 if ( self::is_uploaded_image( $file ) ) {
1118 $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1119 return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1120 } else {
1121 // this is for the users that already offloaded the images before the other fixes
1122 $local_file = get_attached_file( $attachment_id );
1123 if ( ! file_exists( $local_file ) ) {
1124 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1125 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1126 foreach ( $duplicated_images as $id ) {
1127 if ( ! empty( $id ) ) {
1128 $duplicated_meta = wp_get_attachment_metadata( $id );
1129 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1130 $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1131 return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1132 }
1133 }
1134 }
1135 }
1136 }
1137 }
1138 return $url;
1139 }
1140
1141 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1142 return $url;
1143 }
1144
1145 return $this->get_new_offloaded_attachment_url( $url, $attachment_id );
1146 }
1147
1148 /**
1149 * Filter the requested image url.
1150 *
1151 * @param bool|array $image The previous image value (null).
1152 * @param int $attachment_id The ID of the attachment.
1153 * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1154 *
1155 * @return bool|array The image sizes and optimized url.
1156 * @uses filter:image_downsize
1157 */
1158 public function generate_filter_downsize_urls( $image, $attachment_id, $size ) {
1159 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1160 return $image;
1161 }
1162
1163 if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
1164 if ( self::$return_original_url === true ) {
1165 return $image;
1166 }
1167
1168 $sizes2crop = self::size_to_crop();
1169 if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
1170 return $image;
1171 }
1172 $data = image_get_intermediate_size( $attachment_id, $size );
1173 if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) {
1174 return $image;
1175 }
1176 $resize = apply_filters( 'optml_default_crop', [] );
1177 if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
1178 $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
1179 }
1180 $id_filename = [];
1181
1182 preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename );
1183 if ( ! isset( $id_filename[1] ) ) {
1184 return $image;
1185 }
1186 $url = self::get_original_url( $attachment_id );
1187 $optimized_url = ( new Optml_Image(
1188 $url,
1189 [
1190 'width' => $data['width'],
1191 'height' => $data['height'],
1192 'resize' => $resize,
1193 'quality' => $this->settings->get_numeric_quality(),
1194 ],
1195 $this->settings->get( 'cache_buster' )
1196 ) )->get_url();
1197 $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1198 $image = [
1199 $optimized_url,
1200 $data['width'],
1201 $data['height'],
1202 true,
1203 ];
1204
1205 return $image;
1206 }
1207
1208 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1209 return $image;
1210 }
1211
1212 return $this->alter_attachment_image_src( $image, $attachment_id, $size, false );
1213 }
1214
1215 /**
1216 * Get image extension.
1217 *
1218 * @param string $path Image path.
1219 *
1220 * @return string
1221 */
1222 private function get_ext( $path ) {
1223 return pathinfo( $path, PATHINFO_EXTENSION );
1224 }
1225
1226 /**
1227 * Update image meta with optimized cdn path.
1228 *
1229 * @param array $meta Meta information of the image.
1230 * @param int $attachment_id The image attachment ID.
1231 *
1232 * @return array
1233 * @uses filter:wp_generate_attachment_metadata
1234 */
1235 public function generate_image_meta( $meta, $attachment_id ) {
1236
1237 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1238 return $meta;
1239 }
1240
1241 if ( OPTML_DEBUG_MEDIA ) {
1242 do_action( 'optml_log', 'called generate meta' );
1243 }
1244 // No meta, or image was already uploaded.
1245 if ( ! isset( $meta['file'] ) || ! isset( $meta['width'] ) || ! isset( $meta['height'] ) || self::is_uploaded_image( $meta['file'] ) ) {
1246 do_action( 'optml_log', 'invalid meta' );
1247 do_action( 'optml_log', $meta );
1248 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1249
1250 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
1251 return $meta;
1252 }
1253 // Skip images based on filters.
1254 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1255 do_action( 'optml_log', 'optimization filter' );
1256 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1257 return $meta;
1258 }
1259 $original_url = self::get_original_url( $attachment_id );
1260
1261 // Could not find original URL.
1262 if ( $original_url === false ) {
1263 do_action( 'optml_log', 'error getting original url' );
1264 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1265
1266 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
1267 return $meta;
1268 }
1269
1270 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
1271 $original_url = $this->maybe_strip_scaled( $original_url );
1272 $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
1273
1274 $extension = $this->get_ext( $local_file );
1275 $content_type = Optml_Config::$image_extensions [ $extension ];
1276 $temp = explode( '/', $local_file );
1277 $file_name = end( $temp );
1278 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1279 $original_name = $file_name;
1280 if ( OPTML_DEBUG_MEDIA ) {
1281 do_action( 'optml_log', 'file before replace' );
1282 do_action( 'optml_log', $local_file );
1283 }
1284
1285 // check if the current filename is the last deduplicated filename
1286 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1287 // replace the file with the original before deduplication to get the path where the image is uploaded
1288 $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1289 $original_name = self::$last_deduplicated;
1290 self::$last_deduplicated = false;
1291 }
1292 if ( OPTML_DEBUG_MEDIA ) {
1293 do_action( 'optml_log', 'file after replace' );
1294 do_action( 'optml_log', $local_file );
1295 }
1296 if ( ! file_exists( $local_file ) ) {
1297 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1298 do_action( 'optml_log', 'missing file' );
1299 do_action( 'optml_log', $local_file );
1300
1301 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1302 return $meta;
1303 }
1304
1305 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
1306 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1307 do_action( 'optml_log', 'invalid extension' );
1308 do_action( 'optml_log', $extension );
1309
1310 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
1311 return $meta;
1312 }
1313 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1314 do_action( 'optml_log', 'extension filter' );
1315 do_action( 'optml_log', $extension );
1316 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1317 return $meta;
1318 }
1319
1320 $request = new Optml_Api();
1321 $generate_url_response = $request->call_upload_api( $original_url );
1322
1323 if ( is_wp_error( $generate_url_response ) || wp_remote_retrieve_response_code( $generate_url_response ) !== 200 ) {
1324 if ( OPTML_DEBUG_MEDIA ) {
1325 do_action( 'optml_log', ' call to signed url error' );
1326 do_action( 'optml_log', $generate_url_response );
1327 }
1328 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1329
1330 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid signed url.' );
1331 return $meta;
1332 }
1333 $decoded_response = json_decode( $generate_url_response['body'], true );
1334
1335 if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1336 if ( OPTML_DEBUG_MEDIA ) {
1337 do_action( 'optml_log', ' missing table id or upload url' );
1338 do_action( 'optml_log', $decoded_response );
1339 }
1340 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1341
1342 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
1343 return $meta;
1344 }
1345 $table_id = $decoded_response['tableId'];
1346 if ( OPTML_DEBUG_MEDIA ) {
1347 do_action( 'optml_log', ' table id' );
1348 do_action( 'optml_log', $table_id );
1349 }
1350 $upload_signed_url = $decoded_response['uploadUrl'];
1351 $image = file_get_contents( $local_file );
1352 if ( $image === false ) {
1353 do_action( 'optml_log', 'can not find file' );
1354 do_action( 'optml_log', $local_file );
1355 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1356
1357 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1358 return $meta;
1359 }
1360 if ( $upload_signed_url !== 'found_resource' ) {
1361
1362 $request = new Optml_Api();
1363 $result = $request->upload_image( $upload_signed_url, $content_type, $image );
1364
1365 if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1366 do_action( 'optml_log', 'upload error' );
1367 do_action( 'optml_log', $result );
1368 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1369
1370 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1371 return $meta;
1372 }
1373 $file_size = filesize( $local_file );
1374 if ( $file_size === false ) {
1375 $file_size = 0;
1376 }
1377 $request = new Optml_Api();
1378 $result_update = $request->call_upload_api(
1379 $original_url,
1380 'false',
1381 $table_id,
1382 'success',
1383 'false',
1384 $meta['width'],
1385 $meta['height'],
1386 $file_size
1387 );
1388 if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1389 do_action( 'optml_log', 'dynamo update error' );
1390 do_action( 'optml_log', $result_update );
1391 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1392
1393 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has dynamo update error.' );
1394 return $meta;
1395 }
1396 }
1397 $url_to_append = $original_url;
1398 $url_parts = parse_url( $original_url );
1399
1400 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1401 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1402 }
1403 $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1404 $request = new Optml_Api();
1405 if ( $request->check_optimized_url( $optimized_url ) === false ) {
1406 do_action( 'optml_log', 'optimization error' );
1407 do_action( 'optml_log', $optimized_url );
1408 $request->call_upload_api( $original_url, 'true', $table_id );
1409 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1410
1411 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
1412 return $meta;
1413 }
1414 unlink( $local_file );
1415 update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
1416 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1417 $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1418
1419 if ( isset( $meta['sizes'] ) ) {
1420 foreach ( $meta['sizes'] as $key => $value ) {
1421 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1422 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
1423 $meta['sizes'][ $key ]['file'] = $file_name;
1424 }
1425 }
1426
1427 // This is needed for scaled images.
1428 // Otherwise, `-scaled` images will be left behind.
1429 if ( isset( $meta['original_image'] ) ) {
1430 $ext = $this->get_ext( $local_file );
1431 $scaled_path = str_replace( '.' . $ext, '-scaled.' . $ext, $local_file );
1432 file_exists( $scaled_path ) && unlink( $scaled_path );
1433 }
1434
1435 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1436
1437 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1438 foreach ( $duplicated_images as $duplicated_id ) {
1439 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
1440 if ( isset( $duplicated_meta['file'] ) && ! self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1441 $duplicated_meta['file'] = $meta['file'];
1442 if ( $duplicated_meta['sizes'] ) {
1443 foreach ( $meta['sizes'] as $key => $value ) {
1444 $duplicated_meta['sizes'][ $key ]['file'] = $file_name;
1445 }
1446 }
1447 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
1448 update_post_meta( $duplicated_id, self::META_KEYS['offloaded'], 'true' );
1449 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1450 }
1451 }
1452 }
1453 if ( OPTML_DEBUG_MEDIA ) {
1454 do_action( 'optml_log', 'success offload' );
1455 }
1456
1457 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' );
1458 $attachment_page_id = wp_get_post_parent_id( $attachment_id );
1459
1460 if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
1461 self::$offload_update_post = true;
1462 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1463 self::$offload_update_post = false;
1464 }
1465 return $meta;
1466 }
1467
1468 /**
1469 * Get the args for wp query according to the scope.
1470 *
1471 * @param int $batch Number of images to get.
1472 * @param string $action The action for which to get the images.
1473 * @return array|false The query options array or false if not passed a valid action.
1474 */
1475 public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
1476
1477 $args = [
1478 'posts_per_page' => $batch,
1479 'fields' => 'ids',
1480 'ignore_sticky_posts' => false,
1481 'no_found_rows' => true,
1482 ];
1483
1484 if ( $get_images === true ) {
1485 $args['post_type'] = 'attachment';
1486 $args['post_mime_type'] = 'image';
1487 $args['post_status'] = 'inherit';
1488
1489 // Offload args.
1490 if ( $action === 'offload_images' ) {
1491 $args['meta_query'] = [
1492 'relation' => 'AND',
1493 [
1494 'key' => self::META_KEYS['offloaded'],
1495 'compare' => 'NOT EXISTS',
1496 ],
1497 [
1498 'key' => self::META_KEYS['offload_error'],
1499 'compare' => 'NOT EXISTS',
1500 ],
1501 ];
1502 return $args;
1503 }
1504
1505 // Rollback args.
1506 $args['meta_query'] = [
1507 'relation' => 'AND',
1508 [
1509 'key' => self::META_KEYS['offloaded'],
1510 'value' => 'true',
1511 'compare' => '=',
1512 ],
1513 [
1514 'key' => self::META_KEYS['rollback_error'],
1515 'compare' => 'NOT EXISTS',
1516 ],
1517 [
1518 'key' => Optml_Dam::OM_DAM_IMPORTED_FLAG,
1519 'compare' => 'NOT EXISTS',
1520 ],
1521 ];
1522
1523 return $args;
1524 }
1525
1526 $args = self::add_page_meta_query_args( $action, $args );
1527 $post_types = array_filter(
1528 get_post_types(),
1529 function ( $post_type ) {
1530 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
1531 return false;
1532 }
1533
1534 return true;
1535 }
1536 );
1537
1538 $args ['post_type'] = array_values( $post_types );
1539
1540 return $args;
1541 }
1542
1543 /**
1544 * Query the database and upload images to our servers.
1545 *
1546 * @param int $batch Number of images to process in a batch.
1547 * @return array Number of found images and number of successfully processed images.
1548 */
1549 public function upload_images( $batch, $images = [] ) {
1550 if ( empty( $images ) || $images === 'none' ) {
1551 $args = self::get_images_or_pages_query_args( $batch, 'offload_images', true );
1552 $attachments = new \WP_Query( $args );
1553 $ids = $attachments->get_posts();
1554 } else {
1555 $ids = array_slice( $images, 0, $batch );
1556 }
1557 $result = [ 'found_images' => count( $ids ) ];
1558 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1559 return $result;
1560 }
1561
1562 /**
1563 * Query the database and bring back image to media library.
1564 *
1565 * @param int $batch Number of images to process in a batch.
1566 * @return array Number of found images and number of successfully processed images.
1567 */
1568 public function rollback_images( $batch, $images = [] ) {
1569 if ( empty( $images ) || $images === 'none' ) {
1570 $args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true );
1571 $attachments = new \WP_Query( $args );
1572 $ids = $attachments->get_posts();
1573 } else {
1574 $ids = array_slice( $images, 0, $batch );
1575 }
1576 $result = [ 'found_images' => count( $ids ) ];
1577 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1578 return $result;
1579 }
1580
1581 /**
1582 * Update the post with the given id, the images will be updated by the filters we use.
1583 *
1584 * @param int $post_id The post id to update.
1585 * @return bool Whether the update was succesful or not.
1586 */
1587 public function update_page( $post_id ) {
1588 self::$offload_update_post = true;
1589 $post_update = wp_update_post( ['ID' => $post_id] );
1590 self::$offload_update_post = false;
1591 if ( $post_update === 0 ) {
1592 return false;
1593 }
1594 do_action( 'optml_updated_post', $post_id );
1595 return true;
1596 }
1597
1598 /**
1599 * Calculate the number of images in media library and the number of posts/pages.
1600 *
1601 * @param string $action The actions for which to get the number of images.
1602 * @return int Number of images.
1603 */
1604 public static function number_of_images_and_pages( $action ) {
1605 $images_args = self::get_images_or_pages_query_args( -1, $action, true );
1606
1607 $images = new \WP_Query( $images_args );
1608
1609 // With the new mechanism, when offloading images, we don't need to address pages anymore.
1610 // Bail early with the number of images.
1611 if ( $action === 'offload_images' ) {
1612 return $images->post_count;
1613 }
1614
1615 $pages_args = self::get_images_or_pages_query_args( -1, $action );
1616 $pages = new \WP_Query( $pages_args );
1617
1618 return $pages->post_count + $images->post_count;
1619 }
1620
1621 /**
1622 * Calculate the number of images in media library and the number of posts/pages by IDs.
1623 *
1624 * @param string $action The actions for which to get the number of images.
1625 * @return int Number of images.
1626 */
1627 public static function number_of_images_by_ids( $action, $ids ) {
1628 $args = self::get_images_or_pages_query_args( - 1, $action, true );
1629 $args['post__in'] = $ids;
1630 $images = new \WP_Query( $args );
1631
1632 return $images->post_count;
1633 }
1634
1635 /**
1636 * Get pages that contain images by IDs.
1637 *
1638 * @param string $action The actions for which to get the number of images.
1639 * @param array $images Image IDs.
1640 * @param int $batch Batch count.
1641 * @param int $page Page number.
1642 */
1643 public static function get_posts_by_image_ids( $action, $images = [], $batch = 10, $page = 1 ) {
1644 if ( empty( $images ) ) {
1645 return [];
1646 }
1647
1648 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1649 $transient = get_transient( $transient_key );
1650
1651 if ( false !== $transient ) {
1652 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1653 }
1654
1655 global $wpdb;
1656
1657 $image_urls = array_map(
1658 function( $image_id ) {
1659 $meta = wp_get_attachment_metadata( $image_id );
1660 $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1661
1662 return str_replace( '.' . $extension, '', $meta['file'] );
1663 },
1664 $images
1665 );
1666
1667 // Sanitize the image URLs for use in the SQL query.
1668 $urls = array_map( 'esc_url_raw', $image_urls );
1669
1670 // Initialize an empty string to hold the query.
1671 $query = '';
1672
1673 // Iterate through the array and add each URL to the query.
1674 foreach ( $urls as $index => $url ) {
1675 // If it's the first item, we don't need to add OR to the beginning.
1676 if ( $index === 0 ) {
1677 $query .= $wpdb->prepare( 'post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
1678 } else {
1679 $query .= $wpdb->prepare( ' OR post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
1680 }
1681 }
1682
1683 // Get all the posts IDs by using LIMIT and offset in a loop.
1684 $ids = [];
1685 $offset = 0;
1686 $limit = $batch;
1687
1688 while ( true ) {
1689 $posts = $wpdb->get_col(
1690 $wpdb->prepare(
1691 "SELECT ID FROM $wpdb->posts WHERE $query LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1692 $limit,
1693 $offset
1694 )
1695 );
1696
1697 if ( empty( $posts ) ) {
1698 break;
1699 }
1700
1701 $ids = array_merge( $ids, $posts );
1702 $offset += $limit;
1703 }
1704
1705 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
1706
1707 return array_slice( $ids, ( $page - 1 ) * $batch, $batch );
1708 }
1709
1710 /**
1711 * Record process meta,
1712 *
1713 * @param int $count The number of images to process.
1714 *
1715 * @return void
1716 */
1717 public static function record_process_meta( $count ) {
1718 $meta = get_option( 'optml_process_meta', [] );
1719 $meta['count'] = $count;
1720 $meta['start_time'] = time();
1721 update_option( 'optml_process_meta', $meta );
1722 }
1723
1724 /**
1725 * Get process meta,
1726 *
1727 * @return array
1728 */
1729 public static function get_process_meta() {
1730 $res = [];
1731 $meta = get_option( 'optml_process_meta', [] );
1732 $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1733 $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1734 return $res;
1735 }
1736
1737 /**
1738 * Calculate the number of images in media library and the number of posts/pages.
1739 *
1740 * @param string $action The actions for which to get the number of images.
1741 * @param bool $refresh Whether to refresh the cron or not.
1742 * @param array $images The images to process.
1743 *
1744 * @return array Image count and Cron status.
1745 */
1746 public static function get_image_count( $action, $refresh, $images = [] ) {
1747 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1748 $count = 0;
1749 $step = 0;
1750 $batch = 50; // Reduce this to 20 if we have memory issues during testing.
1751
1752 if ( empty( $images ) ) {
1753 $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1754 } else {
1755 $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1756 }
1757
1758 $possible_batch = ceil( $count / 10 );
1759
1760 if ( $possible_batch < $batch ) {
1761 $batch = $possible_batch;
1762 }
1763
1764 // If batch is less than 10, set it to 10.
1765 if ( $batch < 10 ) {
1766 $batch = 10;
1767 }
1768
1769 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1770
1771 if ( false === $refresh && empty( $images ) ) {
1772 $total = ceil( $count / $batch );
1773
1774 $in_progress = 0 !== $count;
1775
1776 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1777
1778 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1779 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1780 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1781 self::record_process_meta( $count );
1782
1783 if ( true === $in_progress ) {
1784 wp_schedule_single_event(
1785 time(),
1786 'optml_start_processing_images',
1787 [
1788 $action,
1789 $batch,
1790 1,
1791 $total,
1792 $step,
1793 ]
1794 );
1795 }
1796 }
1797
1798 if ( false === $refresh && ! empty( $images ) ) {
1799 $in_progress = 0 !== $count;
1800
1801 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1802
1803 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1804 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1805 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1806 self::record_process_meta( $count );
1807
1808 if ( true === $in_progress ) {
1809 wp_schedule_single_event(
1810 time(),
1811 'optml_start_processing_images_by_id',
1812 [
1813 $action,
1814 $batch,
1815 1,
1816 $images,
1817 ]
1818 );
1819 }
1820 }
1821
1822 return [
1823 'count' => $count,
1824 'status' => $in_progress,
1825 'action' => $action === 'offload_images' ? 'offload' : 'rollback',
1826 ];
1827 }
1828
1829 /**
1830 * Start Processing Images by IDs
1831 *
1832 * @param string $action The action for which to get the number of images.
1833 * @param int $batch The batch of images to process.
1834 * @param int $page The page of images to process.
1835 * @param array $image_ids The images to process.
1836 *
1837 * @return void
1838 */
1839 public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1840 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1841 $type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK;
1842
1843 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1844 return;
1845 }
1846
1847 set_time_limit( 0 );
1848
1849 // Only use the legacy offloaded attachments to query the pages that need to be updated.
1850 // We can be confident that these IDs are already marked as offloaded.
1851 $legacy_offloaded = array_filter(
1852 $image_ids,
1853 function( $id ) {
1854 return ! $this->is_new_offloaded_attachment( $id );
1855 }
1856 );
1857
1858 // On the new mechanism, we don't update posts anymore when offloading.
1859 $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
1860
1861 if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
1862 $meta = self::get_process_meta();
1863 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1864
1865 self::$instance->settings->update( $option, 'disabled' );
1866 return;
1867 }
1868
1869 try {
1870 // This will be 0 in the case of offloading now.
1871 if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
1872 $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
1873
1874 if ( isset( $to_update['page'] ) ) {
1875 if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
1876 foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
1877 if ( ! empty( $image_ids ) ) {
1878 $images = array_intersect( $images, $image_ids );
1879 }
1880
1881 if ( empty( $images ) ) {
1882 continue;
1883 }
1884
1885 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1886 Optml_Media_Offload::instance()->update_page( $post_id );
1887 }
1888 }
1889 }
1890
1891 $page = $page + 1;
1892 } else {
1893 // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1894 $images = array_slice( $image_ids, 0, $batch );
1895 $image_ids = array_slice( $image_ids, $batch );
1896 $action === 'rollback_images' ?
1897 Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
1898 Optml_Media_Offload::instance()->upload_images( $batch, $images );
1899 }
1900
1901 wp_schedule_single_event(
1902 time(),
1903 'optml_start_processing_images_by_id',
1904 [
1905 $action,
1906 $batch,
1907 $page,
1908 $image_ids,
1909 ]
1910 );
1911 } catch ( Exception $e ) {
1912 // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
1913 $delay_in_seconds = 10;
1914 self::$instance->logger->add_log( $type, $e->getMessage() );
1915
1916 wp_schedule_single_event(
1917 time() + $delay_in_seconds,
1918 'optml_start_processing_images_by_id',
1919 [
1920 $action,
1921 $batch,
1922 $page,
1923 $image_ids,
1924 ]
1925 );
1926 }
1927 }
1928
1929 /**
1930 * Start Processing Images
1931 *
1932 * @param string $action The action for which to get the number of images.
1933 * @param int $batch The batch of images to process.
1934 * @param int $page The page of images to process.
1935 * @param int $total The total number of pages.
1936 * @param int $step The current step.
1937 *
1938 * @return void
1939 */
1940 public function start_processing_images( $action, $batch, $page, $total, $step ) {
1941 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1942 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1943
1944 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1945 return;
1946 }
1947
1948 if ( $step > $total || 0 === $total ) {
1949 $meta = self::get_process_meta();
1950 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1951
1952 self::$instance->settings->update( $option, 'disabled' );
1953 return;
1954 }
1955
1956 set_time_limit( 0 );
1957
1958 try {
1959 $posts_to_update = $action === 'offload_images' ? [] : Optml_Media_Offload::instance()->update_content( $page, $action, $batch );
1960
1961 // Kept for backward compatibility with old offloading mechanism where pages were modified.
1962 if ( $action === 'rollback_images' && isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) {
1963 $page = $posts_to_update['page'];
1964 if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
1965 foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
1966 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1967 Optml_Media_Offload::instance()->update_page( $post_id );
1968 }
1969 }
1970 } else {
1971 $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch ) : Optml_Media_Offload::instance()->upload_images( $batch );
1972 }
1973
1974 $step = $step + 1;
1975
1976 wp_schedule_single_event(
1977 time(),
1978 'optml_start_processing_images',
1979 [
1980 $action,
1981 $batch,
1982 $page,
1983 $total,
1984 $step,
1985 ]
1986 );
1987 } catch ( Exception $e ) {
1988 // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
1989 $delay_in_seconds = 10;
1990 self::$instance->logger->add_log( $type, $e->getMessage() );
1991
1992 wp_schedule_single_event(
1993 time() + $delay_in_seconds,
1994 'optml_start_processing_images',
1995 [
1996 $action,
1997 $batch,
1998 $page,
1999 $total,
2000 $step,
2001 ]
2002 );
2003 }
2004 }
2005
2006 /**
2007 * Alter attachment image src for offloaded images.
2008 *
2009 * @param array|false $image {
2010 * Array of image data.
2011 *
2012 * @type string $0 Image source URL.
2013 * @type int $1 Image width in pixels.
2014 * @type int $2 Image height in pixels.
2015 * @type bool $3 Whether the image is a resized image.
2016 * }
2017 *
2018 * @param int $attachment_id attachment id.
2019 * @param string|int[] $size image size.
2020 * @param bool $icon Whether the image should be treated as an icon.
2021 *
2022 * @return array $image.
2023 */
2024 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2025 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2026 return $image;
2027 }
2028
2029 $url = get_post( $attachment_id );
2030 $url = $url->guid;
2031 $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id );
2032 $metadata = wp_get_attachment_metadata( $attachment_id );
2033
2034 // Use the original size if the requested size is full.
2035 if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
2036 $image_url = $this->get_new_offloaded_attachment_url(
2037 $url,
2038 $attachment_id,
2039 [
2040 'width' => $metadata['width'],
2041 'height' => $metadata['height'],
2042 'attachment_id' => $attachment_id,
2043 ]
2044 );
2045
2046 return [
2047 $image_url,
2048 $metadata['width'],
2049 $metadata['height'],
2050 false,
2051 ];
2052 }
2053
2054 $crop = false;
2055
2056 // Size can be int [] containing width and height.
2057 if ( is_array( $size ) ) {
2058 $width = $size[0];
2059 $height = $size[1];
2060 $crop = true;
2061 } else {
2062 $sizes = $this->get_all_image_sizes();
2063
2064 if ( ! isset( $sizes[ $size ] ) ) {
2065 return [
2066 $image_url,
2067 $metadata['width'],
2068 $metadata['height'],
2069 false,
2070 ];
2071 }
2072
2073 $width = $sizes[ $size ]['width'];
2074 $height = $sizes[ $size ]['height'];
2075 $crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
2076 }
2077
2078 $sizes2crop = self::size_to_crop();
2079
2080 if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
2081 return $image;
2082 }
2083
2084 $resize = apply_filters( 'optml_default_crop', [] );
2085 $data = image_get_intermediate_size( $attachment_id, $size );
2086
2087 if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
2088 if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
2089 $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
2090 }
2091 }
2092
2093 if ( $crop !== false ) {
2094 $resize = $this->to_optml_crop( $crop );
2095 }
2096
2097 $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, ['width' => $width, 'height' => $height, 'resize' => $resize, 'attachment_id' => $attachment_id] );
2098
2099 return [
2100 $image_url,
2101 $width,
2102 $height,
2103 $crop,
2104 ];
2105 }
2106
2107 /**
2108 * Needed for image sizes inside the editor.
2109 *
2110 * @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js().
2111 * @param WP_Post $attachment Attachment object.
2112 * @param array|false $meta Array of attachment meta data, or false if there is none.
2113 *
2114 * @return array
2115 */
2116 public function alter_attachment_for_js( $response, $attachment, $meta ) {
2117 if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2118 return $response;
2119 }
2120
2121 $sizes = $this->get_all_image_sizes();
2122
2123 foreach ( $sizes as $size => $args ) {
2124 if ( isset( $response['sizes'][ $size ] ) ) {
2125 continue;
2126 }
2127
2128 $args = [
2129 'height' => $args['height'],
2130 'width' => $args['width'],
2131 'crop' => true,
2132 ];
2133
2134 $response['sizes'][ $size ] = array_merge(
2135 $args,
2136 [
2137 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
2138 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2139 ]
2140 );
2141 }
2142
2143 $url_args = [
2144 'height' => $response['height'],
2145 'width' => $response['width'],
2146 'crop' => false,
2147 ];
2148
2149 $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
2150
2151 return $response;
2152 }
2153
2154 /**
2155 * Alter attachment metadata.
2156 *
2157 * @param array $metadata The attachment metadata.
2158 * @param int $id The attachment ID.
2159 *
2160 * @return array
2161 */
2162 public function alter_attachment_metadata( $metadata, $id ) {
2163 if ( ! $this->is_new_offloaded_attachment( $id ) ) {
2164 return $metadata;
2165 }
2166
2167 return $this->get_altered_metadata_for_remote_images( $metadata, $id );
2168 }
2169
2170 /**
2171 * Get offloaded image attachment URL for new offloads.
2172 *
2173 * @param string $url The initial attachment URL.
2174 * @param int $attachment_id The attachment ID.
2175 * @param array $args The additional arguments.
2176 * - width: The width of the image.
2177 * - height: The height of the image.
2178 * - crop: Whether to crop the image.
2179 *
2180 * @return string
2181 */
2182 private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [] ) {
2183 $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2184
2185 // Image might have already passed through this filter.
2186 if ( strpos( $url, $process_flag ) !== false ) {
2187 return $url;
2188 }
2189
2190 $meta = wp_get_attachment_metadata( $attachment_id );
2191 if ( ! isset( $meta['file'] ) ) {
2192 return $url;
2193 }
2194
2195 $default_args = [
2196 'width' => 'auto',
2197 'height' => 'auto',
2198 'resize' => apply_filters( 'optml_default_crop', [] ),
2199 ];
2200
2201 $args = wp_parse_args( $args, $default_args );
2202 // If this is not cropped, we constrain the dimensions to the original image.
2203 if ( empty( $args['resize'] ) && ! in_array( 'auto', [ $args['width'], $args['height'] ], true ) ) {
2204 $dimensions = wp_constrain_dimensions( $meta['width'], $meta['height'], $args['width'], $args['height'] );
2205 $args['width'] = $dimensions[0];
2206 $args['height'] = $dimensions[1];
2207 }
2208
2209 $file = $meta['file'];
2210 if ( self::is_uploaded_image( $file ) ) {
2211 $optimized_url = ( new Optml_Image(
2212 $url,
2213 [
2214 'width' => $args['width'],
2215 'height' => $args['height'],
2216 'quality' => $this->settings->get_numeric_quality(),
2217 'resize' => $args['resize'],
2218 'attachment_id' => $attachment_id,
2219 ],
2220 $this->settings->get( 'cache_buster' )
2221 ) )->get_url();
2222
2223 if ( strpos( $optimized_url, $process_flag ) !== false ) {
2224 return $optimized_url;
2225 }
2226
2227 $process_flag = $process_flag . $file;
2228
2229 return str_replace( '/' . $url, '/' . $process_flag, $optimized_url );
2230 } else {
2231 // this is for the users that already offloaded the images before the other fixes
2232 $local_file = get_attached_file( $attachment_id );
2233 if ( ! file_exists( $local_file ) ) {
2234 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
2235 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
2236 foreach ( $duplicated_images as $id ) {
2237 if ( ! empty( $id ) ) {
2238 $duplicated_meta = wp_get_attachment_metadata( $id );
2239 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2240 $optimized_url = ( new Optml_Image(
2241 $url,
2242 [
2243 'width' => $args['width'],
2244 'height' => $args['height'],
2245 'quality' => $this->settings->get_numeric_quality(),
2246 'attachment_id' => $attachment_id,
2247 ],
2248 $this->settings->get( 'cache_buster' )
2249 ) )->get_url();
2250 return $optimized_url;
2251 }
2252 }
2253 }
2254 }
2255 }
2256 }
2257 return $url;
2258 }
2259
2260 /**
2261 * Replace the URLs in the editor content with the offloaded ones.
2262 *
2263 * @param string $content The incoming content.
2264 *
2265 * @return string
2266 */
2267 public function replace_urls_in_editor_content( $content ) {
2268 $raw_extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
2269
2270 if ( empty( $raw_extracted ) ) {
2271 return $content;
2272 }
2273
2274 $to_replace = [];
2275 foreach ( $raw_extracted as $url ) {
2276 $attachment = $this->get_local_attachement_id_from_url( $url );
2277
2278 // No local attachment.
2279 if ( $attachment['attachment_id'] === 0 ) {
2280 continue;
2281 }
2282
2283 $attachment_id = $attachment['attachment_id'];
2284
2285 // Not offloaded.
2286 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2287 continue;
2288 }
2289
2290 // Get W/H from url.
2291 $size = $this->parse_dimensions_from_filename( $url );
2292 $width = $size[0] !== false ? $size[0] : 'auto';
2293 $height = $size[1] !== false ? $size[1] : 'auto';
2294
2295 // Handle resize.
2296 $sizes2crop = self::size_to_crop();
2297 $resize = apply_filters( 'optml_default_crop', [] );
2298 $sizes = image_get_intermediate_size( $attachment_id, $size );
2299 if ( false !== $sizes ) {
2300 if ( isset( $sizes2crop[ $width . $height ] ) ) {
2301 $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] );
2302 }
2303 }
2304
2305 // Build the optimized URL.
2306 $optimized_url = ( new Optml_Image(
2307 $url,
2308 [
2309 'width' => $width,
2310 'height' => $height,
2311 'quality' => $this->settings->get_numeric_quality(),
2312 'resize' => $resize,
2313 'attachment_id' => $attachment_id,
2314 ],
2315 $this->settings->get( 'cache_buster' )
2316 ) )->get_url();
2317
2318 // Drop any image size from the URL.
2319 $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2320
2321 $to_replace[ $url ] = $optimized_url;
2322 }
2323
2324 return str_replace( array_keys( $to_replace ), array_values( $to_replace ), $content );
2325 }
2326
2327 /**
2328 * Replaces the post content URLs to use Offloaded ones on editor fetch.
2329 *
2330 * @param \WP_REST_Response $response The response object.
2331 * @param \WP_Post $post The post object.
2332 * @param \WP_REST_Request $request The request object.
2333 *
2334 * @return \WP_REST_Response
2335 */
2336 public function pre_filter_rest_content( \WP_REST_Response $response, \WP_Post $post, \WP_REST_Request $request ) {
2337 $context = $request->get_param( 'context' );
2338
2339 if ( $context !== 'edit' ) {
2340 return $response;
2341 }
2342
2343 $data = $response->get_data();
2344
2345 // Actually replace all URLs.
2346 $data['content']['raw'] = $this->replace_urls_in_editor_content( $data['content']['raw'] );
2347
2348 $response->set_data( $data );
2349
2350 return $response;
2351 }
2352
2353 /**
2354 * Legacy function to be used for WordPress versions under 6.0.0.
2355 *
2356 * @param array $post_data Slashed, sanitized, processed post data.
2357 * @param array $postarr Slashed sanitized post data.
2358 * @param array $unsanitized_postarr Un-sanitized post data.
2359 *
2360 * @return array
2361 */
2362 public function legacy_filter_saved_data( $post_data, $postarr, $unsanitized_postarr ) {
2363 return $this->filter_saved_data( $post_data, $postarr, $unsanitized_postarr, true );
2364 }
2365
2366 /**
2367 * Filter post content to use local attachments when saving offloaded images.
2368 *
2369 * @param array $post_data Slashed, sanitized, processed post data.
2370 * @param array $postarr Slashed sanitized post data.
2371 * @param array $unsanitized_postarr Un-sanitized post data.
2372 * @param bool $update Whether this is an existing post being updated or not.
2373 *
2374 * @return array
2375 */
2376 public function filter_saved_data( $post_data, $postarr, $unsanitized_postarr, $update ) {
2377 if ( $postarr['post_status'] === 'trash' ) {
2378 return $post_data;
2379 }
2380
2381 $content = $post_data['post_content'];
2382
2383 $extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
2384 $replace = [];
2385
2386 foreach ( $extracted as $idx => $url ) {
2387 $id = self::get_attachment_id_from_url( $url );
2388
2389 if ( $id === false ) {
2390 continue;
2391 }
2392
2393 $id = (int) $id;
2394
2395 if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2396 continue;
2397 }
2398
2399 $replace[ $url ] = self::get_original_url( $id );
2400
2401 $size = $this->parse_dimension_from_optimized_url( $url );
2402
2403 if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2404 continue;
2405 }
2406
2407 $extension = $this->get_ext( $url );
2408 $metadata = wp_get_attachment_metadata( $id );
2409
2410 // Is this the full URL.
2411 if ( $metadata['width'] === (int) $size[0] && $metadata['height'] === (int) $size[1] ) {
2412 continue;
2413 }
2414
2415 $size_crop_map = self::size_to_crop();
2416
2417 $crop = false;
2418
2419 if ( isset( $size_crop_map[ $size[0] . $size[1] ] ) ) {
2420 $crop = $size_crop_map[ $size[0] . $size[1] ];
2421 }
2422
2423 if ( $crop ) {
2424 $width = $size[0];
2425 $height = $size[1];
2426 } else {
2427 // In case of an image size, we need to calculate the new dimensions for the proper file path.
2428 $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] );
2429
2430 $width = $constrained[0];
2431 $height = $constrained[1];
2432 }
2433 $replace[ $url ] = $this->maybe_strip_scaled( $replace[ $url ] );
2434
2435 $suffix = sprintf( '-%sx%s.%s', $width, $height, $extension );
2436
2437 $replace[ $url ] = str_replace( '.' . $extension, $suffix, $replace[ $url ] );
2438 }
2439
2440 $post_data['post_content'] = str_replace( array_keys( $replace ), array_values( $replace ), $content );
2441
2442 return $post_data;
2443 }
2444
2445 /**
2446 * Alter the image size for the image widget.
2447 *
2448 * @param string $html the attachment image HTML string.
2449 * @param array $settings Control settings.
2450 * @param string $image_size_key Optional. Settings key for image size.
2451 * Default is `image`.
2452 * @param string $image_key Optional. Settings key for image. Default
2453 * is null. If not defined uses image size key
2454 * as the image key.
2455 *
2456 * @return string
2457 */
2458 public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) {
2459 if ( ! isset( $settings['image'] ) ) {
2460 return $html;
2461 }
2462
2463 $image = $settings['image'];
2464
2465 if ( ! isset( $image['id'] ) ) {
2466 return $html;
2467 }
2468
2469 if ( ! $this->is_new_offloaded_attachment( $image['id'] ) ) {
2470 return $html;
2471 }
2472
2473 if ( ! isset( $settings['image_size'] ) ) {
2474 return $html;
2475 }
2476
2477 if ( $settings['image_size'] === 'custom' ) {
2478 if ( ! isset( $settings['image_custom_dimension'] ) ) {
2479 return $html;
2480 }
2481
2482 $custom_dimensions = $settings['image_custom_dimension'];
2483
2484 if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) {
2485 return $html;
2486 }
2487
2488 $new_args = [
2489 'width' => $custom_dimensions['width'],
2490 'height' => $custom_dimensions['height'],
2491 'resize' => $this->to_optml_crop( true ),
2492 ];
2493 $new_url = $this->get_new_offloaded_attachment_url( $image['url'], $image['id'], $new_args );
2494
2495 return str_replace( $image['url'], $new_url, $html );
2496 }
2497
2498 return $html;
2499 }
2500
2501 /**
2502 * Adds new actions for new offloads.
2503 *
2504 * @return void
2505 */
2506 public function add_new_actions() {
2507 add_filter( 'wp_prepare_attachment_for_js', [ self::$instance, 'alter_attachment_for_js' ], 999, 3 );
2508 add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2509 add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2510
2511 // Needed for rendering beaver builder css properly.
2512 add_filter( 'fl_builder_render_css', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2513
2514 // Filter saved data on insert to use local attachments.
2515 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
2516 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
2517 if ( $below_6_0_0 ) {
2518 add_filter( 'wp_insert_post_data', [ self::$instance, 'legacy_filter_saved_data' ], 10, 3 );
2519 } else {
2520 add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2521 }
2522
2523 // Filter loaded data in the editors to use local attachments.
2524 add_filter( 'content_edit_pre', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2525 $types = get_post_types_by_support( 'editor' );
2526 foreach ( $types as $type ) {
2527 $post_type = get_post_type_object( $type );
2528 if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2529 add_filter(
2530 'rest_prepare_' . $type,
2531 [
2532 self::$instance,
2533 'pre_filter_rest_content',
2534 ],
2535 10,
2536 3
2537 );
2538 }
2539 }
2540
2541 add_filter( 'get_attached_file', [$this, 'alter_attached_file_response'], 10, 2 );
2542 add_filter(
2543 'elementor/image_size/get_attachment_image_html',
2544 [
2545 $this,
2546 'alter_elementor_image_size',
2547 ],
2548 10,
2549 4
2550 );
2551
2552 }
2553
2554 /**
2555 * Elementor checks if the file exists before requesting a specific image size.
2556 *
2557 * Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload.
2558 *
2559 * Also needed because some
2560 *
2561 * @param string $file The file path.
2562 * @param int $id The attachment ID.
2563 *
2564 * @return bool|string
2565 */
2566 public function alter_attached_file_response( $file, $id ) {
2567 if ( ! $this->is_new_offloaded_attachment( $id ) ) {
2568 return $file;
2569 }
2570
2571 $metadata = wp_get_attachment_metadata( $id );
2572
2573 if ( isset( $metadata['file'] ) ) {
2574 $uploads = wp_get_upload_dir();
2575
2576 return $uploads['basedir'] . '/' . $metadata['file'];
2577 }
2578
2579 return true;
2580 }
2581
2582 /**
2583 * Maybe strip the `-scaled` from the URL.
2584 *
2585 * @param string $url The url.
2586 *
2587 * @return string
2588 */
2589 public function maybe_strip_scaled( $url ) {
2590 $ext = $this->get_ext( $url );
2591
2592 return str_replace( '-scaled.' . $ext, '.' . $ext, $url );
2593 }
2594 }
2595