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

978 lines 30.8 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,
345 'imageReplace' => $image_replace,
346 'language' => $language,
347 'requested_platform' => $requested_platform,
348 ];
349
350 // Update using API key-based storage with automatic count-based cleanup
351 AIUtils::update_ai_process_data($ai_process_data);
352
353 return [
354 'status' => 'success',
355 'message' => __('The content is being generated in the queue', 'templately'),
356 'process_id' => $process_id,
357 'templates' => !empty($data['templates']) ? $data['templates'] : null,
358 'is_local_site' => !empty($data['is_local_site']) ? $data['is_local_site'] : null,
359 ];
360 }
361
362 return $data;
363 }
364
365 public function ai_update() {
366 add_filter('wp_redirect', '__return_false', 999);
367
368 $template = $this->get_param('template');
369 $process_id = $this->get_param('process_id');
370 $template_id = $this->get_param('template_id');
371 $content_id = $this->get_param('content_id');
372 $type = $this->get_param('type');
373 $isSkipped = $this->get_param('isSkipped', false);
374 $credit_cost = $this->request->get_param('credit_cost');
375
376 error_log('process_id: ' . $process_id);
377
378 // Handle credit cost updates separately
379 if ($this->request->has_param('credit_cost')) {
380 $processed_pages = get_option("templately_ai_processed_pages", []);
381 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
382 $processed_pages[$process_id]['credit_cost'] = $credit_cost;
383 update_option("templately_ai_processed_pages", $processed_pages, false);
384
385 return [
386 'status' => 'success',
387 'data' => [
388 'process_id' => $process_id,
389 'credit_cost' => $credit_cost,
390 ],
391 ];
392 }
393
394 // Always use preview mode for AI content workflow
395 // Validate and get process data using centralized method
396 $process_data = AIUtils::validate_and_get_process_data($process_id);
397 if (is_wp_error($process_data)) {
398 return $process_data;
399 }
400
401 $session_id = $process_data['session_id'];
402 $ai_page_ids = $process_data['ai_page_ids'];
403
404 // Use the common helper function to save the template
405 $result = AIUtils::save_template_to_file(
406 $process_id,
407 $session_id,
408 $content_id,
409 $template,
410 $ai_page_ids,
411 $isSkipped
412 );
413
414 if(is_wp_error($result)){
415 return $result;
416 }
417
418 // Return the result from the helper function
419 if (isset($result['status']) && $result['status'] === 'success') {
420 return $result;
421 }
422
423 // Return error if the helper function failed
424 return $result;
425 }
426
427 public function ai_update_preview() {
428 add_filter('wp_redirect', '__return_false', 999);
429
430 $template = $this->get_param('templates'); // Now expects an array with content_id as keys
431 $process_id = $this->get_param('process_id');
432 $isSkipped = $this->get_param('isSkipped', false);
433 $error = $this->get_param('error', null);
434
435 error_log('process_id: ' . $process_id);
436
437 if (!empty($isSkipped) || !empty($error)) {
438 // Update AI process data with error using API key-based storage
439 $ai_process_data = AIUtils::get_ai_process_data();
440 if (isset($ai_process_data[$process_id])) {
441 $ai_process_data[$process_id]['preview_error'] = $error;
442 AIUtils::update_ai_process_data($ai_process_data);
443 }
444 wp_send_json_error([
445 'status' => 'error',
446 'message' => $error,
447 ]);
448 }
449
450 // Validate template parameter is an array
451 if (!is_array($template) || empty($template)) {
452 return $this->error('invalid_template', __('Template must be a non-empty array with content_id as keys.', 'templately'), 'ai-content/ai-update-preview', 400);
453 }
454
455 // Always use preview mode for AI content workflow
456 // Validate and get process data using centralized method
457 $process_data = AIUtils::validate_and_get_process_data($process_id);
458 if (is_wp_error($process_data)) {
459 return $process_data;
460 }
461
462 $session_id = $process_data['session_id'];
463 $ai_page_ids = $process_data['ai_page_ids'];
464 $results = [];
465 $success_count = 0;
466 $error_count = 0;
467
468 // Process each content_id/template pair
469 foreach ($template as $content_id => $template_data) {
470 // Use the common helper function to save the template (always preview mode)
471 $result = AIUtils::save_template_to_file(
472 $process_id,
473 $session_id,
474 $content_id,
475 $template_data,
476 $ai_page_ids,
477 $isSkipped
478 );
479
480 $results[$content_id] = $result;
481
482 // Track success/error counts
483 if (isset($result['status']) && $result['status'] === 'success') {
484 $success_count++;
485 } else {
486 $error_count++;
487 }
488 }
489
490 // Return consolidated response
491 $overall_status = $error_count === 0 ? 'success' : ($success_count === 0 ? 'error' : 'partial_success');
492
493 // Note: No cleanup needed with API key-based storage and count-based management
494
495 return [
496 'status' => $overall_status,
497 'message' => sprintf(
498 __('Processed %d templates: %d successful, %d failed.', 'templately'),
499 count($template),
500 $success_count,
501 $error_count
502 ),
503 ];
504 }
505
506
507
508 /**
509 * Get attachments from API endpoint
510 *
511 * @return array|\WP_Error
512 */
513 public function get_attachments() {
514 // Get parameters from request
515 $type = $this->get_param('type', 'pack');
516 $id = $this->get_param('pack_id');
517 $requested_platform = $this->get_param('requested_platform', 'templately');
518
519 // Require ID parameter - return error if not provided
520 if (empty($id)) {
521 return $this->error('missing_id', __('Pack ID or ID parameter is required.', 'templately'), 'get_attachments', 400);
522 }
523
524 try {
525 // Construct API endpoint URL
526 $api_endpoint = "get-xml-attachment/{$type}/{$id}";
527
528 // Make API call
529 $extra_headers = [
530 'Accept' => 'application/xml, text/xml',
531 'x-templately-requested-platform' => $requested_platform,
532 ];
533 $response = Helper::make_api_get_request("v2/$api_endpoint", [], $extra_headers, 30);
534
535 // Check for HTTP errors
536 if (is_wp_error($response)) {
537 return $this->error('api_request_failed', __('Failed to fetch attachments from API.', 'templately'), 'get_attachments', 500, ['error_detail' => $response->get_error_message()]);
538 }
539
540 $response_code = wp_remote_retrieve_response_code($response);
541 $xml_content = wp_remote_retrieve_body($response);
542
543 if ($response_code !== 200) {
544 // check if $xml_content contains valid json
545 // ex. '{"status":"error","message":"Attachment XML file not found in pack archive."}'
546 $error_data = @json_decode($xml_content, true);
547 if(is_array($error_data) && isset($error_data['status']) && $error_data['status'] === 'error' && !empty($error_data['message'])){
548 return $this->error('api_http_error', $error_data['message'], 'get_attachments', $response_code);
549 }
550 return $this->error('api_http_error', sprintf(__('API returned HTTP %d error.', 'templately'), $response_code), 'get_attachments', $response_code);
551 }
552
553 // Validate we have XML content
554 if (empty($xml_content)) {
555 return $this->error('no_xml_content', __('No XML content found in API response.', 'templately'), 'get_attachments', 404);
556 }
557
558 // Parse the XML content from API response
559 $parsed_data = $this->parse_xml_content($xml_content);
560
561 if (is_wp_error($parsed_data)) {
562 return $this->error('xml_parse_error', __('Failed to parse XML content.', 'templately'), 'get_attachments', 500, ['error_detail' => $parsed_data->get_error_message()]);
563 }
564
565 // Extract attachments from parsed data
566 $attachments = $this->extract_attachments_from_parsed_data($parsed_data);
567
568 return [
569 'status' => 'success',
570 'data' => $attachments,
571 'message' => sprintf(__('Found %d attachments.', 'templately'), count($attachments)),
572 ];
573
574 } catch (Exception $e) {
575 return $this->error('exception', __('An unexpected error occurred while fetching attachments.', 'templately'), 'get_attachments', 500, ['error_detail' => $e->getMessage()]);
576 }
577 }
578
579
580
581 /**
582 * Parse XML content string using WXR Parser
583 *
584 * @param string $xml_content XML content string
585 * @return array|\WP_Error Parsed data or error
586 */
587 private function parse_xml_content($xml_content) {
588 // Ensure WordPress filesystem functions are available
589 if (!function_exists('wp_tempnam')) {
590 require_once(ABSPATH . 'wp-admin/includes/file.php');
591 }
592
593 // Create a temporary file to store XML content
594 $temp_file = wp_tempnam('templately_attachments');
595 if (!$temp_file) {
596 return new WP_Error('temp_file_failed', __('Failed to create temporary file.', 'templately'));
597 }
598
599 // Write XML content to temporary file
600 $bytes_written = file_put_contents($temp_file, $xml_content);
601 if ($bytes_written === false) {
602 unlink($temp_file);
603 return new WP_Error('write_failed', __('Failed to write XML content to temporary file.', 'templately'));
604 }
605
606 try {
607 // Initialize WXR Parser
608 $parser = new WXR_Parser();
609
610 // Parse the temporary XML file
611 $parsed_data = $parser->parse($temp_file);
612
613 // Clean up temporary file
614 unlink($temp_file);
615
616 return $parsed_data;
617
618 } catch (Exception $e) {
619 // Clean up temporary file on exception
620 if (file_exists($temp_file)) {
621 unlink($temp_file);
622 }
623 return new WP_Error('parse_exception', $e->getMessage());
624 }
625 }
626
627 /**
628 * Extract attachments from parsed WXR data
629 *
630 * @param array $parsed_data Parsed WXR data
631 * @return array Array of attachment data
632 */
633 private function extract_attachments_from_parsed_data($parsed_data) {
634 $attachments = [];
635
636 if (isset($parsed_data['posts']) && is_array($parsed_data['posts'])) {
637 foreach ($parsed_data['posts'] as $post) {
638 // Check if this is an attachment
639 if (isset($post['post_type']) && $post['post_type'] === 'attachment') {
640 $attachment = [
641 'id' => isset($post['post_id']) ? (int) $post['post_id'] : 0,
642 'url' => isset($post['attachment_url']) ? (string) $post['attachment_url'] : '',
643 'title' => isset($post['post_title']) ? (string) $post['post_title'] : '',
644 'type' => isset($post['attachment_type']) ? (string) $post['attachment_type'] : '',
645 ];
646
647 // Extract metadata including dimensions and medium URL
648 $metadata = $this->extract_medium_size_url($post, $attachment['url']);
649
650 // Filter out small images (width or height <= 150px) to ignore small icons
651 if ($metadata && isset($metadata['width']) && isset($metadata['height'])) {
652 if ($metadata['width'] < 150 || $metadata['height'] < 150) {
653 continue; // Skip small images/icons
654 }
655
656 // Add dimensions to attachment data
657 $attachment['width'] = $metadata['width'];
658 $attachment['height'] = $metadata['height'];
659
660 // Add medium URL if available
661 if (isset($metadata['medium_url'])) {
662 $attachment['medium_url'] = $metadata['medium_url'];
663 }
664 } else {
665 // Skip attachments without metadata or dimensions
666 continue;
667 }
668
669 // Only add if we have the required data
670 if ($attachment['id'] && $attachment['url'] && $attachment['title']) {
671 $attachments[] = $attachment;
672 }
673 }
674 }
675 }
676
677 return $attachments;
678 }
679
680 /**
681 * Extract medium size URL from attachment metadata and get image dimensions
682 *
683 * @param array $post Post data from WXR parser
684 * @param string $original_url Original attachment URL
685 * @return array|null Array with medium_url and dimensions if found, null otherwise
686 */
687 private function extract_medium_size_url($post, $original_url) {
688 if (!isset($post['postmeta']) || !is_array($post['postmeta'])) {
689 return null;
690 }
691
692 foreach ($post['postmeta'] as $meta) {
693 if (!isset($meta['key']) || !isset($meta['value'])) {
694 continue;
695 }
696
697 // Only check _wp_attachment_metadata
698 if ($meta['key'] === '_wp_attachment_metadata') {
699 $attachment_metadata = @unserialize($meta['value']);
700 if (is_array($attachment_metadata)) {
701 $result = [];
702
703 // Get original image dimensions
704 $width = isset($attachment_metadata['width']) ? (int) $attachment_metadata['width'] : 0;
705 $height = isset($attachment_metadata['height']) ? (int) $attachment_metadata['height'] : 0;
706
707 $result['width'] = $width;
708 $result['height'] = $height;
709
710 // Check if medium size exists
711 if (isset($attachment_metadata['sizes']['medium']['file'])) {
712 // Construct medium URL from original URL and medium filename
713 $medium_filename = $attachment_metadata['sizes']['medium']['file'];
714 $original_path = dirname(parse_url($original_url, PHP_URL_PATH));
715 $base_url = str_replace(parse_url($original_url, PHP_URL_PATH), '', $original_url);
716 $result['medium_url'] = $base_url . $original_path . '/' . $medium_filename;
717 }
718
719 return $result;
720 }
721 }
722 }
723
724 return null;
725 }
726
727
728
729 /**
730 * Search images endpoint
731 *
732 * @param WP_REST_Request $request
733 * @return WP_REST_Response|WP_Error
734 */
735 public function search_images(WP_REST_Request $request) {
736 // Get and sanitize parameters
737 $query = $this->get_param('query', '');
738 $orientation = $this->get_param('orientation', 'all');
739 $size = $this->get_param('size', 'medium');
740 $color = $this->get_param('color', '');
741 $page = $this->get_param('page', 1, 'absint');
742 $per_page = $this->get_param('per_page', 20, 'absint');
743
744 // Validate required query parameter
745 if (empty($query)) {
746 return $this->error(
747 'missing_query',
748 __('Search query is required.', 'templately'),
749 'search_images',
750 400
751 );
752 }
753
754 // Prepare API request parameters
755 $api_params = [
756 'query' => urlencode($query),
757 'page' => $page,
758 'per_page' => $per_page,
759 ];
760
761 // Add optional parameters if provided
762 if ($orientation !== 'all') {
763 $api_params['orientation'] = $orientation;
764 }
765
766 if (!empty($size)) {
767 $api_params['size'] = $size;
768 }
769
770 if (!empty($color)) {
771 $api_params['color'] = $color;
772 }
773
774 // Make API request to external image service
775 $extra_headers = [
776 'Content-Type' => 'application/json',
777 ];
778
779 $response = Helper::make_api_get_request('v2/images', $api_params, $extra_headers, 30);
780
781 // Handle API response errors
782 if (is_wp_error($response)) {
783 return $this->error(
784 'api_request_failed',
785 __('Failed to fetch images from external service.', 'templately'),
786 'search_images',
787 500
788 );
789 }
790
791 $response_code = wp_remote_retrieve_response_code($response);
792 $response_body = wp_remote_retrieve_body($response);
793
794 if ($response_code !== 200) {
795 return $this->error(
796 'api_response_error',
797 sprintf(__('External API returned error code: %d', 'templately'), $response_code),
798 'search_images',
799 $response_code
800 );
801 }
802
803 // Parse and validate response
804 $data = json_decode($response_body, true);
805 if (json_last_error() !== JSON_ERROR_NONE) {
806 return $this->error(
807 'invalid_response',
808 __('Invalid response from external service.', 'templately'),
809 'search_images',
810 500
811 );
812 }
813
814 // Check if the response has the expected structure and success status
815 if (!isset($data['status']) || $data['status'] !== 'success') {
816 return $this->error(
817 'api_response_error',
818 __('External API returned an error status.', 'templately'),
819 'search_images',
820 500
821 );
822 }
823
824 // Extract nested data from the response
825 $response_data = $data['data'] ?? [];
826 $images = $response_data['images'] ?? [];
827 $total_results = $response_data['total_results'] ?? 0;
828 $current_page = $response_data['page'] ?? $page;
829 $per_page_count = $response_data['per_page'] ?? $per_page;
830
831 // Return successful response with properly mapped data
832 return $this->success([
833 'images' => $images,
834 'total' => $total_results,
835 'page' => $current_page,
836 'per_page' => $per_page_count,
837 'total_pages' => $total_results > 0 ? ceil($total_results / $per_page_count) : 0,
838 ]);
839 }
840
841
842
843 /**
844 * Generate tagline using AI
845 *
846 * @return array|WP_Error
847 */
848 public function generate_tagline() {
849 // Get parameters
850 $prompt = $this->get_param('prompt');
851 $requested_platform = $this->get_param('requested_platform', 'templately');
852
853 // Validate required parameters
854 if (empty($prompt)) {
855 return $this->error(
856 'missing_prompt',
857 __('Prompt is required for tagline generation.', 'templately'),
858 'generate_tagline',
859 400
860 );
861 }
862
863 // Prepare request body
864 $body_data = [
865 'prompt' => $prompt,
866 ];
867
868 // Make API request
869 $extra_headers = [
870 'Content-Type' => 'application/json',
871 'x-templately-requested-platform' => $requested_platform,
872 ];
873
874 $response = Helper::make_api_post_request('v2/generate-tagline', $body_data, $extra_headers, 30);
875
876 // Handle API response errors
877 if (is_wp_error($response)) {
878 return $this->error(
879 'api_request_failed',
880 __('Failed to generate tagline.', 'templately'),
881 'generate_tagline',
882 500,
883 ['error_detail' => $response->get_error_message()]
884 );
885 }
886
887 $response_code = wp_remote_retrieve_response_code($response);
888 $response_body = wp_remote_retrieve_body($response);
889
890 if ($response_code !== 200) {
891 // Try to parse the response body as JSON to get specific error details
892 $data = json_decode($response_body, true);
893
894 // If valid JSON, extract error message and return with proper status code
895 if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
896 $error_message = isset($data['message']) ? $data['message'] : __('Something went wrong. Please try again or contact support.', 'templately');
897 return $this->error(
898 'api_response_error',
899 $error_message,
900 'generate_tagline',
901 $response_code
902 );
903 }
904
905 // Otherwise, return generic error
906 return $this->error(
907 'api_response_error',
908 __('Something went wrong. Please try again or contact support.', 'templately'),
909 'generate_tagline',
910 $response_code
911 );
912 }
913
914 // Parse and validate response
915 $data = json_decode($response_body, true);
916 if (json_last_error() !== JSON_ERROR_NONE) {
917 return $this->error(
918 'invalid_response',
919 __('Invalid response from API.', 'templately'),
920 'generate_tagline',
921 500
922 );
923 }
924
925 // Check if the response has the expected structure
926 if (!isset($data['status'])) {
927 return $this->error(
928 'api_response_error',
929 __('API returned an unexpected response.', 'templately'),
930 'generate_tagline',
931 500
932 );
933 }
934
935 // Return the response as-is
936 return $data;
937 }
938
939 /**
940 * Validate API key against database
941 * Checks if the provided API key exists for any user on the current site
942 * Handles both single-site and multisite WordPress installations
943 *
944 * @param string $api_key The API key to validate
945 * @return bool True if valid, false otherwise
946 */
947 private function validate_api_key_in_db($api_key) {
948 global $wpdb;
949
950 $api_key = sanitize_text_field($api_key);
951
952 if (empty($api_key)) {
953 return false;
954 }
955
956 $meta_key = '_templately_api_key';
957
958 // Handle multisite: key will have site prefix in multisite
959 if (is_multisite()) {
960 // get_user_option() uses the format: {$wpdb->base_prefix}{$blog_id}_{$meta_key}
961 // For current blog, we need to check with the current blog prefix
962 $blog_id = get_current_blog_id();
963 $meta_key = $wpdb->get_blog_prefix($blog_id) . $meta_key;
964 }
965
966 // Query to check if this API key exists for any user
967 $query = $wpdb->prepare(
968 "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1",
969 $meta_key,
970 $api_key
971 );
972
973 $user_id = $wpdb->get_var($query);
974
975 return !empty($user_id);
976 }
977 }
978