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

788 lines 24.9 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\Parsers\WXR_Parser;
20
21 class AIContent extends API {
22 private $endpoint = 'ai-content';
23 private $dev_mode = false;
24
25
26 /**
27 * AIContent constructor.
28 *
29 * @param string $file File path.
30 * @param array $settings Settings.
31 */
32 public function __construct() {
33
34 parent::__construct();
35
36 }
37
38 public function _permission_check(WP_REST_Request $request) {
39 $this->request = $request;
40 $this->api_key = $this->utils('options')->get( 'api_key' );
41 $process_id = $this->get_param('process_id');
42
43 $_route = $request->get_route();
44 if ('/templately/v1/ai-content/ai-update' === $_route || '/templately/v1/ai-content/ai-update-preview' === $_route) {
45 if (empty($process_id)) {
46 return $this->error('invalid_id', __('Invalid ID.', 'templately'), 'calculate_credit', 400);
47 }
48
49 // Check AI process data using API key-based storage
50 $ai_process_data = AIUtils::get_ai_process_data();
51 if (is_array($ai_process_data) && !empty($ai_process_data[$process_id])) {
52 return true;
53 }
54
55 return (bool) AIUtils::get_matched_session_data($process_id);
56 }
57
58 // // Allow access to attachments endpoint
59 // if ('/templately/v1/ai-content/attachments' === $_route) {
60 // return true;
61 // }
62
63 return parent::permission_check($request);
64 }
65
66
67 public function register_routes() {
68 // $this->get( $this->endpoint . '/calculate-credit', [ $this, 'calculate_credit' ] );
69 $this->post($this->endpoint . '/modify-content', [$this, 'modify_content']);
70 $this->post($this->endpoint . '/ai-update', [$this, 'ai_update']);
71 $this->post($this->endpoint . '/ai-update-preview', [$this, 'ai_update_preview']);
72 $this->get($this->endpoint . '/attachments', [$this, 'get_attachments'], [
73 'type' => [
74 'default' => 'pack',
75 'required' => false,
76 'sanitize_callback' => 'sanitize_text_field',
77 ],
78 'id' => [
79 'required' => false,
80 'sanitize_callback' => 'sanitize_text_field',
81 ],
82 'pack_id' => [
83 'required' => false,
84 'sanitize_callback' => 'sanitize_text_field',
85 ],
86 ]);
87 $this->get($this->endpoint . '/images', [$this, 'search_images'], [
88 'query' => [
89 'required' => false,
90 'sanitize_callback' => 'sanitize_text_field',
91 'validate_callback' => function($param, $request, $key) {
92 return is_string($param) && strlen($param) <= 255;
93 },
94 ],
95 'orientation' => [
96 'required' => false,
97 'default' => 'all',
98 'sanitize_callback' => 'sanitize_text_field',
99 'validate_callback' => function($param, $request, $key) {
100 $allowed_orientations = ['all', 'landscape', 'portrait', 'square'];
101 return in_array($param, $allowed_orientations, true);
102 },
103 ],
104 'size' => [
105 'required' => false,
106 'default' => 'medium',
107 'sanitize_callback' => 'sanitize_text_field',
108 'validate_callback' => function($param, $request, $key) {
109 $allowed_sizes = ['small', 'medium', 'large'];
110 return in_array($param, $allowed_sizes, true);
111 },
112 ],
113 'color' => [
114 'required' => false,
115 'sanitize_callback' => 'sanitize_text_field',
116 'validate_callback' => function($param, $request, $key) {
117 return is_string($param) && strlen($param) <= 50;
118 },
119 ],
120 'page' => [
121 'required' => false,
122 'default' => 1,
123 'sanitize_callback' => 'absint',
124 'validate_callback' => function($param, $request, $key) {
125 return is_numeric($param) && $param > 0 && $param <= 1000;
126 },
127 ],
128 'per_page' => [
129 'required' => false,
130 'default' => 20,
131 'sanitize_callback' => 'absint',
132 'validate_callback' => function($param, $request, $key) {
133 return is_numeric($param) && $param > 0 && $param <= 100;
134 },
135 ],
136 ]);
137 // die(rest_url( 'templately/v1/ai-content/ai-update' ));
138 }
139
140 public function calculate_credit() {
141 $pack_id = $this->get_param('pack_id');
142
143 return [
144 'status' => 'success',
145 'data' => [
146 'availableCredit' => 100,
147 ],
148 ];
149
150 if (empty($pack_id)) {
151 return $this->error('invalid_id', __('Invalid ID.', 'templately'), 'calculate_credit', 400);
152 }
153
154 $extra_headers = [
155 'Accept' => 'application/json',
156 ];
157 $response = Helper::make_api_get_request("v2/ai/calculate-credit/pack/$pack_id", [], $extra_headers, 30);
158
159
160 // return $response;
161 if (is_wp_error($response)) {
162 return $this->error('request_failed', __('Request failed.', 'templately'), 'calculate_credit', 500, $response);
163 }
164
165 $body = wp_remote_retrieve_body($response);
166 $data = json_decode($body, true);
167 // error status is ok
168 if (! is_array($data) || ! isset($data['status'])) {
169 return $this->error('invalid_response', __('Invalid response.', 'templately'), 'calculate_credit', 500);
170 }
171
172
173 return $data;
174 }
175
176 public function modify_content() {
177 add_filter('wp_redirect', '__return_false', 999);
178 set_time_limit(3 * MINUTE_IN_SECONDS);
179 ini_set('max_execution_time', 3 * MINUTE_IN_SECONDS);
180
181 $pack_id = $this->get_param('pack_id');
182 $isBusinessNichesNew = $this->get_param('isBusinessNichesNew', false);
183 $ai_page_ids = $this->get_param('ai_page_ids', [], null);
184 $content_ids = $this->get_param('content_ids', [], null);
185 $session_id = $this->get_param('session_id'); // Add session_id parameter
186 $preview_pages = $this->get_param('preview_pages', [], null);
187 $image_replace = $this->get_param('imageReplace', [], null);
188 $platform = $this->get_param('platform');
189
190 // ai content fields
191 $name = $this->get_param('name');
192 $category = $this->get_param('category');
193 $description = $this->get_param('description');
194 $email = $this->get_param('email');
195 $contactNumber = $this->get_param('contactNumber');
196 $businessAddress = $this->get_param('businessAddress');
197 $openingHour = $this->get_param('openingHour');
198
199 if (empty($pack_id)) {
200 return $this->error('invalid_id', __('Invalid ID.', 'templately'), 'modify_content', 400);
201 }
202 if (empty($category)) {
203 return $this->error('invalid_prompt', __('Invalid prompt.', 'templately'), 'modify_content', 400);
204 }
205 if (empty($content_ids) && empty($preview_pages)) {
206 return $this->error('invalid_content_ids', __('Invalid content ids.', 'templately'), 'modify_content', 400);
207 }
208 if (empty($platform)) {
209 return $this->error('invalid_platform', __('Invalid platform.', 'templately'), 'modify_content', 400);
210 }
211
212
213 // $response = get_transient( '__templately_ai_process_id' );
214
215 // if(empty($response)) {
216 $extra_headers = [
217 'Accept' => 'application/json',
218 'x-templately-session-id' => $session_id,
219 ];
220 $body_data = [
221 'business_name' => $name,
222 'business_niches' => $category,
223 'prompt' => $description,
224 'email' => $email,
225 'phone' => $contactNumber,
226 'address' => $businessAddress,
227 'openingHour' => $openingHour,
228 'pack_id' => $pack_id,
229 'content_ids' => $content_ids,
230 'platform' => $platform,
231 'preview_pages' => $preview_pages,
232 'callback' => defined('TEMPLATELY_CALLBACK') ? TEMPLATELY_CALLBACK . '/wp-json/templately/v1/ai-content/ai-update' : rest_url('templately/v1/ai-content/ai-update'),
233 ];
234 $response = Helper::make_api_post_request('v2/ai/modify-content/pack', $body_data, $extra_headers, 15 * MINUTE_IN_SECONDS);
235
236 // set_transient( '__templately_ai_process_id', $response, 60 * 60 * 24 * 30 );
237 // }
238
239 $bk_ai_business_niches = get_option('templately_ai_business_niches', []);
240 if (!empty($business_niches) && $isBusinessNichesNew && ! in_array($business_niches, $bk_ai_business_niches)) {
241 $bk_ai_business_niches[] = $business_niches;
242 update_option('templately_ai_business_niches', $bk_ai_business_niches, false);
243 }
244
245 // return $response;
246 if (is_wp_error($response)) {
247 error_log(print_r($response, true));
248 return $this->error('request_failed', __('Request failed.', 'templately'), 'modify_content', 500, $response->additional_data);
249 }
250
251 $body = wp_remote_retrieve_body($response);
252 $data = json_decode($body, true);
253 // error status is ok, if status is error then return as is
254 if (! is_array($data) || ! isset($data['status'])) {
255 return $this->error('invalid_response', __('Invalid response.', 'templately'), 'modify_content', 500, $data);
256 }
257
258 // "{"status":"success","message":"The content is being generated in the queue","process_id":"01JRQQD39GNWTNF18EWF8YH0BG-271838-pack-408"}"
259 if (isset($data['status']) && $data['status'] === 'success' && isset($data['process_id'])) {
260 $process_id = $data['process_id'];
261
262 // // Save templates to files if available using the common function
263 // if (!empty($data['templates']) && is_array($data['templates'])) {
264 // foreach ($data['templates'] as $content_id => $template_data) {
265 // // Decode template if it's base64 encoded
266 // if (! empty($template_data) && base64_decode($template_data, true) !== false) {
267 // $data['templates'][$content_id] = base64_decode($template_data);
268 // }
269
270 // if (!empty($template_data)) {
271 // AIUtils::save_template_to_file(
272 // $process_id,
273 // $content_id,
274 // $template_data,
275 // $ai_page_ids,
276 // true, // Always use preview mode for AI content workflow
277 // isset($template_data['isSkipped']) ? $template_data['isSkipped'] : false
278 // );
279 // }
280 // }
281 // }
282
283 $ai_process_data[$process_id] = [
284 'name' => $name,
285 'category' => $category,
286 'description' => $description,
287 'email' => $email,
288 'contactNumber' => $contactNumber,
289 'businessAddress' => $businessAddress,
290 'openingHour' => $openingHour,
291 'process_id' => $process_id,
292 'pack_id' => $pack_id,
293 'ai_page_ids' => $ai_page_ids,
294 'ai_preview_ids' => $preview_pages,
295 'content_ids' => $content_ids,
296 'platform' => $platform,
297 'api_key' => $this->api_key,
298 'session_id' => $session_id, // Store session_id for coordination
299 'imageReplace' => $image_replace, // Store session_id for coordination
300 ];
301
302 // Update using API key-based storage with automatic count-based cleanup
303 AIUtils::update_ai_process_data($ai_process_data);
304
305 return [
306 'status' => 'success',
307 'message' => __('The content is being generated in the queue', 'templately'),
308 'process_id' => $process_id,
309 'templates' => !empty($data['templates']) ? $data['templates'] : null,
310 'is_local_site' => !empty($data['is_local_site']) ? $data['is_local_site'] : null,
311 ];
312 }
313
314 return $data;
315 }
316
317 public function ai_update() {
318 add_filter('wp_redirect', '__return_false', 999);
319
320 $template = $this->get_param('template');
321 $process_id = $this->get_param('process_id');
322 $template_id = $this->get_param('template_id');
323 $content_id = $this->get_param('content_id');
324 $type = $this->get_param('type');
325 $isSkipped = $this->get_param('isSkipped', false);
326 $credit_cost = $this->request->get_param('credit_cost');
327
328 error_log('process_id: ' . $process_id);
329
330 // Handle credit cost updates separately
331 if ($this->request->has_param('credit_cost')) {
332 $processed_pages = get_option("templately_ai_processed_pages", []);
333 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
334 $processed_pages[$process_id]['credit_cost'] = $credit_cost;
335 update_option("templately_ai_processed_pages", $processed_pages, false);
336
337 return [
338 'status' => 'success',
339 'data' => [
340 'process_id' => $process_id,
341 'credit_cost' => $credit_cost,
342 ],
343 ];
344 }
345
346 // Always use preview mode for AI content workflow
347 // Validate and get process data using centralized method
348 $process_data = AIUtils::validate_and_get_process_data($process_id);
349 if (is_wp_error($process_data)) {
350 return $process_data;
351 }
352
353 $session_id = $process_data['session_id'];
354 $ai_page_ids = $process_data['ai_page_ids'];
355
356 // Use the common helper function to save the template
357 $result = AIUtils::save_template_to_file(
358 $process_id,
359 $session_id,
360 $content_id,
361 $template,
362 $ai_page_ids,
363 $isSkipped
364 );
365
366 if(is_wp_error($result)){
367 return $result;
368 }
369
370 // Return the result from the helper function
371 if (isset($result['status']) && $result['status'] === 'success') {
372 return $result;
373 }
374
375 // Return error if the helper function failed
376 return $result;
377 }
378
379 public function ai_update_preview() {
380 add_filter('wp_redirect', '__return_false', 999);
381
382 $template = $this->get_param('templates'); // Now expects an array with content_id as keys
383 $process_id = $this->get_param('process_id');
384 $isSkipped = $this->get_param('isSkipped', false);
385 $error = $this->get_param('error', null);
386
387 error_log('process_id: ' . $process_id);
388
389 if (!empty($isSkipped) || !empty($error)) {
390 // Update AI process data with error using API key-based storage
391 $ai_process_data = AIUtils::get_ai_process_data();
392 if (isset($ai_process_data[$process_id])) {
393 $ai_process_data[$process_id]['preview_error'] = $error;
394 AIUtils::update_ai_process_data($ai_process_data);
395 }
396 wp_send_json_error([
397 'status' => 'error',
398 'message' => $error,
399 ]);
400 }
401
402 // Validate template parameter is an array
403 if (!is_array($template) || empty($template)) {
404 return $this->error('invalid_template', __('Template must be a non-empty array with content_id as keys.', 'templately'), 'ai-content/ai-update-preview', 400);
405 }
406
407 // Always use preview mode for AI content workflow
408 // Validate and get process data using centralized method
409 $process_data = AIUtils::validate_and_get_process_data($process_id);
410 if (is_wp_error($process_data)) {
411 return $process_data;
412 }
413
414 $session_id = $process_data['session_id'];
415 $ai_page_ids = $process_data['ai_page_ids'];
416 $results = [];
417 $success_count = 0;
418 $error_count = 0;
419
420 // Process each content_id/template pair
421 foreach ($template as $content_id => $template_data) {
422 // Use the common helper function to save the template (always preview mode)
423 $result = AIUtils::save_template_to_file(
424 $process_id,
425 $session_id,
426 $content_id,
427 $template_data,
428 $ai_page_ids,
429 $isSkipped
430 );
431
432 $results[$content_id] = $result;
433
434 // Track success/error counts
435 if (isset($result['status']) && $result['status'] === 'success') {
436 $success_count++;
437 } else {
438 $error_count++;
439 }
440 }
441
442 // Return consolidated response
443 $overall_status = $error_count === 0 ? 'success' : ($success_count === 0 ? 'error' : 'partial_success');
444
445 // Note: No cleanup needed with API key-based storage and count-based management
446
447 return [
448 'status' => $overall_status,
449 'message' => sprintf(
450 __('Processed %d templates: %d successful, %d failed.', 'templately'),
451 count($template),
452 $success_count,
453 $error_count
454 ),
455 ];
456 }
457
458 /**
459 * Get attachments from API endpoint
460 *
461 * @return array
462 */
463 public function get_attachments() {
464 // Get parameters from request
465 $type = $this->get_param('type', 'pack');
466 $id = $this->get_param('pack_id');
467
468 // Require ID parameter - return error if not provided
469 if (empty($id)) {
470 return $this->error('missing_id', __('Pack ID or ID parameter is required.', 'templately'), 'get_attachments', 400);
471 }
472
473 try {
474 // Construct API endpoint URL
475 $api_endpoint = "get-xml-attachment/{$type}/{$id}";
476
477 // Make API call
478 $extra_headers = [
479 'Accept' => 'application/xml, text/xml',
480 ];
481 $response = Helper::make_api_get_request("v2/$api_endpoint", [], $extra_headers, 30);
482
483 // Check for HTTP errors
484 if (is_wp_error($response)) {
485 return $this->error('api_request_failed', __('Failed to fetch attachments from API.', 'templately'), 'get_attachments', 500, $response->get_error_message());
486 }
487
488 $response_code = wp_remote_retrieve_response_code($response);
489 $xml_content = wp_remote_retrieve_body($response);
490
491 if ($response_code !== 200) {
492 // check if $xml_content contains valid json
493 // ex. '{"status":"error","message":"Attachment XML file not found in pack archive."}'
494 $error_data = @json_decode($xml_content, true);
495 if(is_array($error_data) && isset($error_data['status']) && $error_data['status'] === 'error' && !empty($error_data['message'])){
496 return $this->error('api_http_error', $error_data['message'], 'get_attachments', $response_code);
497 }
498 return $this->error('api_http_error', sprintf(__('API returned HTTP %d error.', 'templately'), $response_code), 'get_attachments', $response_code);
499 }
500
501 // Validate we have XML content
502 if (empty($xml_content)) {
503 return $this->error('no_xml_content', __('No XML content found in API response.', 'templately'), 'get_attachments', 404);
504 }
505
506 // Parse the XML content from API response
507 $parsed_data = $this->parse_xml_content($xml_content);
508
509 if (is_wp_error($parsed_data)) {
510 return $this->error('xml_parse_error', __('Failed to parse XML content.', 'templately'), 'get_attachments', 500, $parsed_data->get_error_message());
511 }
512
513 // Extract attachments from parsed data
514 $attachments = $this->extract_attachments_from_parsed_data($parsed_data);
515
516 return [
517 'status' => 'success',
518 'data' => $attachments,
519 'message' => sprintf(__('Found %d attachments.', 'templately'), count($attachments)),
520 ];
521
522 } catch (Exception $e) {
523 return $this->error('exception', __('An unexpected error occurred while fetching attachments.', 'templately'), 'get_attachments', 500, $e->getMessage());
524 }
525 }
526
527
528
529 /**
530 * Parse XML content string using WXR Parser
531 *
532 * @param string $xml_content XML content string
533 * @return array|WP_Error Parsed data or error
534 */
535 private function parse_xml_content($xml_content) {
536 // Ensure WordPress filesystem functions are available
537 if (!function_exists('wp_tempnam')) {
538 require_once(ABSPATH . 'wp-admin/includes/file.php');
539 }
540
541 // Create a temporary file to store XML content
542 $temp_file = wp_tempnam('templately_attachments');
543 if (!$temp_file) {
544 return new WP_Error('temp_file_failed', __('Failed to create temporary file.', 'templately'));
545 }
546
547 // Write XML content to temporary file
548 $bytes_written = file_put_contents($temp_file, $xml_content);
549 if ($bytes_written === false) {
550 unlink($temp_file);
551 return new WP_Error('write_failed', __('Failed to write XML content to temporary file.', 'templately'));
552 }
553
554 try {
555 // Initialize WXR Parser
556 $parser = new WXR_Parser();
557
558 // Parse the temporary XML file
559 $parsed_data = $parser->parse($temp_file);
560
561 // Clean up temporary file
562 unlink($temp_file);
563
564 return $parsed_data;
565
566 } catch (Exception $e) {
567 // Clean up temporary file on exception
568 if (file_exists($temp_file)) {
569 unlink($temp_file);
570 }
571 return new WP_Error('parse_exception', $e->getMessage());
572 }
573 }
574
575 /**
576 * Extract attachments from parsed WXR data
577 *
578 * @param array $parsed_data Parsed WXR data
579 * @return array Array of attachment data
580 */
581 private function extract_attachments_from_parsed_data($parsed_data) {
582 $attachments = [];
583
584 if (isset($parsed_data['posts']) && is_array($parsed_data['posts'])) {
585 foreach ($parsed_data['posts'] as $post) {
586 // Check if this is an attachment
587 if (isset($post['post_type']) && $post['post_type'] === 'attachment') {
588 $attachment = [
589 'id' => isset($post['post_id']) ? (int) $post['post_id'] : 0,
590 'url' => isset($post['attachment_url']) ? (string) $post['attachment_url'] : '',
591 'title' => isset($post['post_title']) ? (string) $post['post_title'] : '',
592 ];
593
594 // Extract metadata including dimensions and medium URL
595 $metadata = $this->extract_medium_size_url($post, $attachment['url']);
596
597 // Filter out small images (width or height <= 150px) to ignore small icons
598 if ($metadata && isset($metadata['width']) && isset($metadata['height'])) {
599 if ($metadata['width'] < 150 || $metadata['height'] < 150) {
600 continue; // Skip small images/icons
601 }
602
603 // Add dimensions to attachment data
604 $attachment['width'] = $metadata['width'];
605 $attachment['height'] = $metadata['height'];
606
607 // Add medium URL if available
608 if (isset($metadata['medium_url'])) {
609 $attachment['medium_url'] = $metadata['medium_url'];
610 }
611 } else {
612 // Skip attachments without metadata or dimensions
613 continue;
614 }
615
616 // Only add if we have the required data
617 if ($attachment['id'] && $attachment['url'] && $attachment['title']) {
618 $attachments[] = $attachment;
619 }
620 }
621 }
622 }
623
624 return $attachments;
625 }
626
627 /**
628 * Extract medium size URL from attachment metadata and get image dimensions
629 *
630 * @param array $post Post data from WXR parser
631 * @param string $original_url Original attachment URL
632 * @return array|null Array with medium_url and dimensions if found, null otherwise
633 */
634 private function extract_medium_size_url($post, $original_url) {
635 if (!isset($post['postmeta']) || !is_array($post['postmeta'])) {
636 return null;
637 }
638
639 foreach ($post['postmeta'] as $meta) {
640 if (!isset($meta['key']) || !isset($meta['value'])) {
641 continue;
642 }
643
644 // Only check _wp_attachment_metadata
645 if ($meta['key'] === '_wp_attachment_metadata') {
646 $attachment_metadata = @unserialize($meta['value']);
647 if (is_array($attachment_metadata)) {
648 $result = [];
649
650 // Get original image dimensions
651 $width = isset($attachment_metadata['width']) ? (int) $attachment_metadata['width'] : 0;
652 $height = isset($attachment_metadata['height']) ? (int) $attachment_metadata['height'] : 0;
653
654 $result['width'] = $width;
655 $result['height'] = $height;
656
657 // Check if medium size exists
658 if (isset($attachment_metadata['sizes']['medium']['file'])) {
659 // Construct medium URL from original URL and medium filename
660 $medium_filename = $attachment_metadata['sizes']['medium']['file'];
661 $original_path = dirname(parse_url($original_url, PHP_URL_PATH));
662 $base_url = str_replace(parse_url($original_url, PHP_URL_PATH), '', $original_url);
663 $result['medium_url'] = $base_url . $original_path . '/' . $medium_filename;
664 }
665
666 return $result;
667 }
668 }
669 }
670
671 return null;
672 }
673
674
675
676 /**
677 * Search images endpoint
678 *
679 * @param WP_REST_Request $request
680 * @return WP_REST_Response|WP_Error
681 */
682 public function search_images(WP_REST_Request $request) {
683 // Get and sanitize parameters
684 $query = $this->get_param('query', '');
685 $orientation = $this->get_param('orientation', 'all');
686 $size = $this->get_param('size', 'medium');
687 $color = $this->get_param('color', '');
688 $page = $this->get_param('page', 1, 'absint');
689 $per_page = $this->get_param('per_page', 20, 'absint');
690
691 // Validate required query parameter
692 if (empty($query)) {
693 return $this->error(
694 'missing_query',
695 __('Search query is required.', 'templately'),
696 'search_images',
697 400
698 );
699 }
700
701 // Prepare API request parameters
702 $api_params = [
703 'query' => urlencode($query),
704 'page' => $page,
705 'per_page' => $per_page,
706 ];
707
708 // Add optional parameters if provided
709 if ($orientation !== 'all') {
710 $api_params['orientation'] = $orientation;
711 }
712
713 if (!empty($size)) {
714 $api_params['size'] = $size;
715 }
716
717 if (!empty($color)) {
718 $api_params['color'] = $color;
719 }
720
721 // Make API request to external image service
722 $extra_headers = [
723 'Content-Type' => 'application/json',
724 ];
725
726 $response = Helper::make_api_get_request('v2/images', $api_params, $extra_headers, 30);
727
728 // Handle API response errors
729 if (is_wp_error($response)) {
730 return $this->error(
731 'api_request_failed',
732 __('Failed to fetch images from external service.', 'templately'),
733 'search_images',
734 500
735 );
736 }
737
738 $response_code = wp_remote_retrieve_response_code($response);
739 $response_body = wp_remote_retrieve_body($response);
740
741 if ($response_code !== 200) {
742 return $this->error(
743 'api_response_error',
744 sprintf(__('External API returned error code: %d', 'templately'), $response_code),
745 'search_images',
746 $response_code
747 );
748 }
749
750 // Parse and validate response
751 $data = json_decode($response_body, true);
752 if (json_last_error() !== JSON_ERROR_NONE) {
753 return $this->error(
754 'invalid_response',
755 __('Invalid response from external service.', 'templately'),
756 'search_images',
757 500
758 );
759 }
760
761 // Check if the response has the expected structure and success status
762 if (!isset($data['status']) || $data['status'] !== 'success') {
763 return $this->error(
764 'api_response_error',
765 __('External API returned an error status.', 'templately'),
766 'search_images',
767 500
768 );
769 }
770
771 // Extract nested data from the response
772 $response_data = $data['data'] ?? [];
773 $images = $response_data['images'] ?? [];
774 $total_results = $response_data['total_results'] ?? 0;
775 $current_page = $response_data['page'] ?? $page;
776 $per_page_count = $response_data['per_page'] ?? $per_page;
777
778 // Return successful response with properly mapped data
779 return $this->success([
780 'images' => $images,
781 'total' => $total_results,
782 'page' => $current_page,
783 'per_page' => $per_page_count,
784 'total_pages' => $total_results > 0 ? ceil($total_results / $per_page_count) : 0,
785 ]);
786 }
787 }
788