PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 1.10
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v1.10
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / inc / 3rd-party / amazon-s3-and-cloudfront / classes / Main.php

Main.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 1.10, at inc/3rd-party/amazon-s3-and-cloudfront/classes/Main.php

632 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Imagify\ThirdParty\AS3CF;
3
4 use \Imagify\Optimization\File;
5 use \Imagify\ThirdParty\AS3CF\CDN\WP\AS3 as CDN;
6
7 defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
8
9 /**
10 * Imagify WP Offload S3 class.
11 *
12 * @since 1.9
13 * @author Grégory Viguier
14 */
15 class Main extends \Imagify_AS3CF_Deprecated {
16 use \Imagify\Traits\InstanceGetterTrait;
17
18 /**
19 * AS3CF settings.
20 *
21 * @var array
22 * @since 1.9
23 * @access protected
24 * @author Grégory Viguier
25 */
26 protected $s3_settings;
27
28 /**
29 * Filesystem object.
30 *
31 * @var \Imagify_Filesystem
32 * @since 1.9
33 * @access protected
34 * @author Grégory Viguier
35 */
36 protected $filesystem;
37
38 /**
39 * The class constructor.
40 *
41 * @since 1.6.6
42 * @author Grégory Viguier
43 */
44 protected function __construct() {
45 $this->filesystem = \Imagify_Filesystem::get_instance();
46 }
47
48 /**
49 * Launch the hooks.
50 *
51 * @since 1.6.6
52 * @author Grégory Viguier
53 */
54 public function init() {
55 static $done = false;
56
57 if ( $done ) {
58 return;
59 }
60 $done = true;
61
62 /**
63 * WebP images to display with a <picture> tag.
64 */
65 add_action( 'as3cf_init', [ $this, 'store_s3_settings' ] );
66 add_filter( 'imagify_webp_picture_process_image', [ $this, 'picture_tag_webp_image' ] );
67
68 /**
69 * Register CDN.
70 */
71 add_filter( 'imagify_cdn', [ $this, 'register_cdn' ], 8, 3 );
72
73 /**
74 * Optimization process.
75 */
76 add_filter( 'imagify_before_optimize_size', [ $this, 'maybe_copy_file_from_cdn_before_optimization' ], 8, 6 );
77 add_action( 'imagify_after_optimize', [ $this, 'maybe_send_media_to_cdn_after_optimization' ], 8, 2 );
78
79 /**
80 * Restoration process.
81 */
82 add_action( 'imagify_after_restore_media', [ $this, 'maybe_send_media_to_cdn_after_restore' ], 8, 4 );
83
84 /**
85 * WebP support.
86 */
87 add_filter( 'as3cf_attachment_file_paths', [ $this, 'add_webp_images_to_attachment' ], 8, 3 );
88 add_filter( 'mime_types', [ $this, 'add_webp_support' ] );
89
90 /**
91 * Redirections.
92 */
93 add_filter( 'imagify_redirect_to', [ $this, 'redirect_referrer' ] );
94
95 /**
96 * Stats.
97 */
98 add_filter( 'imagify_total_attachment_filesize', [ $this, 'add_stats_for_s3_files' ], 8, 4 );
99 }
100
101
102 /** ----------------------------------------------------------------------------------------- */
103 /** OPTIMIZATION PROCESS ==================================================================== */
104 /** ----------------------------------------------------------------------------------------- */
105
106 /**
107 * On AS3CF init, store its settings.
108 *
109 * @since 1.9
110 * @access public
111 * @author Grégory Viguier
112 *
113 * @param \Amazon_S3_And_CloudFront $as3cf AS3CF’s main instance.
114 */
115 public function store_s3_settings( $as3cf ) {
116 if ( method_exists( $as3cf, 'get_settings' ) ) {
117 $this->store_s3_settings = (array) $as3cf->get_settings();
118 }
119 }
120
121 /**
122 * WebP images to display with a <picture> tag.
123 *
124 * @since 1.9
125 * @see \Imagify\Webp\Picture\Display->process_image()
126 * @author Grégory Viguier
127 *
128 * @param array $data An array of data for this image.
129 * @return array
130 */
131 public function picture_tag_webp_image( $data ) {
132 global $wpdb;
133
134 if ( ! empty( $data['src']['webp_path'] ) ) {
135 // The file is local.
136 return $data;
137 }
138
139 $match = $this->is_s3_url( $data['src']['url'] );
140
141 if ( ! $match ) {
142 // The file is not on S3.
143 return $data;
144 }
145
146 // Get the image ID.
147 $post_id = (int) $wpdb->get_var(
148 $wpdb->prepare(
149 "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
150 // We use only year/month + filename, we should not have any subdir between them for the main file.
151 $match['year_month'] . $match['filename']
152 )
153 );
154
155 if ( $post_id <= 0 ) {
156 // Not in the database.
157 return $data;
158 }
159
160 $s3_info = get_post_meta( $post_id, 'amazonS3_info', true );
161 $imagify_data = get_post_meta( $post_id, '_imagify_data', true );
162
163 if ( ! $s3_info || ! $imagify_data ) {
164 return $data;
165 }
166
167 $webp_size_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_SUFFIX' );
168 $webp_size_name = 'full' . $webp_size_suffix;
169
170 if ( ! empty( $imagify_data['sizes'][ $webp_size_name ]['success'] ) ) {
171 // We have a WebP image.
172 $data['src']['webp_exists'] = true;
173 }
174
175 if ( empty( $data['srcset'] ) ) {
176 return $data;
177 }
178
179 $meta_data = get_post_meta( $post_id, '_wp_attachment_metadata', true );
180
181 if ( empty( $meta_data['sizes'] ) ) {
182 return $data;
183 }
184
185 // Ease the search for corresponding file name.
186 $size_files = [];
187
188 foreach ( $meta_data['sizes'] as $size_name => $size_data ) {
189 $size_files[ $size_data['file'] ] = $size_name;
190 }
191
192 // Look for a corresponding size name.
193 foreach ( $data['srcset'] as $i => $srcset_data ) {
194 if ( empty( $srcset_data['webp_url'] ) ) {
195 // Not a supported image format.
196 continue;
197 }
198 if ( ! empty( $srcset_data['webp_path'] ) ) {
199 // The file is local.
200 continue;
201 }
202
203 $match = $this->is_s3_url( $srcset_data['url'] );
204
205 if ( ! $match ) {
206 // Not on S3.
207 continue;
208 }
209
210 // Try with no subdirs.
211 $filename = $match['filename'];
212
213 if ( isset( $size_files[ $filename ] ) ) {
214 $size_name = $size_files[ $filename ];
215 } else {
216 // Try with subdirs.
217 $filename = $match['subdirs'] . $match['filename'];
218
219 if ( isset( $size_files[ $filename ] ) ) {
220 $size_name = $size_files[ $filename ];
221 } elseif ( preg_match( '@/\d+/$@', $match['subdirs'] ) ) {
222 // Last try: the subdirs may contain the S3 versioning. If not the case, we can still build a pyramid with this code.
223 $filename = preg_replace( '@/\d+/$@', '/', $match['subdirs'] ) . $match['filename'];
224
225 if ( isset( $size_files[ $filename ] ) ) {
226 $size_name = $size_files[ $filename ];
227 } else {
228 continue;
229 }
230 }
231 }
232
233 $webp_size_name = $size_name . $webp_size_suffix;
234
235 if ( ! empty( $imagify_data['sizes'][ $webp_size_name ]['success'] ) ) {
236 // We have a WebP image.
237 $data['srcset'][ $i ]['webp_exists'] = true;
238 }
239 }
240
241 return $data;
242 }
243
244 /**
245 * The CDN to use for this media.
246 *
247 * @since 1.9
248 * @access public
249 * @author Grégory Viguier
250 *
251 * @param bool|PushCDNInterface $cdn A PushCDNInterface instance. False if no CDN is used.
252 * @param int $media_id The media ID.
253 * @param ContextInterface $context The context object.
254 */
255 public function register_cdn( $cdn, $media_id, $context ) {
256 if ( 'wp' !== $context->get_name() ) {
257 return $cdn;
258 }
259 if ( $cdn instanceof PushCDNInterface ) {
260 return $cdn;
261 }
262
263 return new CDN( $media_id );
264 }
265
266 /**
267 * Before performing a file optimization, download the file from the CDN if it is missing.
268 *
269 * @since 1.9
270 * @access public
271 * @author Grégory Viguier
272 *
273 * @param null|\WP_Error $response Null by default.
274 * @param ProcessInterface $process The optimization process instance.
275 * @param File $file The file instance. If $webp is true, $file references the non-webp file.
276 * @param string $thumb_size The media size.
277 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
278 * @param bool $webp The image will be converted to WebP.
279 * @return null|\WP_Error Null. A \WP_Error object on error.
280 */
281 public function maybe_copy_file_from_cdn_before_optimization( $response, $process, $file, $thumb_size, $optimization_level, $webp ) {
282 if ( is_wp_error( $response ) || 'wp' !== $process->get_media()->get_context() ) {
283 return $response;
284 }
285
286 $media = $process->get_media();
287 $cdn = $media->get_cdn();
288
289 if ( ! $cdn instanceof CDN ) {
290 return $response;
291 }
292
293 if ( $this->filesystem->exists( $file->get_path() ) ) {
294 return $response;
295 }
296
297 // Get files from the CDN.
298 $result = $cdn->get_files_from_cdn( [ $file->get_path() ] );
299
300 if ( is_wp_error( $result ) ) {
301 return $result;
302 }
303
304 return $response;
305 }
306
307 /**
308 * After performing a media optimization:
309 * - Save some data,
310 * - Upload the files to the CDN,
311 * - Maybe delete them from the server.
312 *
313 * @since 1.9
314 * @access public
315 * @author Grégory Viguier
316 *
317 * @param ProcessInterface $process The optimization process.
318 * @param array $item The item being processed.
319 */
320 public function maybe_send_media_to_cdn_after_optimization( $process, $item ) {
321 if ( 'wp' !== $process->get_media()->get_context() ) {
322 return;
323 }
324
325 $media = $process->get_media();
326 $cdn = $media->get_cdn();
327
328 if ( ! $cdn instanceof CDN ) {
329 return;
330 }
331
332 $cdn->send_to_cdn( ! empty( $item['data']['is_new_upload'] ) );
333 }
334
335 /**
336 * After restoring a media:
337 * - Save some data,
338 * - Upload the files to the CDN,
339 * - Maybe delete WebP files from the CDN.
340 *
341 * @since 1.9
342 * @access public
343 * @author Grégory Viguier
344 *
345 * @param ProcessInterface $process The optimization process.
346 * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure.
347 * @param array $files The list of files, before restoring them.
348 * @param array $data The optimization data, before deleting it.
349 */
350 public function maybe_send_media_to_cdn_after_restore( $process, $response, $files, $data ) {
351 if ( 'wp' !== $process->get_media()->get_context() ) {
352 return;
353 }
354
355 $media = $process->get_media();
356 $cdn = $media->get_cdn();
357
358 if ( ! $cdn instanceof CDN ) {
359 return;
360 }
361
362 if ( is_wp_error( $response ) ) {
363 $error_code = $response->get_error_code();
364
365 if ( 'copy_failed' === $error_code ) {
366 // No files have been restored.
367 return;
368 }
369
370 // No thumbnails left?
371 }
372
373 $cdn->send_to_cdn( false );
374
375 // Remove WebP files from CDN.
376 $webp_files = [];
377
378 if ( $files ) {
379 // Get the paths to the WebP files.
380 foreach ( $files as $size_name => $file ) {
381 $webp_size_name = $size_name . $process::WEBP_SUFFIX;
382
383 if ( empty( $data['sizes'][ $webp_size_name ]['success'] ) ) {
384 // This size has no WebP version.
385 continue;
386 }
387
388 if ( 0 === strpos( $file['mime-type'], 'image/' ) ) {
389 $webp_file = new File( $file['path'] );
390
391 if ( ! $webp_file->is_webp() ) {
392 $webp_files[] = $webp_file->get_path_to_webp();
393 }
394 }
395 }
396 }
397
398 if ( $webp_files ) {
399 $cdn->remove_files_from_cdn( $webp_files );
400 }
401 }
402
403
404 /** ----------------------------------------------------------------------------------------- */
405 /** OPTIMIZATION PROCESS ==================================================================== */
406 /** ----------------------------------------------------------------------------------------- */
407
408 /**
409 * Add the WebP files to the list of files that the CDN must handle.
410 *
411 * @since 1.9
412 * @access public
413 * @author Grégory Viguier
414 *
415 * @param array $paths A list of file paths, keyed by size name. 'file' for the full size. Includes a 'backup' size and a 'thumb' size.
416 * @param int $attachment_id The media ID.
417 * @param array $metadata The attachment meta data.
418 * @return array
419 */
420 public function add_webp_images_to_attachment( $paths, $attachment_id, $metadata ) {
421 if ( ! $paths ) {
422 // ¯\(°_o)/¯.
423 return $paths;
424 }
425
426 $process = imagify_get_optimization_process( $attachment_id, 'wp' );
427
428 if ( ! $process->is_valid() ) {
429 return $paths;
430 }
431
432 $media = $process->get_media();
433
434 if ( ! $media->is_image() ) {
435 return $paths;
436 }
437
438 // Use the optimization data (the files may not be on the server).
439 $data = $process->get_data()->get_optimization_data();
440
441 if ( empty( $data['sizes'] ) ) {
442 return $paths;
443 }
444
445 foreach ( $paths as $size_name => $file_path ) {
446 if ( 'thumb' === $size_name || 'backup' === $size_name || $process->is_size_webp( $size_name ) ) {
447 continue;
448 }
449
450 if ( 'file' === $size_name ) {
451 $size_name = 'full';
452 }
453
454 $webp_size_name = $size_name . $process::WEBP_SUFFIX;
455
456 if ( empty( $data['sizes'][ $webp_size_name ]['success'] ) ) {
457 // This size has no WebP version.
458 continue;
459 }
460
461 $file = new File( $file_path );
462
463 if ( ! $file->is_webp() ) {
464 $paths[ $webp_size_name ] = $file->get_path_to_webp();
465 }
466 }
467
468 return $paths;
469 }
470
471 /**
472 * Add WebP format to the list of allowed mime types.
473 *
474 * @since 1.9
475 * @access public
476 * @see get_allowed_mime_types()
477 * @author Grégory Viguier
478 *
479 * @param array $mime_types A list of mime types.
480 * @return array
481 */
482 public function add_webp_support( $mime_types ) {
483 $mime_types['webp'] = 'image/webp';
484 return $mime_types;
485 }
486
487
488 /** ----------------------------------------------------------------------------------------- */
489 /** VARIOUS HOOKS =========================================================================== */
490 /** ----------------------------------------------------------------------------------------- */
491
492 /**
493 * After a non-ajax optimization, remove some unnecessary arguments from the referrer used for the redirection.
494 * Those arguments don't break anything, they're just not relevant and display obsolete admin notices.
495 *
496 * @since 1.6.10
497 * @author Grégory Viguier
498 *
499 * @param string $redirect The URL to redirect to.
500 * @return string
501 */
502 public function redirect_referrer( $redirect ) {
503 return remove_query_arg( [ 'as3cfpro-action', 'as3cf_id', 'errors', 'count' ], $redirect );
504 }
505
506 /**
507 * Provide the file sizes and the number of thumbnails for files that are only on S3.
508 *
509 * @since 1.6.7
510 * @author Grégory Viguier
511 *
512 * @param bool $size_and_count False by default.
513 * @param int $image_id The attachment ID.
514 * @param array $files An array of file paths with thumbnail sizes as keys.
515 * @param array $image_ids An array of all attachment IDs.
516 * @return bool|array False by default. Provide an array with the keys 'filesize' (containing the total filesize) and 'thumbnails' (containing the number of thumbnails).
517 */
518 public function add_stats_for_s3_files( $size_and_count, $image_id, $files, $image_ids ) {
519 static $data;
520
521 if ( is_array( $size_and_count ) ) {
522 return $size_and_count;
523 }
524
525 if ( $this->filesystem->exists( $files['full'] ) ) {
526 // If the full size is on the server, that probably means all files are on the server too.
527 return $size_and_count;
528 }
529
530 if ( ! isset( $data ) ) {
531 $data = \Imagify_DB::get_metas( [
532 // Get the filesizes.
533 's3_filesize' => 'wpos3_filesize_total',
534 ], $image_ids );
535
536 $data = array_map( 'absint', $data['s3_filesize'] );
537 }
538
539 if ( empty( $data[ $image_id ] ) ) {
540 // The file is not on S3.
541 return $size_and_count;
542 }
543
544 // We can't take the disallowed sizes into account here.
545 return [
546 'filesize' => (int) $data[ $image_id ],
547 'thumbnails' => count( $files ) - 1,
548 ];
549 }
550
551
552 /** ----------------------------------------------------------------------------------------- */
553 /** TOOLS =================================================================================== */
554 /** ----------------------------------------------------------------------------------------- */
555
556 /**
557 * Tell if an URL is a S3 one.
558 *
559 * @since 1.9
560 * @access public
561 * @author Grégory Viguier
562 *
563 * @param string $url The URL to test.
564 * @return array|bool {
565 * An array if an S3 URL. False otherwise.
566 *
567 * @type string $key Bucket key. Ex: subdir/wp-content/uploads/2019/02/13142432/foobar-480x510.jpg.
568 * @type string $year_month The uploads year/month folders. Ex: 2019/02/.
569 * @type string $subdirs Sub-directories between year/month folders and the filename.
570 * It can be the S3 versioning folder, any folder added by a plugin, or both.
571 * There is no way to know which one it is. Ex: foo/13142432/.
572 * @type string $filename The file name. Ex: foobar-480x510.jpg.
573 * }
574 */
575 public function is_s3_url( $url ) {
576 static $uploads_dir;
577 static $domain;
578
579 /**
580 * Tell if an URL is a S3 one.
581 *
582 * @since 1.9
583 * @author Grégory Viguier
584 *
585 * @param null|array|bool $is Null by default. Must return an array if an S3 URL, or false if not.
586 * @param string $url The URL to test.
587 */
588 $is = apply_filters( 'imagify_as3cf_is_s3_url', null, $url );
589
590 if ( false === $is ) {
591 return false;
592 }
593
594 if ( is_array( $is ) ) {
595 return imagify_merge_intersect( $is, [
596 'key' => '',
597 'year_month' => '',
598 'subdirs' => '',
599 'filename' => '',
600 ] );
601 }
602
603 if ( ! isset( $uploads_dir ) ) {
604 $uploads_dir = wp_parse_url( $this->filesystem->get_upload_baseurl() );
605 $uploads_dir = trim( $uploads_dir['path'], '/' ) . '/';
606 }
607
608 if ( ! isset( $domain ) ) {
609 if ( ! empty( $this->store_s3_settings['cloudfront'] ) ) {
610 $domain = sanitize_text_field( $this->store_s3_settings['cloudfront'] );
611 $domain = preg_replace( '@^(?:https?:)?//@', '//', $domain );
612 $domain = preg_quote( $domain, '@' );
613 } else {
614 $domain = 's3-.+\.amazonaws\.com/[^/]+/';
615 }
616 }
617
618 $pattern = '@^(?:https?:)?//' . $domain . '/(?<key>' . $uploads_dir . '(?<year_month>\d{4}/\d{2}/)?(?<subdirs>.+/)?(?<filename>[^/]+))$@i';
619
620 if ( ! preg_match( $pattern, $url, $match ) ) {
621 return false;
622 }
623
624 unset( $match[0] );
625
626 return array_merge( [
627 'year_month' => '',
628 'subdirs' => '',
629 ], $match );
630 }
631 }
632