PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.5.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.5.2
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.5.2, at includes/API/AIContent.php

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