init(); do_action( 'bdthemes_ai_image_init' ); } return self::$instance; } /** * Admin screens that render this plugin’s own UI (always load assets). * * @return string[] */ private function ai_image_plugin_admin_hooks() { return array( 'settings_page_ai-image-settings', 'media_page_bdt-ai-media-tab' ); } /** * Whether wp.media (Image Generator tab) is expected on this admin screen. * Uses WP_Screen so we do not load on unrelated pages (e.g. plugins, users). * * @param string $hook_suffix Passed from admin_enqueue_scripts (usually WP_Screen::$id). * @return bool */ private function is_ai_image_media_modal_context_screen( $hook_suffix ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen instanceof \WP_Screen ) { // Post / page / any CPT editor. if ( 'post' === $screen->base ) { return true; } // Media library, add media, upload flows. if ( in_array( $screen->base, array( 'upload', 'media' ), true ) ) { return true; } // List tables (featured image, bulk actions, etc.). if ( 'edit' === $screen->base ) { return true; } $modal_screen_ids = array( 'site-editor', 'widgets', 'nav-menus', 'theme-editor', 'customize', 'media-upload', ); if ( in_array( $screen->id, $modal_screen_ids, true ) ) { return true; } } // CPT list: hook id is often `edit-{post_type}` when screen object is unavailable. if ( preg_match( '/^edit-[a-z0-9_-]+$/', $hook_suffix ) ) { return true; } return false; } /** * Whether to load the main admin bundle (CSS/JS) on this screen. * * @param string $hook_suffix Passed from admin_enqueue_scripts. * @return bool */ private function should_enqueue_ai_image_admin_bundle( $hook_suffix ) { if ( in_array( $hook_suffix, $this->ai_image_plugin_admin_hooks(), true ) ) { return true; } if ( get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) === '1' ) { return false; } $load = $this->is_ai_image_media_modal_context_screen( $hook_suffix ); /** * Override whether the Image Generator admin bundle loads (media modal tab enabled). * * @param bool $load Default decision. * @param string $hook_suffix Current admin screen hook suffix / screen id. */ return (bool) apply_filters( 'bdthemes_ai_image_should_enqueue_admin_bundle', $load, $hook_suffix ); } /** * Admin Styles * * @since 1.0.0 */ public function enqueue_admin_styles( $hook_suffix ) { if ( ! $this->should_enqueue_ai_image_admin_bundle( $hook_suffix ) ) { return; } wp_register_style( 'ai-image', BDT_AI_IMAGE_URL . 'build/admin/index.css', array(), BDT_AI_IMAGE_VERSION ); wp_enqueue_style( 'ai-image' ); } /** * Enqueue admin scripts * * @since 1.0.0 * @return void */ public function enqueue_admin_scripts( $hook_suffix ) { if ( ! $this->should_enqueue_ai_image_admin_bundle( $hook_suffix ) ) { return; } $is_plugin_page = in_array( $hook_suffix, $this->ai_image_plugin_admin_hooks(), true ); $asset_file = plugin_dir_path( __FILE__ ) . 'build/admin/index.asset.php'; if ( ! file_exists( $asset_file ) ) { return; } $asset = include $asset_file; wp_register_script( 'ai-image', BDT_AI_IMAGE_URL . 'build/admin/index.js', $asset['dependencies'], $asset['version'], true ); wp_enqueue_script( 'ai-image' ); $provider_ids = array( 'global', 'pexels', 'pixabay', 'openverse', 'wikimedia', 'unsplash', 'giphy', 'openai', 'gemini' ); $provider_enabled = array(); foreach ( $provider_ids as $id ) { $provider_enabled[ $id ] = \bdthemes_ai_image_is_provider_enabled( $id ); } $default_order = array( 'pexels', 'pixabay', 'openverse', 'wikimedia', 'unsplash', 'giphy', 'openai', 'gemini' ); $saved_order = get_option( 'bdthemes_ai_image_provider_order', $default_order ); if ( ! is_array( $saved_order ) || empty( $saved_order ) ) { $saved_order = $default_order; } $provider_order = array_values( array_unique( array_merge( $saved_order, $default_order ) ) ); $max_w = get_option( 'bdthemes_ai_image_max_upload_width', 1600 ); $max_h = get_option( 'bdthemes_ai_image_max_upload_height', 1200 ); $items_per_page = get_option( 'bdthemes_ai_image_items_per_page', 20 ); $general = array( 'max_upload_width' => is_numeric( $max_w ) ? (int) $max_w : 1600, 'max_upload_height' => is_numeric( $max_h ) ? (int) $max_h : 1200, 'default_provider' => get_option( 'bdthemes_ai_image_default_provider', 'pexels' ), 'image_attribution' => get_option( 'bdthemes_ai_image_attribution', '0' ) === '1', 'auto_alt_text' => get_option( 'bdthemes_ai_image_auto_alt_text', '0' ) === '1', 'auto_title' => get_option( 'bdthemes_ai_image_auto_title', '0' ) === '1', 'hide_media_modal_tab' => get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) === '1', 'default_view_mode' => get_option( 'bdthemes_ai_image_default_view_mode', 'grid' ), 'items_per_page' => is_numeric( $items_per_page ) ? (int) $items_per_page : 20, 'thumbnail_size' => get_option( 'bdthemes_ai_image_thumbnail_size', 'small' ), 'load_more_mode' => get_option( 'bdthemes_ai_image_load_more_mode', 'manual' ), ); $script_config = array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'assets_url' => BDT_AI_IMAGE_ASSETS, 'rest_url' => rest_url( 'bdthemes/v1/' ), 'version' => BDT_AI_IMAGE_VERSION, 'settings_url' => admin_url( 'options-general.php?page=ai-image-settings' ), 'generator_url' => admin_url( 'upload.php?page=bdt-ai-media-tab' ), 'media_tab_label' => __( 'Image Generator', 'ai-image' ), 'support_url' => 'https://bdthemes.com/support/?utm_source=WordPress_Repository&utm_medium=Plugin_Page&utm_campaign=WordPress_to_Instant_Image_Generator', 'docs_url' => 'https://bdthemes.com/all-knowledge-base-of-instant-image-generator/?utm_source=WordPress_Repository&utm_medium=Plugin_Page&utm_campaign=WordPress_to_Instant_Image_Generator', 'provider_enabled' => $provider_enabled, 'provider_order' => $provider_order, 'general_settings' => $general, 'image_sizes' => $this->get_all_image_sizes(), ); wp_localize_script( 'ai-image', 'AI_IMAGE_AdminConfig', $script_config ); // Add media modal integration script only on screens where the tab is used (not on plugin settings pages). if ( ! $is_plugin_page && get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) !== '1' ) { wp_enqueue_media(); $this->enqueue_media_modal_script(); } } /** * Enqueue block editor assets for the Image Generator button * * @since 2.0.0 */ public function enqueue_block_editor_assets() { // Load the main admin bundle which includes block-toolbar functionality $asset_file = BDT_AI_IMAGE_PATH . 'build/admin/index.asset.php'; if ( ! file_exists( $asset_file ) ) { return; } $asset = include $asset_file; // Enqueue the admin script (includes block toolbar) wp_enqueue_script( 'ai-image', BDT_AI_IMAGE_URL . 'build/admin/index.js', $asset['dependencies'], $asset['version'], true ); // Enqueue the admin styles wp_enqueue_style( 'ai-image', BDT_AI_IMAGE_URL . 'build/admin/index.css', array(), BDT_AI_IMAGE_VERSION ); // Check if any provider is enabled and build enabled providers list $provider_ids = array( 'pexels', 'pixabay', 'unsplash', 'openverse', 'wikimedia', 'giphy', 'openai', 'gemini' ); $has_enabled_provider = false; $enabled_providers = array(); foreach ( $provider_ids as $id ) { $enabled = \bdthemes_ai_image_is_provider_enabled( $id ); $enabled_providers[ $id ] = $enabled; if ( $enabled ) { $has_enabled_provider = true; } } // Get provider order from settings $default_order = array( 'pexels', 'pixabay', 'openverse', 'wikimedia', 'unsplash', 'giphy', 'openai', 'gemini' ); $saved_order = get_option( 'bdthemes_ai_image_provider_order', $default_order ); if ( ! is_array( $saved_order ) || empty( $saved_order ) ) { $saved_order = $default_order; } $provider_order = array_values( array_unique( array_merge( $saved_order, $default_order ) ) ); // Localize script with config for block toolbar wp_localize_script( 'ai-image', 'AI_IMAGE_BlockToolbar', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'hasEnabledProviders' => $has_enabled_provider, 'enabledProviders' => $enabled_providers, 'providerOrder' => $provider_order, ) ); } /** * Enqueue the inline script that adds the "Image Generator" tab to the new wp.media modal. */ private function enqueue_media_modal_script() { $inline_js = <<<'MEDIAJS' (function( $, wp ){ if ( typeof wp === 'undefined' || ! wp.media || ! wp.media.view ) return; var TAB_ID = 'ai-image-tab'; var TAB_TEXT = ( window.AI_IMAGE_AdminConfig && window.AI_IMAGE_AdminConfig.media_tab_label ) || 'Image Generator'; /** * Custom Backbone view that renders our React app. */ var AiImageContent = wp.media.View.extend({ className: 'ai-image-wrap ai-image-media-modal', initialize: function() { this.$el.attr('style', 'height:100%;overflow-y:auto;padding:16px 20px;background:#f0f0f1;'); }, render: function() { this.$el.html( '
' ); var self = this; // Small delay to ensure DOM is ready before React renders setTimeout(function(){ var root = self.$el.find('#ai-image-generator-modal')[0]; if ( root && window.aiImageRenderApp ) { window.aiImageRenderApp( root ); } }, 50); return this; } }); /** * Add the tab to router for any frame type. */ function addTab( routerView ) { routerView.set( TAB_ID, { text: TAB_TEXT, priority: 200 }); } /** * Bind content creation handler to a frame. */ function bindContentHandler( frame ) { if ( ! frame._aiImageBound ) { frame._aiImageBound = true; frame.on( 'content:create:' + TAB_ID, function() { var view = new AiImageContent(); frame.content.set( view ); }); } // WordPress reopens the modal on the last tab used (the libraryContent // user setting). When that was ours, the content region renders before // the router reaches this function, so nothing answers the create event // and the tab comes up blank. Clicking it does not help either, as it is // already the active mode. Render it now that the handler is in place. if ( frame.content && frame.content.mode() === TAB_ID && ! frame.content.get() ) { frame.content.render(); } } // Override Select frame router (used in Gutenberg, featured image, etc.) var origSelectRouter = wp.media.view.MediaFrame.Select.prototype.browseRouter; wp.media.view.MediaFrame.Select.prototype.browseRouter = function( routerView ) { origSelectRouter.apply( this, arguments ); addTab( routerView ); bindContentHandler( this ); }; // Override Post frame router (used in classic editor "Add Media") if ( wp.media.view.MediaFrame.Post ) { var origPostRouter = wp.media.view.MediaFrame.Post.prototype.browseRouter; wp.media.view.MediaFrame.Post.prototype.browseRouter = function( routerView ) { origPostRouter.apply( this, arguments ); addTab( routerView ); bindContentHandler( this ); }; } })( jQuery, wp ); MEDIAJS; wp_add_inline_script( 'ai-image', $inline_js, 'after' ); } /** * Add AI Image settings page under Settings menu. */ public function add_settings_menu() { add_submenu_page( 'options-general.php', __( 'Image Generator', 'ai-image' ), __( 'Image Generator', 'ai-image' ), 'manage_options', 'ai-image-settings', array( $this, 'render_settings_page' ) ); // The BdThemes deals page (admin_init redirects there). add_submenu_page( 'options-general.php', __( 'Image Generator', 'ai-image' ), esc_html__( 'Up To 86% Off', 'ai-image' ), 'manage_options', self::DEALS_PAGE, array( $this, 'render_settings_page' ) ); add_action( 'admin_head', array( $this, 'deals_menu_assets' ) ); } /** * Send the "Up To 86% Off" menu entry to the BdThemes deals page. * * @return void */ public function redirect_to_deals() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check of the current admin page slug, no form data processed. if ( ! isset( $_GET['page'] ) || self::DEALS_PAGE !== $_GET['page'] || ! current_user_can( 'manage_options' ) ) { return; } // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Intentional redirect to the external BdThemes deals page; wp_safe_redirect() would block this off-site host. wp_redirect( self::DEALS_URL ); exit; } /** * Highlight the deals entry and open it in a new tab. * * @return void */ public function deals_menu_assets() { ?> '; } /** * MIME types accepted when importing an image. * * @return string[] */ private function allowed_import_mime_types() { /** * Filters the MIME types accepted by the image importer. * * @param string[] $mime_types Allowed MIME types. */ return (array) apply_filters( 'bdthemes_ai_image_allowed_import_mime_types', array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ) ); } /** * Maximum size, in bytes, accepted for an imported image. * * @return int */ private function max_import_file_size() { /** * Filters the maximum size of an imported image. * * @param int $max_bytes Maximum size in bytes. */ return (int) apply_filters( 'bdthemes_ai_image_max_import_size', 20 * MB_IN_BYTES ); } /** * Summary of upload_image_to_wp */ public function upload_image_to_wp() { // Verify nonce for security if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp_rest' ) ) { wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'ai-image' ) ) ); } if ( ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to upload files.', 'ai-image' ) ), 403 ); } // Check if image URL is provided if ( ! isset( $_POST['image_url'] ) || empty( $_POST['image_url'] ) ) { wp_send_json_error( array( 'message' => __( 'No image URL provided.', 'ai-image' ) ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized based on type below $raw_image_url = wp_unslash( $_POST['image_url'] ); // A non-string (image_url[]=…) would make the strpos() below throw. if ( ! is_string( $raw_image_url ) ) { wp_send_json_error( array( 'message' => __( 'Invalid image URL.', 'ai-image' ) ) ); } $image_title = isset( $_POST['image_title'] ) ? sanitize_text_field( wp_unslash( $_POST['image_title'] ) ) : ''; $image_author = isset( $_POST['image_author'] ) ? sanitize_text_field( wp_unslash( $_POST['image_author'] ) ) : ''; $upload_dir = wp_upload_dir(); $allowed_mime_types = $this->allowed_import_mime_types(); $max_file_size = $this->max_import_file_size(); $too_large_message = sprintf( /* translators: %s: maximum allowed file size, e.g. "20 MB". */ __( 'Image exceeds the maximum allowed size of %s.', 'ai-image' ), size_format( $max_file_size ) ); // Check if this is a base64 data URI (e.g., from Gemini) $is_base64 = strpos( $raw_image_url, 'data:image/' ) === 0; if ( $is_base64 ) { // Handle base64 data URI // Extract the image data from the data URI // Format: data:image/png;base64,iVBORw0KGgoAAAANS... if ( ! preg_match( '/^data:image\/(\w+);base64,(.+)$/i', $raw_image_url, $matches ) ) { wp_send_json_error( array( 'message' => __( 'Invalid base64 image data.', 'ai-image' ) ) ); } $extension = strtolower( $matches[1] ); $base64_str = $matches[2]; // Validate extension if ( ! in_array( $extension, array( 'png', 'jpg', 'jpeg', 'gif', 'webp' ) ) ) { wp_send_json_error( array( 'message' => sprintf( /* translators: %s: image file extension, e.g. "svg". */ __( 'Unsupported image format: %s', 'ai-image' ), $extension ), ) ); } // Decode base64 data $image_data = base64_decode( $base64_str ); if ( $image_data === false ) { wp_send_json_error( array( 'message' => __( 'Failed to decode base64 image data.', 'ai-image' ) ) ); } if ( empty( $image_data ) ) { wp_send_json_error( array( 'message' => __( 'Decoded base64 image data is empty.', 'ai-image' ) ) ); } if ( strlen( $image_data ) > $max_file_size ) { wp_send_json_error( array( 'message' => $too_large_message ) ); } } else { // Handle regular URL (reject unsafe / local targets to reduce SSRF risk). $image_url = esc_url_raw( $raw_image_url ); if ( empty( $image_url ) || ! wp_http_validate_url( $image_url ) ) { wp_send_json_error( array( 'message' => __( 'Invalid or disallowed image URL.', 'ai-image' ) ) ); } $response = wp_safe_remote_get( $image_url, array( 'timeout' => 60, 'redirection' => 3, // Stop the transport from reading an unbounded body into memory. 'limit_response_size' => $max_file_size, ) ); // Check if the request failed if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); wp_send_json_error( array( 'message' => __( 'Failed to fetch image.', 'ai-image' ), 'error' => $error_message ) ); } // Check if the status code is 200 (success) $status_code = wp_remote_retrieve_response_code( $response ); if ( $status_code !== 200 ) { wp_send_json_error( array( 'message' => sprintf( /* translators: %s: HTTP status code returned by the image host. */ __( 'Failed to fetch image. Status code: %s', 'ai-image' ), $status_code ), ) ); } // Get the image data $image_data = wp_remote_retrieve_body( $response ); if ( empty( $image_data ) ) { wp_send_json_error( array( 'message' => __( 'Failed to retrieve image data.', 'ai-image' ) ) ); } // limit_response_size truncates an oversized body rather than failing, so a // file over the cap would otherwise be saved corrupt. Reject it instead. $content_length = wp_remote_retrieve_header( $response, 'content-length' ); if ( ( is_numeric( $content_length ) && (int) $content_length > $max_file_size ) || strlen( $image_data ) >= $max_file_size ) { wp_send_json_error( array( 'message' => $too_large_message ) ); } // Detect file extension from content-type or URL $content_type = wp_remote_retrieve_header( $response, 'content-type' ); $extension = 'jpg'; if ( ! empty( $content_type ) ) { if ( strpos( $content_type, 'png' ) !== false ) { $extension = 'png'; } elseif ( strpos( $content_type, 'gif' ) !== false ) { $extension = 'gif'; } elseif ( strpos( $content_type, 'webp' ) !== false ) { $extension = 'webp'; } } } // Build filename from image title if available, otherwise use a domain-based name if ( ! empty( $image_title ) ) { // Sanitize the title into a clean slug for the filename $slug = sanitize_title( $image_title ); $slug = preg_replace( '/[^a-z0-9\-]/', '', $slug ); // Limit to 80 chars to avoid extremely long filenames if ( strlen( $slug ) > 80 ) { $slug = substr( $slug, 0, 80 ); } $slug = rtrim( $slug, '-' ); $filename = $slug . '.' . $extension; } else { // Fallback: domain-based random filename $wp_domain_name = get_site_url(); $wp_domain_name = str_replace( array( 'http://', 'https://' ), '', strtolower( $wp_domain_name ) ); $wp_domain_name = preg_replace( '/[^a-z0-9]/', '-', $wp_domain_name ); $filename = $wp_domain_name . '-' . time() . '.' . $extension; } // Ensure unique filename in uploads directory $filename = wp_unique_filename( $upload_dir['path'], $filename ); // Check the file type before anything is written to disk. $wp_filetype = wp_check_filetype( $filename, null ); if ( ! in_array( $wp_filetype['type'], $allowed_mime_types, true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid image type.', 'ai-image' ) ) ); } // Check if the upload directory exists if ( wp_mkdir_p( $upload_dir['path'] ) ) { $file_path = $upload_dir['path'] . '/' . $filename; } else { $file_path = $upload_dir['basedir'] . '/' . $filename; } // Write the image data to the file $write_result = file_put_contents( $file_path, $image_data ); if ( ! $write_result ) { wp_send_json_error( array( 'message' => __( 'Failed to save image to disk.', 'ai-image' ) ) ); } // The extension only describes the file name, so confirm the bytes on disk // really are an image of an accepted type before keeping the file. if ( ! in_array( wp_get_image_mime( $file_path ), $allowed_mime_types, true ) ) { wp_delete_file( $file_path ); wp_send_json_error( array( 'message' => __( 'The downloaded file is not a valid image.', 'ai-image' ) ) ); } // Resize image if it exceeds max dimensions $max_width = get_option( 'bdthemes_ai_image_max_upload_width', 1600 ); $max_height = get_option( 'bdthemes_ai_image_max_upload_height', 1200 ); $max_width = is_numeric( $max_width ) ? (int) $max_width : 1600; $max_height = is_numeric( $max_height ) ? (int) $max_height : 1200; if ( $max_width > 0 && $max_height > 0 ) { require_once( ABSPATH . 'wp-admin/includes/image.php' ); $image_editor = wp_get_image_editor( $file_path ); if ( ! is_wp_error( $image_editor ) ) { $size = $image_editor->get_size(); // Only resize if image exceeds max dimensions if ( $size['width'] > $max_width || $size['height'] > $max_height ) { $image_editor->resize( $max_width, $max_height, false ); $saved = $image_editor->save( $file_path ); // Not fatal: the original file is still valid, so log and keep going. if ( is_wp_error( $saved ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only diagnostics. error_log( '[ai-image] resize failed: ' . $saved->get_error_message() ); } } } } // Read user settings for auto-populating metadata $auto_alt_text = get_option( 'bdthemes_ai_image_auto_alt_text', '0' ) === '1'; $auto_title = get_option( 'bdthemes_ai_image_auto_title', '0' ) === '1'; $image_attribution = get_option( 'bdthemes_ai_image_attribution', '0' ) === '1'; // Determine the WP attachment title if ( $auto_title && ! empty( $image_title ) ) { $attachment_title = $image_title; } else { // When auto title is off, leave empty so WordPress doesn't show a generated name $attachment_title = $auto_title ? sanitize_file_name( $filename ) : ''; } // Build caption from author name if attribution is enabled $caption = ''; if ( $image_attribution && ! empty( $image_author ) ) { $caption = 'Photo by ' . $image_author; } $attachment = array( 'post_mime_type' => $wp_filetype['type'], 'post_title' => $attachment_title, 'post_content' => '', 'post_excerpt' => $caption, 'post_status' => 'inherit', ); // Insert the attachment into the media library $attach_id = wp_insert_attachment( $attachment, $file_path ); if ( ! $attach_id ) { wp_delete_file( $file_path ); wp_send_json_error( array( 'message' => __( 'Failed to upload image.', 'ai-image' ) ) ); } // Set alternative text from the image title if enabled if ( $auto_alt_text && ! empty( $image_title ) ) { update_post_meta( $attach_id, '_wp_attachment_image_alt', $image_title ); } // Generate metadata for the attachment require_once( ABSPATH . 'wp-admin/includes/image.php' ); $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path ); wp_update_attachment_metadata( $attach_id, $attach_data ); // Return success response wp_send_json_success( array( 'attach_id' => $attach_id, 'attach_url' => wp_get_attachment_url( $attach_id ), ) ); } /** * Media Sub Menu */ public function media_sub_menu() { add_media_page( __( 'Image Generator', 'ai-image' ), __( 'Image Generator', 'ai-image' ), 'upload_files', 'bdt-ai-media-tab', function () { ?>
$size ) { if ( ! empty( $size['width'] ) && ! empty( $size['height'] ) ) { add_image_size( $name, (int) $size['width'], (int) $size['height'], ! empty( $size['crop'] ) ); } } } /** * Get all image sizes (WP defaults + custom). */ private function get_all_image_sizes() { global $_wp_additional_image_sizes; $sizes = array(); $default_names = array( 'thumbnail', 'medium', 'medium_large', 'large' ); foreach ( $default_names as $name ) { $w = get_option( $name . '_size_w' ); $h = get_option( $name . '_size_h' ); $crop = get_option( $name . '_crop' ); if ( $w || $h ) { $label = ucfirst( str_replace( '_', ' ', $name ) ); $sizes[] = array( 'label' => $label, 'name' => $name, 'width' => (int) $w, 'height' => (int) $h, 'crop' => ! empty( $crop ), 'source' => 'wordpress', ); } } // Get custom sizes from database to verify they still exist $custom_sizes = get_option( 'bdthemes_ai_image_custom_sizes', array() ); if ( ! is_array( $custom_sizes ) ) $custom_sizes = array(); if ( is_array( $_wp_additional_image_sizes ) ) { foreach ( $_wp_additional_image_sizes as $name => $data ) { // Only include custom sizes that exist in our database if ( isset( $custom_sizes[ $name ] ) ) { $label = ucfirst( str_replace( array( '_', '-' ), ' ', $name ) ); $sizes[] = array( 'label' => $label, 'name' => $name, 'width' => isset( $data['width'] ) ? (int) $data['width'] : 0, 'height' => isset( $data['height'] ) ? (int) $data['height'] : 0, 'crop' => ! empty( $data['crop'] ), 'source' => 'custom', ); } } } return $sizes; } /** * AJAX: add a custom image size. */ public function ajax_add_custom_size() { $this->api_key_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $name = isset( $_POST['name'] ) ? sanitize_title( wp_unslash( $_POST['name'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $width = isset( $_POST['width'] ) ? absint( $_POST['width'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $height = isset( $_POST['height'] ) ? absint( $_POST['height'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $crop = ! empty( $_POST['crop'] ); // Validate name and dimensions if ( empty( $name ) || $width < 1 || $height < 1 ) { wp_send_json_error( array( 'message' => __( 'Invalid name or dimensions.', 'ai-image' ) ) ); } // Check maximum dimensions if ( $width > 1920 ) { wp_send_json_error( array( 'message' => __( 'Width must not exceed 1920 pixels.', 'ai-image' ) ) ); } if ( $height > 3000 ) { wp_send_json_error( array( 'message' => __( 'Height must not exceed 3000 pixels.', 'ai-image' ) ) ); } // Check for reserved WordPress image size names $reserved = array( 'thumbnail', 'medium', 'medium_large', 'large' ); if ( in_array( $name, $reserved, true ) ) { wp_send_json_error( array( 'message' => __( 'This name is reserved by WordPress. Please use a different name.', 'ai-image' ) ) ); } // Check for duplicate custom size names $custom = get_option( 'bdthemes_ai_image_custom_sizes', array() ); if ( ! is_array( $custom ) ) $custom = array(); if ( isset( $custom[ $name ] ) ) { wp_send_json_error( array( 'message' => __( 'A size with this name already exists. Please use a different name.', 'ai-image' ) ) ); } $custom[ $name ] = array( 'width' => $width, 'height' => $height, 'crop' => $crop ); update_option( 'bdthemes_ai_image_custom_sizes', $custom ); add_image_size( $name, $width, $height, $crop ); wp_send_json_success( array( 'message' => __( 'Image size added.', 'ai-image' ), 'sizes' => $this->get_all_image_sizes(), ) ); } /** * AJAX: delete a custom image size. */ public function ajax_delete_custom_size() { $this->api_key_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $name = isset( $_POST['name'] ) ? sanitize_title( wp_unslash( $_POST['name'] ) ) : ''; if ( empty( $name ) ) { wp_send_json_error( array( 'message' => __( 'Invalid name.', 'ai-image' ) ) ); } $custom = get_option( 'bdthemes_ai_image_custom_sizes', array() ); if ( ! is_array( $custom ) ) $custom = array(); if ( ! isset( $custom[ $name ] ) ) { wp_send_json_error( array( 'message' => __( 'Size not found.', 'ai-image' ) ) ); } unset( $custom[ $name ] ); update_option( 'bdthemes_ai_image_custom_sizes', $custom ); wp_send_json_success( array( 'message' => __( 'Image size deleted.', 'ai-image' ), 'sizes' => $this->get_all_image_sizes(), ) ); } /** * Verify nonce and manage_options for API key AJAX handlers. */ private function api_key_ajax_permission_check() { $this->ajax_permission_check( 'manage_options' ); } /** * Verify the nonce and a capability for an AJAX handler. * * @param string $capability Capability the caller must have. */ private function ajax_permission_check( $capability ) { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'ai-image' ) ), 403 ); } if ( ! current_user_can( $capability ) ) { wp_send_json_error( array( 'message' => __( 'Forbidden.', 'ai-image' ) ), 403 ); } } /** * Permission check for the image generation endpoints. * * Generating an image is an authoring action, so it matches the block * toolbar's requirement rather than the administrator-only bar used for * reading and writing API keys. */ private function generation_ajax_permission_check() { $this->ajax_permission_check( 'edit_posts' ); } /** * AJAX: return OpenAI API key (JSON). */ public function ajax_get_openai_key() { $this->api_key_ajax_permission_check(); $key = get_option( 'bdthemes_openai_api_key' ); $key = is_string( $key ) ? trim( $key ) : ''; wp_send_json_success( array( 'api_key' => $key ? $key : null ) ); } /** * AJAX: return Gemini API key (JSON). */ public function ajax_get_gemini_key() { $this->api_key_ajax_permission_check(); $key = get_option( 'bdthemes_gemini_api_key' ); $key = is_string( $key ) ? trim( $key ) : ''; wp_send_json_success( array( 'api_key' => $key ? $key : null ) ); } /** * AJAX: return Pexels API key (JSON). */ public function ajax_get_pexels_key() { $this->api_key_ajax_permission_check(); $key = get_option( 'bdthemes_pexels_api_key' ); $key = is_string( $key ) ? trim( $key ) : ''; wp_send_json_success( array( 'api_key' => $key ? $key : null ) ); } /** * AJAX: return Unsplash API key (JSON). */ public function ajax_get_unsplash_key() { $this->api_key_ajax_permission_check(); $key = get_option( 'bdthemes_unsplash_access_key' ); $key = is_string( $key ) ? trim( $key ) : ''; wp_send_json_success( array( 'api_key' => $key ? $key : null ) ); } /** * AJAX: return Pixabay API key (JSON). */ public function ajax_get_pixabay_key() { $this->api_key_ajax_permission_check(); $key = get_option( 'bdthemes_pixabay_api_key' ); $key = is_string( $key ) ? trim( $key ) : ''; wp_send_json_success( array( 'api_key' => $key ? $key : null ) ); } /** * AJAX: return Giphy API key (JSON). */ public function ajax_get_giphy_key() { $this->api_key_ajax_permission_check(); $key = get_option( 'bdthemes_giphy_api_key' ); $key = is_string( $key ) ? trim( $key ) : ''; wp_send_json_success( array( 'api_key' => $key ? $key : null ) ); } /** * Collect all plugin settings for export. * * @param bool $include_api_keys Whether to include API keys in the export. * @return array */ private function collect_settings_for_export( $include_api_keys = true ) { $provider_ids = array( 'global', 'pexels', 'pixabay', 'unsplash', 'openverse', 'wikimedia', 'giphy', 'openai', 'gemini' ); $settings = array(); if ( $include_api_keys ) { $settings['openai_api_key'] = (string) get_option( 'bdthemes_openai_api_key', '' ); $settings['gemini_api_key'] = (string) get_option( 'bdthemes_gemini_api_key', '' ); $settings['pexels_api_key'] = (string) get_option( 'bdthemes_pexels_api_key', '' ); $settings['unsplash_access_key'] = (string) get_option( 'bdthemes_unsplash_access_key', '' ); $settings['pixabay_api_key'] = (string) get_option( 'bdthemes_pixabay_api_key', '' ); $settings['giphy_api_key'] = (string) get_option( 'bdthemes_giphy_api_key', '' ); } foreach ( $provider_ids as $id ) { $settings[ 'provider_' . $id ] = \bdthemes_ai_image_is_provider_enabled( $id ); } $settings['max_upload_width'] = (int) get_option( 'bdthemes_ai_image_max_upload_width', 1600 ); $settings['max_upload_height'] = (int) get_option( 'bdthemes_ai_image_max_upload_height', 1200 ); $settings['default_provider'] = (string) get_option( 'bdthemes_ai_image_default_provider', 'pexels' ); $settings['image_attribution'] = get_option( 'bdthemes_ai_image_attribution', '0' ) === '1'; $settings['auto_alt_text'] = get_option( 'bdthemes_ai_image_auto_alt_text', '0' ) === '1'; $settings['auto_title'] = get_option( 'bdthemes_ai_image_auto_title', '0' ) === '1'; $settings['hide_media_modal_tab'] = get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) === '1'; $settings['default_view_mode'] = (string) get_option( 'bdthemes_ai_image_default_view_mode', 'grid' ); $settings['items_per_page'] = (int) get_option( 'bdthemes_ai_image_items_per_page', 20 ); $settings['thumbnail_size'] = (string) get_option( 'bdthemes_ai_image_thumbnail_size', 'small' ); $settings['load_more_mode'] = (string) get_option( 'bdthemes_ai_image_load_more_mode', 'manual' ); $settings['provider_order'] = get_option( 'bdthemes_ai_image_provider_order', array() ); $custom_sizes = get_option( 'bdthemes_ai_image_custom_sizes', array() ); $settings['custom_sizes'] = is_array( $custom_sizes ) ? $custom_sizes : array(); if ( ! is_array( $settings['provider_order'] ) ) { $settings['provider_order'] = array(); } return $settings; } /** * Apply dashboard settings from an array (save / import). * * @param array $input Settings payload. * @param bool $apply_api_keys Whether API key fields should be updated. */ private function apply_dashboard_settings( array $input, $apply_api_keys = true ) { if ( $apply_api_keys ) { /* * An empty value is only treated as "clear this key" when the caller confirms * it actually loaded the saved keys first. The dashboard sends empty strings * whenever that load failed, and blindly storing them wipes working * credentials on an unrelated save. */ $allow_clear = ! empty( $input['api_keys_loaded'] ); $api_key_options = array( 'openai_api_key' => 'bdthemes_openai_api_key', 'gemini_api_key' => 'bdthemes_gemini_api_key', 'unsplash_access_key' => 'bdthemes_unsplash_access_key', 'giphy_api_key' => 'bdthemes_giphy_api_key', 'pexels_api_key' => 'bdthemes_pexels_api_key', 'pixabay_api_key' => 'bdthemes_pixabay_api_key', ); foreach ( $api_key_options as $field => $option_name ) { if ( ! isset( $input[ $field ] ) ) { continue; } $value = sanitize_text_field( is_string( $input[ $field ] ) ? trim( $input[ $field ] ) : '' ); if ( '' === $value && ! $allow_clear ) { continue; } update_option( $option_name, $value ); } } $provider_ids = array( 'global', 'pexels', 'pixabay', 'unsplash', 'openverse', 'wikimedia', 'giphy', 'openai', 'gemini' ); foreach ( $provider_ids as $id ) { $key = 'provider_' . $id; if ( array_key_exists( $key, $input ) ) { $val = $input[ $key ]; $enabled = ( $val === true || $val === '1' || $val === 1 ); update_option( 'bdthemes_ai_image_provider_' . $id, $enabled ? '1' : '0' ); } } if ( array_key_exists( 'max_upload_width', $input ) ) { $width = absint( $input['max_upload_width'] ); update_option( 'bdthemes_ai_image_max_upload_width', ( $width >= 1 && $width <= 9999 ) ? $width : 1600 ); } if ( array_key_exists( 'max_upload_height', $input ) ) { $height = absint( $input['max_upload_height'] ); update_option( 'bdthemes_ai_image_max_upload_height', ( $height >= 1 && $height <= 9999 ) ? $height : 1200 ); } if ( array_key_exists( 'default_provider', $input ) ) { update_option( 'bdthemes_ai_image_default_provider', sanitize_text_field( $input['default_provider'] ) ); } if ( array_key_exists( 'image_attribution', $input ) ) { $v = $input['image_attribution']; update_option( 'bdthemes_ai_image_attribution', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' ); } if ( array_key_exists( 'auto_alt_text', $input ) ) { $v = $input['auto_alt_text']; update_option( 'bdthemes_ai_image_auto_alt_text', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' ); } if ( array_key_exists( 'auto_title', $input ) ) { $v = $input['auto_title']; update_option( 'bdthemes_ai_image_auto_title', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' ); } if ( array_key_exists( 'hide_media_modal_tab', $input ) ) { $v = $input['hide_media_modal_tab']; update_option( 'bdthemes_ai_image_hide_media_modal_tab', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' ); } if ( array_key_exists( 'default_view_mode', $input ) ) { $mode = sanitize_text_field( $input['default_view_mode'] ); update_option( 'bdthemes_ai_image_default_view_mode', $mode === 'list' ? 'list' : 'grid' ); } if ( array_key_exists( 'items_per_page', $input ) ) { $items = absint( $input['items_per_page'] ); update_option( 'bdthemes_ai_image_items_per_page', ( $items >= 20 && $items <= 100 ) ? $items : 30 ); } if ( array_key_exists( 'thumbnail_size', $input ) ) { $size = sanitize_text_field( $input['thumbnail_size'] ); $allowed_sizes = array( 'small', 'medium', 'large' ); update_option( 'bdthemes_ai_image_thumbnail_size', in_array( $size, $allowed_sizes, true ) ? $size : 'medium' ); } if ( array_key_exists( 'load_more_mode', $input ) ) { $mode = sanitize_text_field( $input['load_more_mode'] ); $allowed_modes = array( 'auto', 'manual' ); update_option( 'bdthemes_ai_image_load_more_mode', in_array( $mode, $allowed_modes, true ) ? $mode : 'auto' ); } if ( array_key_exists( 'provider_order', $input ) && is_array( $input['provider_order'] ) ) { $valid_ids = array( 'pexels', 'pixabay', 'unsplash', 'openverse', 'wikimedia', 'giphy', 'openai', 'gemini' ); $order = array_values( array_intersect( array_map( 'sanitize_text_field', $input['provider_order'] ), $valid_ids ) ); if ( ! empty( $order ) ) { update_option( 'bdthemes_ai_image_provider_order', $order ); } } } /** * Import custom image sizes from export payload. * * @param mixed $custom_sizes Custom sizes from export file. */ private function apply_custom_sizes_import( $custom_sizes ) { if ( ! is_array( $custom_sizes ) ) { return; } $sanitized = array(); $reserved = array( 'thumbnail', 'medium', 'medium_large', 'large' ); foreach ( $custom_sizes as $name => $size ) { $name = sanitize_title( $name ); if ( empty( $name ) || in_array( $name, $reserved, true ) || ! is_array( $size ) ) { continue; } $width = absint( $size['width'] ?? 0 ); $height = absint( $size['height'] ?? 0 ); if ( $width < 1 || $height < 1 || $width > 1920 || $height > 3000 ) { continue; } $sanitized[ $name ] = array( 'width' => $width, 'height' => $height, 'crop' => ! empty( $size['crop'] ), ); } update_option( 'bdthemes_ai_image_custom_sizes', $sanitized ); } /** * AJAX: export dashboard settings as JSON. */ public function ajax_export_settings() { $this->api_key_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $include_api_keys = ! isset( $_POST['include_api_keys'] ) || $_POST['include_api_keys'] === '1' || $_POST['include_api_keys'] === 'true'; $bundle = array( 'plugin_version' => defined( 'BDT_AI_IMAGE_VERSION' ) ? BDT_AI_IMAGE_VERSION : '', 'exported_at' => gmdate( 'c' ), 'include_api_keys' => (bool) $include_api_keys, 'settings' => $this->collect_settings_for_export( $include_api_keys ), ); wp_send_json_success( array( 'bundle' => $bundle ) ); } /** * AJAX: import dashboard settings from JSON. */ public function ajax_import_settings() { $this->api_key_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- Nonce verified in api_key_ajax_permission_check() $raw = isset( $_POST['data'] ) && is_string( $_POST['data'] ) ? stripslashes( $_POST['data'] ) : ''; if ( $raw === '' ) { wp_send_json_error( array( 'message' => __( 'No import data provided.', 'ai-image' ) ) ); } $bundle = json_decode( $raw, true ); if ( ! is_array( $bundle ) ) { wp_send_json_error( array( 'message' => __( 'Invalid JSON file.', 'ai-image' ) ) ); } $settings = isset( $bundle['settings'] ) && is_array( $bundle['settings'] ) ? $bundle['settings'] : null; if ( ! is_array( $settings ) ) { wp_send_json_error( array( 'message' => __( 'Export file is missing settings.', 'ai-image' ) ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $import_api_keys = ! isset( $_POST['import_api_keys'] ) || $_POST['import_api_keys'] === '1' || $_POST['import_api_keys'] === 'true'; $has_api_keys = isset( $settings['openai_api_key'] ) || isset( $settings['gemini_api_key'] ) || isset( $settings['pexels_api_key'] ) || isset( $settings['unsplash_access_key'] ) || isset( $settings['pixabay_api_key'] ) || isset( $settings['giphy_api_key'] ); $apply_api_keys = $import_api_keys && ( $has_api_keys || ! empty( $bundle['include_api_keys'] ) ); $custom_sizes = $settings['custom_sizes'] ?? null; unset( $settings['custom_sizes'] ); $this->apply_dashboard_settings( $settings, $apply_api_keys ); if ( null !== $custom_sizes ) { $this->apply_custom_sizes_import( $custom_sizes ); } wp_send_json_success( array( 'message' => __( 'Settings imported successfully.', 'ai-image' ) ) ); } /** * AJAX: save dashboard settings (API keys). */ public function ajax_save_settings() { $this->api_key_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- Nonce verified in api_key_ajax_permission_check(), data is JSON decoded and sanitized per field $input = isset( $_POST['data'] ) && is_string( $_POST['data'] ) ? json_decode( stripslashes( $_POST['data'] ), true ) : null; if ( ! is_array( $input ) ) { wp_send_json_error( array( 'message' => __( 'Invalid request.', 'ai-image' ) ) ); } $this->apply_dashboard_settings( $input, true ); wp_send_json_success( array( 'message' => __( 'Settings saved.', 'ai-image' ) ) ); } /** * AJAX: test API key for a provider. */ public function ajax_test_api_key() { $this->api_key_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $provider = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : ''; if ( empty( $provider ) ) { wp_send_json_error( array( 'message' => __( 'Provider required.', 'ai-image' ) ) ); } if ( empty( $api_key ) ) { if ( 'openai' === $provider ) { $api_key = get_option( 'bdthemes_openai_api_key' ); } elseif ( 'unsplash' === $provider ) { $api_key = get_option( 'bdthemes_unsplash_access_key' ); // For Unsplash, if no custom key is saved, use default key for testing if ( empty( trim( $api_key ) ) ) { $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_UNSPLASH_DEFAULT_KEY ); } } elseif ( 'pexels' === $provider ) { $api_key = get_option( 'bdthemes_pexels_api_key' ); // For Pexels, if no custom key is saved, use default key for testing if ( empty( trim( $api_key ) ) ) { $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_PEXELS_DEFAULT_KEY ); } } elseif ( 'pixabay' === $provider ) { $api_key = get_option( 'bdthemes_pixabay_api_key' ); // For Pixabay, if no custom key is saved, use default key for testing if ( empty( trim( $api_key ) ) ) { $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_PIXABAY_DEFAULT_KEY ); } } elseif ( 'giphy' === $provider ) { $api_key = get_option( 'bdthemes_giphy_api_key' ); // For Giphy, if no custom key is saved, use default key for testing if ( empty( trim( $api_key ) ) ) { $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_GIPHY_DEFAULT_KEY ); } } elseif ( 'gemini' === $provider ) { $api_key = get_option( 'bdthemes_gemini_api_key' ); } $api_key = is_string( $api_key ) ? trim( $api_key ) : ''; } // If user provides a key via POST, test that exact key (don't fallback) // The fallback only applies when api_key POST param is completely empty if ( empty( $api_key ) ) { wp_send_json_error( array( 'message' => __( 'No API key provided.', 'ai-image' ) ) ); } $result = $this->test_provider_key( $provider, $api_key ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Key valid.', 'ai-image' ) ) ); } /** * Imagen model ids the generator tries, in order. * * Google retires image models and gates them per account, so a single * hardcoded id breaks for whole classes of keys. Each candidate is tried * until one answers. * * @return string[] Model ids that support the :predict method. */ private function gemini_image_models() { /** * Filters the Imagen models tried, in order. * * @param string[] $models Model ids that support the :predict method. */ return (array) apply_filters( 'bdthemes_ai_image_gemini_models', array( 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'imagen-3.0-generate-002', ) ); } /** * Whether a Gemini models listing contains an image-generation model. * * @param array $body Decoded response from the v1beta/models endpoint. * @return bool */ private function gemini_models_include_imagen( $body ) { if ( empty( $body['models'] ) || ! is_array( $body['models'] ) ) { return false; } $wanted = $this->gemini_image_models(); foreach ( $body['models'] as $model ) { $name = isset( $model['name'] ) ? (string) $model['name'] : ''; if ( '' === $name ) { continue; } // The listing returns "models/"; compare on the id alone. $id = substr( $name, strrpos( $name, '/' ) + 1 ); if ( in_array( $id, $wanted, true ) ) { return true; } // Any other Imagen model the account can call still means generation works. $methods = isset( $model['supportedGenerationMethods'] ) ? (array) $model['supportedGenerationMethods'] : array(); if ( false !== stripos( $id, 'imagen' ) && in_array( 'predict', $methods, true ) ) { return true; } } return false; } /** * Minimal check for provider API key. * * @param string $provider openai|unsplash|giphy * @param string $api_key * @return true|WP_Error */ private function test_provider_key( $provider, $api_key ) { if ( 'openai' === $provider ) { $response = wp_remote_get( 'https://api.openai.com/v1/models', array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key ), 'timeout' => 15, ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); if ( $code === 200 ) { return true; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $msg = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'OpenAI key invalid.', 'ai-image' ); return new \WP_Error( 'openai_test_failed', $msg ); } if ( 'unsplash' === $provider ) { $response = wp_remote_get( 'https://api.unsplash.com/photos?per_page=1', array( 'headers' => array( 'Authorization' => 'Client-ID ' . $api_key ), 'timeout' => 15, ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); if ( $code === 200 ) { return true; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $msg = isset( $body['errors'][0] ) ? $body['errors'][0] : __( 'Unsplash Access key not valid. Please pass a valid Access key.', 'ai-image' ); return new \WP_Error( 'unsplash_test_failed', $msg ); } if ( 'giphy' === $provider ) { $response = wp_remote_get( 'https://api.giphy.com/v1/gifs/trending?api_key=' . urlencode( $api_key ) . '&limit=1', array( 'timeout' => 15 ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); if ( $code === 200 ) { return true; } return new \WP_Error( 'giphy_test_failed', __( 'Giphy API key not valid. Please pass a valid API key.', 'ai-image' ) ); } if ( 'gemini' === $provider ) { $response = wp_remote_get( 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200&key=' . urlencode( $api_key ), array( 'timeout' => 15 ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code === 200 ) { /* * A key that authenticates is not necessarily a key that can * generate an image: Imagen is gated per project. Reporting * "Key valid." here while the generator answers "No Imagen model * is available" left the two screens contradicting each other, * so the test now checks for the capability the plugin needs. */ if ( $this->gemini_models_include_imagen( $body ) ) { return true; } // A truncated list is no proof of absence, so only an exhaustive // page without a match is reported as missing Imagen access. if ( ! empty( $body['nextPageToken'] ) ) { return true; } return new \WP_Error( 'gemini_no_imagen', __( 'Key accepted, but no Imagen model is available for it. Image generation currently requires a Google Cloud project with billing enabled.', 'ai-image' ) ); } $msg = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Gemini key invalid.', 'ai-image' ); return new \WP_Error( 'gemini_test_failed', $msg ); } if ( 'pexels' === $provider ) { /* * A term unlikely to match anything keeps the check cheap while still * proving the key is accepted. It replaces two function declarations * that lived inside this method and fataled with "cannot redeclare" * the second time a key was tested in one request. */ $probe_query = 'aiimg' . wp_generate_password( 12, false, false ); $response = wp_remote_get( 'https://api.pexels.com/v1/search?query=' . urlencode( $probe_query ) . '&per_page=1', array( 'headers' => array( 'Authorization' => $api_key ), 'timeout' => 15, ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); // Check for error responses if ( $code === 401 || $code === 403 ) { return new \WP_Error( 'pexels_test_failed', __( 'Pexels API key not valid. Invalid or unauthorized key.', 'ai-image' ) ); } if ( $code !== 200 ) { return new \WP_Error( 'pexels_test_failed', __( 'Pexels API key not valid. Please pass a valid API key.', 'ai-image' ) ); } // Parse and validate response structure $data = json_decode( $body, true ); // Check JSON decode errors if ( json_last_error() !== JSON_ERROR_NONE ) { return new \WP_Error( 'pexels_test_failed', __( 'Pexels API returned invalid JSON.', 'ai-image' ) ); } // Check for error field in response if ( isset( $data['error'] ) ) { /* translators: %s: error message from Pexels API */ return new \WP_Error( 'pexels_test_failed', sprintf( __( 'Pexels API key not valid: %s', 'ai-image' ), $data['error'] ) ); } // Verify response has the expected photos array structure // A valid API key should return a response with a photos array (even if empty) if ( ! isset( $data['photos'] ) || ! is_array( $data['photos'] ) ) { return new \WP_Error( 'pexels_test_failed', __( 'Pexels API key not valid. Unexpected response format.', 'ai-image' ) ); } return true; } if ( 'pixabay' === $provider ) { $response = wp_remote_get( 'https://pixabay.com/api/?key=' . urlencode( $api_key ) . '&q=nature&per_page=3', array( 'timeout' => 15 ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); if ( $code === 200 ) { return true; } return new \WP_Error( 'pixabay_test_failed', __( 'Pixabay API key not valid. Please pass a valid API key.', 'ai-image' ) ); } return new \WP_Error( 'unknown_provider', __( 'Unknown provider.', 'ai-image' ) ); } /** * AJAX: Generate an OpenAI image. * * The request is made server-side so the API key never reaches the browser. */ public function ajax_generate_openai_image() { $this->generation_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $prompt = isset( $_POST['prompt'] ) ? sanitize_text_field( wp_unslash( $_POST['prompt'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $num_images = isset( $_POST['number_of_images'] ) ? absint( $_POST['number_of_images'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $size = isset( $_POST['size'] ) ? sanitize_text_field( wp_unslash( $_POST['size'] ) ) : '1024x1024'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $quality = isset( $_POST['quality'] ) ? sanitize_text_field( wp_unslash( $_POST['quality'] ) ) : 'medium'; if ( empty( $prompt ) ) { wp_send_json_error( array( 'message' => __( 'Prompt is required.', 'ai-image' ) ) ); } if ( ! in_array( $size, array( '1024x1024', '1024x1536', '1536x1024' ), true ) ) { $size = '1024x1024'; } if ( ! in_array( $quality, array( 'low', 'medium', 'high' ), true ) ) { $quality = 'medium'; } $api_key = get_option( 'bdthemes_openai_api_key' ); $api_key = is_string( $api_key ) ? trim( $api_key ) : ''; if ( empty( $api_key ) ) { wp_send_json_error( array( 'message' => __( 'No OpenAI API key configured.', 'ai-image' ) ) ); } $response = wp_remote_post( 'https://api.openai.com/v1/images/generations', array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, ), 'body' => wp_json_encode( array( 'prompt' => $prompt, 'n' => min( max( 1, $num_images ), 4 ), 'size' => $size, 'model' => 'gpt-image-1', 'quality' => $quality, ) ), 'timeout' => 60, ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'message' => $response->get_error_message() ) ); } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 !== $code ) { $error_msg = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'OpenAI API request failed.', 'ai-image' ); wp_send_json_error( array( 'message' => $error_msg ) ); } $images = array(); $items = isset( $data['data'] ) && is_array( $data['data'] ) ? $data['data'] : array(); $batch_key = time(); foreach ( $items as $index => $item ) { if ( ! empty( $item['url'] ) ) { $url = esc_url_raw( $item['url'] ); } elseif ( ! empty( $item['b64_json'] ) ) { $url = 'data:image/png;base64,' . $item['b64_json']; } else { continue; } $images[] = array( 'url' => $url, 'id' => 'openai-' . $batch_key . '-' . $index, ); } if ( empty( $images ) ) { wp_send_json_error( array( 'message' => __( 'No image returned from OpenAI.', 'ai-image' ) ) ); } wp_send_json_success( array( 'images' => $images ) ); } /** * AJAX: Generate Gemini image (proxy to avoid CORS). */ public function ajax_generate_gemini_image() { $this->generation_ajax_permission_check(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $prompt = isset( $_POST['prompt'] ) ? sanitize_text_field( wp_unslash( $_POST['prompt'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $num_images = isset( $_POST['number_of_images'] ) ? absint( $_POST['number_of_images'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check() $aspect_ratio = isset( $_POST['aspect_ratio'] ) ? sanitize_text_field( wp_unslash( $_POST['aspect_ratio'] ) ) : '1:1'; if ( empty( $prompt ) ) { wp_send_json_error( array( 'message' => __( 'Prompt is required.', 'ai-image' ) ) ); } $api_key = get_option( 'bdthemes_gemini_api_key' ); $api_key = is_string( $api_key ) ? trim( $api_key ) : ''; if ( empty( $api_key ) ) { wp_send_json_error( array( 'message' => __( 'No Gemini API key configured.', 'ai-image' ) ) ); } $models = $this->gemini_image_models(); $body = wp_json_encode( array( 'instances' => array( array( 'prompt' => $prompt ) ), 'parameters' => array( 'sampleCount' => min( max( 1, $num_images ), 4 ), 'aspectRatio' => $aspect_ratio, 'safetyFilterLevel' => 'block_some', 'personGeneration' => 'allow_adult' ) ) ); $last_code = 0; $last_message = __( 'Gemini API request failed.', 'ai-image' ); foreach ( $models as $model ) { $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode( $model ) . ':predict?key=' . urlencode( $api_key ); $response = wp_remote_post( $url, array( 'headers' => array( 'Content-Type' => 'application/json', ), 'body' => $body, 'timeout' => 60, ) ); if ( is_wp_error( $response ) ) { $last_message = $response->get_error_message(); continue; } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 === $code ) { wp_send_json_success( array( 'data' => $data ) ); } $last_code = $code; $last_message = isset( $data['error']['message'] ) ? $data['error']['message'] : $last_message; // 404 means this account cannot see the model; anything else is final. if ( 404 !== $code ) { break; } } if ( 404 === $last_code ) { $last_message = __( 'No Imagen model is available for this Google API key. Image generation currently requires a Google Cloud project with billing enabled.', 'ai-image' ); } elseif ( 429 === $last_code ) { $last_message = __( 'Google rejected the request for exceeding your quota. Imagen has no free-tier quota, so a billed project is required.', 'ai-image' ); } // The upstream payload is logged rather than returned, so internal API // details are not handed to the browser. wp_send_json_error( array( 'message' => $last_message, 'code' => $last_code, ) ); } /** * Init * * @since 1.0.0 */ public function init() { $this->setup_hooks(); } } if ( class_exists( 'BDT_AI_IMG\Plugin' ) ) { \BDT_AI_IMG\Plugin::instance(); }