PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / api / class-site-identity-endpoint.php

class-site-identity-endpoint.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.0, at includes/api/class-site-identity-endpoint.php

1,237 lines 42.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Site Identity API Endpoints Class
4 *
5 * REST API endpoints for site identity management including title formats,
6 * breadcrumb configuration, robots.txt generation, AI-powered site identity
7 * optimization, and global SEO defaults. Provides comprehensive API access to
8 * Site Identity Manager and AI Manager functionality with proper authentication,
9 * validation, and error handling.
10 *
11 * @package ThinkRank
12 * @subpackage API
13 * @since 1.0.0
14 */
15
16 declare(strict_types=1);
17
18 namespace ThinkRank\API;
19
20 use ThinkRank\SEO\Site_Identity_Manager;
21 use ThinkRank\AI\Manager as AI_Manager;
22 use ThinkRank\API\Traits\CSRF_Protection;
23 use WP_REST_Controller;
24 use WP_REST_Request;
25 use WP_REST_Response;
26 use WP_Error;
27
28 // Prevent direct access
29 if (!defined('ABSPATH')) {
30 exit;
31 }
32
33 // Load CSRF Protection trait
34 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php';
35
36 /**
37 * Site Identity API Endpoints Class
38 *
39 * Provides REST API endpoints for site identity operations including
40 * title generation, breadcrumb management, robots.txt configuration,
41 * AI-powered site identity optimization, and rule-based optimization
42 * with proper authentication and validation.
43 *
44 * @since 1.0.0
45 */
46 class Site_Identity_Endpoint extends WP_REST_Controller {
47 use CSRF_Protection;
48
49 /**
50 * Site Identity Manager instance
51 *
52 * @since 1.0.0
53 * @var Site_Identity_Manager
54 */
55 private Site_Identity_Manager $identity_manager;
56
57 /**
58 * API namespace
59 *
60 * @since 1.0.0
61 * @var string
62 */
63 protected $namespace = 'thinkrank/v1';
64
65 /**
66 * API resource base
67 *
68 * @since 1.0.0
69 * @var string
70 */
71 protected $rest_base = 'site-identity';
72
73 /**
74 * AI Manager instance
75 *
76 * @since 1.0.0
77 * @var AI_Manager|null
78 */
79 private ?AI_Manager $ai_manager = null;
80
81 /**
82 * Constructor
83 *
84 * @since 1.0.0
85 */
86 public function __construct() {
87 // Ensure Site Identity Manager is loaded
88 if (!class_exists('ThinkRank\\SEO\\Site_Identity_Manager')) {
89 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-site-identity-manager.php';
90 }
91
92 $this->identity_manager = new Site_Identity_Manager();
93 }
94
95 /**
96 * Get AI Manager instance (lazy loading)
97 *
98 * @since 1.0.0
99 * @return AI_Manager
100 */
101 private function get_ai_manager(): AI_Manager {
102 if ($this->ai_manager === null) {
103 // Try to get from main plugin container first
104 $plugin_instance = \ThinkRank::get_instance();
105 $this->ai_manager = $plugin_instance->get_component('ai');
106
107 // Fallback to direct instantiation if container fails
108 if ($this->ai_manager === null) {
109 $this->ai_manager = new AI_Manager();
110 }
111 }
112
113 return $this->ai_manager;
114 }
115
116 /**
117 * Register API routes
118 *
119 * @since 1.0.0
120 */
121 public function register_routes(): void {
122 // Get site identity settings
123 register_rest_route(
124 $this->namespace,
125 '/' . $this->rest_base . '/settings',
126 [
127 [
128 'methods' => 'GET',
129 'callback' => [$this, 'get_settings'],
130 'permission_callback' => [$this, 'check_permissions']
131 ],
132 [
133 'methods' => 'POST',
134 'callback' => [$this, 'update_settings'],
135 'permission_callback' => [$this, 'check_permissions'],
136 'args' => $this->get_settings_args()
137 ]
138 ]
139 );
140
141 // Generate title with template
142 register_rest_route(
143 $this->namespace,
144 '/' . $this->rest_base . '/title/generate',
145 [
146 [
147 'methods' => 'POST',
148 'callback' => [$this, 'generate_title'],
149 'permission_callback' => [$this, 'check_permissions'],
150 'args' => $this->get_title_generation_args()
151 ]
152 ]
153 );
154
155 // Get title templates
156 register_rest_route(
157 $this->namespace,
158 '/' . $this->rest_base . '/title/templates',
159 [
160 [
161 'methods' => 'GET',
162 'callback' => [$this, 'get_title_templates'],
163 'permission_callback' => [$this, 'check_permissions']
164 ]
165 ]
166 );
167
168 // Generate breadcrumbs
169 register_rest_route(
170 $this->namespace,
171 '/' . $this->rest_base . '/breadcrumbs/generate',
172 [
173 [
174 'methods' => 'POST',
175 'callback' => [$this, 'generate_breadcrumbs'],
176 'permission_callback' => [$this, 'check_permissions'],
177 'args' => $this->get_breadcrumb_generation_args()
178 ]
179 ]
180 );
181
182 // Get breadcrumb types
183 register_rest_route(
184 $this->namespace,
185 '/' . $this->rest_base . '/breadcrumbs/types',
186 [
187 [
188 'methods' => 'GET',
189 'callback' => [$this, 'get_breadcrumb_types'],
190 'permission_callback' => [$this, 'check_permissions']
191 ]
192 ]
193 );
194
195 // Robots.txt management
196 register_rest_route(
197 $this->namespace,
198 '/' . $this->rest_base . '/robots',
199 [
200 [
201 'methods' => 'GET',
202 'callback' => [$this, 'get_robots_txt'],
203 // Reading the robots.txt config is a Site Identity operation —
204 // gate it on the module cap, not the generic 'read' cap.
205 'permission_callback' => [$this, 'check_permissions']
206 ],
207 [
208 'methods' => 'POST',
209 'callback' => [$this, 'update_robots_txt'],
210 // Writing robots.txt to the webroot is site-wide — require the
211 // Site Identity management capability, not just edit_posts.
212 'permission_callback' => [$this, 'check_permissions'],
213 'args' => $this->get_robots_txt_args()
214 ]
215 ]
216 );
217
218 // Site identity optimization (rule-based)
219 register_rest_route(
220 $this->namespace,
221 '/' . $this->rest_base . '/optimize',
222 [
223 [
224 'methods' => 'POST',
225 'callback' => [$this, 'optimize_site_identity'],
226 'permission_callback' => [$this, 'check_permissions'],
227 'args' => $this->get_optimization_args()
228 ]
229 ]
230 );
231
232 // AI-powered site identity optimization
233 register_rest_route(
234 $this->namespace,
235 '/' . $this->rest_base . '/ai-optimize-info',
236 [
237 [
238 'methods' => 'POST',
239 'callback' => [$this, 'ai_optimize_site_info'],
240 'permission_callback' => [$this, 'check_permissions'],
241 'args' => $this->get_ai_optimization_args()
242 ]
243 ]
244 );
245
246 // AI-powered hero content optimization
247 register_rest_route(
248 $this->namespace,
249 '/' . $this->rest_base . '/ai-optimize-hero',
250 [
251 [
252 'methods' => 'POST',
253 'callback' => [$this, 'ai_optimize_hero_content'],
254 'permission_callback' => [$this, 'check_permissions'],
255 'args' => $this->get_hero_optimization_args()
256 ]
257 ]
258 );
259
260 // Validate site identity settings
261 register_rest_route(
262 $this->namespace,
263 '/' . $this->rest_base . '/validate',
264 [
265 [
266 'methods' => 'POST',
267 'callback' => [$this, 'validate_identity_settings'],
268 'permission_callback' => [$this, 'check_permissions'],
269 'args' => $this->get_validation_args()
270 ]
271 ]
272 );
273
274 }
275
276 /**
277 * Get site identity settings
278 *
279 * @since 1.0.0
280 *
281 * @param WP_REST_Request $request Request object
282 * @return WP_REST_Response Response object
283 */
284 public function get_settings(WP_REST_Request $request): WP_REST_Response {
285 try {
286 $context_type = $request->get_param('context_type') ?? 'site';
287 $context_id = $request->get_param('context_id');
288
289 // Get settings from Site Identity Manager
290 $settings = $this->identity_manager->get_settings($context_type, $context_id);
291
292 // Get settings schema for validation
293 $schema = $this->identity_manager->get_settings_schema($context_type);
294
295 return new WP_REST_Response([
296 'success' => true,
297 'data' => [
298 'settings' => $settings,
299 'schema' => $schema,
300 'context_type' => $context_type,
301 'context_id' => $context_id
302 ],
303 'message' => 'Site identity settings retrieved successfully'
304 ], 200);
305
306 } catch (\Exception $e) {
307 return new WP_REST_Response([
308 'success' => false,
309 'error' => 'Failed to retrieve settings: ' . $e->getMessage()
310 ], 500);
311 }
312 }
313
314 /**
315 * Update site identity settings
316 *
317 * @since 1.0.0
318 *
319 * @param WP_REST_Request $request Request object
320 * @return WP_REST_Response|WP_Error Response object or error
321 */
322 public function update_settings(WP_REST_Request $request) {
323 try {
324 $settings = $request->get_param('settings');
325 $context_type = $request->get_param('context_type') ?? 'site';
326 $context_id = $request->get_param('context_id');
327
328 // Validate settings
329 if (empty($settings) || !is_array($settings)) {
330 return new WP_Error(
331 'invalid_settings',
332 'Settings must be provided as an array',
333 ['status' => 400]
334 );
335 }
336
337 // Validate settings using Site Identity Manager
338 $validation = $this->identity_manager->validate_settings($settings);
339
340 if (!$validation['valid']) {
341 return new WP_Error(
342 'validation_failed',
343 'Settings validation failed',
344 [
345 'status' => 400,
346 'validation_errors' => $validation['errors'],
347 'validation_warnings' => $validation['warnings']
348 ]
349 );
350 }
351
352 // Update settings
353 $update_result = $this->identity_manager->save_settings($context_type, $context_id, $settings);
354
355 if (!$update_result) {
356 return new WP_Error(
357 'update_failed',
358 'Failed to update site identity settings',
359 ['status' => 500]
360 );
361 }
362
363 // If this save changed the robots.txt content/toggle and a physical
364 // robots.txt exists, keep it in lockstep. The web server serves that
365 // static file directly (bypassing the robots_txt filter), so without
366 // this the file goes stale and /robots.txt shows the old content
367 // while the textarea shows the new — regardless of what the frontend
368 // believed about the file's existence.
369 if ($context_type === 'site'
370 && (array_key_exists('robots_txt_content', $settings)
371 || array_key_exists('robots_txt_enabled', $settings))
372 ) {
373 $this->identity_manager->sync_robots_txt_file();
374 }
375
376 // Get updated settings
377 $updated_settings = $this->identity_manager->get_settings($context_type, $context_id);
378
379 return new WP_REST_Response([
380 'success' => true,
381 'data' => [
382 'settings' => $updated_settings,
383 'validation' => $validation,
384 'context_type' => $context_type,
385 'context_id' => $context_id
386 ],
387 'message' => 'Site identity settings updated successfully'
388 ], 200);
389
390 } catch (\Exception $e) {
391 return new WP_Error(
392 'update_failed',
393 'Settings update failed: ' . $e->getMessage(),
394 ['status' => 500]
395 );
396 }
397 }
398
399 /**
400 * Generate title using template
401 *
402 * @since 1.0.0
403 *
404 * @param WP_REST_Request $request Request object
405 * @return WP_REST_Response|WP_Error Response object or error
406 */
407 public function generate_title(WP_REST_Request $request) {
408 try {
409 $template_name = $request->get_param('template_name') ?? 'default';
410 $data = $request->get_param('data') ?? [];
411 $context = $request->get_param('context') ?? 'site';
412
413 // Generate title using Site Identity Manager
414 $generated_title = $this->identity_manager->generate_title($template_name, $data, $context);
415
416 // Get available templates for reference
417 $templates = $this->get_available_title_templates();
418
419 return new WP_REST_Response([
420 'success' => true,
421 'data' => [
422 'generated_title' => $generated_title,
423 'template_used' => $template_name,
424 'context' => $context,
425 'input_data' => $data,
426 'available_templates' => $templates
427 ],
428 'message' => 'Title generated successfully'
429 ], 200);
430
431 } catch (\Exception $e) {
432 return new WP_Error(
433 'title_generation_failed',
434 'Title generation failed: ' . $e->getMessage(),
435 ['status' => 500]
436 );
437 }
438 }
439
440 /**
441 * Get available title templates
442 *
443 * @since 1.0.0
444 *
445 * @param WP_REST_Request $request Request object
446 * @return WP_REST_Response Response object
447 */
448 public function get_title_templates(WP_REST_Request $request): WP_REST_Response {
449 $templates = $this->get_available_title_templates();
450
451 return new WP_REST_Response([
452 'success' => true,
453 'data' => [
454 'templates' => $templates,
455 'total_templates' => count($templates)
456 ],
457 'message' => 'Title templates retrieved successfully'
458 ], 200);
459 }
460
461 /**
462 * Generate breadcrumbs
463 *
464 * @since 1.0.0
465 *
466 * @param WP_REST_Request $request Request object
467 * @return WP_REST_Response|WP_Error Response object or error
468 */
469 public function generate_breadcrumbs(WP_REST_Request $request) {
470 try {
471 $breadcrumb_type = $request->get_param('breadcrumb_type') ?? 'hierarchical';
472 $options = $request->get_param('options') ?? [];
473
474 // Generate breadcrumbs using Site Identity Manager
475 $breadcrumbs = $this->identity_manager->generate_breadcrumbs($breadcrumb_type, $options);
476
477 // Get available breadcrumb types for reference
478 $types = $this->get_available_breadcrumb_types();
479
480 return new WP_REST_Response([
481 'success' => true,
482 'data' => [
483 'breadcrumbs' => $breadcrumbs,
484 'breadcrumb_type' => $breadcrumb_type,
485 'options' => $options,
486 'available_types' => $types
487 ],
488 'message' => 'Breadcrumbs generated successfully'
489 ], 200);
490
491 } catch (\Exception $e) {
492 return new WP_Error(
493 'breadcrumb_generation_failed',
494 'Breadcrumb generation failed: ' . $e->getMessage(),
495 ['status' => 500]
496 );
497 }
498 }
499
500 /**
501 * Get available breadcrumb types
502 *
503 * @since 1.0.0
504 *
505 * @param WP_REST_Request $request Request object
506 * @return WP_REST_Response Response object
507 */
508 public function get_breadcrumb_types(WP_REST_Request $request): WP_REST_Response {
509 $types = $this->get_available_breadcrumb_types();
510
511 return new WP_REST_Response([
512 'success' => true,
513 'data' => [
514 'breadcrumb_types' => $types,
515 'total_types' => count($types)
516 ],
517 'message' => 'Breadcrumb types retrieved successfully'
518 ], 200);
519 }
520
521 /**
522 * Get robots.txt configuration
523 *
524 * @since 1.0.0
525 *
526 * @param WP_REST_Request $request Request object
527 * @return WP_REST_Response Response object
528 */
529 public function get_robots_txt(WP_REST_Request $request): WP_REST_Response {
530 try {
531 $custom_rules = $request->get_param('custom_rules') ?? [];
532
533 // Generate robots.txt using Site Identity Manager
534 $robots_data = $this->identity_manager->generate_robots_txt($custom_rules);
535
536 // The editor shows the body that is actually being served (physical
537 // file if present, else the effective content) — header-stripped so
538 // the auto-generated comment/timestamp never lands in the textarea.
539 $robots_data['content'] = $this->identity_manager->get_served_robots_body();
540
541 // How /robots.txt is actually delivered right now, so the screen can
542 // show the served output next to the editable body and flag a
543 // physical file in the web root that has drifted from the settings.
544 $robots_data['effective'] = $this->identity_manager->get_robots_txt_delivery();
545
546 return new WP_REST_Response([
547 'success' => true,
548 'data' => $robots_data,
549 'message' => 'Robots.txt data retrieved successfully'
550 ], 200);
551
552 } catch (\Exception $e) {
553 return new WP_REST_Response([
554 'success' => false,
555 'error' => 'Failed to retrieve robots.txt: ' . $e->getMessage()
556 ], 500);
557 }
558 }
559
560 /**
561 * Update robots.txt configuration
562 *
563 * @since 1.0.0
564 *
565 * @param WP_REST_Request $request Request object
566 * @return WP_REST_Response|WP_Error Response object or error
567 */
568 public function update_robots_txt(WP_REST_Request $request) {
569 try {
570 // Check rate limiting
571 if (!$this->check_robots_rate_limit()) {
572 return new WP_Error(
573 'rate_limit_exceeded',
574 'Too many requests. Please wait a few minutes before trying again.',
575 ['status' => 429]
576 );
577 }
578 $custom_rules = $request->get_param('custom_rules') ?? [];
579 $enable_management = $request->get_param('enable_management') ?? true;
580
581 // Two callers share this route:
582 // - "Generate" rebuilds the content from rules and adopts it as the
583 // stored textarea content ($regenerate = true).
584 // - A plain save that only needs the physical file re-synced to the
585 // already-stored textarea content ($regenerate = false).
586 // Default to true so the historical Generate contract is unchanged.
587 $regenerate = $request->get_param('regenerate');
588 if ($regenerate === null) {
589 $regenerate = true;
590 }
591
592 // Validate custom rules format
593 if (!is_array($custom_rules)) {
594 return new WP_Error(
595 'invalid_rules',
596 'Custom rules must be provided as an array',
597 ['status' => 400]
598 );
599 }
600
601 $settings = ['robots_txt_enabled' => $enable_management];
602
603 if ($regenerate) {
604 // Generate and validate robots.txt from the rules.
605 $robots_data = $this->identity_manager->generate_robots_txt($custom_rules);
606
607 if (!empty($robots_data['validation']['errors'])) {
608 return new WP_Error(
609 'validation_failed',
610 'Robots.txt validation failed',
611 [
612 'status' => 400,
613 'validation_errors' => $robots_data['validation']['errors']
614 ]
615 );
616 }
617
618 // Adopt the freshly generated content as the stored source.
619 $settings['custom_robots_rules'] = $custom_rules;
620 $settings['robots_txt_content'] = $robots_data['content'];
621 }
622
623 $update_result = $this->identity_manager->save_settings('site', null, $settings);
624
625 if (!$update_result) {
626 return new WP_Error(
627 'update_failed',
628 'Failed to update robots.txt settings',
629 ['status' => 500]
630 );
631 }
632
633 // Effective content = the stored textarea content when set, else the
634 // auto-generated body. This is exactly what the live /robots.txt
635 // serves, so writing it here keeps the physical file in lockstep.
636 $effective_content = $this->identity_manager->render_robots_txt();
637
638 // Write robots.txt file to filesystem if management is enabled.
639 $file_write_result = ['success' => false, 'message' => 'File writing disabled'];
640 if ($enable_management && $effective_content !== '') {
641 $write_to_file = $request->get_param('write_to_file') ?? true;
642
643 if ($write_to_file) {
644 $file_write_result = $this->identity_manager->write_robots_txt(
645 $effective_content
646 );
647 }
648 }
649
650 $robots_data = $robots_data ?? [];
651 // Return the header-stripped body so the client textarea reflects
652 // exactly what it should hold (the header is added only at render).
653 $robots_data['content'] = $this->identity_manager->get_served_robots_body();
654
655 // Re-read delivery after the write above so the screen reflects the
656 // file that now exists rather than the state it was in on load.
657 $robots_data['effective'] = $this->identity_manager->get_robots_txt_delivery();
658
659 return new WP_REST_Response([
660 'success' => true,
661 'data' => [
662 'robots_data' => $robots_data,
663 'settings_updated' => $settings,
664 'file_write_result' => $file_write_result
665 ],
666 'message' => $file_write_result['success']
667 ? 'Robots.txt configuration updated and file written successfully'
668 : 'Robots.txt configuration updated (file not written: ' . $file_write_result['message'] . ')'
669 ], 200);
670
671 } catch (\Exception $e) {
672 return new WP_Error(
673 'update_failed',
674 'Robots.txt update failed: ' . $e->getMessage(),
675 ['status' => 500]
676 );
677 }
678 }
679
680 /**
681 * Optimize site identity (rule-based)
682 *
683 * @since 1.0.0
684 *
685 * @param WP_REST_Request $request Request object
686 * @return WP_REST_Response|WP_Error Response object or error
687 */
688 public function optimize_site_identity(WP_REST_Request $request) {
689 try {
690 $identity_data = $request->get_param('identity_data');
691 $options = $request->get_param('options') ?? [];
692
693 // Validate identity data
694 if (empty($identity_data) || !is_array($identity_data)) {
695 return new WP_Error(
696 'invalid_data',
697 'Identity data must be provided as an array',
698 ['status' => 400]
699 );
700 }
701
702 // Optimize site identity using Site Identity Manager with options
703 $optimization_results = $this->identity_manager->optimize_site_identity($identity_data, $options);
704
705 return new WP_REST_Response([
706 'success' => true,
707 'data' => $optimization_results,
708 'message' => 'Site identity optimization completed'
709 ], 200);
710
711 } catch (\Exception $e) {
712 return new WP_Error(
713 'optimization_failed',
714 'Site identity optimization failed: ' . $e->getMessage(),
715 ['status' => 500]
716 );
717 }
718 }
719
720 /**
721 * AI optimize site information
722 *
723 * @since 1.0.0
724 *
725 * @param WP_REST_Request $request Request object
726 * @return WP_REST_Response|WP_Error Response object or error
727 */
728 public function ai_optimize_site_info(WP_REST_Request $request) {
729 try {
730 $site_data = $request->get_param('site_data');
731 $options = [
732 'business_type' => $request->get_param('business_type'),
733 'target_audience' => $request->get_param('target_audience'),
734 'tone' => $request->get_param('tone')
735 ];
736
737 // Validate site data
738 if (empty($site_data) || !is_array($site_data)) {
739 return new WP_Error(
740 'invalid_data',
741 'Site data must be provided as an array',
742 ['status' => 400]
743 );
744 }
745
746 // Only the site name is required. An empty description or tagline
747 // is a valid state — and exactly when AI help is most useful — so
748 // let the optimizer generate them instead of rejecting the request.
749 if (empty($site_data['site_name'])) {
750 return new WP_Error(
751 'missing_field',
752 __('Site name is required to run AI optimization.', 'thinkrank'),
753 ['status' => 400]
754 );
755 }
756
757 // Sanitize site data (description/tagline may be empty by design)
758 $sanitized_site_data = [
759 'site_name' => sanitize_text_field($site_data['site_name']),
760 'site_description' => sanitize_textarea_field($site_data['site_description'] ?? ''),
761 'tagline' => sanitize_text_field($site_data['tagline'] ?? ''),
762 'default_meta_description' => sanitize_textarea_field($site_data['default_meta_description'] ?? '')
763 ];
764
765 // Sanitize options
766 $sanitized_options = [
767 'business_type' => sanitize_text_field($options['business_type'] ?? 'website'),
768 'target_audience' => sanitize_text_field($options['target_audience'] ?? 'general'),
769 'tone' => sanitize_text_field($options['tone'] ?? 'professional')
770 ];
771
772 // Get AI manager and perform optimization
773 $ai_manager = $this->get_ai_manager();
774 $optimization_results = $ai_manager->optimize_site_identity($sanitized_site_data, $sanitized_options);
775
776 return new WP_REST_Response([
777 'success' => true,
778 'data' => $optimization_results,
779 'message' => 'Site identity AI optimization completed'
780 ], 200);
781
782 } catch (\Exception $e) {
783 return new WP_Error(
784 'ai_optimization_failed',
785 'AI optimization failed: ' . $e->getMessage(),
786 ['status' => 500]
787 );
788 }
789 }
790
791 /**
792 * AI-powered hero content optimization
793 *
794 * @since 1.0.0
795 *
796 * @param WP_REST_Request $request Request object
797 * @return WP_REST_Response|WP_Error Response object or error
798 */
799 public function ai_optimize_hero_content(WP_REST_Request $request) {
800 try {
801 $hero_data = $request->get_param('hero_data');
802 $context = $request->get_param('context') ?? [];
803 $options = [
804 'business_type' => $request->get_param('business_type'),
805 'target_audience' => $request->get_param('target_audience'),
806 'tone' => $request->get_param('tone')
807 ];
808
809 // Validate hero data
810 if (empty($hero_data) || !is_array($hero_data)) {
811 return new WP_Error(
812 'invalid_data',
813 'Hero data must be provided as an array',
814 ['status' => 400]
815 );
816 }
817
818 // Sanitize hero data
819 $sanitized_hero_data = [
820 'hero_title' => sanitize_text_field($hero_data['hero_title'] ?? ''),
821 'hero_subtitle' => sanitize_textarea_field($hero_data['hero_subtitle'] ?? ''),
822 'hero_cta_text' => sanitize_text_field($hero_data['hero_cta_text'] ?? ''),
823 'hero_cta_url' => esc_url_raw($hero_data['hero_cta_url'] ?? '')
824 ];
825
826 // Sanitize context data
827 $sanitized_context = [
828 'site_name' => sanitize_text_field($context['site_name'] ?? ''),
829 'site_url' => esc_url_raw($context['site_url'] ?? ''),
830 'business_type' => sanitize_text_field($context['business_type'] ?? ''),
831 'site_description' => sanitize_textarea_field($context['site_description'] ?? '')
832 ];
833
834 // Sanitize options
835 $sanitized_options = [
836 'business_type' => sanitize_text_field($options['business_type'] ?? 'website'),
837 'target_audience' => sanitize_text_field($options['target_audience'] ?? 'general'),
838 'tone' => sanitize_text_field($options['tone'] ?? 'professional'),
839 'context' => $sanitized_context
840 ];
841
842 // Get AI manager and perform optimization
843 $ai_manager = $this->get_ai_manager();
844 $optimization_results = $ai_manager->optimize_homepage_hero($sanitized_hero_data, $sanitized_options);
845
846 return new WP_REST_Response([
847 'success' => true,
848 'data' => $optimization_results,
849 'message' => 'Hero content AI optimization completed'
850 ], 200);
851
852 } catch (\Exception $e) {
853 return new WP_Error(
854 'ai_optimization_failed',
855 'Hero AI optimization failed: ' . $e->getMessage(),
856 ['status' => 500]
857 );
858 }
859 }
860
861 /**
862 * Validate site identity settings
863 *
864 * @since 1.0.0
865 *
866 * @param WP_REST_Request $request Request object
867 * @return WP_REST_Response Response object
868 */
869 public function validate_identity_settings(WP_REST_Request $request): WP_REST_Response {
870 try {
871 $settings = $request->get_param('settings');
872 $tab_context = $request->get_param('tab_context') ?? '';
873
874 // Validate settings using Site Identity Manager with tab context
875 $validation = $this->identity_manager->validate_settings($settings ?? [], $tab_context);
876
877 return new WP_REST_Response([
878 'success' => true,
879 'data' => $validation,
880 'message' => 'Settings validation completed'
881 ], 200);
882
883 } catch (\Exception $e) {
884 return new WP_REST_Response([
885 'success' => false,
886 'error' => 'Validation failed: ' . $e->getMessage()
887 ], 500);
888 }
889 }
890
891 /**
892 * Permission callbacks
893 */
894
895 /**
896 * Check permissions for site identity operations (admin-only)
897 *
898 * @since 1.0.0
899 *
900 * @return bool Permission status
901 */
902 public function check_permissions(): bool {
903 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_site_identity');
904 }
905 private function get_available_title_templates(): array {
906 return [
907 'default' => [
908 'name' => 'Default',
909 'template' => '%title% %separator% %sitename%',
910 'description' => 'Standard title format with site name'
911 ]
912 ];
913 }
914
915 /**
916 * Get available breadcrumb types
917 *
918 * @since 1.0.0
919 *
920 * @return array Breadcrumb types
921 */
922 private function get_available_breadcrumb_types(): array {
923 return [
924 'hierarchical' => [
925 'name' => 'Hierarchical',
926 'description' => 'Based on page hierarchy and categories'
927 ]
928 ];
929 }
930
931 /**
932 * Argument validation methods
933 */
934
935 /**
936 * Get arguments for settings endpoints
937 *
938 * @since 1.0.0
939 *
940 * @return array Arguments array
941 */
942 private function get_settings_args(): array {
943 return [
944 'settings' => [
945 'required' => true,
946 'type' => 'object',
947 'description' => 'Site identity settings to update'
948 ],
949 'context_type' => [
950 'required' => false,
951 'type' => 'string',
952 'enum' => ['site'],
953 'default' => 'site',
954 'description' => 'Context type (site only)'
955 ],
956 'context_id' => [
957 'required' => false,
958 'type' => 'integer',
959 'minimum' => 1,
960 'description' => 'Context ID (not required for site context)'
961 ]
962 ];
963 }
964
965 /**
966 * Get arguments for title generation endpoint
967 *
968 * @since 1.0.0
969 *
970 * @return array Arguments array
971 */
972 private function get_title_generation_args(): array {
973 return [
974 'template_name' => [
975 'required' => false,
976 'type' => 'string',
977 'enum' => ['default', 'simple', 'reverse', 'category', 'author'],
978 'default' => 'default',
979 'description' => 'Title template to use'
980 ],
981 'data' => [
982 'required' => false,
983 'type' => 'object',
984 'description' => 'Data for placeholder replacement'
985 ],
986 'context' => [
987 'required' => false,
988 'type' => 'string',
989 'enum' => ['site'],
990 'default' => 'site',
991 'description' => 'Context (site only)'
992 ]
993 ];
994 }
995
996 /**
997 * Get arguments for breadcrumb generation endpoint
998 *
999 * @since 1.0.0
1000 *
1001 * @return array Arguments array
1002 */
1003 private function get_breadcrumb_generation_args(): array {
1004 return [
1005 'breadcrumb_type' => [
1006 'required' => false,
1007 'type' => 'string',
1008 'enum' => ['hierarchical', 'taxonomy', 'path', 'custom'],
1009 'default' => 'hierarchical',
1010 'description' => 'Type of breadcrumb navigation to generate'
1011 ],
1012 'options' => [
1013 'required' => false,
1014 'type' => 'object',
1015 'description' => 'Additional options for breadcrumb generation'
1016 ]
1017 ];
1018 }
1019
1020 /**
1021 * Get arguments for robots.txt endpoints
1022 *
1023 * @since 1.0.0
1024 *
1025 * @return array Arguments array
1026 */
1027 private function get_robots_txt_args(): array {
1028 return [
1029 'custom_rules' => [
1030 'required' => false,
1031 'type' => 'array',
1032 'items' => [
1033 'type' => 'object'
1034 ],
1035 'description' => 'Custom robots.txt rules'
1036 ],
1037 'enable_management' => [
1038 'required' => false,
1039 'type' => 'boolean',
1040 'default' => true,
1041 'description' => 'Enable automatic robots.txt management'
1042 ],
1043 'regenerate' => [
1044 'required' => false,
1045 'type' => 'boolean',
1046 'default' => true,
1047 'description' => 'Rebuild content from rules (Generate). When false, only re-sync the physical file to the stored content.'
1048 ]
1049 ];
1050 }
1051
1052 /**
1053 * Get arguments for optimization endpoint
1054 *
1055 * @since 1.0.0
1056 *
1057 * @return array Arguments array
1058 */
1059 private function get_optimization_args(): array {
1060 return [
1061 'identity_data' => [
1062 'required' => true,
1063 'type' => 'object',
1064 'description' => 'Site identity data to optimize'
1065 ]
1066 ];
1067 }
1068
1069 /**
1070 * Get arguments for AI optimization endpoint
1071 *
1072 * @since 1.0.0
1073 *
1074 * @return array Arguments array
1075 */
1076 private function get_ai_optimization_args(): array {
1077 return [
1078 'site_data' => [
1079 'required' => true,
1080 'type' => 'object',
1081 'description' => 'Site identity data to optimize with AI',
1082 'properties' => [
1083 'site_name' => [
1084 'type' => 'string',
1085 'description' => 'Site name to optimize'
1086 ],
1087 'site_description' => [
1088 'type' => 'string',
1089 'description' => 'Site description to optimize'
1090 ],
1091 'tagline' => [
1092 'type' => 'string',
1093 'description' => 'Site tagline to optimize'
1094 ]
1095 ]
1096 ],
1097 'business_type' => [
1098 'required' => false,
1099 'type' => 'string',
1100 'default' => 'website',
1101 'sanitize_callback' => 'sanitize_text_field',
1102 'description' => 'Type of business for context'
1103 ],
1104 'target_audience' => [
1105 'required' => false,
1106 'type' => 'string',
1107 'default' => 'general',
1108 'sanitize_callback' => 'sanitize_text_field',
1109 'description' => 'Target audience for optimization'
1110 ],
1111 'tone' => [
1112 'required' => false,
1113 'type' => 'string',
1114 'default' => 'professional',
1115 'sanitize_callback' => 'sanitize_text_field',
1116 'description' => 'Desired tone for optimization'
1117 ]
1118 ];
1119 }
1120
1121 /**
1122 * Get arguments for hero AI optimization endpoint
1123 *
1124 * @since 1.0.0
1125 *
1126 * @return array Arguments array
1127 */
1128 private function get_hero_optimization_args(): array {
1129 return [
1130 'hero_data' => [
1131 'required' => true,
1132 'type' => 'object',
1133 'description' => 'Hero content data to optimize with AI',
1134 'properties' => [
1135 'hero_title' => [
1136 'type' => 'string',
1137 'description' => 'Hero section title'
1138 ],
1139 'hero_subtitle' => [
1140 'type' => 'string',
1141 'description' => 'Hero section subtitle'
1142 ],
1143 'hero_cta_text' => [
1144 'type' => 'string',
1145 'description' => 'Call-to-action button text'
1146 ],
1147 'hero_cta_url' => [
1148 'type' => 'string',
1149 'description' => 'Call-to-action button URL'
1150 ]
1151 ]
1152 ],
1153 'context' => [
1154 'required' => false,
1155 'type' => 'object',
1156 'description' => 'Additional context for optimization',
1157 'properties' => [
1158 'site_name' => [
1159 'type' => 'string',
1160 'description' => 'Site name for context'
1161 ],
1162 'site_url' => [
1163 'type' => 'string',
1164 'description' => 'Site URL for context'
1165 ],
1166 'business_type' => [
1167 'type' => 'string',
1168 'description' => 'Business type for context'
1169 ],
1170 'site_description' => [
1171 'type' => 'string',
1172 'description' => 'Site description for context'
1173 ]
1174 ]
1175 ],
1176 'business_type' => [
1177 'required' => false,
1178 'type' => 'string',
1179 'default' => 'website',
1180 'sanitize_callback' => 'sanitize_text_field',
1181 'description' => 'Type of business for context'
1182 ],
1183 'target_audience' => [
1184 'required' => false,
1185 'type' => 'string',
1186 'default' => 'general',
1187 'sanitize_callback' => 'sanitize_text_field',
1188 'description' => 'Target audience for optimization'
1189 ],
1190 'tone' => [
1191 'required' => false,
1192 'type' => 'string',
1193 'default' => 'professional',
1194 'sanitize_callback' => 'sanitize_text_field',
1195 'description' => 'Desired tone for optimization'
1196 ]
1197 ];
1198 }
1199
1200 /**
1201 * Get arguments for validation endpoint
1202 *
1203 * @since 1.0.0
1204 *
1205 * @return array Arguments array
1206 */
1207 private function get_validation_args(): array {
1208 return [
1209 'settings' => [
1210 'required' => true,
1211 'type' => 'object',
1212 'description' => 'Settings to validate'
1213 ]
1214 ];
1215 }
1216
1217 /**
1218 * Check rate limit for robots.txt operations
1219 *
1220 * @since 1.0.0
1221 * @return bool True if within rate limit
1222 */
1223 private function check_robots_rate_limit(): bool {
1224 $user_id = get_current_user_id();
1225 $rate_key = "thinkrank_robots_rate_{$user_id}";
1226
1227 $requests = get_transient($rate_key) ?: 0;
1228
1229 if ($requests >= 20) { // Max 20 requests per 5 minutes
1230 return false;
1231 }
1232
1233 set_transient($rate_key, $requests + 1, 5 * MINUTE_IN_SECONDS);
1234 return true;
1235 }
1236 }
1237