PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.1
Elementor Website Builder – more than just a page builder v4.3.1
4.3.1 4.3.0 4.3.0-beta3 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 All 454 releases
← All changes | modules/mcp/abilities/manage-elements-ability.php +332 -26 4.3.0-beta2 → 4.3.1 View file →
@@ -23,9 +23,11 @@
23 23 use Elementor\Modules\Mcp\Abilities\Build_Composition\Widget_Type_Resolver;
24 24 use Elementor\Modules\Mcp\Abilities\Build_Composition\Xml_Parser;
25 25 use Elementor\Modules\Mcp\Abilities\Utils\Bulk_Operations_Result;
26 26 use Elementor\Modules\Mcp\Abilities\Utils\Document_Mutation_Save;
27 +use Elementor\Modules\Mcp\Abilities\Utils\Tool_Performance_Metrics;
27 28 use Elementor\Modules\Mcp\Abilities\Utils\Widget_Context_Helper;
29 +use Elementor\Modules\Mcp\Events\Mcp_Event_Dispatcher;
28 30 use Elementor\Modules\Variables\Module as Variables_Module;
29 31 use Elementor\Modules\Variables\Services\Batch_Operations\Batch_Processor;
30 32 use Elementor\Modules\Variables\Services\Variables_Service;
31 33 use Elementor\Modules\Variables\Storage\Variables_Repository;
@@ -138,25 +140,33 @@
138 140 );
139 141 }
140 142
141 143 public function execute( $input = [] ) {
142 - $input = is_array( $input ) ? $input : [];
144 + $started_at = hrtime( true );
145 + $input = is_array( $input ) ? $input : [];
146 + $post_id = isset( $input['post_id'] ) ? (int) $input['post_id'] : 0;
147 + $operations = $input['operations'] ?? null;
143 148
144 149 if ( empty( $input['post_id'] ) ) {
145 - return $this->bad_request( __( 'post_id is required.', 'elementor' ) );
150 + $error = $this->bad_request( __( 'post_id is required.', 'elementor' ) );
151 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, null, $error );
152 + return $error;
146 153 }
147 154
148 - $operations = $input['operations'] ?? null;
149 155 if ( ! is_array( $operations ) ) {
150 - return $this->bad_request( __( 'operations array is required.', 'elementor' ) );
156 + $error = $this->bad_request( __( 'operations array is required.', 'elementor' ) );
157 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, null, $error );
158 + return $error;
151 159 }
152 160
153 161 if ( empty( $operations ) ) {
154 - return $this->bad_request( __( 'operations must not be empty.', 'elementor' ) );
162 + $error = $this->bad_request( __( 'operations must not be empty.', 'elementor' ) );
163 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, null, $error );
164 + return $error;
155 165 }
156 166
157 167 if ( count( $operations ) > self::MAX_BATCH_SIZE ) {
158 - return new \WP_Error(
168 + $error = new \WP_Error(
159 169 'batch_size_exceeded',
160 170 sprintf(
161 171 /* translators: %d: maximum operations per request */
162 172 __( 'Maximum %d operations per request.', 'elementor' ),
@@ -166,32 +176,39 @@
166 176 'status' => \WP_Http::BAD_REQUEST,
167 177 'max_allowed' => self::MAX_BATCH_SIZE,
168 178 ]
169 179 );
180 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, null, $error, $operations );
181 + return $error;
170 182 }
171 183
172 - $post_id = (int) $input['post_id'];
173 -
174 184 if ( ! current_user_can( 'edit_post', $post_id ) ) {
175 - return new \WP_Error(
185 + $error = new \WP_Error(
176 186 'elementor_forbidden',
177 187 __( 'You do not have permission to edit this post.', 'elementor' ),
178 188 [ 'status' => \WP_Http::FORBIDDEN ]
179 189 );
190 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, null, $error, $operations );
191 + return $error;
180 192 }
181 193
182 194 $document = $this->resolve_document( $post_id );
183 195 if ( is_wp_error( $document ) ) {
196 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, null, $document, $operations );
184 197 return $document;
185 198 }
186 199
187 - return $this->handle_bulk( $document, $operations );
200 + return $this->handle_bulk( $document, $operations, $started_at );
188 201 }
189 202
190 - private function handle_bulk( Document $document, array $operations ): array {
191 - $results = new Bulk_Operations_Result();
192 - $tree = $this->get_tree( $document );
193 - $any_change = false;
203 + private function handle_bulk( Document $document, array $operations, int $started_at ): array {
204 + $results = new Bulk_Operations_Result();
205 + $tree = $this->get_tree( $document );
206 + $any_change = false;
207 + $pending_events = [];
208 + $all_warning_codes = [];
209 + $class_attachments = 0;
210 + $interactions_count = 0;
194 211
195 212 foreach ( $operations as $index => $operation ) {
196 213 $index = (int) $index;
197 214
@@ -222,15 +239,26 @@
222 239 if ( ! empty( $outcome['warnings'] ) ) {
223 240 $extra['warnings'] = $outcome['warnings'];
224 241 }
225 242 $results->add_success( $index, $action, $extra );
243 +
244 + $all_warning_codes = array_merge( $all_warning_codes, $outcome['warning_codes'] ?? [] );
245 + $class_attachments += count( $outcome['event_metadata']['applied_classes'] ?? [] );
246 + $interactions_count += count( $outcome['event_metadata']['interactions_events'] ?? [] );
247 +
248 + if ( ! empty( $outcome['event_metadata'] ) ) {
249 + $pending_events[] = [ 'action' => $action ] + $outcome['event_metadata'];
250 + }
226 251 }
227 252
228 - $response = $results->to_array();
253 + $response = $results->to_array();
229 254 $response['post_id'] = (int) $document->get_main_id();
255 + $post_id = (int) $document->get_main_id();
230 256
231 257 if ( ! $any_change ) {
232 - return $this->with_edit_url( $response, $document );
258 + $response = $this->with_edit_url( $response, $document );
259 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, $document, null, $operations, $response, $all_warning_codes, $class_attachments, $interactions_count );
260 + return $response;
233 261 }
234 262
235 263 $save_result = Document_Mutation_Save::elements_preserving_live_status( $this->get_mutator(), $document, $tree );
236 264 if ( is_wp_error( $save_result ) ) {
@@ -235,18 +263,22 @@
235 263 $save_result = Document_Mutation_Save::elements_preserving_live_status( $this->get_mutator(), $document, $tree );
236 264 if ( is_wp_error( $save_result ) ) {
237 265 $response['status'] = 'error';
238 266 $response['save_error'] = $save_result->get_error_message();
239 -
240 - return $this->with_edit_url( $response, $document );
267 + $response = $this->with_edit_url( $response, $document );
268 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, $document, $save_result, $operations, $response, $all_warning_codes, 0, 0 );
269 + return $response;
241 270 }
242 271
243 272 Plugin::$instance->files_manager->clear_cache();
244 273
274 + $this->emit_update_events( $pending_events, $tree );
275 +
245 276 $saved_post = $save_result->get_post();
246 277 $response['version'] = $saved_post ? $saved_post->post_modified_gmt : current_time( 'mysql', true );
247 -
248 - return $this->with_edit_url( $response, $document );
278 + $response = $this->with_edit_url( $response, $document );
279 + $this->emit_mcp_manage_elements_executed( $started_at, $post_id, $document, null, $operations, $response, $all_warning_codes, $class_attachments, $interactions_count );
280 + return $response;
249 281 }
250 282
251 283 private function with_edit_url( array $response, Document $document ): array {
252 284 $response['edit_url'] = $document->get_edit_url();
@@ -320,12 +352,14 @@
320 352
321 353 return [
322 354 'tree' => $new_tree,
323 355 'warnings' => [],
356 + 'warning_codes' => [],
324 357 ];
325 358 }
326 359
327 360 private function apply_duplicate( array $tree, string $element_id ) {
361 + $source_node = $this->get_mutator()->find_by_id( $tree, $element_id );
328 362 $new_tree = $this->get_mutator()->duplicate( $tree, $element_id );
329 363 if ( is_wp_error( $new_tree ) ) {
330 364 return $this->to_public_error( $new_tree );
331 365 }
@@ -332,11 +366,35 @@
332 366
333 367 return [
334 368 'tree' => $new_tree,
335 369 'warnings' => [],
370 + 'warning_codes' => [],
371 + 'event_metadata' => [
372 + 'duplicated_element_types' => is_array( $source_node ) ? $this->collect_element_types( $source_node ) : [],
373 + ],
336 374 ];
337 375 }
338 376
377 + private function collect_element_types( array $node ): array {
378 + $types = [];
379 + $stack = [ $node ];
380 +
381 + while ( ! empty( $stack ) ) {
382 + $current = array_pop( $stack );
383 + $type = $current['widgetType'] ?? $current['elType'] ?? '';
384 +
385 + if ( '' !== $type ) {
386 + $types[] = $type;
387 + }
388 +
389 + foreach ( $current['elements'] ?? [] as $child ) {
390 + $stack[] = $child;
391 + }
392 + }
393 +
394 + return $types;
395 + }
396 +
339 397 private function apply_move( array $tree, string $element_id, array $operation ) {
340 398 $new_parent_id = $operation['new_parent_id'] ?? '';
341 399 if ( ! is_string( $new_parent_id ) || '' === $new_parent_id ) {
342 400 return new \WP_Error( 'invalid_input', __( 'new_parent_id is required for action=move.', 'elementor' ) );
@@ -351,8 +409,9 @@
351 409
352 410 return [
353 411 'tree' => $new_tree,
354 412 'warnings' => [],
413 + 'warning_codes' => [],
355 414 ];
356 415 }
357 416
358 417 private function apply_update( Document $document, array $tree, string $element_id, array $operation ) {
@@ -395,10 +454,14 @@
395 454 return $widget_config;
396 455 }
397 456 $widget_configs = [ $element_type => $widget_config ];
398 457
399 - $variables_service = $this->create_variables_service();
400 - $warnings = [];
458 + $variables_service = $this->create_variables_service();
459 + $warnings = [];
460 + $warning_codes = [];
461 + $applied_classes = [];
462 + $variable_connections = [];
463 + $interactions_events = [];
401 464
402 465 if ( null !== $interactions ) {
403 466 if ( ! is_array( $interactions ) ) {
404 467 return new \WP_Error( 'invalid_input', __( 'interactions must be an array of interaction items.', 'elementor' ) );
@@ -409,8 +472,11 @@
409 472 __( 'Interactions experiment is not active. Interactions were not applied.', 'elementor' ),
410 473 [ 'status' => \WP_Http::BAD_REQUEST ]
411 474 );
412 475 }
476 +
477 + $previous_items = $node_snapshot['interactions']['items'] ?? [];
478 +
413 479 $interactions_applier = new Interactions_Applier( $this->get_plain_values_resolver() );
414 480 $interactions_result = $interactions_applier->apply( $index, [ $element_id => $interactions ] );
415 481 if ( $interactions_result['error'] ) {
416 482 return $interactions_result['error'];
@@ -415,8 +481,10 @@
415 481 if ( $interactions_result['error'] ) {
416 482 return $interactions_result['error'];
417 483 }
418 484 $warnings = array_merge( $warnings, $interactions_result['warnings'] );
485 +
486 + $interactions_events = $this->build_interactions_events( (string) $element_type, $previous_items, $interactions );
419 487 }
420 488
421 489 if ( ! empty( $settings ) ) {
422 490 if ( Element_Config_Applier::COMPONENT_INSTANCE_WIDGET_TYPE === $element_type ) {
@@ -435,9 +503,10 @@
435 503 );
436 504 if ( $config_result['error'] ) {
437 505 return $config_result['error'];
438 506 }
439 - $warnings = array_merge( $warnings, $config_result['warnings'] );
507 + $warnings = array_merge( $warnings, $config_result['warnings'] );
508 + $warning_codes = array_merge( $warning_codes, $config_result['warning_codes'] ?? [] );
440 509 }
441 510 }
442 511
443 512 if ( $has_classes ) {
@@ -448,8 +517,9 @@
448 517 $class_error = $class_applier->apply( $index, [ $element_id => $classes ] );
449 518 if ( $class_error ) {
450 519 return $class_error;
451 520 }
521 + $applied_classes = $classes;
452 522 }
453 523
454 524 if ( $has_style ) {
455 525 $style_applier = new Style_Applier( $this->create_css_converter( $variables_service ), $this->get_active_breakpoints() );
@@ -456,17 +526,116 @@
456 526 $style_result = $style_applier->apply( $index, [ $element_id => $style ], $style_apply_mode, $widget_configs );
457 527 if ( $style_result['error'] ) {
458 528 return $style_result['error'];
459 529 }
460 - $warnings = array_merge( $warnings, $style_result['warnings'] );
530 + $warnings = array_merge( $warnings, $style_result['warnings'] );
531 + $warning_codes = array_merge( $warning_codes, $style_result['warning_codes'] ?? [] );
532 + $variable_connections = $style_result['variable_connections'][ $element_id ] ?? [];
461 533 }
462 534
463 535 return [
464 - 'tree' => $tree,
465 - 'warnings' => $warnings,
536 + 'tree' => $tree,
537 + 'warnings' => $warnings,
538 + 'warning_codes' => $warning_codes,
539 + 'event_metadata' => [
540 + 'element_id' => $element_id,
541 + 'element_type' => (string) $element_type,
542 + 'applied_classes' => $applied_classes,
543 + 'variable_connections' => $variable_connections,
544 + 'interactions_events' => $interactions_events,
545 + ],
466 546 ];
467 547 }
468 548
549 + protected function build_interactions_events( string $element_type, array $previous_items, array $new_items ): array {
550 + if ( [] === $new_items ) {
551 + return [
552 + [
553 + 'event_name' => 'interactions_cleared',
554 + 'payload' => [
555 + 'affected_element_type' => $element_type,
556 + 'target_value' => count( $previous_items ),
557 + ],
558 + ],
559 + ];
560 + }
561 +
562 + $previous_by_id = [];
563 + foreach ( $previous_items as $item ) {
564 + if ( ! is_array( $item ) ) {
565 + continue;
566 + }
567 +
568 + $id = $this->extract_interaction_id( $item );
569 + if ( null !== $id ) {
570 + $previous_by_id[ $id ] = true;
571 + }
572 + }
573 +
574 + $events = [];
575 +
576 + foreach ( $new_items as $item ) {
577 + if ( ! is_array( $item ) ) {
578 + continue;
579 + }
580 +
581 + $id = $item['interaction_id'] ?? '';
582 + $is_update = is_string( $id ) && '' !== $id && isset( $previous_by_id[ $id ] );
583 + $event_name = $is_update ? 'interaction_updated' : 'interaction_created';
584 +
585 + $events[] = [
586 + 'event_name' => $event_name,
587 + 'payload' => [
588 + 'affected_element_type' => $element_type,
589 + 'interaction_trigger' => $this->stringify_interaction_field( $item['trigger'] ?? '' ),
590 + 'interaction_effect' => $this->stringify_interaction_field( $item['animation'] ?? '' ),
591 + ],
592 + ];
593 + }
594 +
595 + return $events;
596 + }
597 +
598 + private function extract_interaction_id( array $item ): ?string {
599 + if ( isset( $item['interaction_id'] ) && is_string( $item['interaction_id'] ) && '' !== $item['interaction_id'] ) {
600 + return $item['interaction_id'];
601 + }
602 +
603 + $nested = $item['value']['interaction_id']['value'] ?? $item['interaction_id']['value'] ?? null;
604 +
605 + if ( is_string( $nested ) && '' !== $nested ) {
606 + return $nested;
607 + }
608 +
609 + return null;
610 + }
611 +
612 + private function stringify_interaction_field( $value ): string {
613 + if ( is_string( $value ) ) {
614 + return $value;
615 + }
616 +
617 + if ( is_array( $value ) ) {
618 + if ( isset( $value['effect'] ) && is_string( $value['effect'] ) ) {
619 + return $value['effect'];
620 + }
621 +
622 + $nested_effect = $value['value']['effect']['value'] ?? $value['effect']['value'] ?? null;
623 + if ( is_string( $nested_effect ) && '' !== $nested_effect ) {
624 + return $nested_effect;
625 + }
626 +
627 + if ( isset( $value['value'] ) && ( is_string( $value['value'] ) || is_numeric( $value['value'] ) ) ) {
628 + return (string) $value['value'];
629 + }
630 + if ( isset( $value['$$type'] ) && is_string( $value['$$type'] ) ) {
631 + return (string) $value['$$type'];
632 + }
633 + }
634 +
635 + return '';
636 + }
637 +
469 638 private function resolve_document( int $post_id ) {
470 639 $document = Plugin::$instance->documents->get_doc_or_auto_save( $post_id, get_current_user_id() )
471 640 ?? Plugin::$instance->documents->get( $post_id );
472 641
@@ -561,6 +730,143 @@
561 730 }
562 731
563 732 private function get_active_breakpoints(): array {
564 733 return array_keys( Plugin::$instance->breakpoints->get_active_breakpoints() );
734 + }
735 +
736 + private function emit_update_events( array $pending_events, array $tree ): void {
737 + if ( empty( $pending_events ) ) {
738 + return;
739 + }
740 +
741 + $all_labels = $this->create_global_classes_repository()->all_labels();
742 + $class_counts = $this->count_class_usage_in_tree( $tree );
743 +
744 + foreach ( $pending_events as $meta ) {
745 + $action = $meta['action'] ?? '';
746 +
747 + if ( 'duplicate' === $action ) {
748 + foreach ( $meta['duplicated_element_types'] ?? [] as $element_name ) {
749 + Mcp_Event_Dispatcher::emit( 'element_added', [
750 + 'element_name' => $element_name,
751 + ] );
752 + }
753 + continue;
754 + }
755 +
756 + $element_type = $meta['element_type'] ?? '';
757 +
758 + foreach ( $meta['applied_classes'] ?? [] as $label ) {
759 + $class_id = array_search( $label, $all_labels, true );
760 +
761 + if ( false === $class_id ) {
762 + continue;
763 + }
764 +
765 + Mcp_Event_Dispatcher::emit( 'class_applied', [
766 + 'target_name' => 'apply_class',
767 + 'id' => (string) $class_id,
768 + 'name' => $label,
769 + 'affected_element_type' => $element_type,
770 + 'total_instances_after_apply' => $class_counts[ $class_id ] ?? 0,
771 + ] );
772 + }
773 +
774 + foreach ( $meta['variable_connections'] as $connection ) {
775 + Mcp_Event_Dispatcher::emit( 'variable_connected', [
776 + 'id' => $connection['variable_id'],
777 + 'var_type' => $connection['var_type'],
778 + 'control_path' => $connection['control_path'],
779 + ] );
780 + }
781 +
782 + foreach ( $meta['interactions_events'] ?? [] as $ie ) {
783 + Mcp_Event_Dispatcher::emit( $ie['event_name'], $ie['payload'] );
784 + }
785 + }
786 + }
787 +
788 + private function count_class_usage_in_tree( array $tree ): array {
789 + $counts = [];
790 + $this->walk_tree_for_class_counts( $tree, $counts );
791 + return $counts;
792 + }
793 +
794 + private function walk_tree_for_class_counts( array $elements, array &$counts ): void {
795 + foreach ( $elements as $element ) {
796 + $class_values = $element['settings']['classes']['value'] ?? [];
797 +
798 + if ( is_array( $class_values ) ) {
799 + foreach ( $class_values as $class_id ) {
800 + if ( is_string( $class_id ) && ! str_starts_with( $class_id, Style_Applier::LOCAL_STYLE_ID_PREFIX ) ) {
801 + $counts[ $class_id ] = ( $counts[ $class_id ] ?? 0 ) + 1;
802 + }
803 + }
804 + }
805 +
806 + if ( ! empty( $element['elements'] ) ) {
807 + $this->walk_tree_for_class_counts( $element['elements'], $counts );
808 + }
809 + }
810 + }
811 +
812 + private function emit_mcp_manage_elements_executed(
813 + int $started_at,
814 + int $post_id,
815 + ?Document $document = null,
816 + ?\WP_Error $top_level_error = null,
817 + array $operations = [],
818 + array $response = [],
819 + array $warning_codes = [],
820 + int $class_attachments = 0,
821 + int $interactions_count = 0
822 + ): void {
823 + $duration_ms = Tool_Performance_Metrics::duration_ms_since( $started_at );
824 +
825 + [ 'status' => $status, 'error_code' => $error_code ] = Tool_Performance_Metrics::resolve_status( $response, $top_level_error );
826 +
827 + $failed_results = array_filter( $response['results'] ?? [], fn( $r ) => 'error' === ( $r['status'] ?? '' ) );
828 + $failed_count = count( $failed_results );
829 + $failed_codes = array_values( array_unique( array_column( $failed_results, 'code' ) ) );
830 +
831 + $ops_count = count( $operations );
832 + $by_action = [];
833 + foreach ( $operations as $op ) {
834 + $action = $op['action'] ?? '';
835 + if ( is_string( $action ) && '' !== $action ) {
836 + $by_action[ $action ] = ( $by_action[ $action ] ?? 0 ) + 1;
837 + }
838 + }
839 +
840 + $dominant_action = '';
841 + if ( ! empty( $by_action ) ) {
842 + arsort( $by_action );
843 + $dominant_action = (string) array_key_first( $by_action );
844 + }
845 +
846 + $payload = [
847 + 'tool_name' => $this->get_ability_id(),
848 + 'status' => $status,
849 + 'duration_ms' => $duration_ms,
850 + 'post_id' => $post_id,
851 + 'action' => $dominant_action,
852 + 'operations_count' => $ops_count,
853 + 'operations_by_type' => $by_action,
854 + 'failed_operations_count' => $failed_count,
855 + 'failed_operation_codes' => $failed_codes,
856 + 'class_attachments_count' => $class_attachments,
857 + 'interactions_applied_count' => $interactions_count,
858 + 'warning_count' => count( array_unique( $warning_codes ) ),
859 + 'warning_types' => array_values( array_unique( $warning_codes ) ),
860 + ];
861 +
862 + if ( null !== $document ) {
863 + $payload['document_type'] = $document->get_name();
864 + }
865 +
866 + if ( null !== $error_code ) {
867 + $payload['error_code'] = $error_code;
868 + }
869 +
870 + Mcp_Event_Dispatcher::emit( 'mcp_manage_elements_executed', $payload );
565 871 }
566 872 }