HEX
Server: LiteSpeed
System: Linux php-prod-1.spaceapp.ru 5.15.0-157-generic #167-Ubuntu SMP Wed Sep 17 21:35:53 UTC 2025 x86_64
User: xnsbb3110 (1041)
PHP: 8.1.33
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/wp-smushit/core/png2jpg/class-png2jpg-optimization.php
<?php

namespace Smush\Core\Png2Jpg;

use Smush\Core\Backups\Backups;
use Smush\Core\File_System;
use Smush\Core\Helper;
use Smush\Core\Media\Media_Item;
use Smush\Core\Media\Media_Item_Optimization;
use Smush\Core\Media\Media_Item_Size;
use Smush\Core\Media\Media_Item_Stats;
use Smush\Core\Settings;
use Smush\Core\Upload_Dir;
use WP_Error;

class Png2Jpg_Optimization extends Media_Item_Optimization {
	const KEY = 'png2jpg_optimization';
	const PNG2JPG_SAVINGS_KEY = 'wp-smush-pngjpg_savings';
	const CONVERTED_PNG_FILES_META = 'converted_png_files';
	const CONVERTED_LARGER_ERROR_KEY = 'converted_image_larger';
	const SKIP_META_KEY = 'skip_png2jpg';
	/**
	 * @var Media_Item
	 */
	private $media_item;
	/**
	 * @var array
	 */
	private $meta;
	/**
	 * @var Media_Item_Stats[]
	 */
	private $size_stats = array();

	private $converted_png_files;

	private $reset_properties = array(
		'meta',
		'size_stats',
		'converted_png_files',
	);

	private $logger;
	private $backups;
	/**
	 * @var WP_Error
	 */
	private $errors;
	/**
	 * @var Settings
	 */
	private $settings;
	/**
	 * @var Png2Jpg_Helper
	 */
	private $helper;
	/**
	 * @var File_System
	 */
	private $fs;
	/**
	 * @var Upload_Dir
	 */
	private $upload_dir;

	/**
	 * @var bool
	 */
	private $skip_convert;

	public function __construct( $media_item ) {
		$this->media_item = $media_item;
		$this->logger     = Helper::logger()->png2jpg();
		$this->backups    = new Backups();
		$this->settings   = Settings::get_instance();
		$this->errors     = new WP_Error();
		$this->helper     = new Png2Jpg_Helper();
		$this->fs         = new File_System();
		$this->upload_dir = new Upload_Dir();
	}

	public function get_key() {
		return self::KEY;
	}

	public function get_name() {
		return __( 'PNG to JPG', 'wp-smushit' );
	}

	public function get_stats() {
		$stats       = new Media_Item_Stats();
		$size_before = 0;
		$size_after  = 0;
		foreach ( $this->get_sizes_to_convert( $this->media_item ) as $size_key => $size ) {
			$size_stats  = $this->get_size_stats( $size_key );
			$size_before += $size_stats->get_size_before();
			$size_after  += $size_stats->get_size_after();
		}

		if ( empty( $size_before ) || empty( $size_after ) ) {
			return $stats;
		}

		$stats->set_size_before( $size_before );
		$stats->set_size_after( $size_after );

		return $stats;
	}

	public function get_size_stats( $size_key ) {
		if ( empty( $this->size_stats[ $size_key ] ) ) {
			$this->size_stats[ $size_key ] = $this->prepare_size_stats( $size_key );
		}

		return $this->size_stats[ $size_key ];
	}

	public function save() {
		$meta = $this->make_meta();
		if ( ! empty( $meta ) ) {
			update_post_meta( $this->media_item->get_id(), self::PNG2JPG_SAVINGS_KEY, $meta );
			$this->reset();
		}
	}

	/**
	 * @return array
	 */
	private function make_meta() {
		$meta = array();

		if ( $this->skip_conversion() ) {
			$meta[ self::SKIP_META_KEY ] = true;

			return $meta;
		}

		foreach ( $this->get_sizes_to_convert( $this->media_item ) as $size_key => $size ) {
			$size_stats = $this->get_size_stats( $size_key );
			if ( ! $size_stats->is_empty() ) {
				$meta[ $size_key ] = $size_stats->to_array();
			}
		}

		if ( ! empty( $this->get_converted_png_files() ) ) {
			$meta[ self::CONVERTED_PNG_FILES_META ] = $this->get_converted_png_files();
		}

		return $meta;
	}

	public function is_optimized() {
		return ! $this->get_stats()->is_empty();
	}

	public function should_optimize() {
		if (
			$this->is_skipped()
			|| $this->media_item->has_errors()
			|| ! $this->settings->is_png2jpg_module_active()
		) {
			return false;
		}

		return $this->can_be_converted( $this->media_item );
	}

	private function is_skipped() {
		if ( $this->media_item->is_skipped() ) {
			return true;
		}

		return $this->skip_conversion();
	}

	public function skip_conversion() {
		if ( is_null( $this->skip_convert ) ) {
			$this->skip_convert = $this->prepare_skip_conversion();
		}

		return $this->skip_convert;
	}

	private function prepare_skip_conversion() {
		$meta = $this->get_meta();
		return ! empty( $meta[ self::SKIP_META_KEY ] );
	}

	private function set_skip_convert( $skip_convert ) {
		$this->skip_convert = (bool) $skip_convert;
	}

	public function should_reoptimize() {
		// PNG 2 JPG conversion happens only once so this is the same as should_optimize
		return $this->should_optimize();
	}

	public function optimize() {
		if ( ! $this->should_optimize() ) {
			return false;
		}

		return $this->convert_media_item( $this->media_item );
	}

	private function get_meta() {
		if ( is_null( $this->meta ) ) {
			$this->meta = $this->fetch_meta();
		}

		return $this->meta;
	}

	private function fetch_meta() {
		$post_meta = get_post_meta( $this->media_item->get_id(), self::PNG2JPG_SAVINGS_KEY, true );

		return empty( $post_meta ) || ! is_array( $post_meta )
			? array()
			: $post_meta;
	}

	private function get_size_meta( $size_key ) {
		$meta = $this->get_meta();
		$size = empty( $meta[ $size_key ] )
			? array()
			: $meta[ $size_key ];

		return empty( $size ) || ! is_array( $size )
			? array()
			: $size;
	}

	private function prepare_size_stats( $size_key ) {
		$stats = new Media_Item_Stats();
		$stats->from_array( $this->get_size_meta( $size_key ) );

		return $stats;
	}

	/**
	 * @param $media_item Media_Item
	 *
	 * @return boolean
	 */
	private function can_be_converted( $media_item ) {
		$id        = $media_item->get_id();
		$full_size = $media_item->get_full_or_scaled_size();
		if ( ! $full_size ) {
			$this->logger->info( sprintf( 'Image [%d] does not have a full size.', $id ) );

			return false;
		}

		$file = $media_item->get_full_or_scaled_size()->get_file_path();
		if ( ! $media_item->is_png() ) {
			$this->logger->info( sprintf( 'File [%s(%d)] does not have the PNG mime-type.', $file, $id ) );

			return false;
		}

		if ( ! $this->helper->supports_imagick() && ! $this->helper->supports_gd() ) {
			$this->logger->warning( 'The site does not support Imagick or GD.' );

			return false;
		}

		$is_optimized = $this->is_optimized();
		if ( $is_optimized ) {
			$this->logger->info( sprintf( 'File [%s(%d)] already tried the conversion.', $file, $id ) );

			return false;
		}

		/**
		 * Filter whether to convert the PNG to JPG or not
		 *
		 * @param bool $should_convert Current choice for image conversion
		 * @param int $id Attachment id
		 * @param string $file File path for the image
		 * @param string $size Image size being converted
		 *
		 * @since 2.4
		 */
		return apply_filters( 'wp_smush_convert_to_jpg', ! $media_item->is_transparent(), $id, $file, 'full' );
	}

	/**
	 * @param $media_item Media_Item
	 *
	 * @return boolean
	 */
	private function convert_media_item( $media_item ) {
		$old_urls               = $media_item->get_size_urls();
		$converted_source_files = array();
		$converted_sizes        = array();
		$sizes                  = $this->get_sizes_to_convert( $media_item ); // We must convert all sizes, regardless of which sizes the user has selected for smushing
		$full_size              = $media_item->get_full_or_scaled_size();
		$full_size_key          = $full_size->get_key();
		$old_main_file_name     = $full_size->get_file_name();
		$new_main_file_name     = $this->get_main_jpg_file_name();

		// Make sure full size is at the top.
		unset( $sizes[ $full_size_key ] );
		$sizes = array_merge(
			array(
				$full_size_key => $full_size,
			),
			$sizes
		);

		foreach ( $sizes as $size_key => $size ) {
			$size_file_path         = $size->get_file_path();
			$file_already_converted = ! empty( $converted_source_files[ $size_file_path ] );
			$new_size_file_name     = $this->get_size_jpg_file_name( $size, $old_main_file_name, $new_main_file_name );
			if ( $file_already_converted ) {
				// The file for the current size was already converted under a different size, just copy the stats.
				$this->copy_size(
					$media_item,
					$converted_source_files[ $size_file_path ],
					$size_key
				);
				$converted_sizes[] = $size_key;
			} else {
				// Convert the file for the current size.
				$size_converted = $this->convert_size( $media_item, $size, $new_size_file_name );
				if ( ! $size_converted ) {
					break;
				}

				$converted_sizes[]                         = $size_key;
				$converted_source_files[ $size_file_path ] = $size_key;
			}
		}

		if ( count( $converted_sizes ) === count( $sizes ) ) {
			$png_file_paths = array_flip( $converted_source_files );

			// All sizes successful, save media item.
			$media_item->set_mime_type( 'image/jpeg' );
			$media_item->save();

			// Save optimization data.
			$this->set_converted_png_files( $this->relative_paths( $png_file_paths ) );
			$this->save();

			$media_item_stats = $this->get_stats();
			do_action(
				'wp_smush_png_jpg_converted',
				$media_item->get_id(),
				$media_item->get_wp_metadata(),
				$media_item_stats ? $media_item_stats->to_array() : array(),
				$png_file_paths
			);

			$this->replace_urls_in_content( $old_urls, $media_item->get_size_urls() );
			$this->delete_files( $png_file_paths );

			return true;
		} else {
			// We can't have some sizes as pngs and others as jpgs, if this is the case then we must delete the successfully converted files
			$converted_files_to_delete = array_map( function ( $converted_size ) use ( $media_item ) {
				return $media_item->get_size( $converted_size )->get_file_path();
			}, $converted_sizes );
			$this->delete_files( $converted_files_to_delete );

			$error_codes = $this->get_errors()->get_error_codes();
			if ( in_array( self::CONVERTED_LARGER_ERROR_KEY, $error_codes ) ) {
				$this->set_skip_convert( true );

				$this->save();
			}

			// Reset all the changes made so far.
			$media_item->reset();
			$this->reset();

			return false;
		}
	}

	/**
	 * @param $media_item Media_Item
	 * @param $media_item_size Media_Item_Size
	 * @param $new_file_name
	 *
	 * @return boolean
	 */
	private function convert_size( $media_item, $media_item_size, $new_file_name ) {
		$new_file_path = $media_item->get_dir() . $new_file_name;

		$result = $this->write_file_for_size( $media_item, $media_item_size, $new_file_path );
		if ( ! $result || empty( $result['filesize'] ) ) {
			return false;
		}

		$size_before           = $media_item_size->get_filesize();
		$size_after            = $result['filesize'];
		$is_full_size          = $media_item->has_full_size() && ( $media_item_size->get_key() === $media_item->get_full_size()->get_key() );
		$converted_file_larger = $size_after > $size_before;
		if (
			$is_full_size               // If this is the full size we don't want the converted file to be larger than original
			&& $converted_file_larger
			&& ! apply_filters( 'wp_smush_png2jpg_allow_larger_converted_file', false )
		) {
			$this->fs->unlink( $new_file_path );
			$this->add_error(
				$media_item_size->get_key(),
				self::CONVERTED_LARGER_ERROR_KEY,
				__( 'Skipped: Smushed file is larger than the original file.', 'wp-smushit' )
			);

			$this->logger->error(
				sprintf(
				/* translators: 1: Converted path, 2: Converted file size, 3: Original path, 4: Original file size */
					__( 'The new file [%1$s](%2$s) is larger than the original file [%3$s](%4$s).', 'wp-smushit' ),
					$this->upload_dir->get_human_readable_path( $new_file_path ),
					size_format( $size_after ),
					$this->upload_dir->get_human_readable_path( $media_item_size->get_file_path() ),
					size_format( $size_before )
				)
			);

			return false;
		}

		$media_item_size->set_file_name( $new_file_name );
		$media_item_size->set_mime_type( 'image/jpeg' );
		$media_item_size->set_filesize( $size_after );

		$size_stats = $this->get_size_stats( $media_item_size->get_key() );
		$size_stats->set_size_before( $size_before );
		$size_stats->set_size_after( $size_after );

		$this->logger->info( sprintf( 'Image [%s] converted from PNG with size [%d] to JPG with size [%d].', $new_file_name, $size_before, $size_after ) );

		do_action(
			'wp_smush_png_jpg_size_converted',
			$media_item->get_id(),
			$media_item_size->get_key(),
			$size_stats->to_array()
		);

		return true;
	}

	/**
	 * @param $media_item Media_Item
	 * @param $media_item_size Media_Item_Size
	 * @param $new_file_path string
	 *
	 * @return array|false
	 */
	private function write_file_for_size( $media_item, $media_item_size, $new_file_path ) {
		$editor = wp_get_image_editor( $media_item_size->get_file_path() );

		$this->logger->info( sprintf( 'Image editor [%s] selected for PNG 2 JPG conversion.', get_class( $editor ) ) );

		if ( is_wp_error( $editor ) ) {
			$this->add_error(
				$media_item_size->get_key(),
				'image_load_error',
				sprintf(
				/* translators: 1: Image path, 2: Image id, 3: Error message. */
					__( 'Image Editor cannot load file [%1$s(%2$d)]: %3$s.', 'wp-smushit' ),
					$this->upload_dir->get_human_readable_path( $media_item_size->get_file_path() ),
					$media_item->get_id(),
					$editor->get_error_message()
				)
			);

			return false;
		}

		$new_image_data = $editor->save( $new_file_path, 'image/jpeg' );
		if ( is_wp_error( $new_image_data ) ) {
			$this->add_error(
				$media_item_size->get_key(),
				'image_save_error',
				/* translators: %s: Error message. */
				sprintf( __( 'The image editor was unable to save the image: %s', 'wp-smushit' ), $new_image_data->get_error_message() )
			);

			return false;
		}

		return $new_image_data;
	}

	private function delete_files( $files ) {
		foreach ( $files as $file ) {
			if ( $this->fs->file_exists( $file ) ) {
				// TODO: create an S3 compatible method for this
				$this->fs->unlink( $file );
			}
		}
	}

	/**
	 * TODO: add the action wp_smush_image_url_updated
	 *
	 * @param $old_url
	 * @param $new_url
	 *
	 * @return bool
	 */
	private function replace_url_in_content( $old_url, $new_url ) {
		global $wpdb;
		$wild         = '%';
		$old_url_like = $wild . $wpdb->esc_like( $old_url ) . $wild;
		$query        = $wpdb->prepare( "SELECT ID, post_content FROM $wpdb->posts WHERE post_content LIKE %s", $old_url_like );
		$rows         = $wpdb->get_results( $query );
		if ( empty( $rows ) || ! is_array( $rows ) ) {
			return true;
		}

		$update_count = 0;
		foreach ( $rows as $row ) {
			// Replace old URLs with new URLs.
			$post_content = $row->post_content;
			$post_content = str_replace( $old_url, $new_url, $post_content );
			// Update Post content.
			$updated = $wpdb->update(
				$wpdb->posts,
				array( 'post_content' => $post_content ),
				array( 'ID' => $row->ID )
			);
			if ( $updated ) {
				$update_count ++;
			}
			clean_post_cache( $row->ID );
		}

		// TODO: do something with this return value, see what we were doing in the legacy code
		return $update_count === count( $rows );
	}

	public function reset() {
		foreach ( $this->reset_properties as $property ) {
			$this->$property = null;
		}
	}

	private function copy_size( $media_item, $source_size_key, $destination_size_key ) {
		$source_size  = $media_item->get_size( $source_size_key );
		$source_stats = $this->get_size_stats( $source_size_key );

		$destination_size  = $media_item->get_size( $destination_size_key );
		$destination_stats = $this->get_size_stats( $destination_size_key );

		$destination_size->set_file_name( $source_size->get_file_name() );
		$destination_size->set_mime_type( $source_size->get_mime_type() );
		$destination_size->set_filesize( $source_stats->get_size_after() );

		$destination_stats->from_array( $source_stats->to_array() );
	}

	/**
	 * @param array $old_urls
	 * @param array $new_urls
	 *
	 * @return void
	 */
	private function replace_urls_in_content( $old_urls, $new_urls ) {
		foreach ( $old_urls as $size_key => $old_size_url ) {
			if ( empty( $new_urls[ $size_key ] ) ) {
				continue;
			}
			$new_size_url = $new_urls[ $size_key ];
			$this->replace_url_in_content( $old_size_url, $new_size_url );
		}
	}

	public function delete_data() {
		delete_post_meta( $this->media_item->get_id(), self::PNG2JPG_SAVINGS_KEY );

		$this->reset();
	}

	public function should_optimize_size( $size ) {
		if ( ! $this->should_optimize() ) {
			return false;
		}

		return array_key_exists(
			$size->get_key(),
			$this->get_sizes_to_convert( $this->media_item )
		);
	}

	/**
	 * @param Media_Item $media_item
	 *
	 * @return array|Media_Item_Size[]
	 */
	private function get_sizes_to_convert( $media_item ) {
		return $media_item->get_sizes();
	}

	public function get_converted_png_files() {
		if ( is_null( $this->converted_png_files ) ) {
			$this->converted_png_files = $this->prepare_converted_png_files();
		}

		return $this->converted_png_files;
	}

	private function prepare_converted_png_files() {
		$meta = $this->get_meta();

		return empty( $meta[ self::CONVERTED_PNG_FILES_META ] )
			? array()
			: $meta[ self::CONVERTED_PNG_FILES_META ];
	}

	public function set_converted_png_files( $converted_png_files ) {
		$this->converted_png_files = $converted_png_files;
	}

	public function can_restore() {
		// If it is optimized then we can restore it
		return $this->is_optimized();
	}

	public function restore() {
		$media_item        = $this->media_item;
		$jpg_urls          = $media_item->get_size_urls();
		$jpg_paths         = $media_item->get_size_paths();
		$restore_file_path = $this->get_restore_file_path();
		$after_restore     = function ( $restored ) use ( $media_item, $jpg_urls, $jpg_paths, $restore_file_path ) {
			// We are doing these changes in a callback so that the other callbacks hooked to wp_smush_after_restore_backup will receive the most up-to-date state of the media item
			if ( $restored ) {
				// Undo all the changes we did during the conversion
				$media_item->get_main_size()->set_file_name( basename( $restore_file_path ) );
				$media_item->set_mime_type( 'image/png' );
				$media_item->save();

				do_action( 'wp_smush_after_restore_png_jpg', $media_item, $jpg_paths, $jpg_urls );

				$this->replace_urls_in_content( $jpg_urls, $media_item->get_size_urls() );
				$this->delete_files( $jpg_paths );

				/**
				 * The DB data will be deleted by {@see delete_data}
				 */
			}
		};

		add_action( 'wp_smush_after_restore_backup', $after_restore, - 10 );
		$restored = $this->backups->restore_backup_to_file_path( $media_item, $restore_file_path );
		remove_action( 'wp_smush_after_restore_backup', $after_restore );

		return $restored;
	}

	private function get_restore_file_path() {
		$media_item          = $this->media_item;
		$converted_png_files = $this->get_converted_png_files();
		$default_backup_size = $media_item->get_default_backup_size();
		$check_file_exists   = true;

		if ( ! empty( $converted_png_files['full'] ) ) {
			// First try to get the original file name from converted file meta
			$file_name = basename( $converted_png_files['full'] );
		} elseif ( $default_backup_size ) {
			// If converted meta didn't work out then
			$backup_file_path = $default_backup_size->get_file_path();
			if ( strpos( $backup_file_path, '.bak' ) ) {
				$backup_file_path = str_replace( '.bak', '', $backup_file_path );
			} else {
				// If the default backup path does not have .bak extension then we don't need to do wp_unique_filename because we know that the file at the restore path is our file
				$check_file_exists = false;
			}
			$file_name = basename( $backup_file_path );
		} else {
			$full_size = $media_item->get_full_size();
			$file_name = $full_size->get_file_name_without_extension() . '.png';
		}

		$restore_file_path = path_join( $media_item->get_dir(), $file_name );
		if ( $check_file_exists && $this->fs->file_exists( $restore_file_path ) ) {
			$unique_filename = wp_unique_filename( $media_item->get_dir(), $file_name );

			return path_join( $media_item->get_dir(), $unique_filename );
		} else {
			return $restore_file_path;
		}
	}

	private function add_error( $size_key, $code, $message ) {
		// Log the error
		$this->logger->error( $message );
		// Add the error

		if ( $size_key ) {
			$message = "[$size_key] $message";
		}
		$this->errors->add( $code, $message );
	}

	public function get_errors() {
		return $this->errors;
	}

	private function relative_paths( $absolute_paths ) {
		$relative_paths = array();
		foreach ( $absolute_paths as $key => $absolute_path ) {
			$dir                    = $this->media_item->get_relative_file_dir();
			$file                   = wp_basename( $absolute_path );
			$relative_paths[ $key ] = "{$dir}$file";
		}

		return $relative_paths;
	}

	private function get_main_jpg_file_name() {
		$media_item     = $this->media_item;
		$full_size      = $media_item->get_full_or_scaled_size();
		$png_extension  = '.' . pathinfo( $full_size->get_file_path(), PATHINFO_EXTENSION );
		$full_file_name = str_replace( $png_extension, '.jpg', $full_size->get_file_name() );

		return $this->fs->file_exists( $media_item->get_dir() . $full_file_name )
			? wp_unique_filename( $media_item->get_dir(), $full_file_name )
			: $full_file_name;
	}

	/**
	 * @param $size Media_Item_Size
	 * @param $main_png_file_name string
	 * @param $main_jpg_file_name string
	 *
	 * @return string
	 */
	private function get_size_jpg_file_name( $size, $main_png_file_name, $main_jpg_file_name ) {
		$png_extension   = '.' . pathinfo( $main_png_file_name, PATHINFO_EXTENSION );
		$png_without_ext = str_replace( $png_extension, '', $main_png_file_name );
		$jpg_without_ext = str_replace( '.jpg', '', $main_jpg_file_name );
		$size_file_name  = str_replace( $png_without_ext, $jpg_without_ext, $size->get_file_name() );
		$size_file_name  = str_replace( $png_extension, '.jpg', $size_file_name );

		return $this->fs->file_exists( $size->get_dir() . $size_file_name )
			? wp_unique_filename( $size->get_dir(), $size_file_name )
			: $size_file_name;
	}

	public function get_optimized_sizes_count() {
		$count = 0;
		$sizes = $this->get_sizes_to_convert( $this->media_item );
		foreach ( $sizes as $size_key => $size ) {
			$size_stats = $this->get_size_stats( $size_key );
			if ( $size_stats && ! $size_stats->is_empty() ) {
				$count ++;
			}
		}

		return $count;
	}
}