PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.6.7
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.6.7
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
templately / includes / API / AIContent.php

AIContent.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.6.7, at includes/API/AIContent.php

1,027 lines 32.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Templately AI Content Importer
5 *
6 * @package Templately
7 * @since 1.0.0
8 */
9
10 namespace Templately\API;
11
12 use Error;
13 use Exception;
14 use Templately\Utils\Helper;
15 use WP_REST_Request;
16 use WP_Error;
17 use Templately\Core\Importer\Utils\Utils;
18 use Templately\Core\Importer\Utils\AIUtils;
19 use Templately\Core\Importer\Utils\SignatureVerifier;
20 use Templately\Core\Importer\Parsers\WXR_Parser;
21
22 class AIContent extends API {
23 private $endpoint = 'ai-content';
24 private $dev_mode = false;
25
26
27 /**
28 * AIContent constructor.
29 *
30 * @param string $file File path.
31 * @param array $settings Settings.
32 */
33 public function __construct() {
34
35 parent::__construct();
36
37 }
38
39 public function _permission_check(WP_REST_Request $request) {
40 $this->request = $request;
41 $this->api_key = $this->utils('options')->get( 'api_key' );
42 $process_id = $this->get_param('process_id');
43
44 $_route = $request->get_route();
45 if ('/templately/v1/ai-content/ai-update' === $_route || '/templately/v1/ai-content/ai-update-preview' === $_route) {
46 Helper::log( [
47 'headers' => $request->get_headers(),
48 'body' => $request->get_params(),
49 ], 'ai_update_request' );
50
51 if (empty($process_id)) {
52 return $this->error('invalid_id', __('Invalid ID.', 'templately'), 'calculate_credit', 400);
53 }
54
55 $header_api_key = sanitize_text_field($request->get_header('x_templately_apikey'));
56 if (empty($header_api_key)) {
57 $header_api_key = sanitize_text_field($request->get_header('X-Templately-Apikey'));
58 }
59
60 // Validate API key from header against database
61 if (empty($header_api_key)) {
62 return $this->error('missing_api_key', __('Missing API key in header.', 'templately'), 'ai-content/permission', 403);
63 }
64
65 $is_valid_key = $this->validate_api_key_in_db($header_api_key);
66 if (!$is_valid_key) {
67 return $this->error('invalid_api_key', __('Invalid API key provided in header.', 'templately'), 'ai-content/permission', 403);
68 }
69
70 // Check AI process data using API key-based storage
71 $ai_process_data = AIUtils::get_ai_process_data();
72 if (is_array($ai_process_data) && !empty($ai_process_data[$process_id])) {
73 return true;
74 }
75
76 return (bool) AIUtils::get_matched_session_data($process_id);
77 }
78
79 // // Allow access to attachments endpoint
80 // if ('/templately/v1/ai-content/attachments' === $_route) {
81 // return true;
82 // }
83 return parent::_permission_check($request);
84 }
85
86
87 public function register_routes() {
88 // $this->get( $this->endpoint . '/calculate-credit', [ $this, 'calculate_credit' ] );
89 $this->post($this->endpoint . '/modify-content', [$this, 'modify_content']);
90 $this->post($this->endpoint . '/ai-update', [$this, 'ai_update']);
91 $this->post($this->endpoint . '/ai-update-preview', [$this, 'ai_update_preview']);
92 $this->post($this->endpoint . '/generate-tagline', [$this, 'generate_tagline']);
93 $this->get($this->endpoint . '/chatbot-conversation', [$this, 'get_chatbot_conversation'], [
94 'chat' => [
95 'required' => true,
96 'sanitize_callback' => 'sanitize_text_field',
97 'validate_callback' => function($param, $request, $key) {
98 return is_string($param) && strlen($param) > 0 && strlen($param) <= 128 && preg_match('/^[A-Za-z0-9\-_]+$/', $param);
99 },
100 ],
101 ]);
102 $this->get($this->endpoint . '/attachments', [$this, 'get_attachments'], [
103 'type' => [
104 'default' => 'pack',
105 'required' => false,
106 'sanitize_callback' => 'sanitize_text_field',
107 ],
108 'id' => [
109 'required' => false,
110 'sanitize_callback' => 'sanitize_text_field',
111 ],
112 'pack_id' => [
113 'required' => false,
114 'sanitize_callback' => 'sanitize_text_field',
115 ],
116 ]);
117 $this->get($this->endpoint . '/images', [$this, 'search_images'], [
118 'query' => [
119 'required' => false,
120 'sanitize_callback' => 'sanitize_text_field',
121 'validate_callback' => function($param, $request, $key) {
122 return is_string($param) && strlen($param) <= 255;
123 },
124 ],
125 'orientation' => [
126 'required' => false,
127 'default' => 'all',
128 'sanitize_callback' => 'sanitize_text_field',
129 'validate_callback' => function($param, $request, $key) {
130 $allowed_orientations = ['all', 'landscape', 'portrait', 'square'];
131 return in_array($param, $allowed_orientations, true);
132 },
133 ],
134 'size' => [
135 'required' => false,
136 'default' => 'medium',
137 'sanitize_callback' => 'sanitize_text_field',
138 'validate_callback' => function($param, $request, $key) {
139 $allowed_sizes = ['small', 'medium', 'large'];
140 return in_array($param, $allowed_sizes, true);
141 },
142 ],
143 'color' => [
144 'required' => false,
145 'sanitize_callback' => 'sanitize_text_field',
146 'validate_callback' => function($param, $request, $key) {
147 return is_string($param) && strlen($param) <= 50;
148 },
149 ],
150 'page' => [
151 'required' => false,
152 'default' => 1,
153 'sanitize_callback' => 'absint',
154 'validate_callback' => function($param, $request, $key) {
155 return is_numeric($param) && $param > 0 && $param <= 1000;
156 },
157 ],
158 'per_page' => [
159 'required' => false,
160 'default' => 20,
161 'sanitize_callback' => 'absint',
162 'validate_callback' => function($param, $request, $key) {
163 return is_numeric($param) && $param > 0 && $param <= 100;
164 },
165 ],
166 ]);
167 // die(rest_url( 'templately/v1/ai-content/ai-update' ));
168 }
169
170 public function calculate_credit() {
171 $pack_id = $this->get_param('pack_id');
172
173 return [
174 'status' => 'success',
175 'data' => [
176 'availableCredit' => 100,
177 ],
178 ];
179
180 if (empty($pack_id)) {
181 return $this->error('invalid_id', __('Invalid ID.', 'templately'), 'calculate_credit', 400);
182 }
183
184 $extra_headers = [
185 'Accept' => 'application/json',
186 ];
187 $response = Helper::make_api_get_request("v2/ai/calculate-credit/pack/$pack_id", [], $extra_headers, 30);
188
189
190 // return $response;
191 if (is_wp_error($response)) {
192 return $this->error('request_failed', __('Request failed.', 'templately'), 'calculate_credit', 500, ['error_detail' => $response->get_error_message()]);
193 }
194
195 $body = wp_remote_retrieve_body($response);
196 $data = json_decode($body, true);
197 // error status is ok
198 if (! is_array($data) || ! isset($data['status'])) {
199 return $this->error('invalid_response', __('Invalid response.', 'templately'), 'calculate_credit', 500);
200 }
201
202
203 return $data;
204 }
205
206 public function modify_content() {
207 add_filter('wp_redirect', '__return_false', 999);
208 set_time_limit(3 * MINUTE_IN_SECONDS);
209 ini_set('max_execution_time', 3 * MINUTE_IN_SECONDS);
210
211 $pack_id = $this->get_param('pack_id');
212 $isBusinessNichesNew = $this->get_param('isBusinessNichesNew', false);
213 $ai_page_ids = $this->get_param('ai_page_ids', [], null);
214 $content_ids = $this->get_param('content_ids', [], null);
215 $session_id = $this->get_param('session_id'); // Add session_id parameter
216
217 // Security: Sanitize session_id if provided
218 if (!empty($session_id)) {
219 $session_id = AIUtils::sanitize_path_component($session_id, 'session_id');
220 if (is_wp_error($session_id)) {
221 return $session_id;
222 }
223 }
224
225 $preview_pages = $this->get_param('preview_pages', [], null);
226 $image_replace = $this->get_param('imageReplace', [], null);
227 $platform = $this->get_param('platform');
228 $language = $this->get_param('language', null);
229
230 // ai content fields
231 $name = $this->get_param('name');
232 $category = $this->get_param('category');
233 $description = $this->get_param('description');
234 $email = $this->get_param('email');
235 $contactNumber = $this->get_param('contactNumber');
236 $businessAddress = $this->get_param('businessAddress');
237 $openingHour = $this->get_param('openingHour');
238 $requested_platform = $this->get_param('requested_platform', 'templately');
239
240 if (empty($pack_id)) {
241 return $this->error('invalid_id', __('Invalid ID.', 'templately'), 'modify_content', 400);
242 }
243 if (empty($category)) {
244 return $this->error('invalid_prompt', __('Invalid prompt.', 'templately'), 'modify_content', 400);
245 }
246 if (empty($content_ids) && empty($preview_pages)) {
247 return $this->error('invalid_content_ids', __('Invalid content ids.', 'templately'), 'modify_content', 400);
248 }
249 if (empty($platform)) {
250 return $this->error('invalid_platform', __('Invalid platform.', 'templately'), 'modify_content', 400);
251 }
252
253
254 // $response = get_transient( '__templately_ai_process_id' );
255
256 // if(empty($response)) {
257 $extra_headers = [
258 'Accept' => 'application/json',
259 'x-templately-session-id' => $session_id,
260 'x-templately-requested-platform' => $requested_platform,
261 ];
262 $body_data = [
263 'business_name' => $name,
264 'business_niches' => $category,
265 'prompt' => $description,
266 'email' => $email,
267 'phone' => $contactNumber,
268 'address' => $businessAddress,
269 'openingHour' => $openingHour,
270 'pack_id' => $pack_id,
271 'content_ids' => $content_ids,
272 'platform' => $platform,
273 'preview_pages' => $preview_pages,
274 'language' => $language,
275 'callback' => defined('TEMPLATELY_CALLBACK') ? TEMPLATELY_CALLBACK . '/wp-json/templately/v1/ai-content/ai-update' : rest_url('templately/v1/ai-content/ai-update'),
276 ];
277 /**
278 * Filter body data before making API request to modify-content endpoint
279 *
280 * @since 3.5.0
281 * @param array $body_data The request body data
282 * @param WP_REST_Request $request The REST request object
283 */
284 $body_data = apply_filters( 'templately_ai_modify_content_body_data', $body_data, $this->request );
285
286 $response = Helper::make_api_post_request('v2/ai/modify-content/pack', $body_data, $extra_headers, 15 * MINUTE_IN_SECONDS);
287
288 // set_transient( '__templately_ai_process_id', $response, 60 * 60 * 24 * 30 );
289 // }
290
291 $bk_ai_business_niches = get_option('templately_ai_business_niches', []);
292 if (!empty($business_niches) && $isBusinessNichesNew && ! in_array($business_niches, $bk_ai_business_niches)) {
293 $bk_ai_business_niches[] = $business_niches;
294 update_option('templately_ai_business_niches', $bk_ai_business_niches, false);
295 }
296
297 // return $response;
298 if (is_wp_error($response)) {
299 error_log(print_r($response, true));
300 return $this->error('request_failed', __('Request failed.', 'templately'), 'modify_content', 500, ['error_data' => $response->get_error_data()]);
301 }
302
303 $body = wp_remote_retrieve_body($response);
304 $data = json_decode($body, true);
305 // error status is ok, if status is error then return as is
306 if (! is_array($data) || ! isset($data['status'])) {
307 return $this->error('invalid_response', __('Invalid response.', 'templately'), 'modify_content', 500, ['data' => $data]);
308 }
309
310 // "{"status":"success","message":"The content is being generated in the queue","process_id":"01JRQQD39GNWTNF18EWF8YH0BG-271838-pack-408"}"
311 if (isset($data['status']) && $data['status'] === 'success' && isset($data['process_id'])) {
312 $process_id = $data['process_id'];
313
314 // // Save templates to files if available using the common function
315 // if (!empty($data['templates']) && is_array($data['templates'])) {
316 // foreach ($data['templates'] as $content_id => $template_data) {
317 // // Decode template if it's base64 encoded
318 // if (! empty($template_data) && base64_decode($template_data, true) !== false) {
319 // $data['templates'][$content_id] = base64_decode($template_data);
320 // }
321
322 // if (!empty($template_data)) {
323 // AIUtils::save_template_to_file(
324 // $process_id,
325 // $content_id,
326 // $template_data,
327 // $ai_page_ids,
328 // true, // Always use preview mode for AI content workflow
329 // isset($template_data['isSkipped']) ? $template_data['isSkipped'] : false
330 // );
331 // }
332 // }
333 // }
334
335 $user = $this->utils('options')->get('user');
336
337 $ai_process_data[$process_id] = [
338 'name' => $name,
339 'category' => $category,
340 'description' => $description,
341 'email' => $email,
342 'contactNumber' => $contactNumber,
343 'businessAddress' => $businessAddress,
344 'openingHour' => $openingHour,
345 'process_id' => $process_id,
346 'pack_id' => $pack_id,
347 'ai_page_ids' => $ai_page_ids,
348 'ai_preview_ids' => $preview_pages,
349 'content_ids' => $content_ids,
350 'platform' => $platform,
351 'api_key' => $this->api_key,
352 'user_id' => isset($user['id']) ? $user['id'] : null,
353 'session_id' => $session_id,
354 'imageReplace' => $image_replace,
355 'language' => $language,
356 'requested_platform' => $requested_platform,
357 ];
358
359 // Update using API key-based storage with automatic count-based cleanup
360 AIUtils::update_ai_process_data($ai_process_data);
361
362 return [
363 'status' => 'success',
364 'message' => __('The content is being generated in the queue', 'templately'),
365 'process_id' => $process_id,
366 'templates' => !empty($data['templates']) ? $data['templates'] : null,
367 'is_local_site' => !empty($data['is_local_site']) ? $data['is_local_site'] : null,
368 ];
369 }
370
371 return $data;
372 }
373
374 public function ai_update() {
375 add_filter('wp_redirect', '__return_false', 999);
376
377 $template = $this->get_param('template');
378 $process_id = $this->get_param('process_id');
379 $template_id = $this->get_param('template_id');
380 $content_id = $this->get_param('content_id');
381 $type = $this->get_param('type');
382 $isSkipped = $this->get_param('isSkipped', false);
383 $credit_cost = $this->request->get_param('credit_cost');
384
385 error_log('process_id: ' . $process_id);
386
387 // Handle credit cost updates separately
388 if ($this->request->has_param('credit_cost')) {
389 $processed_pages = get_option("templately_ai_processed_pages", []);
390 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
391 $processed_pages[$process_id]['credit_cost'] = $credit_cost;
392 update_option("templately_ai_processed_pages", $processed_pages, false);
393
394 return [
395 'status' => 'success',
396 'data' => [
397 'process_id' => $process_id,
398 'credit_cost' => $credit_cost,
399 ],
400 ];
401 }
402
403 // Always use preview mode for AI content workflow
404 // Validate and get process data using centralized method
405 $process_data = AIUtils::validate_and_get_process_data($process_id);
406 if (is_wp_error($process_data)) {
407 return $process_data;
408 }
409
410 $session_id = $process_data['session_id'];
411 $ai_page_ids = $process_data['ai_page_ids'];
412
413 // Use the common helper function to save the template
414 $result = AIUtils::save_template_to_file(
415 $process_id,
416 $session_id,
417 $content_id,
418 $template,
419 $ai_page_ids,
420 $isSkipped
421 );
422
423 if(is_wp_error($result)){
424 return $result;
425 }
426
427 // Return the result from the helper function
428 if (isset($result['status']) && $result['status'] === 'success') {
429 return $result;
430 }
431
432 // Return error if the helper function failed
433 return $result;
434 }
435
436 public function ai_update_preview() {
437 add_filter('wp_redirect', '__return_false', 999);
438
439 $template = $this->get_param('templates'); // Now expects an array with content_id as keys
440 $process_id = $this->get_param('process_id');
441 $isSkipped = $this->get_param('isSkipped', false);
442 $error = $this->get_param('error', null);
443
444 error_log('process_id: ' . $process_id);
445
446 if (!empty($isSkipped) || !empty($error)) {
447 // Update AI process data with error using API key-based storage
448 $ai_process_data = AIUtils::get_ai_process_data();
449 if (isset($ai_process_data[$process_id])) {
450 $ai_process_data[$process_id]['preview_error'] = $error;
451 AIUtils::update_ai_process_data($ai_process_data);
452 }
453 wp_send_json_error([
454 'status' => 'error',
455 'message' => $error,
456 ]);
457 }
458
459 // Validate template parameter is an array
460 if (!is_array($template) || empty($template)) {
461 return $this->error('invalid_template', __('Template must be a non-empty array with content_id as keys.', 'templately'), 'ai-content/ai-update-preview', 400);
462 }
463
464 // Always use preview mode for AI content workflow
465 // Validate and get process data using centralized method
466 $process_data = AIUtils::validate_and_get_process_data($process_id);
467 if (is_wp_error($process_data)) {
468 return $process_data;
469 }
470
471 $session_id = $process_data['session_id'];
472 $ai_page_ids = $process_data['ai_page_ids'];
473 $results = [];
474 $success_count = 0;
475 $error_count = 0;
476
477 // Process each content_id/template pair
478 foreach ($template as $content_id => $template_data) {
479 // Use the common helper function to save the template (always preview mode)
480 $result = AIUtils::save_template_to_file(
481 $process_id,
482 $session_id,
483 $content_id,
484 $template_data,
485 $ai_page_ids,
486 $isSkipped
487 );
488
489 $results[$content_id] = $result;
490
491 // Track success/error counts
492 if (isset($result['status']) && $result['status'] === 'success') {
493 $success_count++;
494 } else {
495 $error_count++;
496 }
497 }
498
499 // Return consolidated response
500 $overall_status = $error_count === 0 ? 'success' : ($success_count === 0 ? 'error' : 'partial_success');
501
502 // Note: No cleanup needed with API key-based storage and count-based management
503
504 return [
505 'status' => $overall_status,
506 'message' => sprintf(
507 __('Processed %d templates: %d successful, %d failed.', 'templately'),
508 count($template),
509 $success_count,
510 $error_count
511 ),
512 ];
513 }
514
515
516
517 /**
518 * Get attachments from API endpoint
519 *
520 * @return array|\WP_Error
521 */
522 public function get_attachments() {
523 // Get parameters from request
524 $type = $this->get_param('type', 'pack');
525 $id = $this->get_param('pack_id');
526 $requested_platform = $this->get_param('requested_platform', 'templately');
527
528 // Require ID parameter - return error if not provided
529 if (empty($id)) {
530 return $this->error('missing_id', __('Pack ID or ID parameter is required.', 'templately'), 'get_attachments', 400);
531 }
532
533 try {
534 // Construct API endpoint URL
535 $api_endpoint = "get-xml-attachment/{$type}/{$id}";
536
537 // Make API call
538 $extra_headers = [
539 'Accept' => 'application/xml, text/xml',
540 'x-templately-requested-platform' => $requested_platform,
541 ];
542 $response = Helper::make_api_get_request("v2/$api_endpoint", [], $extra_headers, 30);
543
544 // Check for HTTP errors
545 if (is_wp_error($response)) {
546 return $this->error('api_request_failed', __('Failed to fetch attachments from API.', 'templately'), 'get_attachments', 500, ['error_detail' => $response->get_error_message()]);
547 }
548
549 $response_code = wp_remote_retrieve_response_code($response);
550 $xml_content = wp_remote_retrieve_body($response);
551
552 if ($response_code !== 200) {
553 // check if $xml_content contains valid json
554 // ex. '{"status":"error","message":"Attachment XML file not found in pack archive."}'
555 $error_data = @json_decode($xml_content, true);
556 if(is_array($error_data) && isset($error_data['status']) && $error_data['status'] === 'error' && !empty($error_data['message'])){
557 return $this->error('api_http_error', $error_data['message'], 'get_attachments', $response_code);
558 }
559 return $this->error('api_http_error', sprintf(__('API returned HTTP %d error.', 'templately'), $response_code), 'get_attachments', $response_code);
560 }
561
562 // Validate we have XML content
563 if (empty($xml_content)) {
564 return $this->error('no_xml_content', __('No XML content found in API response.', 'templately'), 'get_attachments', 404);
565 }
566
567 // Parse the XML content from API response
568 $parsed_data = $this->parse_xml_content($xml_content);
569
570 if (is_wp_error($parsed_data)) {
571 return $this->error('xml_parse_error', __('Failed to parse XML content.', 'templately'), 'get_attachments', 500, ['error_detail' => $parsed_data->get_error_message()]);
572 }
573
574 // Extract attachments from parsed data
575 $attachments = $this->extract_attachments_from_parsed_data($parsed_data);
576
577 return [
578 'status' => 'success',
579 'data' => $attachments,
580 'message' => sprintf(__('Found %d attachments.', 'templately'), count($attachments)),
581 ];
582
583 } catch (Exception $e) {
584 return $this->error('exception', __('An unexpected error occurred while fetching attachments.', 'templately'), 'get_attachments', 500, ['error_detail' => $e->getMessage()]);
585 }
586 }
587
588
589
590 /**
591 * Parse XML content string using WXR Parser
592 *
593 * @param string $xml_content XML content string
594 * @return array|\WP_Error Parsed data or error
595 */
596 private function parse_xml_content($xml_content) {
597 // Ensure WordPress filesystem functions are available
598 if (!function_exists('wp_tempnam')) {
599 require_once(ABSPATH . 'wp-admin/includes/file.php');
600 }
601
602 // Create a temporary file to store XML content
603 $temp_file = wp_tempnam('templately_attachments');
604 if (!$temp_file) {
605 return new WP_Error('temp_file_failed', __('Failed to create temporary file.', 'templately'));
606 }
607
608 // Write XML content to temporary file
609 $bytes_written = file_put_contents($temp_file, $xml_content);
610 if ($bytes_written === false) {
611 unlink($temp_file);
612 return new WP_Error('write_failed', __('Failed to write XML content to temporary file.', 'templately'));
613 }
614
615 try {
616 // Initialize WXR Parser
617 $parser = new WXR_Parser();
618
619 // Parse the temporary XML file
620 $parsed_data = $parser->parse($temp_file);
621
622 // Clean up temporary file
623 unlink($temp_file);
624
625 return $parsed_data;
626
627 } catch (Exception $e) {
628 // Clean up temporary file on exception
629 if (file_exists($temp_file)) {
630 unlink($temp_file);
631 }
632 return new WP_Error('parse_exception', $e->getMessage());
633 }
634 }
635
636 /**
637 * Extract attachments from parsed WXR data
638 *
639 * @param array $parsed_data Parsed WXR data
640 * @return array Array of attachment data
641 */
642 private function extract_attachments_from_parsed_data($parsed_data) {
643 $attachments = [];
644
645 if (isset($parsed_data['posts']) && is_array($parsed_data['posts'])) {
646 foreach ($parsed_data['posts'] as $post) {
647 // Check if this is an attachment
648 if (isset($post['post_type']) && $post['post_type'] === 'attachment') {
649 $attachment = [
650 'id' => isset($post['post_id']) ? (int) $post['post_id'] : 0,
651 'url' => isset($post['attachment_url']) ? (string) $post['attachment_url'] : '',
652 'title' => isset($post['post_title']) ? (string) $post['post_title'] : '',
653 'type' => isset($post['attachment_type']) ? (string) $post['attachment_type'] : '',
654 ];
655
656 // Extract metadata including dimensions and medium URL
657 $metadata = $this->extract_medium_size_url($post, $attachment['url']);
658
659 // Filter out small images (width or height <= 150px) to ignore small icons
660 if ($metadata && isset($metadata['width']) && isset($metadata['height'])) {
661 if ($metadata['width'] < 150 || $metadata['height'] < 150) {
662 continue; // Skip small images/icons
663 }
664
665 // Add dimensions to attachment data
666 $attachment['width'] = $metadata['width'];
667 $attachment['height'] = $metadata['height'];
668
669 // Add medium URL if available
670 if (isset($metadata['medium_url'])) {
671 $attachment['medium_url'] = $metadata['medium_url'];
672 }
673 } else {
674 // Skip attachments without metadata or dimensions
675 continue;
676 }
677
678 // Only add if we have the required data
679 if ($attachment['id'] && $attachment['url'] && $attachment['title']) {
680 $attachments[] = $attachment;
681 }
682 }
683 }
684 }
685
686 return $attachments;
687 }
688
689 /**
690 * Extract medium size URL from attachment metadata and get image dimensions
691 *
692 * @param array $post Post data from WXR parser
693 * @param string $original_url Original attachment URL
694 * @return array|null Array with medium_url and dimensions if found, null otherwise
695 */
696 private function extract_medium_size_url($post, $original_url) {
697 if (!isset($post['postmeta']) || !is_array($post['postmeta'])) {
698 return null;
699 }
700
701 foreach ($post['postmeta'] as $meta) {
702 if (!isset($meta['key']) || !isset($meta['value'])) {
703 continue;
704 }
705
706 // Only check _wp_attachment_metadata
707 if ($meta['key'] === '_wp_attachment_metadata') {
708 $attachment_metadata = @unserialize($meta['value']);
709 if (is_array($attachment_metadata)) {
710 $result = [];
711
712 // Get original image dimensions
713 $width = isset($attachment_metadata['width']) ? (int) $attachment_metadata['width'] : 0;
714 $height = isset($attachment_metadata['height']) ? (int) $attachment_metadata['height'] : 0;
715
716 $result['width'] = $width;
717 $result['height'] = $height;
718
719 // Check if medium size exists
720 if (isset($attachment_metadata['sizes']['medium']['file'])) {
721 // Construct medium URL from original URL and medium filename
722 $medium_filename = $attachment_metadata['sizes']['medium']['file'];
723 $original_path = dirname(parse_url($original_url, PHP_URL_PATH));
724 $base_url = str_replace(parse_url($original_url, PHP_URL_PATH), '', $original_url);
725 $result['medium_url'] = $base_url . $original_path . '/' . $medium_filename;
726 }
727
728 return $result;
729 }
730 }
731 }
732
733 return null;
734 }
735
736
737
738 /**
739 * Search images endpoint
740 *
741 * @param WP_REST_Request $request
742 * @return WP_REST_Response|WP_Error
743 */
744 public function search_images(WP_REST_Request $request) {
745 // Get and sanitize parameters
746 $query = $this->get_param('query', '');
747 $orientation = $this->get_param('orientation', 'all');
748 $size = $this->get_param('size', 'medium');
749 $color = $this->get_param('color', '');
750 $page = $this->get_param('page', 1, 'absint');
751 $per_page = $this->get_param('per_page', 20, 'absint');
752
753 // Validate required query parameter
754 if (empty($query)) {
755 return $this->error(
756 'missing_query',
757 __('Search query is required.', 'templately'),
758 'search_images',
759 400
760 );
761 }
762
763 // Prepare API request parameters
764 $api_params = [
765 'query' => urlencode($query),
766 'page' => $page,
767 'per_page' => $per_page,
768 ];
769
770 // Add optional parameters if provided
771 if ($orientation !== 'all') {
772 $api_params['orientation'] = $orientation;
773 }
774
775 if (!empty($size)) {
776 $api_params['size'] = $size;
777 }
778
779 if (!empty($color)) {
780 $api_params['color'] = $color;
781 }
782
783 // Make API request to external image service
784 $extra_headers = [
785 'Content-Type' => 'application/json',
786 ];
787
788 $response = Helper::make_api_get_request('v2/images', $api_params, $extra_headers, 30);
789
790 // Handle API response errors
791 if (is_wp_error($response)) {
792 return $this->error(
793 'api_request_failed',
794 __('Failed to fetch images from external service.', 'templately'),
795 'search_images',
796 500
797 );
798 }
799
800 $response_code = wp_remote_retrieve_response_code($response);
801 $response_body = wp_remote_retrieve_body($response);
802
803 if ($response_code !== 200) {
804 return $this->error(
805 'api_response_error',
806 sprintf(__('External API returned error code: %d', 'templately'), $response_code),
807 'search_images',
808 $response_code
809 );
810 }
811
812 // Parse and validate response
813 $data = json_decode($response_body, true);
814 if (json_last_error() !== JSON_ERROR_NONE) {
815 return $this->error(
816 'invalid_response',
817 __('Invalid response from external service.', 'templately'),
818 'search_images',
819 500
820 );
821 }
822
823 // Check if the response has the expected structure and success status
824 if (!isset($data['status']) || $data['status'] !== 'success') {
825 return $this->error(
826 'api_response_error',
827 __('External API returned an error status.', 'templately'),
828 'search_images',
829 500
830 );
831 }
832
833 // Extract nested data from the response
834 $response_data = $data['data'] ?? [];
835 $images = $response_data['images'] ?? [];
836 $total_results = $response_data['total_results'] ?? 0;
837 $current_page = $response_data['page'] ?? $page;
838 $per_page_count = $response_data['per_page'] ?? $per_page;
839
840 // Return successful response with properly mapped data
841 return $this->success([
842 'images' => $images,
843 'total' => $total_results,
844 'page' => $current_page,
845 'per_page' => $per_page_count,
846 'total_pages' => $total_results > 0 ? ceil($total_results / $per_page_count) : 0,
847 ]);
848 }
849
850
851
852 /**
853 * Generate tagline using AI
854 *
855 * @return array|WP_Error
856 */
857 public function generate_tagline() {
858 // Get parameters
859 $prompt = $this->get_param('prompt');
860 $requested_platform = $this->get_param('requested_platform', 'templately');
861
862 // Validate required parameters
863 if (empty($prompt)) {
864 return $this->error(
865 'missing_prompt',
866 __('Prompt is required for tagline generation.', 'templately'),
867 'generate_tagline',
868 400
869 );
870 }
871
872 // Prepare request body
873 $body_data = [
874 'prompt' => $prompt,
875 ];
876
877 // Make API request
878 $extra_headers = [
879 'Content-Type' => 'application/json',
880 'x-templately-requested-platform' => $requested_platform,
881 ];
882
883 $response = Helper::make_api_post_request('v2/generate-tagline', $body_data, $extra_headers, 30);
884
885 // Handle API response errors
886 if (is_wp_error($response)) {
887 return $this->error(
888 'api_request_failed',
889 __('Failed to generate tagline.', 'templately'),
890 'generate_tagline',
891 500,
892 ['error_detail' => $response->get_error_message()]
893 );
894 }
895
896 $response_code = wp_remote_retrieve_response_code($response);
897 $response_body = wp_remote_retrieve_body($response);
898
899 if ($response_code !== 200) {
900 // Try to parse the response body as JSON to get specific error details
901 $data = json_decode($response_body, true);
902
903 // If valid JSON, extract error message and return with proper status code
904 if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
905 $error_message = isset($data['message']) ? $data['message'] : __('Something went wrong. Please try again or contact support.', 'templately');
906 return $this->error(
907 'api_response_error',
908 $error_message,
909 'generate_tagline',
910 $response_code
911 );
912 }
913
914 // Otherwise, return generic error
915 return $this->error(
916 'api_response_error',
917 __('Something went wrong. Please try again or contact support.', 'templately'),
918 'generate_tagline',
919 $response_code
920 );
921 }
922
923 // Parse and validate response
924 $data = json_decode($response_body, true);
925 if (json_last_error() !== JSON_ERROR_NONE) {
926 return $this->error(
927 'invalid_response',
928 __('Invalid response from API.', 'templately'),
929 'generate_tagline',
930 500
931 );
932 }
933
934 // Check if the response has the expected structure
935 if (!isset($data['status'])) {
936 return $this->error(
937 'api_response_error',
938 __('API returned an unexpected response.', 'templately'),
939 'generate_tagline',
940 500
941 );
942 }
943
944 // Return the response as-is
945 return $data;
946 }
947
948 /**
949 * Fetch a chatbot conversation by ID from the external Templately chatbot API.
950 *
951 * Used to resume a conversation that began on templately.dev when the user
952 * is redirected into the plugin with ?process=ai&chat={uuid}.
953 *
954 * @return array|\WP_Error Pass-through of the external response { status, data } or WP_Error.
955 */
956 public function get_chatbot_conversation() {
957 $chat = $this->get_param('chat');
958
959 if (empty($chat)) {
960 return $this->error('invalid_chat_id', __('Invalid conversation ID.', 'templately'), 'ai-content/chatbot-conversation', 400);
961 }
962
963 $extra_headers = [
964 'Accept' => 'application/json',
965 ];
966 $response = Helper::make_api_get_request("v2/chatbot/conversation/{$chat}", [], $extra_headers, 30);
967
968 if (is_wp_error($response)) {
969 return $this->error('request_failed', __('Failed to fetch conversation.', 'templately'), 'ai-content/chatbot-conversation', 500, ['error_detail' => $response->get_error_message()]);
970 }
971
972 $response_code = wp_remote_retrieve_response_code($response);
973 $body = wp_remote_retrieve_body($response);
974 $data = json_decode($body, true);
975
976 if ($response_code !== 200) {
977 $message = (is_array($data) && !empty($data['message'])) ? $data['message'] : sprintf(__('API returned HTTP %d error.', 'templately'), $response_code);
978 return $this->error('api_http_error', $message, 'ai-content/chatbot-conversation', $response_code);
979 }
980
981 if (!is_array($data) || !isset($data['status'])) {
982 return $this->error('invalid_response', __('Invalid response.', 'templately'), 'ai-content/chatbot-conversation', 500);
983 }
984
985 return $data;
986 }
987
988 /**
989 * Validate API key against database
990 * Checks if the provided API key exists for any user on the current site
991 * Handles both single-site and multisite WordPress installations
992 *
993 * @param string $api_key The API key to validate
994 * @return bool True if valid, false otherwise
995 */
996 private function validate_api_key_in_db($api_key) {
997 global $wpdb;
998
999 $api_key = sanitize_text_field($api_key);
1000
1001 if (empty($api_key)) {
1002 return false;
1003 }
1004
1005 $meta_key = '_templately_api_key';
1006
1007 // Handle multisite: key will have site prefix in multisite
1008 if (is_multisite()) {
1009 // get_user_option() uses the format: {$wpdb->base_prefix}{$blog_id}_{$meta_key}
1010 // For current blog, we need to check with the current blog prefix
1011 $blog_id = get_current_blog_id();
1012 $meta_key = $wpdb->get_blog_prefix($blog_id) . $meta_key;
1013 }
1014
1015 // Query to check if this API key exists for any user
1016 $query = $wpdb->prepare(
1017 "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1",
1018 $meta_key,
1019 $api_key
1020 );
1021
1022 $user_id = $wpdb->get_var($query);
1023
1024 return !empty($user_id);
1025 }
1026 }
1027