ensure_permissions(); check_ajax_referer( 'adbear_scan_nonce', 'nonce' ); set_transient( self::RESULTS_TRANSIENT_KEY, array(), DAY_IN_SECONDS ); wp_send_json_success( array( 'message' => __( 'Scan initialized.', 'adbear-image-cleaner' ), ) ); } /** * Process one scan batch. * * @return void */ public function ajax_scan_batch() { $this->ensure_permissions(); check_ajax_referer( 'adbear_scan_nonce', 'nonce' ); $offset = isset( $_POST['offset'] ) ? absint( wp_unslash( $_POST['offset'] ) ) : 0; $limit = isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 25; $limit = max( 1, min( 100, $limit ) ); $query = new WP_Query( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => Adbear_Image_Cleaner_Helpers::get_supported_mimes(), 'posts_per_page' => $limit, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', 'no_found_rows' => false, ) ); $batch_results = array(); foreach ( $query->posts as $attachment_id ) { $batch_results[] = $this->analyze_attachment( (int) $attachment_id ); } $results = get_transient( self::RESULTS_TRANSIENT_KEY ); if ( ! is_array( $results ) ) { $results = array(); } $results = array_merge( $results, $batch_results ); set_transient( self::RESULTS_TRANSIENT_KEY, $results, DAY_IN_SECONDS ); $processed = $offset + count( $query->posts ); $total = (int) $query->found_posts; $complete = $processed >= $total; wp_send_json_success( array( 'processed' => $processed, 'total' => $total, 'complete' => $complete, ) ); } /** * Return current unused row set for live table refresh. * * @return void */ public function ajax_get_unused_results() { $this->ensure_permissions(); check_ajax_referer( 'adbear_scan_nonce', 'nonce' ); $rows = $this->get_unused_results(); $rows = array_map( static function ( $row ) { $row['file_size_human'] = Adbear_Image_Cleaner_Helpers::format_bytes( isset( $row['file_size'] ) ? (int) $row['file_size'] : 0 ); return $row; }, $rows ); wp_send_json_success( array( 'rows' => $rows, ) ); } /** * Return only unused/broken entries. * * @return array> */ public function get_unused_results() { $results = get_transient( self::RESULTS_TRANSIENT_KEY ); if ( ! is_array( $results ) ) { return array(); } return array_values( array_filter( $results, static function ( $item ) { return isset( $item['status'] ) && in_array( $item['status'], Adbear_Image_Cleaner_Helpers::get_deletable_statuses(), true ); } ) ); } /** * Analyze one attachment usage status. * * @param int $attachment_id Attachment ID. * @return array */ private function analyze_attachment( $attachment_id ) { $url = (string) wp_get_attachment_url( $attachment_id ); $file_path = (string) get_attached_file( $attachment_id ); $file_name = wp_basename( $file_path ? $file_path : $url ); $upload_date = get_the_date( 'Y-m-d H:i:s', $attachment_id ); $file_exists = $file_path && file_exists( $file_path ); $file_size = $file_exists ? filesize( $file_path ) : 0; $status = 'unused'; $status_label = __( 'Unused', 'adbear-image-cleaner' ); if ( ! $file_exists ) { $status = 'broken'; $status_label = __( 'Broken file', 'adbear-image-cleaner' ); } elseif ( $this->is_used_in_content( $attachment_id, $url, $file_name ) || $this->is_featured_image( $attachment_id ) || $this->is_used_in_woocommerce( $attachment_id ) || $this->is_used_in_acf_meta( $attachment_id, $url ) || $this->is_used_in_postmeta( $attachment_id, $url, $file_name ) || $this->is_used_in_options( $attachment_id, $url, $file_name ) || $this->is_used_in_theme_mods( $attachment_id, $url, $file_name ) || $this->is_site_identity_image( $attachment_id ) ) { $status = 'in_use'; $status_label = __( 'In use', 'adbear-image-cleaner' ); } return array( 'id' => $attachment_id, 'file_name' => $file_name, 'file_path' => $file_path, 'file_size' => (int) $file_size, 'upload_date' => $upload_date ? $upload_date : '', 'status' => $status, 'status_text' => $status_label, ); } /** * Check if used in post content. * * @param int $attachment_id Attachment ID. * @param string $url Attachment URL. * @param string $file_name File basename. * @return bool */ private function is_used_in_content( $attachment_id, $url, $file_name ) { $identifiers = $this->get_attachment_asset_identifiers( $attachment_id, $url, $file_name ); $identifiers[] = 'wp-image-' . $attachment_id; $identifiers[] = '[gallery ids="' . $attachment_id; $identifiers = array_values( array_unique( array_filter( $identifiers ) ) ); foreach ( $identifiers as $identifier ) { $results = get_posts( array( 'post_type' => 'any', 'post_status' => 'any', 's' => $identifier, 'posts_per_page' => 1, 'fields' => 'ids', 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( ! empty( $results ) ) { return true; } } return false; } /** * Get possible image references that may appear in content. * * @param int $attachment_id Attachment ID. * @param string $url Attachment URL. * @param string $file_name Original file name. * @return string[] */ private function get_attachment_asset_identifiers( $attachment_id, $url, $file_name ) { $identifiers = array(); if ( ! empty( $url ) ) { $identifiers[] = $url; $path = wp_parse_url( $url, PHP_URL_PATH ); if ( is_string( $path ) && '' !== $path ) { $identifiers[] = $path; } } if ( ! empty( $file_name ) ) { $identifiers[] = $file_name; } $metadata = wp_get_attachment_metadata( $attachment_id ); if ( is_array( $metadata ) ) { if ( isset( $metadata['file'] ) && is_string( $metadata['file'] ) ) { $identifiers[] = wp_basename( $metadata['file'] ); } if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) { foreach ( $metadata['sizes'] as $size_data ) { if ( isset( $size_data['file'] ) && is_string( $size_data['file'] ) ) { $identifiers[] = $size_data['file']; } } } } return array_values( array_unique( array_filter( $identifiers ) ) ); } /** * Check featured image relation. * * @param int $attachment_id Attachment ID. * @return bool */ private function is_featured_image( $attachment_id ) { $results = get_posts( array( 'post_type' => 'any', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_key' => '_thumbnail_id', 'meta_value' => (string) $attachment_id, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); return ! empty( $results ); } /** * Check WooCommerce product image usage. * * @param int $attachment_id Attachment ID. * @return bool */ private function is_used_in_woocommerce( $attachment_id ) { $thumb_posts = get_posts( array( 'post_type' => 'product', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_key' => '_thumbnail_id', 'meta_value' => (string) $attachment_id, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( ! empty( $thumb_posts ) ) { return true; } $gallery_posts = get_posts( array( 'post_type' => 'product', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_query' => array( array( 'key' => '_product_image_gallery', 'value' => '"' . $attachment_id . '"', 'compare' => 'LIKE', ), ), 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( ! empty( $gallery_posts ) ) { return true; } $gallery_posts = get_posts( array( 'post_type' => 'product', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_query' => array( array( 'key' => '_product_image_gallery', 'value' => (string) $attachment_id, 'compare' => 'LIKE', ), ), 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); return ! empty( $gallery_posts ); } /** * Check attachment usage in ACF-like image meta values. * * @param int $attachment_id Attachment ID. * @param string $url Attachment URL. * @return bool */ private function is_used_in_acf_meta( $attachment_id, $url ) { $id_string = (string) $attachment_id; $checks = array( $id_string ); $meta_keys = array( 'image', 'images', 'gallery', 'thumbnail', 'hero_image', 'banner_image', 'logo', 'icon', 'media', ); if ( ! empty( $url ) ) { $checks[] = $url; } foreach ( $meta_keys as $meta_key ) { foreach ( $checks as $check_value ) { $posts = get_posts( array( 'post_type' => 'any', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_query' => array( array( 'key' => $meta_key, 'value' => $check_value, 'compare' => 'LIKE', ), ), 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( ! empty( $posts ) ) { return true; } } } return false; } /** * Check usage in postmeta values (Elementor/builders/custom meta). * * @param int $attachment_id Attachment ID. * @param string $url Attachment URL. * @param string $file_name Original file name. * @return bool */ private function is_used_in_postmeta( $attachment_id, $url, $file_name ) { $asset_identifiers = $this->get_attachment_asset_identifiers( $attachment_id, $url, $file_name ); $asset_identifiers[] = (string) $attachment_id; $asset_identifiers = array_values( array_unique( array_filter( $asset_identifiers ) ) ); $meta_keys = array( '_thumbnail_id', '_product_image_gallery', 'logo', 'image', 'images', 'gallery', 'hero_image', 'banner_image', 'icon', 'media', '_elementor_data', ); foreach ( $meta_keys as $meta_key ) { foreach ( $asset_identifiers as $identifier ) { $posts = get_posts( array( 'post_type' => 'any', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_query' => array( array( 'key' => $meta_key, 'value' => $identifier, 'compare' => 'LIKE', ), ), 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( ! empty( $posts ) ) { return true; } } } return false; } /** * Check usage in wp_options values. * * @param int $attachment_id Attachment ID. * @param string $url Attachment URL. * @param string $file_name Original file name. * @return bool */ private function is_used_in_options( $attachment_id, $url, $file_name ) { $asset_identifiers = $this->get_attachment_asset_identifiers( $attachment_id, $url, $file_name ); $asset_identifiers[] = (string) $attachment_id; $asset_identifiers = array_values( array_unique( array_filter( $asset_identifiers ) ) ); $option_names = array( 'theme_mods_' . get_option( 'stylesheet' ), 'theme_mods_' . get_option( 'template' ), 'custom_logo', 'site_icon', 'elementor_active_kit', ); foreach ( $option_names as $option_name ) { $option_value = get_option( $option_name ); if ( $this->array_contains_attachment_reference( $option_value, $asset_identifiers, $attachment_id ) ) { return true; } } return false; } /** * Check known site identity image relations. * * @param int $attachment_id Attachment ID. * @return bool */ private function is_site_identity_image( $attachment_id ) { $custom_logo_id = (int) get_theme_mod( 'custom_logo' ); $site_icon_id = (int) get_option( 'site_icon' ); return ( $custom_logo_id > 0 && $custom_logo_id === $attachment_id ) || ( $site_icon_id > 0 && $site_icon_id === $attachment_id ); } /** * Check usage in Customizer theme mods. * * @param int $attachment_id Attachment ID. * @param string $url Attachment URL. * @param string $file_name Original file name. * @return bool */ private function is_used_in_theme_mods( $attachment_id, $url, $file_name ) { $theme_mods = get_theme_mods(); if ( ! is_array( $theme_mods ) || empty( $theme_mods ) ) { return false; } $identifiers = $this->get_attachment_asset_identifiers( $attachment_id, $url, $file_name ); $identifiers = array_values( array_unique( array_filter( $identifiers ) ) ); return $this->array_contains_attachment_reference( $theme_mods, $identifiers, $attachment_id ); } /** * Recursively inspect array values for attachment references. * * @param mixed $value Data tree. * @param string[] $identifiers Known URL/path/file/id string identifiers. * @param int $attachment_id Attachment ID. * @return bool */ private function array_contains_attachment_reference( $value, array $identifiers, $attachment_id ) { if ( is_array( $value ) ) { foreach ( $value as $nested ) { if ( $this->array_contains_attachment_reference( $nested, $identifiers, $attachment_id ) ) { return true; } } return false; } if ( is_numeric( $value ) && (int) $value === $attachment_id ) { return true; } if ( is_string( $value ) && '' !== $value ) { foreach ( $identifiers as $identifier ) { if ( '' !== $identifier && false !== strpos( $value, $identifier ) ) { return true; } } } return false; } /** * Ensure user permissions. * * @return void */ private function ensure_permissions() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to perform this action.', 'adbear-image-cleaner' ), ), 403 ); } } }