PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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.7.1, at wp-mcp-server/tools/class-mcp-tool-plugin-settings.php

696 lines 25.9 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_schema_markup',
164 'default_schema_type'
165 ],
166 'whitelabel' => [
167 'white_label_plugin_name',
168 'whitelabel_otto_name',
169 'whitelabel_logo_url',
170 'whitelabel_domain_url',
171 'white_label_plugin_description',
172 'white_label_plugin_author',
173 'white_label_plugin_author_uri',
174 'white_label_plugin_uri',
175 'white_label_plugin_menu_slug',
176 'white_label_plugin_menu_icon',
177 'whitelabel_settings_password'
178 ],
179 'seo' => [
180 'index_date_archives',
181 'index_tag_archives',
182 'index_author_archives',
183 'index_format_archives',
184 'index_category_archives',
185 'override_robots_tags',
186 'enable_googleinstantindex',
187 'google_index_api_config'
188 ],
189 'social' => [
190 'otto_pixel_uuid',
191 'otto_disable_on_loggedin',
192 'otto_disable_preview_button',
193 'periodic_clear_ottopage_cache',
194 'periodic_clear_ottopost_cache',
195 'periodic_clear_otto_cache'
196 ],
197 'advanced' => [
198 'disable_common_robots_metabox',
199 'disable_advance_robots_metabox',
200 'disable_redirection_metabox',
201 'disable_canonical_metabox',
202 'disable_social_opengraph_metabox',
203 'disable_schema_markup_metabox',
204 'disable_seo_metabox'
205 ],
206 'features' => [
207 'enabled_elementor_plugin_css',
208 'enabled_elementor_plugin_css_color',
209 'import_external_data',
210 'content_genius_sync_roles'
211 ],
212 'api' => [
213 'searchatlas_api_key',
214 'apikey',
215 'google_index_api_config'
216 ]
217 ];
218
219 return $section_map[$section] ?? [];
220 }
221 }
222
223 /**
224 * Update Plugin Settings Tool
225 */
226 class MCP_Tool_Update_Plugin_Settings extends MCP_Tool_Base {
227
228 public function get_name() {
229 return 'wordpress_update_plugin_settings';
230 }
231
232 public function get_description() {
233 return 'Update one or more plugin settings. Allows modifying plugin configuration including features, SEO controls, API keys, whitelabel settings, and more.';
234 }
235
236 public function get_input_schema() {
237 return [
238 'type' => 'object',
239 'properties' => [
240 'settings' => [
241 'type' => 'object',
242 'description' => 'Key-value pairs of settings to update. Each key is a setting name and value is the new value.',
243 'additionalProperties' => true
244 ],
245 'merge' => [
246 'type' => 'boolean',
247 'description' => 'If true (default), merges with existing settings. If false, replaces all settings with provided values.',
248 'default' => true
249 ]
250 ],
251 'required' => ['settings']
252 ];
253 }
254
255 public function execute($params) {
256 // Validate and check permissions
257 $this->validate_params($params);
258 $this->require_capability('manage_options');
259
260 if (empty($params['settings']) || !is_array($params['settings'])) {
261 throw new Exception('Settings must be a non-empty object/array');
262 }
263
264 $merge = isset($params['merge']) ? (bool)$params['merge'] : true;
265
266 // Resolve any aliased or misnamed keys to their canonical nested paths
267 $settings = $this->resolve_setting_aliases($params['settings']);
268
269 // Get current options
270 $current_options = get_option(Metasync::option_name, []);
271
272 if ($merge) {
273 // Deep merge to preserve nested sub-arrays (e.g. general, seo_controls)
274 $new_options = $this->array_merge_deep($current_options, $settings);
275 } else {
276 // Replace all settings
277 $new_options = $settings;
278 }
279
280 // Sanitize sensitive fields
281 $new_options = $this->sanitize_settings($new_options);
282
283 // Encrypt the Search Atlas API key at rest if this update supplied a new
284 // value. It is persisted as enc_v1: ciphertext so the cleartext never
285 // lands in wp_options. Already-encrypted values pass through untouched.
286 if (isset($new_options['general']['searchatlas_api_key'])
287 && !Metasync::is_encrypted_api_key($new_options['general']['searchatlas_api_key'])) {
288 $new_options['general']['searchatlas_api_key'] =
289 Metasync::encrypt_api_key($new_options['general']['searchatlas_api_key']);
290 }
291 if (isset($new_options['searchatlas_api_key'])
292 && !Metasync::is_encrypted_api_key($new_options['searchatlas_api_key'])) {
293 $new_options['searchatlas_api_key'] =
294 Metasync::encrypt_api_key($new_options['searchatlas_api_key']);
295 }
296
297 // The whitelabel settings password is encrypted at rest —
298 // never persist a plaintext value. encrypt_secret() is a no-op on
299 // values that are already encrypted.
300 if (!empty($new_options['whitelabel']['settings_password']) && is_string($new_options['whitelabel']['settings_password'])) {
301 $new_options['whitelabel']['settings_password'] = Metasync::encrypt_secret($new_options['whitelabel']['settings_password']);
302 }
303
304 // Update the option
305 $updated = update_option(Metasync::option_name, $new_options);
306
307 if ($updated === false && $current_options !== $new_options) {
308 throw new Exception('Failed to update plugin settings');
309 }
310
311 // Clear any relevant caches
312 wp_cache_delete(Metasync::option_name, 'options');
313 Metasync::invalidate_api_key_cache();
314
315 return $this->success([
316 'message' => 'Plugin settings updated successfully',
317 'updated_keys' => array_keys($params['settings']),
318 'merge_mode' => $merge,
319 'total_settings' => count($new_options)
320 ]);
321 }
322
323 /**
324 * Resolve aliased or flat keys to their canonical nested location.
325 *
326 * When an MCP consumer sends a flat key like "enable_schema_markup", this
327 * method routes it into the correct nested sub-array ("general") so it
328 * updates the same option the admin UI uses.
329 */
330 private function resolve_setting_aliases($settings) {
331 // Map of flat keys → [section, canonical_key]
332 $nested_key_map = [
333 'enable_schema_markup' => ['general', 'enable_schema_markup'],
334 'enable_schema' => ['general', 'enable_schema_markup'],
335 'default_schema_type' => ['general', 'default_schema_type'],
336 ];
337
338 $resolved = [];
339 foreach ($settings as $key => $value) {
340 if (isset($nested_key_map[$key])) {
341 list($section, $canonical) = $nested_key_map[$key];
342 if (!isset($resolved[$section])) {
343 $resolved[$section] = [];
344 }
345 $resolved[$section][$canonical] = $value;
346 } else {
347 $resolved[$key] = $value;
348 }
349 }
350
351 return $resolved;
352 }
353
354 /**
355 * Recursively merge two arrays, preserving nested sub-arrays.
356 */
357 private function array_merge_deep($base, $override) {
358 foreach ($override as $key => $value) {
359 if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
360 $base[$key] = $this->array_merge_deep($base[$key], $value);
361 } else {
362 $base[$key] = $value;
363 }
364 }
365 return $base;
366 }
367
368 /**
369 * Sanitize settings before saving
370 */
371 private function sanitize_settings($settings) {
372 $sanitized = [];
373
374 foreach ($settings as $key => $value) {
375 $key = sanitize_key($key);
376
377 // Handle different types of values
378 if (is_array($value)) {
379 $sanitized[$key] = $this->sanitize_array($value);
380 } elseif (is_bool($value)) {
381 $sanitized[$key] = (bool)$value;
382 } elseif (is_numeric($value)) {
383 $sanitized[$key] = $value;
384 } elseif ($this->is_sensitive_field($key)) {
385 // Don't sanitize sensitive fields too aggressively
386 $sanitized[$key] = $value;
387 } elseif ($this->is_url_field($key)) {
388 $sanitized[$key] = esc_url_raw($value);
389 } else {
390 $sanitized[$key] = sanitize_text_field($value);
391 }
392 }
393
394 return $sanitized;
395 }
396
397 /**
398 * Sanitize array values recursively
399 */
400 private function sanitize_array($array) {
401 $sanitized = [];
402 foreach ($array as $key => $value) {
403 $key = sanitize_key($key);
404 if (is_array($value)) {
405 $sanitized[$key] = $this->sanitize_array($value);
406 } elseif (is_bool($value)) {
407 $sanitized[$key] = $value;
408 } elseif (is_numeric($value)) {
409 $sanitized[$key] = $value;
410 } else {
411 $sanitized[$key] = sanitize_text_field($value);
412 }
413 }
414 return $sanitized;
415 }
416
417 /**
418 * Check if field is sensitive (API keys, tokens, passwords)
419 */
420 private function is_sensitive_field($key) {
421 $sensitive_patterns = ['api_key', 'apikey', 'token', 'password', 'secret'];
422 foreach ($sensitive_patterns as $pattern) {
423 if (stripos($key, $pattern) !== false) {
424 return true;
425 }
426 }
427 return false;
428 }
429
430 /**
431 * Check if field should be a URL
432 */
433 private function is_url_field($key) {
434 $url_patterns = ['url', 'uri', 'domain', 'logo', 'link'];
435 foreach ($url_patterns as $pattern) {
436 if (stripos($key, $pattern) !== false) {
437 return true;
438 }
439 }
440 return false;
441 }
442 }
443
444 /**
445 * List Available Settings Tool
446 */
447 class MCP_Tool_List_Plugin_Settings_Schema extends MCP_Tool_Base {
448
449 public function get_name() {
450 return 'wordpress_list_plugin_settings_schema';
451 }
452
453 public function get_description() {
454 return 'Get a schema of all available plugin settings with descriptions and expected types. Useful for understanding what settings can be configured.';
455 }
456
457 public function get_input_schema() {
458 return [
459 'type' => 'object',
460 'properties' => (object)[],
461 'required' => []
462 ];
463 }
464
465 public function execute($params) {
466 $this->validate_params($params);
467 $this->require_capability('manage_options');
468
469 $schema = [
470 'general' => [
471 'description' => 'General plugin configuration',
472 'settings' => [
473 'searchatlas_api_key' => [
474 'type' => 'string',
475 'description' => 'Search Atlas API key for integration',
476 'sensitive' => true
477 ],
478 'apikey' => [
479 'type' => 'string',
480 'description' => 'Plugin authentication token',
481 'sensitive' => true
482 ],
483 'hide_dashboard_framework' => [
484 'type' => 'boolean',
485 'description' => 'Hide the dashboard framework'
486 ],
487 'show_admin_bar_status' => [
488 'type' => 'boolean',
489 'description' => 'Show plugin status in admin bar'
490 ],
491 'enable_schema_markup' => [
492 'type' => 'boolean',
493 'description' => 'Enable automatic schema markup generation. Canonical key — do NOT use "enable_schema".'
494 ],
495 'default_schema_type' => [
496 'type' => 'string',
497 'description' => 'Default schema type for new posts (e.g., "article", "WebSite")'
498 ]
499 ]
500 ],
501 'whitelabel' => [
502 'description' => 'White-label branding configuration',
503 'settings' => [
504 'white_label_plugin_name' => [
505 'type' => 'string',
506 'description' => 'Custom plugin name'
507 ],
508 'whitelabel_otto_name' => [
509 'type' => 'string',
510 'description' => 'Custom Otto feature name'
511 ],
512 'whitelabel_logo_url' => [
513 'type' => 'string',
514 'description' => 'URL to custom logo image'
515 ],
516 'whitelabel_domain_url' => [
517 'type' => 'string',
518 'description' => 'Custom dashboard domain URL'
519 ],
520 'white_label_plugin_description' => [
521 'type' => 'string',
522 'description' => 'Custom plugin description'
523 ],
524 'white_label_plugin_author' => [
525 'type' => 'string',
526 'description' => 'Custom author name'
527 ],
528 'white_label_plugin_author_uri' => [
529 'type' => 'string',
530 'description' => 'Custom author URI'
531 ],
532 'white_label_plugin_uri' => [
533 'type' => 'string',
534 'description' => 'Custom plugin URI'
535 ]
536 ]
537 ],
538 'seo' => [
539 'description' => 'SEO controls and indexation settings',
540 'settings' => [
541 'index_date_archives' => [
542 'type' => 'boolean',
543 'description' => 'Disallow date archives from indexation'
544 ],
545 'index_tag_archives' => [
546 'type' => 'boolean',
547 'description' => 'Disallow tag archives from indexation'
548 ],
549 'index_author_archives' => [
550 'type' => 'boolean',
551 'description' => 'Disallow author archives from indexation'
552 ],
553 'index_format_archives' => [
554 'type' => 'boolean',
555 'description' => 'Disallow format archives from indexation'
556 ],
557 'index_category_archives' => [
558 'type' => 'boolean',
559 'description' => 'Disallow category archives from indexation'
560 ],
561 'override_robots_tags' => [
562 'type' => 'boolean',
563 'description' => 'Override robots tags from other plugins'
564 ],
565 'enable_googleinstantindex' => [
566 'type' => 'boolean',
567 'description' => 'Enable Google Instant Indexing'
568 ]
569 ]
570 ],
571 'social' => [
572 'description' => 'Social media and Otto settings',
573 'settings' => [
574 'otto_pixel_uuid' => [
575 'type' => 'string',
576 'description' => 'Otto Pixel UUID for tracking'
577 ],
578 'otto_disable_on_loggedin' => [
579 'type' => 'boolean',
580 'description' => 'Disable Otto for logged-in users'
581 ],
582 'otto_disable_preview_button' => [
583 'type' => 'boolean',
584 'description' => 'Disable Otto frontend toolbar'
585 ]
586 ]
587 ],
588 'advanced' => [
589 'description' => 'Advanced meta box visibility controls',
590 'settings' => [
591 'disable_common_robots_metabox' => [
592 'type' => 'boolean',
593 'description' => 'Disable common robots meta box in post editor'
594 ],
595 'disable_advance_robots_metabox' => [
596 'type' => 'boolean',
597 'description' => 'Disable advanced robots meta box in post editor'
598 ],
599 'disable_redirection_metabox' => [
600 'type' => 'boolean',
601 'description' => 'Disable redirection meta box in post editor'
602 ],
603 'disable_canonical_metabox' => [
604 'type' => 'boolean',
605 'description' => 'Disable canonical meta box in post editor'
606 ],
607 'disable_social_opengraph_metabox' => [
608 'type' => 'boolean',
609 'description' => 'Disable social/OpenGraph meta box in post editor'
610 ],
611 'disable_schema_markup_metabox' => [
612 'type' => 'boolean',
613 'description' => 'Disable schema markup meta box in post editor'
614 ],
615 'disable_seo_metabox' => [
616 'type' => 'boolean',
617 'description' => 'Disable SEO Title & Meta Description meta box in Classic editor'
618 ]
619 ]
620 ],
621 'mcp' => [
622 'description' => 'MCP Server configuration',
623 'settings' => [
624 'metasync_mcp_enabled' => [
625 'type' => 'boolean',
626 'description' => 'Enable/disable the MCP server',
627 'stored_separately' => true,
628 'option_name' => 'metasync_mcp_enabled'
629 ],
630 'metasync_mcp_api_key' => [
631 'type' => 'string',
632 'description' => 'MCP server API key for authentication',
633 'sensitive' => true,
634 'stored_separately' => true,
635 'option_name' => 'metasync_mcp_api_key'
636 ]
637 ]
638 ]
639 ];
640
641 return $this->success([
642 'schema' => $schema,
643 'sections' => array_keys($schema),
644 'total_sections' => count($schema),
645 'note' => 'Some settings like MCP configuration are stored as separate options, not in the main metasync_options array'
646 ]);
647 }
648 }
649
650 /**
651 * Get MCP Server Settings Tool
652 */
653 class MCP_Tool_Get_MCP_Settings extends MCP_Tool_Base {
654
655 public function get_name() {
656 return 'wordpress_get_mcp_settings';
657 }
658
659 public function get_description() {
660 return 'Get MCP server-specific settings including authentication information and endpoint URLs.';
661 }
662
663 public function get_input_schema() {
664 return [
665 'type' => 'object',
666 'properties' => (object)[],
667 'required' => []
668 ];
669 }
670
671 public function execute($params) {
672 $this->validate_params($params);
673 $this->require_capability('manage_options');
674
675 $options = get_option('metasync_options', []);
676 $plugin_auth_token = isset($options['general']['apikey']) ? $options['general']['apikey'] : '';
677
678 return $this->success([
679 'mcp_enabled' => true, // MCP is always enabled
680 'authentication' => [
681 'type' => 'plugin_auth_token',
682 'header' => 'X-API-Key',
683 'token_set' => !empty($plugin_auth_token),
684 'token_length' => !empty($plugin_auth_token) ? strlen($plugin_auth_token) : 0,
685 'token_preview' => !empty($plugin_auth_token) ? substr($plugin_auth_token, 0, 8) . '...' : ''
686 ],
687 'endpoints' => [
688 'rest_endpoint' => rest_url('metasync/v1/mcp'),
689 'health_endpoint' => rest_url('metasync/v1/mcp/health')
690 ],
691 'version' => METASYNC_VERSION
692 ]);
693 }
694 }
695
696