PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 3.11.3
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v3.11.3
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.3, at inc/media_offload.php

2,612 lines 84.0 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 $results = wp_handle_sideload( $file, $overrides, get_the_date( 'Y/m', $id ) );
918 if ( ! empty( $results['error'] ) ) {
919 if ( OPTML_DEBUG_MEDIA ) {
920 do_action( 'optml_log', ' wp_handle_sideload error' );
921 }
922 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
923
924 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' faced wp_handle_sideload error.' );
925 continue;
926 }
927
928 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
929 include_once ABSPATH . '/wp-admin/includes/image.php';
930 }
931 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
932 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
933 continue;
934 }
935 $original_meta = wp_create_image_subsizes( $results['file'], $id );
936 if ( $type === 'image/svg+xml' ) {
937 if ( ! function_exists( 'wp_get_attachment_metadata' ) || ! function_exists( 'wp_update_attachment_metadata' ) ) {
938 include_once ABSPATH . '/wp-admin/includes/post.php';
939 }
940 if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
941 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
942 continue;
943 }
944 $meta = wp_get_attachment_metadata( $id );
945 if ( ! isset( $meta['file'] ) ) {
946 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
947 continue;
948 }
949 $meta['file'] = $results['file'];
950 wp_update_attachment_metadata( $id, $meta );
951 }
952
953 if ( ! function_exists( 'update_attached_file' ) ) {
954 include_once ABSPATH . '/wp-admin/includes/post.php';
955 }
956 if ( ! function_exists( 'update_attached_file' ) ) {
957 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
958 continue;
959 }
960 update_attached_file( $id, $results['file'] );
961
962 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id );
963 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
964 foreach ( $duplicated_images as $duplicated_id ) {
965 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
966 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
967 $duplicated_meta['file'] = $results['file'];
968 if ( isset( $meta ) ) {
969 foreach ( $meta['sizes'] as $key => $value ) {
970 if ( isset( $original_meta['sizes'][ $key ]['file'] ) ) {
971 $duplicated_meta['sizes'][ $key ]['file'] = $original_meta['sizes'][ $key ]['file'];
972 }
973 }
974 }
975 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
976 delete_post_meta( $duplicated_id, self::META_KEYS['offloaded'] );
977 delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
978 }
979 }
980 }
981 $success_back++;
982
983 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
984
985 $original_url = self::get_original_url( $id );
986 if ( $original_url === false ) {
987 continue;
988 }
989 $this->delete_attachment_from_server( $original_url, $id, $table_id );
990 }
991
992 if ( $success_back > 0 ) {
993 if ( OPTML_DEBUG_MEDIA ) {
994 do_action( 'optml_log', ' call update post, success rollback' );
995 do_action( 'optml_log', $success_back );
996 }
997 }
998 return $success_back;
999 }
1000
1001 /**
1002 * Handle the bulk actions.
1003 *
1004 * @param string $redirect The current url from the media library.
1005 * @param string $doaction The current action selected.
1006 * @param array $image_ids The id of the attachments for the selected images.
1007 * @return string The url with the correspondent query args for the executed actions.
1008 */
1009 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
1010
1011 if ( empty( $image_ids ) ) {
1012 return $redirect;
1013 }
1014
1015 $image_ids = array_slice( $image_ids, 0, 20, true );
1016 $redirect = 'admin.php';
1017 $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
1018 $redirect = add_query_arg( 'page', 'optimole', $redirect );
1019 $redirect = add_query_arg( $image_ids, $redirect );
1020 return $redirect;
1021
1022 }
1023
1024 /**
1025 * Register the bulk media actions.
1026 *
1027 * @param array $bulk_array The existing actions array.
1028 * @return array The array with the appended actions.
1029 */
1030 public function register_bulk_media_actions( $bulk_array ) {
1031
1032 $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1033 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
1034 return $bulk_array;
1035
1036 }
1037
1038 /**
1039 * Send delete request to our servers and update the meta.
1040 *
1041 * @param string $original_url Original url of the image.
1042 * @param integer $post_id Image id inside db.
1043 * @param string $table_id Our cloud id for the image.
1044 */
1045 public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
1046 $request = new Optml_Api();
1047 $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
1048
1049 delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
1050 delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
1051 if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
1052 // should add some routine to retry delete once if delete fails
1053 }
1054 }
1055
1056 /**
1057 * Delete an image from our servers after it is removed from media.
1058 *
1059 * @param int $post_id The deleted post id.
1060 */
1061 public function delete_attachment_hook( $post_id ) {
1062 $file = wp_get_attachment_metadata( $post_id );
1063 if ( $file === false ) {
1064 return;
1065 }
1066
1067 // Skip if the image was imported from cloud library.
1068 if ( $this->is_dam_imported_image( $post_id ) ) {
1069 return;
1070 }
1071
1072 if ( ! $this->is_new_offloaded_attachment( $post_id ) && ! $this->is_legacy_offloaded_attachment( $post_id ) ) {
1073 return;
1074 }
1075
1076 $file = $file['file'];
1077 if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
1078 $original_url = self::get_original_url( $post_id );
1079 if ( $original_url === false ) {
1080 return;
1081 }
1082 $table_id = [];
1083
1084 preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $file, $table_id );
1085
1086 if ( ! isset( $table_id[1] ) ) {
1087 return;
1088 }
1089 $this->delete_attachment_from_server( $original_url, $post_id, $table_id[1] );
1090 }
1091 }
1092
1093 /**
1094 * Get optimized URL for an attachment image if it is uploaded to our servers.
1095 *
1096 * @param string $url The current url.
1097 * @param int $attachment_id The attachment image id.
1098 * @return string Optimole cdn URL.
1099 * @uses filter:wp_get_attachment_url
1100 */
1101 public function get_image_attachment_url( $url, $attachment_id ) {
1102 if ( self::$return_original_url === true ) {
1103 return $url;
1104 }
1105
1106 if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
1107 $meta = wp_get_attachment_metadata( $attachment_id );
1108 if ( ! isset( $meta['file'] ) ) {
1109 return $url;
1110 }
1111
1112 // Skip DAM attachment filtering.
1113 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1114 return $url;
1115 }
1116
1117 $file = $meta['file'];
1118 if ( self::is_uploaded_image( $file ) ) {
1119 $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1120 return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1121 } else {
1122 // this is for the users that already offloaded the images before the other fixes
1123 $local_file = get_attached_file( $attachment_id );
1124 if ( ! file_exists( $local_file ) ) {
1125 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1126 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1127 foreach ( $duplicated_images as $id ) {
1128 if ( ! empty( $id ) ) {
1129 $duplicated_meta = wp_get_attachment_metadata( $id );
1130 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1131 $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1132 return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1133 }
1134 }
1135 }
1136 }
1137 }
1138 }
1139 return $url;
1140 }
1141
1142 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1143 return $url;
1144 }
1145
1146 return $this->get_new_offloaded_attachment_url( $url, $attachment_id );
1147 }
1148
1149 /**
1150 * Filter the requested image url.
1151 *
1152 * @param bool|array $image The previous image value (null).
1153 * @param int $attachment_id The ID of the attachment.
1154 * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1155 *
1156 * @return bool|array The image sizes and optimized url.
1157 * @uses filter:image_downsize
1158 */
1159 public function generate_filter_downsize_urls( $image, $attachment_id, $size ) {
1160 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1161 return $image;
1162 }
1163
1164 if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
1165 if ( self::$return_original_url === true ) {
1166 return $image;
1167 }
1168
1169 $sizes2crop = self::size_to_crop();
1170 if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
1171 return $image;
1172 }
1173 $data = image_get_intermediate_size( $attachment_id, $size );
1174 if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) {
1175 return $image;
1176 }
1177 $resize = apply_filters( 'optml_default_crop', [] );
1178 if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
1179 $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
1180 }
1181 $id_filename = [];
1182
1183 preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename );
1184 if ( ! isset( $id_filename[1] ) ) {
1185 return $image;
1186 }
1187 $url = self::get_original_url( $attachment_id );
1188 $optimized_url = ( new Optml_Image(
1189 $url,
1190 [
1191 'width' => $data['width'],
1192 'height' => $data['height'],
1193 'resize' => $resize,
1194 'quality' => $this->settings->get_numeric_quality(),
1195 ],
1196 $this->settings->get( 'cache_buster' )
1197 ) )->get_url();
1198 $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1199 $image = [
1200 $optimized_url,
1201 $data['width'],
1202 $data['height'],
1203 true,
1204 ];
1205
1206 return $image;
1207 }
1208
1209 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1210 return $image;
1211 }
1212
1213 return $this->alter_attachment_image_src( $image, $attachment_id, $size, false );
1214 }
1215
1216 /**
1217 * Get image extension.
1218 *
1219 * @param string $path Image path.
1220 *
1221 * @return string
1222 */
1223 private function get_ext( $path ) {
1224 return pathinfo( $path, PATHINFO_EXTENSION );
1225 }
1226
1227 /**
1228 * Update image meta with optimized cdn path.
1229 *
1230 * @param array $meta Meta information of the image.
1231 * @param int $attachment_id The image attachment ID.
1232 *
1233 * @return array
1234 * @uses filter:wp_generate_attachment_metadata
1235 */
1236 public function generate_image_meta( $meta, $attachment_id ) {
1237
1238 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1239 return $meta;
1240 }
1241
1242 if ( OPTML_DEBUG_MEDIA ) {
1243 do_action( 'optml_log', 'called generate meta' );
1244 }
1245 // No meta, or image was already uploaded.
1246 if ( ! isset( $meta['file'] ) || ! isset( $meta['width'] ) || ! isset( $meta['height'] ) || self::is_uploaded_image( $meta['file'] ) ) {
1247 do_action( 'optml_log', 'invalid meta' );
1248 do_action( 'optml_log', $meta );
1249 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1250
1251 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
1252 return $meta;
1253 }
1254 // Skip images based on filters.
1255 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1256 do_action( 'optml_log', 'optimization filter' );
1257 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1258 return $meta;
1259 }
1260 $original_url = self::get_original_url( $attachment_id );
1261
1262 // Could not find original URL.
1263 if ( $original_url === false ) {
1264 do_action( 'optml_log', 'error getting original url' );
1265 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1266
1267 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
1268 return $meta;
1269 }
1270
1271 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
1272 $original_url = $this->maybe_strip_scaled( $original_url );
1273 $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
1274
1275 $extension = $this->get_ext( $local_file );
1276 $content_type = Optml_Config::$image_extensions [ $extension ];
1277 $temp = explode( '/', $local_file );
1278 $file_name = end( $temp );
1279 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1280 $original_name = $file_name;
1281 if ( OPTML_DEBUG_MEDIA ) {
1282 do_action( 'optml_log', 'file before replace' );
1283 do_action( 'optml_log', $local_file );
1284 }
1285
1286 // check if the current filename is the last deduplicated filename
1287 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1288 // replace the file with the original before deduplication to get the path where the image is uploaded
1289 $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1290 $original_name = self::$last_deduplicated;
1291 self::$last_deduplicated = false;
1292 }
1293 if ( OPTML_DEBUG_MEDIA ) {
1294 do_action( 'optml_log', 'file after replace' );
1295 do_action( 'optml_log', $local_file );
1296 }
1297 if ( ! file_exists( $local_file ) ) {
1298 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1299 do_action( 'optml_log', 'missing file' );
1300 do_action( 'optml_log', $local_file );
1301
1302 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1303 return $meta;
1304 }
1305
1306 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
1307 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1308 do_action( 'optml_log', 'invalid extension' );
1309 do_action( 'optml_log', $extension );
1310
1311 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
1312 return $meta;
1313 }
1314 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1315 do_action( 'optml_log', 'extension filter' );
1316 do_action( 'optml_log', $extension );
1317 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1318 return $meta;
1319 }
1320
1321 $request = new Optml_Api();
1322 $generate_url_response = $request->call_upload_api( $original_url );
1323
1324 if ( is_wp_error( $generate_url_response ) || wp_remote_retrieve_response_code( $generate_url_response ) !== 200 ) {
1325 if ( OPTML_DEBUG_MEDIA ) {
1326 do_action( 'optml_log', ' call to signed url error' );
1327 do_action( 'optml_log', $generate_url_response );
1328 }
1329 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1330
1331 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid signed url.' );
1332 return $meta;
1333 }
1334 $decoded_response = json_decode( $generate_url_response['body'], true );
1335
1336 if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1337 if ( OPTML_DEBUG_MEDIA ) {
1338 do_action( 'optml_log', ' missing table id or upload url' );
1339 do_action( 'optml_log', $decoded_response );
1340 }
1341 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1342
1343 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
1344 return $meta;
1345 }
1346 $table_id = $decoded_response['tableId'];
1347 if ( OPTML_DEBUG_MEDIA ) {
1348 do_action( 'optml_log', ' table id' );
1349 do_action( 'optml_log', $table_id );
1350 }
1351 $upload_signed_url = $decoded_response['uploadUrl'];
1352 $image = file_get_contents( $local_file );
1353 if ( $image === false ) {
1354 do_action( 'optml_log', 'can not find file' );
1355 do_action( 'optml_log', $local_file );
1356 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1357
1358 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1359 return $meta;
1360 }
1361 if ( $upload_signed_url !== 'found_resource' ) {
1362
1363 $request = new Optml_Api();
1364 $result = $request->upload_image( $upload_signed_url, $content_type, $image );
1365
1366 if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1367 do_action( 'optml_log', 'upload error' );
1368 do_action( 'optml_log', $result );
1369 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1370
1371 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1372 return $meta;
1373 }
1374 $file_size = filesize( $local_file );
1375 if ( $file_size === false ) {
1376 $file_size = 0;
1377 }
1378 $request = new Optml_Api();
1379 $result_update = $request->call_upload_api(
1380 $original_url,
1381 'false',
1382 $table_id,
1383 'success',
1384 'false',
1385 $meta['width'],
1386 $meta['height'],
1387 $file_size
1388 );
1389 if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1390 do_action( 'optml_log', 'dynamo update error' );
1391 do_action( 'optml_log', $result_update );
1392 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1393
1394 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has dynamo update error.' );
1395 return $meta;
1396 }
1397 }
1398 $url_to_append = $original_url;
1399 $url_parts = parse_url( $original_url );
1400
1401 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1402 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1403 }
1404 $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1405 $request = new Optml_Api();
1406 if ( $request->check_optimized_url( $optimized_url ) === false ) {
1407 do_action( 'optml_log', 'optimization error' );
1408 do_action( 'optml_log', $optimized_url );
1409 $request->call_upload_api( $original_url, 'true', $table_id );
1410 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1411
1412 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
1413 return $meta;
1414 }
1415 unlink( $local_file );
1416 update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
1417 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1418 $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1419
1420 if ( isset( $meta['sizes'] ) ) {
1421 foreach ( $meta['sizes'] as $key => $value ) {
1422 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1423 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
1424 $meta['sizes'][ $key ]['file'] = $file_name;
1425 }
1426 }
1427
1428 // This is needed for scaled images.
1429 // Otherwise, `-scaled` images will be left behind.
1430 if ( isset( $meta['original_image'] ) ) {
1431 $ext = $this->get_ext( $local_file );
1432 $scaled_path = str_replace( '.' . $ext, '-scaled.' . $ext, $local_file );
1433 file_exists( $scaled_path ) && unlink( $scaled_path );
1434 }
1435
1436 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1437
1438 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1439 foreach ( $duplicated_images as $duplicated_id ) {
1440 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
1441 if ( isset( $duplicated_meta['file'] ) && ! self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1442 $duplicated_meta['file'] = $meta['file'];
1443 if ( $duplicated_meta['sizes'] ) {
1444 foreach ( $meta['sizes'] as $key => $value ) {
1445 $duplicated_meta['sizes'][ $key ]['file'] = $file_name;
1446 }
1447 }
1448 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
1449 update_post_meta( $duplicated_id, self::META_KEYS['offloaded'], 'true' );
1450 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1451 }
1452 }
1453 }
1454 if ( OPTML_DEBUG_MEDIA ) {
1455 do_action( 'optml_log', 'success offload' );
1456 }
1457
1458 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' );
1459 $attachment_page_id = wp_get_post_parent_id( $attachment_id );
1460
1461 if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
1462 self::$offload_update_post = true;
1463 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1464 self::$offload_update_post = false;
1465 }
1466 return $meta;
1467 }
1468
1469 /**
1470 * Get the args for wp query according to the scope.
1471 *
1472 * @param int $batch Number of images to get.
1473 * @param string $action The action for which to get the images.
1474 * @return array|false The query options array or false if not passed a valid action.
1475 */
1476 public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
1477
1478 $args = [
1479 'posts_per_page' => $batch,
1480 'fields' => 'ids',
1481 'ignore_sticky_posts' => false,
1482 'no_found_rows' => true,
1483 ];
1484
1485 if ( $get_images === true ) {
1486 $args['post_type'] = 'attachment';
1487 $args['post_mime_type'] = 'image';
1488 $args['post_status'] = 'inherit';
1489
1490 // Offload args.
1491 if ( $action === 'offload_images' ) {
1492 $args['meta_query'] = [
1493 'relation' => 'AND',
1494 [
1495 'key' => self::META_KEYS['offloaded'],
1496 'compare' => 'NOT EXISTS',
1497 ],
1498 [
1499 'key' => self::META_KEYS['offload_error'],
1500 'compare' => 'NOT EXISTS',
1501 ],
1502 ];
1503 return $args;
1504 }
1505
1506 // Rollback args.
1507 $args['meta_query'] = [
1508 'relation' => 'AND',
1509 [
1510 'key' => self::META_KEYS['offloaded'],
1511 'value' => 'true',
1512 'compare' => '=',
1513 ],
1514 [
1515 'key' => self::META_KEYS['rollback_error'],
1516 'compare' => 'NOT EXISTS',
1517 ],
1518 [
1519 'key' => Optml_Dam::OM_DAM_IMPORTED_FLAG,
1520 'compare' => 'NOT EXISTS',
1521 ],
1522 ];
1523
1524 return $args;
1525 }
1526
1527 $args = self::add_page_meta_query_args( $action, $args );
1528 $post_types = array_filter(
1529 get_post_types(),
1530 function ( $post_type ) {
1531 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
1532 return false;
1533 }
1534
1535 return true;
1536 }
1537 );
1538
1539 $args ['post_type'] = array_values( $post_types );
1540
1541 return $args;
1542 }
1543
1544 /**
1545 * Query the database and upload images to our servers.
1546 *
1547 * @param int $batch Number of images to process in a batch.
1548 * @return array Number of found images and number of successfully processed images.
1549 */
1550 public function upload_images( $batch, $images = [] ) {
1551 if ( empty( $images ) || $images === 'none' ) {
1552 $args = self::get_images_or_pages_query_args( $batch, 'offload_images', true );
1553 $attachments = new \WP_Query( $args );
1554 $ids = $attachments->get_posts();
1555 } else {
1556 $ids = array_slice( $images, 0, $batch );
1557 }
1558 $result = [ 'found_images' => count( $ids ) ];
1559 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1560 return $result;
1561 }
1562
1563 /**
1564 * Query the database and bring back image to media library.
1565 *
1566 * @param int $batch Number of images to process in a batch.
1567 * @return array Number of found images and number of successfully processed images.
1568 */
1569 public function rollback_images( $batch, $images = [] ) {
1570 if ( empty( $images ) || $images === 'none' ) {
1571 $args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true );
1572 $attachments = new \WP_Query( $args );
1573 $ids = $attachments->get_posts();
1574 } else {
1575 $ids = array_slice( $images, 0, $batch );
1576 }
1577 $result = [ 'found_images' => count( $ids ) ];
1578 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1579 return $result;
1580 }
1581
1582 /**
1583 * Update the post with the given id, the images will be updated by the filters we use.
1584 *
1585 * @param int $post_id The post id to update.
1586 * @return bool Whether the update was succesful or not.
1587 */
1588 public function update_page( $post_id ) {
1589 self::$offload_update_post = true;
1590 $post_update = wp_update_post( ['ID' => $post_id] );
1591 self::$offload_update_post = false;
1592 if ( $post_update === 0 ) {
1593 return false;
1594 }
1595 do_action( 'optml_updated_post', $post_id );
1596 return true;
1597 }
1598
1599 /**
1600 * Calculate the number of images in media library and the number of posts/pages.
1601 *
1602 * @param string $action The actions for which to get the number of images.
1603 * @return int Number of images.
1604 */
1605 public static function number_of_images_and_pages( $action ) {
1606 $images_args = self::get_images_or_pages_query_args( -1, $action, true );
1607
1608 $images = new \WP_Query( $images_args );
1609
1610 // With the new mechanism, when offloading images, we don't need to address pages anymore.
1611 // Bail early with the number of images.
1612 if ( $action === 'offload_images' ) {
1613 return $images->post_count;
1614 }
1615
1616 $pages_args = self::get_images_or_pages_query_args( -1, $action );
1617 $pages = new \WP_Query( $pages_args );
1618
1619 return $pages->post_count + $images->post_count;
1620 }
1621
1622 /**
1623 * Calculate the number of images in media library and the number of posts/pages by IDs.
1624 *
1625 * @param string $action The actions for which to get the number of images.
1626 * @return int Number of images.
1627 */
1628 public static function number_of_images_by_ids( $action, $ids ) {
1629 $args = self::get_images_or_pages_query_args( - 1, $action, true );
1630 $args['post__in'] = $ids;
1631 $images = new \WP_Query( $args );
1632
1633 return $images->post_count;
1634 }
1635
1636 /**
1637 * Get pages that contain images by IDs.
1638 *
1639 * @param string $action The actions for which to get the number of images.
1640 * @param array $images Image IDs.
1641 * @param int $batch Batch count.
1642 * @param int $page Page number.
1643 */
1644 public static function get_posts_by_image_ids( $action, $images = [], $batch = 10, $page = 1 ) {
1645 if ( empty( $images ) ) {
1646 return [];
1647 }
1648
1649 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1650 $transient = get_transient( $transient_key );
1651
1652 if ( false !== $transient ) {
1653 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1654 }
1655
1656 global $wpdb;
1657
1658 $image_urls = array_map(
1659 function( $image_id ) {
1660 $meta = wp_get_attachment_metadata( $image_id );
1661 $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1662
1663 return str_replace( '.' . $extension, '', $meta['file'] );
1664 },
1665 $images
1666 );
1667
1668 // Sanitize the image URLs for use in the SQL query.
1669 $urls = array_map( 'esc_url_raw', $image_urls );
1670
1671 // Initialize an empty string to hold the query.
1672 $query = '';
1673
1674 // Iterate through the array and add each URL to the query.
1675 foreach ( $urls as $index => $url ) {
1676 // If it's the first item, we don't need to add OR to the beginning.
1677 if ( $index === 0 ) {
1678 $query .= $wpdb->prepare( 'post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
1679 } else {
1680 $query .= $wpdb->prepare( ' OR post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
1681 }
1682 }
1683
1684 // Get all the posts IDs by using LIMIT and offset in a loop.
1685 $ids = [];
1686 $offset = 0;
1687 $limit = $batch;
1688
1689 while ( true ) {
1690 $posts = $wpdb->get_col(
1691 $wpdb->prepare(
1692 "SELECT ID FROM $wpdb->posts WHERE $query LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1693 $limit,
1694 $offset
1695 )
1696 );
1697
1698 if ( empty( $posts ) ) {
1699 break;
1700 }
1701
1702 $ids = array_merge( $ids, $posts );
1703 $offset += $limit;
1704 }
1705
1706 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
1707
1708 return array_slice( $ids, ( $page - 1 ) * $batch, $batch );
1709 }
1710
1711 /**
1712 * Record process meta,
1713 *
1714 * @param int $count The number of images to process.
1715 *
1716 * @return void
1717 */
1718 public static function record_process_meta( $count ) {
1719 $meta = get_option( 'optml_process_meta', [] );
1720 $meta['count'] = $count;
1721 $meta['start_time'] = time();
1722 update_option( 'optml_process_meta', $meta );
1723 }
1724
1725 /**
1726 * Get process meta,
1727 *
1728 * @return array
1729 */
1730 public static function get_process_meta() {
1731 $res = [];
1732 $meta = get_option( 'optml_process_meta', [] );
1733 $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1734 $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1735 return $res;
1736 }
1737
1738 /**
1739 * Calculate the number of images in media library and the number of posts/pages.
1740 *
1741 * @param string $action The actions for which to get the number of images.
1742 * @param bool $refresh Whether to refresh the cron or not.
1743 * @param array $images The images to process.
1744 *
1745 * @return array Image count and Cron status.
1746 */
1747 public static function get_image_count( $action, $refresh, $images = [] ) {
1748 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1749 $count = 0;
1750 $step = 0;
1751 $batch = apply_filters( 'optimole_offload_batch', 20 ); // Reduce this to smaller if we have memory issues during testing.
1752
1753 if ( empty( $images ) ) {
1754 $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1755 } else {
1756 $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1757 }
1758
1759 $possible_batch = ceil( $count / 10 );
1760
1761 if ( $possible_batch < $batch ) {
1762 $batch = $possible_batch;
1763 }
1764
1765 // If batch is less than 10, set it to 10.
1766 if ( $batch < 10 ) {
1767 $batch = 10;
1768 }
1769
1770 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1771
1772 if ( false === $refresh && empty( $images ) ) {
1773 $total = ceil( $count / $batch );
1774
1775 $in_progress = 0 !== $count;
1776
1777 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1778
1779 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1780 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1781 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1782 self::record_process_meta( $count );
1783
1784 if ( true === $in_progress ) {
1785 self::schedule_action(
1786 time(),
1787 'optml_start_processing_images',
1788 [
1789 $action,
1790 $batch,
1791 1,
1792 $total,
1793 $step,
1794 ]
1795 );
1796 }
1797 }
1798
1799 if ( false === $refresh && ! empty( $images ) ) {
1800 $in_progress = 0 !== $count;
1801
1802 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1803
1804 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1805 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1806 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1807 self::record_process_meta( $count );
1808
1809 if ( true === $in_progress ) {
1810 self::schedule_action(
1811 time(),
1812 'optml_start_processing_images_by_id',
1813 [
1814 $action,
1815 $batch,
1816 1,
1817 $images,
1818 ]
1819 );
1820 }
1821 }
1822
1823 return [
1824 'count' => $count,
1825 'status' => $in_progress,
1826 'action' => $action === 'offload_images' ? 'offload' : 'rollback',
1827 ];
1828 }
1829 /**
1830 * Schedule an action.
1831 *
1832 * @param int $time The time to schedule the action.
1833 * @param string $hook The hook to schedule.
1834 * @param array $args The arguments to pass to the hook.
1835 *
1836 * @return mixed
1837 */
1838 public static function schedule_action( $time, $hook, $args ) {
1839 // We use AS if available to avoid issues with WP Cron.
1840 if ( function_exists( 'as_schedule_single_action' ) ) {
1841 return as_schedule_single_action( $time, $hook, $args );
1842 } else {
1843 return wp_schedule_single_event( $time, $hook, $args );
1844 }
1845 }
1846 /**
1847 * Start Processing Images by IDs
1848 *
1849 * @param string $action The action for which to get the number of images.
1850 * @param int $batch The batch of images to process.
1851 * @param int $page The page of images to process.
1852 * @param array $image_ids The images to process.
1853 *
1854 * @return void
1855 */
1856 public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1857 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1858 $type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK;
1859
1860 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1861 return;
1862 }
1863
1864 set_time_limit( 0 );
1865
1866 // Only use the legacy offloaded attachments to query the pages that need to be updated.
1867 // We can be confident that these IDs are already marked as offloaded.
1868 $legacy_offloaded = array_filter(
1869 $image_ids,
1870 function( $id ) {
1871 return ! $this->is_new_offloaded_attachment( $id );
1872 }
1873 );
1874
1875 // On the new mechanism, we don't update posts anymore when offloading.
1876 $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
1877
1878 if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
1879 $meta = self::get_process_meta();
1880 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1881
1882 self::$instance->settings->update( $option, 'disabled' );
1883 return;
1884 }
1885
1886 try {
1887 // This will be 0 in the case of offloading now.
1888 if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
1889 $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
1890
1891 if ( isset( $to_update['page'] ) ) {
1892 if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
1893 foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
1894 if ( ! empty( $image_ids ) ) {
1895 $images = array_intersect( $images, $image_ids );
1896 }
1897
1898 if ( empty( $images ) ) {
1899 continue;
1900 }
1901
1902 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1903 Optml_Media_Offload::instance()->update_page( $post_id );
1904 }
1905 }
1906 }
1907
1908 $page = $page + 1;
1909 } else {
1910 // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1911 $images = array_slice( $image_ids, 0, $batch );
1912 $image_ids = array_slice( $image_ids, $batch );
1913 $action === 'rollback_images' ?
1914 Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
1915 Optml_Media_Offload::instance()->upload_images( $batch, $images );
1916 }
1917
1918 self::schedule_action(
1919 time(),
1920 'optml_start_processing_images_by_id',
1921 [
1922 $action,
1923 $batch,
1924 $page,
1925 $image_ids,
1926 ]
1927 );
1928 } catch ( Exception $e ) {
1929 // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
1930 $delay_in_seconds = 10;
1931 self::$instance->logger->add_log( $type, $e->getMessage() );
1932
1933 self::schedule_action(
1934 time() + $delay_in_seconds,
1935 'optml_start_processing_images_by_id',
1936 [
1937 $action,
1938 $batch,
1939 $page,
1940 $image_ids,
1941 ]
1942 );
1943 }
1944 }
1945
1946 /**
1947 * Start Processing Images
1948 *
1949 * @param string $action The action for which to get the number of images.
1950 * @param int $batch The batch of images to process.
1951 * @param int $page The page of images to process.
1952 * @param int $total The total number of pages.
1953 * @param int $step The current step.
1954 *
1955 * @return void
1956 */
1957 public function start_processing_images( $action, $batch, $page, $total, $step ) {
1958 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1959 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1960
1961 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1962 return;
1963 }
1964
1965 if ( $step > $total || 0 === $total ) {
1966 $meta = self::get_process_meta();
1967 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1968
1969 self::$instance->settings->update( $option, 'disabled' );
1970 return;
1971 }
1972
1973 set_time_limit( 0 );
1974
1975 try {
1976 $posts_to_update = $action === 'offload_images' ? [] : Optml_Media_Offload::instance()->update_content( $page, $action, $batch );
1977
1978 // Kept for backward compatibility with old offloading mechanism where pages were modified.
1979 if ( $action === 'rollback_images' && isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) {
1980 $page = $posts_to_update['page'];
1981 if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
1982 foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
1983 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1984 Optml_Media_Offload::instance()->update_page( $post_id );
1985 }
1986 }
1987 } else {
1988 $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch ) : Optml_Media_Offload::instance()->upload_images( $batch );
1989 }
1990
1991 $step = $step + 1;
1992
1993 self::schedule_action(
1994 time(),
1995 'optml_start_processing_images',
1996 [
1997 $action,
1998 $batch,
1999 $page,
2000 $total,
2001 $step,
2002 ]
2003 );
2004 } catch ( Exception $e ) {
2005 // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
2006 $delay_in_seconds = 10;
2007 self::$instance->logger->add_log( $type, $e->getMessage() );
2008
2009 self::schedule_action(
2010 time() + $delay_in_seconds,
2011 'optml_start_processing_images',
2012 [
2013 $action,
2014 $batch,
2015 $page,
2016 $total,
2017 $step,
2018 ]
2019 );
2020 }
2021 }
2022
2023 /**
2024 * Alter attachment image src for offloaded images.
2025 *
2026 * @param array|false $image {
2027 * Array of image data.
2028 *
2029 * @type string $0 Image source URL.
2030 * @type int $1 Image width in pixels.
2031 * @type int $2 Image height in pixels.
2032 * @type bool $3 Whether the image is a resized image.
2033 * }
2034 *
2035 * @param int $attachment_id attachment id.
2036 * @param string|int[] $size image size.
2037 * @param bool $icon Whether the image should be treated as an icon.
2038 *
2039 * @return array $image.
2040 */
2041 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2042 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2043 return $image;
2044 }
2045
2046 $url = get_post( $attachment_id );
2047 $url = $url->guid;
2048 $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id );
2049 $metadata = wp_get_attachment_metadata( $attachment_id );
2050
2051 // Use the original size if the requested size is full.
2052 if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
2053 $image_url = $this->get_new_offloaded_attachment_url(
2054 $url,
2055 $attachment_id,
2056 [
2057 'width' => $metadata['width'],
2058 'height' => $metadata['height'],
2059 'attachment_id' => $attachment_id,
2060 ]
2061 );
2062
2063 return [
2064 $image_url,
2065 $metadata['width'],
2066 $metadata['height'],
2067 false,
2068 ];
2069 }
2070
2071 $crop = false;
2072
2073 // Size can be int [] containing width and height.
2074 if ( is_array( $size ) ) {
2075 $width = $size[0];
2076 $height = $size[1];
2077 $crop = true;
2078 } else {
2079 $sizes = $this->get_all_image_sizes();
2080
2081 if ( ! isset( $sizes[ $size ] ) ) {
2082 return [
2083 $image_url,
2084 $metadata['width'],
2085 $metadata['height'],
2086 false,
2087 ];
2088 }
2089
2090 $width = $sizes[ $size ]['width'];
2091 $height = $sizes[ $size ]['height'];
2092 $crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
2093 }
2094
2095 $sizes2crop = self::size_to_crop();
2096
2097 if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
2098 return $image;
2099 }
2100
2101 $resize = apply_filters( 'optml_default_crop', [] );
2102 $data = image_get_intermediate_size( $attachment_id, $size );
2103
2104 if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
2105 if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
2106 $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
2107 }
2108 }
2109
2110 if ( $crop !== false ) {
2111 $resize = $this->to_optml_crop( $crop );
2112 }
2113
2114 $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, ['width' => $width, 'height' => $height, 'resize' => $resize, 'attachment_id' => $attachment_id] );
2115
2116 return [
2117 $image_url,
2118 $width,
2119 $height,
2120 $crop,
2121 ];
2122 }
2123
2124 /**
2125 * Needed for image sizes inside the editor.
2126 *
2127 * @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js().
2128 * @param WP_Post $attachment Attachment object.
2129 * @param array|false $meta Array of attachment meta data, or false if there is none.
2130 *
2131 * @return array
2132 */
2133 public function alter_attachment_for_js( $response, $attachment, $meta ) {
2134 if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2135 return $response;
2136 }
2137
2138 $sizes = $this->get_all_image_sizes();
2139
2140 foreach ( $sizes as $size => $args ) {
2141 if ( isset( $response['sizes'][ $size ] ) ) {
2142 continue;
2143 }
2144
2145 $args = [
2146 'height' => $args['height'],
2147 'width' => $args['width'],
2148 'crop' => true,
2149 ];
2150
2151 $response['sizes'][ $size ] = array_merge(
2152 $args,
2153 [
2154 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
2155 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2156 ]
2157 );
2158 }
2159
2160 $url_args = [
2161 'height' => $response['height'],
2162 'width' => $response['width'],
2163 'crop' => false,
2164 ];
2165
2166 $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
2167
2168 return $response;
2169 }
2170
2171 /**
2172 * Alter attachment metadata.
2173 *
2174 * @param array $metadata The attachment metadata.
2175 * @param int $id The attachment ID.
2176 *
2177 * @return array
2178 */
2179 public function alter_attachment_metadata( $metadata, $id ) {
2180 if ( ! $this->is_new_offloaded_attachment( $id ) ) {
2181 return $metadata;
2182 }
2183
2184 return $this->get_altered_metadata_for_remote_images( $metadata, $id );
2185 }
2186
2187 /**
2188 * Get offloaded image attachment URL for new offloads.
2189 *
2190 * @param string $url The initial attachment URL.
2191 * @param int $attachment_id The attachment ID.
2192 * @param array $args The additional arguments.
2193 * - width: The width of the image.
2194 * - height: The height of the image.
2195 * - crop: Whether to crop the image.
2196 *
2197 * @return string
2198 */
2199 private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [] ) {
2200 $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2201
2202 // Image might have already passed through this filter.
2203 if ( strpos( $url, $process_flag ) !== false ) {
2204 return $url;
2205 }
2206
2207 $meta = wp_get_attachment_metadata( $attachment_id );
2208 if ( ! isset( $meta['file'] ) ) {
2209 return $url;
2210 }
2211
2212 $default_args = [
2213 'width' => 'auto',
2214 'height' => 'auto',
2215 'resize' => apply_filters( 'optml_default_crop', [] ),
2216 ];
2217
2218 $args = wp_parse_args( $args, $default_args );
2219 // If this is not cropped, we constrain the dimensions to the original image.
2220 if ( empty( $args['resize'] ) && ! in_array( 'auto', [ $args['width'], $args['height'] ], true ) ) {
2221 $dimensions = wp_constrain_dimensions( $meta['width'], $meta['height'], $args['width'], $args['height'] );
2222 $args['width'] = $dimensions[0];
2223 $args['height'] = $dimensions[1];
2224 }
2225
2226 $file = $meta['file'];
2227 if ( self::is_uploaded_image( $file ) ) {
2228 $optimized_url = ( new Optml_Image(
2229 $url,
2230 [
2231 'width' => $args['width'],
2232 'height' => $args['height'],
2233 'quality' => $this->settings->get_numeric_quality(),
2234 'resize' => $args['resize'],
2235 'attachment_id' => $attachment_id,
2236 ],
2237 $this->settings->get( 'cache_buster' )
2238 ) )->get_url();
2239
2240 if ( strpos( $optimized_url, $process_flag ) !== false ) {
2241 return $optimized_url;
2242 }
2243
2244 $process_flag = $process_flag . $file;
2245
2246 return str_replace( '/' . $url, '/' . $process_flag, $optimized_url );
2247 } else {
2248 // this is for the users that already offloaded the images before the other fixes
2249 $local_file = get_attached_file( $attachment_id );
2250 if ( ! file_exists( $local_file ) ) {
2251 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
2252 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
2253 foreach ( $duplicated_images as $id ) {
2254 if ( ! empty( $id ) ) {
2255 $duplicated_meta = wp_get_attachment_metadata( $id );
2256 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2257 $optimized_url = ( new Optml_Image(
2258 $url,
2259 [
2260 'width' => $args['width'],
2261 'height' => $args['height'],
2262 'quality' => $this->settings->get_numeric_quality(),
2263 'attachment_id' => $attachment_id,
2264 ],
2265 $this->settings->get( 'cache_buster' )
2266 ) )->get_url();
2267 return $optimized_url;
2268 }
2269 }
2270 }
2271 }
2272 }
2273 }
2274 return $url;
2275 }
2276
2277 /**
2278 * Replace the URLs in the editor content with the offloaded ones.
2279 *
2280 * @param string $content The incoming content.
2281 *
2282 * @return string
2283 */
2284 public function replace_urls_in_editor_content( $content ) {
2285 $raw_extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
2286
2287 if ( empty( $raw_extracted ) ) {
2288 return $content;
2289 }
2290
2291 $to_replace = [];
2292 foreach ( $raw_extracted as $url ) {
2293 $attachment = $this->get_local_attachement_id_from_url( $url );
2294
2295 // No local attachment.
2296 if ( $attachment['attachment_id'] === 0 ) {
2297 continue;
2298 }
2299
2300 $attachment_id = $attachment['attachment_id'];
2301
2302 // Not offloaded.
2303 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2304 continue;
2305 }
2306
2307 // Get W/H from url.
2308 $size = $this->parse_dimensions_from_filename( $url );
2309 $width = $size[0] !== false ? $size[0] : 'auto';
2310 $height = $size[1] !== false ? $size[1] : 'auto';
2311
2312 // Handle resize.
2313 $sizes2crop = self::size_to_crop();
2314 $resize = apply_filters( 'optml_default_crop', [] );
2315 $sizes = image_get_intermediate_size( $attachment_id, $size );
2316 if ( false !== $sizes ) {
2317 if ( isset( $sizes2crop[ $width . $height ] ) ) {
2318 $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] );
2319 }
2320 }
2321
2322 // Build the optimized URL.
2323 $optimized_url = ( new Optml_Image(
2324 $url,
2325 [
2326 'width' => $width,
2327 'height' => $height,
2328 'quality' => $this->settings->get_numeric_quality(),
2329 'resize' => $resize,
2330 'attachment_id' => $attachment_id,
2331 ],
2332 $this->settings->get( 'cache_buster' )
2333 ) )->get_url();
2334
2335 // Drop any image size from the URL.
2336 $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2337
2338 $to_replace[ $url ] = $optimized_url;
2339 }
2340
2341 return str_replace( array_keys( $to_replace ), array_values( $to_replace ), $content );
2342 }
2343
2344 /**
2345 * Replaces the post content URLs to use Offloaded ones on editor fetch.
2346 *
2347 * @param \WP_REST_Response $response The response object.
2348 * @param \WP_Post $post The post object.
2349 * @param \WP_REST_Request $request The request object.
2350 *
2351 * @return \WP_REST_Response
2352 */
2353 public function pre_filter_rest_content( \WP_REST_Response $response, \WP_Post $post, \WP_REST_Request $request ) {
2354 $context = $request->get_param( 'context' );
2355
2356 if ( $context !== 'edit' ) {
2357 return $response;
2358 }
2359
2360 $data = $response->get_data();
2361
2362 // Actually replace all URLs.
2363 $data['content']['raw'] = $this->replace_urls_in_editor_content( $data['content']['raw'] );
2364
2365 $response->set_data( $data );
2366
2367 return $response;
2368 }
2369
2370 /**
2371 * Legacy function to be used for WordPress versions under 6.0.0.
2372 *
2373 * @param array $post_data Slashed, sanitized, processed post data.
2374 * @param array $postarr Slashed sanitized post data.
2375 * @param array $unsanitized_postarr Un-sanitized post data.
2376 *
2377 * @return array
2378 */
2379 public function legacy_filter_saved_data( $post_data, $postarr, $unsanitized_postarr ) {
2380 return $this->filter_saved_data( $post_data, $postarr, $unsanitized_postarr, true );
2381 }
2382
2383 /**
2384 * Filter post content to use local attachments when saving offloaded images.
2385 *
2386 * @param array $post_data Slashed, sanitized, processed post data.
2387 * @param array $postarr Slashed sanitized post data.
2388 * @param array $unsanitized_postarr Un-sanitized post data.
2389 * @param bool $update Whether this is an existing post being updated or not.
2390 *
2391 * @return array
2392 */
2393 public function filter_saved_data( $post_data, $postarr, $unsanitized_postarr, $update ) {
2394 if ( $postarr['post_status'] === 'trash' ) {
2395 return $post_data;
2396 }
2397
2398 $content = $post_data['post_content'];
2399
2400 $extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
2401 $replace = [];
2402
2403 foreach ( $extracted as $idx => $url ) {
2404 $id = self::get_attachment_id_from_url( $url );
2405
2406 if ( $id === false ) {
2407 continue;
2408 }
2409
2410 $id = (int) $id;
2411
2412 if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2413 continue;
2414 }
2415
2416 $replace[ $url ] = self::get_original_url( $id );
2417
2418 $size = $this->parse_dimension_from_optimized_url( $url );
2419
2420 if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2421 continue;
2422 }
2423
2424 $extension = $this->get_ext( $url );
2425 $metadata = wp_get_attachment_metadata( $id );
2426
2427 // Is this the full URL.
2428 if ( $metadata['width'] === (int) $size[0] && $metadata['height'] === (int) $size[1] ) {
2429 continue;
2430 }
2431
2432 $size_crop_map = self::size_to_crop();
2433
2434 $crop = false;
2435
2436 if ( isset( $size_crop_map[ $size[0] . $size[1] ] ) ) {
2437 $crop = $size_crop_map[ $size[0] . $size[1] ];
2438 }
2439
2440 if ( $crop ) {
2441 $width = $size[0];
2442 $height = $size[1];
2443 } else {
2444 // In case of an image size, we need to calculate the new dimensions for the proper file path.
2445 $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] );
2446
2447 $width = $constrained[0];
2448 $height = $constrained[1];
2449 }
2450 $replace[ $url ] = $this->maybe_strip_scaled( $replace[ $url ] );
2451
2452 $suffix = sprintf( '-%sx%s.%s', $width, $height, $extension );
2453
2454 $replace[ $url ] = str_replace( '.' . $extension, $suffix, $replace[ $url ] );
2455 }
2456
2457 $post_data['post_content'] = str_replace( array_keys( $replace ), array_values( $replace ), $content );
2458
2459 return $post_data;
2460 }
2461
2462 /**
2463 * Alter the image size for the image widget.
2464 *
2465 * @param string $html the attachment image HTML string.
2466 * @param array $settings Control settings.
2467 * @param string $image_size_key Optional. Settings key for image size.
2468 * Default is `image`.
2469 * @param string $image_key Optional. Settings key for image. Default
2470 * is null. If not defined uses image size key
2471 * as the image key.
2472 *
2473 * @return string
2474 */
2475 public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) {
2476 if ( ! isset( $settings['image'] ) ) {
2477 return $html;
2478 }
2479
2480 $image = $settings['image'];
2481
2482 if ( ! isset( $image['id'] ) ) {
2483 return $html;
2484 }
2485
2486 if ( ! $this->is_new_offloaded_attachment( $image['id'] ) ) {
2487 return $html;
2488 }
2489
2490 if ( ! isset( $settings['image_size'] ) ) {
2491 return $html;
2492 }
2493
2494 if ( $settings['image_size'] === 'custom' ) {
2495 if ( ! isset( $settings['image_custom_dimension'] ) ) {
2496 return $html;
2497 }
2498
2499 $custom_dimensions = $settings['image_custom_dimension'];
2500
2501 if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) {
2502 return $html;
2503 }
2504
2505 $new_args = [
2506 'width' => $custom_dimensions['width'],
2507 'height' => $custom_dimensions['height'],
2508 'resize' => $this->to_optml_crop( true ),
2509 ];
2510 $new_url = $this->get_new_offloaded_attachment_url( $image['url'], $image['id'], $new_args );
2511
2512 return str_replace( $image['url'], $new_url, $html );
2513 }
2514
2515 return $html;
2516 }
2517
2518 /**
2519 * Adds new actions for new offloads.
2520 *
2521 * @return void
2522 */
2523 public function add_new_actions() {
2524 add_filter( 'wp_prepare_attachment_for_js', [ self::$instance, 'alter_attachment_for_js' ], 999, 3 );
2525 add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2526 add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2527
2528 // Needed for rendering beaver builder css properly.
2529 add_filter( 'fl_builder_render_css', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2530
2531 // Filter saved data on insert to use local attachments.
2532 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
2533 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
2534 if ( $below_6_0_0 ) {
2535 add_filter( 'wp_insert_post_data', [ self::$instance, 'legacy_filter_saved_data' ], 10, 3 );
2536 } else {
2537 add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2538 }
2539
2540 // Filter loaded data in the editors to use local attachments.
2541 add_filter( 'content_edit_pre', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2542 $types = get_post_types_by_support( 'editor' );
2543 foreach ( $types as $type ) {
2544 $post_type = get_post_type_object( $type );
2545 if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2546 add_filter(
2547 'rest_prepare_' . $type,
2548 [
2549 self::$instance,
2550 'pre_filter_rest_content',
2551 ],
2552 10,
2553 3
2554 );
2555 }
2556 }
2557
2558 add_filter( 'get_attached_file', [$this, 'alter_attached_file_response'], 10, 2 );
2559 add_filter(
2560 'elementor/image_size/get_attachment_image_html',
2561 [
2562 $this,
2563 'alter_elementor_image_size',
2564 ],
2565 10,
2566 4
2567 );
2568
2569 }
2570
2571 /**
2572 * Elementor checks if the file exists before requesting a specific image size.
2573 *
2574 * Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload.
2575 *
2576 * Also needed because some
2577 *
2578 * @param string $file The file path.
2579 * @param int $id The attachment ID.
2580 *
2581 * @return bool|string
2582 */
2583 public function alter_attached_file_response( $file, $id ) {
2584 if ( ! $this->is_new_offloaded_attachment( $id ) ) {
2585 return $file;
2586 }
2587
2588 $metadata = wp_get_attachment_metadata( $id );
2589
2590 if ( isset( $metadata['file'] ) ) {
2591 $uploads = wp_get_upload_dir();
2592
2593 return $uploads['basedir'] . '/' . $metadata['file'];
2594 }
2595
2596 return true;
2597 }
2598
2599 /**
2600 * Maybe strip the `-scaled` from the URL.
2601 *
2602 * @param string $url The url.
2603 *
2604 * @return string
2605 */
2606 public function maybe_strip_scaled( $url ) {
2607 $ext = $this->get_ext( $url );
2608
2609 return str_replace( '-scaled.' . $ext, '.' . $ext, $url );
2610 }
2611 }
2612