PluginProbe
Elementor Website Builder – more than just a page builder / 4.0.1
Elementor Website Builder – more than just a page builder v4.0.1
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 / modules / components / components-rest-api.php

components-rest-api.php in Elementor Website Builder – more than just a page builder 4.0.1, at modules/components/components-rest-api.php

709 lines 21.4 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\Components;
4
5 use Elementor\Core\Base\Document;
6 use Elementor\Core\Utils\Api\Error_Builder;
7 use Elementor\Core\Utils\Api\Response_Builder;
8 use Elementor\Core\Utils\Collection;
9 use Elementor\Modules\Components\Documents\Component;
10 use Elementor\Modules\Components\OverridableProps\Component_Overridable_Props_Parser;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 class Components_REST_API {
17 const API_NAMESPACE = 'elementor/v1';
18 const API_BASE = 'components';
19 const LOCK_DOCUMENT_TYPE_NAME = 'components';
20 const STYLES_ROUTE = 'styles';
21 const MAX_COMPONENTS = 100;
22
23 private $repository = null;
24 public function register_hooks() {
25 add_action( 'rest_api_init', fn() => $this->register_routes() );
26 }
27
28 private function get_repository() {
29 if ( ! $this->repository ) {
30 $this->repository = new Components_Repository();
31 }
32
33 return $this->repository;
34 }
35
36 /**
37 * @return Component_Lock_Manager instance
38 */
39 private function get_component_lock_manager() {
40 return Component_Lock_Manager::get_instance();
41 }
42
43 private function register_routes() {
44 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
45 [
46 'methods' => 'GET',
47 'callback' => fn() => $this->route_wrapper( fn() => $this->get_components() ),
48 'permission_callback' => fn() => current_user_can( 'edit_posts' ),
49 ],
50 ] );
51
52 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/' . self::STYLES_ROUTE, [
53 [
54 'methods' => 'GET',
55 'callback' => fn() => $this->route_wrapper( fn() => $this->get_styles() ),
56 'permission_callback' => fn() => current_user_can( 'edit_posts' ),
57 ],
58 ] );
59
60 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
61 [
62 'methods' => 'POST',
63 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_components( $request ) ),
64 'permission_callback' => fn() => current_user_can( 'manage_options' ),
65 'args' => [
66 'status' => [
67 'type' => 'string',
68 'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
69 'required' => true,
70 ],
71 'items' => [
72 'type' => 'array',
73 'required' => true,
74 'items' => [
75 'type' => 'object',
76 'properties' => [
77 'uid' => [
78 'type' => 'string',
79 'required' => true,
80 ],
81 'title' => [
82 'type' => 'string',
83 'required' => true,
84 'minLength' => 2,
85 'maxLength' => 200,
86 ],
87 'elements' => [
88 'type' => 'array',
89 'required' => true,
90 'items' => [
91 'type' => 'object',
92 ],
93 ],
94 'settings' => [
95 'type' => 'object',
96 'required' => false,
97 ],
98 ],
99 ],
100 ],
101 ],
102 ],
103 ] );
104
105 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/create-validate', [
106 [
107 'methods' => 'POST',
108 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_validate_components( $request ) ),
109 'permission_callback' => fn() => current_user_can( 'manage_options' ),
110 'args' => [
111 'items' => [
112 'type' => 'array',
113 'required' => true,
114 'items' => [
115 'type' => 'object',
116 'properties' => [
117 'uid' => [
118 'type' => 'string',
119 'required' => true,
120 ],
121 'title' => [
122 'type' => 'string',
123 'required' => true,
124 'minLength' => 2,
125 'maxLength' => 200,
126 ],
127 'elements' => [
128 'type' => 'array',
129 'required' => true,
130 'items' => [
131 'type' => 'object',
132 ],
133 ],
134 'settings' => [
135 'type' => 'object',
136 'required' => false,
137 ],
138 ],
139 ],
140 ],
141 ],
142 ],
143 ] );
144
145 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/overridable-props', [
146 [
147 'methods' => 'GET',
148 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->get_overridable_props( $request ) ),
149 'permission_callback' => fn() => current_user_can( 'edit_posts' ),
150 'args' => [
151 'componentId' => [
152 'type' => 'integer',
153 'required' => true,
154 'description' => 'The component ID to get overridable props for',
155 ],
156 ],
157 ],
158 ] );
159
160 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/status', [
161 [
162 'methods' => 'PUT',
163 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->update_statuses( $request ) ),
164 'permission_callback' => fn() => current_user_can( 'manage_options' ),
165 'args' => [
166 'status' => [
167 'type' => 'string',
168 'required' => true,
169 'enum' => [ Document::STATUS_PUBLISH ],
170 ],
171 'ids' => [
172 'type' => 'array',
173 'required' => true,
174 'items' => [
175 'type' => 'number',
176 'required' => true,
177 ],
178 ],
179 ],
180 ],
181 ] );
182
183 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/lock', [
184 [
185 'methods' => 'POST',
186 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->lock_component( $request ) ),
187 'permission_callback' => fn() => current_user_can( 'manage_options' ),
188 'args' => [
189 'componentId' => [
190 'type' => 'number',
191 'required' => true,
192 'description' => 'The component ID to unlock',
193 ],
194 ],
195 ],
196 ] );
197
198 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/unlock', [
199 [
200 'methods' => 'POST',
201 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->unlock_component( $request ) ),
202 'permission_callback' => fn() => current_user_can( 'manage_options' ),
203 'args' => [
204 'componentId' => [
205 'type' => 'number',
206 'required' => true,
207 'description' => 'The component ID to unlock',
208 ],
209 ],
210 ],
211 ] );
212
213 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/lock-status', [
214 [
215 'methods' => 'GET',
216 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->get_lock_status( $request ) ),
217 'permission_callback' => fn() => current_user_can( 'manage_options' ),
218 'args' => [
219 'componentId' => [
220 'type' => 'string',
221 'required' => true,
222 'description' => 'The component ID to check lock status',
223 ],
224 ],
225 ],
226 ] );
227
228 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/archive', [
229 [
230 'methods' => 'POST',
231 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->archive_components( $request ) ),
232 'permission_callback' => fn() => current_user_can( 'manage_options' ),
233 'args' => [
234 'componentIds' => [
235 'type' => 'array',
236 'items' => [
237 'type' => 'number',
238 'required' => true,
239 ],
240 'required' => true,
241 'description' => 'The component IDs to archive',
242 ],
243 'status' => [
244 'type' => 'string',
245 'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
246 'required' => true,
247 ],
248 ],
249 ],
250 ] );
251
252 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/update-titles', [
253 [
254 'methods' => 'POST',
255 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->update_components_title( $request ) ),
256 'permission_callback' => fn() => current_user_can( 'manage_options' ),
257 'args' => [
258 'components' => [
259 'type' => 'array',
260 'required' => true,
261 'items' => [
262 'type' => 'object',
263 'properties' => [
264 'componentId' => [
265 'type' => 'number',
266 'required' => true,
267 'description' => 'The component ID to update title',
268 ],
269 'title' => [
270 'type' => 'string',
271 'required' => true,
272 'description' => 'The new title for the component',
273 ],
274 ],
275 ],
276 ],
277 'status' => [
278 'type' => 'string',
279 'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
280 'required' => true,
281 ],
282 ],
283 ],
284 ] );
285 }
286
287 private function get_components() {
288 $components = $this->get_repository()->all();
289
290 $components_list = array_values( $components
291 ->map( fn( $component ) => [
292 'id' => $component['id'],
293 'name' => $component['title'],
294 'uid' => $component['uid'],
295 'isArchived' => $component['is_archived'] ?? false,
296 ] )
297 ->all() );
298
299 return Response_Builder::make( $components_list )->build();
300 }
301
302 private function get_styles() {
303 $components = $this->get_repository()->all();
304
305 $styles = [];
306 $components->each( function( $component ) use ( &$styles ) {
307 $styles[ $component['id'] ] = $component['styles'];
308 } );
309
310 return Response_Builder::make( $styles )->build();
311 }
312
313 private function get_overridable_props( \WP_REST_Request $request ) {
314 $component_id = (int) $request->get_param( 'componentId' );
315
316 if ( ! $component_id ) {
317 return Error_Builder::make( 'invalid_component_id' )
318 ->set_status( 400 )
319 ->set_message( __( 'Invalid component ID', 'elementor' ) )
320 ->build();
321 }
322
323 /** @var Component $document */
324 $document = $this->get_repository()->get( $component_id );
325
326 if ( ! $document ) {
327 return Error_Builder::make( 'component_not_found' )
328 ->set_status( 404 )
329 ->set_message( __( 'Component not found', 'elementor' ) )
330 ->build();
331 }
332
333 // This is a fix for the case where overridable props in element settings where migrated
334 // but the overridable props metadata were not aligned with the new origin values.
335 // In version 4.0.1, we fixed this by running the align_overridable_props_with_elements method after the migration.
336 $document_version = $document->get_elementor_version();
337 $overridable_props_migration_fix_version = '4.0.1';
338 $should_align_overridable_props = version_compare( $document_version, $overridable_props_migration_fix_version, '<=' );
339 if ( $should_align_overridable_props ) {
340 $document->align_overridable_props_with_elements();
341 }
342
343 $overridable = $document->get_json_meta( Component::OVERRIDABLE_PROPS_META_KEY ) ?? null;
344
345 if ( empty( $overridable ) ) {
346 $overridable = null;
347 }
348
349 return Response_Builder::make( $overridable )->build();
350 }
351
352 private function create_components( \WP_REST_Request $request ) {
353 if ( ! Components_Access_Controller::can_create() ) {
354 return $this->get_insufficient_permissions_error( 'create' );
355 }
356
357 $save_status = $request->get_param( 'status' );
358
359 $items = Collection::make( $request->get_param( 'items' ) );
360 $components = $this->get_repository()->all();
361
362 $result = Save_Components_Validator::make( $components )->validate( $items );
363
364 if ( ! $result['success'] ) {
365 return Error_Builder::make( 'components_validation_failed' )
366 ->set_status( 422 )
367 ->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
368 ->build();
369 }
370
371 $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
372
373 if ( ! $circular_result['success'] ) {
374 return Error_Builder::make( 'circular_dependency_detected' )
375 ->set_status( 422 )
376 ->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
377 ->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
378 ->build();
379 }
380
381 $non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );
382
383 if ( ! $non_atomic_result['success'] ) {
384 return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
385 ->set_status( 422 )
386 ->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
387 ->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
388 ->build();
389 }
390
391 $validation_errors = [];
392
393 $created = $items->map_with_keys( function ( $item ) use ( $save_status, &$validation_errors ) {
394 $title = sanitize_text_field( $item['title'] );
395 $content = $item['elements'];
396 $uid = $item['uid'];
397
398 try {
399 $settings = isset( $item['settings'] ) ? $this->parse_settings( $item['settings'] ) : [];
400
401 $status = Document::STATUS_AUTOSAVE === $save_status
402 ? Document::STATUS_DRAFT
403 : $save_status;
404
405 $component_id = $this->get_repository()->create( $title, $content, $status, $uid, $settings );
406
407 return [ $uid => $component_id ];
408 } catch ( \Exception $e ) {
409 $validation_errors[ $uid ] = $e->getMessage();
410 return [ $uid => null ];
411 }
412 } );
413
414 if ( ! empty( $validation_errors ) ) {
415 return Error_Builder::make( 'settings_validation_failed' )
416 ->set_status( 422 )
417 ->set_message( 'Settings validation failed: ' . json_encode( $validation_errors ) )
418 ->build();
419 }
420
421 return Response_Builder::make( $created->all() )
422 ->set_status( 201 )
423 ->build();
424 }
425
426 private function update_statuses( \WP_REST_Request $request ) {
427 if ( ! Components_Access_Controller::can_publish() ) {
428 return $this->get_insufficient_permissions_error( 'publish' );
429 }
430
431 $result = Collection::make( $request->get_param( 'ids' ) )
432 ->reduce(
433 function ( $result, int $component_id ) {
434 $component = $this->get_repository()->get( $component_id );
435
436 if ( ! $component ) {
437 $result['failed'][] = $component_id;
438 return $result;
439 }
440
441 $publish_result = $this->get_repository()->publish_component( $component );
442
443 $result[ $publish_result ? 'success' : 'failed' ][] = $component_id;
444
445 return $result;
446 },
447 [
448 'success' => [],
449 'failed' => [],
450 ]
451 );
452
453 return Response_Builder::make( $result )->build();
454 }
455
456 private function lock_component( \WP_REST_Request $request ) {
457 if ( ! Components_Access_Controller::can_lock() ) {
458 return $this->get_insufficient_permissions_error( 'lock' );
459 }
460
461 $component_id = $request->get_param( 'componentId' );
462 try {
463 $success = $this->get_component_lock_manager()->lock( $component_id );
464 } catch ( \Exception $e ) {
465 error_log( 'Components REST API lock_component error: ' . $e->getMessage() );
466 return Error_Builder::make( 'lock_failed' )
467 ->set_status( 500 )
468 ->set_message( __( 'Failed to lock component', 'elementor' ) )
469 ->build();
470 }
471
472 if ( ! $success ) {
473 return Error_Builder::make( 'lock_failed' )
474 ->set_status( 500 )
475 ->set_message( __( 'Failed to lock component', 'elementor' ) )
476 ->build();
477 }
478
479 return Response_Builder::make( [ 'locked' => $success ] )->build();
480 }
481
482 private function unlock_component( \WP_REST_Request $request ) {
483 if ( ! Components_Access_Controller::can_lock() ) {
484 return $this->get_insufficient_permissions_error( 'unlock' );
485 }
486
487 $component_id = $request->get_param( 'componentId' );
488 try {
489 $success = $this->get_component_lock_manager()->unlock( $component_id );
490 } catch ( \Exception $e ) {
491 error_log( 'Components REST API unlock_component error: ' . $e->getMessage() );
492 return Error_Builder::make( 'unlock_failed' )
493 ->set_status( 500 )
494 ->set_message( __( 'Failed to unlock component', 'elementor' ) )
495 ->build();
496 }
497
498 if ( ! $success ) {
499 return Error_Builder::make( 'unlock_failed' )
500 ->set_status( 500 )
501 ->set_message( __( 'Failed to unlock component', 'elementor' ) )
502 ->build();
503 }
504 return Response_Builder::make( [ 'unlocked' => $success ] )->build();
505 }
506
507 private function get_lock_status( \WP_REST_Request $request ) {
508 if ( ! Components_Access_Controller::can_lock() ) {
509 return $this->get_insufficient_permissions_error( 'lock_status' );
510 }
511
512 $component_id = (int) $request->get_param( 'componentId' );
513 try {
514 $lock_manager = $this->get_component_lock_manager();
515 if ( $lock_manager->is_lock_expired( $component_id ) ) {
516 $lock_manager->unlock( $component_id );
517 }
518
519 $lock_data = $lock_manager->get_lock_data( $component_id );
520 $current_user_id = get_current_user_id();
521
522 // if current user is the lock user, return true
523 if ( $lock_data['locked_by'] && $lock_data['locked_by'] === $current_user_id ) {
524 return Response_Builder::make( [
525 'is_current_user_allow_to_edit' => true,
526 'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
527 ] )->build();
528 }
529
530 // if the user is not the lock user, return false
531 if ( $lock_data['locked_by'] && $lock_data['locked_by'] !== $current_user_id ) {
532 return Response_Builder::make( [
533 'is_current_user_allow_to_edit' => false,
534 'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
535 ] )->build();
536 }
537
538 // if the component is not locked, return true
539 if ( ! $lock_data['locked_by'] ) {
540 return Response_Builder::make( [
541 'is_current_user_allow_to_edit' => true,
542 'locked_by' => null,
543 ] )->build();
544 }
545 } catch ( \Exception $e ) {
546 error_log( 'Components REST API get_lock_status error: ' . $e->getMessage() );
547 return Error_Builder::make( 'get_lock_status_failed' )
548 ->set_status( 500 )
549 ->set_message( __( 'Failed to get lock status', 'elementor' ) )
550 ->build();
551 }
552 }
553
554 private function archive_components( \WP_REST_Request $request ) {
555 if ( ! Components_Access_Controller::can_delete() ) {
556 return $this->get_insufficient_permissions_error( 'delete' );
557 }
558
559 $component_ids = $request->get_param( 'componentIds' );
560 $status = $request->get_param( 'status' );
561
562 try {
563 $result = $this->get_repository()->archive( $component_ids, $status );
564 } catch ( \Exception $e ) {
565 error_log( 'Components REST API archive_components error: ' . $e->getMessage() );
566 return Error_Builder::make( 'archive_failed' )
567 ->set_meta( [ 'error' => $e->getMessage() ] )
568 ->set_status( 500 )
569 ->set_message( __( 'Failed to archive components', 'elementor' ) )
570 ->build();
571 }
572 return Response_Builder::make( $result )->build();
573 }
574
575 private function update_components_title( \WP_REST_Request $request ) {
576 if ( ! Components_Access_Controller::can_rename() ) {
577 return $this->get_insufficient_permissions_error( 'rename' );
578 }
579
580 $failed_ids = [];
581 $success_ids = [];
582 $components = $request->get_param( 'components' );
583 $status = $request->get_param( 'status' );
584
585 foreach ( $components as $component ) {
586 $is_success = $this->get_repository()->update_title( $component['componentId'], $component['title'], $status );
587
588 if ( ! $is_success ) {
589 $failed_ids[] = $component['componentId'];
590 continue;
591 }
592 $success_ids[] = $component['componentId'];
593
594 }
595 return Response_Builder::make( [
596 'failedIds' => $failed_ids,
597 'successIds' => $success_ids,
598 ] )->build();
599 }
600
601 private function create_validate_components( \WP_REST_Request $request ) {
602 if ( ! Components_Access_Controller::can_create() ) {
603 return $this->get_insufficient_permissions_error( 'create' );
604 }
605
606 $items = Collection::make( $request->get_param( 'items' ) );
607 $components = $this->get_repository()->all();
608
609 $result = Save_Components_Validator::make( $components )->validate( $items );
610
611 if ( ! $result['success'] ) {
612 return Error_Builder::make( 'components_validation_failed' )
613 ->set_status( 422 )
614 ->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
615 ->build();
616 }
617
618 $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
619
620 if ( ! $circular_result['success'] ) {
621 return Error_Builder::make( 'circular_dependency_detected' )
622 ->set_status( 422 )
623 ->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
624 ->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
625 ->build();
626 }
627
628 $non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );
629
630 if ( ! $non_atomic_result['success'] ) {
631 return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
632 ->set_status( 422 )
633 ->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
634 ->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
635 ->build();
636 }
637
638 $validation_errors = $items->map_with_keys( function ( $item ) {
639 try {
640 if ( isset( $item['settings'] ) ) {
641 $this->parse_settings( $item['settings'] );
642 }
643 } catch ( \Exception $e ) {
644 return [ $item['uid'] => $e->getMessage() ];
645 }
646
647 return [ $item['uid'] => null ];
648 } )
649 ->filter( fn( $value ) => null !== $value );
650
651 if ( ! $validation_errors->is_empty() ) {
652 return Error_Builder::make( 'settings_validation_failed' )
653 ->set_status( 422 )
654 ->set_message( 'Settings validation failed: ' . json_encode( $validation_errors->all() ) )
655 ->build();
656 }
657
658 return Response_Builder::make()
659 ->set_status( 200 )
660 ->build();
661 }
662
663 private function parse_settings( array $settings ): array {
664 $result = [];
665
666 if ( empty( $settings ) ) {
667 return $result;
668 }
669
670 if ( isset( $settings['overridable_props'] ) ) {
671 $parser = Component_Overridable_Props_Parser::make();
672 $overridable_props_result = $parser->parse( $settings['overridable_props'] );
673
674 if ( ! $overridable_props_result->is_valid() ) {
675 throw new \Exception(
676 esc_html( 'Validation failed for overridable_props: ' . $overridable_props_result->errors()->to_string() )
677 );
678 }
679
680 $result['overridable_props'] = $overridable_props_result->unwrap();
681 }
682
683 return $result;
684 }
685
686 private function route_wrapper( callable $cb ) {
687 try {
688 $response = $cb();
689 } catch ( \Exception $e ) {
690 return Error_Builder::make( 'unexpected_error' )
691 ->set_message( __( 'Something went wrong', 'elementor' ) )
692 ->build();
693 }
694
695 return $response;
696 }
697
698 private function get_insufficient_permissions_error( string $action ) {
699 return Error_Builder::make( 'insufficient_permissions' )
700 ->set_status( 403 )
701 ->set_message( __( 'You do not have permission to perform this action.', 'elementor' ) )
702 ->set_meta( [
703 'action' => $action,
704 'tier' => Components_Access_Controller::get_access_tier(),
705 ] )
706 ->build();
707 }
708 }
709