From 36cdbf051e8e797808ef11a9c38c64ca6eb2855f Mon Sep 17 00:00:00 2001 From: Bas Schuiling Date: Wed, 20 Sep 2023 11:00:34 +0300 Subject: [PATCH 1/5] Updated background removal, remove old replacer.php function --- classes/Controller/ReplaceController.php | 2 +- classes/api.php | 6 +- classes/emr-plugin.php | 2 +- classes/replacer.php | 965 ----------------------- enable-media-replace.php | 2 +- views/do-replace-background.php | 25 +- 6 files changed, 26 insertions(+), 976 deletions(-) delete mode 100644 classes/replacer.php diff --git a/classes/Controller/ReplaceController.php b/classes/Controller/ReplaceController.php index 1aa7a34..0216ca4 100644 --- a/classes/Controller/ReplaceController.php +++ b/classes/Controller/ReplaceController.php @@ -437,7 +437,7 @@ protected function getNewExcerpt($meta) return $excerpt; } - protected function getSourceUrl() + public function getSourceUrl() { if (function_exists('wp_get_original_image_url')) // WP 5.3+ { diff --git a/classes/api.php b/classes/api.php index 2df7418..7b9a3bf 100644 --- a/classes/api.php +++ b/classes/api.php @@ -5,6 +5,8 @@ namespace EnableMediaReplace; use EnableMediaReplace\ShortPixelLogger\ShortPixelLogger as Log; +use EnableMediaReplace\Controller\ReplaceController as ReplaceController; + use Exception; use stdClass; @@ -74,8 +76,8 @@ public function request( array $posted_data ) { return $result; } - $replacer = new Replacer($attachment_id); - $url = $replacer->getSourceUrl(); + $replaceController = new ReplaceController($attachment_id); + $url = $replaceController->getSourceUrl(); $settings = get_option('enable_media_replace', array()); // save settings and show last loaded. $settings['bg_type'] = isset($_POST['background']['type']) ? sanitize_text_field($_POST['background']['type']) : false; diff --git a/classes/emr-plugin.php b/classes/emr-plugin.php index 2606729..50cbd2c 100644 --- a/classes/emr-plugin.php +++ b/classes/emr-plugin.php @@ -103,7 +103,7 @@ public static function get() if (Log::debugIsActive()) { $uploaddir = wp_upload_dir(null, false, false); if (isset($uploaddir['basedir'])) { - $log->setLogPath($uploaddir['basedir'] . "/emr_log"); + $log->setLogPath( trailingslashit($uploaddir['basedir']) . "emr_log"); } } return self::$instance; diff --git a/classes/replacer.php b/classes/replacer.php deleted file mode 100644 index cc3fd4b..0000000 --- a/classes/replacer.php +++ /dev/null @@ -1,965 +0,0 @@ -post_id = $post_id; - - $source_file = false; - if (function_exists('wp_get_original_image_path')) // WP 5.3+ - { - $source_file = wp_get_original_image_path($post_id); - } - - if (false === $source_file) - { - $source_file = trim(get_attached_file($post_id, apply_filters( 'emr_unfiltered_get_attached_file', true ))); - } - - /* It happens that the SourceFile returns relative / incomplete when something messes up get_upload_dir with an error something. - This case shoudl be detected here and create a non-relative path anyhow.. - */ - if (! file_exists($source_file) && $source_file && 0 !== strpos( $source_file, '/' ) && ! preg_match( '|^.:\\\|', $source_file ) ) - { - $file = get_post_meta( $post_id, '_wp_attached_file', true ); - $uploads = wp_get_upload_dir(); - $source_file = $uploads['basedir'] . "/$source_file"; - } - - Log::addDebug('SourceFile ' . $source_file); - $this->sourceFile = $this->fs()->getFile($source_file); - - $this->source_post = get_post($post_id); - $this->source_is_image = wp_attachment_is('image', $this->source_post); - $this->source_metadata = wp_get_attachment_metadata( $post_id ); - - if (function_exists('wp_get_original_image_url')) // WP 5.3+ - { - $source_url = wp_get_original_image_url($post_id); - if ($source_url === false) // not an image, or borked, try the old way - $source_url = wp_get_attachment_url($post_id); - - $this->source_url = $source_url; - } - else - $this->source_url = wp_get_attachment_url($post_id); - - } - - private function fs() - { - return emr()->filesystem(); - } - - public function setMode($mode) - { - $this->replaceMode = $mode; - } - - public function setTimeMode($mode, $datetime = 0) - { - if ($datetime == 0) - $datetime = current_time('mysql'); - - $this->datetime = $datetime; - $this->timeMode = $mode; - } - - /** Replace the sourceFile with a target - * @param $file String Full Path to the Replacement File. This will usually be an uploaded file in /tmp/ - * @param $fileName String The fileName of the uploaded file. This will be used if sourcefile is not to be overwritten. - * @throws RunTimeException Can throw exception if something went wrong with the files. - */ - public function replaceWith($file, $fileName) - { - global $wpdb; - $this->targetName = $fileName; - - $targetFile = $this->getTargetFile(); - $fs = $this->fs(); - - if (is_null($targetFile)) - { - return null; - // $ex = __('Target File could not be set. The source file might not be there. In case of search and replace, a filter might prevent this', "enable-media-replace"); - // throw new \RuntimeException($ex); - } - - $targetFileObj = $fs->getFile($targetFile); - $directoryObj = $targetFileObj->getFileDir(); - $result = $directoryObj->check(); - - if ($result === false) - Log::addError('Directory creation for targetFile failed'); - - $permissions = $this->sourceFile->getPermissions(); - - $this->removeCurrent(); // tries to remove the current files. - /* @todo See if wp_handle_sideload / wp_handle_upload can be more securely used for this */ - // @todo Use FS / File copy for this. - - $fileObj = $fs->getFile($file); - $result_moved = $fileObj->move($targetFileObj); - - if (false === $result_moved) - { - if ($targetFileObj->exists()) - { - Log::addDebug('Could remove file from tmp directory?'); - } - else { - $ex = sprintf( esc_html__('The uploaded file could not be moved to %1$s. This is most likely an issue with permissions, or upload failed.', "enable-media-replace"), $targetFile ); - throw new \RuntimeException($ex); - } - - } - - // init targetFile. - $this->targetFile = $fs->getFile($targetFile); - - if ($permissions > 0) - chmod( $targetFile, $permissions ); // restore permissions - else { - Log::addWarn('Setting permissions failed'); - } - - - // update the file attached. This is required for wp_get_attachment_url to work. - // Using RawFullPath because FullPath does normalize path, which update_attached_file doesn't so in case of windows / strange Apspaths it fails. - $updated = update_attached_file($this->post_id, $this->targetFile->getRawFullPath() ); - if (! $updated) - Log::addError('Update Attached File reports as not updated or same value'); - - $this->target_url = $this->getTargetURL(); //wp_get_attachment_url($this->post_id); - - // Run the filter, so other plugins can hook if needed. - $filtered = apply_filters( 'wp_handle_upload', array( - 'file' => $this->targetFile->getFullPath(), - 'url' => $this->target_url, - 'type' => $this->targetFile->getMime(), - ), 'sideload'); - - // check if file changed during filter. Set changed to attached file meta properly. - if (isset($filtered['file']) && $filtered['file'] != $this->targetFile->getFullPath() ) - { - update_attached_file($this->post_id, $filtered['file'] ); - $this->targetFile = $this->fs()->getFile($filtered['file']); // handle as a new file - Log::addInfo('WP_Handle_upload filter returned different file', $filtered); - } - - // Check and update post mimetype, otherwise badly coded plugins cry. - $post_mime = get_post_mime_type($this->post_id); - $target_mime = $this->targetFile->getMime(); - - // update DB post mime type, if somebody decided to mess it up, and the target one is not empty. - if ($target_mime !== $post_mime && strlen($target_mime) > 0) - { - - \wp_update_post(array('post_mime_type' => $this->targetFile->getMime(), 'ID' => $this->post_id)); - } - - $metadata = wp_generate_attachment_metadata( $this->post_id, $this->targetFile->getFullPath() ); - wp_update_attachment_metadata( $this->post_id, $metadata ); - $this->target_metadata = $metadata; - - /** If author is different from replacer, note this */ - $author_id = get_post_meta($this->post_id, '_emr_replace_author', true); - - if ( intval($this->source_post->post_author) !== get_current_user_id()) - { - update_post_meta($this->post_id, '_emr_replace_author', get_current_user_id()); - } - elseif ($author_id) - { - delete_post_meta($this->post_id, '_emr_replace_author'); - } - - - if ($this->replaceMode == self::MODE_SEARCHREPLACE) - { - // Write new image title. - $title = $this->getNewTitle(); - $excerpt = $this->getNewExcerpt(); - $update_ar = array('ID' => $this->post_id); - $update_ar['post_title'] = $title; - $update_ar['post_name'] = sanitize_title($title); - if ($excerpt !== false) - { - $update_ar['post_excerpt'] = $excerpt; - } - $update_ar['guid'] = $this->target_url; //wp_get_attachment_url($this->post_id); - // $update_ar['post_mime_type'] = $this->targetFile->getFileMime(); - - $post_id = \wp_update_post($update_ar, true); - - // update post doesn't update GUID on updates. - $wpdb->update( $wpdb->posts, array( 'guid' => $this->target_url), array('ID' => $this->post_id) ); - //enable-media-replace-upload-done - - // @todo Replace this one with proper Notices:addError; - if (is_wp_error($post_id)) - { - $errors = $post_id->get_error_messages(); - foreach ($errors as $error) { - echo $error; - } - } - - } // SEARCH REPLACE MODE - - -// FROM HERE STATS THE MODULE - $args = array( - 'thumbnails_only' => ($this->replaceMode == self::MODE_SEARCHREPLACE) ? false : true, - ); - - // Search Replace will also update thumbnails. - $this->doSearchReplace($args); - - /*if(wp_attachment_is_image($this->post_id)) - { - $this->ThumbnailUpdater->setNewMetadata($this->target_metadata); - $result = $this->ThumbnailUpdater->updateThumbnails(); - if (false === $result) - Log::addWarn('Thumbnail Updater returned false'); - }*/ - - - // if all set and done, update the date. - // This must be done after wp_update_posts - $this->updateDate(); // updates the date. - - // Give the caching a kick. Off pending specifics. - $cache_args = array( - 'flush_mode' => 'post', - 'post_id' => $this->post_id, - ); - - $cache = new emrCache(); - $cache->flushCache($cache_args); - - do_action("enable-media-replace-upload-done", $this->target_url, $this->source_url, $this->post_id); - - return true; - } - - protected function getNewTitle() - { - // get basename without extension - $title = basename($this->targetFile->getFileName(), '.' . $this->targetFile->getExtension()); - $meta = $this->target_metadata; - - if (isset($meta['image_meta'])) - { - if (isset($meta['image_meta']['title'])) - { - if (strlen($meta['image_meta']['title']) > 0) - { - $title = $meta['image_meta']['title']; - } - } - } - - // Thanks Jonas Lundman (http://wordpress.org/support/topic/add-filter-hook-suggestion-to) - $title = apply_filters( 'enable_media_replace_title', $title ); - - return $title; - } - - protected function getNewExcerpt() - { - $meta = $this->target_metadata; - $excerpt = false; - - if (isset($meta['image_meta'])) - { - if (isset($meta['image_meta']['caption'])) - { - if (strlen($meta['image_meta']['caption']) > 0) - { - $excerpt = $meta['image_meta']['caption']; - } - } - } - - return $excerpt; - } - - /** Gets the source file after processing. Returns a file */ - public function getSourceFile() - { - return $this->sourceFile; - } - - - public function getSourceUrl() - { - return $this->source_url; - } - - public function setNewTargetLocation($new_rel_location) - { - $uploadDir = wp_upload_dir(); - $newPath = trailingslashit($uploadDir['basedir']) . $new_rel_location; - - // Detect traversal by making sure the canonical path starts with uploads' basedir. - if (($newPath = realpath($newPath)) && strpos($newPath, $uploadDir['basedir']) !== 0) - { - Notices::addError(__('Specificed directory is outside the upload directory. This is not allowed for security reasons', 'enable-media-replace')); - return false; - } - - if (! is_dir($newPath)) - { - Notices::addError(__('Specificed new directory does not exist. Path must be a relative path from the upload directory and exist', 'enable-media-replace')); - return false; - } - $this->target_location = trailingslashit($newPath); - return true; - } - - /** Returns a full target path to place to new file. Including the file name! **/ - protected function getTargetFile() - { - $targetPath = null; - if ($this->replaceMode == self::MODE_REPLACE) - { - $targetFile = $this->sourceFile->getFullPath(); // overwrite source - } - elseif ($this->replaceMode == self::MODE_SEARCHREPLACE) - { - $path = (string) $this->sourceFile->getFileDir(); - if ($this->target_location) // Replace to another path. - { - $otherTarget = $this->fs()->getFile($this->target_location . $this->targetName); - if ($otherTarget->exists()) - { - Notices::addError(__('In specificied directory there is already a file with the same name. Can\'t replace.', 'enable-media-replace')); - return null; - } - $path = $this->target_location; // if all went well. - } - //if ($this->sourceFile->getFileName() == $this->targetName) - $targetpath = $path . $this->targetName; - - // If the source and target path AND filename are identical, user has wrong mode, just overwrite the sourceFile. - if ($targetpath == $this->sourceFile->getFullPath()) - { - $unique = $this->sourceFile->getFileName(); - $this->replaceMode == self::MODE_REPLACE; - } - else - { - $unique = wp_unique_filename($path, $this->targetName); - } - $new_filename = apply_filters( 'emr_unique_filename', $unique, $path, $this->post_id ); - $targetFile = trailingslashit($path) . $new_filename; - } - if (is_dir($targetFile)) // this indicates an error with the source. - { - Log::addWarn('TargetFile is directory ' . $targetFile ); - $upload_dir = wp_upload_dir(); - if (isset($upload_dir['path'])) - { - $targetFile = trailingslashit($upload_dir['path']) . wp_unique_filename($targetFile, $this->targetName); - } - else { - $err = __('EMR could not establish a proper destination for replacement', 'enable-media-replace'); - Notices::addError($err); - Log::addError($err); - // throw new \RuntimeException($err); - // exit($err); // fallback - return null; - } - } - return $targetFile; - } - - /** Since WP functions also can't be trusted here in certain cases, create the URL by ourselves */ - protected function getTargetURL() - { - //$uploads['baseurl'] - $url = wp_get_attachment_url($this->post_id); - $url_basename = basename($url); - - // Seems all worked as normal. - if (strpos($url, '://') >= 0 && $this->targetFile->getFileName() == $url_basename) - return $url; - - // Relative path for some reason - if (strpos($url, '://') === false) - { - $uploads = wp_get_upload_dir(); - $url = str_replace($uploads['basedir'], $uploads['baseurl'], $this->targetFile->getFullPath()); - } - // This can happen when WordPress is not taking from attached file, but wrong /old GUID. Try to replace it to the new one. - elseif ($this->targetFile->getFileName() != $url_basename) - { - $url = str_replace($url_basename, $this->targetFile->getFileName(), $url); - } - - return $url; - - } - - /** Tries to remove all of the old image, without touching the metadata in database - * This might fail on certain files, but this is not an indication of success ( remove might fail, but overwrite can still work) - */ - protected function removeCurrent() - { - $meta = \wp_get_attachment_metadata( $this->post_id ); - $backup_sizes = get_post_meta( $this->post_id, '_wp_attachment_backup_sizes', true ); - - // this must be -scaled if that exists, since wp_delete_attachment_files checks for original_files but doesn't recheck if scaled is included since that the one 'that exists' in WP . $this->source_file replaces original image, not the -scaled one. - $file = $this->sourceFile->getFullPath(); - $result = \wp_delete_attachment_files($this->post_id, $meta, $backup_sizes, $file ); - - // If Attached file is not the same path as file, this indicates a -scaled images is in play. - // Also plugins like Polylang tend to block delete image while there is translation / duplicate item somewhere - // 10/06/22 : Added a hard delete if file still exists. Be gone, hard way. - $attached_file = get_attached_file($this->post_id); - if (file_exists($attached_file)) - { - @unlink($attached_file); - } - - do_action( 'emr_after_remove_current', $this->post_id, $meta, $backup_sizes, $file ); - } - - /** Handle new dates for the replacement */ - protected function updateDate() - { - global $wpdb; - $post_date = $this->datetime; - $post_date_gmt = get_gmt_from_date($post_date); - - $update_ar = array('ID' => $this->post_id); - if ($this->timeMode == static::TIME_UPDATEALL || $this->timeMode == static::TIME_CUSTOM) - { - $update_ar['post_date'] = $post_date; - $update_ar['post_date_gmt'] = $post_date_gmt; - } - else { - //$update_ar['post_date'] = 'post_date'; - // $update_ar['post_date_gmt'] = 'post_date_gmt'; - } - $update_ar['post_modified'] = $post_date; - $update_ar['post_modified_gmt'] = $post_date_gmt; - - $updated = $wpdb->update( $wpdb->posts, $update_ar , array('ID' => $this->post_id) ); - - wp_cache_delete($this->post_id, 'posts'); - - } - - - protected function doSearchReplace($args = array()) - { - $defaults = array( - 'thumbnails_only' => false, - ); - - $args = wp_parse_args($args, $defaults); - - // Search-and-replace filename in post database - // @todo Check this with scaled images. - $base_url = parse_url($this->source_url, PHP_URL_PATH);// emr_get_match_url( $this->source_url); - $base_url = str_replace('.' . pathinfo($base_url, PATHINFO_EXTENSION), '', $base_url); - - - $abspath = $this->fs()->getWPAbsPath(); - /** Fail-safe if base_url is a whole directory, don't go search/replace */ - if (strpos($abspath, $base_url) === 0 && is_dir($base_url)) - { - Log::addError('Search Replace tried to replace to directory - ' . $base_url); - Notices::addError(__('Fail Safe :: Source Location seems to be a directory.', 'enable-media-replace')); - return; - } - - if (strlen(trim($base_url)) == 0) - { - Log::addError('Current Base URL emtpy - ' . $base_url); - Notices::addError(__('Fail Safe :: Source Location returned empty string. Not replacing content','enable-media-replace')); - return; - } - - // get relurls of both source and target. - $urls = $this->getRelativeURLS(); - - if ($args['thumbnails_only']) - { -// if (isset($urls['source']['file']) && $urls['source']) - - /*foreach($urls as $side => $data) - { - if (isset($data['base'])) - { - unset($urls[$side]['base']); - } - if (isset($data['file'])) - { - unset($urls[$side]['file']); - } - } */ - } - - $search_urls = $urls['source']; - $replace_urls = $urls['target']; - - /* If the replacement is much larger than the source, there can be more thumbnails. This leads to disbalance in the search/replace arrays. - Remove those from the equation. If the size doesn't exist in the source, it shouldn't be in use either */ - foreach($replace_urls as $size => $url) - { - if (! isset($search_urls[$size])) - { - Log::addDebug('Dropping size ' . $size . ' - not found in source urls'); - unset($replace_urls[$size]); - } - } - - // Original can be unbalanced - if (isset($search_urls['original'])) - { - if (! isset($replace_urls['original'])) - { - $replace_urls['original'] = $replace_urls['file']; - } - } - - - Log::addDebug('Source', $search_urls); - Log::addDebug('Target', $replace_urls); - /* If on the other hand, some sizes are available in source, but not in target, try to replace them with something closeby. */ - foreach($search_urls as $size => $url) - { - if (! isset($replace_urls[$size])) - { - $closest = $this->findNearestSize($size); - if ($closest) - { - $sourceUrl = $search_urls[$size]; - $baseurl = trailingslashit(str_replace(wp_basename($sourceUrl), '', $sourceUrl)); - Log::addDebug('Nearest size of source ' . $size . ' for target is ' . $closest); - $replace_urls[$size] = $baseurl . $closest; - } - else - { - Log::addDebug('Unset size ' . $size . ' - no closest found in source'); - } - } - elseif ($url === $replace_urls[$size]) { // identical - unset($replace_urls[$size]); - unset($search_urls[$size]); - Log::addDebug('Unset size ' . $size . ' - search and replace identical'); - } - } - - /* If source and target are the same, remove them from replace. This happens when replacing a file with same name, and +/- same dimensions generated. - - After previous loops, for every search there should be a replace size. - */ - foreach($search_urls as $size => $url) - { - $replace_url = isset($replace_urls[$size]) ? $replace_urls[$size] : false; - if ($url == $replace_url) // if source and target as the same, no need for replacing. - { - unset($search_urls[$size]); - unset($replace_urls[$size]); - } - } - - // If the two sides are disbalanced, the str_replace part will cause everything that has an empty replace counterpart to replace it with empty. Unwanted. - if (count($search_urls) !== count($replace_urls)) - { - Log::addError('Unbalanced Replace Arrays, aborting', array($search_urls, $replace_urls, count($search_urls), count($replace_urls) )); - Notices::addError(__('There was an issue with updating your image URLS: Search and replace have different amount of values. Aborting updating thumbnails', 'enable-media-replace')); - return; - } - - Log::addDebug('Doing meta search and replace -', array($search_urls, $replace_urls) ); - Log::addDebug('Searching with BaseuRL ' . $base_url); - - do_action('emr/replace_urls', $search_urls, $replace_urls); - $updated = 0; - - $updated += $this->doReplaceQuery($base_url, $search_urls, $replace_urls); - - $replaceRuns = apply_filters('emr/replacer/custom_replace_query', array(), $base_url, $search_urls, $replace_urls); - Log::addDebug("REPLACE RUNS", $replaceRuns); - foreach($replaceRuns as $component => $run) - { - Log::addDebug('Running additional replace for : '. $component, $run); - $updated += $this->doReplaceQuery($run['base_url'], $run['search_urls'], $run['replace_urls']); - } - - - Log::addDebug("Updated Records : " . $updated); - return $updated; - } // doSearchReplace - - - private function doReplaceQuery($base_url, $search_urls, $replace_urls) - { - global $wpdb; - /* Search and replace in WP_POSTS */ - // Removed $wpdb->remove_placeholder_escape from here, not compatible with WP 4.8 - $posts_sql = $wpdb->prepare( - "SELECT ID, post_content FROM $wpdb->posts WHERE post_status = 'publish' AND post_content LIKE %s", - '%' . $base_url . '%'); - - $rs = $wpdb->get_results( $posts_sql, ARRAY_A ); - $number_of_updates = 0; - - if ( ! empty( $rs ) ) { - foreach ( $rs AS $rows ) { - $number_of_updates = $number_of_updates + 1; - // replace old URLs with new URLs. - - $post_content = $rows["post_content"]; - $post_id = $rows['ID']; - $replaced_content = $this->replaceContent($post_content, $search_urls, $replace_urls); - - if ($replaced_content !== $post_content) - { - $sql = 'UPDATE ' . $wpdb->posts . ' SET post_content = %s WHERE ID = %d'; - $sql = $wpdb->prepare($sql, $replaced_content, $post_id); - - $result = $wpdb->query($sql); - - if ($result === false) - { - Notice::addError('Something went wrong while replacing' . $result->get_error_message() ); - Log::addError('WP-Error during post update', $result); - } - } - - } - } - - $number_of_updates += $this->handleMetaData($base_url, $search_urls, $replace_urls); - return $number_of_updates; - } - - private function handleMetaData($url, $search_urls, $replace_urls) - { - global $wpdb; - - $options = array('post', 'comment', 'term', 'user', 'options'); - $meta_options = apply_filters('emr/metadata_tables', $options); - - - // fields in options to look for. - $option_fields = array('widget_block'); - $option_fields = apply_filters('emr/replacer/option_fields', $option_fields); - - - - $number_of_updates = 0; - $prepare = array('%' . $url . '%'); - - foreach($meta_options as $type) - { - switch($type) - { - case "post": // special case. - $sql = 'SELECT meta_id as id, meta_key, meta_value FROM ' . $wpdb->postmeta . ' - WHERE post_id in (SELECT ID from '. $wpdb->posts . ' where post_status = "publish") AND meta_value like %s'; - $type = 'post'; - - $update_sql = ' UPDATE ' . $wpdb->postmeta . ' SET meta_value = %s WHERE meta_id = %d'; - break; - case "options": // basked case (for guten widgets). - $in_str_arr = array_fill( 0, count( $option_fields ), '%s' ); - $in_str = join( ',', $in_str_arr ); - - $sql = 'SELECT option_id as id, option_name, option_value as meta_value FROM ' . $wpdb->options . ' - WHERE option_value like %s and option_name in (' . $in_str . ')'; - $type = 'option'; - - $prepare = array_merge($prepare, $option_fields); - - $update_sql = ' UPDATE ' . $wpdb->options . ' SET option_value = %s WHERE option_id = %d'; - break; - default: - $table = $wpdb->{$type . 'meta'}; // termmeta, commentmeta etc - - $meta_id = 'meta_id'; - if ($type == 'user') - $meta_id = 'umeta_id'; - - - $sql = 'SELECT ' . $meta_id . ' as id, meta_value FROM ' . $table . ' - WHERE meta_value like %s'; - - $update_sql = " UPDATE $table set meta_value = %s WHERE $meta_id = %d "; - break; - } - - $sql = $wpdb->prepare($sql, $prepare); - - if ($wpdb->last_error) - Log::addWarn('Error' . $wpdb->last_error, $wpdb->last_query); - - // This is a desparate solution. Can't find anyway for wpdb->prepare not the add extra slashes to the query, which messes up the query. - // $postmeta_sql = str_replace('[JSON_URL]', $json_url, $postmeta_sql); - $rsmeta = $wpdb->get_results($sql, ARRAY_A); - - if (! empty($rsmeta)) - { - foreach ($rsmeta as $row) - { - $number_of_updates++; - $content = $row['meta_value']; - - - $id = $row['id']; - - $content = $this->replaceContent($content, $search_urls, $replace_urls); //str_replace($search_urls, $replace_urls, $content); - - $prepared_sql = $wpdb->prepare($update_sql, $content, $id); - - Log::addDebug('Update Meta SQl' . $prepared_sql); - $result = $wpdb->query($prepared_sql); - - if ($wpdb->last_error) - Log::addWarn('Error' . $wpdb->last_error, $wpdb->last_query); - - } - } - } // foreach - - return $number_of_updates; - } // function - - - - /** - * Replaces Content across several levels of possible data - * @param $content String The Content to replace - * @param $search String Search string - * @param $replace String Replacement String - * @param $in_deep Boolean. This is use to prevent serialization of sublevels. Only pass back serialized from top. - */ - private function replaceContent($content, $search, $replace, $in_deep = false) - { - //$is_serial = false; - $content = maybe_unserialize($content); - $isJson = $this->isJSON($content); - - if ($isJson) - { - Log::addDebug('Found JSON Content'); - $content = json_decode($content); - Log::addDebug('J/Son Content', $content); - - } - - if (is_string($content)) // let's check the normal one first. - { - $content = apply_filters('emr/replace/content', $content, $search, $replace); - - $content = str_replace($search, $replace, $content); - } - elseif (is_wp_error($content)) // seen this. - { - //return $content; // do nothing. - } - elseif (is_array($content) ) // array metadata and such. - { - foreach($content as $index => $value) - { - $content[$index] = $this->replaceContent($value, $search, $replace, true); //str_replace($value, $search, $replace); - if (is_string($index)) // If the key is the URL (sigh) - { - $index_replaced = $this->replaceContent($index, $search,$replace, true); - if ($index_replaced !== $index) - $content = $this->change_key($content, array($index => $index_replaced)); - } - } - } - elseif(is_object($content) && '__PHP_Incomplete_Class' !== get_class($content)) // metadata objects, they exist. prevent incomplete classes from deactivated plugins or whatever. They crash. - { - foreach($content as $key => $value) - { - - $content->{$key} = $this->replaceContent($value, $search, $replace, true); //str_replace($value, $search, $replace); - } - } - - if ($isJson && $in_deep === false) // convert back to JSON, if this was JSON. Different than serialize which does WP automatically. - { - Log::addDebug('Value was found to be JSON, encoding'); - // wp-slash -> WP does stripslashes_deep which destroys JSON - $content = json_encode($content, JSON_UNESCAPED_SLASHES); - Log::addDebug('Content returning', array($content)); - } - elseif($in_deep === false && (is_array($content) || is_object($content))) - $content = maybe_serialize($content); - - return $content; - } - - private function change_key($arr, $set) { - if (is_array($arr) && is_array($set)) { - $newArr = array(); - foreach ($arr as $k => $v) { - $key = array_key_exists( $k, $set) ? $set[$k] : $k; - $newArr[$key] = is_array($v) ? $this->change_key($v, $set) : $v; - } - return $newArr; - } - return $arr; - } - - private function getFilesFromMetadata($meta) - { - $fileArray = array(); - if (isset($meta['file'])) - $fileArray['file'] = $meta['file']; - if (isset($meta['original_image'])) - { - $fileArray['original'] = $meta['original_image']; - } - - if (isset($meta['sizes'])) - { - foreach($meta['sizes'] as $name => $data) - { - if (isset($data['file'])) - { - $fileArray[$name] = $data['file']; - } - } - } - - // scaled - return $fileArray; - } - - /* Check if given content is JSON format. */ - private function isJSON($content) - { - if (is_array($content) || is_object($content)) - return false; // can never be. - - $json = json_decode($content); - return $json && $json != $content; - } - - // Get REL Urls of both source and target. - private function getRelativeURLS() - { - $dataArray = array( - 'source' => array('url' => $this->source_url, 'metadata' => $this->getFilesFromMetadata($this->source_metadata) ), - 'target' => array('url' => $this->target_url, 'metadata' => $this->getFilesFromMetadata($this->target_metadata) ), - ); - - $result = array(); - - foreach($dataArray as $index => $item) - { - $result[$index] = array(); - $metadata = $item['metadata']; - - $baseurl = parse_url($item['url'], PHP_URL_PATH); - $result[$index]['base'] = $baseurl; // this is the relpath of the mainfile. - $baseurl = trailingslashit(str_replace( wp_basename($item['url']), '', $baseurl)); // get the relpath of main file. - - foreach($metadata as $name => $filename) - { - $result[$index][$name] = $baseurl . wp_basename($filename); // filename can have a path like 19/08 etc. - } - - } - Log::addDebug('Relative URLS', $result); - return $result; - } - - - /** FindNearestsize - * This works on the assumption that when the exact image size name is not available, find the nearest width with the smallest possible difference to impact the site the least. - */ - private function findNearestSize($sizeName) - { - Log::addDebug('Find Nearest: '. $sizeName); - - if (! isset($this->source_metadata['sizes'][$sizeName]) || ! isset($this->target_metadata['width'])) // This can happen with non-image files like PDF. - { - // Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails. - if (strpos($this->target_url, '.svg') !== false) - { - $svg_file = wp_basename($this->target_url); - return $svg_file; // this is the relpath of the mainfile. - } - - - return false; - } - $old_width = $this->source_metadata['sizes'][$sizeName]['width']; // the width from size not in new image - $new_width = $this->target_metadata['width']; // default check - the width of the main image - - $diff = abs($old_width - $new_width); - // $closest_file = str_replace($this->relPath, '', $this->newMeta['file']); - $closest_file = wp_basename($this->target_metadata['file']); // mainfile as default - - foreach($this->target_metadata['sizes'] as $sizeName => $data) - { - $thisdiff = abs($old_width - $data['width']); - - if ( $thisdiff < $diff ) - { - $closest_file = $data['file']; - if(is_array($closest_file)) { $closest_file = $closest_file[0];} // HelpScout case 709692915 - if(!empty($closest_file)) { - $diff = $thisdiff; - $found_metasize = true; - } - } - } - - - if(empty($closest_file)) return false; - - return $closest_file; - - - } - -} // class diff --git a/enable-media-replace.php b/enable-media-replace.php index 930e629..d2c8d77 100644 --- a/enable-media-replace.php +++ b/enable-media-replace.php @@ -65,7 +65,7 @@ require_once( $plugin_path . 'build/shortpixel/autoload.php' ); require_once( $plugin_path . 'classes/compat.php' ); require_once( $plugin_path . 'classes/functions.php' ); -require_once( $plugin_path . 'classes/replacer.php' ); +//require_once( $plugin_path . 'classes/replacer.php' ); require_once( $plugin_path . 'classes/uihelper.php' ); //require_once( $plugin_path . 'classes/file.php' ); require_once( $plugin_path . 'classes/cache.php' ); diff --git a/views/do-replace-background.php b/views/do-replace-background.php index b40e059..a7c34f8 100644 --- a/views/do-replace-background.php +++ b/views/do-replace-background.php @@ -3,7 +3,8 @@ use EnableMediaReplace\ShortPixelLogger\ShortPixelLogger as Log; use EnableMediaReplace\Notices\NoticeController as Notices; -use \EnableMediaReplace\Replacer as Replacer; +//use \EnableMediaReplace\Replacer as Replacer; +use \EnableMediaReplace\Controller\ReplaceController as ReplaceController; if (! defined('ABSPATH')) { exit; // Exit if accessed directly. @@ -28,11 +29,12 @@ $uiHelper = emr()->uiHelper(); -$replacer = new Replacer($post_id); -$replacer->setMode(\EnableMediaReplace\Replacer::MODE_REPLACE); +$replaceController = new ReplaceController($post_id); -$datetime = current_time('mysql'); -$replacer->setTimeMode( \EnableMediaReplace\Replacer::TIME_UPDATEMODIFIED, $datetime); +//$replacer->setMode(\EnableMediaReplace\Replacer::MODE_REPLACE); + +//$datetime = current_time('mysql'); +//$replacer->setTimeMode( \EnableMediaReplace\Replacer::TIME_UPDATEMODIFIED, $datetime); $api = new Api(); $result = $api->handleDownload($key); @@ -54,8 +56,19 @@ exit(__('Temp file does not exist', 'enable-media-replace')); } + +$params = array( + 'replace_type' => \EnableMediaReplace\Replacer::MODE_REPLACE, + 'timestamp_replace' => \EnableMediaReplace\Replacer::TIME_UPDATEMODIFIED, + 'new_date' => current_time('mysql'), + 'updateFile' => $result->image, + +); +$replaceController->setupParams($params); + + try { - $result = $replacer->replaceWith($result->image, $source->getFileName() , true); + $result = $replaceController->run(); } catch (\RunTimeException $e) { print_r($e->getMessage()); Log::addError($e->getMessage()); From 6f95a46a9270320453cb96be0cd9d6a7c636dcea Mon Sep 17 00:00:00 2001 From: Bas Schuiling Date: Wed, 20 Sep 2023 14:36:30 +0300 Subject: [PATCH 2/5] Fix for notice when serialized object was not serialized, but still has incomplete class --- build/shortpixel/log/src/ShortPixelLogger.php | 17 +++++++++++------ build/shortpixel/log/src/view-debug-box.php | 2 +- build/shortpixel/replacer/src/Replacer.php | 9 ++++++++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/build/shortpixel/log/src/ShortPixelLogger.php b/build/shortpixel/log/src/ShortPixelLogger.php index 109ee90..c849820 100644 --- a/build/shortpixel/log/src/ShortPixelLogger.php +++ b/build/shortpixel/log/src/ShortPixelLogger.php @@ -106,14 +106,19 @@ public function initView() if ($this->is_active && $this->is_manual_request && $user_is_administrator ) { - $content_url = content_url(); - $logPath = $this->logPath; - $pathpos = strpos($logPath, 'wp-content') + strlen('wp-content'); - $logPart = substr($logPath, $pathpos); - $logLink = $content_url . $logPart; + + $logPath = $this->logPath; + $uploads = wp_get_upload_dir(); + + + if ( 0 === strpos( $logPath, $uploads['basedir'] ) ) { // Simple as it should, filepath and basedir share. + // Replace file location with url location. + $logLink = str_replace( $uploads['basedir'], $uploads['baseurl'], $logPath ); + } + $this->view = new \stdClass; - $this->view->logLink = $logLink; + $this->view->logLink = 'view-source:' . esc_url($logLink); add_action('admin_footer', array($this, 'loadView')); } } diff --git a/build/shortpixel/log/src/view-debug-box.php b/build/shortpixel/log/src/view-debug-box.php index e36d03e..5ef8128 100644 --- a/build/shortpixel/log/src/view-debug-box.php +++ b/build/shortpixel/log/src/view-debug-box.php @@ -50,7 +50,7 @@
namespace) ?> Debug Box
- Logfile + Logfile
diff --git a/build/shortpixel/replacer/src/Replacer.php b/build/shortpixel/replacer/src/Replacer.php index 4b9079d..43b3163 100644 --- a/build/shortpixel/replacer/src/Replacer.php +++ b/build/shortpixel/replacer/src/Replacer.php @@ -362,7 +362,14 @@ private function replaceContent($content, $search, $replace, $in_deep = false) // bail directly on incomplete classes. if (true === $this->checkIncomplete($content)) { - return $serialized_content; + // if it was serialized, return the original as not to corrupt data. + if (isset($serialized_content)) + { + return $serialized_content; + } + else { // else just return the content. + return $content; + } } foreach($content as $key => $value) { From 09712fcf0e40a2a778db35dbde079f30d38f56cd Mon Sep 17 00:00:00 2001 From: Bas Schuiling Date: Thu, 21 Sep 2023 12:44:47 +0300 Subject: [PATCH 3/5] Fix - Partial revoke of security fix regarding serialized objects - Has issues with Page Builders corrupting data, plus WordPress itself handles serialized data the same. --- build/shortpixel/replacer/src/Replacer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/shortpixel/replacer/src/Replacer.php b/build/shortpixel/replacer/src/Replacer.php index 43b3163..1f1d56c 100644 --- a/build/shortpixel/replacer/src/Replacer.php +++ b/build/shortpixel/replacer/src/Replacer.php @@ -318,7 +318,7 @@ private function replaceContent($content, $search, $replace, $in_deep = false) if ( true === is_serialized($content)) { $serialized_content = $content; // use to return content back if incomplete classes are found, prevent destroying the original information - $content = Unserialize::unserialize($content, array('allowed_classes' => false)); + $content = Unserialize::unserialize($content); // bail directly on incomplete classes. In < PHP 7.2 is_object is false on incomplete objects! if (true === $this->checkIncomplete($content)) { @@ -500,7 +500,7 @@ private function findNearestSize($sizeName) /* Check if given content is JSON format. */ private function isJSON($content) { - if (is_array($content) || is_object($content)) + if (is_array($content) || is_object($content) || is_null($content)) return false; // can never be. $json = json_decode($content); From 46ab46be6b8ba9e83aae82c956fdc78f25d020d1 Mon Sep 17 00:00:00 2001 From: Bas Schuiling Date: Thu, 21 Sep 2023 13:20:37 +0300 Subject: [PATCH 4/5] Another update - Treat wp_content as strict, while metadata as not strict. Normal WP core updates seem to guard against injections from the admin backend ( but not from direct database injections ), so that is less of a vector than the code editor in guten which was used for security problem --- build/shortpixel/replacer/src/Replacer.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/build/shortpixel/replacer/src/Replacer.php b/build/shortpixel/replacer/src/Replacer.php index 1f1d56c..2416f10 100644 --- a/build/shortpixel/replacer/src/Replacer.php +++ b/build/shortpixel/replacer/src/Replacer.php @@ -216,7 +216,7 @@ private function doReplaceQuery($base_url, $search_urls, $replace_urls) $post_content = $rows["post_content"]; $post_id = $rows['ID']; - $replaced_content = $this->replaceContent($post_content, $search_urls, $replace_urls); + $replaced_content = $this->replaceContent($post_content, $search_urls, $replace_urls, false, true); if ($replaced_content !== $post_content) { @@ -311,14 +311,25 @@ private function handleMetaData($url, $search_urls, $replace_urls) * @param $search String Search string * @param $replace String Replacement String * @param $in_deep Boolean. This is use to prevent serialization of sublevels. Only pass back serialized from top. + * @param $strict_check Boolean . If true, remove all classes from serialization check and fail. This should be done on post_content, not on metadata. */ - private function replaceContent($content, $search, $replace, $in_deep = false) + private function replaceContent($content, $search, $replace, $in_deep = false, $strict_check = false) { //$is_serial = false; if ( true === is_serialized($content)) { $serialized_content = $content; // use to return content back if incomplete classes are found, prevent destroying the original information - $content = Unserialize::unserialize($content); + + if (true === $strict_check) + { + $args = array('allowed_classes' => false); + } + else + { + $args = array('allowed_classes' => true); + } + + $content = Unserialize::unserialize($content, $args); // bail directly on incomplete classes. In < PHP 7.2 is_object is false on incomplete objects! if (true === $this->checkIncomplete($content)) { From 93b2f0ba5dc5e807434fb30cf7c9e983f5e1f096 Mon Sep 17 00:00:00 2001 From: Pedro Dobrescu Date: Fri, 22 Sep 2023 16:54:57 +0300 Subject: [PATCH 5/5] Update the readme file and version numbers for the 4.1.4 release --- enable-media-replace.php | 4 ++-- readme.txt | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/enable-media-replace.php b/enable-media-replace.php index 12d0eab..35074e1 100644 --- a/enable-media-replace.php +++ b/enable-media-replace.php @@ -3,7 +3,7 @@ * Plugin Name: Enable Media Replace * Plugin URI: https://wordpress.org/plugins/enable-media-replace/ * Description: Enable replacing media files by uploading a new file in the "Edit Media" section of the WordPress Media Library. - * Version: 4.1.3 + * Version: 4.1.4 * Author: ShortPixel * Author URI: https://shortpixel.com * GitHub Plugin URI: https://github.com/short-pixel-optimizer/enable-media-replace @@ -25,7 +25,7 @@ * */ -define( 'EMR_VERSION', '4.1.3' ); +define( 'EMR_VERSION', '4.1.4' ); if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. diff --git a/readme.txt b/readme.txt index f9fbf26..a5b3480 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: replace, attachment, media, files, replace image, remove background, repla Requires at least: 4.9.7 Tested up to: 6.3 Requires PHP: 5.6 -Stable tag: 4.1.3 +Stable tag: 4.1.4 Easily replace any attached image/file by simply uploading a new file in the Media Library edit view - a real time saver! @@ -69,6 +69,11 @@ If you want more control over the format in which the time is shown, you can use == Changelog == += 4.1.4 = + +Release date: September 22, 2023 +* Fix: The latest security fix was breaking the replacements made with Beaver Builder (and possibly other builders); + = 4.1.3 = Release date: September 14, 2023