PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta1
Elementor Website Builder – more than just a page builder v4.3.0-beta1
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 4.0.7 All 451 releases
elementor / core / utils / document / document-mutator.php

document-mutator.php in Elementor Website Builder – more than just a page builder 4.3.0-beta1, at core/utils/document/document-mutator.php

428 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Core\Utils\Document;
4
5 use Elementor\Core\Base\Document;
6 use Elementor\Plugin;
7 use Elementor\Utils;
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 class Document_Mutator {
14
15 const DOCUMENT_ROOT = 'document';
16
17 const WIDGET_EL_TYPE = 'widget';
18
19 /** @var \Elementor\Elements_Manager */
20 private $element_manager;
21
22 /** @var \Elementor\Widgets_Manager */
23 private $widgets_manager;
24
25 public function __construct( $element_manager, $widgets_manager ) {
26 $this->element_manager = $element_manager;
27 $this->widgets_manager = $widgets_manager;
28 }
29
30 public static function instance(): self {
31 return new self(
32 Plugin::$instance->elements_manager,
33 Plugin::$instance->widgets_manager
34 );
35 }
36
37 /**
38 * Locate a node by id and return a by-reference index [ $id => &$node ]
39 * so callers can mutate the tree in place.
40 *
41 * @return array<string, array&>
42 */
43 public function build_ref_index( array &$tree, string $id ): array {
44 $index = [];
45 $this->walk_for_ref_index( $tree, $id, $index );
46
47 return $index;
48 }
49
50 private function walk_for_ref_index( array &$tree, string $id, array &$index ): bool {
51 foreach ( $tree as &$node ) {
52 if ( isset( $node['id'] ) && $node['id'] === $id ) {
53 $index[ $id ] = &$node;
54 return true;
55 }
56 if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
57 if ( $this->walk_for_ref_index( $node['elements'], $id, $index ) ) {
58 return true;
59 }
60 }
61 }
62 unset( $node );
63
64 return false;
65 }
66
67 public function find_by_id( array $tree, string $id ): ?array {
68 foreach ( $tree as $node ) {
69 if ( isset( $node['id'] ) && $node['id'] === $id ) {
70 return $node;
71 }
72
73 if ( ! empty( $node['elements'] ) ) {
74 $found = $this->find_by_id( $node['elements'], $id );
75 if ( null !== $found ) {
76 return $found;
77 }
78 }
79 }
80
81 return null;
82 }
83
84 public function generate_id(): string {
85 return Utils::generate_random_string();
86 }
87
88 /**
89 * @return array|\WP_Error
90 */
91 public function insert_at( array $tree, string $parent_id, ?int $index, array $element ) {
92 if ( ! isset( $element['id'] ) ) {
93 $element['id'] = $this->generate_id();
94 }
95
96 if ( self::DOCUMENT_ROOT === $parent_id ) {
97 $root_child_error = $this->check_document_root_child_allowed( $element );
98
99 if ( is_wp_error( $root_child_error ) ) {
100 return $root_child_error;
101 }
102
103 return $this->splice_into( $tree, $index, $element );
104 }
105
106 $result = $this->insert_into_tree( $tree, $parent_id, $index, $element );
107
108 if ( is_wp_error( $result ) ) {
109 return $result;
110 }
111
112 if ( false === $result ) {
113 return new \WP_Error(
114 'elementor_not_found',
115 __( 'Parent element not found.', 'elementor' ),
116 [ 'status' => \WP_Http::NOT_FOUND ]
117 );
118 }
119
120 return $result;
121 }
122
123 /**
124 * @return array|\WP_Error
125 */
126 public function remove( array $tree, string $id ) {
127 $result = $this->remove_from_tree( $tree, $id );
128
129 if ( false === $result ) {
130 return new \WP_Error(
131 'elementor_not_found',
132 __( 'Element not found.', 'elementor' ),
133 [ 'status' => \WP_Http::NOT_FOUND ]
134 );
135 }
136
137 return $result;
138 }
139
140 /**
141 * @return array|\WP_Error
142 */
143 public function move( array $tree, string $id, string $new_parent_id, ?int $index ) {
144 $node = $this->find_by_id( $tree, $id );
145
146 if ( null === $node ) {
147 return new \WP_Error(
148 'elementor_not_found',
149 __( 'Element not found.', 'elementor' ),
150 [ 'status' => \WP_Http::NOT_FOUND ]
151 );
152 }
153
154 $tree = $this->remove( $tree, $id );
155
156 if ( is_wp_error( $tree ) ) {
157 return $tree;
158 }
159
160 return $this->insert_at( $tree, $new_parent_id, $index, $node );
161 }
162
163 /**
164 * Duplicate an element in place, inserting the clone right after the source
165 * inside the same parent. All ids in the clone are regenerated.
166 *
167 * @return array|\WP_Error
168 */
169 public function duplicate( array $tree, string $id ) {
170 $location = $this->locate_parent_and_index( $tree, $id );
171
172 if ( null === $location ) {
173 return new \WP_Error(
174 'elementor_not_found',
175 __( 'Element not found.', 'elementor' ),
176 [ 'status' => \WP_Http::NOT_FOUND ]
177 );
178 }
179
180 [ $parent_id, $index, $node ] = $location;
181
182 return $this->insert_subtree( $tree, $parent_id, $index + 1, $node );
183 }
184
185 /**
186 * Save an elements tree to a document, downgrading `publish` to `draft`
187 * first so no live changes leak out.
188 *
189 * @return true|int|\WP_Error
190 */
191 public function save_as_draft( Document $document, array $elements ) {
192 if ( 'publish' === get_post_status( $document->get_main_id() ) ) {
193 wp_update_post( [
194 'ID' => $document->get_main_id(),
195 'post_status' => 'draft',
196 ] );
197 }
198
199 return $document->save( [ 'elements' => $elements ] );
200 }
201
202 /**
203 * @return array|\WP_Error
204 */
205 public function patch_settings( array $tree, string $id, array $partial_settings ) {
206 $result = $this->patch_settings_in_tree( $tree, $id, $partial_settings );
207
208 if ( false === $result ) {
209 return new \WP_Error(
210 'elementor_not_found',
211 __( 'Element not found.', 'elementor' ),
212 [ 'status' => \WP_Http::NOT_FOUND ]
213 );
214 }
215
216 return $result;
217 }
218
219 /**
220 * Insert a subtree into the document tree, recursively generating IDs for all elements.
221 *
222 * @param array $tree The current document tree.
223 * @param string $parent_id Parent element ID or 'document' for root.
224 * @param int|null $index Insertion index (null = append).
225 * @param array $subtree The subtree to insert (single element with nested children).
226 *
227 * @return array|\WP_Error The updated tree or error.
228 */
229 public function insert_subtree( array $tree, string $parent_id, ?int $index, array $subtree ) {
230 $subtree_with_ids = $this->generate_ids_recursive( $subtree );
231
232 return $this->insert_at( $tree, $parent_id, $index, $subtree_with_ids );
233 }
234
235 /**
236 * Recursively generate IDs for an element and all its children.
237 * Always generates new IDs to avoid duplicates when inserting subtrees.
238 *
239 * @param array $element The element to process.
240 *
241 * @return array The element with new IDs assigned.
242 */
243 private function generate_ids_recursive( array $element ): array {
244 $element['id'] = $this->generate_id();
245
246 if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
247 $element['elements'] = array_map(
248 fn( array $child ) => $this->generate_ids_recursive( $child ),
249 $element['elements']
250 );
251 }
252
253 return $element;
254 }
255
256 /**
257 * @return array{0: string, 1: int, 2: array}|null [ parent_id, index, node ] or null when not found.
258 */
259 private function locate_parent_and_index( array $tree, string $id, string $parent_id = self::DOCUMENT_ROOT ): ?array {
260 foreach ( $tree as $i => $node ) {
261 if ( isset( $node['id'] ) && $node['id'] === $id ) {
262 return [ $parent_id, $i, $node ];
263 }
264
265 if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
266 $found = $this->locate_parent_and_index( $node['elements'], $id, (string) ( $node['id'] ?? '' ) );
267 if ( null !== $found ) {
268 return $found;
269 }
270 }
271 }
272
273 return null;
274 }
275
276 private function splice_into( array $children, ?int $index, array $element ): array {
277 $count = count( $children );
278
279 if ( null === $index || $index >= $count ) {
280 $children[] = $element;
281 return $children;
282 }
283
284 array_splice( $children, $index, 0, [ $element ] );
285 return $children;
286 }
287
288 /**
289 * @return array|false|\WP_Error
290 */
291 private function insert_into_tree( array $tree, string $parent_id, ?int $index, array $element ) {
292 foreach ( $tree as $i => $node ) {
293 if ( isset( $node['id'] ) && $node['id'] === $parent_id ) {
294 if ( isset( $node['elType'] ) && self::WIDGET_EL_TYPE === $node['elType'] ) {
295 return new \WP_Error(
296 'elementor_invalid_parent',
297 __( 'Cannot insert into a widget element.', 'elementor' ),
298 [ 'status' => \WP_Http::BAD_REQUEST ]
299 );
300 }
301
302 $child_type_error = $this->check_child_type_allowed( $node, $element );
303 if ( is_wp_error( $child_type_error ) ) {
304 return $child_type_error;
305 }
306
307 $node['elements'] = $this->splice_into( $node['elements'] ?? [], $index, $element );
308 $tree[ $i ] = $node;
309 return $tree;
310 }
311
312 if ( ! empty( $node['elements'] ) ) {
313 $result = $this->insert_into_tree( $node['elements'], $parent_id, $index, $element );
314
315 if ( false !== $result ) {
316 if ( is_wp_error( $result ) ) {
317 return $result;
318 }
319 $node['elements'] = $result;
320 $tree[ $i ] = $node;
321 return $tree;
322 }
323 }
324 }
325
326 return false;
327 }
328
329 /**
330 * @return array|false
331 */
332 private function remove_from_tree( array $tree, string $id ) {
333 foreach ( $tree as $i => $node ) {
334 if ( isset( $node['id'] ) && $node['id'] === $id ) {
335 array_splice( $tree, $i, 1 );
336 return $tree;
337 }
338
339 if ( ! empty( $node['elements'] ) ) {
340 $result = $this->remove_from_tree( $node['elements'], $id );
341 if ( false !== $result ) {
342 $node['elements'] = $result;
343 $tree[ $i ] = $node;
344 return $tree;
345 }
346 }
347 }
348
349 return false;
350 }
351
352 /**
353 * Mirrors the editor's `Document.isValidChild`: only element types may sit at the document
354 * root, so widgets inserted here would produce a tree the editor itself refuses to create.
355 *
356 * @return null|\WP_Error
357 */
358 private function check_document_root_child_allowed( array $element ): ?\WP_Error {
359 if ( self::WIDGET_EL_TYPE !== ( $element['elType'] ?? '' ) ) {
360 return null;
361 }
362
363 return new \WP_Error(
364 'elementor_invalid_parent',
365 sprintf(
366 /* translators: %s: widget type */
367 __( '"%s" is a widget and cannot be a direct child of the document. Wrap it in a container element such as e-flexbox or e-div-block.', 'elementor' ),
368 $element['widgetType'] ?? self::WIDGET_EL_TYPE
369 ),
370 [ 'status' => \WP_Http::BAD_REQUEST ]
371 );
372 }
373
374 /**
375 * @return null|\WP_Error
376 */
377 private function check_child_type_allowed( array $parent_node, array $child ): ?\WP_Error {
378 $parent_instance = $this->element_manager->get_element_types( $parent_node['elType'] ?? '' );
379
380 if ( ! $parent_instance ) {
381 return null;
382 }
383
384 $config = $parent_instance->get_config();
385 $allowed = $config['allowed_child_types'] ?? [];
386
387 if ( empty( $allowed ) ) {
388 return null;
389 }
390
391 $child_type = $child['widgetType'] ?? $child['elType'] ?? '';
392
393 if ( ! in_array( $child_type, $allowed, true ) ) {
394 return new \WP_Error(
395 'elementor_invalid_parent',
396 __( 'Element type not allowed as child of this parent.', 'elementor' ),
397 [ 'status' => \WP_Http::BAD_REQUEST ]
398 );
399 }
400
401 return null;
402 }
403
404 /**
405 * @return array|false
406 */
407 private function patch_settings_in_tree( array $tree, string $id, array $partial_settings ) {
408 foreach ( $tree as $i => $node ) {
409 if ( isset( $node['id'] ) && $node['id'] === $id ) {
410 $node['settings'] = array_merge( $node['settings'] ?? [], $partial_settings );
411 $tree[ $i ] = $node;
412 return $tree;
413 }
414
415 if ( ! empty( $node['elements'] ) ) {
416 $result = $this->patch_settings_in_tree( $node['elements'], $id, $partial_settings );
417 if ( false !== $result ) {
418 $node['elements'] = $result;
419 $tree[ $i ] = $node;
420 return $tree;
421 }
422 }
423 }
424
425 return false;
426 }
427 }
428