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() { ?>

×
/g,'>'); } // ---- Load Media ---- function loadMedia(page) { currentPage = page || 1; selected = {}; updateBulk(); $('#a1mm-select-all').prop('checked', false); $('#a1mm-list').empty(); $('#a1mm-pagination').empty(); $('#a1mm-empty').hide(); $('#a1mm-loading').show(); var sv = $('#a1mm-sort').val().split('-'); $.post(a1mm.ajax, { action: 'a1tools_load_media', nonce: a1mm.nonceLoad, page: currentPage, per_page: perPage, search: $('#a1mm-search').val(), mime_filter: $('#a1mm-mime-filter').val(), seo_filter: $('#a1mm-seo-filter').val(), orderby: sv[0], order: sv[1] }, function(r) { $('#a1mm-loading').hide(); if (!r || !r.success) { $('#a1mm-empty').show().find('p').text((r&&r.data&&r.data.message)||'Failed to load.'); $('#a1mm-status').text(''); return; } if (!r.data.items.length) { $('#a1mm-empty').show(); $('#a1mm-status').text('0 items'); return; } var d=r.data, f=(d.current_page-1)*perPage+1, t=Math.min(d.current_page*perPage,d.total); $('#a1mm-status').text('Showing '+f+'-'+t+' of '+d.total+' items'); $('#a1mm-bulk-bar').show(); $.each(d.items, function(i,item) { $('#a1mm-list').append(buildItem(item)); }); buildPag(d.current_page, d.pages); }).fail(function() { $('#a1mm-loading').hide(); $('#a1mm-empty').show().find('p').text('Network error.'); }); } // ---- Build Item Card ---- function buildItem(it) { var noExt = it.filename.replace(/\.[^.]+$/,''); var ext = it.filename.split('.').pop(); var tc = 'a1mm-thumb' + (it.is_image ? ' a1mm-clickable' : ''); var th = it.is_image ? '' : ''; // SEO badge var seoClass = it.seo_score >= 4 ? 'a1mm-badge-seo-good' : (it.seo_score >= 2 ? 'a1mm-badge-seo-warn' : 'a1mm-badge-seo-bad'); var badges = 'SEO '+it.seo_score+'/4'; if (it.is_random_name) badges += 'Needs rename'; if (it.is_image && !it.alt_text) badges += 'No alt text'; if (it.file_size > 512000) badges += ''+esc(it.file_size_fmt)+''; if (it.usage_count > 0) badges += 'Used in '+it.usage_count+' post'+(it.usage_count>1?'s':'')+''; else badges += 'Unused'; // Meta line var mp = [esc(it.mime_type), esc(it.file_size_fmt)]; if (it.width && it.height) mp.push(it.width+'×'+it.height); mp.push(esc(it.date)); var h = '
' + '' + '
'+th+'
' + '
' + '
'+esc(it.filename)+'
' + '
'+mp.join('  |  ')+'
' + '
'+badges+'
' + '
' + 'File Name:' + '
.'+esc(ext)+'
' + '
' + '
' + 'Alt Text:' + '
' + '
' + '
' + 'Title:' + '
' + '
' + '
' + 'Caption:' + '
' + '
' + '
' + 'Description:' + '
' + '
' + '
' + '' + '
' + '' + '' + '' + '' + '' + '' + '
' + '
'; h += '
'; return h; } // ---- Pagination ---- function buildPag(cur, tot) { if (tot <= 1) return; var p = $('#a1mm-pagination'); if (cur > 1) p.append(''); p.append('Page '+cur+' of '+tot+''); if (cur < tot) p.append(''); } // ---- Selection ---- function updateBulk() { var c = Object.keys(selected).length; $('#a1mm-selected-count').text(c + ' selected'); } $(document).on('change', '.a1mm-item-cb', function() { var id = $(this).val(); if ($(this).is(':checked')) selected[id] = true; else delete selected[id]; updateBulk(); }); $('#a1mm-select-all').on('change', function() { var checked = $(this).is(':checked'); $('.a1mm-item-cb').each(function() { $(this).prop('checked', checked); var id = $(this).val(); if (checked) selected[id] = true; else delete selected[id]; }); updateBulk(); }); // ---- Save (rename if changed + meta) ---- $(document).on('click', '.a1mm-save-meta-btn', function() { var $it = $(this).closest('.a1mm-item'); var id = $it.data('id'); var $b = $(this); var $m = $it.find('.a1mm-item-msg'); var newName = $it.find('.a1mm-new-name').val().trim(); var curFile = $it.find('.a1mm-current code').text(); var curName = curFile.replace(/\.[^.]+$/,''); var ur = $it.find('.a1mm-refs-cb').is(':checked') ? 1 : 0; $b.prop('disabled',true).text('Saving...'); $m.text('').attr('class','a1mm-item-msg'); $it.find('.a1mm-undo-link').remove(); function saveMeta(msg) { $.post(a1mm.ajax, { action:'a1tools_update_media_meta', nonce:a1mm.nonceMeta, attachment_id: id, alt_text: $it.find('.a1mm-alt-input').val(), title: $it.find('.a1mm-title-input').val(), caption: $it.find('.a1mm-caption-input').val(), description: $it.find('.a1mm-desc-input').val() }, function(r) { $b.prop('disabled',false).text('Save'); if (r.success) { var txt = msg || 'Saved!'; $m.text(txt).attr('class','a1mm-item-msg success'); if ($it.find('.a1mm-alt-input').val()) $it.find('.a1mm-badge-no-alt').remove(); } else { $m.text(r.data.message||'Meta save failed.').attr('class','a1mm-item-msg error'); } }).fail(function() { $b.prop('disabled',false).text('Save'); $m.text('Network error.').attr('class','a1mm-item-msg error'); }); } // Check if filename changed — rename first, then save meta. if (newName && newName !== curName) { $.post(a1mm.ajax, { action:'a1tools_rename_media', nonce:a1mm.nonceRename, attachment_id:id, new_filename:newName, update_refs:ur }, function(r) { if (r.success) { var d = r.data; $it.data('old-name', d.old_filename); $it.find('.a1mm-current code').text(d.new_filename); $it.find('.a1mm-new-name').val(d.new_filename.replace(/\.[^.]+$/,'')); if (d.new_thumbnail_url) $it.find('.a1mm-thumb img').attr('src', d.new_thumbnail_url); $it.data('url', d.new_url); $it.addClass('a1mm-renamed'); $it.find('.a1mm-badge-random').remove(); var mt = 'Renamed & saved!'; if (ur && d.refs_updated > 0) mt = 'Renamed (' + d.refs_updated + ' refs) & saved!'; saveMeta(mt); // Show undo after msg setTimeout(function(){ $m.after('Undo rename'); }, 100); } else { $b.prop('disabled',false).text('Save'); $m.text(r.data.message||'Rename failed.').attr('class','a1mm-item-msg error'); } }).fail(function() { $b.prop('disabled',false).text('Save'); $m.text('Network error.').attr('class','a1mm-item-msg error'); }); } else { saveMeta(); } }); // ---- Undo Rename ---- $(document).on('click', '.a1mm-undo-link', function() { var $it = $(this).closest('.a1mm-item'); var oldName = $it.data('old-name'); if (!oldName) return; var noExt = oldName.replace(/\.[^.]+$/,''); var id = $it.data('id'); var ur = $it.find('.a1mm-refs-cb').is(':checked') ? 1 : 0; var $m = $it.find('.a1mm-item-msg'); $(this).remove(); $m.text('Undoing...').attr('class','a1mm-item-msg'); $.post(a1mm.ajax, { action:'a1tools_rename_media', nonce:a1mm.nonceRename, attachment_id:id, new_filename:noExt, update_refs:ur }, function(r) { if (r.success) { $it.find('.a1mm-current code').text(r.data.new_filename); $it.find('.a1mm-new-name').val(r.data.new_filename.replace(/\.[^.]+$/,'')); if (r.data.new_thumbnail_url) $it.find('.a1mm-thumb img').attr('src', r.data.new_thumbnail_url); $it.data('url', r.data.new_url); $it.data('old-name',''); $it.removeClass('a1mm-renamed'); $m.text('Rename undone.').attr('class','a1mm-item-msg success'); } else { $m.text(r.data.message||'Undo failed.').attr('class','a1mm-item-msg error'); } }); }); // ---- Delete ---- $(document).on('click', '.a1mm-delete-btn', function() { var $it = $(this).closest('.a1mm-item'); var id = $it.data('id'); var fn = $it.find('.a1mm-current code').text(); if (!confirm('Permanently delete "' + fn + '"? This cannot be undone.')) return; var $b = $(this); $b.prop('disabled',true).text('Deleting...'); $.post(a1mm.ajax, { action:'a1tools_delete_media', nonce:a1mm.nonceDelete, attachment_id: id }, function(r) { if (r.success) { $it.slideUp(300, function() { $(this).remove(); }); } else { $b.prop('disabled',false).text('Delete'); alert(r.data.message||'Delete failed.'); } }).fail(function() { $b.prop('disabled',false).text('Delete'); alert('Network error.'); }); }); // ---- Replace File ---- $(document).on('click', '.a1mm-replace-btn', function() { $(this).closest('.a1mm-item').find('.a1mm-replace-file').click(); }); $(document).on('change', '.a1mm-replace-file', function() { var file = this.files[0]; if (!file) return; var $it = $(this).closest('.a1mm-item'); var id = $it.data('id'); var fn = $it.find('.a1mm-current code').text(); var $btn = $it.find('.a1mm-replace-btn'); var $msg = $it.find('.a1mm-item-msg'); var ur = $it.find('.a1mm-refs-cb').is(':checked') ? 1 : 0; if (!confirm('Replace "' + fn + '" with "' + file.name + '"?\nThe old file will be permanently replaced.')) { this.value = ''; return; } $btn.prop('disabled',true).text('Uploading...'); $msg.text('').attr('class','a1mm-item-msg'); var fd = new FormData(); fd.append('action', 'a1tools_replace_media'); fd.append('nonce', a1mm.nonceReplace); fd.append('attachment_id', id); fd.append('file', file); fd.append('update_refs', ur); $.ajax({ url: a1mm.ajax, type: 'POST', data: fd, processData: false, contentType: false, success: function(r) { if (r.success) { var d = r.data; if (d.thumbnail_url) $it.find('.a1mm-thumb img').attr('src', d.thumbnail_url + '?t=' + Date.now()); $it.find('.a1mm-current code').text(d.filename); $it.find('.a1mm-new-name').val(d.filename.replace(/\.[^.]+$/,'')); $it.find('.a1mm-ext').text('.' + d.filename.split('.').pop()); $it.data('url', d.url); // Update meta line var mp = [esc(d.mime_type), esc(d.file_size_fmt)]; if (d.width && d.height) mp.push(d.width+'×'+d.height); $it.find('.a1mm-meta').html(mp.join('  |  ')); // Update size badges $it.find('.a1mm-badge-large').remove(); if (d.file_size > 512000) { $it.find('.a1mm-badges').append(''+esc(d.file_size_fmt)+''); } $msg.text('File replaced!').attr('class','a1mm-item-msg success'); } else { $msg.text(r.data.message||'Replace failed.').attr('class','a1mm-item-msg error'); } $btn.prop('disabled',false).text('Replace'); }, error: function() { $btn.prop('disabled',false).text('Replace'); $msg.text('Upload error.').attr('class','a1mm-item-msg error'); } }); this.value = ''; }); // ---- Bulk Rename ---- $('#a1mm-bulk-rename-btn').on('click', function() { var ids = Object.keys(selected); var pat = $('#a1mm-bulk-pattern').val().trim(); if (!ids.length) { alert('Select items first.'); return; } if (!pat) { alert('Enter a pattern. Use {n} for auto-number.'); return; } if (!confirm('Rename '+ids.length+' files using pattern "'+pat+'"?')) return; var $b = $(this); $b.prop('disabled',true).text('Renaming...'); $.post(a1mm.ajax, { action:'a1tools_bulk_rename', nonce:a1mm.nonceBulkRename, ids:ids, pattern:pat, start_num:1, update_refs:1 }, function(r) { $b.prop('disabled',false).text('Bulk Rename'); if (r.success) { alert(r.data.message); loadMedia(currentPage); } else { alert(r.data.message||'Bulk rename failed.'); } }).fail(function() { $b.prop('disabled',false).text('Bulk Rename'); alert('Network error.'); }); }); // ---- Bulk Alt Text ---- $('#a1mm-bulk-alt-btn').on('click', function() { var ids = Object.keys(selected); var pat = $('#a1mm-bulk-alt').val().trim(); if (!ids.length) { alert('Select items first.'); return; } if (!pat) { alert('Enter alt text. Use {filename} to use each file\'s name.'); return; } if (!confirm('Set alt text for '+ids.length+' files?')) return; var $b = $(this); $b.prop('disabled',true).text('Setting...'); $.post(a1mm.ajax, { action:'a1tools_bulk_update_alt', nonce:a1mm.nonceBulkAlt, ids:ids, alt_pattern:pat }, function(r) { $b.prop('disabled',false).text('Set Alt'); if (r.success) { alert(r.data.message); loadMedia(currentPage); } else { alert(r.data.message||'Failed.'); } }).fail(function() { $b.prop('disabled',false).text('Set Alt'); alert('Network error.'); }); }); // ---- Find Duplicates ---- $('#a1mm-duplicates-btn').on('click', function() { $('#a1mm-dup-overlay').show(); $('#a1mm-dup-content').html('

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 += '

Group '+(i+1)+' — '+esc(g.files[0].size_fmt)+' each ('+g.files.length+' files)

'; $.each(g.files, function(j,f) { html += '
'; if (f.thumb) html += '
'; html += '
'+esc(f.filename)+'
ID: '+f.id+'
'; html += '
'; }); html += '
'; }); $('#a1mm-dup-content').html(html); }).fail(function() { $('#a1mm-dup-content').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; } }