Plugin Directory

Changeset 3345787


Ignore:
Timestamp:
08/17/2025 06:22:32 AM (6 months ago)
Author:
thebitcraft
Message:

502 network error issue fixed and unused media fixed

Location:
media-tracker
Files:
45 added
4 edited

Legend:

Unmodified
Added
Removed
  • media-tracker/trunk/includes/Admin/Unused_Media_List.php

    r3244839 r3345787  
    8484
    8585                /* translators: %s: post title */
    86                 $aria_label = sprintf( __( '“%s” (Edit)', 'media-tracker' ), $item->post_title );
     86                $aria_label = sprintf( __( '"%s" (Edit)', 'media-tracker' ), $item->post_title );
    8787
    8888                $output = '<strong class="has-media-icon">
     
    150150    }
    151151
     152    /**
     153     * Get all used media IDs with optimized queries to prevent timeouts
     154     *
     155     * @return array Array of used media IDs
     156     */
     157    private function get_used_media_ids() {
     158        global $wpdb;
     159
     160        $used_image_ids = [];
     161
     162        // Increase memory limit and execution time
     163        if ( function_exists( 'ini_set' ) ) {
     164            ini_set( 'memory_limit', '512M' );
     165            set_time_limit( 300 );
     166        }
     167
     168        // 1. Get featured images (post thumbnails) - Most common usage
     169        $featured_images = $wpdb->get_col("
     170            SELECT DISTINCT meta_value
     171            FROM {$wpdb->postmeta}
     172            WHERE meta_key = '_thumbnail_id'
     173            AND meta_value != '0'
     174            AND meta_value != ''
     175        ");
     176        if ( $featured_images ) {
     177            $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $featured_images ) );
     178        }
     179
     180        // 2. Get WooCommerce product gallery images
     181        if ( class_exists( 'WooCommerce' ) ) {
     182            $gallery_images = $wpdb->get_col("
     183                SELECT DISTINCT meta_value
     184                FROM {$wpdb->postmeta}
     185                WHERE meta_key = '_product_image_gallery'
     186                AND meta_value != ''
     187                AND meta_value IS NOT NULL
     188            ");
     189
     190            foreach ( $gallery_images as $gallery_string ) {
     191                if ( ! empty( $gallery_string ) ) {
     192                    $gallery_ids = explode( ',', $gallery_string );
     193                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', array_filter( $gallery_ids ) ) );
     194                }
     195            }
     196
     197            // Get WooCommerce variation images
     198            $variation_images = $wpdb->get_col("
     199                SELECT DISTINCT meta_value
     200                FROM {$wpdb->postmeta}
     201                WHERE meta_key = '_thumbnail_id'
     202                AND post_id IN (
     203                    SELECT ID FROM {$wpdb->posts}
     204                    WHERE post_type = 'product_variation'
     205                )
     206                AND meta_value != '0'
     207                AND meta_value != ''
     208            ");
     209            if ( $variation_images ) {
     210                $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $variation_images ) );
     211            }
     212        }
     213
     214        // 3. Get images used in post content (process in batches to avoid memory issues)
     215        $batch_size = 100;
     216        $offset = 0;
     217
     218        while ( true ) {
     219            $posts_with_content = $wpdb->get_results( $wpdb->prepare("
     220                SELECT ID, post_content
     221                FROM {$wpdb->posts}
     222                WHERE post_content LIKE %s
     223                AND post_status = 'publish'
     224                LIMIT %d OFFSET %d
     225            ", '%wp-image-%', $batch_size, $offset ) );
     226
     227            if ( empty( $posts_with_content ) ) {
     228                break;
     229            }
     230
     231            foreach ( $posts_with_content as $post ) {
     232                preg_match_all( '/wp-image-(\d+)/', $post->post_content, $matches );
     233                if ( ! empty( $matches[1] ) ) {
     234                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
     235                }
     236            }
     237
     238            $offset += $batch_size;
     239        }
     240
     241        // 4. Get Elementor images (process in smaller batches)
     242        $elementor_posts = $wpdb->get_col("
     243            SELECT post_id
     244            FROM {$wpdb->postmeta}
     245            WHERE meta_key = '_elementor_data'
     246            LIMIT 50
     247        ");
     248
     249        foreach ( $elementor_posts as $post_id ) {
     250            $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
     251
     252            if ( $elementor_data ) {
     253                $elementor_data = json_decode( $elementor_data, true );
     254
     255                if ( is_array( $elementor_data ) ) {
     256                    array_walk_recursive( $elementor_data, function( $item, $key ) use ( &$used_image_ids ) {
     257                        if ( $key === 'id' && is_numeric( $item ) ) {
     258                            $used_image_ids[] = intval( $item );
     259                        }
     260                    } );
     261                }
     262            }
     263        }
     264
     265        // 5. Get Site Icon ID
     266        $site_icon_id = get_option( 'site_icon' );
     267        if ( $site_icon_id ) {
     268            $used_image_ids[] = intval( $site_icon_id );
     269        }
     270
     271        // 6. Get custom logo
     272        $custom_logo_id = get_theme_mod( 'custom_logo' );
     273        if ( $custom_logo_id ) {
     274            $used_image_ids[] = intval( $custom_logo_id );
     275        }
     276
     277        // 7. Get header image
     278        $header_image_id = get_theme_mod( 'header_image_data' );
     279        if ( is_array( $header_image_id ) && isset( $header_image_id['attachment_id'] ) ) {
     280            $used_image_ids[] = intval( $header_image_id['attachment_id'] );
     281        }
     282
     283        // 8. Get background image
     284        $background_image_id = get_theme_mod( 'background_image_thumb' );
     285        if ( $background_image_id ) {
     286            $used_image_ids[] = intval( $background_image_id );
     287        }
     288
     289        // Remove duplicates and invalid IDs
     290        $used_image_ids = array_unique( array_filter( array_map( 'intval', $used_image_ids ) ) );
     291
     292        return $used_image_ids;
     293    }
     294
    152295    public function prepare_items() {
    153296        global $wpdb;
     
    164307        $offset       = ( $current_page - 1 ) * $per_page;
    165308
    166         // Check if the results are cached.
    167         $cache_key = 'unused_media_list_' . md5( serialize( array( $search, $this->author_id, $current_page, $per_page ) ) );
     309        // Check for force refresh parameter
     310        $force_refresh = isset( $_GET['refresh_cache'] ) && $_GET['refresh_cache'] === '1';
     311
     312        // Check if the results are cached
     313        $cache_key = 'unused_media_list_v2_' . md5( serialize( array( $search, $this->author_id, $current_page, $per_page ) ) );
     314
     315        // Clear cache if forced or if there's recent activity
     316        if ( $force_refresh || $this->should_invalidate_cache() ) {
     317            delete_transient( $cache_key );
     318        }
     319
    168320        $cached_results = get_transient( $cache_key );
    169321
    170         if ( false === $cached_results ) {
    171             // Get all post IDs that use Elementor.
    172             $elementor_posts = $wpdb->get_col("
    173                 SELECT post_id
    174                 FROM {$wpdb->postmeta}
    175                 WHERE meta_key = '_elementor_data'
    176             ");
    177 
    178             // Collect image IDs used by Elementor.
    179             $used_image_ids = [];
    180 
    181             foreach ( $elementor_posts as $post_id ) {
    182                 $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
    183 
    184                 if ( $elementor_data ) {
    185                     $elementor_data = json_decode( $elementor_data, true );
    186 
    187                     if ( is_array( $elementor_data ) ) {
    188                         array_walk_recursive( $elementor_data, function( $item, $key ) use ( &$used_image_ids ) {
    189                             if ( $key === 'id' && is_numeric( $item ) ) {
    190                                 $used_image_ids[] = $item;
    191                             }
    192                         } );
    193                     }
    194                 }
    195             }
    196 
    197             // Get Site Icon ID
    198             $site_icon_id = get_option( 'site_icon' );
    199             if ($site_icon_id) {
    200                 $used_image_ids[] = $site_icon_id;
    201             }
    202 
    203             // Ensure unique IDs.
    204             $used_image_ids = array_unique( $used_image_ids );
    205 
    206             // Build the base query to retrieve unused attachments.
     322        if ( false === $cached_results || $force_refresh ) {
     323            // Get used media IDs with optimized method
     324            $used_image_ids = $this->get_used_media_ids();
     325
     326            // Build optimized query for unused attachments
     327            $where_conditions = [
     328                "p.post_type = 'attachment'",
     329                "p.post_status = 'inherit'"
     330            ];
     331
     332            // Exclude used image IDs with better performance
     333            if ( ! empty( $used_image_ids ) ) {
     334                $used_image_ids_placeholder = implode( ',', array_map( 'intval', $used_image_ids ) );
     335                $where_conditions[] = "p.ID NOT IN ($used_image_ids_placeholder)";
     336            }
     337
     338            // Filter by author ID if provided
     339            if ( $this->author_id ) {
     340                $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id );
     341            }
     342
     343            // If there is a search query, add the search condition
     344            if ( $search ) {
     345                $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
     346            }
     347
     348            $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions );
     349
     350            // Count query first
     351            $count_query = "
     352                SELECT COUNT(*)
     353                FROM {$wpdb->posts} p
     354                $where_clause
     355            ";
     356            $total_items = $wpdb->get_var( $count_query );
     357
     358            // Main query with pagination
    207359            $query = "
    208360                SELECT p.ID, p.post_title, p.guid, p.post_author, p.post_date
    209361                FROM {$wpdb->posts} p
    210                 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.meta_value AND pm.meta_key = '_thumbnail_id'
    211                 LEFT JOIN {$wpdb->posts} pp ON pp.post_content LIKE CONCAT('%wp-image-', p.ID, '%')
    212                 LEFT JOIN {$wpdb->postmeta} site_icon ON p.ID = site_icon.meta_value
    213                     AND site_icon.meta_key = 'site_icon'
    214                 WHERE p.post_type = 'attachment'
    215                 AND pm.meta_value IS NULL
    216                 AND pp.ID IS NULL
    217                 AND site_icon.meta_value IS NULL
     362                $where_clause
     363                ORDER BY p.post_date DESC
     364                LIMIT $offset, $per_page
    218365            ";
    219366
    220             // Exclude used image IDs.
    221             if ( ! empty( $used_image_ids ) ) {
    222                 $used_image_ids_placeholder = implode( ',', array_fill( 0, count( $used_image_ids ), '%d' ) );
    223                 $query .= $wpdb->prepare(
    224                     " AND p.ID NOT IN ($used_image_ids_placeholder)",
    225                     $used_image_ids
    226                 );
    227             }
    228 
    229             // Filter by author ID if provided.
    230             if ( $this->author_id ) {
    231                 $query .= $wpdb->prepare( ' AND p.post_author = %d', $this->author_id );
    232             }
    233 
    234             // If there is a search query, add the search condition.
    235             if ( $search ) {
    236                 $query .= $wpdb->prepare( ' AND p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
    237             }
    238 
    239             // Order by post_date in descending order.
    240             $query .= ' ORDER BY p.post_date DESC';
    241 
    242             // Calculate total items count.
    243             $total_items_query = $wpdb->prepare( "SELECT COUNT(*) FROM ($query) as total_query" );
    244             $total_items       = $wpdb->get_var( $total_items_query );
    245 
    246             // Add pagination to the main query.
    247             $query .= " LIMIT $offset, $per_page";
    248 
    249             // Retrieve items based on the constructed query.
     367            // Retrieve items based on the constructed query
    250368            $this->items = $wpdb->get_results( $query );
    251369
    252             // Cache the results for 1 hour.
    253             set_transient( $cache_key, array( 'items' => $this->items, 'total_items' => $total_items ), HOUR_IN_SECONDS );
     370            // Cache the results for shorter time (10 seconds) with smart invalidation
     371            set_transient( $cache_key, array( 'items' => $this->items, 'total_items' => $total_items ), 10 ); // 10 seconds
     372
     373            // Update last cache time
     374            update_option( 'unused_media_last_cache_time', time() );
    254375        } else {
    255             // Use cached results.
     376            // Use cached results
    256377            $this->items = $cached_results['items'];
    257378            $total_items = $cached_results['total_items'];
    258379        }
    259380
    260         // Define column headers, hidden columns, and sortable columns.
     381        // Define column headers, hidden columns, and sortable columns
    261382        $columns               = $this->get_columns();
    262383        $hidden                = [];
     
    264385        $this->_column_headers = [ $columns, $hidden, $sortable ];
    265386
    266         // Set pagination arguments for display.
     387        // Set pagination arguments for display
    267388        $this->set_pagination_args( array(
    268389            'total_items' => $total_items,
     
    273394
    274395    /**
     396     * Check if cache should be invalidated based on recent media activity
     397     *
     398     * @return bool True if cache should be cleared
     399     */
     400    private function should_invalidate_cache() {
     401        global $wpdb;
     402
     403        // Get the last time cache was created
     404        $last_cache_time = get_option( 'unused_media_last_cache_time', 0 );
     405
     406        // Check if any media was added/modified since last cache
     407        $recent_media_activity = $wpdb->get_var( $wpdb->prepare("
     408            SELECT COUNT(*)
     409            FROM {$wpdb->posts}
     410            WHERE post_type = 'attachment'
     411            AND post_modified_gmt > %s
     412        ", date( 'Y-m-d H:i:s', $last_cache_time ) ) );
     413
     414        // Check if any posts were modified (which might affect image usage)
     415        $recent_post_activity = $wpdb->get_var( $wpdb->prepare("
     416            SELECT COUNT(*)
     417            FROM {$wpdb->posts}
     418            WHERE post_type IN ('post', 'page', 'product')
     419            AND post_modified_gmt > %s
     420        ", date( 'Y-m-d H:i:s', $last_cache_time ) ) );
     421
     422        return ( $recent_media_activity > 0 || $recent_post_activity > 0 );
     423    }
     424
     425    /**
     426     * Force clear all cache manually
     427     */
     428    public function force_clear_cache() {
     429        $this->clear_cache();
     430        delete_option( 'unused_media_last_cache_time' );
     431    }
     432
     433    /**
    275434     * Get total number of items, optionally filtered by search term.
     435     * This method now supports real-time counting for display purposes.
    276436     *
    277437     * @param string $search Optional. Search term to filter items.
     438     * @param bool   $force_fresh Optional. Force fresh calculation without cache.
    278439     * @return int Total number of items.
    279440     */
    280     public function get_total_items( $search = '' ) {
     441    public function get_total_items( $search = '', $force_fresh = false ) {
    281442        global $wpdb;
    282443
    283         // Get all post IDs that use Elementor.
    284         $elementor_posts = $wpdb->get_col("
    285             SELECT post_id
    286             FROM {$wpdb->postmeta}
    287             WHERE meta_key = '_elementor_data'
    288         ");
    289 
    290         // Collect image IDs used by Elementor.
    291         $used_image_ids = [];
    292 
    293         foreach ( $elementor_posts as $post_id ) {
    294             $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
    295 
    296             if ( $elementor_data ) {
    297                 $elementor_data = json_decode( $elementor_data, true );
    298 
    299                 if ( is_array( $elementor_data ) ) {
    300                     array_walk_recursive( $elementor_data, function( $item, $key ) use ( &$used_image_ids ) {
    301                         if ( $key === 'id' && is_numeric( $item ) ) {
    302                             $used_image_ids[] = $item;
    303                         }
    304                     } );
    305                 }
    306             }
    307         }
    308 
    309         // Get Site Icon ID
    310         $site_icon_id = get_option( 'site_icon' );
    311         if ($site_icon_id) {
    312             $used_image_ids[] = $site_icon_id;
    313         }
    314 
    315         // Ensure unique IDs.
    316         $used_image_ids = array_unique( $used_image_ids );
    317 
    318         // Build the base query to count unused attachments.
     444        // Check for force refresh parameter or force_fresh flag
     445        $force_refresh = ( isset( $_GET['refresh_cache'] ) && $_GET['refresh_cache'] === '1' ) || $force_fresh;
     446
     447        $cache_key = 'unused_media_total_' . md5( serialize( array( $search, $this->author_id ) ) );
     448
     449        // Clear cache if forced or if there's recent activity
     450        if ( $force_refresh || $this->should_invalidate_cache() ) {
     451            delete_transient( $cache_key );
     452        }
     453
     454        $cached_total = get_transient( $cache_key );
     455
     456        if ( false !== $cached_total && ! $force_refresh ) {
     457            return $cached_total;
     458        }
     459
     460        // Get used media IDs
     461        $used_image_ids = $this->get_used_media_ids();
     462
     463        // Build the optimized count query
     464        $where_conditions = [
     465            "p.post_type = 'attachment'",
     466            "p.post_status = 'inherit'"
     467        ];
     468
     469        // Exclude used image IDs
     470        if ( ! empty( $used_image_ids ) ) {
     471            $used_image_ids_placeholder = implode( ',', array_map( 'intval', $used_image_ids ) );
     472            $where_conditions[] = "p.ID NOT IN ($used_image_ids_placeholder)";
     473        }
     474
     475        // Filter by author ID if provided
     476        if ( $this->author_id ) {
     477            $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id );
     478        }
     479
     480        // If there is a search query, add the search condition
     481        if ( $search ) {
     482            $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
     483        }
     484
     485        $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions );
     486
    319487        $query = "
    320488            SELECT COUNT(*)
    321489            FROM {$wpdb->posts} p
    322             LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.meta_value AND pm.meta_key = '_thumbnail_id'
    323             LEFT JOIN {$wpdb->posts} pp ON pp.post_content LIKE CONCAT('%wp-image-', p.ID, '%')
    324             LEFT JOIN {$wpdb->postmeta} site_icon ON p.ID = site_icon.meta_value
    325                 AND site_icon.meta_key = 'site_icon'
    326             WHERE p.post_type = 'attachment'
    327             AND pm.meta_value IS NULL
    328             AND pp.ID IS NULL
    329             AND site_icon.meta_value IS NULL
     490            $where_clause
    330491        ";
    331492
    332         // Exclude used image IDs.
    333         if ( ! empty( $used_image_ids ) ) {
    334             $used_image_ids_placeholder = implode( ',', array_fill( 0, count( $used_image_ids ), '%d' ) );
    335             $query .= $wpdb->prepare(
    336                 " AND p.ID NOT IN ($used_image_ids_placeholder)",
    337                 $used_image_ids
    338             );
    339         }
    340 
    341         // Filter by author ID if provided.
    342         if ( $this->author_id ) {
    343             $query .= $wpdb->prepare( ' AND p.post_author = %d', $this->author_id );
    344         }
    345 
    346         // If there is a search query, add the search condition.
    347         if ( $search ) {
    348             $query .= $wpdb->prepare( ' AND p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
    349         }
    350 
    351         // Get total items count.
    352493        $total_items = $wpdb->get_var( $query );
    353494
     495        // Cache for shorter time (10 seconds as you wanted) but with smart invalidation
     496        set_transient( $cache_key, $total_items, 10 ); // 10 seconds
     497
     498        // Update last cache time
     499        update_option( 'unused_media_last_cache_time', time() );
     500
    354501        return $total_items;
     502    }
     503
     504    /**
     505     * Get fresh total count without any caching - for display purposes
     506     *
     507     * @param string $search Optional. Search term to filter items.
     508     * @return int Total number of items.
     509     */
     510    public function get_fresh_total_items( $search = '' ) {
     511        return $this->get_total_items( $search, true );
    355512    }
    356513
     
    396553                    }
    397554
     555                    // Clear cache after deletion
     556                    $this->clear_cache();
     557
    398558                    // Set a transient to display a success message
    399559                    $deleted_count = count( $media_ids );
     
    407567
    408568    /**
     569     * Clear all plugin-related cache
     570     */
     571    private function clear_cache() {
     572        global $wpdb;
     573
     574        // Delete all transients related to this plugin
     575        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_unused_media_%'" );
     576        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_unused_media_%'" );
     577
     578        // Also clear the last cache time option
     579        delete_option( 'unused_media_last_cache_time' );
     580    }
     581
     582    /**
    409583     * Display the success message if a media file was deleted.
    410584     */
     
    412586        if ( $message = get_transient( 'unused_media_delete_message' ) ) {
    413587            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
    414             delete_transient( 'unused_media_delete_message' ); // Remove the message after it's displayed
     588            delete_transient( 'unused_media_delete_message' );
    415589        }
    416590    }
  • media-tracker/trunk/includes/Admin/views/unused-media-list.php

    r3148751 r3345787  
    1414        <div class="media-toolbar-wrap wp-filter">
    1515            <div class="unused-image-found">
    16                 <h2><?php echo '<span>'.esc_html( $unused_media_list->get_total_items() ).'</span>' . ' ' . esc_html__( 'unused media found!', 'media-tracker' ); ?></h2>
     16                <h2><?php echo '<span>'.esc_html( $unused_media_list->get_fresh_total_items() ).'</span>' . ' ' . esc_html__( 'unused media found!', 'media-tracker' ); ?></h2>
    1717            </div>
    1818
  • media-tracker/trunk/media-tracker.php

    r3244839 r3345787  
    55 * Author: TheBitCraft
    66 * Author URI: https://thebitcraft.com/
    7  * Version: 1.0.8
     7 * Version: 1.0.9
    88 * Requires PHP: 7.4
    99 * Requires at least: 5.9
    10  * Tested up to: 6.6.1
     10 * Tested up to: 6.8.1
    1111 * Text Domain: media-tracker
    1212 * License: GPL v2 or later
     
    2828     * @var string
    2929     */
    30     const version = '1.0.8';
     30    const version = '1.0.9';
    3131
    3232    /**
  • media-tracker/trunk/readme.txt

    r3244839 r3345787  
    22
    33Contributors: thebitcraft, rejuancse
    4 Tags: tracker, unused, media cleaner, duplicate
     4Tags: tracker, unused, media cleaner, duplicate, optimizer
    55Requires at least: 5.9
    6 Tested up to: 6.6.1
     6Tested up to: 6.8.1
    77Requires PHP: 7.4
    8 Stable tag: 1.0.8
     8Stable tag: 1.0.9
    99License: GPLv2 or later
    1010License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    2020**🔥 Media Usage: Track and analyze media usage across your WordPress site. This feature allows you to see where each media file is being used, helping you manage and optimize your media library more effectively.
    2121
    22 **🔥 Cleaner: Find media files that are not in use on any posts, pages, or other content. This feature helps you keep your site clutter-free by highlighting files that can be safely removed.
     22**🔥 Unused Media List: Find media files that are not in use on any posts, pages, or other content. This feature helps you keep your site clutter-free by highlighting files that can be safely removed.
    2323
    2424**🔥 Duplicate Images: Detect and manage duplicate images within your media library. Consolidate or remove redundant files to save storage space and maintain an organized media library.
     25
     26
     27Media Tracker is compatible with WordPress Classic Editor, Gutenberg, and Elementor.
    2528
    2629== Installation ==
     
    5962== Changelog ==
    6063
     64= 1.0.9 [17/08/2025] =
     65* 502 network error issue fixed
     66* Images Markled as Unused When Used issue fixed
     67
    6168= 1.0.8 [22/02/2025] =
    6269* Broken link featured removed
Note: See TracChangeset for help on using the changeset viewer.