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; } }