feat: initial release of WooCleanup inactive account manager
- Daily WP-Cron scans all customer-role accounts - Flags users with no completed/processing order in configurable period (default 18 months) - Sends configurable HTML notice email with placeholder support - Deletes account after configurable grace period (default 7 days) - Admin UI under WooCommerce menu: flagged account list with tabs, manual run/send/delete actions, countdown to deletion - Settings page: inactivity period, grace period, email subject/body - HPOS compatible; never touches admin accounts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
592
includes/class-admin.php
Normal file
592
includes/class-admin.php
Normal file
@@ -0,0 +1,592 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCleanup Admin UI
|
||||
*
|
||||
* Adds two pages under the WooCommerce menu:
|
||||
* 1. Account Cleanup – table of flagged accounts with action buttons.
|
||||
* 2. Cleanup Settings – configurable options.
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooCleanup_Admin {
|
||||
|
||||
/** @var WooCleanup */
|
||||
private WooCleanup $plugin;
|
||||
|
||||
public function __construct( WooCleanup $plugin ) {
|
||||
$this->plugin = $plugin;
|
||||
|
||||
add_action( 'admin_menu', [ $this, 'add_menu' ] );
|
||||
add_action( 'admin_post_woo_cleanup_save_settings', [ $this, 'save_settings' ] );
|
||||
add_action( 'admin_post_woo_cleanup_manual_run', [ $this, 'handle_manual_run' ] );
|
||||
add_action( 'admin_post_woo_cleanup_send_notice', [ $this, 'handle_send_notice' ] );
|
||||
add_action( 'admin_post_woo_cleanup_delete_user', [ $this, 'handle_delete_user' ] );
|
||||
add_action( 'admin_notices', [ $this, 'show_admin_notices' ] );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Menu Registration
|
||||
// =========================================================================
|
||||
|
||||
public function add_menu(): void {
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__( 'Account Cleanup', 'woo-cleanup' ),
|
||||
__( 'Account Cleanup', 'woo-cleanup' ),
|
||||
'manage_woocommerce',
|
||||
'woo-cleanup',
|
||||
[ $this, 'render_accounts_page' ]
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__( 'Cleanup Settings', 'woo-cleanup' ),
|
||||
__( 'Cleanup Settings', 'woo-cleanup' ),
|
||||
'manage_woocommerce',
|
||||
'woo-cleanup-settings',
|
||||
[ $this, 'render_settings_page' ]
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Accounts List Page
|
||||
// =========================================================================
|
||||
|
||||
public function render_accounts_page(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to view this page.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$options = $this->plugin->get_options();
|
||||
$grace_days = absint( $options['grace_days'] );
|
||||
$active_tab = isset( $_GET['tab'] ) && $_GET['tab'] === 'notice_sent'
|
||||
? 'notice_sent'
|
||||
: 'pending_notice';
|
||||
|
||||
$pending_users = WooCleanup::get_flagged_users( 'pending_notice' );
|
||||
$noticed_users = WooCleanup::get_flagged_users( 'notice_sent' );
|
||||
$last_run = get_option( 'woo_cleanup_last_run' );
|
||||
|
||||
// Cron next run
|
||||
$next_run = wp_next_scheduled( WooCleanup::CRON_HOOK );
|
||||
|
||||
$users_to_show = $active_tab === 'notice_sent' ? $noticed_users : $pending_users;
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'WooCleanup – Inactive Account Manager', 'woo-cleanup' ); ?></h1>
|
||||
|
||||
<?php $this->output_admin_styles(); ?>
|
||||
|
||||
<!-- Summary bar -->
|
||||
<div class="woo-cleanup-summary">
|
||||
<div class="woo-cleanup-stat">
|
||||
<span class="count"><?php echo count( $pending_users ); ?></span>
|
||||
<span class="label"><?php esc_html_e( 'Awaiting Notice', 'woo-cleanup' ); ?></span>
|
||||
</div>
|
||||
<div class="woo-cleanup-stat woo-cleanup-stat--warning">
|
||||
<span class="count"><?php echo count( $noticed_users ); ?></span>
|
||||
<span class="label"><?php esc_html_e( 'In Grace Period', 'woo-cleanup' ); ?></span>
|
||||
</div>
|
||||
<div class="woo-cleanup-meta">
|
||||
<?php if ( $last_run ) : ?>
|
||||
<span><?php
|
||||
printf(
|
||||
/* translators: %s: human-readable time difference */
|
||||
esc_html__( 'Last run: %s ago', 'woo-cleanup' ),
|
||||
esc_html( human_time_diff( $last_run ) )
|
||||
);
|
||||
?></span>
|
||||
<?php else : ?>
|
||||
<span><?php esc_html_e( 'Cron has not run yet.', 'woo-cleanup' ); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ( $next_run ) : ?>
|
||||
| <span><?php
|
||||
printf(
|
||||
esc_html__( 'Next run: in %s', 'woo-cleanup' ),
|
||||
esc_html( human_time_diff( $next_run ) )
|
||||
);
|
||||
?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<!-- Manual run button -->
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
|
||||
style="margin-left:auto">
|
||||
<?php wp_nonce_field( 'woo_cleanup_manual_run', '_wpnonce' ); ?>
|
||||
<input type="hidden" name="action" value="woo_cleanup_manual_run">
|
||||
<button type="submit" class="button button-secondary">
|
||||
▶ <?php esc_html_e( 'Run Cleanup Now', 'woo-cleanup' ); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ( empty( $options['enabled'] ) ) : ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p>
|
||||
<?php esc_html_e( 'WooCleanup is currently disabled. Enable it on the ', 'woo-cleanup' ); ?>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup-settings' ) ); ?>">
|
||||
<?php esc_html_e( 'Settings page', 'woo-cleanup' ); ?>
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Tabs -->
|
||||
<h2 class="nav-tab-wrapper" style="margin-top:16px">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup&tab=pending_notice' ) ); ?>"
|
||||
class="nav-tab <?php echo $active_tab === 'pending_notice' ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'Awaiting Notice', 'woo-cleanup' ); ?>
|
||||
<span class="woo-cleanup-badge"><?php echo count( $pending_users ); ?></span>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup&tab=notice_sent' ) ); ?>"
|
||||
class="nav-tab <?php echo $active_tab === 'notice_sent' ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'Grace Period (Pending Deletion)', 'woo-cleanup' ); ?>
|
||||
<span class="woo-cleanup-badge woo-cleanup-badge--warning"><?php echo count( $noticed_users ); ?></span>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if ( empty( $users_to_show ) ) : ?>
|
||||
<div class="woo-cleanup-empty">
|
||||
<?php if ( ! $last_run ) : ?>
|
||||
<p><?php esc_html_e( 'No flagged accounts yet. The daily cron has not run. Click "Run Cleanup Now" to scan immediately.', 'woo-cleanup' ); ?></p>
|
||||
<?php else : ?>
|
||||
<p><?php esc_html_e( 'No accounts in this category.', 'woo-cleanup' ); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
|
||||
<table class="wp-list-table widefat fixed striped woo-cleanup-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'User', 'woo-cleanup' ); ?></th>
|
||||
<th><?php esc_html_e( 'Email', 'woo-cleanup' ); ?></th>
|
||||
<th><?php esc_html_e( 'Registered', 'woo-cleanup' ); ?></th>
|
||||
<th><?php esc_html_e( 'Last Order', 'woo-cleanup' ); ?></th>
|
||||
<?php if ( $active_tab === 'pending_notice' ) : ?>
|
||||
<th><?php esc_html_e( 'Inactive Since', 'woo-cleanup' ); ?></th>
|
||||
<?php else : ?>
|
||||
<th><?php esc_html_e( 'Notice Sent', 'woo-cleanup' ); ?></th>
|
||||
<th><?php esc_html_e( 'Deletes On', 'woo-cleanup' ); ?></th>
|
||||
<th><?php esc_html_e( 'Days Left', 'woo-cleanup' ); ?></th>
|
||||
<?php endif; ?>
|
||||
<th><?php esc_html_e( 'Actions', 'woo-cleanup' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $users_to_show as $user ) :
|
||||
$user_id = (int) $user->ID;
|
||||
$inactive_since = (int) get_user_meta( $user_id, WooCleanup::META_INACTIVE_SINCE, true );
|
||||
$notice_ts = (int) get_user_meta( $user_id, WooCleanup::META_NOTICE_SENT, true );
|
||||
$delete_ts = $notice_ts + ( $grace_days * DAY_IN_SECONDS );
|
||||
$days_left = max( 0, (int) ceil( ( $delete_ts - time() ) / DAY_IN_SECONDS ) );
|
||||
$last_order = WooCleanup::get_last_order_date( $user_id );
|
||||
$edit_url = get_edit_user_link( $user_id );
|
||||
$row_class = ( $active_tab === 'notice_sent' && $days_left <= 2 ) ? 'woo-cleanup-row--urgent' : '';
|
||||
?>
|
||||
<tr class="<?php echo esc_attr( $row_class ); ?>">
|
||||
<td>
|
||||
<a href="<?php echo esc_url( $edit_url ); ?>" target="_blank">
|
||||
<?php echo esc_html( $user->display_name ); ?>
|
||||
</a>
|
||||
<br><small style="color:#888">#<?php echo $user_id; ?></small>
|
||||
</td>
|
||||
<td><?php echo esc_html( $user->user_email ); ?></td>
|
||||
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $user->user_registered ) ) ); ?></td>
|
||||
<td><?php echo $last_order ? esc_html( $last_order ) : '<em style="color:#aaa">' . esc_html__( 'Never', 'woo-cleanup' ) . '</em>'; ?></td>
|
||||
|
||||
<?php if ( $active_tab === 'pending_notice' ) : ?>
|
||||
<td><?php echo $inactive_since ? esc_html( date_i18n( get_option( 'date_format' ), $inactive_since ) ) : '—'; ?></td>
|
||||
<?php else : ?>
|
||||
<td><?php echo $notice_ts ? esc_html( date_i18n( get_option( 'date_format' ), $notice_ts ) ) : '—'; ?></td>
|
||||
<td><?php echo $notice_ts ? esc_html( date_i18n( get_option( 'date_format' ), $delete_ts ) ) : '—'; ?></td>
|
||||
<td>
|
||||
<?php if ( $days_left <= 1 ) : ?>
|
||||
<span class="woo-cleanup-days-urgent"><?php echo $days_left === 0 ? esc_html__( 'Today', 'woo-cleanup' ) : esc_html( $days_left . ' ' . __( 'day', 'woo-cleanup' ) ); ?></span>
|
||||
<?php else : ?>
|
||||
<?php echo esc_html( $days_left . ' ' . __( 'days', 'woo-cleanup' ) ); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
|
||||
<td class="woo-cleanup-actions">
|
||||
<?php if ( $active_tab === 'pending_notice' ) : ?>
|
||||
<!-- Send notice now -->
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline">
|
||||
<?php wp_nonce_field( 'woo_cleanup_send_notice_' . $user_id, '_wpnonce' ); ?>
|
||||
<input type="hidden" name="action" value="woo_cleanup_send_notice">
|
||||
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
|
||||
<button type="submit" class="button button-small">
|
||||
<?php esc_html_e( 'Send Notice', 'woo-cleanup' ); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Delete now -->
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
|
||||
style="display:inline"
|
||||
onsubmit="return confirm('<?php echo esc_js( sprintf( __( 'Permanently delete %s? This cannot be undone.', 'woo-cleanup' ), $user->display_name ) ); ?>')">
|
||||
<?php wp_nonce_field( 'woo_cleanup_delete_user_' . $user_id, '_wpnonce' ); ?>
|
||||
<input type="hidden" name="action" value="woo_cleanup_delete_user">
|
||||
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
|
||||
<button type="submit" class="button button-small woo-cleanup-btn-delete">
|
||||
<?php esc_html_e( 'Delete Now', 'woo-cleanup' ); ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- .wrap -->
|
||||
<?php
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Settings Page
|
||||
// =========================================================================
|
||||
|
||||
public function render_settings_page(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to view this page.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$options = $this->plugin->get_options();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'WooCleanup – Settings', 'woo-cleanup' ); ?></h1>
|
||||
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'woo_cleanup_save_settings', '_wpnonce' ); ?>
|
||||
<input type="hidden" name="action" value="woo_cleanup_save_settings">
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
|
||||
<!-- Enable / Disable -->
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Cleanup', 'woo-cleanup' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="woo_cleanup[enabled]" value="1"
|
||||
<?php checked( 1, (int) $options['enabled'] ); ?>>
|
||||
<?php esc_html_e( 'Run the daily cleanup cron', 'woo-cleanup' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Inactivity Period -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc_inactivity_months">
|
||||
<?php esc_html_e( 'Inactivity Period (months)', 'woo-cleanup' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="wc_inactivity_months"
|
||||
name="woo_cleanup[inactivity_months]"
|
||||
value="<?php echo esc_attr( $options['inactivity_months'] ); ?>"
|
||||
min="1" max="120" style="width:80px">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Customers with no completed/processing order in this many months will be flagged. Default: 18.', 'woo-cleanup' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Grace Period -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc_grace_days">
|
||||
<?php esc_html_e( 'Grace Period (days)', 'woo-cleanup' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="wc_grace_days"
|
||||
name="woo_cleanup[grace_days]"
|
||||
value="<?php echo esc_attr( $options['grace_days'] ); ?>"
|
||||
min="1" max="365" style="width:80px">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Days between the notice email and account deletion. Default: 7.', 'woo-cleanup' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Email Subject -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc_email_subject">
|
||||
<?php esc_html_e( 'Notice Email Subject', 'woo-cleanup' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wc_email_subject"
|
||||
name="woo_cleanup[email_subject]"
|
||||
value="<?php echo esc_attr( $options['email_subject'] ); ?>"
|
||||
class="large-text">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Available placeholders: {site_name}, {username}, {email}, {deletion_date}, {grace_days}', 'woo-cleanup' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc_email_body">
|
||||
<?php esc_html_e( 'Notice Email Body', 'woo-cleanup' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea id="wc_email_body"
|
||||
name="woo_cleanup[email_body]"
|
||||
rows="12" class="large-text"><?php echo esc_textarea( $options['email_body'] ); ?></textarea>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Plain text. Available placeholders: {site_name}, {username}, {email}, {deletion_date}, {grace_days}. HTML is supported.', 'woo-cleanup' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Preview Placeholders', 'woo-cleanup' ); ?></h2>
|
||||
<table class="widefat" style="max-width:500px">
|
||||
<thead><tr><th><?php esc_html_e( 'Placeholder', 'woo-cleanup' ); ?></th><th><?php esc_html_e( 'Replaced With', 'woo-cleanup' ); ?></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>{site_name}</code></td><td><?php echo esc_html( get_bloginfo( 'name' ) ); ?></td></tr>
|
||||
<tr><td><code>{username}</code></td><td><?php esc_html_e( "Customer's display name", 'woo-cleanup' ); ?></td></tr>
|
||||
<tr><td><code>{email}</code></td><td><?php esc_html_e( "Customer's email address", 'woo-cleanup' ); ?></td></tr>
|
||||
<tr><td><code>{deletion_date}</code></td><td><?php esc_html_e( 'Calculated deletion date', 'woo-cleanup' ); ?></td></tr>
|
||||
<tr><td><code>{grace_days}</code></td><td><?php echo esc_html( $options['grace_days'] ); ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:20px">
|
||||
<?php submit_button( __( 'Save Settings', 'woo-cleanup' ), 'primary', 'submit', false ); ?>
|
||||
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woo-cleanup' ) ); ?>" class="button button-secondary">
|
||||
<?php esc_html_e( 'View Flagged Accounts', 'woo-cleanup' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Form Handlers (admin-post.php targets)
|
||||
// =========================================================================
|
||||
|
||||
public function save_settings(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' )
|
||||
|| ! check_admin_referer( 'woo_cleanup_save_settings' ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$raw = isset( $_POST['woo_cleanup'] ) ? (array) $_POST['woo_cleanup'] : [];
|
||||
$options = [
|
||||
'enabled' => ! empty( $raw['enabled'] ) ? 1 : 0,
|
||||
'inactivity_months' => max( 1, absint( $raw['inactivity_months'] ?? 18 ) ),
|
||||
'grace_days' => max( 1, absint( $raw['grace_days'] ?? 7 ) ),
|
||||
'email_subject' => sanitize_text_field( $raw['email_subject'] ?? '' ),
|
||||
'email_body' => wp_kses_post( $raw['email_body'] ?? '' ),
|
||||
];
|
||||
|
||||
update_option( 'woo_cleanup_options', $options );
|
||||
|
||||
wp_safe_redirect( add_query_arg(
|
||||
[ 'page' => 'woo-cleanup-settings', 'message' => 'saved' ],
|
||||
admin_url( 'admin.php' )
|
||||
) );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_manual_run(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' )
|
||||
|| ! check_admin_referer( 'woo_cleanup_manual_run' ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$this->plugin->run_cleanup();
|
||||
|
||||
wp_safe_redirect( add_query_arg(
|
||||
[ 'page' => 'woo-cleanup', 'message' => 'ran' ],
|
||||
admin_url( 'admin.php' )
|
||||
) );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_send_notice(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$user_id = absint( $_POST['user_id'] ?? 0 );
|
||||
if ( ! $user_id || ! check_admin_referer( 'woo_cleanup_send_notice_' . $user_id ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$options = $this->plugin->get_options();
|
||||
$this->plugin->send_notice( $user_id, absint( $options['grace_days'] ), $options );
|
||||
update_user_meta( $user_id, WooCleanup::META_NOTICE_SENT, time() );
|
||||
|
||||
wp_safe_redirect( add_query_arg(
|
||||
[ 'page' => 'woo-cleanup', 'message' => 'noticed', 'uid' => $user_id ],
|
||||
admin_url( 'admin.php' )
|
||||
) );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_delete_user(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$user_id = absint( $_POST['user_id'] ?? 0 );
|
||||
if ( ! $user_id || ! check_admin_referer( 'woo_cleanup_delete_user_' . $user_id ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
// Double-check: never delete admins via this form.
|
||||
$user = get_userdata( $user_id );
|
||||
if ( $user && in_array( 'administrator', (array) $user->roles, true ) ) {
|
||||
wp_die( esc_html__( 'Cannot delete administrator accounts.', 'woo-cleanup' ) );
|
||||
}
|
||||
|
||||
$this->plugin->delete_customer( $user_id );
|
||||
|
||||
wp_safe_redirect( add_query_arg(
|
||||
[ 'page' => 'woo-cleanup', 'message' => 'deleted' ],
|
||||
admin_url( 'admin.php' )
|
||||
) );
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Admin Notices
|
||||
// =========================================================================
|
||||
|
||||
public function show_admin_notices(): void {
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || strpos( $screen->id, 'woo-cleanup' ) === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? '';
|
||||
$uid = absint( $_GET['uid'] ?? 0 );
|
||||
|
||||
switch ( $message ) {
|
||||
case 'saved':
|
||||
echo '<div class="notice notice-success is-dismissible"><p>'
|
||||
. esc_html__( 'Settings saved.', 'woo-cleanup' )
|
||||
. '</p></div>';
|
||||
break;
|
||||
case 'ran':
|
||||
echo '<div class="notice notice-success is-dismissible"><p>'
|
||||
. esc_html__( 'Cleanup routine completed. The account list has been updated.', 'woo-cleanup' )
|
||||
. '</p></div>';
|
||||
break;
|
||||
case 'noticed':
|
||||
$user = $uid ? get_userdata( $uid ) : null;
|
||||
echo '<div class="notice notice-success is-dismissible"><p>'
|
||||
. esc_html( sprintf(
|
||||
__( 'Notice email sent to %s.', 'woo-cleanup' ),
|
||||
$user ? $user->display_name : "user #{$uid}"
|
||||
) )
|
||||
. '</p></div>';
|
||||
break;
|
||||
case 'deleted':
|
||||
echo '<div class="notice notice-success is-dismissible"><p>'
|
||||
. esc_html__( 'Account deleted.', 'woo-cleanup' )
|
||||
. '</p></div>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Inline Styles (small footprint; avoids an extra HTTP request)
|
||||
// =========================================================================
|
||||
|
||||
private function output_admin_styles(): void {
|
||||
?>
|
||||
<style>
|
||||
.woo-cleanup-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e4e7;
|
||||
border-radius: 4px;
|
||||
padding: 16px 20px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.woo-cleanup-stat {
|
||||
text-align: center;
|
||||
}
|
||||
.woo-cleanup-stat .count {
|
||||
display: block;
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #2271b1;
|
||||
}
|
||||
.woo-cleanup-stat--warning .count {
|
||||
color: #d63638;
|
||||
}
|
||||
.woo-cleanup-stat .label {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: #646970;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.woo-cleanup-meta {
|
||||
font-size: 0.85em;
|
||||
color: #646970;
|
||||
}
|
||||
.woo-cleanup-badge {
|
||||
display: inline-block;
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 1px 7px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.woo-cleanup-badge--warning {
|
||||
background: #d63638;
|
||||
}
|
||||
.woo-cleanup-table th,
|
||||
.woo-cleanup-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.woo-cleanup-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.woo-cleanup-btn-delete {
|
||||
color: #d63638 !important;
|
||||
border-color: #d63638 !important;
|
||||
}
|
||||
.woo-cleanup-btn-delete:hover {
|
||||
background: #d63638 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.woo-cleanup-row--urgent td {
|
||||
background-color: #fff5f5 !important;
|
||||
}
|
||||
.woo-cleanup-days-urgent {
|
||||
color: #d63638;
|
||||
font-weight: 700;
|
||||
}
|
||||
.woo-cleanup-empty {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e4e7;
|
||||
border-radius: 4px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #646970;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
389
woo-cleanup.php
Normal file
389
woo-cleanup.php
Normal file
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooCleanup – Inactive Account Manager
|
||||
* Description: Identifies inactive WooCommerce customers (no purchase in a configurable period),
|
||||
* sends a configurable notice email, then deletes the account after a grace period.
|
||||
* Admins can review all flagged accounts from the WooCommerce menu.
|
||||
* Version: 1.0.0
|
||||
* Author: WooCleanup
|
||||
* License: GPL-2.0-or-later
|
||||
* Text Domain: woo-cleanup
|
||||
* Requires at least: 5.8
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 6.0
|
||||
* WC tested up to: 8.9
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'WOO_CLEANUP_VERSION', '1.0.0' );
|
||||
define( 'WOO_CLEANUP_FILE', __FILE__ );
|
||||
define( 'WOO_CLEANUP_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
// Activation / deactivation hooks must be registered at file load time.
|
||||
register_activation_hook( __FILE__, [ 'WooCleanup', 'activate' ] );
|
||||
register_deactivation_hook( __FILE__, [ 'WooCleanup', 'deactivate' ] );
|
||||
|
||||
/**
|
||||
* Boot after all plugins are loaded so WooCommerce is available.
|
||||
*/
|
||||
add_action( 'plugins_loaded', static function () {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'admin_notices', static function () {
|
||||
printf(
|
||||
'<div class="notice notice-error"><p>%s</p></div>',
|
||||
esc_html__( 'WooCleanup requires WooCommerce to be installed and active.', 'woo-cleanup' )
|
||||
);
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
require_once WOO_CLEANUP_DIR . 'includes/class-admin.php';
|
||||
|
||||
WooCleanup::get_instance();
|
||||
} );
|
||||
|
||||
// Declare HPOS compatibility.
|
||||
add_action( 'before_woocommerce_init', static function () {
|
||||
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
||||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
|
||||
'custom_order_tables',
|
||||
__FILE__,
|
||||
true
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Core plugin class.
|
||||
*
|
||||
* Responsibilities:
|
||||
* – Scheduling / de-scheduling the daily cron.
|
||||
* – Running the cleanup routine (cron callback).
|
||||
* – Sending notice emails.
|
||||
* – Deleting inactive accounts.
|
||||
* – Providing option helpers consumed by the admin UI.
|
||||
*/
|
||||
class WooCleanup {
|
||||
|
||||
// ── User meta keys ──────────────────────────────────────────────────────
|
||||
const META_INACTIVE_SINCE = '_woo_cleanup_inactive_since';
|
||||
const META_NOTICE_SENT = '_woo_cleanup_notice_sent';
|
||||
|
||||
// ── Cron hook ───────────────────────────────────────────────────────────
|
||||
const CRON_HOOK = 'woo_cleanup_daily_cron';
|
||||
|
||||
// ── Singleton ───────────────────────────────────────────────────────────
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_action( self::CRON_HOOK, [ $this, 'run_cleanup' ] );
|
||||
|
||||
if ( is_admin() ) {
|
||||
new WooCleanup_Admin( $this );
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Activation / Deactivation
|
||||
// =========================================================================
|
||||
|
||||
public static function activate(): void {
|
||||
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
|
||||
wp_schedule_event( time(), 'daily', self::CRON_HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
public static function deactivate(): void {
|
||||
wp_clear_scheduled_hook( self::CRON_HOOK );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Cron: Main Cleanup Routine
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Called once daily by WP-Cron.
|
||||
* Iterates every customer and applies the cleanup workflow.
|
||||
*/
|
||||
public function run_cleanup(): void {
|
||||
$options = $this->get_options();
|
||||
|
||||
if ( empty( $options['enabled'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inactivity_months = absint( $options['inactivity_months'] );
|
||||
$grace_days = absint( $options['grace_days'] );
|
||||
$cutoff = strtotime( "-{$inactivity_months} months" );
|
||||
|
||||
$customer_ids = get_users( [
|
||||
'role' => 'customer',
|
||||
'fields' => 'ID',
|
||||
] );
|
||||
|
||||
foreach ( $customer_ids as $user_id ) {
|
||||
$this->process_customer( (int) $user_id, $cutoff, $grace_days, $options );
|
||||
}
|
||||
|
||||
update_option( 'woo_cleanup_last_run', time(), false );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Per-Customer Workflow
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Evaluate a single customer and take the appropriate action.
|
||||
*
|
||||
* States:
|
||||
* 1. Active (has a recent order) → clear any pending flags, do nothing.
|
||||
* 2. Inactive, no notice sent yet → send notice, record timestamp.
|
||||
* 3. Inactive, notice sent, grace valid → wait.
|
||||
* 4. Inactive, grace period expired → delete account.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param int $cutoff Unix timestamp; orders BEFORE this = inactive.
|
||||
* @param int $grace_days Days between notice and deletion.
|
||||
* @param array $options Plugin options.
|
||||
*/
|
||||
public function process_customer( int $user_id, int $cutoff, int $grace_days, array $options ): void {
|
||||
if ( $this->has_recent_order( $user_id, $cutoff ) ) {
|
||||
// User is active – clear any pending cleanup flags.
|
||||
delete_user_meta( $user_id, self::META_INACTIVE_SINCE );
|
||||
delete_user_meta( $user_id, self::META_NOTICE_SENT );
|
||||
return;
|
||||
}
|
||||
|
||||
// Record when we first noticed inactivity (idempotent).
|
||||
if ( ! get_user_meta( $user_id, self::META_INACTIVE_SINCE, true ) ) {
|
||||
update_user_meta( $user_id, self::META_INACTIVE_SINCE, time() );
|
||||
}
|
||||
|
||||
$notice_sent = (int) get_user_meta( $user_id, self::META_NOTICE_SENT, true );
|
||||
|
||||
if ( ! $notice_sent ) {
|
||||
// No notice sent yet – send it now.
|
||||
if ( $this->send_notice( $user_id, $grace_days, $options ) ) {
|
||||
update_user_meta( $user_id, self::META_NOTICE_SENT, time() );
|
||||
}
|
||||
} else {
|
||||
// Notice already sent – check if the grace period has expired.
|
||||
$delete_at = $notice_sent + ( $grace_days * DAY_IN_SECONDS );
|
||||
if ( time() >= $delete_at ) {
|
||||
$this->delete_customer( $user_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Returns true if the user has at least one order (completed or processing)
|
||||
* created after $cutoff.
|
||||
*/
|
||||
public function has_recent_order( int $user_id, int $cutoff ): bool {
|
||||
$orders = wc_get_orders( [
|
||||
'customer_id' => $user_id,
|
||||
'limit' => 1,
|
||||
'return' => 'ids',
|
||||
'status' => [ 'wc-completed', 'wc-processing', 'wc-on-hold' ],
|
||||
'date_created' => '>' . date( 'Y-m-d', $cutoff ),
|
||||
] );
|
||||
|
||||
return ! empty( $orders );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the inactivity notice email to a user.
|
||||
*
|
||||
* @return bool True on successful hand-off to wp_mail.
|
||||
*/
|
||||
public function send_notice( int $user_id, int $grace_days, array $options ): bool {
|
||||
$user = get_userdata( $user_id );
|
||||
if ( ! $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$deletion_date = date_i18n(
|
||||
get_option( 'date_format' ),
|
||||
time() + ( $grace_days * DAY_IN_SECONDS )
|
||||
);
|
||||
|
||||
$vars = [
|
||||
'{site_name}' => get_bloginfo( 'name' ),
|
||||
'{username}' => $user->display_name,
|
||||
'{email}' => $user->user_email,
|
||||
'{deletion_date}' => $deletion_date,
|
||||
'{grace_days}' => (string) $grace_days,
|
||||
];
|
||||
|
||||
$subject = strtr( $options['email_subject'], $vars );
|
||||
$body = strtr( $options['email_body'], $vars );
|
||||
|
||||
// Wrap plain-text body in a minimal HTML shell so mail clients render
|
||||
// line-breaks and the Content-Type header is honoured.
|
||||
$html = sprintf(
|
||||
'<!DOCTYPE html><html><body style="font-family:Arial,sans-serif;color:#333;'
|
||||
. 'max-width:600px;margin:0 auto;padding:24px;line-height:1.6">%s</body></html>',
|
||||
wpautop( wp_kses_post( $body ) )
|
||||
);
|
||||
|
||||
return wp_mail(
|
||||
$user->user_email,
|
||||
$subject,
|
||||
$html,
|
||||
[ 'Content-Type: text/html; charset=UTF-8' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a customer account.
|
||||
* Orders are intentionally left intact (required for financial records).
|
||||
*/
|
||||
public function delete_customer( int $user_id ): void {
|
||||
if ( ! function_exists( 'wp_delete_user' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/user.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires just before WooCleanup deletes an inactive customer.
|
||||
*
|
||||
* @param int $user_id The user ID about to be deleted.
|
||||
*/
|
||||
do_action( 'woo_cleanup_before_delete_user', $user_id );
|
||||
|
||||
wp_delete_user( $user_id );
|
||||
|
||||
/**
|
||||
* Fires after WooCleanup has deleted an inactive customer.
|
||||
*
|
||||
* @param int $user_id The deleted user ID (no longer valid in DB).
|
||||
*/
|
||||
do_action( 'woo_cleanup_after_delete_user', $user_id );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Options
|
||||
// =========================================================================
|
||||
|
||||
public function get_options(): array {
|
||||
return wp_parse_args(
|
||||
(array) get_option( 'woo_cleanup_options', [] ),
|
||||
$this->get_defaults()
|
||||
);
|
||||
}
|
||||
|
||||
public function get_defaults(): array {
|
||||
return [
|
||||
'enabled' => 1,
|
||||
'inactivity_months' => 18,
|
||||
'grace_days' => 7,
|
||||
'email_subject' => 'Important: Your account at {site_name} is scheduled for deletion',
|
||||
'email_body' => $this->get_default_email_body(),
|
||||
];
|
||||
}
|
||||
|
||||
private function get_default_email_body(): string {
|
||||
return "Dear {username},\n\n"
|
||||
. "We noticed that you haven't made a purchase at {site_name} in quite some time.\n\n"
|
||||
. "As part of our regular account maintenance, inactive accounts are periodically removed. "
|
||||
. "Your account is scheduled for deletion on {deletion_date} ({grace_days} days from today).\n\n"
|
||||
. "To keep your account, simply log in and place an order before that date. "
|
||||
. "If you no longer need your account, no action is required.\n\n"
|
||||
. "If you have any questions, please contact our support team.\n\n"
|
||||
. "Best regards,\nThe {site_name} Team";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Static Queries (consumed by admin page)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Return WP_User objects for all flagged customers.
|
||||
*
|
||||
* @param string $status 'pending_notice' | 'notice_sent' | 'all'
|
||||
* @return WP_User[]
|
||||
*/
|
||||
public static function get_flagged_users( string $status = 'all' ): array {
|
||||
switch ( $status ) {
|
||||
case 'pending_notice':
|
||||
$meta_query = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => self::META_INACTIVE_SINCE,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => self::META_NOTICE_SENT,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
case 'notice_sent':
|
||||
$meta_query = [
|
||||
[
|
||||
'key' => self::META_NOTICE_SENT,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
default: // 'all'
|
||||
$meta_query = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => self::META_INACTIVE_SINCE,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => self::META_NOTICE_SENT,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return get_users( [
|
||||
'role' => 'customer',
|
||||
'meta_query' => $meta_query,
|
||||
'orderby' => 'registered',
|
||||
'order' => 'ASC',
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last order date for a user, or null if they have no orders.
|
||||
*/
|
||||
public static function get_last_order_date( int $user_id ): ?string {
|
||||
$orders = wc_get_orders( [
|
||||
'customer_id' => $user_id,
|
||||
'limit' => 1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'return' => 'objects',
|
||||
] );
|
||||
|
||||
if ( empty( $orders ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var WC_Order $order */
|
||||
$order = reset( $orders );
|
||||
$date = $order->get_date_created();
|
||||
|
||||
return $date ? $date->date_i18n( get_option( 'date_format' ) ) : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user