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 +813 -552 3.11.0 → trunk View file →
@@ -5,8 +5,17 @@
5 5 * @package \Optimole\Inc
6 6 * @author Optimole <[email protected]>
7 7 */
8 8
9 +use OptimoleWP\Offload\Loader;
10 +use Optimole\Sdk\Exception\InvalidArgumentException;
11 +use Optimole\Sdk\Exception\InvalidUploadApiResponseException;
12 +use Optimole\Sdk\Exception\RuntimeException;
13 +use Optimole\Sdk\Exception\UploadApiException;
14 +use Optimole\Sdk\Exception\UploadFailedException;
15 +use Optimole\Sdk\Exception\UploadLimitException;
16 +use Optimole\Sdk\Optimole;
17 +
9 18 /**
10 19 * Class Optml_Admin
11 20 */
12 21 class Optml_Media_Offload extends Optml_App_Replacer {
@@ -34,20 +43,32 @@
34 43 */
35 44 public $logger;
36 45
37 46 const KEYS = [
38 - 'uploaded_flag' => 'id:',
39 - 'not_processed_flag' => 'process:',
47 + 'uploaded_flag' => 'id:',
48 + 'not_processed_flag' => 'process:',
40 49 ];
41 50 const META_KEYS = [
42 - 'offloaded' => 'optimole_offload',
43 - 'offload_error' => 'optimole_offload_error',
51 + 'offloaded' => 'optimole_offload',
52 + 'offload_error' => 'optimole_offload_error',
44 53 'rollback_error' => 'optimole_rollback_error',
45 54 ];
46 55 const OM_OFFLOADED_FLAG = 'om_image_offloaded';
47 56 const POST_OFFLOADED_FLAG = 'optimole_offload_post';
48 57 const POST_ROLLBACK_FLAG = 'optimole_rollback_post';
58 + const RETRYABLE_META_COUNTER = '_optimole_retryable_errors';
59 +
49 60 /**
61 + * Transient name for the transfer lock.
62 + */
63 + const TRANSFER_LOCK_TRANSIENT = 'optml_transfer_lock';
64 +
65 + /**
66 + * Time to live for the transfer lock, in seconds.
67 + */
68 + const TRANSFER_LOCK_TTL = 600;
69 +
70 + /**
50 71 * Flag used inside wp_get_attachment url filter.
51 72 *
52 73 * @var bool Whether or not to return the original url of the image.
53 74 */
@@ -82,8 +103,9 @@
82 103 * Adds page meta query args
83 104 *
84 105 * @param string $action The action for which the args are needed.
85 106 * @param array $args The initial args without the added meta_query args.
107 + *
86 108 * @return array The args with the added meta_query args.
87 109 */
88 110 public static function add_page_meta_query_args( $action, $args ) {
89 111 if ( $action === 'offload_images' ) {
@@ -89,9 +111,9 @@
89 111 if ( $action === 'offload_images' ) {
90 112 $args['meta_query'] = [
91 113 'relation' => 'AND',
92 114 [
93 - 'key' => self::POST_OFFLOADED_FLAG,
115 + 'key' => self::POST_OFFLOADED_FLAG,
94 116 'compare' => 'NOT EXISTS',
95 117 ],
96 118 ];
97 119 }
@@ -98,20 +120,21 @@
98 120 if ( $action === 'rollback_images' ) {
99 121 $args['meta_query'] = [
100 122 'relation' => 'AND',
101 123 [
102 - 'key' => self::POST_ROLLBACK_FLAG,
124 + 'key' => self::POST_ROLLBACK_FLAG,
103 125 'compare' => 'NOT EXISTS',
104 126 ],
105 127 ];
106 128 if ( self::$is_legacy_install ) {
107 129 $args['meta_query'][] = [
108 - 'key' => self::POST_OFFLOADED_FLAG,
109 - 'value' => 'true',
130 + 'key' => self::POST_OFFLOADED_FLAG,
131 + 'value' => 'true',
110 132 'compare' => '=',
111 133 ];
112 134 }
113 135 }
136 +
114 137 return $args;
115 138 }
116 139
117 140 /**
@@ -120,9 +143,10 @@
120 143 * @return int Number of all images.
121 144 */
122 145 public static function number_of_all_images() {
123 146 $total_images_by_mime = wp_count_attachments( 'image' );
124 - return array_sum( (array) $total_images_by_mime );
147 +
148 + return array_sum( (array) $total_images_by_mime );
125 149 }
126 150
127 151 /**
128 152 * Optml_Media_Offload constructor.
@@ -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();
158 + self::$instance->logger = Optml_Logger::instance();
159 +
137 160 if ( self::$instance->settings->is_connected() ) {
138 161 self::$instance->init();
139 162 }
140 - if ( self::$instance->settings->get( 'offload_media' ) === 'enabled' ) {
141 - add_filter( 'image_downsize', [self::$instance, 'generate_filter_downsize_urls'], 10, 3 );
142 - add_filter( 'wp_generate_attachment_metadata', [self::$instance, 'generate_image_meta'], 10, 2 );
143 - add_filter( 'wp_get_attachment_url', [self::$instance, 'get_image_attachment_url'], -999, 2 );
144 - add_filter( 'wp_insert_post_data', [self::$instance, 'filter_uploaded_images'] );
163 + if ( self::$instance->settings->is_offload_enabled() ) {
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' ] );
145 168
146 169 self::$instance->add_new_actions();
147 170
148 - add_action( 'delete_attachment', [self::$instance, 'delete_attachment_hook'], 10 );
149 - add_filter( 'handle_bulk_actions-upload', [self::$instance, 'bulk_action_handler'], 10, 3 );
150 - add_filter( 'bulk_actions-upload', [self::$instance, 'register_bulk_media_actions'] );
151 - add_filter( 'media_row_actions', [self::$instance, 'add_inline_media_action'], 10, 2 );
152 - add_filter( 'wp_calculate_image_srcset', [self::$instance, 'calculate_image_srcset'], 1, 5 );
153 - 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 );
154 178
155 179 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
156 180 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
157 181 if ( $below_6_0_0 ) {
158 - 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 );
159 183 } else {
160 - add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert'], 10, 4 );
184 + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 );
161 185 }
162 186
163 - add_action( 'optml_start_processing_images', [self::$instance, 'start_processing_images'], 10, 5 );
164 - add_action( 'optml_start_processing_images_by_id', [self::$instance, 'start_processing_images_by_id'], 10, 4 );
165 -
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' ] );
166 198 if ( self::$is_legacy_install === null ) {
167 199 self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
168 200 }
169 -
170 - self::$instance->logger = Optml_Logger::instance();
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.
@@ -1510,11 +1602,15 @@
1510 1602 'value' => 'true',
1511 1603 'compare' => '=',
1512 1604 ],
1513 1605 [
1514 - 'key' => self::META_KEYS['rollback_error'],
1606 + 'key' => self::META_KEYS['rollback_error'],
1515 1607 'compare' => 'NOT EXISTS',
1516 1608 ],
1609 + [
1610 + 'key' => Optml_Dam::OM_DAM_IMPORTED_FLAG,
1611 + 'compare' => 'NOT EXISTS',
1612 + ],
1517 1613 ];
1518 1614
1519 1615 return $args;
1520 1616 }
@@ -1539,20 +1635,24 @@
1539 1635 /**
1540 1636 * Query the database and upload images to our servers.
1541 1637 *
1542 1638 * @param int $batch Number of images to process in a batch.
1639 + *
1543 1640 * @return array Number of found images and number of successfully processed images.
1544 1641 */
1545 1642 public function upload_images( $batch, $images = [] ) {
1643 + self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1644 +
1546 1645 if ( empty( $images ) || $images === 'none' ) {
1547 - $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 );
1548 1647 $attachments = new \WP_Query( $args );
1549 - $ids = $attachments->get_posts();
1648 + $ids = $attachments->get_posts();
1550 1649 } else {
1551 1650 $ids = array_slice( $images, 0, $batch );
1552 1651 }
1553 - $result = [ 'found_images' => count( $ids ) ];
1652 + $result = [ 'found_images' => count( $ids ) ];
1554 1653 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1654 +
1555 1655 return $result;
1556 1656 }
1557 1657
1558 1658 /**
@@ -1558,20 +1658,22 @@
1558 1658 /**
1559 1659 * Query the database and bring back image to media library.
1560 1660 *
1561 1661 * @param int $batch Number of images to process in a batch.
1662 + *
1562 1663 * @return array Number of found images and number of successfully processed images.
1563 1664 */
1564 1665 public function rollback_images( $batch, $images = [] ) {
1565 1666 if ( empty( $images ) || $images === 'none' ) {
1566 - $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 );
1567 1668 $attachments = new \WP_Query( $args );
1568 - $ids = $attachments->get_posts();
1669 + $ids = $attachments->get_posts();
1569 1670 } else {
1570 1671 $ids = array_slice( $images, 0, $batch );
1571 1672 }
1572 - $result = [ 'found_images' => count( $ids ) ];
1673 + $result = [ 'found_images' => count( $ids ) ];
1573 1674 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1675 +
1574 1676 return $result;
1575 1677 }
1576 1678
1577 1679 /**
@@ -1577,18 +1679,20 @@
1577 1679 /**
1578 1680 * Update the post with the given id, the images will be updated by the filters we use.
1579 1681 *
1580 1682 * @param int $post_id The post id to update.
1683 + *
1581 1684 * @return bool Whether the update was succesful or not.
1582 1685 */
1583 1686 public function update_page( $post_id ) {
1584 1687 self::$offload_update_post = true;
1585 - $post_update = wp_update_post( ['ID' => $post_id] );
1688 + $post_update = wp_update_post( [ 'ID' => $post_id ] );
1586 1689 self::$offload_update_post = false;
1587 1690 if ( $post_update === 0 ) {
1588 1691 return false;
1589 1692 }
1590 1693 do_action( 'optml_updated_post', $post_id );
1694 +
1591 1695 return true;
1592 1696 }
1593 1697
1594 1698 /**
@@ -1594,12 +1698,13 @@
1594 1698 /**
1595 1699 * Calculate the number of images in media library and the number of posts/pages.
1596 1700 *
1597 1701 * @param string $action The actions for which to get the number of images.
1702 + *
1598 1703 * @return int Number of images.
1599 1704 */
1600 1705 public static function number_of_images_and_pages( $action ) {
1601 - $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 );
1602 1707
1603 1708 $images = new \WP_Query( $images_args );
1604 1709
1605 1710 // With the new mechanism, when offloading images, we don't need to address pages anymore.
@@ -1607,10 +1712,10 @@
1607 1712 if ( $action === 'offload_images' ) {
1608 1713 return $images->post_count;
1609 1714 }
1610 1715
1611 - $pages_args = self::get_images_or_pages_query_args( -1, $action );
1612 - $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 );
1613 1718
1614 1719 return $pages->post_count + $images->post_count;
1615 1720 }
1616 1721
@@ -1617,8 +1722,9 @@
1617 1722 /**
1618 1723 * Calculate the number of images in media library and the number of posts/pages by IDs.
1619 1724 *
1620 1725 * @param string $action The actions for which to get the number of images.
1726 + *
1621 1727 * @return int Number of images.
1622 1728 */
1623 1729 public static function number_of_images_by_ids( $action, $ids ) {
1624 1730 $args = self::get_images_or_pages_query_args( - 1, $action, true );
@@ -1641,9 +1747,9 @@
1641 1747 return [];
1642 1748 }
1643 1749
1644 1750 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1645 - $transient = get_transient( $transient_key );
1751 + $transient = get_transient( $transient_key );
1646 1752
1647 1753 if ( false !== $transient ) {
1648 1754 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1649 1755 }
@@ -1650,10 +1756,10 @@
1650 1756
1651 1757 global $wpdb;
1652 1758
1653 1759 $image_urls = array_map(
1654 - function( $image_id ) {
1655 - $meta = wp_get_attachment_metadata( $image_id );
1760 + function ( $image_id ) {
1761 + $meta = wp_get_attachment_metadata( $image_id );
1656 1762 $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1657 1763
1658 1764 return str_replace( '.' . $extension, '', $meta['file'] );
1659 1765 },
@@ -1676,11 +1782,11 @@
1676 1782 }
1677 1783 }
1678 1784
1679 1785 // Get all the posts IDs by using LIMIT and offset in a loop.
1680 - $ids = [];
1786 + $ids = [];
1681 1787 $offset = 0;
1682 - $limit = $batch;
1788 + $limit = $batch;
1683 1789
1684 1790 while ( true ) {
1685 1791 $posts = $wpdb->get_col(
1686 1792 $wpdb->prepare(
@@ -1693,9 +1799,9 @@
1693 1799 if ( empty( $posts ) ) {
1694 1800 break;
1695 1801 }
1696 1802
1697 - $ids = array_merge( $ids, $posts );
1803 + $ids = array_merge( $ids, $posts );
1698 1804 $offset += $limit;
1699 1805 }
1700 1806
1701 1807 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
@@ -1710,11 +1816,28 @@
1710 1816 *
1711 1817 * @return void
1712 1818 */
1713 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() {
1714 1833 $meta = get_option( 'optml_process_meta', [] );
1715 - $meta['count'] = $count;
1716 - $meta['start_time'] = time();
1834 +
1835 + if ( ! isset( $meta['remaining'] ) ) {
1836 + return;
1837 + }
1838 +
1839 + $meta['remaining'] = $meta['remaining'] - 1;
1717 1840 update_option( 'optml_process_meta', $meta );
1718 1841 }
1719 1842
1720 1843 /**
@@ -1722,12 +1845,14 @@
1722 1845 *
1723 1846 * @return array
1724 1847 */
1725 1848 public static function get_process_meta() {
1726 - $res = [];
1727 - $meta = get_option( 'optml_process_meta', [] );
1849 + $res = [];
1850 + $meta = get_option( 'optml_process_meta', [] );
1728 1851 $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1729 - $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 +
1730 1855 return $res;
1731 1856 }
1732 1857
1733 1858 /**
@@ -1734,23 +1859,18 @@
1734 1859 * Calculate the number of images in media library and the number of posts/pages.
1735 1860 *
1736 1861 * @param string $action The actions for which to get the number of images.
1737 1862 * @param bool $refresh Whether to refresh the cron or not.
1738 - * @param array $images The images to process.
1739 1863 *
1740 1864 * @return array Image count and Cron status.
1741 1865 */
1742 - public static function get_image_count( $action, $refresh, $images = [] ) {
1866 + public static function move_images( $action, $refresh ) {
1743 1867 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1744 - $count = 0;
1745 - $step = 0;
1746 - $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.
1747 1871
1748 - if ( empty( $images ) ) {
1749 - $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1750 - } else {
1751 - $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1752 - }
1872 + $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1753 1873
1754 1874 $possible_batch = ceil( $count / 10 );
1755 1875
1756 1876 if ( $possible_batch < $batch ) {
@@ -1763,22 +1883,42 @@
1763 1883 }
1764 1884
1765 1885 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1766 1886
1767 - if ( false === $refresh && empty( $images ) ) {
1768 - $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 );
1769 1902
1770 - $in_progress = 0 !== $count;
1771 -
1772 1903 self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1773 -
1774 - $type = 'offload_images' === $action ? 'offload' : 'rollback';
1775 1904 self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1776 1905 self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1777 - self::record_process_meta( $count );
1778 1906
1779 - if ( true === $in_progress ) {
1780 - 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(
1781 1921 time(),
1782 1922 'optml_start_processing_images',
1783 1923 [
1784 1924 $action,
@@ -1785,142 +1925,191 @@
1785 1925 $batch,
1786 1926 1,
1787 1927 $total,
1788 1928 $step,
1929 + $lock_token,
1789 1930 ]
1790 1931 );
1791 1932 }
1792 1933 }
1793 1934
1794 - if ( false === $refresh && ! empty( $images ) ) {
1795 - $in_progress = 0 !== $count;
1935 + $response = [
1936 + 'count' => $count,
1937 + 'action' => $type,
1938 + ];
1796 1939
1797 - 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 + }
1798 1946
1799 - $type = 'offload_images' === $action ? 'offload' : 'rollback';
1800 - self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1801 - self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1802 - 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 + }
1803 1950
1804 - if ( true === $in_progress ) {
1805 - wp_schedule_single_event(
1806 - time(),
1807 - 'optml_start_processing_images_by_id',
1808 - [
1809 - $action,
1810 - $batch,
1811 - 1,
1812 - $images,
1813 - ]
1814 - );
1815 - }
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 );
1816 1971 }
1972 + }
1817 1973
1818 - return [
1819 - 'count' => $count,
1820 - 'status' => $in_progress,
1821 - 'action' => $action === 'offload_images' ? 'offload' : 'rollback',
1822 - ];
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 + }
1823 1990 }
1824 1991
1825 1992 /**
1826 - * Start Processing Images by IDs
1993 + * Attempt to acquire the transfer lock for a given action.
1827 1994 *
1828 - * @param string $action The action for which to get the number of images.
1829 - * @param int $batch The batch of images to process.
1830 - * @param int $page The page of images to process.
1831 - * @param array $image_ids The images to process.
1995 + * @param string $action The transfer action ('offload_images'|'rollback_images').
1832 1996 *
1833 - * @return void
1997 + * @return string|false The lock token on success, false if another worker already holds the lock.
1834 1998 */
1835 - public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1836 - $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1837 - $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 + }
1838 2004
1839 - if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1840 - 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;
1841 2032 }
1842 2033
1843 - set_time_limit( 0 );
1844 -
1845 - // Only use the legacy offloaded attachments to query the pages that need to be updated.
1846 - // We can be confident that these IDs are already marked as offloaded.
1847 - $legacy_offloaded = array_filter(
1848 - $image_ids,
1849 - function( $id ) {
1850 - return ! $this->is_new_offloaded_attachment( $id );
1851 - }
2034 + set_transient(
2035 + self::TRANSFER_LOCK_TRANSIENT,
2036 + [
2037 + 'token' => $token,
2038 + 'action' => $action,
2039 + ],
2040 + self::TRANSFER_LOCK_TTL
1852 2041 );
1853 2042
1854 - // On the new mechanism, we don't update posts anymore when offloading.
1855 - $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page );
2043 + return true;
2044 + }
1856 2045
1857 - if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) {
1858 - $meta = self::get_process_meta();
1859 - 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 );
1860 2055
1861 - self::$instance->settings->update( $option, 'disabled' );
2056 + if ( ! is_array( $lock ) || ! isset( $lock['token'] ) || $lock['token'] !== $token ) {
1862 2057 return;
1863 2058 }
1864 2059
1865 - try {
1866 - // This will be 0 in the case of offloading now.
1867 - if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) {
1868 - $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
2060 + delete_transient( self::TRANSFER_LOCK_TRANSIENT );
2061 + }
1869 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 );
1870 2087 if ( isset( $to_update['page'] ) ) {
1871 2088 if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
1872 2089 foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
1873 - if ( ! empty( $image_ids ) ) {
1874 - $images = array_intersect( $images, $image_ids );
1875 - }
1876 -
2090 + $images = array_intersect( $images, [ $id ] );
1877 2091 if ( empty( $images ) ) {
1878 2092 continue;
1879 2093 }
1880 -
1881 2094 Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1882 2095 Optml_Media_Offload::instance()->update_page( $post_id );
1883 2096 }
1884 2097 }
1885 2098 }
1886 -
1887 2099 $page = $page + 1;
1888 - } else {
1889 - // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1890 - $images = array_slice( $image_ids, 0, $batch );
1891 - $image_ids = array_slice( $image_ids, $batch );
1892 - $action === 'rollback_images' ?
1893 - Optml_Media_Offload::instance()->rollback_images( $batch, $images ) :
1894 - Optml_Media_Offload::instance()->upload_images( $batch, $images );
1895 - }
2100 + } while ( ! empty( $to_update['imagesToUpdate'] ) );
1896 2101
1897 - wp_schedule_single_event(
1898 - time(),
1899 - 'optml_start_processing_images_by_id',
1900 - [
1901 - $action,
1902 - $batch,
1903 - $page,
1904 - $image_ids,
1905 - ]
1906 - );
1907 - } catch ( Exception $e ) {
1908 - // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted.
1909 - $delay_in_seconds = 10;
1910 - self::$instance->logger->add_log( $type, $e->getMessage() );
1911 -
1912 - wp_schedule_single_event(
1913 - time() + $delay_in_seconds,
1914 - 'optml_start_processing_images_by_id',
1915 - [
1916 - $action,
1917 - $batch,
1918 - $page,
1919 - $image_ids,
1920 - ]
1921 - );
2102 + } else {
2103 + $action === 'rollback_images' ?
2104 + Optml_Media_Offload::instance()->rollback_images( 1, [ $id ] ) :
2105 + Optml_Media_Offload::instance()->upload_images( 1, [ $id ] );
1922 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 + }
1923 2112 }
1924 2113
1925 2114 /**
1926 2115 * Start Processing Images
@@ -1925,28 +2114,41 @@
1925 2114 /**
1926 2115 * Start Processing Images
1927 2116 *
1928 2117 * @param string $action The action for which to get the number of images.
1929 - * @param int $batch The batch of images to process.
1930 - * @param int $page The page of images to process.
1931 - * @param int $total The total number of pages.
1932 - * @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.
1933 2123 *
1934 2124 * @return void
1935 2125 */
1936 - public function start_processing_images( $action, $batch, $page, $total, $step ) {
2126 + public function start_processing_images( $action, $batch, $page, $total, $step, $lock_token = '' ) {
1937 2127 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1938 2128 $type = 'offload_images' === $action ? 'offload' : 'rollback';
1939 2129
1940 2130 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
2131 + self::release_transfer_lock( $lock_token );
1941 2132 return;
1942 2133 }
1943 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 +
1944 2140 if ( $step > $total || 0 === $total ) {
1945 2141 $meta = self::get_process_meta();
1946 2142 self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
1947 2143
1948 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 +
1949 2151 return;
1950 2152 }
1951 2153
1952 2154 set_time_limit( 0 );
@@ -1968,9 +2170,9 @@
1968 2170 }
1969 2171
1970 2172 $step = $step + 1;
1971 2173
1972 - wp_schedule_single_event(
2174 + self::schedule_action(
1973 2175 time(),
1974 2176 'optml_start_processing_images',
1975 2177 [
1976 2178 $action,
@@ -1977,16 +2179,18 @@
1977 2179 $batch,
1978 2180 $page,
1979 2181 $total,
1980 2182 $step,
2183 + $lock_token,
1981 2184 ]
1982 2185 );
1983 2186 } catch ( Exception $e ) {
1984 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.
1985 2189 $delay_in_seconds = 10;
1986 2190 self::$instance->logger->add_log( $type, $e->getMessage() );
1987 2191
1988 - wp_schedule_single_event(
2192 + self::schedule_action(
1989 2193 time() + $delay_in_seconds,
1990 2194 'optml_start_processing_images',
1991 2195 [
1992 2196 $action,
@@ -1993,8 +2197,9 @@
1993 2197 $batch,
1994 2198 $page,
1995 2199 $total,
1996 2200 $step,
2201 + $lock_token,
1997 2202 ]
1998 2203 );
1999 2204 }
2000 2205 }
@@ -2002,9 +2207,9 @@
2002 2207 /**
2003 2208 * Alter attachment image src for offloaded images.
2004 2209 *
2005 2210 * @param array|false $image {
2006 - * Array of image data.
2211 + * Array of image data.
2007 2212 *
2008 2213 * @type string $0 Image source URL.
2009 2214 * @type int $1 Image width in pixels.
2010 2215 * @type int $2 Image height in pixels.
@@ -2020,12 +2225,14 @@
2020 2225 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2021 2226 if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2022 2227 return $image;
2023 2228 }
2024 -
2025 - $url = get_post( $attachment_id );
2026 - $url = $url->guid;
2027 - $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 + }
2028 2235 $metadata = wp_get_attachment_metadata( $attachment_id );
2029 2236
2030 2237 // Use the original size if the requested size is full.
2031 2238 if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
@@ -2032,12 +2239,13 @@
2032 2239 $image_url = $this->get_new_offloaded_attachment_url(
2033 2240 $url,
2034 2241 $attachment_id,
2035 2242 [
2036 - 'width' => $metadata['width'],
2037 - 'height' => $metadata['height'],
2243 + 'width' => $metadata['width'],
2244 + 'height' => $metadata['height'],
2038 2245 'attachment_id' => $attachment_id,
2039 - ]
2246 + ],
2247 + $metadata
2040 2248 );
2041 2249
2042 2250 return [
2043 2251 $image_url,
@@ -2046,58 +2254,29 @@
2046 2254 false,
2047 2255 ];
2048 2256 }
2049 2257
2050 - $crop = false;
2051 -
2052 - // Size can be int [] containing width and height.
2053 - if ( is_array( $size ) ) {
2054 - $width = $size[0];
2055 - $height = $size[1];
2056 - $crop = true;
2057 - } else {
2058 - $sizes = $this->get_all_image_sizes();
2059 -
2060 - if ( ! isset( $sizes[ $size ] ) ) {
2061 - return [
2062 - $image_url,
2063 - $metadata['width'],
2064 - $metadata['height'],
2065 - false,
2066 - ];
2067 - }
2068 -
2069 - $width = $sizes[ $size ]['width'];
2070 - $height = $sizes[ $size ]['height'];
2071 - $crop = is_array( $sizes[ $size ]['crop'] ) ? $sizes[ $size ]['crop'] : (bool) $sizes[ $size ]['crop'];
2072 - }
2073 -
2074 - $sizes2crop = self::size_to_crop();
2075 -
2076 - 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 ) ) {
2077 2259 return $image;
2078 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 + );
2079 2273
2080 - $resize = apply_filters( 'optml_default_crop', [] );
2081 - $data = image_get_intermediate_size( $attachment_id, $size );
2082 -
2083 - if ( is_array( $data ) && isset( $data['width'] ) && isset( $data['height'] ) ) { // @phpstan-ignore-line - these both exist.
2084 - if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
2085 - $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
2086 - }
2087 - }
2088 -
2089 - if ( $crop !== false ) {
2090 - $resize = $this->to_optml_crop( $crop );
2091 - }
2092 -
2093 - $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, ['width' => $width, 'height' => $height, 'resize' => $resize, 'attachment_id' => $attachment_id] );
2094 -
2095 2274 return [
2096 2275 $image_url,
2097 - $width,
2098 - $height,
2099 - $crop,
2276 + $sizes['width'],
2277 + $sizes['height'],
2278 + $size === 'full', // @phpstan-ignore-line
2100 2279 ];
2101 2280 }
2102 2281
2103 2282 /**
@@ -2113,21 +2292,22 @@
2113 2292 if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2114 2293 return $response;
2115 2294 }
2116 2295
2117 - $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();
2118 2304
2119 2305 foreach ( $sizes as $size => $args ) {
2120 2306 if ( isset( $response['sizes'][ $size ] ) ) {
2121 2307 continue;
2122 2308 }
2123 -
2124 - $args = [
2125 - 'height' => $args['height'],
2126 - 'width' => $args['width'],
2127 - 'crop' => true,
2128 - ];
2129 -
2309 + $args = $this->size_to_dimension( $size, $meta );
2130 2310 $response['sizes'][ $size ] = array_merge(
2131 2311 $args,
2132 2312 [
2133 2313 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
@@ -2134,17 +2314,10 @@
2134 2314 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2135 2315 ]
2136 2316 );
2137 2317 }
2318 + $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $meta );
2138 2319
2139 - $url_args = [
2140 - 'height' => $response['height'],
2141 - 'width' => $response['width'],
2142 - 'crop' => false,
2143 - ];
2144 -
2145 - $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $url_args );
2146 -
2147 2320 return $response;
2148 2321 }
2149 2322
2150 2323 /**
@@ -2165,18 +2338,18 @@
2165 2338
2166 2339 /**
2167 2340 * Get offloaded image attachment URL for new offloads.
2168 2341 *
2169 - * @param string $url The initial attachment URL.
2170 - * @param int $attachment_id The attachment ID.
2171 - * @param array $args The additional arguments.
2172 - * - width: The width of the image.
2173 - * - height: The height of the image.
2174 - * - crop: Whether to crop the image.
2175 - *
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.
2176 2349 * @return string
2177 2350 */
2178 - 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 ) {
2179 2352 $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2180 2353
2181 2354 // Image might have already passed through this filter.
2182 2355 if ( strpos( $url, $process_flag ) !== false ) {
@@ -2181,10 +2354,13 @@
2181 2354 // Image might have already passed through this filter.
2182 2355 if ( strpos( $url, $process_flag ) !== false ) {
2183 2356 return $url;
2184 2357 }
2185 -
2186 - $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 + }
2187 2363 if ( ! isset( $meta['file'] ) ) {
2188 2364 return $url;
2189 2365 }
2190 2366
@@ -2203,27 +2379,11 @@
2203 2379 }
2204 2380
2205 2381 $file = $meta['file'];
2206 2382 if ( self::is_uploaded_image( $file ) ) {
2207 - $optimized_url = ( new Optml_Image(
2208 - $url,
2209 - [
2210 - 'width' => $args['width'],
2211 - 'height' => $args['height'],
2212 - 'quality' => $this->settings->get_numeric_quality(),
2213 - 'resize' => $args['resize'],
2214 - 'attachment_id' => $attachment_id,
2215 - ],
2216 - $this->settings->get( 'cache_buster' )
2217 - ) )->get_url();
2383 + $optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2218 2384
2219 - if ( strpos( $optimized_url, $process_flag ) !== false ) {
2220 - return $optimized_url;
2221 - }
2222 -
2223 - $process_flag = $process_flag . $file;
2224 -
2225 - 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;
2226 2386 } else {
2227 2387 // this is for the users that already offloaded the images before the other fixes
2228 2388 $local_file = get_attached_file( $attachment_id );
2229 2389 if ( ! file_exists( $local_file ) ) {
@@ -2232,19 +2392,9 @@
2232 2392 foreach ( $duplicated_images as $id ) {
2233 2393 if ( ! empty( $id ) ) {
2234 2394 $duplicated_meta = wp_get_attachment_metadata( $id );
2235 2395 if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2236 - $optimized_url = ( new Optml_Image(
2237 - $url,
2238 - [
2239 - 'width' => $args['width'],
2240 - 'height' => $args['height'],
2241 - 'quality' => $this->settings->get_numeric_quality(),
2242 - 'attachment_id' => $attachment_id,
2243 - ],
2244 - $this->settings->get( 'cache_buster' )
2245 - ) )->get_url();
2246 - return $optimized_url;
2396 + return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2247 2397 }
2248 2398 }
2249 2399 }
2250 2400 }
@@ -2272,8 +2422,11 @@
2272 2422 $attachment = $this->get_local_attachement_id_from_url( $url );
2273 2423
2274 2424 // No local attachment.
2275 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 + }
2276 2429 continue;
2277 2430 }
2278 2431
2279 2432 $attachment_id = $attachment['attachment_id'];
@@ -2298,19 +2451,9 @@
2298 2451 }
2299 2452 }
2300 2453
2301 2454 // Build the optimized URL.
2302 - $optimized_url = ( new Optml_Image(
2303 - $url,
2304 - [
2305 - 'width' => $width,
2306 - 'height' => $height,
2307 - 'quality' => $this->settings->get_numeric_quality(),
2308 - 'resize' => $resize,
2309 - 'attachment_id' => $attachment_id,
2310 - ],
2311 - $this->settings->get( 'cache_buster' )
2312 - ) )->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 );
2313 2456
2314 2457 // Drop any image size from the URL.
2315 2458 $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2316 2459
@@ -2346,8 +2489,21 @@
2346 2489 return $response;
2347 2490 }
2348 2491
2349 2492 /**
2493 + * Legacy function to be used for WordPress versions under 6.0.0.
2494 + *
2495 + * @param array $post_data Slashed, sanitized, processed post data.
2496 + * @param array $postarr Slashed sanitized post data.
2497 + * @param array $unsanitized_postarr Un-sanitized post data.
2498 + *
2499 + * @return array
2500 + */
2501 + public function legacy_filter_saved_data( $post_data, $postarr, $unsanitized_postarr ) {
2502 + return $this->filter_saved_data( $post_data, $postarr, $unsanitized_postarr, true );
2503 + }
2504 +
2505 + /**
2350 2506 * Filter post content to use local attachments when saving offloaded images.
2351 2507 *
2352 2508 * @param array $post_data Slashed, sanitized, processed post data.
2353 2509 * @param array $postarr Slashed sanitized post data.
@@ -2378,12 +2534,21 @@
2378 2534 if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2379 2535 continue;
2380 2536 }
2381 2537
2382 - $replace[ $url ] = self::get_original_url( $id );
2538 + $original = self::get_original_url( $id );
2383 2539
2540 + if ( $original === false ) {
2541 + continue;
2542 + }
2543 +
2544 + $replace[ $url ] = $original;
2545 +
2384 2546 $size = $this->parse_dimension_from_optimized_url( $url );
2385 2547
2548 + if ( $size[0] === false || $size[1] === false ) {
2549 + continue;
2550 + }
2386 2551 if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2387 2552 continue;
2388 2553 }
2389 2554
@@ -2407,9 +2572,9 @@
2407 2572 $width = $size[0];
2408 2573 $height = $size[1];
2409 2574 } else {
2410 2575 // In case of an image size, we need to calculate the new dimensions for the proper file path.
2411 - $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] );
2412 2577
2413 2578 $width = $constrained[0];
2414 2579 $height = $constrained[1];
2415 2580 }
@@ -2428,12 +2593,12 @@
2428 2593 /**
2429 2594 * Alter the image size for the image widget.
2430 2595 *
2431 2596 * @param string $html the attachment image HTML string.
2432 - * @param array $settings Control settings.
2597 + * @param array $settings Control settings.
2433 2598 * @param string $image_size_key Optional. Settings key for image size.
2434 2599 * Default is `image`.
2435 - * @param string $image_key Optional. Settings key for image. Default
2600 + * @param string $image_key Optional. Settings key for image. Default
2436 2601 * is null. If not defined uses image size key
2437 2602 * as the image key.
2438 2603 *
2439 2604 * @return string
@@ -2491,32 +2656,40 @@
2491 2656 add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2492 2657 add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2493 2658
2494 2659 // Needed for rendering beaver builder css properly.
2495 - 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 );
2496 2661
2497 2662 // Filter saved data on insert to use local attachments.
2498 - add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2663 + // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
2664 + $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
2665 + if ( $below_6_0_0 ) {
2666 + add_filter( 'wp_insert_post_data', [ self::$instance, 'legacy_filter_saved_data' ], 10, 3 );
2667 + } else {
2668 + add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 );
2669 + }
2499 2670
2500 2671 // Filter loaded data in the editors to use local attachments.
2501 - add_filter( 'content_edit_pre', [self::$instance, 'replace_urls_in_editor_content'], 10, 1 );
2502 - $types = get_post_types_by_support( 'editor' );
2503 - foreach ( $types as $type ) {
2504 - $post_type = get_post_type_object( $type );
2505 - if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) {
2506 - add_filter(
2507 - 'rest_prepare_' . $type,
2508 - [
2509 - self::$instance,
2510 - 'pre_filter_rest_content',
2511 - ],
2512 - 10,
2513 - 3
2514 - );
2515 - }
2516 - }
2672 + add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2517 2673
2518 - 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 );
2519 2692 add_filter(
2520 2693 'elementor/image_size/get_attachment_image_html',
2521 2694 [
2522 2695 $this,
@@ -2524,9 +2697,8 @@
2524 2697 ],
2525 2698 10,
2526 2699 4
2527 2700 );
2528 -
2529 2701 }
2530 2702
2531 2703 /**
2532 2704 * Elementor checks if the file exists before requesting a specific image size.
@@ -2566,6 +2738,95 @@
2566 2738 public function maybe_strip_scaled( $url ) {
2567 2739 $ext = $this->get_ext( $url );
2568 2740
2569 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'] );
2570 2831 }
2571 2832 }