284 lines
9.0 KiB
PHP
284 lines
9.0 KiB
PHP
<?php
|
|
namespace AIOSEO\Plugin\Common\Ai;
|
|
|
|
// Exit if accessed directly.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* AI Image handler for managing WordPress attachments.
|
|
*
|
|
* @since 4.8.8
|
|
*/
|
|
class Image {
|
|
/**
|
|
* The hook name for generating image metadata.
|
|
*
|
|
* @since 4.8.8
|
|
*
|
|
* @var string
|
|
*/
|
|
private $generateImageMetadataHook = 'aioseo_generate_ai_image_metadata';
|
|
|
|
/**
|
|
* Class constructor.
|
|
*
|
|
* @since 4.8.8
|
|
*/
|
|
public function __construct() {
|
|
add_action( $this->generateImageMetadataHook, [ $this, 'generateImageMetadata' ], 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* Returns the data for Vue.
|
|
*
|
|
* @since 4.8.9
|
|
*
|
|
* @param int|null $objectId The object ID.
|
|
* @return array The data.
|
|
*/
|
|
public function getVueDataEdit( $objectId = null ) {
|
|
$objectId = $objectId ?: absint( get_the_ID() );
|
|
|
|
return [
|
|
'extend' => [
|
|
'imageBlockToolbar' => apply_filters( 'aioseo_ai_image_generator_extend_image_block_toolbar', true, $objectId ),
|
|
'imageBlockPlaceholder' => apply_filters( 'aioseo_ai_image_generator_extend_image_block_placeholder', true, $objectId ),
|
|
'featuredImageButton' => apply_filters( 'aioseo_ai_image_generator_extend_featured_image_button', true, $objectId ),
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Creates a WordPress attachment from base64 image data.
|
|
*
|
|
* @since 4.8.8
|
|
*
|
|
* @param string $base64Data The base64 encoded image data.
|
|
* @param string $prompt The AI prompt used to generate the image.
|
|
* @param string $format The image format (jpg, png, etc.).
|
|
* @param int $postId The post ID to attach the image to.
|
|
* @param array $metadata Additional metadata (quality, style, aspectRatio, etc.).
|
|
* @return array The attachment data on success, false on failure.
|
|
* @throws \Exception If the attachment creation fails.
|
|
*/
|
|
public function createAttachment( $base64Data, $prompt, $format, $postId, $metadata = [] ) {
|
|
if ( empty( $base64Data ) || empty( $prompt ) || empty( $format ) || empty( $postId ) ) {
|
|
throw new \Exception( 'Invalid parameters.' );
|
|
}
|
|
|
|
$imageData = base64_decode( $base64Data );
|
|
if ( false === $imageData ) {
|
|
throw new \Exception( 'Failed to decode base64 image data.' );
|
|
}
|
|
|
|
if ( ! in_array( $format, aioseo()->helpers->getAllowedImageExtensions(), true ) ) {
|
|
throw new \Exception( 'Invalid image format.' );
|
|
}
|
|
|
|
$quality = trim( $metadata['quality'] ?? '' );
|
|
$style = trim( $metadata['style'] ?? '' );
|
|
$aspectRatio = trim( $metadata['aspectRatio'] ?? '' );
|
|
|
|
$filenameContext = substr( $prompt, 0, 25 ) . '-' . $quality . '-' . $style . '-' . $aspectRatio . '-' . date_i18n( 'Ymd-His' );
|
|
$filename = 'aioseo-ai-' . aioseo()->helpers->toLowerCase( sanitize_file_name( $filenameContext ) ) . '.' . $format;
|
|
|
|
$upload = wp_upload_bits( $filename, null, $imageData );
|
|
if ( ! empty( $upload['error'] ) ) {
|
|
throw new \Exception( esc_html( sprintf( 'Failed to upload image. Error: %s', $upload['error'] ) ) );
|
|
}
|
|
|
|
$attachmentData = [
|
|
'post_title' => substr( $prompt, 0, 60 ),
|
|
'post_content' => '',
|
|
'post_parent' => $postId,
|
|
'post_mime_type' => 'image/' . $format,
|
|
'guid' => $upload['url']
|
|
];
|
|
|
|
$attachmentId = wp_insert_attachment( $attachmentData, $upload['file'], $postId, true );
|
|
if ( is_wp_error( $attachmentId ) ) {
|
|
wp_delete_file( $upload['file'] );
|
|
|
|
throw new \Exception( esc_html( sprintf( 'Failed to insert attachment. Error: %s', $attachmentId->get_error_message() ) ) );
|
|
}
|
|
|
|
if ( ! $attachmentId ) {
|
|
wp_delete_file( $upload['file'] );
|
|
|
|
throw new \Exception( 'Failed to insert attachment. No attachment ID returned.' );
|
|
}
|
|
|
|
update_post_meta( $attachmentId, '_aioseo_ai_generated', 1 );
|
|
update_post_meta( $attachmentId, '_aioseo_ai_data', [
|
|
'prompt' => $prompt,
|
|
'quality' => $quality,
|
|
'style' => $style,
|
|
'aspectRatio' => $aspectRatio
|
|
] );
|
|
|
|
$parentImageId = ! empty( $metadata['parentImageId'] ) ? (int) $metadata['parentImageId'] : 0;
|
|
if ( $parentImageId ) {
|
|
update_post_meta( $attachmentId, '_aioseo_ai_parent', $parentImageId );
|
|
}
|
|
|
|
// Generate attachment metadata (thumbnails) asynchronously via Action Scheduler to avoid timeout.
|
|
aioseo()->actionScheduler->scheduleAsync( $this->generateImageMetadataHook, [ $attachmentId, $upload['file'] ] );
|
|
|
|
$src = wp_get_attachment_image_src( $attachmentId, 'full' );
|
|
|
|
list( $url, $width, $height ) = $src;
|
|
|
|
if ( ! $width || ! $height ) {
|
|
list( $width, $height ) = [ 0, 0 ];
|
|
$wpImageSize = wp_getimagesize( $upload['file'] );
|
|
if ( $wpImageSize ) {
|
|
list( $width, $height ) = $wpImageSize;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'alt' => trim( wp_strip_all_tags( get_post_meta( $attachmentId, '_wp_attachment_image_alt', true ) ) ),
|
|
'aspectRatio' => $aspectRatio,
|
|
'format' => $format,
|
|
'height' => $height,
|
|
'id' => $attachmentId,
|
|
'parentImageId' => $parentImageId,
|
|
'prompt' => $prompt,
|
|
'quality' => $quality,
|
|
'style' => $style,
|
|
'url' => $url,
|
|
'width' => $width,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Gets AI-generated images for a specific post.
|
|
*
|
|
* @since 4.8.8
|
|
*
|
|
* @param int $postId The post ID.
|
|
* @return array Array of AI image data.
|
|
*/
|
|
public function getByPostId( $postId ) {
|
|
$images = [];
|
|
if ( empty( $postId ) ) {
|
|
return $images;
|
|
}
|
|
|
|
// Get all attachments for this post that are AI-generated.
|
|
$attachmentIds = get_posts( [
|
|
'post_type' => 'attachment',
|
|
'post_parent' => $postId,
|
|
'post_status' => 'inherit',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids',
|
|
'meta_key' => '_aioseo_ai_generated', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
'meta_value' => 1, // phpcs:ignore HM.Performance.SlowMetaQuery.slow_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
|
'meta_compare' => '='
|
|
] );
|
|
|
|
if ( empty( $attachmentIds ) ) {
|
|
return $images;
|
|
}
|
|
|
|
foreach ( $attachmentIds as $attachmentId ) {
|
|
$images[] = $this->buildImageData( $attachmentId );
|
|
}
|
|
|
|
return $images;
|
|
}
|
|
|
|
/**
|
|
* Deletes the images and updates the parent image id.
|
|
*
|
|
* @since 4.8.8
|
|
*
|
|
* @param array $ids The attachment IDs.
|
|
* @return void
|
|
*/
|
|
public function deleteImages( $ids ) {
|
|
foreach ( $ids as $id ) {
|
|
wp_delete_attachment( $id, true );
|
|
|
|
// Update all images post meta that have the parent image id set to the deleted image id.
|
|
$attachmentIds = get_posts( [
|
|
'post_type' => 'attachment',
|
|
'post_status' => 'inherit',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids',
|
|
'meta_key' => '_aioseo_ai_parent', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
'meta_value' => $id, // phpcs:ignore HM.Performance.SlowMetaQuery.slow_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
|
'meta_compare' => '='
|
|
] );
|
|
|
|
foreach ( $attachmentIds as $attachmentId ) {
|
|
delete_post_meta( $attachmentId, '_aioseo_ai_parent' );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates attachment metadata (thumbnails) for AI-generated images.
|
|
* This is called asynchronously via Action Scheduler to avoid blocking the REST API response.
|
|
*
|
|
* @since 4.8.8
|
|
*
|
|
* @param int $attachmentId The attachment ID.
|
|
* @param string $file Path to the image file.
|
|
* @return void
|
|
*/
|
|
public function generateImageMetadata( $attachmentId, $file ) {
|
|
if ( ! $attachmentId || ! $file || ! file_exists( $file ) || ! get_post( $attachmentId ) ) {
|
|
return;
|
|
}
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
|
|
$metadata = wp_generate_attachment_metadata( $attachmentId, $file );
|
|
if ( $metadata ) {
|
|
wp_update_attachment_metadata( $attachmentId, $metadata );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the image data for a specific attachment.
|
|
*
|
|
* @since 4.8.8
|
|
*
|
|
* @param int $attachmentId The attachment ID.
|
|
* @return array The image data.
|
|
*/
|
|
private function buildImageData( $attachmentId ) {
|
|
$aiData = get_post_meta( $attachmentId, '_aioseo_ai_data', true );
|
|
$aiParent = get_post_meta( $attachmentId, '_aioseo_ai_parent', true );
|
|
|
|
$mimeType = get_post_mime_type( $attachmentId );
|
|
$src = wp_get_attachment_image_src( $attachmentId, 'full' );
|
|
|
|
list( $url, $width, $height ) = $src;
|
|
|
|
if ( ! $width || ! $height ) {
|
|
list( $width, $height ) = [ 0, 0 ];
|
|
$wpImageSize = wp_getimagesize( get_attached_file( $attachmentId ) );
|
|
if ( $wpImageSize ) {
|
|
list( $width, $height ) = $wpImageSize;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'alt' => trim( wp_strip_all_tags( get_post_meta( $attachmentId, '_wp_attachment_image_alt', true ) ) ),
|
|
'aspectRatio' => $aiData['aspectRatio'] ?? null,
|
|
'format' => $mimeType ? str_replace( 'image/', '', $mimeType ) : '',
|
|
'height' => $height,
|
|
'id' => $attachmentId,
|
|
'parentImageId' => ! empty( $aiParent ) ? (int) $aiParent : 0,
|
|
'prompt' => $aiData['prompt'] ?? null,
|
|
'quality' => $aiData['quality'] ?? null,
|
|
'style' => $aiData['style'] ?? null,
|
|
'url' => $url,
|
|
'width' => $width
|
|
];
|
|
}
|
|
} |