PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
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 1.1.2 1.1.3 1.1.4 All 33 releases
xspeed / includes / modules / Database / DatabaseModule.php

DatabaseModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.7, at includes/modules/Database/DatabaseModule.php

286 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Database cleanup module — read-first scan + selective cleanup +
4 * OPTIMIZE TABLE + optional auto-cleanup schedule.
5 *
6 * Tier: Free per FEATURES.md Database §1–11. (DB-side activity log
7 * Database §12 stays Pro.)
8 *
9 * @package XSpeed
10 */
11
12 declare(strict_types=1);
13
14 namespace XSpeed\Modules\Database;
15
16 defined( 'ABSPATH' ) || exit;
17
18 use XSpeed\Database_Cleaner;
19 use XSpeed\Module;
20 use XSpeed\Settings_Manager;
21
22 final class DatabaseModule extends Module {
23
24 public const SLUG = 'database';
25 public const TIER = self::TIER_FREE;
26 public const VERSION = '1.0.0';
27
28 public function ui_metadata(): array {
29 return array(
30 'label' => __( 'Database', 'xspeed' ),
31 'icon' => 'Trash2',
32 'description' => __( 'Remove old revisions, spam and other leftovers, and optimize database tables.', 'xspeed' ),
33 'custom_panel' => 'DatabaseCleanerPanel',
34 'group' => 'tools',
35 );
36 }
37
38 public function settings_schema(): array {
39 return array(
40 'schedule' => array(
41 'type' => 'enum',
42 'default' => 'manual',
43 'options' => array( 'manual', 'hourly', 'daily', 'weekly' ),
44 'option_labels' => array(
45 'manual' => 'Manual',
46 'hourly' => 'Hourly',
47 'daily' => 'Daily',
48 'weekly' => 'Weekly',
49 ),
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' ),
52 ),
53 'included_types' => array(
54 'type' => 'list',
55 'default' => array(),
56 'item_type' => 'string',
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 ),
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 ),
82 );
83 }
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
144 public function rest_routes(): array {
145 // Schema POST/GET come from the base; we add three custom routes.
146 $default = parent::rest_routes();
147 return array_merge(
148 $default,
149 array(
150 array(
151 'path' => '/scan',
152 'methods' => 'GET',
153 'callback' => array( $this, 'rest_scan' ),
154 ),
155 array(
156 'path' => '/clean',
157 'methods' => 'POST',
158 'callback' => array( $this, 'rest_clean' ),
159 ),
160 array(
161 'path' => '/optimize',
162 'methods' => 'POST',
163 'callback' => array( $this, 'rest_optimize' ),
164 ),
165 )
166 );
167 }
168
169 public function cli_commands(): array {
170 return array(
171 array(
172 'name' => 'xspeed db',
173 'callback' => array( $this, 'cli_handler' ),
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.',
176 'synopsis' => array(
177 array(
178 'type' => 'positional',
179 'name' => 'action',
180 'options' => array( 'scan', 'clean', 'optimize' ),
181 'optional' => false,
182 ),
183 array(
184 'type' => 'assoc',
185 'name' => 'types',
186 'optional' => true,
187 ),
188 ),
189 ),
190 );
191 }
192
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 );
196 add_action( Database_Cleaner::CRON_HOOK, array( Database_Cleaner::class, 'cron_tick' ) );
197 add_action( 'update_option_xspeed_module_database', array( $this, 'on_settings_change' ), 10, 2 );
198 add_action( 'add_option_xspeed_module_database', array( $this, 'on_settings_added' ), 10, 2 );
199 }
200
201 public function deactivate(): void {
202 wp_clear_scheduled_hook( Database_Cleaner::CRON_HOOK );
203 }
204
205 public function on_settings_change( $old, $new ): void {
206 // A revision-limit save must not reschedule cleanup: apply_schedule()
207 // restarts the timer, which pulled a weekly run forward on every save.
208 $old = is_array( $old ) ? $old : array();
209 if ( is_array( $new )
210 && ( $old['schedule'] ?? null ) === ( $new['schedule'] ?? null )
211 && ( $old['included_types'] ?? null ) === ( $new['included_types'] ?? null ) ) {
212 return;
213 }
214 $schedule = is_array( $new ) ? (string) ( $new['schedule'] ?? 'manual' ) : 'manual';
215 $types = is_array( $new ) && isset( $new['included_types'] ) && is_array( $new['included_types'] ) ? $new['included_types'] : array();
216 Database_Cleaner::apply_schedule( $schedule, $types );
217 }
218
219 public function on_settings_added( $name, $value ): void {
220 $schedule = is_array( $value ) ? (string) ( $value['schedule'] ?? 'manual' ) : 'manual';
221 $types = is_array( $value ) && isset( $value['included_types'] ) && is_array( $value['included_types'] ) ? $value['included_types'] : array();
222 Database_Cleaner::apply_schedule( $schedule, $types );
223 }
224
225 public function rest_scan( \WP_REST_Request $request ) {
226 return rest_ensure_response(
227 array(
228 'scan' => Database_Cleaner::scan(),
229 'labels' => Database_Cleaner::type_labels(),
230 )
231 );
232 }
233
234 public function rest_clean( \WP_REST_Request $request ) {
235 $params = $request->get_json_params();
236 if ( ! is_array( $params ) ) {
237 $params = $request->get_params();
238 }
239 $types = isset( $params['types'] ) && is_array( $params['types'] ) ? $params['types'] : array();
240 if ( empty( $types ) ) {
241 return new \WP_Error( 'xspeed_db_no_types', __( 'Provide at least one cleanup type.', 'xspeed' ), array( 'status' => 400 ) );
242 }
243 return rest_ensure_response( Database_Cleaner::clean( $types ) );
244 }
245
246 public function rest_optimize( \WP_REST_Request $request ) {
247 return rest_ensure_response(
248 array(
249 'optimized' => Database_Cleaner::optimize_tables(),
250 )
251 );
252 }
253
254 public function cli_handler( array $args, array $assoc ): void {
255 $action = $args[0] ?? 'scan';
256
257 switch ( $action ) {
258 case 'scan':
259 foreach ( Database_Cleaner::scan() as $key => $row ) {
260 \WP_CLI::log( sprintf( '%-22s %s — %d rows', $key, $row['label'], $row['count'] ) );
261 }
262 return;
263
264 case 'clean':
265 $types = isset( $assoc['types'] ) ? array_filter( array_map( 'trim', explode( ',', (string) $assoc['types'] ) ) ) : array_keys( Database_Cleaner::type_labels() );
266 $res = Database_Cleaner::clean( $types );
267 foreach ( $res['cleaned'] as $key => $affected ) {
268 \WP_CLI::log( sprintf( '%-22s %d removed', $key, $affected ) );
269 }
270 \WP_CLI::success( 'Cleanup complete.' );
271 return;
272
273 case 'optimize':
274 $res = Database_Cleaner::optimize_tables();
275 foreach ( $res as $table => $ok ) {
276 \WP_CLI::log( sprintf( '%-40s %s', $table, $ok ? 'ok' : 'failed' ) );
277 }
278 \WP_CLI::success( 'Tables optimized.' );
279 return;
280
281 default:
282 \WP_CLI::error( "Unknown action: $action" );
283 }
284 }
285 }
286