get_admin_css() ); wp_register_script( 'a1tools-media-mgmt', false, array( 'jquery' ), A1TOOLS_VERSION, true ); wp_enqueue_script( 'a1tools-media-mgmt' ); wp_localize_script( 'a1tools-media-mgmt', 'a1mm', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonceLoad' => wp_create_nonce( 'a1tools_load_media' ), 'nonceRename' => wp_create_nonce( 'a1tools_rename_media' ), 'nonceMeta' => wp_create_nonce( 'a1tools_update_media_meta' ), 'nonceBulkRename' => wp_create_nonce( 'a1tools_bulk_rename' ), 'nonceBulkAlt' => wp_create_nonce( 'a1tools_bulk_update_alt' ), 'nonceDuplicates' => wp_create_nonce( 'a1tools_find_duplicates' ), 'nonceUnused' => wp_create_nonce( 'a1tools_scan_unused' ), 'nonceDelete' => wp_create_nonce( 'a1tools_delete_media' ), 'nonceReplace' => wp_create_nonce( 'a1tools_replace_media' ), ) ); wp_add_inline_script( 'a1tools-media-mgmt', $this->get_admin_js() ); } // ========================================================================= // Helpers // ========================================================================= private function is_random_filename( $name ) { $patterns = array( '/^(IMG|DSC|DCIM|DSCN|DSCF|SAM|WP|MG|P\d)[-_]/i', '/^(Screenshot|Screen[\s_\-]?Shot|Capture|Untitled|File)[-_\s]/i', '/^(image|photo|img|pic)[-_]?\d/i', '/^[a-f0-9]{8,}$/i', '/^[a-f0-9]{8}-[a-f0-9]{4}/i', '/^\d{6,}/', '/^(IMG|VID|MOV|AUD)[\d_]{6,}/i', '/^[A-Z]{2,4}\d{4,}/', '/^e[a-f0-9]{20,}/i', '/^wp-image-\d+/i', '/^cropped-/i', ); foreach ( $patterns as $p ) { if ( preg_match( $p, $name ) ) { return true; } } return false; } private function get_batch_usage( $attachment_ids ) { if ( empty( $attachment_ids ) ) { return array(); } global $wpdb; $usage = array_fill_keys( $attachment_ids, 0 ); // Featured images. $placeholders = implode( ',', array_fill( 0, count( $attachment_ids ), '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $featured = $wpdb->get_col( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders is a safe list of %d tokens. "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id' AND meta_value IN ($placeholders)", ...$attachment_ids ) ); foreach ( $featured as $fid ) { if ( isset( $usage[ (int) $fid ] ) ) { $usage[ (int) $fid ] += 1; } } // Post content references (batch by filename). foreach ( $attachment_ids as $aid ) { $url = wp_get_attachment_url( $aid ); if ( ! $url ) continue; $basename = basename( $url ); $name_part = pathinfo( $basename, PATHINFO_FILENAME ); if ( strlen( $name_part ) < 3 ) continue; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT ID) FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_type NOT IN ('revision','attachment','nav_menu_item')", '%' . $wpdb->esc_like( $name_part ) . '%' ) ); $usage[ $aid ] += $count; } return $usage; } // ========================================================================= // AJAX: Load Media // ========================================================================= public function ajax_load_media() { check_ajax_referer( 'a1tools_load_media', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $page = max( 1, absint( $_POST['page'] ?? 1 ) ); $per_page = min( 100, max( 1, absint( $_POST['per_page'] ?? 40 ) ) ); $search = sanitize_text_field( wp_unslash( $_POST['search'] ?? '' ) ); $mime = sanitize_text_field( wp_unslash( $_POST['mime_filter'] ?? '' ) ); $seo_filter = sanitize_text_field( wp_unslash( $_POST['seo_filter'] ?? '' ) ); $orderby = sanitize_text_field( wp_unslash( $_POST['orderby'] ?? 'date' ) ); $order = strtoupper( sanitize_text_field( wp_unslash( $_POST['order'] ?? 'DESC' ) ) ); if ( ! in_array( $orderby, array( 'date', 'title', 'name', 'modified' ), true ) ) { $orderby = 'date'; } if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) { $order = 'DESC'; } $args = array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => $orderby, 'order' => $order, ); if ( ! empty( $search ) ) { $args['s'] = $search; } if ( ! empty( $mime ) ) { $args['post_mime_type'] = $mime; } // SEO filter: missing_alt uses meta_query. if ( 'missing_alt' === $seo_filter ) { $args['post_mime_type'] = 'image'; $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- required to find images missing alt text. 'relation' => 'OR', array( 'key' => '_wp_attachment_image_alt', 'compare' => 'NOT EXISTS' ), array( 'key' => '_wp_attachment_image_alt', 'value' => '', 'compare' => '=' ), ); } // For needs_rename, large_files, unused, optimized — we need post__in filtering. if ( in_array( $seo_filter, array( 'needs_rename', 'large_files', 'unused', 'optimized' ), true ) ) { $filtered_ids = $this->get_filtered_ids( $seo_filter, $mime ); if ( empty( $filtered_ids ) ) { wp_send_json_success( array( 'items' => array(), 'total' => 0, 'pages' => 0, 'current_page' => 1 ) ); } $args['post__in'] = $filtered_ids; } $query = new WP_Query( $args ); $ids = wp_list_pluck( $query->posts, 'ID' ); $usage_map = $this->get_batch_usage( $ids ); $items = array(); foreach ( $query->posts as $post ) { $filepath = get_attached_file( $post->ID ); $filename = $filepath ? basename( $filepath ) : ''; $name_only = pathinfo( $filename, PATHINFO_FILENAME ); $meta = wp_get_attachment_metadata( $post->ID ); $thumb = wp_get_attachment_image_src( $post->ID, 'thumbnail' ); $filesize = ( $filepath && file_exists( $filepath ) ) ? filesize( $filepath ) : 0; $alt = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ); $is_image = wp_attachment_is_image( $post->ID ); $is_random = $this->is_random_filename( $name_only ); $used = isset( $usage_map[ $post->ID ] ) ? $usage_map[ $post->ID ] : 0; $seo = 0; if ( ! $is_random ) $seo++; if ( ! empty( $alt ) ) $seo++; if ( $filesize < 512000 ) $seo++; if ( $used > 0 ) $seo++; $items[] = array( 'id' => $post->ID, 'title' => $post->post_title, 'filename' => $filename, 'url' => wp_get_attachment_url( $post->ID ), 'thumbnail_url' => $thumb ? $thumb[0] : wp_mime_type_icon( $post->ID ), 'is_image' => $is_image, 'mime_type' => $post->post_mime_type, 'file_size' => $filesize, 'file_size_fmt' => size_format( $filesize ), 'width' => isset( $meta['width'] ) ? (int) $meta['width'] : 0, 'height' => isset( $meta['height'] ) ? (int) $meta['height'] : 0, 'date' => get_the_date( 'Y-m-d', $post ), 'alt_text' => $alt ? $alt : '', 'caption' => $post->post_excerpt, 'description' => $post->post_content, 'usage_count' => $used, 'is_random_name' => $is_random, 'seo_score' => $seo, ); } wp_send_json_success( array( 'items' => $items, 'total' => (int) $query->found_posts, 'pages' => (int) $query->max_num_pages, 'current_page' => $page, ) ); } private function get_filtered_ids( $filter, $mime = '' ) { global $wpdb; if ( 'needs_rename' === $filter ) { if ( ! empty( $mime ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT p.ID, pm.meta_value AS filepath FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' AND p.post_mime_type LIKE %s", $mime . '%' ) ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( "SELECT p.ID, pm.meta_value AS filepath FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit'" ); } $ids = array(); foreach ( $rows as $r ) { $name = pathinfo( $r->filepath, PATHINFO_FILENAME ); if ( $this->is_random_filename( $name ) ) { $ids[] = (int) $r->ID; } } return $ids; } if ( 'large_files' === $filter ) { if ( ! empty( $mime ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT p.ID, pm.meta_value AS filepath FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' AND p.post_mime_type LIKE %s", $mime . '%' ) ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( "SELECT p.ID, pm.meta_value AS filepath FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit'" ); } $upload_dir = wp_get_upload_dir(); $base = trailingslashit( $upload_dir['basedir'] ); $ids = array(); foreach ( $rows as $r ) { $path = $base . $r->filepath; if ( file_exists( $path ) && filesize( $path ) > 512000 ) { $ids[] = (int) $r->ID; } } return $ids; } if ( 'unused' === $filter ) { $cached = get_transient( 'a1tools_unused_media_ids' ); if ( false !== $cached ) { return $cached; } // Get ALL attachment IDs. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $all_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit'" ); // Get featured image IDs. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $featured = $wpdb->get_col( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id'" ); $featured = array_map( 'intval', $featured ); $unused = array(); foreach ( $all_ids as $aid ) { $aid = (int) $aid; if ( in_array( $aid, $featured, true ) ) continue; $url = wp_get_attachment_url( $aid ); if ( ! $url ) { $unused[] = $aid; continue; } $name_part = pathinfo( basename( $url ), PATHINFO_FILENAME ); if ( strlen( $name_part ) < 3 ) { $unused[] = $aid; continue; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $found = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_type NOT IN ('revision','attachment','nav_menu_item') LIMIT 1", '%' . $wpdb->esc_like( $name_part ) . '%' ) ); if ( 0 === $found ) { $unused[] = $aid; } } set_transient( 'a1tools_unused_media_ids', $unused, 300 ); return $unused; } if ( 'optimized' === $filter ) { // Get items with good filename, alt text, small size, and used. if ( ! empty( $mime ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT p.ID, pm.meta_value AS filepath, alt.meta_value AS alt_text FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' LEFT JOIN {$wpdb->postmeta} alt ON p.ID = alt.post_id AND alt.meta_key = '_wp_attachment_image_alt' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' AND p.post_mime_type LIKE %s", $mime . '%' ) ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( "SELECT p.ID, pm.meta_value AS filepath, alt.meta_value AS alt_text FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' LEFT JOIN {$wpdb->postmeta} alt ON p.ID = alt.post_id AND alt.meta_key = '_wp_attachment_image_alt' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit'" ); } $upload_dir = wp_get_upload_dir(); $base = trailingslashit( $upload_dir['basedir'] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $featured = $wpdb->get_col( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id'" ); $featured = array_map( 'intval', $featured ); $ids = array(); foreach ( $rows as $r ) { $name = pathinfo( $r->filepath, PATHINFO_FILENAME ); $score = 0; if ( ! $this->is_random_filename( $name ) ) $score++; if ( ! empty( $r->alt_text ) ) $score++; $path = $base . $r->filepath; if ( file_exists( $path ) && filesize( $path ) < 512000 ) $score++; // Check usage (simplified). if ( in_array( (int) $r->ID, $featured, true ) ) { $score++; } else { $name_part = pathinfo( $r->filepath, PATHINFO_FILENAME ); if ( strlen( $name_part ) >= 3 ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $found = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_type NOT IN ('revision','attachment','nav_menu_item') LIMIT 1", '%' . $wpdb->esc_like( $name_part ) . '%' ) ); if ( $found > 0 ) $score++; } } if ( $score >= 4 ) { $ids[] = (int) $r->ID; } } return $ids; } return array(); } // ========================================================================= // AJAX: Rename Media // ========================================================================= public function ajax_rename_media() { check_ajax_referer( 'a1tools_rename_media', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $attachment_id = absint( $_POST['attachment_id'] ?? 0 ); $new_name = sanitize_text_field( wp_unslash( $_POST['new_filename'] ?? '' ) ); $update_refs = ! empty( $_POST['update_refs'] ); if ( ! $attachment_id ) { wp_send_json_error( array( 'message' => 'Invalid attachment ID.' ) ); } $result = $this->do_rename( $attachment_id, $new_name, $update_refs ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( $result ); } // ========================================================================= // AJAX: Update Media Meta (alt, title) // ========================================================================= public function ajax_update_media_meta() { check_ajax_referer( 'a1tools_update_media_meta', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $id = absint( $_POST['attachment_id'] ?? 0 ); if ( ! $id ) { wp_send_json_error( array( 'message' => 'Invalid attachment ID.' ) ); } // Alt text. if ( isset( $_POST['alt_text'] ) ) { update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( wp_unslash( $_POST['alt_text'] ) ) ); } // Title, caption, description. $update = array( 'ID' => $id ); if ( isset( $_POST['title'] ) ) { $update['post_title'] = sanitize_text_field( wp_unslash( $_POST['title'] ) ); } if ( isset( $_POST['caption'] ) ) { $update['post_excerpt'] = sanitize_text_field( wp_unslash( $_POST['caption'] ) ); } if ( isset( $_POST['description'] ) ) { $update['post_content'] = wp_kses_post( wp_unslash( $_POST['description'] ) ); } if ( count( $update ) > 1 ) { wp_update_post( $update ); } wp_send_json_success( array( 'message' => 'Saved.' ) ); } // ========================================================================= // AJAX: Bulk Rename // ========================================================================= public function ajax_bulk_rename() { check_ajax_referer( 'a1tools_bulk_rename', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $ids = array_map( 'absint', (array) ( $_POST['ids'] ?? array() ) ); $pattern = sanitize_text_field( wp_unslash( $_POST['pattern'] ?? '' ) ); $start = max( 1, absint( $_POST['start_num'] ?? 1 ) ); $update_refs = ! empty( $_POST['update_refs'] ); if ( empty( $ids ) || empty( $pattern ) ) { wp_send_json_error( array( 'message' => 'Select items and enter a pattern.' ) ); } $results = array(); $success = 0; $failed = 0; $counter = $start; foreach ( $ids as $id ) { $name = str_replace( '{n}', $counter, $pattern ); $res = $this->do_rename( $id, $name, $update_refs ); if ( is_wp_error( $res ) ) { $results[] = array( 'id' => $id, 'error' => $res->get_error_message() ); $failed++; } else { $results[] = array( 'id' => $id, 'new_filename' => $res['new_filename'] ); $success++; } $counter++; } wp_send_json_success( array( 'results' => $results, 'success' => $success, 'failed' => $failed, 'message' => sprintf( '%d renamed, %d failed.', $success, $failed ), ) ); } // ========================================================================= // AJAX: Bulk Update Alt Text // ========================================================================= public function ajax_bulk_update_alt() { check_ajax_referer( 'a1tools_bulk_update_alt', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $ids = array_map( 'absint', (array) ( $_POST['ids'] ?? array() ) ); $pattern = sanitize_text_field( wp_unslash( $_POST['alt_pattern'] ?? '' ) ); if ( empty( $ids ) || empty( $pattern ) ) { wp_send_json_error( array( 'message' => 'Select items and enter alt text.' ) ); } $count = 0; foreach ( $ids as $id ) { $filepath = get_attached_file( $id ); $fname = $filepath ? pathinfo( basename( $filepath ), PATHINFO_FILENAME ) : ''; $alt = str_replace( '{filename}', ucwords( str_replace( '-', ' ', $fname ) ), $pattern ); update_post_meta( $id, '_wp_attachment_image_alt', $alt ); $count++; } wp_send_json_success( array( 'message' => sprintf( 'Alt text updated for %d items.', $count ), 'count' => $count ) ); } // ========================================================================= // AJAX: Find Duplicates // ========================================================================= public function ajax_find_duplicates() { check_ajax_referer( 'a1tools_find_duplicates', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( "SELECT p.ID, pm.meta_value AS filepath FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file' WHERE p.post_type = 'attachment' AND p.post_status = 'inherit'" ); $upload_dir = wp_get_upload_dir(); $base = trailingslashit( $upload_dir['basedir'] ); // Group by file size. $size_groups = array(); foreach ( $rows as $r ) { $path = $base . $r->filepath; if ( ! file_exists( $path ) ) continue; $size = filesize( $path ); if ( $size < 1000 ) continue; // Skip tiny files. $size_groups[ $size ][] = array( 'id' => (int) $r->ID, 'path' => $path, 'filepath' => $r->filepath ); } // For groups with 2+ files of same size, compare hashes. $duplicates = array(); foreach ( $size_groups as $size => $files ) { if ( count( $files ) < 2 ) continue; $hash_groups = array(); foreach ( $files as $f ) { $hash = md5_file( $f['path'] ); $hash_groups[ $hash ][] = $f; } foreach ( $hash_groups as $hash => $group ) { if ( count( $group ) < 2 ) continue; $dup_items = array(); foreach ( $group as $g ) { $thumb = wp_get_attachment_image_src( $g['id'], 'thumbnail' ); $dup_items[] = array( 'id' => $g['id'], 'filename' => basename( $g['filepath'] ), 'url' => wp_get_attachment_url( $g['id'] ), 'thumb' => $thumb ? $thumb[0] : '', 'size_fmt' => size_format( $size ), ); } $duplicates[] = array( 'size' => $size, 'hash' => $hash, 'files' => $dup_items ); } } wp_send_json_success( array( 'groups' => $duplicates, 'count' => count( $duplicates ), 'message' => count( $duplicates ) ? sprintf( 'Found %d duplicate groups.', count( $duplicates ) ) : 'No duplicates found.', ) ); } // ========================================================================= // AJAX: Scan Unused // ========================================================================= public function ajax_scan_unused() { check_ajax_referer( 'a1tools_scan_unused', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } delete_transient( 'a1tools_unused_media_ids' ); $unused = $this->get_filtered_ids( 'unused' ); wp_send_json_success( array( 'count' => count( $unused ), 'message' => count( $unused ) ? sprintf( 'Found %d unused media files.', count( $unused ) ) : 'All media files are in use.', ) ); } // ========================================================================= // AJAX: Delete Media // ========================================================================= public function ajax_delete_media() { check_ajax_referer( 'a1tools_delete_media', 'nonce' ); if ( ! current_user_can( 'delete_posts' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $id = absint( $_POST['attachment_id'] ?? 0 ); if ( ! $id ) { wp_send_json_error( array( 'message' => 'Invalid attachment ID.' ) ); } $post = get_post( $id ); if ( ! $post || 'attachment' !== $post->post_type ) { wp_send_json_error( array( 'message' => 'Attachment not found.' ) ); } $deleted = wp_delete_attachment( $id, true ); if ( ! $deleted ) { wp_send_json_error( array( 'message' => 'Could not delete the file.' ) ); } wp_send_json_success( array( 'message' => 'File deleted.', 'id' => $id ) ); } // ========================================================================= // AJAX: Replace Media File // ========================================================================= public function ajax_replace_media() { check_ajax_referer( 'a1tools_replace_media', 'nonce' ); if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ) ); } $id = absint( $_POST['attachment_id'] ?? 0 ); if ( ! $id ) { wp_send_json_error( array( 'message' => 'Invalid attachment ID.' ) ); } $post = get_post( $id ); if ( ! $post || 'attachment' !== $post->post_type ) { wp_send_json_error( array( 'message' => 'Attachment not found.' ) ); } if ( empty( $_FILES['file'] ) ) { wp_send_json_error( array( 'message' => 'No file uploaded.' ) ); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; // Upload file to a temporary location. $upload = wp_handle_upload( $_FILES['file'], array( 'test_form' => false ) ); if ( isset( $upload['error'] ) ) { wp_send_json_error( array( 'message' => $upload['error'] ) ); } global $wpdb; $old_file = get_attached_file( $id ); $old_meta = wp_get_attachment_metadata( $id ); $upload_dir = wp_upload_dir(); $old_dir = dirname( $old_file ); // Delete old thumbnails. if ( ! empty( $old_meta['sizes'] ) ) { foreach ( $old_meta['sizes'] as $size_data ) { $thumb_path = trailingslashit( $old_dir ) . $size_data['file']; if ( file_exists( $thumb_path ) ) { wp_delete_file( $thumb_path ); } } } // Delete old original-image backup if exists. if ( ! empty( $old_meta['original_image'] ) ) { $orig_path = trailingslashit( $old_dir ) . $old_meta['original_image']; if ( file_exists( $orig_path ) ) { wp_delete_file( $orig_path ); } } // Delete old main file. if ( file_exists( $old_file ) ) { wp_delete_file( $old_file ); } // Keep same base name, allow extension to change. $old_name = pathinfo( $old_file, PATHINFO_FILENAME ); $old_ext = pathinfo( $old_file, PATHINFO_EXTENSION ); $new_ext = pathinfo( $upload['file'], PATHINFO_EXTENSION ); $new_basename = $old_name . '.' . $new_ext; $new_file = trailingslashit( $old_dir ) . $new_basename; // Ensure target directory exists. wp_mkdir_p( $old_dir ); // Move uploaded file to old location. if ( ! $this->move_file( $upload['file'], $new_file ) ) { wp_send_json_error( array( 'message' => 'Could not move uploaded file.' ) ); } // Update _wp_attached_file. $relative = ltrim( str_replace( wp_normalize_path( $upload_dir['basedir'] ), '', wp_normalize_path( $new_file ) ), '/\\' ); update_post_meta( $id, '_wp_attached_file', $relative ); // Regenerate all thumbnails. $new_meta = wp_generate_attachment_metadata( $id, $new_file ); wp_update_attachment_metadata( $id, $new_meta ); // Update MIME type and GUID. $new_mime = $upload['type']; $new_url = $upload_dir['baseurl'] . '/' . $relative; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->update( $wpdb->posts, array( 'post_mime_type' => $new_mime, 'guid' => $new_url, ), array( 'ID' => $id ) ); // If extension changed and update_refs is on, update references. $update_refs = ! empty( $_POST['update_refs'] ); if ( $update_refs && strtolower( $old_ext ) !== strtolower( $new_ext ) ) { $old_relative = ltrim( str_replace( wp_normalize_path( $upload_dir['basedir'] ), '', wp_normalize_path( $old_file ) ), '/\\' ); $old_url = $upload_dir['baseurl'] . '/' . $old_relative; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $posts_with_ref = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_type NOT IN ('revision','attachment')", '%' . $wpdb->esc_like( $old_url ) . '%' ) ); foreach ( $posts_with_ref as $pid ) { $content = get_post_field( 'post_content', $pid ); $content = str_replace( $old_url, $new_url, $content ); wp_update_post( array( 'ID' => $pid, 'post_content' => $content ) ); } } // Thumbnail for card update. $thumb_src = wp_get_attachment_image_src( $id, 'thumbnail' ); wp_send_json_success( array( 'message' => 'File replaced successfully.', 'filename' => $new_basename, 'url' => $new_url, 'thumbnail_url' => $thumb_src ? $thumb_src[0] : '', 'mime_type' => $new_mime, 'file_size' => filesize( $new_file ), 'file_size_fmt' => size_format( filesize( $new_file ) ), 'width' => $new_meta['width'] ?? 0, 'height' => $new_meta['height'] ?? 0, ) ); } // ========================================================================= // Rename Logic (preserved from v1) // ========================================================================= private function sanitize_seo_filename( $name ) { $name = strtolower( $name ); $name = preg_replace( '/[\s_]+/', '-', $name ); $name = preg_replace( '/[^a-z0-9\-]/', '', $name ); $name = preg_replace( '/-+/', '-', $name ); return trim( $name, '-' ); } private function get_unique_filename( $dir, $basename ) { if ( ! file_exists( trailingslashit( $dir ) . $basename ) ) { return $basename; } $name = pathinfo( $basename, PATHINFO_FILENAME ); $ext = pathinfo( $basename, PATHINFO_EXTENSION ); $i = 2; while ( file_exists( trailingslashit( $dir ) . $name . '-' . $i . '.' . $ext ) ) { $i++; } return $name . '-' . $i . '.' . $ext; } /** * Move/rename a file using WP_Filesystem instead of PHP rename(). * * @param string $from Source path. * @param string $to Destination path. * @return bool True on success, false on failure. */ private function move_file( $from, $to ) { global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } return $wp_filesystem->move( $from, $to, true ); } private function do_rename( $attachment_id, $new_name, $update_refs = false ) { $post = get_post( $attachment_id ); if ( ! $post || 'attachment' !== $post->post_type ) { return new WP_Error( 'invalid', 'Attachment not found.' ); } $lock_key = 'a1tools_rename_lock_' . $attachment_id; if ( get_transient( $lock_key ) ) { return new WP_Error( 'locked', 'File is being renamed. Try again.' ); } set_transient( $lock_key, true, 30 ); $old_filepath = get_attached_file( $attachment_id ); if ( ! $old_filepath || ! file_exists( $old_filepath ) ) { delete_transient( $lock_key ); return new WP_Error( 'missing', 'Original file not found on disk.' ); } $upload_dir = wp_get_upload_dir(); $dir = dirname( $old_filepath ); $old_ext = strtolower( pathinfo( $old_filepath, PATHINFO_EXTENSION ) ); $old_name = pathinfo( $old_filepath, PATHINFO_FILENAME ); $new_name = $this->sanitize_seo_filename( $new_name ); if ( empty( $new_name ) ) { delete_transient( $lock_key ); return new WP_Error( 'empty', 'Filename cannot be empty after sanitization.' ); } if ( $new_name === $old_name ) { delete_transient( $lock_key ); return new WP_Error( 'same', 'New filename is the same as current.' ); } $real_dir = realpath( $dir ); $real_uploads = realpath( $upload_dir['basedir'] ); if ( false === $real_dir || false === $real_uploads || 0 !== strpos( $real_dir, $real_uploads ) ) { delete_transient( $lock_key ); return new WP_Error( 'security', 'File is outside uploads directory.' ); } $new_basename = $this->get_unique_filename( $dir, $new_name . '.' . $old_ext ); $new_name = pathinfo( $new_basename, PATHINFO_FILENAME ); $new_filepath = trailingslashit( $dir ) . $new_basename; $old_relative = get_post_meta( $attachment_id, '_wp_attached_file', true ); $old_url = trailingslashit( $upload_dir['baseurl'] ) . $old_relative; $metadata = wp_get_attachment_metadata( $attachment_id ); $old_sizes = ( is_array( $metadata ) && ! empty( $metadata['sizes'] ) ) ? $metadata['sizes'] : array(); if ( ! $this->move_file( $old_filepath, $new_filepath ) ) { delete_transient( $lock_key ); return new WP_Error( 'disk', 'Could not rename file on disk.' ); } $renamed_sizes = array(); foreach ( $old_sizes as $sk => $sd ) { $otp = trailingslashit( $dir ) . $sd['file']; $ntf = str_replace( $old_name, $new_name, $sd['file'] ); $ntp = trailingslashit( $dir ) . $ntf; if ( file_exists( $otp ) ) { $this->move_file( $otp, $ntp ); } $renamed_sizes[ $sk ] = $ntf; } if ( is_array( $metadata ) && ! empty( $metadata['original_image'] ) ) { $old_orig = $metadata['original_image']; $new_orig = str_replace( $old_name, $new_name, $old_orig ); $oop = trailingslashit( $dir ) . $old_orig; $nop = trailingslashit( $dir ) . $new_orig; if ( file_exists( $oop ) ) { $this->move_file( $oop, $nop ); } $metadata['original_image'] = $new_orig; } $new_relative = str_replace( basename( $old_relative ), $new_basename, $old_relative ); update_post_meta( $attachment_id, '_wp_attached_file', $new_relative ); if ( is_array( $metadata ) ) { $metadata['file'] = $new_relative; foreach ( $renamed_sizes as $sk => $ntf ) { if ( isset( $metadata['sizes'][ $sk ] ) ) { $metadata['sizes'][ $sk ]['file'] = $ntf; } } wp_update_attachment_metadata( $attachment_id, $metadata ); } $new_url = trailingslashit( $upload_dir['baseurl'] ) . $new_relative; global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->update( $wpdb->posts, array( 'guid' => $new_url ), array( 'ID' => $attachment_id ) ); wp_update_post( array( 'ID' => $attachment_id, 'post_title' => ucwords( str_replace( '-', ' ', $new_name ) ), 'post_name' => $new_name, ) ); $refs_updated = 0; if ( $update_refs ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $refs_updated += (int) $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s AND ID != %d", $old_url, $new_url, '%' . $wpdb->esc_like( $old_url ) . '%', $attachment_id ) ); $sub_dir = dirname( $old_relative ); foreach ( $old_sizes as $sk => $sd ) { if ( ! isset( $renamed_sizes[ $sk ] ) ) continue; $osu = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $sd['file']; $nsu = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $renamed_sizes[ $sk ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s", $osu, $nsu, '%' . $wpdb->esc_like( $osu ) . '%' ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_value LIKE %s", $old_url, $new_url, '%' . $wpdb->esc_like( $old_url ) . '%' ) ); foreach ( $old_sizes as $sk => $sd ) { if ( ! isset( $renamed_sizes[ $sk ] ) ) continue; $osu = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $sd['file']; $nsu = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $renamed_sizes[ $sk ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_value LIKE %s", $osu, $nsu, '%' . $wpdb->esc_like( $osu ) . '%' ) ); } } clean_post_cache( $attachment_id ); delete_transient( $lock_key ); $new_thumb_url = $new_url; if ( ! empty( $renamed_sizes['thumbnail'] ) ) { $new_thumb_url = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( dirname( $new_relative ) ) . $renamed_sizes['thumbnail']; } return array( 'attachment_id' => $attachment_id, 'old_filename' => basename( $old_relative ), 'new_filename' => $new_basename, 'new_url' => $new_url, 'new_thumbnail_url' => $new_thumb_url, 'new_title' => ucwords( str_replace( '-', ' ', $new_name ) ), 'refs_updated' => $refs_updated, 'message' => 'File renamed successfully.', ); } // ========================================================================= // Page Rendering // ========================================================================= public function render_page() { ?>
'+esc(it.filename)+'Scanning for duplicates... This may take a moment.
'); $.post(a1mm.ajax, { action:'a1tools_find_duplicates', nonce:a1mm.nonceDuplicates }, function(r) { if (!r.success || !r.data.groups.length) { $('#a1mm-dup-content').html('' + esc(r.data ? r.data.message : 'No duplicates found.') + '
'); return; } var html = 'Found '+r.data.count+' duplicate group'+(r.data.count>1?'s':'')+':
'; $.each(r.data.groups, function(i,g) { html += 'Error scanning for duplicates.
'); }); }); $('.a1mm-modal-close, #a1mm-dup-overlay').on('click', function(e) { if (e.target === this) $('#a1mm-dup-overlay').hide(); }); // ---- Event Handlers ---- $(document).on('click', '.a1mm-page-btn', function() { loadMedia(parseInt($(this).data('page'),10)); $('html,body').animate({scrollTop:0},200); }); $('#a1mm-search-btn').on('click', function() { loadMedia(1); }); $('#a1mm-search').on('keypress', function(e) { if(e.which===13) loadMedia(1); }); $('#a1mm-mime-filter, #a1mm-sort').on('change', function() { loadMedia(1); }); $(document).on('keypress', '.a1mm-new-name', function(e) { if(e.which===13) $(this).closest('.a1mm-item').find('.a1mm-save-meta-btn').click(); }); $(document).on('keypress', '.a1mm-meta-input', function(e) { if(e.which===13 && !$(this).is('textarea')) $(this).closest('.a1mm-item').find('.a1mm-save-meta-btn').click(); }); // ---- Lightbox ---- $(document).on('click', '.a1mm-thumb.a1mm-clickable', function(e) { e.stopPropagation(); var $it = $(this).closest('.a1mm-item'); var url = $it.data('url'); var fn = $it.find('.a1mm-current code').text(); if (url) { $('#a1mm-lightbox-img').attr('src', url); $('#a1mm-lightbox-name').text(fn); $('#a1mm-lightbox').addClass('active'); } }); $('#a1mm-lightbox').on('click', function() { $(this).removeClass('active'); $('#a1mm-lightbox-img').attr('src',''); }); $(document).on('keydown', function(e) { if (e.which===27) { if ($('#a1mm-lightbox').hasClass('active')) { $('#a1mm-lightbox').removeClass('active'); $('#a1mm-lightbox-img').attr('src',''); } $('#a1mm-dup-overlay').hide(); } }); // ---- SEO filter: unused needs pre-scan ---- $('#a1mm-seo-filter').on('change', function() { if ($(this).val() === 'unused') { $('#a1mm-status').text('Scanning for unused media...'); $.post(a1mm.ajax, { action:'a1tools_scan_unused', nonce:a1mm.nonceUnused }, function() { loadMedia(1); }); return; } loadMedia(1); }); // Initial load loadMedia(1); })(jQuery); JSEOF; } }