t' => false, 'post_excerpt' => false, ]; private $default_prompts = [ 'manual_prompt_alt' => 'The ALT is used mainly for SEO. It should be stuffed with keywords, but also describe the image. It should be a short sentence, typically starting with a uppercase letter and ending with a period.', 'manual_prompt_caption' => 'It should be a very short description of the image, what\'s happening on it, in one sentence which typically starts with a uppercase letter and ends with a period.', 'manual_prompt_filename' => 'Based on the rules we set before, actually generate 5 different filenames. They should be all quite different, from the less creative to the most creative. Separate them by a comma (,) and do not end with a period. Do not forget the extension which is used in Current Filename.', 'manual_prompt_title' => 'It should be a concise, catchy title for the image that summarizes its main subject or theme. The title should be capitalized like a headline, typically 5-10 words long, and not end with a period. It should be engaging and optimized for both users and SEO.', 'manual_prompt_description' => 'It should be an actual description the image, what\'s happening on it, in one paragraph. It is not adressing the user, but describing the image.', ]; private $option_name = 'mfrh_options'; private $log_file = 'media-file-renamer.log'; static private $plugin_option_name = 'mfrh_options'; public function __construct() { $this->site_url = get_site_url(); $this->upload_folder = wp_upload_dir(); $this->contentDir = substr( $this->upload_folder['baseurl'], 1 + strlen( $this->site_url ) ); $this->allow_usage = apply_filters( 'mfrh_allow_usage', false ); $this->allow_setup = apply_filters( 'mfrh_allow_setup', false ); add_action( 'init', array( $this, 'init' ) ); } function init() { // Before use get_option function, it has to set up Meow_MFRH_Admin. // Part of the core, settings and stuff $this->is_cli = defined( 'WP_CLI' ) && WP_CLI; $this->allow_setup = apply_filters( 'mfrh_allow_setup', current_user_can( 'manage_options' ) ); $this->admin = new Meow_MFRH_Admin( $this->allow_setup, $this ); $this->engine = new Meow_MFRH_Engine( $this ); if ( class_exists( 'MeowPro_MFRH_Core' ) ) { $this->pro = new MeowPro_MFRH_Core( $this, $this->admin, $this->engine ); } // This should be checked after the init (is_rest checks the capacities) $this->is_rest = MeowKit_MFRH_Helpers::is_rest(); $this->images_only = $this->get_option( 'images_only', false ) == 1; $this->featured_only = $this->get_option( 'featured_only', false ) == 1; // Check the roles $this->allow_usage = apply_filters( 'mfrh_allow_usage', current_user_can( 'administrator' ) ); if ( !$this->is_cli && !$this->allow_usage ) { return; } // Languages load_plugin_textdomain( MFRH_DOMAIN, false, basename( MFRH_PATH ) . '/languages' ); // Initialize $this->method = apply_filters( 'mfrh_method', $this->get_option( 'auto_rename', 'media_title' ) ); $this->method_secondary = apply_filters( 'mfrh_method', $this->get_option( 'auto_rename_secondary', 'none' ), '_secondary' ); $this->method_tertiary = apply_filters( 'mfrh_method', $this->get_option( 'auto_rename_tertiary', 'none' ), '_tertiary' ); add_action( 'add_attachment', [ $this, 'on_upload_hook' ] ); add_filter( 'wp_handle_upload_prefilter', [ $this, 'on_upload_hook_prefilter' ] ); // Cron hook for delayed on-upload processing add_action( 'mfrh_delayed_on_upload', [ $this, 'on_upload_cron_handler' ], 10, 1 ); add_action( 'attachment_updated', array( $this, 'attachment_fields_to_save' ), 10, 3 ); add_action( 'updated_post_meta', array( $this, 'on_alt_text_updated' ), 10, 4 ); if ( $this->get_option( 'filename_prefix', '' ) != '' || $this->get_option( 'filename_suffix', '' ) != '' ) { add_filter( 'mfrh_new_filename', [ $this, 'add_prefix_suffix' ], 10, 3 ); } if ( !empty( $this->get_option( 'filename_replace', [] ) ) ) { add_filter( 'mfrh_new_filename', [ $this, 'replace_filename' ], 10, 3 ); } if ( !empty( $this->get_option( 'filename_replace_regex', [] ) ) ) { add_filter( 'mfrh_new_filename', [ $this, 'replace_filename_regex' ], 10, 3 ); } // Only for REST if ( $this->is_rest ) { $this->rest = new Meow_MFRH_Rest( $this, $this->admin ); } // Side-updates should be ran for CLI and REST if ( is_admin() || $this->is_rest || $this->is_cli ) { new Meow_MFRH_Updates( $this ); do_action( 'mfrh_initialize_parsers' ); if ( $this->get_option( 'rename_on_save', false ) ) { add_action( 'save_post', array( $this, 'save_post' ) ); } } // Admin screens if ( is_admin() ) { new Meow_MFRH_UI( $this ); } } /** * * TOOLS / HELPERS * */ static function get_plugin_option( $option, $default ) { $options = get_option( Meow_MFRH_Core::$plugin_option_name, null ); return $options[$option] ?? $default; } // Check if the file exists, if it is, return the real path for it // https://stackoverflow.com/questions/3964793/php-case-insensitive-version-of-file-exists static function sensitive_file_exists( $filename ) { // Normalize the incoming $filename if (class_exists('Normalizer')) { $filename = Normalizer::normalize($filename, Normalizer::FORM_C); } $original_filename = $filename; $caseInsensitive = Meow_MFRH_Core::get_plugin_option( 'case_insensitive_check', false ); $output = false; $directoryName = mfrh_dirname( $filename ); // Gather all files in the directory. $fileArray = glob( $directoryName . '/*', GLOB_NOSORT ); $i = $caseInsensitive ? 'i' : ''; // Extract just the basename from $filename if path separators exist if ( preg_match( "/\\\|\//", $filename ) ) { $array = preg_split("/\\\|\//", $filename); $filename = $array[ count($array) - 1 ]; // Re-normalize after splitting, just to be safe if (class_exists('Normalizer')) { $filename = Normalizer::normalize($filename, Normalizer::FORM_C); } } // Now compare each file in the directory foreach ( $fileArray as $file ) { // Normalize the file path from the filesystem if (class_exists('Normalizer')) { $file = Normalizer::normalize($file, Normalizer::FORM_C); } if ( preg_match("/\/" . preg_quote($filename, '/') . "$/{$i}u", $file) ) { $output = $file; break; } } return $output; } static function rmdir_recursive( $directory ) { foreach ( glob( "{$directory}/*" ) as $file ) { if ( is_dir( $file ) ) Meow_MFRH_Core::rmdir_recursive( $file ); else unlink( $file ); } rmdir( $directory ); } function wpml_media_is_installed() { return defined( 'WPML_MEDIA_VERSION' ); } // To avoid issue with WPML Media for instance function is_real_media( $id ) { if ( $this->wpml_media_is_installed() ) { global $sitepress; $language = $sitepress->get_default_language( $id ); return icl_object_id( $id, 'attachment', true, $language ) == $id; } return true; } function is_header_image( $id ) { static $headers = false; if ( $headers == false ) { global $wpdb; $headers = $wpdb->get_col( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_is_custom_header'" ); } return in_array( $id, $headers ); } function generate_unique_filename( $method, $actual, $dirname, $filename, $counter = null ) { // We should never have "none" since we only call this function when the method is not "none" if ( $method === 'none' ) { $this->log( "⚠️ Method is 'none', returning original filename." ); return $filename; } $new_filename = $filename; $whereisdot = strrpos( $new_filename, '.' ); $basename = substr( $new_filename, 0, $whereisdot ); $extension = substr( $new_filename, $whereisdot + 1 ); switch ( $method ) { case 'increment': if ( !is_null( $counter ) ) { $new_filename = $basename . '-' . $counter . '.' . $extension; } break; case 'hash': // Generate a hash-based suffix $hash_suffix = substr( md5( $filename . time() . rand() ), 0, 8 ); $new_filename = $basename . '-' . $hash_suffix . '.' . $extension; break; case 'random': // Generate a random string suffix $random_suffix = ''; $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; for ( $i = 0; $i < 8; $i++ ) { $random_suffix .= $characters[rand(0, strlen($characters) - 1)]; } $new_filename = $basename . '-' . $random_suffix . '.' . $extension; break; default: // Fallback to increment method if ( !is_null( $counter ) ) { $new_filename = $basename . '-' . $counter . '.' . $extension; } break; } if ( $actual == $new_filename ) return false; if ( file_exists( $dirname . "/" . $new_filename ) ) { if ( $method === 'increment' ) { return $this->generate_unique_filename( $method, $actual, $dirname, $filename, is_null( $counter ) ? 2 : $counter + 1 ); } else { return $this->generate_unique_filename( $method, $actual, $dirname, $filename ); } } return $new_filename; } function get_uploads_directory_hierarchy() { $uploads_dir = wp_upload_dir(); $base_dir = $uploads_dir['basedir']; $directories = array(); // Get all subdirectories of the base directory $dir_iterator = new RecursiveDirectoryIterator( $base_dir, FilesystemIterator::KEY_AS_PATHNAME|FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS ); $iterator = new RecursiveIteratorIterator( $dir_iterator, RecursiveIteratorIterator::SELF_FIRST ); foreach ( $iterator as $file ) { if ($file->isDir()) { // Remove base_dir from path $directory = str_replace( $base_dir, '', $file->getPathname() ); if ( $directory ) { $directories[] = $directory; } } } // Remove the base "uploads" root directory and ensure all paths are relative to it $directories = array_map( function( $dir ) { return ltrim( $dir, '/' ); }, $directories ); // Return the hierarchy as a JSON file return json_encode($directories); } /** * Returns all the media sharing the same file * @param string $file The attached file path * @param int|array $excludes The post ID(s) to exclude from the results * @return array An array of IDs */ function get_posts_by_attached_file( $file, $excludes = null ) { global $wpdb; $r = array (); $q = <<< SQL SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '%s' AND meta_value = '%s' SQL; $rows = $wpdb->get_results( $wpdb->prepare( $q, '_wp_attached_file', _wp_relative_upload_path( $file ) ), OBJECT ); if ( $rows && is_array( $rows ) ) { if ( !is_array( $excludes ) ) $excludes = $excludes ? array ( (int) $excludes ) : array (); foreach ( $rows as $item ) { $id = (int) $item->post_id; if ( in_array( $id, $excludes ) ) continue; $r[] = $id; } $r = array_unique( $r ); } return $r; } /***************************************************************************** RENAME ON UPLOAD *****************************************************************************/ function set_sync_on_upload( $option ) { $this->on_upload_fields = [ 'sync_alt' => $this->get_option( 'sync_on_' . $option . '_alt', false ), 'post_title' => $this->get_option( 'sync_on_' . $option . '_title', false ), 'post_content' => $this->get_option( 'sync_on_' . $option . '_description', false ), 'post_excerpt' => $this->get_option( 'sync_on_' . $option . '_caption', false ), ]; } function get_prefilter_option( $option ) { return $this->get_option( 'sync_on_' . $option . '_filename', false ); } function on_upload_hook_prefilter( $file ) { if ( empty( $file ) || empty( $file['tmp_name'] ) ) { $this->log( "⚠️ File is empty or no name. (Prefilter)" ); return $file; } // Check if the user has opted to skip on upload for this session if ( isset( $_COOKIE['mfrh_skip_on_upload'] ) && $_COOKIE['mfrh_skip_on_upload'] === '1' ) { $this->log( "⏭️ Skipping On Upload (user requested skip for this session)." ); return $file; } // Skip prefilter if "Run Later" is enabled if ( $this->get_option( 'on_upload_run_later', false ) ) { $this->log( "⏳ Skipping prefilter (Run Later mode enabled)." ); return $file; } $this->log( "⏰ Event: New Upload on Prefilter (" . $file['name'] . ")" ); // If it's not an image, we don't do anything $filetype = $this->get_mime_type( $file['tmp_name'] ); if( strpos( $filetype, 'image' ) === false ){ $this->log( "⚠️ Not an image." ); return $file; } $upload_methods = [ $this->get_option( 'on_upload_method', 'none' ), $this->get_option( 'on_upload_method_secondary', 'none' ), $this->get_option( 'on_upload_method_tertiary', 'none' ), ]; // Check if all methods are set to "none" $all_none_methods = true; foreach ( $upload_methods as $method ) { if ( $method !== 'none' ) { $all_none_methods = false; break; } } if ( $all_none_methods ) { $filename = apply_filters( 'mfrh_new_filename', $file['name'], $file['name'], 0 ); $file['name'] = $filename; return $file; } $done = false; foreach ( $upload_methods as $method ) { if ( $done ) { break; } if ( !$this->get_prefilter_option( $method ) ) { continue; } switch ( $method ) { case 'upload_exif': // "auto" => EXIF Title list( $done, $file ) = $this->exif_data_upload_prefilter( $file ); break; case 'upload_clean': list( $done, $file ) = $this->clean_upload_prefilter( $file ); break; case 'upload_vision': list( $done, $file ) = $this->vision_rename_ai_on_upload_prefilter( $file ); break; default: $done = false; break; } } return $file; } function on_upload_hook( $id ) { if ( !wp_attachment_is_image( $id ) ) { $this->log( "⚠️ Not an image." ); return; } // Check if the user has opted to skip on upload for this session if ( isset( $_COOKIE['mfrh_skip_on_upload'] ) && $_COOKIE['mfrh_skip_on_upload'] === '1' ) { $this->log( "⏭️ Skipping On Upload (user requested skip for this session)." ); return; } // Check if "Run Later" is enabled - schedule cron instead of immediate processing if ( $this->get_option( 'on_upload_run_later', false ) ) { $this->log( "⏳ Scheduling delayed on-upload processing for attachment ID: $id" ); wp_schedule_single_event( time() + ( 5 * 60 ), 'mfrh_delayed_on_upload', [ $id ] ); return; } $this->process_on_upload( $id ); } function on_upload_cron_handler( $id ) { $this->log( "⏰ Cron: Processing delayed on-upload for attachment ID: $id" ); $this->process_on_upload( $id ); } function process_on_upload( $id ) { $post = get_post( $id ); if(!$post) { $this->log( "⚠️ Post not found." ); return; } $this->log( "⏰ Processing Upload (" . $post->post_title . ")" ); $done = false; $upload_methods = [ $this->get_option( 'on_upload_method', 'none' ), $this->get_option( 'on_upload_method_secondary', 'none' ), $this->get_option( 'on_upload_method_tertiary', 'none' ), ]; // Check if we're in "Run Later" mode: if so, we may need to handle filename renaming 8as it should be done in prefilter normally) $run_later = $this->get_option( 'on_upload_run_later', false ); $filename_renamed = false; foreach ( $upload_methods as $method ) { if ( $done ) { break; } $this->set_sync_on_upload( $method ); // Handle filename renaming if "Run Later" is enabled, filename sync is enabled for this method, // and we haven't already renamed the file if ( $run_later && !$filename_renamed && $this->get_prefilter_option( $method ) ) { $new_filename = $this->generate_on_upload_filename( $post, $method ); if ( $new_filename ) { $this->log( "🗒️ Renaming file to: $new_filename" ); $this->engine->rename( $post->ID, $new_filename, false, 'on_upload' ); // Refresh post data after rename $post = get_post( $id ); $filename_renamed = true; } } switch ( $method ) { case 'upload_exif': // "auto" => EXIF Title $this->log( "🗒️ Trying EXIF Title... " ); $done = $this->exif_data_upload( $post ); break; case 'upload_clean': $this->log( "🗒️ Trying Clean Upload... " ); $done = $this->clean_upload( $post ); break; case 'upload_vision': $this->log( "🗒️ Trying Vision Rename AI... " ); $done = $this->vision_rename_ai_on_upload( $post ); break; default: $done = false; break; } } // If no upload method handled the file, check if it should be marked as pending. if ( !$done ) { $output = []; $this->check_attachment( get_post( $id, ARRAY_A ), $output ); } $this->log( "👌 Done." ); } /** * Unified function to compute a new filename based on the upload method. * Used by both prefilter (during upload) and delayed processing (Run Later). */ function compute_upload_filename( $method, $file_path, $original_filename, $post_id = 0, $title_source = null ) { switch ( $method ) { case 'upload_exif': $title = $this->get_exif_data( $file_path, 'title' ); if ( !empty( $title ) ) { $filename = $this->engine->new_filename( $title, $original_filename ); if ( $filename ) { $this->log( "👌 Title EXIF found." ); return apply_filters( 'mfrh_new_filename', $filename, $original_filename, $post_id ); } } else { $this->log( "😭 Title EXIF not found." ); } break; case 'upload_clean': $source = $title_source ?? $original_filename; $image_title = preg_replace( '%\s*[-_\s]+\s*%', ' ', $source ); $image_title = ucwords( strtolower( $image_title ) ); $filename = $this->engine->new_filename( $image_title, $original_filename ); if ( $filename ) { $this->log( "👌 Clean Upload found." ); return apply_filters( 'mfrh_new_filename', $filename, $original_filename, $post_id ); } break; case 'upload_vision': if ( !has_filter( 'mfrh_vision_suggestion' ) ) { if ( $this->pro ) { add_filter( 'mfrh_vision_suggestion', array( $this->pro, 'vision_suggestion' ), 10, 4 ); } else { $this->log( '⚠️ Vision AI is enabled but no filter is set.' ); return null; } } // For prefilter (post_id = 0), pass the file path; for post-upload, pass post_id $ai_filename = $this->ai_suggestion( $post_id ?: null, 'filename', $post_id ? null : $file_path ); if ( $ai_filename ) { $filename = $this->engine->new_filename( $ai_filename, $original_filename ); if ( $filename ) { $this->log( "👌 Vision AI found." ); return apply_filters( 'mfrh_new_filename', $filename, $original_filename, $post_id ); } } else { $this->log( '⚠️ Vision AI: No filename suggestion returned.' ); } break; } return null; } /** * Generate a new filename for the "Run Later" on-upload processing. */ function generate_on_upload_filename( $post, $method ) { $file = get_attached_file( $post->ID ); $path_parts = mfrh_pathinfo( $file ); $old_filename = $path_parts['basename']; return $this->compute_upload_filename( $method, $file, $old_filename, $post->ID, $post->post_title ); } function rename_media_on_post_upload( $post ) { try { $parent_post_id = $post->post_parent; if ( $parent_post_id ) { $parent_post = get_post( $parent_post_id ); if ( is_null( $parent_post ) ) { $this->log( "⚠️ Parent post not found." ); return false; } if ( $parent_post && $parent_post->post_type === 'post' ) { $new_title = $parent_post->post_title . ' - ' . $post->post_title; $new_title = wp_unique_filename( dirname( get_attached_file( $post->ID ) ), $new_title ); $my_image_meta = [ 'ID' => $post->ID, 'post_title' => $new_title, ]; $result = wp_update_post( $my_image_meta ); return $result != 0; } } return false; } catch (Exception $e) { $this->log( '⚠️ Rename Media on Post Upload failed: ' . $e->getMessage() ); return false; } } /** * Get the EXIF data from an image. If a field is specified, only that field will be returned. * Otherwise, an associative array containing all the EXIF data will be returned. * @param string $path The path to the file * @param string $field The field to return * @return array An associative array containing the EXIF data, or a specific field if specified */ function get_exif_data( $path, $field = null ) { require_once( ABSPATH . 'wp-admin/includes/image.php' ); $exif = wp_read_image_metadata( $path ); if ( empty( $exif ) ) { return null; } if ( !empty( $field ) ) { return isset( $exif[ $field ] ) ? $exif[ $field ] : null; } $allData = []; if ( !empty( $exif['title'] ) ) { $allData['title'] = $exif['title']; } if ( !empty( $exif['caption'] ) ) { $allData['caption'] = $exif['caption']; } if ( !empty( $exif['keywords'] ) ) { $allData['keywords'] = implode( ', ', $exif['keywords'] ); } return $allData; } function exif_data_upload_prefilter ( $file ) { try { $filename = $this->compute_upload_filename( 'upload_exif', $file['tmp_name'], $file['name'] ); if ( $filename ) { $file['name'] = $filename; $this->log( "New file should be: " . $file['name'] ); return [ true, $file ]; } return [ false, $file ]; } catch ( Exception $e ) { $this->log( '⚠️ EXIF Data failed: ' . $e->getMessage() ); return [ false, $file ]; } } function exif_data_upload ( $post ) { try { $file = get_attached_file( $post->ID ); $title = $this->get_exif_data( $file, 'title' ); if ( !empty( $title ) ) { $my_image_title = $title; $my_image_title = preg_replace( '%\s*[-_\s]+\s*%', ' ', $my_image_title ); $my_image_title = ucwords( strtolower( $my_image_title ) ); $my_image_meta = array( 'ID' => $post->ID ); foreach ( $this->on_upload_fields as $field => $sync ) { if ( $sync ){ if ( $field == 'sync_alt' ) { $my_image_title = apply_filters( 'mfrh_exif_upload_alt', $my_image_title ); update_post_meta( $post->ID, '_wp_attachment_image_alt', $my_image_title ); } else { $value = apply_filters( 'mfrh_exif_upload_' . $field, $my_image_title ); $my_image_meta[$field] = $value; } } } $result = wp_update_post( $my_image_meta ); return $result != 0; } return false; } catch ( Exception $e ) { $this->log( '⚠️ EXIF Data failed: ' . $e->getMessage() ); return false; } } function clean_upload_prefilter( $file ) { try { $filename = $this->compute_upload_filename( 'upload_clean', $file['tmp_name'], $file['name'] ); if ( $filename ) { $file['name'] = $filename; $this->log( "New file should be: " . $file['name'] ); return [ true, $file ]; } return [ false, $file ]; } catch (Exception $e) { $this->log( '⚠️ Clean Upload failed: ' . $e->getMessage() ); return [ false, $file ]; } } function clean_upload( $post ) { try { $my_image_title = preg_replace('%\s*[-_\s]+\s*%', ' ', $post->post_title); $my_image_title = ucwords(strtolower($my_image_title)); $my_image_meta = [ 'ID' => $post->ID, ]; foreach ( $this->on_upload_fields as $field => $sync ) { if ( $sync ){ if ( $field == 'sync_alt' ) { $my_image_title = apply_filters( 'mfrh_clean_upload_alt', $my_image_title ); update_post_meta($post->ID, '_wp_attachment_image_alt', $my_image_title); } else { $value = apply_filters( 'mfrh_exif_upload_' . $field, $my_image_title ); $my_image_meta[$field] = $value; } } } $result = wp_update_post($my_image_meta); return $result != 0; return true; } catch (Exception $e) { $this->log( '⚠️ Clean Upload failed: ' . $e->getMessage() ); return false; } } function vision_rename_ai_on_upload_prefilter( $file ) { try { // Check file is readable before calling AI $binary = file_get_contents( $file['tmp_name'] ); if ( !$binary ) { $this->log( '⚠️ Vision AI: Could not read the file.' ); return [ false, $file ]; } $filename = $this->compute_upload_filename( 'upload_vision', $file['tmp_name'], $file['name'] ); if ( $filename ) { $file['name'] = $filename; $this->log( "New file should be: " . $file['name'] ); return [ true, $file ]; } return [ false, $file ]; } catch (Exception $e) { $this->log( '⚠️ Vision AI failed: ' . $e->getMessage() ); return [ false, $file ]; } } function vision_rename_ai_on_upload( $post ){ try { if ( !has_filter( 'mfrh_vision_suggestion' ) ) { if ( $this->pro ) { add_filter( 'mfrh_vision_suggestion', array( $this->pro, 'vision_suggestion' ), 10, 4 ); } else { $this->log( '⚠️ Vision AI is enabled but no filter is set.' ); return false; } } $conversion = [ 'sync_alt' => 'alternative text', 'post_title' => 'title', 'post_content' => 'description', 'post_excerpt' => 'caption', ]; $results = []; $my_image_meta = [ 'ID' => $post->ID, ]; foreach ( $this->on_upload_fields as $field => $sync ) { if ( $sync ){ $metadataType = $conversion[$field]; $newMetadata = $this->ai_suggestion( $post->ID, $metadataType ); if ( $newMetadata ) { if ( $field == 'sync_alt' ) { $alt = apply_filters( 'mfrh_vision_upload_alt', $newMetadata ); update_post_meta( $post->ID, '_wp_attachment_image_alt', $alt ); } else { $value = apply_filters( 'mfrh_exif_upload_' . $field, $newMetadata ); $my_image_meta [ $field ] = $value; } } else { $this->log( '⚠️ Vision AI: No ' . $metadataType . ' suggestion returned.' ); } } } $result = wp_update_post( $my_image_meta ); return $result != 0; } catch (Exception $e) { $this->log( '⚠️ Vision AI failed: ' . $e->getMessage() ); return false; } } function after_image_upload( $metadata, $attachment_id, $context ) { if ( $this->currently_uploading ) { $metadata = apply_filters( 'mfrh_after_upload', $metadata, $attachment_id ); } return $metadata; } function wp_handle_upload_prefilter( $file ) { $this->log( "⏰ Event: New Upload (" . $file['name'] . ")" ); $pp = mfrh_pathinfo( $file['name'] ); $this->currently_uploading = true; // If everything's fine, renames in based on the Title in the EXIF switch ( $this->method ) { case 'media_title': $this->log( "🗒️ Trying Media Title" ); $exif = wp_read_image_metadata( $file['tmp_name'] ); if ( !empty( $exif ) && isset( $exif[ 'title' ] ) && !empty( $exif[ 'title' ] ) ) { $new_filename = $this->engine->new_filename( $exif[ 'title' ], $file['name'] ); if ( !is_null( $new_filename ) ) { $file['name'] = $new_filename; $this->log( "👌 Title EXIF found." ); $this->log( "New file should be: " . $file['name'] ); } return $file; }else{ $this->log( "😭 Title EXIF not found." ); } break; case 'post_title': $this->log( "🗒️ Trying Post Title" ); if ( !isset( $_POST['post_id'] ) || $_POST['post_id'] < 1 ) break; $post = get_post( $_POST['post_id'] ); if ( !empty( $post ) && !empty( $post->post_title ) ) { $new_filename = $this->engine->new_filename( $post->post_title, $file['name'] ); if ( !is_null( $new_filename ) ) { $file['name'] = $new_filename; $this->log( "👌 Post Title found." ); $this->log( "New file should be: " . $file['name'] ); } return $file; }else{ $this->log( "😭 Post Title not found." ); } case 'post_acf_field': if ( !isset( $_POST['post_id'] ) || $_POST['post_id'] < 1 ) break; $acf_field_name = $this->get_option('acf_field_name', false); if ($acf_field_name) { $new_filename = $this->engine->new_filename( get_field($acf_field_name, $_POST['post_id']), $file['name'] ); if ( !is_null( $new_filename ) ) { $file['name'] = $new_filename; $this->log( "New file should be: " . $file['name'] ); } return $file; } break; } // Otherwise, let's do the basics based on the filename // The name will be modified at this point so let's keep it in a global // and re-inject it later global $mfrh_title_override; $mfrh_title_override = $pp['filename']; add_filter( 'wp_read_image_metadata', array( $this, 'wp_read_image_metadata' ), 10, 2 ); // Modify the filename $pp = mfrh_pathinfo( $file['name'] ); $new_filename = $this->engine->new_filename( $pp['filename'], $file['name'] ); if ( !is_null( $new_filename ) ) { $file['name'] = $new_filename; } return $file; } function wp_read_image_metadata( $meta, $file ) { // Override the title, without this it is using the new filename global $mfrh_title_override; $meta['title'] = $mfrh_title_override; return $meta; } /****************************************************************************/ function add_prefix_suffix( $new, $old, $post ) { $prefix = $this->get_option( 'filename_prefix', '' ); $suffix = $this->get_option( 'filename_suffix', '' ); if ( $prefix != '' ) { $new = $prefix . $new; } if ( $suffix != '' ) { $new = $new . $suffix; } return $new; } function replace_filename( $new, $old, $post ) { $replacements = $this->get_option( 'filename_replace', [] ); if ( !empty( $replacements ) ) { foreach ( $replacements as $replacement ) { $replacement = explode( '=>', $replacement ); if ( count( $replacement ) == 2 ) { $new = str_replace( $replacement[0], $replacement[1], $new ); } } } return $new; } function replace_filename_regex( $new, $old, $post ) { $replacements = $this->get_option( 'filename_replace_regex', [] ); if ( !empty( $replacements ) ) { foreach ( $replacements as $replacement ) { $replacement = explode( '=>', $replacement ); if ( count( $replacement ) == 2 ) { $pattern = '/' . str_replace( '/', '\/', $replacement[0] ) . '/'; $new = preg_replace( $pattern, $replacement[1], $new ); } } } return $new; } // Return false if everything is fine, otherwise return true with an output // which details the conditions and results about the renaming. function check_attachment( $post, &$output = array(), $manual_filename = null, $force_rename = false, $preview = true, $skipped_methods = [] ) { $id = $post['ID']; $old_filepath = get_attached_file( $id ); if( PHP_OS_FAMILY == 'Windows' ) { $old_filepath = str_replace( '\\', '/', $old_filepath ); } $old_filepath = !$force_rename ? Meow_MFRH_Core::sensitive_file_exists( $old_filepath ): $old_filepath; $path_parts = mfrh_pathinfo( $old_filepath ); if ( $this->images_only ) { $is_image = in_array( $post['post_mime_type'], $this->images_mime_types ); if ( !$is_image ) { $this->log( "😭 Not an image." ); return false; } } // If the file doesn't exist, let's not go further. if ( !$force_rename && ( !isset( $path_parts['dirname'] ) || !isset( $path_parts['basename'] ) ) ) { if (!isset($path_parts['dirname'])) { $this->log("😕 Directory name is missing in path parts."); } if (!isset($path_parts['basename'])) { $this->log("😕 Base name is missing in path parts."); } $this->log("😭 File doesn't exist. Path parts: " . print_r($path_parts, true) . " | " . $old_filepath ); return false; } //print_r( $path_parts ); $directory = isset( $path_parts['dirname'] ) ? $path_parts['dirname'] : null; $old_filename = isset( $path_parts['basename'] ) ? $path_parts['basename'] : null; // Check if media/file is dead if ( !$force_rename && ( !$old_filepath || !file_exists( $old_filepath ) ) ) { if (!$old_filepath) { $this->log("😕 Old file path is missing."); } else if (!file_exists($old_filepath)) { $this->log("😕 File path provided but file does not exist: " . $old_filepath); } delete_post_meta( $id, '_require_file_renaming' ); $this->log( "😭 File doesn't exist." ); return false; } // Is it forced/manual // Check mfrh_new_filename (coming from manual input) if it is different than previous filename if ( empty( $manual_filename ) && isset( $post['mfrh_new_filename'] ) ) { if ( strtolower( $post['mfrh_new_filename'] ) != strtolower( $old_filename ) ){ $manual_filename = $post['mfrh_new_filename']; } } if ( $force_rename ) { $new_filename = $manual_filename; $output['manual'] = true; } else if ( !empty( $manual_filename ) ) { // Through the new_filename function to rename when the sanitize option is enabled. // To validate the filename (i.g. space will be “-“), use the $manual_filename as the first argument $text. $new_filename = $this->get_option( 'manual_sanitize', true ) ? $this->engine->new_filename( $manual_filename, $old_filename, null, $post ) : $manual_filename; $this->last_used_method = 'manual'; $output['manual'] = true; } else { if ( $this->method === 'none') { // If no methods are selected, let the user do an automatic rename to trigger filters $new_filename = $this->engine->new_filename( $old_filename, $old_filename, null, $post ); if ( $new_filename !== $old_filename ) { $this->log( "⚡ Without method, but with filters." ); } else { delete_post_meta( $id, '_require_file_renaming' ); $this->log( "😭 No method." ); return false; } } $lock_enabled = $this->get_option( 'lock' ); if ( $lock_enabled && get_post_meta( $id, '_manual_file_renaming', true ) ) { $this->log( "😭 Locked." ); return false; } // Skip header images if ( $this->is_header_image( $id ) ) { delete_post_meta( $id, '_require_file_renaming' ); $this->log( "😭 Header Image." ); return false; } // check if the filter exists if ( !has_filter( 'mfrh_base_for_rename' ) && class_exists( 'MeowPro_MFRH_Core' ) ) { $this->log( " ⚠️ The filter was not found. Initializing Meow Pro." ); $this->pro = new MeowPro_MFRH_Core( $this, $this->admin, $this->engine ); add_filter( 'mfrh_base_for_rename', [ $this->pro, 'base_for_rename' ], 10, 4); } $base_for_rename = $this->pro ? apply_filters( 'mfrh_base_for_rename', $post['post_title'], $id, $preview, $skipped_methods ) : $this->core_base_for_rename( $id, $skipped_methods ); if ( $base_for_rename !== '{VISION}') { $new_filename = $this->engine->new_filename( $base_for_rename, $old_filename, null, $post ); } else { // Check if this file was already renamed with AI Vision $history = get_post_meta( $id, '_mfrh_history', true ); if ( !empty( $history ) && is_array( $history ) && !empty( $history['filename'] ) ) { $last_filename = end( $history['filename'] ); if ( isset( $last_filename['method'] ) && $last_filename['method'] === 'vision' ) { // Already renamed with AI Vision, no longer pending delete_post_meta( $id, '_require_file_renaming' ); return false; } } // TODO: This should be probably handled by the UI, not here. $new_filename = 'Auto with AI Vision...'; } if ( is_null( $new_filename ) ) { $this->log( "😭 No new filename." ); return false; // Leave it as it is } } // If a filename has a counter, and the ideal is without the counter, let's ignore it $ideal = preg_replace( '/-[1-9]{1,10}\./', '$1.', $old_filename ); if ( !$manual_filename ) { if ( $ideal == $new_filename ) { delete_post_meta( $id, '_require_file_renaming' ); $this->log( "😭 Ideal filename." ); return false; } } // Filename is equal to sanitized title if ( $new_filename == $old_filename ) { delete_post_meta( $id, '_require_file_renaming' ); $this->log( "😭 Same filename." ); return false; } // Check for case issue, numbering //if ( !$force_rename ) { $ideal_filename = $new_filename; $new_filepath = trailingslashit( $directory ) . $new_filename; $existing_file = Meow_MFRH_Core::sensitive_file_exists( $new_filepath ); $case_issue = strtolower( $old_filename ) == strtolower( $new_filename ); if ( !$force_rename && $existing_file && !$case_issue ) { $method = apply_filters( 'mfrh_unique', 'none' ); if ( $method ) { $new_filename = $this->generate_unique_filename( $method, $ideal, $directory, $new_filename ); if ( !$new_filename ) { $this->log( "😭 Unique: No new filename." ); delete_post_meta( $id, '_require_file_renaming' ); return false; } $new_filepath = trailingslashit( $directory ) . $new_filename; $existing_file = Meow_MFRH_Core::sensitive_file_exists( $new_filepath ); } } //} // Send info to the requester function $output['post_id'] = $id; $output['post_name'] = $post['post_name']; $output['post_title'] = $post['post_title']; $output['current_filename'] = $old_filename; $output['current_filepath'] = $old_filepath; $output['ideal_filename'] = $ideal_filename; $output['proposed_filename'] = $new_filename; $output['desired_filepath'] = $new_filepath; $output['case_issue'] = $case_issue; $output['manual'] = !empty( $manual_filename ); $output['locked'] = get_post_meta( $id, '_manual_file_renaming', true ); $output['proposed_filename_exists'] = !!$existing_file; $output['original_image'] = null; $output['used_method'] = $this->last_used_method; $output['skipped_methods'] = empty( $skipped_methods ) ? null : $skipped_methods; // If the ideal filename already exists // Maybe that's the original_image! If yes, we should let it go through // as the original_rename will be renamed into another filename anyway. if ( !!$existing_file ) { $meta = wp_get_attachment_metadata( $id ); if ( isset( $meta['original_image'] ) && $new_filename === $meta['original_image'] ) { $output['original_image'] = $meta['original_image']; $output['proposed_filename_exists'] = false; } } // -- if ( $output['proposed_filename_exists'] && empty( $manual_filename ) ) { $skipped_methods = array_merge( $skipped_methods, [ $this->last_used_method ] ); if ( count( $skipped_methods ) < 3 ) { return $this->check_attachment( $post, $output, $manual_filename, $force_rename, $preview, $skipped_methods ); } else { $this->log( "😭 More than 3 skipped methods, no new filename." ); } } // Set the '_require_file_renaming', even though it's not really used at this point (but will be, // with the new UI). if ( !get_post_meta( $post['ID'], '_require_file_renaming', true ) && !$output['locked']) { add_post_meta( $post['ID'], '_require_file_renaming', true, true ); } return true; } function core_base_for_rename( $id , $skipped_methods = [] ) { $base_for_rename = null; $methods = [ $this->method, $this->method_secondary, $this->method_tertiary ]; foreach ( $methods as $method ) { if ( in_array( $method, $skipped_methods ) ) { $this->log( "✒️ Method " . $method . " was skipped." ); continue; } switch ( $method ) { case 'none': $base_for_rename = null; break; case 'media_title': $base_for_rename = get_the_title( $id ); break; case 'alt_text': $image_alt = get_post_meta( $id, '_wp_attachment_image_alt', true ); $base_for_rename = $image_alt; break; default: $this->log( "⚠️ Method " . $method . " not found." ); break; } if ( !is_null( $base_for_rename ) ) { $this->last_used_method = $method; break; } else { $this->log( "✒️ Method " . $method . " returned null. Trying next method." ); } } return !is_null($base_for_rename) ? html_entity_decode($base_for_rename) : ''; } function check_text() { $issues = array(); global $wpdb; $ids = $wpdb->get_col( " SELECT p.ID FROM $wpdb->posts p WHERE post_status = 'inherit' AND post_type = 'attachment' " ); foreach ( $ids as $id ) if ( $this->check_attachment( get_post( $id, ARRAY_A ), $output ) ) array_push( $issues, $output ); return $issues; } /** * * RENAME ON SAVE / PUBLISH * Originally proposed by Ben Heller * Added and modified by Jordy Meow */ function save_post( $post_id ) { $this->log( '⏰ Event: Save Post' ); $status = get_post_status( $post_id ); if ( !in_array( $status, array( 'publish', 'draft', 'future', 'private' ) ) ) { $this->log( "😭 Not a valid status for renaming: " . $status ); return; } $args = array( 'post_type' => 'attachment', 'numberposts' => -1, 'post_status' =>'any', 'post_parent' => $post_id ); $medias = get_posts( $args ); if ( $medias ) { foreach ( $medias as $attach ) { $this->engine->rename( $attach->ID, null, false, 'updated' ); } } else { $this->log( "😭 No media found on save. ( The medias might be attached to a prior post parent )." ); } } /** * * EDITOR * */ function attachment_fields_to_save( $post_id, $post_after, $post_before ) { // Check that it's actually an attachment post type if ( get_post_type( $post_id ) !== 'attachment' ) { return; } // Clear cached proposed filename if title changed (since it may affect the ideal filename) if ( $post_before->post_title !== $post_after->post_title ) { delete_post_meta( $post_id, '_mfrh_proposed_filename' ); delete_post_meta( $post_id, '_mfrh_used_method' ); delete_post_meta( $post_id, '_mfrh_ideal_filename' ); delete_post_meta( $post_id, '_mfrh_proposed_filename_exists' ); } $should_rename = $this->get_option( 'trigger_on_attachment_update', false ); if ( !$should_rename ) { return; } // If the filename is being changed, we should not rename the file, we should only rename when a meta field is being changed. $before_post_name = $post_before->post_name; $after_post_name = $post_after->post_name; if ( $before_post_name != $after_post_name ) { $this->log( "🟠 [Attachment updated] Filename was changed, no need to rename the file." ); return; } $this->log( '⏰ Event: Attachment updated.' ); $this->engine->rename( $post_after, null, false, 'updated' ); } /** * Clear cached proposed filename when alt text changes. */ function on_alt_text_updated( $meta_id, $post_id, $meta_key, $meta_value ) { if ( $meta_key !== '_wp_attachment_image_alt' ) { return; } if ( get_post_type( $post_id ) !== 'attachment' ) { return; } delete_post_meta( $post_id, '_mfrh_proposed_filename' ); delete_post_meta( $post_id, '_mfrh_used_method' ); delete_post_meta( $post_id, '_mfrh_ideal_filename' ); delete_post_meta( $post_id, '_mfrh_proposed_filename_exists' ); } function log_sql( $data, $antidata ) { if ( !$this->get_option( 'logsql' ) || !$this->admin->is_registered() ) return; $dir = wp_upload_dir(); $dir = $dir['basedir']; $fh = fopen( trailingslashit( $dir ) . 'mfrh_sql.log', 'a' ); $fh_anti = fopen( trailingslashit( $dir ) . 'mfrh_sql_revert.log', 'a' ); fwrite( $fh, "{$data}\n" ); fwrite( $fh_anti, "{$antidata}\n" ); fclose( $fh ); fclose( $fh_anti ); } // MFRH_PREFIX function get_logs_path() { $uploads_dir = wp_upload_dir(); $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] ); $path = $this->get_option( 'logs_path' ); if ( $path && file_exists( $path ) ) { // make sure the path is legal (within the uploads directory with the MFRH_PREFIX prefix and log extension) if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MFRH_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) { $path = null; } else { return $path; } } if ( !$path ) { $path = $uploads_dir_path . MFRH_PREFIX . "_" . $this->random_ascii_chars() . ".log"; if ( !file_exists( $path ) ) { touch( $path ); } $options = $this->get_all_options(); $options['logs_path'] = $path; $this->update_options( $options ); } return $path; } public function get_nonce( $force = false ) { // NONCE GENERATION LOGIC: // - For logged-out users (unless forced): Return null - they must use /start_session endpoint // - For logged-in users: Create user-specific nonce tied to their WP session // - With $force=true: Always create nonce (used by /start_session endpoint) // // This ensures logged-in users get a nonce matching their auth context on page load, // preventing rest_cookie_invalid_nonce errors when cookies are present. if ( !$force && !is_user_logged_in() ) { return null; } if ( isset( $this->nonce ) ) { return $this->nonce; } $this->nonce = wp_create_nonce( 'wp_rest' ); return $this->nonce; } function log( $data = null ) { $log = $this->get_option( 'log', false ); if ( !$log ) { return false; } $php_logs = $this->get_option( 'php_error_logs', false ); if ( $php_logs ) { error_log( $data ); } $log_file_path = $this->get_logs_path(); $fh = @fopen( $log_file_path, 'a' ); if ( !$fh ) { return false; } $date = date( "Y-m-d H:i:s" ); if ( is_null( $data ) ) { fwrite( $fh, "\n" ); } else { fwrite( $fh, "$date: {$data}\n" ); } fclose( $fh ); return true; } function get_logs() { $log_file_path = $this->get_logs_path(); if ( !file_exists( $log_file_path ) ) { return "No logs found."; } $content = file_get_contents( $log_file_path ); $lines = explode( "\n", $content ); $lines = array_filter( $lines ); $lines = array_reverse( $lines ); $content = implode( "\n", $lines ); return $content; } function clear_logs() { $logPath = $this->get_logs_path(); if ( file_exists( $logPath ) ) { unlink( $logPath ); } $options = $this->get_all_options(); $options['logs_path'] = null; $this->update_options( $options ); } // Only replace the first occurence function str_replace( $needle, $replace, $haystack ) { if ( empty( $needle ) || empty( $haystack ) ) { return $haystack; } $pos = strpos( $haystack, $needle ); if ( $pos !== false ) $haystack = substr_replace( $haystack, $replace, $pos, strlen( $needle ) ); return $haystack; } /** * * RENAME FILES + COFFEE TIME */ // From a url to the shortened and cleaned url (for example '2025/02/file.png') function clean_url( $url ) { $dirIndex = strpos( $url, $this->contentDir ); if ( empty( $url ) || $dirIndex === false ) { $finalUrl = null; } else { $finalUrl = urldecode( substr( $url, 1 + strlen( $this->contentDir ) + $dirIndex ) ); } return $finalUrl; } function call_hooks_rename_url( $post, $orig_image_url, $new_image_url, $size = 'N/A' ) { // With the full URLs // 2021/11/03: I am not sure we need this, since the clean URLs would also match // do_action( 'mfrh_url_renamed', $post, $orig_image_url, $new_image_url ); // With clean URLs relative to /uploads $cleaned_orig_image_url = $this->clean_url( $orig_image_url ); $cleaned_new_image_url = $this->clean_url( $new_image_url ); if ( !empty( $cleaned_orig_image_url ) && !empty( $cleaned_new_image_url ) ) { do_action( 'mfrh_url_renamed', $post, $cleaned_orig_image_url, $cleaned_new_image_url, $size ); } // With DB URLs (honestly, not sure about this...) // $upload_dir = wp_upload_dir(); // do_action( 'mfrh_url_renamed', $post, str_replace( $upload_dir, "", $orig_image_url ), // str_replace( $upload_dir, "", $new_image_url ) ); } // Security: validates that a user-supplied relative path stays inside the uploads // directory. Prevents path traversal in move() and create_folder() (e.g. an admin // passing '../../wp-content/themes'). realpath() can't run on a not-yet-created // target, so we reject '..' segments and verify the deepest existing ancestor // (resolved through symlinks) is still under the uploads base. Returns the cleaned // relative path. Throws on any attempt to escape the uploads directory. private function sanitize_uploads_relative_path( $relative_path ) { $relative_path = str_replace( '\\', '/', (string) $relative_path ); if ( strpos( $relative_path, "\0" ) !== false ) { throw new Exception( __( 'Invalid path.', 'media-file-renamer' ) ); } $segments = array(); foreach ( explode( '/', trim( $relative_path, '/' ) ) as $segment ) { if ( $segment === '' || $segment === '.' ) { continue; } if ( $segment === '..' ) { throw new Exception( __( 'Invalid path.', 'media-file-renamer' ) ); } $segments[] = $segment; } $clean = implode( '/', $segments ); $upload_dir = wp_upload_dir(); $basedir = str_replace( '\\', '/', $upload_dir['basedir'] ); $real_base = realpath( $basedir ); $real_base = $real_base ? str_replace( '\\', '/', $real_base ) : $basedir; // Walk up to the deepest existing ancestor and confirm it resolves inside the // uploads base (this catches symlinks within uploads that point elsewhere). $ancestor = $basedir . ( $clean !== '' ? '/' . $clean : '' ); while ( $ancestor && ! file_exists( $ancestor ) ) { $parent = dirname( $ancestor ); if ( $parent === $ancestor ) { break; } $ancestor = $parent; } $real_ancestor = realpath( $ancestor ); if ( $real_ancestor !== false ) { $real_ancestor = str_replace( '\\', '/', $real_ancestor ); if ( strpos( trailingslashit( $real_ancestor ), trailingslashit( $real_base ) ) !== 0 ) { throw new Exception( __( 'Invalid path.', 'media-file-renamer' ) ); } } return $clean; } function create_folder( $directory_path ) { $upload_dir = wp_upload_dir(); $new_directory_path = trailingslashit( $upload_dir['basedir'] ) . $this->sanitize_uploads_relative_path( $directory_path ); if ( file_exists( $new_directory_path ) ) { $this->log( "🚫 The directory already existed: $new_directory_path" ); throw new Exception( __( 'The directory already existed.', 'media-file-renamer') ); } if ( !mkdir( $new_directory_path, 0777, true ) ) { $this->log( "🚫 The directory couldn't be created: $new_directory_path" ); throw new Exception( __( "The directory couldn't be created.", 'media-file-renamer') ); } $this->log( "✅ The directory was created: $new_directory_path" ); } function move( $media, $newPath ) { $id = null; $post = null; if ( PHP_OS_FAMILY == 'Windows' ) { $newPath = str_replace( '\\', '/', $newPath ); } // Check the arguments if ( is_numeric( $media ) ) { $id = $media; $post = get_post( $media, ARRAY_A ); } else if ( is_array( $media ) ) { $id = $media['ID']; $post = $media; } else { die( 'Media File Renamer: move() requires the ID or the array for the media.' ); } // Prepare the variables $orig_attachment_url = null; $old_filepath = get_attached_file( $id ); if ( PHP_OS_FAMILY == 'Windows' ) { $old_filepath = str_replace( '\\', '/', $old_filepath ); } $path_parts = mfrh_pathinfo( $old_filepath ); $old_ext = $path_parts['extension']; $upload_dir = wp_upload_dir(); if ( PHP_OS_FAMILY == 'Windows' ) { $upload_dir['basedir'] = str_replace( '\\', '/', $upload_dir['basedir'] ); } $old_directory = trim( str_replace( $upload_dir['basedir'], '', $path_parts['dirname'] ), '/' ); // '2011/01' $new_directory = $this->sanitize_uploads_relative_path( $newPath ); // Throws on path traversal. $filename = $path_parts['basename']; // 'whatever.jpeg' $new_filepath = trailingslashit( trailingslashit( $upload_dir['basedir'] ) . $new_directory ) . $filename; $this->log( "🏁 Move Media: " . $filename ); $this->log( "The new directory will be: " . mfrh_dirname( $new_filepath ) ); // Create the directory if it does not exist if ( !file_exists( mfrh_dirname( $new_filepath ) ) ) { mkdir( mfrh_dirname( $new_filepath ), 0777, true ); } // There is no support for UNDO (as the current process of Media File Renamer doesn't keep the path for the undo, only the filename... so the move breaks this - let's deal with this later). // Move the main media file if ( !$this->engine->rename_file( $old_filepath, $new_filepath ) ) { $this->log( "🚫 File $old_filepath ➡️ $new_filepath" ); return false; } $this->log( "✅ File $old_filepath ➡️ $new_filepath" ); do_action( 'mfrh_path_renamed', $post, $old_filepath, $new_filepath ); // Update the attachment meta $meta = wp_get_attachment_metadata( $id ); if ( $meta ) { if ( isset( $meta['file'] ) && !empty( $meta['file'] ) ) $meta['file'] = $this->str_replace( $old_directory, $new_directory, $meta['file'] ); if ( isset( $meta['url'] ) && !empty( $meta['url'] ) && strlen( $meta['url'] ) > 4 ) $meta['url'] = $this->str_replace( $old_directory, $new_directory, $meta['url'] ); //wp_update_attachment_metadata( $id, $meta ); } // Better to check like this rather than with wp_attachment_is_image // PDFs also have thumbnails now, since WP 4.7 $has_thumbnails = isset( $meta['sizes'] ); if ( $has_thumbnails ) { // Support for the original image if it was "-rescaled". $is_scaled_image = isset( $meta['original_image'] ) && !empty( $meta['original_image'] ); if ( $is_scaled_image ) { $meta_old_filename = $meta['original_image']; $meta_old_filepath = trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $old_directory ) . $meta_old_filename; $meta_new_filepath = trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $new_directory ) . $meta_old_filename; if ( !$this->engine->rename_file( $meta_old_filepath, $meta_new_filepath ) ) { $this->log( "🚫 File $meta_old_filepath ➡️ $meta_new_filepath" ); } else { $this->log( "✅ File $meta_old_filepath ➡️ $meta_new_filepath" ); do_action( 'mfrh_path_renamed', $post, $meta_old_filepath, $meta_new_filepath ); } } // Image Sizes (Thumbnails) $orig_image_urls = array(); $orig_image_data = wp_get_attachment_image_src( $id, 'full' ); $orig_image_urls['full'] = $orig_image_data[0]; foreach ( $meta['sizes'] as $size => $meta_size ) { if ( !isset($meta['sizes'][$size]['file'] ) ) continue; $meta_old_filename = $meta['sizes'][$size]['file']; $meta_old_filepath = trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $old_directory ) . $meta_old_filename; $meta_new_filepath = trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $new_directory ) . $meta_old_filename; $orig_image_data = wp_get_attachment_image_src( $id, $size ); $orig_image_urls[$size] = $orig_image_data[0]; // Double check files exist before trying to rename. if ( file_exists( $meta_old_filepath ) && ( ( !file_exists( $meta_new_filepath ) ) || is_writable( $meta_new_filepath ) ) ) { // Get the actual extension of the thumbnail file (important for PDF thumbnails which are .jpg) $meta_ext = pathinfo( $meta_old_filename, PATHINFO_EXTENSION ); // WP Retina 2x is detected, let's rename those files as well if ( function_exists( 'wr2x_get_retina' ) ) { $wr2x_old_filepath = $this->str_replace( '.' . $meta_ext, '@2x.' . $meta_ext, $meta_old_filepath ); $wr2x_new_filepath = $this->str_replace( '.' . $meta_ext, '@2x.' . $meta_ext, $meta_new_filepath ); $same_filename = basename( $wr2x_old_filepath ) === basename( $wr2x_new_filepath ); if ( !$same_filename && file_exists( $wr2x_old_filepath ) && ( ( !file_exists( $wr2x_new_filepath ) ) || is_writable( $wr2x_new_filepath ) ) ) { // Rename retina file if ( !$this->engine->rename_file( $wr2x_old_filepath, $wr2x_new_filepath ) ) { $this->log( "🚫 Retina $wr2x_old_filepath ➡️ $wr2x_new_filepath" ); return $post; } $this->log( "✅ Retina $wr2x_old_filepath ➡️ $wr2x_new_filepath" ); do_action( 'mfrh_path_renamed', $post, $wr2x_old_filepath, $wr2x_new_filepath ); } } // Rename meta file if ( !$this->engine->rename_file( $meta_old_filepath, $meta_new_filepath ) ) { $this->log( "🚫 File $meta_old_filepath ➡️ $meta_new_filepath" ); return false; } // Success, call other plugins $this->log( "✅ File $meta_old_filepath ➡️ $meta_new_filepath" ); do_action( 'mfrh_path_renamed', $post, $meta_old_filepath, $meta_new_filepath ); } } } else { $orig_attachment_url = wp_get_attachment_url( $id ); } // Update DB: Media and Metadata $new_filepath = str_replace( $upload_dir['basedir'] . '/', '', $new_filepath ); update_attached_file( $id, $new_filepath ); if ( $meta ) { wp_update_attachment_metadata( $id, $meta ); } // Handle the WebP, Avif and PDF thumbnails if they exist // We do this after the main rename, as we will resave new metadata for the different sizes, and we want to make sure the paths are correct for those files. $this->engine->rename_alternative_image_formats( $old_filepath, $old_ext, $new_filepath, $old_ext ); clean_post_cache( $id ); // TODO: Would be good to know what this WP function actually does (might be useless) // If the guid is not par of the sizes ( which happens for PDFs for example where sizes are only thumbnails and not the full file), we should update it as well, otherwise there will be broken links. $guid_found_in_sizes = false; foreach( $orig_image_urls as $size => $url ) { if( $post['guid'] === $url ) { $guid_found_in_sizes = true; break; } } if ( !$guid_found_in_sizes ) { $this->call_post_actions( $id, $post, $meta, false, [], $post['guid'] ); do_action( 'mfrh_media_renamed', $post, $old_filepath, $new_filepath, false, 'move' ); } // Post actions $this->call_post_actions( $id, $post, $meta, $has_thumbnails, $orig_image_urls, $orig_attachment_url ); do_action( 'mfrh_media_renamed', $post, $old_filepath, $new_filepath, false, 'move' ); return true; } function get_human_readable_language() { $locale = get_locale(); if ( class_exists( 'Locale' ) ) { return Locale::getDisplayName( $locale, $locale ); } // Sorry if your language is not listed here. It's not because I do not love you! $languages = array( 'en_US' => 'English (United States)', 'fr_FR' => 'Français (France)', 'de_DE' => 'Deutsch (Deutschland)', 'es_ES' => 'Español (España)', 'it_IT' => 'Italiano (Italia)', 'pt_PT' => 'Português (Portugal)', 'pt_BR' => 'Português (Brasil)', 'ja_JP' => '日本語 (日本)', 'zh_CN' => '中文 (中国)', 'zh_TW' => '中文 (台灣)', 'ko_KR' => '한국어 (대한민국)', 'ru_RU' => 'Русский (Россия)', 'vi_VN' => 'Tiếng Việt (Việt Nam)', 'tr_TR' => 'Türkçe (Türkiye)' ); return isset( $languages[$locale] ) ? $languages[$locale] : 'English (United States)'; } /** * Generates a new suggestion for media metadata based on type. * * @param int $mediaId ID of the media. * @param string $metadataType Type of metadata (title, description, alt text, caption, filename). * @return string New metadata suggestion. */ function ai_suggestion( $mediaId, $metadataType, $binary_path = null ) { $this->last_vision_error = null; $is_binary = !is_null( $binary_path ); $keywords = ""; // Prepare metadata from the entry. $metadata = []; $entry = null; if ( !$is_binary ) { $entry = $this->get_media_status_one( $mediaId ); $metadata = [ 'title' => $entry->post_title, 'alt' => $entry->image_alt, 'description' => $entry->image_description, 'caption' => $entry->image_caption, 'filename' => $entry->current_filename, ]; } // Adjust metadata type for the prompt. $readableType = $metadataType === 'alt' ? 'alternative text' : $metadataType; $promptType = $metadataType === 'filename' ? "$readableType. Should be ASCII-friendly, lowercase, respecting filename standards, words separated by hyphens, but do not use the language (or locale) in the filename" : "$readableType. Should be human-readable with spaces and punctuation"; // Start constructing the prompt. $prompt = "Suggest a new $promptType in " . $this->get_human_readable_language() . "."; if ( !$is_binary ) { $prompt .= "\n\nAdditional information to craft this $readableType:\n"; foreach ( $metadata as $type => $value ) { if ( !empty ( $value ) ) { $prompt .= "* Current " . ucfirst ( $type ) . ": $value.\n"; } } $prompt .= "The values above might also be modified soon, so it's only for reference.\n\n"; $keywords = $this->get_seo_keywords( $mediaId ); if ( !empty( $keywords ) ) { $prompt .= "* SEO keywords related to the media: $keywords.\n"; } } // Define max lengths for each metadata type. $lengths = [ 'alternative text' => '16 words', 'title' => '64 characters', 'description' => '256 characters', 'filename' => '64 characters', 'caption' => '155 characters', ]; // Append specifications for the new metadata. $use_manual_prompts = $this->get_option( 'manual_prompt', false ); $length = $lengths[$readableType]; $prompt .= "This new $readableType must be shorter than $length, SEO-optimized, humanly-readable. Only return the $readableType."; // If it's a description, mention that it should describe the image, of what's happening on it in one paragraph. if ( $metadataType === 'description' ) { $prompt .= " " . ( $use_manual_prompts ? $this->get_option( 'manual_prompt_description', $this->default_prompts['manual_prompt_description'] ) : $this->default_prompts['manual_prompt_description'] ); } else if ( $metadataType === 'caption' ) { $prompt .= " " . ( $use_manual_prompts ? $this->get_option( 'manual_prompt_caption', $this->default_prompts['manual_prompt_caption'] ) : $this->default_prompts['manual_prompt_caption'] ) ; } else if ( $metadataType === 'alternative text' ) { $prompt .= " " . ( $use_manual_prompts ? $this->get_option( 'manual_prompt_alt', $this->default_prompts['manual_prompt_alt'] ) : $this->default_prompts['manual_prompt_alt'] ); } else if ( $metadataType === 'filename' ) { $prompt .= " " . ( $use_manual_prompts ? $this->get_option( 'manual_prompt_filename', $this->default_prompts['manual_prompt_filename'] ) : $this->default_prompts['manual_prompt_filename'] ); } else if ( $metadataType === 'title' ) { $prompt .= " " . ( $use_manual_prompts ? $this->get_option( 'manual_prompt_title', $this->default_prompts['manual_prompt_title'] ) : $this->default_prompts['manual_prompt_title'] ); } // Give the user a chance to modify the prompt. $prompt = apply_filters( 'mfrh_ai_prompt', $prompt, $metadataType, $entry, $keywords ); // Get new metadata, first trying a filter, then defaulting to an AI query. $use_vision = $this->get_option( 'vision_rename_ai', false ); $newMetadata = ''; if( $use_vision ) { $this->log( '🤖 Using Vision for a media query.' ); $newMetadata = apply_filters( 'mfrh_vision_suggestion', null, $mediaId, $binary_path, $prompt ); } if( !$use_vision ) { global $mwai; if ( is_null( $mwai ) ) { $this->log( '🚫 AI Engine is missing for a text query.' ); return ''; } $this->log( '🤖 Using Text for a media query.' ); $newMetadata = $mwai->simpleTextQuery( $prompt, [ 'scope' => 'renamer' ] ); } if( empty( $newMetadata ) ) { $this->log( '🚫 Vision AI Engine did not return any suggestion for a media query.' ); return ''; } // Clean up the new metadata. $newMetadata = trim( $newMetadata ); $newMetadata = str_replace( ['"', "'"], '', $newMetadata ); // If it's a filename, additional cleaning and checks are needed. if ( $metadataType === 'filename' ) { $newMetadata = strtolower( $newMetadata ); if ( substr( $newMetadata, -1 ) === '.' ) { $newMetadata = substr( $newMetadata, 0, -1 ); } if ( strpos( $newMetadata, ',' ) !== false ) { $newMetadata = explode( ',', $newMetadata ); $newMetadata = array_map( 'trim', $newMetadata ); } // Let's get the file which doesn't exist yet. $path = get_attached_file( $mediaId ); $directory_path = dirname( $path ); foreach ( $newMetadata as $filename ) { $newMetadata = $filename; $newPath = $directory_path . '/' . $filename; if ( !file_exists( $newPath ) ) { break; } } } return $newMetadata; } function get_seo_keywords( $attachment_id ) { $parent_id = get_post_field( 'post_parent', $attachment_id ); // Check both attachment and parent $ids_to_check = array_filter( [ $attachment_id, $parent_id ] ); $found_keywords = ''; foreach ( $ids_to_check as $id ) { // SEO Engine $kw = get_post_meta( $id, '_mwseo_keywords', true ); // Yoast fallback if ( empty( $kw ) ) $kw = get_post_meta( $id, '_yoast_wpseo_focuskw', true ); // Rank Math fallback if ( empty( $kw ) ) $kw = get_post_meta( $id, 'rank_math_focus_keyword', true ); // SEOPress fallback if ( empty( $kw ) ) $kw = get_post_meta( $id, '_seopress_analysis_target_kw', true ); if ( ! empty( $kw ) ) { $found_keywords = is_array( $kw ) ? implode( ', ', $kw ) : $kw; break; } } return $found_keywords; } /** * Get the status for many Media IDs. * * @param integer $mediaId * @return object|null */ function get_media_status_one( $mediaId ) { global $wpdb; $entry = $wpdb->get_row( $wpdb->prepare( "SELECT p.ID, p.post_title, p.post_parent, p.post_content AS image_description, p.post_excerpt AS image_caption, MAX(CASE WHEN pm.meta_key = '_wp_attached_file' THEN pm.meta_value END) AS current_filename, MAX(CASE WHEN pm.meta_key = '_original_filename' THEN pm.meta_value END) AS original_filename, MAX(CASE WHEN pm.meta_key = '_wp_attachment_metadata' THEN pm.meta_value END) AS metadata, MAX(CASE WHEN pm.meta_key = '_wp_attachment_image_alt' THEN pm.meta_value END) AS image_alt, MAX(CASE WHEN pm.meta_key = '_require_file_renaming' THEN pm.meta_value END) AS pending, MAX(CASE WHEN pm.meta_key = '_manual_file_renaming' THEN pm.meta_value END) AS locked FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON pm.post_id = p.ID WHERE p.ID = %d AND post_type='attachment' AND (pm.meta_key = '_wp_attached_file' OR pm.meta_key = '_original_filename' OR pm.meta_key = '_wp_attachment_metadata' OR pm.meta_key = '_wp_attachment_image_alt' OR pm.meta_key = '_require_file_renaming' OR pm.meta_key = '_manual_file_renaming' ) GROUP BY p.ID", $mediaId ) ); if ( empty( $entry ) ) { error_log( "Media File Renamer: Could not find the status for the Media ID: $mediaId." ); return null; } $this->consolidate_media_status( $entry ); return $entry; } /** * Organize the data of the entry. * It is used by get_media_status and get_media_status_one. * * @param [type] $entry * @return void */ function consolidate_media_status( &$entry ) { // $metadata = unserialize( $entry->metadata ); $entry->issues = []; $entry->ID = (int)$entry->ID; $entry->post_parent = !empty( $entry->post_parent ) ? (int)$entry->post_parent : null; $entry->post_parent_title = !empty( $entry->post_parent ) ? get_the_title( $entry->post_parent ) : null; $entry->thumbnail_url = wp_get_attachment_thumb_url( $entry->ID ); $entry->url = wp_get_attachment_url( $entry->ID ); // Extract path from current_filename (already fetched in query, no need for extra get_post_meta call) $attached_file = $entry->current_filename; if ( !empty( $attached_file ) ) { $path = $attached_file; if ( substr( $path, 0, 1 ) !== '/' ) { $path = '/' . $path; } $path = substr( $path, 0, strrpos( $path, '/' ) ); $entry->path = empty( $path ) ? '/' : $path; } else { $entry->issues[] = 'missing_file'; } $entry->current_filename = pathinfo( $entry->current_filename, PATHINFO_BASENAME ); $entry->locked = $entry->locked === '1'; $entry->pending = $entry->pending === '1'; $entry->proposed_filename = null; $lock_enabled = $this->get_option( 'lock' ); if ( !$lock_enabled || !$entry->locked ) { // Use cached values from Re-analyze Library if available (much faster) $has_cached = !empty( $entry->cached_proposed_filename ); if ( $has_cached ) { $entry->proposed_filename = $entry->cached_proposed_filename; $entry->proposed_filename_exists = $entry->cached_proposed_filename_exists === '1'; $entry->used_method = $entry->cached_used_method; $entry->ideal_filename = $entry->cached_ideal_filename; } else { // Fallback for media not yet analyzed - compute on the fly $force_rename = $this->get_option( 'force_rename' ); $output = []; $this->check_attachment( get_post( $entry->ID, ARRAY_A ), $output, null, $force_rename ); if ( isset( $output['ideal_filename'] ) ) { $entry->ideal_filename = $output['ideal_filename']; } if ( isset( $output['proposed_filename'] ) ) { $entry->proposed_filename = $output['proposed_filename']; $entry->proposed_filename_exists = $output['proposed_filename_exists']; } if ( isset( $output['used_method'] ) ) { $entry->used_method = $output['used_method']; } if ( isset( $output['skipped_methods'] ) ) { $entry->skipped_methods = $output['skipped_methods']; } } } // Clean up cached fields from the response (not needed by frontend) unset( $entry->cached_proposed_filename ); unset( $entry->cached_used_method ); unset( $entry->cached_ideal_filename ); unset( $entry->cached_proposed_filename_exists ); } // Call the actions so that the plugin's plugins can update everything else (than the files) // Called by rename() and move() function call_post_actions( $id, $post, $meta, $has_thumbnails, $orig_image_urls, $orig_attachment_url ) { if ( $has_thumbnails ) { $orig_image_url = $orig_image_urls['full']; $new_image_data = wp_get_attachment_image_src( $id, 'full' ); $new_image_url = $new_image_data[0]; $this->call_hooks_rename_url( $post, $orig_image_url, $new_image_url, 'full' ); if ( !empty( $meta['sizes'] ) ) { foreach ( $meta['sizes'] as $size => $meta_size ) { if ( isset( $orig_image_urls[$size] ) ) { $orig_image_url = $orig_image_urls[$size]; $new_image_data = wp_get_attachment_image_src( $id, $size ); $new_image_url = $new_image_data[0]; $this->call_hooks_rename_url( $post, $orig_image_url, $new_image_url, $size ); } } } } else { $new_attachment_url = wp_get_attachment_url( $id ); $this->call_hooks_rename_url( $post, $orig_attachment_url, $new_attachment_url, 'full' ); } // HTTP REFERER set to the new media link if ( isset( $_REQUEST['_wp_original_http_referer'] ) && strpos( $_REQUEST['_wp_original_http_referer'], '/wp-admin/' ) === false ) { $_REQUEST['_wp_original_http_referer'] = get_permalink( $id ); } } /** * The params should be an array with the following keys: * - Method (string): 'manual', 'vision', 'ai', 'auto', 'undo' * - Metadata (string): 'title', 'alt', 'description', 'filename' * - Original (string): The original value * - New (string): The new value * - Date (date): The date of the change * * @param int $media_id * @param array $params * @return void * */ function add_to_media_history( $media_id, $params ) { $history = get_post_meta( $media_id, '_mfrh_history', true ); if ( !is_array( $history ) ) { $history = array(); } $metadata = $params['metadata']; if (!isset($history[$metadata])) { $history[$metadata] = array(); } $history[$metadata][] = $params; $history_limit = $this->get_option( 'history_limit', 4); if ( count( $history[$metadata] ) > $history_limit ) { array_shift( $history[$metadata] ); } update_post_meta( $media_id, '_mfrh_history', $history ); } function update_media( $id, $postTitle, $imageAlt, $imageDescription, $imageCaption, $method, $sync = false) { $errors = []; if ( !$id || ( !$postTitle && !$imageAlt && !$imageDescription && !$imageCaption ) ) { $errors[] = __( 'The update title or alt parameters are missing.', 'media-file-renamer' ); } if ( $postTitle ) { $previousPostTitle = get_post_field( 'post_title', $id ); if ( $previousPostTitle !== $postTitle ) { $result = wp_update_post( [ 'ID' => $id, 'post_title' => $postTitle ], true ); if ( is_wp_error( $result ) ) { $errors = array_merge($errors, $result->get_error_messages()); } $this->add_to_media_history( $id, [ 'method' => $method, 'metadata' => 'title', 'original' => $previousPostTitle, 'new' => $postTitle, 'sync' => $sync, 'date' => date( 'Y-m-d H:i:s' ), ] ); } } if ( $imageAlt ) { $previousImageAlt = get_post_meta( $id, '_wp_attachment_image_alt', true ); if ( $previousImageAlt !== $imageAlt ) { $result = update_post_meta( $id, '_wp_attachment_image_alt', $imageAlt ); if ( !$result ) { $errors[] = __( 'The image alt could not be updated.', 'media-file-renamer' ); } $this->add_to_media_history( $id, [ 'method' => $method, 'metadata' => 'alt', 'original' => $previousImageAlt, 'new' => $imageAlt, 'sync' => $sync, 'date' => date( 'Y-m-d H:i:s' ), ] ); } } if ( $imageDescription ) { $previousImageDescription = get_post_field( 'post_content', $id ); if ( $previousImageDescription !== $imageDescription ) { $result = wp_update_post( [ 'ID' => $id, 'post_content' => $imageDescription ], true ); if ( is_wp_error( $result ) ) { $errors = array_merge($errors, $result->get_error_messages()); } $this->add_to_media_history( $id, [ 'method' => $method, 'metadata' => 'description', 'original' => $previousImageDescription, 'new' => $imageDescription, 'sync' => $sync, 'date' => date( 'Y-m-d H:i:s' ), ] ); } } if ( $imageCaption ) { $previousImageCaption = get_post_field( 'post_excerpt', $id ); if ( $previousImageCaption !== $imageCaption ) { $result = wp_update_post( [ 'ID' => $id, 'post_excerpt' => $imageCaption ], true ); if ( is_wp_error( $result ) ) { $errors = array_merge($errors, $result->get_error_messages()); } $this->add_to_media_history( $id, [ 'method' => $method, 'metadata' => 'caption', 'original' => $previousImageCaption, 'new' => $imageCaption, 'sync' => $sync, 'date' => date( 'Y-m-d H:i:s' ), ] ); } } return ['errors' => $errors]; } function undo( $mediaId ) { $original_filename = get_post_meta( $mediaId, '_original_filename', true ); if ( empty( $original_filename ) ) { return true; } $res = $this->engine->rename( $mediaId, $original_filename, true, 'undo' ); if ( !!$res ) { delete_post_meta( $mediaId, '_original_filename' ); } return $res; } /** * Linking with api.php call (l.20) */ function rename( $mediaId, $manual ){ $res = $this->engine->rename( $mediaId, $manual ); return $res; } /** * Locks a post to be manual-rename only * @param int|WP_Post $post The post to lock * @return True on success, false on failure */ function lock( $post ) { //TODO: We should probably only take an ID as the argument $id = $post instanceof WP_Post ? $post->ID : $post; delete_post_meta( $id, '_require_file_renaming' ); update_post_meta( $id, '_manual_file_renaming', true, true ); return true; } /** * Unlocks a locked post * @param int|WP_Post $post The post to unlock * @return True on success, false on failure */ function unlock( $post ) { delete_post_meta( $post instanceof WP_Post ? $post->ID : $post, '_manual_file_renaming' ); return true; } /** * Determines whether a post is locked * @param int|WP_Post $post The post to check * @return Boolean */ function is_locked( $post ) { return get_post_meta( $post instanceof WP_Post ? $post->ID : $post, '_manual_file_renaming', true ) === true; } /** * * Roles & Access Rights * */ public function can_access_settings() { return apply_filters( 'mfrh_allow_setup', current_user_can( 'manage_options' ) ); } public function can_access_features() { return apply_filters( 'mfrh_allow_usage', current_user_can( 'administrator' ) ); } #region Options function reset_options() { delete_option( $this->option_name ); } function reset_metadata() { global $wpdb; // Delete the specific meta keys to reset the status $count = $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key IN ( '_require_file_renaming', '_manual_file_renaming', '_original_filename', '_mfrh_proposed_filename', '_mfrh_used_method', '_mfrh_ideal_filename', '_mfrh_proposed_filename_exists' )" ); return $count; } function get_option( $option, $default = null ) { $options = $this->get_all_options(); return $options[$option] ?? $default; } function list_options() { $options = []; $saved_options = get_option( $this->option_name, null ); $options = $this->check_options( $saved_options ); return $options; } function needs_registered_options() { return array( 'convert_to_ascii', 'unique_files', 'sync_alt', 'sync_media_title', 'force_rename', 'logsql', ); } function get_all_options() { $options = $this->list_options(); $needs_registered_options = $this->needs_registered_options(); foreach ( $options as $key => $value ) { if ( in_array( $key, $needs_registered_options ) ) { //$options[ $key ] = $this->admin->is_registered() && $value; $wanted_value = $options[$key]; $invalid_value = is_string( $wanted_value ) ? 'none' : false; $valid = isset( $this->admin ) && $this->admin->is_registered(); $options[$key] = $valid ? $value : $invalid_value; continue; } } return $options; } function toggle_parser( $parser ) { $options = $this->get_all_options(); if ( key_exists( $parser, $options['parsers'] ) ) { $options['parsers'][$parser]['enabled'] = !$options['parsers'][$parser]['enabled']; } return $this->update_options( $options ); } function update_options( $options ) { if ( !update_option( $this->option_name, $options, false ) ) { return false; } list($options, $result, $message) = $this->sanitize_options(); $validation_result = $this->createValidationResult( $result, $message ); return [ $options, $validation_result['result'], $validation_result['message'] ]; } // Make sure the options are all there and correct. function check_options( $options = [] ) { $options = empty( $options ) || !is_array( $options ) ? [] : $options; $hasChanges = false; foreach ( MFRH_OPTIONS as $key => $value ) { if ( !isset( $options[$key] ) ) { $options[$key] = $value; $hasChanges = true; } } $parsers = [ 'elementor' => [ 'display' => 'Elementor', 'exists' => defined( 'ELEMENTOR_VERSION' ), 'enabled' => true, ], 'oxygen' => [ 'display' => 'Oxygen Builder', 'exists' => class_exists( 'CT_Component' ), 'enabled' => true, ], 'beaver_builder' => [ 'display' => 'Beaver Builder', 'exists' => class_exists( 'FLBuilderModel' ), 'enabled' => true, ], 'wpml' => [ 'display' => 'WPML', 'exists' => function_exists( 'icl_object_id' ), 'enabled' => true, ], 'woocommerce' => [ 'display' => 'WooCommerce', 'exists' => class_exists( 'WooCommerce' ), 'enabled' => true, ], ]; foreach ( $parsers as $key => $parser ) { if ( !isset( $options['parsers'][$key] ) ) { $options['parsers'][$key] = $parser; $hasChanges = true; } else { // Keep the display name and exists status, but not the enabled status. $options['parsers'][$key]['display'] = $parser['display']; $options['parsers'][$key]['exists'] = $parser['exists']; } } if ( $hasChanges ) { update_option( $this->option_name , $options ); } return $options; } function get_mime_type( $file ) { $mimeType = null; // Let's try to use mime_content_type if the function exists if ( function_exists( 'mime_content_type' ) ) { $mimeType = mime_content_type( $file ); } // Otherwise, let's check the file extension (which can actually also be an URL) if ( !$mimeType ) { $extension = pathinfo( $file, PATHINFO_EXTENSION ); $extension = strtolower( $extension ); $mimeTypes = [ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', 'bmp' => 'image/bmp', 'tiff' => 'image/tiff', 'tif' => 'image/tiff', 'svg' => 'image/svg+xml', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', ]; $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null; } return $mimeType; } // Validate and keep the options clean and logical. function sanitize_options() { $options = $this->get_all_options(); $result = true; $message = null; // Reset some options if not registered if( !$this->admin->is_registered() ) { $options['convert_to_ascii'] = false; $options['vision_rename_ai'] = false; $options['logsql'] = false; } $needs_update = false; $force_rename = $options['force_rename']; $unique_files = $options['unique_files'] && $options['unique_files'] !== 'none'; if ( $force_rename && $unique_files ) { $options['force_rename'] = false; $result = false; $message = __( 'Force Rename and Numbered Files cannot be used at the same time. Please use Force Rename only when you are trying to repair a broken install. For now, Force Rename has been disabled.', 'media-file-renamer' ); } $lock = $options['lock']; $has_lock_options = $options['autolock_auto'] || $options['autolock_manual']; if ( !$lock && $has_lock_options ) { $options['autolock_auto'] = false; $options['autolock_manual'] = false; $needs_update = true; } if ( !$options['move'] && $options['mode'] === 'move' ) { $options['mode'] = 'rename'; $needs_update = true; } $isVisionEnabled = $options['vision_rename_ai']; if ( !$isVisionEnabled ) { $visionRename = $options['auto_rename'] === 'vision' || $options['auto_rename_secondary'] === 'vision' || $options['auto_rename_tertiary'] === 'vision'; if ( $visionRename ) { $options['auto_rename'] = $options['auto_rename'] === 'vision' ? 'none' : $options['auto_rename']; $options['auto_rename_secondary'] = $options['auto_rename_secondary'] === 'vision' ? 'none' : $options['auto_rename_secondary']; $options['auto_rename_tertiary'] = $options['auto_rename_tertiary'] === 'vision' ? 'none' : $options['auto_rename_tertiary']; $needs_update = true; } $visionUpload = $options['on_upload_method'] === 'upload_vision' || $options['on_upload_method_secondary'] === 'upload_vision' || $options['on_upload_method_tertiary'] === 'upload_vision'; if ( $visionUpload ) { $options['on_upload_method'] = $options['on_upload_method'] === 'upload_vision' ? 'none' : $options['on_upload_method']; $options['on_upload_method_secondary'] = $options['on_upload_method_secondary'] === 'upload_vision' ? 'none' : $options['on_upload_method_secondary']; $options['on_upload_method_tertiary'] = $options['on_upload_method_tertiary'] === 'upload_vision' ? 'none' : $options['on_upload_method_tertiary']; $needs_update = true; } } if ( $options['manual_prompt'] ) { $prompt_types = ['title', 'alt', 'description', 'caption', 'filename']; foreach ( $prompt_types as $type ) { $option_key = 'manual_prompt_' . $type; if ( empty( $options[$option_key] ) ) { $options[$option_key] = MFRH_OPTIONS[$option_key]; $needs_update = true; } } } if ( !$result || $needs_update ) { update_option( $this->option_name, $options, false ); } return [ $options, $result, $message ]; } function createValidationResult( $result = true, $message = null) { $message = $message ? $message : __( 'Option updated.', 'media-file-renamer' ); return [ 'result' => $result, 'message' => $message ]; } #endregion private function random_ascii_chars( $length = 8 ) { $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) ); $characters_length = count( $characters ); $random_string = ''; for ($i = 0; $i < $length; $i++) { $random_string .= $characters[rand(0, $characters_length - 1)]; } return $random_string; } }