PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / trunk
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization vtrunk
4.2.15 4.2.14 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 All 136 releases
← All changes | inc/media_offload.php +691 -559 3.12.0 → trunk View file →
@@ -5,8 +5,17 @@
5 5 * @package \Optimole\Inc
6 6 * @author Optimole <[email protected]>
7 7 */
8 8
9 +use OptimoleWP\Offload\Loader;
10 +use Optimole\Sdk\Exception\InvalidArgumentException;
11 +use Optimole\Sdk\Exception\InvalidUploadApiResponseException;
12 +use Optimole\Sdk\Exception\RuntimeException;
13 +use Optimole\Sdk\Exception\UploadApiException;
14 +use Optimole\Sdk\Exception\UploadFailedException;
15 +use Optimole\Sdk\Exception\UploadLimitException;
16 +use Optimole\Sdk\Optimole;
17 +
9 18 /**
10 19 * Class Optml_Admin
11 20 */
12 21 class Optml_Media_Offload extends Optml_App_Replacer {
@@ -34,20 +43,32 @@
34 43 */
35 44 public $logger;
36 45
37 46 const KEYS = [
38 - 'uploaded_flag' => 'id:',
39 - 'not_processed_flag' => 'process:',
47 + 'uploaded_flag' => 'id:',
48 + 'not_processed_flag' => 'process:',
40 49 ];
41 50 const META_KEYS = [
42 - 'offloaded' => 'optimole_offload',
43 - 'offload_error' => 'optimole_offload_error',
51 + 'offloaded' => 'optimole_offload',
52 + 'offload_error' => 'optimole_offload_error',
44 53 'rollback_error' => 'optimole_rollback_error',
45 54 ];
46 55 const OM_OFFLOADED_FLAG = 'om_image_offloaded';
47 56 const POST_OFFLOADED_FLAG = 'optimole_offload_post';
48 57 const POST_ROLLBACK_FLAG = 'optimole_rollback_post';
58 + const RETRYABLE_META_COUNTER = '_optimole_retryable_errors';
59 +
49 60 /**
61 + * Transient name for the transfer lock.
62 + */
63 + const TRANSFER_LOCK_TRANSIENT = 'optml_transfer_lock';
64 +
65 + /**
66 + * Time to live for the transfer lock, in seconds.
67 + */
68 + const TRANSFER_LOCK_TTL = 600;
69 +
70 + /**
50 71 * Flag used inside wp_get_attachment url filter.
51 72 *
52 73 * @var bool Whether or not to return the original url of the image.
53 74 */
@@ -82,8 +103,9 @@
82 103 * Adds page meta query args
83 104 *
84 105 * @param string $action The action for which the args are needed.
85 106 * @param array $args The initial args without the added meta_query args.
107 + *
86 108 * @return array The args with the added meta_query args.
87 109 */
88 110 public static function add_page_meta_query_args( $action, $args ) {
89 111 if ( $action === 'offload_images' ) {
@@ -89,9 +111,9 @@
89 111 if ( $action === 'offload_images' ) {
90 112 $args['meta_query'] = [
91 113 'relation' => 'AND',
92 114 [
93 - 'key' => self::POST_OFFLOADED_FLAG,
115 + 'key' => self::POST_OFFLOADED_FLAG,
94 116 'compare' => 'NOT EXISTS',
95 117 ],
96 118 ];
97 119 }
@@ -98,20 +120,21 @@
98 120 if ( $action === 'rollback_images' ) {
99 121 $args['meta_query'] = [
100 122 'relation' => 'AND',
101 123 [
102 - 'key' => self::POST_ROLLBACK_FLAG,
124 + 'key' => self::POST_ROLLBACK_FLAG,
103 125 'compare' => 'NOT EXISTS',
104 126 ],
105 127 ];
106 128 if ( self::$is_legacy_install ) {
107 129 $args['meta_query'][] = [
108 - 'key' => self::POST_OFFLOADED_FLAG,
109 - 'value' => 'true',
130 + 'key' => self::POST_OFFLOADED_FLAG,
131 + 'value' => 'true',
110 132 'compare' => '=',
111 133 ];
112 134 }
113 135 }
136 +
114 137 return $args;
115 138 }
116 139
117 140 /**
@@ -120,9 +143,10 @@
120 143 * @return int Number of all images.
121 144 */
122 145 public static function number_of_all_images() {
123 146 $total_images_by_mime = wp_count_attachments( 'image' );
124 - return array_sum( (array) $total_images_by_mime );
147 +
148 + return array_sum( (array) $total_images_by_mime );
125 149 }
126 150
127 151 /**
128 152 * Optml_Media_Offload constructor.
@@ -128,64 +152,100 @@
128 152 * Optml_Media_Offload constructor.
129 153 */
130 154 public static function instance() {
131 155 if ( null === self::$instance || self::is_phpunit_test() ) {
132 - self::$instance = new self();
156 + self::$instance = new self();
133 157 self::$instance->settings = new Optml_Settings();
134 - self::$instance->logger = Optml_Logger::instance();
158 + self::$instance->logger = Optml_Logger::instance();
135 159
136 160 if ( self::$instance->settings->is_connected() ) {
137 161 self::$instance->init();
138 162 }
139 163 if ( self::$instance->settings->is_offload_enabled() ) {
140 - add_filter( 'image_downsize', [self::$instance, 'generate_filter_downsize_urls'], 10, 3 );
141 - add_filter( 'wp_generate_attachment_metadata', [self::$instance, 'generate_image_meta'], 10, 2 );
142 - add_filter( 'wp_get_attachment_url', [self::$instance, 'get_image_attachment_url'], -999, 2 );
143 - add_filter( 'wp_insert_post_data', [self::$instance, 'filter_uploaded_images'] );
164 + add_filter( 'image_downsize', [ self::$instance, 'generate_filter_downsize_urls' ], 10, 3 );
165 + add_filter( 'wp_generate_attachment_metadata', [ self::$instance, 'generate_image_meta' ], 10, 2 );
166 + add_filter( 'wp_get_attachment_url', [ self::$instance, 'get_image_attachment_url' ], - 999, 2 );
167 + add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_uploaded_images' ] );
144 168
145 169 self::$instance->add_new_actions();
146 170
147 - add_action( 'delete_attachment', [self::$instance, 'delete_attachment_hook'], 10 );
148 - add_filter( 'handle_bulk_actions-upload', [self::$instance, 'bulk_action_handler'], 10, 3 );
149 - add_filter( 'bulk_actions-upload', [self::$instance, 'register_bulk_media_actions'] );
150 - add_filter( 'media_row_actions', [self::$instance, 'add_inline_media_action'], 10, 2 );
151 - add_filter( 'wp_calculate_image_srcset', [self::$instance, 'calculate_image_srcset'], 1, 5 );
152 - add_action( 'post_updated', [self::$instance, 'update_offload_meta'], 10, 3 );
171 + add_action( 'delete_attachment', [ self::$instance, 'delete_attachment_hook' ], 10 );
172 + add_filter( 'handle_bulk_actions-upload', [ self::$instance, 'bulk_action_handler' ], 10, 3 );
173 + // TODO: Uncomment this when bulk actions are implemented
174 + // add_filter( 'bulk_actions-upload', [ self::$instance, 'register_bulk_media_actions' ] );
175 + add_filter( 'media_row_actions', [ self::$instance, 'add_inline_media_action' ], 10, 2 );
176 + add_filter( 'wp_calculate_image_srcset', [ self::$instance, 'calculate_image_srcset' ], 1, 5 );
177 + add_action( 'post_updated', [ self::$instance, 'update_offload_meta' ], 10, 3 );
153 178
154 179 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
155 180 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
156 181 if ( $below_6_0_0 ) {
157 - add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert_legacy'], 10, 3 );
182 + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert_legacy' ], 10, 3 );
158 183 } else {
159 - add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert'], 10, 4 );
184 + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 );
160 185 }
161 186
162 - add_action( 'optml_start_processing_images', [self::$instance, 'start_processing_images'], 10, 5 );
163 - add_action( 'optml_start_processing_images_by_id', [self::$instance, 'start_processing_images_by_id'], 10, 4 );
164 -
187 + add_action( 'optml_start_processing_images', [ self::$instance, 'start_processing_images' ], 10, 6 );
188 + add_action(
189 + 'optml_move_images_by_id',
190 + [
191 + self::$instance,
192 + 'move_single_image',
193 + ],
194 + 10,
195 + 2
196 + );
197 + add_action( 'init', [ self::$instance, 'maybe_reschedule' ] );
165 198 if ( self::$is_legacy_install === null ) {
166 199 self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
167 200 }
201 + ( new Loader() )->register_hooks();
168 202 }
169 203 }
204 +
170 205 return self::$instance;
171 206 }
172 207
173 208 /**
209 + * Reschedule the transfer cron in case is missing or was lost.
210 + *
211 + * @return void
212 + */
213 + public function maybe_reschedule() {
214 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
215 + // If this is in pending, we do nothing.
216 + if ( false !== $lock ) {
217 + return;
218 + }
219 + // If there is no transfer in progress, we do nothing.
220 + if ( self::$instance->settings->get( 'transfer_status' ) === 'disabled' ) {
221 + return;
222 + }
223 + $transfer_type = self::$instance->settings->get( 'transfer_status' );
224 + $in_progress = self::$instance->settings->get( 'rollback_images' === $transfer_type ? 'rollback_status' : 'offloading_status' ) !== 'disabled';
225 + // We check if there is an in progress transfer.
226 + if ( ! $in_progress ) {
227 + return;
228 + }
229 + self::$instance->logger->add_log( $transfer_type, 'Cron missed, attempt to reschedule.' );
230 + self::move_images( $transfer_type, false );
231 + }
232 +
233 + /**
174 234 * Function for `update_attached_file` filter-hook.
175 235 *
176 - * @param string $file Path to the attached file to update.
236 + * @param string $file Path to the attached file to update.
177 237 * @param int $attachment_id Attachment ID.
178 238 *
179 239 * @return string
180 240 */
181 - function wp_update_attached_file_filter( $file, $attachment_id ) {
241 + public function wp_update_attached_file_filter( $file, $attachment_id ) {
182 242
183 243 if ( OPTML_DEBUG_MEDIA ) {
184 244 do_action( 'optml_log', 'called updated attached' );
185 245 }
186 - $info = pathinfo( $file );
187 - $file_name = basename( $file );
246 + $info = pathinfo( $file );
247 + $file_name = basename( $file );
188 248 $no_ext_file_name = basename( $file, '.' . $info['extension'] );
189 249 // if we have current deduplication set and it contains the filename that is updated
190 250 // we replace the updated filename with the deduplicated filename
191 251 if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) {
@@ -197,9 +257,10 @@
197 257 }
198 258 if ( OPTML_DEBUG_MEDIA ) {
199 259 do_action( 'optml_log', self::$last_deduplicated );
200 260 }
201 - remove_filter( 'update_attached_file', [self::$instance, 'wp_update_attached_file_filter'], 10 );
261 + remove_filter( 'update_attached_file', [ self::$instance, 'wp_update_attached_file_filter' ], 10 );
262 +
202 263 return $file;
203 264 }
204 265
205 266 /**
@@ -210,18 +271,17 @@
210 271 * 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
211 272 * 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
212 273 * with the deduplicated one which we stored into `$current_file_deduplication` variable
213 274 *
214 - * @param array $data An array of slashed, sanitized, and processed attachment post data.
215 - * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
275 + * @param array $data An array of slashed, sanitized, and processed attachment post data.
276 + * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
216 277 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
217 - * @param bool $update Whether this is an existing attachment post being updated.
278 + * @param bool $update Whether this is an existing attachment post being updated.
218 279 *
280 + * @return array
219 281 * @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0.
220 - *
221 - * @return array
222 282 */
223 - function insert( $data, $postarr, $unsanitized_postarr, $update ) {
283 + public function insert( $data, $postarr, $unsanitized_postarr, $update ) {
224 284
225 285 // the post name is unique against the database so not affected by removing the files
226 286 // https://developer.wordpress.org/reference/functions/wp_unique_post_slug/
227 287 if ( OPTML_DEBUG_MEDIA ) {
@@ -232,11 +292,11 @@
232 292 return $data;
233 293 }
234 294
235 295 $filename = wp_basename( $data['guid'] );
236 - $ext = $this->get_ext( $filename );
296 + $ext = $this->get_ext( $filename );
237 297 // skip if the file is not an image
238 - if ( ! isset( Optml_Config::$all_extensions[ $ext ] ) && ! in_array( $ext, ['jpg', 'jpeg', 'jpe'], true ) ) {
298 + if ( ! isset( Optml_Config::$all_extensions[ $ext ] ) && ! in_array( $ext, [ 'jpg', 'jpeg', 'jpe' ], true ) ) {
239 299 return $data;
240 300 }
241 301
242 302 // on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg`
@@ -260,9 +320,9 @@
260 320
261 321 $data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] );
262 322 // we store the deduplication to be used and add the filter for updating the attached_file meta
263 323 self::$current_file_deduplication = $to_replace_with;
264 - add_filter( 'update_attached_file', [self::$instance, 'wp_update_attached_file_filter'], 10, 2 );
324 + add_filter( 'update_attached_file', [ self::$instance, 'wp_update_attached_file_filter' ], 10, 2 );
265 325 }
266 326 if ( OPTML_DEBUG_MEDIA ) {
267 327 do_action( 'optml_log', 'data after' );
268 328 do_action( 'optml_log', $data );
@@ -273,15 +333,15 @@
273 333
274 334 /**
275 335 * Wrapper for the `insert` method for WP versions < 6.0.0.
276 336 *
277 - * @param array $data An array of slashed, sanitized, and processed attachment post data.
278 - * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
337 + * @param array $data An array of slashed, sanitized, and processed attachment post data.
338 + * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
279 339 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
280 340 *
281 341 * @return array
282 342 */
283 - function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
343 + public function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
284 344 return $this->insert( $data, $postarr, $unsanitized_postarr, false );
285 345 }
286 346
287 347 /**
@@ -289,11 +349,11 @@
289 349 *
290 350 * @param int $post_ID Updated post id.
291 351 * @param WP_Post $post_after Post before the update.
292 352 * @param WP_Post $post_before Post after the update.
293 - * @uses action:post_updated
294 353 *
295 354 * @return void
355 + * @uses action:post_updated
296 356 */
297 357 public function update_offload_meta( $post_ID, $post_after, $post_before ) {
298 358 if ( self::$offload_update_post === true ) {
299 359 return;
@@ -327,17 +387,18 @@
327 387
328 388 /**
329 389 * Replace image URLs in the srcset attributes.
330 390 *
331 - * @param array $sources Array of image sources.
332 - * @param array $size_array Array of width and height values in pixels (in that order).
333 - * @param string $image_src The 'src' of the image.
334 - * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
335 - * @param int $attachment_id Image attachment ID.
391 + * @param mixed|array<int, array{url: string, descriptor: string, value: int}> $sources Array of image sources.
392 + * @param array{0: int, 1: int} $size_array Array of width and height values in pixels (in that order).
393 + * @param string $image_src The 'src' of the image.
394 + * @param array<string, mixed> $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
395 + * @param int $attachment_id Image attachment ID or 0.
336 396 *
337 - * @return array
397 + * @return array<int, array{url: string, descriptor: string, value: int}>|mixed
338 398 */
339 399 public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
400 +
340 401 if ( ! is_array( $sources ) ) {
341 402 return $sources;
342 403 }
343 404
@@ -355,8 +416,9 @@
355 416 }
356 417
357 418 $sources[ $width ]['url'] = $optimized_url[0];
358 419 }
420 +
359 421 return $sources;
360 422 }
361 423
362 424 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
@@ -369,12 +431,10 @@
369 431 if ( $requested_height < 1 || $requested_width < 1 ) {
370 432 return $sources;
371 433 }
372 434
373 - $requested_ratio = $requested_width / $requested_height;
374 -
375 435 $image_sizes = $this->get_all_image_sizes();
376 - $crop = false;
436 + $crop = false;
377 437
378 438 // Loop through image sizes to make sure we're using the right cropping.
379 439 foreach ( $image_sizes as $size_name => $args ) {
380 440 if ( $args['width'] !== $requested_width && $args['height'] !== $requested_height ) {
@@ -384,9 +444,8 @@
384 444 if ( isset( $args['crop'] ) ) {
385 445 $crop = (bool) $args['crop'];
386 446 }
387 447 }
388 -
389 448 foreach ( $sources as $width => $source ) {
390 449 $filename = ( $image_meta['file'] );
391 450 $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename, false );
392 451
@@ -408,15 +467,9 @@
408 467
409 468 continue;
410 469 }
411 470
412 - $size_ratio = $size['width'] / $size['height'];
413 -
414 - // We need a srcset with the same aspect ratio.
415 - // Otherwise, we'll display different images on different devices.
416 - if ( $requested_ratio !== $size_ratio ) {
417 - unset( $sources[ $width ] );
418 -
471 + if ( ! wp_image_matches_ratio( $size['width'], $size['height'], $requested_width, $requested_height ) ) {
419 472 continue;
420 473 }
421 474
422 475 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size['name'] );
@@ -425,19 +478,25 @@
425 478 unset( $sources[ $width ] );
426 479
427 480 continue;
428 481 }
429 -
430 482 $sources[ $width ]['url'] = $optimized_url[0];
431 483 }
432 -
433 484 // Add the requested size to the srcset.
434 485 $sources[ $requested_width ] = [
435 - 'url' => $image_src,
486 + 'url' => $image_src,
436 487 'descriptor' => 'w',
437 - 'value' => $requested_width,
488 + 'value' => $requested_width,
438 489 ];
439 490
491 + if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
492 + $max_width = max( array_keys( $sources ) );
493 + $sources[ $max_width * 2 ] = [
494 + 'url' => str_replace( '/w:', '/dpr:2/w:', $sources[ $max_width ]['url'] ),
495 + 'descriptor' => 'x',
496 + 'value' => 2,
497 + ];
498 + }
440 499 return $sources;
441 500 }
442 501
443 502 /**
@@ -443,8 +502,9 @@
443 502 /**
444 503 * Check if the image is stored on our servers or not.
445 504 *
446 505 * @param string $src Image src or url.
506 + *
447 507 * @return bool Whether image is upload or not.
448 508 */
449 509 public static function is_not_processed_image( $src ) {
450 510 return strpos( $src, self::KEYS['not_processed_flag'] ) !== false;
@@ -453,8 +513,9 @@
453 513 /**
454 514 * Check if the image is stored on our servers or not.
455 515 *
456 516 * @param string $src Image src or url.
517 + *
457 518 * @return bool Whether image is upload or not.
458 519 */
459 520 public static function is_uploaded_image( $src ) {
460 521 return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false;
@@ -478,13 +539,15 @@
478 539
479 540 /**
480 541 * Get attachment id from url
481 542 *
482 - * @param string $url The optimized url .
543 + * @param string $url The optimized url .
544 + *
483 545 * @return false|mixed The attachment id .
484 546 */
485 547 public static function get_attachment_id_from_url( $url ) {
486 548 preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
549 +
487 550 return isset( $attachment_id[1] ) ? $attachment_id[1] : false;
488 551 }
489 552
490 553 /**
@@ -490,28 +553,22 @@
490 553 /**
491 554 * Get attachment id from local url
492 555 *
493 556 * @param string $url The url to look for.
557 + *
494 558 * @return array The attachment id and the size from the url.
495 559 */
496 560 public function get_local_attachement_id_from_url( $url ) {
497 561
498 - $size = 'full';
562 + $size = 'full';
499 563 $found_size = $this->parse_dimensions_from_filename( $url );
500 - $strip_url = $url;
501 - $scaled_url = $url;
564 + $url = $this->add_schema( $url );
502 565 if ( $found_size[0] !== false && $found_size[1] !== false ) {
503 566 $size = $found_size;
504 - $strip_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '', $url );
505 - $scaled_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '-scaled', $url );
506 - }
507 - $strip_url = $this->add_schema( $strip_url );
508 567
509 - $attachment_id = attachment_url_to_postid( $strip_url );
510 - if ( $attachment_id === 0 ) {
511 - $scaled_url = $this->add_schema( $scaled_url );
512 - $attachment_id = attachment_url_to_postid( $scaled_url );
513 568 }
569 + $url = $this->add_schema( $url );
570 + $attachment_id = $this->attachment_url_to_post_id( $url );
514 571
515 572 return [ 'attachment_id' => $attachment_id, 'size' => $size ];
516 573 }
517 574
@@ -529,9 +586,9 @@
529 586 if ( OPTML_DEBUG_MEDIA ) {
530 587 do_action( 'optml_log', 'content to update' );
531 588 do_action( 'optml_log', $content );
532 589 }
533 - $images = Optml_Manager::instance()->extract_urls_from_content( $content );
590 + $images = Optml_Manager::instance()->extract_urls_from_content( $content );
534 591 if ( ! isset( $images[0] ) ) {
535 592 return $data;
536 593 }
537 594 if ( OPTML_DEBUG_MEDIA ) {
@@ -539,10 +596,10 @@
539 596 do_action( 'optml_log', $images );
540 597 }
541 598 foreach ( $images as $url ) {
542 599 $is_original_uploaded = self::is_uploaded_image( $url );
543 - $attachment_id = false;
544 - $size = 'thumbnail';
600 + $attachment_id = false;
601 + $size = 'thumbnail';
545 602 if ( $is_original_uploaded ) {
546 603 $found_size = $this->parse_dimension_from_optimized_url( $url );
547 604 if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) {
548 605 $size = $found_size;
@@ -548,11 +605,11 @@
548 605 $size = $found_size;
549 606 }
550 607 $attachment_id = self::get_attachment_id_from_url( $url );
551 608 } else {
552 - $id_and_size = $this->get_local_attachement_id_from_url( $url );
609 + $id_and_size = $this->get_local_attachement_id_from_url( $url );
553 610 $attachment_id = $id_and_size['attachment_id'];
554 - $size = $id_and_size['size'];
611 + $size = $id_and_size['size'];
555 612 }
556 613
557 614 if ( OPTML_DEBUG_MEDIA ) {
558 615 do_action( 'optml_log', 'image id and found size' );
@@ -576,8 +633,9 @@
576 633 }
577 634 $content = str_replace( $url, $optimized_url[0], $content );
578 635 }
579 636 $data['post_content'] = wp_slash( $content );
637 +
580 638 return $data;
581 639 }
582 640
583 641 /**
@@ -584,18 +642,19 @@
584 642 * Get all images that need to be updated from a post.
585 643 *
586 644 * @param string $post_content The content of the post.
587 645 * @param string $job The job name.
646 + *
588 647 * @return array An array containing the image ids.
589 648 */
590 649 public function get_image_id_from_content( $post_content, $job ) {
591 - $content = trim( wp_unslash( $post_content ) );
592 - $images = Optml_Manager::instance()->extract_urls_from_content( $content );
650 + $content = trim( wp_unslash( $post_content ) );
651 + $images = Optml_Manager::instance()->extract_urls_from_content( $content );
593 652 $found_images = [];
594 653 if ( isset( $images[0] ) ) {
595 654 foreach ( $images as $url ) {
596 655 $is_original_uploaded = self::is_uploaded_image( $url );
597 - $attachment_id = false;
656 + $attachment_id = false;
598 657 if ( $is_original_uploaded ) {
599 658 if ( $job === 'rollback_images' ) {
600 659 $attachment_id = self::get_attachment_id_from_url( $url );
601 660 }
@@ -600,9 +659,9 @@
600 659 $attachment_id = self::get_attachment_id_from_url( $url );
601 660 }
602 661 } else {
603 662 if ( $job === 'offload_images' ) {
604 - $id_and_size = $this->get_local_attachement_id_from_url( $url );
663 + $id_and_size = $this->get_local_attachement_id_from_url( $url );
605 664 $attachment_id = $id_and_size['attachment_id'];
606 665 }
607 666 }
608 667 if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) {
@@ -610,8 +669,9 @@
610 669 }
611 670 $found_images[] = intval( $attachment_id );
612 671 }
613 672 }
673 +
614 674 return apply_filters( 'optml_content_images_to_update', $found_images, $content );
615 675 }
616 676
617 677 /**
@@ -630,12 +690,13 @@
630 690 }
631 691 $post_types = array_values(
632 692 array_filter(
633 693 get_post_types(),
634 - function( $post_type ) {
694 + function ( $post_type ) {
635 695 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
636 696 return false;
637 697 }
698 +
638 699 return true;
639 700 }
640 701 )
641 702 );
@@ -669,12 +730,12 @@
669 730 if ( get_post_type() !== 'attachment' ) {
670 731 $ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job );
671 732 if ( count( $ids ) > 0 ) {
672 733 $images_to_update[ $content_id ] = $ids;
673 - $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
734 + $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
674 735 if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) {
675 736 foreach ( $duplicated_pages as $duplicated_id ) {
676 - $duplicated_ids = $this->get_image_id_from_content( get_post_field( 'post_content', $duplicated_id ), $job );
737 + $duplicated_ids = $this->get_image_id_from_content( get_post_field( 'post_content', $duplicated_id ), $job );
677 738 $images_to_update[ $duplicated_id ] = $duplicated_ids;
678 739 }
679 740 }
680 741 }
@@ -687,12 +748,13 @@
687 748 delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG );
688 749 }
689 750 }
690 751 }
691 - $page ++;
752 + ++$page;
692 753 }
693 - $result['page'] = $page;
754 + $result['page'] = $page;
694 755 $result['imagesToUpdate'] = $images_to_update;
756 +
695 757 return $result;
696 758 }
697 759
698 760 /**
@@ -698,9 +760,9 @@
698 760 /**
699 761 * Add inline action to push to our servers.
700 762 *
701 763 * @param array $actions All actions.
702 - * @param \WP_Post $post The current post image object.
764 + * @param \WP_Post $post The current post image object.
703 765 *
704 766 * @return array
705 767 */
706 768 public function add_inline_media_action( $actions, $post ) {
@@ -711,41 +773,20 @@
711 773 $file = $meta['file'];
712 774 if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) {
713 775 return $actions;
714 776 }
715 - if ( ! self::is_uploaded_image( $file ) ) {
716 - $upload_action_url = add_query_arg(
717 - [
718 - 'page' => 'optimole',
719 - 'optimole_action' => 'offload_images',
720 - '0' => $post->ID,
721 - ],
722 - 'admin.php'
723 - );
777 + $actions['optml_actions'] = sprintf(
778 + '<span class="spinner"></span><a class="move-image-optml %s" data-action="offload_image" href="#" aria-label="%s" data-id="%s">%s</a><a class="move-image-optml %s" data-action="rollback_image" href="#" aria-label="%s" data-id="%s">%s</a>',
779 + self::is_uploaded_image( $file ) ? 'hidden' : '',
780 + esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
781 + $post->ID,
782 + esc_html__( 'Offload to Optimole', 'optimole-wp' ),
783 + self::is_uploaded_image( $file ) ? '' : 'hidden',
784 + esc_attr__( 'Restore image to media library', 'optimole-wp' ),
785 + $post->ID,
786 + esc_html__( 'Restore image to media library', 'optimole-wp' )
787 + );
724 788
725 - $actions['offload_images'] = sprintf(
726 - '<a href="%s" aria-label="%s">%s</a>',
727 - $upload_action_url,
728 - esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
729 - esc_html__( 'Offload to Optimole', 'optimole-wp' )
730 - );
731 - }
732 - if ( self::is_uploaded_image( $file ) ) {
733 - $rollback_action_url = add_query_arg(
734 - [
735 - 'page' => 'optimole',
736 - 'optimole_action' => 'rollback_images',
737 - '0' => $post->ID,
738 - ],
739 - 'admin.php'
740 - );
741 - $actions['rollback_images'] = sprintf(
742 - '<a href="%s" aria-label="%s">%s</a>',
743 - $rollback_action_url,
744 - esc_attr__( 'Restore image to media library', 'optimole-wp' ),
745 - esc_html__( 'Restore image to media library', 'optimole-wp' )
746 - );
747 - }
748 789 return $actions;
749 790 }
750 791
751 792 /**
@@ -751,8 +792,9 @@
751 792 /**
752 793 * Upload images to our servers and update inside pages.
753 794 *
754 795 * @param array $image_ids The id of the attachments for the selected images.
796 + *
755 797 * @return int The number of successfully processed images.
756 798 */
757 799 public function upload_and_update_existing_images( $image_ids ) {
758 800 $success_up = 0;
@@ -762,16 +804,17 @@
762 804 }
763 805 foreach ( $image_ids as $id ) {
764 806 if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) {
765 807 // if this meta flag below failed at the initial update but the file meta above is updated it will cause an infinite query loop
766 - update_post_meta( $id, 'optimole_offload', 'true' );
767 - $success_up ++;
808 + update_post_meta( $id, self::META_KEYS['offloaded'], 'true' );
809 + update_post_meta( $id, self::OM_OFFLOADED_FLAG, true );
810 + ++$success_up;
768 811 continue;
769 812 }
770 813
771 814 $meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id );
772 815 if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) {
773 - $success_up ++;
816 + ++$success_up;
774 817 wp_update_attachment_metadata( $id, $meta );
775 818 }
776 819 }
777 820 if ( $success_up > 0 ) {
@@ -779,8 +822,9 @@
779 822 do_action( 'optml_log', ' call post update, succesful images: ' );
780 823 do_action( 'optml_log', $success_up );
781 824 }
782 825 }
826 +
783 827 return $success_up;
784 828 }
785 829
786 830 /**
@@ -786,14 +830,16 @@
786 830 /**
787 831 * Return the original url of an image attachment.
788 832 *
789 833 * @param integer $post_id Image attachment id.
834 + *
790 835 * @return string|bool The original url of the image.
791 836 */
792 837 public static function get_original_url( $post_id ) {
793 838 self::$return_original_url = true;
794 - $original_url = wp_get_attachment_url( $post_id );
839 + $original_url = wp_get_attachment_url( $post_id );
795 840 self::$return_original_url = false;
841 +
796 842 return $original_url;
797 843 }
798 844
799 845 /**
@@ -799,8 +845,9 @@
799 845 /**
800 846 * Bring images back to media library and update inside pages.
801 847 *
802 848 * @param array $image_ids The id of the attachments for the selected images.
849 + *
803 850 * @return int The number of successfully processed images.
804 851 */
805 852 public function rollback_and_update_images( $image_ids ) {
806 853 $success_back = 0;
@@ -817,28 +864,29 @@
817 864 $current_meta = wp_get_attachment_metadata( $id );
818 865 if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) {
819 866 delete_post_meta( $id, self::META_KEYS['offloaded'] );
820 867 delete_post_meta( $id, self::OM_OFFLOADED_FLAG );
821 - $success_back++;
868 + ++$success_back;
822 869 continue;
823 870 }
824 - $table_id = [];
871 +
825 872 // Account for scaled images.
826 - $source_file = isset( $current_meta['original_image'] ) ? $current_meta['original_image'] : $current_meta['file']; // @phpstan-ignore-line - this exists for scaled images.
827 - $filename = pathinfo( $source_file, PATHINFO_BASENAME );
828 - preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $table_id );
829 - if ( ! isset( $table_id[1] ) ) {
873 + $source_file = isset( $current_meta['original_image'] ) ? $current_meta['original_image'] : $current_meta['file'];
874 + $filename = pathinfo( $source_file, PATHINFO_BASENAME );
875 + $image_id = preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $matches ) ? $matches[1] : null;
876 +
877 + if ( null === $image_id ) {
830 878 continue;
831 879 }
832 - $table_id = $table_id[1];
880 +
833 881 if ( OPTML_DEBUG_MEDIA ) {
834 882 do_action( 'optml_log', ' image cloud id ' );
835 - do_action( 'optml_log', $table_id );
883 + do_action( 'optml_log', $image_id );
836 884 }
837 - $request = new Optml_Api();
838 - $get_response = $request->call_upload_api( '', 'false', $table_id, 'false', 'true' );
839 885
840 - if ( is_wp_error( $get_response ) || wp_remote_retrieve_response_code( $get_response ) !== 200 ) {
886 + $image_url = Optimole::offload()->getImageUrl( $image_id );
887 +
888 + if ( null === $image_url ) {
841 889 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
842 890 if ( OPTML_DEBUG_MEDIA ) {
843 891 do_action( 'optml_log', ' error get url' );
844 892 }
@@ -843,13 +891,12 @@
843 891 do_action( 'optml_log', ' error get url' );
844 892 }
845 893
846 894 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
895 +
847 896 continue;
848 897 }
849 898
850 - $get_url = json_decode( $get_response['body'], true )['getUrl'];
851 -
852 899 if ( ! function_exists( 'download_url' ) ) {
853 900 include_once ABSPATH . 'wp-admin/includes/file.php';
854 901 }
855 902 if ( ! function_exists( 'download_url' ) ) {
@@ -856,9 +903,9 @@
856 903 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
857 904 continue;
858 905 }
859 906 $timeout_seconds = 60;
860 - $temp_file = download_url( $get_url, $timeout_seconds );
907 + $temp_file = download_url( $image_url, $timeout_seconds );
861 908
862 909 if ( is_wp_error( $temp_file ) ) {
863 910 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
864 911 if ( OPTML_DEBUG_MEDIA ) {
@@ -864,9 +911,9 @@
864 911 if ( OPTML_DEBUG_MEDIA ) {
865 912 do_action( 'optml_log', ' download_url error ' );
866 913 }
867 914
868 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
915 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error downloading URL.' );
869 916 continue;
870 917 }
871 918
872 919 $extension = $this->get_ext( $filename );
@@ -892,12 +939,12 @@
892 939 ];
893 940
894 941 $overrides = [
895 942 // do not expect the default form data from normal uploads
896 - 'test_form' => false,
943 + 'test_form' => false,
897 944
898 945 // Setting this to false lets WordPress allow empty files, not recommended.
899 - 'test_size' => true,
946 + 'test_size' => true,
900 947
901 948 // A properly uploaded file will pass this test. There should be no reason to override this one.
902 949 'test_upload' => true,
903 950 ];
@@ -974,17 +1021,17 @@
974 1021 delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
975 1022 }
976 1023 }
977 1024 }
978 - $success_back++;
1025 + ++$success_back;
979 1026
980 1027 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
981 1028
982 - $original_url = self::get_original_url( $id );
1029 + $original_url = self::get_original_url( $id );
983 1030 if ( $original_url === false ) {
984 1031 continue;
985 1032 }
986 - $this->delete_attachment_from_server( $original_url, $id, $table_id );
1033 + $this->delete_attachment_from_server( $original_url, $id, $image_id );
987 1034 }
988 1035
989 1036 if ( $success_back > 0 ) {
990 1037 if ( OPTML_DEBUG_MEDIA ) {
@@ -991,8 +1038,9 @@
991 1038 do_action( 'optml_log', ' call update post, success rollback' );
992 1039 do_action( 'optml_log', $success_back );
993 1040 }
994 1041 }
1042 +
995 1043 return $success_back;
996 1044 }
997 1045
998 1046 /**
@@ -997,11 +1045,12 @@
997 1045
998 1046 /**
999 1047 * Handle the bulk actions.
1000 1048 *
1001 - * @param string $redirect The current url from the media library.
1002 - * @param string $doaction The current action selected.
1049 + * @param string $redirect The current url from the media library.
1050 + * @param string $doaction The current action selected.
1003 1051 * @param array $image_ids The id of the attachments for the selected images.
1052 + *
1004 1053 * @return string The url with the correspondent query args for the executed actions.
1005 1054 */
1006 1055 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
1007 1056
@@ -1009,28 +1058,29 @@
1009 1058 return $redirect;
1010 1059 }
1011 1060
1012 1061 $image_ids = array_slice( $image_ids, 0, 20, true );
1013 - $redirect = 'admin.php';
1014 - $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
1015 - $redirect = add_query_arg( 'page', 'optimole', $redirect );
1016 - $redirect = add_query_arg( $image_ids, $redirect );
1062 + $redirect = 'admin.php';
1063 + $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
1064 + $redirect = add_query_arg( 'page', 'optimole', $redirect );
1065 + $redirect = add_query_arg( $image_ids, $redirect );
1066 +
1017 1067 return $redirect;
1018 -
1019 1068 }
1020 1069
1021 1070 /**
1022 1071 * Register the bulk media actions.
1023 1072 *
1024 - * @param array $bulk_array The existing actions array.
1025 - * @return array The array with the appended actions.
1073 + * @param array $bulk_array The existing actions array.
1074 + *
1075 + * @return array The array with the appended actions.
1026 1076 */
1027 1077 public function register_bulk_media_actions( $bulk_array ) {
1028 1078
1029 - $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1079 + $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1030 1080 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
1081 +
1031 1082 return $bulk_array;
1032 -
1033 1083 }
1034 1084
1035 1085 /**
1036 1086 * Send delete request to our servers and update the meta.
@@ -1036,19 +1086,15 @@
1036 1086 * Send delete request to our servers and update the meta.
1037 1087 *
1038 1088 * @param string $original_url Original url of the image.
1039 1089 * @param integer $post_id Image id inside db.
1040 - * @param string $table_id Our cloud id for the image.
1090 + * @param string $image_id Our cloud id for the image.
1041 1091 */
1042 - public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
1043 - $request = new Optml_Api();
1044 - $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
1092 + public function delete_attachment_from_server( $original_url, $post_id, $image_id ) {
1093 + Optimole::offload()->deleteImage( $image_id );
1045 1094
1046 1095 delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
1047 1096 delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
1048 - if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
1049 - // should add some routine to retry delete once if delete fails
1050 - }
1051 1097 }
1052 1098
1053 1099 /**
1054 1100 * Delete an image from our servers after it is removed from media.
@@ -1071,9 +1117,9 @@
1071 1117 }
1072 1118
1073 1119 $file = $file['file'];
1074 1120 if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
1075 - $original_url = self::get_original_url( $post_id );
1121 + $original_url = self::get_original_url( $post_id );
1076 1122 if ( $original_url === false ) {
1077 1123 return;
1078 1124 }
1079 1125 $table_id = [];
@@ -1091,8 +1137,9 @@
1091 1137 * Get optimized URL for an attachment image if it is uploaded to our servers.
1092 1138 *
1093 1139 * @param string $url The current url.
1094 1140 * @param int $attachment_id The attachment image id.
1141 + *
1095 1142 * @return string Optimole cdn URL.
1096 1143 * @uses filter:wp_get_attachment_url
1097 1144 */
1098 1145 public function get_image_attachment_url( $url, $attachment_id ) {
@@ -1112,10 +1159,9 @@
1112 1159 }
1113 1160
1114 1161 $file = $meta['file'];
1115 1162 if ( self::is_uploaded_image( $file ) ) {
1116 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1117 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1163 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1118 1164 } else {
1119 1165 // this is for the users that already offloaded the images before the other fixes
1120 1166 $local_file = get_attached_file( $attachment_id );
1121 1167 if ( ! file_exists( $local_file ) ) {
@@ -1124,10 +1170,9 @@
1124 1170 foreach ( $duplicated_images as $id ) {
1125 1171 if ( ! empty( $id ) ) {
1126 1172 $duplicated_meta = wp_get_attachment_metadata( $id );
1127 1173 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1128 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1129 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1174 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1130 1175 }
1131 1176 }
1132 1177 }
1133 1178 }
@@ -1145,11 +1190,11 @@
1145 1190
1146 1191 /**
1147 1192 * Filter the requested image url.
1148 1193 *
1149 - * @param bool|array $image The previous image value (null).
1194 + * @param bool|array $image The previous image value (null).
1150 1195 * @param int $attachment_id The ID of the attachment.
1151 - * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1196 + * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1152 1197 *
1153 1198 * @return bool|array The image sizes and optimized url.
1154 1199 * @uses filter:image_downsize
1155 1200 */
@@ -1181,27 +1226,15 @@
1181 1226 if ( ! isset( $id_filename[1] ) ) {
1182 1227 return $image;
1183 1228 }
1184 1229 $url = self::get_original_url( $attachment_id );
1185 - $optimized_url = ( new Optml_Image(
1186 - $url,
1187 - [
1188 - 'width' => $data['width'],
1189 - 'height' => $data['height'],
1190 - 'resize' => $resize,
1191 - 'quality' => $this->settings->get_numeric_quality(),
1192 - ],
1193 - $this->settings->get( 'cache_buster' )
1194 - ) )->get_url();
1195 - $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1196 - $image = [
1197 - $optimized_url,
1230 +
1231 + return [
1232 + str_replace( $url, $id_filename[1], $this->get_optimized_image_url( $url, $data['width'], $data['height'], $resize ) ),
1198 1233 $data['width'],
1199 1234 $data['height'],
1200 1235 true,
1201 1236 ];
1202 -
1203 - return $image;
1204 1237 }
1205 1238
1206 1239 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1207 1240 return $image;
@@ -1212,9 +1245,9 @@
1212 1245
1213 1246 /**
1214 1247 * Get image extension.
1215 1248 *
1216 - * @param string $path Image path.
1249 + * @param string $path Image path.
1217 1250 *
1218 1251 * @return string
1219 1252 */
1220 1253 private function get_ext( $path ) {
@@ -1221,11 +1254,34 @@
1221 1254 return pathinfo( $path, PATHINFO_EXTENSION );
1222 1255 }
1223 1256
1224 1257 /**
1258 + * Mark an image as having a retryable error.
1259 + *
1260 + * @param int $attachment_id The attachment ID.
1261 + * @param string $reason The reason for the error.
1262 + */
1263 + public static function mark_retryable_error( $attachment_id, $reason ) {
1264 + static $allowed_retries = 5;
1265 +
1266 + $retries = get_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER, true );
1267 + $retries = empty( $retries ) ? 0 : (int) $retries;
1268 + if ( $retries >= $allowed_retries ) {
1269 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' ' . $reason . '. Reached the maximum number of retries.' );
1270 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1271 +
1272 + return;
1273 + }
1274 +
1275 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' ' . $reason . '. Marked for retry, retries done: ' . $retries );
1276 +
1277 + update_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER, ( $retries + 1 ) );
1278 + }
1279 +
1280 + /**
1225 1281 * Update image meta with optimized cdn path.
1226 1282 *
1227 - * @param array $meta Meta information of the image.
1283 + * @param array $meta Meta information of the image.
1228 1284 * @param int $attachment_id The image attachment ID.
1229 1285 *
1230 1286 * @return array
1231 1287 * @uses filter:wp_generate_attachment_metadata
@@ -1249,8 +1305,9 @@
1249 1305 do_action( 'optml_log', $meta );
1250 1306 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1251 1307
1252 1308 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
1309 +
1253 1310 return $meta;
1254 1311 }
1255 1312 // Skip images based on filters.
1256 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
@@ -1255,11 +1312,12 @@
1255 1312 // Skip images based on filters.
1256 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1257 1314 do_action( 'optml_log', 'optimization filter' );
1258 1315 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1316 +
1259 1317 return $meta;
1260 1318 }
1261 - $original_url = self::get_original_url( $attachment_id );
1319 + $original_url = self::get_original_url( $attachment_id );
1262 1320
1263 1321 // Could not find original URL.
1264 1322 if ( $original_url === false ) {
1265 1323 do_action( 'optml_log', 'error getting original url' );
@@ -1265,8 +1323,9 @@
1265 1323 do_action( 'optml_log', 'error getting original url' );
1266 1324 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1267 1325
1268 1326 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
1327 +
1269 1328 return $meta;
1270 1329 }
1271 1330
1272 1331 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
@@ -1272,14 +1331,14 @@
1272 1331 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
1273 1332 $original_url = $this->maybe_strip_scaled( $original_url );
1274 1333 $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
1275 1334
1276 - $extension = $this->get_ext( $local_file );
1277 - $content_type = Optml_Config::$image_extensions [ $extension ];
1278 - $temp = explode( '/', $local_file );
1279 - $file_name = end( $temp );
1335 + $extension = $this->get_ext( $local_file );
1336 + $content_type = Optml_Config::$image_extensions [ $extension ];
1337 + $temp = explode( '/', $local_file );
1338 + $file_name = end( $temp );
1280 1339 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1281 - $original_name = $file_name;
1340 + $original_name = $file_name;
1282 1341 if ( OPTML_DEBUG_MEDIA ) {
1283 1342 do_action( 'optml_log', 'file before replace' );
1284 1343 do_action( 'optml_log', $local_file );
1285 1344 }
@@ -1286,10 +1345,10 @@
1286 1345
1287 1346 // check if the current filename is the last deduplicated filename
1288 1347 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1289 1348 // replace the file with the original before deduplication to get the path where the image is uploaded
1290 - $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1291 - $original_name = self::$last_deduplicated;
1349 + $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1350 + $original_name = self::$last_deduplicated;
1292 1351 self::$last_deduplicated = false;
1293 1352 }
1294 1353 if ( OPTML_DEBUG_MEDIA ) {
1295 1354 do_action( 'optml_log', 'file after replace' );
@@ -1300,8 +1359,9 @@
1300 1359 do_action( 'optml_log', 'missing file' );
1301 1360 do_action( 'optml_log', $local_file );
1302 1361
1303 1362 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1363 +
1304 1364 return $meta;
1305 1365 }
1306 1366
1307 1367 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
@@ -1309,8 +1369,9 @@
1309 1369 do_action( 'optml_log', 'invalid extension' );
1310 1370 do_action( 'optml_log', $extension );
1311 1371
1312 1372 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
1373 +
1313 1374 return $meta;
1314 1375 }
1315 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1316 1377 do_action( 'optml_log', 'extension filter' );
@@ -1315,127 +1376,106 @@
1315 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1316 1377 do_action( 'optml_log', 'extension filter' );
1317 1378 do_action( 'optml_log', $extension );
1318 1379 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1380 +
1319 1381 return $meta;
1320 1382 }
1321 1383
1322 - $request = new Optml_Api();
1323 - $generate_url_response = $request->call_upload_api( $original_url );
1384 + $offload_manager = Optimole::offload();
1385 + $offload_usage = $offload_manager->getUsage();
1324 1386
1325 - if ( is_wp_error( $generate_url_response ) || wp_remote_retrieve_response_code( $generate_url_response ) !== 200 ) {
1387 + $current_run = self::get_process_meta();
1388 + $remaining = isset( $current_run['remaining'] ) ? absint( $current_run['remaining'] ) : 0;
1326 1389
1327 - $decoded_response = json_decode( $generate_url_response['body'], true );
1390 + if ( $remaining + $offload_usage->getCurrent() >= $offload_usage->getLimit() ) {
1391 + if ( OPTML_DEBUG_MEDIA ) {
1392 + do_action( 'optml_log', 'limit exceeded' );
1393 + do_action( 'optml_log', $offload_usage );
1394 + }
1328 1395
1329 - // Handle limit exceeded
1330 - if ( isset( $decoded_response['error'] ) && $decoded_response['error'] === 'limit_exceeded' ) {
1331 - if ( OPTML_DEBUG_MEDIA ) {
1332 - do_action( 'optml_log', 'limit exceeded error' );
1333 - do_action( 'optml_log', $decoded_response );
1334 - }
1396 + self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
1335 1397
1336 - self::$instance->settings->update( 'offload_limit', absint( $decoded_response['limit'] ) );
1337 - self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
1338 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: reached asset offload limit.' );
1398 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: offloading images would exceed limit.' );
1339 1399
1340 - return $meta;
1341 - }
1342 -
1343 - if ( OPTML_DEBUG_MEDIA ) {
1344 - do_action( 'optml_log', ' call to signed url error' );
1345 - do_action( 'optml_log', $generate_url_response );
1346 - }
1347 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1348 -
1349 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid signed url.' );
1350 1400 return $meta;
1351 1401 }
1352 - $decoded_response = json_decode( $generate_url_response['body'], true );
1353 1402
1354 - // Update the offload limit if it has changed.
1355 - if ( isset( $decoded_response['limit'] ) && isset( $decoded_response['count'] ) ) {
1356 - $remote_limit = absint( $decoded_response['limit'] );
1403 + try {
1404 + $image_id = $offload_manager->uploadImage( $local_file, $original_url );
1357 1405
1358 - self::$instance->settings->update( 'offload_limit', $remote_limit );
1406 + if ( OPTML_DEBUG_MEDIA ) {
1407 + do_action( 'optml_log', 'image id' );
1408 + do_action( 'optml_log', $image_id );
1409 + }
1359 1410
1360 - $current_run = self::get_process_meta();
1361 - $remaining = isset( $current_run['remaining'] ) ? absint( $current_run['remaining'] ) : 0;
1411 + // We clear the retry counter if we reach this point.
1412 + delete_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER );
1413 + } catch ( InvalidArgumentException $exception ) {
1414 + if ( OPTML_DEBUG_MEDIA ) {
1415 + do_action( 'optml_log', 'invalid argument exception' );
1416 + do_action( 'optml_log', $exception );
1417 + }
1362 1418
1363 - if ( $remaining + $decoded_response['count'] >= $remote_limit ) {
1364 - if ( OPTML_DEBUG_MEDIA ) {
1365 - do_action( 'optml_log', 'limit exceeded' );
1366 - do_action( 'optml_log', $decoded_response );
1367 - }
1419 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1368 1420
1369 - self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
1370 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: offloading images would exceed limit.' );
1421 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' file is missing or unreadable.' );
1371 1422
1372 - return $meta;
1423 + return $meta;
1424 + } catch ( InvalidUploadApiResponseException $exception ) {
1425 + if ( OPTML_DEBUG_MEDIA ) {
1426 + do_action( 'optml_log', 'missing table id or upload url' );
1427 + do_action( 'optml_log', $exception );
1373 1428 }
1374 - }
1375 1429
1376 - if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1377 - if ( OPTML_DEBUG_MEDIA ) {
1378 - do_action( 'optml_log', ' missing table id or upload url' );
1379 - do_action( 'optml_log', $decoded_response );
1380 - }
1381 1430 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1382 1431
1383 1432 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
1433 +
1384 1434 return $meta;
1385 - }
1386 - $table_id = $decoded_response['tableId'];
1387 - if ( OPTML_DEBUG_MEDIA ) {
1388 - do_action( 'optml_log', ' table id' );
1389 - do_action( 'optml_log', $table_id );
1390 - }
1391 - $upload_signed_url = $decoded_response['uploadUrl'];
1392 - $image = file_get_contents( $local_file );
1393 - if ( $image === false ) {
1394 - do_action( 'optml_log', 'can not find file' );
1395 - do_action( 'optml_log', $local_file );
1435 + } catch ( UploadFailedException $exception ) {
1436 + if ( OPTML_DEBUG_MEDIA ) {
1437 + do_action( 'optml_log', 'upload error' );
1438 + do_action( 'optml_log', $exception );
1439 + }
1440 +
1396 1441 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1397 1442
1398 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1443 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1444 +
1399 1445 return $meta;
1400 - }
1401 - if ( $upload_signed_url !== 'found_resource' ) {
1446 + } catch ( UploadLimitException $exception ) {
1447 + if ( OPTML_DEBUG_MEDIA ) {
1448 + do_action( 'optml_log', 'limit exceeded' );
1449 + do_action( 'optml_log', $exception );
1450 + }
1402 1451
1403 - $request = new Optml_Api();
1404 - $result = $request->upload_image( $upload_signed_url, $content_type, $image );
1452 + self::$instance->settings->update( 'offload_limit', $exception->getUsage()->getLimit() );
1453 + self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
1405 1454
1406 - if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1407 - do_action( 'optml_log', 'upload error' );
1408 - do_action( 'optml_log', $result );
1409 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1455 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: upload limit exceeded' );
1410 1456
1411 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1412 - return $meta;
1457 + return $meta;
1458 + } catch ( UploadApiException $exception ) {
1459 + if ( OPTML_DEBUG_MEDIA ) {
1460 + do_action( 'optml_log', 'upload api error' );
1461 + do_action( 'optml_log', $exception );
1413 1462 }
1414 - $file_size = filesize( $local_file );
1415 - if ( $file_size === false ) {
1416 - $file_size = 0;
1463 +
1464 + self::mark_retryable_error( $attachment_id, 'Error from upload api:' . $exception->getMessage() );
1465 +
1466 + return $meta;
1467 + } catch ( RuntimeException $exception ) {
1468 + if ( OPTML_DEBUG_MEDIA ) {
1469 + do_action( 'optml_log', 'runtime exception' );
1470 + do_action( 'optml_log', $exception );
1417 1471 }
1418 - $request = new Optml_Api();
1419 - $result_update = $request->call_upload_api(
1420 - $original_url,
1421 - 'false',
1422 - $table_id,
1423 - 'success',
1424 - 'false',
1425 - $meta['width'],
1426 - $meta['height'],
1427 - $file_size
1428 - );
1429 - if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1430 - do_action( 'optml_log', 'dynamo update error' );
1431 - do_action( 'optml_log', $result_update );
1432 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1433 1472
1434 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has dynamo update error.' );
1435 - return $meta;
1436 - }
1473 + self::mark_retryable_error( $attachment_id, 'Unknown error from upload api: ' . $exception->getMessage() );
1474 +
1475 + return $meta;
1437 1476 }
1477 +
1438 1478 $url_to_append = $original_url;
1439 1479 $url_parts = parse_url( $original_url );
1440 1480
1441 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
@@ -1440,24 +1480,31 @@
1440 1480
1441 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1442 1482 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1443 1483 }
1444 - $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1445 - $request = new Optml_Api();
1446 - if ( $request->check_optimized_url( $optimized_url ) === false ) {
1484 +
1485 + $optimized_url = $this->get_media_optimized_url( $url_to_append, $image_id );
1486 +
1487 + if ( ( new Optml_Api() )->check_optimized_url( $optimized_url ) === false ) {
1447 1488 do_action( 'optml_log', 'optimization error' );
1448 1489 do_action( 'optml_log', $optimized_url );
1449 - $request->call_upload_api( $original_url, 'true', $table_id );
1490 +
1491 + Optimole::offload()->deleteImage( $image_id );
1492 +
1450 1493 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1451 1494
1452 1495 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
1496 +
1453 1497 return $meta;
1454 1498 }
1455 - unlink( $local_file );
1499 +
1500 + @unlink( $local_file );
1501 +
1456 1502 update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
1457 1503 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1458 - $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1459 1504
1505 + $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $image_id . '/' . $url_to_append;
1506 +
1460 1507 if ( isset( $meta['sizes'] ) ) {
1461 1508 foreach ( $meta['sizes'] as $key => $value ) {
1462 1509 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1463 1510 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
@@ -1503,8 +1550,9 @@
1503 1550 self::$offload_update_post = true;
1504 1551 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1505 1552 self::$offload_update_post = false;
1506 1553 }
1554 +
1507 1555 return $meta;
1508 1556 }
1509 1557
1510 1558 /**
@@ -1511,8 +1559,9 @@
1511 1559 * Get the args for wp query according to the scope.
1512 1560 *
1513 1561 * @param int $batch Number of images to get.
1514 1562 * @param string $action The action for which to get the images.
1563 + *
1515 1564 * @return array|false The query options array or false if not passed a valid action.
1516 1565 */
1517 1566 public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
1518 1567
@@ -1523,11 +1572,11 @@
1523 1572 'no_found_rows' => true,
1524 1573 ];
1525 1574
1526 1575 if ( $get_images === true ) {
1527 - $args['post_type'] = 'attachment';
1576 + $args['post_type'] = 'attachment';
1528 1577 $args['post_mime_type'] = 'image';
1529 - $args['post_status'] = 'inherit';
1578 + $args['post_status'] = 'inherit';
1530 1579
1531 1580 // Offload args.
1532 1581 if ( $action === 'offload_images' ) {
1533 1582 $args['meta_query'] = [
@@ -1540,8 +1589,9 @@
1540 1589 'key' => self::META_KEYS['offload_error'],
1541 1590 'compare' => 'NOT EXISTS',
1542 1591 ],
1543 1592 ];
1593 +
1544 1594 return $args;
1545 1595 }
1546 1596
1547 1597 // Rollback args.
@@ -1585,8 +1635,9 @@
1585 1635 /**
1586 1636 * Query the database and upload images to our servers.
1587 1637 *
1588 1638 * @param int $batch Number of images to process in a batch.
1639 + *
1589 1640 * @return array Number of found images and number of successfully processed images.
1590 1641 */
1591 1642 public function upload_images( $batch, $images = [] ) {
1592 1643 self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
@@ -1591,16 +1642,17 @@
1591 1642 public function upload_images( $batch, $images = [] ) {
1592 1643 self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1593 1644
1594 1645 if ( empty( $images ) || $images === 'none' ) {
1595 - $args = self::get_images_or_pages_query_args( $batch, 'offload_images', true );
1646 + $args = self::get_images_or_pages_query_args( $batch, 'offload_images', true );
1596 1647 $attachments = new \WP_Query( $args );
1597 - $ids = $attachments->get_posts();
1648 + $ids = $attachments->get_posts();
1598 1649 } else {
1599 1650 $ids = array_slice( $images, 0, $batch );
1600 1651 }
1601 - $result = [ 'found_images' => count( $ids ) ];
1652 + $result = [ 'found_images' => count( $ids ) ];
1602 1653 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1654 +
1603 1655 return $result;
1604 1656 }
1605 1657
1606 1658 /**
@@ -1606,20 +1658,22 @@
1606 1658 /**
1607 1659 * Query the database and bring back image to media library.
1608 1660 *
1609 1661 * @param int $batch Number of images to process in a batch.
1662 + *
1610 1663 * @return array Number of found images and number of successfully processed images.
1611 1664 */
1612 1665 public function rollback_images( $batch, $images = [] ) {
1613 1666 if ( empty( $images ) || $images === 'none' ) {
1614 - $args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true );
1667 + $args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true );
1615 1668 $attachments = new \WP_Query( $args );
1616 - $ids = $attachments->get_posts();
1669 + $ids = $attachments->get_posts();
1617 1670 } else {
1618 1671 $ids = array_slice( $images, 0, $batch );
1619 1672 }
1620 - $result = [ 'found_images' => count( $ids ) ];
1673 + $result = [ 'found_images' => count( $ids ) ];
1621 1674 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1675 +
1622 1676 return $result;
1623 1677 }
1624 1678
1625 1679 /**
@@ -1625,18 +1679,20 @@
1625 1679 /**
1626 1680 * Update the post with the given id, the images will be updated by the filters we use.
1627 1681 *
1628 1682 * @param int $post_id The post id to update.
1683 + *
1629 1684 * @return bool Whether the update was succesful or not.
1630 1685 */
1631 1686 public function update_page( $post_id ) {
1632 1687 self::$offload_update_post = true;
1633 - $post_update = wp_update_post( ['ID' => $post_id] );
1688 + $post_update = wp_update_post( [ 'ID' => $post_id ] );
1634 1689 self::$offload_update_post = false;
1635 1690 if ( $post_update === 0 ) {
1636 1691 return false;
1637 1692 }
1638 1693 do_action( 'optml_updated_post', $post_id );
1694 +
1639 1695 return true;
1640 1696 }
1641 1697
1642 1698 /**
@@ -1642,12 +1698,13 @@
1642 1698 /**
1643 1699 * Calculate the number of images in media library and the number of posts/pages.
1644 1700 *
1645 1701 * @param string $action The actions for which to get the number of images.
1702 + *
1646 1703 * @return int Number of images.
1647 1704 */
1648 1705 public static function number_of_images_and_pages( $action ) {
1649 - $images_args = self::get_images_or_pages_query_args( -1, $action, true );
1706 + $images_args = self::get_images_or_pages_query_args( - 1, $action, true );
1650 1707
1651 1708 $images = new \WP_Query( $images_args );
1652 1709
1653 1710 // With the new mechanism, when offloading images, we don't need to address pages anymore.
@@ -1655,10 +1712,10 @@
1655 1712 if ( $action === 'offload_images' ) {
1656 1713 return $images->post_count;
1657 1714 }
1658 1715
1659 - $pages_args = self::get_images_or_pages_query_args( -1, $action );
1660 - $pages = new \WP_Query( $pages_args );
1716 + $pages_args = self::get_images_or_pages_query_args( - 1, $action );
1717 + $pages = new \WP_Query( $pages_args );
1661 1718
1662 1719 return $pages->post_count + $images->post_count;
1663 1720 }
1664 1721
@@ -1665,8 +1722,9 @@
1665 1722 /**
1666 1723 * Calculate the number of images in media library and the number of posts/pages by IDs.
1667 1724 *
1668 1725 * @param string $action The actions for which to get the number of images.
1726 + *
1669 1727 * @return int Number of images.
1670 1728 */
1671 1729 public static function number_of_images_by_ids( $action, $ids ) {
1672 1730 $args = self::get_images_or_pages_query_args( - 1, $action, true );
@@ -1689,9 +1747,9 @@
1689 1747 return [];
1690 1748 }
1691 1749
1692 1750 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1693 - $transient = get_transient( $transient_key );
1751 + $transient = get_transient( $transient_key );
1694 1752
1695 1753 if ( false !== $transient ) {
1696 1754 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1697 1755 }
@@ -1698,10 +1756,10 @@
1698 1756
1699 1757 global $wpdb;
1700 1758
1701 1759 $image_urls = array_map(
1702 - function( $image_id ) {
1703 - $meta = wp_get_attachment_metadata( $image_id );
1760 + function ( $image_id ) {
1761 + $meta = wp_get_attachment_metadata( $image_id );
1704 1762 $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1705 1763
1706 1764 return str_replace( '.' . $extension, '', $meta['file'] );
1707 1765 },
@@ -1724,11 +1782,11 @@
1724 1782 }
1725 1783 }
1726 1784
1727 1785 // Get all the posts IDs by using LIMIT and offset in a loop.
1728 - $ids = [];
1786 + $ids = [];
1729 1787 $offset = 0;
1730 - $limit = $batch;
1788 + $limit = $batch;
1731 1789
1732 1790 while ( true ) {
1733 1791 $posts = $wpdb->get_col(
1734 1792 $wpdb->prepare(
@@ -1741,9 +1799,9 @@
1741 1799 if ( empty( $posts ) ) {
1742 1800 break;
1743 1801 }
1744 1802
1745 - $ids = array_merge( $ids, $posts );
1803 + $ids = array_merge( $ids, $posts );
1746 1804 $offset += $limit;
1747 1805 }
1748 1806
1749 1807 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
@@ -1758,11 +1816,11 @@
1758 1816 *
1759 1817 * @return void
1760 1818 */
1761 1819 public static function record_process_meta( $count ) {
1762 - $meta = get_option( 'optml_process_meta', [] );
1763 - $meta['count'] = $count;
1764 - $meta['remaining'] = $count;
1820 + $meta = get_option( 'optml_process_meta', [] );
1821 + $meta['count'] = $count;
1822 + $meta['remaining'] = $count;
1765 1823 $meta['start_time'] = time();
1766 1824 update_option( 'optml_process_meta', $meta );
1767 1825 }
1768 1826
@@ -1787,13 +1845,14 @@
1787 1845 *
1788 1846 * @return array
1789 1847 */
1790 1848 public static function get_process_meta() {
1791 - $res = [];
1792 - $meta = get_option( 'optml_process_meta', [] );
1849 + $res = [];
1850 + $meta = get_option( 'optml_process_meta', [] );
1793 1851 $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1794 - $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1795 - $res['remaining'] = isset( $meta['remaining'] ) ? $meta['remaining'] : $res['count'];
1852 + $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1853 + $res['remaining'] = isset( $meta['remaining'] ) ? $meta['remaining'] : $res['count'];
1854 +
1796 1855 return $res;
1797 1856 }
1798 1857
1799 1858 /**
@@ -1800,23 +1859,18 @@
1800 1859 * Calculate the number of images in media library and the number of posts/pages.
1801 1860 *
1802 1861 * @param string $action The actions for which to get the number of images.
1803 1862 * @param bool $refresh Whether to refresh the cron or not.
1804 - * @param array $images The images to process.
1805 1863 *
1806 1864 * @return array Image count and Cron status.
1807 1865 */
1808 - public static function get_image_count( $action, $refresh, $images = [] ) {
1866 + public static function move_images( $action, $refresh ) {
1809 1867 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1810 - $count = 0;
1811 - $step = 0;
1812 - $batch = apply_filters( 'optimole_offload_batch', 20 ); // Reduce this to smaller if we have memory issues during testing.
1868 + $count = 0;
1869 + $step = 0;
1870 + $batch = apply_filters( 'optimole_offload_batch', 20 ); // Reduce this to smaller if we have memory issues during testing.
1813 1871
1814 - if ( empty( $images ) ) {
1815 - $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1816 - } else {
1817 - $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1818 - }
1872 + $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1819 1873
1820 1874 $possible_batch = ceil( $count / 10 );
1821 1875
1822 1876 if ( $possible_batch < $batch ) {
@@ -1832,12 +1886,18 @@
1832 1886
1833 1887 if ( $count === 0 ) {
1834 1888 $in_progress = false;
1835 1889 }
1836 -
1837 1890 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1838 -
1891 + self::$instance->settings->update( 'transfer_status', $action );
1839 1892 if ( false === $refresh ) {
1893 + // We check also the alternative action to avoid doing both in the same time and disable the running one.
1894 + $in_progress_b = self::$instance->settings->get( 'rollback_images' === $action ? 'offloading_status' : 'rollback_status' ) !== 'disabled';
1895 + // We do this only if there is a mass action in progress, not individual ones.
1896 + if ( $in_progress_b ) {
1897 + // We stop the oposite action from going any further.
1898 + self::$instance->settings->update( 'rollback_images' === $action ? 'offloading_status' : 'rollback_status', 'disabled' );
1899 + }
1840 1900 self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1841 1901 self::record_process_meta( $count );
1842 1902
1843 1903 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
@@ -1851,9 +1911,12 @@
1851 1911 'action' => $type,
1852 1912 ];
1853 1913 }
1854 1914
1855 - if ( empty( $images ) ) {
1915 + // We acquire a lock to prevent multiple workers from running the same action concurrently.
1916 + $lock_token = self::acquire_transfer_lock( $action );
1917 +
1918 + if ( false !== $lock_token ) {
1856 1919 $total = ceil( $count / $batch );
1857 1920 self::schedule_action(
1858 1921 time(),
1859 1922 'optml_start_processing_images',
@@ -1862,21 +1925,11 @@
1862 1925 $batch,
1863 1926 1,
1864 1927 $total,
1865 1928 $step,
1929 + $lock_token,
1866 1930 ]
1867 1931 );
1868 - } else {
1869 - self::schedule_action(
1870 - time(),
1871 - 'optml_start_processing_images_by_id',
1872 - [
1873 - $action,
1874 - $batch,
1875 - 1,
1876 - $images,
1877 - ]
1878 - );
1879 1932 }
1880 1933 }
1881 1934
1882 1935 $response = [
@@ -1898,8 +1951,9 @@
1898 1951 $response['status'] = $in_progress;
1899 1952
1900 1953 return $response;
1901 1954 }
1955 +
1902 1956 /**
1903 1957 * Schedule an action.
1904 1958 *
1905 1959 * @param int $time The time to schedule the action.
@@ -1908,12 +1962,8 @@
1908 1962 *
1909 1963 * @return mixed
1910 1964 */
1911 1965 public static function schedule_action( $time, $hook, $args ) {
1912 - if ( self::$instance->settings->is_offload_limit_reached() ) {
1913 - return null;
1914 - }
1915 -
1916 1966 // We use AS if available to avoid issues with WP Cron.
1917 1967 if ( function_exists( 'as_schedule_single_action' ) ) {
1918 1968 return as_schedule_single_action( $time, $hook, $args );
1919 1969 } else {
@@ -1919,106 +1969,147 @@
1919 1969 } else {
1920 1970 return wp_schedule_single_event( $time, $hook, $args );
1921 1971 }
1922 1972 }
1973 +
1923 1974 /**
1924 - * Start Processing Images by IDs
1975 + * Check if an action hook is scheduled.
1925 1976 *
1926 - * @param string $action The action for which to get the number of images.
1927 - * @param int $batch The batch of images to process.
1928 - * @param int $page The page of images to process.
1929 - * @param array $image_ids The images to process.
1977 + * @param string $hook The hook to check.
1930 1978 *
1931 - * @return void
1979 + * @return bool
1932 1980 */
1933 - public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1934 - $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1935 - $type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK;
1981 + public static function is_scheduled( $hook ) {
1982 + if ( function_exists( 'as_has_scheduled_action' ) ) {
1983 + return as_has_scheduled_action( $hook );
1984 + } elseif ( function_exists( 'as_next_scheduled_action' ) ) {
1985 + // For older versions of AS.
1986 + return as_next_scheduled_action( $hook ) !== false;
1987 + } else {
1988 + return wp_next_scheduled( $hook ) !== false;
1989 + }
1990 + }
1936 1991
1937 - if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1938 - return;
1992 + /**
1993 + * Attempt to acquire the transfer lock for a given action.
1994 + *
1995 + * @param string $action The transfer action ('offload_images'|'rollback_images').
1996 + *
1997 + * @return string|false The lock token on success, false if another worker already holds the lock.
1998 + */
1999 + public static function acquire_transfer_lock( $action ) {
2000 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
2001 + if ( false !== $lock ) {
2002 + return false;
1939 2003 }
1940 2004
1941 - set_time_limit( 0 );
2005 + $token = wp_generate_uuid4();
1942 2006
1943 - // Only use the legacy offloaded attachments to query the pages that need to be updated.
1944 - // We can be confident that these IDs are already marked as offloaded.
1945 - $legacy_offloaded = array_filter(
1946 - $image_ids,
1947 - function( $id ) {
1948 - return ! $this->is_new_offloaded_attachment( $id );
1949 - }
2007 + set_transient(
2008 + self::TRANSFER_LOCK_TRANSIENT,
2009 + [
2010 + 'token' => $token,
2011 + 'action' => $action,
2012 + ],
2013 + self::TRANSFER_LOCK_TTL
1950 2014 );
1951 2015
1952 - // On the new mechanism, we don't update posts anymore when offloading.
1953 - $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
2016 + return $token;
2017 + }
1954 2018
1955 - if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
1956 - $meta = self::get_process_meta();
1957 - self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
2019 + /**
2020 + * Renew the transfer lock if we still own it, extending its expiration.
2021 + *
2022 + * @param string $token The lock token this worker was given when it started the chain.
2023 + * @param string $action The transfer action currently being processed.
2024 + *
2025 + * @return bool True if we still own the lock and renewed it, false if ownership was lost.
2026 + */
2027 + public static function renew_transfer_lock( $token, $action ) {
2028 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
1958 2029
1959 - self::$instance->settings->update( $option, 'disabled' );
2030 + if ( ! is_array( $lock ) || ! isset( $lock['token'] ) || $lock['token'] !== $token ) {
2031 + return false;
2032 + }
2033 +
2034 + set_transient(
2035 + self::TRANSFER_LOCK_TRANSIENT,
2036 + [
2037 + 'token' => $token,
2038 + 'action' => $action,
2039 + ],
2040 + self::TRANSFER_LOCK_TTL
2041 + );
2042 +
2043 + return true;
2044 + }
2045 +
2046 + /**
2047 + * Release the transfer lock if we still own it, allowing another worker to acquire it.
2048 + *
2049 + * @param string $token The lock token to release.
2050 + *
2051 + * @return void
2052 + */
2053 + public static function release_transfer_lock( $token ) {
2054 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
2055 +
2056 + if ( ! is_array( $lock ) || ! isset( $lock['token'] ) || $lock['token'] !== $token ) {
1960 2057 return;
1961 2058 }
1962 2059
1963 - try {
1964 - // This will be 0 in the case of offloading now.
1965 - if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
1966 - $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
2060 + delete_transient( self::TRANSFER_LOCK_TRANSIENT );
2061 + }
1967 2062
2063 + /**
2064 + * Start Processing Images by IDs
2065 + *
2066 + * @param string $action The action for which to get the number of images.
2067 + * @param int $id The images to process.
2068 + *
2069 + * @throws Exception If there is an error.
2070 + * @return void
2071 + */
2072 + public function move_single_image( $action, $id ) {
2073 + set_time_limit( 0 );
2074 +
2075 + // Only use the legacy offloaded attachments to query the pages that need to be updated.
2076 + // We can be confident that these IDs are already marked as offloaded.
2077 + $legacy_offloaded = ! $this->is_new_offloaded_attachment( $id );
2078 + $page_in = [];
2079 + if ( $legacy_offloaded ) {
2080 + $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, [ $id ] );
2081 + }
2082 + // This will be 0 in the case of offloading now.
2083 + if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
2084 + $page = 0;
2085 + do {
2086 + $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, 100, $page_in );
1968 2087 if ( isset( $to_update['page'] ) ) {
1969 2088 if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
1970 2089 foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
1971 - if ( ! empty( $image_ids ) ) {
1972 - $images = array_intersect( $images, $image_ids );
1973 - }
1974 -
2090 + $images = array_intersect( $images, [ $id ] );
1975 2091 if ( empty( $images ) ) {
1976 2092 continue;
1977 2093 }
1978 -
1979 2094 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1980 2095 Optml_Media_Offload::instance()->update_page( $post_id );
1981 2096 }
1982 2097 }
1983 2098 }
1984 -
1985 2099 $page = $page + 1;
1986 - } else {
1987 - // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1988 - $images = array_slice( $image_ids, 0, $batch );
1989 - $image_ids = array_slice( $image_ids, $batch );
1990 - $action === 'rollback_images' ?
1991 - Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
1992 - Optml_Media_Offload::instance()->upload_images( $batch, $images );
1993 - }
2100 + } while ( ! empty( $to_update['imagesToUpdate'] ) );
1994 2101
1995 - self::schedule_action(
1996 - time(),
1997 - 'optml_start_processing_images_by_id',
1998 - [
1999 - $action,
2000 - $batch,
2001 - $page,
2002 - $image_ids,
2003 - ]
2004 - );
2005 - } catch ( Exception $e ) {
2006 - // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
2007 - $delay_in_seconds = 10;
2008 - self::$instance->logger->add_log( $type, $e->getMessage() );
2009 -
2010 - self::schedule_action(
2011 - time() + $delay_in_seconds,
2012 - 'optml_start_processing_images_by_id',
2013 - [
2014 - $action,
2015 - $batch,
2016 - $page,
2017 - $image_ids,
2018 - ]
2019 - );
2102 + } else {
2103 + $action === 'rollback_images' ?
2104 + Optml_Media_Offload::instance()->rollback_images( 1, [ $id ] ) :
2105 + Optml_Media_Offload::instance()->upload_images( 1, [ $id ] );
2020 2106 }
2107 + if ( empty( $page_in ) && $legacy_offloaded === false ) {
2108 + $meta = self::get_process_meta();
2109 + self::$instance->logger->add_log( $action, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
2110 + return;
2111 + }
2021 2112 }
2022 2113
2023 2114 /**
2024 2115 * Start Processing Images
@@ -2023,23 +2114,30 @@
2023 2114 /**
2024 2115 * Start Processing Images
2025 2116 *
2026 2117 * @param string $action The action for which to get the number of images.
2027 - * @param int $batch The batch of images to process.
2028 - * @param int $page The page of images to process.
2029 - * @param int $total The total number of pages.
2030 - * @param int $step The current step.
2118 + * @param int $batch The batch of images to process.
2119 + * @param int $page The page of images to process.
2120 + * @param int $total The total number of pages.
2121 + * @param int $step The current step.
2122 + * @param string $lock_token The transfer lock token owned by this processing chain.
2031 2123 *
2032 2124 * @return void
2033 2125 */
2034 - public function start_processing_images( $action, $batch, $page, $total, $step ) {
2126 + public function start_processing_images( $action, $batch, $page, $total, $step, $lock_token = '' ) {
2035 2127 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
2036 2128 $type = 'offload_images' === $action ? 'offload' : 'rollback';
2037 2129
2038 2130 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
2131 + self::release_transfer_lock( $lock_token );
2039 2132 return;
2040 2133 }
2041 2134
2135 + // If we don't own the lock anymore, stop processing.
2136 + if ( ! self::renew_transfer_lock( $lock_token, $action ) ) {
2137 + return;
2138 + }
2139 +
2042 2140 if ( $step > $total || 0 === $total ) {
2043 2141 $meta = self::get_process_meta();
2044 2142 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
2045 2143
@@ -2046,8 +2144,11 @@
2046 2144 self::$instance->settings->update( $option, 'disabled' );
2047 2145
2048 2146 self::$instance->settings->update( 'show_offload_finish_notice', $type );
2049 2147
2148 + // Transfer completed successfully: release the lock.
2149 + self::release_transfer_lock( $lock_token );
2150 +
2050 2151 return;
2051 2152 }
2052 2153
2053 2154 set_time_limit( 0 );
@@ -2078,12 +2179,14 @@
2078 2179 $batch,
2079 2180 $page,
2080 2181 $total,
2081 2182 $step,
2183 + $lock_token,
2082 2184 ]
2083 2185 );
2084 2186 } catch ( Exception $e ) {
2085 2187 // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
2188 + // This is a retryable error, so the lock is kept rather than released.
2086 2189 $delay_in_seconds = 10;
2087 2190 self::$instance->logger->add_log( $type, $e->getMessage() );
2088 2191
2089 2192 self::schedule_action(
@@ -2094,8 +2197,9 @@
2094 2197 $batch,
2095 2198 $page,
2096 2199 $total,
2097 2200 $step,
2201 + $lock_token,
2098 2202 ]
2099 2203 );
2100 2204 }
2101 2205 }
@@ -2103,9 +2207,9 @@
2103 2207 /**
2104 2208 * Alter attachment image src for offloaded images.
2105 2209 *
2106 2210 * @param array|false $image {
2107 - * Array of image data.
2211 + * Array of image data.
2108 2212 *
2109 2213 * @type string $0 Image source URL.
2110 2214 * @type int $1 Image width in pixels.
2111 2215 * @type int $2 Image height in pixels.
@@ -2121,12 +2225,14 @@
2121 2225 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2122 2226 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2123 2227 return $image;
2124 2228 }
2125 -
2126 - $url = get_post( $attachment_id );
2127 - $url = $url->guid;
2128 - $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id );
2229 + if ( isset( $image[0] ) ) {
2230 + $url = $image[0];
2231 + } else {
2232 + $url = get_post( $attachment_id );
2233 + $url = $url->guid;
2234 + }
2129 2235 $metadata = wp_get_attachment_metadata( $attachment_id );
2130 2236
2131 2237 // Use the original size if the requested size is full.
2132 2238 if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
@@ -2133,12 +2239,13 @@
2133 2239 $image_url = $this->get_new_offloaded_attachment_url(
2134 2240 $url,
2135 2241 $attachment_id,
2136 2242 [
2137 - 'width' => $metadata['width'],
2138 - 'height' => $metadata['height'],
2243 + 'width' => $metadata['width'],
2244 + 'height' => $metadata['height'],
2139 2245 'attachment_id' => $attachment_id,
2140 - ]
2246 + ],
2247 + $metadata
2141 2248 );
2142 2249
2143 2250 return [
2144 2251 $image_url,
@@ -2147,58 +2254,29 @@
2147 2254 false,
2148 2255 ];
2149 2256 }
2150 2257
2151 - $crop = false;
2152 -
2153 - // Size can be int [] containing width and height.
2154 - if ( is_array( $size ) ) {
2155 - $width = $size[0];
2156 - $height = $size[1];
2157 - $crop = true;
2158 - } else {
2159 - $sizes = $this->get_all_image_sizes();
2160 -
2161 - if ( ! isset( $sizes[ $size ] ) ) {
2162 - return [
2163 - $image_url,
2164 - $metadata['width'],
2165 - $metadata['height'],
2166 - false,
2167 - ];
2168 - }
2169 -
2170 - $width = $sizes[ $size ]['width'];
2171 - $height = $sizes[ $size ]['height'];
2172 - $crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
2173 - }
2174 -
2175 - $sizes2crop = self::size_to_crop();
2176 -
2177 - if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
2258 + if ( doing_action( 'wp_insert_post_data' ) && wp_attachment_is( 'video', $attachment_id ) ) {
2178 2259 return $image;
2179 2260 }
2261 + $sizes = $this->size_to_dimension( $size, $metadata );
2262 + $image_url = $this->get_new_offloaded_attachment_url(
2263 + $url,
2264 + $attachment_id,
2265 + [
2266 + 'width' => $sizes['width'],
2267 + 'height' => $sizes['height'],
2268 + 'resize' => $sizes['resize'] ?? [],
2269 + 'attachment_id' => $attachment_id,
2270 + ],
2271 + $metadata
2272 + );
2180 2273
2181 - $resize = apply_filters( 'optml_default_crop', [] );
2182 - $data = image_get_intermediate_size( $attachment_id, $size );
2183 -
2184 - if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
2185 - if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
2186 - $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
2187 - }
2188 - }
2189 -
2190 - if ( $crop !== false ) {
2191 - $resize = $this->to_optml_crop( $crop );
2192 - }
2193 -
2194 - $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, ['width' => $width, 'height' => $height, 'resize' => $resize, 'attachment_id' => $attachment_id] );
2195 -
2196 2274 return [
2197 2275 $image_url,
2198 - $width,
2199 - $height,
2200 - $crop,
2276 + $sizes['width'],
2277 + $sizes['height'],
2278 + $size === 'full', // @phpstan-ignore-line
2201 2279 ];
2202 2280 }
2203 2281
2204 2282 /**
@@ -2214,21 +2292,22 @@
2214 2292 if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2215 2293 return $response;
2216 2294 }
2217 2295
2218 - $sizes = $this->get_all_image_sizes();
2296 + $meta = [];
2297 + if ( isset( $response['width'] ) ) {
2298 + $meta['width'] = $response['width'];
2299 + }
2300 + if ( isset( $response['height'] ) ) {
2301 + $meta['height'] = $response['height'];
2302 + }
2303 + $sizes = Optml_App_Replacer::image_sizes();
2219 2304
2220 2305 foreach ( $sizes as $size => $args ) {
2221 2306 if ( isset( $response['sizes'][ $size ] ) ) {
2222 2307 continue;
2223 2308 }
2224 -
2225 - $args = [
2226 - 'height' => $args['height'],
2227 - 'width' => $args['width'],
2228 - 'crop' => true,
2229 - ];
2230 -
2309 + $args = $this->size_to_dimension( $size, $meta );
2231 2310 $response['sizes'][ $size ] = array_merge(
2232 2311 $args,
2233 2312 [
2234 2313 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
@@ -2235,17 +2314,10 @@
2235 2314 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2236 2315 ]
2237 2316 );
2238 2317 }
2318 + $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $meta );
2239 2319
2240 - $url_args = [
2241 - 'height' => $response['height'],
2242 - 'width' => $response['width'],
2243 - 'crop' => false,
2244 - ];
2245 -
2246 - $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
2247 -
2248 2320 return $response;
2249 2321 }
2250 2322
2251 2323 /**
@@ -2266,18 +2338,18 @@
2266 2338
2267 2339 /**
2268 2340 * Get offloaded image attachment URL for new offloads.
2269 2341 *
2270 - * @param string $url The initial attachment URL.
2271 - * @param int $attachment_id The attachment ID.
2272 - * @param array $args The additional arguments.
2273 - * - width: The width of the image.
2274 - * - height: The height of the image.
2275 - * - crop: Whether to crop the image.
2276 - *
2342 + * @param string $url The initial attachment URL.
2343 + * @param int $attachment_id The attachment ID.
2344 + * @param array $args The additional arguments.
2345 + * - width: The width of the image.
2346 + * - height: The height of the image.
2347 + * - crop: Whether to crop the image.
2348 + * @param array|null $attachment_metadata The attachment metadata.
2277 2349 * @return string
2278 2350 */
2279 - private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [] ) {
2351 + private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [], $attachment_metadata = null ) {
2280 2352 $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2281 2353
2282 2354 // Image might have already passed through this filter.
2283 2355 if ( strpos( $url, $process_flag ) !== false ) {
@@ -2282,10 +2354,13 @@
2282 2354 // Image might have already passed through this filter.
2283 2355 if ( strpos( $url, $process_flag ) !== false ) {
2284 2356 return $url;
2285 2357 }
2286 -
2287 - $meta = wp_get_attachment_metadata( $attachment_id );
2358 + if ( $attachment_metadata === null ) {
2359 + $meta = wp_get_attachment_metadata( $attachment_id );
2360 + } else {
2361 + $meta = $attachment_metadata;
2362 + }
2288 2363 if ( ! isset( $meta['file'] ) ) {
2289 2364 return $url;
2290 2365 }
2291 2366
@@ -2304,27 +2379,11 @@
2304 2379 }
2305 2380
2306 2381 $file = $meta['file'];
2307 2382 if ( self::is_uploaded_image( $file ) ) {
2308 - $optimized_url = ( new Optml_Image(
2309 - $url,
2310 - [
2311 - 'width' => $args['width'],
2312 - 'height' => $args['height'],
2313 - 'quality' => $this->settings->get_numeric_quality(),
2314 - 'resize' => $args['resize'],
2315 - 'attachment_id' => $attachment_id,
2316 - ],
2317 - $this->settings->get( 'cache_buster' )
2318 - ) )->get_url();
2383 + $optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2319 2384
2320 - if ( strpos( $optimized_url, $process_flag ) !== false ) {
2321 - return $optimized_url;
2322 - }
2323 -
2324 - $process_flag = $process_flag . $file;
2325 -
2326 - return str_replace( '/' . $url, '/' . $process_flag, $optimized_url );
2385 + return strpos( $optimized_url, $process_flag ) === false ? str_replace( '/' . ltrim( $file, '/' ), '/' . $process_flag . $file, $optimized_url ) : $optimized_url;
2327 2386 } else {
2328 2387 // this is for the users that already offloaded the images before the other fixes
2329 2388 $local_file = get_attached_file( $attachment_id );
2330 2389 if ( ! file_exists( $local_file ) ) {
@@ -2333,19 +2392,9 @@
2333 2392 foreach ( $duplicated_images as $id ) {
2334 2393 if ( ! empty( $id ) ) {
2335 2394 $duplicated_meta = wp_get_attachment_metadata( $id );
2336 2395 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2337 - $optimized_url = ( new Optml_Image(
2338 - $url,
2339 - [
2340 - 'width' => $args['width'],
2341 - 'height' => $args['height'],
2342 - 'quality' => $this->settings->get_numeric_quality(),
2343 - 'attachment_id' => $attachment_id,
2344 - ],
2345 - $this->settings->get( 'cache_buster' )
2346 - ) )->get_url();
2347 - return $optimized_url;
2396 + return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2348 2397 }
2349 2398 }
2350 2399 }
2351 2400 }
@@ -2373,8 +2422,11 @@
2373 2422 $attachment = $this->get_local_attachement_id_from_url( $url );
2374 2423
2375 2424 // No local attachment.
2376 2425 if ( $attachment['attachment_id'] === 0 ) {
2426 + if ( $this->can_replace_url( $url ) ) {
2427 + $to_replace[ $url ] = $this->get_optimized_image_url( $url, 'auto', 'auto' );
2428 + }
2377 2429 continue;
2378 2430 }
2379 2431
2380 2432 $attachment_id = $attachment['attachment_id'];
@@ -2399,19 +2451,9 @@
2399 2451 }
2400 2452 }
2401 2453
2402 2454 // Build the optimized URL.
2403 - $optimized_url = ( new Optml_Image(
2404 - $url,
2405 - [
2406 - 'width' => $width,
2407 - 'height' => $height,
2408 - 'quality' => $this->settings->get_numeric_quality(),
2409 - 'resize' => $resize,
2410 - 'attachment_id' => $attachment_id,
2411 - ],
2412 - $this->settings->get( 'cache_buster' )
2413 - ) )->get_url();
2455 + $optimized_url = $this->get_optimized_image_url( self::KEYS['not_processed_flag'] . $attachment_id . '/' . ltrim( $this->get_offloaded_attachment_url( $attachment_id, $url ), '/' ), $width, $height, $resize );
2414 2456
2415 2457 // Drop any image size from the URL.
2416 2458 $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2417 2459
@@ -2492,12 +2534,21 @@
2492 2534 if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2493 2535 continue;
2494 2536 }
2495 2537
2496 - $replace[ $url ] = self::get_original_url( $id );
2538 + $original = self::get_original_url( $id );
2497 2539
2540 + if ( $original === false ) {
2541 + continue;
2542 + }
2543 +
2544 + $replace[ $url ] = $original;
2545 +
2498 2546 $size = $this->parse_dimension_from_optimized_url( $url );
2499 2547
2548 + if ( $size[0] === false || $size[1] === false ) {
2549 + continue;
2550 + }
2500 2551 if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2501 2552 continue;
2502 2553 }
2503 2554
@@ -2521,9 +2572,9 @@
2521 2572 $width = $size[0];
2522 2573 $height = $size[1];
2523 2574 } else {
2524 2575 // In case of an image size, we need to calculate the new dimensions for the proper file path.
2525 - $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] );
2576 + $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], (int) $size[0], (int) $size[1] );
2526 2577
2527 2578 $width = $constrained[0];
2528 2579 $height = $constrained[1];
2529 2580 }
@@ -2542,12 +2593,12 @@
2542 2593 /**
2543 2594 * Alter the image size for the image widget.
2544 2595 *
2545 2596 * @param string $html the attachment image HTML string.
2546 - * @param array $settings Control settings.
2597 + * @param array $settings Control settings.
2547 2598 * @param string $image_size_key Optional. Settings key for image size.
2548 2599 * Default is `image`.
2549 - * @param string $image_key Optional. Settings key for image. Default
2600 + * @param string $image_key Optional. Settings key for image. Default
2550 2601 * is null. If not defined uses image size key
2551 2602 * as the image key.
2552 2603 *
2553 2604 * @return string
@@ -2605,9 +2656,9 @@
2605 2656 add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2606 2657 add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2607 2658
2608 2659 // Needed for rendering beaver builder css properly.
2609 - add_filter( 'fl_builder_render_css', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2660 + add_filter( 'fl_builder_render_css', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2610 2661
2611 2662 // Filter saved data on insert to use local attachments.
2612 2663 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
2613 2664 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
@@ -2617,26 +2668,28 @@
2617 2668 add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2618 2669 }
2619 2670
2620 2671 // Filter loaded data in the editors to use local attachments.
2621 - add_filter( 'content_edit_pre', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2622 - $types = get_post_types_by_support( 'editor' );
2623 - foreach ( $types as $type ) {
2624 - $post_type = get_post_type_object( $type );
2625 - if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2626 - add_filter(
2627 - 'rest_prepare_' . $type,
2628 - [
2629 - self::$instance,
2630 - 'pre_filter_rest_content',
2631 - ],
2632 - 10,
2633 - 3
2634 - );
2635 - }
2636 - }
2672 + add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2637 2673
2638 - add_filter( 'get_attached_file', [$this, 'alter_attached_file_response'], 10, 2 );
2674 + add_action(
2675 + 'init',
2676 + function () {
2677 + $types = get_post_types_by_support( 'editor' );
2678 +
2679 + foreach ( $types as $type ) {
2680 +
2681 + $post_type = get_post_type_object( $type );
2682 +
2683 + if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2684 + add_filter( 'rest_prepare_' . $type, [ self::$instance, 'pre_filter_rest_content' ], 10, 3 );
2685 + }
2686 + }
2687 + },
2688 + PHP_INT_MAX
2689 + );
2690 +
2691 + add_filter( 'get_attached_file', [ $this, 'alter_attached_file_response' ], 10, 2 );
2639 2692 add_filter(
2640 2693 'elementor/image_size/get_attachment_image_html',
2641 2694 [
2642 2695 $this,
@@ -2644,9 +2697,8 @@
2644 2697 ],
2645 2698 10,
2646 2699 4
2647 2700 );
2648 -
2649 2701 }
2650 2702
2651 2703 /**
2652 2704 * Elementor checks if the file exists before requesting a specific image size.
@@ -2695,6 +2747,86 @@
2695 2747 * @return bool
2696 2748 */
2697 2749 public static function is_phpunit_test() {
2698 2750 return defined( 'OPTML_PHPUNIT_TESTING' ) && OPTML_PHPUNIT_TESTING === true;
2751 + }
2752 +
2753 + /**
2754 + * Get offloaded image attachment URL based on the given attachment ID and URL.
2755 + *
2756 + * @param mixed $attachment_id The attachment ID.
2757 + * @param string $url The attachment URL.
2758 + *
2759 + * @return string
2760 + */
2761 + private function get_offloaded_attachment_url( $attachment_id, $url ) {
2762 + if ( ! $this->settings->is_offload_enabled() || ! is_numeric( $attachment_id ) ) {
2763 + return $url;
2764 + } elseif ( empty( $attachment_id ) && strpos( $url, self::KEYS['not_processed_flag'] ) !== false ) {
2765 + $attachment_id = (int) self::get_attachment_id_from_url( $url );
2766 + } elseif ( empty( $attachment_id ) ) {
2767 + $attachment_id = $this->attachment_url_to_post_id( $url );
2768 + }
2769 +
2770 + if ( $attachment_id > 0 || ! empty( get_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true ) ) ) {
2771 + $url = wp_get_attachment_metadata( $attachment_id )['file'];
2772 + }
2773 +
2774 + return $url;
2775 + }
2776 +
2777 + /**
2778 + * Cleanup the offload errors meta.
2779 + *
2780 + * @param string $meta_key The meta key to delete. Defaults to the offload error key.
2781 + *
2782 + * @return int|bool Number of rows affected/selected or false on error.
2783 + */
2784 + public static function clear_offload_errors_meta( $meta_key = '' ) {
2785 + global $wpdb;
2786 +
2787 + if ( empty( $meta_key ) ) {
2788 + $meta_key = self::META_KEYS['offload_error'];
2789 + }
2790 +
2791 + // Collect the affected attachments before the bulk delete so their object
2792 + // caches can be invalidated. A raw DELETE bypasses the meta/query caches,
2793 + // which would otherwise leave stale WP_Query results for subsequent queries.
2794 + $post_ids = $wpdb->get_col(
2795 + $wpdb->prepare(
2796 + "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s",
2797 + $meta_key
2798 + )
2799 + );
2800 +
2801 + $result = $wpdb->query(
2802 + $wpdb->prepare(
2803 + "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
2804 + $meta_key
2805 + )
2806 + );
2807 +
2808 + foreach ( $post_ids as $post_id ) {
2809 + wp_cache_delete( (int) $post_id, 'post_meta' );
2810 + }
2811 +
2812 + // Bump the posts last_changed so cached WP_Query results (which are keyed
2813 + // on it) are recomputed on the next query. The raw DELETE above does not
2814 + // touch the object cache, so without this the retried rollback/offload
2815 + // query could return a stale set that still excludes the cleared posts.
2816 + wp_cache_set( 'last_changed', microtime(), 'posts' );
2817 +
2818 + return $result;
2819 + }
2820 +
2821 + /**
2822 + * Cleanup the rollback errors meta.
2823 + *
2824 + * Used when the user retries the rollback process so previously errored
2825 + * attachments are considered again for restore.
2826 + *
2827 + * @return int|bool Number of rows affected/selected or false on error.
2828 + */
2829 + public static function clear_rollback_errors_meta() {
2830 + return self::clear_offload_errors_meta( self::META_KEYS['rollback_error'] );
2699 2831 }
2700 2832 }