PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.2
1.4.2 1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 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 All 36 releases
← All changes | includes/modules/Database/DatabaseModule.php +122 -9 1.0.9 → 1.4.2 View file →
@@ -26,12 +26,13 @@
26 26 public const VERSION = '1.0.0';
27 27
28 28 public function ui_metadata(): array {
29 29 return array(
30 - 'label' => 'Database',
30 + 'label' => __( 'Database', 'xspeed' ),
31 31 'icon' => 'Trash2',
32 - 'description' => 'Scan + clean WordPress bloat (revisions, spam, transients, orphan meta) and optimize tables.',
32 + 'description' => __( 'Remove old revisions, spam and other leftovers, and optimize database tables.', 'xspeed' ),
33 33 'custom_panel' => 'DatabaseCleanerPanel',
34 + 'group' => 'tools',
34 35 );
35 36 }
36 37
37 38 public function settings_schema(): array {
@@ -45,21 +46,102 @@
45 46 'hourly' => 'Hourly',
46 47 'daily' => 'Daily',
47 48 'weekly' => 'Weekly',
48 49 ),
49 - 'label' => 'Auto-Cleanup Schedule',
50 - 'description' => 'How often to run cleanup automatically. Manual means cleanup only runs when you press the button.',
50 + 'label' => __( 'Automatic cleanup', 'xspeed' ),
51 + 'description' => __( 'How often xSpeed cleans the database on its own. Manual means it only cleans when you press the button.', 'xspeed' ),
51 52 ),
52 53 'included_types' => array(
53 54 'type' => 'list',
54 55 'default' => array(),
55 56 'item_type' => 'string',
56 - 'label' => 'Auto-Cleanup Types',
57 - 'description' => 'Which cleanup categories run on the schedule above. Leave empty to keep auto-cleanup disabled even if a schedule is set.',
57 + 'label' => __( 'What to clean automatically', 'xspeed' ),
58 + 'description' => __( 'The items the scheduled cleanup removes. If you leave this empty, nothing is cleaned, even with a schedule set.', 'xspeed' ),
59 + 'dependsOn' => array(
60 + 'any' => array(
61 + array( 'field' => 'schedule', 'value' => 'hourly' ),
62 + array( 'field' => 'schedule', 'value' => 'daily' ),
63 + array( 'field' => 'schedule', 'value' => 'weekly' ),
64 + ),
65 + ),
58 66 ),
67 + 'limit_revisions' => array(
68 + 'type' => 'bool',
69 + 'default' => false,
70 + 'label' => __( 'Limit post revisions', 'xspeed' ),
71 + 'description' => __( 'Cap how many revisions WordPress keeps for each post. Cleanup removes revisions that already exist; this stops them piling up again.', 'xspeed' ),
72 + ),
73 + 'revisions_to_keep' => array(
74 + 'type' => 'int',
75 + 'default' => 5,
76 + 'min' => 0,
77 + 'max' => 500,
78 + 'label' => __( 'Revisions to keep', 'xspeed' ),
79 + 'description' => __( '0 turns revisions off completely. Older revisions are trimmed the next time each post is saved; to remove the backlog now, clean Post Revisions above.', 'xspeed' ),
80 + 'dependsOn' => array( 'field' => 'limit_revisions' ),
81 + ),
59 82 );
60 83 }
61 84
85 + /**
86 + * WP_POST_REVISIONS as wp-config.php (or an earlier plugin) set it, or
87 + * null when nobody did. Read in boot(), on plugins_loaded: after that,
88 + * core defines it as `true` itself, so defined() stops telling the two
89 + * cases apart.
90 + *
91 + * Not the schema's `constants` pinning (#398): that tests defined() at
92 + * request time, when core's own default makes it always true.
93 + *
94 + * @var bool|int|null
95 + */
96 + private static $configured_revisions = null;
97 +
98 + /**
99 + * @param int $num Revisions WordPress would keep; -1 means all.
100 + * @param \WP_Post $post
101 + * @return int
102 + */
103 + public static function filter_revisions_to_keep( $num, $post ) {
104 + // A wp-config.php value is the site owner's decision; leave it alone.
105 + if ( null !== self::$configured_revisions ) {
106 + return $num;
107 + }
108 + // Core already answered 0 for post types without revision support.
109 + if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'revisions' ) ) {
110 + return $num;
111 + }
112 + $opts = Settings_Manager::get( self::SLUG );
113 + if ( empty( $opts['limit_revisions'] ) ) {
114 + return $num;
115 + }
116 + return max( 0, (int) ( $opts['revisions_to_keep'] ?? 5 ) );
117 + }
118 +
119 + public function ui_notices(): array {
120 + if ( null === self::$configured_revisions ) {
121 + return array();
122 + }
123 + $value = self::$configured_revisions;
124 + if ( true === $value ) {
125 + $shown = 'true';
126 + } elseif ( false === $value ) {
127 + $shown = 'false';
128 + } else {
129 + $shown = (string) (int) $value;
130 + }
131 + return array(
132 + array(
133 + 'tone' => 'info',
134 + 'title' => __( 'The revision limit is set outside xSpeed', 'xspeed' ),
135 + 'body' => sprintf(
136 + /* translators: %s: value of WP_POST_REVISIONS, e.g. 5 or false. */
137 + __( 'WP_POST_REVISIONS is set to %s, usually in wp-config.php, so Limit Post Revisions below has no effect. Remove that definition to manage the limit here.', 'xspeed' ),
138 + $shown
139 + ),
140 + ),
141 + );
142 + }
143 +
62 144 public function rest_routes(): array {
63 145 // Schema POST/GET come from the base; we add three custom routes.
64 146 $default = parent::rest_routes();
65 147 return array_merge(
@@ -89,8 +171,9 @@
89 171 array(
90 172 'name' => 'xspeed db',
91 173 'callback' => array( $this, 'cli_handler' ),
92 174 'shortdesc' => 'Scan or clean WordPress bloat. Use --types=a,b to clean specific categories.',
175 + 'ai_hint' => 'Find and remove database bloat (post revisions, spam comments, expired transients, orphaned metadata). Always `scan` first to see what would go; `clean` is destructive and requires confirmation.',
93 176 'synopsis' => array(
94 177 array(
95 178 'type' => 'positional',
96 179 'name' => 'action',
@@ -107,8 +190,10 @@
107 190 );
108 191 }
109 192
110 193 public function boot(): void {
194 + self::$configured_revisions = defined( 'WP_POST_REVISIONS' ) ? constant( 'WP_POST_REVISIONS' ) : null;
195 + add_filter( 'wp_revisions_to_keep', array( __CLASS__, 'filter_revisions_to_keep' ), 10, 2 );
111 196 add_action( Database_Cleaner::CRON_HOOK, array( Database_Cleaner::class, 'cron_tick' ) );
112 197 add_action( 'update_option_xspeed_module_database', array( $this, 'on_settings_change' ), 10, 2 );
113 198 add_action( 'add_option_xspeed_module_database', array( $this, 'on_settings_added' ), 10, 2 );
114 199 }
@@ -116,11 +201,27 @@
116 201 public function deactivate(): void {
117 202 wp_clear_scheduled_hook( Database_Cleaner::CRON_HOOK );
118 203 }
119 204
205 + /**
206 + * The schedule or the selected types changed: reschedule, and drop the
207 + * scheduled clean's resume point. A cursor left in place would carry on
208 + * deleting a category the user has just switched off. Any other save
209 + * returns early and keeps both. clean() also discards a cursor written for
210 + * a different selection, so this is the belt to that brace.
211 + */
120 212 public function on_settings_change( $old, $new ): void {
213 + // A revision-limit save must not reschedule cleanup: apply_schedule()
214 + // restarts the timer, which pulled a weekly run forward on every save.
215 + $old = is_array( $old ) ? $old : array();
216 + if ( is_array( $new )
217 + && ( $old['schedule'] ?? null ) === ( $new['schedule'] ?? null )
218 + && ( $old['included_types'] ?? null ) === ( $new['included_types'] ?? null ) ) {
219 + return;
220 + }
121 221 $schedule = is_array( $new ) ? (string) ( $new['schedule'] ?? 'manual' ) : 'manual';
122 222 $types = is_array( $new ) && isset( $new['included_types'] ) && is_array( $new['included_types'] ) ? $new['included_types'] : array();
223 + Database_Cleaner::reset_cursor();
123 224 Database_Cleaner::apply_schedule( $schedule, $types );
124 225 }
125 226
126 227 public function on_settings_added( $name, $value ): void {
@@ -125,8 +226,9 @@
125 226
126 227 public function on_settings_added( $name, $value ): void {
127 228 $schedule = is_array( $value ) ? (string) ( $value['schedule'] ?? 'manual' ) : 'manual';
128 229 $types = is_array( $value ) && isset( $value['included_types'] ) && is_array( $value['included_types'] ) ? $value['included_types'] : array();
230 + Database_Cleaner::reset_cursor();
129 231 Database_Cleaner::apply_schedule( $schedule, $types );
130 232 }
131 233
132 234 public function rest_scan( \WP_REST_Request $request ) {
@@ -163,9 +265,9 @@
163 265
164 266 switch ( $action ) {
165 267 case 'scan':
166 268 foreach ( Database_Cleaner::scan() as $key => $row ) {
167 - \WP_CLI::log( sprintf( '%-22s %s — %d rows', $key, $row['label'], $row['count'] ) );
269 + \WP_CLI::log( sprintf( '%-26s %s — %d rows', $key, $row['label'], $row['count'] ) );
168 270 }
169 271 return;
170 272
171 273 case 'clean':
@@ -170,10 +272,21 @@
170 272
171 273 case 'clean':
172 274 $types = isset( $assoc['types'] ) ? array_filter( array_map( 'trim', explode( ',', (string) $assoc['types'] ) ) ) : array_keys( Database_Cleaner::type_labels() );
173 275 $res = Database_Cleaner::clean( $types );
174 - foreach ( $res['cleaned'] as $key => $affected ) {
175 - \WP_CLI::log( sprintf( '%-22s %d removed', $key, $affected ) );
276 + foreach ( $res['skipped'] as $key ) {
277 + \WP_CLI::warning( sprintf( 'Unknown type "%s" skipped. Run `wp xspeed db scan` for the list.', $key ) );
278 + }
279 + foreach ( $res['remaining'] as $key => $left ) {
280 + \WP_CLI::log( sprintf( '%-26s %d removed, %d left', $key, (int) ( $res['cleaned'][ $key ] ?? 0 ), $left ) );
281 + }
282 + $left = array_sum( $res['remaining'] );
283 + if ( $res['partial'] && $left > 0 ) {
284 + // Each run is time-boxed so a large trash can't outlive the
285 + // request. Nothing carries over: the next run starts over
286 + // on whatever still matches.
287 + \WP_CLI::warning( sprintf( 'Stopped at the time limit. %d left — run it again to continue.', $left ) );
288 + return;
176 289 }
177 290 \WP_CLI::success( 'Cleanup complete.' );
178 291 return;
179 292