PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.26.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.26.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 1.26.0, at includes/api/class-site-identity-endpoint.php

1,228 lines 41.5 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 return new WP_REST_Response([
542 'success' => true,
543 'data' => $robots_data,
544 'message' => 'Robots.txt data retrieved successfully'
545 ], 200);
546
547 } catch (\Exception $e) {
548 return new WP_REST_Response([
549 'success' => false,
550 'error' => 'Failed to retrieve robots.txt: ' . $e->getMessage()
551 ], 500);
552 }
553 }
554
555 /**
556 * Update robots.txt configuration
557 *
558 * @since 1.0.0
559 *
560 * @param WP_REST_Request $request Request object
561 * @return WP_REST_Response|WP_Error Response object or error
562 */
563 public function update_robots_txt(WP_REST_Request $request) {
564 try {
565 // Check rate limiting
566 if (!$this->check_robots_rate_limit()) {
567 return new WP_Error(
568 'rate_limit_exceeded',
569 'Too many requests. Please wait a few minutes before trying again.',
570 ['status' => 429]
571 );
572 }
573 $custom_rules = $request->get_param('custom_rules') ?? [];
574 $enable_management = $request->get_param('enable_management') ?? true;
575
576 // Two callers share this route:
577 // - "Generate" rebuilds the content from rules and adopts it as the
578 // stored textarea content ($regenerate = true).
579 // - A plain save that only needs the physical file re-synced to the
580 // already-stored textarea content ($regenerate = false).
581 // Default to true so the historical Generate contract is unchanged.
582 $regenerate = $request->get_param('regenerate');
583 if ($regenerate === null) {
584 $regenerate = true;
585 }
586
587 // Validate custom rules format
588 if (!is_array($custom_rules)) {
589 return new WP_Error(
590 'invalid_rules',
591 'Custom rules must be provided as an array',
592 ['status' => 400]
593 );
594 }
595
596 $settings = ['robots_txt_enabled' => $enable_management];
597
598 if ($regenerate) {
599 // Generate and validate robots.txt from the rules.
600 $robots_data = $this->identity_manager->generate_robots_txt($custom_rules);
601
602 if (!empty($robots_data['validation']['errors'])) {
603 return new WP_Error(
604 'validation_failed',
605 'Robots.txt validation failed',
606 [
607 'status' => 400,
608 'validation_errors' => $robots_data['validation']['errors']
609 ]
610 );
611 }
612
613 // Adopt the freshly generated content as the stored source.
614 $settings['custom_robots_rules'] = $custom_rules;
615 $settings['robots_txt_content'] = $robots_data['content'];
616 }
617
618 $update_result = $this->identity_manager->save_settings('site', null, $settings);
619
620 if (!$update_result) {
621 return new WP_Error(
622 'update_failed',
623 'Failed to update robots.txt settings',
624 ['status' => 500]
625 );
626 }
627
628 // Effective content = the stored textarea content when set, else the
629 // auto-generated body. This is exactly what the live /robots.txt
630 // serves, so writing it here keeps the physical file in lockstep.
631 $effective_content = $this->identity_manager->render_robots_txt();
632
633 // Write robots.txt file to filesystem if management is enabled.
634 $file_write_result = ['success' => false, 'message' => 'File writing disabled'];
635 if ($enable_management && $effective_content !== '') {
636 $write_to_file = $request->get_param('write_to_file') ?? true;
637
638 if ($write_to_file) {
639 $file_write_result = $this->identity_manager->write_robots_txt(
640 $effective_content
641 );
642 }
643 }
644
645 $robots_data = $robots_data ?? [];
646 // Return the header-stripped body so the client textarea reflects
647 // exactly what it should hold (the header is added only at render).
648 $robots_data['content'] = $this->identity_manager->get_served_robots_body();
649
650 return new WP_REST_Response([
651 'success' => true,
652 'data' => [
653 'robots_data' => $robots_data,
654 'settings_updated' => $settings,
655 'file_write_result' => $file_write_result
656 ],
657 'message' => $file_write_result['success']
658 ? 'Robots.txt configuration updated and file written successfully'
659 : 'Robots.txt configuration updated (file not written: ' . $file_write_result['message'] . ')'
660 ], 200);
661
662 } catch (\Exception $e) {
663 return new WP_Error(
664 'update_failed',
665 'Robots.txt update failed: ' . $e->getMessage(),
666 ['status' => 500]
667 );
668 }
669 }
670
671 /**
672 * Optimize site identity (rule-based)
673 *
674 * @since 1.0.0
675 *
676 * @param WP_REST_Request $request Request object
677 * @return WP_REST_Response|WP_Error Response object or error
678 */
679 public function optimize_site_identity(WP_REST_Request $request) {
680 try {
681 $identity_data = $request->get_param('identity_data');
682 $options = $request->get_param('options') ?? [];
683
684 // Validate identity data
685 if (empty($identity_data) || !is_array($identity_data)) {
686 return new WP_Error(
687 'invalid_data',
688 'Identity data must be provided as an array',
689 ['status' => 400]
690 );
691 }
692
693 // Optimize site identity using Site Identity Manager with options
694 $optimization_results = $this->identity_manager->optimize_site_identity($identity_data, $options);
695
696 return new WP_REST_Response([
697 'success' => true,
698 'data' => $optimization_results,
699 'message' => 'Site identity optimization completed'
700 ], 200);
701
702 } catch (\Exception $e) {
703 return new WP_Error(
704 'optimization_failed',
705 'Site identity optimization failed: ' . $e->getMessage(),
706 ['status' => 500]
707 );
708 }
709 }
710
711 /**
712 * AI optimize site information
713 *
714 * @since 1.0.0
715 *
716 * @param WP_REST_Request $request Request object
717 * @return WP_REST_Response|WP_Error Response object or error
718 */
719 public function ai_optimize_site_info(WP_REST_Request $request) {
720 try {
721 $site_data = $request->get_param('site_data');
722 $options = [
723 'business_type' => $request->get_param('business_type'),
724 'target_audience' => $request->get_param('target_audience'),
725 'tone' => $request->get_param('tone')
726 ];
727
728 // Validate site data
729 if (empty($site_data) || !is_array($site_data)) {
730 return new WP_Error(
731 'invalid_data',
732 'Site data must be provided as an array',
733 ['status' => 400]
734 );
735 }
736
737 // Only the site name is required. An empty description or tagline
738 // is a valid state — and exactly when AI help is most useful — so
739 // let the optimizer generate them instead of rejecting the request.
740 if (empty($site_data['site_name'])) {
741 return new WP_Error(
742 'missing_field',
743 __('Site name is required to run AI optimization.', 'thinkrank'),
744 ['status' => 400]
745 );
746 }
747
748 // Sanitize site data (description/tagline may be empty by design)
749 $sanitized_site_data = [
750 'site_name' => sanitize_text_field($site_data['site_name']),
751 'site_description' => sanitize_textarea_field($site_data['site_description'] ?? ''),
752 'tagline' => sanitize_text_field($site_data['tagline'] ?? ''),
753 'default_meta_description' => sanitize_textarea_field($site_data['default_meta_description'] ?? '')
754 ];
755
756 // Sanitize options
757 $sanitized_options = [
758 'business_type' => sanitize_text_field($options['business_type'] ?? 'website'),
759 'target_audience' => sanitize_text_field($options['target_audience'] ?? 'general'),
760 'tone' => sanitize_text_field($options['tone'] ?? 'professional')
761 ];
762
763 // Get AI manager and perform optimization
764 $ai_manager = $this->get_ai_manager();
765 $optimization_results = $ai_manager->optimize_site_identity($sanitized_site_data, $sanitized_options);
766
767 return new WP_REST_Response([
768 'success' => true,
769 'data' => $optimization_results,
770 'message' => 'Site identity AI optimization completed'
771 ], 200);
772
773 } catch (\Exception $e) {
774 return new WP_Error(
775 'ai_optimization_failed',
776 'AI optimization failed: ' . $e->getMessage(),
777 ['status' => 500]
778 );
779 }
780 }
781
782 /**
783 * AI-powered hero content optimization
784 *
785 * @since 1.0.0
786 *
787 * @param WP_REST_Request $request Request object
788 * @return WP_REST_Response|WP_Error Response object or error
789 */
790 public function ai_optimize_hero_content(WP_REST_Request $request) {
791 try {
792 $hero_data = $request->get_param('hero_data');
793 $context = $request->get_param('context') ?? [];
794 $options = [
795 'business_type' => $request->get_param('business_type'),
796 'target_audience' => $request->get_param('target_audience'),
797 'tone' => $request->get_param('tone')
798 ];
799
800 // Validate hero data
801 if (empty($hero_data) || !is_array($hero_data)) {
802 return new WP_Error(
803 'invalid_data',
804 'Hero data must be provided as an array',
805 ['status' => 400]
806 );
807 }
808
809 // Sanitize hero data
810 $sanitized_hero_data = [
811 'hero_title' => sanitize_text_field($hero_data['hero_title'] ?? ''),
812 'hero_subtitle' => sanitize_textarea_field($hero_data['hero_subtitle'] ?? ''),
813 'hero_cta_text' => sanitize_text_field($hero_data['hero_cta_text'] ?? ''),
814 'hero_cta_url' => esc_url_raw($hero_data['hero_cta_url'] ?? '')
815 ];
816
817 // Sanitize context data
818 $sanitized_context = [
819 'site_name' => sanitize_text_field($context['site_name'] ?? ''),
820 'site_url' => esc_url_raw($context['site_url'] ?? ''),
821 'business_type' => sanitize_text_field($context['business_type'] ?? ''),
822 'site_description' => sanitize_textarea_field($context['site_description'] ?? '')
823 ];
824
825 // Sanitize options
826 $sanitized_options = [
827 'business_type' => sanitize_text_field($options['business_type'] ?? 'website'),
828 'target_audience' => sanitize_text_field($options['target_audience'] ?? 'general'),
829 'tone' => sanitize_text_field($options['tone'] ?? 'professional'),
830 'context' => $sanitized_context
831 ];
832
833 // Get AI manager and perform optimization
834 $ai_manager = $this->get_ai_manager();
835 $optimization_results = $ai_manager->optimize_homepage_hero($sanitized_hero_data, $sanitized_options);
836
837 return new WP_REST_Response([
838 'success' => true,
839 'data' => $optimization_results,
840 'message' => 'Hero content AI optimization completed'
841 ], 200);
842
843 } catch (\Exception $e) {
844 return new WP_Error(
845 'ai_optimization_failed',
846 'Hero AI optimization failed: ' . $e->getMessage(),
847 ['status' => 500]
848 );
849 }
850 }
851
852 /**
853 * Validate site identity settings
854 *
855 * @since 1.0.0
856 *
857 * @param WP_REST_Request $request Request object
858 * @return WP_REST_Response Response object
859 */
860 public function validate_identity_settings(WP_REST_Request $request): WP_REST_Response {
861 try {
862 $settings = $request->get_param('settings');
863 $tab_context = $request->get_param('tab_context') ?? '';
864
865 // Validate settings using Site Identity Manager with tab context
866 $validation = $this->identity_manager->validate_settings($settings ?? [], $tab_context);
867
868 return new WP_REST_Response([
869 'success' => true,
870 'data' => $validation,
871 'message' => 'Settings validation completed'
872 ], 200);
873
874 } catch (\Exception $e) {
875 return new WP_REST_Response([
876 'success' => false,
877 'error' => 'Validation failed: ' . $e->getMessage()
878 ], 500);
879 }
880 }
881
882 /**
883 * Permission callbacks
884 */
885
886 /**
887 * Check permissions for site identity operations (admin-only)
888 *
889 * @since 1.0.0
890 *
891 * @return bool Permission status
892 */
893 public function check_permissions(): bool {
894 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_site_identity');
895 }
896 private function get_available_title_templates(): array {
897 return [
898 'default' => [
899 'name' => 'Default',
900 'template' => '%title% %separator% %sitename%',
901 'description' => 'Standard title format with site name'
902 ]
903 ];
904 }
905
906 /**
907 * Get available breadcrumb types
908 *
909 * @since 1.0.0
910 *
911 * @return array Breadcrumb types
912 */
913 private function get_available_breadcrumb_types(): array {
914 return [
915 'hierarchical' => [
916 'name' => 'Hierarchical',
917 'description' => 'Based on page hierarchy and categories'
918 ]
919 ];
920 }
921
922 /**
923 * Argument validation methods
924 */
925
926 /**
927 * Get arguments for settings endpoints
928 *
929 * @since 1.0.0
930 *
931 * @return array Arguments array
932 */
933 private function get_settings_args(): array {
934 return [
935 'settings' => [
936 'required' => true,
937 'type' => 'object',
938 'description' => 'Site identity settings to update'
939 ],
940 'context_type' => [
941 'required' => false,
942 'type' => 'string',
943 'enum' => ['site'],
944 'default' => 'site',
945 'description' => 'Context type (site only)'
946 ],
947 'context_id' => [
948 'required' => false,
949 'type' => 'integer',
950 'minimum' => 1,
951 'description' => 'Context ID (not required for site context)'
952 ]
953 ];
954 }
955
956 /**
957 * Get arguments for title generation endpoint
958 *
959 * @since 1.0.0
960 *
961 * @return array Arguments array
962 */
963 private function get_title_generation_args(): array {
964 return [
965 'template_name' => [
966 'required' => false,
967 'type' => 'string',
968 'enum' => ['default', 'simple', 'reverse', 'category', 'author'],
969 'default' => 'default',
970 'description' => 'Title template to use'
971 ],
972 'data' => [
973 'required' => false,
974 'type' => 'object',
975 'description' => 'Data for placeholder replacement'
976 ],
977 'context' => [
978 'required' => false,
979 'type' => 'string',
980 'enum' => ['site'],
981 'default' => 'site',
982 'description' => 'Context (site only)'
983 ]
984 ];
985 }
986
987 /**
988 * Get arguments for breadcrumb generation endpoint
989 *
990 * @since 1.0.0
991 *
992 * @return array Arguments array
993 */
994 private function get_breadcrumb_generation_args(): array {
995 return [
996 'breadcrumb_type' => [
997 'required' => false,
998 'type' => 'string',
999 'enum' => ['hierarchical', 'taxonomy', 'path', 'custom'],
1000 'default' => 'hierarchical',
1001 'description' => 'Type of breadcrumb navigation to generate'
1002 ],
1003 'options' => [
1004 'required' => false,
1005 'type' => 'object',
1006 'description' => 'Additional options for breadcrumb generation'
1007 ]
1008 ];
1009 }
1010
1011 /**
1012 * Get arguments for robots.txt endpoints
1013 *
1014 * @since 1.0.0
1015 *
1016 * @return array Arguments array
1017 */
1018 private function get_robots_txt_args(): array {
1019 return [
1020 'custom_rules' => [
1021 'required' => false,
1022 'type' => 'array',
1023 'items' => [
1024 'type' => 'object'
1025 ],
1026 'description' => 'Custom robots.txt rules'
1027 ],
1028 'enable_management' => [
1029 'required' => false,
1030 'type' => 'boolean',
1031 'default' => true,
1032 'description' => 'Enable automatic robots.txt management'
1033 ],
1034 'regenerate' => [
1035 'required' => false,
1036 'type' => 'boolean',
1037 'default' => true,
1038 'description' => 'Rebuild content from rules (Generate). When false, only re-sync the physical file to the stored content.'
1039 ]
1040 ];
1041 }
1042
1043 /**
1044 * Get arguments for optimization endpoint
1045 *
1046 * @since 1.0.0
1047 *
1048 * @return array Arguments array
1049 */
1050 private function get_optimization_args(): array {
1051 return [
1052 'identity_data' => [
1053 'required' => true,
1054 'type' => 'object',
1055 'description' => 'Site identity data to optimize'
1056 ]
1057 ];
1058 }
1059
1060 /**
1061 * Get arguments for AI optimization endpoint
1062 *
1063 * @since 1.0.0
1064 *
1065 * @return array Arguments array
1066 */
1067 private function get_ai_optimization_args(): array {
1068 return [
1069 'site_data' => [
1070 'required' => true,
1071 'type' => 'object',
1072 'description' => 'Site identity data to optimize with AI',
1073 'properties' => [
1074 'site_name' => [
1075 'type' => 'string',
1076 'description' => 'Site name to optimize'
1077 ],
1078 'site_description' => [
1079 'type' => 'string',
1080 'description' => 'Site description to optimize'
1081 ],
1082 'tagline' => [
1083 'type' => 'string',
1084 'description' => 'Site tagline to optimize'
1085 ]
1086 ]
1087 ],
1088 'business_type' => [
1089 'required' => false,
1090 'type' => 'string',
1091 'default' => 'website',
1092 'sanitize_callback' => 'sanitize_text_field',
1093 'description' => 'Type of business for context'
1094 ],
1095 'target_audience' => [
1096 'required' => false,
1097 'type' => 'string',
1098 'default' => 'general',
1099 'sanitize_callback' => 'sanitize_text_field',
1100 'description' => 'Target audience for optimization'
1101 ],
1102 'tone' => [
1103 'required' => false,
1104 'type' => 'string',
1105 'default' => 'professional',
1106 'sanitize_callback' => 'sanitize_text_field',
1107 'description' => 'Desired tone for optimization'
1108 ]
1109 ];
1110 }
1111
1112 /**
1113 * Get arguments for hero AI optimization endpoint
1114 *
1115 * @since 1.0.0
1116 *
1117 * @return array Arguments array
1118 */
1119 private function get_hero_optimization_args(): array {
1120 return [
1121 'hero_data' => [
1122 'required' => true,
1123 'type' => 'object',
1124 'description' => 'Hero content data to optimize with AI',
1125 'properties' => [
1126 'hero_title' => [
1127 'type' => 'string',
1128 'description' => 'Hero section title'
1129 ],
1130 'hero_subtitle' => [
1131 'type' => 'string',
1132 'description' => 'Hero section subtitle'
1133 ],
1134 'hero_cta_text' => [
1135 'type' => 'string',
1136 'description' => 'Call-to-action button text'
1137 ],
1138 'hero_cta_url' => [
1139 'type' => 'string',
1140 'description' => 'Call-to-action button URL'
1141 ]
1142 ]
1143 ],
1144 'context' => [
1145 'required' => false,
1146 'type' => 'object',
1147 'description' => 'Additional context for optimization',
1148 'properties' => [
1149 'site_name' => [
1150 'type' => 'string',
1151 'description' => 'Site name for context'
1152 ],
1153 'site_url' => [
1154 'type' => 'string',
1155 'description' => 'Site URL for context'
1156 ],
1157 'business_type' => [
1158 'type' => 'string',
1159 'description' => 'Business type for context'
1160 ],
1161 'site_description' => [
1162 'type' => 'string',
1163 'description' => 'Site description for context'
1164 ]
1165 ]
1166 ],
1167 'business_type' => [
1168 'required' => false,
1169 'type' => 'string',
1170 'default' => 'website',
1171 'sanitize_callback' => 'sanitize_text_field',
1172 'description' => 'Type of business for context'
1173 ],
1174 'target_audience' => [
1175 'required' => false,
1176 'type' => 'string',
1177 'default' => 'general',
1178 'sanitize_callback' => 'sanitize_text_field',
1179 'description' => 'Target audience for optimization'
1180 ],
1181 'tone' => [
1182 'required' => false,
1183 'type' => 'string',
1184 'default' => 'professional',
1185 'sanitize_callback' => 'sanitize_text_field',
1186 'description' => 'Desired tone for optimization'
1187 ]
1188 ];
1189 }
1190
1191 /**
1192 * Get arguments for validation endpoint
1193 *
1194 * @since 1.0.0
1195 *
1196 * @return array Arguments array
1197 */
1198 private function get_validation_args(): array {
1199 return [
1200 'settings' => [
1201 'required' => true,
1202 'type' => 'object',
1203 'description' => 'Settings to validate'
1204 ]
1205 ];
1206 }
1207
1208 /**
1209 * Check rate limit for robots.txt operations
1210 *
1211 * @since 1.0.0
1212 * @return bool True if within rate limit
1213 */
1214 private function check_robots_rate_limit(): bool {
1215 $user_id = get_current_user_id();
1216 $rate_key = "thinkrank_robots_rate_{$user_id}";
1217
1218 $requests = get_transient($rate_key) ?: 0;
1219
1220 if ($requests >= 20) { // Max 20 requests per 5 minutes
1221 return false;
1222 }
1223
1224 set_transient($rate_key, $requests + 1, 5 * MINUTE_IN_SECONDS);
1225 return true;
1226 }
1227 }
1228