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

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