d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
230 lines
7.4 KiB
PHP
230 lines
7.4 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Postmeta_Cleaner — wp_postmeta garbage cleanup (v2.9.0 Phase 0).
|
|
*
|
|
* Identifies and removes three classes of low-value rows from wp_postmeta
|
|
* that bloat the table without serving any business purpose:
|
|
*
|
|
* - transients — stale `_transient_*` and `_transient_timeout_*` rows
|
|
* (often left by HivePress model version cache)
|
|
* - wp_old_date — WP core internal record of post date changes (no app value)
|
|
* - edit_locks — `_edit_lock` rows whose lock timestamp is > 24h old
|
|
* (orphaned from interrupted edit sessions)
|
|
*
|
|
* Runs before any Entity Bridge migration so subsequent ratio measurements
|
|
* reflect real data, not garbage. Pure DB layer — no Hook Bus / Entity Bridge
|
|
* coupling so cleanup is safe even when post entity bridge is disabled.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.9.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Wp_postmeta garbage cleanup (v2.9.0 Phase 0).
|
|
*/
|
|
class TMDO_Postmeta_Cleaner {
|
|
|
|
public const TARGET_TRANSIENTS = 'transients';
|
|
public const TARGET_WP_OLD_DATE = 'wp_old_date';
|
|
public const TARGET_EDIT_LOCKS = 'edit_locks';
|
|
public const TARGET_ALL = 'all';
|
|
|
|
public const VALID_TARGETS = array(
|
|
self::TARGET_TRANSIENTS,
|
|
self::TARGET_WP_OLD_DATE,
|
|
self::TARGET_EDIT_LOCKS,
|
|
self::TARGET_ALL,
|
|
);
|
|
|
|
/**
|
|
* Seconds after which an _edit_lock is considered stale.
|
|
* WP refreshes locks on a 15-second heartbeat; 24h is intentionally
|
|
* conservative to avoid disturbing any active edit session.
|
|
*/
|
|
private const EDIT_LOCK_STALE_THRESHOLD = 86400;
|
|
|
|
/**
|
|
* Count rows that would be cleaned for the given target.
|
|
*
|
|
* @param string $target One of TARGET_* constants.
|
|
* @return array{transients:int, wp_old_date:int, edit_locks:int, total:int}
|
|
* @throws InvalidArgumentException When $target is not a valid target.
|
|
*/
|
|
public static function count_garbage( string $target = self::TARGET_ALL ): array {
|
|
self::assert_valid_target( $target );
|
|
|
|
$counts = array(
|
|
'transients' => 0,
|
|
'wp_old_date' => 0,
|
|
'edit_locks' => 0,
|
|
'total' => 0,
|
|
);
|
|
|
|
if ( self::target_includes( $target, self::TARGET_TRANSIENTS ) ) {
|
|
$counts['transients'] = self::count_transients();
|
|
}
|
|
if ( self::target_includes( $target, self::TARGET_WP_OLD_DATE ) ) {
|
|
$counts['wp_old_date'] = self::count_wp_old_date();
|
|
}
|
|
if ( self::target_includes( $target, self::TARGET_EDIT_LOCKS ) ) {
|
|
$counts['edit_locks'] = self::count_stale_edit_locks();
|
|
}
|
|
|
|
$counts['total'] = $counts['transients'] + $counts['wp_old_date'] + $counts['edit_locks'];
|
|
return $counts;
|
|
}
|
|
|
|
/**
|
|
* Delete garbage rows for the given target.
|
|
*
|
|
* @param string $target One of TARGET_* constants.
|
|
* @return array{transients:int, wp_old_date:int, edit_locks:int, total:int}
|
|
* @throws InvalidArgumentException When $target is not a valid target.
|
|
*/
|
|
public static function delete_garbage( string $target = self::TARGET_ALL ): array {
|
|
self::assert_valid_target( $target );
|
|
|
|
$deleted = array(
|
|
'transients' => 0,
|
|
'wp_old_date' => 0,
|
|
'edit_locks' => 0,
|
|
'total' => 0,
|
|
);
|
|
|
|
if ( self::target_includes( $target, self::TARGET_TRANSIENTS ) ) {
|
|
$deleted['transients'] = self::delete_transients();
|
|
}
|
|
if ( self::target_includes( $target, self::TARGET_WP_OLD_DATE ) ) {
|
|
$deleted['wp_old_date'] = self::delete_wp_old_date();
|
|
}
|
|
if ( self::target_includes( $target, self::TARGET_EDIT_LOCKS ) ) {
|
|
$deleted['edit_locks'] = self::delete_stale_edit_locks();
|
|
}
|
|
|
|
$deleted['total'] = $deleted['transients'] + $deleted['wp_old_date'] + $deleted['edit_locks'];
|
|
return $deleted;
|
|
}
|
|
|
|
/**
|
|
* Whether $target selects $bucket (i.e. target=all or target=bucket).
|
|
*
|
|
* @param string $target Selected target.
|
|
* @param string $bucket Bucket constant (TARGET_TRANSIENTS / WP_OLD_DATE / EDIT_LOCKS).
|
|
* @return bool
|
|
*/
|
|
private static function target_includes( string $target, string $bucket ): bool {
|
|
return self::TARGET_ALL === $target || $bucket === $target;
|
|
}
|
|
|
|
/**
|
|
* Validate target parameter.
|
|
*
|
|
* @param string $target Target to validate.
|
|
* @return void
|
|
* @throws InvalidArgumentException When $target is not in VALID_TARGETS.
|
|
*/
|
|
private static function assert_valid_target( string $target ): void {
|
|
if ( in_array( $target, self::VALID_TARGETS, true ) ) {
|
|
return;
|
|
}
|
|
// Exception messages bubble up to PHP's error handler / WP_CLI; not user output.
|
|
$msg = sprintf( 'Invalid target "%s". Valid: %s', $target, implode( ', ', self::VALID_TARGETS ) );
|
|
throw new InvalidArgumentException( $msg ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
|
}
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal cleanup class: $wpdb->postmeta is WP-managed and never user input. PreparedSQL.InterpolatedNotPrepared fires on multi-line $wpdb->prepare() literals where the only interpolation is the trusted table name; user-controlled values use placeholders.
|
|
|
|
/**
|
|
* Count rows matching transient meta_key patterns in wp_postmeta.
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function count_transients(): int {
|
|
global $wpdb;
|
|
$table = $wpdb->postmeta;
|
|
return (int) $wpdb->get_var(
|
|
"SELECT COUNT(*) FROM `{$table}` WHERE meta_key LIKE '\\_transient\\_%' OR meta_key LIKE '\\_transient\\_timeout\\_%'"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete rows matching transient meta_key patterns from wp_postmeta.
|
|
*
|
|
* @return int Affected row count.
|
|
*/
|
|
private static function delete_transients(): int {
|
|
global $wpdb;
|
|
$table = $wpdb->postmeta;
|
|
return (int) $wpdb->query(
|
|
"DELETE FROM `{$table}` WHERE meta_key LIKE '\\_transient\\_%' OR meta_key LIKE '\\_transient\\_timeout\\_%'"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Count _wp_old_date rows in wp_postmeta.
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function count_wp_old_date(): int {
|
|
global $wpdb;
|
|
$table = $wpdb->postmeta;
|
|
return (int) $wpdb->get_var(
|
|
"SELECT COUNT(*) FROM `{$table}` WHERE meta_key = '_wp_old_date'"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete _wp_old_date rows from wp_postmeta.
|
|
*
|
|
* @return int Affected row count.
|
|
*/
|
|
private static function delete_wp_old_date(): int {
|
|
global $wpdb;
|
|
$table = $wpdb->postmeta;
|
|
return (int) $wpdb->query(
|
|
"DELETE FROM `{$table}` WHERE meta_key = '_wp_old_date'"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Count stale _edit_lock rows (lock_ts older than 24h) in wp_postmeta.
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function count_stale_edit_locks(): int {
|
|
global $wpdb;
|
|
$table = $wpdb->postmeta;
|
|
$cutoff = time() - self::EDIT_LOCK_STALE_THRESHOLD;
|
|
// _edit_lock format is "<unix_ts>:<user_id>"; SUBSTRING_INDEX extracts the timestamp.
|
|
return (int) $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT COUNT(*) FROM `{$table}` WHERE meta_key = '_edit_lock' AND CAST(SUBSTRING_INDEX(meta_value, ':', 1) AS UNSIGNED) < %d",
|
|
$cutoff
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete stale _edit_lock rows (lock_ts older than 24h) from wp_postmeta.
|
|
*
|
|
* @return int Affected row count.
|
|
*/
|
|
private static function delete_stale_edit_locks(): int {
|
|
global $wpdb;
|
|
$table = $wpdb->postmeta;
|
|
$cutoff = time() - self::EDIT_LOCK_STALE_THRESHOLD;
|
|
return (int) $wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM `{$table}` WHERE meta_key = '_edit_lock' AND CAST(SUBSTRING_INDEX(meta_value, ':', 1) AS UNSIGNED) < %d",
|
|
$cutoff
|
|
)
|
|
);
|
|
}
|
|
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
}
|