' . sprintf(
__( 'My Pro ID post (empty image) cleanup complete. %d posts were deleted. Check logs below.', 'my-pro-id' ),
$deleted_count
) . '
';
} else {
$message .= '
' . __( 'No empty (image) My Pro ID posts found to delete.', 'my-pro-id' ) . '
';
}
}
//--- 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 .= '
' . 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
) . '
';
} else {
$message .= '
' . __( 'No more non-admin normal posts found to delete in this batch.', 'my-pro-id' ) . '
';
}
}
// --- 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 .= '
' . 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
) . '
';
} else {
$message .= '
' . __( 'No more normal posts found to delete in this batch.', 'my-pro-id' ) . '
';
}
}
// --- 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 .= '
' . 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
) . '
';
} else {
$message .= '
' . __( 'No My Pro ID posts without a phone number found to delete in this batch.', 'my-pro-id' ) . '
';
}
}
//--- 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 .= '
' . __('Error:', 'my-pro-id') . ' ' . __('Incorrect password for "Delete All Except Admin". Deletion aborted.', 'my-pro-id') . '
';
}
// 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.
?>
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();
?>