| 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 |
|