PluginProbe
Elementor Website Builder – more than just a page builder / 3.26.4
Elementor Website Builder – more than just a page builder v3.26.4
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / modules / ai / module.php

module.php in Elementor Website Builder – more than just a page builder 3.26.4, at modules/ai/module.php

1,398 lines 37.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Modules\Ai;
3
4 use Elementor\Core\Base\Module as BaseModule;
5 use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
6 use Elementor\Modules\Ai\Feature_Intro\Product_Image_Unification_Intro;
7 use Elementor\Plugin;
8 use Elementor\Core\Utils\Collection;
9 use Elementor\Modules\Ai\Connect\Ai;
10 use Elementor\User;
11 use Elementor\Utils;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 class Module extends BaseModule {
18 const HISTORY_TYPE_ALL = 'all';
19 const HISTORY_TYPE_TEXT = 'text';
20 const HISTORY_TYPE_CODE = 'code';
21 const HISTORY_TYPE_IMAGE = 'images';
22 const HISTORY_TYPE_BLOCK = 'blocks';
23 const VALID_HISTORY_TYPES = [
24 self::HISTORY_TYPE_ALL,
25 self::HISTORY_TYPE_TEXT,
26 self::HISTORY_TYPE_CODE,
27 self::HISTORY_TYPE_IMAGE,
28 self::HISTORY_TYPE_BLOCK,
29 ];
30
31 public function get_name() {
32 return 'ai';
33 }
34
35 public function __construct() {
36 parent::__construct();
37
38 if ( is_admin() ) {
39 ( new Preferences() )->register();
40 }
41
42 if ( ! Plugin::$instance->experiments->is_feature_active( 'container' ) ) {
43 return;
44 }
45
46 if ( ! Preferences::is_ai_enabled( get_current_user_id() ) ) {
47 return;
48 }
49
50 add_action( 'elementor/connect/apps/register', function ( ConnectModule $connect_module ) {
51 $connect_module->register_app( 'ai', Ai::get_class_name() );
52 } );
53
54 add_action( 'elementor/ajax/register_actions', function( $ajax ) {
55 $handlers = [
56 'ai_get_user_information' => [ $this, 'ajax_ai_get_user_information' ],
57 'ai_get_remote_config' => [ $this, 'ajax_ai_get_remote_config' ],
58 'ai_get_remote_frontend_config' => [ $this, 'ajax_ai_get_remote_frontend_config' ],
59 'ai_get_completion_text' => [ $this, 'ajax_ai_get_completion_text' ],
60 'ai_get_excerpt' => [ $this, 'ajax_ai_get_excerpt' ],
61 'ai_get_featured_image' => [ $this, 'ajax_ai_get_featured_image' ],
62 'ai_get_edit_text' => [ $this, 'ajax_ai_get_edit_text' ],
63 'ai_get_custom_code' => [ $this, 'ajax_ai_get_custom_code' ],
64 'ai_get_custom_css' => [ $this, 'ajax_ai_get_custom_css' ],
65 'ai_set_get_started' => [ $this, 'ajax_ai_set_get_started' ],
66 'ai_set_status_feedback' => [ $this, 'ajax_ai_set_status_feedback' ],
67 'ai_get_image_prompt_enhancer' => [ $this, 'ajax_ai_get_image_prompt_enhancer' ],
68 'ai_get_text_to_image' => [ $this, 'ajax_ai_get_text_to_image' ],
69 'ai_get_image_to_image' => [ $this, 'ajax_ai_get_image_to_image' ],
70 'ai_get_image_to_image_mask' => [ $this, 'ajax_ai_get_image_to_image_mask' ],
71 'ai_get_image_to_image_mask_cleanup' => [ $this, 'ajax_ai_get_image_to_image_mask_cleanup' ],
72 'ai_get_image_to_image_outpainting' => [ $this, 'ajax_ai_get_image_to_image_outpainting' ],
73 'ai_get_image_to_image_upscale' => [ $this, 'ajax_ai_get_image_to_image_upscale' ],
74 'ai_get_image_to_image_remove_background' => [ $this, 'ajax_ai_get_image_to_image_remove_background' ],
75 'ai_get_image_to_image_replace_background' => [ $this, 'ajax_ai_get_image_to_image_replace_background' ],
76 'ai_upload_image' => [ $this, 'ajax_ai_upload_image' ],
77 'ai_generate_layout' => [ $this, 'ajax_ai_generate_layout' ],
78 'ai_get_layout_prompt_enhancer' => [ $this, 'ajax_ai_get_layout_prompt_enhancer' ],
79 'ai_get_history' => [ $this, 'ajax_ai_get_history' ],
80 'ai_delete_history_item' => [ $this, 'ajax_ai_delete_history_item' ],
81 'ai_toggle_favorite_history_item' => [ $this, 'ajax_ai_toggle_favorite_history_item' ],
82 'ai_get_product_image_unification' => [ $this, 'ajax_ai_get_product_image_unification' ],
83 ];
84
85 foreach ( $handlers as $tag => $callback ) {
86 $ajax->register_ajax_action( $tag, $callback );
87 }
88 } );
89
90 add_action( 'elementor/editor/before_enqueue_scripts', function() {
91 $this->enqueue_main_script();
92 $this->enqueue_layout_script();
93 } );
94
95 add_action( 'elementor/editor/after_enqueue_styles', function() {
96 wp_enqueue_style(
97 'elementor-ai-editor',
98 $this->get_css_assets_url( 'modules/ai/editor' ),
99 [],
100 ELEMENTOR_VERSION
101 );
102 } );
103
104 add_action( 'elementor/preview/enqueue_styles', function() {
105 wp_enqueue_style(
106 'elementor-ai-layout-preview',
107 $this->get_css_assets_url( 'modules/ai/layout-preview' ),
108 [],
109 ELEMENTOR_VERSION
110 );
111 } );
112
113 if ( is_admin() ) {
114 add_action( 'wp_enqueue_media', [ $this, 'enqueue_ai_media_library' ] );
115 add_action( 'admin_head', [ $this, 'enqueue_ai_media_library_upload_screen' ] );
116
117 if ( current_user_can( 'edit_products' ) || current_user_can( 'publish_products' ) ) {
118 add_action( 'admin_init', [ $this, 'enqueue_ai_products_page_scripts' ] );
119 add_action( 'current_screen', [ $this, 'enqueue_ai_single_product_page_scripts' ] );
120 add_action( 'wp_ajax_elementor-ai-get-product-images', [ $this, 'get_product_images_ajax' ] );
121 add_action( 'wp_ajax_elementor-ai-set-product-images', [ $this, 'set_product_images_ajax' ] );
122 Product_Image_Unification_Intro::add_hooks();
123 }
124 }
125
126 add_action( 'enqueue_block_editor_assets', function() {
127 wp_enqueue_script( 'elementor-ai-gutenberg',
128 $this->get_js_assets_url( 'ai-gutenberg' ),
129 [
130 'jquery',
131 'elementor-v2-ui',
132 'elementor-v2-icons',
133 'wp-blocks',
134 'wp-element',
135 'wp-editor',
136 'wp-data',
137 'wp-components',
138 'wp-compose',
139 'wp-i18n',
140 'wp-hooks',
141 'elementor-ai-media-library',
142 ],
143 ELEMENTOR_VERSION, true );
144
145 wp_localize_script(
146 'elementor-ai-gutenberg',
147 'ElementorAiConfig',
148 [
149 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
150 'connect_url' => $this->get_ai_connect_url(),
151 ]
152 );
153
154 wp_set_script_translations( 'elementor-ai-gutenberg', 'elementor' );
155 });
156
157 add_filter( 'elementor/document/save/data', function ( $data ) {
158 return $this->remove_temporary_containers( $data );
159 } );
160 }
161
162 private function get_current_screen() {
163 $is_wc = class_exists( 'WooCommerce' ) && post_type_exists( 'product' );
164 if ( ! $is_wc ) {
165 return 'other';
166 }
167
168 $is_products_page = isset( $_GET['post_type'] ) && 'product' === $_GET['post_type'];
169 if ( $is_products_page ) {
170 return 'wc-products';
171 }
172
173 $screen = get_current_screen();
174 $is_single_product_page = isset( $screen->post_type ) && ( 'product' === $screen->post_type && 'post' === $screen->base );
175 if ( $is_single_product_page ) {
176 return 'wc-single-product';
177 }
178
179 return 'other';
180 }
181
182 public function enqueue_ai_products_page_scripts() {
183 if ( 'wc-products' !== $this->get_current_screen() ) {
184 return;
185 }
186
187 $this->add_wc_scripts();
188
189 }
190
191 public function enqueue_ai_single_product_page_scripts() {
192 if ( 'wc-single-product' !== $this->get_current_screen() ) {
193 return;
194 }
195
196 $this->add_wc_scripts();
197
198 }
199
200 private function add_products_bulk_action( $bulk_actions ) {
201 $bulk_actions['elementor-ai-unify-product-images'] = __( 'Unify with Elementor AI', 'elementor' );
202 return $bulk_actions;
203 }
204
205 public function get_product_images_ajax() {
206 check_ajax_referer( 'elementor-ai-unify-product-images_nonce', 'nonce' );
207
208 $post_ids = isset( $_POST['post_ids'] ) ? array_map( 'intval', $_POST['post_ids'] ) : [];
209 $is_galley_only = isset( $_POST['is_galley_only'] ) && sanitize_text_field( wp_unslash( $_POST['is_galley_only'] ) );
210
211 $image_ids = [];
212
213 foreach ( $post_ids as $post_id ) {
214 if ( $is_galley_only ) {
215 $product = wc_get_product( $post_id );
216 $gallery_image_ids = $product->get_gallery_image_ids();
217 foreach ( $gallery_image_ids as $image_id ) {
218 $image_ids[] = [
219 'productId' => $post_id,
220 'id' => $image_id,
221 'image_url' => wp_get_attachment_url( $image_id ),
222 ];
223 }
224 continue;
225 }
226
227 $image_id = get_post_thumbnail_id( $post_id );
228
229 if ( ! $image_id ) {
230 $product = wc_get_product( $post_id );
231 $gallery_image_ids = $product->get_gallery_image_ids();
232 if ( ! empty( $gallery_image_ids ) ) {
233 $image_id = $gallery_image_ids[0];
234 }
235 }
236
237 $image_ids[] = [
238 'productId' => $post_id,
239 'id' => $image_id ? $image_id : 'No Image',
240 'image_url' => $image_id ? wp_get_attachment_url( $image_id ) : 'No Image',
241 ];
242 }
243
244 wp_send_json_success( [ 'product_images' => array_slice( $image_ids, 0, 10 ) ] );
245
246 wp_die();
247 }
248
249 private function get_attachment_id_by_url( $url ) {
250 $attachments = get_posts( [
251 'post_type' => 'attachment',
252 'meta_query' => [
253 [
254 'key' => '_wp_attached_file',
255 'value' => basename( $url ),
256 'compare' => 'LIKE',
257 ],
258 ],
259 'fields' => 'ids',
260 'numberposts' => 1,
261 ] );
262
263 return ! empty( $attachments ) ? $attachments[0] : null;
264 }
265
266 public function set_product_images_ajax() {
267 check_ajax_referer( 'elementor-ai-unify-product-images_nonce', 'nonce' );
268
269 $product_id = isset( $_POST['productId'] ) ? sanitize_text_field( wp_unslash( $_POST['productId'] ) ) : '';
270 $image_url = isset( $_POST['image_url'] ) ? sanitize_text_field( wp_unslash( $_POST['image_url'] ) ) : '';
271 $image_to_add = isset( $_POST['image_to_add'] ) ? intval( wp_unslash( $_POST['image_to_add'] ) ) : null;
272 $image_to_remove = isset( $_POST['image_to_remove'] ) ? intval( wp_unslash( $_POST['image_to_remove'] ) ) : null;
273 $is_product_gallery = isset( $_POST['is_product_gallery'] ) && sanitize_text_field( wp_unslash( $_POST['is_product_gallery'] ) ) === 'true';
274
275 if ( ! $product_id || ! $image_url ) {
276 throw new \Exception( 'Product ID and Image URL are required' );
277 }
278
279 $product = wc_get_product( $product_id );
280 if ( ! $product ) {
281 throw new \Exception( 'Product not found' );
282 }
283
284 $attachment_id = $this->get_attachment_id_by_url( $image_url );
285 if ( is_wp_error( $attachment_id ) ) {
286 throw new \Exception( 'Image upload failed' );
287 }
288
289 if ( $is_product_gallery ) {
290 $this->update_product_gallery( $product, $image_to_remove, $image_to_add );
291 } else {
292 $product->set_image_id( $attachment_id );
293 $product->save();
294 }
295
296 wp_send_json_success( [
297 'message' => __( 'Image added successfully', 'elementor' ),
298 'refresh' => true,
299 ] );
300 }
301
302 public function enqueue_ai_media_library_upload_screen() {
303 $screen = get_current_screen();
304 if ( ! $screen || 'upload' !== $screen->id ) {
305 return;
306 }
307
308 $this->enqueue_ai_media_library();
309 }
310
311 public function enqueue_ai_media_library() {
312 wp_enqueue_script( 'elementor-ai-media-library',
313 $this->get_js_assets_url( 'ai-media-library' ),
314 [
315 'jquery',
316 'elementor-v2-ui',
317 'elementor-v2-icons',
318 'media-grid',
319 ],
320 ELEMENTOR_VERSION,
321 true
322 );
323
324 wp_localize_script(
325 'elementor-ai-media-library',
326 'ElementorAiConfig',
327 [
328 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
329 'connect_url' => $this->get_ai_connect_url(),
330 ]
331 );
332
333 wp_set_script_translations( 'elementor-ai-media-library', 'elementor' );
334 }
335
336 private function enqueue_main_script() {
337 wp_enqueue_script(
338 'elementor-ai',
339 $this->get_js_assets_url( 'ai' ),
340 [
341 'react',
342 'react-dom',
343 'backbone-marionette',
344 'elementor-web-cli',
345 'wp-date',
346 'elementor-common',
347 'elementor-editor-modules',
348 'elementor-editor-document',
349 'elementor-v2-ui',
350 'elementor-v2-icons',
351 ],
352 ELEMENTOR_VERSION,
353 true
354 );
355
356 $config = [
357 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
358 'connect_url' => $this->get_ai_connect_url(),
359 ];
360
361 if ( $this->get_ai_app()->is_connected() ) {
362 // Use a cached version, don't call the API on every editor load.
363 $config['usage'] = $this->get_ai_app()->get_cached_usage();
364 }
365
366 wp_localize_script(
367 'elementor-ai',
368 'ElementorAiConfig',
369 $config
370 );
371
372 wp_set_script_translations( 'elementor-ai', 'elementor' );
373 }
374
375 private function enqueue_layout_script() {
376 wp_enqueue_script(
377 'elementor-ai-layout',
378 $this->get_js_assets_url( 'ai-layout' ),
379 [
380 'react',
381 'react-dom',
382 'backbone-marionette',
383 'elementor-common',
384 'elementor-web-cli',
385 'elementor-editor-modules',
386 'elementor-ai',
387 'elementor-v2-ui',
388 'elementor-v2-icons',
389 ],
390 ELEMENTOR_VERSION,
391 true
392 );
393
394 wp_set_script_translations( 'elementor-ai-layout', 'elementor' );
395 }
396
397 private function remove_temporary_containers( $data ) {
398 if ( empty( $data['elements'] ) ) {
399 return $data;
400 }
401
402 // If for some reason the document has been saved during an AI Layout session,
403 // ensure that the temporary containers are removed from the data.
404 $data['elements'] = array_filter( $data['elements'], function( $element ) {
405 $is_preview_container = strpos( $element['id'], 'e-ai-preview-container' ) === 0;
406 $is_screenshot_container = strpos( $element['id'], 'e-ai-screenshot-container' ) === 0;
407
408 return ! $is_preview_container && ! $is_screenshot_container;
409 } );
410
411 return $data;
412 }
413
414 private function get_ai_connect_url() {
415 $app = $this->get_ai_app();
416
417 return $app->get_admin_url( 'authorize', [
418 'utm_source' => 'ai-popup',
419 'utm_campaign' => 'connect-account',
420 'utm_medium' => 'wp-dash',
421 'source' => 'generic',
422 ] );
423 }
424
425 public function ajax_ai_get_user_information( $data ) {
426 $app = $this->get_ai_app();
427
428 if ( ! $app->is_connected() ) {
429 return [
430 'is_connected' => false,
431 'connect_url' => $this->get_ai_connect_url(),
432 ];
433 }
434
435 $user_usage = wp_parse_args( $app->get_usage(), [
436 'hasAiSubscription' => false,
437 'usedQuota' => 0,
438 'quota' => 100,
439 ] );
440
441 return [
442 'is_connected' => true,
443 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
444 'usage' => $user_usage,
445 ];
446 }
447
448 public function ajax_ai_get_remote_config() {
449 $app = $this->get_ai_app();
450
451 if ( ! $app->is_connected() ) {
452 return [];
453 }
454
455 return $app->get_remote_config();
456 }
457
458 public function ajax_ai_get_remote_frontend_config( $data ) {
459 $callback = function () use ( $data ) {
460 return $this->get_ai_app()->get_remote_frontend_config( $data );
461 };
462
463 return Utils::get_cached_callback( $callback, 'ai_remote_frontend_config-' . get_current_user_id(), HOUR_IN_SECONDS );
464 }
465
466 public function verify_upload_permissions( $data ) {
467 $referer = wp_get_referer();
468
469 if ( str_contains( $referer, 'wp-admin/upload.php' ) && current_user_can( 'upload_files' ) ) {
470 return;
471 }
472 $this->verify_permissions( $data['editor_post_id'] );
473 }
474
475 private function verify_permissions( $editor_post_id ) {
476 $document = Plugin::$instance->documents->get( $editor_post_id );
477
478 if ( ! $document ) {
479 throw new \Exception( 'Document not found' );
480 }
481
482 if ( $document->is_built_with_elementor() ) {
483 if ( ! $document->is_editable_by_current_user() ) {
484 throw new \Exception( 'Access denied' );
485 }
486 } else {
487 if ( ! current_user_can( 'edit_post', $editor_post_id ) ) {
488 throw new \Exception( 'Access denied' );
489 }
490 }
491 }
492
493 public function ajax_ai_get_image_prompt_enhancer( $data ) {
494 $this->verify_upload_permissions( $data );
495
496 $app = $this->get_ai_app();
497
498 if ( empty( $data['prompt'] ) ) {
499 throw new \Exception( 'Missing prompt' );
500 }
501
502 if ( ! $app->is_connected() ) {
503 throw new \Exception( 'not_connected' );
504 }
505 $request_ids = $this->get_request_ids( $data['payload'] );
506
507 $result = $app->get_image_prompt_enhanced( $data['prompt'], [], $request_ids );
508 $this->throw_on_error( $result );
509
510 return [
511 'text' => $result['text'],
512 'response_id' => $result['responseId'],
513 'usage' => $result['usage'],
514 ];
515 }
516
517 public function ajax_ai_get_completion_text( $data ) {
518 $this->verify_permissions( $data['editor_post_id'] );
519
520 $app = $this->get_ai_app();
521
522 if ( empty( $data['payload']['prompt'] ) ) {
523 throw new \Exception( 'Missing prompt' );
524 }
525
526 if ( ! $app->is_connected() ) {
527 throw new \Exception( 'not_connected' );
528 }
529
530 $context = $this->get_request_context( $data );
531
532 $request_ids = $this->get_request_ids( $data['payload'] );
533
534 $result = $app->get_completion_text( $data['payload']['prompt'], $context, $request_ids );
535 $this->throw_on_error( $result );
536
537 return [
538 'text' => $result['text'],
539 'response_id' => $result['responseId'],
540 'usage' => $result['usage'],
541 ];
542 }
543
544
545 public function ajax_ai_get_excerpt( $data ): array {
546 $app = $this->get_ai_app();
547
548 if ( empty( $data['payload']['content'] ) ) {
549 throw new \Exception( 'Missing content' );
550 }
551
552 if ( ! $app->is_connected() ) {
553 throw new \Exception( 'Not connected' );
554 }
555
556 $context = $this->get_request_context( $data );
557
558 $request_ids = $this->get_request_ids( $data['payload'] );
559
560 $result = $app->get_excerpt( $data['payload']['content'], $context, $request_ids );
561 $this->throw_on_error( $result );
562
563 return [
564 'text' => $result['text'],
565 'response_id' => $result['responseId'],
566 'usage' => $result['usage'],
567 ];
568 }
569
570 public function ajax_ai_get_featured_image( $data ): array {
571 $this->verify_upload_permissions( $data );
572
573 if ( empty( $data['payload']['prompt'] ) ) {
574 throw new \Exception( 'Missing prompt' );
575 }
576
577 $app = $this->get_ai_app();
578
579 if ( ! $app->is_connected() ) {
580 throw new \Exception( 'not_connected' );
581 }
582
583 $context = $this->get_request_context( $data );
584 $request_ids = $this->get_request_ids( $data['payload'] );
585
586 $result = $app->get_featured_image( $data, $context, $request_ids );
587
588 $this->throw_on_error( $result );
589
590 return [
591 'images' => $result['images'],
592 'response_id' => $result['responseId'],
593 'usage' => $result['usage'],
594 ];
595 }
596
597 private function get_ai_app() : Ai {
598 return Plugin::$instance->common->get_component( 'connect' )->get_app( 'ai' );
599 }
600
601 private function get_request_context( $data ) {
602 if ( empty( $data['context'] ) ) {
603 return [];
604 }
605
606 return $data['context'];
607 }
608
609 private function get_request_ids( $data ) {
610 if ( empty( $data['requestIds'] ) ) {
611 return new \stdClass();
612 }
613
614 return $data['requestIds'];
615 }
616
617 public function ajax_ai_get_edit_text( $data ) {
618 $this->verify_permissions( $data['editor_post_id'] );
619
620 $app = $this->get_ai_app();
621
622 if ( empty( $data['payload']['input'] ) ) {
623 throw new \Exception( 'Missing input' );
624 }
625
626 if ( empty( $data['payload']['instruction'] ) ) {
627 throw new \Exception( 'Missing instruction' );
628 }
629
630 if ( ! $app->is_connected() ) {
631 throw new \Exception( 'not_connected' );
632 }
633
634 $context = $this->get_request_context( $data );
635
636 $request_ids = $this->get_request_ids( $data['payload'] );
637
638 $result = $app->get_edit_text( $data, $context, $request_ids );
639 $this->throw_on_error( $result );
640
641 return [
642 'text' => $result['text'],
643 'response_id' => $result['responseId'],
644 'usage' => $result['usage'],
645 ];
646 }
647
648 public function ajax_ai_get_custom_code( $data ) {
649 $app = $this->get_ai_app();
650
651 if ( empty( $data['payload']['prompt'] ) ) {
652 throw new \Exception( 'Missing prompt' );
653 }
654
655 if ( empty( $data['payload']['language'] ) ) {
656 throw new \Exception( 'Missing language' );
657 }
658
659 if ( ! $app->is_connected() ) {
660 throw new \Exception( 'not_connected' );
661 }
662
663 $context = $this->get_request_context( $data );
664
665 $request_ids = $this->get_request_ids( $data['payload'] );
666
667 $result = $app->get_custom_code( $data, $context, $request_ids );
668 $this->throw_on_error( $result );
669
670 return [
671 'text' => $result['text'],
672 'response_id' => $result['responseId'],
673 'usage' => $result['usage'],
674 ];
675 }
676
677 public function ajax_ai_get_custom_css( $data ) {
678 $this->verify_permissions( $data['editor_post_id'] );
679
680 $app = $this->get_ai_app();
681
682 if ( empty( $data['payload']['prompt'] ) ) {
683 throw new \Exception( 'Missing prompt' );
684 }
685
686 if ( empty( $data['payload']['html_markup'] ) ) {
687 $data['html_markup'] = '';
688 }
689
690 if ( empty( $data['payload']['element_id'] ) ) {
691 throw new \Exception( 'Missing element_id' );
692 }
693
694 if ( ! $app->is_connected() ) {
695 throw new \Exception( 'not_connected' );
696 }
697
698 $context = $this->get_request_context( $data );
699 $request_ids = $this->get_request_ids( $data['payload'] );
700
701 $result = $app->get_custom_css( $data, $context, $request_ids );
702 $this->throw_on_error( $result );
703
704 return [
705 'text' => $result['text'],
706 'response_id' => $result['responseId'],
707 'usage' => $result['usage'],
708 ];
709 }
710
711 public function ajax_ai_set_get_started( $data ) {
712 $app = $this->get_ai_app();
713
714 User::set_introduction_viewed( [
715 'introductionKey' => 'ai_get_started',
716 ] );
717
718 return $app->set_get_started();
719 }
720
721 public function ajax_ai_set_status_feedback( $data ) {
722 if ( empty( $data['response_id'] ) ) {
723 throw new \Exception( 'Missing response_id' );
724 }
725
726 $app = $this->get_ai_app();
727
728 if ( ! $app->is_connected() ) {
729 throw new \Exception( 'not_connected' );
730 }
731
732 $app->set_status_feedback( $data['response_id'] );
733
734 return [];
735 }
736
737 public function ajax_ai_get_text_to_image( $data ) {
738 $this->verify_upload_permissions( $data );
739
740 if ( empty( $data['payload']['prompt'] ) ) {
741 throw new \Exception( 'Missing prompt' );
742 }
743
744 $app = $this->get_ai_app();
745
746 if ( ! $app->is_connected() ) {
747 throw new \Exception( 'not_connected' );
748 }
749
750 $context = $this->get_request_context( $data );
751 $request_ids = $this->get_request_ids( $data['payload'] );
752
753 $result = $app->get_text_to_image( $data, $context, $request_ids );
754
755 $this->throw_on_error( $result );
756
757 return [
758 'images' => $result['images'],
759 'response_id' => $result['responseId'],
760 'usage' => $result['usage'],
761 ];
762 }
763
764 public function ajax_ai_get_image_to_image( $data ) {
765 $this->verify_upload_permissions( $data );
766
767 $app = $this->get_ai_app();
768
769 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
770 throw new \Exception( 'Missing Image' );
771 }
772
773 if ( empty( $data['payload']['settings'] ) ) {
774 throw new \Exception( 'Missing prompt settings' );
775 }
776
777 if ( ! $app->is_connected() ) {
778 throw new \Exception( 'not_connected' );
779 }
780
781 $context = $this->get_request_context( $data );
782 $request_ids = $this->get_request_ids( $data['payload'] );
783
784 $result = $app->get_image_to_image( [
785 'prompt' => $data['payload']['prompt'],
786 'promptSettings' => $data['payload']['settings'],
787 'attachment_id' => $data['payload']['image']['id'],
788 ], $context, $request_ids );
789
790 $this->throw_on_error( $result );
791
792 return [
793 'images' => $result['images'],
794 'response_id' => $result['responseId'],
795 'usage' => $result['usage'],
796 ];
797 }
798
799 public function ajax_ai_get_image_to_image_upscale( $data ) {
800 $this->verify_upload_permissions( $data );
801
802 $app = $this->get_ai_app();
803
804 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
805 throw new \Exception( 'Missing Image' );
806 }
807
808 if ( empty( $data['payload']['promptSettings'] ) ) {
809 throw new \Exception( 'Missing prompt settings' );
810 }
811
812 if ( ! $app->is_connected() ) {
813 throw new \Exception( 'not_connected' );
814 }
815
816 $context = $this->get_request_context( $data );
817 $request_ids = $this->get_request_ids( $data['payload'] );
818
819 $result = $app->get_image_to_image_upscale( [
820 'promptSettings' => $data['payload']['promptSettings'],
821 'attachment_id' => $data['payload']['image']['id'],
822 ], $context, $request_ids );
823
824 $this->throw_on_error( $result );
825
826 return [
827 'images' => $result['images'],
828 'response_id' => $result['responseId'],
829 'usage' => $result['usage'],
830 ];
831 }
832
833 public function ajax_ai_get_image_to_image_replace_background( $data ) {
834 $this->verify_upload_permissions( $data );
835
836 $app = $this->get_ai_app();
837
838 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
839 throw new \Exception( 'Missing Image' );
840 }
841
842 if ( empty( $data['payload']['prompt'] ) ) {
843 throw new \Exception( 'Prompt Missing' );
844 }
845
846 if ( ! $app->is_connected() ) {
847 throw new \Exception( 'not_connected' );
848 }
849
850 $context = $this->get_request_context( $data );
851 $request_ids = $this->get_request_ids( $data['payload'] );
852
853 $result = $app->get_image_to_image_replace_background( [
854 'attachment_id' => $data['payload']['image']['id'],
855 'prompt' => $data['payload']['prompt'],
856 ], $context, $request_ids );
857
858 $this->throw_on_error( $result );
859
860 return [
861 'images' => $result['images'],
862 'response_id' => $result['responseId'],
863 'usage' => $result['usage'],
864 ];
865 }
866
867 public function ajax_ai_get_image_to_image_remove_background( $data ) {
868 $this->verify_upload_permissions( $data );
869
870 $app = $this->get_ai_app();
871
872 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
873 throw new \Exception( 'Missing Image' );
874 }
875
876 if ( ! $app->is_connected() ) {
877 throw new \Exception( 'not_connected' );
878 }
879
880 $context = $this->get_request_context( $data );
881 $request_ids = $this->get_request_ids( $data['payload'] );
882 $result = $app->get_image_to_image_remove_background( [
883 'attachment_id' => $data['payload']['image']['id'],
884 ], $context, $request_ids );
885
886 $this->throw_on_error( $result );
887
888 return [
889 'images' => $result['images'],
890 'response_id' => $result['responseId'],
891 'usage' => $result['usage'],
892 ];
893 }
894
895 public function ajax_ai_get_image_to_image_mask( $data ) {
896 $this->verify_upload_permissions( $data );
897
898 $app = $this->get_ai_app();
899
900 if ( empty( $data['payload']['prompt'] ) ) {
901 throw new \Exception( 'Missing prompt' );
902 }
903
904 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
905 throw new \Exception( 'Missing Image' );
906 }
907
908 if ( empty( $data['payload']['settings'] ) ) {
909 throw new \Exception( 'Missing prompt settings' );
910 }
911
912 if ( ! $app->is_connected() ) {
913 throw new \Exception( 'not_connected' );
914 }
915
916 if ( empty( $data['payload']['mask'] ) ) {
917 throw new \Exception( 'Missing Mask' );
918 }
919
920 $context = $this->get_request_context( $data );
921 $request_ids = $this->get_request_ids( $data['payload'] );
922
923 $result = $app->get_image_to_image_mask( [
924 'prompt' => $data['payload']['prompt'],
925 'attachment_id' => $data['payload']['image']['id'],
926 'mask' => $data['payload']['mask'],
927 ], $context, $request_ids );
928
929 $this->throw_on_error( $result );
930
931 return [
932 'images' => $result['images'],
933 'response_id' => $result['responseId'],
934 'usage' => $result['usage'],
935 ];
936 }
937 public function ajax_ai_get_image_to_image_mask_cleanup( $data ) {
938 $this->verify_upload_permissions( $data );
939
940 $app = $this->get_ai_app();
941
942 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
943 throw new \Exception( 'Missing Image' );
944 }
945
946 if ( empty( $data['payload']['settings'] ) ) {
947 throw new \Exception( 'Missing prompt settings' );
948 }
949
950 if ( ! $app->is_connected() ) {
951 throw new \Exception( 'not_connected' );
952 }
953
954 if ( empty( $data['payload']['mask'] ) ) {
955 throw new \Exception( 'Missing Mask' );
956 }
957
958 $context = $this->get_request_context( $data );
959 $request_ids = $this->get_request_ids( $data['payload'] );
960
961 $result = $app->get_image_to_image_mask_cleanup( [
962 'attachment_id' => $data['payload']['image']['id'],
963 'mask' => $data['payload']['mask'],
964 ], $context, $request_ids );
965
966 $this->throw_on_error( $result );
967
968 return [
969 'images' => $result['images'],
970 'response_id' => $result['responseId'],
971 'usage' => $result['usage'],
972 ];
973 }
974
975 public function ajax_ai_get_image_to_image_outpainting( $data ) {
976 $this->verify_upload_permissions( $data );
977
978 $app = $this->get_ai_app();
979
980 if ( ! $app->is_connected() ) {
981 throw new \Exception( 'not_connected' );
982 }
983
984 if ( empty( $data['payload']['mask'] ) ) {
985 throw new \Exception( 'Missing Expended Image' );
986 }
987
988 $context = $this->get_request_context( $data );
989 $request_ids = $this->get_request_ids( $data['payload'] );
990 $result = $app->get_image_to_image_out_painting( [
991 'size' => $data['payload']['size'],
992 'position' => $data['payload']['position'],
993 'mask' => $data['payload']['mask'],
994 'image_base64' => $data['payload']['image_base64'],
995 ], $context, $request_ids );
996
997 $this->throw_on_error( $result );
998
999 return [
1000 'images' => $result['images'],
1001 'response_id' => $result['responseId'],
1002 'usage' => $result['usage'],
1003 ];
1004 }
1005
1006 public function ajax_ai_upload_image( $data ) {
1007 if ( empty( $data['image'] ) ) {
1008 throw new \Exception( 'Missing image data' );
1009 }
1010
1011 $image = $data['image'];
1012
1013 if ( empty( $image['image_url'] ) ) {
1014 throw new \Exception( 'Missing image_url' );
1015 }
1016
1017 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
1018
1019 if ( is_wp_error( $image_data ) ) {
1020 throw new \Exception( $image_data->get_error_message() );
1021 }
1022
1023 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
1024 $app = $this->get_ai_app();
1025 $app->set_used_gallery_image( $image['id'] );
1026 }
1027
1028 return [
1029 'image' => array_merge( $image_data, $data ),
1030 ];
1031 }
1032
1033 public function ajax_ai_generate_layout( $data ) {
1034 $this->verify_permissions( $data['editor_post_id'] );
1035
1036 $app = $this->get_ai_app();
1037
1038 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
1039 throw new \Exception( 'Missing prompt / attachments' );
1040 }
1041
1042 if ( ! $app->is_connected() ) {
1043 throw new \Exception( 'not_connected' );
1044 }
1045
1046 $result = $app->generate_layout(
1047 $data,
1048 $this->prepare_generate_layout_context( $data )
1049 );
1050
1051 if ( is_wp_error( $result ) ) {
1052 $message = $result->get_error_message();
1053
1054 if ( is_array( $message ) ) {
1055 $message = implode( ', ', $message );
1056 throw new \Exception( $message );
1057 }
1058
1059 $this->throw_on_error( $result );
1060 }
1061
1062 $elements = $result['text']['elements'] ?? [];
1063 $base_template_id = $result['baseTemplateId'] ?? null;
1064 $template_type = $result['templateType'] ?? null;
1065
1066 if ( empty( $elements ) || ! is_array( $elements ) ) {
1067 throw new \Exception( 'unknown_error' );
1068 }
1069
1070 if ( 1 === count( $elements ) ) {
1071 $template = $elements[0];
1072 } else {
1073 $template = [
1074 'elType' => 'container',
1075 'elements' => $elements,
1076 'settings' => [
1077 'content_width' => 'full',
1078 'flex_gap' => [
1079 'column' => '0',
1080 'row' => '0',
1081 'unit' => 'px',
1082 ],
1083 'padding' => [
1084 'unit' => 'px',
1085 'top' => '0',
1086 'right' => '0',
1087 'bottom' => '0',
1088 'left' => '0',
1089 'isLinked' => true,
1090 ],
1091 ],
1092 ];
1093 }
1094
1095 return [
1096 'all' => [],
1097 'text' => $template,
1098 'response_id' => $result['responseId'],
1099 'usage' => $result['usage'],
1100 'base_template_id' => $base_template_id,
1101 'template_type' => $template_type,
1102 ];
1103 }
1104
1105 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
1106 $this->verify_permissions( $data['editor_post_id'] );
1107
1108 $app = $this->get_ai_app();
1109
1110 if ( empty( $data['prompt'] ) ) {
1111 throw new \Exception( 'Missing prompt' );
1112 }
1113
1114 if ( ! $app->is_connected() ) {
1115 throw new \Exception( 'not_connected' );
1116 }
1117
1118 $result = $app->get_layout_prompt_enhanced(
1119 $data['prompt'],
1120 $data['enhance_type'],
1121 $this->prepare_generate_layout_context( $data )
1122 );
1123
1124 $this->throw_on_error( $result );
1125
1126 return [
1127 'text' => $result['text'] ?? $data['prompt'],
1128 'response_id' => $result['responseId'] ?? '',
1129 'usage' => $result['usage'] ?? '',
1130 ];
1131 }
1132
1133 private function prepare_generate_layout_context( $data ) {
1134 $request_context = $this->get_request_context( $data );
1135 $kit = Plugin::$instance->kits_manager->get_active_kit();
1136
1137 if ( ! $kit ) {
1138 return $request_context;
1139 }
1140
1141 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
1142
1143 $colors = $kits_data
1144 ->filter( function ( $_, $key ) {
1145 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
1146 } )
1147 ->flatten()
1148 ->filter( function ( $val ) {
1149 return ! empty( $val['_id'] );
1150 } )
1151 ->map( function ( $val ) {
1152 return [
1153 'id' => $val['_id'],
1154 'label' => $val['title'] ?? null,
1155 'value' => $val['color'] ?? null,
1156 ];
1157 } );
1158
1159 $typography = $kits_data
1160 ->filter( function ( $_, $key ) {
1161 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
1162 } )
1163 ->flatten()
1164 ->filter( function ( $val ) {
1165 return ! empty( $val['_id'] );
1166 } )
1167 ->map( function ( $val ) {
1168 $font_size = null;
1169
1170 if ( isset(
1171 $val['typography_font_size']['unit'],
1172 $val['typography_font_size']['size']
1173 ) ) {
1174 $prop = $val['typography_font_size'];
1175
1176 $font_size = 'custom' === $prop['unit']
1177 ? $prop['size']
1178 : $prop['size'] . $prop['unit'];
1179 }
1180
1181 return [
1182 'id' => $val['_id'],
1183 'label' => $val['title'] ?? null,
1184 'value' => [
1185 'family' => $val['typography_font_family'] ?? null,
1186 'weight' => $val['typography_font_weight'] ?? null,
1187 'style' => $val['typography_font_style'] ?? null,
1188 'size' => $font_size,
1189 ],
1190 ];
1191 } );
1192
1193 $request_context['globals'] = [
1194 'colors' => $colors->all(),
1195 'typography' => $typography->all(),
1196 ];
1197
1198 return $request_context;
1199 }
1200
1201 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
1202 if ( ! current_user_can( 'upload_files' ) ) {
1203 throw new \Exception( 'Not Allowed to Upload images' );
1204 }
1205
1206 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
1207
1208 if ( ! empty( $attachment_id['error'] ) ) {
1209 return new \WP_Error( 'upload_error', $attachment_id['error'] );
1210 }
1211
1212 return [
1213 'id' => $attachment_id,
1214 'url' => wp_get_attachment_image_url( $attachment_id, 'full' ),
1215 'alt' => $image_title,
1216 'source' => 'library',
1217 ];
1218 }
1219
1220 public function ajax_ai_get_history( $data ): array {
1221 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
1222
1223 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
1224 throw new \Exception( 'Invalid history type' );
1225 }
1226
1227 $page = sanitize_text_field( $data['page'] ?? 1 );
1228 $limit = sanitize_text_field( $data['limit'] ?? 10 );
1229
1230 $app = $this->get_ai_app();
1231
1232 if ( ! $app->is_connected() ) {
1233 throw new \Exception( 'not_connected' );
1234 }
1235
1236 $context = $this->get_request_context( $data );
1237
1238 $result = $app->get_history_by_type( $type, $page, $limit, $context );
1239
1240 if ( is_wp_error( $result ) ) {
1241 throw new \Exception( $result->get_error_message() );
1242 }
1243
1244 return $result;
1245 }
1246
1247 public function ajax_ai_delete_history_item( $data ): array {
1248 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1249 throw new \Exception( 'Missing id parameter' );
1250 }
1251
1252 $app = $this->get_ai_app();
1253
1254 if ( ! $app->is_connected() ) {
1255 throw new \Exception( 'not_connected' );
1256 }
1257
1258 $context = $this->get_request_context( $data );
1259
1260 $result = $app->delete_history_item( $data['id'], $context );
1261
1262 if ( is_wp_error( $result ) ) {
1263 throw new \Exception( $result->get_error_message() );
1264 }
1265
1266 return [];
1267 }
1268
1269 public function ajax_ai_toggle_favorite_history_item( $data ): array {
1270 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1271 throw new \Exception( 'Missing id parameter' );
1272 }
1273
1274 $app = $this->get_ai_app();
1275
1276 if ( ! $app->is_connected() ) {
1277 throw new \Exception( 'not_connected' );
1278 }
1279
1280 $context = $this->get_request_context( $data );
1281
1282 $result = $app->toggle_favorite_history_item( $data['id'], $context );
1283
1284 if ( is_wp_error( $result ) ) {
1285 throw new \Exception( $result->get_error_message() );
1286 }
1287
1288 return [];
1289 }
1290
1291 public function ajax_ai_get_product_image_unification( $data ): array {
1292 $data['editor_post_id'] = $data['payload']['postId'];
1293 $this->verify_upload_permissions( $data );
1294
1295 $app = $this->get_ai_app();
1296
1297 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
1298 throw new \Exception( __( 'Missing Image', 'elementor' ) );
1299 }
1300
1301 if ( empty( $data['payload']['settings'] ) ) {
1302 throw new \Exception( __( 'Missing prompt settings', 'elementor' ) );
1303 }
1304
1305 if ( ! $app->is_connected() ) {
1306 throw new \Exception( __( 'not_connected', 'elementor' ) );
1307 }
1308
1309 $context = $this->get_request_context( $data );
1310 $request_ids = $this->get_request_ids( $data['payload'] );
1311
1312 $result = $app->get_unify_product_images( [
1313 'promptSettings' => $data['payload']['settings'],
1314 'attachment_id' => $data['payload']['image']['id'],
1315 ], $context, $request_ids );
1316
1317 $this->throw_on_error( $result );
1318
1319 return [
1320 'images' => $result['images'],
1321 'response_id' => $result['responseId'],
1322 'usage' => $result['usage'],
1323 ];
1324 }
1325
1326 /**
1327 * @param mixed $result
1328 */
1329 private function throw_on_error( $result ): void {
1330 if ( is_wp_error( $result ) ) {
1331 wp_send_json_error( [
1332 'message' => $result->get_error_message(),
1333 'extra_data' => $result->get_error_data(),
1334 ] );
1335 }
1336 }
1337
1338 /**
1339 * @return void
1340 */
1341 public function add_wc_scripts(): void {
1342 wp_enqueue_script( 'elementor-ai-unify-product-images',
1343 $this->get_js_assets_url( 'ai-unify-product-images' ),
1344 [
1345 'jquery',
1346 'elementor-v2-ui',
1347 'elementor-v2-icons',
1348 'wp-components',
1349 'elementor-common',
1350 ],
1351 ELEMENTOR_VERSION,
1352 true
1353 );
1354
1355 wp_localize_script(
1356 'elementor-ai-unify-product-images',
1357 'UnifyProductImagesConfig',
1358 [
1359 'get_product_images_url' => admin_url( 'admin-ajax.php' ),
1360 'set_product_images_url' => admin_url( 'admin-ajax.php' ),
1361 'nonce' => wp_create_nonce( 'elementor-ai-unify-product-images_nonce' ),
1362 'placeholder' => ELEMENTOR_ASSETS_URL . 'images/app/ai/product-image-unification-example.gif?' . ELEMENTOR_VERSION,
1363 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
1364 'connect_url' => $this->get_ai_connect_url(),
1365 ]
1366 );
1367
1368 add_filter( 'bulk_actions-edit-product', function ( $data ) {
1369 return $this->add_products_bulk_action( $data );
1370 });
1371
1372 wp_set_script_translations( 'elementor-ai-unify-product-images', 'elementor' );
1373 }
1374
1375 /**
1376 * @param $product
1377 * @param int|null $image_to_remove
1378 * @param int|null $image_to_add
1379 * @return void
1380 * @throws \Exception
1381 */
1382 private function update_product_gallery( $product, ?int $image_to_remove, ?int $image_to_add ): void {
1383 $gallery_image_ids = $product->get_gallery_image_ids();
1384
1385 $index = array_search( $image_to_remove, $gallery_image_ids );
1386 if ( false !== $index ) {
1387 unset( $gallery_image_ids[ $index ] );
1388 }
1389
1390 if ( ! in_array( $image_to_add, $gallery_image_ids ) ) {
1391 $gallery_image_ids[] = $image_to_add;
1392 }
1393
1394 $product->set_gallery_image_ids( $gallery_image_ids );
1395 $product->save();
1396 }
1397 }
1398