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', 'a1toolsMM', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonceLoad' => wp_create_nonce( 'a1tools_load_media' ), 'nonceRename' => wp_create_nonce( 'a1tools_rename_media' ), ) ); wp_add_inline_script( 'a1tools-media-mgmt', $this->get_admin_js() ); } // ========================================================================= // AJAX: Load Media Items // ========================================================================= /** * AJAX handler — return paginated media items. */ 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( $_POST['search'] ?? '' ); $mime = sanitize_text_field( $_POST['mime_filter'] ?? '' ); $orderby = sanitize_text_field( $_POST['orderby'] ?? 'date' ); $order = strtoupper( sanitize_text_field( $_POST['order'] ?? 'DESC' ) ); if ( ! in_array( $orderby, array( 'date', 'title', 'name' ), 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; } $query = new WP_Query( $args ); $items = array(); foreach ( $query->posts as $post ) { $filepath = get_attached_file( $post->ID ); $filename = $filepath ? basename( $filepath ) : ''; $meta = wp_get_attachment_metadata( $post->ID ); $thumb = wp_get_attachment_image_src( $post->ID, 'thumbnail' ); $filesize = ( $filepath && file_exists( $filepath ) ) ? filesize( $filepath ) : 0; $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' => wp_attachment_is_image( $post->ID ), 'mime_type' => $post->post_mime_type, 'file_size' => $filesize, 'file_size_formatted' => 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 ), ); } wp_send_json_success( array( 'items' => $items, 'total' => (int) $query->found_posts, 'pages' => (int) $query->max_num_pages, 'current_page' => $page, ) ); } // ========================================================================= // AJAX: Rename Media // ========================================================================= /** * AJAX handler — rename a single media attachment. */ 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( $_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 ); } // ========================================================================= // Rename Logic // ========================================================================= /** * Sanitize a filename for SEO: lowercase, alphanumeric + hyphens. * * @param string $name Raw name (no extension). * @return string Sanitized name. */ 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 ); $name = trim( $name, '-' ); return $name; } /** * Get a unique filename in a directory. * * @param string $dir Directory path. * @param string $basename Desired filename with extension. * @return string Unique filename. */ 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; } /** * Perform the full rename operation for one attachment. * * @param int $attachment_id The attachment ID. * @param string $new_name Desired new filename (no extension). * @param bool $update_refs Whether to update post_content references. * @return array|WP_Error Result array or error. */ 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.' ); } // Transient lock to prevent concurrent renames. $lock_key = 'a1tools_rename_lock_' . $attachment_id; if ( get_transient( $lock_key ) ) { return new WP_Error( 'locked', 'This file is currently being renamed. Try again in a moment.' ); } 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 ); // Sanitize new name. $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 the current one.' ); } // Security: ensure we stay within the uploads directory. $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 the uploads directory.' ); } // Compute unique filename. $new_basename = $this->get_unique_filename( $dir, $new_name . '.' . $old_ext ); $new_name = pathinfo( $new_basename, PATHINFO_FILENAME ); $new_filepath = trailingslashit( $dir ) . $new_basename; // Save old relative path and metadata BEFORE any changes. $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(); // STEP 1: Rename main file. if ( ! @rename( $old_filepath, $new_filepath ) ) { delete_transient( $lock_key ); return new WP_Error( 'disk', 'Could not rename file on disk. Check directory permissions.' ); } // STEP 2: Rename thumbnail files. $renamed_sizes = array(); foreach ( $old_sizes as $size_key => $size_data ) { $old_thumb_path = trailingslashit( $dir ) . $size_data['file']; $new_thumb_file = str_replace( $old_name, $new_name, $size_data['file'] ); $new_thumb_path = trailingslashit( $dir ) . $new_thumb_file; if ( file_exists( $old_thumb_path ) ) { @rename( $old_thumb_path, $new_thumb_path ); } $renamed_sizes[ $size_key ] = $new_thumb_file; } // STEP 3: Handle WP 5.3+ original_image. if ( is_array( $metadata ) && ! empty( $metadata['original_image'] ) ) { $old_orig = $metadata['original_image']; $new_orig = str_replace( $old_name, $new_name, $old_orig ); $old_orig_path = trailingslashit( $dir ) . $old_orig; $new_orig_path = trailingslashit( $dir ) . $new_orig; if ( file_exists( $old_orig_path ) ) { @rename( $old_orig_path, $new_orig_path ); } $metadata['original_image'] = $new_orig; } // STEP 4: Update _wp_attached_file. $new_relative = str_replace( basename( $old_relative ), $new_basename, $old_relative ); update_post_meta( $attachment_id, '_wp_attached_file', $new_relative ); // STEP 5: Update _wp_attachment_metadata. if ( is_array( $metadata ) ) { $metadata['file'] = $new_relative; foreach ( $renamed_sizes as $size_key => $new_thumb_file ) { if ( isset( $metadata['sizes'][ $size_key ] ) ) { $metadata['sizes'][ $size_key ]['file'] = $new_thumb_file; } } wp_update_attachment_metadata( $attachment_id, $metadata ); } // STEP 6: Update guid. $new_url = trailingslashit( $upload_dir['baseurl'] ) . $new_relative; global $wpdb; $wpdb->update( $wpdb->posts, array( 'guid' => $new_url ), array( 'ID' => $attachment_id ) ); // STEP 7: Update post_title and post_name. wp_update_post( array( 'ID' => $attachment_id, 'post_title' => ucwords( str_replace( '-', ' ', $new_name ) ), 'post_name' => $new_name, ) ); // STEP 8: Optionally update references in posts and postmeta. $refs_updated = 0; if ( $update_refs ) { // Replace main file URL. $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 ) ); // Replace each thumbnail URL. $sub_dir = dirname( $old_relative ); foreach ( $old_sizes as $size_key => $size_data ) { if ( ! isset( $renamed_sizes[ $size_key ] ) ) { continue; } $old_size_url = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $size_data['file']; $new_size_url = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $renamed_sizes[ $size_key ]; $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s", $old_size_url, $new_size_url, '%' . $wpdb->esc_like( $old_size_url ) . '%' ) ); } // Update postmeta (Elementor data, page builder JSON, etc.). $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 $size_key => $size_data ) { if ( ! isset( $renamed_sizes[ $size_key ] ) ) { continue; } $old_size_url = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $size_data['file']; $new_size_url = trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $sub_dir ) . $renamed_sizes[ $size_key ]; $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_value LIKE %s", $old_size_url, $new_size_url, '%' . $wpdb->esc_like( $old_size_url ) . '%' ) ); } } // Clean caches. clean_post_cache( $attachment_id ); delete_transient( $lock_key ); // Build new thumbnail URL for response. $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 // ========================================================================= /** * Render the Media Management admin page. */ public function render_page() { ?>
' + escHtml(item.filename) + '