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

792 lines 25.1 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 $user = $this->utils('options')->get('user');
284
285 $ai_process_data[$process_id] = [
286 'name' => $name,
287 'category' => $category,
288 'description' => $description,
289 'email' => $email,
290 'contactNumber' => $contactNumber,
291 'businessAddress' => $businessAddress,
292 'openingHour' => $openingHour,
293 'process_id' => $process_id,
294 'pack_id' => $pack_id,
295 'ai_page_ids' => $ai_page_ids,
296 'ai_preview_ids' => $preview_pages,
297 'content_ids' => $content_ids,
298 'platform' => $platform,
299 'api_key' => $this->api_key,
300 'user_id' => isset($user['id']) ? $user['id'] : null,
301 'session_id' => $session_id, // Store session_id for coordination
302 'imageReplace' => $image_replace, // Store session_id for coordination
303 ];
304
305 // Update using API key-based storage with automatic count-based cleanup
306 AIUtils::update_ai_process_data($ai_process_data);
307
308 return [
309 'status' => 'success',
310 'message' => __('The content is being generated in the queue', 'templately'),
311 'process_id' => $process_id,
312 'templates' => !empty($data['templates']) ? $data['templates'] : null,
313 'is_local_site' => !empty($data['is_local_site']) ? $data['is_local_site'] : null,
314 ];
315 }
316
317 return $data;
318 }
319
320 public function ai_update() {
321 add_filter('wp_redirect', '__return_false', 999);
322
323 $template = $this->get_param('template');
324 $process_id = $this->get_param('process_id');
325 $template_id = $this->get_param('template_id');
326 $content_id = $this->get_param('content_id');
327 $type = $this->get_param('type');
328 $isSkipped = $this->get_param('isSkipped', false);
329 $credit_cost = $this->request->get_param('credit_cost');
330
331 error_log('process_id: ' . $process_id);
332
333 // Handle credit cost updates separately
334 if ($this->request->has_param('credit_cost')) {
335 $processed_pages = get_option("templately_ai_processed_pages", []);
336 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
337 $processed_pages[$process_id]['credit_cost'] = $credit_cost;
338 update_option("templately_ai_processed_pages", $processed_pages, false);
339
340 return [
341 'status' => 'success',
342 'data' => [
343 'process_id' => $process_id,
344 'credit_cost' => $credit_cost,
345 ],
346 ];
347 }
348
349 // Always use preview mode for AI content workflow
350 // Validate and get process data using centralized method
351 $process_data = AIUtils::validate_and_get_process_data($process_id);
352 if (is_wp_error($process_data)) {
353 return $process_data;
354 }
355
356 $session_id = $process_data['session_id'];
357 $ai_page_ids = $process_data['ai_page_ids'];
358
359 // Use the common helper function to save the template
360 $result = AIUtils::save_template_to_file(
361 $process_id,
362 $session_id,
363 $content_id,
364 $template,
365 $ai_page_ids,
366 $isSkipped
367 );
368
369 if(is_wp_error($result)){
370 return $result;
371 }
372
373 // Return the result from the helper function
374 if (isset($result['status']) && $result['status'] === 'success') {
375 return $result;
376 }
377
378 // Return error if the helper function failed
379 return $result;
380 }
381
382 public function ai_update_preview() {
383 add_filter('wp_redirect', '__return_false', 999);
384
385 $template = $this->get_param('templates'); // Now expects an array with content_id as keys
386 $process_id = $this->get_param('process_id');
387 $isSkipped = $this->get_param('isSkipped', false);
388 $error = $this->get_param('error', null);
389
390 error_log('process_id: ' . $process_id);
391
392 if (!empty($isSkipped) || !empty($error)) {
393 // Update AI process data with error using API key-based storage
394 $ai_process_data = AIUtils::get_ai_process_data();
395 if (isset($ai_process_data[$process_id])) {
396 $ai_process_data[$process_id]['preview_error'] = $error;
397 AIUtils::update_ai_process_data($ai_process_data);
398 }
399 wp_send_json_error([
400 'status' => 'error',
401 'message' => $error,
402 ]);
403 }
404
405 // Validate template parameter is an array
406 if (!is_array($template) || empty($template)) {
407 return $this->error('invalid_template', __('Template must be a non-empty array with content_id as keys.', 'templately'), 'ai-content/ai-update-preview', 400);
408 }
409
410 // Always use preview mode for AI content workflow
411 // Validate and get process data using centralized method
412 $process_data = AIUtils::validate_and_get_process_data($process_id);
413 if (is_wp_error($process_data)) {
414 return $process_data;
415 }
416
417 $session_id = $process_data['session_id'];
418 $ai_page_ids = $process_data['ai_page_ids'];
419 $results = [];
420 $success_count = 0;
421 $error_count = 0;
422
423 // Process each content_id/template pair
424 foreach ($template as $content_id => $template_data) {
425 // Use the common helper function to save the template (always preview mode)
426 $result = AIUtils::save_template_to_file(
427 $process_id,
428 $session_id,
429 $content_id,
430 $template_data,
431 $ai_page_ids,
432 $isSkipped
433 );
434
435 $results[$content_id] = $result;
436
437 // Track success/error counts
438 if (isset($result['status']) && $result['status'] === 'success') {
439 $success_count++;
440 } else {
441 $error_count++;
442 }
443 }
444
445 // Return consolidated response
446 $overall_status = $error_count === 0 ? 'success' : ($success_count === 0 ? 'error' : 'partial_success');
447
448 // Note: No cleanup needed with API key-based storage and count-based management
449
450 return [
451 'status' => $overall_status,
452 'message' => sprintf(
453 __('Processed %d templates: %d successful, %d failed.', 'templately'),
454 count($template),
455 $success_count,
456 $error_count
457 ),
458 ];
459 }
460
461 /**
462 * Get attachments from API endpoint
463 *
464 * @return array
465 */
466 public function get_attachments() {
467 // Get parameters from request
468 $type = $this->get_param('type', 'pack');
469 $id = $this->get_param('pack_id');
470
471 // Require ID parameter - return error if not provided
472 if (empty($id)) {
473 return $this->error('missing_id', __('Pack ID or ID parameter is required.', 'templately'), 'get_attachments', 400);
474 }
475
476 try {
477 // Construct API endpoint URL
478 $api_endpoint = "get-xml-attachment/{$type}/{$id}";
479
480 // Make API call
481 $extra_headers = [
482 'Accept' => 'application/xml, text/xml',
483 ];
484 $response = Helper::make_api_get_request("v2/$api_endpoint", [], $extra_headers, 30);
485
486 // Check for HTTP errors
487 if (is_wp_error($response)) {
488 return $this->error('api_request_failed', __('Failed to fetch attachments from API.', 'templately'), 'get_attachments', 500, $response->get_error_message());
489 }
490
491 $response_code = wp_remote_retrieve_response_code($response);
492 $xml_content = wp_remote_retrieve_body($response);
493
494 if ($response_code !== 200) {
495 // check if $xml_content contains valid json
496 // ex. '{"status":"error","message":"Attachment XML file not found in pack archive."}'
497 $error_data = @json_decode($xml_content, true);
498 if(is_array($error_data) && isset($error_data['status']) && $error_data['status'] === 'error' && !empty($error_data['message'])){
499 return $this->error('api_http_error', $error_data['message'], 'get_attachments', $response_code);
500 }
501 return $this->error('api_http_error', sprintf(__('API returned HTTP %d error.', 'templately'), $response_code), 'get_attachments', $response_code);
502 }
503
504 // Validate we have XML content
505 if (empty($xml_content)) {
506 return $this->error('no_xml_content', __('No XML content found in API response.', 'templately'), 'get_attachments', 404);
507 }
508
509 // Parse the XML content from API response
510 $parsed_data = $this->parse_xml_content($xml_content);
511
512 if (is_wp_error($parsed_data)) {
513 return $this->error('xml_parse_error', __('Failed to parse XML content.', 'templately'), 'get_attachments', 500, $parsed_data->get_error_message());
514 }
515
516 // Extract attachments from parsed data
517 $attachments = $this->extract_attachments_from_parsed_data($parsed_data);
518
519 return [
520 'status' => 'success',
521 'data' => $attachments,
522 'message' => sprintf(__('Found %d attachments.', 'templately'), count($attachments)),
523 ];
524
525 } catch (Exception $e) {
526 return $this->error('exception', __('An unexpected error occurred while fetching attachments.', 'templately'), 'get_attachments', 500, $e->getMessage());
527 }
528 }
529
530
531
532 /**
533 * Parse XML content string using WXR Parser
534 *
535 * @param string $xml_content XML content string
536 * @return array|WP_Error Parsed data or error
537 */
538 private function parse_xml_content($xml_content) {
539 // Ensure WordPress filesystem functions are available
540 if (!function_exists('wp_tempnam')) {
541 require_once(ABSPATH . 'wp-admin/includes/file.php');
542 }
543
544 // Create a temporary file to store XML content
545 $temp_file = wp_tempnam('templately_attachments');
546 if (!$temp_file) {
547 return new WP_Error('temp_file_failed', __('Failed to create temporary file.', 'templately'));
548 }
549
550 // Write XML content to temporary file
551 $bytes_written = file_put_contents($temp_file, $xml_content);
552 if ($bytes_written === false) {
553 unlink($temp_file);
554 return new WP_Error('write_failed', __('Failed to write XML content to temporary file.', 'templately'));
555 }
556
557 try {
558 // Initialize WXR Parser
559 $parser = new WXR_Parser();
560
561 // Parse the temporary XML file
562 $parsed_data = $parser->parse($temp_file);
563
564 // Clean up temporary file
565 unlink($temp_file);
566
567 return $parsed_data;
568
569 } catch (Exception $e) {
570 // Clean up temporary file on exception
571 if (file_exists($temp_file)) {
572 unlink($temp_file);
573 }
574 return new WP_Error('parse_exception', $e->getMessage());
575 }
576 }
577
578 /**
579 * Extract attachments from parsed WXR data
580 *
581 * @param array $parsed_data Parsed WXR data
582 * @return array Array of attachment data
583 */
584 private function extract_attachments_from_parsed_data($parsed_data) {
585 $attachments = [];
586
587 if (isset($parsed_data['posts']) && is_array($parsed_data['posts'])) {
588 foreach ($parsed_data['posts'] as $post) {
589 // Check if this is an attachment
590 if (isset($post['post_type']) && $post['post_type'] === 'attachment') {
591 $attachment = [
592 'id' => isset($post['post_id']) ? (int) $post['post_id'] : 0,
593 'url' => isset($post['attachment_url']) ? (string) $post['attachment_url'] : '',
594 'title' => isset($post['post_title']) ? (string) $post['post_title'] : '',
595 'type' => isset($post['attachment_type']) ? (string) $post['attachment_type'] : '',
596 ];
597
598 // Extract metadata including dimensions and medium URL
599 $metadata = $this->extract_medium_size_url($post, $attachment['url']);
600
601 // Filter out small images (width or height <= 150px) to ignore small icons
602 if ($metadata && isset($metadata['width']) && isset($metadata['height'])) {
603 if ($metadata['width'] < 150 || $metadata['height'] < 150) {
604 continue; // Skip small images/icons
605 }
606
607 // Add dimensions to attachment data
608 $attachment['width'] = $metadata['width'];
609 $attachment['height'] = $metadata['height'];
610
611 // Add medium URL if available
612 if (isset($metadata['medium_url'])) {
613 $attachment['medium_url'] = $metadata['medium_url'];
614 }
615 } else {
616 // Skip attachments without metadata or dimensions
617 continue;
618 }
619
620 // Only add if we have the required data
621 if ($attachment['id'] && $attachment['url'] && $attachment['title']) {
622 $attachments[] = $attachment;
623 }
624 }
625 }
626 }
627
628 return $attachments;
629 }
630
631 /**
632 * Extract medium size URL from attachment metadata and get image dimensions
633 *
634 * @param array $post Post data from WXR parser
635 * @param string $original_url Original attachment URL
636 * @return array|null Array with medium_url and dimensions if found, null otherwise
637 */
638 private function extract_medium_size_url($post, $original_url) {
639 if (!isset($post['postmeta']) || !is_array($post['postmeta'])) {
640 return null;
641 }
642
643 foreach ($post['postmeta'] as $meta) {
644 if (!isset($meta['key']) || !isset($meta['value'])) {
645 continue;
646 }
647
648 // Only check _wp_attachment_metadata
649 if ($meta['key'] === '_wp_attachment_metadata') {
650 $attachment_metadata = @unserialize($meta['value']);
651 if (is_array($attachment_metadata)) {
652 $result = [];
653
654 // Get original image dimensions
655 $width = isset($attachment_metadata['width']) ? (int) $attachment_metadata['width'] : 0;
656 $height = isset($attachment_metadata['height']) ? (int) $attachment_metadata['height'] : 0;
657
658 $result['width'] = $width;
659 $result['height'] = $height;
660
661 // Check if medium size exists
662 if (isset($attachment_metadata['sizes']['medium']['file'])) {
663 // Construct medium URL from original URL and medium filename
664 $medium_filename = $attachment_metadata['sizes']['medium']['file'];
665 $original_path = dirname(parse_url($original_url, PHP_URL_PATH));
666 $base_url = str_replace(parse_url($original_url, PHP_URL_PATH), '', $original_url);
667 $result['medium_url'] = $base_url . $original_path . '/' . $medium_filename;
668 }
669
670 return $result;
671 }
672 }
673 }
674
675 return null;
676 }
677
678
679
680 /**
681 * Search images endpoint
682 *
683 * @param WP_REST_Request $request
684 * @return WP_REST_Response|WP_Error
685 */
686 public function search_images(WP_REST_Request $request) {
687 // Get and sanitize parameters
688 $query = $this->get_param('query', '');
689 $orientation = $this->get_param('orientation', 'all');
690 $size = $this->get_param('size', 'medium');
691 $color = $this->get_param('color', '');
692 $page = $this->get_param('page', 1, 'absint');
693 $per_page = $this->get_param('per_page', 20, 'absint');
694
695 // Validate required query parameter
696 if (empty($query)) {
697 return $this->error(
698 'missing_query',
699 __('Search query is required.', 'templately'),
700 'search_images',
701 400
702 );
703 }
704
705 // Prepare API request parameters
706 $api_params = [
707 'query' => urlencode($query),
708 'page' => $page,
709 'per_page' => $per_page,
710 ];
711
712 // Add optional parameters if provided
713 if ($orientation !== 'all') {
714 $api_params['orientation'] = $orientation;
715 }
716
717 if (!empty($size)) {
718 $api_params['size'] = $size;
719 }
720
721 if (!empty($color)) {
722 $api_params['color'] = $color;
723 }
724
725 // Make API request to external image service
726 $extra_headers = [
727 'Content-Type' => 'application/json',
728 ];
729
730 $response = Helper::make_api_get_request('v2/images', $api_params, $extra_headers, 30);
731
732 // Handle API response errors
733 if (is_wp_error($response)) {
734 return $this->error(
735 'api_request_failed',
736 __('Failed to fetch images from external service.', 'templately'),
737 'search_images',
738 500
739 );
740 }
741
742 $response_code = wp_remote_retrieve_response_code($response);
743 $response_body = wp_remote_retrieve_body($response);
744
745 if ($response_code !== 200) {
746 return $this->error(
747 'api_response_error',
748 sprintf(__('External API returned error code: %d', 'templately'), $response_code),
749 'search_images',
750 $response_code
751 );
752 }
753
754 // Parse and validate response
755 $data = json_decode($response_body, true);
756 if (json_last_error() !== JSON_ERROR_NONE) {
757 return $this->error(
758 'invalid_response',
759 __('Invalid response from external service.', 'templately'),
760 'search_images',
761 500
762 );
763 }
764
765 // Check if the response has the expected structure and success status
766 if (!isset($data['status']) || $data['status'] !== 'success') {
767 return $this->error(
768 'api_response_error',
769 __('External API returned an error status.', 'templately'),
770 'search_images',
771 500
772 );
773 }
774
775 // Extract nested data from the response
776 $response_data = $data['data'] ?? [];
777 $images = $response_data['images'] ?? [];
778 $total_results = $response_data['total_results'] ?? 0;
779 $current_page = $response_data['page'] ?? $page;
780 $per_page_count = $response_data['per_page'] ?? $per_page;
781
782 // Return successful response with properly mapped data
783 return $this->success([
784 'images' => $images,
785 'total' => $total_results,
786 'page' => $current_page,
787 'per_page' => $per_page_count,
788 'total_pages' => $total_results > 0 ? ceil($total_results / $per_page_count) : 0,
789 ]);
790 }
791 }
792