PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
← All changes | includes/class-database-cleaner.php +43 -10 1.0.31.3.3 View file →
@@ -71,10 +71,22 @@
71 71 'clean_sql_tpl' => "DELETE FROM %1\$scomments WHERE comment_approved = 'trash'",
72 72 ),
73 73 'expired_transients' => array(
74 74 'label' => 'Expired Transients',
75 - 'scan_sql_tpl' => "SELECT COUNT(*) FROM %1\$soptions WHERE option_name LIKE '_transient_timeout_%%' AND CAST(option_value AS UNSIGNED) < UNIX_TIMESTAMP()",
76 - 'clean_sql_tpl' => "DELETE a, b FROM %1\$soptions a INNER JOIN %1\$soptions b ON b.option_name = REPLACE(a.option_name, '_transient_timeout_', '_transient_') WHERE a.option_name LIKE '_transient_timeout_%%' AND CAST(a.option_value AS UNSIGNED) < UNIX_TIMESTAMP()",
75 + // Scan counts one row per EXPIRED transient — the timeout
76 + // row — across BOTH the normal (_transient_timeout_*) and the
77 + // site/network (_site_transient_timeout_*) families. The
78 + // parentheses around the two LIKEs are required so the AND
79 + // expiry condition applies to both, not just the second.
80 + // (FBS-82149 Bug 1: site transients were never matched;
81 + // Bug 3: scan counts logical transients = 1 per timeout row.)
82 + 'scan_sql_tpl' => "SELECT COUNT(*) FROM %1\$soptions WHERE ( option_name LIKE '_transient_timeout_%%' OR option_name LIKE '_site_transient_timeout_%%' ) AND CAST( option_value AS UNSIGNED ) < UNIX_TIMESTAMP()",
83 + // Clean deletes the timeout row AND its value sibling via a
84 + // LEFT JOIN so an ORPHAN timeout row (value sibling missing)
85 + // is still removed — an INNER JOIN silently kept those, so a
86 + // "clean" never drove the scan count to zero. REPLACE maps
87 + // both families to their value-key name. (FBS-82149 Bug 2.)
88 + 'clean_sql_tpl' => "DELETE a, b FROM %1\$soptions a LEFT JOIN %1\$soptions b ON b.option_name = REPLACE( REPLACE( a.option_name, '_site_transient_timeout_', '_site_transient_' ), '_transient_timeout_', '_transient_' ) WHERE ( a.option_name LIKE '_transient_timeout_%%' OR a.option_name LIKE '_site_transient_timeout_%%' ) AND CAST( a.option_value AS UNSIGNED ) < UNIX_TIMESTAMP()",
77 89 ),
78 90 'orphan_postmeta' => array(
79 91 'label' => 'Orphan Post Meta',
80 92 'scan_sql_tpl' => "SELECT COUNT(*) FROM %1\$spostmeta pm LEFT JOIN %1\$sposts p ON p.ID = pm.post_id WHERE p.ID IS NULL",
@@ -97,14 +109,11 @@
97 109 public static function scan( $wpdb_in = null ): array {
98 110 $wpdb = self::wpdb( $wpdb_in );
99 111 $out = array();
100 112 foreach ( self::types() as $key => $spec ) {
101 - $sql = sprintf( $spec['scan_sql_tpl'], $wpdb->prefix );
102 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL is built from a hardcoded template registry (types()); only $wpdb->prefix is interpolated. Read-only scan called from a manage_options REST endpoint; results aren't cached on purpose so the user always sees current row counts.
103 - $count = (int) $wpdb->get_var( $sql );
104 113 $out[ $key ] = array(
105 114 'label' => $spec['label'],
106 - 'count' => $count,
115 + 'count' => self::count_for( $spec, $wpdb ),
107 116 );
108 117 }
109 118 return $out;
110 119 }
@@ -109,8 +118,22 @@
109 118 return $out;
110 119 }
111 120
112 121 /**
122 + * Run a type's scan SQL and return its COUNT. Shared by scan() and by
123 + * clean() (before/after) so the "rows removed" number is always in the
124 + * same unit the UI shows. (FBS-82149 Bug 3.)
125 + *
126 + * @param array{scan_sql_tpl:string} $spec
127 + * @param \wpdb $wpdb
128 + */
129 + private static function count_for( array $spec, $wpdb ): int {
130 + $sql = sprintf( $spec['scan_sql_tpl'], $wpdb->prefix );
131 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL is built from a hardcoded template registry (types()); only $wpdb->prefix is interpolated. Read-only count; not cached so the user always sees current row counts.
132 + return (int) $wpdb->get_var( $sql );
133 + }
134 +
135 + /**
113 136 * Destructive cleanup for the supplied list of type slugs. Anything
114 137 * not in the static types() table is silently skipped — never run
115 138 * arbitrary SQL because someone POSTed an unexpected slug.
116 139 *
@@ -128,14 +151,24 @@
128 151 foreach ( $types as $key ) {
129 152 if ( ! isset( $registry[ $key ] ) ) {
130 153 continue;
131 154 }
155 + // Report `affected` in the SAME unit the scan counts + the UI
156 + // button shows: the reduction in the scan count (logical items),
157 + // not raw rows deleted. Without this the paired transient DELETE
158 + // (timeout + value = 2 rows per transient) reported "2" while the
159 + // button said "(1)". Measure before/after so every type — paired
160 + // or single-row — reports a consistent, user-meaningful count.
161 + // (FBS-82149 Bug 3.)
162 + $before = self::count_for( $registry[ $key ], $wpdb );
163 +
132 164 $sql = sprintf( $registry[ $key ]['clean_sql_tpl'], $wpdb->prefix );
133 165 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL built from a hardcoded template registry (types()); only $wpdb->prefix is interpolated. Destructive cleanup explicitly bypasses the object cache.
134 - $affected = (int) $wpdb->query( $sql );
135 - if ( $affected < 0 ) {
136 - $affected = 0; // wpdb returns -1 on error.
137 - }
166 + $wpdb->query( $sql );
167 +
168 + $after = self::count_for( $registry[ $key ], $wpdb );
169 + $affected = max( 0, $before - $after );
170 +
138 171 $results[ $key ] = $affected;
139 172 $total += $affected;
140 173 }
141 174