PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / trunk
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization vtrunk
4.2.15 4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 All 136 releases
← All changes | inc/media_offload.php +1582 -509 3.10.0 → trunk View file →
@@ -5,14 +5,25 @@
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 {
13 22 use Optml_Normalizer;
23 + use Optml_Dam_Offload_Utils;
14 24
25 +
15 26 /**
16 27 * Hold the settings object.
17 28 *
18 29 * @var Optml_Settings Settings object.
@@ -24,15 +35,40 @@
24 35 * @var Optml_Media_Offload
25 36 */
26 37 private static $instance = null;
27 38
39 + /**
40 + * Hold the logger object.
41 + *
42 + * @var Optml_Logger
43 + */
44 + public $logger;
45 +
28 46 const KEYS = [
29 - 'uploaded_flag' => 'id:',
30 - 'not_processed_flag' => 'process:',
47 + 'uploaded_flag' => 'id:',
48 + 'not_processed_flag' => 'process:',
31 49 ];
50 + const META_KEYS = [
51 + 'offloaded' => 'optimole_offload',
52 + 'offload_error' => 'optimole_offload_error',
53 + 'rollback_error' => 'optimole_rollback_error',
54 + ];
55 + const OM_OFFLOADED_FLAG = 'om_image_offloaded';
32 56 const POST_OFFLOADED_FLAG = 'optimole_offload_post';
33 57 const POST_ROLLBACK_FLAG = 'optimole_rollback_post';
58 + const RETRYABLE_META_COUNTER = '_optimole_retryable_errors';
59 +
34 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 + /**
35 71 * Flag used inside wp_get_attachment url filter.
36 72 *
37 73 * @var bool Whether or not to return the original url of the image.
38 74 */
@@ -67,8 +103,9 @@
67 103 * Adds page meta query args
68 104 *
69 105 * @param string $action The action for which the args are needed.
70 106 * @param array $args The initial args without the added meta_query args.
107 + *
71 108 * @return array The args with the added meta_query args.
72 109 */
73 110 public static function add_page_meta_query_args( $action, $args ) {
74 111 if ( $action === 'offload_images' ) {
@@ -74,9 +111,9 @@
74 111 if ( $action === 'offload_images' ) {
75 112 $args['meta_query'] = [
76 113 'relation' => 'AND',
77 114 [
78 - 'key' => self::POST_OFFLOADED_FLAG,
115 + 'key' => self::POST_OFFLOADED_FLAG,
79 116 'compare' => 'NOT EXISTS',
80 117 ],
81 118 ];
82 119 }
@@ -83,20 +120,21 @@
83 120 if ( $action === 'rollback_images' ) {
84 121 $args['meta_query'] = [
85 122 'relation' => 'AND',
86 123 [
87 - 'key' => self::POST_ROLLBACK_FLAG,
124 + 'key' => self::POST_ROLLBACK_FLAG,
88 125 'compare' => 'NOT EXISTS',
89 126 ],
90 127 ];
91 128 if ( self::$is_legacy_install ) {
92 129 $args['meta_query'][] = [
93 - 'key' => self::POST_OFFLOADED_FLAG,
94 - 'value' => 'true',
130 + 'key' => self::POST_OFFLOADED_FLAG,
131 + 'value' => 'true',
95 132 'compare' => '=',
96 133 ];
97 134 }
98 135 }
136 +
99 137 return $args;
100 138 }
101 139
102 140 /**
@@ -105,70 +143,109 @@
105 143 * @return int Number of all images.
106 144 */
107 145 public static function number_of_all_images() {
108 146 $total_images_by_mime = wp_count_attachments( 'image' );
109 - return array_sum( (array) $total_images_by_mime );
147 +
148 + return array_sum( (array) $total_images_by_mime );
110 149 }
150 +
111 151 /**
112 152 * Optml_Media_Offload constructor.
113 153 */
114 154 public static function instance() {
115 - if ( null === self::$instance ||
116 - ( self::$instance->settings !== null && ( ! self::$instance->settings->is_connected()
117 - || self::$instance->settings->get( 'offload_media' ) === 'disabled'
118 - || self::$instance->settings->get( 'cloud_images' ) === 'disabled' ) ) ) {
119 - self::$instance = new self();
155 + if ( null === self::$instance || self::is_phpunit_test() ) {
156 + self::$instance = new self();
120 157 self::$instance->settings = new Optml_Settings();
158 + self::$instance->logger = Optml_Logger::instance();
159 +
121 160 if ( self::$instance->settings->is_connected() ) {
122 161 self::$instance->init();
123 162 }
124 - if ( self::$instance->settings->get( 'offload_media' ) === 'enabled' ) {
125 - add_filter( 'image_downsize', [self::$instance, 'generate_filter_downsize_urls'], 10, 3 );
126 - add_filter( 'wp_generate_attachment_metadata', [self::$instance, 'generate_image_meta'], 10, 2 );
127 - add_filter( 'wp_get_attachment_url', [self::$instance, 'get_image_attachment_url'], -999, 2 );
128 - add_filter( 'wp_insert_post_data', [self::$instance, 'filter_uploaded_images'] );
129 - add_action( 'delete_attachment', [self::$instance, 'delete_attachment_hook'], 10 );
130 - add_filter( 'handle_bulk_actions-upload', [self::$instance, 'bulk_action_handler'], 10, 3 );
131 - add_filter( 'bulk_actions-upload', [self::$instance, 'register_bulk_media_actions'] );
132 - add_filter( 'media_row_actions', [self::$instance, 'add_inline_media_action'], 10, 2 );
133 - add_filter( 'wp_calculate_image_srcset', [self::$instance, 'calculate_image_srcset'], 1, 5 );
134 - add_action( 'post_updated', [self::$instance, 'update_offload_meta'], 10, 3 );
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' ] );
135 168
169 + self::$instance->add_new_actions();
170 +
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 );
178 +
136 179 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
137 180 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
138 181 if ( $below_6_0_0 ) {
139 - 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 );
140 183 } else {
141 - add_filter( 'wp_insert_attachment_data', [self::$instance, 'insert'], 10, 4 );
184 + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 );
142 185 }
143 186
144 - add_action( 'optml_start_processing_images', [self::$instance, 'start_processing_images'], 10, 5 );
145 - add_action( 'optml_start_processing_images_by_id', [self::$instance, 'start_processing_images_by_id'], 10, 4 );
146 -
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' ] );
147 198 if ( self::$is_legacy_install === null ) {
148 199 self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600;
149 200 }
201 + ( new Loader() )->register_hooks();
150 202 }
151 203 }
204 +
152 205 return self::$instance;
153 206 }
154 207
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 + }
155 232
156 233 /**
157 234 * Function for `update_attached_file` filter-hook.
158 235 *
159 - * @param string $file Path to the attached file to update.
236 + * @param string $file Path to the attached file to update.
160 237 * @param int $attachment_id Attachment ID.
161 238 *
162 239 * @return string
163 240 */
164 - function wp_update_attached_file_filter( $file, $attachment_id ) {
241 + public function wp_update_attached_file_filter( $file, $attachment_id ) {
165 242
166 243 if ( OPTML_DEBUG_MEDIA ) {
167 244 do_action( 'optml_log', 'called updated attached' );
168 245 }
169 - $info = pathinfo( $file );
170 - $file_name = basename( $file );
246 + $info = pathinfo( $file );
247 + $file_name = basename( $file );
171 248 $no_ext_file_name = basename( $file, '.' . $info['extension'] );
172 249 // if we have current deduplication set and it contains the filename that is updated
173 250 // we replace the updated filename with the deduplicated filename
174 251 if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) {
@@ -180,9 +257,10 @@
180 257 }
181 258 if ( OPTML_DEBUG_MEDIA ) {
182 259 do_action( 'optml_log', self::$last_deduplicated );
183 260 }
184 - 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 +
185 263 return $file;
186 264 }
187 265
188 266 /**
@@ -193,18 +271,17 @@
193 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
194 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
195 273 * with the deduplicated one which we stored into `$current_file_deduplication` variable
196 274 *
197 - * @param array $data An array of slashed, sanitized, and processed attachment post data.
198 - * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
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.
199 277 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
200 - * @param bool $update Whether this is an existing attachment post being updated.
278 + * @param bool $update Whether this is an existing attachment post being updated.
201 279 *
280 + * @return array
202 281 * @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0.
203 - *
204 - * @return array
205 282 */
206 - function insert( $data, $postarr, $unsanitized_postarr, $update ) {
283 + public function insert( $data, $postarr, $unsanitized_postarr, $update ) {
207 284
208 285 // the post name is unique against the database so not affected by removing the files
209 286 // https://developer.wordpress.org/reference/functions/wp_unique_post_slug/
210 287 if ( OPTML_DEBUG_MEDIA ) {
@@ -215,11 +292,11 @@
215 292 return $data;
216 293 }
217 294
218 295 $filename = wp_basename( $data['guid'] );
219 - $ext = $this->get_ext( $filename );
296 + $ext = $this->get_ext( $filename );
220 297 // skip if the file is not an image
221 - 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 ) ) {
222 299 return $data;
223 300 }
224 301
225 302 // on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg`
@@ -243,9 +320,9 @@
243 320
244 321 $data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] );
245 322 // we store the deduplication to be used and add the filter for updating the attached_file meta
246 323 self::$current_file_deduplication = $to_replace_with;
247 - 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 );
248 325 }
249 326 if ( OPTML_DEBUG_MEDIA ) {
250 327 do_action( 'optml_log', 'data after' );
251 328 do_action( 'optml_log', $data );
@@ -256,15 +333,15 @@
256 333
257 334 /**
258 335 * Wrapper for the `insert` method for WP versions < 6.0.0.
259 336 *
260 - * @param array $data An array of slashed, sanitized, and processed attachment post data.
261 - * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
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.
262 339 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post().
263 340 *
264 341 * @return array
265 342 */
266 - function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
343 + public function insert_legacy( $data, $postarr, $unsanitized_postarr ) {
267 344 return $this->insert( $data, $postarr, $unsanitized_postarr, false );
268 345 }
269 346
270 347 /**
@@ -272,11 +349,11 @@
272 349 *
273 350 * @param int $post_ID Updated post id.
274 351 * @param WP_Post $post_after Post before the update.
275 352 * @param WP_Post $post_before Post after the update.
276 - * @uses action:post_updated
277 353 *
278 354 * @return void
355 + * @uses action:post_updated
279 356 */
280 357 public function update_offload_meta( $post_ID, $post_after, $post_before ) {
281 358 if ( self::$offload_update_post === true ) {
282 359 return;
@@ -287,8 +364,9 @@
287 364
288 365 // revisions are skipped inside the function no need to check them before
289 366 delete_post_meta( $post_ID, self::POST_ROLLBACK_FLAG );
290 367 }
368 +
291 369 /**
292 370 * Get image size name from width and meta.
293 371 *
294 372 * @param array $sizes Image sizes .
@@ -294,29 +372,30 @@
294 372 * @param array $sizes Image sizes .
295 373 * @param integer $width Size width.
296 374 * @param string $filename Image filename.
297 375 *
298 - * @return null|string
376 + * @return null|string|array
299 377 */
300 - public static function get_image_name_from_width( $sizes, $width, $filename ) {
378 + public static function get_image_size_from_width( $sizes, $width, $filename, $just_name = true ) {
301 379 foreach ( $sizes as $name => $size ) {
302 380 if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
303 - return $name;
381 + return $just_name ? $name : array_merge( $size, [ 'name' => $name ] );
304 382 }
305 383 }
306 384
307 385 return null;
308 386 }
387 +
309 388 /**
310 389 * Replace image URLs in the srcset attributes.
311 390 *
312 - * @param array $sources Array of image sources.
313 - * @param array $size_array Array of width and height values in pixels (in that order).
314 - * @param string $image_src The 'src' of the image.
315 - * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
316 - * @param int $attachment_id Image attachment ID.
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.
317 396 *
318 - * @return array
397 + * @return array<int, array{url: string, descriptor: string, value: int}>|mixed
319 398 */
320 399 public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
321 400
322 401 if ( ! is_array( $sources ) ) {
@@ -322,60 +401,127 @@
322 401 if ( ! is_array( $sources ) ) {
323 402 return $sources;
324 403 }
325 404
326 - if ( ! Optml_Media_Offload::is_uploaded_image( $image_src ) || ! isset( $image_meta['file'] ) || ! Optml_Media_Offload::is_uploaded_image( $image_meta['file'] ) ) {
405 + if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
406 + if ( ! Optml_Media_Offload::is_uploaded_image( $image_src ) || ! isset( $image_meta['file'] ) || ! Optml_Media_Offload::is_uploaded_image( $image_meta['file'] ) ) {
407 + return $sources;
408 + }
409 + foreach ( $sources as $width => $source ) {
410 + $filename = wp_basename( $image_meta['file'] );
411 + $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename );
412 + $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
413 +
414 + if ( false === $optimized_url ) {
415 + continue;
416 + }
417 +
418 + $sources[ $width ]['url'] = $optimized_url[0];
419 + }
420 +
327 421 return $sources;
328 422 }
423 +
424 + if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
425 + return $sources;
426 + }
427 +
428 + $requested_width = $size_array[0];
429 + $requested_height = $size_array[1];
430 +
431 + if ( $requested_height < 1 || $requested_width < 1 ) {
432 + return $sources;
433 + }
434 +
435 + $image_sizes = $this->get_all_image_sizes();
436 + $crop = false;
437 +
438 + // Loop through image sizes to make sure we're using the right cropping.
439 + foreach ( $image_sizes as $size_name => $args ) {
440 + if ( $args['width'] !== $requested_width && $args['height'] !== $requested_height ) {
441 + continue;
442 + }
443 +
444 + if ( isset( $args['crop'] ) ) {
445 + $crop = (bool) $args['crop'];
446 + }
447 + }
329 448 foreach ( $sources as $width => $source ) {
330 - $filename = wp_basename( $image_meta['file'] );
331 - $size = $this->get_image_name_from_width( $image_meta['sizes'], $width, $filename );
332 - $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
449 + $filename = ( $image_meta['file'] );
450 + $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename, false );
333 451
452 + if ( $size === null || ! isset( $size['name'] ) ) {
453 + unset( $sources[ $width ] );
454 +
455 + continue;
456 + }
457 +
458 + if ( ! isset( $image_sizes[ $size['name'] ] ) || (bool) $image_sizes[ $size['name'] ]['crop'] !== $crop ) {
459 + unset( $sources[ $width ] );
460 +
461 + continue;
462 + }
463 +
464 + // Some image sizes might have 0 values for width or height.
465 + if ( $size['width'] < 1 || $size['height'] < 1 ) {
466 + unset( $sources[ $width ] );
467 +
468 + continue;
469 + }
470 +
471 + if ( ! wp_image_matches_ratio( $size['width'], $size['height'], $requested_width, $requested_height ) ) {
472 + continue;
473 + }
474 +
475 + $optimized_url = wp_get_attachment_image_src( $attachment_id, $size['name'] );
476 +
334 477 if ( false === $optimized_url ) {
478 + unset( $sources[ $width ] );
479 +
335 480 continue;
336 481 }
482 + $sources[ $width ]['url'] = $optimized_url[0];
483 + }
484 + // Add the requested size to the srcset.
485 + $sources[ $requested_width ] = [
486 + 'url' => $image_src,
487 + 'descriptor' => 'w',
488 + 'value' => $requested_width,
489 + ];
337 490
338 - $sources[ $width ]['url'] = $optimized_url[0];
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 + ];
339 498 }
340 499 return $sources;
341 500 }
342 - /**
343 - * Get the dimension from optimized url.
344 - *
345 - * @param string $url The image url.
346 - * @return array Contains the width and height values in this order.
347 - */
348 - public static function parse_dimension_from_optimized_url( $url ) {
349 - $catch = [];
350 - $height = 'auto';
351 - $width = 'auto';
352 - preg_match( '/\/w:(.*)\/h:(.*)\/q:/', $url, $catch );
353 - if ( isset( $catch[1] ) && isset( $catch[2] ) ) {
354 - $width = $catch[1];
355 - $height = $catch[2];
356 - }
357 - return [$width, $height];
358 - }
359 501
360 502 /**
361 503 * Check if the image is stored on our servers or not.
362 504 *
363 505 * @param string $src Image src or url.
506 + *
364 507 * @return bool Whether image is upload or not.
365 508 */
366 509 public static function is_not_processed_image( $src ) {
367 510 return strpos( $src, self::KEYS['not_processed_flag'] ) !== false;
368 511 }
512 +
369 513 /**
370 514 * Check if the image is stored on our servers or not.
371 515 *
372 516 * @param string $src Image src or url.
517 + *
373 518 * @return bool Whether image is upload or not.
374 519 */
375 520 public static function is_uploaded_image( $src ) {
376 521 return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false;
377 522 }
523 +
378 524 /**
379 525 * Get the attachment ID from the image tag.
380 526 *
381 527 * @param string $image Image tag.
@@ -393,13 +539,15 @@
393 539
394 540 /**
395 541 * Get attachment id from url
396 542 *
397 - * @param string $url The optimized url .
543 + * @param string $url The optimized url .
544 + *
398 545 * @return false|mixed The attachment id .
399 546 */
400 547 public static function get_attachment_id_from_url( $url ) {
401 548 preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
549 +
402 550 return isset( $attachment_id[1] ) ? $attachment_id[1] : false;
403 551 }
404 552
405 553 /**
@@ -405,31 +553,26 @@
405 553 /**
406 554 * Get attachment id from local url
407 555 *
408 556 * @param string $url The url to look for.
557 + *
409 558 * @return array The attachment id and the size from the url.
410 559 */
411 - private function get_local_attachement_id_from_url( $url ) {
560 + public function get_local_attachement_id_from_url( $url ) {
412 561
413 - $size = 'full';
562 + $size = 'full';
414 563 $found_size = $this->parse_dimensions_from_filename( $url );
415 - $strip_url = $url;
416 - $scaled_url = $url;
564 + $url = $this->add_schema( $url );
417 565 if ( $found_size[0] !== false && $found_size[1] !== false ) {
418 566 $size = $found_size;
419 - $strip_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '', $url );
420 - $scaled_url = str_replace( '-' . $found_size[0] . 'x' . $found_size[1], '-scaled', $url );
421 - }
422 - $strip_url = $this->add_schema( $strip_url );
423 567
424 - $attachment_id = attachment_url_to_postid( $strip_url );
425 - if ( $attachment_id === 0 ) {
426 - $scaled_url = $this->add_schema( $scaled_url );
427 - $attachment_id = attachment_url_to_postid( $scaled_url );
428 568 }
569 + $url = $this->add_schema( $url );
570 + $attachment_id = $this->attachment_url_to_post_id( $url );
429 571
430 572 return [ 'attachment_id' => $attachment_id, 'size' => $size ];
431 573 }
574 +
432 575 /**
433 576 * Filter out the urls that are saved to our servers when saving to the DB.
434 577 *
435 578 * @param array $data The post data array to save.
@@ -443,9 +586,9 @@
443 586 if ( OPTML_DEBUG_MEDIA ) {
444 587 do_action( 'optml_log', 'content to update' );
445 588 do_action( 'optml_log', $content );
446 589 }
447 - $images = Optml_Manager::instance()->extract_urls_from_content( $content );
590 + $images = Optml_Manager::instance()->extract_urls_from_content( $content );
448 591 if ( ! isset( $images[0] ) ) {
449 592 return $data;
450 593 }
451 594 if ( OPTML_DEBUG_MEDIA ) {
@@ -453,10 +596,10 @@
453 596 do_action( 'optml_log', $images );
454 597 }
455 598 foreach ( $images as $url ) {
456 599 $is_original_uploaded = self::is_uploaded_image( $url );
457 - $attachment_id = false;
458 - $size = 'thumbnail';
600 + $attachment_id = false;
601 + $size = 'thumbnail';
459 602 if ( $is_original_uploaded ) {
460 603 $found_size = $this->parse_dimension_from_optimized_url( $url );
461 604 if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) {
462 605 $size = $found_size;
@@ -462,11 +605,11 @@
462 605 $size = $found_size;
463 606 }
464 607 $attachment_id = self::get_attachment_id_from_url( $url );
465 608 } else {
466 - $id_and_size = $this->get_local_attachement_id_from_url( $url );
609 + $id_and_size = $this->get_local_attachement_id_from_url( $url );
467 610 $attachment_id = $id_and_size['attachment_id'];
468 - $size = $id_and_size['size'];
611 + $size = $id_and_size['size'];
469 612 }
470 613
471 614 if ( OPTML_DEBUG_MEDIA ) {
472 615 do_action( 'optml_log', 'image id and found size' );
@@ -472,9 +615,9 @@
472 615 do_action( 'optml_log', 'image id and found size' );
473 616 do_action( 'optml_log', $attachment_id );
474 617 do_action( 'optml_log', $size );
475 618 }
476 - if ( false === $attachment_id || ! wp_attachment_is_image( $attachment_id ) ) {
619 + if ( false === $attachment_id || ! $this->is_legacy_offloaded_attachment( $attachment_id ) || ! wp_attachment_is_image( $attachment_id ) ) {
477 620 continue;
478 621 }
479 622 $optimized_url = wp_get_attachment_image_src( $attachment_id, $size );
480 623 if ( OPTML_DEBUG_MEDIA ) {
@@ -490,8 +633,9 @@
490 633 }
491 634 $content = str_replace( $url, $optimized_url[0], $content );
492 635 }
493 636 $data['post_content'] = wp_slash( $content );
637 +
494 638 return $data;
495 639 }
496 640
497 641 /**
@@ -498,18 +642,19 @@
498 642 * Get all images that need to be updated from a post.
499 643 *
500 644 * @param string $post_content The content of the post.
501 645 * @param string $job The job name.
646 + *
502 647 * @return array An array containing the image ids.
503 648 */
504 649 public function get_image_id_from_content( $post_content, $job ) {
505 - $content = trim( wp_unslash( $post_content ) );
506 - $images = Optml_Manager::instance()->extract_urls_from_content( $content );
650 + $content = trim( wp_unslash( $post_content ) );
651 + $images = Optml_Manager::instance()->extract_urls_from_content( $content );
507 652 $found_images = [];
508 653 if ( isset( $images[0] ) ) {
509 654 foreach ( $images as $url ) {
510 655 $is_original_uploaded = self::is_uploaded_image( $url );
511 - $attachment_id = false;
656 + $attachment_id = false;
512 657 if ( $is_original_uploaded ) {
513 658 if ( $job === 'rollback_images' ) {
514 659 $attachment_id = self::get_attachment_id_from_url( $url );
515 660 }
@@ -514,9 +659,9 @@
514 659 $attachment_id = self::get_attachment_id_from_url( $url );
515 660 }
516 661 } else {
517 662 if ( $job === 'offload_images' ) {
518 - $id_and_size = $this->get_local_attachement_id_from_url( $url );
663 + $id_and_size = $this->get_local_attachement_id_from_url( $url );
519 664 $attachment_id = $id_and_size['attachment_id'];
520 665 }
521 666 }
522 667 if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) {
@@ -524,8 +669,9 @@
524 669 }
525 670 $found_images[] = intval( $attachment_id );
526 671 }
527 672 }
673 +
528 674 return apply_filters( 'optml_content_images_to_update', $found_images, $content );
529 675 }
530 676
531 677 /**
@@ -544,12 +690,13 @@
544 690 }
545 691 $post_types = array_values(
546 692 array_filter(
547 693 get_post_types(),
548 - function( $post_type ) {
694 + function ( $post_type ) {
549 695 if ( $post_type === 'attachment' || $post_type === 'revision' ) {
550 696 return false;
551 697 }
698 +
552 699 return true;
553 700 }
554 701 )
555 702 );
@@ -554,10 +701,13 @@
554 701 )
555 702 );
556 703 $query_args = apply_filters(
557 704 'optml_replacement_wp_query_args',
558 - ['post_type' => $post_types, 'post_status' => 'any', 'fields' => 'ids',
559 - 'posts_per_page' => $batch,
705 + [
706 + 'post_type' => $post_types,
707 + 'post_status' => 'any',
708 + 'fields' => 'ids',
709 + 'posts_per_page' => $batch,
560 710 'update_post_meta_cache' => true,
561 711 'update_post_term_cache' => false,
562 712 ]
563 713 );
@@ -580,12 +730,12 @@
580 730 if ( get_post_type() !== 'attachment' ) {
581 731 $ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job );
582 732 if ( count( $ids ) > 0 ) {
583 733 $images_to_update[ $content_id ] = $ids;
584 - $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
734 + $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id );
585 735 if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) {
586 736 foreach ( $duplicated_pages as $duplicated_id ) {
587 - $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 );
588 738 $images_to_update[ $duplicated_id ] = $duplicated_ids;
589 739 }
590 740 }
591 741 }
@@ -598,19 +748,21 @@
598 748 delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG );
599 749 }
600 750 }
601 751 }
602 - $page ++;
752 + ++$page;
603 753 }
604 - $result['page'] = $page;
754 + $result['page'] = $page;
605 755 $result['imagesToUpdate'] = $images_to_update;
756 +
606 757 return $result;
607 758 }
759 +
608 760 /**
609 761 * Add inline action to push to our servers.
610 762 *
611 763 * @param array $actions All actions.
612 - * @param \WP_Post $post The current post image object.
764 + * @param \WP_Post $post The current post image object.
613 765 *
614 766 * @return array
615 767 */
616 768 public function add_inline_media_action( $actions, $post ) {
@@ -621,47 +773,28 @@
621 773 $file = $meta['file'];
622 774 if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) {
623 775 return $actions;
624 776 }
625 - if ( ! self::is_uploaded_image( $file ) ) {
626 - $upload_action_url = add_query_arg(
627 - [
628 - 'page' => 'optimole',
629 - 'optimole_action' => 'offload_images',
630 - '0' => $post->ID,
631 - ],
632 - 'admin.php'
633 - );
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 + );
634 788
635 - $actions['offload_images'] = sprintf(
636 - '<a href="%s" aria-label="%s">%s</a>',
637 - $upload_action_url,
638 - esc_attr__( 'Offload to Optimole', 'optimole-wp' ),
639 - esc_html__( 'Offload to Optimole', 'optimole-wp' )
640 - );
641 - }
642 - if ( self::is_uploaded_image( $file ) ) {
643 - $rollback_action_url = add_query_arg(
644 - [
645 - 'page' => 'optimole',
646 - 'optimole_action' => 'rollback_images',
647 - '0' => $post->ID,
648 - ],
649 - 'admin.php'
650 - );
651 - $actions['rollback_images'] = sprintf(
652 - '<a href="%s" aria-label="%s">%s</a>',
653 - $rollback_action_url,
654 - esc_attr__( 'Restore image to media library', 'optimole-wp' ),
655 - esc_html__( 'Restore image to media library', 'optimole-wp' )
656 - );
657 - }
658 789 return $actions;
659 790 }
791 +
660 792 /**
661 793 * Upload images to our servers and update inside pages.
662 794 *
663 795 * @param array $image_ids The id of the attachments for the selected images.
796 + *
664 797 * @return int The number of successfully processed images.
665 798 */
666 799 public function upload_and_update_existing_images( $image_ids ) {
667 800 $success_up = 0;
@@ -671,16 +804,17 @@
671 804 }
672 805 foreach ( $image_ids as $id ) {
673 806 if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) {
674 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
675 - update_post_meta( $id, 'optimole_offload', 'true' );
676 - $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;
677 811 continue;
678 812 }
679 813
680 814 $meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id );
681 815 if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) {
682 - $success_up ++;
816 + ++$success_up;
683 817 wp_update_attachment_metadata( $id, $meta );
684 818 }
685 819 }
686 820 if ( $success_up > 0 ) {
@@ -688,8 +822,9 @@
688 822 do_action( 'optml_log', ' call post update, succesful images: ' );
689 823 do_action( 'optml_log', $success_up );
690 824 }
691 825 }
826 +
692 827 return $success_up;
693 828 }
694 829
695 830 /**
@@ -695,20 +830,24 @@
695 830 /**
696 831 * Return the original url of an image attachment.
697 832 *
698 833 * @param integer $post_id Image attachment id.
834 + *
699 835 * @return string|bool The original url of the image.
700 836 */
701 837 public static function get_original_url( $post_id ) {
702 838 self::$return_original_url = true;
703 - $original_url = wp_get_attachment_url( $post_id );
839 + $original_url = wp_get_attachment_url( $post_id );
704 840 self::$return_original_url = false;
841 +
705 842 return $original_url;
706 843 }
844 +
707 845 /**
708 846 * Bring images back to media library and update inside pages.
709 847 *
710 848 * @param array $image_ids The id of the attachments for the selected images.
849 + *
711 850 * @return int The number of successfully processed images.
712 851 */
713 852 public function rollback_and_update_images( $image_ids ) {
714 853 $success_back = 0;
@@ -718,56 +857,63 @@
718 857 }
719 858
720 859 foreach ( $image_ids as $id ) {
721 860 // Skip DAM attachment filtering.
722 - if ( ! empty( get_post_meta( $id, Optml_Dam::OM_DAM_IMPORTED_FLAG, true ) ) ) {
861 + if ( $this->is_dam_imported_image( $id ) ) {
723 862 continue;
724 863 }
725 864 $current_meta = wp_get_attachment_metadata( $id );
726 865 if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) {
727 - delete_post_meta( $id, 'optimole_offload' );
728 - $success_back++;
866 + delete_post_meta( $id, self::META_KEYS['offloaded'] );
867 + delete_post_meta( $id, self::OM_OFFLOADED_FLAG );
868 + ++$success_back;
729 869 continue;
730 870 }
731 - $table_id = [];
732 - $filename = pathinfo( $current_meta['file'], PATHINFO_BASENAME );
733 - preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $table_id );
734 - if ( ! isset( $table_id[1] ) ) {
871 +
872 + // Account for scaled images.
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 ) {
735 878 continue;
736 879 }
737 - $table_id = $table_id[1];
880 +
738 881 if ( OPTML_DEBUG_MEDIA ) {
739 882 do_action( 'optml_log', ' image cloud id ' );
740 - do_action( 'optml_log', $table_id );
883 + do_action( 'optml_log', $image_id );
741 884 }
742 - $request = new Optml_Api();
743 - $get_response = $request->call_upload_api( '', 'false', $table_id, 'false', 'true' );
744 885
745 - if ( is_wp_error( $get_response ) || wp_remote_retrieve_response_code( $get_response ) !== 200 ) {
746 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
886 + $image_url = Optimole::offload()->getImageUrl( $image_id );
887 +
888 + if ( null === $image_url ) {
889 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
747 890 if ( OPTML_DEBUG_MEDIA ) {
748 891 do_action( 'optml_log', ' error get url' );
749 892 }
893 +
894 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' );
895 +
750 896 continue;
751 897 }
752 898
753 - $get_url = json_decode( $get_response['body'], true )['getUrl'];
754 -
755 899 if ( ! function_exists( 'download_url' ) ) {
756 900 include_once ABSPATH . 'wp-admin/includes/file.php';
757 901 }
758 902 if ( ! function_exists( 'download_url' ) ) {
759 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
903 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
760 904 continue;
761 905 }
762 906 $timeout_seconds = 60;
763 - $temp_file = download_url( $get_url, $timeout_seconds );
907 + $temp_file = download_url( $image_url, $timeout_seconds );
764 908
765 909 if ( is_wp_error( $temp_file ) ) {
766 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
910 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
767 911 if ( OPTML_DEBUG_MEDIA ) {
768 912 do_action( 'optml_log', ' download_url error ' );
769 913 }
914 +
915 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error downloading URL.' );
770 916 continue;
771 917 }
772 918
773 919 $extension = $this->get_ext( $filename );
@@ -776,9 +922,11 @@
776 922 if ( OPTML_DEBUG_MEDIA ) {
777 923 do_action( 'optml_log', ' image has invalid extension' );
778 924 do_action( 'optml_log', $extension );
779 925 }
780 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
926 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
927 +
928 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has invalid extension.' );
781 929 continue;
782 930 }
783 931
784 932 $type = Optml_Config::$image_extensions [ $extension ];
@@ -791,12 +939,12 @@
791 939 ];
792 940
793 941 $overrides = [
794 942 // do not expect the default form data from normal uploads
795 - 'test_form' => false,
943 + 'test_form' => false,
796 944
797 945 // Setting this to false lets WordPress allow empty files, not recommended.
798 - 'test_size' => true,
946 + 'test_size' => true,
799 947
800 948 // A properly uploaded file will pass this test. There should be no reason to override this one.
801 949 'test_upload' => true,
802 950 ];
@@ -804,19 +952,21 @@
804 952 if ( ! function_exists( 'wp_handle_sideload' ) ) {
805 953 include_once ABSPATH . '/wp-admin/includes/file.php';
806 954 }
807 955 if ( ! function_exists( 'wp_handle_sideload' ) ) {
808 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
956 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
809 957 continue;
810 958 }
811 959
812 960 // Move the temporary file into the uploads directory.
813 - $results = wp_handle_sideload( $file, $overrides );
961 + $results = wp_handle_sideload( $file, $overrides, get_the_date( 'Y/m', $id ) );
814 962 if ( ! empty( $results['error'] ) ) {
815 963 if ( OPTML_DEBUG_MEDIA ) {
816 964 do_action( 'optml_log', ' wp_handle_sideload error' );
817 965 }
818 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
966 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
967 +
968 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' faced wp_handle_sideload error.' );
819 969 continue;
820 970 }
821 971
822 972 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
@@ -822,9 +972,9 @@
822 972 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
823 973 include_once ABSPATH . '/wp-admin/includes/image.php';
824 974 }
825 975 if ( ! function_exists( 'wp_create_image_subsizes' ) ) {
826 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
976 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
827 977 continue;
828 978 }
829 979 $original_meta = wp_create_image_subsizes( $results['file'], $id );
830 980 if ( $type === 'image/svg+xml' ) {
@@ -831,14 +981,14 @@
831 981 if ( ! function_exists( 'wp_get_attachment_metadata' ) || ! function_exists( 'wp_update_attachment_metadata' ) ) {
832 982 include_once ABSPATH . '/wp-admin/includes/post.php';
833 983 }
834 984 if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
835 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
985 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
836 986 continue;
837 987 }
838 988 $meta = wp_get_attachment_metadata( $id );
839 989 if ( ! isset( $meta['file'] ) ) {
840 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
990 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
841 991 continue;
842 992 }
843 993 $meta['file'] = $results['file'];
844 994 wp_update_attachment_metadata( $id, $meta );
@@ -847,12 +997,13 @@
847 997 if ( ! function_exists( 'update_attached_file' ) ) {
848 998 include_once ABSPATH . '/wp-admin/includes/post.php';
849 999 }
850 1000 if ( ! function_exists( 'update_attached_file' ) ) {
851 - update_post_meta( $id, 'optimole_rollback_error', 'true' );
1001 + update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' );
852 1002 continue;
853 1003 }
854 1004 update_attached_file( $id, $results['file'] );
1005 +
855 1006 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id );
856 1007 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
857 1008 foreach ( $duplicated_images as $duplicated_id ) {
858 1009 $duplicated_meta = wp_get_attachment_metadata( $duplicated_id );
@@ -865,18 +1016,22 @@
865 1016 }
866 1017 }
867 1018 }
868 1019 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
869 - delete_post_meta( $duplicated_id, 'optimole_offload' );
1020 + delete_post_meta( $duplicated_id, self::META_KEYS['offloaded'] );
1021 + delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG );
870 1022 }
871 1023 }
872 1024 }
873 - $success_back++;
874 - $original_url = self::get_original_url( $id );
1025 + ++$success_back;
1026 +
1027 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' );
1028 +
1029 + $original_url = self::get_original_url( $id );
875 1030 if ( $original_url === false ) {
876 1031 continue;
877 1032 }
878 - $this->delete_attachment_from_server( $original_url, $id, $table_id );
1033 + $this->delete_attachment_from_server( $original_url, $id, $image_id );
879 1034 }
880 1035
881 1036 if ( $success_back > 0 ) {
882 1037 if ( OPTML_DEBUG_MEDIA ) {
@@ -883,8 +1038,9 @@
883 1038 do_action( 'optml_log', ' call update post, success rollback' );
884 1039 do_action( 'optml_log', $success_back );
885 1040 }
886 1041 }
1042 +
887 1043 return $success_back;
888 1044 }
889 1045
890 1046 /**
@@ -889,11 +1045,12 @@
889 1045
890 1046 /**
891 1047 * Handle the bulk actions.
892 1048 *
893 - * @param string $redirect The current url from the media library.
894 - * @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.
895 1051 * @param array $image_ids The id of the attachments for the selected images.
1052 + *
896 1053 * @return string The url with the correspondent query args for the executed actions.
897 1054 */
898 1055 public function bulk_action_handler( $redirect, $doaction, $image_ids ) {
899 1056
@@ -901,27 +1058,29 @@
901 1058 return $redirect;
902 1059 }
903 1060
904 1061 $image_ids = array_slice( $image_ids, 0, 20, true );
905 - $redirect = 'admin.php';
906 - $redirect = add_query_arg( 'optimole_action', $doaction, $redirect );
907 - $redirect = add_query_arg( 'page', 'optimole', $redirect );
908 - $redirect = add_query_arg( $image_ids, $redirect );
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 +
909 1067 return $redirect;
1068 + }
910 1069
911 - }
912 1070 /**
913 1071 * Register the bulk media actions.
914 1072 *
915 - * @param array $bulk_array The existing actions array.
916 - * @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.
917 1076 */
918 1077 public function register_bulk_media_actions( $bulk_array ) {
919 1078
920 - $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
1079 + $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' );
921 1080 $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' );
1081 +
922 1082 return $bulk_array;
923 -
924 1083 }
925 1084
926 1085 /**
927 1086 * Send delete request to our servers and update the meta.
@@ -927,19 +1086,17 @@
927 1086 * Send delete request to our servers and update the meta.
928 1087 *
929 1088 * @param string $original_url Original url of the image.
930 1089 * @param integer $post_id Image id inside db.
931 - * @param string $table_id Our cloud id for the image.
1090 + * @param string $image_id Our cloud id for the image.
932 1091 */
933 - public function delete_attachment_from_server( $original_url, $post_id, $table_id ) {
934 - $request = new Optml_Api();
935 - $delete_response = $request->call_upload_api( $original_url, 'true', $table_id );
1092 + public function delete_attachment_from_server( $original_url, $post_id, $image_id ) {
1093 + Optimole::offload()->deleteImage( $image_id );
936 1094
937 - delete_post_meta( $post_id, 'optimole_offload' );
938 - if ( is_wp_error( $delete_response ) || wp_remote_retrieve_response_code( $delete_response ) !== 200 ) {
939 - // should add some routine to retry delete once if delete fails
940 - }
1095 + delete_post_meta( $post_id, self::META_KEYS['offloaded'] );
1096 + delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG );
941 1097 }
1098 +
942 1099 /**
943 1100 * Delete an image from our servers after it is removed from media.
944 1101 *
945 1102 * @param int $post_id The deleted post id.
@@ -950,16 +1107,19 @@
950 1107 return;
951 1108 }
952 1109
953 1110 // Skip if the image was imported from cloud library.
954 - if ( $this->is_dam_import( $post_id ) ) {
1111 + if ( $this->is_dam_imported_image( $post_id ) ) {
955 1112 return;
956 1113 }
957 1114
1115 + if ( ! $this->is_new_offloaded_attachment( $post_id ) && ! $this->is_legacy_offloaded_attachment( $post_id ) ) {
1116 + return;
1117 + }
1118 +
958 1119 $file = $file['file'];
959 - if ( self::is_uploaded_image( $file ) ) {
960 -
961 - $original_url = self::get_original_url( $post_id );
1120 + if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) {
1121 + $original_url = self::get_original_url( $post_id );
962 1122 if ( $original_url === false ) {
963 1123 return;
964 1124 }
965 1125 $table_id = [];
@@ -977,8 +1137,9 @@
977 1137 * Get optimized URL for an attachment image if it is uploaded to our servers.
978 1138 *
979 1139 * @param string $url The current url.
980 1140 * @param int $attachment_id The attachment image id.
1141 + *
981 1142 * @return string Optimole cdn URL.
982 1143 * @uses filter:wp_get_attachment_url
983 1144 */
984 1145 public function get_image_attachment_url( $url, $attachment_id ) {
@@ -984,94 +1145,109 @@
984 1145 public function get_image_attachment_url( $url, $attachment_id ) {
985 1146 if ( self::$return_original_url === true ) {
986 1147 return $url;
987 1148 }
988 - $meta = wp_get_attachment_metadata( $attachment_id );
989 - if ( ! isset( $meta['file'] ) ) {
990 - return $url;
991 - }
992 1149
993 - // Skip DAM attachment filtering.
994 - if ( $this->is_dam_import( $attachment_id ) ) {
995 - return $url;
996 - }
1150 + if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
1151 + $meta = wp_get_attachment_metadata( $attachment_id );
1152 + if ( ! isset( $meta['file'] ) ) {
1153 + return $url;
1154 + }
997 1155
998 - $file = $meta['file'];
999 - if ( self::is_uploaded_image( $file ) ) {
1000 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1001 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $optimized_url );
1002 - } else {
1003 - // this is for the users that already offloaded the images before the other fixes
1004 - $local_file = get_attached_file( $attachment_id );
1005 - if ( ! file_exists( $local_file ) ) {
1006 - $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1007 - if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1008 - foreach ( $duplicated_images as $id ) {
1009 - if ( ! empty( $id ) ) {
1010 - $duplicated_meta = wp_get_attachment_metadata( $id );
1011 - if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1012 - $optimized_url = ( new Optml_Image( $url, ['width' => 'auto', 'height' => 'auto', 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1013 - return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $optimized_url );
1156 + // Skip DAM attachment filtering.
1157 + if ( $this->is_dam_imported_image( $attachment_id ) ) {
1158 + return $url;
1159 + }
1160 +
1161 + $file = $meta['file'];
1162 + if ( self::is_uploaded_image( $file ) ) {
1163 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1164 + } else {
1165 + // this is for the users that already offloaded the images before the other fixes
1166 + $local_file = get_attached_file( $attachment_id );
1167 + if ( ! file_exists( $local_file ) ) {
1168 + $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1169 + if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1170 + foreach ( $duplicated_images as $id ) {
1171 + if ( ! empty( $id ) ) {
1172 + $duplicated_meta = wp_get_attachment_metadata( $id );
1173 + if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
1174 + return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $this->get_optimized_image_url( $url, 'auto', 'auto' ) );
1175 + }
1014 1176 }
1015 1177 }
1016 1178 }
1017 1179 }
1018 1180 }
1181 + return $url;
1019 1182 }
1020 - return $url;
1183 +
1184 + if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1185 + return $url;
1186 + }
1187 +
1188 + return $this->get_new_offloaded_attachment_url( $url, $attachment_id );
1021 1189 }
1190 +
1022 1191 /**
1023 1192 * Filter the requested image url.
1024 1193 *
1025 - * @param bool|array $image The previous image value (null).
1194 + * @param bool|array $image The previous image value (null).
1026 1195 * @param int $attachment_id The ID of the attachment.
1027 - * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1196 + * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order).
1028 1197 *
1029 1198 * @return bool|array The image sizes and optimized url.
1030 1199 * @uses filter:image_downsize
1031 1200 */
1032 1201 public function generate_filter_downsize_urls( $image, $attachment_id, $size ) {
1033 - if ( self::$return_original_url === true ) {
1202 + if ( $this->is_dam_imported_image( $attachment_id ) ) {
1034 1203 return $image;
1035 1204 }
1036 1205
1037 - if ( $this->is_dam_import( $attachment_id ) ) {
1038 - return $image;
1206 + if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) {
1207 + if ( self::$return_original_url === true ) {
1208 + return $image;
1209 + }
1210 +
1211 + $sizes2crop = self::size_to_crop();
1212 + if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
1213 + return $image;
1214 + }
1215 + $data = image_get_intermediate_size( $attachment_id, $size );
1216 + if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) {
1217 + return $image;
1218 + }
1219 + $resize = apply_filters( 'optml_default_crop', [] );
1220 + if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
1221 + $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
1222 + }
1223 + $id_filename = [];
1224 +
1225 + preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename );
1226 + if ( ! isset( $id_filename[1] ) ) {
1227 + return $image;
1228 + }
1229 + $url = self::get_original_url( $attachment_id );
1230 +
1231 + return [
1232 + str_replace( $url, $id_filename[1], $this->get_optimized_image_url( $url, $data['width'], $data['height'], $resize ) ),
1233 + $data['width'],
1234 + $data['height'],
1235 + true,
1236 + ];
1039 1237 }
1040 1238
1041 - $sizes2crop = self::size_to_crop();
1042 - if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) {
1239 + if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
1043 1240 return $image;
1044 1241 }
1045 - $data = image_get_intermediate_size( $attachment_id, $size );
1046 - if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) {
1047 - return $image;
1048 - }
1049 - $resize = apply_filters( 'optml_default_crop', [] );
1050 - if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) {
1051 - $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] );
1052 - }
1053 - $id_filename = [];
1054 1242
1055 - preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename );
1056 - if ( ! isset( $id_filename[1] ) ) {
1057 - return $image;
1058 - }
1059 - $url = self::get_original_url( $attachment_id );
1060 - $optimized_url = ( new Optml_Image( $url, ['width' => $data['width'], 'height' => $data['height'], 'resize' => $resize, 'quality' => $this->settings->get_numeric_quality()], $this->settings->get( 'cache_buster' ) ) )->get_url();
1061 - $optimized_url = str_replace( $url, $id_filename[1], $optimized_url );
1062 - $image = [
1063 - $optimized_url,
1064 - $data['width'],
1065 - $data['height'],
1066 - true,
1067 - ];
1068 - return $image;
1243 + return $this->alter_attachment_image_src( $image, $attachment_id, $size, false );
1069 1244 }
1245 +
1070 1246 /**
1071 1247 * Get image extension.
1072 1248 *
1073 - * @param string $path Image path.
1249 + * @param string $path Image path.
1074 1250 *
1075 1251 * @return string
1076 1252 */
1077 1253 private function get_ext( $path ) {
@@ -1076,12 +1252,36 @@
1076 1252 */
1077 1253 private function get_ext( $path ) {
1078 1254 return pathinfo( $path, PATHINFO_EXTENSION );
1079 1255 }
1256 +
1080 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 + /**
1081 1281 * Update image meta with optimized cdn path.
1082 1282 *
1083 - * @param array $meta Meta information of the image.
1283 + * @param array $meta Meta information of the image.
1084 1284 * @param int $attachment_id The image attachment ID.
1085 1285 *
1086 1286 * @return array
1087 1287 * @uses filter:wp_generate_attachment_metadata
@@ -1087,42 +1287,58 @@
1087 1287 * @uses filter:wp_generate_attachment_metadata
1088 1288 */
1089 1289 public function generate_image_meta( $meta, $attachment_id ) {
1090 1290
1091 - if ( $this->is_dam_import( $attachment_id ) ) {
1291 + if ( $this->is_dam_imported_image( $attachment_id ) ) {
1092 1292 return $meta;
1093 1293 }
1094 1294
1295 + if ( self::$instance->settings->is_offload_limit_reached() ) {
1296 + return $meta;
1297 + }
1298 +
1095 1299 if ( OPTML_DEBUG_MEDIA ) {
1096 1300 do_action( 'optml_log', 'called generate meta' );
1097 1301 }
1302 + // No meta, or image was already uploaded.
1098 1303 if ( ! isset( $meta['file'] ) || ! isset( $meta['width'] ) || ! isset( $meta['height'] ) || self::is_uploaded_image( $meta['file'] ) ) {
1099 1304 do_action( 'optml_log', 'invalid meta' );
1100 1305 do_action( 'optml_log', $meta );
1101 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1306 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1307 +
1308 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' );
1309 +
1102 1310 return $meta;
1103 1311 }
1312 + // Skip images based on filters.
1104 1313 if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) {
1105 1314 do_action( 'optml_log', 'optimization filter' );
1106 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1315 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1316 +
1107 1317 return $meta;
1108 1318 }
1109 - $original_url = self::get_original_url( $attachment_id );
1319 + $original_url = self::get_original_url( $attachment_id );
1110 1320
1321 + // Could not find original URL.
1111 1322 if ( $original_url === false ) {
1112 1323 do_action( 'optml_log', 'error getting original url' );
1113 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1324 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1325 +
1326 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' );
1327 +
1114 1328 return $meta;
1115 1329 }
1116 1330
1117 - $local_file = get_attached_file( $attachment_id );
1331 + // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images.
1332 + $original_url = $this->maybe_strip_scaled( $original_url );
1333 + $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) );
1118 1334
1119 - $extension = $this->get_ext( $local_file );
1120 - $content_type = Optml_Config::$image_extensions [ $extension ];
1121 - $temp = explode( '/', $local_file );
1122 - $file_name = end( $temp );
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 );
1123 1339 $no_ext_filename = str_replace( '.' . $extension, '', $file_name );
1124 - $original_name = $file_name;
1340 + $original_name = $file_name;
1125 1341 if ( OPTML_DEBUG_MEDIA ) {
1126 1342 do_action( 'optml_log', 'file before replace' );
1127 1343 do_action( 'optml_log', $local_file );
1128 1344 }
@@ -1129,10 +1345,10 @@
1129 1345
1130 1346 // check if the current filename is the last deduplicated filename
1131 1347 if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) {
1132 1348 // replace the file with the original before deduplication to get the path where the image is uploaded
1133 - $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1134 - $original_name = self::$last_deduplicated;
1349 + $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file );
1350 + $original_name = self::$last_deduplicated;
1135 1351 self::$last_deduplicated = false;
1136 1352 }
1137 1353 if ( OPTML_DEBUG_MEDIA ) {
1138 1354 do_action( 'optml_log', 'file after replace' );
@@ -1138,113 +1354,157 @@
1138 1354 do_action( 'optml_log', 'file after replace' );
1139 1355 do_action( 'optml_log', $local_file );
1140 1356 }
1141 1357 if ( ! file_exists( $local_file ) ) {
1142 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1358 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1143 1359 do_action( 'optml_log', 'missing file' );
1144 1360 do_action( 'optml_log', $local_file );
1361 +
1362 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' );
1363 +
1145 1364 return $meta;
1146 1365 }
1147 1366
1148 1367 if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) {
1149 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1368 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1150 1369 do_action( 'optml_log', 'invalid extension' );
1151 1370 do_action( 'optml_log', $extension );
1371 +
1372 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' );
1373 +
1152 1374 return $meta;
1153 1375 }
1154 1376 if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) {
1155 1377 do_action( 'optml_log', 'extension filter' );
1156 1378 do_action( 'optml_log', $extension );
1157 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1379 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1380 +
1158 1381 return $meta;
1159 1382 }
1160 1383
1161 - $request = new Optml_Api();
1162 - $generate_url_response = $request->call_upload_api( $original_url );
1384 + $offload_manager = Optimole::offload();
1385 + $offload_usage = $offload_manager->getUsage();
1163 1386
1164 - 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() ) {
1165 1391 if ( OPTML_DEBUG_MEDIA ) {
1166 - do_action( 'optml_log', ' call to signed url error' );
1167 - do_action( 'optml_log', $generate_url_response );
1392 + do_action( 'optml_log', 'limit exceeded' );
1393 + do_action( 'optml_log', $offload_usage );
1168 1394 }
1169 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1395 +
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 +
1170 1400 return $meta;
1171 1401 }
1172 - $decoded_response = json_decode( $generate_url_response['body'], true );
1173 1402
1174 - if ( ! isset( $decoded_response['tableId'] ) || ! isset( $decoded_response['uploadUrl'] ) ) {
1403 + try {
1404 + $image_id = $offload_manager->uploadImage( $local_file, $original_url );
1405 +
1175 1406 if ( OPTML_DEBUG_MEDIA ) {
1176 - do_action( 'optml_log', ' missing table id or upload url' );
1177 - do_action( 'optml_log', $decoded_response );
1407 + do_action( 'optml_log', 'image id' );
1408 + do_action( 'optml_log', $image_id );
1178 1409 }
1179 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
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 +
1419 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1420 +
1421 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' file is missing or unreadable.' );
1422 +
1180 1423 return $meta;
1181 - }
1182 - $table_id = $decoded_response['tableId'];
1183 - if ( OPTML_DEBUG_MEDIA ) {
1184 - do_action( 'optml_log', ' table id' );
1185 - do_action( 'optml_log', $table_id );
1186 - }
1187 - $upload_signed_url = $decoded_response['uploadUrl'];
1188 - $image = file_get_contents( $local_file );
1189 - if ( $image === false ) {
1190 - do_action( 'optml_log', 'can not find file' );
1191 - do_action( 'optml_log', $local_file );
1192 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
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 +
1432 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' );
1433 +
1193 1434 return $meta;
1194 - }
1195 - if ( $upload_signed_url !== 'found_resource' ) {
1435 + } catch ( UploadFailedException $exception ) {
1436 + if ( OPTML_DEBUG_MEDIA ) {
1437 + do_action( 'optml_log', 'upload error' );
1438 + do_action( 'optml_log', $exception );
1439 + }
1196 1440
1197 - $request = new Optml_Api();
1198 - $result = $request->upload_image( $upload_signed_url, $content_type, $image );
1441 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1199 1442
1200 - if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) !== 200 ) {
1201 - do_action( 'optml_log', 'upload error' );
1202 - do_action( 'optml_log', $result );
1203 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1204 - return $meta;
1443 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' );
1444 +
1445 + return $meta;
1446 + } catch ( UploadLimitException $exception ) {
1447 + if ( OPTML_DEBUG_MEDIA ) {
1448 + do_action( 'optml_log', 'limit exceeded' );
1449 + do_action( 'optml_log', $exception );
1205 1450 }
1206 - $file_size = filesize( $local_file );
1207 - if ( $file_size === false ) {
1208 - $file_size = 0;
1451 +
1452 + self::$instance->settings->update( 'offload_limit', $exception->getUsage()->getLimit() );
1453 + self::$instance->settings->update( 'offload_limit_reached', 'enabled' );
1454 +
1455 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: upload limit exceeded' );
1456 +
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 );
1209 1462 }
1210 - $request = new Optml_Api();
1211 - $result_update = $request->call_upload_api(
1212 - $original_url,
1213 - 'false',
1214 - $table_id,
1215 - 'success',
1216 - 'false',
1217 - $meta['width'],
1218 - $meta['height'],
1219 - $file_size
1220 - );
1221 - if ( is_wp_error( $result_update ) || wp_remote_retrieve_response_code( $result_update ) !== 200 ) {
1222 - do_action( 'optml_log', 'dynamo update error' );
1223 - do_action( 'optml_log', $result_update );
1224 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1225 - return $meta;
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 );
1226 1471 }
1472 +
1473 + self::mark_retryable_error( $attachment_id, 'Unknown error from upload api: ' . $exception->getMessage() );
1474 +
1475 + return $meta;
1227 1476 }
1477 +
1228 1478 $url_to_append = $original_url;
1229 - $url_parts = parse_url( $original_url );
1479 + $url_parts = parse_url( $original_url );
1230 1480
1231 1481 if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) {
1232 1482 $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name;
1233 1483 }
1234 - $optimized_url = $this->get_media_optimized_url( $url_to_append, $table_id );
1235 - $request = new Optml_Api();
1236 - if ( $request->check_optimized_url( $optimized_url ) === false ) {
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 ) {
1237 1488 do_action( 'optml_log', 'optimization error' );
1238 1489 do_action( 'optml_log', $optimized_url );
1239 - $request->call_upload_api( $original_url, 'true', $table_id );
1240 - update_post_meta( $attachment_id, 'optimole_offload_error', 'true' );
1490 +
1491 + Optimole::offload()->deleteImage( $image_id );
1492 +
1493 + update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' );
1494 +
1495 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' );
1496 +
1241 1497 return $meta;
1242 1498 }
1243 - unlink( $local_file );
1244 - update_post_meta( $attachment_id, 'optimole_offload', 'true' );
1245 - $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $table_id . '/' . $url_to_append;
1246 1499
1500 + @unlink( $local_file );
1501 +
1502 + update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' );
1503 + update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1504 +
1505 + $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $image_id . '/' . $url_to_append;
1506 +
1247 1507 if ( isset( $meta['sizes'] ) ) {
1248 1508 foreach ( $meta['sizes'] as $key => $value ) {
1249 1509 $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file );
1250 1510 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
@@ -1250,8 +1510,17 @@
1250 1510 file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path );
1251 1511 $meta['sizes'][ $key ]['file'] = $file_name;
1252 1512 }
1253 1513 }
1514 +
1515 + // This is needed for scaled images.
1516 + // Otherwise, `-scaled` images will be left behind.
1517 + if ( isset( $meta['original_image'] ) ) {
1518 + $ext = $this->get_ext( $local_file );
1519 + $scaled_path = str_replace( '.' . $ext, '-scaled.' . $ext, $local_file );
1520 + file_exists( $scaled_path ) && unlink( $scaled_path );
1521 + }
1522 +
1254 1523 $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
1255 1524
1256 1525 if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
1257 1526 foreach ( $duplicated_images as $duplicated_id ) {
@@ -1263,9 +1532,10 @@
1263 1532 $duplicated_meta['sizes'][ $key ]['file'] = $file_name;
1264 1533 }
1265 1534 }
1266 1535 wp_update_attachment_metadata( $duplicated_id, $duplicated_meta );
1267 - update_post_meta( $duplicated_id, 'optimole_offload', 'true' );
1536 + update_post_meta( $duplicated_id, self::META_KEYS['offloaded'], 'true' );
1537 + update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true );
1268 1538 }
1269 1539 }
1270 1540 }
1271 1541 if ( OPTML_DEBUG_MEDIA ) {
@@ -1270,8 +1540,11 @@
1270 1540 }
1271 1541 if ( OPTML_DEBUG_MEDIA ) {
1272 1542 do_action( 'optml_log', 'success offload' );
1273 1543 }
1544 +
1545 + self::decrement_process_meta_remaining();
1546 + self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' );
1274 1547 $attachment_page_id = wp_get_post_parent_id( $attachment_id );
1275 1548
1276 1549 if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) {
1277 1550 self::$offload_update_post = true;
@@ -1277,18 +1550,22 @@
1277 1550 self::$offload_update_post = true;
1278 1551 update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' );
1279 1552 self::$offload_update_post = false;
1280 1553 }
1554 +
1281 1555 return $meta;
1282 1556 }
1283 1557
1284 - /** Get the args for wp query according to the scope.
1558 + /**
1559 + * Get the args for wp query according to the scope.
1285 1560 *
1286 1561 * @param int $batch Number of images to get.
1287 1562 * @param string $action The action for which to get the images.
1563 + *
1288 1564 * @return array|false The query options array or false if not passed a valid action.
1289 1565 */
1290 - public static function get_images_query_args( $batch, $action, $get_images = false ) {
1566 + public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) {
1567 +
1291 1568 $args = [
1292 1569 'posts_per_page' => $batch,
1293 1570 'fields' => 'ids',
1294 1571 'ignore_sticky_posts' => false,
@@ -1293,92 +1570,110 @@
1293 1570 'fields' => 'ids',
1294 1571 'ignore_sticky_posts' => false,
1295 1572 'no_found_rows' => true,
1296 1573 ];
1574 +
1297 1575 if ( $get_images === true ) {
1298 - $args ['post_type'] = 'attachment';
1299 - $args ['post_mime_type'] = 'image';
1300 - $args ['post_status'] = 'inherit';
1576 + $args['post_type'] = 'attachment';
1577 + $args['post_mime_type'] = 'image';
1578 + $args['post_status'] = 'inherit';
1579 +
1580 + // Offload args.
1301 1581 if ( $action === 'offload_images' ) {
1302 1582 $args['meta_query'] = [
1303 1583 'relation' => 'AND',
1304 1584 [
1305 - 'key' => 'optimole_offload',
1585 + 'key' => self::META_KEYS['offloaded'],
1306 1586 'compare' => 'NOT EXISTS',
1307 1587 ],
1308 1588 [
1309 - 'key' => 'optimole_offload_error',
1589 + 'key' => self::META_KEYS['offload_error'],
1310 1590 'compare' => 'NOT EXISTS',
1311 1591 ],
1312 1592 ];
1593 +
1313 1594 return $args;
1314 1595 }
1315 - if ( $action === 'rollback_images' ) {
1316 - $args['meta_query'] = [
1317 - 'relation' => 'AND',
1318 - [
1319 - 'key' => 'optimole_offload',
1320 - 'value' => 'true',
1321 - 'compare' => '=',
1322 - ],
1323 - [
1324 - 'key' => 'optimole_rollback_error',
1325 - 'compare' => 'NOT EXISTS',
1326 - ],
1327 - ];
1328 - return $args;
1329 - }
1330 - } else {
1331 - $args = self::add_page_meta_query_args( $action, $args );
1596 +
1597 + // Rollback args.
1598 + $args['meta_query'] = [
1599 + 'relation' => 'AND',
1600 + [
1601 + 'key' => self::META_KEYS['offloaded'],
1602 + 'value' => 'true',
1603 + 'compare' => '=',
1604 + ],
1605 + [
1606 + 'key' => self::META_KEYS['rollback_error'],
1607 + 'compare' => 'NOT EXISTS',
1608 + ],
1609 + [
1610 + 'key' => Optml_Dam::OM_DAM_IMPORTED_FLAG,
1611 + 'compare' => 'NOT EXISTS',
1612 + ],
1613 + ];
1614 +
1615 + return $args;
1332 1616 }
1333 - $post_types = array_values(
1334 - array_filter(
1335 - get_post_types(),
1336 - function( $post_type ) {
1337 - if ( $post_type === 'attachment' || $post_type === 'revision' ) {
1338 - return false;
1339 - }
1340 - return true;
1617 +
1618 + $args = self::add_page_meta_query_args( $action, $args );
1619 + $post_types = array_filter(
1620 + get_post_types(),
1621 + function ( $post_type ) {
1622 + if ( $post_type === 'attachment' || $post_type === 'revision' ) {
1623 + return false;
1341 1624 }
1342 - )
1625 +
1626 + return true;
1627 + }
1343 1628 );
1344 - $args ['post_type'] = $post_types;
1629 +
1630 + $args ['post_type'] = array_values( $post_types );
1631 +
1345 1632 return $args;
1346 1633 }
1634 +
1347 1635 /**
1348 1636 * Query the database and upload images to our servers.
1349 1637 *
1350 1638 * @param int $batch Number of images to process in a batch.
1639 + *
1351 1640 * @return array Number of found images and number of successfully processed images.
1352 1641 */
1353 1642 public function upload_images( $batch, $images = [] ) {
1643 + self::$instance->settings->update( 'offload_limit_reached', 'disabled' );
1644 +
1354 1645 if ( empty( $images ) || $images === 'none' ) {
1355 - $args = self::get_images_query_args( $batch, 'offload_images', true );
1646 + $args = self::get_images_or_pages_query_args( $batch, 'offload_images', true );
1356 1647 $attachments = new \WP_Query( $args );
1357 - $ids = $attachments->get_posts();
1648 + $ids = $attachments->get_posts();
1358 1649 } else {
1359 1650 $ids = array_slice( $images, 0, $batch );
1360 1651 }
1361 - $result = [ 'found_images' => count( $ids ) ];
1652 + $result = [ 'found_images' => count( $ids ) ];
1362 1653 $result['success_offload'] = $this->upload_and_update_existing_images( $ids );
1654 +
1363 1655 return $result;
1364 1656 }
1657 +
1365 1658 /**
1366 1659 * Query the database and bring back image to media library.
1367 1660 *
1368 1661 * @param int $batch Number of images to process in a batch.
1662 + *
1369 1663 * @return array Number of found images and number of successfully processed images.
1370 1664 */
1371 1665 public function rollback_images( $batch, $images = [] ) {
1372 1666 if ( empty( $images ) || $images === 'none' ) {
1373 - $args = self::get_images_query_args( $batch, 'rollback_images', true );
1667 + $args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true );
1374 1668 $attachments = new \WP_Query( $args );
1375 - $ids = $attachments->get_posts();
1669 + $ids = $attachments->get_posts();
1376 1670 } else {
1377 1671 $ids = array_slice( $images, 0, $batch );
1378 1672 }
1379 - $result = [ 'found_images' => count( $ids ) ];
1673 + $result = [ 'found_images' => count( $ids ) ];
1380 1674 $result['success_rollback'] = $this->rollback_and_update_images( $ids );
1675 +
1381 1676 return $result;
1382 1677 }
1383 1678
1384 1679 /**
@@ -1384,18 +1679,20 @@
1384 1679 /**
1385 1680 * Update the post with the given id, the images will be updated by the filters we use.
1386 1681 *
1387 1682 * @param int $post_id The post id to update.
1683 + *
1388 1684 * @return bool Whether the update was succesful or not.
1389 1685 */
1390 1686 public function update_page( $post_id ) {
1391 1687 self::$offload_update_post = true;
1392 - $post_update = wp_update_post( ['ID' => $post_id] );
1688 + $post_update = wp_update_post( [ 'ID' => $post_id ] );
1393 1689 self::$offload_update_post = false;
1394 1690 if ( $post_update === 0 ) {
1395 1691 return false;
1396 1692 }
1397 1693 do_action( 'optml_updated_post', $post_id );
1694 +
1398 1695 return true;
1399 1696 }
1400 1697
1401 1698 /**
@@ -1401,17 +1698,26 @@
1401 1698 /**
1402 1699 * Calculate the number of images in media library and the number of posts/pages.
1403 1700 *
1404 1701 * @param string $action The actions for which to get the number of images.
1702 + *
1405 1703 * @return int Number of images.
1406 1704 */
1407 1705 public static function number_of_images_and_pages( $action ) {
1408 - $args = self::get_images_query_args( -1, $action );
1409 - $pages = new \WP_Query( $args );
1410 - $pages_count = $pages->post_count;
1411 - $args = self::get_images_query_args( -1, $action, true );
1412 - $images = new \WP_Query( $args );
1413 - return $pages_count + $images->post_count;
1706 + $images_args = self::get_images_or_pages_query_args( - 1, $action, true );
1707 +
1708 + $images = new \WP_Query( $images_args );
1709 +
1710 + // With the new mechanism, when offloading images, we don't need to address pages anymore.
1711 + // Bail early with the number of images.
1712 + if ( $action === 'offload_images' ) {
1713 + return $images->post_count;
1714 + }
1715 +
1716 + $pages_args = self::get_images_or_pages_query_args( - 1, $action );
1717 + $pages = new \WP_Query( $pages_args );
1718 +
1719 + return $pages->post_count + $images->post_count;
1414 1720 }
1415 1721
1416 1722 /**
1417 1723 * Calculate the number of images in media library and the number of posts/pages by IDs.
@@ -1416,14 +1722,16 @@
1416 1722 /**
1417 1723 * Calculate the number of images in media library and the number of posts/pages by IDs.
1418 1724 *
1419 1725 * @param string $action The actions for which to get the number of images.
1726 + *
1420 1727 * @return int Number of images.
1421 1728 */
1422 1729 public static function number_of_images_by_ids( $action, $ids ) {
1423 - $args = self::get_images_query_args( -1, $action, true );
1730 + $args = self::get_images_or_pages_query_args( - 1, $action, true );
1424 1731 $args['post__in'] = $ids;
1425 - $images = new \WP_Query( $args );
1732 + $images = new \WP_Query( $args );
1733 +
1426 1734 return $images->post_count;
1427 1735 }
1428 1736
1429 1737 /**
@@ -1439,9 +1747,9 @@
1439 1747 return [];
1440 1748 }
1441 1749
1442 1750 $transient_key = 'optml_images_' . md5( serialize( $images ) );
1443 - $transient = get_transient( $transient_key );
1751 + $transient = get_transient( $transient_key );
1444 1752
1445 1753 if ( false !== $transient ) {
1446 1754 return array_slice( $transient, ( $page - 1 ) * $batch, $batch );
1447 1755 }
@@ -1447,18 +1755,16 @@
1447 1755 }
1448 1756
1449 1757 global $wpdb;
1450 1758
1451 - $image_urls = array_map( 'wp_get_attachment_url', $images );
1759 + $image_urls = array_map(
1760 + function ( $image_id ) {
1761 + $meta = wp_get_attachment_metadata( $image_id );
1762 + $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] );
1452 1763
1453 - $image_urls = array_map(
1454 - function( $url ) {
1455 - $path_info = pathinfo( $url );
1456 - $directory = $path_info['dirname'];
1457 - $filename = $path_info['filename'];
1458 - return $directory . '/' . $filename;
1764 + return str_replace( '.' . $extension, '', $meta['file'] );
1459 1765 },
1460 - $image_urls
1766 + $images
1461 1767 );
1462 1768
1463 1769 // Sanitize the image URLs for use in the SQL query.
1464 1770 $urls = array_map( 'esc_url_raw', $image_urls );
@@ -1476,11 +1782,11 @@
1476 1782 }
1477 1783 }
1478 1784
1479 1785 // Get all the posts IDs by using LIMIT and offset in a loop.
1480 - $ids = [];
1786 + $ids = [];
1481 1787 $offset = 0;
1482 - $limit = $batch;
1788 + $limit = $batch;
1483 1789
1484 1790 while ( true ) {
1485 1791 $posts = $wpdb->get_col(
1486 1792 $wpdb->prepare(
@@ -1493,9 +1799,9 @@
1493 1799 if ( empty( $posts ) ) {
1494 1800 break;
1495 1801 }
1496 1802
1497 - $ids = array_merge( $ids, $posts );
1803 + $ids = array_merge( $ids, $posts );
1498 1804 $offset += $limit;
1499 1805 }
1500 1806
1501 1807 set_transient( $transient_key, $ids, HOUR_IN_SECONDS );
@@ -1503,27 +1809,68 @@
1503 1809 return array_slice( $ids, ( $page - 1 ) * $batch, $batch );
1504 1810 }
1505 1811
1506 1812 /**
1813 + * Record process meta,
1814 + *
1815 + * @param int $count The number of images to process.
1816 + *
1817 + * @return void
1818 + */
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() {
1833 + $meta = get_option( 'optml_process_meta', [] );
1834 +
1835 + if ( ! isset( $meta['remaining'] ) ) {
1836 + return;
1837 + }
1838 +
1839 + $meta['remaining'] = $meta['remaining'] - 1;
1840 + update_option( 'optml_process_meta', $meta );
1841 + }
1842 +
1843 + /**
1844 + * Get process meta,
1845 + *
1846 + * @return array
1847 + */
1848 + public static function get_process_meta() {
1849 + $res = [];
1850 + $meta = get_option( 'optml_process_meta', [] );
1851 + $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0;
1852 + $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0;
1853 + $res['remaining'] = isset( $meta['remaining'] ) ? $meta['remaining'] : $res['count'];
1854 +
1855 + return $res;
1856 + }
1857 +
1858 + /**
1507 1859 * Calculate the number of images in media library and the number of posts/pages.
1508 1860 *
1509 1861 * @param string $action The actions for which to get the number of images.
1510 1862 * @param bool $refresh Whether to refresh the cron or not.
1511 - * @param array $images The images to process.
1512 1863 *
1513 1864 * @return array Image count and Cron status.
1514 1865 */
1515 - public static function get_image_count( $action, $refresh, $images = [] ) {
1866 + public static function move_images( $action, $refresh ) {
1516 1867 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
1517 - $count = 0;
1518 - $step = 0;
1519 - $batch = 50; // Reduce this to 20 if you we have memory issues during testing.
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.
1520 1871
1521 - if ( empty( $images ) ) {
1522 - $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1523 - } else {
1524 - $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images );
1525 - }
1872 + $count = Optml_Media_Offload::number_of_images_and_pages( $action );
1526 1873
1527 1874 $possible_batch = ceil( $count / 10 );
1528 1875
1529 1876 if ( $possible_batch < $batch ) {
@@ -1536,17 +1883,42 @@
1536 1883 }
1537 1884
1538 1885 $in_progress = self::$instance->settings->get( $option ) !== 'disabled';
1539 1886
1540 - if ( false === $refresh && empty( $images ) ) {
1541 - $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 );
1542 1902
1543 - $in_progress = 0 !== $count;
1903 + self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1904 + self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR );
1905 + self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' );
1544 1906
1545 - self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' );
1907 + if ( $in_progress !== true ) {
1908 + return [
1909 + 'count' => $count,
1910 + 'status' => $in_progress,
1911 + 'action' => $type,
1912 + ];
1913 + }
1546 1914
1547 - if ( true === $in_progress ) {
1548 - wp_schedule_single_event(
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(
1549 1921 time(),
1550 1922 'optml_start_processing_images',
1551 1923 [
1552 1924 $action,
@@ -1553,122 +1925,191 @@
1553 1925 $batch,
1554 1926 1,
1555 1927 $total,
1556 1928 $step,
1929 + $lock_token,
1557 1930 ]
1558 1931 );
1559 1932 }
1560 1933 }
1561 1934
1562 - if ( false === $refresh && ! empty( $images ) ) {
1563 - $in_progress = 0 !== $count;
1935 + $response = [
1936 + 'count' => $count,
1937 + 'action' => $type,
1938 + ];
1564 1939
1565 - 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 + }
1566 1946
1567 - if ( true === $in_progress ) {
1568 - wp_schedule_single_event(
1569 - time(),
1570 - 'optml_start_processing_images_by_id',
1571 - [
1572 - $action,
1573 - $batch,
1574 - 1,
1575 - $images,
1576 - ]
1577 - );
1578 - }
1947 + $response['reached_limit'] = self::$instance->settings->is_offload_limit_reached();
1948 + $response['offload_limit'] = self::$instance->settings->get( 'offload_limit' );
1579 1949 }
1580 1950
1581 - return [
1582 - 'count' => $count,
1583 - 'status' => $in_progress,
1584 - ];
1951 + $response['status'] = $in_progress;
1952 +
1953 + return $response;
1585 1954 }
1586 1955
1587 1956 /**
1588 - * Start Processing Images by IDs
1957 + * Schedule an action.
1589 1958 *
1590 - * @param string $action The action for which to get the number of images.
1591 - * @param int $batch The batch of images to process.
1592 - * @param int $page The page of images to process.
1593 - * @param array $image_ids The images to process.
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.
1594 1962 *
1595 - * @return void
1963 + * @return mixed
1596 1964 */
1597 - public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) {
1598 - $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
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 );
1971 + }
1972 + }
1599 1973
1600 - if ( self::$instance->settings->get( $option ) === 'disabled' ) {
1601 - return;
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;
1602 1989 }
1990 + }
1603 1991
1604 - set_time_limit( 0 );
1605 - $page_in = Optml_Media_Offload::get_posts_by_image_ids( $action, $image_ids, $batch, $page );
1992 + /**
1993 + * Attempt to acquire the transfer lock for a given action.
1994 + *
1995 + * @param string $action The transfer action ('offload_images'|'rollback_images').
1996 + *
1997 + * @return string|false The lock token on success, false if another worker already holds the lock.
1998 + */
1999 + public static function acquire_transfer_lock( $action ) {
2000 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
2001 + if ( false !== $lock ) {
2002 + return false;
2003 + }
1606 2004
1607 - if ( empty( $image_ids ) && empty( $page_in ) ) {
1608 - self::$instance->settings->update( $option, 'disabled' );
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;
2032 + }
2033 +
2034 + set_transient(
2035 + self::TRANSFER_LOCK_TRANSIENT,
2036 + [
2037 + 'token' => $token,
2038 + 'action' => $action,
2039 + ],
2040 + self::TRANSFER_LOCK_TTL
2041 + );
2042 +
2043 + return true;
2044 + }
2045 +
2046 + /**
2047 + * Release the transfer lock if we still own it, allowing another worker to acquire it.
2048 + *
2049 + * @param string $token The lock token to release.
2050 + *
2051 + * @return void
2052 + */
2053 + public static function release_transfer_lock( $token ) {
2054 + $lock = get_transient( self::TRANSFER_LOCK_TRANSIENT );
2055 +
2056 + if ( ! is_array( $lock ) || ! isset( $lock['token'] ) || $lock['token'] !== $token ) {
1609 2057 return;
1610 2058 }
1611 2059
1612 - try {
1613 - if ( 0 !== count( $page_in ) ) {
1614 - $posts_to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in );
2060 + delete_transient( self::TRANSFER_LOCK_TRANSIENT );
2061 + }
1615 2062
1616 - if ( isset( $posts_to_update['page'] ) ) {
1617 - if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
1618 - foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
1619 - if ( ! empty( $image_ids ) ) {
1620 - $images = array_intersect( $images, $image_ids );
1621 - }
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 );
1622 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 );
2087 + if ( isset( $to_update['page'] ) ) {
2088 + if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) {
2089 + foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) {
2090 + $images = array_intersect( $images, [ $id ] );
1623 2091 if ( empty( $images ) ) {
1624 2092 continue;
1625 2093 }
1626 -
1627 - if ( $action === 'offload_images' ) {
1628 - Optml_Media_Offload::instance()->upload_and_update_existing_images( $images );
1629 - }
1630 - if ( $action === 'rollback_images' ) {
1631 - Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1632 - }
2094 + Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1633 2095 Optml_Media_Offload::instance()->update_page( $post_id );
1634 2096 }
1635 2097 }
1636 2098 }
1637 -
1638 2099 $page = $page + 1;
1639 - } else {
1640 - // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images.
1641 - $images = array_slice( $image_ids, 0, $batch );
1642 - $image_ids = array_slice( $image_ids, $batch );
1643 - $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch, $images ) : Optml_Media_Offload::instance()->upload_images( $batch, $images );
1644 - }
2100 + } while ( ! empty( $to_update['imagesToUpdate'] ) );
1645 2101
1646 - wp_schedule_single_event(
1647 - time(),
1648 - 'optml_start_processing_images_by_id',
1649 - [
1650 - $action,
1651 - $batch,
1652 - $page,
1653 - $image_ids,
1654 - ]
1655 - );
1656 - } catch ( Exception $e ) {
1657 - // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted.
1658 - $delay_in_seconds = 10;
1659 -
1660 - wp_schedule_single_event(
1661 - time() + $delay_in_seconds,
1662 - 'optml_start_processing_images_by_id',
1663 - [
1664 - $action,
1665 - $batch,
1666 - $page,
1667 - $image_ids,
1668 - ]
1669 - );
2102 + } else {
2103 + $action === 'rollback_images' ?
2104 + Optml_Media_Offload::instance()->rollback_images( 1, [ $id ] ) :
2105 + Optml_Media_Offload::instance()->upload_images( 1, [ $id ] );
1670 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 + }
1671 2112 }
1672 2113
1673 2114 /**
1674 2115 * Start Processing Images
@@ -1673,24 +2114,41 @@
1673 2114 /**
1674 2115 * Start Processing Images
1675 2116 *
1676 2117 * @param string $action The action for which to get the number of images.
1677 - * @param int $batch The batch of images to process.
1678 - * @param int $page The page of images to process.
1679 - * @param int $total The total number of pages.
1680 - * @param int $step The current step.
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.
1681 2123 *
1682 2124 * @return void
1683 2125 */
1684 - public function start_processing_images( $action, $batch, $page, $total, $step ) {
2126 + public function start_processing_images( $action, $batch, $page, $total, $step, $lock_token = '' ) {
1685 2127 $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status';
2128 + $type = 'offload_images' === $action ? 'offload' : 'rollback';
1686 2129
1687 2130 if ( self::$instance->settings->get( $option ) === 'disabled' ) {
2131 + self::release_transfer_lock( $lock_token );
1688 2132 return;
1689 2133 }
1690 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 +
1691 2140 if ( $step > $total || 0 === $total ) {
2141 + $meta = self::get_process_meta();
2142 + self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' );
2143 +
1692 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 +
1693 2151 return;
1694 2152 }
1695 2153
1696 2154 set_time_limit( 0 );
@@ -1695,20 +2153,16 @@
1695 2153
1696 2154 set_time_limit( 0 );
1697 2155
1698 2156 try {
1699 - $posts_to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch );
2157 + $posts_to_update = $action === 'offload_images' ? [] : Optml_Media_Offload::instance()->update_content( $page, $action, $batch );
1700 2158
1701 - if ( isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) {
2159 + // Kept for backward compatibility with old offloading mechanism where pages were modified.
2160 + if ( $action === 'rollback_images' && isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) {
1702 2161 $page = $posts_to_update['page'];
1703 2162 if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) {
1704 2163 foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) {
1705 - if ( $action === 'offload_images' ) {
1706 - Optml_Media_Offload::instance()->upload_and_update_existing_images( $images );
1707 - }
1708 - if ( $action === 'rollback_images' ) {
1709 - Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1710 - }
2164 + Optml_Media_Offload::instance()->rollback_and_update_images( $images );
1711 2165 Optml_Media_Offload::instance()->update_page( $post_id );
1712 2166 }
1713 2167 }
1714 2168 } else {
@@ -1716,9 +2170,9 @@
1716 2170 }
1717 2171
1718 2172 $step = $step + 1;
1719 2173
1720 - wp_schedule_single_event(
2174 + self::schedule_action(
1721 2175 time(),
1722 2176 'optml_start_processing_images',
1723 2177 [
1724 2178 $action,
@@ -1725,15 +2179,18 @@
1725 2179 $batch,
1726 2180 $page,
1727 2181 $total,
1728 2182 $step,
2183 + $lock_token,
1729 2184 ]
1730 2185 );
1731 2186 } catch ( Exception $e ) {
1732 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.
1733 2189 $delay_in_seconds = 10;
2190 + self::$instance->logger->add_log( $type, $e->getMessage() );
1734 2191
1735 - wp_schedule_single_event(
2192 + self::schedule_action(
1736 2193 time() + $delay_in_seconds,
1737 2194 'optml_start_processing_images',
1738 2195 [
1739 2196 $action,
@@ -1740,8 +2197,9 @@
1740 2197 $batch,
1741 2198 $page,
1742 2199 $total,
1743 2200 $step,
2201 + $lock_token,
1744 2202 ]
1745 2203 );
1746 2204 }
1747 2205 }
@@ -1746,14 +2204,629 @@
1746 2204 }
1747 2205 }
1748 2206
1749 2207 /**
1750 - * Check if the attachment is a DAM import.
2208 + * Alter attachment image src for offloaded images.
1751 2209 *
1752 - * @param int $attachment_id Attachment ID.
2210 + * @param array|false $image {
2211 + * Array of image data.
1753 2212 *
2213 + * @type string $0 Image source URL.
2214 + * @type int $1 Image width in pixels.
2215 + * @type int $2 Image height in pixels.
2216 + * @type bool $3 Whether the image is a resized image.
2217 + * }
2218 + *
2219 + * @param int $attachment_id attachment id.
2220 + * @param string|int[] $size image size.
2221 + * @param bool $icon Whether the image should be treated as an icon.
2222 + *
2223 + * @return array $image.
2224 + */
2225 + public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
2226 + if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2227 + return $image;
2228 + }
2229 + if ( isset( $image[0] ) ) {
2230 + $url = $image[0];
2231 + } else {
2232 + $url = get_post( $attachment_id );
2233 + $url = $url->guid;
2234 + }
2235 + $metadata = wp_get_attachment_metadata( $attachment_id );
2236 +
2237 + // Use the original size if the requested size is full.
2238 + if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) {
2239 + $image_url = $this->get_new_offloaded_attachment_url(
2240 + $url,
2241 + $attachment_id,
2242 + [
2243 + 'width' => $metadata['width'],
2244 + 'height' => $metadata['height'],
2245 + 'attachment_id' => $attachment_id,
2246 + ],
2247 + $metadata
2248 + );
2249 +
2250 + return [
2251 + $image_url,
2252 + $metadata['width'],
2253 + $metadata['height'],
2254 + false,
2255 + ];
2256 + }
2257 +
2258 + if ( doing_action( 'wp_insert_post_data' ) && wp_attachment_is( 'video', $attachment_id ) ) {
2259 + return $image;
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 + );
2273 +
2274 + return [
2275 + $image_url,
2276 + $sizes['width'],
2277 + $sizes['height'],
2278 + $size === 'full', // @phpstan-ignore-line
2279 + ];
2280 + }
2281 +
2282 + /**
2283 + * Needed for image sizes inside the editor.
2284 + *
2285 + * @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js().
2286 + * @param WP_Post $attachment Attachment object.
2287 + * @param array|false $meta Array of attachment meta data, or false if there is none.
2288 + *
2289 + * @return array
2290 + */
2291 + public function alter_attachment_for_js( $response, $attachment, $meta ) {
2292 + if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) {
2293 + return $response;
2294 + }
2295 +
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();
2304 +
2305 + foreach ( $sizes as $size => $args ) {
2306 + if ( isset( $response['sizes'][ $size ] ) ) {
2307 + continue;
2308 + }
2309 + $args = $this->size_to_dimension( $size, $meta );
2310 + $response['sizes'][ $size ] = array_merge(
2311 + $args,
2312 + [
2313 + 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ),
2314 + 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
2315 + ]
2316 + );
2317 + }
2318 + $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $meta );
2319 +
2320 + return $response;
2321 + }
2322 +
2323 + /**
2324 + * Alter attachment metadata.
2325 + *
2326 + * @param array $metadata The attachment metadata.
2327 + * @param int $id The attachment ID.
2328 + *
2329 + * @return array
2330 + */
2331 + public function alter_attachment_metadata( $metadata, $id ) {
2332 + if ( ! $this->is_new_offloaded_attachment( $id ) ) {
2333 + return $metadata;
2334 + }
2335 +
2336 + return $this->get_altered_metadata_for_remote_images( $metadata, $id );
2337 + }
2338 +
2339 + /**
2340 + * Get offloaded image attachment URL for new offloads.
2341 + *
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.
2349 + * @return string
2350 + */
2351 + private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [], $attachment_metadata = null ) {
2352 + $process_flag = self::KEYS['not_processed_flag'] . $attachment_id;
2353 +
2354 + // Image might have already passed through this filter.
2355 + if ( strpos( $url, $process_flag ) !== false ) {
2356 + return $url;
2357 + }
2358 + if ( $attachment_metadata === null ) {
2359 + $meta = wp_get_attachment_metadata( $attachment_id );
2360 + } else {
2361 + $meta = $attachment_metadata;
2362 + }
2363 + if ( ! isset( $meta['file'] ) ) {
2364 + return $url;
2365 + }
2366 +
2367 + $default_args = [
2368 + 'width' => 'auto',
2369 + 'height' => 'auto',
2370 + 'resize' => apply_filters( 'optml_default_crop', [] ),
2371 + ];
2372 +
2373 + $args = wp_parse_args( $args, $default_args );
2374 + // If this is not cropped, we constrain the dimensions to the original image.
2375 + if ( empty( $args['resize'] ) && ! in_array( 'auto', [ $args['width'], $args['height'] ], true ) ) {
2376 + $dimensions = wp_constrain_dimensions( $meta['width'], $meta['height'], $args['width'], $args['height'] );
2377 + $args['width'] = $dimensions[0];
2378 + $args['height'] = $dimensions[1];
2379 + }
2380 +
2381 + $file = $meta['file'];
2382 + if ( self::is_uploaded_image( $file ) ) {
2383 + $optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2384 +
2385 + return strpos( $optimized_url, $process_flag ) === false ? str_replace( '/' . ltrim( $file, '/' ), '/' . $process_flag . $file, $optimized_url ) : $optimized_url;
2386 + } else {
2387 + // this is for the users that already offloaded the images before the other fixes
2388 + $local_file = get_attached_file( $attachment_id );
2389 + if ( ! file_exists( $local_file ) ) {
2390 + $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id );
2391 + if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) {
2392 + foreach ( $duplicated_images as $id ) {
2393 + if ( ! empty( $id ) ) {
2394 + $duplicated_meta = wp_get_attachment_metadata( $id );
2395 + if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) {
2396 + return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] );
2397 + }
2398 + }
2399 + }
2400 + }
2401 + }
2402 + }
2403 + return $url;
2404 + }
2405 +
2406 + /**
2407 + * Replace the URLs in the editor content with the offloaded ones.
2408 + *
2409 + * @param string $content The incoming content.
2410 + *
2411 + * @return string
2412 + */
2413 + public function replace_urls_in_editor_content( $content ) {
2414 + $raw_extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
2415 +
2416 + if ( empty( $raw_extracted ) ) {
2417 + return $content;
2418 + }
2419 +
2420 + $to_replace = [];
2421 + foreach ( $raw_extracted as $url ) {
2422 + $attachment = $this->get_local_attachement_id_from_url( $url );
2423 +
2424 + // No local attachment.
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 + }
2429 + continue;
2430 + }
2431 +
2432 + $attachment_id = $attachment['attachment_id'];
2433 +
2434 + // Not offloaded.
2435 + if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) {
2436 + continue;
2437 + }
2438 +
2439 + // Get W/H from url.
2440 + $size = $this->parse_dimensions_from_filename( $url );
2441 + $width = $size[0] !== false ? $size[0] : 'auto';
2442 + $height = $size[1] !== false ? $size[1] : 'auto';
2443 +
2444 + // Handle resize.
2445 + $sizes2crop = self::size_to_crop();
2446 + $resize = apply_filters( 'optml_default_crop', [] );
2447 + $sizes = image_get_intermediate_size( $attachment_id, $size );
2448 + if ( false !== $sizes ) {
2449 + if ( isset( $sizes2crop[ $width . $height ] ) ) {
2450 + $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] );
2451 + }
2452 + }
2453 +
2454 + // Build the optimized 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 );
2456 +
2457 + // Drop any image size from the URL.
2458 + $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url );
2459 +
2460 + $to_replace[ $url ] = $optimized_url;
2461 + }
2462 +
2463 + return str_replace( array_keys( $to_replace ), array_values( $to_replace ), $content );
2464 + }
2465 +
2466 + /**
2467 + * Replaces the post content URLs to use Offloaded ones on editor fetch.
2468 + *
2469 + * @param \WP_REST_Response $response The response object.
2470 + * @param \WP_Post $post The post object.
2471 + * @param \WP_REST_Request $request The request object.
2472 + *
2473 + * @return \WP_REST_Response
2474 + */
2475 + public function pre_filter_rest_content( \WP_REST_Response $response, \WP_Post $post, \WP_REST_Request $request ) {
2476 + $context = $request->get_param( 'context' );
2477 +
2478 + if ( $context !== 'edit' ) {
2479 + return $response;
2480 + }
2481 +
2482 + $data = $response->get_data();
2483 +
2484 + // Actually replace all URLs.
2485 + $data['content']['raw'] = $this->replace_urls_in_editor_content( $data['content']['raw'] );
2486 +
2487 + $response->set_data( $data );
2488 +
2489 + return $response;
2490 + }
2491 +
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 + /**
2506 + * Filter post content to use local attachments when saving offloaded images.
2507 + *
2508 + * @param array $post_data Slashed, sanitized, processed post data.
2509 + * @param array $postarr Slashed sanitized post data.
2510 + * @param array $unsanitized_postarr Un-sanitized post data.
2511 + * @param bool $update Whether this is an existing post being updated or not.
2512 + *
2513 + * @return array
2514 + */
2515 + public function filter_saved_data( $post_data, $postarr, $unsanitized_postarr, $update ) {
2516 + if ( $postarr['post_status'] === 'trash' ) {
2517 + return $post_data;
2518 + }
2519 +
2520 + $content = $post_data['post_content'];
2521 +
2522 + $extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content );
2523 + $replace = [];
2524 +
2525 + foreach ( $extracted as $idx => $url ) {
2526 + $id = self::get_attachment_id_from_url( $url );
2527 +
2528 + if ( $id === false ) {
2529 + continue;
2530 + }
2531 +
2532 + $id = (int) $id;
2533 +
2534 + if ( $this->is_legacy_offloaded_attachment( $id ) ) {
2535 + continue;
2536 + }
2537 +
2538 + $original = self::get_original_url( $id );
2539 +
2540 + if ( $original === false ) {
2541 + continue;
2542 + }
2543 +
2544 + $replace[ $url ] = $original;
2545 +
2546 + $size = $this->parse_dimension_from_optimized_url( $url );
2547 +
2548 + if ( $size[0] === false || $size[1] === false ) {
2549 + continue;
2550 + }
2551 + if ( $size[0] === 'auto' || $size[1] === 'auto' ) {
2552 + continue;
2553 + }
2554 +
2555 + $extension = $this->get_ext( $url );
2556 + $metadata = wp_get_attachment_metadata( $id );
2557 +
2558 + // Is this the full URL.
2559 + if ( $metadata['width'] === (int) $size[0] && $metadata['height'] === (int) $size[1] ) {
2560 + continue;
2561 + }
2562 +
2563 + $size_crop_map = self::size_to_crop();
2564 +
2565 + $crop = false;
2566 +
2567 + if ( isset( $size_crop_map[ $size[0] . $size[1] ] ) ) {
2568 + $crop = $size_crop_map[ $size[0] . $size[1] ];
2569 + }
2570 +
2571 + if ( $crop ) {
2572 + $width = $size[0];
2573 + $height = $size[1];
2574 + } else {
2575 + // In case of an image size, we need to calculate the new dimensions for the proper file path.
2576 + $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], (int) $size[0], (int) $size[1] );
2577 +
2578 + $width = $constrained[0];
2579 + $height = $constrained[1];
2580 + }
2581 + $replace[ $url ] = $this->maybe_strip_scaled( $replace[ $url ] );
2582 +
2583 + $suffix = sprintf( '-%sx%s.%s', $width, $height, $extension );
2584 +
2585 + $replace[ $url ] = str_replace( '.' . $extension, $suffix, $replace[ $url ] );
2586 + }
2587 +
2588 + $post_data['post_content'] = str_replace( array_keys( $replace ), array_values( $replace ), $content );
2589 +
2590 + return $post_data;
2591 + }
2592 +
2593 + /**
2594 + * Alter the image size for the image widget.
2595 + *
2596 + * @param string $html the attachment image HTML string.
2597 + * @param array $settings Control settings.
2598 + * @param string $image_size_key Optional. Settings key for image size.
2599 + * Default is `image`.
2600 + * @param string $image_key Optional. Settings key for image. Default
2601 + * is null. If not defined uses image size key
2602 + * as the image key.
2603 + *
2604 + * @return string
2605 + */
2606 + public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) {
2607 + if ( ! isset( $settings['image'] ) ) {
2608 + return $html;
2609 + }
2610 +
2611 + $image = $settings['image'];
2612 +
2613 + if ( ! isset( $image['id'] ) ) {
2614 + return $html;
2615 + }
2616 +
2617 + if ( ! $this->is_new_offloaded_attachment( $image['id'] ) ) {
2618 + return $html;
2619 + }
2620 +
2621 + if ( ! isset( $settings['image_size'] ) ) {
2622 + return $html;
2623 + }
2624 +
2625 + if ( $settings['image_size'] === 'custom' ) {
2626 + if ( ! isset( $settings['image_custom_dimension'] ) ) {
2627 + return $html;
2628 + }
2629 +
2630 + $custom_dimensions = $settings['image_custom_dimension'];
2631 +
2632 + if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) {
2633 + return $html;
2634 + }
2635 +
2636 + $new_args = [
2637 + 'width' => $custom_dimensions['width'],
2638 + 'height' => $custom_dimensions['height'],
2639 + 'resize' => $this->to_optml_crop( true ),
2640 + ];
2641 + $new_url = $this->get_new_offloaded_attachment_url( $image['url'], $image['id'], $new_args );
2642 +
2643 + return str_replace( $image['url'], $new_url, $html );
2644 + }
2645 +
2646 + return $html;
2647 + }
2648 +
2649 + /**
2650 + * Adds new actions for new offloads.
2651 + *
2652 + * @return void
2653 + */
2654 + public function add_new_actions() {
2655 + add_filter( 'wp_prepare_attachment_for_js', [ self::$instance, 'alter_attachment_for_js' ], 999, 3 );
2656 + add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 );
2657 + add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 );
2658 +
2659 + // Needed for rendering beaver builder css properly.
2660 + add_filter( 'fl_builder_render_css', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2661 +
2662 + // Filter saved data on insert to use local attachments.
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 + }
2670 +
2671 + // Filter loaded data in the editors to use local attachments.
2672 + add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 );
2673 +
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 );
2692 + add_filter(
2693 + 'elementor/image_size/get_attachment_image_html',
2694 + [
2695 + $this,
2696 + 'alter_elementor_image_size',
2697 + ],
2698 + 10,
2699 + 4
2700 + );
2701 + }
2702 +
2703 + /**
2704 + * Elementor checks if the file exists before requesting a specific image size.
2705 + *
2706 + * Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload.
2707 + *
2708 + * Also needed because some
2709 + *
2710 + * @param string $file The file path.
2711 + * @param int $id The attachment ID.
2712 + *
2713 + * @return bool|string
2714 + */
2715 + public function alter_attached_file_response( $file, $id ) {
2716 + if ( ! $this->is_new_offloaded_attachment( $id ) ) {
2717 + return $file;
2718 + }
2719 +
2720 + $metadata = wp_get_attachment_metadata( $id );
2721 +
2722 + if ( isset( $metadata['file'] ) ) {
2723 + $uploads = wp_get_upload_dir();
2724 +
2725 + return $uploads['basedir'] . '/' . $metadata['file'];
2726 + }
2727 +
2728 + return true;
2729 + }
2730 +
2731 + /**
2732 + * Maybe strip the `-scaled` from the URL.
2733 + *
2734 + * @param string $url The url.
2735 + *
2736 + * @return string
2737 + */
2738 + public function maybe_strip_scaled( $url ) {
2739 + $ext = $this->get_ext( $url );
2740 +
2741 + return str_replace( '-scaled.' . $ext, '.' . $ext, $url );
2742 + }
2743 +
2744 + /**
2745 + * Is it a PHPUnit test run.
2746 + *
1754 2747 * @return bool
1755 2748 */
1756 - private function is_dam_import( $attachment_id ) {
1757 - return ! empty( get_post_meta( $attachment_id, Optml_Dam::OM_DAM_IMPORTED_FLAG, true ) );
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'] );
1758 2831 }
1759 2832 }