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 / overridable-props-builder.php

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

569 lines 17.9 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\Modules\Components\Components_Repository;
6 use Elementor\Modules\Components\Documents\Component_Overridable_Prop;
7 use Elementor\Modules\Components\PropTypes\Component_Override_Parser;
8 use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
9 use Elementor\Modules\Components\PropTypes\Override_Prop_Type;
10 use Elementor\Modules\Components\PropTypes\Overrides_Prop_Type;
11 use Elementor\Modules\Components\Utils\Parsing_Utils;
12 use Elementor\Modules\Components\Widgets\Component_Instance;
13 use WP_Http;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Builds native component overridable props from MCP definitions.
21 */
22 class Overridable_Props_Builder {
23
24 const DEFAULT_GROUP_LABEL = 'Default';
25 const GROUP_ID_PREFIX = 'group';
26 const GROUP_ID_SUFFIX_LENGTH = 7;
27
28 private ?Components_Repository $repository;
29
30 public function __construct( ?Components_Repository $repository = null ) {
31 $this->repository = $repository;
32 }
33
34 public static function make( ?Components_Repository $repository = null ): self {
35 return new self( $repository );
36 }
37
38 /**
39 * @param array $elements Elements tree containing the referenced targets.
40 * @param array $prop_definitions Overridable prop definitions keyed by override key.
41 *
42 * @return array{props: array, groups: array{items: array, order: array}}|\WP_Error
43 */
44 public function build( array &$elements, array $prop_definitions ) {
45 if ( empty( $prop_definitions ) ) {
46 return [
47 'props' => [],
48 'groups' => [
49 'items' => [],
50 'order' => [],
51 ],
52 ];
53 }
54
55 $props = [];
56 $groups = [];
57 $group_order = [];
58 $group_ids_by_label = [];
59 $errors = [];
60 $updated_elements = $elements;
61 $override_keys_by_element_prop = [];
62
63 foreach ( $prop_definitions as $override_key => $definition ) {
64 $prop = $this->build_prop(
65 (string) $override_key,
66 $definition,
67 $updated_elements,
68 $group_ids_by_label,
69 $groups,
70 $group_order,
71 $override_keys_by_element_prop
72 );
73
74 if ( is_wp_error( $prop ) ) {
75 $errors[] = $prop->get_error_message();
76 continue;
77 }
78
79 $props[ $override_key ] = $prop;
80 }
81
82 if ( ! empty( $errors ) ) {
83 return new \WP_Error(
84 'invalid_overridable_props',
85 implode( ' ', $errors ),
86 [ 'status' => WP_Http::BAD_REQUEST ]
87 );
88 }
89
90 $elements = $updated_elements;
91
92 return [
93 'props' => $props,
94 'groups' => [
95 'items' => $groups,
96 'order' => $group_order,
97 ],
98 ];
99 }
100
101 /**
102 * @return array|\WP_Error
103 */
104 private function build_prop(
105 string $override_key,
106 $definition,
107 array &$elements,
108 array &$group_ids_by_label,
109 array &$groups,
110 array &$group_order,
111 array &$override_keys_by_element_prop
112 ) {
113 $common = $this->validate_common_definition( $override_key, $definition );
114 if ( is_wp_error( $common ) ) {
115 return $common;
116 }
117 [ 'target' => $target, 'prop_key' => $prop_key, 'label' => $label, 'group_label' => $group_label ] = $common;
118
119 $element = &$this->find_element_ref( $elements, $target );
120
121 if ( null === $element ) {
122 return $this->invalid_definition( sprintf( '[%s] target "%s" was not found in the element tree.', $override_key, $target ) );
123 }
124
125 $element_id = (string) ( $element['id'] ?? $target );
126
127 if ( isset( $override_keys_by_element_prop[ $element_id ][ $prop_key ] ) ) {
128 return $this->invalid_definition(
129 sprintf(
130 '[%1$s] target "%2$s" and prop_key "%3$s" are duplicated by override key "%4$s".',
131 $override_key,
132 $target,
133 $prop_key,
134 $override_keys_by_element_prop[ $element_id ][ $prop_key ]
135 )
136 );
137 }
138
139 $override_keys_by_element_prop[ $element_id ][ $prop_key ] = $override_key;
140
141 $widget_type = (string) ( $element['widgetType'] ?? '' );
142
143 if ( Component_Instance::get_element_type() === $widget_type ) {
144 return $this->build_nested_component_prop(
145 $override_key,
146 $prop_key,
147 $label,
148 $group_label,
149 $element,
150 $group_ids_by_label,
151 $groups,
152 $group_order
153 );
154 }
155
156 return $this->build_raw_widget_prop(
157 $override_key,
158 $prop_key,
159 $label,
160 $group_label,
161 $element,
162 $group_ids_by_label,
163 $groups,
164 $group_order
165 );
166 }
167
168 /**
169 * @return array{target: string, prop_key: string, label: string, group_label: string}|\WP_Error
170 */
171 private function validate_common_definition( string $override_key, $definition ) {
172 if ( ! is_array( $definition ) ) {
173 return $this->invalid_definition( sprintf( '[%s] overridable_props entry must be an object.', $override_key ) );
174 }
175
176 if ( sanitize_key( $override_key ) !== $override_key ) {
177 return $this->invalid_definition( sprintf( '[%s] override keys must be slugs: lowercase letters, digits, dashes and underscores only.', $override_key ) );
178 }
179
180 $target = $definition['target'] ?? null;
181 $prop_key = $definition['prop_key'] ?? null;
182 $label = $definition['label'] ?? null;
183 $group_label = is_string( $definition['group'] ?? null ) && '' !== $definition['group']
184 ? $definition['group']
185 : self::DEFAULT_GROUP_LABEL;
186
187 if ( ! is_string( $target ) || '' === $target || ! is_string( $prop_key ) || '' === $prop_key || ! is_string( $label ) || '' === $label ) {
188 return $this->invalid_definition( sprintf( '[%s] overridable_props entries require a non-empty target, prop_key, and label.', $override_key ) );
189 }
190
191 return [
192 'target' => $target,
193 'prop_key' => $prop_key,
194 'label' => $label,
195 'group_label' => $group_label,
196 ];
197 }
198
199 /**
200 * @return array|\WP_Error
201 */
202 private function build_raw_widget_prop(
203 string $override_key,
204 string $prop_key,
205 string $label,
206 string $group_label,
207 array &$element,
208 array &$group_ids_by_label,
209 array &$groups,
210 array &$group_order
211 ) {
212 $el_type = (string) ( $element['elType'] ?? '' );
213 $widget_type = (string) ( $element['widgetType'] ?? '' );
214
215 try {
216 $prop_type = Parsing_Utils::get_prop_type( $el_type, $widget_type, $prop_key );
217 } catch ( \Exception $e ) {
218 return $this->invalid_definition( sprintf( '[%s] %s', $override_key, $e->getMessage() ) );
219 }
220
221 $origin_value = $element['settings'][ $prop_key ] ?? $prop_type->get_default();
222
223 $element['settings'][ $prop_key ] = [
224 '$$type' => Overridable_Prop_Type::get_key(),
225 'value' => [
226 'override_key' => $override_key,
227 'origin_value' => $origin_value,
228 ],
229 ];
230
231 $group_id = $this->resolve_group_id( $group_label, $group_ids_by_label, $groups, $group_order );
232 $groups[ $group_id ]['props'][] = $override_key;
233
234 return [
235 'overrideKey' => $override_key,
236 'label' => $label,
237 'elementId' => (string) $element['id'],
238 'elType' => $el_type,
239 'widgetType' => $this->resolve_widgets_cache_key( $el_type, $widget_type ),
240 'propKey' => $prop_key,
241 'originValue' => $origin_value,
242 'groupId' => $group_id,
243 ];
244 }
245
246 /**
247 * Expose-further path. `$prop_key` names the **inner component's** exposed override key,
248 * not a real setting key on the `<e-component>` element (which only has `component_instance`).
249 *
250 * @return array|\WP_Error
251 */
252 private function build_nested_component_prop(
253 string $override_key,
254 string $inner_override_key,
255 string $label,
256 string $group_label,
257 array &$element,
258 array &$group_ids_by_label,
259 array &$groups,
260 array &$group_order
261 ) {
262 $inner_component_id = $this->extract_inner_component_id( $element );
263
264 if ( null === $inner_component_id ) {
265 return new \WP_Error(
266 'invalid_overridable_prop_definition',
267 sprintf(
268 '[%s] target "%s" is an <e-component> instance but has no valid component_instance settings. Set component_id on the instance (via element_config) before exposing an override through it.',
269 $override_key,
270 (string) ( $element['id'] ?? '' )
271 )
272 );
273 }
274
275 $inner_component = $this->get_repository()->get( $inner_component_id, false );
276
277 if ( ! $inner_component ) {
278 return $this->invalid_definition(
279 sprintf( '[%s] inner component %d referenced by target "%s" was not found.', $override_key, $inner_component_id, (string) $element['id'] )
280 );
281 }
282
283 $inner_props = $inner_component->get_overridable_props()->props;
284 $inner_prop = $inner_props[ $inner_override_key ] ?? null;
285
286 if ( ! $inner_prop ) {
287 $available = empty( $inner_props ) ? '(none)' : implode( ', ', array_keys( $inner_props ) );
288 return $this->invalid_definition(
289 sprintf(
290 '[%s] component %d has no exposed override "%s". Available: %s. Use one of these as prop_key when the target is an <e-component>, or expose the underlying raw widget on the inner component first.',
291 $override_key,
292 $inner_component_id,
293 $inner_override_key,
294 $available
295 )
296 );
297 }
298
299 $origin_prop_fields = $this->resolve_origin_prop_fields( $inner_prop );
300
301 $inner_override_value = $this->take_existing_override_value( $element, $inner_override_key );
302
303 $override_envelope = $this->build_override_envelope( $inner_override_key, $inner_component_id, $inner_override_value );
304
305 $overridable_envelope = [
306 '$$type' => Overridable_Prop_Type::get_key(),
307 'value' => [
308 'override_key' => $override_key,
309 'origin_value' => $override_envelope,
310 ],
311 ];
312
313 $this->upsert_component_instance_override( $element, $override_key, $overridable_envelope );
314
315 $group_id = $this->resolve_group_id( $group_label, $group_ids_by_label, $groups, $group_order );
316 $groups[ $group_id ]['props'][] = $override_key;
317
318 return [
319 'overrideKey' => $override_key,
320 'label' => $label,
321 'elementId' => (string) $element['id'],
322 'elType' => 'widget',
323 'widgetType' => Component_Instance::get_element_type(),
324 'propKey' => $inner_override_key,
325 'originValue' => $inner_override_value,
326 'groupId' => $group_id,
327 'originPropFields' => $origin_prop_fields,
328 ];
329 }
330
331 private function extract_inner_component_id( array $element ): ?int {
332 $raw = $element['settings']['component_instance']['value']['component_id']['value'] ?? null;
333
334 if ( ! is_numeric( $raw ) || (int) $raw <= 0 ) {
335 return null;
336 }
337
338 return (int) $raw;
339 }
340
341 /**
342 * N-level chaining: if the inner overridable prop is itself a chained expose (already carries
343 * `origin_prop_fields`), thread those through so the outer entry points all the way at the raw
344 * widget's schema. Otherwise, derive the fields from the inner prop's own element metadata.
345 */
346 private function resolve_origin_prop_fields( Component_Overridable_Prop $inner_prop ): array {
347 if ( $inner_prop->origin_prop_fields ) {
348 return [
349 'elType' => (string) $inner_prop->origin_prop_fields['el_type'],
350 'widgetType' => (string) $inner_prop->origin_prop_fields['widget_type'],
351 'propKey' => (string) $inner_prop->origin_prop_fields['prop_key'],
352 'elementId' => (string) $inner_prop->origin_prop_fields['element_id'],
353 ];
354 }
355
356 return [
357 'elType' => (string) $inner_prop->el_type,
358 'widgetType' => (string) $inner_prop->widget_type,
359 'propKey' => (string) $inner_prop->prop_key,
360 'elementId' => (string) $inner_prop->element_id,
361 ];
362 }
363
364 /**
365 * @param string $inner_override_key Override key exposed by the inner component.
366 * @param int $inner_component_id Component the inner override key belongs to.
367 * @param array|null $inner_override_value Per-instance value for the inner override, or null to inherit
368 * the inner component's own origin value.
369 */
370 private function build_override_envelope( string $inner_override_key, int $inner_component_id, ?array $inner_override_value ): array {
371 return [
372 '$$type' => Override_Prop_Type::get_key(),
373 'value' => [
374 'override_key' => $inner_override_key,
375 'override_value' => $inner_override_value,
376 'schema_source' => [
377 'type' => Component_Override_Parser::get_override_type(),
378 'id' => $inner_component_id,
379 ],
380 ],
381 ];
382 }
383
384 /**
385 * Both a literal `override` (placed via `element_config`) and the `overridable` envelope we are about to
386 * write address the nested instance by the same inner override key. Two entries for one key make the
387 * resolvers last-write-wins, silently discarding whichever came first, so the existing entry is consumed
388 * here: its value moves into the new envelope's `override_value` and the old entry is dropped.
389 *
390 * @return array|null The value carried by the removed entry, or null when there was none.
391 */
392 private function take_existing_override_value( array &$element, string $inner_override_key ): ?array {
393 $this->ensure_component_instance_shape( $element );
394
395 $overrides_list = &$element['settings']['component_instance']['value']['overrides']['value'];
396 $taken_value = null;
397
398 foreach ( $overrides_list as $index => $existing ) {
399 if ( $inner_override_key !== $this->resolve_inner_override_key( $existing ) ) {
400 continue;
401 }
402
403 $taken_value = $this->resolve_inner_override_value( $existing );
404 unset( $overrides_list[ $index ] );
405 }
406
407 $overrides_list = array_values( $overrides_list );
408
409 return $taken_value;
410 }
411
412 /**
413 * An entry addresses the nested instance either directly (`override`) or through an already-exposed
414 * wrapper (`overridable` whose `origin_value` is the `override`).
415 */
416 private function resolve_inner_override( $entry ): ?array {
417 if ( ! is_array( $entry ) ) {
418 return null;
419 }
420
421 if ( Overridable_Prop_Type::get_key() === ( $entry['$$type'] ?? null ) ) {
422 $entry = $entry['value']['origin_value'] ?? null;
423 }
424
425 if ( ! is_array( $entry ) || Override_Prop_Type::get_key() !== ( $entry['$$type'] ?? null ) ) {
426 return null;
427 }
428
429 return $entry;
430 }
431
432 private function resolve_inner_override_key( $entry ): ?string {
433 $override = $this->resolve_inner_override( $entry );
434
435 return isset( $override['value']['override_key'] ) ? (string) $override['value']['override_key'] : null;
436 }
437
438 private function resolve_inner_override_value( $entry ): ?array {
439 $override = $this->resolve_inner_override( $entry );
440 $value = $override['value']['override_value'] ?? null;
441
442 return is_array( $value ) ? $value : null;
443 }
444
445 /**
446 * Replace-or-append semantics on the nested instance's `overrides` list, keyed by the outer
447 * override_key stored on the `Overridable_Prop_Type` envelope — mirrors how the editor's
448 * `getMatchingOverride`/`setInstanceValue` flow keys entries when the user re-edits an
449 * exposed-further prop.
450 */
451 private function upsert_component_instance_override( array &$element, string $override_key, array $overridable_envelope ): void {
452 $this->ensure_component_instance_shape( $element );
453
454 $overrides_list = &$element['settings']['component_instance']['value']['overrides']['value'];
455
456 foreach ( $overrides_list as $index => $existing ) {
457 $is_overridable = ( $existing['$$type'] ?? null ) === Overridable_Prop_Type::get_key();
458 $existing_key = $existing['value']['override_key'] ?? null;
459
460 if ( $is_overridable && $existing_key === $override_key ) {
461 $overrides_list[ $index ] = $overridable_envelope;
462 return;
463 }
464 }
465
466 $overrides_list[] = $overridable_envelope;
467 }
468
469 private function ensure_component_instance_shape( array &$element ): void {
470 if ( ! isset( $element['settings']['component_instance']['value']['overrides'] ) ) {
471 $element['settings']['component_instance']['value']['overrides'] = [
472 '$$type' => Overrides_Prop_Type::get_key(),
473 'value' => [],
474 ];
475 return;
476 }
477
478 if ( ! isset( $element['settings']['component_instance']['value']['overrides']['value'] )
479 || ! is_array( $element['settings']['component_instance']['value']['overrides']['value'] ) ) {
480 $element['settings']['component_instance']['value']['overrides']['value'] = [];
481 }
482 }
483
484 private function get_repository(): Components_Repository {
485 if ( ! $this->repository ) {
486 $this->repository = new Components_Repository();
487 }
488
489 return $this->repository;
490 }
491
492 private function invalid_definition( string $message ): \WP_Error {
493 return new \WP_Error( 'invalid_overridable_prop_definition', $message );
494 }
495
496 /**
497 * The editor's `getWidgetsCache()` is keyed by the atomic widget/element type name (e.g. `e-heading`,
498 * `e-flexbox`). Widgets carry it on `widgetType`; atomic elements carry it on `elType` and leave
499 * `widgetType` unset. `OverrideControl` looks the persisted `widgetType` up in the cache to find the
500 * origin prop type — it must be a real cache key, or the panel throws "Prop type not found".
501 */
502 private function resolve_widgets_cache_key( string $el_type, string $widget_type ): string {
503 return '' !== $widget_type ? $widget_type : $el_type;
504 }
505
506 private function resolve_group_id( string $label, array &$group_ids_by_label, array &$groups, array &$group_order ): string {
507 if ( isset( $group_ids_by_label[ $label ] ) ) {
508 return $group_ids_by_label[ $label ];
509 }
510
511 $group_id = $this->generate_group_id();
512
513 $group_ids_by_label[ $label ] = $group_id;
514 $groups[ $group_id ] = [
515 'id' => $group_id,
516 'label' => $label,
517 'props' => [],
518 ];
519 $group_order[] = $group_id;
520
521 return $group_id;
522 }
523
524 /**
525 * Mirrors the editor's `generateUniqueId( 'group' )` format. Group ids must not be derived from the
526 * label: `@elementor/ui`'s `SortableGroup` treats the id "default" as "no group id" and hands the
527 * whole multi-group map back to the props panel, which then crashes on `items.map`.
528 */
529 private function generate_group_id(): string {
530 return sprintf(
531 '%s-%d-%s',
532 self::GROUP_ID_PREFIX,
533 (int) round( microtime( true ) * 1000 ),
534 strtolower( wp_generate_password( self::GROUP_ID_SUFFIX_LENGTH, false, false ) )
535 );
536 }
537
538 /**
539 * Matches the persisted element id, or the configuration-id the element was compiled from —
540 * compiled trees get machine-generated ids, so the caller can only address them by the
541 * configuration-id it already used in `xml_structure`.
542 *
543 * @return array|null
544 */
545 private function &find_element_ref( array &$elements, string $target ) {
546 $not_found = null;
547
548 foreach ( $elements as &$element ) {
549 $configuration_id = $element['editor_settings']['title'] ?? null;
550
551 if ( ( $element['id'] ?? null ) === $target || $configuration_id === $target ) {
552 return $element;
553 }
554
555 if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
556 $found = &$this->find_element_ref( $element['elements'], $target );
557
558 if ( null !== $found ) {
559 unset( $element );
560 return $found;
561 }
562 }
563 }
564 unset( $element );
565
566 return $not_found;
567 }
568 }
569