checkerClass === null ) {
trigger_error(
sprintf(
'PUC %s does not support updates for %ss %s',
htmlentities(self::$latestCompatibleVersion),
strtolower($type),
$service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
),
E_USER_ERROR
);
return null;
}
//Add the current namespace to the class name(s).
if ( version_compare(PHP_VERSION, '5.3', '>=') ) {
$checkerClass = __NAMESPACE__ . '\\' . $checkerClass;
}
if ( !isset($apiClass) ) {
//Plain old update checker.
return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
} else {
//VCS checker + an API client.
$apiClass = self::getCompatibleClassVersion($apiClass);
if ( $apiClass === null ) {
trigger_error(sprintf(
'PUC %s does not support %s',
htmlentities(self::$latestCompatibleVersion),
htmlentities($service)
), E_USER_ERROR);
return null;
}
if ( version_compare(PHP_VERSION, '5.3', '>=') && (strpos($apiClass, '\\') === false) ) {
$apiClass = __NAMESPACE__ . '\\' . $apiClass;
}
return new $checkerClass(
new $apiClass($metadataUrl),
$id,
$slug,
$checkPeriod,
$optionName,
$muPluginFile
);
}
}
/**
*
* Normalize a filesystem path. Introduced in WP 3.9.
* Copying here allows use of the class on earlier versions.
* This version adapted from WP 4.8.2 (unchanged since 4.5.0)
*
* @param string $path Path to normalize.
* @return string Normalized path.
*/
public static function normalizePath($path) {
if ( function_exists('wp_normalize_path') ) {
return wp_normalize_path($path);
}
$path = str_replace('\\', '/', $path);
$path = preg_replace('|(?<=.)/+|', '/', $path);
if ( substr($path, 1, 1) === ':' ) {
$path = ucfirst($path);
}
return $path;
}
/**
* Check if the path points to a plugin file.
*
* @param string $absolutePath Normalized path.
* @return bool
*/
protected static function isPluginFile($absolutePath) {
//Is the file inside the "plugins" or "mu-plugins" directory?
$pluginDir = self::normalizePath(WP_PLUGIN_DIR);
$muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
return true;
}
//Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
if ( !is_file($absolutePath) ) {
return false;
}
//Does it have a valid plugin header?
//This is a last-ditch check for plugins symlinked from outside the WP root.
if ( function_exists('get_file_data') ) {
$headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
return !empty($headers['Name']);
}
return false;
}
/**
* Get the name of the theme's directory from a full path to a file inside that directory.
* E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
*
* Note that subdirectories are currently not supported. For example,
* "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
*
* @param string $absolutePath Normalized path.
* @return string|null Directory name, or NULL if the path doesn't point to a theme.
*/
protected static function getThemeDirectoryName($absolutePath) {
if ( is_file($absolutePath) ) {
$absolutePath = dirname($absolutePath);
}
if ( file_exists($absolutePath . '/style.css') ) {
return basename($absolutePath);
}
return null;
}
/**
* Get the service URI from the file header.
*
* @param string $fullPath
* @return string
*/
private static function getServiceURI($fullPath) {
//Look for the URI
if ( is_readable($fullPath) ) {
$seek = array(
'github' => 'GitHub URI',
'gitlab' => 'GitLab URI',
'bucket' => 'BitBucket URI',
);
$seek = apply_filters('puc_get_source_uri', $seek);
$data = get_file_data($fullPath, $seek);
foreach ($data as $key => $uri) {
if ( $uri ) {
return $uri;
}
}
}
//URI was not found so throw an error.
throw new RuntimeException(
sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath))
);
}
/**
* Get the name of the hosting service that the URL points to.
*
* @param string $metadataUrl
* @return string|null
*/
private static function getVcsService($metadataUrl) {
$service = null;
//Which hosting service does the URL point to?
$host = parse_url($metadataUrl, PHP_URL_HOST);
$path = parse_url($metadataUrl, PHP_URL_PATH);
//Check if the path looks like "/user-name/repository".
//For GitLab.com it can also be "/user/group1/group2/.../repository".
$repoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
if ( $host === 'gitlab.com' ) {
$repoRegex = '@^/?(?:[^/#?&]++/){1,20}(?:[^/#?&]++)/?$@';
}
if ( preg_match($repoRegex, $path) ) {
$knownServices = array(
'github.com' => 'GitHub',
'bitbucket.org' => 'BitBucket',
'gitlab.com' => 'GitLab',
);
if ( isset($knownServices[$host]) ) {
$service = $knownServices[$host];
}
}
return apply_filters('puc_get_vcs_service', $service, $host, $path, $metadataUrl);
}
/**
* Get the latest version of the specified class that has the same major version number
* as this factory class.
*
* @param string $class Partial class name.
* @return string|null Full class name.
*/
protected static function getCompatibleClassVersion($class) {
if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
return self::$classVersions[$class][self::$latestCompatibleVersion];
}
return null;
}
/**
* Get the specific class name for the latest available version of a class.
*
* @param string $class
* @return null|string
*/
public static function getLatestClassVersion($class) {
if ( !self::$sorted ) {
self::sortVersions();
}
if ( isset(self::$classVersions[$class]) ) {
return reset(self::$classVersions[$class]);
} else {
return null;
}
}
/**
* Sort available class versions in descending order (i.e. newest first).
*/
protected static function sortVersions() {
foreach ( self::$classVersions as $class => $versions ) {
uksort($versions, array(__CLASS__, 'compareVersions'));
self::$classVersions[$class] = $versions;
}
self::$sorted = true;
}
protected static function compareVersions($a, $b) {
return -version_compare($a, $b);
}
/**
* Register a version of a class.
*
* @access private This method is only for internal use by the library.
*
* @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
* @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
* @param string $version Version number, e.g. '1.2'.
*/
public static function addVersion($generalClass, $versionedClass, $version) {
if ( empty(self::$myMajorVersion) ) {
$nameParts = explode('_', __CLASS__, 3);
self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
}
//Store the greatest version number that matches our major version.
$components = explode('.', $version);
if ( $components[0] === self::$myMajorVersion ) {
if (
empty(self::$latestCompatibleVersion)
|| version_compare($version, self::$latestCompatibleVersion, '>')
) {
self::$latestCompatibleVersion = $version;
}
}
if ( !isset(self::$classVersions[$generalClass]) ) {
self::$classVersions[$generalClass] = array();
}
self::$classVersions[$generalClass][$version] = $versionedClass;
self::$sorted = false;
}
}
endif;
h = 'png2jpg';
}
return $should_resmush;
}
/**
* Update the image URL, MIME Type, Attached File, file path in Meta, URL in post content
*
* @param string $id Attachment ID.
* @param string $o_file Original File Path that has to be replaced.
* @param string $n_file New File Path which replaces the old file.
* @param string $meta Attachment Meta.
* @param string $size_k Image Size.
* @param string $o_type Operation Type "conversion", "restore".
*
* @return array Attachment Meta with updated file path.
*/
public function update_image_path( $id, $o_file, $n_file, $meta, $size_k, $o_type = 'conversion' ) {
// Upload Directory.
$upload_dir = wp_upload_dir();
// Upload Path.
$upload_path = trailingslashit( $upload_dir['basedir'] );
$dir_name = pathinfo( $o_file, PATHINFO_DIRNAME );
// Full Path to new file.
$n_file_path = path_join( $dir_name, $n_file );
// Current URL for image.
$o_url = wp_get_attachment_url( $id );
// Update URL for image size.
if ( 'full' !== $size_k ) {
$base_url = dirname( $o_url );
$o_url = $base_url . '/' . basename( $o_file );
}
// Update File path, Attached File, GUID.
$meta = empty( $meta ) ? wp_get_attachment_metadata( $id ) : $meta;
$mime = Helper::get_mime_type( $n_file_path );
/**
* If there's no fileinfo extension installed, the mime type will be returned as false.
* As a fallback, we set it manually.
*
* @since 3.8.3
*/
if ( false === $mime ) {
$mime = 'conversion' === $o_type ? 'image/jpeg' : 'image/png';
}
$del_file = true;
// Update File Path, Attached file, Mime Type for Image.
if ( 'full' === $size_k ) {
if ( ! empty( $meta ) ) {
$new_file = str_replace( $upload_path, '', $n_file_path );
$meta['file'] = $new_file;
// Update Attached File.
if ( ! update_attached_file( $id, $meta['file'] ) ) {
$del_file = false;
}
}
// Update Mime type.
if ( ! wp_update_post(
array(
'ID' => $id,
'post_mime_type' => $mime,
)
) ) {
$del_file = false;
}
} else {
$meta['sizes'][ $size_k ]['file'] = basename( $n_file );
$meta['sizes'][ $size_k ]['mime-type'] = $mime;
}
// To be called after the attached file key is updated for the image.
if ( ! $this->update_image_url( $id, $size_k, $n_file, $o_url ) ) {
$del_file = false;
}
/**
* Delete the Original files if backup not enabled
* We only delete the file if we don't have any issues while updating the DB.
* SMUSH-1088?focusedCommentId=92914.
*/
if ( $del_file && 'conversion' === $o_type ) {
// We might need to backup the full size file, will delete it later if we don't need to use it for backup.
if ( 'full' !== $size_k ) {
/**
* We only need to keep the original file as a backup file.
* and try to delete this file on cloud too, e.g S3.
*/
Helper::delete_permanently( $o_file, $id );
}
}
return $meta;
}
/**
* Replace the file if there are savings, and return savings
*
* @param string $file Original File Path.
* @param array $result Array structure.
* @param string $n_file Updated File path.
*
* @return array
*/
private function replace_file( $file = '', $result = array(), $n_file = '' ) {
if ( empty( $file ) || empty( $n_file ) ) {
return $result;
}
// Get the file size of original image.
$o_file_size = filesize( $file );
$n_file = path_join( dirname( $file ), $n_file );
$n_file_size = filesize( $n_file );
// If there aren't any savings return.
if ( $n_file_size >= $o_file_size ) {
// Delete the JPG image and return.
unlink( $n_file );
Helper::logger()->png2jpg()->notice( sprintf( 'The new file [%s](%s) is larger than the original file [%s](%s).', Helper::clean_file_path( $n_file ), size_format( $n_file_size ), Helper::clean_file_path( $file ), size_format( $o_file_size ) ) );
return $result;
}
// Get the savings.
$savings = $o_file_size - $n_file_size;
// Store Stats.
$savings = array(
'bytes' => $savings,
'size_before' => $o_file_size,
'size_after' => $n_file_size,
);
$result['savings'] = $savings;
return $result;
}
/**
* Perform the conversion process, using WordPress Image Editor API
*
* @param string $id Attachment ID.
* @param string $file Attachment File path.
* @param string $meta Attachment meta.
* @param string $size Image size, default empty for full image.
*
* @return array $result array(
* 'meta' => array Update Attachment metadata
* 'savings' => Reduction of Image size in bytes
* )
*/
private function convert_to_jpg( $id = '', $file = '', $meta = '', $size = 'full' ) {
$result = array(
'meta' => $meta,
'savings' => '',
);
// Flag: Whether the image was converted or not.
if ( 'full' === $size ) {
$result['converted'] = false;
}
// If any of the values is not set.
if ( empty( $id ) || empty( $file ) || empty( $meta ) || ! file_exists( $file ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'Meta file [%s(%d)] is empty or file not found.', Helper::clean_file_path( $file ), $id ) );
return $result;
}
$editor = wp_get_image_editor( $file );
if ( is_wp_error( $editor ) ) {
// Use custom method maybe.
Helper::logger()->png2jpg()->error( sprintf( 'Image Editor cannot load file [%s(%d)]: %s.', Helper::clean_file_path( $file ), $id, $editor->get_error_message() ) );
return $result;
}
$n_file = pathinfo( $file );
if ( ! empty( $n_file['filename'] ) && $n_file['dirname'] ) {
// Get a unique File name.
$file_detail = Helper::cache_get( $id, 'convert_to_jpg' );
if ( $file_detail ) {
list( $old_main_filename, $new_main_filename ) = $file_detail;
/**
* Thumbnail name.
* E.g.
* test-150x150.jpg
* test-1-150x150.jpg
*/
if ( $old_main_filename !== $new_main_filename ) {
$n_file['filename'] = str_replace( $old_main_filename, $new_main_filename, $n_file['filename'] );
}
$n_file['filename'] .= '.jpg';
} else {
$org_filename = $n_file['filename'];
/**
* Get unique file name for the main file.
* E.g.
* test.png => test.jpg
* test.png => test-1.jpg
*/
$n_file['filename'] = wp_unique_filename( $n_file['dirname'], $org_filename . '.jpg' );
Helper::cache_set( $id, array( $org_filename, pathinfo( $n_file['filename'], PATHINFO_FILENAME ) ), 'convert_to_jpg' );
}
$n_file = path_join( $n_file['dirname'], $n_file['filename'] );
} else {
Helper::logger()->png2jpg()->error( sprintf( 'Cannot retrieve the path info of file [%s(%d)].', Helper::clean_file_path( $file ), $id ) );
return $result;
}
// Save PNG as JPG.
$new_image_info = $editor->save( $n_file, 'image/jpeg' );
// If image editor was unable to save the image, return.
if ( is_wp_error( $new_image_info ) ) {
return $result;
}
$n_file = ! empty( $new_image_info ) ? $new_image_info['file'] : '';
// Replace file, and get savings.
$result = $this->replace_file( $file, $result, $n_file );
if ( ! empty( $result['savings'] ) ) {
if ( 'full' === $size ) {
$result['converted'] = true;
}
// Update the File Details. and get updated meta.
$result['meta'] = $this->update_image_path( $id, $file, $n_file, $meta, $size );
/**
* Perform a action after the image URL is updated in post content
*/
do_action( 'wp_smush_image_url_changed', $id, $file, $n_file, $size );
}
return $result;
}
/**
* Convert a PNG to JPG, Lossless Conversion, if we have any savings
*
* @param string $id Image ID.
* @param string|array $meta Image meta.
*
* @uses Backup::add_to_image_backup_sizes()
*
* @return mixed|string
*
* TODO: Save cumulative savings
*/
public function png_to_jpg( $id = '', $meta = '' ) {
// If we don't have meta or ID, or if not a premium user.
if ( empty( $id ) || empty( $meta ) || ! $this->is_active() || ! Helper::is_smushable( $id ) ) {
return $meta;
}
$file = Helper::get_attached_file( $id );// S3+.
// Whether to convert to jpg or not.
$should_convert = $this->can_be_converted( $id, 'full', '', $file );
if ( ! $should_convert ) {
return $meta;
}
$result['meta'] = $meta;
/**
* Allow to force convert the PNG to JPG via filter wp_smush_convert_to_jpg.
*
* @since 3.9.6
* @see self::can_be_converted()
*/
// Perform the conversion, and update path.
$result = $this->convert_to_jpg( $id, $file, $result['meta'] );
$savings['full'] = ! empty( $result['savings'] ) ? $result['savings'] : '';
// If original image was converted and other sizes are there for the image, Convert all other image sizes.
if ( $result['converted'] ) {
if ( ! empty( $meta['sizes'] ) ) {
$converted_thumbs = array();
foreach ( $meta['sizes'] as $size_k => $data ) {
// Some thumbnail sizes are using the same image path, so check if the thumbnail file is converted.
if ( isset( $converted_thumbs[ $data['file'] ] ) ) {
// Update converted thumbnail size.
$result['meta']['sizes'][ $size_k ]['file'] = $result['meta']['sizes'][ $converted_thumbs[ $data['file'] ] ]['file'];
$result['meta']['sizes'][ $size_k ]['mime-type'] = $result['meta']['sizes'][ $converted_thumbs[ $data['file'] ] ]['mime-type'];
continue;
}
$s_file = path_join( dirname( $file ), $data['file'] );
/**
* Check if the file exists on the server,
* if not, might try to download it from the cloud (s3).
*
* @since 3.9.6
*/
if ( ! Helper::exists_or_downloaded( $s_file, $id ) ) {
continue;
}
/**
* Since these sizes are derived from the main png file,
* We can safely perform the conversion.
*/
$result = $this->convert_to_jpg( $id, $s_file, $result['meta'], $size_k );
if ( ! empty( $result['savings'] ) ) {
$savings[ $size_k ] = $result['savings'];
/**
* Save converted thumbnail file, allow to try to convert the thumbnail to PNG again if it was failed.
*/
$converted_thumbs[ $data['file'] ] = $size_k;
}
}
}
$mod = WP_Smush::get_instance()->core()->mod;
// Save the original File URL.
/**
* Filter: wp_smush_png2jpg_enable_backup
*
* Whether to backup the PNG before converting it to JPG or not
*
* It's safe when we try to backup the PNG file before converting it to JPG when disabled backup option.
* But if exists the backup file, we can delete the PNG file to free up space.
* Note, if enabling resize the image, the backup file is a file that has already been resized, not the original file.
* Use this filter to disable this option:
* add_filter('wp_smush_png2jpg_enable_backup', '__return_false' );
*/
if ( $mod->backup->is_active() || apply_filters( 'wp_smush_png2jpg_enable_backup', ! $mod->backup->is_active(), $id, $file ) ) {
if ( ! $mod->backup->maybe_backup_image( $id, $file ) ) {
/**
* Delete the original file if the backup file exists.
*
* Note, we use size key smush-png2jpg-full for PNG2JPG file to support S3 private media,
* to remove converted JPG file after restoring in private folder.
*
* @see Smush\Core\Integrations\S3::get_object_key()
*/
Helper::delete_permanently( array( 'smush-png2jpg-full' => $file ), $id );// S3+.
}
}
// Remove webp images created from the png version, if any.
$mod->webp->delete_images( $id, true, $file );
/**
* Do action, if the PNG to JPG conversion was successful
*/
do_action( 'wp_smush_png_jpg_converted', $id, $meta, $savings );
/**
* The file converted to JPG,
* we can clear the temp cache related to this converting.
*/
Helper::cache_delete( 'png2jpg_can_be_converted' );
Helper::cache_delete( 'convert_to_jpg' );
}
// Update the Final Stats.
update_post_meta( $id, 'wp-smush-pngjpg_savings', $savings );
return $result['meta'];
}
/**
* Get JPG quality from WordPress Image Editor
*
* @param string $file File.
*
* @return int Quality for JPEG images
*/
private function get_quality( $file ) {
if ( empty( $file ) ) {
return 82;
}
$editor = wp_get_image_editor( $file );
if ( ! is_wp_error( $editor ) ) {
$quality = $editor->get_quality();
} else {
Helper::logger()->png2jpg()->error( sprintf( 'Image Editor cannot load image [%s].', Helper::clean_file_path( $file ) ) );
}
// Choose the default quality if we didn't get it.
if ( ! isset( $quality ) || $quality < 1 || $quality > 100 ) {
// The default quality.
$quality = 82;
}
return $quality;
}
/**
* Check whether the given attachment was converted from PNG to JPG.
*
* @param int $id Attachment ID.
*
* @since 3.9.6 Use this function to check if an image is converted from PNG to JPG.
* @see Backup::get_backup_file() To check the backup file.
*
* @return int|false False if the image id is empty.
* 0 Not yet converted, -1 Tried to convert but it failed or not saving, 1 Convert successfully.
*/
public function is_converted( $id ) {
if ( empty( $id ) ) {
return false;
}
$savings = get_post_meta( $id, 'wp-smush-pngjpg_savings', true );
$is_converted = 0;
if ( ! empty( $savings ) ) {
$is_converted = -1;// The image was tried to convert to JPG but it failed or larger than the original file.
if ( ! empty( $savings['full'] ) ) {
$is_converted = 1;// The image was converted to JPG successfully.
}
}
return $is_converted;
}
/**
* Update Image URL in post content
*
* @param string $id Attachment ID.
* @param string $size_k Image Size.
* @param string $n_file New File Path which replaces the old file.
* @param string $o_url URL to search for.
*/
private function update_image_url( $id, $size_k, $n_file, $o_url ) {
if ( 'full' === $size_k ) {
// Get the updated image URL.
$n_url = wp_get_attachment_url( $id );
} else {
$n_url = trailingslashit( dirname( $o_url ) ) . basename( $n_file );
}
// Update In Post Content, Loop Over a set of posts to avoid the query failure for large sites.
global $wpdb;
// Get existing Images with current URL.
$query = $wpdb->prepare(
"SELECT ID, post_content FROM $wpdb->posts WHERE post_content LIKE '%%%s%%'",
$o_url
);
$rows = $wpdb->get_results( $query, ARRAY_A );
if ( empty( $rows ) || ! is_array( $rows ) ) {
return true;
}
// Iterate over rows to update post content.
$total = count( $rows );
foreach ( $rows as $row ) {
// replace old URLs with new URLs.
$post_content = $row['post_content'];
$post_content = str_replace( $o_url, $n_url, $post_content );
// Update Post content.
if ( $wpdb->update(
$wpdb->posts,
array(
'post_content' => $post_content,
),
array(
'ID' => $row['ID'],
)
) ) {
$total --;
}
clean_post_cache( $row['ID'] );
}
// SMUSH-1088?focusedCommentId=92914.
return 0 === $total;
}
}