@adsim/wordpress-mcp-server 4.6.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/README.md +851 -499
- package/companion/mcp-diagnostics.php +1184 -0
- package/dxt/manifest.json +715 -98
- package/index.js +166 -4786
- package/package.json +14 -6
- package/src/data/plugin-performance-data.json +59 -0
- package/src/shared/api.js +79 -0
- package/src/shared/audit.js +39 -0
- package/src/shared/context.js +15 -0
- package/src/shared/governance.js +98 -0
- package/src/shared/utils.js +148 -0
- package/src/tools/comments.js +50 -0
- package/src/tools/content.js +353 -0
- package/src/tools/core.js +114 -0
- package/src/tools/editorial.js +634 -0
- package/src/tools/fse.js +370 -0
- package/src/tools/health.js +160 -0
- package/src/tools/index.js +96 -0
- package/src/tools/intelligence.js +2082 -0
- package/src/tools/links.js +118 -0
- package/src/tools/media.js +71 -0
- package/src/tools/performance.js +219 -0
- package/src/tools/plugins.js +368 -0
- package/src/tools/schema.js +417 -0
- package/src/tools/security.js +590 -0
- package/src/tools/seo.js +1633 -0
- package/src/tools/taxonomy.js +115 -0
- package/src/tools/users.js +188 -0
- package/src/tools/woocommerce.js +1008 -0
- package/src/tools/workflow.js +409 -0
- package/src/transport/http.js +39 -0
- package/tests/unit/helpers/pagination.test.js +43 -0
- package/tests/unit/tools/bulkUpdate.test.js +188 -0
- package/tests/unit/tools/diagnostics.test.js +397 -0
- package/tests/unit/tools/dynamicFiltering.test.js +100 -8
- package/tests/unit/tools/editorialIntelligence.test.js +817 -0
- package/tests/unit/tools/fse.test.js +548 -0
- package/tests/unit/tools/multilingual.test.js +653 -0
- package/tests/unit/tools/performance.test.js +351 -0
- package/tests/unit/tools/runWorkflow.test.js +150 -0
- package/tests/unit/tools/schema.test.js +477 -0
- package/tests/unit/tools/security.test.js +695 -0
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/users.crud.test.js +399 -0
- package/tests/unit/tools/validateBlocks.test.js +186 -0
- package/tests/unit/tools/visualStaging.test.js +271 -0
- package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Plugin Name: MCP Diagnostics Companion
|
|
4
|
+
* Description: Exposes secure REST endpoints for WordPress MCP Server diagnostics (debug log, cron events, transients, hooks).
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Requires PHP: 7.4
|
|
7
|
+
* Author: AdSim
|
|
8
|
+
* License: MIT
|
|
9
|
+
*
|
|
10
|
+
* Installation: Copy this file to wp-content/mu-plugins/mcp-diagnostics.php
|
|
11
|
+
*
|
|
12
|
+
* Security: All endpoints require 'manage_options' capability (Administrator).
|
|
13
|
+
* Schema endpoints require 'edit_posts' capability.
|
|
14
|
+
* Read endpoints are read-only; schema POST/DELETE respect WP_READ_ONLY.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
if ( ! defined( 'ABSPATH' ) ) {
|
|
18
|
+
exit;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
add_action( 'rest_api_init', 'mcp_diagnostics_register_routes' );
|
|
22
|
+
add_action( 'wp_head', 'mcp_schema_output_jsonld', 1 );
|
|
23
|
+
|
|
24
|
+
function mcp_diagnostics_register_routes() {
|
|
25
|
+
$namespace = 'mcp-diagnostics/v1';
|
|
26
|
+
|
|
27
|
+
register_rest_route( $namespace, '/debug-log', array(
|
|
28
|
+
'methods' => 'GET',
|
|
29
|
+
'callback' => 'mcp_diagnostics_debug_log',
|
|
30
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
31
|
+
'args' => array(
|
|
32
|
+
'lines' => array(
|
|
33
|
+
'type' => 'integer',
|
|
34
|
+
'default' => 100,
|
|
35
|
+
'minimum' => 1,
|
|
36
|
+
'maximum' => 500,
|
|
37
|
+
'sanitize_callback' => 'absint',
|
|
38
|
+
),
|
|
39
|
+
'level' => array(
|
|
40
|
+
'type' => 'string',
|
|
41
|
+
'default' => 'all',
|
|
42
|
+
'enum' => array( 'error', 'warning', 'notice', 'all' ),
|
|
43
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
) );
|
|
47
|
+
|
|
48
|
+
register_rest_route( $namespace, '/cron-events', array(
|
|
49
|
+
'methods' => 'GET',
|
|
50
|
+
'callback' => 'mcp_diagnostics_cron_events',
|
|
51
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
52
|
+
) );
|
|
53
|
+
|
|
54
|
+
register_rest_route( $namespace, '/transients', array(
|
|
55
|
+
'methods' => 'GET',
|
|
56
|
+
'callback' => 'mcp_diagnostics_transients',
|
|
57
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
58
|
+
'args' => array(
|
|
59
|
+
'filter' => array(
|
|
60
|
+
'type' => 'string',
|
|
61
|
+
'default' => 'all',
|
|
62
|
+
'enum' => array( 'all', 'expired', 'active' ),
|
|
63
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
64
|
+
),
|
|
65
|
+
'search' => array(
|
|
66
|
+
'type' => 'string',
|
|
67
|
+
'default' => '',
|
|
68
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
69
|
+
),
|
|
70
|
+
'per_page' => array(
|
|
71
|
+
'type' => 'integer',
|
|
72
|
+
'default' => 50,
|
|
73
|
+
'minimum' => 1,
|
|
74
|
+
'maximum' => 200,
|
|
75
|
+
'sanitize_callback' => 'absint',
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
) );
|
|
79
|
+
|
|
80
|
+
register_rest_route( $namespace, '/database-bloat', array(
|
|
81
|
+
'methods' => 'GET',
|
|
82
|
+
'callback' => 'mcp_diagnostics_database_bloat',
|
|
83
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
84
|
+
) );
|
|
85
|
+
|
|
86
|
+
register_rest_route( $namespace, '/password-reset', array(
|
|
87
|
+
'methods' => 'POST',
|
|
88
|
+
'callback' => 'mcp_diagnostics_password_reset',
|
|
89
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
90
|
+
'args' => array(
|
|
91
|
+
'user_id' => array(
|
|
92
|
+
'type' => 'integer',
|
|
93
|
+
'required' => true,
|
|
94
|
+
'sanitize_callback' => 'absint',
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
) );
|
|
98
|
+
|
|
99
|
+
// ── Schema.org JSON-LD management ──
|
|
100
|
+
register_rest_route( $namespace, '/schema/(?P<post_id>\d+)', array(
|
|
101
|
+
array(
|
|
102
|
+
'methods' => 'GET',
|
|
103
|
+
'callback' => 'mcp_diagnostics_schema_get',
|
|
104
|
+
'permission_callback' => 'mcp_diagnostics_schema_permission_check',
|
|
105
|
+
'args' => array(
|
|
106
|
+
'post_id' => array(
|
|
107
|
+
'type' => 'integer',
|
|
108
|
+
'required' => true,
|
|
109
|
+
'sanitize_callback' => 'absint',
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
array(
|
|
114
|
+
'methods' => 'POST',
|
|
115
|
+
'callback' => 'mcp_diagnostics_schema_set',
|
|
116
|
+
'permission_callback' => 'mcp_diagnostics_schema_permission_check',
|
|
117
|
+
'args' => array(
|
|
118
|
+
'post_id' => array(
|
|
119
|
+
'type' => 'integer',
|
|
120
|
+
'required' => true,
|
|
121
|
+
'sanitize_callback' => 'absint',
|
|
122
|
+
),
|
|
123
|
+
'schema' => array(
|
|
124
|
+
'type' => 'string',
|
|
125
|
+
'required' => true,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
array(
|
|
130
|
+
'methods' => 'DELETE',
|
|
131
|
+
'callback' => 'mcp_diagnostics_schema_delete',
|
|
132
|
+
'permission_callback' => 'mcp_diagnostics_schema_permission_check',
|
|
133
|
+
'args' => array(
|
|
134
|
+
'post_id' => array(
|
|
135
|
+
'type' => 'integer',
|
|
136
|
+
'required' => true,
|
|
137
|
+
'sanitize_callback' => 'absint',
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
) );
|
|
142
|
+
|
|
143
|
+
// ── Polylang Free REST fallback ──
|
|
144
|
+
register_rest_route( $namespace, '/polylang/languages', array(
|
|
145
|
+
'methods' => 'GET',
|
|
146
|
+
'callback' => 'mcp_diagnostics_polylang_languages',
|
|
147
|
+
'permission_callback' => '__return_true',
|
|
148
|
+
) );
|
|
149
|
+
|
|
150
|
+
register_rest_route( $namespace, '/polylang/translations/(?P<post_id>\d+)', array(
|
|
151
|
+
'methods' => 'GET',
|
|
152
|
+
'callback' => 'mcp_diagnostics_polylang_translations',
|
|
153
|
+
'permission_callback' => '__return_true',
|
|
154
|
+
'args' => array(
|
|
155
|
+
'post_id' => array(
|
|
156
|
+
'type' => 'integer',
|
|
157
|
+
'required' => true,
|
|
158
|
+
'sanitize_callback' => 'absint',
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
) );
|
|
162
|
+
|
|
163
|
+
// ── WooCommerce Intelligence endpoints ──
|
|
164
|
+
register_rest_route( $namespace, '/wc-abandoned-carts', array(
|
|
165
|
+
'methods' => 'GET',
|
|
166
|
+
'callback' => 'mcp_diagnostics_wc_abandoned_carts',
|
|
167
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
168
|
+
'args' => array(
|
|
169
|
+
'days' => array(
|
|
170
|
+
'type' => 'integer',
|
|
171
|
+
'default' => 30,
|
|
172
|
+
'minimum' => 1,
|
|
173
|
+
'maximum' => 365,
|
|
174
|
+
'sanitize_callback' => 'absint',
|
|
175
|
+
),
|
|
176
|
+
'min_value' => array(
|
|
177
|
+
'type' => 'number',
|
|
178
|
+
'default' => 0,
|
|
179
|
+
'sanitize_callback' => 'floatval',
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
) );
|
|
183
|
+
|
|
184
|
+
// ── Security Audit endpoints ──
|
|
185
|
+
register_rest_route( $namespace, '/user-activity', array(
|
|
186
|
+
'methods' => 'GET',
|
|
187
|
+
'callback' => 'mcp_diagnostics_user_activity',
|
|
188
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
189
|
+
) );
|
|
190
|
+
|
|
191
|
+
register_rest_route( $namespace, '/file-permissions', array(
|
|
192
|
+
'methods' => 'GET',
|
|
193
|
+
'callback' => 'mcp_diagnostics_file_permissions',
|
|
194
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
195
|
+
) );
|
|
196
|
+
|
|
197
|
+
register_rest_route( $namespace, '/modified-files', array(
|
|
198
|
+
'methods' => 'GET',
|
|
199
|
+
'callback' => 'mcp_diagnostics_modified_files',
|
|
200
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
201
|
+
'args' => array(
|
|
202
|
+
'days' => array(
|
|
203
|
+
'type' => 'integer',
|
|
204
|
+
'default' => 7,
|
|
205
|
+
'minimum' => 1,
|
|
206
|
+
'maximum' => 90,
|
|
207
|
+
'sanitize_callback' => 'absint',
|
|
208
|
+
),
|
|
209
|
+
'paths' => array(
|
|
210
|
+
'type' => 'string',
|
|
211
|
+
'default' => 'plugins,themes',
|
|
212
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
213
|
+
),
|
|
214
|
+
'extensions' => array(
|
|
215
|
+
'type' => 'string',
|
|
216
|
+
'default' => '.php,.js',
|
|
217
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
) );
|
|
221
|
+
|
|
222
|
+
register_rest_route( $namespace, '/hooks', array(
|
|
223
|
+
'methods' => 'GET',
|
|
224
|
+
'callback' => 'mcp_diagnostics_hooks',
|
|
225
|
+
'permission_callback' => 'mcp_diagnostics_permission_check',
|
|
226
|
+
'args' => array(
|
|
227
|
+
'type' => array(
|
|
228
|
+
'type' => 'string',
|
|
229
|
+
'default' => 'all',
|
|
230
|
+
'enum' => array( 'actions', 'filters', 'all' ),
|
|
231
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
232
|
+
),
|
|
233
|
+
'search' => array(
|
|
234
|
+
'type' => 'string',
|
|
235
|
+
'default' => '',
|
|
236
|
+
'sanitize_callback' => 'sanitize_text_field',
|
|
237
|
+
),
|
|
238
|
+
'per_page' => array(
|
|
239
|
+
'type' => 'integer',
|
|
240
|
+
'default' => 50,
|
|
241
|
+
'minimum' => 1,
|
|
242
|
+
'maximum' => 200,
|
|
243
|
+
'sanitize_callback' => 'absint',
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
) );
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Permission check: require manage_options (Administrator).
|
|
251
|
+
*/
|
|
252
|
+
function mcp_diagnostics_permission_check( $request ) {
|
|
253
|
+
if ( ! current_user_can( 'manage_options' ) ) {
|
|
254
|
+
return new WP_Error(
|
|
255
|
+
'rest_forbidden',
|
|
256
|
+
__( 'MCP Diagnostics requires Administrator privileges.' ),
|
|
257
|
+
array( 'status' => 403 )
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* GET /mcp-diagnostics/v1/debug-log
|
|
265
|
+
* Reads the last N lines of wp-content/debug.log.
|
|
266
|
+
*/
|
|
267
|
+
function mcp_diagnostics_debug_log( $request ) {
|
|
268
|
+
$max_lines = $request->get_param( 'lines' );
|
|
269
|
+
$level = $request->get_param( 'level' );
|
|
270
|
+
|
|
271
|
+
$log_file = WP_CONTENT_DIR . '/debug.log';
|
|
272
|
+
|
|
273
|
+
if ( ! file_exists( $log_file ) ) {
|
|
274
|
+
return new WP_REST_Response( array(
|
|
275
|
+
'lines' => array(),
|
|
276
|
+
'total_lines' => 0,
|
|
277
|
+
'file_size' => 0,
|
|
278
|
+
'last_modified' => null,
|
|
279
|
+
'message' => 'debug.log not found. Ensure WP_DEBUG and WP_DEBUG_LOG are enabled in wp-config.php.',
|
|
280
|
+
), 200 );
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
$file_size = filesize( $log_file );
|
|
284
|
+
$last_modified = gmdate( 'c', filemtime( $log_file ) );
|
|
285
|
+
|
|
286
|
+
// Read last N lines efficiently via tail-like approach
|
|
287
|
+
$all_lines = array();
|
|
288
|
+
$handle = fopen( $log_file, 'r' );
|
|
289
|
+
if ( $handle ) {
|
|
290
|
+
// For files > 1MB, seek from end
|
|
291
|
+
$chunk_size = 65536;
|
|
292
|
+
$buffer = '';
|
|
293
|
+
|
|
294
|
+
if ( $file_size > $chunk_size ) {
|
|
295
|
+
$pos = $file_size;
|
|
296
|
+
while ( $pos > 0 && count( $all_lines ) < $max_lines ) {
|
|
297
|
+
$read_size = min( $chunk_size, $pos );
|
|
298
|
+
$pos -= $read_size;
|
|
299
|
+
fseek( $handle, $pos );
|
|
300
|
+
$buffer = fread( $handle, $read_size ) . $buffer;
|
|
301
|
+
$all_lines = explode( "\n", $buffer );
|
|
302
|
+
}
|
|
303
|
+
$all_lines = array_slice( $all_lines, -$max_lines );
|
|
304
|
+
} else {
|
|
305
|
+
$content = fread( $handle, $file_size );
|
|
306
|
+
$all_lines = explode( "\n", $content );
|
|
307
|
+
$all_lines = array_slice( $all_lines, -$max_lines );
|
|
308
|
+
}
|
|
309
|
+
fclose( $handle );
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Filter empty lines
|
|
313
|
+
$all_lines = array_values( array_filter( $all_lines, function( $line ) {
|
|
314
|
+
return trim( $line ) !== '';
|
|
315
|
+
} ) );
|
|
316
|
+
|
|
317
|
+
// Filter by level
|
|
318
|
+
if ( $level !== 'all' ) {
|
|
319
|
+
$level_upper = strtoupper( $level );
|
|
320
|
+
$all_lines = array_values( array_filter( $all_lines, function( $line ) use ( $level_upper ) {
|
|
321
|
+
// Match common PHP error format: "PHP Warning:", "PHP Fatal error:", "PHP Notice:"
|
|
322
|
+
$line_upper = strtoupper( $line );
|
|
323
|
+
switch ( $level_upper ) {
|
|
324
|
+
case 'ERROR':
|
|
325
|
+
return strpos( $line_upper, 'ERROR' ) !== false || strpos( $line_upper, 'FATAL' ) !== false;
|
|
326
|
+
case 'WARNING':
|
|
327
|
+
return strpos( $line_upper, 'WARNING' ) !== false;
|
|
328
|
+
case 'NOTICE':
|
|
329
|
+
return strpos( $line_upper, 'NOTICE' ) !== false || strpos( $line_upper, 'DEPRECATED' ) !== false;
|
|
330
|
+
default:
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
} ) );
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return new WP_REST_Response( array(
|
|
337
|
+
'lines' => $all_lines,
|
|
338
|
+
'total_lines' => count( $all_lines ),
|
|
339
|
+
'file_size' => $file_size,
|
|
340
|
+
'last_modified' => $last_modified,
|
|
341
|
+
), 200 );
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* GET /mcp-diagnostics/v1/cron-events
|
|
346
|
+
* Lists all scheduled WP-Cron events.
|
|
347
|
+
*/
|
|
348
|
+
function mcp_diagnostics_cron_events( $request ) {
|
|
349
|
+
$crons = _get_cron_array();
|
|
350
|
+
$events = array();
|
|
351
|
+
|
|
352
|
+
if ( ! is_array( $crons ) ) {
|
|
353
|
+
return new WP_REST_Response( array( 'events' => array() ), 200 );
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
$schedules = wp_get_schedules();
|
|
357
|
+
|
|
358
|
+
foreach ( $crons as $timestamp => $cron_group ) {
|
|
359
|
+
foreach ( $cron_group as $hook => $hook_events ) {
|
|
360
|
+
foreach ( $hook_events as $key => $event ) {
|
|
361
|
+
$schedule_name = $event['schedule'] ?? false;
|
|
362
|
+
$interval = 0;
|
|
363
|
+
if ( $schedule_name && isset( $schedules[ $schedule_name ] ) ) {
|
|
364
|
+
$interval = $schedules[ $schedule_name ]['interval'];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
$events[] = array(
|
|
368
|
+
'hook' => $hook,
|
|
369
|
+
'args' => $event['args'] ?? array(),
|
|
370
|
+
'schedule' => $schedule_name ?: 'single',
|
|
371
|
+
'interval' => $interval ?: null,
|
|
372
|
+
'next_run' => (int) $timestamp,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Sort by next_run ascending
|
|
379
|
+
usort( $events, function( $a, $b ) {
|
|
380
|
+
return $a['next_run'] - $b['next_run'];
|
|
381
|
+
} );
|
|
382
|
+
|
|
383
|
+
return new WP_REST_Response( array( 'events' => $events ), 200 );
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* GET /mcp-diagnostics/v1/transients
|
|
388
|
+
* Lists database transients.
|
|
389
|
+
*/
|
|
390
|
+
function mcp_diagnostics_transients( $request ) {
|
|
391
|
+
global $wpdb;
|
|
392
|
+
|
|
393
|
+
$filter = $request->get_param( 'filter' );
|
|
394
|
+
$search = $request->get_param( 'search' );
|
|
395
|
+
$per_page = $request->get_param( 'per_page' );
|
|
396
|
+
$now = time();
|
|
397
|
+
|
|
398
|
+
$where = array( "option_name LIKE '_transient_%'" );
|
|
399
|
+
$where[] = "option_name NOT LIKE '_transient_timeout_%'";
|
|
400
|
+
|
|
401
|
+
if ( $search ) {
|
|
402
|
+
$like = '%' . $wpdb->esc_like( $search ) . '%';
|
|
403
|
+
$where[] = $wpdb->prepare( 'option_name LIKE %s', $like );
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
$sql = "SELECT option_name, LENGTH(option_value) as value_size FROM {$wpdb->options} WHERE " . implode( ' AND ', $where ) . " LIMIT {$per_page}";
|
|
407
|
+
|
|
408
|
+
$rows = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
|
409
|
+
$transients = array();
|
|
410
|
+
|
|
411
|
+
foreach ( $rows as $row ) {
|
|
412
|
+
$key = str_replace( '_transient_', '', $row->option_name );
|
|
413
|
+
$timeout_row = $wpdb->get_var( $wpdb->prepare(
|
|
414
|
+
"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
|
|
415
|
+
'_transient_timeout_' . $key
|
|
416
|
+
) );
|
|
417
|
+
$expiration = $timeout_row ? (int) $timeout_row : 0;
|
|
418
|
+
$is_expired = $expiration > 0 && $expiration < $now;
|
|
419
|
+
|
|
420
|
+
if ( $filter === 'expired' && ! $is_expired ) continue;
|
|
421
|
+
if ( $filter === 'active' && $is_expired ) continue;
|
|
422
|
+
|
|
423
|
+
$transients[] = array(
|
|
424
|
+
'key' => $key,
|
|
425
|
+
'expiration' => $expiration ?: null,
|
|
426
|
+
'size_bytes' => (int) $row->value_size,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return new WP_REST_Response( array( 'transients' => $transients ), 200 );
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* POST /mcp-diagnostics/v1/password-reset
|
|
435
|
+
* Triggers a password reset email for a user.
|
|
436
|
+
*/
|
|
437
|
+
/**
|
|
438
|
+
* GET /mcp-diagnostics/v1/database-bloat
|
|
439
|
+
* Analyzes database bloat: revisions, expired transients, table sizes.
|
|
440
|
+
*/
|
|
441
|
+
function mcp_diagnostics_database_bloat( $request ) {
|
|
442
|
+
global $wpdb;
|
|
443
|
+
|
|
444
|
+
// Count revisions
|
|
445
|
+
$revision_count = (int) $wpdb->get_var(
|
|
446
|
+
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'"
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// Count auto-drafts
|
|
450
|
+
$autodraft_count = (int) $wpdb->get_var(
|
|
451
|
+
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'auto-draft'"
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Count trashed posts
|
|
455
|
+
$trash_count = (int) $wpdb->get_var(
|
|
456
|
+
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'trash'"
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// Count spam comments
|
|
460
|
+
$spam_comments = (int) $wpdb->get_var(
|
|
461
|
+
"SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
// Count trashed comments
|
|
465
|
+
$trash_comments = (int) $wpdb->get_var(
|
|
466
|
+
"SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_approved = 'trash'"
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Count expired transients
|
|
470
|
+
$now = time();
|
|
471
|
+
$expired_transients = (int) $wpdb->get_var( $wpdb->prepare(
|
|
472
|
+
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s AND option_value < %d",
|
|
473
|
+
$wpdb->esc_like( '_transient_timeout_' ) . '%',
|
|
474
|
+
$now
|
|
475
|
+
) );
|
|
476
|
+
|
|
477
|
+
// Total transients
|
|
478
|
+
$total_transients = (int) $wpdb->get_var(
|
|
479
|
+
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%'"
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// Table sizes
|
|
483
|
+
$db_name = DB_NAME;
|
|
484
|
+
$tables = $wpdb->get_results( $wpdb->prepare(
|
|
485
|
+
"SELECT table_name AS name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb, table_rows AS rows
|
|
486
|
+
FROM information_schema.tables WHERE table_schema = %s ORDER BY (data_length + index_length) DESC LIMIT 20",
|
|
487
|
+
$db_name
|
|
488
|
+
) );
|
|
489
|
+
|
|
490
|
+
$total_size_mb = 0;
|
|
491
|
+
$table_report = array();
|
|
492
|
+
foreach ( $tables as $t ) {
|
|
493
|
+
$total_size_mb += (float) $t->size_mb;
|
|
494
|
+
$table_report[] = array(
|
|
495
|
+
'table' => $t->name,
|
|
496
|
+
'size_mb' => (float) $t->size_mb,
|
|
497
|
+
'rows' => (int) $t->rows,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Orphan postmeta
|
|
502
|
+
$orphan_postmeta = (int) $wpdb->get_var(
|
|
503
|
+
"SELECT COUNT(*) FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.ID IS NULL"
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Build recommendations
|
|
507
|
+
$recommendations = array();
|
|
508
|
+
if ( $revision_count > 100 ) {
|
|
509
|
+
$recommendations[] = "Delete old revisions ($revision_count found). Consider WP_POST_REVISIONS constant to limit.";
|
|
510
|
+
}
|
|
511
|
+
if ( $expired_transients > 50 ) {
|
|
512
|
+
$recommendations[] = "Clean expired transients ($expired_transients found). Use wp transient delete --expired via WP-CLI.";
|
|
513
|
+
}
|
|
514
|
+
if ( $autodraft_count > 20 ) {
|
|
515
|
+
$recommendations[] = "Remove auto-drafts ($autodraft_count found).";
|
|
516
|
+
}
|
|
517
|
+
if ( $spam_comments > 100 ) {
|
|
518
|
+
$recommendations[] = "Empty spam comments ($spam_comments found).";
|
|
519
|
+
}
|
|
520
|
+
if ( $orphan_postmeta > 100 ) {
|
|
521
|
+
$recommendations[] = "Clean orphan postmeta ($orphan_postmeta rows with no parent post).";
|
|
522
|
+
}
|
|
523
|
+
if ( $trash_count > 50 ) {
|
|
524
|
+
$recommendations[] = "Empty trash ($trash_count trashed posts).";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return new WP_REST_Response( array(
|
|
528
|
+
'revisions' => $revision_count,
|
|
529
|
+
'auto_drafts' => $autodraft_count,
|
|
530
|
+
'trashed_posts' => $trash_count,
|
|
531
|
+
'spam_comments' => $spam_comments,
|
|
532
|
+
'trashed_comments' => $trash_comments,
|
|
533
|
+
'transients_total' => $total_transients,
|
|
534
|
+
'transients_expired' => $expired_transients,
|
|
535
|
+
'orphan_postmeta' => $orphan_postmeta,
|
|
536
|
+
'database_size_mb' => round( $total_size_mb, 2 ),
|
|
537
|
+
'tables' => $table_report,
|
|
538
|
+
'recommendations' => $recommendations,
|
|
539
|
+
), 200 );
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function mcp_diagnostics_password_reset( $request ) {
|
|
543
|
+
$user_id = $request->get_param( 'user_id' );
|
|
544
|
+
$user = get_userdata( $user_id );
|
|
545
|
+
|
|
546
|
+
if ( ! $user ) {
|
|
547
|
+
return new WP_Error(
|
|
548
|
+
'user_not_found',
|
|
549
|
+
__( 'User not found.' ),
|
|
550
|
+
array( 'status' => 404 )
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Generate reset key and send email
|
|
555
|
+
$key = get_password_reset_key( $user );
|
|
556
|
+
if ( is_wp_error( $key ) ) {
|
|
557
|
+
return new WP_Error(
|
|
558
|
+
'reset_key_error',
|
|
559
|
+
__( 'Could not generate password reset key.' ),
|
|
560
|
+
array( 'status' => 500 )
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
$locale = get_user_locale( $user );
|
|
565
|
+
$message = sprintf(
|
|
566
|
+
/* translators: %s: password reset URL */
|
|
567
|
+
__( 'Someone has requested a password reset for your account. Visit the following address to reset your password: %s' ),
|
|
568
|
+
network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user->user_login ), 'login' )
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
$title = sprintf( __( '[%s] Password Reset' ), get_bloginfo( 'name' ) );
|
|
572
|
+
$sent = wp_mail( $user->user_email, $title, $message );
|
|
573
|
+
|
|
574
|
+
if ( ! $sent ) {
|
|
575
|
+
return new WP_Error(
|
|
576
|
+
'email_failed',
|
|
577
|
+
__( 'Password reset email could not be sent.' ),
|
|
578
|
+
array( 'status' => 500 )
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return new WP_REST_Response( array(
|
|
583
|
+
'success' => true,
|
|
584
|
+
'user_id' => $user_id,
|
|
585
|
+
'message' => 'Password reset email sent.',
|
|
586
|
+
), 200 );
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* GET /mcp-diagnostics/v1/hooks
|
|
591
|
+
* Lists registered actions and filters.
|
|
592
|
+
*/
|
|
593
|
+
function mcp_diagnostics_hooks( $request ) {
|
|
594
|
+
global $wp_filter, $wp_actions;
|
|
595
|
+
|
|
596
|
+
$type = $request->get_param( 'type' );
|
|
597
|
+
$search = $request->get_param( 'search' );
|
|
598
|
+
$per_page = $request->get_param( 'per_page' );
|
|
599
|
+
|
|
600
|
+
$hooks = array();
|
|
601
|
+
$count = 0;
|
|
602
|
+
|
|
603
|
+
$action_names = is_array( $wp_actions ) ? array_keys( $wp_actions ) : array();
|
|
604
|
+
|
|
605
|
+
foreach ( $wp_filter as $hook_name => $hook_obj ) {
|
|
606
|
+
if ( $count >= $per_page ) break;
|
|
607
|
+
|
|
608
|
+
if ( $search && strpos( $hook_name, $search ) === false ) continue;
|
|
609
|
+
|
|
610
|
+
$is_action = in_array( $hook_name, $action_names, true );
|
|
611
|
+
$hook_type = $is_action ? 'action' : 'filter';
|
|
612
|
+
|
|
613
|
+
if ( $type === 'actions' && ! $is_action ) continue;
|
|
614
|
+
if ( $type === 'filters' && $is_action ) continue;
|
|
615
|
+
|
|
616
|
+
$callbacks = array();
|
|
617
|
+
if ( $hook_obj instanceof WP_Hook ) {
|
|
618
|
+
foreach ( $hook_obj->callbacks as $priority => $funcs ) {
|
|
619
|
+
foreach ( $funcs as $func_data ) {
|
|
620
|
+
$func_name = mcp_diagnostics_get_callback_name( $func_data['function'] );
|
|
621
|
+
$callbacks[] = array(
|
|
622
|
+
'function' => $func_name,
|
|
623
|
+
'priority' => $priority,
|
|
624
|
+
'accepted_args' => $func_data['accepted_args'],
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
$hooks[] = array(
|
|
631
|
+
'name' => $hook_name,
|
|
632
|
+
'type' => $hook_type,
|
|
633
|
+
'callbacks' => $callbacks,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
$count++;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return new WP_REST_Response( array( 'hooks' => $hooks ), 200 );
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Convert a callback to a human-readable string.
|
|
644
|
+
*/
|
|
645
|
+
function mcp_diagnostics_get_callback_name( $callback ) {
|
|
646
|
+
if ( is_string( $callback ) ) {
|
|
647
|
+
return $callback;
|
|
648
|
+
}
|
|
649
|
+
if ( is_array( $callback ) && count( $callback ) === 2 ) {
|
|
650
|
+
$class = is_object( $callback[0] ) ? get_class( $callback[0] ) : (string) $callback[0];
|
|
651
|
+
$method = $callback[1];
|
|
652
|
+
return $class . '::' . $method;
|
|
653
|
+
}
|
|
654
|
+
if ( $callback instanceof Closure ) {
|
|
655
|
+
return '{closure}';
|
|
656
|
+
}
|
|
657
|
+
return '{unknown}';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ============================================================
|
|
661
|
+
// Schema.org JSON-LD — REST endpoints + wp_head output
|
|
662
|
+
// ============================================================
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Permission check for schema endpoints: require edit_posts capability.
|
|
666
|
+
*/
|
|
667
|
+
function mcp_diagnostics_schema_permission_check( $request ) {
|
|
668
|
+
if ( ! current_user_can( 'edit_posts' ) ) {
|
|
669
|
+
return new WP_Error(
|
|
670
|
+
'rest_forbidden',
|
|
671
|
+
__( 'Schema management requires edit_posts capability.' ),
|
|
672
|
+
array( 'status' => 403 )
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Check if the site is in read-only mode.
|
|
680
|
+
*/
|
|
681
|
+
function mcp_diagnostics_is_read_only() {
|
|
682
|
+
if ( defined( 'WP_READ_ONLY' ) && WP_READ_ONLY ) {
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
if ( get_option( 'mcp_read_only', false ) ) {
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* GET /mcp-diagnostics/v1/schema/{post_id}
|
|
693
|
+
* Read the _custom_schema_jsonld meta for a post.
|
|
694
|
+
*/
|
|
695
|
+
function mcp_diagnostics_schema_get( $request ) {
|
|
696
|
+
$post_id = $request->get_param( 'post_id' );
|
|
697
|
+
$post = get_post( $post_id );
|
|
698
|
+
|
|
699
|
+
if ( ! $post ) {
|
|
700
|
+
return new WP_Error( 'not_found', __( 'Post not found.' ), array( 'status' => 404 ) );
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
$schema = get_post_meta( $post_id, '_custom_schema_jsonld', true );
|
|
704
|
+
|
|
705
|
+
return new WP_REST_Response( array(
|
|
706
|
+
'post_id' => $post_id,
|
|
707
|
+
'schema' => $schema ?: null,
|
|
708
|
+
), 200 );
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* POST /mcp-diagnostics/v1/schema/{post_id}
|
|
713
|
+
* Write the _custom_schema_jsonld meta for a post.
|
|
714
|
+
*/
|
|
715
|
+
function mcp_diagnostics_schema_set( $request ) {
|
|
716
|
+
if ( mcp_diagnostics_is_read_only() ) {
|
|
717
|
+
return new WP_Error( 'read_only', __( 'Site is in read-only mode.' ), array( 'status' => 403 ) );
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
$post_id = $request->get_param( 'post_id' );
|
|
721
|
+
$schema = $request->get_param( 'schema' );
|
|
722
|
+
$post = get_post( $post_id );
|
|
723
|
+
|
|
724
|
+
if ( ! $post ) {
|
|
725
|
+
return new WP_Error( 'not_found', __( 'Post not found.' ), array( 'status' => 404 ) );
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Validate JSON
|
|
729
|
+
$decoded = json_decode( $schema );
|
|
730
|
+
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
|
731
|
+
return new WP_Error( 'invalid_json', __( 'Schema must be valid JSON.' ), array( 'status' => 400 ) );
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
update_post_meta( $post_id, '_custom_schema_jsonld', $schema );
|
|
735
|
+
|
|
736
|
+
return new WP_REST_Response( array(
|
|
737
|
+
'post_id' => $post_id,
|
|
738
|
+
'status' => 'saved',
|
|
739
|
+
'schema' => $schema,
|
|
740
|
+
), 200 );
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* DELETE /mcp-diagnostics/v1/schema/{post_id}
|
|
745
|
+
* Remove the _custom_schema_jsonld meta from a post.
|
|
746
|
+
*/
|
|
747
|
+
function mcp_diagnostics_schema_delete( $request ) {
|
|
748
|
+
if ( mcp_diagnostics_is_read_only() ) {
|
|
749
|
+
return new WP_Error( 'read_only', __( 'Site is in read-only mode.' ), array( 'status' => 403 ) );
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
$post_id = $request->get_param( 'post_id' );
|
|
753
|
+
$post = get_post( $post_id );
|
|
754
|
+
|
|
755
|
+
if ( ! $post ) {
|
|
756
|
+
return new WP_Error( 'not_found', __( 'Post not found.' ), array( 'status' => 404 ) );
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
delete_post_meta( $post_id, '_custom_schema_jsonld' );
|
|
760
|
+
|
|
761
|
+
return new WP_REST_Response( array(
|
|
762
|
+
'post_id' => $post_id,
|
|
763
|
+
'status' => 'deleted',
|
|
764
|
+
), 200 );
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Output JSON-LD schema in wp_head for singular posts/pages.
|
|
769
|
+
*/
|
|
770
|
+
function mcp_schema_output_jsonld() {
|
|
771
|
+
if ( ! is_singular() ) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
$post_id = get_the_ID();
|
|
776
|
+
if ( ! $post_id ) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
$schema = get_post_meta( $post_id, '_custom_schema_jsonld', true );
|
|
781
|
+
if ( empty( $schema ) ) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Validate JSON before outputting
|
|
786
|
+
$decoded = json_decode( $schema );
|
|
787
|
+
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
echo '<script type="application/ld+json">' . $schema . '</script>' . "\n";
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ============================================================
|
|
795
|
+
// Polylang Free REST fallback endpoints
|
|
796
|
+
// ============================================================
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* GET /mcp-diagnostics/v1/polylang/languages
|
|
800
|
+
* Returns Polylang languages via pll_languages_list() if Polylang is active.
|
|
801
|
+
*/
|
|
802
|
+
function mcp_diagnostics_polylang_languages( $request ) {
|
|
803
|
+
if ( ! function_exists( 'pll_languages_list' ) ) {
|
|
804
|
+
return new WP_REST_Response( array(
|
|
805
|
+
'languages' => array(),
|
|
806
|
+
'message' => 'Polylang is not active.',
|
|
807
|
+
), 200 );
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
$languages = array();
|
|
811
|
+
$pll_langs = PLL()->model->get_languages_list();
|
|
812
|
+
|
|
813
|
+
foreach ( $pll_langs as $lang ) {
|
|
814
|
+
$languages[] = array(
|
|
815
|
+
'slug' => $lang->slug,
|
|
816
|
+
'name' => $lang->name,
|
|
817
|
+
'native_name' => $lang->name,
|
|
818
|
+
'locale' => $lang->locale,
|
|
819
|
+
'is_default' => (bool) $lang->is_default,
|
|
820
|
+
'home_url' => $lang->home_url ?? null,
|
|
821
|
+
'flag' => $lang->flag_url ?? null,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return new WP_REST_Response( array( 'languages' => $languages ), 200 );
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* GET /mcp-diagnostics/v1/polylang/translations/{post_id}
|
|
830
|
+
* Returns {lang_code: post_id} via pll_get_post() for each language.
|
|
831
|
+
*/
|
|
832
|
+
function mcp_diagnostics_polylang_translations( $request ) {
|
|
833
|
+
$post_id = $request->get_param( 'post_id' );
|
|
834
|
+
|
|
835
|
+
if ( ! function_exists( 'pll_get_post' ) || ! function_exists( 'pll_languages_list' ) ) {
|
|
836
|
+
return new WP_REST_Response( array(
|
|
837
|
+
'post_id' => $post_id,
|
|
838
|
+
'translations' => array(),
|
|
839
|
+
'message' => 'Polylang is not active.',
|
|
840
|
+
), 200 );
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
$post = get_post( $post_id );
|
|
844
|
+
if ( ! $post ) {
|
|
845
|
+
return new WP_Error( 'not_found', __( 'Post not found.' ), array( 'status' => 404 ) );
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
$translations = array();
|
|
849
|
+
$lang_slugs = pll_languages_list( array( 'fields' => 'slug' ) );
|
|
850
|
+
|
|
851
|
+
foreach ( $lang_slugs as $slug ) {
|
|
852
|
+
$tr_id = pll_get_post( $post_id, $slug );
|
|
853
|
+
if ( $tr_id ) {
|
|
854
|
+
$translations[ $slug ] = (int) $tr_id;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return new WP_REST_Response( array(
|
|
859
|
+
'post_id' => $post_id,
|
|
860
|
+
'translations' => $translations,
|
|
861
|
+
), 200 );
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ============================================================
|
|
865
|
+
// Security Audit endpoints
|
|
866
|
+
// ============================================================
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* GET /mcp-diagnostics/v1/user-activity
|
|
870
|
+
* Returns last login/activity timestamps for administrator users.
|
|
871
|
+
*/
|
|
872
|
+
function mcp_diagnostics_user_activity( $request ) {
|
|
873
|
+
$admins = get_users( array( 'role' => 'administrator', 'number' => 100 ) );
|
|
874
|
+
$results = array();
|
|
875
|
+
|
|
876
|
+
foreach ( $admins as $user ) {
|
|
877
|
+
$last_login = null;
|
|
878
|
+
|
|
879
|
+
// Try common last_login meta keys
|
|
880
|
+
$meta_keys = array( 'last_login', 'wfls-last-login', 'woocommerce_last_active' );
|
|
881
|
+
foreach ( $meta_keys as $key ) {
|
|
882
|
+
$val = get_user_meta( $user->ID, $key, true );
|
|
883
|
+
if ( $val ) {
|
|
884
|
+
$last_login = is_numeric( $val ) ? gmdate( 'c', (int) $val ) : $val;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Fallback: session_tokens (last key timestamp)
|
|
890
|
+
if ( ! $last_login ) {
|
|
891
|
+
$sessions = get_user_meta( $user->ID, 'session_tokens', true );
|
|
892
|
+
if ( is_array( $sessions ) && ! empty( $sessions ) ) {
|
|
893
|
+
$latest = 0;
|
|
894
|
+
foreach ( $sessions as $token ) {
|
|
895
|
+
if ( isset( $token['login'] ) && $token['login'] > $latest ) {
|
|
896
|
+
$latest = $token['login'];
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if ( $latest > 0 ) {
|
|
900
|
+
$last_login = gmdate( 'c', $latest );
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
$results[] = array(
|
|
906
|
+
'user_id' => $user->ID,
|
|
907
|
+
'login' => $user->user_login,
|
|
908
|
+
'email' => $user->user_email,
|
|
909
|
+
'last_login' => $last_login,
|
|
910
|
+
'last_active' => $last_login,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return new WP_REST_Response( array( 'users' => $results ), 200 );
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* GET /mcp-diagnostics/v1/file-permissions
|
|
919
|
+
* Checks permissions of critical WordPress files and directories.
|
|
920
|
+
*/
|
|
921
|
+
function mcp_diagnostics_file_permissions( $request ) {
|
|
922
|
+
$checks = array(
|
|
923
|
+
array( 'path' => ABSPATH . 'wp-config.php', 'recommended' => '400', 'max_safe' => '440' ),
|
|
924
|
+
array( 'path' => ABSPATH . '.htaccess', 'recommended' => '644', 'max_safe' => '644' ),
|
|
925
|
+
array( 'path' => WP_CONTENT_DIR . '/uploads', 'recommended' => '755', 'max_safe' => '755' ),
|
|
926
|
+
array( 'path' => ABSPATH . WPINC, 'recommended' => '755', 'max_safe' => '755' ),
|
|
927
|
+
array( 'path' => ABSPATH . 'wp-admin', 'recommended' => '755', 'max_safe' => '755' ),
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
$files = array();
|
|
931
|
+
foreach ( $checks as $check ) {
|
|
932
|
+
$path = $check['path'];
|
|
933
|
+
$exists = file_exists( $path );
|
|
934
|
+
$perm_octal = '';
|
|
935
|
+
$perm_human = '';
|
|
936
|
+
|
|
937
|
+
if ( $exists ) {
|
|
938
|
+
$perms = fileperms( $path );
|
|
939
|
+
$perm_octal = decoct( $perms & 0777 );
|
|
940
|
+
// Build human-readable
|
|
941
|
+
$perm_human = '';
|
|
942
|
+
$perm_human .= ( $perms & 0x0100 ) ? 'r' : '-';
|
|
943
|
+
$perm_human .= ( $perms & 0x0080 ) ? 'w' : '-';
|
|
944
|
+
$perm_human .= ( $perms & 0x0040 ) ? 'x' : '-';
|
|
945
|
+
$perm_human .= ( $perms & 0x0020 ) ? 'r' : '-';
|
|
946
|
+
$perm_human .= ( $perms & 0x0010 ) ? 'w' : '-';
|
|
947
|
+
$perm_human .= ( $perms & 0x0008 ) ? 'x' : '-';
|
|
948
|
+
$perm_human .= ( $perms & 0x0004 ) ? 'r' : '-';
|
|
949
|
+
$perm_human .= ( $perms & 0x0002 ) ? 'w' : '-';
|
|
950
|
+
$perm_human .= ( $perms & 0x0001 ) ? 'x' : '-';
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
$files[] = array(
|
|
954
|
+
'path' => str_replace( ABSPATH, '', $path ),
|
|
955
|
+
'permission_octal' => $perm_octal,
|
|
956
|
+
'permission_human' => $perm_human,
|
|
957
|
+
'recommended' => $check['recommended'],
|
|
958
|
+
'exists' => $exists,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return new WP_REST_Response( array( 'files' => $files ), 200 );
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* GET /mcp-diagnostics/v1/modified-files
|
|
967
|
+
* Lists recently modified files in wp-content subdirectories.
|
|
968
|
+
*/
|
|
969
|
+
function mcp_diagnostics_modified_files( $request ) {
|
|
970
|
+
$days = $request->get_param( 'days' );
|
|
971
|
+
$paths_str = $request->get_param( 'paths' );
|
|
972
|
+
$ext_str = $request->get_param( 'extensions' );
|
|
973
|
+
$since = strtotime( "-{$days} days" );
|
|
974
|
+
$paths = array_map( 'trim', explode( ',', $paths_str ) );
|
|
975
|
+
$extensions = array_map( 'trim', explode( ',', $ext_str ) );
|
|
976
|
+
$max_files = 500;
|
|
977
|
+
$results = array();
|
|
978
|
+
|
|
979
|
+
foreach ( $paths as $rel_path ) {
|
|
980
|
+
$dir = WP_CONTENT_DIR . '/' . $rel_path;
|
|
981
|
+
if ( ! is_dir( $dir ) ) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
$iterator = new RecursiveIteratorIterator(
|
|
986
|
+
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
|
|
987
|
+
RecursiveIteratorIterator::SELF_FIRST
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
foreach ( $iterator as $file ) {
|
|
991
|
+
if ( count( $results ) >= $max_files ) {
|
|
992
|
+
break 2;
|
|
993
|
+
}
|
|
994
|
+
if ( ! $file->isFile() ) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
$ext = '.' . $file->getExtension();
|
|
999
|
+
if ( ! in_array( $ext, $extensions, true ) ) {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
$mtime = $file->getMTime();
|
|
1004
|
+
if ( $mtime < $since ) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
$full_path = $file->getPathname();
|
|
1009
|
+
$rel = str_replace( ABSPATH, '', $full_path );
|
|
1010
|
+
|
|
1011
|
+
$results[] = array(
|
|
1012
|
+
'path' => $rel,
|
|
1013
|
+
'modified_at' => gmdate( 'c', $mtime ),
|
|
1014
|
+
'size' => $file->getSize(),
|
|
1015
|
+
'extension' => $ext,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Sort by modification time descending
|
|
1021
|
+
usort( $results, function( $a, $b ) {
|
|
1022
|
+
return strtotime( $b['modified_at'] ) - strtotime( $a['modified_at'] );
|
|
1023
|
+
} );
|
|
1024
|
+
|
|
1025
|
+
return new WP_REST_Response( array( 'files' => $results ), 200 );
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ============================================================
|
|
1029
|
+
// WooCommerce Intelligence endpoints
|
|
1030
|
+
// ============================================================
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* GET /mcp-diagnostics/v1/wc-abandoned-carts
|
|
1034
|
+
* Detects abandoned cart data from available sources.
|
|
1035
|
+
*/
|
|
1036
|
+
function mcp_diagnostics_wc_abandoned_carts( $request ) {
|
|
1037
|
+
global $wpdb;
|
|
1038
|
+
|
|
1039
|
+
$days = $request->get_param( 'days' );
|
|
1040
|
+
$min_value = $request->get_param( 'min_value' );
|
|
1041
|
+
$since = time() - ( $days * 86400 );
|
|
1042
|
+
$max_carts = 1000;
|
|
1043
|
+
|
|
1044
|
+
// Source 1: Abandoned Cart Lite/Pro plugin table
|
|
1045
|
+
$ac_table = $wpdb->prefix . 'ac_abandoned_cart_history';
|
|
1046
|
+
if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $ac_table ) ) === $ac_table ) {
|
|
1047
|
+
$rows = $wpdb->get_results( $wpdb->prepare(
|
|
1048
|
+
"SELECT id, abandoned_cart_info, abandoned_cart_time, cart_total
|
|
1049
|
+
FROM {$ac_table}
|
|
1050
|
+
WHERE abandoned_cart_time >= %d
|
|
1051
|
+
AND recovered_cart = 0
|
|
1052
|
+
ORDER BY abandoned_cart_time DESC
|
|
1053
|
+
LIMIT %d",
|
|
1054
|
+
$since,
|
|
1055
|
+
$max_carts
|
|
1056
|
+
) );
|
|
1057
|
+
|
|
1058
|
+
$carts = array();
|
|
1059
|
+
foreach ( $rows as $row ) {
|
|
1060
|
+
$value = floatval( $row->cart_total );
|
|
1061
|
+
if ( $value < $min_value ) continue;
|
|
1062
|
+
$info = maybe_unserialize( $row->abandoned_cart_info );
|
|
1063
|
+
$products = array();
|
|
1064
|
+
if ( is_array( $info ) ) {
|
|
1065
|
+
foreach ( $info as $item ) {
|
|
1066
|
+
$products[] = array(
|
|
1067
|
+
'product_id' => isset( $item['product_id'] ) ? (int) $item['product_id'] : 0,
|
|
1068
|
+
'name' => isset( $item['product_name'] ) ? $item['product_name'] : '',
|
|
1069
|
+
'quantity' => isset( $item['quantity'] ) ? (int) $item['quantity'] : 1,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
$carts[] = array(
|
|
1074
|
+
'cart_value' => $value,
|
|
1075
|
+
'abandoned_at' => gmdate( 'c', (int) $row->abandoned_cart_time ),
|
|
1076
|
+
'products' => $products,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return new WP_REST_Response( array(
|
|
1081
|
+
'available' => true,
|
|
1082
|
+
'source' => 'abandoned_cart_plugin',
|
|
1083
|
+
'carts' => $carts,
|
|
1084
|
+
), 200 );
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Source 2: CartFlows plugin table
|
|
1088
|
+
$cf_table = $wpdb->prefix . 'cartflows_ca_cart_abandonment';
|
|
1089
|
+
if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $cf_table ) ) === $cf_table ) {
|
|
1090
|
+
$rows = $wpdb->get_results( $wpdb->prepare(
|
|
1091
|
+
"SELECT id, cart_contents, cart_total, time
|
|
1092
|
+
FROM {$cf_table}
|
|
1093
|
+
WHERE time >= %s
|
|
1094
|
+
AND order_status = 'abandoned'
|
|
1095
|
+
ORDER BY time DESC
|
|
1096
|
+
LIMIT %d",
|
|
1097
|
+
gmdate( 'Y-m-d H:i:s', $since ),
|
|
1098
|
+
$max_carts
|
|
1099
|
+
) );
|
|
1100
|
+
|
|
1101
|
+
$carts = array();
|
|
1102
|
+
foreach ( $rows as $row ) {
|
|
1103
|
+
$value = floatval( $row->cart_total );
|
|
1104
|
+
if ( $value < $min_value ) continue;
|
|
1105
|
+
$contents = maybe_unserialize( $row->cart_contents );
|
|
1106
|
+
$products = array();
|
|
1107
|
+
if ( is_array( $contents ) ) {
|
|
1108
|
+
foreach ( $contents as $item ) {
|
|
1109
|
+
$products[] = array(
|
|
1110
|
+
'product_id' => isset( $item['product_id'] ) ? (int) $item['product_id'] : 0,
|
|
1111
|
+
'name' => isset( $item['name'] ) ? $item['name'] : '',
|
|
1112
|
+
'quantity' => isset( $item['quantity'] ) ? (int) $item['quantity'] : 1,
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
$carts[] = array(
|
|
1117
|
+
'cart_value' => $value,
|
|
1118
|
+
'abandoned_at' => $row->time,
|
|
1119
|
+
'products' => $products,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return new WP_REST_Response( array(
|
|
1124
|
+
'available' => true,
|
|
1125
|
+
'source' => 'cartflows',
|
|
1126
|
+
'carts' => $carts,
|
|
1127
|
+
), 200 );
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Source 3: WooCommerce native sessions
|
|
1131
|
+
$wc_table = $wpdb->prefix . 'woocommerce_sessions';
|
|
1132
|
+
if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wc_table ) ) === $wc_table ) {
|
|
1133
|
+
$rows = $wpdb->get_results( $wpdb->prepare(
|
|
1134
|
+
"SELECT session_key, session_value, session_expiry
|
|
1135
|
+
FROM {$wc_table}
|
|
1136
|
+
WHERE session_expiry >= %d
|
|
1137
|
+
ORDER BY session_expiry DESC
|
|
1138
|
+
LIMIT %d",
|
|
1139
|
+
$since,
|
|
1140
|
+
$max_carts
|
|
1141
|
+
) );
|
|
1142
|
+
|
|
1143
|
+
$carts = array();
|
|
1144
|
+
foreach ( $rows as $row ) {
|
|
1145
|
+
$session_data = maybe_unserialize( $row->session_value );
|
|
1146
|
+
if ( ! is_array( $session_data ) || empty( $session_data['cart'] ) ) {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
$cart_data = maybe_unserialize( $session_data['cart'] );
|
|
1150
|
+
if ( ! is_array( $cart_data ) || empty( $cart_data ) ) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
$cart_value = 0;
|
|
1155
|
+
$products = array();
|
|
1156
|
+
foreach ( $cart_data as $item ) {
|
|
1157
|
+
$price = isset( $item['line_total'] ) ? floatval( $item['line_total'] ) : 0;
|
|
1158
|
+
$cart_value += $price;
|
|
1159
|
+
$products[] = array(
|
|
1160
|
+
'product_id' => isset( $item['product_id'] ) ? (int) $item['product_id'] : 0,
|
|
1161
|
+
'name' => '',
|
|
1162
|
+
'quantity' => isset( $item['quantity'] ) ? (int) $item['quantity'] : 1,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if ( $cart_value < $min_value ) continue;
|
|
1167
|
+
|
|
1168
|
+
$carts[] = array(
|
|
1169
|
+
'cart_value' => round( $cart_value, 2 ),
|
|
1170
|
+
'abandoned_at' => gmdate( 'c', (int) $row->session_expiry ),
|
|
1171
|
+
'products' => $products,
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return new WP_REST_Response( array(
|
|
1176
|
+
'available' => true,
|
|
1177
|
+
'source' => 'wc_sessions',
|
|
1178
|
+
'carts' => $carts,
|
|
1179
|
+
), 200 );
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// No source available
|
|
1183
|
+
return new WP_REST_Response( array( 'available' => false ), 200 );
|
|
1184
|
+
}
|