File: /home/slfopp7cb1df/www/shaneconrad.me/wp-content/plugins/templately/includes/API/LogoGeneration.php
<?php
/**
* Templately Logo Generation API
*
* @package Templately
* @since 3.4.0
*/
namespace Templately\API;
use Templately\Utils\Helper;
use WP_REST_Request;
use Templately\Core\Importer\Utils\Utils;
use Templately\Core\Importer\Utils\AIUtils;
class LogoGeneration extends API {
private $endpoint = 'logo-generation';
/**
* LogoGeneration constructor.
*/
public function __construct() {
parent::__construct();
}
public function _permission_check( WP_REST_Request $request ) {
$this->request = $request;
$this->api_key = $this->utils( 'options' )->get( 'api_key' );
$process_id = $this->get_param( 'process_id' );
$_route = $request->get_route();
// Handle logo generation callback endpoint
if ( '/templately/v1/logo-generation/callback' === $_route ) {
Helper::log(
[
'headers' => $request->get_headers(),
'body' => $request->get_params(),
],
'logo_generation_callback_request'
);
if ( empty( $process_id ) ) {
return $this->error( 'invalid_id', __( 'Invalid ID.', 'templately' ), 'logo_generation_callback', 400 );
}
$header_api_key = sanitize_text_field( $request->get_header( 'x_templately_apikey' ) );
if ( empty( $header_api_key ) ) {
$header_api_key = sanitize_text_field( $request->get_header( 'X-Templately-Apikey' ) );
}
$is_valid_key = $this->validate_api_key_in_db( $header_api_key );
if ( ! $is_valid_key ) {
return $this->error( 'invalid_api_key', __( 'Invalid API key provided in header.', 'templately' ), 'logo-generation/permission', 403 );
}
if(defined('TEMPLATELY_DEV_API') && TEMPLATELY_DEV_API){
sleep(5);
}
// Check if process_id exists in logo generation processes
$logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
if ( isset( $logo_generation_processes[ $process_id ] ) ) {
return true;
}
Helper::log( 'Invalid or expired logo generation process ID.', 'logo_generation_callback' );
return $this->error( 'invalid_process_id', __( 'Invalid or expired logo generation process ID.', 'templately' ), 'logo_generation_callback', 400 );
}
return parent::_permission_check( $request );
}
public function register_routes() {
$this->post( $this->endpoint . '/generate', [ $this, 'generate_logo' ] );
$this->post( $this->endpoint . '/callback', [ $this, 'ai_update_logo_generation' ] );
$this->get( $this->endpoint . '/data', [ $this, 'get_logo_generation_data' ] );
$this->post( $this->endpoint . '/poll', [ $this, 'poll_logo_generation' ] );
$this->get( $this->endpoint . '/available-credits', [ $this, 'get_available_credits' ] );
}
/**
* Generate logo using AI - Async callback pattern
*
* @return array|\WP_Error
*/
public function generate_logo() {
// Get parameters
$business_name = $this->get_param( 'business_name', '' );
$description = $this->get_param( 'description' );
$quality = $this->get_param( 'quality', 'auto' );
$size = $this->get_param( 'size', 'auto' );
$output_format = $this->get_param( 'output_format', 'png' );
$category = $this->get_param( 'category', '' );
$quantity = $this->get_param( 'quantity', 1 );
$requested_platform = $this->get_param( 'requested_platform', 'templately' );
// Validate required parameters
if ( empty( $description ) ) {
return $this->error(
'missing_description',
__( 'Description is required for logo generation.', 'templately' ),
'generate_logo',
400
);
}
// Validate size parameter
$allowed_sizes = [ 'auto', '1024x1024', '1536x1024', '1024x1536' ];
if ( ! empty( $size ) && ! in_array( $size, $allowed_sizes, true ) ) {
return $this->error(
'invalid_size',
__( 'Invalid size parameter. Allowed values: auto, 1024x1024, 1536x1024, 1024x1536', 'templately' ),
'generate_logo',
400
);
}
// Validate quality parameter
$allowed_qualities = [ 'auto', 'high', 'medium', 'low' ];
if ( ! empty( $quality ) && ! in_array( $quality, $allowed_qualities, true ) ) {
return $this->error(
'invalid_quality',
__( 'Invalid quality parameter. Allowed values: auto, high, medium, low', 'templately' ),
'generate_logo',
400
);
}
// Validate output_format parameter
$allowed_formats = [ 'png', 'jpeg' ];
if ( ! empty( $output_format ) && ! in_array( $output_format, $allowed_formats, true ) ) {
return $this->error(
'invalid_output_format',
__( 'Invalid output format. Allowed values: png, jpeg', 'templately' ),
'generate_logo',
400
);
}
// Validate quantity parameter
if ( ! empty( $quantity ) && ( ! is_numeric( $quantity ) || $quantity < 1 || $quantity > 10 ) ) {
return $this->error(
'invalid_quantity',
__( 'Invalid quantity. Must be between 1 and 10', 'templately' ),
'generate_logo',
400
);
}
// Prepare request body
$body_data = [
'business_name' => $business_name,
'description' => $description,
'quality' => $quality,
'size' => $size,
'output_format' => $output_format,
'category' => $category,
'quantity' => (int) $quantity,
'call_back_url' => defined( 'TEMPLATELY_CALLBACK' ) ? TEMPLATELY_CALLBACK . '/wp-json/templately/v1/logo-generation/callback' : rest_url( 'templately/v1/logo-generation/callback' ),
];
// Make API request
$extra_headers = [
'Content-Type' => 'application/json',
'x-templately-requested-platform' => $requested_platform,
];
$response = Helper::make_api_post_request( 'v2/generate-logo', $body_data, $extra_headers, 60 );
// Handle API response errors
if ( is_wp_error( $response ) ) {
return $this->error(
'api_request_failed',
__( 'Something went wrong. Please try again or contact support.', 'templately' ),
'generate_logo',
500,
[ 'error_detail' => $response->get_error_message() ]
);
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
if ( 200 !== $response_code ) {
// Try to parse the response body as JSON to get specific error details
$data = json_decode( $response_body, true );
// If valid JSON, extract error message and return with proper status code
if ( JSON_ERROR_NONE === json_last_error() && is_array( $data ) ) {
$error_message = isset( $data['message'] ) ? $data['message'] : __( 'Something went wrong. Please try again or contact support.', 'templately' );
return $this->error(
'api_response_error',
$error_message,
'generate_logo',
$response_code
);
}
// Otherwise, return generic error
return $this->error(
'api_response_error',
__( 'Something went wrong. Please try again or contact support.', 'templately' ),
'generate_logo',
$response_code
);
}
// Parse and validate response
$data = json_decode( $response_body, true );
if ( JSON_ERROR_NONE !== json_last_error() ) {
return $this->error(
'invalid_response',
__( 'Invalid response from API.', 'templately' ),
'generate_logo',
500
);
}
// Check if the response has the expected structure
if ( ! isset( $data['status'] ) ) {
return $this->error(
'api_response_error',
__( 'API returned an unexpected response.', 'templately' ),
'generate_logo',
500
);
}
// Expected response format: { status: 'success', message: '...', statusCode: 200, process_id: '...', is_local_site: true/false }
if ( 'success' === $data['status'] && isset( $data['process_id'] ) ) {
$process_id = $data['process_id'];
// Save process_id in a global option for security validation in callback
$logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
$logo_generation_processes[ $process_id ] = [
'created_at' => time(),
'api_key' => $this->api_key,
];
update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
// Return the process_id to frontend for polling, including is_local_site flag
$response_data = [
'status' => 'success',
'message' => $data['message'] ?? __( 'Logo generation started', 'templately' ),
'process_id' => $process_id,
];
// Include is_local_site flag from API response if available
if ( isset( $data['is_local_site'] ) ) {
$response_data['is_local_site'] = (bool) $data['is_local_site'];
}
return $response_data;
}
// Return error response from API
return $data;
}
/**
* Handle logo generation callback from API
*
* This endpoint is called by the Templately API when logo generation is complete.
* It processes the generated images and stores them for frontend retrieval.
*
* @return array|\WP_Error
*/
public function ai_update_logo_generation() {
add_filter( 'wp_redirect', '__return_false', 999 );
$process_id = $this->get_param( 'process_id' );
$credit_cost = $this->request->get_param( 'credit_cost' );
$remaining_credit = $this->request->get_param( 'remaining_credit' );
if ( null === $remaining_credit ) {
$remaining_credit = $this->request->get_param( 'available_credit' );
}
error_log( 'Logo generation callback - process_id: ' . $process_id . ' remaining_credit: ' . ( $remaining_credit ?? 'null' ) );
// NOTE: Pass null as sanitizer to preserve base64-encoded image data in the images array
// The images array contains b64_json fields with base64-encoded image data
// Sanitization would corrupt the base64 encoding
$images = $this->get_param( 'images', [], null );
// Validate process_id exists in our saved processes
// Note: API key validation is already done in _permission_check() before this handler is called
$logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
if ( empty( $logo_generation_processes[ $process_id ] ) ) {
return $this->error(
'invalid_process_id',
__( 'Invalid or expired logo generation process ID.', 'templately' ),
'ai_update_logo_generation',
400
);
}
// Process logo images
$uploaded_images = [];
$has_errors = false;
if ( is_array( $images ) && ! empty( $images ) ) {
foreach ( $images as $index => $image_data ) {
if ( isset( $image_data['b64_json'] ) && ! empty( $image_data['b64_json'] ) ) {
$upload_result = Utils::upload_logo_base64( $image_data['b64_json'] );
if ( is_array( $upload_result ) && isset( $upload_result['error'] ) ) {
error_log( 'Logo upload error for image ' . $index . ': ' . $upload_result['error'] );
$has_errors = true;
} elseif ( is_array( $upload_result ) && isset( $upload_result['id'] ) ) {
$uploaded_images[] = [
'id' => $upload_result['id'],
'url' => $upload_result['url'],
];
}
}
}
}
// Store uploaded logo data for frontend retrieval
$logo_generation_data = get_option( 'templately_logo_generation_data', [] );
$logo_generation_data[ $process_id ] = [
'images' => $uploaded_images,
'credit_cost' => $credit_cost,
'remaining_credit' => $remaining_credit,
'completed_at' => time(),
];
update_option( 'templately_logo_generation_data', $logo_generation_data, false );
// Clean up the process from the processes list
unset( $logo_generation_processes[ $process_id ] );
update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
// Return success response
$response_data = [
'status' => 'success',
'message' => __( 'Logo generation completed successfully.', 'templately' ),
'data' => [
'process_id' => $process_id,
'images' => $uploaded_images,
],
];
if ( null !== $credit_cost ) {
$response_data['data']['credit_cost'] = $credit_cost;
}
if ( null !== $remaining_credit ) {
$response_data['data']['remaining_credit'] = $remaining_credit;
}
return $response_data;
}
/**
* Get logo generation data by process_id
*
* This endpoint is called by the frontend to retrieve completed logo generation data.
* For local sites, it will attempt to poll the API if data is not found locally.
* For production sites, the data is stored by the ai_update_logo_generation() callback endpoint.
*
* @return array|\WP_Error
*/
public function get_logo_generation_data() {
$process_id = $this->get_param( 'process_id' );
$is_local_site = $this->get_param( 'is_local_site', false );
// Get logo generation data
$logo_generation_data = get_option( 'templately_logo_generation_data', [] );
// If data exists locally, return it
if ( isset( $logo_generation_data[ $process_id ] ) ) {
$data = $logo_generation_data[ $process_id ];
return [
'status' => 'success',
'data' => $data,
];
}
// For local sites, attempt to poll the API if data not found
// This must be done BEFORE checking if process is active, to ensure we get fresh data
if ( $is_local_site ) {
// Use the polling helper function from AIUtils
$polling_result = AIUtils::poll_for_logo_generation( $process_id );
if ( ! is_wp_error( $polling_result ) ) {
// Extract logo data from polling result
$logo_data = $polling_result['data'] ?? [];
// Process and upload images if available
if ( ! empty( $logo_data['images'] ) && is_array( $logo_data['images'] ) ) {
$uploaded_images = [];
foreach ( $logo_data['images'] as $image ) {
// Upload image using the same method as ai_update_logo_generation
$upload_result = Utils::upload_logo_base64( $image['b64_json'] );
if ( ! is_wp_error( $upload_result ) ) {
$uploaded_images[] = $upload_result;
}
}
$logo_data['images'] = $uploaded_images;
}
// Store the polled data in the option for future retrieval
$logo_generation_data[ $process_id ] = $logo_data;
update_option( 'templately_logo_generation_data', $logo_generation_data, false );
// Clean up the process from the processes list
$logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
unset( $logo_generation_processes[ $process_id ] );
update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
// Return the polled data
return [
'status' => 'success',
'data' => $logo_data,
];
}
}
// Check if process is still active in the processes list
// For local sites, this is checked AFTER attempting API polling
// For production sites, this is checked early to avoid unnecessary API calls
$logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
if ( isset( $logo_generation_processes[ $process_id ] ) ) {
// Process is still running - return pending status to continue polling
return [
'status' => 'pending',
'message' => __( 'Logo generation in progress', 'templately' ),
];
}
// Data not found, process not active, and polling failed or not a local site
return $this->error(
'not_found',
__( 'Logo generation data not found.', 'templately' ),
'get_logo_generation_data',
404
);
}
/**
* Poll for logo generation status on local sites
*
* This endpoint is called by the frontend on local sites to poll for logo generation completion.
* It makes a GET request to the API endpoint and processes the results.
*
* @return array|\WP_Error
*/
public function poll_logo_generation() {
$process_id = $this->get_param( 'process_id' );
if ( empty( $process_id ) ) {
return $this->error(
'missing_process_id',
__( 'Process ID is required for logo polling.', 'templately' ),
'poll_logo_generation',
400
);
}
// Use the polling helper function from AIUtils
$polling_result = AIUtils::poll_for_logo_generation( $process_id );
if ( is_wp_error( $polling_result ) ) {
return $polling_result;
}
// Extract logo data from polling result
$logo_data = $polling_result['data'] ?? [];
// Process and upload images if available
if ( ! empty( $logo_data['images'] ) && is_array( $logo_data['images'] ) ) {
$uploaded_images = [];
foreach ( $logo_data['images'] as $image ) {
// Upload image using the same method as ai_update_logo_generation
$upload_result = Utils::upload_logo_base64( $image );
if ( ! is_wp_error( $upload_result ) ) {
$uploaded_images[] = $upload_result;
}
}
$logo_data['images'] = $uploaded_images;
}
// Store the polled data in the option for future retrieval
$logo_generation_data = get_option( 'templately_logo_generation_data', [] );
$logo_generation_data[ $process_id ] = $logo_data;
update_option( 'templately_logo_generation_data', $logo_generation_data, false );
// Clean up the process from the processes list
$logo_generation_processes = get_option( 'templately_logo_generation_processes', [] );
unset( $logo_generation_processes[ $process_id ] );
update_option( 'templately_logo_generation_processes', $logo_generation_processes, false );
// Return success response
$response_data = [
'status' => 'success',
'message' => __( 'Logo generation data retrieved successfully.', 'templately' ),
'data' => [
'process_id' => $process_id,
'images' => $logo_data['images'] ?? [],
],
];
if ( isset( $logo_data['credit_cost'] ) ) {
$response_data['data']['credit_cost'] = $logo_data['credit_cost'];
}
if ( isset( $logo_data['remaining_credit'] ) ) {
$response_data['data']['remaining_credit'] = $logo_data['remaining_credit'];
} elseif ( isset( $logo_data['available_credit'] ) ) {
$response_data['data']['remaining_credit'] = $logo_data['available_credit'];
}
return $response_data;
}
/**
* Get available credits for the user
*
* @return array
*/
public function get_available_credits() {
$response = Helper::make_api_get_request( 'v2/ai/available-credits' );
if ( is_wp_error( $response ) ) {
return [
'status' => 'error',
'message' => $response->get_error_message(),
'data' => [
'available_credit' => 0,
]
];
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$credits = isset( $body['data']['available_credit'] ) ? (int) $body['data']['available_credit'] : 0;
return [
'status' => 'success',
'data' => [
'available_credit' => $credits,
],
];
}
/**
* Validate API key against database
* Checks if the provided API key exists for any user on the current site
* Uses Options class for proper multisite handling
*
* @param string $api_key The API key to validate
* @return bool True if valid, false otherwise
*/
private function validate_api_key_in_db( $api_key ) {
$api_key = sanitize_text_field( $api_key );
if ( empty( $api_key ) ) {
return false;
}
// Get all users and check their API keys
$users = get_users( [ 'fields' => 'ID' ] );
$options = $this->utils( 'options' );
foreach ( $users as $user_id ) {
$stored_api_key = $options->get_user_meta( $user_id, '_templately_api_key', true );
if ( $stored_api_key === $api_key ) {
return true;
}
}
return false;
}
}