PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.14
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.14
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 2.6.4 2.6.3 All 139 releases
metasync / wp-mcp-server / tools / class-mcp-tool-robots-sitemap.php

class-mcp-tool-robots-sitemap.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.14, at wp-mcp-server/tools/class-mcp-tool-robots-sitemap.php

812 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tool: Robots.txt and Sitemap
4 *
5 * Provides tools for managing robots.txt and sitemap.
6 *
7 * @package MetaSync
8 * @subpackage MCP_Server/Tools
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 /**
16 * Get Robots.txt Tool
17 */
18 class MCP_Tool_Get_Robots_Txt extends MCP_Tool_Base {
19
20 public function get_name() {
21 return 'wordpress_get_robots_txt';
22 }
23
24 public function get_description() {
25 return 'Get the current robots.txt content';
26 }
27
28 public function get_input_schema() {
29 return [
30 'type' => 'object',
31 'properties' => (object)[],
32 'required' => []
33 ];
34 }
35
36 public function execute($params) {
37 $this->validate_params($params);
38 $this->require_capability('manage_options');
39
40 // Load MetaSync Robots.txt class
41 if (!class_exists('Metasync_Robots_Txt')) {
42 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'robots-txt/class-metasync-robots-txt.php';
43 }
44
45 $robots_txt = Metasync_Robots_Txt::get_instance();
46
47 // Read robots.txt using plugin's method
48 $content = $robots_txt->read_robots_file();
49
50 if (is_wp_error($content)) {
51 throw new Exception($content->get_error_message());
52 }
53
54 return $this->success([
55 'content' => $content,
56 'file_exists' => $robots_txt->file_exists(),
57 'file_path' => ABSPATH . 'robots.txt',
58 'url' => site_url('robots.txt'),
59 'is_writable' => $robots_txt->is_writable()
60 ]);
61 }
62 }
63
64 /**
65 * Update Robots.txt Tool
66 */
67 class MCP_Tool_Update_Robots_Txt extends MCP_Tool_Base {
68
69 public function get_name() {
70 return 'wordpress_update_robots_txt';
71 }
72
73 public function get_description() {
74 return 'Update the robots.txt file content';
75 }
76
77 public function get_input_schema() {
78 return [
79 'type' => 'object',
80 'properties' => [
81 'content' => [
82 'type' => 'string',
83 'description' => 'New robots.txt content'
84 ]
85 ],
86 'required' => ['content']
87 ];
88 }
89
90 public function execute($params) {
91 $this->validate_params($params);
92 $this->require_capability('manage_options');
93
94 $content = $params['content']; // Don't sanitize - preserve exact formatting
95
96 // Load MetaSync Robots.txt class
97 if (!class_exists('Metasync_Robots_Txt')) {
98 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'robots-txt/class-metasync-robots-txt.php';
99 }
100
101 $robots_txt = Metasync_Robots_Txt::get_instance();
102
103 // Write using plugin's method (automatically creates backup)
104 $result = $robots_txt->write_robots_file($content);
105
106 if (is_wp_error($result)) {
107 throw new Exception($result->get_error_message());
108 }
109
110 return $this->success([
111 'file_path' => ABSPATH . 'robots.txt',
112 'bytes_written' => strlen($content),
113 'url' => site_url('robots.txt'),
114 'backup_created' => true
115 ], 'Robots.txt updated successfully (backup created)');
116 }
117 }
118
119 /**
120 * Get Sitemap Status Tool
121 */
122 class MCP_Tool_Get_Sitemap_Status extends MCP_Tool_Base {
123
124 public function get_name() {
125 return 'wordpress_get_sitemap_status';
126 }
127
128 public function get_description() {
129 return 'Get MetaSync sitemap status and configuration';
130 }
131
132 public function get_input_schema() {
133 return [
134 'type' => 'object',
135 'properties' => (object)[],
136 'required' => []
137 ];
138 }
139
140 public function execute($params) {
141 $this->validate_params($params);
142 $this->require_capability('read');
143
144 // Load MetaSync sitemap generator for authoritative existence check
145 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'sitemap/class-metasync-sitemap-generator.php';
146
147 // Get MetaSync sitemap info
148 $sitemap_files = get_option('metasync_sitemap_files', []);
149 $total_urls = get_option('metasync_sitemap_total_urls', 0);
150 $last_generated = get_option('metasync_sitemap_last_generated', false);
151 $auto_update = get_option('metasync_sitemap_auto_update', false);
152
153 // Use the plugin's three-step check (virtual → tracked → physical)
154 // instead of raw file_exists() which misses virtual/tracked sitemaps
155 $sitemap_generator = new Metasync_Sitemap_Generator();
156
157 $sitemap_info = [
158 'sitemap_index_url' => home_url('/sitemap_index.xml'),
159 'sitemap_files' => $sitemap_files,
160 'total_urls' => $total_urls,
161 'last_generated' => $last_generated,
162 'auto_update_enabled' => $auto_update,
163 'sitemap_exists' => $sitemap_generator->sitemap_exists()
164 ];
165
166 // Check if WordPress XML sitemaps are enabled (WP 5.5+)
167 $wp_sitemaps_enabled = function_exists('wp_sitemaps_get_server');
168 $wp_sitemap_disabled = get_option('metasync_disable_wp_sitemap', false);
169
170 $sitemap_info['wordpress_core_sitemap_disabled'] = $wp_sitemap_disabled;
171 $sitemap_info['wordpress_core_sitemap_enabled'] = $wp_sitemaps_enabled && !$wp_sitemap_disabled;
172
173 if ($wp_sitemaps_enabled && !$wp_sitemap_disabled) {
174 // Get post types in sitemap
175 $post_types = get_post_types(['public' => true], 'names');
176 $sitemap_info['post_types'] = array_values($post_types);
177
178 // Check if sitemaps are disabled
179 $sitemap_info['disabled'] = (bool) get_option('blog_public') === false;
180 }
181
182 // Check for common sitemap plugins
183 $sitemap_plugins = [
184 'yoast' => defined('WPSEO_VERSION'),
185 'rank_math' => defined('RANK_MATH_VERSION'),
186 'all_in_one_seo' => defined('AIOSEO_VERSION')
187 ];
188
189 $sitemap_info['plugins'] = $sitemap_plugins;
190 $sitemap_info['has_sitemap_plugin'] = in_array(true, $sitemap_plugins, true);
191
192 return $this->success($sitemap_info);
193 }
194 }
195
196 /**
197 * Regenerate Sitemap Tool
198 */
199 class MCP_Tool_Regenerate_Sitemap extends MCP_Tool_Base {
200
201 public function get_name() {
202 return 'wordpress_regenerate_sitemap';
203 }
204
205 public function get_description() {
206 return 'Regenerate MetaSync XML sitemap (generates sitemap_index.xml and individual sitemap files)';
207 }
208
209 public function get_input_schema() {
210 return [
211 'type' => 'object',
212 'properties' => (object)[],
213 'required' => []
214 ];
215 }
216
217 public function execute($params) {
218 $this->validate_params($params);
219 $this->require_capability('manage_options');
220
221 // Load MetaSync sitemap generator
222 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'sitemap/class-metasync-sitemap-generator.php';
223
224 if (!class_exists('Metasync_Sitemap_Generator')) {
225 throw new Exception('MetaSync sitemap generator is not available');
226 }
227
228 $sitemap_generator = new Metasync_Sitemap_Generator();
229
230 // Generate the sitemap
231 $result = $sitemap_generator->generate_sitemap();
232
233 if (is_wp_error($result)) {
234 throw new Exception($result->get_error_message());
235 }
236
237 // Get sitemap info
238 $sitemap_url = home_url('/sitemap_index.xml');
239 $last_generated = $sitemap_generator->get_last_generated_time();
240
241 return $this->success([
242 'regenerated' => true,
243 'sitemap_url' => $sitemap_url,
244 'sitemap_index' => $sitemap_url,
245 'last_generated' => $last_generated,
246 'message' => 'MetaSync sitemap generated successfully'
247 ], 'Sitemap regenerated successfully');
248 }
249 }
250
251 /**
252 * Exclude from Sitemap Tool
253 */
254 class MCP_Tool_Exclude_From_Sitemap extends MCP_Tool_Base {
255
256 public function get_name() {
257 return 'wordpress_exclude_from_sitemap';
258 }
259
260 public function get_description() {
261 return 'Exclude a post or page from the sitemap by setting noindex';
262 }
263
264 public function get_input_schema() {
265 return [
266 'type' => 'object',
267 'properties' => [
268 'post_id' => [
269 'type' => 'integer',
270 'description' => 'Post or page ID to exclude',
271 'minimum' => 1
272 ],
273 'exclude' => [
274 'type' => 'boolean',
275 'description' => 'True to exclude, false to include',
276 'default' => true
277 ]
278 ],
279 'required' => ['post_id']
280 ];
281 }
282
283 public function execute($params) {
284 $this->validate_params($params);
285 $this->require_capability('edit_posts');
286
287 $post_id = $this->sanitize_integer($params['post_id']);
288 $exclude = isset($params['exclude']) ? (bool)$params['exclude'] : true;
289
290 // Verify post exists
291 $post = $this->verify_post_exists($post_id);
292
293 // Set robots meta (noindex excludes from sitemap)
294 $robots_value = $exclude ? 'noindex' : '';
295 update_post_meta($post_id, '_metasync_robots_index', $robots_value);
296
297 return $this->success([
298 'post_id' => $post_id,
299 'post_title' => $post->post_title,
300 'excluded_from_sitemap' => $exclude,
301 'robots_setting' => $robots_value ?: 'index'
302 ], $exclude ? 'Post excluded from sitemap' : 'Post included in sitemap');
303 }
304 }
305
306 /**
307 * Add Robots Rule Tool
308 */
309 class MCP_Tool_Add_Robots_Rule extends MCP_Tool_Base {
310
311 public function get_name() {
312 return 'wordpress_add_robots_rule';
313 }
314
315 public function get_description() {
316 return 'Add a specific rule to robots.txt';
317 }
318
319 public function get_input_schema() {
320 return [
321 'type' => 'object',
322 'properties' => [
323 'rule' => [
324 'type' => 'string',
325 'description' => 'The robots.txt rule to add (e.g., "Disallow: /admin/")',
326 ],
327 'user_agent' => [
328 'type' => 'string',
329 'description' => 'User-agent for the rule (default: *)',
330 ],
331 ],
332 'required' => ['rule']
333 ];
334 }
335
336 public function execute($params) {
337 $this->validate_params($params);
338 $this->require_capability('manage_options');
339
340 $rule = trim($params['rule']);
341 $user_agent = isset($params['user_agent']) ? trim($params['user_agent']) : '*';
342
343 // Get current content
344 $robots_file = ABSPATH . 'robots.txt';
345 $content = file_exists($robots_file) ? file_get_contents($robots_file) : '';
346
347 // Parse content to find or create user-agent block
348 $lines = explode("\n", $content);
349 $new_lines = [];
350 $found_user_agent = false;
351 $added = false;
352
353 foreach ($lines as $line) {
354 $new_lines[] = $line;
355
356 // Check if we found the matching user-agent
357 if (stripos($line, "User-agent: $user_agent") !== false) {
358 $found_user_agent = true;
359 }
360
361 // If we found the user-agent and hit a blank line or another user-agent, add rule before
362 if ($found_user_agent && !$added) {
363 if (trim($line) === '' || (stripos($line, 'User-agent:') !== false && $line !== "User-agent: $user_agent")) {
364 array_pop($new_lines); // Remove the blank/user-agent line temporarily
365 $new_lines[] = $rule;
366 $new_lines[] = $line; // Add it back
367 $added = true;
368 $found_user_agent = false;
369 }
370 }
371 }
372
373 // If user-agent not found, add new block
374 if (!$added) {
375 if (!empty($content) && substr($content, -1) !== "\n") {
376 $new_lines[] = '';
377 }
378 $new_lines[] = "User-agent: $user_agent";
379 $new_lines[] = $rule;
380 }
381
382 $new_content = implode("\n", $new_lines);
383
384 // Write to file
385 $result = file_put_contents($robots_file, $new_content);
386
387 if ($result === false) {
388 throw new Exception('Failed to write robots.txt file');
389 }
390
391 return $this->success([
392 'rule' => $rule,
393 'user_agent' => $user_agent,
394 'robots_txt_length' => strlen($new_content),
395 'message' => 'Rule added to robots.txt successfully',
396 ]);
397 }
398 }
399
400 /**
401 * Remove Robots Rule Tool
402 */
403 class MCP_Tool_Remove_Robots_Rule extends MCP_Tool_Base {
404
405 public function get_name() {
406 return 'wordpress_remove_robots_rule';
407 }
408
409 public function get_description() {
410 return 'Remove a specific rule from robots.txt';
411 }
412
413 public function get_input_schema() {
414 return [
415 'type' => 'object',
416 'properties' => [
417 'rule' => [
418 'type' => 'string',
419 'description' => 'The exact rule to remove (e.g., "Disallow: /admin/")',
420 ],
421 ],
422 'required' => ['rule']
423 ];
424 }
425
426 public function execute($params) {
427 $this->validate_params($params);
428 $this->require_capability('manage_options');
429
430 $rule = trim($params['rule']);
431
432 // Get current content
433 $robots_file = ABSPATH . 'robots.txt';
434
435 if (!file_exists($robots_file)) {
436 throw new Exception('robots.txt file does not exist');
437 }
438
439 $content = file_get_contents($robots_file);
440 $lines = explode("\n", $content);
441 $new_lines = [];
442 $removed = false;
443
444 foreach ($lines as $line) {
445 if (trim($line) === trim($rule)) {
446 $removed = true;
447 continue; // Skip this line
448 }
449 $new_lines[] = $line;
450 }
451
452 if (!$removed) {
453 throw new Exception('Rule not found in robots.txt');
454 }
455
456 $new_content = implode("\n", $new_lines);
457
458 // Write to file
459 $result = file_put_contents($robots_file, $new_content);
460
461 if ($result === false) {
462 throw new Exception('Failed to write robots.txt file');
463 }
464
465 return $this->success([
466 'rule' => $rule,
467 'robots_txt_length' => strlen($new_content),
468 'message' => 'Rule removed from robots.txt successfully',
469 ]);
470 }
471 }
472
473 /**
474 * Parse Robots.txt Tool
475 */
476 class MCP_Tool_Parse_Robots_Txt extends MCP_Tool_Base {
477
478 public function get_name() {
479 return 'wordpress_parse_robots_txt';
480 }
481
482 public function get_description() {
483 return 'Parse robots.txt into structured format';
484 }
485
486 public function get_input_schema() {
487 return [
488 'type' => 'object',
489 'properties' => (object)[],
490 'required' => []
491 ];
492 }
493
494 public function execute($params) {
495 $this->validate_params($params);
496 $this->require_capability('manage_options');
497
498 // Get current content
499 $robots_file = ABSPATH . 'robots.txt';
500 $exists = file_exists($robots_file);
501
502 if (!$exists) {
503 return $this->success([
504 'exists' => false,
505 'parsed' => [],
506 'message' => 'robots.txt file does not exist',
507 ]);
508 }
509
510 $content = file_get_contents($robots_file);
511 $lines = explode("\n", $content);
512
513 $parsed = [];
514 $current_agent = null;
515
516 foreach ($lines as $line) {
517 $line = trim($line);
518
519 // Skip empty lines and comments
520 if (empty($line) || $line[0] === '#') {
521 continue;
522 }
523
524 // Check for User-agent
525 if (stripos($line, 'User-agent:') === 0) {
526 $agent = trim(substr($line, 11));
527 $current_agent = $agent;
528 if (!isset($parsed[$agent])) {
529 $parsed[$agent] = [
530 'allow' => [],
531 'disallow' => [],
532 'sitemap' => [],
533 'other' => [],
534 ];
535 }
536 continue;
537 }
538
539 // If no user-agent yet, treat as global
540 if ($current_agent === null) {
541 $current_agent = 'global';
542 $parsed[$current_agent] = [
543 'allow' => [],
544 'disallow' => [],
545 'sitemap' => [],
546 'other' => [],
547 ];
548 }
549
550 // Parse directives
551 if (stripos($line, 'Allow:') === 0) {
552 $parsed[$current_agent]['allow'][] = trim(substr($line, 6));
553 } elseif (stripos($line, 'Disallow:') === 0) {
554 $parsed[$current_agent]['disallow'][] = trim(substr($line, 9));
555 } elseif (stripos($line, 'Sitemap:') === 0) {
556 $parsed[$current_agent]['sitemap'][] = trim(substr($line, 8));
557 } else {
558 $parsed[$current_agent]['other'][] = $line;
559 }
560 }
561
562 return $this->success([
563 'exists' => true,
564 'raw_content' => $content,
565 'parsed' => $parsed,
566 'user_agents' => array_keys($parsed),
567 ]);
568 }
569 }
570
571 /**
572 * Validate Robots.txt Tool
573 */
574 class MCP_Tool_Validate_Robots_Txt extends MCP_Tool_Base {
575
576 public function get_name() {
577 return 'wordpress_validate_robots_txt';
578 }
579
580 public function get_description() {
581 return 'Validate robots.txt syntax and check for common issues';
582 }
583
584 public function get_input_schema() {
585 return [
586 'type' => 'object',
587 'properties' => [
588 'content' => [
589 'type' => 'string',
590 'description' => 'robots.txt content to validate (optional, validates existing file if not provided)',
591 ],
592 ],
593 ];
594 }
595
596 public function execute($params) {
597 $this->validate_params($params);
598 $this->require_capability('manage_options');
599
600 // Get content to validate
601 if (isset($params['content'])) {
602 $content = $params['content'];
603 } else {
604 $robots_file = ABSPATH . 'robots.txt';
605 if (!file_exists($robots_file)) {
606 return $this->success([
607 'valid' => true,
608 'errors' => [],
609 'warnings' => [],
610 'message' => 'robots.txt file does not exist',
611 ]);
612 }
613 $content = file_get_contents($robots_file);
614 }
615
616 $lines = explode("\n", $content);
617 $errors = [];
618 $warnings = [];
619 $has_user_agent = false;
620
621 foreach ($lines as $line_num => $line) {
622 $line = trim($line);
623 $line_number = $line_num + 1;
624
625 // Skip empty lines and comments
626 if (empty($line) || $line[0] === '#') {
627 continue;
628 }
629
630 // Check for User-agent
631 if (stripos($line, 'User-agent:') === 0) {
632 $has_user_agent = true;
633 $agent = trim(substr($line, 11));
634 if (empty($agent)) {
635 $errors[] = "Line {$line_number}: User-agent cannot be empty";
636 }
637 continue;
638 }
639
640 // Check for valid directives
641 $valid_directives = ['Allow:', 'Disallow:', 'Sitemap:', 'Crawl-delay:'];
642 $is_valid = false;
643 foreach ($valid_directives as $directive) {
644 if (stripos($line, $directive) === 0) {
645 $is_valid = true;
646 break;
647 }
648 }
649
650 if (!$is_valid) {
651 $warnings[] = "Line {$line_number}: Unrecognized directive: {$line}";
652 }
653
654 // Check if directive appears before User-agent
655 if (!$has_user_agent && $is_valid) {
656 $warnings[] = "Line {$line_number}: Directive appears before any User-agent declaration";
657 }
658 }
659
660 // Check for common issues
661 if (!$has_user_agent && !empty(array_filter($lines))) {
662 $warnings[] = 'No User-agent directive found';
663 }
664
665 $is_valid = empty($errors);
666
667 return $this->success([
668 'valid' => $is_valid,
669 'errors' => $errors,
670 'warnings' => $warnings,
671 'line_count' => count($lines),
672 'message' => $is_valid ? 'robots.txt is valid' : 'robots.txt has validation errors',
673 ]);
674 }
675 }
676
677 /**
678 * Get News Sitemap Tool
679 */
680 class MCP_Tool_Get_News_Sitemap extends MCP_Tool_Base {
681
682 public function get_name() {
683 return 'wordpress_get_news_sitemap';
684 }
685
686 public function get_description() {
687 return 'Get the Google News sitemap XML content';
688 }
689
690 public function get_input_schema() {
691 return [
692 'type' => 'object',
693 'properties' => (object)[],
694 'required' => []
695 ];
696 }
697
698 public function execute($params) {
699 $this->validate_params($params);
700 $this->require_capability('manage_options');
701
702 $settings = get_option('metasync_news_sitemap_settings', []);
703 $enabled = !empty($settings['enabled']);
704
705 $xml = $this->get_sitemap_content('news-sitemap.xml');
706
707 return $this->success([
708 'content' => $xml,
709 'url' => home_url('/news-sitemap.xml'),
710 'enabled' => $enabled,
711 ]);
712 }
713
714 /**
715 * Read sitemap content from physical file, transient, or legacy option.
716 *
717 * @param string $filename The sitemap filename.
718 * @return string XML content or empty string.
719 */
720 private function get_sitemap_content($filename)
721 {
722 // 1. Physical file
723 $physical_path = ABSPATH . $filename;
724 if (file_exists($physical_path) && is_readable($physical_path)) {
725 return file_get_contents($physical_path);
726 }
727
728 // 2. Transient-based virtual storage
729 $cache_key = 'metasync_vsm_' . md5($filename);
730 $content = get_transient($cache_key);
731 if (false !== $content) {
732 return $content;
733 }
734
735 // 3. Legacy option storage
736 $virtual_sitemaps = get_option('metasync_sitemap_virtual', []);
737 if (isset($virtual_sitemaps[$filename])) {
738 return $virtual_sitemaps[$filename];
739 }
740
741 return '';
742 }
743 }
744
745 /**
746 * Get Video Sitemap Tool
747 */
748 class MCP_Tool_Get_Video_Sitemap extends MCP_Tool_Base {
749
750 public function get_name() {
751 return 'wordpress_get_video_sitemap';
752 }
753
754 public function get_description() {
755 return 'Get the video sitemap XML content';
756 }
757
758 public function get_input_schema() {
759 return [
760 'type' => 'object',
761 'properties' => (object)[],
762 'required' => []
763 ];
764 }
765
766 public function execute($params) {
767 $this->validate_params($params);
768 $this->require_capability('manage_options');
769
770 $settings = get_option('metasync_video_sitemap_settings', []);
771 $enabled = !empty($settings['enabled']);
772
773 $xml = $this->get_sitemap_content('video-sitemap.xml');
774
775 return $this->success([
776 'content' => $xml,
777 'url' => home_url('/video-sitemap.xml'),
778 'enabled' => $enabled,
779 ]);
780 }
781
782 /**
783 * Read sitemap content from physical file, transient, or legacy option.
784 *
785 * @param string $filename The sitemap filename.
786 * @return string XML content or empty string.
787 */
788 private function get_sitemap_content($filename)
789 {
790 // 1. Physical file
791 $physical_path = ABSPATH . $filename;
792 if (file_exists($physical_path) && is_readable($physical_path)) {
793 return file_get_contents($physical_path);
794 }
795
796 // 2. Transient-based virtual storage
797 $cache_key = 'metasync_vsm_' . md5($filename);
798 $content = get_transient($cache_key);
799 if (false !== $content) {
800 return $content;
801 }
802
803 // 3. Legacy option storage
804 $virtual_sitemaps = get_option('metasync_sitemap_virtual', []);
805 if (isset($virtual_sitemaps[$filename])) {
806 return $virtual_sitemaps[$filename];
807 }
808
809 return '';
810 }
811 }
812