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

1,760 lines 57.9 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
15 /**
16 * Hold the settings object.
17 *
18 * @var Optml_Settings Settings object.
19 */
20 public $settings;
21 /**
22 * Cached object instance.
23 *
24 * @var Optml_Media_Offload
25 */
26 private static $instance = null;
27
28 const KEYS = [
29 'uploaded_flag' => 'id:',
30 'not_processed_flag' => 'process:',
31 ];
32 const POST_OFFLOADED_FLAG = 'optimole_offload_post';
33 const POST_ROLLBACK_FLAG = 'optimole_rollback_post';
34 /**
35 * Flag used inside wp_get_attachment url filter.
36 *
37 * @var bool Whether or not to return the original url of the image.
38 */
39 private static $return_original_url = false;
40 /**
41 * Flag used inside wp_get_attachment url filter.
42 *
43 * @var bool Whether or not to return the original url of the image.
44 */
45 private static $offload_update_post = false;
46
47 /**
48 * Flag used inside wp_unique_filename filter.
49 *
50 * @var bool|string Whether to skip our custom deduplication.
51 */
52 private static $current_file_deduplication = false;
53 /**
54 * Keeps the last deduplicated lower case value.
55 *
56 * @var bool|string Used to check if the current processed image was deduplicated.
57 */
58 private static $last_deduplicated = false;
59 /**
60 * Checks if the plugin was installed before adding POST_OFFLOADED_FLAG.
61 *
62 * @var bool Used when applying the flags for the page query.
63 */
64 private static $is_legacy_install = null;
65
66 /**
67 * Adds page meta query args
68 *
69 * @param string $action The action for which the args are needed.
70 * @param array $args The initial args without the added meta_query args.
71 * @return array The args with the added meta_query args.
72 */
73 public static function add_page_meta_query_args( $action, $args ) {
74 if ( $action === 'offload_images' ) {
75 $args['meta_query'] = [
76 'relation' => 'AND',
77 [
78 'key' => self::POST_OFFLOADED_FLAG,
79 'compare' => 'NOT EXISTS',
80 ],
81 ];
82 }
83 if ( $action === 'rollback_images' ) {
84 $args['meta_query'] = [
85 'relation' => 'AND',
86 [
87 'key' => self::POST_ROLLBACK_FLAG,
88 'compare' => 'NOT EXISTS',
89 ],
90 ];
91 if ( self::$is_legacy_install ) {
92 $args['meta_query'][] = [
93 'key' => self::POST_OFFLOADED_FLAG,
94 'value' => 'true',
95 'compare' => '=',
96 ];
97 }
98 }
99 return $args;
100 }
101
102 /**
103 * Get count of all images from db.
104 *
105 * @return int Number of all images.
106 */
107 public static function number_of_all_images() {
108 $total_images_by_mime = wp_count_attachments( 'image' );
109 return array_sum( (array) $total_images_by_mime );
110 }
111 /**
112 * Optml_Media_Offload constructor.
113 */
114 public static function instance() {
115 if ( null === self::$instance ||
116 ( self::$instance->settings !== null && ( ! self::$instance->settings->is_connected()
117 || self::$instance->settings->get( 'offload_media' ) === 'disabled'
118 || self::$instance->settings->get( 'cloud_images' ) === 'disabled' ) ) ) {
119 self::$instance = new self();
120 self::$instance->settings = new Optml_Settings();
121 if ( self::$instance->settings->is_connected() ) {
122 self::$instance->init();
123 }
124 if ( self::$instance->settings->get( 'offload_media' ) === 'enabled' ) {
125 add_filter( 'image_downsize', [self::$instance, 'generate_filter_downsize_urls'], 10, 3 );
126 add_filter( 'wp_generate_attachment_metadata', [self::$instance, 'generate_image_meta'], 10, 2 );
127 add_filter( 'wp_get_attachment_url', [self::$instance, 'get_image_attachment_url'], -999, 2 );
128 add_filter( 'wp_insert_post_data', [self::$instance, 'filter_uploaded_images'] );
129 add_action( 'delete_attachment', [self::$instance, 'delete_attachment_hook'], 10 );
130 add_filter( 'handle_bulk_actions-upload', [self::$instance, 'bulk_action_handler'], 10, 3 );
131 add_filter( 'bulk_actions-upload', [self::$instance, 'register_bulk_media_actions'] );
132 add_filter( 'media_row_actions', [self::$instance, 'add_inline_media_action'], 10, 2 );
133 add_filter( 'wp_calculate_image_srcset', [self::$instance, 'calculate_image_srcset'], 1, 5 );
134 add_action( 'post_updated', [self::$instance, 'update_offload_meta'], 10, 3 );
135
136 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
137 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
138 if ( $below_6_0_0 ) {
139 add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert_legacy'], 10, 3 );
140 } else {
141 add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert'], 10, 4 );
142 }
143
144 add_action( 'optml_start_processing_images', [self::$instance, 'start_processing_images'], 10, 5 );
145 add_action( 'optml_start_processing_images_by_id', [self::$instance, 'start_processing_images_by_id'], 10, 4 );
146
147 if ( self::$is_legacy_install === null ) {
148 self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
149 }
150 }
151 }
152 return self::$instance;
153 }
154
155
156 /**
157 * Function for `update_attached_file` filter-hook.
158 *
159 * @param string $file Path to the attached file to update.
160 * @param int $attachment_id Attachment ID.
161 *
162 * @return string
163 */
164 function wp_update_attached_file_filter( $file, $attachment_id ) {
165
166 if ( OPTML_DEBUG_MEDIA ) {
167 do_action( 'optml_log', 'called updated attached' );
168 }
169 $info = pathinfo( $file );
170 $file_name = basename( $file );
171 $no_ext_file_name = basename( $file, '.' . $info['extension'] );
172 // if we have current deduplication set and it contains the filename that is updated
173 // we replace the updated filename with the deduplicated filename
174 if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) {
175 $file = str_replace( $file_name, self::$current_file_deduplication, $file );
176 // we need to store the filename we replaced to check when uploading the image if it was deduplicated
177 self::$last_deduplicated = $file_name;
178
179 self::$current_file_deduplication = false;
180 }
181 if ( OPTML_DEBUG_MEDIA ) {
182 do_action( 'optml_log', self::$last_deduplicated );
183 }
184 remove_filter( 'update_attached_file', [self::$instance, 'wp_update_attached_file_filter'], 10 );
185 return $file;
186 }
187
188 /**
189 * Function for `wp_insert_attachment_data` filter-hook.
190 * Because we remove the images when new images are added the wp deduplication using the files will not work
191 * 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
192 * For creating a unique quid by replacing the filename with the slug inside the existing guid
193 * 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
194 * 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
195 * with the deduplicated one which we stored into `$current_file_deduplication` variable
196 *
197 * @param array $data An array of slashed, sanitized, and processed attachment post data.
198 * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
199 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
200 * @param bool $update Whether this is an existing attachment post being updated.
201 *
202 * @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0.
203 *
204 * @return array
205 */
206 function insert( $data, $postarr, $unsanitized_postarr, $update ) {
207
208 // the post name is unique against the database so not affected by removing the files
209 // https://developer.wordpress.org/reference/functions/wp_unique_post_slug/
210 if ( OPTML_DEBUG_MEDIA ) {
211 do_action( 'optml_log', 'data before' );
212 do_action( 'optml_log', $data );
213 }
214 if ( empty( $data['guid'] ) ) {
215 return $data;
216 }
217
218 $filename = wp_basename( $data['guid'] );
219 $ext = $this->get_ext( $filename );
220 // skip if the file is not an image
221 if ( ! isset( Optml_Config::$all_extensions[ $ext ] ) && ! in_array( $ext, ['jpg', 'jpeg', 'jpe'], true ) ) {
222 return $data;
223 }
224
225 // on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg`
226 // we remove that as it is redundant for the file name deduplication we are using it
227 $sanitized_post_name = str_replace( '-' . $ext, '', $data['post_name'] );
228
229 // with the wp deduplication working the post_title is identical to the post_name
230 // so when they are different it means we need to deduplicate using the post_name
231 if ( ! empty( $data['post_name'] ) && $data['post_title'] !== $sanitized_post_name ) {
232 // we append the extension to the post_name to create a filename
233 // and use it to replace the filename in the guid
234 $no_ext_filename = str_replace( '.' . $ext, '', $filename );
235
236 $no_ext_filename_sanitized = sanitize_title( $no_ext_filename );
237
238 // get the deduplication addition from the database post_name
239 $diff = str_replace( strtolower( $no_ext_filename_sanitized ), '', $sanitized_post_name );
240
241 // create the deduplicated filename
242 $to_replace_with = $no_ext_filename . $diff . '.' . $ext;
243
244 $data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] );
245 // we store the deduplication to be used and add the filter for updating the attached_file meta
246 self::$current_file_deduplication = $to_replace_with;
247 add_filter( 'update_attached_file', [self::$instance, 'wp_update_attached_file_filter'], 10, 2 );
248 }
249 if ( OPTML_DEBUG_MEDIA ) {
250 do_action( 'optml_log', 'data after' );
251 do_action( 'optml_log', $data );
252 }
253
254 return $data;
255 }
256
257 /**
258 * Wrapper for the `insert` method for WP versions < 6.0.0.
259 *
260 * @param array $data An array of slashed, sanitized, and processed attachment post data.
261 * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
262 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
263 *
264 * @return array
265 */
266 function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
267 return $this->insert( $data, $postarr, $unsanitized_postarr, false );
268 }
269
270 /**
271 * Update offload meta when the page is updated.
272 *
273 * @param int $post_ID Updated post id.
274 * @param WP_Post $post_after Post before the update.
275 * @param WP_Post $post_before Post after the update.
276 * @uses action:post_updated
277 *
278 * @return void
279 */
280 public function update_offload_meta( $post_ID, $post_after, $post_before ) {
281 if ( self::$offload_update_post === true ) {
282 return;
283 }
284 if ( get_post_type( $post_ID ) === 'attachment' ) {
285 return;
286 }
287
288 // revisions are skipped inside the function no need to check them before
289 delete_post_meta( $post_ID, self::POST_ROLLBACK_FLAG );
290 }
291 /**
292 * Get image size name from width and meta.
293 *
294 * @param array $sizes Image sizes .
295 * @param integer $width Size width.
296 * @param string $filename Image filename.
297 *
298 * @return null|string
299 */
300 public static function get_image_name_from_width( $sizes, $width, $filename ) {
301 foreach ( $sizes as $name => $size ) {
302 if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
303 return $name;
304 }
305 }
306
307 return null;
308 }
309 /**
310 * Replace image URLs in the srcset attributes.
311 *
312 * @param array $sources Array of image sources.
313 * @param array $size_array Array of width and height values in pixels (in that order).
314 * @param string $image_src The 'src' of the image.
315 * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
316 * @param int $attachment_id Image attachment ID.
317 *
318 * @return array
319 */
320 public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
321
322 if ( ! is_array( $sources ) ) {
323 return $sources;
324 }
325
326 if ( ! Optml_Media_Offload::is_uploaded_image( $image_src ) || ! isset( $image_meta['file'] ) || ! Optml_Media_Offload::is_uploaded_image( $image_meta['file'] ) ) {
327 return $sources;
328 }
329 foreach ( $sources as $width => $source ) {
330 $filename = wp_basename( $image_meta['file'] );
331 $size = $this->get_image_name_from_width( $image_meta['sizes'], $width, $filename );
332 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
333
334 if ( false === $optimized_url ) {
335 continue;
336 }
337
338 $sources[ $width ]['url'] = $optimized_url[0];
339 }
340 return $sources;
341 }
342 /**
343 * Get the dimension from optimized url.
344 *
345 * @param string $url The image url.
346 * @return array Contains the width and height values in this order.
347 */
348 public static function parse_dimension_from_optimized_url( $url ) {
349 $catch = [];
350 $height = 'auto';
351 $width = 'auto';
352 preg_match( '/\/w:(.*)\/h:(.*)\/q:/', $url, $catch );
353 if ( isset( $catch[1] ) && isset( $catch[2] ) ) {
354 $width = $catch[1];
355 $height = $catch[2];
356 }
357 return [$width, $height];
358 }
359
360 /**
361 * Check if the image is stored on our servers or not.
362 *
363 * @param string $src Image src or url.
364 * @return bool Whether image is upload or not.
365 */
366 public static function is_not_processed_image( $src ) {
367 return strpos( $src, self::KEYS['not_processed_flag'] ) !== false;
368 }
369 /**
370 * Check if the image is stored on our servers or not.
371 *
372 * @param string $src Image src or url.
373 * @return bool Whether image is upload or not.
374 */
375 public static function is_uploaded_image( $src ) {
376 return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false;
377 }
378 /**
379 * Get the attachment ID from the image tag.
380 *
381 * @param string $image Image tag.
382 *
383 * @return int|false
384 */
385 public function get_id_from_tag( $image ) {
386 $attachment_id = false;
387 if ( preg_match( '#class=["|\']?[^"\']*(wp-image-|wp-video-)([\d]+)[^"\']*["|\']?#i', $image, $found ) ) {
388 $attachment_id = intval( $found[2] );
389 }
390
391 return $attachment_id;
392 }
393
394 /**
395 * Get attachment id from url
396 *
397 * @param string $url The optimized url .
398 * @return false|mixed The attachment id .
399 */
400 public static function get_attachment_id_from_url( $url ) {
401 preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
402 return isset( $attachment_id[1] ) ? $attachment_id[1] : false;
403 }
404
405 /**
406 * Get attachment id from local url
407 *
408 * @param string $url The url to look for.
409 * @return array The attachment id and the size from the url.
410 */
411 private function get_local_attachement_id_from_url( $url ) {
412
413 $size = 'full';
414 $found_size = $this->parse_dimensions_from_filename( $url );
415 $strip_url = $url;
416 $scaled_url = $url;
417 if ( $found_size[0] !== false && $found_size[1] !== false ) {
418 $size = $found_size;
419 $strip_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '', $url );
420 $scaled_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '-scaled', $url );
421 }
422 $strip_url = $this->add_schema( $strip_url );
423
424 $attachment_id = attachment_url_to_postid( $strip_url );
425 if ( $attachment_id === 0 ) {
426 $scaled_url = $this->add_schema( $scaled_url );
427 $attachment_id = attachment_url_to_postid( $scaled_url );
428 }
429
430 return [ 'attachment_id' => $attachment_id, 'size' => $size ];
431 }
432 /**
433 * Filter out the urls that are saved to our servers when saving to the DB.
434 *
435 * @param array $data The post data array to save.
436 *
437 * @return array
438 * @uses filter:wp_insert_post_data
439 */
440 public function filter_uploaded_images( $data ) {
441
442 $content = trim( wp_unslash( $data['post_content'] ) );
443 if ( OPTML_DEBUG_MEDIA ) {
444 do_action( 'optml_log', 'content to update' );
445 do_action( 'optml_log', $content );
446 }
447 $images = Optml_Manager::instance()->extract_urls_from_content( $content );
448 if ( ! isset( $images[0] ) ) {
449 return $data;
450 }
451 if ( OPTML_DEBUG_MEDIA ) {
452 do_action( 'optml_log', 'images to update' );
453 do_action( 'optml_log', $images );
454 }
455 foreach ( $images as $url ) {
456 $is_original_uploaded = self::is_uploaded_image( $url );
457 $attachment_id = false;
458 $size = 'thumbnail';
459 if ( $is_original_uploaded ) {
460 $found_size = $this->parse_dimension_from_optimized_url( $url );
461 if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) {
462 $size = $found_size;
463 }
464 $attachment_id = self::get_attachment_id_from_url( $url );
465 } else {
466 $id_and_size = $this->get_local_attachement_id_from_url( $url );
467 $attachment_id = $id_and_size['attachment_id'];
468 $size = $id_and_size['size'];
469 }
470
471 if ( OPTML_DEBUG_MEDIA ) {
472 do_action( 'optml_log', 'image id and found size' );
473 do_action( 'optml_log', $attachment_id );
474 do_action( 'optml_log', $size );
475 }
476 if ( false === $attachment_id || ! wp_attachment_is_image( $attachment_id ) ) {
477 continue;
478 }
479 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
480 if ( OPTML_DEBUG_MEDIA ) {
481 do_action( 'optml_log', ' image url to replace with ' );
482 do_action( 'optml_log', $optimized_url );
483 }
484
485 if ( ! isset( $optimized_url[0] ) ) {
486 continue;
487 }
488 if ( $is_original_uploaded === self::is_uploaded_image( $optimized_url[0] ) ) {
489 continue;
490 }
491 $content = str_replace( $url, $optimized_url[0], $content );
492 }
493 $data['post_content'] = wp_slash( $content );
494 return $data;
495 }
496
497 /**
498 * Get all images that need to be updated from a post.
499 *
500 * @param string $post_content The content of the post.
501 * @param string $job The job name.
502 * @return array An array containing the image ids.
503 */
504 public function get_image_id_from_content( $post_content, $job ) {
505 $content = trim( wp_unslash( $post_content ) );
506 $images = Optml_Manager::instance()->extract_urls_from_content( $content );
507 $found_images = [];
508 if ( isset( $images[0] ) ) {
509 foreach ( $images as $url ) {
510 $is_original_uploaded = self::is_uploaded_image( $url );
511 $attachment_id = false;
512 if ( $is_original_uploaded ) {
513 if ( $job === 'rollback_images' ) {
514 $attachment_id = self::get_attachment_id_from_url( $url );
515 }
516 } else {
517 if ( $job === 'offload_images' ) {
518 $id_and_size = $this->get_local_attachement_id_from_url( $url );
519 $attachment_id = $id_and_size['attachment_id'];
520 }
521 }
522 if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) {
523 continue;
524 }
525 $found_images[] = intval( $attachment_id );
526 }
527 }
528 return apply_filters( 'optml_content_images_to_update', $found_images, $content );
529 }
530
531 /**
532 * Get the posts ids and the images from them that need sync/rollback.
533 *
534 * @param int $page The current page from the query.
535 * @param string $job The job name rollback_images/offload_images.
536 * @param int $batch How many posts to query on a page.
537 * @param array $page_in The pages that need to be updated.
538 *
539 * @return array An array containing the page of the query and an array containing the images for every post that need to be updated.
540 */
541 public function update_content( $page, $job, $batch = 1, $page_in = [] ) {
542 if ( OPTML_DEBUG_MEDIA ) {
543 do_action( 'optml_log', ' updating_content ' );
544 }
545 $post_types = array_values(
546 array_filter(
547 get_post_types(),
548 function( $post_type ) {
549 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
550 return false;
551 }
552 return true;
553 }
554 )
555 );
556 $query_args = apply_filters(
557 'optml_replacement_wp_query_args',
558 ['post_type' => $post_types, 'post_status' => 'any', 'fields' => 'ids',
559 'posts_per_page' => $batch,
560 'update_post_meta_cache' => true,
561 'update_post_term_cache' => false,
562 ]
563 );
564
565 $query_args = self::add_page_meta_query_args( $job, $query_args );
566
567 if ( ! empty( $page_in ) ) {
568 $query_args['post__in'] = $page_in;
569 }
570
571 $content = new \WP_Query( $query_args );
572 if ( OPTML_DEBUG_MEDIA ) {
573 do_action( 'optml_log', $page );
574 }
575 $images_to_update = [];
576 if ( $content->have_posts() ) {
577 while ( $content->have_posts() ) {
578 $content->the_post();
579 $content_id = get_the_ID();
580 if ( get_post_type() !== 'attachment' ) {
581 $ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job );
582 if ( count( $ids ) > 0 ) {
583 $images_to_update[ $content_id ] = $ids;
584 $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
585 if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) {
586 foreach ( $duplicated_pages as $duplicated_id ) {
587 $duplicated_ids = $this->get_image_id_from_content( get_post_field( 'post_content', $duplicated_id ), $job );
588 $images_to_update[ $duplicated_id ] = $duplicated_ids;
589 }
590 }
591 }
592 if ( $job === 'offload_images' ) {
593 update_post_meta( $content_id, self::POST_OFFLOADED_FLAG, 'true' );
594 delete_post_meta( $content_id, self::POST_ROLLBACK_FLAG );
595 }
596 if ( $job === 'rollback_images' ) {
597 update_post_meta( $content_id, self::POST_ROLLBACK_FLAG, 'true' );
598 delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG );
599 }
600 }
601 }
602 $page ++;
603 }
604 $result['page'] = $page;
605 $result['imagesToUpdate'] = $images_to_update;
606 return $result;
607 }
608 /**
609 * Add inline action to push to our servers.
610 *
611 * @param array $actions All actions.
612 * @param \WP_Post $post The current post image object.
613 *
614 * @return array
615 */
616 public function add_inline_media_action( $actions, $post ) {
617 $meta = wp_get_attachment_metadata( $post->ID );
618 if ( ! isset( $meta['file'] ) ) {
619 return $actions;
620 }
621 $file = $meta['file'];
622 if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) {
623 return $actions;
624 }
625 if ( ! self::is_uploaded_image( $file ) ) {
626 $upload_action_url = add_query_arg(
627 [
628 'page' => 'optimole',
629 'optimole_action' => 'offload_images',
630 '0' => $post->ID,
631 ],
632 'admin.php'
633 );
634
635 $actions['offload_images'] = sprintf(
636 '<a href="%s" aria-label="%s">%s</a>',
637 $upload_action_url,
638 esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
639 esc_html__( 'Offload to Optimole', 'optimole-wp' )
640 );
641 }
642 if ( self::is_uploaded_image( $file ) ) {
643 $rollback_action_url = add_query_arg(
644 [
645 'page' => 'optimole',
646 'optimole_action' => 'rollback_images',
647 '0' => $post->ID,
648 ],
649 'admin.php'
650 );
651 $actions['rollback_images'] = sprintf(
652 '<a href="%s" aria-label="%s">%s</a>',
653 $rollback_action_url,
654 esc_attr__( 'Restore image to media library', 'optimole-wp' ),
655 esc_html__( 'Restore image to media library', 'optimole-wp' )
656 );
657 }
658 return $actions;
659 }
660 /**
661 * Upload images to our servers and update inside pages.
662 *
663 * @param array $image_ids The id of the attachments for the selected images.
664 * @return int The number of successfully processed images.
665 */
666 public function upload_and_update_existing_images( $image_ids ) {
667 $success_up = 0;
668 if ( OPTML_DEBUG_MEDIA ) {
669 do_action( 'optml_log', ' images to upload ' );
670 do_action( 'optml_log', $image_ids );
671 }
672 foreach ( $image_ids as $id ) {
673 if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) {
674 // if this meta flag below failed at the initial update but the file meta above is updated it will cause an infinite query loop
675 update_post_meta( $id, 'optimole_offload', 'true' );
676 $success_up ++;
677 continue;
678 }
679
680 $meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id );
681 if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) {
682 $success_up ++;
683 wp_update_attachment_metadata( $id, $meta );
684 }
685 }
686 if ( $success_up > 0 ) {
687 if ( OPTML_DEBUG_MEDIA ) {
688 do_action( 'optml_log', ' call post update, succesful images: ' );
689 do_action( 'optml_log', $success_up );
690 }
691 }
692 return $success_up;
693 }
694
695 /**
696 * Return the original url of an image attachment.
697 *
698 * @param integer $post_id Image attachment id.
699 * @return string|bool The original url of the image.
700 */
701 public static function get_original_url( $post_id ) {
702 self::$return_original_url = true;
703 $original_url = wp_get_attachment_url( $post_id );
704 self::$return_original_url = false;
705 return $original_url;
706 }
707 /**
708 * Bring images back to media library and update inside pages.
709 *
710 * @param array $image_ids The id of the attachments for the selected images.
711 * @return int The number of successfully processed images.
712 */
713 public function rollback_and_update_images( $image_ids ) {
714 $success_back = 0;
715 if ( OPTML_DEBUG_MEDIA ) {
716 do_action( 'optml_log', ' images to rollback ' );
717 do_action( 'optml_log', $image_ids );
718 }
719
720 foreach ( $image_ids as $id ) {
721 // Skip DAM attachment filtering.
722 if ( ! empty( get_post_meta( $id, Optml_Dam::OM_DAM_IMPORTED_FLAG, true ) ) ) {
723 continue;
724 }
725 $current_meta = wp_get_attachment_metadata( $id );
726 if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) {
727 delete_post_meta( $id, 'optimole_offload' );
728 $success_back++;
729 continue;
730 }
731 $table_id = [];
732 $filename = pathinfo( $current_meta['file'], PATHINFO_BASENAME );
733 preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $table_id );
734 if ( ! isset( $table_id[1] ) ) {
735 continue;
736 }
737 $table_id = $table_id[1];
738 if ( OPTML_DEBUG_MEDIA ) {
739 do_action( 'optml_log', ' image cloud id ' );
740 do_action( 'optml_log', $table_id );
741 }
742 $request = new Optml_Api();
743 $get_response = $request->call_upload_api( '', 'false', $table_id, 'false', 'true' );
744
745 if ( is_wp_error( $get_response ) || wp_remote_retrieve_response_code( $get_response ) !== 200 ) {
746 update_post_meta( $id, 'optimole_rollback_error', 'true' );
747 if ( OPTML_DEBUG_MEDIA ) {
748 do_action( 'optml_log', ' error get url' );
749 }
750 continue;
751 }
752
753 $get_url = json_decode( $get_response['body'], true )['getUrl'];
754
755 if ( ! function_exists( 'download_url' ) ) {
756 include_once ABSPATH . 'wp-admin/includes/file.php';
757 }
758 if ( ! function_exists( 'download_url' ) ) {
759 update_post_meta( $id, 'optimole_rollback_error', 'true' );
760 continue;
761 }
762 $timeout_seconds = 60;
763 $temp_file = download_url( $get_url, $timeout_seconds );
764
765 if ( is_wp_error( $temp_file ) ) {
766 update_post_meta( $id, 'optimole_rollback_error', 'true' );
767 if ( OPTML_DEBUG_MEDIA ) {
768 do_action( 'optml_log', ' download_url error ' );
769 }
770 continue;
771 }
772
773 $extension = $this->get_ext( $filename );
774
775 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
776 if ( OPTML_DEBUG_MEDIA ) {
777 do_action( 'optml_log', ' image has invalid extension' );
778 do_action( 'optml_log', $extension );
779 }
780 update_post_meta( $id, 'optimole_rollback_error', 'true' );
781 continue;
782 }
783
784 $type = Optml_Config::$image_extensions [ $extension ];
785 $file = [
786 'name' => $filename,
787 'type' => $type,
788 'tmp_name' => $temp_file,
789 'error' => 0,
790 'size' => filesize( $temp_file ),
791 ];
792
793 $overrides = [
794 // do not expect the default form data from normal uploads
795 'test_form' => false,
796
797 // Setting this to false lets WordPress allow empty files, not recommended.
798 'test_size' => true,
799
800 // A properly uploaded file will pass this test. There should be no reason to override this one.
801 'test_upload' => true,
802 ];
803
804 if ( ! function_exists( 'wp_handle_sideload' ) ) {
805 include_once ABSPATH . '/wp-admin/includes/file.php';
806 }
807 if ( ! function_exists( 'wp_handle_sideload' ) ) {
808 update_post_meta( $id, 'optimole_rollback_error', 'true' );
809 continue;
810 }
811
812 // Move the temporary file into the uploads directory.
813 $results = wp_handle_sideload( $file, $overrides );
814 if ( ! empty( $results['error'] ) ) {
815 if ( OPTML_DEBUG_MEDIA ) {
816 do_action( 'optml_log', ' wp_handle_sideload error' );
817 }
818 update_post_meta( $id, 'optimole_rollback_error', 'true' );
819 continue;
820 }
821
822 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
823 include_once ABSPATH . '/wp-admin/includes/image.php';
824 }
825 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
826 update_post_meta( $id, 'optimole_rollback_error', 'true' );
827 continue;
828 }
829 $original_meta = wp_create_image_subsizes( $results['file'], $id );
830 if ( $type === 'image/svg+xml' ) {
831 if ( ! function_exists( 'wp_get_attachment_metadata' ) || ! function_exists( 'wp_update_attachment_metadata' ) ) {
832 include_once ABSPATH . '/wp-admin/includes/post.php';
833 }
834 if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
835 update_post_meta( $id, 'optimole_rollback_error', 'true' );
836 continue;
837 }
838 $meta = wp_get_attachment_metadata( $id );
839 if ( ! isset( $meta['file'] ) ) {
840 update_post_meta( $id, 'optimole_rollback_error', 'true' );
841 continue;
842 }
843 $meta['file'] = $results['file'];
844 wp_update_attachment_metadata( $id, $meta );
845 }
846
847 if ( ! function_exists( 'update_attached_file' ) ) {
848 include_once ABSPATH . '/wp-admin/includes/post.php';
849 }
850 if ( ! function_exists( 'update_attached_file' ) ) {
851 update_post_meta( $id, 'optimole_rollback_error', 'true' );
852 continue;
853 }
854 update_attached_file( $id, $results['file'] );
855 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id );
856 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
857 foreach ( $duplicated_images as $duplicated_id ) {
858 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
859 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
860 $duplicated_meta['file'] = $results['file'];
861 if ( isset( $meta ) ) {
862 foreach ( $meta['sizes'] as $key => $value ) {
863 if ( isset( $original_meta['sizes'][ $key ]['file'] ) ) {
864 $duplicated_meta['sizes'][ $key ]['file'] = $original_meta['sizes'][ $key ]['file'];
865 }
866 }
867 }
868 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
869 delete_post_meta( $duplicated_id, 'optimole_offload' );
870 }
871 }
872 }
873 $success_back++;
874 $original_url = self::get_original_url( $id );
875 if ( $original_url === false ) {
876 continue;
877 }
878 $this->delete_attachment_from_server( $original_url, $id, $table_id );
879 }
880
881 if ( $success_back > 0 ) {
882 if ( OPTML_DEBUG_MEDIA ) {
883 do_action( 'optml_log', ' call update post, success rollback' );
884 do_action( 'optml_log', $success_back );
885 }
886 }
887 return $success_back;
888 }
889
890 /**
891 * Handle the bulk actions.
892 *
893 * @param string $redirect The current url from the media library.
894 * @param string $doaction The current action selected.
895 * @param array $image_ids The id of the attachments for the selected images.
896 * @return string The url with the correspondent query args for the executed actions.
897 */
898 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
899
900 if ( empty( $image_ids ) ) {
901 return $redirect;
902 }
903
904 $image_ids = array_slice( $image_ids, 0, 20, true );
905 $redirect = 'admin.php';
906 $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
907 $redirect = add_query_arg( 'page', 'optimole', $redirect );
908 $redirect = add_query_arg( $image_ids, $redirect );
909 return $redirect;
910
911 }
912 /**
913 * Register the bulk media actions.
914 *
915 * @param array $bulk_array The existing actions array.
916 * @return array The array with the appended actions.
917 */
918 public function register_bulk_media_actions( $bulk_array ) {
919
920 $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
921 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
922 return $bulk_array;
923
924 }
925
926 /**
927 * Send delete request to our servers and update the meta.
928 *
929 * @param string $original_url Original url of the image.
930 * @param integer $post_id Image id inside db.
931 * @param string $table_id Our cloud id for the image.
932 */
933 public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
934 $request = new Optml_Api();
935 $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
936
937 delete_post_meta( $post_id, 'optimole_offload' );
938 if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
939 // should add some routine to retry delete once if delete fails
940 }
941 }
942 /**
943 * Delete an image from our servers after it is removed from media.
944 *
945 * @param int $post_id The deleted post id.
946 */
947 public function delete_attachment_hook( $post_id ) {
948 $file = wp_get_attachment_metadata( $post_id );
949 if ( $file === false ) {
950 return;
951 }
952
953 // Skip if the image was imported from cloud library.
954 if ( $this->is_dam_import( $post_id ) ) {
955 return;
956 }
957
958 $file = $file['file'];
959 if ( self::is_uploaded_image( $file ) ) {
960
961 $original_url = self::get_original_url( $post_id );
962 if ( $original_url === false ) {
963 return;
964 }
965 $table_id = [];
966
967 preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $file, $table_id );
968
969 if ( ! isset( $table_id[1] ) ) {
970 return;
971 }
972 $this->delete_attachment_from_server( $original_url, $post_id, $table_id[1] );
973 }
974 }
975
976 /**
977 * Get optimized URL for an attachment image if it is uploaded to our servers.
978 *
979 * @param string $url The current url.
980 * @param int $attachment_id The attachment image id.
981 * @return string Optimole cdn URL.
982 * @uses filter:wp_get_attachment_url
983 */
984 public function get_image_attachment_url( $url, $attachment_id ) {
985 if ( self::$return_original_url === true ) {
986 return $url;
987 }
988 $meta = wp_get_attachment_metadata( $attachment_id );
989 if ( ! isset( $meta['file'] ) ) {
990 return $url;
991 }
992
993 // Skip DAM attachment filtering.
994 if ( $this->is_dam_import( $attachment_id ) ) {
995 return $url;
996 }
997
998 $file = $meta['file'];
999 if ( self::is_uploaded_image( $file ) ) {
1000 $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1001 return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1002 } else {
1003 // this is for the users that already offloaded the images before the other fixes
1004 $local_file = get_attached_file( $attachment_id );
1005 if ( ! file_exists( $local_file ) ) {
1006 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1007 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1008 foreach ( $duplicated_images as $id ) {
1009 if ( ! empty( $id ) ) {
1010 $duplicated_meta = wp_get_attachment_metadata( $id );
1011 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1012 $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1013 return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1014 }
1015 }
1016 }
1017 }
1018 }
1019 }
1020 return $url;
1021 }
1022 /**
1023 * Filter the requested image url.
1024 *
1025 * @param bool|array $image The previous image value (null).
1026 * @param int $attachment_id The ID of the attachment.
1027 * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1028 *
1029 * @return bool|array The image sizes and optimized url.
1030 * @uses filter:image_downsize
1031 */
1032 public function generate_filter_downsize_urls( $image, $attachment_id, $size ) {
1033 if ( self::$return_original_url === true ) {
1034 return $image;
1035 }
1036
1037 if ( $this->is_dam_import( $attachment_id ) ) {
1038 return $image;
1039 }
1040
1041 $sizes2crop = self::size_to_crop();
1042 if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
1043 return $image;
1044 }
1045 $data = image_get_intermediate_size( $attachment_id, $size );
1046 if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) {
1047 return $image;
1048 }
1049 $resize = apply_filters( 'optml_default_crop', [] );
1050 if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
1051 $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
1052 }
1053 $id_filename = [];
1054
1055 preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename );
1056 if ( ! isset( $id_filename[1] ) ) {
1057 return $image;
1058 }
1059 $url = self::get_original_url( $attachment_id );
1060 $optimized_url = ( new Optml_Image( $url, ['width' => $data['width'], 'height' => $data['height'], 'resize' => $resize, 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1061 $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1062 $image = [
1063 $optimized_url,
1064 $data['width'],
1065 $data['height'],
1066 true,
1067 ];
1068 return $image;
1069 }
1070 /**
1071 * Get image extension.
1072 *
1073 * @param string $path Image path.
1074 *
1075 * @return string
1076 */
1077 private function get_ext( $path ) {
1078 return pathinfo( $path, PATHINFO_EXTENSION );
1079 }
1080 /**
1081 * Update image meta with optimized cdn path.
1082 *
1083 * @param array $meta Meta information of the image.
1084 * @param int $attachment_id The image attachment ID.
1085 *
1086 * @return array
1087 * @uses filter:wp_generate_attachment_metadata
1088 */
1089 public function generate_image_meta( $meta, $attachment_id ) {
1090
1091 if ( $this->is_dam_import( $attachment_id ) ) {
1092 return $meta;
1093 }
1094
1095 if ( OPTML_DEBUG_MEDIA ) {
1096 do_action( 'optml_log', 'called generate meta' );
1097 }
1098 if ( ! isset( $meta['file'] ) || ! isset( $meta['width'] ) || ! isset( $meta['height'] ) || self::is_uploaded_image( $meta['file'] ) ) {
1099 do_action( 'optml_log', 'invalid meta' );
1100 do_action( 'optml_log', $meta );
1101 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1102 return $meta;
1103 }
1104 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1105 do_action( 'optml_log', 'optimization filter' );
1106 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1107 return $meta;
1108 }
1109 $original_url = self::get_original_url( $attachment_id );
1110
1111 if ( $original_url === false ) {
1112 do_action( 'optml_log', 'error getting original url' );
1113 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1114 return $meta;
1115 }
1116
1117 $local_file = get_attached_file( $attachment_id );
1118
1119 $extension = $this->get_ext( $local_file );
1120 $content_type = Optml_Config::$image_extensions [ $extension ];
1121 $temp = explode( '/', $local_file );
1122 $file_name = end( $temp );
1123 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1124 $original_name = $file_name;
1125 if ( OPTML_DEBUG_MEDIA ) {
1126 do_action( 'optml_log', 'file before replace' );
1127 do_action( 'optml_log', $local_file );
1128 }
1129
1130 // check if the current filename is the last deduplicated filename
1131 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1132 // replace the file with the original before deduplication to get the path where the image is uploaded
1133 $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1134 $original_name = self::$last_deduplicated;
1135 self::$last_deduplicated = false;
1136 }
1137 if ( OPTML_DEBUG_MEDIA ) {
1138 do_action( 'optml_log', 'file after replace' );
1139 do_action( 'optml_log', $local_file );
1140 }
1141 if ( ! file_exists( $local_file ) ) {
1142 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1143 do_action( 'optml_log', 'missing file' );
1144 do_action( 'optml_log', $local_file );
1145 return $meta;
1146 }
1147
1148 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
1149 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1150 do_action( 'optml_log', 'invalid extension' );
1151 do_action( 'optml_log', $extension );
1152 return $meta;
1153 }
1154 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1155 do_action( 'optml_log', 'extension filter' );
1156 do_action( 'optml_log', $extension );
1157 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1158 return $meta;
1159 }
1160
1161 $request = new Optml_Api();
1162 $generate_url_response = $request->call_upload_api( $original_url );
1163
1164 if ( is_wp_error( $generate_url_response ) || wp_remote_retrieve_response_code( $generate_url_response ) !== 200 ) {
1165 if ( OPTML_DEBUG_MEDIA ) {
1166 do_action( 'optml_log', ' call to signed url error' );
1167 do_action( 'optml_log', $generate_url_response );
1168 }
1169 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1170 return $meta;
1171 }
1172 $decoded_response = json_decode( $generate_url_response['body'], true );
1173
1174 if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1175 if ( OPTML_DEBUG_MEDIA ) {
1176 do_action( 'optml_log', ' missing table id or upload url' );
1177 do_action( 'optml_log', $decoded_response );
1178 }
1179 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1180 return $meta;
1181 }
1182 $table_id = $decoded_response['tableId'];
1183 if ( OPTML_DEBUG_MEDIA ) {
1184 do_action( 'optml_log', ' table id' );
1185 do_action( 'optml_log', $table_id );
1186 }
1187 $upload_signed_url = $decoded_response['uploadUrl'];
1188 $image = file_get_contents( $local_file );
1189 if ( $image === false ) {
1190 do_action( 'optml_log', 'can not find file' );
1191 do_action( 'optml_log', $local_file );
1192 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1193 return $meta;
1194 }
1195 if ( $upload_signed_url !== 'found_resource' ) {
1196
1197 $request = new Optml_Api();
1198 $result = $request->upload_image( $upload_signed_url, $content_type, $image );
1199
1200 if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1201 do_action( 'optml_log', 'upload error' );
1202 do_action( 'optml_log', $result );
1203 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1204 return $meta;
1205 }
1206 $file_size = filesize( $local_file );
1207 if ( $file_size === false ) {
1208 $file_size = 0;
1209 }
1210 $request = new Optml_Api();
1211 $result_update = $request->call_upload_api(
1212 $original_url,
1213 'false',
1214 $table_id,
1215 'success',
1216 'false',
1217 $meta['width'],
1218 $meta['height'],
1219 $file_size
1220 );
1221 if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1222 do_action( 'optml_log', 'dynamo update error' );
1223 do_action( 'optml_log', $result_update );
1224 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1225 return $meta;
1226 }
1227 }
1228 $url_to_append = $original_url;
1229 $url_parts = parse_url( $original_url );
1230
1231 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1232 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1233 }
1234 $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1235 $request = new Optml_Api();
1236 if ( $request->check_optimized_url( $optimized_url ) === false ) {
1237 do_action( 'optml_log', 'optimization error' );
1238 do_action( 'optml_log', $optimized_url );
1239 $request->call_upload_api( $original_url, 'true', $table_id );
1240 update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1241 return $meta;
1242 }
1243 unlink( $local_file );
1244 update_post_meta( $attachment_id, 'optimole_offload', 'true' );
1245 $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1246
1247 if ( isset( $meta['sizes'] ) ) {
1248 foreach ( $meta['sizes'] as $key => $value ) {
1249 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1250 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
1251 $meta['sizes'][ $key ]['file'] = $file_name;
1252 }
1253 }
1254 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1255
1256 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1257 foreach ( $duplicated_images as $duplicated_id ) {
1258 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
1259 if ( isset( $duplicated_meta['file'] ) && ! self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1260 $duplicated_meta['file'] = $meta['file'];
1261 if ( $duplicated_meta['sizes'] ) {
1262 foreach ( $meta['sizes'] as $key => $value ) {
1263 $duplicated_meta['sizes'][ $key ]['file'] = $file_name;
1264 }
1265 }
1266 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
1267 update_post_meta( $duplicated_id, 'optimole_offload', 'true' );
1268 }
1269 }
1270 }
1271 if ( OPTML_DEBUG_MEDIA ) {
1272 do_action( 'optml_log', 'success offload' );
1273 }
1274 $attachment_page_id = wp_get_post_parent_id( $attachment_id );
1275
1276 if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
1277 self::$offload_update_post = true;
1278 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1279 self::$offload_update_post = false;
1280 }
1281 return $meta;
1282 }
1283
1284 /** Get the args for wp query according to the scope.
1285 *
1286 * @param int $batch Number of images to get.
1287 * @param string $action The action for which to get the images.
1288 * @return array|false The query options array or false if not passed a valid action.
1289 */
1290 public static function get_images_query_args( $batch, $action, $get_images = false ) {
1291 $args = [
1292 'posts_per_page' => $batch,
1293 'fields' => 'ids',
1294 'ignore_sticky_posts' => false,
1295 'no_found_rows' => true,
1296 ];
1297 if ( $get_images === true ) {
1298 $args ['post_type'] = 'attachment';
1299 $args ['post_mime_type'] = 'image';
1300 $args ['post_status'] = 'inherit';
1301 if ( $action === 'offload_images' ) {
1302 $args['meta_query'] = [
1303 'relation' => 'AND',
1304 [
1305 'key' => 'optimole_offload',
1306 'compare' => 'NOT EXISTS',
1307 ],
1308 [
1309 'key' => 'optimole_offload_error',
1310 'compare' => 'NOT EXISTS',
1311 ],
1312 ];
1313 return $args;
1314 }
1315 if ( $action === 'rollback_images' ) {
1316 $args['meta_query'] = [
1317 'relation' => 'AND',
1318 [
1319 'key' => 'optimole_offload',
1320 'value' => 'true',
1321 'compare' => '=',
1322 ],
1323 [
1324 'key' => 'optimole_rollback_error',
1325 'compare' => 'NOT EXISTS',
1326 ],
1327 ];
1328 return $args;
1329 }
1330 } else {
1331 $args = self::add_page_meta_query_args( $action, $args );
1332 }
1333 $post_types = array_values(
1334 array_filter(
1335 get_post_types(),
1336 function( $post_type ) {
1337 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
1338 return false;
1339 }
1340 return true;
1341 }
1342 )
1343 );
1344 $args ['post_type'] = $post_types;
1345 return $args;
1346 }
1347 /**
1348 * Query the database and upload images to our servers.
1349 *
1350 * @param int $batch Number of images to process in a batch.
1351 * @return array Number of found images and number of successfully processed images.
1352 */
1353 public function upload_images( $batch, $images = [] ) {
1354 if ( empty( $images ) || $images === 'none' ) {
1355 $args = self::get_images_query_args( $batch, 'offload_images', true );
1356 $attachments = new \WP_Query( $args );
1357 $ids = $attachments->get_posts();
1358 } else {
1359 $ids = array_slice( $images, 0, $batch );
1360 }
1361 $result = [ 'found_images' => count( $ids ) ];
1362 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1363 return $result;
1364 }
1365 /**
1366 * Query the database and bring back image to media library.
1367 *
1368 * @param int $batch Number of images to process in a batch.
1369 * @return array Number of found images and number of successfully processed images.
1370 */
1371 public function rollback_images( $batch, $images = [] ) {
1372 if ( empty( $images ) || $images === 'none' ) {
1373 $args = self::get_images_query_args( $batch, 'rollback_images', true );
1374 $attachments = new \WP_Query( $args );
1375 $ids = $attachments->get_posts();
1376 } else {
1377 $ids = array_slice( $images, 0, $batch );
1378 }
1379 $result = [ 'found_images' => count( $ids ) ];
1380 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1381 return $result;
1382 }
1383
1384 /**
1385 * Update the post with the given id, the images will be updated by the filters we use.
1386 *
1387 * @param int $post_id The post id to update.
1388 * @return bool Whether the update was succesful or not.
1389 */
1390 public function update_page( $post_id ) {
1391 self::$offload_update_post = true;
1392 $post_update = wp_update_post( ['ID' => $post_id] );
1393 self::$offload_update_post = false;
1394 if ( $post_update === 0 ) {
1395 return false;
1396 }
1397 do_action( 'optml_updated_post', $post_id );
1398 return true;
1399 }
1400
1401 /**
1402 * Calculate the number of images in media library and the number of posts/pages.
1403 *
1404 * @param string $action The actions for which to get the number of images.
1405 * @return int Number of images.
1406 */
1407 public static function number_of_images_and_pages( $action ) {
1408 $args = self::get_images_query_args( -1, $action );
1409 $pages = new \WP_Query( $args );
1410 $pages_count = $pages->post_count;
1411 $args = self::get_images_query_args( -1, $action, true );
1412 $images = new \WP_Query( $args );
1413 return $pages_count + $images->post_count;
1414 }
1415
1416 /**
1417 * Calculate the number of images in media library and the number of posts/pages by IDs.
1418 *
1419 * @param string $action The actions for which to get the number of images.
1420 * @return int Number of images.
1421 */
1422 public static function number_of_images_by_ids( $action, $ids ) {
1423 $args = self::get_images_query_args( -1, $action, true );
1424 $args['post__in'] = $ids;
1425 $images = new \WP_Query( $args );
1426 return $images->post_count;
1427 }
1428
1429 /**
1430 * Get pages that contain images by IDs.
1431 *
1432 * @param string $action The actions for which to get the number of images.
1433 * @param array $images Image IDs.
1434 * @param int $batch Batch count.
1435 * @param int $page Page number.
1436 */
1437 public static function get_posts_by_image_ids( $action, $images = [], $batch = 10, $page = 1 ) {
1438 if ( empty( $images ) ) {
1439 return [];
1440 }
1441
1442 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1443 $transient = get_transient( $transient_key );
1444
1445 if ( false !== $transient ) {
1446 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1447 }
1448
1449 global $wpdb;
1450
1451 $image_urls = array_map( 'wp_get_attachment_url', $images );
1452
1453 $image_urls = array_map(
1454 function( $url ) {
1455 $path_info = pathinfo( $url );
1456 $directory = $path_info['dirname'];
1457 $filename = $path_info['filename'];
1458 return $directory . '/' . $filename;
1459 },
1460 $image_urls
1461 );
1462
1463 // Sanitize the image URLs for use in the SQL query.
1464 $urls = array_map( 'esc_url_raw', $image_urls );
1465
1466 // Initialize an empty string to hold the query.
1467 $query = '';
1468
1469 // Iterate through the array and add each URL to the query.
1470 foreach ( $urls as $index => $url ) {
1471 // If it's the first item, we don't need to add OR to the beginning.
1472 if ( $index === 0 ) {
1473 $query .= $wpdb->prepare( 'post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
1474 } else {
1475 $query .= $wpdb->prepare( ' OR post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
1476 }
1477 }
1478
1479 // Get all the posts IDs by using LIMIT and offset in a loop.
1480 $ids = [];
1481 $offset = 0;
1482 $limit = $batch;
1483
1484 while ( true ) {
1485 $posts = $wpdb->get_col(
1486 $wpdb->prepare(
1487 "SELECT ID FROM $wpdb->posts WHERE $query LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1488 $limit,
1489 $offset
1490 )
1491 );
1492
1493 if ( empty( $posts ) ) {
1494 break;
1495 }
1496
1497 $ids = array_merge( $ids, $posts );
1498 $offset += $limit;
1499 }
1500
1501 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
1502
1503 return array_slice( $ids, ( $page - 1 ) * $batch, $batch );
1504 }
1505
1506 /**
1507 * Calculate the number of images in media library and the number of posts/pages.
1508 *
1509 * @param string $action The actions for which to get the number of images.
1510 * @param bool $refresh Whether to refresh the cron or not.
1511 * @param array $images The images to process.
1512 *
1513 * @return array Image count and Cron status.
1514 */
1515 public static function get_image_count( $action, $refresh, $images = [] ) {
1516 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1517 $count = 0;
1518 $step = 0;
1519 $batch = 50; // Reduce this to 20 if you we have memory issues during testing.
1520
1521 if ( empty( $images ) ) {
1522 $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1523 } else {
1524 $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1525 }
1526
1527 $possible_batch = ceil( $count / 10 );
1528
1529 if ( $possible_batch < $batch ) {
1530 $batch = $possible_batch;
1531 }
1532
1533 // If batch is less than 10, set it to 10.
1534 if ( $batch < 10 ) {
1535 $batch = 10;
1536 }
1537
1538 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1539
1540 if ( false === $refresh && empty( $images ) ) {
1541 $total = ceil( $count / $batch );
1542
1543 $in_progress = 0 !== $count;
1544
1545 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1546
1547 if ( true === $in_progress ) {
1548 wp_schedule_single_event(
1549 time(),
1550 'optml_start_processing_images',
1551 [
1552 $action,
1553 $batch,
1554 1,
1555 $total,
1556 $step,
1557 ]
1558 );
1559 }
1560 }
1561
1562 if ( false === $refresh && ! empty( $images ) ) {
1563 $in_progress = 0 !== $count;
1564
1565 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1566
1567 if ( true === $in_progress ) {
1568 wp_schedule_single_event(
1569 time(),
1570 'optml_start_processing_images_by_id',
1571 [
1572 $action,
1573 $batch,
1574 1,
1575 $images,
1576 ]
1577 );
1578 }
1579 }
1580
1581 return [
1582 'count' => $count,
1583 'status' => $in_progress,
1584 ];
1585 }
1586
1587 /**
1588 * Start Processing Images by IDs
1589 *
1590 * @param string $action The action for which to get the number of images.
1591 * @param int $batch The batch of images to process.
1592 * @param int $page The page of images to process.
1593 * @param array $image_ids The images to process.
1594 *
1595 * @return void
1596 */
1597 public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1598 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1599
1600 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1601 return;
1602 }
1603
1604 set_time_limit( 0 );
1605 $page_in = Optml_Media_Offload::get_posts_by_image_ids( $action, $image_ids, $batch, $page );
1606
1607 if ( empty( $image_ids ) && empty( $page_in ) ) {
1608 self::$instance->settings->update( $option, 'disabled' );
1609 return;
1610 }
1611
1612 try {
1613 if ( 0 !== count( $page_in ) ) {
1614 $posts_to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
1615
1616 if ( isset( $posts_to_update['page'] ) ) {
1617 if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
1618 foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
1619 if ( ! empty( $image_ids ) ) {
1620 $images = array_intersect( $images, $image_ids );
1621 }
1622
1623 if ( empty( $images ) ) {
1624 continue;
1625 }
1626
1627 if ( $action === 'offload_images' ) {
1628 Optml_Media_Offload::instance()->upload_and_update_existing_images( $images );
1629 }
1630 if ( $action === 'rollback_images' ) {
1631 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1632 }
1633 Optml_Media_Offload::instance()->update_page( $post_id );
1634 }
1635 }
1636 }
1637
1638 $page = $page + 1;
1639 } else {
1640 // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1641 $images = array_slice( $image_ids, 0, $batch );
1642 $image_ids = array_slice( $image_ids, $batch );
1643 $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch, $images ) : Optml_Media_Offload::instance()->upload_images( $batch, $images );
1644 }
1645
1646 wp_schedule_single_event(
1647 time(),
1648 'optml_start_processing_images_by_id',
1649 [
1650 $action,
1651 $batch,
1652 $page,
1653 $image_ids,
1654 ]
1655 );
1656 } catch ( Exception $e ) {
1657 // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
1658 $delay_in_seconds = 10;
1659
1660 wp_schedule_single_event(
1661 time() + $delay_in_seconds,
1662 'optml_start_processing_images_by_id',
1663 [
1664 $action,
1665 $batch,
1666 $page,
1667 $image_ids,
1668 ]
1669 );
1670 }
1671 }
1672
1673 /**
1674 * Start Processing Images
1675 *
1676 * @param string $action The action for which to get the number of images.
1677 * @param int $batch The batch of images to process.
1678 * @param int $page The page of images to process.
1679 * @param int $total The total number of pages.
1680 * @param int $step The current step.
1681 *
1682 * @return void
1683 */
1684 public function start_processing_images( $action, $batch, $page, $total, $step ) {
1685 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1686
1687 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1688 return;
1689 }
1690
1691 if ( $step > $total || 0 === $total ) {
1692 self::$instance->settings->update( $option, 'disabled' );
1693 return;
1694 }
1695
1696 set_time_limit( 0 );
1697
1698 try {
1699 $posts_to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch );
1700
1701 if ( isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) {
1702 $page = $posts_to_update['page'];
1703 if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
1704 foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
1705 if ( $action === 'offload_images' ) {
1706 Optml_Media_Offload::instance()->upload_and_update_existing_images( $images );
1707 }
1708 if ( $action === 'rollback_images' ) {
1709 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1710 }
1711 Optml_Media_Offload::instance()->update_page( $post_id );
1712 }
1713 }
1714 } else {
1715 $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch ) : Optml_Media_Offload::instance()->upload_images( $batch );
1716 }
1717
1718 $step = $step + 1;
1719
1720 wp_schedule_single_event(
1721 time(),
1722 'optml_start_processing_images',
1723 [
1724 $action,
1725 $batch,
1726 $page,
1727 $total,
1728 $step,
1729 ]
1730 );
1731 } catch ( Exception $e ) {
1732 // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
1733 $delay_in_seconds = 10;
1734
1735 wp_schedule_single_event(
1736 time() + $delay_in_seconds,
1737 'optml_start_processing_images',
1738 [
1739 $action,
1740 $batch,
1741 $page,
1742 $total,
1743 $step,
1744 ]
1745 );
1746 }
1747 }
1748
1749 /**
1750 * Check if the attachment is a DAM import.
1751 *
1752 * @param int $attachment_id Attachment ID.
1753 *
1754 * @return bool
1755 */
1756 private function is_dam_import( $attachment_id ) {
1757 return ! empty( get_post_meta( $attachment_id, Optml_Dam::OM_DAM_IMPORTED_FLAG, true ) );
1758 }
1759 }
1760