PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / trunk
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization vtrunk
4.2.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 3.12.0 All 135 releases
← All changes | inc/media_offload.php +786 -548 3.11.1 → trunk View file →
@@ -5,8 +5,17 @@
5 5 * @package \Optimole\Inc
6 6 * @author Optimole <[email protected]>
7 7 */
8 8
9 +use OptimoleWP\Offload\Loader;
10 +use Optimole\Sdk\Exception\InvalidArgumentException;
11 +use Optimole\Sdk\Exception\InvalidUploadApiResponseException;
12 +use Optimole\Sdk\Exception\RuntimeException;
13 +use Optimole\Sdk\Exception\UploadApiException;
14 +use Optimole\Sdk\Exception\UploadFailedException;
15 +use Optimole\Sdk\Exception\UploadLimitException;
16 +use Optimole\Sdk\Optimole;
17 +
9 18 /**
10 19 * Class Optml_Admin
11 20 */
12 21 class Optml_Media_Offload extends Optml_App_Replacer {
@@ -34,20 +43,32 @@
34 43 */
35 44 public $logger;
36 45
37 46 const KEYS = [
38 - 'uploaded_flag' => 'id:',
39 - 'not_processed_flag' => 'process:',
47 + 'uploaded_flag' => 'id:',
48 + 'not_processed_flag' => 'process:',
40 49 ];
41 50 const META_KEYS = [
42 - 'offloaded' => 'optimole_offload',
43 - 'offload_error' => 'optimole_offload_error',
51 + 'offloaded' => 'optimole_offload',
52 + 'offload_error' => 'optimole_offload_error',
44 53 'rollback_error' => 'optimole_rollback_error',
45 54 ];
46 55 const OM_OFFLOADED_FLAG = 'om_image_offloaded';
47 56 const POST_OFFLOADED_FLAG = 'optimole_offload_post';
48 57 const POST_ROLLBACK_FLAG = 'optimole_rollback_post';
58 + const RETRYABLE_META_COUNTER = '_optimole_retryable_errors';
59 +
49 60 /**
61 + * Transient name for the transfer lock.
62 + */
63 + const TRANSFER_LOCK_TRANSIENT = 'optml_transfer_lock';
64 +
65 + /**
66 + * Time to live for the transfer lock, in seconds.
67 + */
68 + const TRANSFER_LOCK_TTL = 600;
69 +
70 + /**
50 71 * Flag used inside wp_get_attachment url filter.
51 72 *
52 73 * @var bool Whether or not to return the original url of the image.
53 74 */
@@ -82,8 +103,9 @@
82 103 * Adds page meta query args
83 104 *
84 105 * @param string $action The action for which the args are needed.
85 106 * @param array $args The initial args without the added meta_query args.
107 + *
86 108 * @return array The args with the added meta_query args.
87 109 */
88 110 public static function add_page_meta_query_args( $action, $args ) {
89 111 if ( $action === 'offload_images' ) {
@@ -89,9 +111,9 @@
89 111 if ( $action === 'offload_images' ) {
90 112 $args['meta_query'] = [
91 113 'relation' => 'AND',
92 114 [
93 - 'key' => self::POST_OFFLOADED_FLAG,
115 + 'key' => self::POST_OFFLOADED_FLAG,
94 116 'compare' => 'NOT EXISTS',
95 117 ],
96 118 ];
97 119 }
@@ -98,20 +120,21 @@
98 120 if ( $action === 'rollback_images' ) {
99 121 $args['meta_query'] = [
100 122 'relation' => 'AND',
101 123 [
102 - 'key' => self::POST_ROLLBACK_FLAG,
124 + 'key' => self::POST_ROLLBACK_FLAG,
103 125 'compare' => 'NOT EXISTS',
104 126 ],
105 127 ];
106 128 if ( self::$is_legacy_install ) {
107 129 $args['meta_query'][] = [
108 - 'key' => self::POST_OFFLOADED_FLAG,
109 - 'value' => 'true',
130 + 'key' => self::POST_OFFLOADED_FLAG,
131 + 'value' => 'true',
110 132 'compare' => '=',
111 133 ];
112 134 }
113 135 }
136 +
114 137 return $args;
115 138 }
116 139
117 140 /**
@@ -120,9 +143,10 @@
120 143 * @return int Number of all images.
121 144 */
122 145 public static function number_of_all_images() {
123 146 $total_images_by_mime = wp_count_attachments( 'image' );
124 - return array_sum( (array) $total_images_by_mime );
147 +
148 + return array_sum( (array) $total_images_by_mime );
125 149 }
126 150
127 151 /**
128 152 * Optml_Media_Offload constructor.
@@ -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 ];
@@ -913,10 +957,9 @@
913 957 continue;
914 958 }
915 959
916 960 // Move the temporary file into the uploads directory.
917 - $upload_date = $this->is_new_offloaded_attachment( $id ) ? get_the_date( 'Y/m', $id ) : null;
918 - $results = wp_handle_sideload( $file, $overrides, $upload_date );
961 + $results = wp_handle_sideload( $file, $overrides, get_the_date( 'Y/m', $id ) );
919 962 if ( ! empty( $results['error'] ) ) {
920 963 if ( OPTML_DEBUG_MEDIA ) {
921 964 do_action( 'optml_log', ' wp_handle_sideload error' );
922 965 }
@@ -957,8 +1000,10 @@
957 1000 if ( ! function_exists( 'update_attached_file' ) ) {
958 1001 update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
959 1002 continue;
960 1003 }
1004 + update_attached_file( $id, $results['file'] );
1005 +
961 1006 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id );
962 1007 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
963 1008 foreach ( $duplicated_images as $duplicated_id ) {
964 1009 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
@@ -976,17 +1021,17 @@
976 1021 delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
977 1022 }
978 1023 }
979 1024 }
980 - $success_back++;
1025 + ++$success_back;
981 1026
982 1027 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
983 1028
984 - $original_url = self::get_original_url( $id );
1029 + $original_url = self::get_original_url( $id );
985 1030 if ( $original_url === false ) {
986 1031 continue;
987 1032 }
988 - $this->delete_attachment_from_server( $original_url, $id, $table_id );
1033 + $this->delete_attachment_from_server( $original_url, $id, $image_id );
989 1034 }
990 1035
991 1036 if ( $success_back > 0 ) {
992 1037 if ( OPTML_DEBUG_MEDIA ) {
@@ -993,8 +1038,9 @@
993 1038 do_action( 'optml_log', ' call update post, success rollback' );
994 1039 do_action( 'optml_log', $success_back );
995 1040 }
996 1041 }
1042 +
997 1043 return $success_back;
998 1044 }
999 1045
1000 1046 /**
@@ -999,11 +1045,12 @@
999 1045
1000 1046 /**
1001 1047 * Handle the bulk actions.
1002 1048 *
1003 - * @param string $redirect The current url from the media library.
1004 - * @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.
1005 1051 * @param array $image_ids The id of the attachments for the selected images.
1052 + *
1006 1053 * @return string The url with the correspondent query args for the executed actions.
1007 1054 */
1008 1055 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
1009 1056
@@ -1011,28 +1058,29 @@
1011 1058 return $redirect;
1012 1059 }
1013 1060
1014 1061 $image_ids = array_slice( $image_ids, 0, 20, true );
1015 - $redirect = 'admin.php';
1016 - $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
1017 - $redirect = add_query_arg( 'page', 'optimole', $redirect );
1018 - $redirect = add_query_arg( $image_ids, $redirect );
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 +
1019 1067 return $redirect;
1020 -
1021 1068 }
1022 1069
1023 1070 /**
1024 1071 * Register the bulk media actions.
1025 1072 *
1026 - * @param array $bulk_array The existing actions array.
1027 - * @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.
1028 1076 */
1029 1077 public function register_bulk_media_actions( $bulk_array ) {
1030 1078
1031 - $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1079 + $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1032 1080 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
1081 +
1033 1082 return $bulk_array;
1034 -
1035 1083 }
1036 1084
1037 1085 /**
1038 1086 * Send delete request to our servers and update the meta.
@@ -1038,19 +1086,15 @@
1038 1086 * Send delete request to our servers and update the meta.
1039 1087 *
1040 1088 * @param string $original_url Original url of the image.
1041 1089 * @param integer $post_id Image id inside db.
1042 - * @param string $table_id Our cloud id for the image.
1090 + * @param string $image_id Our cloud id for the image.
1043 1091 */
1044 - public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
1045 - $request = new Optml_Api();
1046 - $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
1092 + public function delete_attachment_from_server( $original_url, $post_id, $image_id ) {
1093 + Optimole::offload()->deleteImage( $image_id );
1047 1094
1048 1095 delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
1049 1096 delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
1050 - if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
1051 - // should add some routine to retry delete once if delete fails
1052 - }
1053 1097 }
1054 1098
1055 1099 /**
1056 1100 * Delete an image from our servers after it is removed from media.
@@ -1073,9 +1117,9 @@
1073 1117 }
1074 1118
1075 1119 $file = $file['file'];
1076 1120 if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
1077 - $original_url = self::get_original_url( $post_id );
1121 + $original_url = self::get_original_url( $post_id );
1078 1122 if ( $original_url === false ) {
1079 1123 return;
1080 1124 }
1081 1125 $table_id = [];
@@ -1093,8 +1137,9 @@
1093 1137 * Get optimized URL for an attachment image if it is uploaded to our servers.
1094 1138 *
1095 1139 * @param string $url The current url.
1096 1140 * @param int $attachment_id The attachment image id.
1141 + *
1097 1142 * @return string Optimole cdn URL.
1098 1143 * @uses filter:wp_get_attachment_url
1099 1144 */
1100 1145 public function get_image_attachment_url( $url, $attachment_id ) {
@@ -1114,10 +1159,9 @@
1114 1159 }
1115 1160
1116 1161 $file = $meta['file'];
1117 1162 if ( self::is_uploaded_image( $file ) ) {
1118 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1119 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1163 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1120 1164 } else {
1121 1165 // this is for the users that already offloaded the images before the other fixes
1122 1166 $local_file = get_attached_file( $attachment_id );
1123 1167 if ( ! file_exists( $local_file ) ) {
@@ -1126,10 +1170,9 @@
1126 1170 foreach ( $duplicated_images as $id ) {
1127 1171 if ( ! empty( $id ) ) {
1128 1172 $duplicated_meta = wp_get_attachment_metadata( $id );
1129 1173 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1130 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1131 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1174 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1132 1175 }
1133 1176 }
1134 1177 }
1135 1178 }
@@ -1147,11 +1190,11 @@
1147 1190
1148 1191 /**
1149 1192 * Filter the requested image url.
1150 1193 *
1151 - * @param bool|array $image The previous image value (null).
1194 + * @param bool|array $image The previous image value (null).
1152 1195 * @param int $attachment_id The ID of the attachment.
1153 - * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1196 + * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1154 1197 *
1155 1198 * @return bool|array The image sizes and optimized url.
1156 1199 * @uses filter:image_downsize
1157 1200 */
@@ -1183,27 +1226,15 @@
1183 1226 if ( ! isset( $id_filename[1] ) ) {
1184 1227 return $image;
1185 1228 }
1186 1229 $url = self::get_original_url( $attachment_id );
1187 - $optimized_url = ( new Optml_Image(
1188 - $url,
1189 - [
1190 - 'width' => $data['width'],
1191 - 'height' => $data['height'],
1192 - 'resize' => $resize,
1193 - 'quality' => $this->settings->get_numeric_quality(),
1194 - ],
1195 - $this->settings->get( 'cache_buster' )
1196 - ) )->get_url();
1197 - $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1198 - $image = [
1199 - $optimized_url,
1230 +
1231 + return [
1232 + str_replace( $url, $id_filename[1], $this->get_optimized_image_url( $url, $data['width'], $data['height'], $resize ) ),
1200 1233 $data['width'],
1201 1234 $data['height'],
1202 1235 true,
1203 1236 ];
1204 -
1205 - return $image;
1206 1237 }
1207 1238
1208 1239 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1209 1240 return $image;
@@ -1214,9 +1245,9 @@
1214 1245
1215 1246 /**
1216 1247 * Get image extension.
1217 1248 *
1218 - * @param string $path Image path.
1249 + * @param string $path Image path.
1219 1250 *
1220 1251 * @return string
1221 1252 */
1222 1253 private function get_ext( $path ) {
@@ -1223,11 +1254,34 @@
1223 1254 return pathinfo( $path, PATHINFO_EXTENSION );
1224 1255 }
1225 1256
1226 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 + /**
1227 1281 * Update image meta with optimized cdn path.
1228 1282 *
1229 - * @param array $meta Meta information of the image.
1283 + * @param array $meta Meta information of the image.
1230 1284 * @param int $attachment_id The image attachment ID.
1231 1285 *
1232 1286 * @return array
1233 1287 * @uses filter:wp_generate_attachment_metadata
@@ -1237,8 +1291,12 @@
1237 1291 if ( $this->is_dam_imported_image( $attachment_id ) ) {
1238 1292 return $meta;
1239 1293 }
1240 1294
1295 + if ( self::$instance->settings->is_offload_limit_reached() ) {
1296 + return $meta;
1297 + }
1298 +
1241 1299 if ( OPTML_DEBUG_MEDIA ) {
1242 1300 do_action( 'optml_log', 'called generate meta' );
1243 1301 }
1244 1302 // No meta, or image was already uploaded.
@@ -1247,8 +1305,9 @@
1247 1305 do_action( 'optml_log', $meta );
1248 1306 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1249 1307
1250 1308 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
1309 +
1251 1310 return $meta;
1252 1311 }
1253 1312 // Skip images based on filters.
1254 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
@@ -1253,11 +1312,12 @@
1253 1312 // Skip images based on filters.
1254 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1255 1314 do_action( 'optml_log', 'optimization filter' );
1256 1315 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1316 +
1257 1317 return $meta;
1258 1318 }
1259 - $original_url = self::get_original_url( $attachment_id );
1319 + $original_url = self::get_original_url( $attachment_id );
1260 1320
1261 1321 // Could not find original URL.
1262 1322 if ( $original_url === false ) {
1263 1323 do_action( 'optml_log', 'error getting original url' );
@@ -1263,8 +1323,9 @@
1263 1323 do_action( 'optml_log', 'error getting original url' );
1264 1324 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1265 1325
1266 1326 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
1327 +
1267 1328 return $meta;
1268 1329 }
1269 1330
1270 1331 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
@@ -1270,14 +1331,14 @@
1270 1331 // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
1271 1332 $original_url = $this->maybe_strip_scaled( $original_url );
1272 1333 $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
1273 1334
1274 - $extension = $this->get_ext( $local_file );
1275 - $content_type = Optml_Config::$image_extensions [ $extension ];
1276 - $temp = explode( '/', $local_file );
1277 - $file_name = end( $temp );
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 );
1278 1339 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1279 - $original_name = $file_name;
1340 + $original_name = $file_name;
1280 1341 if ( OPTML_DEBUG_MEDIA ) {
1281 1342 do_action( 'optml_log', 'file before replace' );
1282 1343 do_action( 'optml_log', $local_file );
1283 1344 }
@@ -1284,10 +1345,10 @@
1284 1345
1285 1346 // check if the current filename is the last deduplicated filename
1286 1347 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1287 1348 // replace the file with the original before deduplication to get the path where the image is uploaded
1288 - $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1289 - $original_name = self::$last_deduplicated;
1349 + $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1350 + $original_name = self::$last_deduplicated;
1290 1351 self::$last_deduplicated = false;
1291 1352 }
1292 1353 if ( OPTML_DEBUG_MEDIA ) {
1293 1354 do_action( 'optml_log', 'file after replace' );
@@ -1298,8 +1359,9 @@
1298 1359 do_action( 'optml_log', 'missing file' );
1299 1360 do_action( 'optml_log', $local_file );
1300 1361
1301 1362 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1363 +
1302 1364 return $meta;
1303 1365 }
1304 1366
1305 1367 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
@@ -1307,8 +1369,9 @@
1307 1369 do_action( 'optml_log', 'invalid extension' );
1308 1370 do_action( 'optml_log', $extension );
1309 1371
1310 1372 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
1373 +
1311 1374 return $meta;
1312 1375 }
1313 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1314 1377 do_action( 'optml_log', 'extension filter' );
@@ -1313,88 +1376,106 @@
1313 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1314 1377 do_action( 'optml_log', 'extension filter' );
1315 1378 do_action( 'optml_log', $extension );
1316 1379 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1380 +
1317 1381 return $meta;
1318 1382 }
1319 1383
1320 - $request = new Optml_Api();
1321 - $generate_url_response = $request->call_upload_api( $original_url );
1384 + $offload_manager = Optimole::offload();
1385 + $offload_usage = $offload_manager->getUsage();
1322 1386
1323 - 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() ) {
1324 1391 if ( OPTML_DEBUG_MEDIA ) {
1325 - do_action( 'optml_log', ' call to signed url error' );
1326 - do_action( 'optml_log', $generate_url_response );
1392 + do_action( 'optml_log', 'limit exceeded' );
1393 + do_action( 'optml_log', $offload_usage );
1327 1394 }
1328 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1329 1395
1330 - 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 +
1331 1400 return $meta;
1332 1401 }
1333 - $decoded_response = json_decode( $generate_url_response['body'], true );
1334 1402
1335 - if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1403 + try {
1404 + $image_id = $offload_manager->uploadImage( $local_file, $original_url );
1405 +
1336 1406 if ( OPTML_DEBUG_MEDIA ) {
1337 - do_action( 'optml_log', ' missing table id or upload url' );
1338 - do_action( 'optml_log', $decoded_response );
1407 + do_action( 'optml_log', 'image id' );
1408 + do_action( 'optml_log', $image_id );
1339 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 +
1340 1419 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1341 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 +
1342 1432 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
1433 +
1343 1434 return $meta;
1344 - }
1345 - $table_id = $decoded_response['tableId'];
1346 - if ( OPTML_DEBUG_MEDIA ) {
1347 - do_action( 'optml_log', ' table id' );
1348 - do_action( 'optml_log', $table_id );
1349 - }
1350 - $upload_signed_url = $decoded_response['uploadUrl'];
1351 - $image = file_get_contents( $local_file );
1352 - if ( $image === false ) {
1353 - do_action( 'optml_log', 'can not find file' );
1354 - do_action( 'optml_log', $local_file );
1435 + } catch ( UploadFailedException $exception ) {
1436 + if ( OPTML_DEBUG_MEDIA ) {
1437 + do_action( 'optml_log', 'upload error' );
1438 + do_action( 'optml_log', $exception );
1439 + }
1440 +
1355 1441 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1356 1442
1357 - 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 +
1358 1445 return $meta;
1359 - }
1360 - 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 + }
1361 1451
1362 - $request = new Optml_Api();
1363 - $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' );
1364 1454
1365 - if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1366 - do_action( 'optml_log', 'upload error' );
1367 - do_action( 'optml_log', $result );
1368 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1455 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: upload limit exceeded' );
1369 1456
1370 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1371 - 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 );
1372 1462 }
1373 - $file_size = filesize( $local_file );
1374 - if ( $file_size === false ) {
1375 - $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 );
1376 1471 }
1377 - $request = new Optml_Api();
1378 - $result_update = $request->call_upload_api(
1379 - $original_url,
1380 - 'false',
1381 - $table_id,
1382 - 'success',
1383 - 'false',
1384 - $meta['width'],
1385 - $meta['height'],
1386 - $file_size
1387 - );
1388 - if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1389 - do_action( 'optml_log', 'dynamo update error' );
1390 - do_action( 'optml_log', $result_update );
1391 - update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1392 1472
1393 - self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has dynamo update error.' );
1394 - return $meta;
1395 - }
1473 + self::mark_retryable_error( $attachment_id, 'Unknown error from upload api: ' . $exception->getMessage() );
1474 +
1475 + return $meta;
1396 1476 }
1477 +
1397 1478 $url_to_append = $original_url;
1398 1479 $url_parts = parse_url( $original_url );
1399 1480
1400 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
@@ -1399,24 +1480,31 @@
1399 1480
1400 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1401 1482 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1402 1483 }
1403 - $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1404 - $request = new Optml_Api();
1405 - if ( $request->check_optimized_url( $optimized_url ) === false ) {
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 ) {
1406 1488 do_action( 'optml_log', 'optimization error' );
1407 1489 do_action( 'optml_log', $optimized_url );
1408 - $request->call_upload_api( $original_url, 'true', $table_id );
1490 +
1491 + Optimole::offload()->deleteImage( $image_id );
1492 +
1409 1493 update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1410 1494
1411 1495 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
1496 +
1412 1497 return $meta;
1413 1498 }
1414 - unlink( $local_file );
1499 +
1500 + @unlink( $local_file );
1501 +
1415 1502 update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
1416 1503 update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1417 - $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1418 1504
1505 + $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $image_id . '/' . $url_to_append;
1506 +
1419 1507 if ( isset( $meta['sizes'] ) ) {
1420 1508 foreach ( $meta['sizes'] as $key => $value ) {
1421 1509 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1422 1510 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
@@ -1453,8 +1541,9 @@
1453 1541 if ( OPTML_DEBUG_MEDIA ) {
1454 1542 do_action( 'optml_log', 'success offload' );
1455 1543 }
1456 1544
1545 + self::decrement_process_meta_remaining();
1457 1546 self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' );
1458 1547 $attachment_page_id = wp_get_post_parent_id( $attachment_id );
1459 1548
1460 1549 if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
@@ -1461,8 +1550,9 @@
1461 1550 self::$offload_update_post = true;
1462 1551 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1463 1552 self::$offload_update_post = false;
1464 1553 }
1554 +
1465 1555 return $meta;
1466 1556 }
1467 1557
1468 1558 /**
@@ -1469,8 +1559,9 @@
1469 1559 * Get the args for wp query according to the scope.
1470 1560 *
1471 1561 * @param int $batch Number of images to get.
1472 1562 * @param string $action The action for which to get the images.
1563 + *
1473 1564 * @return array|false The query options array or false if not passed a valid action.
1474 1565 */
1475 1566 public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
1476 1567
@@ -1481,11 +1572,11 @@
1481 1572 'no_found_rows' => true,
1482 1573 ];
1483 1574
1484 1575 if ( $get_images === true ) {
1485 - $args['post_type'] = 'attachment';
1576 + $args['post_type'] = 'attachment';
1486 1577 $args['post_mime_type'] = 'image';
1487 - $args['post_status'] = 'inherit';
1578 + $args['post_status'] = 'inherit';
1488 1579
1489 1580 // Offload args.
1490 1581 if ( $action === 'offload_images' ) {
1491 1582 $args['meta_query'] = [
@@ -1498,8 +1589,9 @@
1498 1589 'key' => self::META_KEYS['offload_error'],
1499 1590 'compare' => 'NOT EXISTS',
1500 1591 ],
1501 1592 ];
1593 +
1502 1594 return $args;
1503 1595 }
1504 1596
1505 1597 // Rollback args.
@@ -1543,20 +1635,24 @@
1543 1635 /**
1544 1636 * Query the database and upload images to our servers.
1545 1637 *
1546 1638 * @param int $batch Number of images to process in a batch.
1639 + *
1547 1640 * @return array Number of found images and number of successfully processed images.
1548 1641 */
1549 1642 public function upload_images( $batch, $images = [] ) {
1643 + self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1644 +
1550 1645 if ( empty( $images ) || $images === 'none' ) {
1551 - $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 );
1552 1647 $attachments = new \WP_Query( $args );
1553 - $ids = $attachments->get_posts();
1648 + $ids = $attachments->get_posts();
1554 1649 } else {
1555 1650 $ids = array_slice( $images, 0, $batch );
1556 1651 }
1557 - $result = [ 'found_images' => count( $ids ) ];
1652 + $result = [ 'found_images' => count( $ids ) ];
1558 1653 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1654 +
1559 1655 return $result;
1560 1656 }
1561 1657
1562 1658 /**
@@ -1562,20 +1658,22 @@
1562 1658 /**
1563 1659 * Query the database and bring back image to media library.
1564 1660 *
1565 1661 * @param int $batch Number of images to process in a batch.
1662 + *
1566 1663 * @return array Number of found images and number of successfully processed images.
1567 1664 */
1568 1665 public function rollback_images( $batch, $images = [] ) {
1569 1666 if ( empty( $images ) || $images === 'none' ) {
1570 - $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 );
1571 1668 $attachments = new \WP_Query( $args );
1572 - $ids = $attachments->get_posts();
1669 + $ids = $attachments->get_posts();
1573 1670 } else {
1574 1671 $ids = array_slice( $images, 0, $batch );
1575 1672 }
1576 - $result = [ 'found_images' => count( $ids ) ];
1673 + $result = [ 'found_images' => count( $ids ) ];
1577 1674 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1675 +
1578 1676 return $result;
1579 1677 }
1580 1678
1581 1679 /**
@@ -1581,18 +1679,20 @@
1581 1679 /**
1582 1680 * Update the post with the given id, the images will be updated by the filters we use.
1583 1681 *
1584 1682 * @param int $post_id The post id to update.
1683 + *
1585 1684 * @return bool Whether the update was succesful or not.
1586 1685 */
1587 1686 public function update_page( $post_id ) {
1588 1687 self::$offload_update_post = true;
1589 - $post_update = wp_update_post( ['ID' => $post_id] );
1688 + $post_update = wp_update_post( [ 'ID' => $post_id ] );
1590 1689 self::$offload_update_post = false;
1591 1690 if ( $post_update === 0 ) {
1592 1691 return false;
1593 1692 }
1594 1693 do_action( 'optml_updated_post', $post_id );
1694 +
1595 1695 return true;
1596 1696 }
1597 1697
1598 1698 /**
@@ -1598,12 +1698,13 @@
1598 1698 /**
1599 1699 * Calculate the number of images in media library and the number of posts/pages.
1600 1700 *
1601 1701 * @param string $action The actions for which to get the number of images.
1702 + *
1602 1703 * @return int Number of images.
1603 1704 */
1604 1705 public static function number_of_images_and_pages( $action ) {
1605 - $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 );
1606 1707
1607 1708 $images = new \WP_Query( $images_args );
1608 1709
1609 1710 // With the new mechanism, when offloading images, we don't need to address pages anymore.
@@ -1611,10 +1712,10 @@
1611 1712 if ( $action === 'offload_images' ) {
1612 1713 return $images->post_count;
1613 1714 }
1614 1715
1615 - $pages_args = self::get_images_or_pages_query_args( -1, $action );
1616 - $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 );
1617 1718
1618 1719 return $pages->post_count + $images->post_count;
1619 1720 }
1620 1721
@@ -1621,8 +1722,9 @@
1621 1722 /**
1622 1723 * Calculate the number of images in media library and the number of posts/pages by IDs.
1623 1724 *
1624 1725 * @param string $action The actions for which to get the number of images.
1726 + *
1625 1727 * @return int Number of images.
1626 1728 */
1627 1729 public static function number_of_images_by_ids( $action, $ids ) {
1628 1730 $args = self::get_images_or_pages_query_args( - 1, $action, true );
@@ -1645,9 +1747,9 @@
1645 1747 return [];
1646 1748 }
1647 1749
1648 1750 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1649 - $transient = get_transient( $transient_key );
1751 + $transient = get_transient( $transient_key );
1650 1752
1651 1753 if ( false !== $transient ) {
1652 1754 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1653 1755 }
@@ -1654,10 +1756,10 @@
1654 1756
1655 1757 global $wpdb;
1656 1758
1657 1759 $image_urls = array_map(
1658 - function( $image_id ) {
1659 - $meta = wp_get_attachment_metadata( $image_id );
1760 + function ( $image_id ) {
1761 + $meta = wp_get_attachment_metadata( $image_id );
1660 1762 $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1661 1763
1662 1764 return str_replace( '.' . $extension, '', $meta['file'] );
1663 1765 },
@@ -1680,11 +1782,11 @@
1680 1782 }
1681 1783 }
1682 1784
1683 1785 // Get all the posts IDs by using LIMIT and offset in a loop.
1684 - $ids = [];
1786 + $ids = [];
1685 1787 $offset = 0;
1686 - $limit = $batch;
1788 + $limit = $batch;
1687 1789
1688 1790 while ( true ) {
1689 1791 $posts = $wpdb->get_col(
1690 1792 $wpdb->prepare(
@@ -1697,9 +1799,9 @@
1697 1799 if ( empty( $posts ) ) {
1698 1800 break;
1699 1801 }
1700 1802
1701 - $ids = array_merge( $ids, $posts );
1803 + $ids = array_merge( $ids, $posts );
1702 1804 $offset += $limit;
1703 1805 }
1704 1806
1705 1807 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
@@ -1714,11 +1816,28 @@
1714 1816 *
1715 1817 * @return void
1716 1818 */
1717 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() {
1718 1833 $meta = get_option( 'optml_process_meta', [] );
1719 - $meta['count'] = $count;
1720 - $meta['start_time'] = time();
1834 +
1835 + if ( ! isset( $meta['remaining'] ) ) {
1836 + return;
1837 + }
1838 +
1839 + $meta['remaining'] = $meta['remaining'] - 1;
1721 1840 update_option( 'optml_process_meta', $meta );
1722 1841 }
1723 1842
1724 1843 /**
@@ -1726,12 +1845,14 @@
1726 1845 *
1727 1846 * @return array
1728 1847 */
1729 1848 public static function get_process_meta() {
1730 - $res = [];
1731 - $meta = get_option( 'optml_process_meta', [] );
1849 + $res = [];
1850 + $meta = get_option( 'optml_process_meta', [] );
1732 1851 $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1733 - $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 +
1734 1855 return $res;
1735 1856 }
1736 1857
1737 1858 /**
@@ -1738,23 +1859,18 @@
1738 1859 * Calculate the number of images in media library and the number of posts/pages.
1739 1860 *
1740 1861 * @param string $action The actions for which to get the number of images.
1741 1862 * @param bool $refresh Whether to refresh the cron or not.
1742 - * @param array $images The images to process.
1743 1863 *
1744 1864 * @return array Image count and Cron status.
1745 1865 */
1746 - public static function get_image_count( $action, $refresh, $images = [] ) {
1866 + public static function move_images( $action, $refresh ) {
1747 1867 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1748 - $count = 0;
1749 - $step = 0;
1750 - $batch = 50; // Reduce this to 20 if we have memory issues during testing.
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.
1751 1871
1752 - if ( empty( $images ) ) {
1753 - $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1754 - } else {
1755 - $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1756 - }
1872 + $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1757 1873
1758 1874 $possible_batch = ceil( $count / 10 );
1759 1875
1760 1876 if ( $possible_batch < $batch ) {
@@ -1767,22 +1883,42 @@
1767 1883 }
1768 1884
1769 1885 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1770 1886
1771 - if ( false === $refresh && empty( $images ) ) {
1772 - $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 );
1773 1902
1774 - $in_progress = 0 !== $count;
1775 -
1776 1903 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1777 -
1778 - $type = 'offload_images' === $action ? 'offload' : 'rollback';
1779 1904 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1780 1905 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1781 - self::record_process_meta( $count );
1782 1906
1783 - if ( true === $in_progress ) {
1784 - wp_schedule_single_event(
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 );
1920 + self::schedule_action(
1785 1921 time(),
1786 1922 'optml_start_processing_images',
1787 1923 [
1788 1924 $action,
@@ -1789,142 +1925,191 @@
1789 1925 $batch,
1790 1926 1,
1791 1927 $total,
1792 1928 $step,
1929 + $lock_token,
1793 1930 ]
1794 1931 );
1795 1932 }
1796 1933 }
1797 1934
1798 - if ( false === $refresh && ! empty( $images ) ) {
1799 - $in_progress = 0 !== $count;
1935 + $response = [
1936 + 'count' => $count,
1937 + 'action' => $type,
1938 + ];
1800 1939
1801 - 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 + }
1802 1946
1803 - $type = 'offload_images' === $action ? 'offload' : 'rollback';
1804 - self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1805 - self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1806 - self::record_process_meta( $count );
1947 + $response['reached_limit'] = self::$instance->settings->is_offload_limit_reached();
1948 + $response['offload_limit'] = self::$instance->settings->get( 'offload_limit' );
1949 + }
1807 1950
1808 - if ( true === $in_progress ) {
1809 - wp_schedule_single_event(
1810 - time(),
1811 - 'optml_start_processing_images_by_id',
1812 - [
1813 - $action,
1814 - $batch,
1815 - 1,
1816 - $images,
1817 - ]
1818 - );
1819 - }
1951 + $response['status'] = $in_progress;
1952 +
1953 + return $response;
1954 + }
1955 +
1956 + /**
1957 + * Schedule an action.
1958 + *
1959 + * @param int $time The time to schedule the action.
1960 + * @param string $hook The hook to schedule.
1961 + * @param array $args The arguments to pass to the hook.
1962 + *
1963 + * @return mixed
1964 + */
1965 + public static function schedule_action( $time, $hook, $args ) {
1966 + // We use AS if available to avoid issues with WP Cron.
1967 + if ( function_exists( 'as_schedule_single_action' ) ) {
1968 + return as_schedule_single_action( $time, $hook, $args );
1969 + } else {
1970 + return wp_schedule_single_event( $time, $hook, $args );
1820 1971 }
1972 + }
1821 1973
1822 - return [
1823 - 'count' => $count,
1824 - 'status' => $in_progress,
1825 - 'action' => $action === 'offload_images' ? 'offload' : 'rollback',
1826 - ];
1974 + /**
1975 + * Check if an action hook is scheduled.
1976 + *
1977 + * @param string $hook The hook to check.
1978 + *
1979 + * @return bool
1980 + */
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 + }
1827 1990 }
1828 1991
1829 1992 /**
1830 - * Start Processing Images by IDs
1993 + * Attempt to acquire the transfer lock for a given action.
1831 1994 *
1832 - * @param string $action The action for which to get the number of images.
1833 - * @param int $batch The batch of images to process.
1834 - * @param int $page The page of images to process.
1835 - * @param array $image_ids The images to process.
1995 + * @param string $action The transfer action ('offload_images'|'rollback_images').
1836 1996 *
1837 - * @return void
1997 + * @return string|false The lock token on success, false if another worker already holds the lock.
1838 1998 */
1839 - public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1840 - $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1841 - $type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK;
1999 + public static function acquire_transfer_lock( $action ) {
2000 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
2001 + if ( false !== $lock ) {
2002 + return false;
2003 + }
1842 2004
1843 - if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1844 - return;
2005 + $token = wp_generate_uuid4();
2006 +
2007 + set_transient(
2008 + self::TRANSFER_LOCK_TRANSIENT,
2009 + [
2010 + 'token' => $token,
2011 + 'action' => $action,
2012 + ],
2013 + self::TRANSFER_LOCK_TTL
2014 + );
2015 +
2016 + return $token;
2017 + }
2018 +
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 );
2029 +
2030 + if ( ! is_array( $lock ) || ! isset( $lock['token'] ) || $lock['token'] !== $token ) {
2031 + return false;
1845 2032 }
1846 2033
1847 - set_time_limit( 0 );
1848 -
1849 - // Only use the legacy offloaded attachments to query the pages that need to be updated.
1850 - // We can be confident that these IDs are already marked as offloaded.
1851 - $legacy_offloaded = array_filter(
1852 - $image_ids,
1853 - function( $id ) {
1854 - return ! $this->is_new_offloaded_attachment( $id );
1855 - }
2034 + set_transient(
2035 + self::TRANSFER_LOCK_TRANSIENT,
2036 + [
2037 + 'token' => $token,
2038 + 'action' => $action,
2039 + ],
2040 + self::TRANSFER_LOCK_TTL
1856 2041 );
1857 2042
1858 - // On the new mechanism, we don't update posts anymore when offloading.
1859 - $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
2043 + return true;
2044 + }
1860 2045
1861 - if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
1862 - $meta = self::get_process_meta();
1863 - self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
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 );
1864 2055
1865 - self::$instance->settings->update( $option, 'disabled' );
2056 + if ( ! is_array( $lock ) || ! isset( $lock['token'] ) || $lock['token'] !== $token ) {
1866 2057 return;
1867 2058 }
1868 2059
1869 - try {
1870 - // This will be 0 in the case of offloading now.
1871 - if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
1872 - $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
2060 + delete_transient( self::TRANSFER_LOCK_TRANSIENT );
2061 + }
1873 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 );
1874 2087 if ( isset( $to_update['page'] ) ) {
1875 2088 if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
1876 2089 foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
1877 - if ( ! empty( $image_ids ) ) {
1878 - $images = array_intersect( $images, $image_ids );
1879 - }
1880 -
2090 + $images = array_intersect( $images, [ $id ] );
1881 2091 if ( empty( $images ) ) {
1882 2092 continue;
1883 2093 }
1884 -
1885 2094 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1886 2095 Optml_Media_Offload::instance()->update_page( $post_id );
1887 2096 }
1888 2097 }
1889 2098 }
1890 -
1891 2099 $page = $page + 1;
1892 - } else {
1893 - // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1894 - $images = array_slice( $image_ids, 0, $batch );
1895 - $image_ids = array_slice( $image_ids, $batch );
1896 - $action === 'rollback_images' ?
1897 - Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
1898 - Optml_Media_Offload::instance()->upload_images( $batch, $images );
1899 - }
2100 + } while ( ! empty( $to_update['imagesToUpdate'] ) );
1900 2101
1901 - wp_schedule_single_event(
1902 - time(),
1903 - 'optml_start_processing_images_by_id',
1904 - [
1905 - $action,
1906 - $batch,
1907 - $page,
1908 - $image_ids,
1909 - ]
1910 - );
1911 - } catch ( Exception $e ) {
1912 - // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
1913 - $delay_in_seconds = 10;
1914 - self::$instance->logger->add_log( $type, $e->getMessage() );
1915 -
1916 - wp_schedule_single_event(
1917 - time() + $delay_in_seconds,
1918 - 'optml_start_processing_images_by_id',
1919 - [
1920 - $action,
1921 - $batch,
1922 - $page,
1923 - $image_ids,
1924 - ]
1925 - );
2102 + } else {
2103 + $action === 'rollback_images' ?
2104 + Optml_Media_Offload::instance()->rollback_images( 1, [ $id ] ) :
2105 + Optml_Media_Offload::instance()->upload_images( 1, [ $id ] );
1926 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 + }
1927 2112 }
1928 2113
1929 2114 /**
1930 2115 * Start Processing Images
@@ -1929,28 +2114,41 @@
1929 2114 /**
1930 2115 * Start Processing Images
1931 2116 *
1932 2117 * @param string $action The action for which to get the number of images.
1933 - * @param int $batch The batch of images to process.
1934 - * @param int $page The page of images to process.
1935 - * @param int $total The total number of pages.
1936 - * @param int $step The current step.
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.
1937 2123 *
1938 2124 * @return void
1939 2125 */
1940 - public function start_processing_images( $action, $batch, $page, $total, $step ) {
2126 + public function start_processing_images( $action, $batch, $page, $total, $step, $lock_token = '' ) {
1941 2127 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1942 2128 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1943 2129
1944 2130 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
2131 + self::release_transfer_lock( $lock_token );
1945 2132 return;
1946 2133 }
1947 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 +
1948 2140 if ( $step > $total || 0 === $total ) {
1949 2141 $meta = self::get_process_meta();
1950 2142 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1951 2143
1952 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 +
1953 2151 return;
1954 2152 }
1955 2153
1956 2154 set_time_limit( 0 );
@@ -1972,9 +2170,9 @@
1972 2170 }
1973 2171
1974 2172 $step = $step + 1;
1975 2173
1976 - wp_schedule_single_event(
2174 + self::schedule_action(
1977 2175 time(),
1978 2176 'optml_start_processing_images',
1979 2177 [
1980 2178 $action,
@@ -1981,16 +2179,18 @@
1981 2179 $batch,
1982 2180 $page,
1983 2181 $total,
1984 2182 $step,
2183 + $lock_token,
1985 2184 ]
1986 2185 );
1987 2186 } catch ( Exception $e ) {
1988 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.
1989 2189 $delay_in_seconds = 10;
1990 2190 self::$instance->logger->add_log( $type, $e->getMessage() );
1991 2191
1992 - wp_schedule_single_event(
2192 + self::schedule_action(
1993 2193 time() + $delay_in_seconds,
1994 2194 'optml_start_processing_images',
1995 2195 [
1996 2196 $action,
@@ -1997,8 +2197,9 @@
1997 2197 $batch,
1998 2198 $page,
1999 2199 $total,
2000 2200 $step,
2201 + $lock_token,
2001 2202 ]
2002 2203 );
2003 2204 }
2004 2205 }
@@ -2006,9 +2207,9 @@
2006 2207 /**
2007 2208 * Alter attachment image src for offloaded images.
2008 2209 *
2009 2210 * @param array|false $image {
2010 - * Array of image data.
2211 + * Array of image data.
2011 2212 *
2012 2213 * @type string $0 Image source URL.
2013 2214 * @type int $1 Image width in pixels.
2014 2215 * @type int $2 Image height in pixels.
@@ -2024,12 +2225,14 @@
2024 2225 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2025 2226 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2026 2227 return $image;
2027 2228 }
2028 -
2029 - $url = get_post( $attachment_id );
2030 - $url = $url->guid;
2031 - $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 + }
2032 2235 $metadata = wp_get_attachment_metadata( $attachment_id );
2033 2236
2034 2237 // Use the original size if the requested size is full.
2035 2238 if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
@@ -2036,12 +2239,13 @@
2036 2239 $image_url = $this->get_new_offloaded_attachment_url(
2037 2240 $url,
2038 2241 $attachment_id,
2039 2242 [
2040 - 'width' => $metadata['width'],
2041 - 'height' => $metadata['height'],
2243 + 'width' => $metadata['width'],
2244 + 'height' => $metadata['height'],
2042 2245 'attachment_id' => $attachment_id,
2043 - ]
2246 + ],
2247 + $metadata
2044 2248 );
2045 2249
2046 2250 return [
2047 2251 $image_url,
@@ -2050,58 +2254,29 @@
2050 2254 false,
2051 2255 ];
2052 2256 }
2053 2257
2054 - $crop = false;
2055 -
2056 - // Size can be int [] containing width and height.
2057 - if ( is_array( $size ) ) {
2058 - $width = $size[0];
2059 - $height = $size[1];
2060 - $crop = true;
2061 - } else {
2062 - $sizes = $this->get_all_image_sizes();
2063 -
2064 - if ( ! isset( $sizes[ $size ] ) ) {
2065 - return [
2066 - $image_url,
2067 - $metadata['width'],
2068 - $metadata['height'],
2069 - false,
2070 - ];
2071 - }
2072 -
2073 - $width = $sizes[ $size ]['width'];
2074 - $height = $sizes[ $size ]['height'];
2075 - $crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
2076 - }
2077 -
2078 - $sizes2crop = self::size_to_crop();
2079 -
2080 - if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
2258 + if ( doing_action( 'wp_insert_post_data' ) && wp_attachment_is( 'video', $attachment_id ) ) {
2081 2259 return $image;
2082 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 + );
2083 2273
2084 - $resize = apply_filters( 'optml_default_crop', [] );
2085 - $data = image_get_intermediate_size( $attachment_id, $size );
2086 -
2087 - if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
2088 - if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
2089 - $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
2090 - }
2091 - }
2092 -
2093 - if ( $crop !== false ) {
2094 - $resize = $this->to_optml_crop( $crop );
2095 - }
2096 -
2097 - $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, ['width' => $width, 'height' => $height, 'resize' => $resize, 'attachment_id' => $attachment_id] );
2098 -
2099 2274 return [
2100 2275 $image_url,
2101 - $width,
2102 - $height,
2103 - $crop,
2276 + $sizes['width'],
2277 + $sizes['height'],
2278 + $size === 'full', // @phpstan-ignore-line
2104 2279 ];
2105 2280 }
2106 2281
2107 2282 /**
@@ -2117,21 +2292,22 @@
2117 2292 if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2118 2293 return $response;
2119 2294 }
2120 2295
2121 - $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();
2122 2304
2123 2305 foreach ( $sizes as $size => $args ) {
2124 2306 if ( isset( $response['sizes'][ $size ] ) ) {
2125 2307 continue;
2126 2308 }
2127 -
2128 - $args = [
2129 - 'height' => $args['height'],
2130 - 'width' => $args['width'],
2131 - 'crop' => true,
2132 - ];
2133 -
2309 + $args = $this->size_to_dimension( $size, $meta );
2134 2310 $response['sizes'][ $size ] = array_merge(
2135 2311 $args,
2136 2312 [
2137 2313 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
@@ -2138,17 +2314,10 @@
2138 2314 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2139 2315 ]
2140 2316 );
2141 2317 }
2318 + $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $meta );
2142 2319
2143 - $url_args = [
2144 - 'height' => $response['height'],
2145 - 'width' => $response['width'],
2146 - 'crop' => false,
2147 - ];
2148 -
2149 - $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
2150 -
2151 2320 return $response;
2152 2321 }
2153 2322
2154 2323 /**
@@ -2169,18 +2338,18 @@
2169 2338
2170 2339 /**
2171 2340 * Get offloaded image attachment URL for new offloads.
2172 2341 *
2173 - * @param string $url The initial attachment URL.
2174 - * @param int $attachment_id The attachment ID.
2175 - * @param array $args The additional arguments.
2176 - * - width: The width of the image.
2177 - * - height: The height of the image.
2178 - * - crop: Whether to crop the image.
2179 - *
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.
2180 2349 * @return string
2181 2350 */
2182 - 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 ) {
2183 2352 $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2184 2353
2185 2354 // Image might have already passed through this filter.
2186 2355 if ( strpos( $url, $process_flag ) !== false ) {
@@ -2185,10 +2354,13 @@
2185 2354 // Image might have already passed through this filter.
2186 2355 if ( strpos( $url, $process_flag ) !== false ) {
2187 2356 return $url;
2188 2357 }
2189 -
2190 - $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 + }
2191 2363 if ( ! isset( $meta['file'] ) ) {
2192 2364 return $url;
2193 2365 }
2194 2366
@@ -2207,27 +2379,11 @@
2207 2379 }
2208 2380
2209 2381 $file = $meta['file'];
2210 2382 if ( self::is_uploaded_image( $file ) ) {
2211 - $optimized_url = ( new Optml_Image(
2212 - $url,
2213 - [
2214 - 'width' => $args['width'],
2215 - 'height' => $args['height'],
2216 - 'quality' => $this->settings->get_numeric_quality(),
2217 - 'resize' => $args['resize'],
2218 - 'attachment_id' => $attachment_id,
2219 - ],
2220 - $this->settings->get( 'cache_buster' )
2221 - ) )->get_url();
2383 + $optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2222 2384
2223 - if ( strpos( $optimized_url, $process_flag ) !== false ) {
2224 - return $optimized_url;
2225 - }
2226 -
2227 - $process_flag = $process_flag . $file;
2228 -
2229 - return str_replace( '/' . $url, '/' . $process_flag, $optimized_url );
2385 + return strpos( $optimized_url, $process_flag ) === false ? str_replace( '/' . ltrim( $file, '/' ), '/' . $process_flag . $file, $optimized_url ) : $optimized_url;
2230 2386 } else {
2231 2387 // this is for the users that already offloaded the images before the other fixes
2232 2388 $local_file = get_attached_file( $attachment_id );
2233 2389 if ( ! file_exists( $local_file ) ) {
@@ -2236,19 +2392,9 @@
2236 2392 foreach ( $duplicated_images as $id ) {
2237 2393 if ( ! empty( $id ) ) {
2238 2394 $duplicated_meta = wp_get_attachment_metadata( $id );
2239 2395 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2240 - $optimized_url = ( new Optml_Image(
2241 - $url,
2242 - [
2243 - 'width' => $args['width'],
2244 - 'height' => $args['height'],
2245 - 'quality' => $this->settings->get_numeric_quality(),
2246 - 'attachment_id' => $attachment_id,
2247 - ],
2248 - $this->settings->get( 'cache_buster' )
2249 - ) )->get_url();
2250 - return $optimized_url;
2396 + return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2251 2397 }
2252 2398 }
2253 2399 }
2254 2400 }
@@ -2276,8 +2422,11 @@
2276 2422 $attachment = $this->get_local_attachement_id_from_url( $url );
2277 2423
2278 2424 // No local attachment.
2279 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 + }
2280 2429 continue;
2281 2430 }
2282 2431
2283 2432 $attachment_id = $attachment['attachment_id'];
@@ -2302,19 +2451,9 @@
2302 2451 }
2303 2452 }
2304 2453
2305 2454 // Build the optimized URL.
2306 - $optimized_url = ( new Optml_Image(
2307 - $url,
2308 - [
2309 - 'width' => $width,
2310 - 'height' => $height,
2311 - 'quality' => $this->settings->get_numeric_quality(),
2312 - 'resize' => $resize,
2313 - 'attachment_id' => $attachment_id,
2314 - ],
2315 - $this->settings->get( 'cache_buster' )
2316 - ) )->get_url();
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 );
2317 2456
2318 2457 // Drop any image size from the URL.
2319 2458 $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2320 2459
@@ -2395,12 +2534,21 @@
2395 2534 if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2396 2535 continue;
2397 2536 }
2398 2537
2399 - $replace[ $url ] = self::get_original_url( $id );
2538 + $original = self::get_original_url( $id );
2400 2539
2540 + if ( $original === false ) {
2541 + continue;
2542 + }
2543 +
2544 + $replace[ $url ] = $original;
2545 +
2401 2546 $size = $this->parse_dimension_from_optimized_url( $url );
2402 2547
2548 + if ( $size[0] === false || $size[1] === false ) {
2549 + continue;
2550 + }
2403 2551 if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2404 2552 continue;
2405 2553 }
2406 2554
@@ -2424,9 +2572,9 @@
2424 2572 $width = $size[0];
2425 2573 $height = $size[1];
2426 2574 } else {
2427 2575 // In case of an image size, we need to calculate the new dimensions for the proper file path.
2428 - $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] );
2576 + $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], (int) $size[0], (int) $size[1] );
2429 2577
2430 2578 $width = $constrained[0];
2431 2579 $height = $constrained[1];
2432 2580 }
@@ -2445,12 +2593,12 @@
2445 2593 /**
2446 2594 * Alter the image size for the image widget.
2447 2595 *
2448 2596 * @param string $html the attachment image HTML string.
2449 - * @param array $settings Control settings.
2597 + * @param array $settings Control settings.
2450 2598 * @param string $image_size_key Optional. Settings key for image size.
2451 2599 * Default is `image`.
2452 - * @param string $image_key Optional. Settings key for image. Default
2600 + * @param string $image_key Optional. Settings key for image. Default
2453 2601 * is null. If not defined uses image size key
2454 2602 * as the image key.
2455 2603 *
2456 2604 * @return string
@@ -2508,9 +2656,9 @@
2508 2656 add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2509 2657 add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2510 2658
2511 2659 // Needed for rendering beaver builder css properly.
2512 - 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 );
2513 2661
2514 2662 // Filter saved data on insert to use local attachments.
2515 2663 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
2516 2664 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
@@ -2520,26 +2668,28 @@
2520 2668 add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2521 2669 }
2522 2670
2523 2671 // Filter loaded data in the editors to use local attachments.
2524 - add_filter( 'content_edit_pre', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2525 - $types = get_post_types_by_support( 'editor' );
2526 - foreach ( $types as $type ) {
2527 - $post_type = get_post_type_object( $type );
2528 - if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2529 - add_filter(
2530 - 'rest_prepare_' . $type,
2531 - [
2532 - self::$instance,
2533 - 'pre_filter_rest_content',
2534 - ],
2535 - 10,
2536 - 3
2537 - );
2538 - }
2539 - }
2672 + add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2540 2673
2541 - 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 );
2542 2692 add_filter(
2543 2693 'elementor/image_size/get_attachment_image_html',
2544 2694 [
2545 2695 $this,
@@ -2547,9 +2697,8 @@
2547 2697 ],
2548 2698 10,
2549 2699 4
2550 2700 );
2551 -
2552 2701 }
2553 2702
2554 2703 /**
2555 2704 * Elementor checks if the file exists before requesting a specific image size.
@@ -2589,6 +2738,95 @@
2589 2738 public function maybe_strip_scaled( $url ) {
2590 2739 $ext = $this->get_ext( $url );
2591 2740
2592 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'] );
2593 2831 }
2594 2832 }