PluginProbe
Elementor Website Builder – more than just a page builder / trunk
Elementor Website Builder – more than just a page builder vtrunk
4.3.3 4.3.2 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 All 456 releases
elementor / modules / mcp / abilities / appliers / element-config-applier.php

element-config-applier.php in Elementor Website Builder – more than just a page builder trunk, at modules/mcp/abilities/appliers/element-config-applier.php

316 lines 10.0 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\Parsers\Props_Parser;
7 use Elementor\Modules\AtomicWidgets\PlainResolvers\Plain_Values_Resolver;
8 use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
9 use Elementor\Modules\Components\Components_Repository;
10 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Dynamic_Hoister;
11 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Non_Style_Allowlist;
12 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Settings_Validator;
13 use Elementor\Modules\Mcp\Abilities\Build_Composition\Widget_Type_Resolver;
14 use Elementor\Modules\Mcp\Abilities\Prop_Canonicalizer;
15 use Elementor\Modules\Mcp\Abilities\Utils\Dropped_Plain_Keys_Finder;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 class Element_Config_Applier {
22
23 const COMPONENT_INSTANCE_WIDGET_TYPE = 'e-component';
24
25 private Widget_Type_Resolver $type_resolver;
26 private Plain_Values_Resolver $plain_values_resolver;
27 private ?V3_Dynamic_Hoister $v3_dynamic_hoister;
28
29 public function __construct(
30 Widget_Type_Resolver $type_resolver,
31 Plain_Values_Resolver $plain_values_resolver,
32 ?V3_Dynamic_Hoister $v3_dynamic_hoister = null
33 ) {
34 $this->type_resolver = $type_resolver;
35 $this->plain_values_resolver = $plain_values_resolver;
36 $this->v3_dynamic_hoister = $v3_dynamic_hoister;
37 }
38
39 /**
40 * @param array<string, array&> $config_id_index Index of subtree refs.
41 * @param array<string, array<string, mixed>> $element_config Per-config-id settings.
42 * @param array<string, array> $widget_configs Resolved type configs.
43 * @param Document|null $document Target document, when one already exists.
44 *
45 * @return array{ error: ?\WP_Error, warnings: string[] }
46 */
47 public function apply( array &$config_id_index, array $element_config, array $widget_configs, ?Document $document = null ): array {
48 $errors = [];
49 $warnings = [];
50 $warning_codes = [];
51 $component_entries = [];
52
53 foreach ( $element_config as $config_id => $settings ) {
54 if ( ! isset( $config_id_index[ $config_id ] ) || ! is_array( $settings ) ) {
55 continue;
56 }
57
58 $node = &$config_id_index[ $config_id ];
59 $tag = $node['widgetType'] ?? $node['elType'] ?? null;
60
61 if ( self::COMPONENT_INSTANCE_WIDGET_TYPE === $tag ) {
62 $component_entries[ $config_id ] = $settings;
63 continue;
64 }
65
66 if ( V3_Node_Bridge::is_v3_node( $node ) ) {
67 $widget_type = (string) $tag;
68 $filter = V3_Non_Style_Allowlist::filter( $widget_type, $settings );
69 if ( $filter['error'] ) {
70 $errors[] = sprintf( '[%s] %s', $config_id, $filter['error']->get_error_message() );
71 continue;
72 }
73
74 $widget_config = is_array( $widget_configs[ $widget_type ] ?? null ) ? $widget_configs[ $widget_type ] : [];
75 $controls = is_array( $widget_config['controls'] ?? null ) ? $widget_config['controls'] : [];
76
77 $hoist_outcome = $this->get_v3_dynamic_hoister()->hoist( $widget_type, $filter['allowed'], $controls );
78
79 foreach ( $hoist_outcome['errors'] as $error_message ) {
80 $errors[] = sprintf( '[%s] %s', $config_id, $error_message );
81 }
82
83 $shape = V3_Settings_Validator::validate_shape( $widget_type, $hoist_outcome['primitives'], $widget_config );
84
85 if ( ! empty( $shape['valid'] ) ) {
86 $node['settings'] = $this->merge_with_clears( $node['settings'] ?? [], $shape['valid'] );
87 }
88
89 if ( $shape['error'] ) {
90 $errors[] = sprintf( '[%s] %s', $config_id, $shape['error']->get_error_message() );
91 }
92
93 if ( ! empty( $hoist_outcome['shortcodes'] ) ) {
94 $existing = is_array( $node['settings']['__dynamic__'] ?? null ) ? $node['settings']['__dynamic__'] : [];
95 $node['settings']['__dynamic__'] = array_merge( $existing, $hoist_outcome['shortcodes'] );
96 }
97
98 continue;
99 }
100
101 $schema = $this->type_resolver->get_props_schema( $tag, $widget_configs );
102
103 if ( ! $schema ) {
104 $node['settings'] = $this->merge_with_clears( $node['settings'] ?? [], $settings );
105 continue;
106 }
107
108 $outcome = $this->resolve_settings_against_schema( $settings, $schema, $tag, $config_id, $errors, $warnings, $warning_codes );
109
110 $this->warn_dropped_object_sub_keys(
111 $node['settings'] ?? [],
112 $outcome['resolved'],
113 $tag,
114 $config_id,
115 $warnings,
116 $warning_codes
117 );
118
119 $node['settings'] = array_merge( $node['settings'] ?? [], $outcome['resolved'] );
120
121 foreach ( $outcome['cleared'] as $cleared_key ) {
122 unset( $node['settings'][ $cleared_key ] );
123 }
124
125 $validation_error = $this->validate_settings( $node['settings'], $schema );
126 if ( $validation_error ) {
127 $errors[] = sprintf(
128 '[%s] Settings validation failed on element type "%s": %s. See elementor://widgets/schema/%s.',
129 $config_id,
130 $tag,
131 $validation_error,
132 $tag
133 );
134 }
135 }
136 unset( $node );
137
138 $component_error = empty( $component_entries )
139 ? null
140 : $this->apply_component_entries( $config_id_index, $component_entries, $document );
141
142 return [
143 'error' => $this->combine_errors( $errors, $component_error ),
144 'warnings' => $warnings,
145 'warning_codes' => array_values( array_unique( $warning_codes ) ),
146 ];
147 }
148
149 private function combine_errors( array $settings_errors, ?\WP_Error $component_error ): ?\WP_Error {
150 if ( empty( $settings_errors ) ) {
151 return $component_error;
152 }
153
154 if ( $component_error ) {
155 $settings_errors[] = $component_error->get_error_message();
156 }
157
158 return new \WP_Error(
159 'elementor_invalid_settings',
160 implode( ' ', $settings_errors ),
161 [ 'status' => \WP_Http::BAD_REQUEST ]
162 );
163 }
164
165 private function apply_component_entries( array &$config_id_index, array $component_entries, ?Document $document ): ?\WP_Error {
166 return $this->create_component_applier()->apply( $config_id_index, $component_entries, $document );
167 }
168
169 private function create_component_applier(): Component_Instance_Applier {
170 return new Component_Instance_Applier( new Components_Repository(), $this->plain_values_resolver );
171 }
172
173 private function resolve_settings_against_schema(
174 array $settings,
175 array $schema,
176 string $element_type,
177 string $config_id,
178 array &$errors,
179 array &$warnings,
180 array &$warning_codes = []
181 ): array {
182 $alias_map = Prop_Canonicalizer::build_alias_map( $schema );
183 $resolved = [];
184 $cleared = [];
185
186 foreach ( $settings as $name => $value ) {
187 $canonical = Prop_Canonicalizer::resolve_canonical_key( $schema, $name, $alias_map );
188
189 if ( null === $canonical ) {
190 $warnings[] = sprintf(
191 '[%s] Property "%s" is not supported on element type "%s" and was skipped.',
192 $config_id,
193 $name,
194 $element_type
195 );
196 $warning_codes[] = 'prop_not_supported';
197 continue;
198 }
199
200 if ( null === $value ) {
201 $cleared[] = $canonical;
202 continue;
203 }
204
205 $prop_type = $schema[ $canonical ] ?? null;
206
207 if ( ! $prop_type instanceof Prop_Type ) {
208 continue;
209 }
210
211 $resolved_value = $this->plain_values_resolver->resolve( $value, $prop_type );
212
213 if ( null === $resolved_value ) {
214 $errors[] = sprintf(
215 '[%s] Property "%s" on "%s" could not be resolved. See elementor://widgets/schema/%s.',
216 $config_id,
217 $canonical,
218 $element_type,
219 $element_type
220 );
221 continue;
222 }
223
224 $dropped_paths = Dropped_Plain_Keys_Finder::find( $value, $resolved_value, $canonical );
225
226 if ( ! empty( $dropped_paths ) ) {
227 $this->warn_plain_keys_dropped( $dropped_paths, $canonical, $element_type, $config_id, $warnings, $warning_codes );
228 }
229
230 $resolved[ $canonical ] = $resolved_value;
231 }
232
233 return [
234 'resolved' => $resolved,
235 'cleared' => $cleared,
236 ];
237 }
238
239 private function warn_dropped_object_sub_keys( array $node_settings, array $resolved, string $element_type, string $config_id, array &$warnings, array &$warning_codes ): void {
240 foreach ( $resolved as $key => $value ) {
241 $stored = $node_settings[ $key ] ?? null;
242
243 if (
244 ! $this->is_object_prop_value( $stored )
245 || ! $this->is_object_prop_value( $value )
246 || $stored['$$type'] !== $value['$$type']
247 ) {
248 continue;
249 }
250
251 $stored_sub_values = array_filter( $stored['value'], fn( $sub_value ) => null !== $sub_value );
252 $dropped_sub_keys = array_keys( array_diff_key( $stored_sub_values, $value['value'] ) );
253
254 if ( empty( $dropped_sub_keys ) ) {
255 continue;
256 }
257
258 $warnings[] = sprintf(
259 '[%s] Property "%s" on "%s" was replaced as a whole, dropping its stored "%s". Send the full value (as returned by elementor/get-page-structure) to keep them. See elementor://widgets/schema/%s.',
260 $config_id,
261 $key,
262 $element_type,
263 implode( '", "', $dropped_sub_keys ),
264 $element_type
265 );
266 $warning_codes[] = 'prop_subkeys_dropped';
267 }
268 }
269
270 private function warn_plain_keys_dropped( array $dropped_paths, string $key, string $element_type, string $config_id, array &$warnings, array &$warning_codes ): void {
271 $warnings[] = sprintf(
272 '[%s] Property "%s" on "%s" was saved without "%s": each is either not a field of the prop or has a value that could not be resolved. Use the shape from elementor://widgets/schema/%s.',
273 $config_id,
274 $key,
275 $element_type,
276 implode( '", "', $dropped_paths ),
277 $element_type
278 );
279 $warning_codes[] = 'prop_keys_dropped';
280 }
281
282 private function is_object_prop_value( $value ): bool {
283 return is_array( $value )
284 && isset( $value['$$type'] )
285 && is_array( $value['value'] ?? null )
286 && ! empty( $value['value'] )
287 && ! wp_is_numeric_array( $value['value'] );
288 }
289
290 private function merge_with_clears( array $existing, array $incoming ): array {
291 $merged = $existing;
292 foreach ( $incoming as $key => $value ) {
293 if ( null === $value ) {
294 unset( $merged[ $key ] );
295 continue;
296 }
297 $merged[ $key ] = $value;
298 }
299 return $merged;
300 }
301
302 private function validate_settings( array $settings, array $schema ): ?string {
303 $result = Props_Parser::make( $schema )->parse( $settings );
304
305 return $result->is_valid() ? null : $result->errors()->to_string();
306 }
307
308 private function get_v3_dynamic_hoister(): V3_Dynamic_Hoister {
309 if ( null === $this->v3_dynamic_hoister ) {
310 $this->v3_dynamic_hoister = new V3_Dynamic_Hoister();
311 }
312
313 return $this->v3_dynamic_hoister;
314 }
315 }
316