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 / appliers / component-instance-applier.php

component-instance-applier.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/mcp/abilities/appliers/component-instance-applier.php

446 lines 14.1 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\Appliers;
4
5 use Elementor\Core\Base\Document;
6 use Elementor\Modules\AtomicWidgets\PlainResolvers\Plain_Values_Resolver;
7 use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
8 use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
9 use Elementor\Modules\Components\Circular_Dependency_Validator;
10 use Elementor\Modules\Components\Components_Access_Controller;
11 use Elementor\Modules\Components\Components_Repository;
12 use Elementor\Modules\Components\Documents\Component as Component_Document;
13 use Elementor\Modules\Components\Documents\Component_Overridable_Prop;
14 use Elementor\Modules\Components\PropTypes\Component_Instance_Prop_Type;
15 use Elementor\Modules\Components\PropTypes\Component_Override_Parser;
16 use Elementor\Modules\Components\PropTypes\Override_Prop_Type;
17 use Elementor\Modules\Components\PropTypes\Overrides_Prop_Type;
18 use Elementor\Modules\Components\Utils\Parsing_Utils;
19 use Elementor\Modules\Components\Widgets\Component_Instance;
20 use Elementor\Modules\Mcp\Abilities\Utils\Insufficient_Permissions_Error;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 class Component_Instance_Applier {
27
28 private Components_Repository $repository;
29 private Plain_Values_Resolver $plain_values_resolver;
30
31 public function __construct( Components_Repository $repository, Plain_Values_Resolver $plain_values_resolver ) {
32 $this->repository = $repository;
33 $this->plain_values_resolver = $plain_values_resolver;
34 }
35
36 /**
37 * Builds the `component_instance` PropValue for each entry and writes it directly
38 * into `$node['settings']['component_instance']` — same side-effect shape as
39 * Style_Applier writing into `$node['styles']`.
40 *
41 * @param array<string, array&> $config_id_index Index of subtree refs (from Subtree_Builder).
42 * @param array<string, array{component_id:int,overrides?:array<string,mixed>}> $component_instances Per-config-id shorthand.
43 * @param Document|null $document Target document, when one already exists.
44 * @return \WP_Error|null
45 */
46 public function apply( array &$config_id_index, array $component_instances, ?Document $document ): ?\WP_Error {
47 if ( empty( $component_instances ) ) {
48 return null;
49 }
50
51 $access_error = $this->get_access_error( $document );
52 if ( $access_error ) {
53 return $access_error;
54 }
55
56 $errors = [];
57
58 foreach ( $component_instances as $config_id => $shorthand ) {
59 if ( ! isset( $config_id_index[ $config_id ] ) ) {
60 $errors[] = sprintf( '[%s] configuration-id not found in xml_structure.', $config_id );
61 continue;
62 }
63
64 $node = &$config_id_index[ $config_id ];
65
66 if ( ( $node['widgetType'] ?? null ) !== Component_Instance::get_element_type() ) {
67 $errors[] = sprintf(
68 '[%s] component_instances entries are only valid for <e-component> elements (got "%s").',
69 $config_id,
70 $node['widgetType'] ?? ( $node['elType'] ?? 'unknown' )
71 );
72 continue;
73 }
74
75 if ( ! is_array( $shorthand ) ) {
76 $errors[] = sprintf( '[%s] value must be an object with component_id and optional overrides.', $config_id );
77 continue;
78 }
79
80 $envelope = $this->build_envelope( $config_id, $shorthand, $document, $errors );
81
82 if ( null === $envelope ) {
83 continue;
84 }
85
86 $node['settings'] = array_merge( $node['settings'] ?? [], [ 'component_instance' => $envelope ] );
87 }
88 unset( $node );
89
90 if ( empty( $errors ) ) {
91 return null;
92 }
93
94 return new \WP_Error(
95 'elementor_invalid_component_instance',
96 implode( ' ', $errors ),
97 [ 'status' => \WP_Http::BAD_REQUEST ]
98 );
99 }
100
101 private function build_envelope( string $config_id, array $shorthand, ?Document $document, array &$errors ): ?array {
102 $component_id = (int) ( $shorthand['component_id'] ?? 0 );
103
104 if ( ! $component_id ) {
105 if ( isset( $shorthand['component_instance'] ) && is_array( $shorthand['component_instance'] ) ) {
106 $errors[] = sprintf(
107 '[%s] Invalid shape for <e-component> in element_config. Do not nest under "component_instance" — use the flat shape { "component_id": <int>, "overrides": { ... } } directly. See elementor/list-components.',
108 $config_id
109 );
110 } else {
111 $errors[] = sprintf( '[%s] component_id must be a non-zero integer.', $config_id );
112 }
113
114 return null;
115 }
116
117 $component = $this->load_valid_component( $config_id, $component_id, $document, $errors );
118 if ( ! $component ) {
119 return null;
120 }
121
122 $overridable_props = $component->get_overridable_props()->props;
123 $raw_overrides = is_array( $shorthand['overrides'] ?? null ) ? $shorthand['overrides'] : [];
124
125 $override_errors = $this->validate_override_keys( $config_id, $raw_overrides, $overridable_props );
126 if ( ! empty( $override_errors ) ) {
127 $errors = array_merge( $errors, $override_errors );
128 return null;
129 }
130
131 return $this->assemble_envelope(
132 $component_id,
133 $this->build_overrides_value( $raw_overrides, $overridable_props, $component_id )
134 );
135 }
136
137 /**
138 * Partial update entry-point for `manage-elements action=update` on `<e-component>` nodes.
139 *
140 * Contract (differs from apply()):
141 * - `component_id` is optional; if omitted, recovered from the node's existing envelope.
142 * - `overrides` is a **per-key merge** onto existing overrides.
143 * * A supplied non-null value replaces (or appends) that override key.
144 * * A supplied `null` value removes that override key from the envelope.
145 * * Override keys not mentioned in the payload are preserved untouched.
146 *
147 * @param array<string, array&> $config_id_index
148 * @param array<string, array{component_id?:int, overrides?:array<string, mixed|null>}> $partial_shorthands
149 * @param Document $document
150 */
151 public function apply_partial( array &$config_id_index, array $partial_shorthands, Document $document ): ?\WP_Error {
152 if ( empty( $partial_shorthands ) ) {
153 return null;
154 }
155
156 $access_error = $this->get_access_error( $document );
157 if ( $access_error ) {
158 return $access_error;
159 }
160
161 $errors = [];
162
163 foreach ( $partial_shorthands as $config_id => $shorthand ) {
164 if ( ! isset( $config_id_index[ $config_id ] ) ) {
165 $errors[] = sprintf( '[%s] configuration-id not found in xml_structure.', $config_id );
166 continue;
167 }
168
169 $node = &$config_id_index[ $config_id ];
170
171 if ( ( $node['widgetType'] ?? null ) !== Component_Instance::get_element_type() ) {
172 $errors[] = sprintf(
173 '[%s] apply_partial() is only valid for <e-component> elements (got "%s").',
174 $config_id,
175 $node['widgetType'] ?? ( $node['elType'] ?? 'unknown' )
176 );
177 continue;
178 }
179
180 if ( ! is_array( $shorthand ) ) {
181 $errors[] = sprintf( '[%s] settings must be an object with optional component_id and overrides.', $config_id );
182 continue;
183 }
184
185 $existing_settings = is_array( $node['settings'] ?? null ) ? $node['settings'] : [];
186
187 $component_id = $this->resolve_component_id( $shorthand, $existing_settings );
188 if ( ! $component_id ) {
189 $errors[] = sprintf(
190 '[%s] component_id could not be resolved. Either the target is not an existing <e-component> instance or component_id must be supplied.',
191 $config_id
192 );
193 continue;
194 }
195
196 $component = $this->load_valid_component( $config_id, $component_id, $document, $errors );
197 if ( ! $component ) {
198 continue;
199 }
200
201 $overridable_props = $component->get_overridable_props()->props;
202 $partial_overrides = is_array( $shorthand['overrides'] ?? null ) ? $shorthand['overrides'] : [];
203
204 $key_errors = $this->validate_override_keys( $config_id, $partial_overrides, $overridable_props );
205 if ( ! empty( $key_errors ) ) {
206 $errors = array_merge( $errors, $key_errors );
207 continue;
208 }
209
210 $existing_overrides_list = $this->extract_overrides_list( $existing_settings );
211 $merged_overrides_list = $this->merge_overrides_list(
212 $existing_overrides_list,
213 $partial_overrides,
214 $overridable_props,
215 $component_id
216 );
217
218 $node['settings'] = array_merge(
219 $node['settings'] ?? [],
220 [ 'component_instance' => $this->assemble_envelope( $component_id, $merged_overrides_list ) ]
221 );
222 }
223 unset( $node );
224
225 if ( empty( $errors ) ) {
226 return null;
227 }
228
229 return new \WP_Error(
230 'elementor_invalid_component_instance',
231 implode( ' ', $errors ),
232 [ 'status' => \WP_Http::BAD_REQUEST ]
233 );
234 }
235
236 private function get_access_error( ?Document $document ): ?\WP_Error {
237 if ( $document instanceof Component_Document ) {
238 return Components_Access_Controller::can_edit()
239 ? null
240 : Insufficient_Permissions_Error::for_action( 'update' );
241 }
242
243 if ( null === $document ) {
244 return Components_Access_Controller::can_create()
245 ? null
246 : Insufficient_Permissions_Error::for_action( 'create' );
247 }
248
249 return Components_Access_Controller::can_add_to_page()
250 ? null
251 : Insufficient_Permissions_Error::for_action( 'add_to_page' );
252 }
253
254 private function assemble_envelope( int $component_id, array $overrides_list ): array {
255 return [
256 '$$type' => Component_Instance_Prop_Type::get_key(),
257 'value' => [
258 'component_id' => [
259 '$$type' => Number_Prop_Type::get_key(),
260 'value' => $component_id,
261 ],
262 'overrides' => [
263 '$$type' => Overrides_Prop_Type::get_key(),
264 'value' => $overrides_list,
265 ],
266 ],
267 ];
268 }
269
270 private function resolve_component_id( array $shorthand, array $existing_settings ): int {
271 $supplied = (int) ( $shorthand['component_id'] ?? 0 );
272
273 if ( $supplied ) {
274 return $supplied;
275 }
276
277 return (int) Component_Instance_Prop_Type::extract_component_id( $existing_settings );
278 }
279
280 private function extract_overrides_list( array $existing_settings ): array {
281 $list = $existing_settings['component_instance']['value']['overrides']['value'] ?? [];
282
283 return is_array( $list ) ? $list : [];
284 }
285
286 private function merge_overrides_list( array $existing_list, array $partial_overrides, array $overridable_props, int $component_id ): array {
287 $updates = [];
288 $deletions = [];
289 foreach ( $partial_overrides as $key => $value ) {
290 if ( null === $value ) {
291 $deletions[ $key ] = true;
292 } else {
293 $updates[ $key ] = $value;
294 }
295 }
296
297 $new_items = $this->build_overrides_value( $updates, $overridable_props, $component_id );
298 $new_items_by_key = [];
299 foreach ( $new_items as $item ) {
300 $key = $item['value']['override_key'] ?? null;
301 if ( is_string( $key ) && '' !== $key ) {
302 $new_items_by_key[ $key ] = $item;
303 }
304 }
305
306 $merged = [];
307 foreach ( $existing_list as $item ) {
308 $key = $item['value']['override_key'] ?? null;
309 if ( ! is_string( $key ) || '' === $key ) {
310 $merged[] = $item;
311 continue;
312 }
313 if ( isset( $deletions[ $key ] ) ) {
314 continue;
315 }
316 if ( isset( $new_items_by_key[ $key ] ) ) {
317 $merged[] = $new_items_by_key[ $key ];
318 unset( $new_items_by_key[ $key ] );
319 continue;
320 }
321 $merged[] = $item;
322 }
323
324 foreach ( $new_items_by_key as $item ) {
325 $merged[] = $item;
326 }
327
328 return $merged;
329 }
330
331 private function load_valid_component( string $config_id, int $component_id, ?Document $document, array &$errors ) {
332 $component = $this->repository->get( $component_id, false );
333
334 if ( ! $component ) {
335 $errors[] = sprintf( '[%s] Component %d not found.', $config_id, $component_id );
336 return null;
337 }
338
339 if ( $component->get_is_archived() ) {
340 $errors[] = sprintf( '[%s] Component %d is archived and cannot be placed.', $config_id, $component_id );
341 return null;
342 }
343
344 if ( $document instanceof Component_Document ) {
345 $circular_result = Circular_Dependency_Validator::make()->validate(
346 $document->get_main_id(),
347 [ $this->make_placeholder_element( $component_id ) ]
348 );
349
350 if ( ! $circular_result['success'] ) {
351 $errors[] = sprintf( '[%s] %s', $config_id, implode( ' ', $circular_result['messages'] ) );
352 return null;
353 }
354 }
355
356 return $component;
357 }
358
359 private function validate_override_keys( string $config_id, array $raw_overrides, array $overridable_props ): array {
360 $errors = [];
361 $valid_keys = array_keys( $overridable_props );
362
363 foreach ( array_keys( $raw_overrides ) as $override_key ) {
364 if ( ! array_key_exists( $override_key, $overridable_props ) ) {
365 $errors[] = sprintf(
366 '[%s] Unknown override key "%s". Valid keys: %s.',
367 $config_id,
368 $override_key,
369 empty( $valid_keys ) ? '(none)' : implode( ', ', $valid_keys )
370 );
371 }
372 }
373
374 return $errors;
375 }
376
377 private function build_overrides_value( array $raw_overrides, array $overridable_props, int $component_id ): array {
378 $overrides = [];
379
380 foreach ( $raw_overrides as $override_key => $raw_value ) {
381 $prop = $overridable_props[ $override_key ] ?? null;
382
383 $overrides[] = [
384 '$$type' => Override_Prop_Type::get_key(),
385 'value' => [
386 'override_key' => $override_key,
387 'override_value' => $this->resolve_override_value( $raw_value, $prop ),
388 'schema_source' => [
389 'type' => Component_Override_Parser::get_override_type(),
390 'id' => $component_id,
391 ],
392 ],
393 ];
394 }
395
396 return $overrides;
397 }
398
399 private function resolve_override_value( $raw_value, ?Component_Overridable_Prop $prop ) {
400 if ( null === $raw_value ) {
401 return null;
402 }
403
404 $origin_prop_type = $this->resolve_origin_prop_type( $prop );
405
406 if ( ! $origin_prop_type instanceof Prop_Type ) {
407 return $raw_value;
408 }
409
410 $resolved = $this->plain_values_resolver->resolve( $raw_value, $origin_prop_type );
411
412 return $resolved ?? $raw_value;
413 }
414
415 private function resolve_origin_prop_type( ?Component_Overridable_Prop $prop ) {
416 if ( ! $prop ) {
417 return null;
418 }
419
420 try {
421 if ( $prop->origin_prop_fields ) {
422 return Parsing_Utils::get_prop_type(
423 $prop->origin_prop_fields['el_type'],
424 $prop->origin_prop_fields['widget_type'],
425 $prop->origin_prop_fields['prop_key']
426 );
427 }
428
429 return Parsing_Utils::get_prop_type( $prop->el_type, $prop->widget_type, $prop->prop_key );
430 } catch ( \Exception $e ) {
431 return null;
432 }
433 }
434
435 private function make_placeholder_element( int $component_id ): array {
436 return [
437 'elType' => 'widget',
438 'widgetType' => Component_Instance::get_element_type(),
439 'elements' => [],
440 'settings' => [
441 'component_instance' => $this->assemble_envelope( $component_id, [] ),
442 ],
443 ];
444 }
445 }
446