class-helper.php 27.9 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969
<?php
/**
 * Helpers class.
 *
 * @package Smush\Core
 * @version 1.0
 *
 * @author Umesh Kumar <umesh@incsub.com>
 *
 * @copyright (c) 2017, Incsub (http://incsub.com)
 */

namespace Smush\Core;

use finfo;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Stats;
use Smush\Core\Png2Jpg\Png2Jpg_Optimization;
use WP_Smush;
use WDEV_Logger;

if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class Helper
 */
class Helper {
	/**
	 * Temporary cache.
	 *
	 * We use this instead of WP_Object_Cache to avoid save data to memory cache (persistent caching).
	 *
	 * And to avoid it take memory space, we also reset the group cache each we get a new key,
	 * it means one group only has one key.
	 * It's useful when we want to save result a function.
	 *
	 * Leave group is null to set and get the value by unique key.
	 *
	 * It's useful to avoid checking something multiple times.
	 *
	 * @since 3.9.6
	 *
	 * @var array
	 */
	private static $temp_cache = array();

	/**
	 * WPMUDEV Logger lib.
	 *
	 * @access private
	 *
	 * @var null|WDEV_Logger
	 */
	private static $logger;

	/**
	 * Get WDEV_Logger instance.
	 *
	 * @return WDEV_Logger
	 */
	public static function logger() {
		if ( null === self::$logger ) {
			$swiched_blog = false;
			// On MU site, move all log files into the log folder [wp-content/uploads/smush] on the main site.
			if ( is_multisite() && ! is_main_site() ) {
				switch_to_blog( get_main_site_id() );
				$swiched_blog = true;
			}
			$upload_dir = wp_get_upload_dir();

			$log_dir = 'smush';
			if ( false !== strpos( $upload_dir['basedir'], WP_CONTENT_DIR ) ) {
				$log_dir = str_replace( trailingslashit( WP_CONTENT_DIR ), '', $upload_dir['basedir'] ) . '/smush';
			}

			if ( $swiched_blog ) {
				restore_current_blog();
			}

			self::$logger = WDEV_Logger::create(
				array(
					'log_dir'    => $log_dir,
					'is_private' => true,
					'modules'    => array(
						'smush'        => array(
							'is_global_module' => true,
						),
						'cdn'          => array(),
						'lazy'         => array(),
						'webp'         => array(),
						'png2jpg'      => array(),
						'resize'       => array(),
						'dir'          => array(),
						'backup'       => array(),
						'api'          => array(),
						'integrations' => array(),
					),
				)
			);
		}

		return self::$logger;
	}

	/**
	 * Clean file path.
	 *
	 * @param string $file File path.
	 * @return string
	 */
	public static function clean_file_path( $file ) {
		return str_replace( WP_CONTENT_DIR, '', $file );
	}

	/**
	 * Get value from temporary cache.
	 *
	 * @param string      $key Key name.
	 * @param string|null $group Group name.
	 * @param mixed       $default Default value, default is NULL.
	 *
	 *      Uses:
	 *      if( null !== Helper::cache_get( 'your_key', 'your_group' ) ){
	 *           // Do your something with temporary cache value.
	 *      }
	 *      // Maybe setting it with Helper::cache_set.
	 *
	 * @since 3.9.6
	 *
	 * @return mixed The cached result.
	 */
	public static function cache_get( $key, $group = null, $default = null ) {
		// Add current blog id to support MU site.
		$current_blog_id = get_current_blog_id();

		// Get cache for current blog.
		$temp_cache = array();
		if ( isset( self::$temp_cache[ $current_blog_id ] ) ) {
			$temp_cache = self::$temp_cache[ $current_blog_id ];
		}

		/**
		 * Add a filter to force cache.
		 * It might be helpful when we debug.
		 */
		if ( apply_filters( 'wp_smush_force_cache', false, $key, $group, $temp_cache ) ) {
			$locked_groups = array(
				// Required for cache png2jpg()->can_be_converted() before resizing.
				'png2jpg_can_be_converted',
				// Required for cache unique file name of png2jpg()->convert_to_jpg().
				'convert_to_jpg',
			);

			if ( ! in_array( $group, $locked_groups, true ) ) {
				return null;
			}
		}

		$value = $default;
		if ( isset( $group ) ) {
			if ( isset( $temp_cache[ $group ][ $key ] ) ) {
				$value = $temp_cache[ $group ][ $key ];
			} elseif ( isset( $temp_cache[ $group ] ) ) {
				// Get a new key, reset group.
				unset( $temp_cache[ $group ] );
			}
		} elseif ( isset( $temp_cache[ $key ] ) ) {
			// Get the value by key.
			$value = $temp_cache[ $key ];
		}

		return $value;
	}

	/**
	 * Save value to temporary cache.
	 *
	 * @since 3.9.6
	 *
	 * @param string      $key Key name.
	 * @param mixed       $value Data to cache.
	 * @param string|null $group Group name.
	 *
	 * Note, we return the provided value to use it inside some methods.
	 * @return mixed Returns the provided value.
	 */
	public static function cache_set( $key, $value, $group = null ) {
		// Add current blog id to support MU site.
		$current_blog_id = get_current_blog_id();
		if ( isset( $group ) ) {
			// Reset group and set the value.
			self::$temp_cache[ $current_blog_id ][ $group ] = array( $key => $value );
		} else {
			// Save value by unique key.
			self::$temp_cache[ $current_blog_id ][ $key ] = $value;
		}
		return $value;
	}

	/**
	 * Clear cache by group or key.
	 *
	 * @since 3.9.6
	 *
	 * @param string $cache_key Group name or unique key name.
	 */
	public static function cache_delete( $cache_key ) {
		// Add current blog id to support MU site.
		$current_blog_id = get_current_blog_id();

		// Delete temp cache by cache key.
		if ( isset( $cache_key, self::$temp_cache[ $current_blog_id ][ $cache_key ] ) ) {
			unset( self::$temp_cache[ $current_blog_id ][ $cache_key ] );
		}

		return true;
	}

	/**
	 * Get mime type for file.
	 *
	 * @since 3.1.0  Moved here as a helper function.
	 *
	 * @param string $path  Image path.
	 *
	 * @return bool|string
	 */
	public static function get_mime_type( $path ) {
		// These mime functions only work on local files/streams.
		if ( ! stream_is_local( $path ) ) {
			return false;
		}

		// Get the File mime.
		if ( class_exists( 'finfo' ) ) {
			$file_info = new finfo( FILEINFO_MIME_TYPE );
		} else {
			$file_info = false;
		}

		if ( $file_info ) {
			$mime = file_exists( $path ) ? $file_info->file( $path ) : '';
		} elseif ( function_exists( 'mime_content_type' ) ) {
			$mime = mime_content_type( $path );
		} else {
			$mime = false;
		}

		return $mime;
	}

	/**
	 * Filter the Posts object as per mime type.
	 *
	 * @param array $posts Object of Posts.
	 *
	 * @return array  Array of post IDs.
	 */
	public static function filter_by_mime( $posts ) {
		if ( empty( $posts ) ) {
			return $posts;
		}

		foreach ( $posts as $post_k => $post ) {
			if ( ! isset( $post->post_mime_type ) || ! in_array( $post->post_mime_type, Core::$mime_types, true ) ) {
				unset( $posts[ $post_k ] );
			} else {
				$posts[ $post_k ] = $post->ID;
			}
		}

		return $posts;
	}

	/**
	 * Iterate over PNG->JPG Savings to return cummulative savings for an image
	 *
	 * @param string $attachment_id  Attachment ID.
	 *
	 * @return array
	 */
	public static function get_pngjpg_savings( $attachment_id = '' ) {
		$media_item           = Media_Item_Cache::get_instance()->get( $attachment_id );
		$png2jpg_optimization = new Png2Jpg_Optimization( $media_item );
		$stats                = $png2jpg_optimization->is_optimized()
			? $png2jpg_optimization->get_stats() :
			new Media_Item_Stats();

		return $stats->to_array();
	}

	/**
	 * Get the link to the media library page for the image.
	 *
	 * @since 2.9.0
	 *
	 * @param int    $id    Image ID.
	 * @param string $name  Image file name.
	 * @param bool   $src   Return only src. Default - return link.
	 *
	 * @return string
	 */
	public static function get_image_media_link( $id, $name, $src = false ) {
		$mode = get_user_option( 'media_library_mode' );
		if ( 'grid' === $mode ) {
			$link = admin_url( "upload.php?item=$id" );
		} else {
			$link = admin_url( "post.php?post=$id&action=edit" );
		}

		if ( ! $src ) {
			return "<a href='$link'>$name</a>";
		}

		return $link;
	}

	/**
	 * Returns current user name to be displayed
	 *
	 * @return string
	 */
	public static function get_user_name() {
		$current_user = wp_get_current_user();
		return ! empty( $current_user->first_name ) ? $current_user->first_name : $current_user->display_name;
	}

	/**
	 * Allows to filter the error message sent to the user
	 *
	 * @param string $error          Error message.
	 * @param string $attachment_id  Attachment ID.
	 *
	 * @return mixed|null|string
	 */
	public static function filter_error( $error = '', $attachment_id = '' ) {
		if ( empty( $error ) ) {
			return null;
		}

		/**
		 * Replace the 500 server error with a more appropriate error message.
		 */
		if ( false !== strpos( $error, '500 Internal Server Error' ) ) {
			$error = esc_html__( "Couldn't process image due to bad headers. Try re-saving the image in an image editor, then upload it again.", 'wp-smushit' );
		} elseif ( strpos( $error, 'timed out' ) ) {
			$error = esc_html__( "Timeout error. You can increase the request timeout to make sure Smush has enough time to process larger files. `define('WP_SMUSH_TIMEOUT', 150);`", 'wp-smushit' );
		}

		/**
		 * Used internally to modify the error message
		 */
		return apply_filters( 'wp_smush_error', $error, $attachment_id );
	}

	/**
	 * Format metadata from $_POST request.
	 *
	 * Post request in WordPress will convert all values
	 * to string. Make sure image height and width are int.
	 * This is required only when Async requests are used.
	 * See - https://wordpress.org/support/topic/smushit-overwrites-image-meta-crop-sizes-as-string-instead-of-int/
	 *
	 * @since 2.8.0
	 *
	 * @param array $meta Metadata of attachment.
	 *
	 * @return array
	 */
	public static function format_meta_from_post( $meta = array() ) {
		// Do not continue in case meta is empty.
		if ( empty( $meta ) ) {
			return $meta;
		}

		// If metadata is array proceed.
		if ( is_array( $meta ) ) {

			// Walk through each items and format.
			array_walk_recursive( $meta, array( self::class, 'format_attachment_meta_item' ) );
		}

		return $meta;
	}

	/**
	 * If current item is width or height, make sure it is int.
	 *
	 * @since 2.8.0
	 *
	 * @param mixed  $value Meta item value.
	 * @param string $key Meta item key.
	 */
	public static function format_attachment_meta_item( &$value, $key ) {
		if ( 'height' === $key || 'width' === $key ) {
			$value = (int) $value;
		}

		/**
		 * Allows to format single item in meta.
		 *
		 * This filter will be used only for Async, post requests.
		 *
		 * @param mixed $value Meta item value.
		 * @param string $key Meta item key.
		 */
		$value = apply_filters( 'wp_smush_format_attachment_meta_item', $value, $key );
	}

	/**
	 * Check to see if file is animated.
	 *
	 * @since 3.0    Moved from class-resize.php
	 * @since 3.9.6  Add a new param $mime_type.
	 *
	 * @param string       $file_path  Image file path.
	 * @param int          $id         Attachment ID.
	 * @param false|string $mime_type  Mime type.
	 *
	 * @return bool|int
	 */
	public static function check_animated_status( $file_path, $id, $mime_type = false ) {
		$media_item = Media_Item_Cache::get_instance()->get( $id );

		return $media_item->is_animated();
	}

	public static function check_animated_file_contents( $file_path ) {
		$filecontents = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

		$str_loc = 0;
		$count   = 0;

		// There is no point in continuing after we find a 2nd frame.
		while ( $count < 2 ) {
			$where1 = strpos( $filecontents, "\x00\x21\xF9\x04", $str_loc );
			if ( false === $where1 ) {
				break;
			} else {
				$str_loc = $where1 + 1;
				$where2  = strpos( $filecontents, "\x00\x2C", $str_loc );
				if ( false === $where2 ) {
					break;
				} else {
					if ( $where2 === $where1 + 8 ) {
						$count ++;
					}
					$str_loc = $where2 + 1;
				}
			}
		}

		return $count > 1;
	}

	/**
	 * Verify the file size limit.
	 *
	 * @param int $attachment_id Attachment ID.
	 *
	 * Note: We only use this method to verify an image before smushing it,
	 * we still need to verify the file size of every thumbnail files while smushing them.
	 *
	 * @return bool|int Return the file size if the size limit is exceeded, otherwise return FALSE.
	 */
	public static function size_limit_exceeded( $attachment_id ) {
		$original_file_path = self::get_attached_file( $attachment_id, 'original' );
		if ( ! file_exists( $original_file_path ) ) {
			$original_file_path = self::get_attached_file( $attachment_id );
		}

		if ( ! file_exists( $original_file_path ) ) {
			return false;
		}
		$max_file_size = WP_Smush::is_pro() ? WP_SMUSH_PREMIUM_MAX_BYTES : WP_SMUSH_MAX_BYTES;
		$file_size     = filesize( $original_file_path );

		return $file_size > $max_file_size ? $file_size : false;
	}

	/**
	 * Original File path
	 *
	 * @param string $original_file  Original file.
	 *
	 * @return string File Path
	 */
	public static function original_file( $original_file = '' ) {
		$uploads     = wp_get_upload_dir();
		$upload_path = $uploads['basedir'];

		return path_join( $upload_path, $original_file );
	}

	/**
	 * Gets the WPMU DEV API key.
	 *
	 * @since 3.8.6
	 *
	 * @return string|false
	 */
	public static function get_wpmudev_apikey() {
		// If API key defined manually, get that.
		if ( defined( 'WPMUDEV_APIKEY' ) && WPMUDEV_APIKEY ) {
			return WPMUDEV_APIKEY;
		}

		// If dashboard plugin is active, get API key from db.
		if ( class_exists( 'WPMUDEV_Dashboard' ) ) {
			return get_site_option( 'wpmudev_apikey' );
		}

		return false;
	}

	/**
	 * Get upsell URL.
	 *
	 * @since 3.9.1
	 *
	 * @param string $utm_campaign  Campaing string.
	 *
	 * @return string
	 */
	public static function get_url( $utm_campaign = '' ) {
		$upgrade_url = 'https://wpmudev.com/project/wp-smush-pro/';

		return add_query_arg(
			array(
				'utm_source'   => 'smush',
				'utm_medium'   => 'plugin',
				'utm_campaign' => $utm_campaign,
			),
			$upgrade_url
		);
	}

	/**
	 * Get Smush page URL.
	 *
	 * @param string $page Page URL.
	 *
	 * @return string
	 */
	public static function get_page_url( $page = 'smush-bulk' ) {
		if ( is_multisite() && is_network_admin() ) {
			return network_admin_url( 'admin.php?page=' . $page );
		}

		return admin_url( 'admin.php?page=' . $page );
	}

	/**
	 * Get the extension of a file.
	 *
	 * @param string $file File path or file name.
	 * @param string $expected_ext The expected extension.
	 *
	 * @return bool|string Returns extension of the file, or false if it's not the same as the expected extension.
	 */
	public static function get_file_ext( $file, $expected_ext = '' ) {
		$ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
		if ( ! empty( $expected_ext ) ) {
			return $expected_ext === $ext ? $ext : false;
		} else {
			return $ext;
		}
	}

	/**
	 * Returns TRUE if the current request is REST API but is not media endpoint.
	 *
	 * @since 3.9.7
	 */
	public static function is_non_rest_media() {
		static $is_not_rest_media;
		if ( null === $is_not_rest_media ) {
			$is_not_rest_media = false;
			// We need to check if this call originated from Gutenberg and allow only media.
			if ( ! empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
				$route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] );

				// Only allow media routes.
				if ( empty( $route ) || '/wp/v2/media' !== $route ) {
					// If not - return image metadata.
					$is_not_rest_media = true;
				}
			}
		}
		return $is_not_rest_media;
	}

	/**
	 * Checks if user is allowed to perform the ajax actions.
	 * As previous we allowed for logged in user, so add a hook filter to allow
	 * user can custom the capability. It might also helpful when user custom admin menu via Branda.
	 *
	 * @since 3.13.0
	 *
	 * @param string $capability Capability default is manage_options.
	 * @return boolean
	 */
	public static function is_user_allowed( $capability = 'manage_options' ) {
		$capability = empty( $capability ) ? 'manage_options' : $capability;
		return current_user_can( apply_filters( 'wp_smush_admin_cap', $capability ) );
	}

	/*------ S3 Compatible Methods ------*/

	/**
	 * Return unfiltered path for Smush or restore.
	 *
	 * @since 3.9.6
	 *
	 * @param int    $attachment_id  Attachment ID.
	 * @param string $type           false|original|scaled|smush|backup|resize|check-resize.
	 * @param bool   $unfiltered     Whether to get unfiltered path or not.
	 *
	 * $type = original|backup => Try to get the original image file path.
	 * $type = false|smush     => Get the file path base on the setting "compress original".
	 * $type = scaled|resize   => Get the full file path, for large jpg it's scaled file not the original file.
	 *
	 * @return bool|string
	 */
	public static function get_raw_attached_file( $attachment_id, $type = 'smush', $unfiltered = false ) {
		if ( function_exists( 'wp_get_original_image_path' ) ) {
			if ( 'backup' === $type ) {
				$type = 'original';
			} elseif ( 'resize' === $type || 'check-resize' === $type ) {
				$type = 'scaled';
			}
			// We will get the original file if we are doing for backup or restore, or smush original file.
			if ( 'original' === $type || 'scaled' !== $type && Settings::get_instance()->get( 'original' ) ) {
				$file_path = wp_get_original_image_path( $attachment_id, $unfiltered );
			} else {
				$file_path = get_attached_file( $attachment_id, $unfiltered );
			}
		} else {
			$file_path = get_attached_file( $attachment_id, $unfiltered );
		}

		return $file_path;
	}

	/**
	 * Return file path for Smush, restore or checking resize.
	 *
	 * Add a hook for third party download the file,
	 * if it's not available on the server.
	 *
	 * @param int    $attachment_id  Attachment ID.
	 * @param string $type           false|original|smush|backup|resize
	 * $type = smush|backup  => Get the file path and download the attached file if it doesn't exist.
	 * $type = check-resize  => Get the file path ( if it exists ), or filtered file path if it doesn't exist.
	 * $type = original      => Only get the original file path (not scaled file).
	 * $type = scaled|resize => Get the full file path, for large jpg it's scaled file not the original file.
	 * $type = false         => Get the file path base on the setting "compress original".
	 *
	 * @since 3.9.6 Moved S3 to S3 integration.
	 * Add a hook filter to allow 3rd party to custom the result.
	 *
	 * @return bool|string
	 */
	public static function get_attached_file( $attachment_id, $type = 'smush' ) {
		if ( empty( $attachment_id ) ) {
			return false;
		}

		/**
		 * Add a hook to allow 3rd party to custom the result.
		 *
		 * @param null|string $file_path        File path or file url(checking resize).
		 * @param int         $attachment_id    Attachment ID.
		 * @param bool        $should_download  Should download the file if it doesn't exist.
		 * @param bool        $should_real_path Expecting a real file path instead an URL.
		 * @param string      $type             false|original|smush|backup|resize|scaled|check-resize.
		 *
		 * @usedby Smush\Core\Integrations\S3::get_attached_file
		 */
		// If the site is using S3, we only need to download the file when doing smush, backup or resizing.
		$should_download = in_array( $type, array( 'smush', 'backup', 'resize' ), true );
		// But when restoring/smushing we are expecting a real file path.
		$should_real_path = 'check-resize' !== $type;
		$file_path        = apply_filters( 'wp_smush_get_attached_file', null, $attachment_id, $should_download, $should_real_path, $type );

		if ( is_null( $file_path ) ) {
			$file_path = self::get_raw_attached_file( $attachment_id, $type );
		}

		return $file_path;
	}

	/**
	 * Custom for function wp_update_attachment_metadata
	 * We use this method to reset our S3 config before updating the metadata.
	 *
	 * @param int   $attachment_id Attachment ID.
	 * @param array $meta Metadata.
	 * @return bool
	 */
	public static function wp_update_attachment_metadata( $attachment_id, $meta ) {
		/**
		 * Fire before calling wp_update_attachment_metadata.
		 *
		 * @param int   $attachment_id Attachment ID.
		 * @param array $meta Metadata.
		 *
		 * @hooked Smush\Core\Integrations\S3::release_smush_mode()
		 * This will help we to upload the attachments, and remove them if it's required.
		 */
		do_action( 'wp_smush_before_update_attachment_metadata', $attachment_id, $meta );
		return wp_update_attachment_metadata( $attachment_id, $meta );
	}

	/**
	 * Check if the file exists on the server or cloud (S3).
	 *
	 * @since 3.9.6
	 *
	 * @param string|int $file  File path or File ID.
	 * @param int|null   $attachment_id File ID.
	 * @param bool       $should_download Whether to download the file or not.
	 * @param bool       $force_cache Whether check for result from the cache for full image or not.
	 *
	 * @return bool
	 */
	public static function file_exists( $file, $attachment_id = null, $should_download = false, $force_cache = false ) {
		// If file is an attachment id we will reset the arguments.
		// Use is_numeric for common case.
		if ( $file && is_numeric( $file ) ) {
			$attachment_id = $file;
			$file          = null;
		}

		// If the file path is not empty we will try to check file_exists first.
		if ( empty( $file ) ) {
			$file_exists = null;
		} else {
			$file_exists = file_exists( $file );
			if ( $file_exists ) {
				return true;
			}
		}

		// Only continue if provided Attachment ID.
		if ( $attachment_id < 1 ) {
			return false;
		}

		/**
		 * Check if there is a cached for full image.
		 */
		if ( null === $file && ! $force_cache ) {
			// Use different key for the download case.
			$cache_key = 'helper_file_exists' . intval( $should_download );

			$cached_file_exists = self::cache_get( $attachment_id, $cache_key );
			if ( null !== $cached_file_exists ) {
				return $cached_file_exists;
			}
		}

		/**
		 * Add a hook to allow 3rd party to custom the result.
		 *
		 * @param bool|null   $file_exists Current status.
		 * @param string|null $file Full file path.
		 * @param int         $attachment_id Attachment ID.
		 * @param bool        $should_download Whether to download the file if it's missing on the server or not.
		 *
		 * @usedby Smush\Core\Integrations\S3::file_exists_on_s3
		 */
		$file_exists = apply_filters( 'wp_smush_file_exists', $file_exists, $file, $attachment_id, $should_download );

		// If it doesn't check and file is null, we will try to get the attached file from $attachment_id to check.
		if ( is_null( $file_exists ) && ! $file ) {
			$file = get_attached_file( $attachment_id );
			if ( $file ) {
				$file_exists = file_exists( $file );
			}
		}

		/**
		 * Cache the result for full image,
		 * It also avoid we download again the not found image when enabling S3.
		 */
		if ( isset( $cache_key ) ) {
			return self::cache_set( $attachment_id, $file_exists, $cache_key );
		}

		return $file_exists;
	}

	/**
	 * Check if the file exists, will try to download if it is not on the server (e.g s3).
	 *
	 * @since 3.9.6
	 *
	 * @param string|int $file          File path or File ID.
	 * @param int|null   $attachment_id File ID.
	 *
	 * @return bool Returns TRUE if file exists on the server.
	 */
	public static function exists_or_downloaded( $file, $attachment_id = null ) {
		return self::file_exists( $file, $attachment_id, true );
	}

	/**
	 * Check if the file is an image, is supported in Smush and exists, and then cache the result.
	 *
	 * @since 3.9.6
	 *
	 * @param int|null $attachment_id File ID.
	 *
	 * @return bool|0 Returns TRUE if file is smushable, FALSE If the image does not exist, and 0 is not an image or is not supported
	 */
	public static function is_smushable( $attachment_id ) {
		if ( empty( $attachment_id ) ) {
			return null;// Nothing to check.
		}

		$is_smushable = self::cache_get( $attachment_id, 'is_smushable' );
		if ( ! is_null( $is_smushable ) ) {
			return $is_smushable;
		}
		// Set is_smushable is 0 (not false) to detect is not an image or image not found.
		$is_smushable = 0;
		$mime         = get_post_mime_type( $attachment_id );
		if (
			apply_filters( 'wp_smush_resmush_mime_supported', in_array( $mime, Core::$mime_types, true ), $mime )
			&& wp_attachment_is_image( $attachment_id )
		) {
			$is_smushable = self::file_exists( $attachment_id );
		}

		/**
		 * Cache and returns the result.
		 * Also added a hook for third-party.
		 *
		 * @param bool  $is_smushable   0 if is not an image or mime type not supported | TRUE if image exists and otherwise is FALSE.
		 * @param int   $attachment_id  Attachment ID.
		 * @param array $mime_types     List supported mime types.
		 */
		return apply_filters( 'wp_smush_is_smushable', self::cache_set( $attachment_id, $is_smushable, 'is_smushable' ), $attachment_id, Core::$mime_types );
	}

	/**
	 * Delete a file path from server and cloud (e.g s3).
	 *
	 * @since 3.9.6
	 *
	 * @param string|array $file_paths File path or list of file paths to remove.
	 * @param int          $attachment_id Attachment ID.
	 * @param bool         $only_exists_file Whether to call the action wp_smush_after_remove_file even the file doesn't exits or not.
	 *
	 * Current we only use this method to delete the file when after converting PNG to JPG or after restore, or when delete the files.
	 */
	public static function delete_permanently( $file_paths, $attachment_id, $only_exists_file = true ) {
		if ( empty( $file_paths ) ) {
			return;
		}
		$file_paths = (array) $file_paths;

		$removed = true;
		foreach ( $file_paths as $file_path ) {
			if ( file_exists( $file_path ) ) {
				if ( ! unlink( $file_path ) ) {
					$removed = false;
					// Log the error.
					self::logger()->error( sprintf( 'Cannot delete file [%s(%d)].', self::clean_file_path( $file_path ), $attachment_id ) );
				}
			}
		}

		if ( $removed || ! $only_exists_file ) {
			/**
			 * Fires after removing a file on server.
			 *
			 * @param int          $attachment_id Attachment ID.
			 * @param string|array $file_paths File path or list of file paths.
			 * @param bool         $removed Unlink status.
			 */
			do_action( 'wp_smush_after_remove_file', $attachment_id, $file_paths, $removed );
		}
	}

	/*------ End S3 Compatible Methods ------*/

	public static function get_image_sizes() {
		// Get from cache if available to avoid duplicate looping.
		$sizes = wp_cache_get( 'get_image_sizes', 'smush_image_sizes' );
		if ( $sizes ) {
			return $sizes;
		}

		return self::fetch_image_sizes();
	}

	public static function fetch_image_sizes() {
		global $_wp_additional_image_sizes;
		$additional_sizes = get_intermediate_image_sizes();
		$sizes            = array();

		if ( empty( $additional_sizes ) ) {
			return $sizes;
		}

		// Create the full array with sizes and crop info.
		foreach ( $additional_sizes as $_size ) {
			if ( in_array( $_size, array( 'thumbnail', 'medium', 'large' ), true ) ) {
				$sizes[ $_size ]['width']  = get_option( $_size . '_size_w' );
				$sizes[ $_size ]['height'] = get_option( $_size . '_size_h' );
				$sizes[ $_size ]['crop']   = (bool) get_option( $_size . '_crop' );
			} elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) {
				$sizes[ $_size ] = array(
					'width'  => $_wp_additional_image_sizes[ $_size ]['width'],
					'height' => $_wp_additional_image_sizes[ $_size ]['height'],
					'crop'   => $_wp_additional_image_sizes[ $_size ]['crop'],
				);
			}
		}

		// Medium Large.
		if ( ! isset( $sizes['medium_large'] ) || empty( $sizes['medium_large'] ) ) {
			$width  = (int) get_option( 'medium_large_size_w' );
			$height = (int) get_option( 'medium_large_size_h' );

			$sizes['medium_large'] = array(
				'width'  => $width,
				'height' => $height,
			);
		}

		// Set cache to avoid this loop next time.
		wp_cache_set( 'get_image_sizes', $sizes, 'smush_image_sizes' );

		return $sizes;
	}

	public static function loopback_supported() {
		$method_available = class_exists( '\WP_Site_Health' )
		                    && method_exists( '\WP_Site_Health', 'get_instance' )
		                    && method_exists( \WP_Site_Health::get_instance(), 'can_perform_loopback' );

		if ( $method_available ) {
			$loopback = \WP_Site_Health::get_instance()->can_perform_loopback();

			return $loopback->status === 'good';
		}

		return true;
	}

	public static function get_recheck_images_link() {
		if ( is_network_admin() ) {
			// Users can't run re-check images on the network admin side at the moment, @see: SMUSH-369.
			return '';
		}

		$recheck_images_link = add_query_arg(
			array( 'smush-action' => 'start-scan-media' ),
			self::get_page_url( 'smush-bulk' )
		);

		return $recheck_images_link;
	}
}