PluginProbe
Instant AI Image Generator – Create & Import Images / 2.1.6
Instant AI Image Generator – Create & Import Images v2.1.6
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.1.6, at plugin.php

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