PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.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 All 112 releases
templately / modules / template-customizer / REST / LogoGeneration.php

LogoGeneration.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/template-customizer/REST/LogoGeneration.php

571 lines 19.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 Logo Generation API
5 *
6 * @package Templately
7 * @since 3.4.0
8 */
9
10 namespace Templately\Modules\TemplateCustomizer\REST;
11
12 use Templately\API\API;
13 use Templately\Utils\Helper;
14 use Templately\Utils\Response\ErrorCode;
15 use WP_REST_Request;
16 use Templately\Modules\FullSiteImport\Utils\Utils;
17 use Templately\Modules\FullSiteImport\Utils\AIUtils;
18 use Templately\Modules\FullSiteImport\Utils\SignatureVerifier;
19
20 class LogoGeneration extends API {
21 private $endpoint = 'logo-generation';
22
23 /**
24 * LogoGeneration constructor.
25 */
26 public function __construct() {
27 parent::__construct();
28 }
29
30 public function _permission_check( WP_REST_Request $request ) {
31 $this->request = $request;
32 $this->api_key = $this->utils( 'options' )->get( 'api_key' );
33 $process_id = $this->get_param( 'process_id' );
34
35 $_route = $request->get_route();
36
37 // Handle logo generation callback endpoint
38 if ( '/templately/v1/logo-generation/callback' === $_route ) {
39 // Redact the API-key header before logging — a credential must NEVER
40 // reach the log, even under WP_DEBUG_LOG (Helper::log's guard). This
41 // route authenticates on `x_templately_apikey`, read a few lines below,
42 // so an unredacted dump writes the key that authorizes the very request
43 // being logged into debug.log, which is web-readable on many hosts.
44 // Same treatment as ai-content/ai-update.
45 $safe_headers = $request->get_headers();
46 foreach ( [ 'x_templately_apikey', 'x-templately-apikey' ] as $redact_key ) {
47 if ( isset( $safe_headers[ $redact_key ] ) ) {
48 $safe_headers[ $redact_key ] = [ '[redacted]' ];
49 }
50 }
51 Helper::log(
52 [
53 'headers' => $safe_headers,
54 'body' => $request->get_params(),
55 ],
56 'logo_generation_callback_request'
57 );
58
59 if ( empty( $process_id ) ) {
60 return $this->error( 'invalid_id', __( 'Invalid ID.', 'templately' ), 'logo_generation_callback', 400 );
61 }
62
63 $header_api_key = sanitize_text_field( $request->get_header( 'x_templately_apikey' ) );
64 if ( empty( $header_api_key ) ) {
65 $header_api_key = sanitize_text_field( $request->get_header( 'X-Templately-Apikey' ) );
66 }
67
68 $is_valid_key = $this->validate_api_key_in_db( $header_api_key );
69 if ( ! $is_valid_key ) {
70 return $this->error( 'invalid_api_key', __( 'Invalid API key provided in header.', 'templately' ), 'logo-generation/permission', 403 );
71 }
72
73 // Verify the callback HMAC signature (034 FR-001) — log-only by default;
74 // rejects only in enforce mode, once cloud-signed traffic is confirmed.
75 $verified = SignatureVerifier::verify_request( $request, $header_api_key, 'logo-generation/permission' );
76 if ( is_wp_error( $verified ) ) {
77 return $verified;
78 }
79
80 if(defined('TEMPLATELY_DEV_API') && TEMPLATELY_DEV_API){
81 sleep(5);
82 }
83
84 // Check if process_id exists in logo generation processes
85 $logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
86 if ( isset( $logo_generation_processes[ $process_id ] ) ) {
87 return true;
88 }
89
90 Helper::log( 'Invalid or expired logo generation process ID.', 'logo_generation_callback' );
91
92 return $this->error( 'invalid_process_id', __( 'Invalid or expired logo generation process ID.', 'templately' ), 'logo_generation_callback', 400 );
93 }
94
95 return parent::_permission_check( $request );
96 }
97
98 public function register_routes() {
99 $this->post( $this->endpoint . '/generate', [ $this, 'generate_logo' ] );
100 $this->post( $this->endpoint . '/callback', [ $this, 'ai_update_logo_generation' ] );
101 $this->get( $this->endpoint . '/data', [ $this, 'get_logo_generation_data' ] );
102 $this->post( $this->endpoint . '/poll', [ $this, 'poll_logo_generation' ] );
103 $this->get( $this->endpoint . '/available-credits', [ $this, 'get_available_credits' ] );
104 }
105
106 /**
107 * Generate logo using AI - Async callback pattern
108 *
109 * @return array|\WP_Error
110 */
111 public function generate_logo() {
112 // Get parameters
113 $business_name = $this->get_param( 'business_name', '' );
114 $description = $this->get_param( 'description' );
115 $quality = $this->get_param( 'quality', 'auto' );
116 $size = $this->get_param( 'size', 'auto' );
117 $output_format = $this->get_param( 'output_format', 'png' );
118 $category = $this->get_param( 'category', '' );
119 $quantity = $this->get_param( 'quantity', 1 );
120 $requested_platform = $this->get_param( 'requested_platform', 'templately' );
121
122 // Validate required parameters
123 if ( empty( $description ) ) {
124 return $this->error(
125 'missing_description',
126 __( 'Description is required for logo generation.', 'templately' ),
127 'generate_logo',
128 400
129 );
130 }
131
132 // Validate size parameter
133 $allowed_sizes = [ 'auto', '1024x1024', '1536x1024', '1024x1536' ];
134 if ( ! empty( $size ) && ! in_array( $size, $allowed_sizes, true ) ) {
135 return $this->error(
136 'invalid_size',
137 __( 'Invalid size parameter. Allowed values: auto, 1024x1024, 1536x1024, 1024x1536', 'templately' ),
138 'generate_logo',
139 400
140 );
141 }
142
143 // Validate quality parameter
144 $allowed_qualities = [ 'auto', 'high', 'medium', 'low' ];
145 if ( ! empty( $quality ) && ! in_array( $quality, $allowed_qualities, true ) ) {
146 return $this->error(
147 'invalid_quality',
148 __( 'Invalid quality parameter. Allowed values: auto, high, medium, low', 'templately' ),
149 'generate_logo',
150 400
151 );
152 }
153
154 // Validate output_format parameter
155 $allowed_formats = [ 'png', 'jpeg' ];
156 if ( ! empty( $output_format ) && ! in_array( $output_format, $allowed_formats, true ) ) {
157 return $this->error(
158 'invalid_output_format',
159 __( 'Invalid output format. Allowed values: png, jpeg', 'templately' ),
160 'generate_logo',
161 400
162 );
163 }
164
165 // Validate quantity parameter
166 if ( ! empty( $quantity ) && ( ! is_numeric( $quantity ) || $quantity < 1 || $quantity > 10 ) ) {
167 return $this->error(
168 'invalid_quantity',
169 __( 'Invalid quantity. Must be between 1 and 10', 'templately' ),
170 'generate_logo',
171 400
172 );
173 }
174
175 // Prepare request body
176 $body_data = [
177 'business_name' => $business_name,
178 'description' => $description,
179 'quality' => $quality,
180 'size' => $size,
181 'output_format' => $output_format,
182 'category' => $category,
183 'quantity' => (int) $quantity,
184 'call_back_url' => defined( 'TEMPLATELY_CALLBACK' ) ? TEMPLATELY_CALLBACK . '/wp-json/templately/v1/logo-generation/callback' : rest_url( 'templately/v1/logo-generation/callback' ),
185 ];
186
187 // Make API request
188 $extra_headers = [
189 'Content-Type' => 'application/json',
190 'x-templately-requested-platform' => $requested_platform,
191 ];
192
193 // 043 — one normalizer replaces the transport check, the status check, the
194 // JSON-parse check and the message extraction that used to live here.
195 $normalized = Helper::api_post( 'v2/generate-logo', $body_data, $extra_headers, 60 );
196
197 if ( $normalized->is_error() ) {
198 $error = $normalized->error();
199
200 return $this->error( $error->code(), $error->message(), 'generate_logo', $error->status() );
201 }
202
203 // The normalizer already guaranteed this is decoded JSON from a 2xx; what is
204 // left is the endpoint's own success contract.
205 $data = $normalized->payload();
206
207 if ( ! is_array( $data ) || ! isset( $data['status'] ) ) {
208 return $this->error(
209 ErrorCode::SERVER_ERROR,
210 __( 'API returned an unexpected response.', 'templately' ),
211 'generate_logo',
212 500
213 );
214 }
215
216 // Expected response format: { status: 'success', message: '...', statusCode: 200, process_id: '...', is_local_site: true/false }
217 if ( 'success' === $data['status'] && isset( $data['process_id'] ) ) {
218 $process_id = $data['process_id'];
219
220 // Save process_id in a global option for security validation in callback
221 $logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
222 $logo_generation_processes[ $process_id ] = [
223 'created_at' => time(),
224 'api_key' => $this->api_key,
225 ];
226 update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
227
228 // Return the process_id to frontend for polling, including is_local_site flag
229 $response_data = [
230 'status' => 'success',
231 'message' => $data['message'] ?? __( 'Logo generation started', 'templately' ),
232 'process_id' => $process_id,
233 ];
234
235 // Pass through ONLY what the remote API returns — no local Helper::is_local_site()
236 // fallback — so we can observe whether the API actually reports is_local_site.
237 $response_data['is_local_site'] = isset( $data['is_local_site'] )
238 ? (bool) $data['is_local_site']
239 : null;
240
241 return $response_data;
242 }
243
244 // Return error response from API
245 return $data;
246 }
247
248 /**
249 * Handle logo generation callback from API
250 *
251 * This endpoint is called by the Templately API when logo generation is complete.
252 * It processes the generated images and stores them for frontend retrieval.
253 *
254 * @return array|\WP_Error
255 */
256 public function ai_update_logo_generation() {
257 add_filter( 'wp_redirect', '__return_false', 999 );
258
259 $process_id = $this->get_param( 'process_id' );
260 $credit_cost = $this->request->get_param( 'credit_cost' );
261 $remaining_credit = $this->request->get_param( 'remaining_credit' );
262
263 if ( null === $remaining_credit ) {
264 $remaining_credit = $this->request->get_param( 'available_credit' );
265 }
266
267 Helper::log( 'Logo generation callback - process_id: ' . $process_id . ' remaining_credit: ' . ( $remaining_credit ?? 'null' ), 'logo_generation_callback' );
268 // NOTE: Pass null as sanitizer to preserve base64-encoded image data in the images array
269 // The images array contains b64_json fields with base64-encoded image data
270 // Sanitization would corrupt the base64 encoding
271 $images = $this->get_param( 'images', [], null );
272
273
274 // Validate process_id exists in our saved processes
275 // Note: API key validation is already done in _permission_check() before this handler is called
276 $logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
277 if ( empty( $logo_generation_processes[ $process_id ] ) ) {
278 return $this->error(
279 'invalid_process_id',
280 __( 'Invalid or expired logo generation process ID.', 'templately' ),
281 'ai_update_logo_generation',
282 400
283 );
284 }
285
286 // Process logo images
287 $uploaded_images = [];
288 $has_errors = false;
289
290 if ( is_array( $images ) && ! empty( $images ) ) {
291 foreach ( $images as $index => $image_data ) {
292 if ( isset( $image_data['b64_json'] ) && ! empty( $image_data['b64_json'] ) ) {
293 $upload_result = Utils::upload_logo_base64( $image_data['b64_json'] );
294
295 if ( is_array( $upload_result ) && isset( $upload_result['error'] ) ) {
296 Helper::log( 'Logo upload error for image ' . $index . ': ' . $upload_result['error'], 'logo_generation_callback', 'error' );
297 $has_errors = true;
298 } elseif ( is_array( $upload_result ) && isset( $upload_result['id'] ) ) {
299 $uploaded_images[] = [
300 'id' => $upload_result['id'],
301 'url' => $upload_result['url'],
302 ];
303 }
304 }
305 }
306 }
307
308 // Store uploaded logo data for frontend retrieval
309 $logo_generation_data = get_option( 'templately_logo_generation_data', [] );
310 $logo_generation_data[ $process_id ] = [
311 'images' => $uploaded_images,
312 'credit_cost' => $credit_cost,
313 'remaining_credit' => $remaining_credit,
314 'completed_at' => time(),
315 ];
316 update_option( 'templately_logo_generation_data', $logo_generation_data, false );
317
318 // Clean up the process from the processes list
319 unset( $logo_generation_processes[ $process_id ] );
320 update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
321
322 // Return success response
323 $response_data = [
324 'status' => 'success',
325 'message' => __( 'Logo generation completed successfully.', 'templately' ),
326 'data' => [
327 'process_id' => $process_id,
328 'images' => $uploaded_images,
329 ],
330 ];
331
332 if ( null !== $credit_cost ) {
333 $response_data['data']['credit_cost'] = $credit_cost;
334 }
335
336 if ( null !== $remaining_credit ) {
337 $response_data['data']['remaining_credit'] = $remaining_credit;
338 }
339
340 return $response_data;
341 }
342
343 /**
344 * Get logo generation data by process_id
345 *
346 * This endpoint is called by the frontend to retrieve completed logo generation data.
347 * For local sites, it will attempt to poll the API if data is not found locally.
348 * For production sites, the data is stored by the ai_update_logo_generation() callback endpoint.
349 *
350 * @return array|\WP_Error
351 */
352 public function get_logo_generation_data() {
353 $process_id = $this->get_param( 'process_id' );
354 $is_local_site = $this->get_param( 'is_local_site', false );
355
356 // Get logo generation data
357 $logo_generation_data = get_option( 'templately_logo_generation_data', [] );
358
359 // If data exists locally, return it
360 if ( isset( $logo_generation_data[ $process_id ] ) ) {
361 $data = $logo_generation_data[ $process_id ];
362
363 return [
364 'status' => 'success',
365 'data' => $data,
366 ];
367 }
368
369 // For local sites, attempt to poll the API if data not found
370 // This must be done BEFORE checking if process is active, to ensure we get fresh data
371 if ( $is_local_site ) {
372 // Use the polling helper function from AIUtils
373 $polling_result = AIUtils::poll_for_logo_generation( $process_id );
374
375 if ( ! is_wp_error( $polling_result ) ) {
376 // Extract logo data from polling result
377 $logo_data = $polling_result['data'] ?? [];
378
379 // Process and upload images if available
380 if ( ! empty( $logo_data['images'] ) && is_array( $logo_data['images'] ) ) {
381 $uploaded_images = [];
382
383 foreach ( $logo_data['images'] as $image ) {
384 // Upload image using the same method as ai_update_logo_generation
385 $upload_result = Utils::upload_logo_base64( $image['b64_json'] );
386
387 if ( ! is_wp_error( $upload_result ) ) {
388 $uploaded_images[] = $upload_result;
389 }
390 }
391
392 $logo_data['images'] = $uploaded_images;
393 }
394
395 // Store the polled data in the option for future retrieval
396 $logo_generation_data[ $process_id ] = $logo_data;
397 update_option( 'templately_logo_generation_data', $logo_generation_data, false );
398
399 // Clean up the process from the processes list
400 $logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
401 unset( $logo_generation_processes[ $process_id ] );
402 update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
403
404 // Return the polled data
405 return [
406 'status' => 'success',
407 'data' => $logo_data,
408 ];
409 }
410 }
411
412 // Check if process is still active in the processes list
413 // For local sites, this is checked AFTER attempting API polling
414 // For production sites, this is checked early to avoid unnecessary API calls
415 $logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
416 if ( isset( $logo_generation_processes[ $process_id ] ) ) {
417 // Process is still running - return pending status to continue polling
418 return [
419 'status' => 'pending',
420 'message' => __( 'Logo generation in progress', 'templately' ),
421 ];
422 }
423
424 // Data not found, process not active, and polling failed or not a local site
425 return $this->error(
426 'not_found',
427 __( 'Logo generation data not found.', 'templately' ),
428 'get_logo_generation_data',
429 404
430 );
431 }
432
433 /**
434 * Poll for logo generation status on local sites
435 *
436 * This endpoint is called by the frontend on local sites to poll for logo generation completion.
437 * It makes a GET request to the API endpoint and processes the results.
438 *
439 * @return array|\WP_Error
440 */
441 public function poll_logo_generation() {
442 $process_id = $this->get_param( 'process_id' );
443
444 if ( empty( $process_id ) ) {
445 return $this->error(
446 'missing_process_id',
447 __( 'Process ID is required for logo polling.', 'templately' ),
448 'poll_logo_generation',
449 400
450 );
451 }
452
453 // Use the polling helper function from AIUtils
454 $polling_result = AIUtils::poll_for_logo_generation( $process_id );
455
456 if ( is_wp_error( $polling_result ) ) {
457 return $polling_result;
458 }
459
460 // Extract logo data from polling result
461 $logo_data = $polling_result['data'] ?? [];
462
463 // Process and upload images if available
464 if ( ! empty( $logo_data['images'] ) && is_array( $logo_data['images'] ) ) {
465 $uploaded_images = [];
466
467 foreach ( $logo_data['images'] as $image ) {
468 // Upload image using the same method as ai_update_logo_generation
469 $upload_result = Utils::upload_logo_base64( $image );
470
471 if ( ! is_wp_error( $upload_result ) ) {
472 $uploaded_images[] = $upload_result;
473 }
474 }
475
476 $logo_data['images'] = $uploaded_images;
477 }
478
479 // Store the polled data in the option for future retrieval
480 $logo_generation_data = get_option( 'templately_logo_generation_data', [] );
481 $logo_generation_data[ $process_id ] = $logo_data;
482 update_option( 'templately_logo_generation_data', $logo_generation_data, false );
483
484 // Clean up the process from the processes list
485 $logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
486 unset( $logo_generation_processes[ $process_id ] );
487 update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
488
489 // Return success response
490 $response_data = [
491 'status' => 'success',
492 'message' => __( 'Logo generation data retrieved successfully.', 'templately' ),
493 'data' => [
494 'process_id' => $process_id,
495 'images' => $logo_data['images'] ?? [],
496 ],
497 ];
498
499 if ( isset( $logo_data['credit_cost'] ) ) {
500 $response_data['data']['credit_cost'] = $logo_data['credit_cost'];
501 }
502
503 if ( isset( $logo_data['remaining_credit'] ) ) {
504 $response_data['data']['remaining_credit'] = $logo_data['remaining_credit'];
505 } elseif ( isset( $logo_data['available_credit'] ) ) {
506 $response_data['data']['remaining_credit'] = $logo_data['available_credit'];
507 }
508
509 return $response_data;
510 }
511
512 /**
513 * Get available credits for the user
514 *
515 * @return array
516 */
517 public function get_available_credits() {
518 $response = Helper::make_api_get_request( 'v2/ai/available-credits' );
519
520 if ( is_wp_error( $response ) ) {
521 return [
522 'status' => 'error',
523 'message' => $response->get_error_message(),
524 'data' => [
525 'available_credit' => 0,
526 ]
527 ];
528 }
529
530 $body = json_decode( wp_remote_retrieve_body( $response ), true );
531 $credits = isset( $body['data']['available_credit'] ) ? (int) $body['data']['available_credit'] : 0;
532
533 return [
534 'status' => 'success',
535 'data' => [
536 'available_credit' => $credits,
537 ],
538 ];
539 }
540
541 /**
542 * Validate API key against database
543 * Checks if the provided API key exists for any user on the current site
544 * Uses Options class for proper multisite handling
545 *
546 * @param string $api_key The API key to validate
547 * @return bool True if valid, false otherwise
548 */
549 private function validate_api_key_in_db( $api_key ) {
550 $api_key = sanitize_text_field( $api_key );
551
552 if ( empty( $api_key ) ) {
553 return false;
554 }
555
556 // Get all users and check their API keys
557 $users = get_users( [ 'fields' => 'ID' ] );
558 $options = $this->utils( 'options' );
559
560 foreach ( $users as $user_id ) {
561 $stored_api_key = $options->get_user_meta( $user_id, '_templately_api_key', true );
562 if ( $stored_api_key === $api_key ) {
563 return true;
564 }
565 }
566
567 return false;
568 }
569 }
570
571