Skip to content

Commit 19c72cc

Browse files
Options, Meta APIs: Clean up expired orphaned transient timeouts and resolve cross-network collision.
- Updates delete_transient() and delete_site_transient() to always delete orphaned timeout rows. - Updates delete_expired_transients() to delete expired orphaned transient and site transient timeouts across single-site (options table) and multisite (sitemeta table). - Enforces b.site_id = a.site_id on both multisite cleanup queries to resolve cross-network data loss (#65969). - Adds comprehensive single-site and multisite test coverage using factory-created networks. - Documents the return value behavior in docblocks for delete_transient() and delete_site_transient(). Props team evolve. Fixes #65863. See #65969.
1 parent 4a8a86b commit 19c72cc

3 files changed

Lines changed: 479 additions & 7 deletions

File tree

‎src/wp-includes/option.php‎

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,10 @@ function wp_filter_default_autoload_value_via_option_size( $autoload, $option, $
13721372
/**
13731373
* Deletes a transient.
13741374
*
1375+
* If an orphaned transient timeout option exists without a corresponding transient
1376+
* option, the timeout option is deleted, but the function returns false because
1377+
* no transient value option existed to be deleted.
1378+
*
13751379
* @since 2.8.0
13761380
*
13771381
* @param string $transient Transient name. Expected to not be SQL-escaped.
@@ -1397,9 +1401,7 @@ function delete_transient( $transient ) {
13971401
$option = '_transient_' . $transient;
13981402
$result = delete_option( $option );
13991403

1400-
if ( $result ) {
1401-
delete_option( $option_timeout );
1402-
}
1404+
delete_option( $option_timeout );
14031405
}
14041406

14051407
if ( $result ) {
@@ -1655,6 +1657,19 @@ function delete_expired_transients( $force_db = false ) {
16551657
)
16561658
);
16571659

1660+
$wpdb->query(
1661+
$wpdb->prepare(
1662+
"DELETE a FROM {$wpdb->options} a
1663+
LEFT JOIN {$wpdb->options} b
1664+
ON b.option_name = CONCAT( '_transient_', SUBSTRING( a.option_name, 20 ) )
1665+
WHERE a.option_name LIKE %s
1666+
AND a.option_value < %d
1667+
AND b.option_id IS NULL",
1668+
$wpdb->esc_like( '_transient_timeout_' ) . '%',
1669+
time()
1670+
)
1671+
);
1672+
16581673
if ( ! is_multisite() ) {
16591674
// Single site stores site transients in the options table.
16601675
$wpdb->query(
@@ -1669,6 +1684,19 @@ function delete_expired_transients( $force_db = false ) {
16691684
time()
16701685
)
16711686
);
1687+
1688+
$wpdb->query(
1689+
$wpdb->prepare(
1690+
"DELETE a FROM {$wpdb->options} a
1691+
LEFT JOIN {$wpdb->options} b
1692+
ON b.option_name = CONCAT( '_site_transient_', SUBSTRING( a.option_name, 25 ) )
1693+
WHERE a.option_name LIKE %s
1694+
AND a.option_value < %d
1695+
AND b.option_id IS NULL",
1696+
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
1697+
time()
1698+
)
1699+
);
16721700
} elseif ( is_main_site() && is_main_network() ) {
16731701
// Multisite stores site transients in the sitemeta table.
16741702
$wpdb->query(
@@ -1677,12 +1705,27 @@ function delete_expired_transients( $force_db = false ) {
16771705
WHERE a.meta_key LIKE %s
16781706
AND a.meta_key NOT LIKE %s
16791707
AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) )
1708+
AND b.site_id = a.site_id
16801709
AND b.meta_value < %d",
16811710
$wpdb->esc_like( '_site_transient_' ) . '%',
16821711
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
16831712
time()
16841713
)
16851714
);
1715+
1716+
$wpdb->query(
1717+
$wpdb->prepare(
1718+
"DELETE a FROM {$wpdb->sitemeta} a
1719+
LEFT JOIN {$wpdb->sitemeta} b
1720+
ON b.meta_key = CONCAT( '_site_transient_', SUBSTRING( a.meta_key, 25 ) )
1721+
AND b.site_id = a.site_id
1722+
WHERE a.meta_key LIKE %s
1723+
AND a.meta_value < %d
1724+
AND b.meta_id IS NULL",
1725+
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
1726+
time()
1727+
)
1728+
);
16861729
}
16871730
}
16881731

@@ -2503,9 +2546,13 @@ function update_network_option( $network_id, $option, $value ) {
25032546
/**
25042547
* Deletes a site transient.
25052548
*
2549+
* If an orphaned site transient timeout option exists without a corresponding transient
2550+
* option, the timeout option is deleted, but the function returns false because
2551+
* no transient value option existed to be deleted.
2552+
*
25062553
* @since 2.9.0
25072554
*
2508-
* @param string $transient Transient name. Expected to not be SQL-escaped.
2555+
* @param string $transient Site transient name. Expected to not be SQL-escaped.
25092556
* @return bool True if the transient was deleted, false otherwise.
25102557
*/
25112558
function delete_site_transient( $transient ) {
@@ -2528,9 +2575,7 @@ function delete_site_transient( $transient ) {
25282575
$option = '_site_transient_' . $transient;
25292576
$result = delete_site_option( $option );
25302577

2531-
if ( $result ) {
2532-
delete_site_option( $option_timeout );
2533-
}
2578+
delete_site_option( $option_timeout );
25342579
}
25352580

25362581
if ( $result ) {

0 commit comments

Comments
 (0)