PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.26
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.26
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 / tools / class-mcp-tool-plugin-settings.php

class-mcp-tool-plugin-settings.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.26, at wp-mcp-server/tools/class-mcp-tool-plugin-settings.php

701 lines 26.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tool: Plugin Settings Management
4 *
5 * Provides tools for viewing and managing WordPress plugin settings.
6 * This allows AI agents to read and update all MetaSync plugin configuration.
7 *
8 * @package MetaSync
9 * @subpackage MCP_Server/Tools
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 /**
17 * Get Plugin Settings Tool
18 */
19 class MCP_Tool_Get_Plugin_Settings extends MCP_Tool_Base {
20
21 public function get_name() {
22 return 'wordpress_get_plugin_settings';
23 }
24
25 public function get_description() {
26 return 'Get all plugin settings or settings for a specific section. Returns the complete plugin configuration including features, SEO controls, API keys, and more.';
27 }
28
29 public function get_input_schema() {
30 return [
31 'type' => 'object',
32 'properties' => [
33 'section' => [
34 'type' => 'string',
35 'description' => 'Optional: specific settings section to retrieve (e.g., "general", "whitelabel", "seo"). If omitted, returns all settings.',
36 'enum' => [
37 'general',
38 'whitelabel',
39 'seo',
40 'social',
41 'advanced',
42 'features',
43 'api',
44 'all'
45 ]
46 ],
47 'keys' => [
48 'type' => 'array',
49 'description' => 'Optional: specific setting keys to retrieve. If provided, only these keys will be returned.',
50 'items' => [
51 'type' => 'string'
52 ]
53 ]
54 ],
55 'required' => []
56 ];
57 }
58
59 public function execute($params) {
60 // Validate and check permissions
61 $this->validate_params($params);
62 $this->require_capability('manage_options');
63
64 // Get all plugin options
65 $all_options = get_option(Metasync::option_name, []);
66
67 // If specific keys requested
68 if (!empty($params['keys']) && is_array($params['keys'])) {
69 $result = [];
70 foreach ($params['keys'] as $key) {
71 $key = $this->sanitize_string($key);
72 if (isset($all_options[$key])) {
73 $result[$key] = $all_options[$key];
74 }
75 }
76 return $this->success([
77 'settings' => $this->mask_sensitive_values($result),
78 'count' => count($result)
79 ]);
80 }
81
82 // If section specified (options are nested e.g. $all_options['general']['apikey'])
83 if (!empty($params['section']) && $params['section'] !== 'all') {
84 $section = $this->sanitize_string($params['section']);
85
86 if (isset($all_options[$section]) && is_array($all_options[$section])) {
87 $result = $all_options[$section];
88 } else {
89 // Fallback: map section to flat keys for legacy structure
90 $section_keys = $this->get_section_keys($section);
91 $result = [];
92 foreach ($section_keys as $key) {
93 if (isset($all_options[$key])) {
94 $result[$key] = $all_options[$key];
95 }
96 }
97 }
98
99 return $this->success([
100 'section' => $section,
101 'settings' => $this->mask_sensitive_values($result),
102 'count' => is_array($result) ? count($result) : 0
103 ]);
104 }
105
106 // Return all settings
107 return $this->success([
108 'settings' => $this->mask_sensitive_values($all_options),
109 'count' => count($all_options),
110 'available_sections' => [
111 'general' => 'API keys, integration settings',
112 'whitelabel' => 'Branding and white-label configuration',
113 'seo' => 'SEO controls and indexation settings',
114 'social' => 'Social media and OpenGraph settings',
115 'advanced' => 'Advanced plugin features',
116 'features' => 'Feature toggles and visibility',
117 'api' => 'API configuration and tokens'
118 ]
119 ]);
120 }
121
122 /**
123 * Recursively mask sensitive values before returning settings to a caller.
124 *
125 * The Search Atlas API key is stored encrypted at rest (enc_v1: ciphertext).
126 * Neither the ciphertext nor the decrypted plaintext may leave the server,
127 * so any 'searchatlas_api_key' entry is replaced with a masked display
128 * (8 bullets + last 4 chars of the real key), or '' when not configured.
129 *
130 * @param mixed $data
131 * @return mixed
132 */
133 private function mask_sensitive_values($data) {
134 if (!is_array($data)) {
135 return $data;
136 }
137 foreach ($data as $key => $value) {
138 if (is_array($value)) {
139 $data[$key] = $this->mask_sensitive_values($value);
140 } elseif ($key === 'searchatlas_api_key') {
141 $decrypted = Metasync::get_searchatlas_api_key();
142 if ($decrypted === false || $decrypted === '') {
143 $data[$key] = '';
144 } else {
145 $data[$key] = str_repeat('•', 8) . substr($decrypted, -4);
146 }
147 }
148 }
149 return $data;
150 }
151
152 /**
153 * Get setting keys for a specific section
154 */
155 private function get_section_keys($section) {
156 $section_map = [
157 'general' => [
158 'searchatlas_api_key',
159 'apikey',
160 'permalink_structure',
161 'hide_dashboard_framework',
162 'show_admin_bar_status',
163 'enable_auto_updates',
164 'enable_schema_markup',
165 'default_schema_type'
166 ],
167 'whitelabel' => [
168 'white_label_plugin_name',
169 'whitelabel_otto_name',
170 'whitelabel_logo_url',
171 'whitelabel_domain_url',
172 'white_label_plugin_description',
173 'white_label_plugin_author',
174 'white_label_plugin_author_uri',
175 'white_label_plugin_uri',
176 'white_label_plugin_menu_slug',
177 'white_label_plugin_menu_icon',
178 'whitelabel_settings_password'
179 ],
180 'seo' => [
181 'index_date_archives',
182 'index_tag_archives',
183 'index_author_archives',
184 'index_format_archives',
185 'index_category_archives',
186 'override_robots_tags',
187 'enable_googleinstantindex',
188 'google_index_api_config'
189 ],
190 'social' => [
191 'otto_pixel_uuid',
192 'otto_disable_on_loggedin',
193 'otto_disable_preview_button',
194 'periodic_clear_ottopage_cache',
195 'periodic_clear_ottopost_cache',
196 'periodic_clear_otto_cache'
197 ],
198 'advanced' => [
199 'disable_common_robots_metabox',
200 'disable_advance_robots_metabox',
201 'disable_redirection_metabox',
202 'disable_canonical_metabox',
203 'disable_social_opengraph_metabox',
204 'disable_schema_markup_metabox',
205 'disable_seo_metabox'
206 ],
207 'features' => [
208 'enabled_elementor_plugin_css',
209 'enabled_elementor_plugin_css_color',
210 'import_external_data',
211 'content_genius_sync_roles'
212 ],
213 'api' => [
214 'searchatlas_api_key',
215 'apikey',
216 'google_index_api_config'
217 ]
218 ];
219
220 return $section_map[$section] ?? [];
221 }
222 }
223
224 /**
225 * Update Plugin Settings Tool
226 */
227 class MCP_Tool_Update_Plugin_Settings extends MCP_Tool_Base {
228
229 public function get_name() {
230 return 'wordpress_update_plugin_settings';
231 }
232
233 public function get_description() {
234 return 'Update one or more plugin settings. Allows modifying plugin configuration including features, SEO controls, API keys, whitelabel settings, and more.';
235 }
236
237 public function get_input_schema() {
238 return [
239 'type' => 'object',
240 'properties' => [
241 'settings' => [
242 'type' => 'object',
243 'description' => 'Key-value pairs of settings to update. Each key is a setting name and value is the new value.',
244 'additionalProperties' => true
245 ],
246 'merge' => [
247 'type' => 'boolean',
248 'description' => 'If true (default), merges with existing settings. If false, replaces all settings with provided values.',
249 'default' => true
250 ]
251 ],
252 'required' => ['settings']
253 ];
254 }
255
256 public function execute($params) {
257 // Validate and check permissions
258 $this->validate_params($params);
259 $this->require_capability('manage_options');
260
261 if (empty($params['settings']) || !is_array($params['settings'])) {
262 throw new Exception('Settings must be a non-empty object/array');
263 }
264
265 $merge = isset($params['merge']) ? (bool)$params['merge'] : true;
266
267 // Resolve any aliased or misnamed keys to their canonical nested paths
268 $settings = $this->resolve_setting_aliases($params['settings']);
269
270 // Get current options
271 $current_options = get_option(Metasync::option_name, []);
272
273 if ($merge) {
274 // Deep merge to preserve nested sub-arrays (e.g. general, seo_controls)
275 $new_options = $this->array_merge_deep($current_options, $settings);
276 } else {
277 // Replace all settings
278 $new_options = $settings;
279 }
280
281 // Sanitize sensitive fields
282 $new_options = $this->sanitize_settings($new_options);
283
284 // Encrypt the Search Atlas API key at rest if this update supplied a new
285 // value. It is persisted as enc_v1: ciphertext so the cleartext never
286 // lands in wp_options. Already-encrypted values pass through untouched.
287 if (isset($new_options['general']['searchatlas_api_key'])
288 && !Metasync::is_encrypted_api_key($new_options['general']['searchatlas_api_key'])) {
289 $new_options['general']['searchatlas_api_key'] =
290 Metasync::encrypt_api_key($new_options['general']['searchatlas_api_key']);
291 }
292 if (isset($new_options['searchatlas_api_key'])
293 && !Metasync::is_encrypted_api_key($new_options['searchatlas_api_key'])) {
294 $new_options['searchatlas_api_key'] =
295 Metasync::encrypt_api_key($new_options['searchatlas_api_key']);
296 }
297
298 // The whitelabel settings password is encrypted at rest —
299 // never persist a plaintext value. encrypt_secret() is a no-op on
300 // values that are already encrypted.
301 if (!empty($new_options['whitelabel']['settings_password']) && is_string($new_options['whitelabel']['settings_password'])) {
302 $new_options['whitelabel']['settings_password'] = Metasync::encrypt_secret($new_options['whitelabel']['settings_password']);
303 }
304
305 // Update the option
306 $updated = update_option(Metasync::option_name, $new_options);
307
308 if ($updated === false && $current_options !== $new_options) {
309 throw new Exception('Failed to update plugin settings');
310 }
311
312 // Clear any relevant caches
313 wp_cache_delete(Metasync::option_name, 'options');
314 Metasync::invalidate_api_key_cache();
315
316 return $this->success([
317 'message' => 'Plugin settings updated successfully',
318 'updated_keys' => array_keys($params['settings']),
319 'merge_mode' => $merge,
320 'total_settings' => count($new_options)
321 ]);
322 }
323
324 /**
325 * Resolve aliased or flat keys to their canonical nested location.
326 *
327 * When an MCP consumer sends a flat key like "enable_schema_markup", this
328 * method routes it into the correct nested sub-array ("general") so it
329 * updates the same option the admin UI uses.
330 */
331 private function resolve_setting_aliases($settings) {
332 // Map of flat keys → [section, canonical_key]
333 $nested_key_map = [
334 'enable_schema_markup' => ['general', 'enable_schema_markup'],
335 'enable_schema' => ['general', 'enable_schema_markup'],
336 'default_schema_type' => ['general', 'default_schema_type'],
337 ];
338
339 $resolved = [];
340 foreach ($settings as $key => $value) {
341 if (isset($nested_key_map[$key])) {
342 list($section, $canonical) = $nested_key_map[$key];
343 if (!isset($resolved[$section])) {
344 $resolved[$section] = [];
345 }
346 $resolved[$section][$canonical] = $value;
347 } else {
348 $resolved[$key] = $value;
349 }
350 }
351
352 return $resolved;
353 }
354
355 /**
356 * Recursively merge two arrays, preserving nested sub-arrays.
357 */
358 private function array_merge_deep($base, $override) {
359 foreach ($override as $key => $value) {
360 if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
361 $base[$key] = $this->array_merge_deep($base[$key], $value);
362 } else {
363 $base[$key] = $value;
364 }
365 }
366 return $base;
367 }
368
369 /**
370 * Sanitize settings before saving
371 */
372 private function sanitize_settings($settings) {
373 $sanitized = [];
374
375 foreach ($settings as $key => $value) {
376 $key = sanitize_key($key);
377
378 // Handle different types of values
379 if (is_array($value)) {
380 $sanitized[$key] = $this->sanitize_array($value);
381 } elseif (is_bool($value)) {
382 $sanitized[$key] = (bool)$value;
383 } elseif (is_numeric($value)) {
384 $sanitized[$key] = $value;
385 } elseif ($this->is_sensitive_field($key)) {
386 // Don't sanitize sensitive fields too aggressively
387 $sanitized[$key] = $value;
388 } elseif ($this->is_url_field($key)) {
389 $sanitized[$key] = esc_url_raw($value);
390 } else {
391 $sanitized[$key] = sanitize_text_field($value);
392 }
393 }
394
395 return $sanitized;
396 }
397
398 /**
399 * Sanitize array values recursively
400 */
401 private function sanitize_array($array) {
402 $sanitized = [];
403 foreach ($array as $key => $value) {
404 $key = sanitize_key($key);
405 if (is_array($value)) {
406 $sanitized[$key] = $this->sanitize_array($value);
407 } elseif (is_bool($value)) {
408 $sanitized[$key] = $value;
409 } elseif (is_numeric($value)) {
410 $sanitized[$key] = $value;
411 } else {
412 $sanitized[$key] = sanitize_text_field($value);
413 }
414 }
415 return $sanitized;
416 }
417
418 /**
419 * Check if field is sensitive (API keys, tokens, passwords)
420 */
421 private function is_sensitive_field($key) {
422 $sensitive_patterns = ['api_key', 'apikey', 'token', 'password', 'secret'];
423 foreach ($sensitive_patterns as $pattern) {
424 if (stripos($key, $pattern) !== false) {
425 return true;
426 }
427 }
428 return false;
429 }
430
431 /**
432 * Check if field should be a URL
433 */
434 private function is_url_field($key) {
435 $url_patterns = ['url', 'uri', 'domain', 'logo', 'link'];
436 foreach ($url_patterns as $pattern) {
437 if (stripos($key, $pattern) !== false) {
438 return true;
439 }
440 }
441 return false;
442 }
443 }
444
445 /**
446 * List Available Settings Tool
447 */
448 class MCP_Tool_List_Plugin_Settings_Schema extends MCP_Tool_Base {
449
450 public function get_name() {
451 return 'wordpress_list_plugin_settings_schema';
452 }
453
454 public function get_description() {
455 return 'Get a schema of all available plugin settings with descriptions and expected types. Useful for understanding what settings can be configured.';
456 }
457
458 public function get_input_schema() {
459 return [
460 'type' => 'object',
461 'properties' => (object)[],
462 'required' => []
463 ];
464 }
465
466 public function execute($params) {
467 $this->validate_params($params);
468 $this->require_capability('manage_options');
469
470 $schema = [
471 'general' => [
472 'description' => 'General plugin configuration',
473 'settings' => [
474 'searchatlas_api_key' => [
475 'type' => 'string',
476 'description' => 'Search Atlas API key for integration',
477 'sensitive' => true
478 ],
479 'apikey' => [
480 'type' => 'string',
481 'description' => 'Plugin authentication token',
482 'sensitive' => true
483 ],
484 'hide_dashboard_framework' => [
485 'type' => 'boolean',
486 'description' => 'Hide the dashboard framework'
487 ],
488 'show_admin_bar_status' => [
489 'type' => 'boolean',
490 'description' => 'Show plugin status in admin bar'
491 ],
492 'enable_auto_updates' => [
493 'type' => 'boolean',
494 'description' => 'Enable automatic plugin updates'
495 ],
496 'enable_schema_markup' => [
497 'type' => 'boolean',
498 'description' => 'Enable automatic schema markup generation. Canonical key — do NOT use "enable_schema".'
499 ],
500 'default_schema_type' => [
501 'type' => 'string',
502 'description' => 'Default schema type for new posts (e.g., "article", "WebSite")'
503 ]
504 ]
505 ],
506 'whitelabel' => [
507 'description' => 'White-label branding configuration',
508 'settings' => [
509 'white_label_plugin_name' => [
510 'type' => 'string',
511 'description' => 'Custom plugin name'
512 ],
513 'whitelabel_otto_name' => [
514 'type' => 'string',
515 'description' => 'Custom Otto feature name'
516 ],
517 'whitelabel_logo_url' => [
518 'type' => 'string',
519 'description' => 'URL to custom logo image'
520 ],
521 'whitelabel_domain_url' => [
522 'type' => 'string',
523 'description' => 'Custom dashboard domain URL'
524 ],
525 'white_label_plugin_description' => [
526 'type' => 'string',
527 'description' => 'Custom plugin description'
528 ],
529 'white_label_plugin_author' => [
530 'type' => 'string',
531 'description' => 'Custom author name'
532 ],
533 'white_label_plugin_author_uri' => [
534 'type' => 'string',
535 'description' => 'Custom author URI'
536 ],
537 'white_label_plugin_uri' => [
538 'type' => 'string',
539 'description' => 'Custom plugin URI'
540 ]
541 ]
542 ],
543 'seo' => [
544 'description' => 'SEO controls and indexation settings',
545 'settings' => [
546 'index_date_archives' => [
547 'type' => 'boolean',
548 'description' => 'Disallow date archives from indexation'
549 ],
550 'index_tag_archives' => [
551 'type' => 'boolean',
552 'description' => 'Disallow tag archives from indexation'
553 ],
554 'index_author_archives' => [
555 'type' => 'boolean',
556 'description' => 'Disallow author archives from indexation'
557 ],
558 'index_format_archives' => [
559 'type' => 'boolean',
560 'description' => 'Disallow format archives from indexation'
561 ],
562 'index_category_archives' => [
563 'type' => 'boolean',
564 'description' => 'Disallow category archives from indexation'
565 ],
566 'override_robots_tags' => [
567 'type' => 'boolean',
568 'description' => 'Override robots tags from other plugins'
569 ],
570 'enable_googleinstantindex' => [
571 'type' => 'boolean',
572 'description' => 'Enable Google Instant Indexing'
573 ]
574 ]
575 ],
576 'social' => [
577 'description' => 'Social media and Otto settings',
578 'settings' => [
579 'otto_pixel_uuid' => [
580 'type' => 'string',
581 'description' => 'Otto Pixel UUID for tracking'
582 ],
583 'otto_disable_on_loggedin' => [
584 'type' => 'boolean',
585 'description' => 'Disable Otto for logged-in users'
586 ],
587 'otto_disable_preview_button' => [
588 'type' => 'boolean',
589 'description' => 'Disable Otto frontend toolbar'
590 ]
591 ]
592 ],
593 'advanced' => [
594 'description' => 'Advanced meta box visibility controls',
595 'settings' => [
596 'disable_common_robots_metabox' => [
597 'type' => 'boolean',
598 'description' => 'Disable common robots meta box in post editor'
599 ],
600 'disable_advance_robots_metabox' => [
601 'type' => 'boolean',
602 'description' => 'Disable advanced robots meta box in post editor'
603 ],
604 'disable_redirection_metabox' => [
605 'type' => 'boolean',
606 'description' => 'Disable redirection meta box in post editor'
607 ],
608 'disable_canonical_metabox' => [
609 'type' => 'boolean',
610 'description' => 'Disable canonical meta box in post editor'
611 ],
612 'disable_social_opengraph_metabox' => [
613 'type' => 'boolean',
614 'description' => 'Disable social/OpenGraph meta box in post editor'
615 ],
616 'disable_schema_markup_metabox' => [
617 'type' => 'boolean',
618 'description' => 'Disable schema markup meta box in post editor'
619 ],
620 'disable_seo_metabox' => [
621 'type' => 'boolean',
622 'description' => 'Disable SEO Title & Meta Description meta box in Classic editor'
623 ]
624 ]
625 ],
626 'mcp' => [
627 'description' => 'MCP Server configuration',
628 'settings' => [
629 'metasync_mcp_enabled' => [
630 'type' => 'boolean',
631 'description' => 'Enable/disable the MCP server',
632 'stored_separately' => true,
633 'option_name' => 'metasync_mcp_enabled'
634 ],
635 'metasync_mcp_api_key' => [
636 'type' => 'string',
637 'description' => 'MCP server API key for authentication',
638 'sensitive' => true,
639 'stored_separately' => true,
640 'option_name' => 'metasync_mcp_api_key'
641 ]
642 ]
643 ]
644 ];
645
646 return $this->success([
647 'schema' => $schema,
648 'sections' => array_keys($schema),
649 'total_sections' => count($schema),
650 'note' => 'Some settings like MCP configuration are stored as separate options, not in the main metasync_options array'
651 ]);
652 }
653 }
654
655 /**
656 * Get MCP Server Settings Tool
657 */
658 class MCP_Tool_Get_MCP_Settings extends MCP_Tool_Base {
659
660 public function get_name() {
661 return 'wordpress_get_mcp_settings';
662 }
663
664 public function get_description() {
665 return 'Get MCP server-specific settings including authentication information and endpoint URLs.';
666 }
667
668 public function get_input_schema() {
669 return [
670 'type' => 'object',
671 'properties' => (object)[],
672 'required' => []
673 ];
674 }
675
676 public function execute($params) {
677 $this->validate_params($params);
678 $this->require_capability('manage_options');
679
680 $options = get_option('metasync_options', []);
681 $plugin_auth_token = isset($options['general']['apikey']) ? $options['general']['apikey'] : '';
682
683 return $this->success([
684 'mcp_enabled' => true, // MCP is always enabled
685 'authentication' => [
686 'type' => 'plugin_auth_token',
687 'header' => 'X-API-Key',
688 'token_set' => !empty($plugin_auth_token),
689 'token_length' => !empty($plugin_auth_token) ? strlen($plugin_auth_token) : 0,
690 'token_preview' => !empty($plugin_auth_token) ? substr($plugin_auth_token, 0, 8) . '...' : ''
691 ],
692 'endpoints' => [
693 'rest_endpoint' => rest_url('metasync/v1/mcp'),
694 'health_endpoint' => rest_url('metasync/v1/mcp/health')
695 ],
696 'version' => METASYNC_VERSION
697 ]);
698 }
699 }
700
701