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 ]; } }