PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
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.6, at wp-mcp-server/class-metasync-mcp-sync-logger.php

402 lines 14.7 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' => $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 * Rollback a single sync history entry that was logged by MCP Client.
317 *
318 * Reads the `before` state stored in meta_data and restores it.
319 *
320 * @param int $sync_history_id
321 * @return array ['success' => bool, 'message' => string]
322 */
323 public static function rollback($sync_history_id) {
324 $sync_db = new Metasync_Sync_History_Database();
325 $record = $sync_db->get_by_id(intval($sync_history_id));
326
327 if (!$record) {
328 return [ 'success' => false, 'message' => 'Sync history record not found.' ];
329 }
330
331 if ($record->source !== 'MCP Client') {
332 return [ 'success' => false, 'message' => 'Only MCP Client entries can be rolled back.' ];
333 }
334
335 $meta = json_decode($record->meta_data, true);
336
337 if (empty($meta['before'])) {
338 return [ 'success' => false, 'message' => 'No before-state available for rollback.' ];
339 }
340
341 $tool_name = isset($meta['tool']) ? $meta['tool'] : '';
342 $before = $meta['before'];
343
344 try {
345 $result = self::apply_rollback($tool_name, $before);
346 if ($result) {
347 return [ 'success' => true, 'message' => 'Rollback applied successfully.' ];
348 }
349 return [ 'success' => false, 'message' => 'Rollback not supported for this tool.' ];
350 } catch (Exception $e) {
351 return [ 'success' => false, 'message' => 'Rollback failed: ' . $e->getMessage() ];
352 }
353 }
354
355 /**
356 * Apply the rollback using the before-state data.
357 *
358 * @param string $tool_name
359 * @param array $before
360 * @return bool
361 */
362 private static function apply_rollback($tool_name, $before) {
363 // Restore single post meta value
364 if ($tool_name === 'wordpress_update_post_meta') {
365 if (isset($before['post_id'], $before['meta_key'])) {
366 update_post_meta($before['post_id'], $before['meta_key'], $before['meta_value']);
367 return true;
368 }
369 }
370
371 // Restore schema markup
372 if (in_array($tool_name, ['wordpress_update_schema_markup', 'wordpress_add_schema_type'], true)) {
373 if (isset($before['post_id'])) {
374 update_post_meta($before['post_id'], '_metasync_schema_markup', $before['schema']);
375 return true;
376 }
377 }
378
379 // Restore post term relationships
380 if (in_array($tool_name, ['wordpress_set_post_tags', 'wordpress_set_post_categories'], true)) {
381 if (isset($before['post_id'], $before['taxonomy'], $before['term_ids'])) {
382 wp_set_object_terms($before['post_id'], $before['term_ids'], $before['taxonomy']);
383 return true;
384 }
385 }
386
387 // Restore a term (category or tag) that was updated
388 if (in_array($tool_name, ['wordpress_update_category', 'wordpress_update_tag'], true)) {
389 if (isset($before['term_id'], $before['taxonomy'])) {
390 wp_update_term($before['term_id'], $before['taxonomy'], [
391 'name' => $before['name'],
392 'slug' => $before['slug'],
393 'description' => $before['description'],
394 ]);
395 return true;
396 }
397 }
398
399 return false;
400 }
401 }
402