Source: Providers/OpenAI/DallE.php

<?php
/**
 * OpenAI DALL·E integration
 */

namespace Classifai\Providers\OpenAI;

use Classifai\Providers\Provider;
use Classifai\Providers\OpenAI\APIRequest;
use function Classifai\get_asset_info;
use WP_Error;

class DallE extends Provider {

	use \Classifai\Providers\OpenAI\OpenAI;

	/**
	 * OpenAI DALL·E URL
	 *
	 * @var string
	 */
	protected $dalle_url = 'https://api.openai.com/v1/images/generations';

	/**
	 * Maximum number of characters a prompt can have
	 *
	 * @var int
	 */
	public $max_prompt_chars = 1000;

	/**
	 * OpenAI DALL·E constructor.
	 *
	 * @param string $service The service this class belongs to.
	 */
	public function __construct( $service ) {
		parent::__construct(
			'OpenAI',
			'DALL·E',
			'openai_dalle',
			$service
		);

		// Set the onboarding options.
		$this->onboarding_options = array(
			'title'    => __( 'OpenAI DALL·E', 'classifai' ),
			'fields'   => array( 'api-key' ),
			'features' => array(
				'enable_image_gen' => __( 'Image generation', 'classifai' ),
			),
		);
	}

	/**
	 * Register what we need for the provider.
	 *
	 * This only fires if can_register returns true.
	 */
	public function register() {
		if ( $this->is_feature_enabled() ) {
			add_action( 'admin_menu', [ $this, 'register_generate_media_page' ], 0 );
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
			add_action( 'print_media_templates', [ $this, 'print_media_templates' ] );
		}
	}

	/**
	 * Registers a Media > Generate Image submenu
	 */
	public function register_generate_media_page() {
		$settings         = $this->get_settings();
		$number_of_images = absint( $settings['number'] );

		add_submenu_page(
			'upload.php',
			$number_of_images > 1 ? esc_html__( 'Generate Images', 'classifai' ) : esc_html__( 'Generate Image', 'classifai' ),
			$number_of_images > 1 ? esc_html__( 'Generate Images', 'classifai' ) : esc_html__( 'Generate Image', 'classifai' ),
			'upload_files',
			esc_url( admin_url( 'upload.php?action=classifai-generate-image' ) ),
			''
		);
	}

	/**
	 * Enqueue the admin scripts.
	 *
	 * @param string $hook_suffix The current admin page.
	 */
	public function enqueue_admin_scripts( $hook_suffix = '' ) {
		if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix && 'upload.php' !== $hook_suffix ) {
			return;
		}

		$settings         = $this->get_settings();
		$number_of_images = absint( $settings['number'] );

		wp_enqueue_media();

		wp_enqueue_style(
			'classifai-image-processing-style',
			CLASSIFAI_PLUGIN_URL . 'dist/media-modal.css',
			[],
			CLASSIFAI_PLUGIN_VERSION,
			'all'
		);

		wp_enqueue_script(
			'classifai-generate-images',
			CLASSIFAI_PLUGIN_URL . 'dist/media-modal.js',
			[ 'jquery', 'wp-api', 'wp-media-utils', 'wp-url' ],
			get_asset_info( 'media-modal', 'version' ),
			true
		);

		wp_enqueue_script(
			'classifai-inserter-media-category',
			CLASSIFAI_PLUGIN_URL . 'dist/inserter-media-category.js',
			get_asset_info( 'inserter-media-category', 'dependencies' ),
			get_asset_info( 'inserter-media-category', 'version' ),
			true
		);

		/**
		 * Filter the default attribution added to generated images.
		 *
		 * @since 2.1.0
		 * @hook classifai_dalle_caption
		 *
		 * @param {string} $caption Attribution to be added as a caption to the image.
		 *
		 * @return {string} Caption.
		 */
		$caption = apply_filters(
			'classifai_dalle_caption',
			sprintf(
				/* translators: %1$s is replaced with the OpenAI DALL·E URL */
				esc_html__( 'Image generated by <a href="%s">OpenAI\'s DALL·E</a>', 'classifai' ),
				'https://openai.com/research/dall-e'
			)
		);

		wp_localize_script(
			'classifai-generate-images',
			'classifaiDalleData',
			[
				'endpoint'   => 'classifai/v1/openai/generate-image',
				'tabText'    => $number_of_images > 1 ? esc_html__( 'Generate images', 'classifai' ) : esc_html__( 'Generate image', 'classifai' ),
				'errorText'  => esc_html__( 'Something went wrong. No results found', 'classifai' ),
				'buttonText' => esc_html__( 'Select image', 'classifai' ),
				'caption'    => $caption,
			]
		);

		if ( 'upload.php' === $hook_suffix ) {
			$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

			if ( 'classifai-generate-image' === $action ) {
				wp_enqueue_script(
					'classifai-generate-images-media-upload',
					CLASSIFAI_PLUGIN_URL . 'dist/generate-image-media-upload.js',
					[ 'jquery' ],
					get_asset_info( 'classifai-generate-images-media-upload', 'version' ),
					true
				);

				wp_localize_script(
					'classifai-generate-images-media-upload',
					'classifaiGenerateImages',
					[
						'upload_url' => esc_url( admin_url( 'upload.php' ) ),
					]
				);
			}
		}
	}

	/**
	 * Print the templates we need for our media modal integration.
	 */
	public function print_media_templates() {
		$settings         = $this->get_settings();
		$number_of_images = absint( $settings['number'] );
		?>

		<?php // Template for the Generate images tab content. Includes prompt input. ?>
		<script type="text/html" id="tmpl-dalle-prompt">
			<div class="prompt-view">
				<p>
					<?php
					if ( $number_of_images > 1 ) {
						esc_html_e( 'Enter a prompt below to generate images.', 'classifai' );
					} else {
						esc_html_e( 'Enter a prompt below to generate an image.', 'classifai' );
					}
					?>
				</p>
				<p>
					<?php
					if ( $number_of_images > 1 ) {
						esc_html_e( 'Once images are generated, choose one or more of those to import into your Media Library and then choose one image to insert.', 'classifai' );
					} else {
						esc_html_e( 'Once an image is generated, you can import it into your Media Library and then select to insert.', 'classifai' );
					}
					?>
				</p>
				<textarea class="prompt" placeholder="<?php esc_attr_e( 'Enter prompt', 'classifai' ); ?>" rows="4" maxlength="<?php echo absint( $this->max_prompt_chars ); ?>"></textarea>
				<button type="button" class="button button-secondary button-large button-generate">
					<?php
					if ( $number_of_images > 1 ) {
						esc_html_e( 'Generate images', 'classifai' );
					} else {
						esc_html_e( 'Generate image', 'classifai' );
					}
					?>
				</button>
				<span class="error"></span>
			</div>
			<div class="generated-images">
				<h2 class="prompt-text hidden">
					<?php
					if ( $number_of_images > 1 ) {
						esc_html_e( 'Images generated from prompt:', 'classifai' );
					} else {
						esc_html_e( 'Image generated from prompt:', 'classifai' );
					}
					?>
					<span></span>
				</h2>
				<span class="spinner"></span>
				<ul></ul>
			</div>
		</script>

		<?php
		// Template for a single generated image.
		/* phpcs:disable WordPressVIPMinimum.Security.Mustache.OutputNotation */
		?>
		<script type="text/html" id="tmpl-dalle-image">
			<div class="generated-image">
				<img src="data:image/png;base64,{{{ data.url }}}" />
				<button type="button" class="components-button button-secondary button-import"><?php esc_html_e( 'Import into Media Library', 'classifai' ); ?></button>
				<button type="button" class="components-button is-tertiary button-import-insert"><?php esc_html_e( 'Import and Insert', 'classifai' ); ?></button>
				<span class="spinner"></span>
				<span class="error"></span>
			</div>
		</script>
		<?php
		/* phpcs:enable WordPressVIPMinimum.Security.Mustache.OutputNotation */
		?>

		<?php
	}

	/**
	 * Setup fields
	 */
	public function setup_fields_sections() {
		$default_settings = $this->get_default_settings();

		$this->setup_api_fields( $default_settings['api_key'] );

		add_settings_field(
			'enable-image-gen',
			esc_html__( 'Enable image generation', 'classifai' ),
			[ $this, 'render_input' ],
			$this->get_option_name(),
			$this->get_option_name(),
			[
				'label_for'     => 'enable_image_gen',
				'input_type'    => 'checkbox',
				'default_value' => $default_settings['enable_image_gen'],
				'description'   => __( 'When enabled, a new Generate images tab will be shown in the media upload flow, allowing you to generate and import images.', 'classifai' ),
			]
		);

		// Get all roles that have the upload_files cap.
		$roles = get_editable_roles() ?? [];
		$roles = array_filter(
			$roles,
			function( $role ) {
				return isset( $role['capabilities'], $role['capabilities']['upload_files'] ) && $role['capabilities']['upload_files'];
			}
		);
		$roles = array_combine( array_keys( $roles ), array_column( $roles, 'name' ) );

		/**
		 * Filter the allowed WordPress roles for DALL·E
		 *
		 * @since 2.3.0
		 * @hook classifai_openai_dalle_allowed_image_roles
		 *
		 * @param {array} $roles            Array of arrays containing role information.
		 * @param {array} $default_settings Default setting values.
		 *
		 * @return {array} Roles array.
		 */
		$roles = apply_filters( 'classifai_openai_dalle_allowed_image_roles', $roles, $default_settings );

		add_settings_field(
			'roles',
			esc_html__( 'Allowed roles', 'classifai' ),
			[ $this, 'render_checkbox_group' ],
			$this->get_option_name(),
			$this->get_option_name(),
			[
				'label_for'      => 'roles',
				'options'        => $roles,
				'default_values' => $default_settings['roles'],
				'description'    => __( 'Choose which roles are allowed to generate images. Note that the roles above only include those that have permissions to upload media.', 'classifai' ),
			]
		);

		add_settings_field(
			'number',
			esc_html__( 'Number of images', 'classifai' ),
			[ $this, 'render_select' ],
			$this->get_option_name(),
			$this->get_option_name(),
			[
				'label_for'     => 'number',
				'options'       => array_combine( range( 1, 10 ), range( 1, 10 ) ),
				'default_value' => $default_settings['number'],
				'description'   => __( 'Number of images that will be generated in one request. Note that each image will incur separate costs.', 'classifai' ),
			]
		);

		add_settings_field(
			'size',
			esc_html__( 'Image size', 'classifai' ),
			[ $this, 'render_select' ],
			$this->get_option_name(),
			$this->get_option_name(),
			[
				'label_for'     => 'size',
				'options'       => [
					'256x256'   => '256x256',
					'512x512'   => '512x512',
					'1024x1024' => '1024x1024',
				],
				'default_value' => $default_settings['size'],
				'description'   => __( 'Size of generated images.', 'classifai' ),
			]
		);
	}

	/**
	 * Sanitization for the options being saved.
	 *
	 * @param array $settings Array of settings about to be saved.
	 * @return array The sanitized settings to be saved.
	 */
	public function sanitize_settings( $settings ) {
		$new_settings = $this->get_settings();
		$new_settings = array_merge(
			$new_settings,
			$this->sanitize_api_key_settings( $new_settings, $settings )
		);

		if ( empty( $settings['enable_image_gen'] ) || 1 !== (int) $settings['enable_image_gen'] ) {
			$new_settings['enable_image_gen'] = 'no';
		} else {
			$new_settings['enable_image_gen'] = '1';
		}

		if ( isset( $settings['roles'] ) && is_array( $settings['roles'] ) ) {
			$new_settings['roles'] = array_map( 'sanitize_text_field', $settings['roles'] );
		} else {
			$new_settings['roles'] = array_keys( get_editable_roles() ?? [] );
		}

		if ( isset( $settings['number'] ) && is_numeric( $settings['number'] ) && (int) $settings['number'] >= 1 && (int) $settings['number'] <= 10 ) {
			$new_settings['number'] = absint( $settings['number'] );
		} else {
			$new_settings['number'] = 1;
		}

		if ( isset( $settings['size'] ) && in_array( $settings['size'], [ '256x256', '512x512', '1024x1024' ], true ) ) {
			$new_settings['size'] = sanitize_text_field( $settings['size'] );
		} else {
			$new_settings['size'] = '1024x1024';
		}

		return $new_settings;
	}

	/**
	 * Resets settings for the provider.
	 */
	public function reset_settings() {
		update_option( $this->get_option_name(), $this->get_default_settings() );
	}

	/**
	 * Default settings for ChatGPT
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'authenticated'    => false,
			'api_key'          => '',
			'enable_image_gen' => false,
			'roles'            => array_keys( get_editable_roles() ?? [] ),
			'number'           => 1,
			'size'             => '1024x1024',
		];
	}

	/**
	 * Provides debug information related to the provider.
	 *
	 * @param array|null $settings Settings array. If empty, settings will be retrieved.
	 * @param boolean    $configured Whether the provider is correctly configured. If null, the option will be retrieved.
	 * @return string|array
	 */
	public function get_provider_debug_information( $settings = null, $configured = null ) {
		if ( is_null( $settings ) ) {
			$settings = $this->sanitize_settings( $this->get_settings() );
		}

		$authenticated = 1 === intval( $settings['authenticated'] ?? 0 );
		$enabled       = 1 === intval( $settings['enable_image_gen'] ?? 0 );

		return [
			__( 'Authenticated', 'classifai' )    => $authenticated ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ),
			__( 'Generate images', 'classifai' )  => $enabled ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ),
			__( 'Allowed roles', 'classifai' )    => implode( ', ', $settings['roles'] ?? [] ),
			__( 'Number of images', 'classifai' ) => absint( $settings['number'] ?? 1 ),
			__( 'Image size', 'classifai' )       => sanitize_text_field( $settings['size'] ?? '1024x1024' ),
			__( 'Latest response', 'classifai' )  => $this->get_formatted_latest_response( get_transient( 'classifai_openai_dalle_latest_response' ) ),
		];
	}

	/**
	 * Entry point for the generate-image REST endpoint.
	 *
	 * @param string $prompt The prompt used to generate an image.
	 * @param array  $args Optional arguments passed to endpoint.
	 * @return string|WP_Error
	 */
	public function generate_image_callback( string $prompt = '', array $args = [] ) {
		if ( ! $prompt ) {
			return new WP_Error( 'prompt_required', esc_html__( 'A prompt is required to generate an image.', 'classifai' ) );
		}

		$settings = $this->get_settings();
		$args     = wp_parse_args(
			array_filter( $args ),
			[
				'num'    => $settings['number'] ?? 1,
				'size'   => $settings['size'] ?? '1024x1024',
				'format' => 'url',
			]
		);

		// These checks already ran in the REST permission_callback,
		// but we run them again here in case this method is called directly.
		if ( ! $this->is_feature_enabled() ) {
			// Note that we purposely leave off the textdomain here as this is the same error
			// message core uses, so we want translations to load from there.
			return new WP_Error( 'rest_forbidden', esc_html__( 'Sorry, you are not allowed to do that.' ) );
		}

		if ( empty( $settings ) || ( isset( $settings['authenticated'] ) && false === $settings['authenticated'] ) || ( isset( $settings['enable_image_gen'] ) && 'no' === $settings['enable_image_gen'] ) ) {
			return new WP_Error( 'not_enabled', esc_html__( 'Image generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) );
		}

		/**
		 * Filter the prompt we will send to DALL·E.
		 *
		 * @since 2.0.0
		 * @hook classifai_dalle_prompt
		 *
		 * @param {string} $prompt Prompt we are sending to DALL·E.
		 *
		 * @return {string} Prompt.
		 */
		$prompt = apply_filters( 'classifai_dalle_prompt', $prompt );

		// If our prompt exceeds the max length, throw an error.
		if ( mb_strlen( $prompt ) > $this->max_prompt_chars ) {
			return new WP_Error( 'invalid_param', esc_html__( 'Your image prompt is too long. Please ensure it doesn\'t exceed 1000 characters.', 'classifai' ) );
		}

		$request = new APIRequest( $settings['api_key'] ?? '' );

		/**
		 * Filter the request body before sending to DALL·E.
		 *
		 * @since 2.0.0
		 * @hook classifai_dalle_request_body
		 *
		 * @param {array} $body Request body that will be sent to DALL·E.
		 *
		 * @return {array} Request body.
		 */
		$body = apply_filters(
			'classifai_dalle_request_body',
			[
				'prompt'          => sanitize_text_field( $prompt ),
				'n'               => absint( $args['num'] ),
				'size'            => sanitize_text_field( $args['size'] ),
				'response_format' => sanitize_text_field( $args['format'] ),
			]
		);

		// Make our API request.
		$response = $request->post(
			$this->dalle_url,
			[
				'body' => wp_json_encode( $body ),
			]
		);

		set_transient( 'classifai_openai_dalle_latest_response', $response, DAY_IN_SECONDS * 30 );

		// Extract out the image response, if it exists.
		if ( ! is_wp_error( $response ) && ! empty( $response['data'] ) ) {
			$cleaned_response = [];

			foreach ( $response['data'] as $data ) {
				if ( ! empty( $data[ $args['format'] ] ) ) {
					if ( 'url' === $args['format'] ) {
						$cleaned_response[] = [ 'url' => esc_url_raw( $data[ $args['format'] ] ) ];
					} else {
						$cleaned_response[] = [ 'url' => $data[ $args['format'] ] ];
					}
				}
			}

			$response = $cleaned_response;
		}

		return $response;
	}

	/**
	 * Checks whether we can generate images.
	 *
	 * @return bool
	 */
	public function is_feature_enabled() {
		$access   = false;
		$settings = $this->get_settings();

		// Check if the current user has permission to generate images.
		$roles      = $settings['roles'] ?? [];
		$user_roles = wp_get_current_user()->roles ?? [];

		if (
			current_user_can( 'upload_files' )
			&& ( ! empty( $roles ) && empty( array_diff( $user_roles, $roles ) ) )
			&& ( isset( $settings['enable_image_gen'] ) && 1 === (int) $settings['enable_image_gen'] )
		) {
			$access = true;
		}

		/**
		 * Filter to override permission to use the image gen feature.
		 *
		 * @since 2.3.0
		 * @hook classifai_openai_dalle_enable_image_gen
		 *
		 * @param {bool} $access  Current access value.
		 * @param {array} $settings Feature settings.
		 *
		 * @return {bool} Should the user have access?
		 */
		return apply_filters( 'classifai_openai_dalle_enable_image_gen', $access, $settings );
	}

}