PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.15
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.15
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 +764 -543 3.11.3 → 4.2.15 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.
@@ -127,68 +151,101 @@
127 151 /**
128 152 * Optml_Media_Offload constructor.
129 153 */
130 154 public static function instance() {
131 - if ( null === self::$instance ||
132 - ( self::$instance->settings !== null && ( ! self::$instance->settings->is_connected()
133 - || self::$instance->settings->get( 'offload_media' ) === 'disabled'
134 - || self::$instance->settings->get( 'cloud_images' ) === 'disabled' ) ) ) {
135 - self::$instance = new self();
155 + if ( null === self::$instance || self::is_phpunit_test() ) {
156 + self::$instance = new self();
136 157 self::$instance->settings = new Optml_Settings();
137 - self::$instance->logger = Optml_Logger::instance();
158 + self::$instance->logger = Optml_Logger::instance();
138 159
139 160 if ( self::$instance->settings->is_connected() ) {
140 161 self::$instance->init();
141 162 }
142 163 if ( self::$instance->settings->is_offload_enabled() ) {
143 - add_filter( 'image_downsize', [self::$instance, 'generate_filter_downsize_urls'], 10, 3 );
144 - add_filter( 'wp_generate_attachment_metadata', [self::$instance, 'generate_image_meta'], 10, 2 );
145 - add_filter( 'wp_get_attachment_url', [self::$instance, 'get_image_attachment_url'], -999, 2 );
146 - add_filter( 'wp_insert_post_data', [self::$instance, 'filter_uploaded_images'] );
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' ] );
147 168
148 169 self::$instance->add_new_actions();
149 170
150 - add_action( 'delete_attachment', [self::$instance, 'delete_attachment_hook'], 10 );
151 - add_filter( 'handle_bulk_actions-upload', [self::$instance, 'bulk_action_handler'], 10, 3 );
152 - add_filter( 'bulk_actions-upload', [self::$instance, 'register_bulk_media_actions'] );
153 - add_filter( 'media_row_actions', [self::$instance, 'add_inline_media_action'], 10, 2 );
154 - add_filter( 'wp_calculate_image_srcset', [self::$instance, 'calculate_image_srcset'], 1, 5 );
155 - add_action( 'post_updated', [self::$instance, 'update_offload_meta'], 10, 3 );
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 );
156 178
157 179 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
158 180 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
159 181 if ( $below_6_0_0 ) {
160 - 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 );
161 183 } else {
162 - add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert'], 10, 4 );
184 + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 );
163 185 }
164 186
165 - add_action( 'optml_start_processing_images', [self::$instance, 'start_processing_images'], 10, 5 );
166 - add_action( 'optml_start_processing_images_by_id', [self::$instance, 'start_processing_images_by_id'], 10, 4 );
167 -
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' ] );
168 198 if ( self::$is_legacy_install === null ) {
169 199 self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
170 200 }
201 + ( new Loader() )->register_hooks();
171 202 }
172 203 }
204 +
173 205 return self::$instance;
174 206 }
175 207
176 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 + /**
177 234 * Function for `update_attached_file` filter-hook.
178 235 *
179 - * @param string $file Path to the attached file to update.
236 + * @param string $file Path to the attached file to update.
180 237 * @param int $attachment_id Attachment ID.
181 238 *
182 239 * @return string
183 240 */
184 - function wp_update_attached_file_filter( $file, $attachment_id ) {
241 + public function wp_update_attached_file_filter( $file, $attachment_id ) {
185 242
186 243 if ( OPTML_DEBUG_MEDIA ) {
187 244 do_action( 'optml_log', 'called updated attached' );
188 245 }
189 - $info = pathinfo( $file );
190 - $file_name = basename( $file );
246 + $info = pathinfo( $file );
247 + $file_name = basename( $file );
191 248 $no_ext_file_name = basename( $file, '.' . $info['extension'] );
192 249 // if we have current deduplication set and it contains the filename that is updated
193 250 // we replace the updated filename with the deduplicated filename
194 251 if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) {
@@ -200,9 +257,10 @@
200 257 }
201 258 if ( OPTML_DEBUG_MEDIA ) {
202 259 do_action( 'optml_log', self::$last_deduplicated );
203 260 }
204 - 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 +
205 263 return $file;
206 264 }
207 265
208 266 /**
@@ -213,18 +271,17 @@
213 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
214 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
215 273 * with the deduplicated one which we stored into `$current_file_deduplication` variable
216 274 *
217 - * @param array $data An array of slashed, sanitized, and processed attachment post data.
218 - * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
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.
219 277 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
220 - * @param bool $update Whether this is an existing attachment post being updated.
278 + * @param bool $update Whether this is an existing attachment post being updated.
221 279 *
280 + * @return array
222 281 * @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0.
223 - *
224 - * @return array
225 282 */
226 - function insert( $data, $postarr, $unsanitized_postarr, $update ) {
283 + public function insert( $data, $postarr, $unsanitized_postarr, $update ) {
227 284
228 285 // the post name is unique against the database so not affected by removing the files
229 286 // https://developer.wordpress.org/reference/functions/wp_unique_post_slug/
230 287 if ( OPTML_DEBUG_MEDIA ) {
@@ -235,11 +292,11 @@
235 292 return $data;
236 293 }
237 294
238 295 $filename = wp_basename( $data['guid'] );
239 - $ext = $this->get_ext( $filename );
296 + $ext = $this->get_ext( $filename );
240 297 // skip if the file is not an image
241 - 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 ) ) {
242 299 return $data;
243 300 }
244 301
245 302 // on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg`
@@ -263,9 +320,9 @@
263 320
264 321 $data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] );
265 322 // we store the deduplication to be used and add the filter for updating the attached_file meta
266 323 self::$current_file_deduplication = $to_replace_with;
267 - 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 );
268 325 }
269 326 if ( OPTML_DEBUG_MEDIA ) {
270 327 do_action( 'optml_log', 'data after' );
271 328 do_action( 'optml_log', $data );
@@ -276,15 +333,15 @@
276 333
277 334 /**
278 335 * Wrapper for the `insert` method for WP versions < 6.0.0.
279 336 *
280 - * @param array $data An array of slashed, sanitized, and processed attachment post data.
281 - * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
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.
282 339 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
283 340 *
284 341 * @return array
285 342 */
286 - function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
343 + public function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
287 344 return $this->insert( $data, $postarr, $unsanitized_postarr, false );
288 345 }
289 346
290 347 /**
@@ -292,11 +349,11 @@
292 349 *
293 350 * @param int $post_ID Updated post id.
294 351 * @param WP_Post $post_after Post before the update.
295 352 * @param WP_Post $post_before Post after the update.
296 - * @uses action:post_updated
297 353 *
298 354 * @return void
355 + * @uses action:post_updated
299 356 */
300 357 public function update_offload_meta( $post_ID, $post_after, $post_before ) {
301 358 if ( self::$offload_update_post === true ) {
302 359 return;
@@ -330,17 +387,18 @@
330 387
331 388 /**
332 389 * Replace image URLs in the srcset attributes.
333 390 *
334 - * @param array $sources Array of image sources.
335 - * @param array $size_array Array of width and height values in pixels (in that order).
336 - * @param string $image_src The 'src' of the image.
337 - * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
338 - * @param int $attachment_id Image attachment ID.
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.
339 396 *
340 - * @return array
397 + * @return array<int, array{url: string, descriptor: string, value: int}>|mixed
341 398 */
342 399 public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
400 +
343 401 if ( ! is_array( $sources ) ) {
344 402 return $sources;
345 403 }
346 404
@@ -358,8 +416,9 @@
358 416 }
359 417
360 418 $sources[ $width ]['url'] = $optimized_url[0];
361 419 }
420 +
362 421 return $sources;
363 422 }
364 423
365 424 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
@@ -372,12 +431,10 @@
372 431 if ( $requested_height < 1 || $requested_width < 1 ) {
373 432 return $sources;
374 433 }
375 434
376 - $requested_ratio = $requested_width / $requested_height;
377 -
378 435 $image_sizes = $this->get_all_image_sizes();
379 - $crop = false;
436 + $crop = false;
380 437
381 438 // Loop through image sizes to make sure we're using the right cropping.
382 439 foreach ( $image_sizes as $size_name => $args ) {
383 440 if ( $args['width'] !== $requested_width && $args['height'] !== $requested_height ) {
@@ -387,9 +444,8 @@
387 444 if ( isset( $args['crop'] ) ) {
388 445 $crop = (bool) $args['crop'];
389 446 }
390 447 }
391 -
392 448 foreach ( $sources as $width => $source ) {
393 449 $filename = ( $image_meta['file'] );
394 450 $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename, false );
395 451
@@ -411,15 +467,9 @@
411 467
412 468 continue;
413 469 }
414 470
415 - $size_ratio = $size['width'] / $size['height'];
416 -
417 - // We need a srcset with the same aspect ratio.
418 - // Otherwise, we'll display different images on different devices.
419 - if ( $requested_ratio !== $size_ratio ) {
420 - unset( $sources[ $width ] );
421 -
471 + if ( ! wp_image_matches_ratio( $size['width'], $size['height'], $requested_width, $requested_height ) ) {
422 472 continue;
423 473 }
424 474
425 475 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size['name'] );
@@ -428,19 +478,25 @@
428 478 unset( $sources[ $width ] );
429 479
430 480 continue;
431 481 }
432 -
433 482 $sources[ $width ]['url'] = $optimized_url[0];
434 483 }
435 -
436 484 // Add the requested size to the srcset.
437 485 $sources[ $requested_width ] = [
438 - 'url' => $image_src,
486 + 'url' => $image_src,
439 487 'descriptor' => 'w',
440 - 'value' => $requested_width,
488 + 'value' => $requested_width,
441 489 ];
442 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 + }
443 499 return $sources;
444 500 }
445 501
446 502 /**
@@ -446,8 +502,9 @@
446 502 /**
447 503 * Check if the image is stored on our servers or not.
448 504 *
449 505 * @param string $src Image src or url.
506 + *
450 507 * @return bool Whether image is upload or not.
451 508 */
452 509 public static function is_not_processed_image( $src ) {
453 510 return strpos( $src, self::KEYS['not_processed_flag'] ) !== false;
@@ -456,8 +513,9 @@
456 513 /**
457 514 * Check if the image is stored on our servers or not.
458 515 *
459 516 * @param string $src Image src or url.
517 + *
460 518 * @return bool Whether image is upload or not.
461 519 */
462 520 public static function is_uploaded_image( $src ) {
463 521 return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false;
@@ -481,13 +539,15 @@
481 539
482 540 /**
483 541 * Get attachment id from url
484 542 *
485 - * @param string $url The optimized url .
543 + * @param string $url The optimized url .
544 + *
486 545 * @return false|mixed The attachment id .
487 546 */
488 547 public static function get_attachment_id_from_url( $url ) {
489 548 preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
549 +
490 550 return isset( $attachment_id[1] ) ? $attachment_id[1] : false;
491 551 }
492 552
493 553 /**
@@ -493,28 +553,22 @@
493 553 /**
494 554 * Get attachment id from local url
495 555 *
496 556 * @param string $url The url to look for.
557 + *
497 558 * @return array The attachment id and the size from the url.
498 559 */
499 560 public function get_local_attachement_id_from_url( $url ) {
500 561
501 - $size = 'full';
562 + $size = 'full';
502 563 $found_size = $this->parse_dimensions_from_filename( $url );
503 - $strip_url = $url;
504 - $scaled_url = $url;
564 + $url = $this->add_schema( $url );
505 565 if ( $found_size[0] !== false && $found_size[1] !== false ) {
506 566 $size = $found_size;
507 - $strip_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '', $url );
508 - $scaled_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '-scaled', $url );
509 - }
510 - $strip_url = $this->add_schema( $strip_url );
511 567
512 - $attachment_id = attachment_url_to_postid( $strip_url );
513 - if ( $attachment_id === 0 ) {
514 - $scaled_url = $this->add_schema( $scaled_url );
515 - $attachment_id = attachment_url_to_postid( $scaled_url );
516 568 }
569 + $url = $this->add_schema( $url );
570 + $attachment_id = $this->attachment_url_to_post_id( $url );
517 571
518 572 return [ 'attachment_id' => $attachment_id, 'size' => $size ];
519 573 }
520 574
@@ -532,9 +586,9 @@
532 586 if ( OPTML_DEBUG_MEDIA ) {
533 587 do_action( 'optml_log', 'content to update' );
534 588 do_action( 'optml_log', $content );
535 589 }
536 - $images = Optml_Manager::instance()->extract_urls_from_content( $content );
590 + $images = Optml_Manager::instance()->extract_urls_from_content( $content );
537 591 if ( ! isset( $images[0] ) ) {
538 592 return $data;
539 593 }
540 594 if ( OPTML_DEBUG_MEDIA ) {
@@ -542,10 +596,10 @@
542 596 do_action( 'optml_log', $images );
543 597 }
544 598 foreach ( $images as $url ) {
545 599 $is_original_uploaded = self::is_uploaded_image( $url );
546 - $attachment_id = false;
547 - $size = 'thumbnail';
600 + $attachment_id = false;
601 + $size = 'thumbnail';
548 602 if ( $is_original_uploaded ) {
549 603 $found_size = $this->parse_dimension_from_optimized_url( $url );
550 604 if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) {
551 605 $size = $found_size;
@@ -551,11 +605,11 @@
551 605 $size = $found_size;
552 606 }
553 607 $attachment_id = self::get_attachment_id_from_url( $url );
554 608 } else {
555 - $id_and_size = $this->get_local_attachement_id_from_url( $url );
609 + $id_and_size = $this->get_local_attachement_id_from_url( $url );
556 610 $attachment_id = $id_and_size['attachment_id'];
557 - $size = $id_and_size['size'];
611 + $size = $id_and_size['size'];
558 612 }
559 613
560 614 if ( OPTML_DEBUG_MEDIA ) {
561 615 do_action( 'optml_log', 'image id and found size' );
@@ -579,8 +633,9 @@
579 633 }
580 634 $content = str_replace( $url, $optimized_url[0], $content );
581 635 }
582 636 $data['post_content'] = wp_slash( $content );
637 +
583 638 return $data;
584 639 }
585 640
586 641 /**
@@ -587,18 +642,19 @@
587 642 * Get all images that need to be updated from a post.
588 643 *
589 644 * @param string $post_content The content of the post.
590 645 * @param string $job The job name.
646 + *
591 647 * @return array An array containing the image ids.
592 648 */
593 649 public function get_image_id_from_content( $post_content, $job ) {
594 - $content = trim( wp_unslash( $post_content ) );
595 - $images = Optml_Manager::instance()->extract_urls_from_content( $content );
650 + $content = trim( wp_unslash( $post_content ) );
651 + $images = Optml_Manager::instance()->extract_urls_from_content( $content );
596 652 $found_images = [];
597 653 if ( isset( $images[0] ) ) {
598 654 foreach ( $images as $url ) {
599 655 $is_original_uploaded = self::is_uploaded_image( $url );
600 - $attachment_id = false;
656 + $attachment_id = false;
601 657 if ( $is_original_uploaded ) {
602 658 if ( $job === 'rollback_images' ) {
603 659 $attachment_id = self::get_attachment_id_from_url( $url );
604 660 }
@@ -603,9 +659,9 @@
603 659 $attachment_id = self::get_attachment_id_from_url( $url );
604 660 }
605 661 } else {
606 662 if ( $job === 'offload_images' ) {
607 - $id_and_size = $this->get_local_attachement_id_from_url( $url );
663 + $id_and_size = $this->get_local_attachement_id_from_url( $url );
608 664 $attachment_id = $id_and_size['attachment_id'];
609 665 }
610 666 }
611 667 if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) {
@@ -613,8 +669,9 @@
613 669 }
614 670 $found_images[] = intval( $attachment_id );
615 671 }
616 672 }
673 +
617 674 return apply_filters( 'optml_content_images_to_update', $found_images, $content );
618 675 }
619 676
620 677 /**
@@ -633,12 +690,13 @@
633 690 }
634 691 $post_types = array_values(
635 692 array_filter(
636 693 get_post_types(),
637 - function( $post_type ) {
694 + function ( $post_type ) {
638 695 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
639 696 return false;
640 697 }
698 +
641 699 return true;
642 700 }
643 701 )
644 702 );
@@ -672,12 +730,12 @@
672 730 if ( get_post_type() !== 'attachment' ) {
673 731 $ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job );
674 732 if ( count( $ids ) > 0 ) {
675 733 $images_to_update[ $content_id ] = $ids;
676 - $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
734 + $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
677 735 if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) {
678 736 foreach ( $duplicated_pages as $duplicated_id ) {
679 - $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 );
680 738 $images_to_update[ $duplicated_id ] = $duplicated_ids;
681 739 }
682 740 }
683 741 }
@@ -690,12 +748,13 @@
690 748 delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG );
691 749 }
692 750 }
693 751 }
694 - $page ++;
752 + ++$page;
695 753 }
696 - $result['page'] = $page;
754 + $result['page'] = $page;
697 755 $result['imagesToUpdate'] = $images_to_update;
756 +
698 757 return $result;
699 758 }
700 759
701 760 /**
@@ -701,9 +760,9 @@
701 760 /**
702 761 * Add inline action to push to our servers.
703 762 *
704 763 * @param array $actions All actions.
705 - * @param \WP_Post $post The current post image object.
764 + * @param \WP_Post $post The current post image object.
706 765 *
707 766 * @return array
708 767 */
709 768 public function add_inline_media_action( $actions, $post ) {
@@ -714,41 +773,20 @@
714 773 $file = $meta['file'];
715 774 if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) {
716 775 return $actions;
717 776 }
718 - if ( ! self::is_uploaded_image( $file ) ) {
719 - $upload_action_url = add_query_arg(
720 - [
721 - 'page' => 'optimole',
722 - 'optimole_action' => 'offload_images',
723 - '0' => $post->ID,
724 - ],
725 - 'admin.php'
726 - );
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 + );
727 788
728 - $actions['offload_images'] = sprintf(
729 - '<a href="%s" aria-label="%s">%s</a>',
730 - $upload_action_url,
731 - esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
732 - esc_html__( 'Offload to Optimole', 'optimole-wp' )
733 - );
734 - }
735 - if ( self::is_uploaded_image( $file ) ) {
736 - $rollback_action_url = add_query_arg(
737 - [
738 - 'page' => 'optimole',
739 - 'optimole_action' => 'rollback_images',
740 - '0' => $post->ID,
741 - ],
742 - 'admin.php'
743 - );
744 - $actions['rollback_images'] = sprintf(
745 - '<a href="%s" aria-label="%s">%s</a>',
746 - $rollback_action_url,
747 - esc_attr__( 'Restore image to media library', 'optimole-wp' ),
748 - esc_html__( 'Restore image to media library', 'optimole-wp' )
749 - );
750 - }
751 789 return $actions;
752 790 }
753 791
754 792 /**
@@ -754,8 +792,9 @@
754 792 /**
755 793 * Upload images to our servers and update inside pages.
756 794 *
757 795 * @param array $image_ids The id of the attachments for the selected images.
796 + *
758 797 * @return int The number of successfully processed images.
759 798 */
760 799 public function upload_and_update_existing_images( $image_ids ) {
761 800 $success_up = 0;
@@ -765,16 +804,17 @@
765 804 }
766 805 foreach ( $image_ids as $id ) {
767 806 if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) {
768 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
769 - update_post_meta( $id, 'optimole_offload', 'true' );
770 - $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;
771 811 continue;
772 812 }
773 813
774 814 $meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id );
775 815 if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) {
776 - $success_up ++;
816 + ++$success_up;
777 817 wp_update_attachment_metadata( $id, $meta );
778 818 }
779 819 }
780 820 if ( $success_up > 0 ) {
@@ -782,8 +822,9 @@
782 822 do_action( 'optml_log', ' call post update, succesful images: ' );
783 823 do_action( 'optml_log', $success_up );
784 824 }
785 825 }
826 +
786 827 return $success_up;
787 828 }
788 829
789 830 /**
@@ -789,14 +830,16 @@
789 830 /**
790 831 * Return the original url of an image attachment.
791 832 *
792 833 * @param integer $post_id Image attachment id.
834 + *
793 835 * @return string|bool The original url of the image.
794 836 */
795 837 public static function get_original_url( $post_id ) {
796 838 self::$return_original_url = true;
797 - $original_url = wp_get_attachment_url( $post_id );
839 + $original_url = wp_get_attachment_url( $post_id );
798 840 self::$return_original_url = false;
841 +
799 842 return $original_url;
800 843 }
801 844
802 845 /**
@@ -802,8 +845,9 @@
802 845 /**
803 846 * Bring images back to media library and update inside pages.
804 847 *
805 848 * @param array $image_ids The id of the attachments for the selected images.
849 + *
806 850 * @return int The number of successfully processed images.
807 851 */
808 852 public function rollback_and_update_images( $image_ids ) {
809 853 $success_back = 0;
@@ -820,28 +864,29 @@
820 864 $current_meta = wp_get_attachment_metadata( $id );
821 865 if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) {
822 866 delete_post_meta( $id, self::META_KEYS['offloaded'] );
823 867 delete_post_meta( $id, self::OM_OFFLOADED_FLAG );
824 - $success_back++;
868 + ++$success_back;
825 869 continue;
826 870 }
827 - $table_id = [];
871 +
828 872 // Account for scaled images.
829 - $source_file = isset( $current_meta['original_image'] ) ? $current_meta['original_image'] : $current_meta['file']; // @phpstan-ignore-line - this exists for scaled images.
830 - $filename = pathinfo( $source_file, PATHINFO_BASENAME );
831 - preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $table_id );
832 - if ( ! isset( $table_id[1] ) ) {
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 ) {
833 878 continue;
834 879 }
835 - $table_id = $table_id[1];
880 +
836 881 if ( OPTML_DEBUG_MEDIA ) {
837 882 do_action( 'optml_log', ' image cloud id ' );
838 - do_action( 'optml_log', $table_id );
883 + do_action( 'optml_log', $image_id );
839 884 }
840 - $request = new Optml_Api();
841 - $get_response = $request->call_upload_api( '', 'false', $table_id, 'false', 'true' );
842 885
843 - 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 ) {
844 889 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
845 890 if ( OPTML_DEBUG_MEDIA ) {
846 891 do_action( 'optml_log', ' error get url' );
847 892 }
@@ -846,13 +891,12 @@
846 891 do_action( 'optml_log', ' error get url' );
847 892 }
848 893
849 894 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
895 +
850 896 continue;
851 897 }
852 898
853 - $get_url = json_decode( $get_response['body'], true )['getUrl'];
854 -
855 899 if ( ! function_exists( 'download_url' ) ) {
856 900 include_once ABSPATH . 'wp-admin/includes/file.php';
857 901 }
858 902 if ( ! function_exists( 'download_url' ) ) {
@@ -859,9 +903,9 @@
859 903 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
860 904 continue;
861 905 }
862 906 $timeout_seconds = 60;
863 - $temp_file = download_url( $get_url, $timeout_seconds );
907 + $temp_file = download_url( $image_url, $timeout_seconds );
864 908
865 909 if ( is_wp_error( $temp_file ) ) {
866 910 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
867 911 if ( OPTML_DEBUG_MEDIA ) {
@@ -867,9 +911,9 @@
867 911 if ( OPTML_DEBUG_MEDIA ) {
868 912 do_action( 'optml_log', ' download_url error ' );
869 913 }
870 914
871 - 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.' );
872 916 continue;
873 917 }
874 918
875 919 $extension = $this->get_ext( $filename );
@@ -895,12 +939,12 @@
895 939 ];
896 940
897 941 $overrides = [
898 942 // do not expect the default form data from normal uploads
899 - 'test_form' => false,
943 + 'test_form' => false,
900 944
901 945 // Setting this to false lets WordPress allow empty files, not recommended.
902 - 'test_size' => true,
946 + 'test_size' => true,
903 947
904 948 // A properly uploaded file will pass this test. There should be no reason to override this one.
905 949 'test_upload' => true,
906 950 ];
@@ -977,17 +1021,17 @@
977 1021 delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
978 1022 }
979 1023 }
980 1024 }
981 - $success_back++;
1025 + ++$success_back;
982 1026
983 1027 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
984 1028
985 - $original_url = self::get_original_url( $id );
1029 + $original_url = self::get_original_url( $id );
986 1030 if ( $original_url === false ) {
987 1031 continue;
988 1032 }
989 - $this->delete_attachment_from_server( $original_url, $id, $table_id );
1033 + $this->delete_attachment_from_server( $original_url, $id, $image_id );
990 1034 }
991 1035
992 1036 if ( $success_back > 0 ) {
993 1037 if ( OPTML_DEBUG_MEDIA ) {
@@ -994,8 +1038,9 @@
994 1038 do_action( 'optml_log', ' call update post, success rollback' );
995 1039 do_action( 'optml_log', $success_back );
996 1040 }
997 1041 }
1042 +
998 1043 return $success_back;
999 1044 }
1000 1045
1001 1046 /**
@@ -1000,11 +1045,12 @@
1000 1045
1001 1046 /**
1002 1047 * Handle the bulk actions.
1003 1048 *
1004 - * @param string $redirect The current url from the media library.
1005 - * @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.
1006 1051 * @param array $image_ids The id of the attachments for the selected images.
1052 + *
1007 1053 * @return string The url with the correspondent query args for the executed actions.
1008 1054 */
1009 1055 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
1010 1056
@@ -1012,28 +1058,29 @@
1012 1058 return $redirect;
1013 1059 }
1014 1060
1015 1061 $image_ids = array_slice( $image_ids, 0, 20, true );
1016 - $redirect = 'admin.php';
1017 - $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
1018 - $redirect = add_query_arg( 'page', 'optimole', $redirect );
1019 - $redirect = add_query_arg( $image_ids, $redirect );
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 +
1020 1067 return $redirect;
1021 -
1022 1068 }
1023 1069
1024 1070 /**
1025 1071 * Register the bulk media actions.
1026 1072 *
1027 - * @param array $bulk_array The existing actions array.
1028 - * @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.
1029 1076 */
1030 1077 public function register_bulk_media_actions( $bulk_array ) {
1031 1078
1032 - $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1079 + $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1033 1080 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
1081 +
1034 1082 return $bulk_array;
1035 -
1036 1083 }
1037 1084
1038 1085 /**
1039 1086 * Send delete request to our servers and update the meta.
@@ -1039,19 +1086,15 @@
1039 1086 * Send delete request to our servers and update the meta.
1040 1087 *
1041 1088 * @param string $original_url Original url of the image.
1042 1089 * @param integer $post_id Image id inside db.
1043 - * @param string $table_id Our cloud id for the image.
1090 + * @param string $image_id Our cloud id for the image.
1044 1091 */
1045 - public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
1046 - $request = new Optml_Api();
1047 - $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
1092 + public function delete_attachment_from_server( $original_url, $post_id, $image_id ) {
1093 + Optimole::offload()->deleteImage( $image_id );
1048 1094
1049 1095 delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
1050 1096 delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
1051 - if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
1052 - // should add some routine to retry delete once if delete fails
1053 - }
1054 1097 }
1055 1098
1056 1099 /**
1057 1100 * Delete an image from our servers after it is removed from media.
@@ -1074,9 +1117,9 @@
1074 1117 }
1075 1118
1076 1119 $file = $file['file'];
1077 1120 if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
1078 - $original_url = self::get_original_url( $post_id );
1121 + $original_url = self::get_original_url( $post_id );
1079 1122 if ( $original_url === false ) {
1080 1123 return;
1081 1124 }
1082 1125 $table_id = [];
@@ -1094,8 +1137,9 @@
1094 1137 * Get optimized URL for an attachment image if it is uploaded to our servers.
1095 1138 *
1096 1139 * @param string $url The current url.
1097 1140 * @param int $attachment_id The attachment image id.
1141 + *
1098 1142 * @return string Optimole cdn URL.
1099 1143 * @uses filter:wp_get_attachment_url
1100 1144 */
1101 1145 public function get_image_attachment_url( $url, $attachment_id ) {
@@ -1115,10 +1159,9 @@
1115 1159 }
1116 1160
1117 1161 $file = $meta['file'];
1118 1162 if ( self::is_uploaded_image( $file ) ) {
1119 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1120 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1163 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1121 1164 } else {
1122 1165 // this is for the users that already offloaded the images before the other fixes
1123 1166 $local_file = get_attached_file( $attachment_id );
1124 1167 if ( ! file_exists( $local_file ) ) {
@@ -1127,10 +1170,9 @@
1127 1170 foreach ( $duplicated_images as $id ) {
1128 1171 if ( ! empty( $id ) ) {
1129 1172 $duplicated_meta = wp_get_attachment_metadata( $id );
1130 1173 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1131 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1132 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1174 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1133 1175 }
1134 1176 }
1135 1177 }
1136 1178 }
@@ -1148,11 +1190,11 @@
1148 1190
1149 1191 /**
1150 1192 * Filter the requested image url.
1151 1193 *
1152 - * @param bool|array $image The previous image value (null).
1194 + * @param bool|array $image The previous image value (null).
1153 1195 * @param int $attachment_id The ID of the attachment.
1154 - * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1196 + * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1155 1197 *
1156 1198 * @return bool|array The image sizes and optimized url.
1157 1199 * @uses filter:image_downsize
1158 1200 */
@@ -1184,27 +1226,15 @@
1184 1226 if ( ! isset( $id_filename[1] ) ) {
1185 1227 return $image;
1186 1228 }
1187 1229 $url = self::get_original_url( $attachment_id );
1188 - $optimized_url = ( new Optml_Image(
1189 - $url,
1190 - [
1191 - 'width' => $data['width'],
1192 - 'height' => $data['height'],
1193 - 'resize' => $resize,
1194 - 'quality' => $this->settings->get_numeric_quality(),
1195 - ],
1196 - $this->settings->get( 'cache_buster' )
1197 - ) )->get_url();
1198 - $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1199 - $image = [
1200 - $optimized_url,
1230 +
1231 + return [
1232 + str_replace( $url, $id_filename[1], $this->get_optimized_image_url( $url, $data['width'], $data['height'], $resize ) ),
1201 1233 $data['width'],
1202 1234 $data['height'],
1203 1235 true,
1204 1236 ];
1205 -
1206 - return $image;
1207 1237 }
1208 1238
1209 1239 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1210 1240 return $image;
@@ -1215,9 +1245,9 @@
1215 1245
1216 1246 /**
1217 1247 * Get image extension.
1218 1248 *
1219 - * @param string $path Image path.
1249 + * @param string $path Image path.
1220 1250 *
1221 1251 * @return string
1222 1252 */
1223 1253 private function get_ext( $path ) {
@@ -1224,11 +1254,34 @@
1224 1254 return pathinfo( $path, PATHINFO_EXTENSION );
1225 1255 }
1226 1256
1227 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 + /**
1228 1281 * Update image meta with optimized cdn path.
1229 1282 *
1230 - * @param array $meta Meta information of the image.
1283 + * @param array $meta Meta information of the image.
1231 1284 * @param int $attachment_id The image attachment ID.
1232 1285 *
1233 1286 * @return array
1234 1287 * @uses filter:wp_generate_attachment_metadata
@@ -1238,8 +1291,12 @@
1238 1291 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1239 1292 return $meta;
1240 1293 }
1241 1294
1295 + if ( self::$instance->settings->is_offload_limit_reached() ) {
1296 + return $meta;
1297 + }
1298 +
1242 1299 if ( OPTML_DEBUG_MEDIA ) {
1243 1300 do_action( 'optml_log', 'called generate meta' );
1244 1301 }
1245 1302 // No meta, or image was already uploaded.
@@ -1248,8 +1305,9 @@
1248 1305 do_action( 'optml_log', $meta );
1249 1306 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1250 1307
1251 1308 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
1309 +
1252 1310 return $meta;
1253 1311 }
1254 1312 // Skip images based on filters.
1255 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
@@ -1254,11 +1312,12 @@
1254 1312 // Skip images based on filters.
1255 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1256 1314 do_action( 'optml_log', 'optimization filter' );
1257 1315 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1316 +
1258 1317 return $meta;
1259 1318 }
1260 - $original_url = self::get_original_url( $attachment_id );
1319 + $original_url = self::get_original_url( $attachment_id );
1261 1320
1262 1321 // Could not find original URL.
1263 1322 if ( $original_url === false ) {
1264 1323 do_action( 'optml_log', 'error getting original url' );
@@ -1264,8 +1323,9 @@
1264 1323 do_action( 'optml_log', 'error getting original url' );
1265 1324 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1266 1325
1267 1326 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
1327 +
1268 1328 return $meta;
1269 1329 }
1270 1330
1271 1331 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
@@ -1271,14 +1331,14 @@
1271 1331 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
1272 1332 $original_url = $this->maybe_strip_scaled( $original_url );
1273 1333 $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
1274 1334
1275 - $extension = $this->get_ext( $local_file );
1276 - $content_type = Optml_Config::$image_extensions [ $extension ];
1277 - $temp = explode( '/', $local_file );
1278 - $file_name = end( $temp );
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 );
1279 1339 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1280 - $original_name = $file_name;
1340 + $original_name = $file_name;
1281 1341 if ( OPTML_DEBUG_MEDIA ) {
1282 1342 do_action( 'optml_log', 'file before replace' );
1283 1343 do_action( 'optml_log', $local_file );
1284 1344 }
@@ -1285,10 +1345,10 @@
1285 1345
1286 1346 // check if the current filename is the last deduplicated filename
1287 1347 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1288 1348 // replace the file with the original before deduplication to get the path where the image is uploaded
1289 - $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1290 - $original_name = self::$last_deduplicated;
1349 + $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1350 + $original_name = self::$last_deduplicated;
1291 1351 self::$last_deduplicated = false;
1292 1352 }
1293 1353 if ( OPTML_DEBUG_MEDIA ) {
1294 1354 do_action( 'optml_log', 'file after replace' );
@@ -1299,8 +1359,9 @@
1299 1359 do_action( 'optml_log', 'missing file' );
1300 1360 do_action( 'optml_log', $local_file );
1301 1361
1302 1362 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1363 +
1303 1364 return $meta;
1304 1365 }
1305 1366
1306 1367 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
@@ -1308,8 +1369,9 @@
1308 1369 do_action( 'optml_log', 'invalid extension' );
1309 1370 do_action( 'optml_log', $extension );
1310 1371
1311 1372 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
1373 +
1312 1374 return $meta;
1313 1375 }
1314 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1315 1377 do_action( 'optml_log', 'extension filter' );
@@ -1314,88 +1376,106 @@
1314 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1315 1377 do_action( 'optml_log', 'extension filter' );
1316 1378 do_action( 'optml_log', $extension );
1317 1379 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1380 +
1318 1381 return $meta;
1319 1382 }
1320 1383
1321 - $request = new Optml_Api();
1322 - $generate_url_response = $request->call_upload_api( $original_url );
1384 + $offload_manager = Optimole::offload();
1385 + $offload_usage = $offload_manager->getUsage();
1323 1386
1324 - 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;
1389 +
1390 + if ( $remaining + $offload_usage->getCurrent() >= $offload_usage->getLimit() ) {
1325 1391 if ( OPTML_DEBUG_MEDIA ) {
1326 - do_action( 'optml_log', ' call to signed url error' );
1327 - do_action( 'optml_log', $generate_url_response );
1392 + do_action( 'optml_log', 'limit exceeded' );
1393 + do_action( 'optml_log', $offload_usage );
1328 1394 }
1329 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1330 1395
1331 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid signed url.' );
1396 + self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
1397 +
1398 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: offloading images would exceed limit.' );
1399 +
1332 1400 return $meta;
1333 1401 }
1334 - $decoded_response = json_decode( $generate_url_response['body'], true );
1335 1402
1336 - if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1403 + try {
1404 + $image_id = $offload_manager->uploadImage( $local_file, $original_url );
1405 +
1337 1406 if ( OPTML_DEBUG_MEDIA ) {
1338 - do_action( 'optml_log', ' missing table id or upload url' );
1339 - do_action( 'optml_log', $decoded_response );
1407 + do_action( 'optml_log', 'image id' );
1408 + do_action( 'optml_log', $image_id );
1340 1409 }
1410 +
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 + }
1418 +
1341 1419 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1342 1420
1421 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' file is missing or unreadable.' );
1422 +
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 );
1428 + }
1429 +
1430 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1431 +
1343 1432 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
1433 +
1344 1434 return $meta;
1345 - }
1346 - $table_id = $decoded_response['tableId'];
1347 - if ( OPTML_DEBUG_MEDIA ) {
1348 - do_action( 'optml_log', ' table id' );
1349 - do_action( 'optml_log', $table_id );
1350 - }
1351 - $upload_signed_url = $decoded_response['uploadUrl'];
1352 - $image = file_get_contents( $local_file );
1353 - if ( $image === false ) {
1354 - do_action( 'optml_log', 'can not find file' );
1355 - do_action( 'optml_log', $local_file );
1435 + } catch ( UploadFailedException $exception ) {
1436 + if ( OPTML_DEBUG_MEDIA ) {
1437 + do_action( 'optml_log', 'upload error' );
1438 + do_action( 'optml_log', $exception );
1439 + }
1440 +
1356 1441 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1357 1442
1358 - 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 +
1359 1445 return $meta;
1360 - }
1361 - 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 + }
1362 1451
1363 - $request = new Optml_Api();
1364 - $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' );
1365 1454
1366 - if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1367 - do_action( 'optml_log', 'upload error' );
1368 - do_action( 'optml_log', $result );
1369 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1455 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: upload limit exceeded' );
1370 1456
1371 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1372 - 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 );
1373 1462 }
1374 - $file_size = filesize( $local_file );
1375 - if ( $file_size === false ) {
1376 - $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 );
1377 1471 }
1378 - $request = new Optml_Api();
1379 - $result_update = $request->call_upload_api(
1380 - $original_url,
1381 - 'false',
1382 - $table_id,
1383 - 'success',
1384 - 'false',
1385 - $meta['width'],
1386 - $meta['height'],
1387 - $file_size
1388 - );
1389 - if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1390 - do_action( 'optml_log', 'dynamo update error' );
1391 - do_action( 'optml_log', $result_update );
1392 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1393 1472
1394 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has dynamo update error.' );
1395 - return $meta;
1396 - }
1473 + self::mark_retryable_error( $attachment_id, 'Unknown error from upload api: ' . $exception->getMessage() );
1474 +
1475 + return $meta;
1397 1476 }
1477 +
1398 1478 $url_to_append = $original_url;
1399 1479 $url_parts = parse_url( $original_url );
1400 1480
1401 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
@@ -1400,24 +1480,31 @@
1400 1480
1401 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1402 1482 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1403 1483 }
1404 - $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1405 - $request = new Optml_Api();
1406 - if ( $request->check_optimized_url( $optimized_url ) === false ) {
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 ) {
1407 1488 do_action( 'optml_log', 'optimization error' );
1408 1489 do_action( 'optml_log', $optimized_url );
1409 - $request->call_upload_api( $original_url, 'true', $table_id );
1490 +
1491 + Optimole::offload()->deleteImage( $image_id );
1492 +
1410 1493 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1411 1494
1412 1495 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
1496 +
1413 1497 return $meta;
1414 1498 }
1415 - unlink( $local_file );
1499 +
1500 + @unlink( $local_file );
1501 +
1416 1502 update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
1417 1503 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1418 - $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1419 1504
1505 + $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $image_id . '/' . $url_to_append;
1506 +
1420 1507 if ( isset( $meta['sizes'] ) ) {
1421 1508 foreach ( $meta['sizes'] as $key => $value ) {
1422 1509 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1423 1510 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
@@ -1454,8 +1541,9 @@
1454 1541 if ( OPTML_DEBUG_MEDIA ) {
1455 1542 do_action( 'optml_log', 'success offload' );
1456 1543 }
1457 1544
1545 + self::decrement_process_meta_remaining();
1458 1546 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' );
1459 1547 $attachment_page_id = wp_get_post_parent_id( $attachment_id );
1460 1548
1461 1549 if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
@@ -1462,8 +1550,9 @@
1462 1550 self::$offload_update_post = true;
1463 1551 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1464 1552 self::$offload_update_post = false;
1465 1553 }
1554 +
1466 1555 return $meta;
1467 1556 }
1468 1557
1469 1558 /**
@@ -1470,8 +1559,9 @@
1470 1559 * Get the args for wp query according to the scope.
1471 1560 *
1472 1561 * @param int $batch Number of images to get.
1473 1562 * @param string $action The action for which to get the images.
1563 + *
1474 1564 * @return array|false The query options array or false if not passed a valid action.
1475 1565 */
1476 1566 public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
1477 1567
@@ -1482,11 +1572,11 @@
1482 1572 'no_found_rows' => true,
1483 1573 ];
1484 1574
1485 1575 if ( $get_images === true ) {
1486 - $args['post_type'] = 'attachment';
1576 + $args['post_type'] = 'attachment';
1487 1577 $args['post_mime_type'] = 'image';
1488 - $args['post_status'] = 'inherit';
1578 + $args['post_status'] = 'inherit';
1489 1579
1490 1580 // Offload args.
1491 1581 if ( $action === 'offload_images' ) {
1492 1582 $args['meta_query'] = [
@@ -1499,8 +1589,9 @@
1499 1589 'key' => self::META_KEYS['offload_error'],
1500 1590 'compare' => 'NOT EXISTS',
1501 1591 ],
1502 1592 ];
1593 +
1503 1594 return $args;
1504 1595 }
1505 1596
1506 1597 // Rollback args.
@@ -1544,20 +1635,24 @@
1544 1635 /**
1545 1636 * Query the database and upload images to our servers.
1546 1637 *
1547 1638 * @param int $batch Number of images to process in a batch.
1639 + *
1548 1640 * @return array Number of found images and number of successfully processed images.
1549 1641 */
1550 1642 public function upload_images( $batch, $images = [] ) {
1643 + self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1644 +
1551 1645 if ( empty( $images ) || $images === 'none' ) {
1552 - $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 );
1553 1647 $attachments = new \WP_Query( $args );
1554 - $ids = $attachments->get_posts();
1648 + $ids = $attachments->get_posts();
1555 1649 } else {
1556 1650 $ids = array_slice( $images, 0, $batch );
1557 1651 }
1558 - $result = [ 'found_images' => count( $ids ) ];
1652 + $result = [ 'found_images' => count( $ids ) ];
1559 1653 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1654 +
1560 1655 return $result;
1561 1656 }
1562 1657
1563 1658 /**
@@ -1563,20 +1658,22 @@
1563 1658 /**
1564 1659 * Query the database and bring back image to media library.
1565 1660 *
1566 1661 * @param int $batch Number of images to process in a batch.
1662 + *
1567 1663 * @return array Number of found images and number of successfully processed images.
1568 1664 */
1569 1665 public function rollback_images( $batch, $images = [] ) {
1570 1666 if ( empty( $images ) || $images === 'none' ) {
1571 - $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 );
1572 1668 $attachments = new \WP_Query( $args );
1573 - $ids = $attachments->get_posts();
1669 + $ids = $attachments->get_posts();
1574 1670 } else {
1575 1671 $ids = array_slice( $images, 0, $batch );
1576 1672 }
1577 - $result = [ 'found_images' => count( $ids ) ];
1673 + $result = [ 'found_images' => count( $ids ) ];
1578 1674 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1675 +
1579 1676 return $result;
1580 1677 }
1581 1678
1582 1679 /**
@@ -1582,18 +1679,20 @@
1582 1679 /**
1583 1680 * Update the post with the given id, the images will be updated by the filters we use.
1584 1681 *
1585 1682 * @param int $post_id The post id to update.
1683 + *
1586 1684 * @return bool Whether the update was succesful or not.
1587 1685 */
1588 1686 public function update_page( $post_id ) {
1589 1687 self::$offload_update_post = true;
1590 - $post_update = wp_update_post( ['ID' => $post_id] );
1688 + $post_update = wp_update_post( [ 'ID' => $post_id ] );
1591 1689 self::$offload_update_post = false;
1592 1690 if ( $post_update === 0 ) {
1593 1691 return false;
1594 1692 }
1595 1693 do_action( 'optml_updated_post', $post_id );
1694 +
1596 1695 return true;
1597 1696 }
1598 1697
1599 1698 /**
@@ -1599,12 +1698,13 @@
1599 1698 /**
1600 1699 * Calculate the number of images in media library and the number of posts/pages.
1601 1700 *
1602 1701 * @param string $action The actions for which to get the number of images.
1702 + *
1603 1703 * @return int Number of images.
1604 1704 */
1605 1705 public static function number_of_images_and_pages( $action ) {
1606 - $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 );
1607 1707
1608 1708 $images = new \WP_Query( $images_args );
1609 1709
1610 1710 // With the new mechanism, when offloading images, we don't need to address pages anymore.
@@ -1612,10 +1712,10 @@
1612 1712 if ( $action === 'offload_images' ) {
1613 1713 return $images->post_count;
1614 1714 }
1615 1715
1616 - $pages_args = self::get_images_or_pages_query_args( -1, $action );
1617 - $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 );
1618 1718
1619 1719 return $pages->post_count + $images->post_count;
1620 1720 }
1621 1721
@@ -1622,8 +1722,9 @@
1622 1722 /**
1623 1723 * Calculate the number of images in media library and the number of posts/pages by IDs.
1624 1724 *
1625 1725 * @param string $action The actions for which to get the number of images.
1726 + *
1626 1727 * @return int Number of images.
1627 1728 */
1628 1729 public static function number_of_images_by_ids( $action, $ids ) {
1629 1730 $args = self::get_images_or_pages_query_args( - 1, $action, true );
@@ -1646,9 +1747,9 @@
1646 1747 return [];
1647 1748 }
1648 1749
1649 1750 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1650 - $transient = get_transient( $transient_key );
1751 + $transient = get_transient( $transient_key );
1651 1752
1652 1753 if ( false !== $transient ) {
1653 1754 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1654 1755 }
@@ -1655,10 +1756,10 @@
1655 1756
1656 1757 global $wpdb;
1657 1758
1658 1759 $image_urls = array_map(
1659 - function( $image_id ) {
1660 - $meta = wp_get_attachment_metadata( $image_id );
1760 + function ( $image_id ) {
1761 + $meta = wp_get_attachment_metadata( $image_id );
1661 1762 $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1662 1763
1663 1764 return str_replace( '.' . $extension, '', $meta['file'] );
1664 1765 },
@@ -1681,11 +1782,11 @@
1681 1782 }
1682 1783 }
1683 1784
1684 1785 // Get all the posts IDs by using LIMIT and offset in a loop.
1685 - $ids = [];
1786 + $ids = [];
1686 1787 $offset = 0;
1687 - $limit = $batch;
1788 + $limit = $batch;
1688 1789
1689 1790 while ( true ) {
1690 1791 $posts = $wpdb->get_col(
1691 1792 $wpdb->prepare(
@@ -1698,9 +1799,9 @@
1698 1799 if ( empty( $posts ) ) {
1699 1800 break;
1700 1801 }
1701 1802
1702 - $ids = array_merge( $ids, $posts );
1803 + $ids = array_merge( $ids, $posts );
1703 1804 $offset += $limit;
1704 1805 }
1705 1806
1706 1807 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
@@ -1715,11 +1816,28 @@
1715 1816 *
1716 1817 * @return void
1717 1818 */
1718 1819 public static function record_process_meta( $count ) {
1820 + $meta = get_option( 'optml_process_meta', [] );
1821 + $meta['count'] = $count;
1822 + $meta['remaining'] = $count;
1823 + $meta['start_time'] = time();
1824 + update_option( 'optml_process_meta', $meta );
1825 + }
1826 +
1827 + /**
1828 + * Update the process meta count.
1829 + *
1830 + * @return void
1831 + */
1832 + public static function decrement_process_meta_remaining() {
1719 1833 $meta = get_option( 'optml_process_meta', [] );
1720 - $meta['count'] = $count;
1721 - $meta['start_time'] = time();
1834 +
1835 + if ( ! isset( $meta['remaining'] ) ) {
1836 + return;
1837 + }
1838 +
1839 + $meta['remaining'] = $meta['remaining'] - 1;
1722 1840 update_option( 'optml_process_meta', $meta );
1723 1841 }
1724 1842
1725 1843 /**
@@ -1727,12 +1845,14 @@
1727 1845 *
1728 1846 * @return array
1729 1847 */
1730 1848 public static function get_process_meta() {
1731 - $res = [];
1732 - $meta = get_option( 'optml_process_meta', [] );
1849 + $res = [];
1850 + $meta = get_option( 'optml_process_meta', [] );
1733 1851 $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1734 - $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1852 + $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1853 + $res['remaining'] = isset( $meta['remaining'] ) ? $meta['remaining'] : $res['count'];
1854 +
1735 1855 return $res;
1736 1856 }
1737 1857
1738 1858 /**
@@ -1739,23 +1859,18 @@
1739 1859 * Calculate the number of images in media library and the number of posts/pages.
1740 1860 *
1741 1861 * @param string $action The actions for which to get the number of images.
1742 1862 * @param bool $refresh Whether to refresh the cron or not.
1743 - * @param array $images The images to process.
1744 1863 *
1745 1864 * @return array Image count and Cron status.
1746 1865 */
1747 - public static function get_image_count( $action, $refresh, $images = [] ) {
1866 + public static function move_images( $action, $refresh ) {
1748 1867 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1749 - $count = 0;
1750 - $step = 0;
1751 - $batch = apply_filters( 'optimole_offload_batch', 20 ); // Reduce this to smaller if we have memory issues during testing.
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.
1752 1871
1753 - if ( empty( $images ) ) {
1754 - $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1755 - } else {
1756 - $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1757 - }
1872 + $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1758 1873
1759 1874 $possible_batch = ceil( $count / 10 );
1760 1875
1761 1876 if ( $possible_batch < $batch ) {
@@ -1768,21 +1883,41 @@
1768 1883 }
1769 1884
1770 1885 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1771 1886
1772 - if ( false === $refresh && empty( $images ) ) {
1773 - $total = ceil( $count / $batch );
1887 + if ( $count === 0 ) {
1888 + $in_progress = false;
1889 + }
1890 + $type = 'offload_images' === $action ? 'offload' : 'rollback';
1891 + self::$instance->settings->update( 'transfer_status', $action );
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 + }
1900 + self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1901 + self::record_process_meta( $count );
1774 1902
1775 - $in_progress = 0 !== $count;
1776 -
1777 1903 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1778 -
1779 - $type = 'offload_images' === $action ? 'offload' : 'rollback';
1780 1904 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1781 1905 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1782 - self::record_process_meta( $count );
1783 1906
1784 - if ( true === $in_progress ) {
1907 + if ( $in_progress !== true ) {
1908 + return [
1909 + 'count' => $count,
1910 + 'status' => $in_progress,
1911 + 'action' => $type,
1912 + ];
1913 + }
1914 +
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 ) {
1919 + $total = ceil( $count / $batch );
1785 1920 self::schedule_action(
1786 1921 time(),
1787 1922 'optml_start_processing_images',
1788 1923 [
@@ -1790,43 +1925,35 @@
1790 1925 $batch,
1791 1926 1,
1792 1927 $total,
1793 1928 $step,
1929 + $lock_token,
1794 1930 ]
1795 1931 );
1796 1932 }
1797 1933 }
1798 1934
1799 - if ( false === $refresh && ! empty( $images ) ) {
1800 - $in_progress = 0 !== $count;
1935 + $response = [
1936 + 'count' => $count,
1937 + 'action' => $type,
1938 + ];
1801 1939
1802 - self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1940 + if ( $type === 'offload' ) {
1941 + $offload_limit_reached = self::$instance->settings->is_offload_limit_reached();
1942 + if ( $offload_limit_reached ) {
1943 + $in_progress = false;
1944 + self::$instance->settings->update( $option, 'disabled' );
1945 + }
1803 1946
1804 - $type = 'offload_images' === $action ? 'offload' : 'rollback';
1805 - self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1806 - self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1807 - self::record_process_meta( $count );
1947 + $response['reached_limit'] = self::$instance->settings->is_offload_limit_reached();
1948 + $response['offload_limit'] = self::$instance->settings->get( 'offload_limit' );
1949 + }
1808 1950
1809 - if ( true === $in_progress ) {
1810 - self::schedule_action(
1811 - time(),
1812 - 'optml_start_processing_images_by_id',
1813 - [
1814 - $action,
1815 - $batch,
1816 - 1,
1817 - $images,
1818 - ]
1819 - );
1820 - }
1821 - }
1951 + $response['status'] = $in_progress;
1822 1952
1823 - return [
1824 - 'count' => $count,
1825 - 'status' => $in_progress,
1826 - 'action' => $action === 'offload_images' ? 'offload' : 'rollback',
1827 - ];
1953 + return $response;
1828 1954 }
1955 +
1829 1956 /**
1830 1957 * Schedule an action.
1831 1958 *
1832 1959 * @param int $time The time to schedule the action.
@@ -1842,106 +1969,147 @@
1842 1969 } else {
1843 1970 return wp_schedule_single_event( $time, $hook, $args );
1844 1971 }
1845 1972 }
1973 +
1846 1974 /**
1847 - * Start Processing Images by IDs
1975 + * Check if an action hook is scheduled.
1848 1976 *
1849 - * @param string $action The action for which to get the number of images.
1850 - * @param int $batch The batch of images to process.
1851 - * @param int $page The page of images to process.
1852 - * @param array $image_ids The images to process.
1977 + * @param string $hook The hook to check.
1853 1978 *
1854 - * @return void
1979 + * @return bool
1855 1980 */
1856 - public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1857 - $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1858 - $type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK;
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 + }
1859 1991
1860 - if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1861 - 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;
1862 2003 }
1863 2004
1864 - set_time_limit( 0 );
2005 + $token = wp_generate_uuid4();
1865 2006
1866 - // Only use the legacy offloaded attachments to query the pages that need to be updated.
1867 - // We can be confident that these IDs are already marked as offloaded.
1868 - $legacy_offloaded = array_filter(
1869 - $image_ids,
1870 - function( $id ) {
1871 - return ! $this->is_new_offloaded_attachment( $id );
1872 - }
2007 + set_transient(
2008 + self::TRANSFER_LOCK_TRANSIENT,
2009 + [
2010 + 'token' => $token,
2011 + 'action' => $action,
2012 + ],
2013 + self::TRANSFER_LOCK_TTL
1873 2014 );
1874 2015
1875 - // On the new mechanism, we don't update posts anymore when offloading.
1876 - $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
2016 + return $token;
2017 + }
1877 2018
1878 - if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
1879 - $meta = self::get_process_meta();
1880 - self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
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 );
1881 2029
1882 - 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 ) {
1883 2057 return;
1884 2058 }
1885 2059
1886 - try {
1887 - // This will be 0 in the case of offloading now.
1888 - if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
1889 - $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
2060 + delete_transient( self::TRANSFER_LOCK_TRANSIENT );
2061 + }
1890 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 );
1891 2087 if ( isset( $to_update['page'] ) ) {
1892 2088 if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
1893 2089 foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
1894 - if ( ! empty( $image_ids ) ) {
1895 - $images = array_intersect( $images, $image_ids );
1896 - }
1897 -
2090 + $images = array_intersect( $images, [ $id ] );
1898 2091 if ( empty( $images ) ) {
1899 2092 continue;
1900 2093 }
1901 -
1902 2094 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1903 2095 Optml_Media_Offload::instance()->update_page( $post_id );
1904 2096 }
1905 2097 }
1906 2098 }
1907 -
1908 2099 $page = $page + 1;
1909 - } else {
1910 - // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1911 - $images = array_slice( $image_ids, 0, $batch );
1912 - $image_ids = array_slice( $image_ids, $batch );
1913 - $action === 'rollback_images' ?
1914 - Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
1915 - Optml_Media_Offload::instance()->upload_images( $batch, $images );
1916 - }
2100 + } while ( ! empty( $to_update['imagesToUpdate'] ) );
1917 2101
1918 - self::schedule_action(
1919 - time(),
1920 - 'optml_start_processing_images_by_id',
1921 - [
1922 - $action,
1923 - $batch,
1924 - $page,
1925 - $image_ids,
1926 - ]
1927 - );
1928 - } catch ( Exception $e ) {
1929 - // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
1930 - $delay_in_seconds = 10;
1931 - self::$instance->logger->add_log( $type, $e->getMessage() );
1932 -
1933 - self::schedule_action(
1934 - time() + $delay_in_seconds,
1935 - 'optml_start_processing_images_by_id',
1936 - [
1937 - $action,
1938 - $batch,
1939 - $page,
1940 - $image_ids,
1941 - ]
1942 - );
2102 + } else {
2103 + $action === 'rollback_images' ?
2104 + Optml_Media_Offload::instance()->rollback_images( 1, [ $id ] ) :
2105 + Optml_Media_Offload::instance()->upload_images( 1, [ $id ] );
1943 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 + }
1944 2112 }
1945 2113
1946 2114 /**
1947 2115 * Start Processing Images
@@ -1946,28 +2114,41 @@
1946 2114 /**
1947 2115 * Start Processing Images
1948 2116 *
1949 2117 * @param string $action The action for which to get the number of images.
1950 - * @param int $batch The batch of images to process.
1951 - * @param int $page The page of images to process.
1952 - * @param int $total The total number of pages.
1953 - * @param int $step The current step.
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.
1954 2123 *
1955 2124 * @return void
1956 2125 */
1957 - public function start_processing_images( $action, $batch, $page, $total, $step ) {
2126 + public function start_processing_images( $action, $batch, $page, $total, $step, $lock_token = '' ) {
1958 2127 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1959 2128 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1960 2129
1961 2130 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
2131 + self::release_transfer_lock( $lock_token );
1962 2132 return;
1963 2133 }
1964 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 +
1965 2140 if ( $step > $total || 0 === $total ) {
1966 2141 $meta = self::get_process_meta();
1967 2142 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1968 2143
1969 2144 self::$instance->settings->update( $option, 'disabled' );
2145 +
2146 + self::$instance->settings->update( 'show_offload_finish_notice', $type );
2147 +
2148 + // Transfer completed successfully: release the lock.
2149 + self::release_transfer_lock( $lock_token );
2150 +
1970 2151 return;
1971 2152 }
1972 2153
1973 2154 set_time_limit( 0 );
@@ -1998,12 +2179,14 @@
1998 2179 $batch,
1999 2180 $page,
2000 2181 $total,
2001 2182 $step,
2183 + $lock_token,
2002 2184 ]
2003 2185 );
2004 2186 } catch ( Exception $e ) {
2005 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.
2006 2189 $delay_in_seconds = 10;
2007 2190 self::$instance->logger->add_log( $type, $e->getMessage() );
2008 2191
2009 2192 self::schedule_action(
@@ -2014,8 +2197,9 @@
2014 2197 $batch,
2015 2198 $page,
2016 2199 $total,
2017 2200 $step,
2201 + $lock_token,
2018 2202 ]
2019 2203 );
2020 2204 }
2021 2205 }
@@ -2023,9 +2207,9 @@
2023 2207 /**
2024 2208 * Alter attachment image src for offloaded images.
2025 2209 *
2026 2210 * @param array|false $image {
2027 - * Array of image data.
2211 + * Array of image data.
2028 2212 *
2029 2213 * @type string $0 Image source URL.
2030 2214 * @type int $1 Image width in pixels.
2031 2215 * @type int $2 Image height in pixels.
@@ -2041,12 +2225,14 @@
2041 2225 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2042 2226 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2043 2227 return $image;
2044 2228 }
2045 -
2046 - $url = get_post( $attachment_id );
2047 - $url = $url->guid;
2048 - $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 + }
2049 2235 $metadata = wp_get_attachment_metadata( $attachment_id );
2050 2236
2051 2237 // Use the original size if the requested size is full.
2052 2238 if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
@@ -2053,12 +2239,13 @@
2053 2239 $image_url = $this->get_new_offloaded_attachment_url(
2054 2240 $url,
2055 2241 $attachment_id,
2056 2242 [
2057 - 'width' => $metadata['width'],
2058 - 'height' => $metadata['height'],
2243 + 'width' => $metadata['width'],
2244 + 'height' => $metadata['height'],
2059 2245 'attachment_id' => $attachment_id,
2060 - ]
2246 + ],
2247 + $metadata
2061 2248 );
2062 2249
2063 2250 return [
2064 2251 $image_url,
@@ -2067,58 +2254,29 @@
2067 2254 false,
2068 2255 ];
2069 2256 }
2070 2257
2071 - $crop = false;
2072 -
2073 - // Size can be int [] containing width and height.
2074 - if ( is_array( $size ) ) {
2075 - $width = $size[0];
2076 - $height = $size[1];
2077 - $crop = true;
2078 - } else {
2079 - $sizes = $this->get_all_image_sizes();
2080 -
2081 - if ( ! isset( $sizes[ $size ] ) ) {
2082 - return [
2083 - $image_url,
2084 - $metadata['width'],
2085 - $metadata['height'],
2086 - false,
2087 - ];
2088 - }
2089 -
2090 - $width = $sizes[ $size ]['width'];
2091 - $height = $sizes[ $size ]['height'];
2092 - $crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
2093 - }
2094 -
2095 - $sizes2crop = self::size_to_crop();
2096 -
2097 - if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
2258 + if ( doing_action( 'wp_insert_post_data' ) && wp_attachment_is( 'video', $attachment_id ) ) {
2098 2259 return $image;
2099 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 + );
2100 2273
2101 - $resize = apply_filters( 'optml_default_crop', [] );
2102 - $data = image_get_intermediate_size( $attachment_id, $size );
2103 -
2104 - if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
2105 - if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
2106 - $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
2107 - }
2108 - }
2109 -
2110 - if ( $crop !== false ) {
2111 - $resize = $this->to_optml_crop( $crop );
2112 - }
2113 -
2114 - $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, ['width' => $width, 'height' => $height, 'resize' => $resize, 'attachment_id' => $attachment_id] );
2115 -
2116 2274 return [
2117 2275 $image_url,
2118 - $width,
2119 - $height,
2120 - $crop,
2276 + $sizes['width'],
2277 + $sizes['height'],
2278 + $size === 'full', // @phpstan-ignore-line
2121 2279 ];
2122 2280 }
2123 2281
2124 2282 /**
@@ -2134,21 +2292,22 @@
2134 2292 if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2135 2293 return $response;
2136 2294 }
2137 2295
2138 - $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();
2139 2304
2140 2305 foreach ( $sizes as $size => $args ) {
2141 2306 if ( isset( $response['sizes'][ $size ] ) ) {
2142 2307 continue;
2143 2308 }
2144 -
2145 - $args = [
2146 - 'height' => $args['height'],
2147 - 'width' => $args['width'],
2148 - 'crop' => true,
2149 - ];
2150 -
2309 + $args = $this->size_to_dimension( $size, $meta );
2151 2310 $response['sizes'][ $size ] = array_merge(
2152 2311 $args,
2153 2312 [
2154 2313 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
@@ -2155,17 +2314,10 @@
2155 2314 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2156 2315 ]
2157 2316 );
2158 2317 }
2318 + $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $meta );
2159 2319
2160 - $url_args = [
2161 - 'height' => $response['height'],
2162 - 'width' => $response['width'],
2163 - 'crop' => false,
2164 - ];
2165 -
2166 - $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
2167 -
2168 2320 return $response;
2169 2321 }
2170 2322
2171 2323 /**
@@ -2186,18 +2338,18 @@
2186 2338
2187 2339 /**
2188 2340 * Get offloaded image attachment URL for new offloads.
2189 2341 *
2190 - * @param string $url The initial attachment URL.
2191 - * @param int $attachment_id The attachment ID.
2192 - * @param array $args The additional arguments.
2193 - * - width: The width of the image.
2194 - * - height: The height of the image.
2195 - * - crop: Whether to crop the image.
2196 - *
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.
2197 2349 * @return string
2198 2350 */
2199 - 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 ) {
2200 2352 $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2201 2353
2202 2354 // Image might have already passed through this filter.
2203 2355 if ( strpos( $url, $process_flag ) !== false ) {
@@ -2202,10 +2354,13 @@
2202 2354 // Image might have already passed through this filter.
2203 2355 if ( strpos( $url, $process_flag ) !== false ) {
2204 2356 return $url;
2205 2357 }
2206 -
2207 - $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 + }
2208 2363 if ( ! isset( $meta['file'] ) ) {
2209 2364 return $url;
2210 2365 }
2211 2366
@@ -2224,27 +2379,11 @@
2224 2379 }
2225 2380
2226 2381 $file = $meta['file'];
2227 2382 if ( self::is_uploaded_image( $file ) ) {
2228 - $optimized_url = ( new Optml_Image(
2229 - $url,
2230 - [
2231 - 'width' => $args['width'],
2232 - 'height' => $args['height'],
2233 - 'quality' => $this->settings->get_numeric_quality(),
2234 - 'resize' => $args['resize'],
2235 - 'attachment_id' => $attachment_id,
2236 - ],
2237 - $this->settings->get( 'cache_buster' )
2238 - ) )->get_url();
2383 + $optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2239 2384
2240 - if ( strpos( $optimized_url, $process_flag ) !== false ) {
2241 - return $optimized_url;
2242 - }
2243 -
2244 - $process_flag = $process_flag . $file;
2245 -
2246 - return str_replace( '/' . $url, '/' . $process_flag, $optimized_url );
2385 + return strpos( $optimized_url, $process_flag ) === false ? str_replace( '/' . ltrim( $file, '/' ), '/' . $process_flag . $file, $optimized_url ) : $optimized_url;
2247 2386 } else {
2248 2387 // this is for the users that already offloaded the images before the other fixes
2249 2388 $local_file = get_attached_file( $attachment_id );
2250 2389 if ( ! file_exists( $local_file ) ) {
@@ -2253,19 +2392,9 @@
2253 2392 foreach ( $duplicated_images as $id ) {
2254 2393 if ( ! empty( $id ) ) {
2255 2394 $duplicated_meta = wp_get_attachment_metadata( $id );
2256 2395 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2257 - $optimized_url = ( new Optml_Image(
2258 - $url,
2259 - [
2260 - 'width' => $args['width'],
2261 - 'height' => $args['height'],
2262 - 'quality' => $this->settings->get_numeric_quality(),
2263 - 'attachment_id' => $attachment_id,
2264 - ],
2265 - $this->settings->get( 'cache_buster' )
2266 - ) )->get_url();
2267 - return $optimized_url;
2396 + return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2268 2397 }
2269 2398 }
2270 2399 }
2271 2400 }
@@ -2293,8 +2422,11 @@
2293 2422 $attachment = $this->get_local_attachement_id_from_url( $url );
2294 2423
2295 2424 // No local attachment.
2296 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 + }
2297 2429 continue;
2298 2430 }
2299 2431
2300 2432 $attachment_id = $attachment['attachment_id'];
@@ -2319,19 +2451,9 @@
2319 2451 }
2320 2452 }
2321 2453
2322 2454 // Build the optimized URL.
2323 - $optimized_url = ( new Optml_Image(
2324 - $url,
2325 - [
2326 - 'width' => $width,
2327 - 'height' => $height,
2328 - 'quality' => $this->settings->get_numeric_quality(),
2329 - 'resize' => $resize,
2330 - 'attachment_id' => $attachment_id,
2331 - ],
2332 - $this->settings->get( 'cache_buster' )
2333 - ) )->get_url();
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 );
2334 2456
2335 2457 // Drop any image size from the URL.
2336 2458 $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2337 2459
@@ -2412,12 +2534,21 @@
2412 2534 if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2413 2535 continue;
2414 2536 }
2415 2537
2416 - $replace[ $url ] = self::get_original_url( $id );
2538 + $original = self::get_original_url( $id );
2417 2539
2540 + if ( $original === false ) {
2541 + continue;
2542 + }
2543 +
2544 + $replace[ $url ] = $original;
2545 +
2418 2546 $size = $this->parse_dimension_from_optimized_url( $url );
2419 2547
2548 + if ( $size[0] === false || $size[1] === false ) {
2549 + continue;
2550 + }
2420 2551 if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2421 2552 continue;
2422 2553 }
2423 2554
@@ -2441,9 +2572,9 @@
2441 2572 $width = $size[0];
2442 2573 $height = $size[1];
2443 2574 } else {
2444 2575 // In case of an image size, we need to calculate the new dimensions for the proper file path.
2445 - $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] );
2576 + $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], (int) $size[0], (int) $size[1] );
2446 2577
2447 2578 $width = $constrained[0];
2448 2579 $height = $constrained[1];
2449 2580 }
@@ -2462,12 +2593,12 @@
2462 2593 /**
2463 2594 * Alter the image size for the image widget.
2464 2595 *
2465 2596 * @param string $html the attachment image HTML string.
2466 - * @param array $settings Control settings.
2597 + * @param array $settings Control settings.
2467 2598 * @param string $image_size_key Optional. Settings key for image size.
2468 2599 * Default is `image`.
2469 - * @param string $image_key Optional. Settings key for image. Default
2600 + * @param string $image_key Optional. Settings key for image. Default
2470 2601 * is null. If not defined uses image size key
2471 2602 * as the image key.
2472 2603 *
2473 2604 * @return string
@@ -2525,9 +2656,9 @@
2525 2656 add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2526 2657 add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2527 2658
2528 2659 // Needed for rendering beaver builder css properly.
2529 - 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 );
2530 2661
2531 2662 // Filter saved data on insert to use local attachments.
2532 2663 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
2533 2664 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
@@ -2537,26 +2668,28 @@
2537 2668 add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2538 2669 }
2539 2670
2540 2671 // Filter loaded data in the editors to use local attachments.
2541 - add_filter( 'content_edit_pre', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2542 - $types = get_post_types_by_support( 'editor' );
2543 - foreach ( $types as $type ) {
2544 - $post_type = get_post_type_object( $type );
2545 - if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2546 - add_filter(
2547 - 'rest_prepare_' . $type,
2548 - [
2549 - self::$instance,
2550 - 'pre_filter_rest_content',
2551 - ],
2552 - 10,
2553 - 3
2554 - );
2555 - }
2556 - }
2672 + add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2557 2673
2558 - 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 );
2559 2692 add_filter(
2560 2693 'elementor/image_size/get_attachment_image_html',
2561 2694 [
2562 2695 $this,
@@ -2564,9 +2697,8 @@
2564 2697 ],
2565 2698 10,
2566 2699 4
2567 2700 );
2568 -
2569 2701 }
2570 2702
2571 2703 /**
2572 2704 * Elementor checks if the file exists before requesting a specific image size.
@@ -2606,6 +2738,95 @@
2606 2738 public function maybe_strip_scaled( $url ) {
2607 2739 $ext = $this->get_ext( $url );
2608 2740
2609 2741 return str_replace( '-scaled.' . $ext, '.' . $ext, $url );
2742 + }
2743 +
2744 + /**
2745 + * Is it a PHPUnit test run.
2746 + *
2747 + * @return bool
2748 + */
2749 + public static function is_phpunit_test() {
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'] );
2610 2831 }
2611 2832 }