$28 GRAYBYTE WORDPRESS FILE MANAGER $66

SERVER : vnpttt-amd7f72-h1.vietnix.vn #1 SMP Fri May 24 12:42:50 UTC 2024
SERVER IP : 103.200.23.149 | ADMIN IP 216.73.216.22
OPTIONS : CRL = ON | WGT = ON | SDO = OFF | PKEX = OFF
DEACTIVATED : NONE

/home/bqrcodec/test2.proid.vn/MH Code Snippets/

HOME
Current File : /home/bqrcodec/test2.proid.vn/MH Code Snippets//clean-media.txt
<?php
/**
 * Plugin Name: My Pro ID Image Cleanup (Enhanced Deletion Features)
 * Description: Provides settings to schedule/run jobs deleting unused images, empty my-pro-id posts, normal posts (with options for all or non-admin), my-pro-id posts without phone numbers, and dangerous bulk deletion options.
 * Version: 1.7
 * Author: Your Name
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

class My_Pro_ID_Image_Cleanup {

    private static $instance = null;
    private $option_name      = 'my_pro_id_cleanup_settings';
    private $log_option_name  = 'my_pro_id_cleanup_logs';       // Stores image deletion logs
    private $post_log_option  = 'my_pro_id_empty_post_logs';    // Stores my-pro-id post (empty image) deletion logs
    private $normal_post_log_option = 'my_pro_id_normal_post_logs'; // Stores normal post (non-admin) deletion logs
    private $all_normal_post_log_option = 'my_pro_id_all_normal_post_logs'; // NEW: Stores all normal post deletion logs
    private $no_phone_post_log_option = 'my_pro_id_no_phone_post_logs'; // NEW: Stores my-pro-id post (no phone) deletion logs
    private $all_posts_non_admin_media_log_option = 'my_pro_id_all_posts_non_admin_media_logs'; // NEW: Stores log for deleting all posts & non-admin media
    private $cron_hook        = 'my_pro_id_cleanup_cron_event';

    /**
     * Singleton pattern
     */
    public static function init() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor
     */
    private function __construct() {
        // Activation/Deactivation hooks
        register_activation_hook( __FILE__, array( $this, 'on_plugin_activation' ) );
        register_deactivation_hook( __FILE__, array( $this, 'on_plugin_deactivation' ) );

        // Admin menu & settings
        add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
        add_action( 'admin_init', array( $this, 'register_settings' ) );

        // Cron job
        add_action( $this->cron_hook, array( $this, 'cron_cleanup_callback' ) );
    }

    /**
     * Plugin activation: schedule the cron event based on default or saved settings.
     */
    public function on_plugin_activation() {
        $this->schedule_cron_job();
    }

    /**
     * Plugin deactivation: remove scheduled cron event.
     */
    public function on_plugin_deactivation() {
        $timestamp = wp_next_scheduled( $this->cron_hook );
        if ( $timestamp ) {
            wp_unschedule_event( $timestamp, $this->cron_hook );
        }
    }

    /**
     * Set up or update the cron job based on current settings.
     */
    private function schedule_cron_job() {
        // Clear any existing schedules first
        $timestamp = wp_next_scheduled( $this->cron_hook );
        if ( $timestamp ) {
            wp_unschedule_event( $timestamp, $this->cron_hook );
        }

        // Get settings
        $settings  = get_option( $this->option_name, array() );
        $schedule  = isset( $settings['schedule'] ) ? $settings['schedule'] : 'daily';
        $hour      = isset( $settings['time'] ) ? intval( $settings['time'] ) : 2; // 2 AM default

        // Add custom schedules for monthly or weekly if needed
        add_filter( 'cron_schedules', array( $this, 'add_cron_schedules' ) );

        // Calculate the next run
        $next_run = $this->get_next_run_timestamp( $schedule, $hour );
        if ($next_run && $schedule) { // Ensure we have valid values before scheduling
            wp_schedule_event( $next_run, $schedule, $this->cron_hook );
        }
    }

    /**
     * Determine next run timestamp for the selected schedule and hour
     */
    private function get_next_run_timestamp( $schedule, $hour ) {
        $current_time = current_time( 'timestamp' );
        $today        = strtotime( 'today ' . $hour . ':00:00', $current_time );

        // If today's hour is already passed, schedule for tomorrow
        if ( $current_time >= $today ) {
            $start_time = strtotime( 'tomorrow ' . $hour . ':00:00' );
        } else {
            $start_time = $today;
        }

        // Adjust for weekly/monthly if the first calculated time is not correct for the interval logic
        // This basic calculation works for daily, but weekly/monthly might need more complex logic
        // depending on the exact day/date desired. For simplicity, we rely on wp_schedule_event's recurrence.
        return $start_time;
    }

    /**
     * Add custom cron schedules for weekly and monthly
     */
    public function add_cron_schedules( $schedules ) {
        if ( ! isset( $schedules['weekly'] ) ) {
            $schedules['weekly'] = array(
                'interval' => 7 * 24 * 3600,
                'display'  => __( 'Once Weekly', 'my-pro-id' ),
            );
        }
        if ( ! isset( $schedules['monthly'] ) ) {
            // Approx 30 days. Adjust if needed.
            $schedules['monthly'] = array(
                'interval' => 30 * 24 * 3600,
                'display'  => __( 'Once Monthly', 'my-pro-id' ),
            );
        }
        return $schedules;
    }

    /**
     * The Cron job callback: auto-delete unattached images in batches
     */
    public function cron_cleanup_callback() {
        $settings = get_option( $this->option_name, array() );
        $batch    = ! empty( $settings['batch_size'] ) ? intval( $settings['batch_size'] ) : 500;

        $this->delete_unattached_images( $batch );
    }

    /**
     * Delete unattached images in batches up to $limit
     */
    private function delete_unattached_images( $limit ) {
        // 1. Gather used image IDs
        $used_image_ids = $this->get_used_image_ids();

        // 2. Query unattached attachments
        $query_args = array(
            'post_type'      => 'attachment',
            'post_status'    => 'inherit', // Attachments usually have 'inherit' status
            'posts_per_page' => $limit,
            'orderby'        => 'date',
            'order'          => 'DESC',
            'post_parent'    => 0, // unattached
            'fields'         => 'ids' // Only get IDs
        );
        $attachments = get_posts( $query_args );

        if ( empty( $attachments ) ) {
            return; // No unattached images found in this batch
        }

        $deleted_items = array();
        foreach ( $attachments as $attachment_id ) {
            // Check if the attachment is an image (optional but good practice)
             if ( ! wp_attachment_is_image( $attachment_id ) ) {
                 continue;
             }

            // If not in used list, delete
            if ( ! in_array( $attachment_id, $used_image_ids, true ) ) {
                 $attachment_url = wp_get_attachment_url( $attachment_id );
                 $delete_result = wp_delete_attachment( $attachment_id, true ); // true = force delete
                 if ($delete_result) {
                     $deleted_items[] = array(
                         'id'  => $attachment_id,
                         'url' => $attachment_url ? $attachment_url : 'URL not found',
                     );
                 }
            }
        }

        if ( ! empty( $deleted_items ) ) {
            $this->append_log( $deleted_items, $this->log_option_name, 'Deleted Image ID: %d (%s)' ); // Log the deleted images
        }
    }

    /**
     * Generic function to append deletion log to an option in the database
     * @param array $items Array of items deleted (can be IDs or associative arrays)
     * @param string $option_key The option name to store the log
     * @param string $log_format Sprintf format string for the log entry (e.g., '%s - Deleted ID: %d')
     */
    private function append_log( $items, $option_key, $log_format ) {
        $logs = get_option( $option_key, array() );
        $now  = current_time( 'mysql' );

        foreach ( $items as $item ) {
            // Adjust based on whether $item is simple ID or array
            if ( is_array($item) && isset($item['id']) ) {
                $id = $item['id'];
                $extra = isset($item['url']) ? $item['url'] : (isset($item['title']) ? $item['title'] : '');
                $logs[] = sprintf( $log_format, $now, $id, $extra );
            } elseif ( is_numeric($item) ) {
                $logs[] = sprintf( $log_format, $now, $item );
            } else {
                // Handle unexpected item format if necessary
                $logs[] = sprintf('%s - Logged item: %s', $now, print_r($item, true));
            }
        }
         // Keep the log from growing indefinitely (optional)
         $max_log_entries = 1000;
         if (count($logs) > $max_log_entries) {
            $logs = array_slice($logs, count($logs) - $max_log_entries);
         }
        update_option( $option_key, $logs );
    }

    /**
     * Returns array of image IDs used by my-pro-id posts in the designated metafields
     */
    private function get_used_image_ids() {
        global $wpdb;

        // Metafields to check (ensure this list is accurate for your setup)
        $meta_keys = array(
            'kid_image',
            'kid_gallery',
            'avatar1',
            'cover-image',
            'logocongty',
            'gioi_thieu_cover',
            'gallery',
            'line_qr',
            'viber_qr',
            'zalo_qr_code',
            'wechat_qr',
            'bank-qr-1',
        );

        // Repeater fields / complex fields that might contain IDs
        // ACF repeaters often store data serialized or with sub-field keys like 'repeater_field_0_sub_field'
        // You might need more complex logic to extract IDs from these.
        // Assuming simple storage for these examples:
        $repeater_subfields = array(
            'productsimage', // Example: Assuming this stores IDs directly or in a simple array
            '505_profile_image', // Example: Assuming this stores IDs directly
        );

        // Combine simple and complex keys for the initial query
        $all_meta_keys = array_merge( $meta_keys, $repeater_subfields );

        // 1) Get all my-pro-id post IDs
        $my_pro_id_posts = get_posts( array(
            'post_type'      => 'my-pro-id',
            'post_status'    => 'any',
            'posts_per_page' => -1, // Get all
            'fields'         => 'ids',
        ) );

        if ( empty( $my_pro_id_posts ) ) {
            return array(); // No posts, so no used images
        }

        // 2) Gather all meta values for those posts and relevant keys
        $placeholders           = implode( ', ', array_fill( 0, count( $my_pro_id_posts ), '%d' ) );
        $placeholders_meta_keys = implode( ', ', array_fill( 0, count( $all_meta_keys ), '%s' ) );

        $sql = $wpdb->prepare( "
            SELECT meta_value
            FROM {$wpdb->postmeta}
            WHERE post_id IN ($placeholders)
              AND meta_key IN ($placeholders_meta_keys)
        ", array_merge( $my_pro_id_posts, $all_meta_keys ) );

        $raw_meta_values = $wpdb->get_col( $sql );

        // 3) Extract attachment IDs from various formats (simple ID, serialized array, etc.)
        $used_image_ids = array();
        foreach ( $raw_meta_values as $meta_value ) {
             if ( empty($meta_value) ) continue;

            // Try unserializing first
            $maybe_array = maybe_unserialize( $meta_value );

            if ( is_array( $maybe_array ) ) {
                 // Handle arrays (e.g., galleries, some repeater formats)
                 foreach ( $maybe_array as $inner_value ) {
                     if ( is_numeric( $inner_value ) && $inner_value > 0 ) {
                         $used_image_ids[] = (int) $inner_value;
                     }
                      // Add checks here for more complex structures if needed (e.g., array of arrays with 'id' key)
                 }
            } elseif ( is_numeric( $meta_value ) && $meta_value > 0 ) {
                 // Handle simple numeric IDs
                 $used_image_ids[] = (int) $meta_value;
            }
             // Add more checks if IDs are stored in other formats (e.g., JSON strings)
        }

        // Add Featured Images (Post Thumbnails) for 'my-pro-id' posts
         foreach ($my_pro_id_posts as $post_id) {
             if (has_post_thumbnail($post_id)) {
                 $thumb_id = get_post_thumbnail_id($post_id);
                 if ($thumb_id) {
                     $used_image_ids[] = $thumb_id;
                 }
             }
         }


        return array_unique( $used_image_ids ); // Return unique IDs
    }


    /**
     * ADMIN SETTINGS PAGE
     */
    public function add_settings_page() {
        add_options_page(
            'My Pro ID Cleanup',     // Page Title
            'My Pro ID Cleanup',     // Menu Title
            'manage_options',        // Capability required
            'my-pro-id-cleanup',     // Menu Slug
            array( $this, 'render_settings_page' ) // Callback function
        );
    }

    /**
     * Register setting & fields
     */
    public function register_settings() {
        register_setting( 'my_pro_id_cleanup_group', $this->option_name, array( $this, 'sanitize_settings' ) );

        // Section for Auto-Cleanup Schedule
        add_settings_section(
            'my_pro_id_cleanup_section',
            __( 'Auto-Cleanup Schedule (Unused Images)', 'my-pro-id' ),
            null, // Callback for section description (optional)
            'my-pro-id-cleanup' // Page slug
        );

        // Field: Batch size (images)
        add_settings_field(
            'batch_size',
            __( 'Number of images to process per run', 'my-pro-id' ),
            array( $this, 'field_batch_size_cb' ),
            'my-pro-id-cleanup',
            'my_pro_id_cleanup_section'
        );

        // Field: Schedule Frequency
        add_settings_field(
            'schedule',
            __( 'Cleanup Frequency', 'my-pro-id' ),
            array( $this, 'field_schedule_cb' ),
            'my-pro-id-cleanup',
            'my_pro_id_cleanup_section'
        );

        // Field: Time of Day
        add_settings_field(
            'time',
            __( 'Hour to Run (0-23)', 'my-pro-id' ),
            array( $this, 'field_time_cb' ),
            'my-pro-id-cleanup',
            'my_pro_id_cleanup_section'
        );
    }

    /**
     * Sanitize settings input before saving. Reschedule cron on save.
     */
    public function sanitize_settings( $input ) {
        $sanitized = array();

        // Sanitize Batch Size
        $sanitized['batch_size'] = isset( $input['batch_size'] ) ? absint( $input['batch_size'] ) : 500;
        if ($sanitized['batch_size'] < 1) $sanitized['batch_size'] = 500; // Ensure minimum value

        // Sanitize Schedule
        $possible_schedules = array( 'daily', 'weekly', 'monthly' );
        $sanitized['schedule'] = isset( $input['schedule'] ) && in_array( $input['schedule'], $possible_schedules, true )
            ? $input['schedule']
            : 'daily'; // Default to daily

        // Sanitize Time
        $sanitized['time'] = ( isset( $input['time'] ) && is_numeric( $input['time'] ) )
            ? min( 23, max( 0, intval( $input['time'] ) ) ) // Clamp between 0 and 23
            : 2; // Default to 2 AM

        // Reschedule cron event whenever settings are updated
        $this->schedule_cron_job();

        return $sanitized;
    }

    // Callback functions to render the settings fields
    public function field_batch_size_cb() {
        $options    = get_option( $this->option_name );
        $batch_size = ! empty( $options['batch_size'] ) ? $options['batch_size'] : 500;
        echo '<input type="number" name="' . esc_attr($this->option_name) . '[batch_size]" value="' . esc_attr( $batch_size ) . '" min="1" step="1" />';
        echo '<p class="description">' . __('Number of unattached images to check and potentially delete during each scheduled run.', 'my-pro-id') . '</p>';
    }

    public function field_schedule_cb() {
        $options  = get_option( $this->option_name );
        $schedule = ! empty( $options['schedule'] ) ? $options['schedule'] : 'daily';

        echo '<select name="' . esc_attr($this->option_name) . '[schedule]">';
        echo '<option value="daily" ' . selected( $schedule, 'daily', false ) . '>Daily</option>';
        echo '<option value="weekly" ' . selected( $schedule, 'weekly', false ) . '>Weekly</option>';
        echo '<option value="monthly" ' . selected( $schedule, 'monthly', false ) . '>Monthly</option>';
        echo '</select>';
        echo '<p class="description">' . __('How often the automatic image cleanup should run.', 'my-pro-id') . '</p>';

    }

    public function field_time_cb() {
        $options = get_option( $this->option_name );
        $time    = isset( $options['time'] ) ? $options['time'] : 2;
        echo '<input type="number" name="' . esc_attr($this->option_name) . '[time]" value="' . esc_attr( $time ) . '" min="0" max="23" step="1" />';
         echo '<p class="description">' . __('The hour of the day (24-hour format) the cleanup should start.', 'my-pro-id') . '</p>';
    }

    /**
     * Render the settings page content
     */
    public function render_settings_page() {
        // Check user capability
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die(__( 'You do not have sufficient permissions to access this page.' ));
        }

        //--- PROCESS MANUAL ACTIONS FIRST ---
        $message = ''; // Store feedback messages

        //--- MANUAL IMAGE CLEANUP ---
        if ( isset( $_POST['my_pro_id_manual_cleanup'] ) && check_admin_referer( 'my_pro_id_manual_cleanup_action' ) ) {
            $limit = isset( $_POST['manual_limit'] ) ? absint( $_POST['manual_limit'] ) : 10;
            if ($limit < 1) $limit = 10;
            $this->delete_unattached_images( $limit ); // Re-use the batch deletion logic
            $message .= '<div id="message" class="updated notice is-dismissible"><p>' . __('Manual image cleanup executed. Check logs below.', 'my-pro-id') . '</p></div>';
        }

        //--- MANUAL POST CLEANUP (EMPTY my-pro-id) ---
        if ( isset( $_POST['my_pro_id_post_cleanup'] ) && check_admin_referer( 'my_pro_id_post_cleanup_action' ) ) {
            $post_limit    = isset( $_POST['post_limit'] ) ? absint( $_POST['post_limit'] ) : 100;
             if ($post_limit < 1) $post_limit = 100;
            $deleted_count = $this->delete_empty_my_pro_id_posts( $post_limit );
             if ($deleted_count > 0) {
                $message .= '<div id="message" class="updated notice is-dismissible"><p>' . sprintf(
                    __( 'My Pro ID post (empty image) cleanup complete. %d posts were deleted. Check logs below.', 'my-pro-id' ),
                    $deleted_count
                ) . '</p></div>';
             } else {
                 $message .= '<div id="message" class="notice notice-info is-dismissible"><p>' . __( 'No empty (image) My Pro ID posts found to delete.', 'my-pro-id' ) . '</p></div>';
             }
        }

        //--- MANUAL NORMAL POST CLEANUP (NON-ADMIN) ---
        if ( isset( $_POST['my_pro_id_manual_normal_post_cleanup'] ) && check_admin_referer( 'my_pro_id_normal_post_cleanup_action' ) ) {
             $batch_size = isset( $_POST['normal_post_batch_size'] ) ? absint( $_POST['normal_post_batch_size'] ) : 100;
             if ($batch_size < 1) $batch_size = 100;
             $deleted_count = $this->delete_normal_posts_in_batch( $batch_size ); // Call the non-admin deletion function

             if ( $deleted_count > 0 ) {
                 $message .= '<div id="message" class="updated notice is-dismissible"><p>' . sprintf(
                     __( 'Normal post (non-admin) cleanup complete for this batch. %d posts were deleted. Run again to delete more. Check logs below.', 'my-pro-id' ),
                     $deleted_count
                 ) . '</p></div>';
             } else {
                  $message .= '<div id="message" class="notice notice-info is-dismissible"><p>' . __( 'No more non-admin normal posts found to delete in this batch.', 'my-pro-id' ) . '</p></div>';
             }
         }

        // --- NEW: MANUAL ALL NORMAL POST CLEANUP ---
        if ( isset( $_POST['my_pro_id_manual_all_normal_post_cleanup'] ) && check_admin_referer( 'my_pro_id_all_normal_post_cleanup_action' ) ) {
            $batch_size = isset( $_POST['all_normal_post_batch_size'] ) ? absint( $_POST['all_normal_post_batch_size'] ) : 100;
            if ($batch_size < 1) $batch_size = 100;
            $deleted_count = $this->delete_all_normal_posts_in_batch( $batch_size ); // Call the new deletion function

            if ( $deleted_count > 0 ) {
                $message .= '<div id="message" class="updated notice is-dismissible"><p>' . sprintf(
                    __( 'ALL normal post cleanup complete for this batch. %d posts (including admin posts) were deleted. Run again to delete more. Check logs below.', 'my-pro-id' ),
                    $deleted_count
                ) . '</p></div>';
            } else {
                 $message .= '<div id="message" class="notice notice-info is-dismissible"><p>' . __( 'No more normal posts found to delete in this batch.', 'my-pro-id' ) . '</p></div>';
            }
        }

        // --- NEW: MANUAL MY PRO ID POSTS WITHOUT PHONE CLEANUP ---
        if ( isset( $_POST['my_pro_id_no_phone_post_cleanup'] ) && check_admin_referer( 'my_pro_id_no_phone_post_cleanup_action' ) ) {
            $batch_size = isset( $_POST['no_phone_post_batch_size'] ) ? absint( $_POST['no_phone_post_batch_size'] ) : 100;
            if ($batch_size < 1) $batch_size = 100;
            $deleted_count = $this->delete_my_pro_id_posts_without_phone( $batch_size ); // Call the new deletion function

            if ( $deleted_count > 0 ) {
                $message .= '<div id="message" class="updated notice is-dismissible"><p>' . sprintf(
                    __( 'My Pro ID posts (no phone) cleanup complete for this batch. %d posts were deleted. Run again to delete more. Check logs below.', 'my-pro-id' ),
                    $deleted_count
                ) . '</p></div>';
            } else {
                 $message .= '<div id="message" class="notice notice-info is-dismissible"><p>' . __( 'No My Pro ID posts without a phone number found to delete in this batch.', 'my-pro-id' ) . '</p></div>';
            }
        }


        //--- DANGER ZONE ACTIONS ---
        $correct_password   = 'iLoveProID@123'; // Example password - CHANGE THIS or use a better method!

        //--- DELETE ALL EXCEPT ADMIN ---
        if ( isset( $_POST['my_pro_id_delete_all_except_admin'] ) && check_admin_referer( 'my_pro_id_delete_all_except_admin_action' ) ) {
            $password_submitted = isset( $_POST['confirmation_password_1'] ) ? sanitize_text_field( $_POST['confirmation_password_1'] ) : '';

            if ( ! hash_equals( $correct_password, $password_submitted ) ) { // Use hash_equals for timing attack resistance
                $message .= '<div id="message" class="error notice is-dismissible"><p><strong>' . __('Error:', 'my-pro-id') . '</strong> ' . __('Incorrect password for "Delete All Except Admin". Deletion aborted.', 'my-pro-id') . '</p></div>';
            } else {
                // Password matches, proceed with deletion
                $deleted_all_count = $this->delete_all_posts_and_media_except_admin();
                $message .= '<div id="message" class="updated notice is-dismissible"><p>' . sprintf(
                    __('"Delete All Except Admin" complete. %d non-admin posts/media items were removed.', 'my-pro-id'),
                    intval( $deleted_all_count )
                 ) . '</p></div>';
            }
        }

        // --- NEW: DELETE ALL POSTS & NON-ADMIN MEDIA ---
        if ( isset( $_POST['my_pro_id_delete_all_posts_non_admin_media'] ) && check_admin_referer( 'my_pro_id_delete_all_posts_non_admin_media_action' ) ) {
            $password_submitted = isset( $_POST['confirmation_password_2'] ) ? sanitize_text_field( $_POST['confirmation_password_2'] ) : '';

            if ( ! hash_equals( $correct_password, $password_submitted ) ) {
                $message .= '<div id="message" class="error notice is-dismissible"><p><strong>' . __('Error:', 'my-pro-id') . '</strong> ' . __('Incorrect password for "Delete All Posts & Non-Admin Media". Deletion aborted.', 'my-pro-id') . '</p></div>';
            } else {
                // Password matches, proceed with deletion
                $deleted_count = $this->delete_all_posts_and_non_admin_media(); // Call the new function
                $message .= '<div id="message" class="updated notice is-dismissible"><p>' . sprintf(
                    __('"Delete All Posts & Non-Admin Media" complete. %d items (all posts + non-admin media) were removed. Check logs below.', 'my-pro-id'),
                    intval( $deleted_count )
                 ) . '</p></div>';
            }
        }


        //--- RENDER THE PAGE CONTENT ---
        ?>
        <div class="wrap">
            <h1><?php esc_html_e( 'My Pro ID Cleanup Settings', 'my-pro-id' ); ?></h1>

            <?php echo $message; // Display feedback messages at the top ?>

            <form method="post" action="options.php">
                <?php
                settings_fields( 'my_pro_id_cleanup_group' ); // Nonce, action, option_page fields
                do_settings_sections( 'my-pro-id-cleanup' );  // Renders the sections and fields
                submit_button( __( 'Save Auto-Cleanup Settings', 'my-pro-id' ) );
                ?>
            </form>

            <hr />

            <h2><?php esc_html_e( 'Manual Cleanup Actions', 'my-pro-id' ); ?></h2>

            <div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
                <h3><?php esc_html_e( 'Unused Image Cleanup', 'my-pro-id' ); ?></h3>
                <p><?php esc_html_e( 'Manually delete a batch of images that are not attached to any post/page and not found in the specified \'my-pro-id\' fields.', 'my-pro-id' ); ?></p>
                <form method="post" action=""> <?php // Post to the current page ?>
                    <?php wp_nonce_field( 'my_pro_id_manual_cleanup_action' ); ?>
                    <label for="manual_limit"><?php esc_html_e( 'Images to process:', 'my-pro-id' ); ?></label>
                    <input type="number" id="manual_limit" name="manual_limit" value="50" min="1" step="1" style="width: 80px;" />
                    <button type="submit" name="my_pro_id_manual_cleanup" class="button button-secondary">
                        <?php esc_html_e( 'Delete Unused Image Batch', 'my-pro-id' ); ?>
                    </button>
                     <p class="description"><?php esc_html_e( 'Run this multiple times if you have many images to clean up.', 'my-pro-id' ); ?></p>
                </form>
            </div>

            <div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
                <h3><?php esc_html_e( 'Delete My Pro ID Posts without Images', 'my-pro-id' ); ?></h3>
                <p><?php esc_html_e( 'Deletes a batch of \'my-pro-id\' posts that do not have any images in the specified meta fields.', 'my-pro-id' ); ?></p>
                <form method="post" action="">
                    <?php wp_nonce_field( 'my_pro_id_post_cleanup_action' ); ?>
                    <label for="post_limit"><?php esc_html_e( 'Posts to delete:', 'my-pro-id' ); ?></label>
                    <input type="number" id="post_limit" name="post_limit" value="100" min="1" step="1" style="width: 80px;" />
                    <button type="submit" name="my_pro_id_post_cleanup" class="button button-secondary">
                        <?php esc_html_e( 'Delete Empty (Image) My Pro ID Post Batch', 'my-pro-id' ); ?>
                    </button>
                     <p class="description"><?php esc_html_e( 'Run this multiple times if needed.', 'my-pro-id' ); ?></p>
                </form>
            </div>

            <div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
                <h3><?php esc_html_e( 'Delete My Pro ID Posts without Phone Number', 'my-pro-id' ); ?></h3>
                <p><?php esc_html_e( 'Deletes a batch of \'my-pro-id\' posts that do not have a value in the \'phone\' meta field.', 'my-pro-id' ); ?></p>
                <form method="post" action="">
                    <?php wp_nonce_field( 'my_pro_id_no_phone_post_cleanup_action' ); ?>
                    <label for="no_phone_post_batch_size"><?php esc_html_e( 'Posts to delete:', 'my-pro-id' ); ?></label>
                    <input type="number" id="no_phone_post_batch_size" name="no_phone_post_batch_size" value="100" min="1" step="1" style="width: 80px;" />
                    <button type="submit" name="my_pro_id_no_phone_post_cleanup" class="button button-secondary">
                        <?php esc_html_e( 'Delete No-Phone My Pro ID Post Batch', 'my-pro-id' ); ?>
                    </button>
                     <p class="description"><?php esc_html_e( 'Run this multiple times if needed.', 'my-pro-id' ); ?></p>
                </form>
            </div>

            <div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
                 <h3><?php esc_html_e( 'Delete Normal Posts (Non-Admin Authors)', 'my-pro-id' ); ?></h3>
                <p><?php esc_html_e( 'Delete standard WordPress posts (type \'post\') in batches. Posts authored by administrators will NOT be deleted.', 'my-pro-id' ); ?></p>
                <form method="post" action="">
                    <?php wp_nonce_field( 'my_pro_id_normal_post_cleanup_action' ); // Add a nonce field ?>
                    <label for="normal_post_batch_size"><?php esc_html_e( 'Posts to delete:', 'my-pro-id' ); ?></label>
                    <input type="number" id="normal_post_batch_size" name="normal_post_batch_size" value="100" min="1" step="1" style="width: 80px;" />
                    <button type="submit" name="my_pro_id_manual_normal_post_cleanup" class="button button-secondary">
                        <?php esc_html_e( 'Delete Non-Admin Normal Posts Batch', 'my-pro-id' ); ?>
                    </button>
                     <p class="description"><?php esc_html_e( 'Run this multiple times to delete more posts. This permanently deletes posts (skips trash).', 'my-pro-id' ); ?></p>
                </form>
            </div>

            <div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
                <h3><?php esc_html_e( 'Delete ALL Normal Posts (Including Admins)', 'my-pro-id' ); ?></h3>
                <p><strong><?php esc_html_e( 'Warning:', 'my-pro-id' ); ?></strong> <?php esc_html_e( 'This will delete ALL standard WordPress posts (type \'post\'), including those created by administrators. Use with caution.', 'my-pro-id' ); ?></p>
                <form method="post" action="">
                    <?php wp_nonce_field( 'my_pro_id_all_normal_post_cleanup_action' ); ?>
                    <label for="all_normal_post_batch_size"><?php esc_html_e( 'Posts to delete:', 'my-pro-id' ); ?></label>
                    <input type="number" id="all_normal_post_batch_size" name="all_normal_post_batch_size" value="100" min="1" step="1" style="width: 80px;" />
                    <button type="submit" name="my_pro_id_manual_all_normal_post_cleanup" class="button button-primary" onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to delete ALL normal posts, including those by administrators?', 'my-pro-id' ); ?>');">
                        <?php esc_html_e( 'Delete ALL Normal Posts Batch', 'my-pro-id' ); ?>
                    </button>
                     <p class="description"><?php esc_html_e( 'Run this multiple times to delete more posts. This permanently deletes posts (skips trash).', 'my-pro-id' ); ?></p>
                </form>
            </div>


            <hr />

            <div style="border: 2px solid red; padding: 15px; margin-top: 20px; border-radius: 5px;">
                <h2 style="color:red; margin-top: 0;"><?php esc_html_e( 'Danger Zone: Bulk Deletion', 'my-pro-id' ); ?></h2>
                 <p><strong><?php esc_html_e( 'EXTREME WARNING:', 'my-pro-id' ); ?></strong> <?php esc_html_e( 'Actions in this section are highly destructive and cannot be undone. Make a full backup before proceeding!', 'my-pro-id' ); ?></p>
                 <p><?php esc_html_e( 'Password for confirmation:', 'my-pro-id' ); ?> <code><?php echo esc_html($correct_password); ?></code> <?php esc_html_e( '(Strongly recommend changing this in the plugin code!)', 'my-pro-id' ); ?></p>

                 <div style="margin-bottom: 20px; padding: 10px; border: 1px solid darkred; border-radius: 5px;">
                    <h3 style="color:darkred;"><?php esc_html_e( 'Option 1: Delete All Posts & Media (Except Admin Authorship)', 'my-pro-id' ); ?></h3>
                    <p><?php esc_html_e( 'This will permanently delete ALL posts, pages, custom post types, and media library items that were NOT authored by users with the Administrator role.', 'my-pro-id' ); ?></p>
                    <form method="post" action="">
                        <?php wp_nonce_field( 'my_pro_id_delete_all_except_admin_action' ); ?>
                        <p>
                            <label for="confirmation_password_1">
                                <strong><?php esc_html_e( 'Enter Password to Confirm:', 'my-pro-id' ); ?></strong>
                            </label><br />
                            <input type="password" name="confirmation_password_1" id="confirmation_password_1" required style="border-color: red;" />
                        </p>
                        <button type="submit" name="my_pro_id_delete_all_except_admin" class="button button-danger" style="background-color:red; color:#fff; border-color: darkred;" onclick="return confirm('<?php esc_attr_e( 'ARE YOU ABSOLUTELY SURE? This will delete all NON-ADMIN content permanently. There is NO UNDO. Click OK only if you have a backup and understand the consequences.', 'my-pro-id' ); ?>');">
                            <?php esc_html_e( 'DELETE ALL NON-ADMIN CONTENT NOW', 'my-pro-id' ); ?>
                        </button>
                    </form>
                </div>

                 <div style="padding: 10px; border: 1px solid darkred; border-radius: 5px;">
                    <h3 style="color:darkred;"><?php esc_html_e( 'Option 2: Delete ALL Posts (Any Author) & Non-Admin Media', 'my-pro-id' ); ?></h3>
                    <p><?php esc_html_e( 'This will permanently delete ALL posts, pages, and custom post types (REGARDLESS OF AUTHOR) and ALL media library items that were NOT authored by administrators.', 'my-pro-id' ); ?></p>
                    <form method="post" action="">
                        <?php wp_nonce_field( 'my_pro_id_delete_all_posts_non_admin_media_action' ); ?>
                        <p>
                            <label for="confirmation_password_2">
                                <strong><?php esc_html_e( 'Enter Password to Confirm:', 'my-pro-id' ); ?></strong>
                            </label><br />
                            <input type="password" name="confirmation_password_2" id="confirmation_password_2" required style="border-color: red;" />
                        </p>
                        <button type="submit" name="my_pro_id_delete_all_posts_non_admin_media" class="button button-danger" style="background-color:red; color:#fff; border-color: darkred;" onclick="return confirm('<?php esc_attr_e( 'ARE YOU ABSOLUTELY SURE? This will delete ALL posts (including admin posts) and all NON-ADMIN media permanently. There is NO UNDO. Click OK only if you have a backup and understand the consequences.', 'my-pro-id' ); ?>');">
                            <?php esc_html_e( 'DELETE ALL POSTS & NON-ADMIN MEDIA NOW', 'my-pro-id' ); ?>
                        </button>
                    </form>
                </div>
            </div>

            <hr style="margin-top: 30px;" />

            <h2><?php esc_html_e( 'Activity Logs', 'my-pro-id' ); ?></h2>
             <p><?php esc_html_e( 'Showing the most recent 20 entries for each log type.', 'my-pro-id' ); ?></p>

            <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
                <?php
                // Helper function to display logs
                function display_log_section($title, $log_option, $empty_message) {
                    echo '<div style="border: 1px solid #eee; padding: 10px; border-radius: 4px;">';
                    echo '<h3>' . esc_html($title) . '</h3>';
                    $logs = get_option( $log_option, array() );
                    if ( ! empty( $logs ) ) {
                        $logs = array_reverse( $logs ); // Newest first
                        $logs = array_slice( $logs, 0, 20 );

                        echo '<ul style="max-height: 200px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; list-style: disc; margin-left: 20px; font-size: 0.9em;">';
                        foreach ( $logs as $entry ) {
                            echo '<li>' . esc_html( $entry ) . '</li>';
                        }
                        echo '</ul>';
                    } else {
                        echo '<p>' . esc_html( $empty_message ) . '</p>';
                    }
                     echo '</div>';
                }

                // Display existing and new logs
                display_log_section(__( 'Deleted Images Log', 'my-pro-id' ), $this->log_option_name, __( 'No images have been deleted yet via this tool.', 'my-pro-id' ));
                display_log_section(__( 'Deleted My Pro ID Posts (Empty Image) Log', 'my-pro-id' ), $this->post_log_option, __( 'No empty (image) my-pro-id posts have been deleted yet.', 'my-pro-id' ));
                display_log_section(__( 'Deleted My Pro ID Posts (No Phone) Log', 'my-pro-id' ), $this->no_phone_post_log_option, __( 'No my-pro-id posts without phone numbers have been deleted yet.', 'my-pro-id' )); // NEW
                display_log_section(__( 'Deleted Normal Posts (Non-Admin) Log', 'my-pro-id' ), $this->normal_post_log_option, __( 'No normal posts (non-admin) have been deleted via this tool yet.', 'my-pro-id' ));
                display_log_section(__( 'Deleted ALL Normal Posts Log', 'my-pro-id' ), $this->all_normal_post_log_option, __( 'No normal posts (including admin) have been deleted via this tool yet.', 'my-pro-id' )); // NEW
                display_log_section(__( 'Bulk Delete: All Posts & Non-Admin Media Log', 'my-pro-id' ), $this->all_posts_non_admin_media_log_option, __( 'The "Delete All Posts & Non-Admin Media" action has not been run yet.', 'my-pro-id' )); // NEW
                // Note: No specific log for "Delete All Except Admin" shown here, assumes it's too broad or covered by other logs implicitly. Add if needed.

                ?>
            </div>
        </div><?php
    }

    /**
     * Find and delete empty my-pro-id posts up to a limit.
     * This version processes posts in batches within the function call until the limit is reached.
     *
     * @param int $max_delete Maximum total number of posts to delete in this run.
     * @return int The number of posts actually deleted.
     */
    private function delete_empty_my_pro_id_posts( $max_delete ) {
        $deleted_count = 0;
        $processed_ids = array(); // Keep track of posts checked in this run to avoid infinite loops if query is slow

        $batch_size = 500; // Process in batches of 500 to avoid memory issues

        while ($deleted_count < $max_delete) {
            $remaining_to_find = $max_delete - $deleted_count;
            $limit_for_query = min($batch_size, $remaining_to_find);

            $empty_post_ids_batch = $this->get_empty_my_pro_id_posts( $limit_for_query, $processed_ids );

            if ( empty( $empty_post_ids_batch ) ) {
                break; // No more empty posts found in this iteration
            }

            $deleted_in_batch = 0;
            $ids_to_log = array();
            foreach ( $empty_post_ids_batch as $pid ) {
                if ($deleted_count >= $max_delete) break; // Stop if we reached the overall limit

                $delete_result = wp_delete_post( $pid, true ); // true = force delete
                if ( $delete_result !== false && $delete_result !== null ) {
                     $deleted_count++;
                     $deleted_in_batch++;
                     $ids_to_log[] = $pid;
                     $processed_ids[] = $pid; // Add to processed list
                } else {
                     // Deletion failed, maybe log this?
                     $processed_ids[] = $pid; // Add to processed even if failed to avoid retrying immediately
                }
            }

            // Log the posts deleted in this batch
            if ( !empty($ids_to_log) ) {
                $this->append_log( $ids_to_log, $this->post_log_option, '%s - Deleted empty (image) my-pro-id post #%d' );
            }

            // Safety break if no posts were deleted in a batch where posts were found
             if ($deleted_in_batch == 0 && !empty($empty_post_ids_batch)) {
                 // This might indicate an issue with deletion permissions or the posts themselves
                 break;
             }

        } // end while loop

        return $deleted_count;
    }


    /**
     * Return up to $limit my-pro-id post IDs that have NO associated images in specified fields.
     *
     * @param int   $limit Max number of IDs to return.
     * @param array $exclude_ids Post IDs to exclude from the search (already processed).
     * @return array Array of post IDs.
     */
    private function get_empty_my_pro_id_posts( $limit, $exclude_ids = array() ) {
         global $wpdb;

         // Metafields where images might be stored (ensure this list is accurate)
         $meta_keys = array(
             'kid_image', 'kid_gallery', 'avatar1', 'cover-image', 'logocongty',
             'gioi_thieu_cover', 'gallery', 'line_qr', 'viber_qr', 'zalo_qr_code',
             'wechat_qr', 'bank-qr-1', 'productsimage', '505_profile_image',
             '_thumbnail_id' // Also check the standard featured image meta key
         );

         // 1. Query all relevant my-pro-id post IDs, excluding those already processed
         $query_args = array(
             'post_type'      => 'my-pro-id',
             'post_status'    => 'any',
             'posts_per_page' => -1, // Check all initially
             'fields'         => 'ids',
             'orderby'        => 'ID',
             'order'          => 'ASC', // Process older posts first
         );
         if (!empty($exclude_ids)) {
             $query_args['post__not_in'] = $exclude_ids;
         }
         $all_relevant_posts = get_posts( $query_args );

         if ( empty( $all_relevant_posts ) ) {
             return array(); // No more posts to check
         }

         // 2. Find posts that HAVE any value in the specified meta keys OR a featured image
         $posts_with_potential_images = array();

         // Chunk the post IDs to avoid massive SQL queries if there are many posts
         $post_chunks = array_chunk($all_relevant_posts, 500);
         $placeholders_meta_keys = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) );

         foreach ($post_chunks as $post_chunk) {
             $placeholders_for_posts = implode( ', ', array_fill( 0, count( $post_chunk ), '%d' ) );
             $sql = $wpdb->prepare("
                 SELECT DISTINCT post_id
                 FROM {$wpdb->postmeta}
                 WHERE meta_key IN ($placeholders_meta_keys)
                   AND post_id IN ($placeholders_for_posts)
                   AND meta_value IS NOT NULL AND meta_value != '' AND meta_value != 'a:0:{}' -- Check for empty serialized arrays too
             ", array_merge( $meta_keys, $post_chunk ));

             $posts_with_meta = $wpdb->get_col( $sql );
             if (!empty($posts_with_meta)) {
                 $posts_with_potential_images = array_merge($posts_with_potential_images, $posts_with_meta);
             }
         }

         // Convert to integers and ensure uniqueness
         $posts_with_potential_images = array_map( 'intval', $posts_with_potential_images );
         $posts_with_potential_images = array_unique($posts_with_potential_images);


         // 3. "Empty" posts are those in $all_relevant_posts but NOT in $posts_with_potential_images
         $empty_posts = array_diff( $all_relevant_posts, $posts_with_potential_images );

         // 4. Return only up to the specified limit
         $empty_posts = array_slice( $empty_posts, 0, $limit );

         return $empty_posts;
    }

    /**
     * NEW: Find and delete 'my-pro-id' posts without a 'phone' meta value.
     *
     * @param int $max_delete Maximum total number of posts to delete in this run.
     * @return int The number of posts actually deleted.
     */
    private function delete_my_pro_id_posts_without_phone( $max_delete ) {
        $deleted_count = 0;
        $processed_ids = array();
        $batch_size = 500;

        while ($deleted_count < $max_delete) {
            $remaining_to_find = $max_delete - $deleted_count;
            $limit_for_query = min($batch_size, $remaining_to_find);

            $no_phone_post_ids_batch = $this->get_my_pro_id_posts_without_phone( $limit_for_query, $processed_ids );

            if ( empty( $no_phone_post_ids_batch ) ) {
                break; // No more posts without phone found
            }

            $deleted_in_batch = 0;
            $ids_to_log = array();
            foreach ( $no_phone_post_ids_batch as $pid ) {
                if ($deleted_count >= $max_delete) break;

                $delete_result = wp_delete_post( $pid, true ); // force delete
                if ( $delete_result !== false && $delete_result !== null ) {
                     $deleted_count++;
                     $deleted_in_batch++;
                     $ids_to_log[] = $pid;
                     $processed_ids[] = $pid;
                } else {
                     $processed_ids[] = $pid; // Add to processed even if failed
                }
            }

            // Log the deleted posts
            if ( !empty($ids_to_log) ) {
                $this->append_log( $ids_to_log, $this->no_phone_post_log_option, '%s - Deleted my-pro-id post (no phone) #%d' );
            }

             if ($deleted_in_batch == 0 && !empty($no_phone_post_ids_batch)) {
                 break; // Safety break
             }
        } // end while

        return $deleted_count;
    }

    /**
     * NEW: Return up to $limit my-pro-id post IDs that do NOT have a 'phone' meta field or where it's empty.
     *
     * @param int   $limit Max number of IDs to return.
     * @param array $exclude_ids Post IDs to exclude from the search.
     * @return array Array of post IDs.
     */
    private function get_my_pro_id_posts_without_phone( $limit, $exclude_ids = array() ) {
        global $wpdb;
        $phone_meta_key = 'phone'; // The meta key for the phone number

        // 1. Query all relevant my-pro-id post IDs, excluding processed ones
        $query_args = array(
            'post_type'      => 'my-pro-id',
            'post_status'    => 'any',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'orderby'        => 'ID',
            'order'          => 'ASC',
        );
        if (!empty($exclude_ids)) {
            $query_args['post__not_in'] = $exclude_ids;
        }
        $all_relevant_posts = get_posts( $query_args );

        if ( empty( $all_relevant_posts ) ) {
            return array();
        }

        // 2. Find posts that HAVE a non-empty 'phone' meta value
        $posts_with_phone = array();
        $post_chunks = array_chunk($all_relevant_posts, 500);

        foreach ($post_chunks as $post_chunk) {
            $placeholders_for_posts = implode( ', ', array_fill( 0, count( $post_chunk ), '%d' ) );
            $sql = $wpdb->prepare("
                SELECT DISTINCT post_id
                FROM {$wpdb->postmeta}
                WHERE meta_key = %s
                  AND post_id IN ($placeholders_for_posts)
                  AND meta_value IS NOT NULL AND meta_value != ''
            ", array_merge( array($phone_meta_key), $post_chunk ));

            $posts_found = $wpdb->get_col( $sql );
            if (!empty($posts_found)) {
                $posts_with_phone = array_merge($posts_with_phone, $posts_found);
            }
        }

        $posts_with_phone = array_map( 'intval', $posts_with_phone );
        $posts_with_phone = array_unique($posts_with_phone);

        // 3. Posts without phone are those in $all_relevant_posts but NOT in $posts_with_phone
        $posts_without_phone = array_diff( $all_relevant_posts, $posts_with_phone );

        // 4. Return up to the limit
        $posts_without_phone = array_slice( $posts_without_phone, 0, $limit );

        return $posts_without_phone;
    }


     /**
      * Delete standard WordPress posts ('post' type) in a batch, excluding admin posts.
      *
      * @param int $batch_size The maximum number of posts to delete in this batch.
      * @return int The number of posts actually deleted in this batch.
      */
     private function delete_normal_posts_in_batch( $batch_size ) {
         // 1. Get admin user IDs to exclude their posts
         $admin_users = get_users( array( 'role__in' => ['administrator'], 'fields' => 'ID' ) ); // More specific role query
         $admin_ids = !empty($admin_users) ? $admin_users : array(1); // Default to exclude user 1 if no admins found (edge case)

         // 2. Query for posts to delete
         $query_args = array(
             'post_type'        => 'post',              // Target only standard posts
             'post_status'      => 'any',               // Consider all statuses (publish, draft, trash, etc.)
             'posts_per_page'   => $batch_size,         // Limit to the batch size
             'post_author__not_in' => $admin_ids,       // Exclude posts by admins
             'fields'           => 'ids',               // Only get post IDs for efficiency
             'orderby'          => 'ID',                // Order consistently
             'order'            => 'ASC'                // Delete older posts first
         );
         $posts_to_delete_ids = get_posts( $query_args );

         if ( empty( $posts_to_delete_ids ) ) {
             return 0; // No posts found to delete in this batch
         }

         // 3. Delete the posts
         $deleted_count = 0;
         $deleted_ids_log = array();
         foreach ( $posts_to_delete_ids as $post_id ) {
             $delete_result = wp_delete_post( $post_id, true ); // true = force delete, bypass trash
             if ( $delete_result !== false && $delete_result !== null ) { // Check if deletion was successful
                 $deleted_count++;
                 $deleted_ids_log[] = $post_id;
             }
             // Optional: Log failures?
         }

         // 4. Log the deleted posts (optional but recommended)
         if ( $deleted_count > 0 ) {
             $this->append_log( $deleted_ids_log, $this->normal_post_log_option, '%s - Deleted normal post (non-admin) #%d' );
         }

         return $deleted_count;
     }

     /**
      * NEW: Delete ALL standard WordPress posts ('post' type) in a batch, including admin posts.
      *
      * @param int $batch_size The maximum number of posts to delete in this batch.
      * @return int The number of posts actually deleted in this batch.
      */
     private function delete_all_normal_posts_in_batch( $batch_size ) {
         // 1. Query for posts to delete (no author exclusion)
         $query_args = array(
             'post_type'        => 'post',              // Target only standard posts
             'post_status'      => 'any',               // Consider all statuses
             'posts_per_page'   => $batch_size,         // Limit to the batch size
             // 'post_author__not_in' => $admin_ids,    // NO exclusion for this function
             'fields'           => 'ids',               // Only get post IDs
             'orderby'          => 'ID',                // Order consistently
             'order'            => 'ASC'                // Delete older posts first
         );
         $posts_to_delete_ids = get_posts( $query_args );

         if ( empty( $posts_to_delete_ids ) ) {
             return 0; // No posts found to delete
         }

         // 2. Delete the posts
         $deleted_count = 0;
         $deleted_ids_log = array();
         foreach ( $posts_to_delete_ids as $post_id ) {
             $delete_result = wp_delete_post( $post_id, true ); // force delete
             if ( $delete_result !== false && $delete_result !== null ) {
                 $deleted_count++;
                 $deleted_ids_log[] = $post_id;
             }
         }

         // 3. Log the deleted posts
         if ( $deleted_count > 0 ) {
             $this->append_log( $deleted_ids_log, $this->all_normal_post_log_option, '%s - Deleted normal post (all authors) #%d' );
         }

         return $deleted_count;
     }


    /**
     * Delete all posts and media *except* those authored by admins.
     * Returns the total number of items deleted.
     * USE WITH EXTREME CAUTION.
     */
    private function delete_all_posts_and_media_except_admin() {
        // 1. Get admin user IDs
         $admin_users = get_users( array( 'role__in' => ['administrator'], 'fields' => 'ID' ) );
         $admin_ids = !empty($admin_users) ? $admin_users : array(1); // Default exclude user 1


        // 2. Query posts/pages/custom types (excluding attachments for now) not by admins
        $post_types = get_post_types( array('public' => true, 'show_ui' => true), 'names' ); // Get CPTs too
         unset($post_types['attachment']); // Handle attachments separately

        $deleted_count = 0;
        $batch_size = 200; // Process in batches

        foreach ($post_types as $pt) {
            while(true) {
                 $args = array(
                    'post_type'        => $pt,
                    'post_status'      => 'any',
                    'post_author__not_in' => $admin_ids, // Exclude admin posts
                    'posts_per_page'   => $batch_size,
                    // 'offset'           => $offset, // Offset not needed when deleting
                    'fields'           => 'ids',
                    'orderby'          => 'ID',
                    'order'            => 'ASC',
                    'suppress_filters' => true, // Improve performance slightly
                    'ignore_sticky_posts' => true,
                );
                $posts_in_batch = get_posts($args);

                if (empty($posts_in_batch)) {
                    break; // No more posts of this type found
                }

                $batch_deleted_ids = [];
                foreach ($posts_in_batch as $post_id) {
                     $delete_result = wp_delete_post( $post_id, true ); // Force delete
                     if ($delete_result) {
                         $deleted_count++;
                         $batch_deleted_ids[] = $post_id;
                     }
                 }
                 // Optional: Log batch deletions here if needed, but this function doesn't have its own log by default

                 // If fewer posts were found than the batch size, we're done with this post type
                 if (count($posts_in_batch) < $batch_size) {
                     break;
                 }
                 // Add a small delay to prevent server overload (optional)
                 // sleep(1);
             } // end while loop for batches
        } // end foreach post_type


        // 3. Query and delete attachments not authored by admins
         while(true) {
             $args = array(
                 'post_type'        => 'attachment',
                 'post_status'      => 'any', // Usually 'inherit' but 'any' is safer
                 'post_author__not_in' => $admin_ids, // Exclude admin media
                 'posts_per_page'   => $batch_size,
                 // 'offset'           => $offset, // Offset not needed
                 'fields'           => 'ids',
                 'orderby'          => 'ID',
                 'order'            => 'ASC',
                 'suppress_filters' => true,
             );
             $attachments_in_batch = get_posts($args);

             if (empty($attachments_in_batch)) {
                 break; // No more attachments found
             }

             $batch_deleted_ids = [];
             foreach ($attachments_in_batch as $att_id) {
                 $delete_result = wp_delete_attachment( $att_id, true ); // Force delete attachment
                 if ($delete_result) {
                     $deleted_count++;
                     $batch_deleted_ids[] = $att_id;
                 }
             }
             // Optional: Log batch deletions here

              if (count($attachments_in_batch) < $batch_size) {
                 break;
             }
             // sleep(1); // Optional delay
         }


        return $deleted_count;
    }

    /**
     * NEW: Delete ALL posts (any author) and all media *except* media authored by admins.
     * Returns the total number of items deleted.
     * USE WITH EXTREME CAUTION.
     */
    private function delete_all_posts_and_non_admin_media() {
        // 1. Get admin user IDs (only needed for media exclusion)
         $admin_users = get_users( array( 'role__in' => ['administrator'], 'fields' => 'ID' ) );
         $admin_ids = !empty($admin_users) ? $admin_users : array(1); // Default exclude user 1 media


        // 2. Query ALL posts/pages/custom types (excluding attachments) REGARDLESS OF AUTHOR
        $post_types = get_post_types( array('public' => true, 'show_ui' => true), 'names' );
        unset($post_types['attachment']); // Handle attachments separately

        $deleted_count = 0;
        $batch_size = 200; // Process in batches
        $log_items = []; // Collect items for logging

        foreach ($post_types as $pt) {
            while(true) {
                 $args = array(
                    'post_type'        => $pt,
                    'post_status'      => 'any',
                    // 'post_author__not_in' => $admin_ids, // NO author exclusion for posts
                    'posts_per_page'   => $batch_size,
                    'fields'           => 'ids',
                    'orderby'          => 'ID',
                    'order'            => 'ASC',
                    'suppress_filters' => true,
                    'ignore_sticky_posts' => true,
                );
                $posts_in_batch = get_posts($args);

                if (empty($posts_in_batch)) {
                    break; // No more posts of this type
                }

                foreach ($posts_in_batch as $post_id) {
                     $delete_result = wp_delete_post( $post_id, true ); // Force delete
                     if ($delete_result) {
                         $deleted_count++;
                         $log_items[] = ['type' => $pt, 'id' => $post_id];
                     }
                 }

                 if (count($posts_in_batch) < $batch_size) {
                     break;
                 }
                 // sleep(1); // Optional delay
             } // end while loop for batches
        } // end foreach post_type


        // 3. Query and delete attachments NOT authored by admins
         while(true) {
             $args = array(
                 'post_type'        => 'attachment',
                 'post_status'      => 'any',
                 'post_author__not_in' => $admin_ids, // Exclude admin media
                 'posts_per_page'   => $batch_size,
                 'fields'           => 'ids',
                 'orderby'          => 'ID',
                 'order'            => 'ASC',
                 'suppress_filters' => true,
             );
             $attachments_in_batch = get_posts($args);

             if (empty($attachments_in_batch)) {
                 break; // No more non-admin attachments
             }

             foreach ($attachments_in_batch as $att_id) {
                 $delete_result = wp_delete_attachment( $att_id, true ); // Force delete attachment
                 if ($delete_result) {
                     $deleted_count++;
                      $log_items[] = ['type' => 'attachment', 'id' => $att_id];
                 }
             }

              if (count($attachments_in_batch) < $batch_size) {
                 break;
             }
             // sleep(1); // Optional delay
         }

         // 4. Log the bulk deletion action
         if (!empty($log_items)) {
             $log_entry = sprintf('%s - Deleted %d items (All Posts & Non-Admin Media). Sample IDs: %s...',
                current_time('mysql'),
                $deleted_count,
                implode(', ', array_slice(array_map(function($item){ return $item['type'].':'.$item['id']; }, $log_items), 0, 5)) // Log first 5 deleted items
             );
             $this->append_log( [$log_entry], $this->all_posts_non_admin_media_log_option, '%s' ); // Use generic log format
         }


        return $deleted_count;
    }


} // End class My_Pro_ID_Image_Cleanup

// Initialize the plugin
My_Pro_ID_Image_Cleanup::init();

?>

Current_dir [ WRITEABLE ] Document_root [ WRITEABLE ]


[ Back ]
NAME
SIZE
LAST TOUCH
USER
CAN-I?
FUNCTIONS
..
--
19 Mar 2026 10.15 AM
bqrcodec / nobody
0755
clean-media.txt
61.697 KB
19 Mar 2026 8.44 AM
bqrcodec / bqrcodec
0644

GRAYBYTE WORDPRESS FILE MANAGER @ 2026 CONTACT ME
Static GIF