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

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

426 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Content Brief API Endpoint
4 *
5 * Handles REST API endpoints for content brief generation
6 *
7 * @package ThinkRank
8 * @subpackage API
9 * @since 1.0.0
10 */
11
12 namespace ThinkRank\API;
13
14 use ThinkRank\AI\Content_Brief_Generator;
15 use ThinkRank\AI\Manager as AI_Manager;
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use WP_Error;
19
20 /**
21 * Content Brief Endpoint class
22 */
23 class Content_Brief_Endpoint {
24
25 /**
26 * API namespace
27 */
28 const NAMESPACE = 'thinkrank/v1';
29
30 /**
31 * Content brief generator instance
32 *
33 * @var Content_Brief_Generator|null
34 */
35 private ?Content_Brief_Generator $generator = null;
36
37 /**
38 * Constructor
39 */
40 public function __construct() {
41 // Don't instantiate generator here - do it lazily when needed
42 }
43
44 /**
45 * Get generator instance (lazy loading)
46 *
47 * @return Content_Brief_Generator
48 * @throws \Exception If generator cannot be created
49 */
50 private function get_generator(): Content_Brief_Generator {
51 if ($this->generator === null) {
52 // Get AI client from AI Manager with proper timeout configuration
53 $ai_manager = new AI_Manager();
54 $ai_client = $ai_manager->get_client();
55
56 $this->generator = new Content_Brief_Generator(null, $ai_client);
57 }
58 return $this->generator;
59 }
60
61 /**
62 * Register REST API routes
63 *
64 * @return void
65 */
66 public function register_routes(): void {
67 // Generate content brief
68 register_rest_route(self::NAMESPACE, '/content-brief/generate', [
69 'methods' => 'POST',
70 'callback' => [$this, 'generate_brief'],
71 'permission_callback' => [$this, 'check_permissions'],
72 'args' => [
73 'target_keywords' => [
74 'required' => true,
75 'type' => 'array',
76 'items' => [
77 'type' => 'string',
78 'minLength' => 1
79 ],
80 'minItems' => 1,
81 'validate_callback' => [$this, 'validate_keywords']
82 ],
83 'content_type' => [
84 'type' => 'string',
85 'default' => 'blog_post',
86 'enum' => ['blog_post', 'product_page', 'landing_page', 'tutorial']
87 ],
88 'target_audience' => [
89 'type' => 'string',
90 'default' => 'general',
91 'enum' => ['beginners', 'professionals', 'general', 'experts']
92 ],
93 'content_length' => [
94 'type' => 'string',
95 'default' => 'medium',
96 'enum' => ['short', 'medium', 'long']
97 ],
98 'tone' => [
99 'type' => 'string',
100 'default' => 'professional',
101 'enum' => ['professional', 'casual', 'technical', 'friendly']
102 ],
103 'competitor_urls' => [
104 'type' => 'array',
105 'items' => [
106 'type' => 'string',
107 'format' => 'uri'
108 ],
109 'default' => []
110 ],
111 'additional_context' => [
112 'type' => 'string',
113 'default' => ''
114 ]
115 ]
116 ]);
117
118 // Save content brief
119 register_rest_route(self::NAMESPACE, '/content-brief/save', [
120 'methods' => 'POST',
121 'callback' => [$this, 'save_brief'],
122 'permission_callback' => [$this, 'check_permissions'],
123 'args' => [
124 'brief_data' => [
125 'required' => true,
126 'type' => 'object'
127 ]
128 ]
129 ]);
130
131 // Get user's content briefs
132 register_rest_route(self::NAMESPACE, '/content-brief/list', [
133 'methods' => 'GET',
134 'callback' => [$this, 'get_briefs'],
135 'permission_callback' => [$this, 'check_permissions'],
136 'args' => [
137 'limit' => [
138 'type' => 'integer',
139 'default' => 10,
140 'minimum' => 1,
141 'maximum' => 50
142 ],
143 'offset' => [
144 'type' => 'integer',
145 'default' => 0,
146 'minimum' => 0
147 ]
148 ]
149 ]);
150
151 // Delete content brief
152 register_rest_route(self::NAMESPACE, '/content-brief/(?P<id>\d+)', [
153 'methods' => 'DELETE',
154 'callback' => [$this, 'delete_brief'],
155 'permission_callback' => [$this, 'check_permissions'],
156 'args' => [
157 'id' => [
158 'required' => true,
159 'type' => 'integer',
160 'minimum' => 1
161 ]
162 ]
163 ]);
164
165 // Export content brief
166 register_rest_route(self::NAMESPACE, '/content-brief/(?P<id>\d+)/export', [
167 'methods' => 'GET',
168 'callback' => [$this, 'export_brief'],
169 'permission_callback' => [$this, 'check_permissions'],
170 'args' => [
171 'id' => [
172 'required' => true,
173 'type' => 'integer',
174 'minimum' => 1
175 ],
176 'format' => [
177 'type' => 'string',
178 'default' => 'pdf',
179 'enum' => ['pdf', 'docx', 'txt']
180 ]
181 ]
182 ]);
183 }
184
185 /**
186 * Generate content brief
187 *
188 * @param WP_REST_Request $request Request object
189 * @return WP_REST_Response|WP_Error Response object
190 */
191 public function generate_brief(WP_REST_Request $request): WP_REST_Response|WP_Error {
192 try {
193 $params = [
194 'target_keywords' => $request->get_param('target_keywords'),
195 'content_type' => $request->get_param('content_type'),
196 'target_audience' => $request->get_param('target_audience'),
197 'content_length' => $request->get_param('content_length'),
198 'tone' => $request->get_param('tone'),
199 'competitor_urls' => $request->get_param('competitor_urls'),
200 'additional_context' => $request->get_param('additional_context')
201 ];
202
203 $brief_data = $this->get_generator()->generate_brief($params);
204
205 return new WP_REST_Response([
206 'success' => true,
207 'data' => $brief_data,
208 'message' => 'Content brief generated successfully'
209 ], 200);
210
211 } catch (\Exception $e) {
212 return new WP_Error(
213 'brief_generation_failed',
214 $e->getMessage(),
215 ['status' => 500]
216 );
217 }
218 }
219
220 /**
221 * Save content brief
222 *
223 * @param WP_REST_Request $request Request object
224 * @return WP_REST_Response|WP_Error Response object
225 */
226 public function save_brief(WP_REST_Request $request): WP_REST_Response|WP_Error {
227 try {
228 $brief_data = $request->get_param('brief_data');
229
230 // This would typically save an existing brief or update it
231 // For now, we'll return success as the generation already saves
232
233 return new WP_REST_Response([
234 'success' => true,
235 'message' => 'Content brief saved successfully'
236 ], 200);
237
238 } catch (\Exception $e) {
239 return new WP_Error(
240 'brief_save_failed',
241 $e->getMessage(),
242 ['status' => 500]
243 );
244 }
245 }
246
247 /**
248 * Get user's content briefs
249 *
250 * @param WP_REST_Request $request Request object
251 * @return WP_REST_Response|WP_Error Response object
252 */
253 public function get_briefs(WP_REST_Request $request): WP_REST_Response|WP_Error {
254 try {
255 $limit = $request->get_param('limit');
256 $offset = $request->get_param('offset');
257
258 $briefs = $this->get_generator()->get_user_briefs($limit, $offset);
259
260 return new WP_REST_Response([
261 'success' => true,
262 'data' => $briefs,
263 'total' => count($briefs)
264 ], 200);
265
266 } catch (\Exception $e) {
267 // If no API key is configured, return empty list instead of error
268 if (strpos($e->getMessage(), 'Please configure your AI provider') !== false) {
269 return new WP_REST_Response([
270 'success' => true,
271 'data' => [],
272 'total' => 0
273 ], 200);
274 }
275
276 return new WP_Error(
277 'briefs_fetch_failed',
278 $e->getMessage(),
279 ['status' => 500]
280 );
281 }
282 }
283
284 /**
285 * Delete content brief
286 *
287 * @param WP_REST_Request $request Request object
288 * @return WP_REST_Response|WP_Error Response object
289 */
290 public function delete_brief(WP_REST_Request $request): WP_REST_Response|WP_Error {
291 try {
292 $brief_id = $request->get_param('id');
293 $success = $this->get_generator()->delete_brief($brief_id);
294
295 if ($success) {
296 return new WP_REST_Response([
297 'success' => true,
298 'message' => 'Content brief deleted successfully'
299 ], 200);
300 } else {
301 return new WP_Error(
302 'brief_delete_failed',
303 'Failed to delete content brief',
304 ['status' => 500]
305 );
306 }
307
308 } catch (\Exception $e) {
309 return new WP_Error(
310 'brief_delete_failed',
311 $e->getMessage(),
312 ['status' => 500]
313 );
314 }
315 }
316
317 /**
318 * Export content brief
319 *
320 * @param WP_REST_Request $request Request object
321 * @return WP_REST_Response|WP_Error Response object
322 */
323 public function export_brief(WP_REST_Request $request): WP_REST_Response|WP_Error {
324 try {
325 $brief_id = $request->get_param('id');
326 $format = $request->get_param('format');
327
328 // For now, return a simple text export
329 // In a full implementation, this would generate PDF/DOCX files
330 $briefs = $this->get_generator()->get_user_briefs(1, 0);
331 $brief = $briefs[0] ?? null;
332
333 if (!$brief) {
334 return new WP_Error(
335 'brief_not_found',
336 'Content brief not found',
337 ['status' => 404]
338 );
339 }
340
341 $export_data = $this->format_brief_for_export($brief, $format);
342
343 return new WP_REST_Response([
344 'success' => true,
345 'data' => $export_data,
346 'format' => $format
347 ], 200);
348
349 } catch (\Exception $e) {
350 return new WP_Error(
351 'brief_export_failed',
352 $e->getMessage(),
353 ['status' => 500]
354 );
355 }
356 }
357
358 /**
359 * Format brief for export
360 *
361 * @param array $brief Brief data
362 * @param string $format Export format
363 * @return string Formatted content
364 */
365 private function format_brief_for_export(array $brief, string $format): string {
366 $brief_data = $brief['brief_data'];
367
368 $content = "Content Brief: " . $brief['title'] . "\n\n";
369 $content .= "Target Keywords: " . implode(', ', $brief['target_keywords']) . "\n";
370 $content .= "Content Type: " . $brief['content_type'] . "\n\n";
371
372 if (!empty($brief_data['outline'])) {
373 $content .= "Content Outline:\n";
374 foreach ($brief_data['outline'] as $item) {
375 $indent = str_repeat(' ', $item['level'] - 1);
376 $content .= $indent . "H{$item['level']}: " . $item['heading'];
377 if ($item['word_count'] > 0) {
378 $content .= " ({$item['word_count']} words)";
379 }
380 $content .= "\n";
381 }
382 }
383
384 $content .= "\nGenerated on: " . $brief['created_at'];
385
386 return $content;
387 }
388
389 /**
390 * Validate keywords parameter
391 *
392 * @param array $keywords Keywords to validate
393 * @return bool|WP_Error Validation result
394 */
395 public function validate_keywords(array $keywords): bool|WP_Error {
396 if (empty($keywords)) {
397 return new WP_Error(
398 'invalid_keywords',
399 'At least one keyword is required',
400 ['status' => 400]
401 );
402 }
403
404 foreach ($keywords as $keyword) {
405 if (!is_string($keyword) || empty(trim($keyword))) {
406 return new WP_Error(
407 'invalid_keyword',
408 'All keywords must be non-empty strings',
409 ['status' => 400]
410 );
411 }
412 }
413
414 return true;
415 }
416
417 /**
418 * Check permissions for API access
419 *
420 * @return bool Permission status
421 */
422 public function check_permissions(): bool {
423 return current_user_can('edit_posts');
424 }
425 }
426