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

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