PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
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 trunk, at inc/3rd-party/amazon-s3-and-cloudfront/classes/Main.php

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