PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
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 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / modules / mcp / abilities / utils / composition-compiler.php

composition-compiler.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/mcp/abilities/utils/composition-compiler.php

356 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\Mcp\Abilities\Utils;
4
5 use Elementor\Core\Base\Document;
6 use Elementor\Modules\AtomicWidgets\CssConverter\Converter_Registry_Factory;
7 use Elementor\Modules\AtomicWidgets\CssConverter\Css_Converter;
8 use Elementor\Modules\AtomicWidgets\CssConverter\Expander_Registry_Factory;
9 use Elementor\Modules\AtomicWidgets\CssConverter\Metrics\Null_Failure_Reporter;
10 use Elementor\Modules\AtomicWidgets\CssConverter\Variable_Prop_Value_Transformer;
11 use Elementor\Modules\AtomicWidgets\Module as AtomicWidgetsModule;
12 use Elementor\Modules\AtomicWidgets\PlainResolvers\Plain_Values_Resolver;
13 use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
14 use Elementor\Modules\Interactions\Module as Interactions_Module;
15 use Elementor\Modules\Mcp\Abilities\Appliers\Class_Applier;
16 use Elementor\Modules\Mcp\Abilities\Appliers\Element_Config_Applier;
17 use Elementor\Modules\Mcp\Abilities\Appliers\Interactions_Applier;
18 use Elementor\Modules\Mcp\Abilities\Appliers\Style_Applier;
19 use Elementor\Modules\Mcp\Abilities\Build_Composition\Form_Structure_Validator;
20 use Elementor\Modules\Mcp\Abilities\Build_Composition\Subtree_Builder;
21 use Elementor\Modules\Mcp\Abilities\Build_Composition\Widget_Type_Resolver;
22 use Elementor\Modules\Mcp\Abilities\Build_Composition\Xml_Parser;
23 use Elementor\Modules\Variables\Module as Variables_Module;
24 use Elementor\Modules\Variables\Services\Batch_Operations\Batch_Processor;
25 use Elementor\Modules\Variables\Services\Variables_Service;
26 use Elementor\Modules\Variables\Storage\Variables_Repository;
27 use Elementor\Plugin;
28
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit;
31 }
32
33 final class Composition_Compiler {
34
35 private const DEFAULT_PARENT_ID = 'document';
36 private const DOCUMENT_ROOT_WRAPPER = 'e-div-block';
37
38 public const COMPONENT_PARENT_ID = 'component';
39
40 public static function make(): self {
41 return new self();
42 }
43
44 /**
45 * @param array $input Composition input using the `elementor/build-composition` shapes.
46 * @param ?Document $document Context for resolving dynamic values, when a target document exists.
47 * @param array $document_tree Existing document tree used for insertion-context validation.
48 * @param string $parent_id Target parent used for insertion-context validation.
49 *
50 * @return array{elements: array[], warnings: string[], dom: \DOMDocument, xml_parser: Xml_Parser}|\WP_Error
51 */
52 public function compile(
53 array $input,
54 ?Document $document = null,
55 array $document_tree = [],
56 string $parent_id = self::DEFAULT_PARENT_ID
57 ) {
58 $xml_parser = new Xml_Parser();
59 $type_resolver = new Widget_Type_Resolver( $xml_parser );
60 $subtree_builder = new Subtree_Builder( $xml_parser );
61
62 $dom = $xml_parser->parse( (string) ( $input['xml_structure'] ?? '' ) );
63 if ( is_wp_error( $dom ) ) {
64 return $dom;
65 }
66
67 [
68 'configs' => $widget_configs,
69 'unknown_tag_errors' => $unknown_widget_tag_errors,
70 ] = $type_resolver->collect_referenced_widget_configs( $dom );
71
72 $validation_error = $this->validate_xml_before_apply(
73 $dom,
74 $xml_parser,
75 $document_tree,
76 $parent_id,
77 $unknown_widget_tag_errors
78 );
79 if ( $validation_error ) {
80 return $validation_error;
81 }
82
83 $wrapping_result = $this->wrap_document_root_content( $dom, $widget_configs, $parent_id, $type_resolver, $xml_parser );
84 if ( is_wp_error( $wrapping_result ) ) {
85 return $wrapping_result;
86 }
87
88 $widget_configs = $wrapping_result['widget_configs'];
89
90 $child_type_errors = $type_resolver->collect_child_type_and_required_child_errors( $dom, $widget_configs );
91 if ( ! empty( $child_type_errors ) ) {
92 return new \WP_Error(
93 'elementor_invalid_child_type',
94 implode( ' ', $child_type_errors ),
95 [ 'status' => \WP_Http::BAD_REQUEST ]
96 );
97 }
98
99 $subtrees = $subtree_builder->build( $dom, $widget_configs );
100 if ( empty( $subtrees ) ) {
101 return new \WP_Error(
102 'empty_composition',
103 __( 'xml_structure did not contain any elements. Pass raw XML tags (e.g. <e-flexbox configuration-id="..."></e-flexbox>) — do not wrap the value in <![CDATA[...]]> or other text-only content.', 'elementor' ),
104 [ 'status' => \WP_Http::BAD_REQUEST ]
105 );
106 }
107
108 $index = $subtree_builder->index_by_config_id( $subtrees, $dom );
109
110 $variables_service = $this->create_variables_service();
111
112 $config_applier = new Element_Config_Applier( $type_resolver, $this->get_plain_values_resolver() );
113 $config_result = $config_applier->apply( $index, $this->as_map( $input['element_config'] ?? [] ), $widget_configs, $document );
114 if ( $config_result['error'] ) {
115 return $config_result['error'];
116 }
117
118 $class_applier = new Class_Applier( $this->create_global_classes_repository() );
119 $class_error = $class_applier->apply( $index, $this->as_map( $input['classes'] ?? [] ) );
120 if ( $class_error ) {
121 return $class_error;
122 }
123
124 $style_applier = new Style_Applier( $this->create_css_converter( $variables_service ), $this->get_active_breakpoints() );
125 $style_result = $style_applier->apply( $index, $this->as_map( $input['style'] ?? [] ), 'patch', $widget_configs );
126 if ( $style_result['error'] ) {
127 return $style_result['error'];
128 }
129
130 $interactions_result = $this->apply_interactions( $index, $this->as_map( $input['interactions'] ?? [] ) );
131 if ( $interactions_result['error'] ) {
132 return $interactions_result['error'];
133 }
134
135 return [
136 'elements' => $subtrees,
137 'warnings' => array_merge( $wrapping_result['warnings'], $config_result['warnings'], $style_result['warnings'], $interactions_result['warnings'] ),
138 'warning_codes' => array_values( array_unique( array_merge(
139 $wrapping_result['warning_codes'] ?? [],
140 $config_result['warning_codes'] ?? [],
141 $style_result['warning_codes'] ?? [],
142 $interactions_result['warning_codes'] ?? []
143 ) ) ),
144 'dom' => $dom,
145 'xml_parser' => $xml_parser,
146 ];
147 }
148
149 /**
150 * @param \DOMDocument $dom
151 * @param Xml_Parser $xml_parser
152 * @param array $document_tree
153 * @param string $parent_id
154 * @param string[] $unknown_widget_tag_errors
155 *
156 * @return \WP_Error|null
157 */
158 private function validate_xml_before_apply(
159 \DOMDocument $dom,
160 Xml_Parser $xml_parser,
161 array $document_tree,
162 string $parent_id,
163 array $unknown_widget_tag_errors
164 ) {
165 $errors = array_merge(
166 $this->tag_errors( 'elementor_duplicate_configuration_id', $xml_parser->collect_duplicate_configuration_id_errors( $dom ) ),
167 $this->tag_errors( 'elementor_invalid_form_structure', ( new Form_Structure_Validator( $xml_parser ) )->collect_errors( $dom, $document_tree, $parent_id ) ),
168 $this->tag_errors( 'elementor_unknown_type', $unknown_widget_tag_errors ),
169 );
170
171 if ( empty( $errors ) ) {
172 return null;
173 }
174
175 $status = [ 'status' => \WP_Http::BAD_REQUEST ];
176 $joined = implode( ' ', array_column( $errors, 'message' ) );
177
178 $aggregated = new \WP_Error( $errors[0]['code'], $joined, $status );
179 foreach ( array_slice( $errors, 1 ) as $error ) {
180 $aggregated->add( $error['code'], $error['message'], $status );
181 }
182
183 return $aggregated;
184 }
185
186 /**
187 * @param string $code Error code to tag each message with.
188 * @param string[] $messages List of error messages.
189 *
190 * @return array<int, array{code: string, message: string}>
191 */
192 private function tag_errors( string $code, array $messages ): array {
193 return array_map(
194 fn ( $message ) => [
195 'code' => $code,
196 'message' => $message,
197 ],
198 $messages
199 );
200 }
201
202 private function wrap_document_root_content(
203 \DOMDocument $dom,
204 array $widget_configs,
205 string $parent_id,
206 Widget_Type_Resolver $type_resolver,
207 Xml_Parser $xml_parser
208 ) {
209 if ( self::DEFAULT_PARENT_ID !== $parent_id ) {
210 return [
211 'widget_configs' => $widget_configs,
212 'warnings' => [],
213 'warning_codes' => [],
214 ];
215 }
216
217 $root = $xml_parser->get_root( $dom );
218 if ( ! $root ) {
219 return [
220 'widget_configs' => $widget_configs,
221 'warnings' => [],
222 'warning_codes' => [],
223 ];
224 }
225
226 $root_children = $xml_parser->get_child_elements( $root );
227 $has_widget = false;
228 foreach ( $root_children as $child ) {
229 $tag = $xml_parser->get_tag_name( $child );
230 $config = $widget_configs[ $tag ] ?? [];
231
232 if ( 'widget' !== ( $config['elType'] ?? null ) ) {
233 continue;
234 }
235
236 $has_widget = true;
237 break;
238 }
239
240 if ( ! $has_widget ) {
241 return [
242 'widget_configs' => $widget_configs,
243 'warnings' => [],
244 'warning_codes' => [],
245 ];
246 }
247
248 $wrapper_config = $type_resolver->resolve_type_config( self::DOCUMENT_ROOT_WRAPPER );
249 if ( is_wp_error( $wrapper_config ) ) {
250 return $wrapper_config;
251 }
252
253 $widget_configs[ self::DOCUMENT_ROOT_WRAPPER ] = $wrapper_config;
254
255 $wrapper = $dom->createElement( self::DOCUMENT_ROOT_WRAPPER );
256 $root->appendChild( $wrapper );
257
258 foreach ( $root_children as $child ) {
259 $wrapper->appendChild( $child );
260 }
261
262 return [
263 'widget_configs' => $widget_configs,
264 'warnings' => [ __( 'Direct document-root content was wrapped in an e-div-block element.', 'elementor' ) ],
265 'warning_codes' => [ 'root_auto_wrapped' ],
266 ];
267 }
268
269 private function as_map( $value ): array {
270 if ( is_object( $value ) ) {
271 $value = (array) $value;
272 }
273
274 return is_array( $value ) ? $value : [];
275 }
276
277 private function get_active_breakpoints(): array {
278 return array_keys( Plugin::$instance->breakpoints->get_active_breakpoints() );
279 }
280
281 private function create_css_converter( ?Variables_Service $variables_service ): Css_Converter {
282 $variable_transformer = $variables_service
283 ? new Variable_Prop_Value_Transformer( $variables_service )
284 : null;
285
286 return new Css_Converter(
287 Converter_Registry_Factory::create( $variables_service ),
288 new Null_Failure_Reporter(),
289 Expander_Registry_Factory::create( $variables_service ),
290 $variable_transformer
291 );
292 }
293
294 private function create_variables_service(): ?Variables_Service {
295 if ( ! $this->is_variables_active() ) {
296 return null;
297 }
298
299 $kit = Plugin::$instance->kits_manager->get_active_kit();
300
301 if ( ! $kit ) {
302 return null;
303 }
304
305 return new Variables_Service(
306 new Variables_Repository( $kit ),
307 new Batch_Processor()
308 );
309 }
310
311 private function get_plain_values_resolver(): Plain_Values_Resolver {
312 return AtomicWidgetsModule::instance()->get_settings_plain_values_resolver();
313 }
314
315 /**
316 * @param array<string, array&> $index
317 * @param array<string, array<int, array>> $interactions
318 *
319 * @return array{error: \WP_Error|null, warnings: string[]}
320 */
321 private function apply_interactions( array &$index, array $interactions ): array {
322 if ( empty( $interactions ) ) {
323 return [
324 'error' => null,
325 'warnings' => [],
326 'warning_codes' => [],
327 ];
328 }
329
330 if ( ! Plugin::$instance->experiments->is_feature_active( Interactions_Module::EXPERIMENT_NAME ) ) {
331 return [
332 'error' => null,
333 'warnings' => [ __( 'Interactions experiment is not active. Interactions were not applied.', 'elementor' ) ],
334 'warning_codes' => [ 'interactions_experiment_off' ],
335 ];
336 }
337
338 $applier = new Interactions_Applier( $this->get_plain_values_resolver() );
339
340 return $applier->apply( $index, $interactions ) + [ 'warning_codes' => [] ];
341 }
342
343 private function is_variables_active(): bool {
344 $experiments = Plugin::$instance->experiments;
345
346 return $experiments->is_feature_active( Variables_Module::EXPERIMENT_NAME )
347 && $experiments->is_feature_active( AtomicWidgetsModule::EXPERIMENT_NAME );
348 }
349
350 private function create_global_classes_repository(): Global_Classes_Repository {
351 $kit = Plugin::$instance->kits_manager->get_active_kit();
352
353 return Global_Classes_Repository::make( $kit );
354 }
355 }
356