PluginProbe
Instant AI Image Generator – Create & Import Images / 2.0.1
Instant AI Image Generator – Create & Import Images v2.0.1
2.1.6 2.1.5 2.1.4 trunk 1.5.1 1.5.10 1.5.11 1.5.2 1.5.3 1.5.4 1.5.5 1.5.7 1.5.8 1.5.9 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3
ai-image / plugin.php

plugin.php in Instant AI Image Generator – Create & Import Images 2.0.1, at plugin.php

1,189 lines 43.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main Plugin File
4 */
5
6 namespace BDT_AI_IMG;
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 die;
10 }
11
12 // Load plugin constants
13 require_once plugin_dir_path( __FILE__ ) . 'admin/constants.php';
14
15 /**
16 * The main plugin class
17 */
18 final class Plugin {
19
20 /**
21 * Instance
22 *
23 * @var object
24 * @since 1.0.0
25 */
26 private static $instance;
27
28 /**
29 * Instance
30 *
31 * @return object
32 * @since 1.0.0
33 */
34 public static function instance() {
35 if ( is_null( self::$instance ) ) {
36 self::$instance = new self();
37 self::$instance->init();
38
39 do_action( 'bdthemes_ai_image_init' );
40 }
41 return self::$instance;
42 }
43
44 /**
45 * Admin Styles
46 *
47 * @since 1.0.0
48 */
49 public function enqueue_admin_styles( $hook_suffix ) {
50 $is_plugin_page = ( 'settings_page_ai-image-settings' === $hook_suffix || 'media_page_bdt-ai-media-tab' === $hook_suffix );
51 $media_modal_enabled = get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) !== '1';
52
53 if ( ! $is_plugin_page && ! $media_modal_enabled ) {
54 return;
55 }
56
57 // On plugin pages always load; on other pages only if media modal is enabled
58 if ( $is_plugin_page || $media_modal_enabled ) {
59 wp_register_style( 'ai-image', BDT_AI_IMAGE_URL . 'build/admin/index.css', array(), BDT_AI_IMAGE_VERSION );
60 wp_enqueue_style( 'ai-image' );
61 }
62 }
63
64 /**
65 * Enqueue admin scripts
66 *
67 * @since 1.0.0
68 * @return void
69 */
70 public function enqueue_admin_scripts( $hook_suffix ) {
71 $is_plugin_page = ( 'settings_page_ai-image-settings' === $hook_suffix || 'media_page_bdt-ai-media-tab' === $hook_suffix );
72 $media_modal_enabled = get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) !== '1';
73
74 if ( ! $is_plugin_page && ! $media_modal_enabled ) {
75 return;
76 }
77
78 $asset_file = plugin_dir_path( __FILE__ ) . 'build/admin/index.asset.php';
79
80 if ( ! file_exists( $asset_file ) ) {
81 return;
82 }
83
84 $asset = include $asset_file;
85
86 wp_register_script( 'ai-image', BDT_AI_IMAGE_URL . 'build/admin/index.js', $asset['dependencies'], $asset['version'], true );
87
88 wp_enqueue_script( 'ai-image' );
89
90 $provider_ids = array( 'global', 'pexels', 'pixabay', 'openverse', 'unsplash', 'giphy', 'openai', 'gemini' );
91 $provider_enabled = array();
92 $enabled_by_default = array( 'global', 'pexels', 'pixabay', 'openverse', 'unsplash', 'giphy' );
93 foreach ( $provider_ids as $id ) {
94 $default_value = in_array( $id, $enabled_by_default, true ) ? '1' : '0';
95 $val = get_option( 'bdthemes_ai_image_provider_' . $id, $default_value );
96 $provider_enabled[ $id ] = ( $val === '1' || $val === true );
97 }
98
99 $default_order = array( 'pexels', 'pixabay','openverse', 'unsplash', 'giphy', 'openai', 'gemini' );
100 $saved_order = get_option( 'bdthemes_ai_image_provider_order', $default_order );
101 if ( ! is_array( $saved_order ) || empty( $saved_order ) ) {
102 $saved_order = $default_order;
103 }
104 $provider_order = array_values( array_unique( array_merge( $saved_order, $default_order ) ) );
105
106 $max_w = get_option( 'bdthemes_ai_image_max_upload_width', 1600 );
107 $max_h = get_option( 'bdthemes_ai_image_max_upload_height', 1200 );
108 $items_per_page = get_option( 'bdthemes_ai_image_items_per_page', 20 );
109 $general = array(
110 'max_upload_width' => is_numeric( $max_w ) ? (int) $max_w : 1600,
111 'max_upload_height' => is_numeric( $max_h ) ? (int) $max_h : 1200,
112 'default_provider' => get_option( 'bdthemes_ai_image_default_provider', 'pexels' ),
113 'image_attribution' => get_option( 'bdthemes_ai_image_attribution', '0' ) === '1',
114 'auto_alt_text' => get_option( 'bdthemes_ai_image_auto_alt_text', '0' ) === '1',
115 'auto_title' => get_option( 'bdthemes_ai_image_auto_title', '0' ) === '1',
116 'hide_media_modal_tab' => get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) === '1',
117 'default_view_mode' => get_option( 'bdthemes_ai_image_default_view_mode', 'grid' ),
118 'items_per_page' => is_numeric( $items_per_page ) ? (int) $items_per_page : 20,
119 'thumbnail_size' => get_option( 'bdthemes_ai_image_thumbnail_size', 'small' ),
120 'load_more_mode' => get_option( 'bdthemes_ai_image_load_more_mode', 'manual' ),
121 );
122
123 $script_config = array(
124 'ajax_url' => admin_url( 'admin-ajax.php' ),
125 'nonce' => wp_create_nonce( 'wp_rest' ),
126 'assets_url' => BDT_AI_IMAGE_ASSETS,
127 'rest_url' => rest_url( 'bdthemes/v1/' ),
128 'version' => BDT_AI_IMAGE_VERSION,
129 'settings_url' => admin_url( 'options-general.php?page=ai-image-settings' ),
130 'generator_url' => admin_url( 'upload.php?page=bdt-ai-media-tab' ),
131 'support_url' => 'https://bdthemes.com/support/?utm_source=WordPress_Repository&utm_medium=Plugin_Page&utm_campaign=WordPress_to_Instant_Image_Generator',
132 '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',
133 'provider_enabled' => $provider_enabled,
134 'provider_order' => $provider_order,
135 'general_settings' => $general,
136 'image_sizes' => $this->get_all_image_sizes(),
137 );
138
139 wp_localize_script(
140 'ai-image',
141 'AI_IMAGE_AdminConfig',
142 $script_config
143 );
144
145 // Add media modal integration script on non-plugin pages
146 if ( ! $is_plugin_page && $media_modal_enabled ) {
147 wp_enqueue_media();
148 $this->enqueue_media_modal_script();
149 }
150 }
151
152 /**
153 * Enqueue block editor assets for the Image Generator button
154 *
155 * @since 2.0.0
156 */
157 public function enqueue_block_editor_assets() {
158 // Load the main admin bundle which includes block-toolbar functionality
159 $asset_file = BDT_AI_IMAGE_PATH . 'build/admin/index.asset.php';
160
161 if ( ! file_exists( $asset_file ) ) {
162 return;
163 }
164
165 $asset = include $asset_file;
166
167 // Enqueue the admin script (includes block toolbar)
168 wp_enqueue_script(
169 'ai-image',
170 BDT_AI_IMAGE_URL . 'build/admin/index.js',
171 $asset['dependencies'],
172 $asset['version'],
173 true
174 );
175
176 // Enqueue the admin styles
177 wp_enqueue_style(
178 'ai-image',
179 BDT_AI_IMAGE_URL . 'build/admin/index.css',
180 array(),
181 BDT_AI_IMAGE_VERSION
182 );
183
184 // Check if any provider is enabled and build enabled providers list
185 $provider_ids = array( 'pexels', 'pixabay', 'unsplash', 'openverse', 'giphy', 'openai', 'gemini' );
186 $has_enabled_provider = false;
187 $enabled_providers = array();
188 $enabled_by_default = array( 'pexels', 'pixabay', 'openverse', 'unsplash', 'giphy' );
189
190 foreach ( $provider_ids as $id ) {
191 $default_value = in_array( $id, $enabled_by_default, true ) ? '1' : '0';
192 $enabled = get_option( 'bdthemes_ai_image_provider_' . $id, $default_value ) === '1';
193 $enabled_providers[ $id ] = $enabled;
194 if ( $enabled ) {
195 $has_enabled_provider = true;
196 }
197 }
198
199 // Get provider order from settings
200 $default_order = array( 'pexels', 'pixabay', 'unsplash', 'openverse', 'giphy', 'openai', 'gemini' );
201 $saved_order = get_option( 'bdthemes_ai_image_provider_order', $default_order );
202 if ( ! is_array( $saved_order ) || empty( $saved_order ) ) {
203 $saved_order = $default_order;
204 }
205 $provider_order = array_values( array_unique( array_merge( $saved_order, $default_order ) ) );
206
207 // Localize script with config for block toolbar
208 wp_localize_script(
209 'ai-image',
210 'AI_IMAGE_BlockToolbar',
211 array(
212 'ajax_url' => admin_url( 'admin-ajax.php' ),
213 'nonce' => wp_create_nonce( 'wp_rest' ),
214 'hasEnabledProviders' => $has_enabled_provider,
215 'enabledProviders' => $enabled_providers,
216 'providerOrder' => $provider_order,
217 )
218 );
219 }
220
221 /**
222 * Enqueue the inline script that adds the "Image Generator" tab to the new wp.media modal.
223 */
224 private function enqueue_media_modal_script() {
225 $inline_js = <<<'MEDIAJS'
226 (function( $, wp ){
227 if ( typeof wp === 'undefined' || ! wp.media || ! wp.media.view ) return;
228
229 var TAB_ID = 'ai-image-tab';
230 var TAB_TEXT = 'Image Generator';
231
232 /**
233 * Custom Backbone view that renders our React app.
234 */
235 var AiImageContent = wp.media.View.extend({
236 className: 'ai-image-wrap ai-image-media-modal',
237 initialize: function() {
238 this.$el.attr('style', 'height:100%;overflow-y:auto;padding:16px 20px;background:#f0f0f1;');
239 },
240 render: function() {
241 this.$el.html( '<div id="ai-image-generator-modal"></div>' );
242 var self = this;
243 // Small delay to ensure DOM is ready before React renders
244 setTimeout(function(){
245 var root = self.$el.find('#ai-image-generator-modal')[0];
246 if ( root && window.aiImageRenderApp ) {
247 window.aiImageRenderApp( root );
248 }
249 }, 50);
250 return this;
251 }
252 });
253
254 /**
255 * Add the tab to router for any frame type.
256 */
257 function addTab( routerView ) {
258 routerView.set( TAB_ID, {
259 text: TAB_TEXT,
260 priority: 200
261 });
262 }
263
264 /**
265 * Bind content creation handler to a frame.
266 */
267 function bindContentHandler( frame ) {
268 if ( frame._aiImageBound ) return;
269 frame._aiImageBound = true;
270
271 frame.on( 'content:create:' + TAB_ID, function() {
272 var view = new AiImageContent();
273 frame.content.set( view );
274 });
275 }
276
277 // Override Select frame router (used in Gutenberg, featured image, etc.)
278 var origSelectRouter = wp.media.view.MediaFrame.Select.prototype.browseRouter;
279 wp.media.view.MediaFrame.Select.prototype.browseRouter = function( routerView ) {
280 origSelectRouter.apply( this, arguments );
281 addTab( routerView );
282 bindContentHandler( this );
283 };
284
285 // Override Post frame router (used in classic editor "Add Media")
286 if ( wp.media.view.MediaFrame.Post ) {
287 var origPostRouter = wp.media.view.MediaFrame.Post.prototype.browseRouter;
288 wp.media.view.MediaFrame.Post.prototype.browseRouter = function( routerView ) {
289 origPostRouter.apply( this, arguments );
290 addTab( routerView );
291 bindContentHandler( this );
292 };
293 }
294
295 })( jQuery, wp );
296 MEDIAJS;
297 wp_add_inline_script( 'ai-image', $inline_js, 'after' );
298 }
299
300 /**
301 * Add AI Image settings page under Settings menu.
302 */
303 public function add_settings_menu() {
304 add_submenu_page(
305 'options-general.php',
306 __( 'Image Generator', 'ai-image' ),
307 __( 'Image Generator', 'ai-image' ),
308 'manage_options',
309 'ai-image-settings',
310 array( $this, 'render_settings_page' )
311 );
312 }
313
314 /**
315 * Settings page callback: output root for React.
316 */
317 public function render_settings_page() {
318 echo '<div id="ai-image-dashboard" class="ai-image-dashboard-wrap ai-image-wrap"></div>';
319 }
320
321 /**
322 * Summary of upload_image_to_wp
323 */
324 public function upload_image_to_wp() {
325
326 // Verify nonce for security
327 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp_rest' ) ) {
328 wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
329 }
330
331 // Check if image URL is provided
332 if ( ! isset( $_POST['image_url'] ) || empty( $_POST['image_url'] ) ) {
333 wp_send_json_error( array( 'message' => 'No image URL provided.' ) );
334 }
335
336 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized based on type below
337 $raw_image_url = wp_unslash( $_POST['image_url'] );
338 $image_title = isset( $_POST['image_title'] ) ? sanitize_text_field( wp_unslash( $_POST['image_title'] ) ) : '';
339 $image_author = isset( $_POST['image_author'] ) ? sanitize_text_field( wp_unslash( $_POST['image_author'] ) ) : '';
340 $upload_dir = wp_upload_dir();
341
342 // Check if this is a base64 data URI (e.g., from Gemini)
343 $is_base64 = strpos( $raw_image_url, 'data:image/' ) === 0;
344
345 if ( $is_base64 ) {
346 // Handle base64 data URI
347 // Extract the image data from the data URI
348 // Format: data:image/png;base64,iVBORw0KGgoAAAANS...
349 if ( ! preg_match( '/^data:image\/(\w+);base64,(.+)$/i', $raw_image_url, $matches ) ) {
350 wp_send_json_error( array( 'message' => 'Invalid base64 image data.' ) );
351 }
352
353 $extension = strtolower( $matches[1] );
354 $base64_str = $matches[2];
355
356 // Validate extension
357 if ( ! in_array( $extension, array( 'png', 'jpg', 'jpeg', 'gif', 'webp' ) ) ) {
358 wp_send_json_error( array( 'message' => 'Unsupported image format: ' . $extension ) );
359 }
360
361 // Decode base64 data
362 $image_data = base64_decode( $base64_str );
363
364 if ( $image_data === false ) {
365 wp_send_json_error( array( 'message' => 'Failed to decode base64 image data.' ) );
366 }
367
368 if ( empty( $image_data ) ) {
369 wp_send_json_error( array( 'message' => 'Decoded base64 image data is empty.' ) );
370 }
371
372 } else {
373 // Handle regular URL
374 $image_url = esc_url_raw( $raw_image_url );
375
376 // Fetch the image from the URL
377 $response = wp_remote_get( $image_url, array( 'timeout' => 60 ) );
378
379 // Check if the request failed
380 if ( is_wp_error( $response ) ) {
381 $error_message = $response->get_error_message();
382 wp_send_json_error( array( 'message' => 'Failed to fetch image.', 'error' => $error_message ) );
383 }
384
385 // Check if the status code is 200 (success)
386 $status_code = wp_remote_retrieve_response_code( $response );
387 if ( $status_code !== 200 ) {
388 wp_send_json_error( array( 'message' => 'Failed to fetch image. Status code: ' . $status_code ) );
389 }
390
391 // Get the image data
392 $image_data = wp_remote_retrieve_body( $response );
393 if ( empty( $image_data ) ) {
394 wp_send_json_error( array( 'message' => 'Failed to retrieve image data.' ) );
395 }
396
397 // Detect file extension from content-type or URL
398 $content_type = wp_remote_retrieve_header( $response, 'content-type' );
399 $extension = 'jpg';
400 if ( ! empty( $content_type ) ) {
401 if ( strpos( $content_type, 'png' ) !== false ) {
402 $extension = 'png';
403 } elseif ( strpos( $content_type, 'gif' ) !== false ) {
404 $extension = 'gif';
405 } elseif ( strpos( $content_type, 'webp' ) !== false ) {
406 $extension = 'webp';
407 }
408 }
409 }
410
411 // Build filename from image title if available, otherwise use a domain-based name
412 if ( ! empty( $image_title ) ) {
413 // Sanitize the title into a clean slug for the filename
414 $slug = sanitize_title( $image_title );
415 $slug = preg_replace( '/[^a-z0-9\-]/', '', $slug );
416 // Limit to 80 chars to avoid extremely long filenames
417 if ( strlen( $slug ) > 80 ) {
418 $slug = substr( $slug, 0, 80 );
419 }
420 $slug = rtrim( $slug, '-' );
421 $filename = $slug . '.' . $extension;
422 } else {
423 // Fallback: domain-based random filename
424 $wp_domain_name = get_site_url();
425 $wp_domain_name = str_replace( array( 'http://', 'https://' ), '', strtolower( $wp_domain_name ) );
426 $wp_domain_name = preg_replace( '/[^a-z0-9]/', '-', $wp_domain_name );
427 $filename = $wp_domain_name . '-' . time() . '.' . $extension;
428 }
429
430 // Ensure unique filename in uploads directory
431 $filename = wp_unique_filename( $upload_dir['path'], $filename );
432
433 // Check if the upload directory exists
434 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
435 $file_path = $upload_dir['path'] . '/' . $filename;
436 } else {
437 $file_path = $upload_dir['basedir'] . '/' . $filename;
438 }
439
440 // Write the image data to the file
441 $write_result = file_put_contents( $file_path, $image_data );
442 if ( ! $write_result ) {
443 wp_send_json_error( array( 'message' => 'Failed to save image to disk.' ) );
444 }
445
446 // Check the file type
447 $wp_filetype = wp_check_filetype( $filename, null );
448 if ( ! in_array( $wp_filetype['type'], array( 'image/jpeg', 'image/png', 'image/gif' ) ) ) {
449 wp_send_json_error( array( 'message' => 'Invalid image type.' ) );
450 }
451
452 // Resize image if it exceeds max dimensions
453 $max_width = get_option( 'bdthemes_ai_image_max_upload_width', 1600 );
454 $max_height = get_option( 'bdthemes_ai_image_max_upload_height', 1200 );
455 $max_width = is_numeric( $max_width ) ? (int) $max_width : 1600;
456 $max_height = is_numeric( $max_height ) ? (int) $max_height : 1200;
457
458 if ( $max_width > 0 && $max_height > 0 ) {
459 require_once( ABSPATH . 'wp-admin/includes/image.php' );
460 $image_editor = wp_get_image_editor( $file_path );
461
462 if ( ! is_wp_error( $image_editor ) ) {
463 $size = $image_editor->get_size();
464
465 // Only resize if image exceeds max dimensions
466 if ( $size['width'] > $max_width || $size['height'] > $max_height ) {
467 $image_editor->resize( $max_width, $max_height, false );
468 $saved = $image_editor->save( $file_path );
469 }
470 }
471 }
472
473 // Read user settings for auto-populating metadata
474 $auto_alt_text = get_option( 'bdthemes_ai_image_auto_alt_text', '1' ) === '1';
475 $auto_title = get_option( 'bdthemes_ai_image_auto_title', '1' ) === '1';
476 $image_attribution = get_option( 'bdthemes_ai_image_attribution', '0' ) === '1';
477
478 // Determine the WP attachment title
479 if ( $auto_title && ! empty( $image_title ) ) {
480 $attachment_title = $image_title;
481 } else {
482 // When auto title is off, leave empty so WordPress doesn't show a generated name
483 $attachment_title = $auto_title ? sanitize_file_name( $filename ) : '';
484 }
485
486 // Build caption from author name if attribution is enabled
487 $caption = '';
488 if ( $image_attribution && ! empty( $image_author ) ) {
489 $caption = 'Photo by ' . $image_author;
490 }
491
492 $attachment = array(
493 'post_mime_type' => $wp_filetype['type'],
494 'post_title' => $attachment_title,
495 'post_content' => '',
496 'post_excerpt' => $caption,
497 'post_status' => 'inherit',
498 );
499
500 // Insert the attachment into the media library
501 $attach_id = wp_insert_attachment( $attachment, $file_path );
502 if ( ! $attach_id ) {
503 wp_send_json_error( array( 'message' => 'Failed to upload image.' ) );
504 }
505
506 // Set alternative text from the image title if enabled
507 if ( $auto_alt_text && ! empty( $image_title ) ) {
508 update_post_meta( $attach_id, '_wp_attachment_image_alt', $image_title );
509 }
510
511 // Generate metadata for the attachment
512 require_once( ABSPATH . 'wp-admin/includes/image.php' );
513 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
514 wp_update_attachment_metadata( $attach_id, $attach_data );
515
516 // Return success response
517 wp_send_json_success( array(
518 'attach_id' => $attach_id,
519 'attach_url' => wp_get_attachment_url( $attach_id ),
520 ) );
521 }
522
523 /**
524 * Media Sub Menu
525 */
526 public function media_sub_menu() {
527 add_media_page( 'Image Generator', 'Image Generator', 'read', 'bdt-ai-media-tab', function () {
528 ?>
529 <div class="wrap ai-image-wrap">
530 <div id="ai-image-generator"></div>
531 </div>
532 <?php
533 } );
534 }
535
536 /**
537 * Media upload tabs
538 */
539 public function add_media_tab( $tabs ) {
540 if ( get_option( 'bdthemes_ai_image_hide_media_modal_tab', '0' ) === '1' ) {
541 return $tabs;
542 }
543 $tabs['ai_image'] = __( 'Image Generator 🪄', 'ai-image' );
544 return $tabs;
545 }
546
547 public function media_tab_content() {
548 wp_iframe( array( $this, 'media_tab_content_callback' ) );
549 }
550
551 public function media_tab_content_callback() {
552 ?>
553 <div class="ai-image-wrap ai-image-media-modal">
554 <div id="ai-image-generator"></div>
555 </div>
556 <?php
557 }
558
559 /**
560 * Setup hooks.
561 *
562 * @since 1.0.0
563 */
564 private function setup_hooks() {
565 add_action( 'after_setup_theme', array( $this, 'register_custom_image_sizes' ) );
566 add_action( 'admin_menu', array( $this, 'add_settings_menu' ), 9 );
567 add_action( 'admin_menu', array( $this, 'media_sub_menu' ), 20 );
568 add_filter( 'media_upload_tabs', array( $this, 'add_media_tab' ) );
569 add_action( 'media_upload_ai_image', array( $this, 'media_tab_content' ) );
570
571 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ), 999 );
572 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ), 999 );
573 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
574 add_action( 'wp_ajax_upload_image_to_wp', array( $this, 'upload_image_to_wp' ) );
575 add_action( 'wp_ajax_ai_image_get_openai_key', array( $this, 'ajax_get_openai_key' ) );
576 add_action( 'wp_ajax_ai_image_get_gemini_key', array( $this, 'ajax_get_gemini_key' ) );
577 add_action( 'wp_ajax_ai_image_get_pexels_key', array( $this, 'ajax_get_pexels_key' ) );
578 add_action( 'wp_ajax_ai_image_get_unsplash_key', array( $this, 'ajax_get_unsplash_key' ) );
579 add_action( 'wp_ajax_ai_image_get_pixabay_key', array( $this, 'ajax_get_pixabay_key' ) );
580 add_action( 'wp_ajax_ai_image_get_giphy_key', array( $this, 'ajax_get_giphy_key' ) );
581 add_action( 'wp_ajax_ai_image_save_settings', array( $this, 'ajax_save_settings' ) );
582 add_action( 'wp_ajax_ai_image_test_api_key', array( $this, 'ajax_test_api_key' ) );
583 add_action( 'wp_ajax_ai_image_generate_gemini', array( $this, 'ajax_generate_gemini_image' ) );
584 add_action( 'wp_ajax_ai_image_add_custom_size', array( $this, 'ajax_add_custom_size' ) );
585 add_action( 'wp_ajax_ai_image_delete_custom_size', array( $this, 'ajax_delete_custom_size' ) );
586 }
587
588 /**
589 * Register custom image sizes on init.
590 */
591 public function register_custom_image_sizes() {
592 $custom = get_option( 'bdthemes_ai_image_custom_sizes', array() );
593 if ( ! is_array( $custom ) ) return;
594 foreach ( $custom as $name => $size ) {
595 if ( ! empty( $size['width'] ) && ! empty( $size['height'] ) ) {
596 add_image_size( $name, (int) $size['width'], (int) $size['height'], ! empty( $size['crop'] ) );
597 }
598 }
599 }
600
601 /**
602 * Get all image sizes (WP defaults + custom).
603 */
604 private function get_all_image_sizes() {
605 global $_wp_additional_image_sizes;
606 $sizes = array();
607 $default_names = array( 'thumbnail', 'medium', 'medium_large', 'large' );
608 foreach ( $default_names as $name ) {
609 $w = get_option( $name . '_size_w' );
610 $h = get_option( $name . '_size_h' );
611 $crop = get_option( $name . '_crop' );
612 if ( $w || $h ) {
613 $label = ucfirst( str_replace( '_', ' ', $name ) );
614 $sizes[] = array(
615 'label' => $label,
616 'name' => $name,
617 'width' => (int) $w,
618 'height' => (int) $h,
619 'crop' => ! empty( $crop ),
620 'source' => 'wordpress',
621 );
622 }
623 }
624
625 // Get custom sizes from database to verify they still exist
626 $custom_sizes = get_option( 'bdthemes_ai_image_custom_sizes', array() );
627 if ( ! is_array( $custom_sizes ) ) $custom_sizes = array();
628
629 if ( is_array( $_wp_additional_image_sizes ) ) {
630 foreach ( $_wp_additional_image_sizes as $name => $data ) {
631 // Only include custom sizes that exist in our database
632 if ( isset( $custom_sizes[ $name ] ) ) {
633 $label = ucfirst( str_replace( array( '_', '-' ), ' ', $name ) );
634 $sizes[] = array(
635 'label' => $label,
636 'name' => $name,
637 'width' => isset( $data['width'] ) ? (int) $data['width'] : 0,
638 'height' => isset( $data['height'] ) ? (int) $data['height'] : 0,
639 'crop' => ! empty( $data['crop'] ),
640 'source' => 'custom',
641 );
642 }
643 }
644 }
645 return $sizes;
646 }
647
648 /**
649 * AJAX: add a custom image size.
650 */
651 public function ajax_add_custom_size() {
652 $this->api_key_ajax_permission_check();
653 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
654 $name = isset( $_POST['name'] ) ? sanitize_title( wp_unslash( $_POST['name'] ) ) : '';
655 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
656 $width = isset( $_POST['width'] ) ? absint( $_POST['width'] ) : 0;
657 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
658 $height = isset( $_POST['height'] ) ? absint( $_POST['height'] ) : 0;
659 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
660 $crop = ! empty( $_POST['crop'] );
661
662 // Validate name and dimensions
663 if ( empty( $name ) || $width < 1 || $height < 1 ) {
664 wp_send_json_error( array( 'message' => __( 'Invalid name or dimensions.', 'ai-image' ) ) );
665 }
666
667 // Check maximum dimensions
668 if ( $width > 1920 ) {
669 wp_send_json_error( array( 'message' => __( 'Width must not exceed 1920 pixels.', 'ai-image' ) ) );
670 }
671
672 if ( $height > 3000 ) {
673 wp_send_json_error( array( 'message' => __( 'Height must not exceed 3000 pixels.', 'ai-image' ) ) );
674 }
675
676 // Check for reserved WordPress image size names
677 $reserved = array( 'thumbnail', 'medium', 'medium_large', 'large' );
678 if ( in_array( $name, $reserved, true ) ) {
679 wp_send_json_error( array( 'message' => __( 'This name is reserved by WordPress. Please use a different name.', 'ai-image' ) ) );
680 }
681
682 // Check for duplicate custom size names
683 $custom = get_option( 'bdthemes_ai_image_custom_sizes', array() );
684 if ( ! is_array( $custom ) ) $custom = array();
685 if ( isset( $custom[ $name ] ) ) {
686 wp_send_json_error( array( 'message' => __( 'A size with this name already exists. Please use a different name.', 'ai-image' ) ) );
687 }
688
689 $custom[ $name ] = array( 'width' => $width, 'height' => $height, 'crop' => $crop );
690 update_option( 'bdthemes_ai_image_custom_sizes', $custom );
691 add_image_size( $name, $width, $height, $crop );
692 wp_send_json_success( array(
693 'message' => __( 'Image size added.', 'ai-image' ),
694 'sizes' => $this->get_all_image_sizes(),
695 ) );
696 }
697
698 /**
699 * AJAX: delete a custom image size.
700 */
701 public function ajax_delete_custom_size() {
702 $this->api_key_ajax_permission_check();
703 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
704 $name = isset( $_POST['name'] ) ? sanitize_title( wp_unslash( $_POST['name'] ) ) : '';
705 if ( empty( $name ) ) {
706 wp_send_json_error( array( 'message' => __( 'Invalid name.', 'ai-image' ) ) );
707 }
708 $custom = get_option( 'bdthemes_ai_image_custom_sizes', array() );
709 if ( ! is_array( $custom ) ) $custom = array();
710 if ( ! isset( $custom[ $name ] ) ) {
711 wp_send_json_error( array( 'message' => __( 'Size not found.', 'ai-image' ) ) );
712 }
713 unset( $custom[ $name ] );
714 update_option( 'bdthemes_ai_image_custom_sizes', $custom );
715 wp_send_json_success( array(
716 'message' => __( 'Image size deleted.', 'ai-image' ),
717 'sizes' => $this->get_all_image_sizes(),
718 ) );
719 }
720
721 /**
722 * Verify nonce and manage_options for API key AJAX handlers.
723 */
724 private function api_key_ajax_permission_check() {
725 $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : '';
726 if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
727 wp_send_json_error( array( 'message' => 'Invalid nonce.' ), 403 );
728 }
729 if ( ! current_user_can( 'manage_options' ) ) {
730 wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 );
731 }
732 }
733
734 /**
735 * AJAX: return OpenAI API key (JSON).
736 */
737 public function ajax_get_openai_key() {
738 $this->api_key_ajax_permission_check();
739 $key = get_option( 'bdthemes_openai_api_key' );
740 $key = is_string( $key ) ? trim( $key ) : '';
741 wp_send_json_success( array( 'api_key' => $key ? $key : null ) );
742 }
743
744 /**
745 * AJAX: return Gemini API key (JSON).
746 */
747 public function ajax_get_gemini_key() {
748 $this->api_key_ajax_permission_check();
749 $key = get_option( 'bdthemes_gemini_api_key' );
750 $key = is_string( $key ) ? trim( $key ) : '';
751 wp_send_json_success( array( 'api_key' => $key ? $key : null ) );
752 }
753
754 /**
755 * AJAX: return Pexels API key (JSON).
756 */
757 public function ajax_get_pexels_key() {
758 $this->api_key_ajax_permission_check();
759 $key = get_option( 'bdthemes_pexels_api_key' );
760 $key = is_string( $key ) ? trim( $key ) : '';
761 wp_send_json_success( array( 'api_key' => $key ? $key : null ) );
762 }
763
764 /**
765 * AJAX: return Unsplash API key (JSON).
766 */
767 public function ajax_get_unsplash_key() {
768 $this->api_key_ajax_permission_check();
769 $key = get_option( 'bdthemes_unsplash_access_key' );
770 $key = is_string( $key ) ? trim( $key ) : '';
771 wp_send_json_success( array( 'api_key' => $key ? $key : null ) );
772 }
773
774 /**
775 * AJAX: return Pixabay API key (JSON).
776 */
777 public function ajax_get_pixabay_key() {
778 $this->api_key_ajax_permission_check();
779 $key = get_option( 'bdthemes_pixabay_api_key' );
780 $key = is_string( $key ) ? trim( $key ) : '';
781 wp_send_json_success( array( 'api_key' => $key ? $key : null ) );
782 }
783
784 /**
785 * AJAX: return Giphy API key (JSON).
786 */
787 public function ajax_get_giphy_key() {
788 $this->api_key_ajax_permission_check();
789 $key = get_option( 'bdthemes_giphy_api_key' );
790 $key = is_string( $key ) ? trim( $key ) : '';
791 wp_send_json_success( array( 'api_key' => $key ? $key : null ) );
792 }
793
794 /**
795 * AJAX: save dashboard settings (API keys).
796 */
797 public function ajax_save_settings() {
798 $this->api_key_ajax_permission_check();
799 // 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
800 $input = isset( $_POST['data'] ) && is_string( $_POST['data'] ) ? json_decode( stripslashes( $_POST['data'] ), true ) : null;
801 if ( ! is_array( $input ) ) {
802 wp_send_json_error( array( 'message' => __( 'Invalid request.', 'ai-image' ) ) );
803 }
804 if ( isset( $input['openai_api_key'] ) ) {
805 update_option( 'bdthemes_openai_api_key', sanitize_text_field( is_string( $input['openai_api_key'] ) ? trim( $input['openai_api_key'] ) : '' ) );
806 }
807 if ( isset( $input['gemini_api_key'] ) ) {
808 update_option( 'bdthemes_gemini_api_key', sanitize_text_field( is_string( $input['gemini_api_key'] ) ? trim( $input['gemini_api_key'] ) : '' ) );
809 }
810 if ( isset( $input['unsplash_access_key'] ) ) {
811 update_option( 'bdthemes_unsplash_access_key', sanitize_text_field( is_string( $input['unsplash_access_key'] ) ? trim( $input['unsplash_access_key'] ) : '' ) );
812 }
813 if ( isset( $input['giphy_api_key'] ) ) {
814 update_option( 'bdthemes_giphy_api_key', sanitize_text_field( is_string( $input['giphy_api_key'] ) ? trim( $input['giphy_api_key'] ) : '' ) );
815 }
816 if ( isset( $input['pexels_api_key'] ) ) {
817 update_option( 'bdthemes_pexels_api_key', sanitize_text_field( is_string( $input['pexels_api_key'] ) ? trim( $input['pexels_api_key'] ) : '' ) );
818 }
819 if ( isset( $input['pixabay_api_key'] ) ) {
820 update_option( 'bdthemes_pixabay_api_key', sanitize_text_field( is_string( $input['pixabay_api_key'] ) ? trim( $input['pixabay_api_key'] ) : '' ) );
821 }
822 $provider_ids = array( 'global', 'pexels', 'pixabay', 'unsplash', 'openverse', 'giphy', 'openai', 'gemini' );
823 foreach ( $provider_ids as $id ) {
824 $key = 'provider_' . $id;
825 if ( array_key_exists( $key, $input ) ) {
826 $val = $input[ $key ];
827 $enabled = ( $val === true || $val === '1' || $val === 1 );
828 update_option( 'bdthemes_ai_image_provider_' . $id, $enabled ? '1' : '0' );
829 }
830 }
831 if ( array_key_exists( 'max_upload_width', $input ) ) {
832 update_option( 'bdthemes_ai_image_max_upload_width', absint( $input['max_upload_width'] ) ?: 1600 );
833 }
834 if ( array_key_exists( 'max_upload_height', $input ) ) {
835 update_option( 'bdthemes_ai_image_max_upload_height', absint( $input['max_upload_height'] ) ?: 1200 );
836 }
837 if ( array_key_exists( 'default_provider', $input ) ) {
838 update_option( 'bdthemes_ai_image_default_provider', sanitize_text_field( $input['default_provider'] ) );
839 }
840 if ( array_key_exists( 'image_attribution', $input ) ) {
841 $v = $input['image_attribution'];
842 update_option( 'bdthemes_ai_image_attribution', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' );
843 }
844 if ( array_key_exists( 'auto_alt_text', $input ) ) {
845 $v = $input['auto_alt_text'];
846 update_option( 'bdthemes_ai_image_auto_alt_text', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' );
847 }
848 if ( array_key_exists( 'auto_title', $input ) ) {
849 $v = $input['auto_title'];
850 update_option( 'bdthemes_ai_image_auto_title', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' );
851 }
852 if ( array_key_exists( 'hide_media_modal_tab', $input ) ) {
853 $v = $input['hide_media_modal_tab'];
854 update_option( 'bdthemes_ai_image_hide_media_modal_tab', ( $v === true || $v === '1' || $v === 1 ) ? '1' : '0' );
855 }
856 if ( array_key_exists( 'default_view_mode', $input ) ) {
857 $mode = sanitize_text_field( $input['default_view_mode'] );
858 update_option( 'bdthemes_ai_image_default_view_mode', $mode === 'list' ? 'list' : 'grid' );
859 }
860 if ( array_key_exists( 'items_per_page', $input ) ) {
861 $items = absint( $input['items_per_page'] );
862 update_option( 'bdthemes_ai_image_items_per_page', ( $items >= 20 && $items <= 100 ) ? $items : 30 );
863 }
864 if ( array_key_exists( 'thumbnail_size', $input ) ) {
865 $size = sanitize_text_field( $input['thumbnail_size'] );
866 $allowed_sizes = array( 'small', 'medium', 'large' );
867 update_option( 'bdthemes_ai_image_thumbnail_size', in_array( $size, $allowed_sizes, true ) ? $size : 'medium' );
868 }
869 if ( array_key_exists( 'load_more_mode', $input ) ) {
870 $mode = sanitize_text_field( $input['load_more_mode'] );
871 $allowed_modes = array( 'auto', 'manual' );
872 update_option( 'bdthemes_ai_image_load_more_mode', in_array( $mode, $allowed_modes, true ) ? $mode : 'auto' );
873 }
874 if ( array_key_exists( 'provider_order', $input ) && is_array( $input['provider_order'] ) ) {
875 $valid_ids = array( 'pexels', 'pixabay', 'unsplash', 'openverse', 'giphy', 'openai', 'gemini' );
876 $order = array_values( array_intersect( array_map( 'sanitize_text_field', $input['provider_order'] ), $valid_ids ) );
877 if ( ! empty( $order ) ) {
878 update_option( 'bdthemes_ai_image_provider_order', $order );
879 }
880 }
881 wp_send_json_success( array( 'message' => __( 'Settings saved.', 'ai-image' ) ) );
882 }
883
884 /**
885 * AJAX: test API key for a provider.
886 */
887 public function ajax_test_api_key() {
888 $this->api_key_ajax_permission_check();
889 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
890 $provider = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '';
891 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
892 $api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : '';
893 if ( empty( $provider ) ) {
894 wp_send_json_error( array( 'message' => __( 'Provider required.', 'ai-image' ) ) );
895 }
896 if ( empty( $api_key ) ) {
897 if ( 'openai' === $provider ) {
898 $api_key = get_option( 'bdthemes_openai_api_key' );
899 } elseif ( 'unsplash' === $provider ) {
900 $api_key = get_option( 'bdthemes_unsplash_access_key' );
901 // For Unsplash, if no custom key is saved, use default key for testing
902 if ( empty( trim( $api_key ) ) ) {
903 $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_UNSPLASH_DEFAULT_KEY );
904 }
905 } elseif ( 'pexels' === $provider ) {
906 $api_key = get_option( 'bdthemes_pexels_api_key' );
907 // For Pexels, if no custom key is saved, use default key for testing
908 if ( empty( trim( $api_key ) ) ) {
909 $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_PEXELS_DEFAULT_KEY );
910 }
911 } elseif ( 'pixabay' === $provider ) {
912 $api_key = get_option( 'bdthemes_pixabay_api_key' );
913 // For Pixabay, if no custom key is saved, use default key for testing
914 if ( empty( trim( $api_key ) ) ) {
915 $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_PIXABAY_DEFAULT_KEY );
916 }
917 } elseif ( 'giphy' === $provider ) {
918 $api_key = get_option( 'bdthemes_giphy_api_key' );
919 // For Giphy, if no custom key is saved, use default key for testing
920 if ( empty( trim( $api_key ) ) ) {
921 $api_key = \BDT_AI_IMG\decrypt_key( AI_IMAGE_GIPHY_DEFAULT_KEY );
922 }
923 } elseif ( 'gemini' === $provider ) {
924 $api_key = get_option( 'bdthemes_gemini_api_key' );
925 }
926 $api_key = is_string( $api_key ) ? trim( $api_key ) : '';
927 }
928 // If user provides a key via POST, test that exact key (don't fallback)
929 // The fallback only applies when api_key POST param is completely empty
930 if ( empty( $api_key ) ) {
931 wp_send_json_error( array( 'message' => __( 'No API key provided.', 'ai-image' ) ) );
932 }
933 $result = $this->test_provider_key( $provider, $api_key );
934 if ( is_wp_error( $result ) ) {
935 wp_send_json_error( array( 'message' => $result->get_error_message() ) );
936 }
937 wp_send_json_success( array( 'message' => __( 'Key valid.', 'ai-image' ) ) );
938 }
939
940 /**
941 * Minimal check for provider API key.
942 *
943 * @param string $provider openai|unsplash|giphy
944 * @param string $api_key
945 * @return true|WP_Error
946 */
947 private function test_provider_key( $provider, $api_key ) {
948 if ( 'openai' === $provider ) {
949 $response = wp_remote_get(
950 'https://api.openai.com/v1/models',
951 array(
952 'headers' => array( 'Authorization' => 'Bearer ' . $api_key ),
953 'timeout' => 15,
954 )
955 );
956 if ( is_wp_error( $response ) ) {
957 return $response;
958 }
959 $code = wp_remote_retrieve_response_code( $response );
960 if ( $code === 200 ) {
961 return true;
962 }
963 $body = json_decode( wp_remote_retrieve_body( $response ), true );
964 $msg = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'OpenAI key invalid.', 'ai-image' );
965 return new \WP_Error( 'openai_test_failed', $msg );
966 }
967 if ( 'unsplash' === $provider ) {
968 $response = wp_remote_get(
969 'https://api.unsplash.com/photos?per_page=1',
970 array(
971 'headers' => array( 'Authorization' => 'Client-ID ' . $api_key ),
972 'timeout' => 15,
973 )
974 );
975 if ( is_wp_error( $response ) ) {
976 return $response;
977 }
978 $code = wp_remote_retrieve_response_code( $response );
979 if ( $code === 200 ) {
980 return true;
981 }
982 $body = json_decode( wp_remote_retrieve_body( $response ), true );
983 $msg = isset( $body['errors'][0] ) ? $body['errors'][0] : __( 'Unsplash Access key not valid. Please pass a valid Access key.', 'ai-image' );
984 return new \WP_Error( 'unsplash_test_failed', $msg );
985 }
986 if ( 'giphy' === $provider ) {
987 $response = wp_remote_get(
988 'https://api.giphy.com/v1/gifs/trending?api_key=' . urlencode( $api_key ) . '&limit=1',
989 array( 'timeout' => 15 )
990 );
991 if ( is_wp_error( $response ) ) {
992 return $response;
993 }
994 $code = wp_remote_retrieve_response_code( $response );
995 if ( $code === 200 ) {
996 return true;
997 }
998 return new \WP_Error( 'giphy_test_failed', __( 'Giphy API key not valid. Please pass a valid API key.', 'ai-image' ) );
999 }
1000 if ( 'gemini' === $provider ) {
1001 $response = wp_remote_get(
1002 'https://generativelanguage.googleapis.com/v1beta/models?key=' . urlencode( $api_key ),
1003 array( 'timeout' => 15 )
1004 );
1005 if ( is_wp_error( $response ) ) {
1006 return $response;
1007 }
1008 $code = wp_remote_retrieve_response_code( $response );
1009 if ( $code === 200 ) {
1010 return true;
1011 }
1012 $body = json_decode( wp_remote_retrieve_body( $response ), true );
1013 $msg = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Gemini key invalid.', 'ai-image' );
1014 return new \WP_Error( 'gemini_test_failed', $msg );
1015 }
1016 if ( 'pexels' === $provider ) {
1017 function generateRandomWords($count = 5) {
1018 $letters = 'abcdefghijklmnopqrstuvwxyz';
1019
1020 function createWord($letters) {
1021 $length = wp_rand(4, 8); // word length between 4–8 characters
1022 $word = '';
1023
1024 for ($i = 0; $i < $length; $i++) {
1025 $word .= $letters[wp_rand(0, strlen($letters) - 1)];
1026 }
1027
1028 return $word;
1029 }
1030
1031 $words = [];
1032
1033 for ($i = 0; $i < $count; $i++) {
1034 $words[] = createWord($letters);
1035 }
1036
1037 return $words;
1038 }
1039 $response = wp_remote_get(
1040 'https://api.pexels.com/v1/search?query=' . urlencode(implode(' ', generateRandomWords())) . '&per_page=1',
1041 array(
1042 'headers' => array( 'Authorization' => $api_key ),
1043 'timeout' => 15,
1044 )
1045 );
1046 if ( is_wp_error( $response ) ) {
1047 return $response;
1048 }
1049 $code = wp_remote_retrieve_response_code( $response );
1050 $body = wp_remote_retrieve_body( $response );
1051
1052 // Check for error responses
1053 if ( $code === 401 || $code === 403 ) {
1054 return new \WP_Error( 'pexels_test_failed', __( 'Pexels API key not valid. Invalid or unauthorized key.', 'ai-image' ) );
1055 }
1056
1057 if ( $code !== 200 ) {
1058 return new \WP_Error( 'pexels_test_failed', __( 'Pexels API key not valid. Please pass a valid API key.', 'ai-image' ) );
1059 }
1060
1061 // Parse and validate response structure
1062 $data = json_decode( $body, true );
1063
1064 // Check JSON decode errors
1065 if ( json_last_error() !== JSON_ERROR_NONE ) {
1066 return new \WP_Error( 'pexels_test_failed', __( 'Pexels API returned invalid JSON.', 'ai-image' ) );
1067 }
1068
1069 // Check for error field in response
1070 if ( isset( $data['error'] ) ) {
1071 /* translators: %s: error message from Pexels API */
1072 return new \WP_Error( 'pexels_test_failed', sprintf( __( 'Pexels API key not valid: %s', 'ai-image' ), $data['error'] ) );
1073 }
1074
1075 // Verify response has the expected photos array structure
1076 // A valid API key should return a response with a photos array (even if empty)
1077 if ( ! isset( $data['photos'] ) || ! is_array( $data['photos'] ) ) {
1078 return new \WP_Error( 'pexels_test_failed', __( 'Pexels API key not valid. Unexpected response format.', 'ai-image' ) );
1079 }
1080
1081 return true;
1082 }
1083 if ( 'pixabay' === $provider ) {
1084 $response = wp_remote_get(
1085 'https://pixabay.com/api/?key=' . urlencode( $api_key ) . '&q=nature&per_page=3',
1086 array( 'timeout' => 15 )
1087 );
1088 if ( is_wp_error( $response ) ) {
1089 return $response;
1090 }
1091 $code = wp_remote_retrieve_response_code( $response );
1092 if ( $code === 200 ) {
1093 return true;
1094 }
1095 return new \WP_Error( 'pixabay_test_failed', __( 'Pixabay API key not valid. Please pass a valid API key.', 'ai-image' ) );
1096 }
1097 return new \WP_Error( 'unknown_provider', __( 'Unknown provider.', 'ai-image' ) );
1098 }
1099
1100 /**
1101 * AJAX: Generate Gemini image (proxy to avoid CORS).
1102 */
1103 public function ajax_generate_gemini_image() {
1104 $this->api_key_ajax_permission_check();
1105
1106 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
1107 $prompt = isset( $_POST['prompt'] ) ? sanitize_text_field( wp_unslash( $_POST['prompt'] ) ) : '';
1108 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
1109 $num_images = isset( $_POST['number_of_images'] ) ? absint( $_POST['number_of_images'] ) : 1;
1110 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in api_key_ajax_permission_check()
1111 $aspect_ratio = isset( $_POST['aspect_ratio'] ) ? sanitize_text_field( wp_unslash( $_POST['aspect_ratio'] ) ) : '1:1';
1112
1113 if ( empty( $prompt ) ) {
1114 wp_send_json_error( array( 'message' => __( 'Prompt is required.', 'ai-image' ) ) );
1115 }
1116
1117 $api_key = get_option( 'bdthemes_gemini_api_key' );
1118 $api_key = is_string( $api_key ) ? trim( $api_key ) : '';
1119
1120 if ( empty( $api_key ) ) {
1121 wp_send_json_error( array( 'message' => __( 'No Gemini API key configured.', 'ai-image' ) ) );
1122 }
1123
1124 // Use Imagen 4.0 model which is available in the API
1125 // Available models: imagen-4.0-generate-001 (stable), imagen-4.0-fast-generate-001, imagen-4.0-ultra-generate-001
1126 // These models use :predict method
1127 $url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=' . urlencode( $api_key );
1128
1129 // Correct request format for predict API
1130 $body = wp_json_encode( array(
1131 'instances' => array(
1132 array(
1133 'prompt' => $prompt
1134 )
1135 ),
1136 'parameters' => array(
1137 'sampleCount' => min( max( 1, $num_images ), 4 ),
1138 'aspectRatio' => $aspect_ratio,
1139 'safetyFilterLevel' => 'block_some',
1140 'personGeneration' => 'allow_adult'
1141 )
1142 ) );
1143
1144 $response = wp_remote_post( $url, array(
1145 'headers' => array(
1146 'Content-Type' => 'application/json',
1147 ),
1148 'body' => $body,
1149 'timeout' => 60,
1150 ) );
1151
1152 if ( is_wp_error( $response ) ) {
1153 wp_send_json_error( array( 'message' => $response->get_error_message() ) );
1154 }
1155
1156 $code = wp_remote_retrieve_response_code( $response );
1157 $response_body = wp_remote_retrieve_body( $response );
1158 $data = json_decode( $response_body, true );
1159
1160 if ( $code !== 200 ) {
1161 $error_msg = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Gemini API request failed.', 'ai-image' );
1162 $error_details = isset( $data['error'] ) ? $data['error'] : null;
1163
1164 wp_send_json_error( array(
1165 'message' => $error_msg,
1166 'code' => $code,
1167 'raw_response' => $data,
1168 'error_details' => $error_details
1169 ) );
1170 }
1171
1172 wp_send_json_success( array( 'data' => $data ) );
1173 }
1174
1175 /**
1176 * Init
1177 *
1178 * @since 1.0.0
1179 */
1180 public function init() {
1181 $this->setup_hooks();
1182 }
1183
1184 }
1185
1186 if ( class_exists( 'BDT_AI_IMG\Plugin' ) ) {
1187 \BDT_AI_IMG\Plugin::instance();
1188 }
1189