PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.10.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-llms-txt-endpoint.php

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

1,000 lines 34.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * LLMs.txt API Endpoints Class
4 *
5 * REST API endpoints for LLMs.txt file management including content generation,
6 * AI-powered optimization, file writing, and status monitoring. Provides
7 * comprehensive API access to LLMs.txt Manager and AI Manager functionality
8 * with proper authentication, validation, and error handling.
9 *
10 * @package ThinkRank
11 * @subpackage API
12 * @since 1.0.0
13 */
14
15 declare(strict_types=1);
16
17 namespace ThinkRank\API;
18
19 use ThinkRank\SEO\LLMs_Txt_Manager;
20 use ThinkRank\AI\Manager as AI_Manager;
21 use ThinkRank\API\Traits\CSRF_Protection;
22 use WP_REST_Controller;
23 use WP_REST_Request;
24 use WP_REST_Response;
25 use WP_Error;
26
27 // Load CSRF Protection trait
28 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php';
29
30 /**
31 * LLMs.txt API Endpoints Class
32 *
33 * Provides REST API endpoints for LLMs.txt operations including
34 * content generation, AI-powered optimization, file management,
35 * and status monitoring with proper authentication and validation.
36 *
37 * @since 1.0.0
38 */
39 class LLMs_Txt_Endpoint extends WP_REST_Controller {
40 use CSRF_Protection;
41
42 /**
43 * LLMs.txt Manager instance
44 *
45 * @since 1.0.0
46 * @var LLMs_Txt_Manager
47 */
48 private LLMs_Txt_Manager $llms_txt_manager;
49
50 /**
51 * API namespace
52 *
53 * @since 1.0.0
54 * @var string
55 */
56 protected $namespace = 'thinkrank/v1';
57
58 /**
59 * API resource base
60 *
61 * @since 1.0.0
62 * @var string
63 */
64 protected $rest_base = 'llms-txt';
65
66 /**
67 * AI Manager instance
68 *
69 * @since 1.0.0
70 * @var AI_Manager|null
71 */
72 private ?AI_Manager $ai_manager = null;
73
74 /**
75 * Constructor
76 *
77 * @since 1.0.0
78 */
79 public function __construct() {
80 // Ensure LLMs.txt Manager is loaded
81 if (!class_exists('ThinkRank\\SEO\\LLMs_Txt_Manager')) {
82 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-llms-txt-manager.php';
83 }
84
85 $this->llms_txt_manager = new LLMs_Txt_Manager();
86 }
87
88 /**
89 * Get AI Manager instance
90 *
91 * @since 1.0.0
92 *
93 * @return AI_Manager AI Manager instance
94 * @throws \Exception If AI Manager cannot be initialized
95 */
96 private function get_ai_manager(): AI_Manager {
97 if (!$this->ai_manager) {
98 // Ensure AI Manager is loaded
99 if (!class_exists('ThinkRank\\AI\\Manager')) {
100 require_once THINKRANK_PLUGIN_DIR . 'includes/ai/class-manager.php';
101 }
102
103 $this->ai_manager = new AI_Manager();
104 }
105
106 return $this->ai_manager;
107 }
108
109 /**
110 * Register API routes
111 *
112 * @since 1.0.0
113 */
114 public function register_routes(): void {
115 // Get/Update LLMs.txt settings
116 register_rest_route(
117 $this->namespace,
118 '/' . $this->rest_base . '/settings',
119 [
120 [
121 'methods' => 'GET',
122 'callback' => [$this, 'get_settings'],
123 'permission_callback' => [$this, 'check_read_permissions']
124 ],
125 [
126 'methods' => 'POST',
127 'callback' => [$this, 'update_settings'],
128 'permission_callback' => [$this, 'check_csrf_permissions'],
129 'args' => $this->get_settings_args()
130 ]
131 ]
132 );
133
134 // Generate LLMs.txt file
135 register_rest_route(
136 $this->namespace,
137 '/' . $this->rest_base . '/generate',
138 [
139 [
140 'methods' => 'POST',
141 'callback' => [$this, 'generate_llms_txt'],
142 'permission_callback' => [$this, 'check_csrf_permissions'],
143 'args' => $this->get_generation_args()
144 ]
145 ]
146 );
147
148 // AI-powered LLMs.txt optimization
149 register_rest_route(
150 $this->namespace,
151 '/' . $this->rest_base . '/ai-optimize',
152 [
153 [
154 'methods' => 'POST',
155 'callback' => [$this, 'ai_optimize_llms_txt'],
156 'permission_callback' => [$this, 'check_csrf_permissions'],
157 'args' => $this->get_ai_optimization_args()
158 ]
159 ]
160 );
161
162 // Get LLMs.txt file status
163 register_rest_route(
164 $this->namespace,
165 '/' . $this->rest_base . '/status',
166 [
167 [
168 'methods' => 'GET',
169 'callback' => [$this, 'get_llms_txt_status'],
170 'permission_callback' => [$this, 'check_read_permissions']
171 ]
172 ]
173 );
174
175 // Validate LLMs.txt settings (read-only validation, no CSRF needed)
176 register_rest_route(
177 $this->namespace,
178 '/' . $this->rest_base . '/validate',
179 [
180 [
181 'methods' => 'POST',
182 'callback' => [$this, 'validate_llms_txt_settings'],
183 'permission_callback' => [$this, 'check_read_permissions'],
184 'args' => $this->get_validation_args()
185 ]
186 ]
187 );
188
189 // Get latest optimization results
190 register_rest_route(
191 $this->namespace,
192 '/' . $this->rest_base . '/optimization-results',
193 [
194 [
195 'methods' => 'GET',
196 'callback' => [$this, 'get_optimization_results'],
197 'permission_callback' => [$this, 'check_read_permissions']
198 ]
199 ]
200 );
201
202 // Get overview data (combined endpoint for performance)
203 register_rest_route(
204 $this->namespace,
205 '/' . $this->rest_base . '/overview',
206 [
207 [
208 'methods' => 'GET',
209 'callback' => [$this, 'get_overview_data'],
210 'permission_callback' => [$this, 'check_read_permissions']
211 ]
212 ]
213 );
214 }
215
216 /**
217 * Get LLMs.txt settings
218 *
219 * @since 1.0.0
220 *
221 * @param WP_REST_Request $request Request object
222 * @return WP_REST_Response Response object
223 */
224 public function get_settings(WP_REST_Request $request): WP_REST_Response {
225 try {
226 $context_type = $request->get_param('context_type') ?? 'site';
227 $context_id = $request->get_param('context_id');
228
229 // Get settings from LLMs.txt Manager
230 $settings = $this->llms_txt_manager->get_settings($context_type, $context_id);
231
232 // Get settings schema for validation
233 $schema = $this->llms_txt_manager->get_settings_schema($context_type);
234
235 return new WP_REST_Response([
236 'success' => true,
237 'data' => [
238 'settings' => $settings,
239 'schema' => $schema,
240 'context_type' => $context_type,
241 'context_id' => $context_id
242 ],
243 'message' => 'LLMs.txt settings retrieved successfully'
244 ], 200);
245
246 } catch (\Exception $e) {
247 return new WP_REST_Response([
248 'success' => false,
249 'error' => 'Failed to retrieve LLMs.txt settings: ' . $e->getMessage()
250 ], 500);
251 }
252 }
253
254 /**
255 * Update LLMs.txt settings
256 *
257 * @since 1.0.0
258 *
259 * @param WP_REST_Request $request Request object
260 * @return WP_REST_Response|WP_Error Response object or error
261 */
262 public function update_settings(WP_REST_Request $request) {
263 try {
264 $settings = $request->get_param('settings');
265 $context_type = $request->get_param('context_type') ?? 'site';
266 $context_id = $request->get_param('context_id');
267
268 // Validate settings
269 if (empty($settings) || !is_array($settings)) {
270 return new WP_Error(
271 'invalid_settings',
272 'Settings must be provided as an array',
273 ['status' => 400]
274 );
275 }
276
277 // Validate settings using LLMs.txt Manager
278 $validation = $this->llms_txt_manager->validate_settings($settings);
279
280 if (!$validation['valid']) {
281 return new WP_Error(
282 'validation_failed',
283 'Settings validation failed',
284 [
285 'status' => 400,
286 'validation_errors' => $validation['errors'],
287 'validation_warnings' => $validation['warnings']
288 ]
289 );
290 }
291
292 // Update settings
293 $update_result = $this->llms_txt_manager->save_settings($context_type, $context_id, $settings);
294
295 if (!$update_result) {
296 return new WP_Error(
297 'update_failed',
298 'Failed to update LLMs.txt settings',
299 ['status' => 500]
300 );
301 }
302
303 return new WP_REST_Response([
304 'success' => true,
305 'data' => [
306 'settings' => $settings,
307 'validation' => $validation
308 ],
309 'message' => 'LLMs.txt settings updated successfully'
310 ], 200);
311
312 } catch (\Exception $e) {
313 return new WP_Error(
314 'update_failed',
315 'Failed to update LLMs.txt settings: ' . $e->getMessage(),
316 ['status' => 500]
317 );
318 }
319 }
320
321 /**
322 * Generate LLMs.txt file
323 *
324 * @since 1.0.0
325 *
326 * @param WP_REST_Request $request Request object
327 * @return WP_REST_Response|WP_Error Response object or error
328 */
329 public function generate_llms_txt(WP_REST_Request $request) {
330 try {
331 // Check rate limiting
332 if (!$this->check_llms_rate_limit()) {
333 return new WP_Error(
334 'rate_limit_exceeded',
335 'Too many requests. Please wait a few minutes before trying again.',
336 ['status' => 429]
337 );
338 }
339 $user_input = $request->get_param('user_input');
340 $options = $request->get_param('options') ?? [];
341 $save_to_file = $request->get_param('save_to_file') ?? true;
342
343 // Validate user input
344 if (empty($user_input) || !is_array($user_input)) {
345 return new WP_Error(
346 'invalid_input',
347 'User input must be provided as an array',
348 ['status' => 400]
349 );
350 }
351
352 // Validate required fields
353 $required_fields = ['website_description', 'key_features', 'target_audience'];
354 foreach ($required_fields as $field) {
355 if (empty($user_input[$field])) {
356 return new WP_Error(
357 'missing_field',
358 "Required field '{$field}' is missing or empty",
359 ['status' => 400]
360 );
361 }
362 }
363
364 // Sanitize user input
365 $sanitized_input = [
366 'site_name' => sanitize_text_field($user_input['site_name'] ?? ''),
367 'website_description' => sanitize_textarea_field($user_input['website_description']),
368 'key_features' => sanitize_textarea_field($user_input['key_features']),
369 'target_audience' => sanitize_text_field($user_input['target_audience']),
370 'business_type' => sanitize_text_field($user_input['business_type'] ?? 'website'),
371 'technical_stack' => sanitize_textarea_field($user_input['technical_stack'] ?? ''),
372 'development_approach' => sanitize_textarea_field($user_input['development_approach'] ?? ''),
373 'setup_instructions' => sanitize_textarea_field($user_input['setup_instructions'] ?? ''),
374 'ai_context_custom' => sanitize_textarea_field($user_input['ai_context_custom'] ?? ''),
375 'documentation_links' => $this->llms_txt_manager->sanitize_llms_content($user_input['documentation_links'] ?? ''),
376 'technical_links' => $this->llms_txt_manager->sanitize_llms_content($user_input['technical_links'] ?? ''),
377 'optional_links' => $this->llms_txt_manager->sanitize_llms_content($user_input['optional_links'] ?? ''),
378 'custom_sections' => $this->llms_txt_manager->sanitize_llms_content($user_input['custom_sections'] ?? '')
379 ];
380
381 // Generate LLMs.txt using LLMs.txt Manager
382 $generation_result = $this->llms_txt_manager->generate_llms_txt($sanitized_input, $options);
383
384 if (!$generation_result['validation']['valid']) {
385 return new WP_Error(
386 'generation_failed',
387 'LLMs.txt generation validation failed',
388 [
389 'status' => 400,
390 'validation_errors' => $generation_result['validation']['errors']
391 ]
392 );
393 }
394
395 // Write to file if requested
396 $file_write_result = ['success' => false, 'message' => 'File writing disabled'];
397 if ($save_to_file && !empty($generation_result['content'])) {
398 $file_write_result = $this->llms_txt_manager->write_llms_txt_to_file(
399 $generation_result['content']
400 );
401 }
402
403 return new WP_REST_Response([
404 'success' => true,
405 'data' => [
406 'content' => $generation_result['content'],
407 'sections' => $generation_result['sections'],
408 'metadata' => $generation_result['metadata'],
409 'validation' => $generation_result['validation'],
410 'file_info' => $generation_result['file_info'],
411 'file_write_result' => $file_write_result
412 ],
413 'message' => 'LLMs.txt generated successfully'
414 ], 200);
415
416 } catch (\Exception $e) {
417 return new WP_Error(
418 'generation_failed',
419 'LLMs.txt generation failed: ' . $e->getMessage(),
420 ['status' => 500]
421 );
422 }
423 }
424
425 /**
426 * AI optimize LLMs.txt content
427 *
428 * @since 1.0.0
429 *
430 * @param WP_REST_Request $request Request object
431 * @return WP_REST_Response|WP_Error Response object or error
432 */
433 public function ai_optimize_llms_txt(WP_REST_Request $request) {
434 try {
435 // Check rate limiting for AI operations
436 if (!$this->check_ai_rate_limit()) {
437 return new WP_Error(
438 'rate_limit_exceeded',
439 'Too many AI requests. Please wait a few minutes before trying again.',
440 ['status' => 429]
441 );
442 }
443
444 $website_data = $request->get_param('website_data');
445 $options = [
446 'business_type' => $request->get_param('business_type'),
447 'target_audience' => $request->get_param('target_audience'),
448 'tone' => $request->get_param('tone')
449 ];
450
451 // Validate website data
452 if (empty($website_data) || !is_array($website_data)) {
453 return new WP_Error(
454 'invalid_data',
455 'Website data must be provided as an array',
456 ['status' => 400]
457 );
458 }
459
460 // Validate required website data fields
461 $required_fields = ['website_description', 'key_features'];
462 foreach ($required_fields as $field) {
463 if (empty($website_data[$field])) {
464 return new WP_Error(
465 'missing_field',
466 "Required field '{$field}' is missing or empty",
467 ['status' => 400]
468 );
469 }
470 }
471
472 // Sanitize website data
473 $sanitized_website_data = [
474 'website_description' => sanitize_textarea_field($website_data['website_description']),
475 'key_features' => sanitize_textarea_field($website_data['key_features']),
476 'target_audience' => sanitize_text_field($website_data['target_audience'] ?? ''),
477 'business_type' => sanitize_text_field($website_data['business_type'] ?? 'website'),
478 'technical_stack' => sanitize_textarea_field($website_data['technical_stack'] ?? ''),
479 'development_approach' => sanitize_textarea_field($website_data['development_approach'] ?? '')
480 ];
481
482 // Sanitize options
483 $sanitized_options = [
484 'business_type' => sanitize_text_field($options['business_type'] ?? 'website'),
485 'target_audience' => sanitize_text_field($options['target_audience'] ?? 'general'),
486 'tone' => sanitize_text_field($options['tone'] ?? 'professional')
487 ];
488
489 // Get AI manager and perform optimization
490 $ai_manager = $this->get_ai_manager();
491 $optimization_results = $ai_manager->optimize_llms_txt($sanitized_website_data, $sanitized_options);
492
493 // Store optimization results in database (following Site Identity pattern)
494 $this->store_optimization_results($optimization_results);
495
496 return new WP_REST_Response([
497 'success' => true,
498 'data' => $optimization_results,
499 'message' => 'LLMs.txt AI optimization completed'
500 ], 200);
501
502 } catch (\Exception $e) {
503 return new WP_Error(
504 'ai_optimization_failed',
505 'AI optimization failed: ' . $e->getMessage(),
506 ['status' => 500]
507 );
508 }
509 }
510
511 /**
512 * Store optimization results in seo_analysis table (following Site Identity pattern)
513 *
514 * @since 1.0.0
515 *
516 * @param array $optimization Optimization results
517 * @return void
518 */
519 private function store_optimization_results(array $optimization): void {
520 global $wpdb;
521
522 $table_name = $wpdb->prefix . 'thinkrank_seo_analysis';
523
524 // Only store if we have meaningful results
525 if (empty($optimization['suggestions'])) {
526 return;
527 }
528
529 $analysis_type = 'llms_txt_ai_optimization';
530
531 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SEO analysis storage requires direct database access
532 $wpdb->insert(
533 $table_name,
534 [
535 'context_type' => 'site',
536 'context_id' => null,
537 'analysis_type' => $analysis_type,
538 'analysis_data' => wp_json_encode($optimization),
539 'score' => $optimization['score'] ?? 0,
540 'status' => 'completed',
541 'recommendations' => wp_json_encode($optimization['suggestions'] ?? []),
542 'analyzed_by' => get_current_user_id()
543 ],
544 ['%s', '%d', '%s', '%s', '%d', '%s', '%s', '%d']
545 );
546 }
547
548 /**
549 * Get optimization results history (Content Brief style)
550 *
551 * @since 1.0.0
552 *
553 * @param WP_REST_Request $request Request object
554 * @return WP_REST_Response|WP_Error Response object
555 */
556 public function get_optimization_results(WP_REST_Request $request) {
557 try {
558 global $wpdb;
559
560 $table_name = $wpdb->prefix . 'thinkrank_seo_analysis';
561 $limit = $request->get_param('limit') ?? 5; // Default to 5 recent results
562
563 // Get recent LLMs.txt optimization results
564 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SEO analysis retrieval requires direct database access
565 $sql = "SELECT analysis_data, recommendations, score, created_at
566 FROM {$table_name}
567 WHERE analysis_type = %s AND context_type = %s
568 ORDER BY created_at DESC
569 LIMIT %d";
570
571 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Analysis data retrieval requires direct database access
572 $results = $wpdb->get_results(
573 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name is validated with WordPress prefix, SQL is properly prepared
574 $wpdb->prepare($sql, 'llms_txt_ai_optimization', 'site', $limit)
575 );
576
577 if (!$results) {
578 return new WP_REST_Response([
579 'success' => true,
580 'data' => [],
581 'message' => 'No optimization results found'
582 ], 200);
583 }
584
585 $history = [];
586 foreach ($results as $result) {
587 $analysis_data = json_decode($result->analysis_data, true);
588 $suggestions = json_decode($result->recommendations, true);
589
590 $history[] = [
591 'suggestions' => $suggestions,
592 'provider' => $analysis_data['provider'] ?? '',
593 'model' => $analysis_data['model'] ?? $analysis_data['ai_model'] ?? '',
594 'score' => $result->score,
595 'created_at' => $result->created_at,
596 'formatted_date' => wp_date('M j, Y g:i A', strtotime($result->created_at))
597 ];
598 }
599
600 return new WP_REST_Response([
601 'success' => true,
602 'data' => $history,
603 'message' => 'Optimization results history retrieved'
604 ], 200);
605
606 } catch (\Exception $e) {
607 return new WP_Error(
608 'optimization_results_failed',
609 'Failed to retrieve optimization results: ' . $e->getMessage(),
610 ['status' => 500]
611 );
612 }
613 }
614
615 /**
616 * Get LLMs.txt file status
617 *
618 * @since 1.0.0
619 *
620 * @param WP_REST_Request $request Request object
621 * @return WP_REST_Response Response object
622 */
623 public function get_llms_txt_status(WP_REST_Request $request): WP_REST_Response {
624 try {
625 // Get file status using LLMs.txt Manager
626 $status = $this->llms_txt_manager->get_llms_txt_status();
627
628 return new WP_REST_Response([
629 'success' => true,
630 'data' => $status,
631 'message' => 'LLMs.txt status retrieved successfully'
632 ], 200);
633
634 } catch (\Exception $e) {
635 return new WP_REST_Response([
636 'success' => false,
637 'error' => 'Failed to retrieve LLMs.txt status: ' . $e->getMessage()
638 ], 500);
639 }
640 }
641
642 /**
643 * Validate LLMs.txt settings
644 *
645 * @since 1.0.0
646 *
647 * @param WP_REST_Request $request Request object
648 * @return WP_REST_Response Response object
649 */
650 public function validate_llms_txt_settings(WP_REST_Request $request): WP_REST_Response {
651 try {
652 $settings = $request->get_param('settings');
653
654 // Sanitize user input for validation
655 $sanitized_input = [
656 'site_name' => sanitize_text_field($settings['site_name'] ?? ''),
657 'website_description' => sanitize_textarea_field($settings['website_description'] ?? ''),
658 'key_features' => sanitize_textarea_field($settings['key_features'] ?? ''),
659 'target_audience' => sanitize_text_field($settings['target_audience'] ?? ''),
660 'business_type' => sanitize_text_field($settings['business_type'] ?? 'website'),
661 'technical_stack' => sanitize_textarea_field($settings['technical_stack'] ?? ''),
662 'development_approach' => sanitize_textarea_field($settings['development_approach'] ?? ''),
663 'setup_instructions' => sanitize_textarea_field($settings['setup_instructions'] ?? ''),
664 'documentation_links' => $this->llms_txt_manager->sanitize_llms_content($settings['documentation_links'] ?? ''),
665 'technical_links' => $this->llms_txt_manager->sanitize_llms_content($settings['technical_links'] ?? ''),
666 'optional_links' => $this->llms_txt_manager->sanitize_llms_content($settings['optional_links'] ?? ''),
667 'custom_sections' => $this->llms_txt_manager->sanitize_llms_content($settings['custom_sections'] ?? '')
668 ];
669
670 // Validate user input using LLMs.txt Manager
671 $validation = $this->llms_txt_manager->validate_llms_txt_input($sanitized_input);
672
673 return new WP_REST_Response([
674 'success' => true,
675 'data' => $validation,
676 'message' => 'LLMs.txt validation completed'
677 ], 200);
678
679 } catch (\Exception $e) {
680 return new WP_REST_Response([
681 'success' => false,
682 'error' => 'Validation failed: ' . $e->getMessage()
683 ], 500);
684 }
685 }
686
687 /**
688 * Permission callbacks
689 */
690
691 /**
692 * Check permissions for LLMs.txt operations (admin-only)
693 *
694 * @since 1.0.0
695 *
696 * @return bool Permission status
697 */
698 public function check_permissions(): bool {
699 return current_user_can('manage_options');
700 }
701
702 /**
703 * Helper methods
704 */
705
706 /**
707 * Validate context type and ID
708 *
709 * @since 1.0.0
710 *
711 * @param string $context_type Context type
712 * @param int|null $context_id Context ID
713 * @return bool Validation status
714 */
715 private function validate_context(string $context_type): bool {
716 // Only support 'site' context in development stage
717 return $context_type === 'site';
718 }
719
720 /**
721 * Argument validation methods
722 */
723
724 /**
725 * Get arguments for settings endpoints
726 *
727 * @since 1.0.0
728 *
729 * @return array Arguments array
730 */
731 private function get_settings_args(): array {
732 return [
733 'settings' => [
734 'required' => true,
735 'type' => 'object',
736 'description' => 'LLMs.txt settings to update'
737 ],
738 'context_type' => [
739 'required' => false,
740 'type' => 'string',
741 'enum' => ['site'],
742 'default' => 'site',
743 'description' => 'Context type (site only)'
744 ],
745 'context_id' => [
746 'required' => false,
747 'type' => 'integer',
748 'minimum' => 1,
749 'description' => 'Context ID (not required for site context)'
750 ]
751 ];
752 }
753
754 /**
755 * Get arguments for generation endpoint
756 *
757 * @since 1.0.0
758 *
759 * @return array Arguments array
760 */
761 private function get_generation_args(): array {
762 return [
763 'user_input' => [
764 'required' => true,
765 'type' => 'object',
766 'description' => 'User input data for LLMs.txt generation',
767 'properties' => [
768 'website_description' => [
769 'type' => 'string',
770 'description' => 'Website description'
771 ],
772 'key_features' => [
773 'type' => 'string',
774 'description' => 'Key website features'
775 ],
776 'target_audience' => [
777 'type' => 'string',
778 'description' => 'Target audience'
779 ],
780 'business_type' => [
781 'type' => 'string',
782 'description' => 'Business type'
783 ],
784 'technical_stack' => [
785 'type' => 'string',
786 'description' => 'Technical stack information'
787 ]
788 ]
789 ],
790 'options' => [
791 'required' => false,
792 'type' => 'object',
793 'description' => 'Generation options'
794 ],
795 'save_to_file' => [
796 'required' => false,
797 'type' => 'boolean',
798 'default' => true,
799 'description' => 'Whether to save generated content to file'
800 ]
801 ];
802 }
803
804 /**
805 * Get arguments for AI optimization endpoint
806 *
807 * @since 1.0.0
808 *
809 * @return array Arguments array
810 */
811 private function get_ai_optimization_args(): array {
812 return [
813 'website_data' => [
814 'required' => true,
815 'type' => 'object',
816 'description' => 'Website data to optimize with AI',
817 'properties' => [
818 'website_description' => [
819 'type' => 'string',
820 'description' => 'Website description to optimize'
821 ],
822 'key_features' => [
823 'type' => 'string',
824 'description' => 'Key features to optimize'
825 ],
826 'target_audience' => [
827 'type' => 'string',
828 'description' => 'Target audience information'
829 ],
830 'business_type' => [
831 'type' => 'string',
832 'description' => 'Business type'
833 ]
834 ]
835 ],
836 'business_type' => [
837 'required' => false,
838 'type' => 'string',
839 'default' => 'website',
840 'sanitize_callback' => 'sanitize_text_field',
841 'description' => 'Type of business for context'
842 ],
843 'target_audience' => [
844 'required' => false,
845 'type' => 'string',
846 'default' => 'general',
847 'sanitize_callback' => 'sanitize_text_field',
848 'description' => 'Target audience for optimization'
849 ],
850 'tone' => [
851 'required' => false,
852 'type' => 'string',
853 'default' => 'professional',
854 'sanitize_callback' => 'sanitize_text_field',
855 'description' => 'Desired tone for optimization'
856 ]
857 ];
858 }
859
860 /**
861 * Get arguments for validation endpoint
862 *
863 * @since 1.0.0
864 *
865 * @return array Arguments array
866 */
867 private function get_validation_args(): array {
868 return [
869 'settings' => [
870 'required' => true,
871 'type' => 'object',
872 'description' => 'Settings to validate'
873 ]
874 ];
875 }
876
877 /**
878 * Get overview data (combined endpoint for performance)
879 *
880 * Combines settings, file status, and optimization history into a single API call
881 * to reduce initial load time and improve user experience.
882 *
883 * @since 1.0.0
884 *
885 * @param WP_REST_Request $request Request object
886 * @return WP_REST_Response Response object
887 */
888 public function get_overview_data(WP_REST_Request $request): WP_REST_Response {
889 try {
890 // Get all data in parallel to minimize processing time
891 $settings = $this->llms_txt_manager->get_settings('site');
892 $file_status = $this->llms_txt_manager->get_llms_txt_status();
893
894 // Get recent optimization history (limit to 5 for performance)
895 $optimization_history = $this->get_recent_optimization_history(5);
896
897 return new WP_REST_Response([
898 'success' => true,
899 'data' => [
900 'settings' => $settings,
901 'file_status' => $file_status,
902 'optimization_history' => $optimization_history
903 ]
904 ], 200);
905
906 } catch (Exception $e) {
907 return new WP_REST_Response([
908 'success' => false,
909 'error' => 'Failed to load overview data: ' . $e->getMessage()
910 ], 500);
911 }
912 }
913
914 /**
915 * Get recent optimization history (optimized version)
916 *
917 * @since 1.0.0
918 *
919 * @param int $limit Number of results to return
920 * @return array Optimization history
921 */
922 private function get_recent_optimization_history(int $limit = 5): array {
923 global $wpdb;
924
925 $table_name = $wpdb->prefix . 'thinkrank_ai_usage';
926
927 // Optimized query with limit and specific fields only
928 $sql = "SELECT
929 id,
930 created_at,
931 tokens_used,
932 metadata
933 FROM {$table_name}
934 WHERE action = 'llms_txt_optimization'
935 ORDER BY created_at DESC
936 LIMIT %d";
937
938 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Usage analytics retrieval requires direct database access
939 $results = $wpdb->get_results(
940 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name is validated with WordPress prefix, SQL is properly prepared
941 $wpdb->prepare($sql, $limit), ARRAY_A);
942
943 if (empty($results)) {
944 return [];
945 }
946
947 // Process results efficiently
948 return array_map(function($result) {
949 $metadata = json_decode($result['metadata'], true) ?? [];
950 return [
951 'id' => (int) $result['id'],
952 'created_at' => $result['created_at'],
953 'tokens_used' => (int) $result['tokens_used'],
954 'improvements_made' => $metadata['improvements_made'] ?? [],
955 'suggestions' => $metadata['suggestions'] ?? []
956 ];
957 }, $results);
958 }
959
960 /**
961 * Check rate limit for LLMs.txt generation operations
962 *
963 * @since 1.0.0
964 * @return bool True if within rate limit
965 */
966 private function check_llms_rate_limit(): bool {
967 $user_id = get_current_user_id();
968 $rate_key = "thinkrank_llms_rate_{$user_id}";
969
970 $requests = get_transient($rate_key) ?: 0;
971
972 if ($requests >= 3) { // Max 3 requests per 5 minutes
973 return false;
974 }
975
976 set_transient($rate_key, $requests + 1, 5 * MINUTE_IN_SECONDS);
977 return true;
978 }
979
980 /**
981 * Check rate limit for AI optimization operations
982 *
983 * @since 1.0.0
984 * @return bool True if within rate limit
985 */
986 private function check_ai_rate_limit(): bool {
987 $user_id = get_current_user_id();
988 $rate_key = "thinkrank_ai_rate_{$user_id}";
989
990 $requests = get_transient($rate_key) ?: 0;
991
992 if ($requests >= 2) { // Max 2 AI requests per 10 minutes (more restrictive)
993 return false;
994 }
995
996 set_transient($rate_key, $requests + 1, 10 * MINUTE_IN_SECONDS);
997 return true;
998 }
999 }
1000