PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.1
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 2.0.1, at includes/api/class-llms-txt-endpoint.php

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