PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
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 1.1.5 All 32 releases
xspeed / includes / modules / Database / DatabaseModule.php

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

277 lines 9.3 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' => __( 'Scan + clean WordPress bloat (revisions, spam, transients, orphan meta) and optimize tables.', 'xspeed' ),
33 'custom_panel' => 'DatabaseCleanerPanel',
34 );
35 }
36
37 public function settings_schema(): array {
38 return array(
39 'schedule' => array(
40 'type' => 'enum',
41 'default' => 'manual',
42 'options' => array( 'manual', 'hourly', 'daily', 'weekly' ),
43 'option_labels' => array(
44 'manual' => 'Manual',
45 'hourly' => 'Hourly',
46 'daily' => 'Daily',
47 'weekly' => 'Weekly',
48 ),
49 'label' => __( 'Auto-Cleanup Schedule', 'xspeed' ),
50 'description' => __( 'How often to run cleanup automatically. Manual means cleanup only runs when you press the button.', 'xspeed' ),
51 ),
52 'included_types' => array(
53 'type' => 'list',
54 'default' => array(),
55 'item_type' => 'string',
56 'label' => __( 'Auto-Cleanup Types', 'xspeed' ),
57 'description' => __( 'Which cleanup categories run on the schedule above. Leave empty to keep auto-cleanup disabled even if a schedule is set.', 'xspeed' ),
58 ),
59 'limit_revisions' => array(
60 'type' => 'bool',
61 'default' => false,
62 'label' => __( 'Limit Post Revisions', 'xspeed' ),
63 'description' => __( 'Cap how many revisions WordPress keeps for each post. Cleanup removes revisions that already exist; this stops them piling up again.', 'xspeed' ),
64 ),
65 'revisions_to_keep' => array(
66 'type' => 'int',
67 'default' => 5,
68 'min' => 0,
69 'max' => 500,
70 'label' => __( 'Revisions to Keep', 'xspeed' ),
71 'description' => __( 'Used when Limit Post Revisions is on. 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' ),
72 ),
73 );
74 }
75
76 /**
77 * WP_POST_REVISIONS as wp-config.php (or an earlier plugin) set it, or
78 * null when nobody did. Read in boot(), on plugins_loaded: after that,
79 * core defines it as `true` itself, so defined() stops telling the two
80 * cases apart.
81 *
82 * Not the schema's `constants` pinning (#398): that tests defined() at
83 * request time, when core's own default makes it always true.
84 *
85 * @var bool|int|null
86 */
87 private static $configured_revisions = null;
88
89 /**
90 * @param int $num Revisions WordPress would keep; -1 means all.
91 * @param \WP_Post $post
92 * @return int
93 */
94 public static function filter_revisions_to_keep( $num, $post ) {
95 // A wp-config.php value is the site owner's decision; leave it alone.
96 if ( null !== self::$configured_revisions ) {
97 return $num;
98 }
99 // Core already answered 0 for post types without revision support.
100 if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'revisions' ) ) {
101 return $num;
102 }
103 $opts = Settings_Manager::get( self::SLUG );
104 if ( empty( $opts['limit_revisions'] ) ) {
105 return $num;
106 }
107 return max( 0, (int) ( $opts['revisions_to_keep'] ?? 5 ) );
108 }
109
110 public function ui_notices(): array {
111 if ( null === self::$configured_revisions ) {
112 return array();
113 }
114 $value = self::$configured_revisions;
115 if ( true === $value ) {
116 $shown = 'true';
117 } elseif ( false === $value ) {
118 $shown = 'false';
119 } else {
120 $shown = (string) (int) $value;
121 }
122 return array(
123 array(
124 'tone' => 'info',
125 'title' => __( 'The revision limit is set outside xSpeed', 'xspeed' ),
126 'body' => sprintf(
127 /* translators: %s: value of WP_POST_REVISIONS, e.g. 5 or false. */
128 __( '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' ),
129 $shown
130 ),
131 ),
132 );
133 }
134
135 public function rest_routes(): array {
136 // Schema POST/GET come from the base; we add three custom routes.
137 $default = parent::rest_routes();
138 return array_merge(
139 $default,
140 array(
141 array(
142 'path' => '/scan',
143 'methods' => 'GET',
144 'callback' => array( $this, 'rest_scan' ),
145 ),
146 array(
147 'path' => '/clean',
148 'methods' => 'POST',
149 'callback' => array( $this, 'rest_clean' ),
150 ),
151 array(
152 'path' => '/optimize',
153 'methods' => 'POST',
154 'callback' => array( $this, 'rest_optimize' ),
155 ),
156 )
157 );
158 }
159
160 public function cli_commands(): array {
161 return array(
162 array(
163 'name' => 'xspeed db',
164 'callback' => array( $this, 'cli_handler' ),
165 'shortdesc' => 'Scan or clean WordPress bloat. Use --types=a,b to clean specific categories.',
166 '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.',
167 'synopsis' => array(
168 array(
169 'type' => 'positional',
170 'name' => 'action',
171 'options' => array( 'scan', 'clean', 'optimize' ),
172 'optional' => false,
173 ),
174 array(
175 'type' => 'assoc',
176 'name' => 'types',
177 'optional' => true,
178 ),
179 ),
180 ),
181 );
182 }
183
184 public function boot(): void {
185 self::$configured_revisions = defined( 'WP_POST_REVISIONS' ) ? constant( 'WP_POST_REVISIONS' ) : null;
186 add_filter( 'wp_revisions_to_keep', array( __CLASS__, 'filter_revisions_to_keep' ), 10, 2 );
187 add_action( Database_Cleaner::CRON_HOOK, array( Database_Cleaner::class, 'cron_tick' ) );
188 add_action( 'update_option_xspeed_module_database', array( $this, 'on_settings_change' ), 10, 2 );
189 add_action( 'add_option_xspeed_module_database', array( $this, 'on_settings_added' ), 10, 2 );
190 }
191
192 public function deactivate(): void {
193 wp_clear_scheduled_hook( Database_Cleaner::CRON_HOOK );
194 }
195
196 public function on_settings_change( $old, $new ): void {
197 // A revision-limit save must not reschedule cleanup: apply_schedule()
198 // restarts the timer, which pulled a weekly run forward on every save.
199 $old = is_array( $old ) ? $old : array();
200 if ( is_array( $new )
201 && ( $old['schedule'] ?? null ) === ( $new['schedule'] ?? null )
202 && ( $old['included_types'] ?? null ) === ( $new['included_types'] ?? null ) ) {
203 return;
204 }
205 $schedule = is_array( $new ) ? (string) ( $new['schedule'] ?? 'manual' ) : 'manual';
206 $types = is_array( $new ) && isset( $new['included_types'] ) && is_array( $new['included_types'] ) ? $new['included_types'] : array();
207 Database_Cleaner::apply_schedule( $schedule, $types );
208 }
209
210 public function on_settings_added( $name, $value ): void {
211 $schedule = is_array( $value ) ? (string) ( $value['schedule'] ?? 'manual' ) : 'manual';
212 $types = is_array( $value ) && isset( $value['included_types'] ) && is_array( $value['included_types'] ) ? $value['included_types'] : array();
213 Database_Cleaner::apply_schedule( $schedule, $types );
214 }
215
216 public function rest_scan( \WP_REST_Request $request ) {
217 return rest_ensure_response(
218 array(
219 'scan' => Database_Cleaner::scan(),
220 'labels' => Database_Cleaner::type_labels(),
221 )
222 );
223 }
224
225 public function rest_clean( \WP_REST_Request $request ) {
226 $params = $request->get_json_params();
227 if ( ! is_array( $params ) ) {
228 $params = $request->get_params();
229 }
230 $types = isset( $params['types'] ) && is_array( $params['types'] ) ? $params['types'] : array();
231 if ( empty( $types ) ) {
232 return new \WP_Error( 'xspeed_db_no_types', __( 'Provide at least one cleanup type.', 'xspeed' ), array( 'status' => 400 ) );
233 }
234 return rest_ensure_response( Database_Cleaner::clean( $types ) );
235 }
236
237 public function rest_optimize( \WP_REST_Request $request ) {
238 return rest_ensure_response(
239 array(
240 'optimized' => Database_Cleaner::optimize_tables(),
241 )
242 );
243 }
244
245 public function cli_handler( array $args, array $assoc ): void {
246 $action = $args[0] ?? 'scan';
247
248 switch ( $action ) {
249 case 'scan':
250 foreach ( Database_Cleaner::scan() as $key => $row ) {
251 \WP_CLI::log( sprintf( '%-22s %s — %d rows', $key, $row['label'], $row['count'] ) );
252 }
253 return;
254
255 case 'clean':
256 $types = isset( $assoc['types'] ) ? array_filter( array_map( 'trim', explode( ',', (string) $assoc['types'] ) ) ) : array_keys( Database_Cleaner::type_labels() );
257 $res = Database_Cleaner::clean( $types );
258 foreach ( $res['cleaned'] as $key => $affected ) {
259 \WP_CLI::log( sprintf( '%-22s %d removed', $key, $affected ) );
260 }
261 \WP_CLI::success( 'Cleanup complete.' );
262 return;
263
264 case 'optimize':
265 $res = Database_Cleaner::optimize_tables();
266 foreach ( $res as $table => $ok ) {
267 \WP_CLI::log( sprintf( '%-40s %s', $table, $ok ? 'ok' : 'failed' ) );
268 }
269 \WP_CLI::success( 'Tables optimized.' );
270 return;
271
272 default:
273 \WP_CLI::error( "Unknown action: $action" );
274 }
275 }
276 }
277