| Current File : /home/digitaw/www/wp-content/plugins/simple-history/inc/services/class-setup-purge-db-cron.php |
<?php
namespace Simple_History\Services;
use Simple_History\Helpers;
/**
* Setup a wp-cron job that daily checks if the database should be cleared.
*/
class Setup_Purge_DB_Cron extends Service {
/** @inheritdoc */
public function loaded() {
add_action( 'after_setup_theme', array( $this, 'setup_cron' ) );
// phpcs:disable
// Uncomment the next lines to force add the events purged message (without actually purging the db).
// add_action(
// 'init',
// function () {
// do_action( 'simple_history/db/events_purged', 60, 5000 );
// }
// );
// phpcs:enable
}
/**
* Setup a wp-cron job that daily checks if the database should be cleared.
*/
public function setup_cron() {
add_action( 'simple_history/maybe_purge_db', array( $this, 'maybe_purge_db' ) );
if ( ! wp_next_scheduled( 'simple_history/maybe_purge_db' ) ) {
wp_schedule_event( time(), 'daily', 'simple_history/maybe_purge_db' );
}
}
/**
* Runs the purge_db() method sometimes.
*
* Fired from action `simple_history/maybe_purge_db``
* that is scheduled to run once a day.
*
* The db is purged only on Sundays by default,
* this is to keep the history clean. If it was done
* every day it could pollute the log with a lot of
* "Simple History removed X events that were older than Y days".
*
* @since 2.0.17
*/
public function maybe_purge_db() {
/**
* Day of week today.
*
* @int $current_day_of_week
*/
$current_day_of_week = (int) gmdate( 'N' );
/**
* Day number to purge db on.
*
* @int $day_of_week_to_purge_db
*/
$day_of_week_to_purge_db = 7;
/**
* Filter to change day of week to purge db on.
* Default is 7 (sunday).
*
* @param int $day_of_week_to_purge_db
* @since 4.1.0
*/
$day_of_week_to_purge_db = apply_filters( 'simple_history/day_of_week_to_purge_db', $day_of_week_to_purge_db );
if ( $current_day_of_week === $day_of_week_to_purge_db ) {
$this->purge_db();
}
}
/**
* Removes old entries from the db.
*
* Removes in batches of 100 000 rows.
*/
public function purge_db() {
$do_purge_history = true;
$do_purge_history = apply_filters( 'simple_history_allow_db_purge', $do_purge_history );
$do_purge_history = apply_filters( 'simple_history/allow_db_purge', $do_purge_history );
if ( ! $do_purge_history ) {
return;
}
$days = Helpers::get_clear_history_interval();
// Never clear log if days = 0.
if ( $days === 0 ) {
return;
}
$table_name = $this->simple_history->get_events_table_name();
$table_name_contexts = $this->simple_history->get_contexts_table_name();
global $wpdb;
// Track total rows deleted across all batches.
$total_rows = 0;
// Build the WHERE clause for selecting events to purge.
$where = $this->get_purge_where_clause( $days, $table_name );
// Process deletions in batches of 100,000 rows to avoid memory exhaustion,
// query timeouts, and long table locks. Loop continues until no old events remain.
while ( 1 > 0 ) {
// Get id of rows to delete.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
$sql = "SELECT id FROM {$table_name} WHERE {$where} LIMIT 100000";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$ids_to_delete = $wpdb->get_col( $sql );
if ( empty( $ids_to_delete ) ) {
// Nothing more to delete.
break;
}
$sql_ids_in = implode( ',', $ids_to_delete );
// Remove rows + contexts.
$sql_delete_history = "DELETE FROM {$table_name} WHERE id IN ($sql_ids_in)";
$sql_delete_history_context = "DELETE FROM {$table_name_contexts} WHERE history_id IN ($sql_ids_in)";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( $sql_delete_history );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( $sql_delete_history_context );
$num_rows_purged_in_batch = is_countable( $ids_to_delete ) ? count( $ids_to_delete ) : 0;
$total_rows += $num_rows_purged_in_batch;
/**
* Fires after a batch of events have been purged from the database.
* Note: This fires for each batch of 100,000 rows.
*
* @param int $days Number of days to keep events.
* @param int $num_rows_purged_in_batch Number of rows deleted in this batch.
*/
do_action( 'simple_history/db/events_purged', $days, $num_rows_purged_in_batch );
Helpers::clear_cache();
}
/**
* Fires after all events have been purged from the database.
* This fires once when the entire purge operation is complete.
* Total rows can be 0 if no events were purged.
*
* @param int $days Number of days to keep events.
* @param int $total_rows Total number of rows deleted across all batches.
* @since 5.21.0
*/
do_action( 'simple_history/db/purge_done', $days, $total_rows );
}
/**
* Build the WHERE clause for the purge query.
*
* @param int $days Number of days to keep events.
* @param string $table_name Events table name.
* @return string SQL WHERE clause (without the WHERE keyword).
*/
private function get_purge_where_clause( $days, $table_name ) {
global $wpdb;
// Default: delete events older than X days.
$where = $wpdb->prepare(
'DATE_ADD(date, INTERVAL %d DAY) < NOW()',
$days
);
/**
* Filter the SQL WHERE clause used when purging old events.
*
* This filter allows advanced customization of which events to delete.
* Use it to implement per-logger retention, keep certain events forever,
* or add any custom deletion criteria.
*
* Available columns in the events table:
* - id (bigint)
* - logger (varchar) - e.g. 'SimpleUserLogger', 'SimplePostLogger'
* - level (varchar) - 'debug', 'info', 'warning', 'error', 'critical'
* - date (datetime)
* - message (varchar)
* - initiator (varchar) - 'wp_user', 'web_user', 'wp_cli', 'wp', 'other'
*
* IMPORTANT: You are responsible for returning valid SQL.
* Always use $wpdb->prepare() for dynamic values.
*
* @since 5.21.0
*
* @param string $where SQL WHERE clause (without "WHERE" keyword).
* @param int $days Default retention days from settings.
* @param string $table_name Events table name (for reference).
*
* @example Keep SimpleOptionsLogger events forever (exclude from purge).
*
* ```php
* add_filter( 'simple_history/purge_db_where', function( $where, $days, $table ) {
* global $wpdb;
* return $where . $wpdb->prepare( ' AND logger != %s', 'SimpleOptionsLogger' );
* }, 10, 3 );
* ```
*
* @example Keep events with level "warning" or higher forever.
*
* ```php
* add_filter( 'simple_history/purge_db_where', function( $where, $days, $table ) {
* return $where . " AND level NOT IN ('warning', 'error', 'critical')";
* }, 10, 3 );
* ```
*
* @example Different retention per logger (replaces default WHERE).
*
* ```php
* add_filter( 'simple_history/purge_db_where', function( $where, $days, $table ) {
* global $wpdb;
*
* // Define custom retention per logger (in days, 0 = keep forever).
* $retention = [
* 'SimpleUserLogger' => 365, // Login events: 1 year.
* 'SimplePostLogger' => 180, // Post changes: 6 months.
* 'SimpleOptionsLogger' => 0, // Settings changes: forever.
* ];
*
* $conditions = [];
*
* foreach ( $retention as $logger => $logger_days ) {
* // Skip loggers that should be kept forever.
* if ( $logger_days === 0 ) {
* continue;
* }
* $conditions[] = $wpdb->prepare(
* '(logger = %s AND DATE_ADD(date, INTERVAL %d DAY) < NOW())',
* $logger,
* $logger_days
* );
* }
*
* // All other loggers: use default retention, but exclude "keep forever" loggers.
* $keep_forever = array_keys( array_filter( $retention, fn( $d ) => $d === 0 ) );
* if ( ! empty( $keep_forever ) ) {
* $placeholders = implode( ',', array_fill( 0, count( $keep_forever ), '%s' ) );
* $conditions[] = $wpdb->prepare(
* "(logger NOT IN ($placeholders) AND $where)",
* ...$keep_forever
* );
* } else {
* $conditions[] = "($where)";
* }
*
* return '(' . implode( ' OR ', $conditions ) . ')';
* }, 10, 3 );
* ```
*/
$where = apply_filters( 'simple_history/purge_db_where', $where, $days, $table_name );
return $where;
}
}