PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.8
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.8
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / wp-mcp-server / class-metasync-mcp-sync-logger.php

class-metasync-mcp-sync-logger.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.8, at wp-mcp-server/class-metasync-mcp-sync-logger.php

431 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Sync Logger
4 *
5 * Logs all write operations performed via MCP tools into the metasync_sync_history
6 * table so they are visible in the Sync Log admin page. Before-state is captured
7 * and stored in meta_data JSON to enable rollback.
8 *
9 * @package MetaSync
10 * @subpackage MCP_Server
11 */
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 class Metasync_MCP_Sync_Logger {
18
19 /**
20 * Tool names that represent write operations and should be logged.
21 * Covers post-meta, schema, taxonomy, redirects, robots, code snippets.
22 */
23 private static $write_tool_prefixes = [
24 'wordpress_create_',
25 'wordpress_update_',
26 'wordpress_delete_',
27 'wordpress_set_',
28 'wordpress_add_',
29 ];
30
31 /**
32 * Temporarily holds before-state keyed by tool name while the tool executes.
33 * @var array
34 */
35 private $before_states = [];
36
37 /**
38 * Constructor — wire up action hooks.
39 */
40 public function __construct() {
41 add_action('metasync_mcp_tool_before_execute', [ $this, 'capture_before_state' ], 10, 2);
42 add_action('metasync_mcp_tool_executed', [ $this, 'log_tool_execution' ], 10, 4);
43 }
44
45 /**
46 * Determine whether a tool name is a write operation.
47 *
48 * @param string $tool_name
49 * @return bool
50 */
51 public static function is_write_tool($tool_name) {
52 foreach (self::$write_tool_prefixes as $prefix) {
53 if (strncmp($tool_name, $prefix, strlen($prefix)) === 0) {
54 return true;
55 }
56 }
57 return false;
58 }
59
60 /**
61 * Capture before-state immediately before the tool executes.
62 * Fires on metasync_mcp_tool_before_execute.
63 *
64 * @param string $tool_name
65 * @param array $params
66 */
67 public function capture_before_state($tool_name, $params) {
68 if (!self::is_write_tool($tool_name)) {
69 return;
70 }
71 $this->before_states[$tool_name] = $this->fetch_before_state($tool_name, $params);
72 }
73
74 /**
75 * Log the tool execution after it completes.
76 * Fires on metasync_mcp_tool_executed.
77 *
78 * @param string $tool_name
79 * @param array $params
80 * @param mixed $result
81 * @param float $execution_time
82 */
83 public function log_tool_execution($tool_name, $params, $result, $execution_time) {
84 if (!self::is_write_tool($tool_name)) {
85 return;
86 }
87
88 // Only log successful operations.
89 if (is_array($result) && isset($result['success']) && $result['success'] === false) {
90 return;
91 }
92
93 try {
94 $before_state = isset($this->before_states[$tool_name])
95 ? $this->before_states[$tool_name]
96 : null;
97
98 // Free memory immediately.
99 unset($this->before_states[$tool_name]);
100
101 $sync_db = new Metasync_Sync_History_Database();
102
103 $post_id = isset($params['post_id']) ? intval($params['post_id']) : null;
104 $url = $post_id ? get_permalink($post_id) : null;
105
106 $sync_db->add([
107 'title' => $this->build_log_title($tool_name, $params),
108 'source' => 'MCP Client',
109 'status' => 'published',
110 'content_type' => $tool_name,
111 'url' => $url ?: '',
112 'meta_data' => wp_json_encode([
113 'tool' => $tool_name,
114 'params' => self::redact_sensitive_params($params),
115 'before' => $before_state,
116 ]),
117 'created_at' => current_time('mysql'),
118 ]);
119 } catch (Exception $e) {
120 // Never break MCP execution because of logging.
121 error_log('MetaSync MCP Sync Logger error: ' . $e->getMessage());
122 }
123 }
124
125 /**
126 * Fetch the current (before) state for a write tool based on its params.
127 *
128 * @param string $tool_name
129 * @param array $params
130 * @return array|null
131 */
132 private function fetch_before_state($tool_name, $params) {
133 // Post meta (single key update)
134 if ($tool_name === 'wordpress_update_post_meta') {
135 $post_id = isset($params['post_id']) ? intval($params['post_id']) : 0;
136 $meta_key = isset($params['meta_key']) ? sanitize_text_field($params['meta_key']) : '';
137 if ($post_id && $meta_key) {
138 return [
139 'post_id' => $post_id,
140 'meta_key' => $meta_key,
141 'meta_value' => get_post_meta($post_id, $meta_key, true),
142 ];
143 }
144 }
145
146 // Schema markup
147 if (in_array($tool_name, ['wordpress_update_schema_markup', 'wordpress_add_schema_type'], true)) {
148 $post_id = isset($params['post_id']) ? intval($params['post_id']) : 0;
149 if ($post_id) {
150 return [
151 'post_id' => $post_id,
152 'schema' => get_post_meta($post_id, '_metasync_schema_markup', true),
153 ];
154 }
155 }
156
157 // Post tags / categories (relationship)
158 if (in_array($tool_name, ['wordpress_set_post_tags', 'wordpress_set_post_categories'], true)) {
159 $post_id = isset($params['post_id']) ? intval($params['post_id']) : 0;
160 $taxonomy = ($tool_name === 'wordpress_set_post_tags') ? 'post_tag' : 'category';
161 if ($post_id) {
162 $terms = wp_get_object_terms($post_id, $taxonomy, ['fields' => 'ids']);
163 return [
164 'post_id' => $post_id,
165 'taxonomy' => $taxonomy,
166 'term_ids' => is_wp_error($terms) ? [] : $terms,
167 ];
168 }
169 }
170
171 // Category / tag update or delete
172 if (in_array($tool_name, [
173 'wordpress_update_category', 'wordpress_delete_category',
174 'wordpress_update_tag', 'wordpress_delete_tag',
175 ], true)) {
176 $term_id = isset($params['id']) ? intval($params['id']) : 0;
177 $taxonomy = strpos($tool_name, 'category') !== false ? 'category' : 'post_tag';
178 if ($term_id) {
179 $term = get_term($term_id, $taxonomy);
180 if ($term && !is_wp_error($term)) {
181 return [
182 'term_id' => $term_id,
183 'taxonomy' => $taxonomy,
184 'name' => $term->name,
185 'slug' => $term->slug,
186 'description' => $term->description,
187 ];
188 }
189 }
190 }
191
192 // For all other write tools just record the params as before-state context.
193 return [ 'params_snapshot' => $params ];
194 }
195
196 /**
197 * Build a human-readable log title.
198 *
199 * @param string $tool_name
200 * @param array $params
201 * @return string
202 */
203 private function build_log_title($tool_name, $params) {
204 $post_id = isset($params['post_id']) ? intval($params['post_id']) : null;
205 $post_title = $post_id ? get_the_title($post_id) : null;
206
207 // Field-specific titles for post meta updates
208 if ($tool_name === 'wordpress_update_post_meta') {
209 $meta_key = isset($params['meta_key']) ? sanitize_text_field($params['meta_key']) : '';
210 $meta_field_map = [
211 '_metasync_metatitle' => 'Meta Title',
212 '_metasync_metadesc' => 'Meta Description',
213 '_metasync_focus_keyword' => 'Focus Keyword',
214 '_metasync_robots_index' => 'Robots Index',
215 '_metasync_canonical_url' => 'Canonical URL',
216 '_metasync_og_enabled' => 'Open Graph',
217 '_metasync_og_title' => 'OG Title',
218 '_metasync_og_description' => 'OG Description',
219 '_metasync_og_image' => 'OG Image',
220 '_metasync_og_url' => 'OG URL',
221 '_metasync_og_type' => 'OG Type',
222 ];
223
224 $field_name = isset($meta_field_map[$meta_key])
225 ? $meta_field_map[$meta_key]
226 : ucwords(str_replace(['_metasync_', '_'], ['', ' '], $meta_key));
227
228 if ($post_title) {
229 return $field_name . ' Updated: ' . $post_title;
230 }
231 return $field_name . ' Updated via MCP';
232 }
233
234 // Schema-specific titles
235 if ($tool_name === 'wordpress_add_schema_type') {
236 $schema_type = isset($params['schema_type']) ? sanitize_text_field($params['schema_type']) : 'Schema';
237 if ($post_title) {
238 return 'Added ' . $schema_type . ' schema: ' . $post_title;
239 }
240 return 'Added ' . $schema_type . ' schema via MCP';
241 }
242
243 if ($tool_name === 'wordpress_update_schema_markup') {
244 if ($post_title) {
245 return 'Updated schema markup: ' . $post_title;
246 }
247 return 'Updated schema markup via MCP';
248 }
249
250 // Tag-specific titles
251 if ($tool_name === 'wordpress_create_tag') {
252 $name = isset($params['name']) ? sanitize_text_field($params['name']) : 'Tag';
253 return 'Created tag: ' . $name;
254 }
255
256 if ($tool_name === 'wordpress_update_tag') {
257 $id = isset($params['id']) ? intval($params['id']) : 0;
258 $term = $id ? get_term($id, 'post_tag') : null;
259 $tag_name = ($term && !is_wp_error($term)) ? $term->name : 'Tag';
260 return 'Updated tag: ' . $tag_name;
261 }
262
263 if ($tool_name === 'wordpress_delete_tag') {
264 $id = isset($params['id']) ? intval($params['id']) : 0;
265 $term = $id ? get_term($id, 'post_tag') : null;
266 $tag_name = ($term && !is_wp_error($term)) ? $term->name : 'Tag';
267 return 'Deleted tag: ' . $tag_name;
268 }
269
270 if ($tool_name === 'wordpress_set_post_tags') {
271 if ($post_title) {
272 return 'Set post tags: ' . $post_title;
273 }
274 return 'Set post tags via MCP';
275 }
276
277 // Category-specific titles
278 if ($tool_name === 'wordpress_create_category') {
279 $name = isset($params['name']) ? sanitize_text_field($params['name']) : 'Category';
280 return 'Created category: ' . $name;
281 }
282
283 if ($tool_name === 'wordpress_update_category') {
284 $id = isset($params['id']) ? intval($params['id']) : 0;
285 $term = $id ? get_term($id, 'category') : null;
286 $cat_name = ($term && !is_wp_error($term)) ? $term->name : 'Category';
287 return 'Updated category: ' . $cat_name;
288 }
289
290 if ($tool_name === 'wordpress_delete_category') {
291 $id = isset($params['id']) ? intval($params['id']) : 0;
292 $term = $id ? get_term($id, 'category') : null;
293 $cat_name = ($term && !is_wp_error($term)) ? $term->name : 'Category';
294 return 'Deleted category: ' . $cat_name;
295 }
296
297 if ($tool_name === 'wordpress_set_post_categories') {
298 if ($post_title) {
299 return 'Set post categories: ' . $post_title;
300 }
301 return 'Set post categories via MCP';
302 }
303
304 // Fallback for unknown tools
305 $action = ucwords(str_replace(['wordpress_', '_'], ['', ' '], $tool_name));
306 if ($post_title) {
307 return $action . ': ' . $post_title;
308 }
309 if (isset($params['name'])) {
310 return $action . ': ' . sanitize_text_field($params['name']);
311 }
312 return $action . ' via MCP';
313 }
314
315 /**
316 * Redact sensitive values from params before logging.
317 *
318 * Keys matching common credential patterns are replaced with '***REDACTED***'.
319 *
320 * @param mixed $params
321 * @return mixed
322 */
323 private static function redact_sensitive_params($params) {
324 if (!is_array($params)) {
325 return $params;
326 }
327
328 $sensitive_keys = ['apikey', 'api_key', 'token', 'password', 'secret', 'access_key', 'private_key'];
329 $redacted = [];
330
331 foreach ($params as $key => $value) {
332 if (is_string($key) && in_array(strtolower($key), $sensitive_keys, true)) {
333 $redacted[$key] = '***REDACTED***';
334 } elseif (is_array($value)) {
335 $redacted[$key] = self::redact_sensitive_params($value);
336 } else {
337 $redacted[$key] = $value;
338 }
339 }
340
341 return $redacted;
342 }
343
344 /**
345 * Rollback a single sync history entry that was logged by MCP Client.
346 *
347 * Reads the `before` state stored in meta_data and restores it.
348 *
349 * @param int $sync_history_id
350 * @return array ['success' => bool, 'message' => string]
351 */
352 public static function rollback($sync_history_id) {
353 $sync_db = new Metasync_Sync_History_Database();
354 $record = $sync_db->get_by_id(intval($sync_history_id));
355
356 if (!$record) {
357 return [ 'success' => false, 'message' => 'Sync history record not found.' ];
358 }
359
360 if ($record->source !== 'MCP Client') {
361 return [ 'success' => false, 'message' => 'Only MCP Client entries can be rolled back.' ];
362 }
363
364 $meta = json_decode($record->meta_data, true);
365
366 if (empty($meta['before'])) {
367 return [ 'success' => false, 'message' => 'No before-state available for rollback.' ];
368 }
369
370 $tool_name = isset($meta['tool']) ? $meta['tool'] : '';
371 $before = $meta['before'];
372
373 try {
374 $result = self::apply_rollback($tool_name, $before);
375 if ($result) {
376 return [ 'success' => true, 'message' => 'Rollback applied successfully.' ];
377 }
378 return [ 'success' => false, 'message' => 'Rollback not supported for this tool.' ];
379 } catch (Exception $e) {
380 return [ 'success' => false, 'message' => 'Rollback failed: ' . $e->getMessage() ];
381 }
382 }
383
384 /**
385 * Apply the rollback using the before-state data.
386 *
387 * @param string $tool_name
388 * @param array $before
389 * @return bool
390 */
391 private static function apply_rollback($tool_name, $before) {
392 // Restore single post meta value
393 if ($tool_name === 'wordpress_update_post_meta') {
394 if (isset($before['post_id'], $before['meta_key'])) {
395 update_post_meta($before['post_id'], $before['meta_key'], $before['meta_value']);
396 return true;
397 }
398 }
399
400 // Restore schema markup
401 if (in_array($tool_name, ['wordpress_update_schema_markup', 'wordpress_add_schema_type'], true)) {
402 if (isset($before['post_id'])) {
403 update_post_meta($before['post_id'], '_metasync_schema_markup', $before['schema']);
404 return true;
405 }
406 }
407
408 // Restore post term relationships
409 if (in_array($tool_name, ['wordpress_set_post_tags', 'wordpress_set_post_categories'], true)) {
410 if (isset($before['post_id'], $before['taxonomy'], $before['term_ids'])) {
411 wp_set_object_terms($before['post_id'], $before['term_ids'], $before['taxonomy']);
412 return true;
413 }
414 }
415
416 // Restore a term (category or tag) that was updated
417 if (in_array($tool_name, ['wordpress_update_category', 'wordpress_update_tag'], true)) {
418 if (isset($before['term_id'], $before['taxonomy'])) {
419 wp_update_term($before['term_id'], $before['taxonomy'], [
420 'name' => $before['name'],
421 'slug' => $before['slug'],
422 'description' => $before['description'],
423 ]);
424 return true;
425 }
426 }
427
428 return false;
429 }
430 }
431