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

710 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 'componentIds' => [
152 'type' => 'array',
153 'items' => [
154 'type' => 'integer',
155 ],
156 'required' => true,
157 'description' => 'The component IDs to get overridable props for',
158 ],
159 ],
160 ],
161 ] );
162
163 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/status', [
164 [
165 'methods' => 'PUT',
166 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->update_statuses( $request ) ),
167 'permission_callback' => fn() => current_user_can( 'manage_options' ),
168 'args' => [
169 'status' => [
170 'type' => 'string',
171 'required' => true,
172 'enum' => [ Document::STATUS_PUBLISH ],
173 ],
174 'ids' => [
175 'type' => 'array',
176 'required' => true,
177 'items' => [
178 'type' => 'number',
179 'required' => true,
180 ],
181 ],
182 ],
183 ],
184 ] );
185
186 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/lock', [
187 [
188 'methods' => 'POST',
189 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->lock_component( $request ) ),
190 'permission_callback' => fn() => current_user_can( 'manage_options' ),
191 'args' => [
192 'componentId' => [
193 'type' => 'number',
194 'required' => true,
195 'description' => 'The component ID to unlock',
196 ],
197 ],
198 ],
199 ] );
200
201 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/unlock', [
202 [
203 'methods' => 'POST',
204 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->unlock_component( $request ) ),
205 'permission_callback' => fn() => current_user_can( 'manage_options' ),
206 'args' => [
207 'componentId' => [
208 'type' => 'number',
209 'required' => true,
210 'description' => 'The component ID to unlock',
211 ],
212 ],
213 ],
214 ] );
215
216 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/lock-status', [
217 [
218 'methods' => 'GET',
219 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->get_lock_status( $request ) ),
220 'permission_callback' => fn() => current_user_can( 'manage_options' ),
221 'args' => [
222 'componentId' => [
223 'type' => 'string',
224 'required' => true,
225 'description' => 'The component ID to check lock status',
226 ],
227 ],
228 ],
229 ] );
230
231 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/archive', [
232 [
233 'methods' => 'POST',
234 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->archive_components( $request ) ),
235 'permission_callback' => fn() => current_user_can( 'manage_options' ),
236 'args' => [
237 'componentIds' => [
238 'type' => 'array',
239 'items' => [
240 'type' => 'number',
241 'required' => true,
242 ],
243 'required' => true,
244 'description' => 'The component IDs to archive',
245 ],
246 'status' => [
247 'type' => 'string',
248 'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
249 'required' => true,
250 ],
251 ],
252 ],
253 ] );
254
255 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/update-titles', [
256 [
257 'methods' => 'POST',
258 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->update_components_title( $request ) ),
259 'permission_callback' => fn() => current_user_can( 'manage_options' ),
260 'args' => [
261 'components' => [
262 'type' => 'array',
263 'required' => true,
264 'items' => [
265 'type' => 'object',
266 'properties' => [
267 'componentId' => [
268 'type' => 'number',
269 'required' => true,
270 'description' => 'The component ID to update title',
271 ],
272 'title' => [
273 'type' => 'string',
274 'required' => true,
275 'description' => 'The new title for the component',
276 ],
277 ],
278 ],
279 ],
280 'status' => [
281 'type' => 'string',
282 'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
283 'required' => true,
284 ],
285 ],
286 ],
287 ] );
288 }
289
290 private function get_components() {
291 $components = $this->get_repository()->all();
292
293 $components_list = array_values( $components
294 ->map( fn( $component ) => [
295 'id' => $component['id'],
296 'name' => $component['title'],
297 'uid' => $component['uid'],
298 'isArchived' => $component['is_archived'] ?? false,
299 ] )
300 ->all() );
301
302 return Response_Builder::make( $components_list )->build();
303 }
304
305 private function get_styles() {
306 $components = $this->get_repository()->all();
307
308 $styles = [];
309 $components->each( function( $component ) use ( &$styles ) {
310 $styles[ $component['id'] ] = $component['styles'];
311 } );
312
313 return Response_Builder::make( $styles )->build();
314 }
315
316 private function get_overridable_props( \WP_REST_Request $request ) {
317 $component_ids = $request->get_param( 'componentIds' );
318
319 $data = [];
320 $errors = [];
321
322 foreach ( $component_ids as $component_id ) {
323 $component_id = (int) $component_id;
324
325 /** @var Component $document */
326 $document = $this->get_repository()->get( $component_id );
327
328 if ( ! $document ) {
329 $errors[ $component_id ] = 'component_not_found';
330 continue;
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 );
344
345 $data[ $component_id ] = empty( $overridable ) ? null : $overridable;
346 }
347
348 return Response_Builder::make( $data )
349 ->set_meta( [ 'errors' => $errors ] )
350 ->build();
351 }
352
353 private function create_components( \WP_REST_Request $request ) {
354 if ( ! Components_Access_Controller::can_create() ) {
355 return $this->get_insufficient_permissions_error( 'create' );
356 }
357
358 $save_status = $request->get_param( 'status' );
359
360 $items = Collection::make( $request->get_param( 'items' ) );
361 $components = $this->get_repository()->all();
362
363 $result = Save_Components_Validator::make( $components )->validate( $items );
364
365 if ( ! $result['success'] ) {
366 return Error_Builder::make( 'components_validation_failed' )
367 ->set_status( 422 )
368 ->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
369 ->build();
370 }
371
372 $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
373
374 if ( ! $circular_result['success'] ) {
375 return Error_Builder::make( 'circular_dependency_detected' )
376 ->set_status( 422 )
377 ->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
378 ->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
379 ->build();
380 }
381
382 $non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );
383
384 if ( ! $non_atomic_result['success'] ) {
385 return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
386 ->set_status( 422 )
387 ->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
388 ->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
389 ->build();
390 }
391
392 $validation_errors = [];
393
394 $created = $items->map_with_keys( function ( $item ) use ( $save_status, &$validation_errors ) {
395 $title = sanitize_text_field( $item['title'] );
396 $content = $item['elements'];
397 $uid = $item['uid'];
398
399 try {
400 $settings = isset( $item['settings'] ) ? $this->parse_settings( $item['settings'] ) : [];
401
402 $status = Document::STATUS_AUTOSAVE === $save_status
403 ? Document::STATUS_DRAFT
404 : $save_status;
405
406 $component_id = $this->get_repository()->create( $title, $content, $status, $uid, $settings );
407
408 return [ $uid => $component_id ];
409 } catch ( \Exception $e ) {
410 $validation_errors[ $uid ] = $e->getMessage();
411 return [ $uid => null ];
412 }
413 } );
414
415 if ( ! empty( $validation_errors ) ) {
416 return Error_Builder::make( 'settings_validation_failed' )
417 ->set_status( 422 )
418 ->set_message( 'Settings validation failed: ' . json_encode( $validation_errors ) )
419 ->build();
420 }
421
422 return Response_Builder::make( $created->all() )
423 ->set_status( 201 )
424 ->build();
425 }
426
427 private function update_statuses( \WP_REST_Request $request ) {
428 if ( ! Components_Access_Controller::can_publish() ) {
429 return $this->get_insufficient_permissions_error( 'publish' );
430 }
431
432 $result = Collection::make( $request->get_param( 'ids' ) )
433 ->reduce(
434 function ( $result, int $component_id ) {
435 $component = $this->get_repository()->get( $component_id );
436
437 if ( ! $component ) {
438 $result['failed'][] = $component_id;
439 return $result;
440 }
441
442 $publish_result = $this->get_repository()->publish_component( $component );
443
444 $result[ $publish_result ? 'success' : 'failed' ][] = $component_id;
445
446 return $result;
447 },
448 [
449 'success' => [],
450 'failed' => [],
451 ]
452 );
453
454 return Response_Builder::make( $result )->build();
455 }
456
457 private function lock_component( \WP_REST_Request $request ) {
458 if ( ! Components_Access_Controller::can_lock() ) {
459 return $this->get_insufficient_permissions_error( 'lock' );
460 }
461
462 $component_id = $request->get_param( 'componentId' );
463 try {
464 $success = $this->get_component_lock_manager()->lock( $component_id );
465 } catch ( \Exception $e ) {
466 error_log( 'Components REST API lock_component error: ' . $e->getMessage() );
467 return Error_Builder::make( 'lock_failed' )
468 ->set_status( 500 )
469 ->set_message( __( 'Failed to lock component', 'elementor' ) )
470 ->build();
471 }
472
473 if ( ! $success ) {
474 return Error_Builder::make( 'lock_failed' )
475 ->set_status( 500 )
476 ->set_message( __( 'Failed to lock component', 'elementor' ) )
477 ->build();
478 }
479
480 return Response_Builder::make( [ 'locked' => $success ] )->build();
481 }
482
483 private function unlock_component( \WP_REST_Request $request ) {
484 if ( ! Components_Access_Controller::can_lock() ) {
485 return $this->get_insufficient_permissions_error( 'unlock' );
486 }
487
488 $component_id = $request->get_param( 'componentId' );
489 try {
490 $success = $this->get_component_lock_manager()->unlock( $component_id );
491 } catch ( \Exception $e ) {
492 error_log( 'Components REST API unlock_component error: ' . $e->getMessage() );
493 return Error_Builder::make( 'unlock_failed' )
494 ->set_status( 500 )
495 ->set_message( __( 'Failed to unlock component', 'elementor' ) )
496 ->build();
497 }
498
499 if ( ! $success ) {
500 return Error_Builder::make( 'unlock_failed' )
501 ->set_status( 500 )
502 ->set_message( __( 'Failed to unlock component', 'elementor' ) )
503 ->build();
504 }
505 return Response_Builder::make( [ 'unlocked' => $success ] )->build();
506 }
507
508 private function get_lock_status( \WP_REST_Request $request ) {
509 if ( ! Components_Access_Controller::can_lock() ) {
510 return $this->get_insufficient_permissions_error( 'lock_status' );
511 }
512
513 $component_id = (int) $request->get_param( 'componentId' );
514 try {
515 $lock_manager = $this->get_component_lock_manager();
516 if ( $lock_manager->is_lock_expired( $component_id ) ) {
517 $lock_manager->unlock( $component_id );
518 }
519
520 $lock_data = $lock_manager->get_lock_data( $component_id );
521 $current_user_id = get_current_user_id();
522
523 // if current user is the lock user, return true
524 if ( $lock_data['locked_by'] && $lock_data['locked_by'] === $current_user_id ) {
525 return Response_Builder::make( [
526 'is_current_user_allow_to_edit' => true,
527 'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
528 ] )->build();
529 }
530
531 // if the user is not the lock user, return false
532 if ( $lock_data['locked_by'] && $lock_data['locked_by'] !== $current_user_id ) {
533 return Response_Builder::make( [
534 'is_current_user_allow_to_edit' => false,
535 'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
536 ] )->build();
537 }
538
539 // if the component is not locked, return true
540 if ( ! $lock_data['locked_by'] ) {
541 return Response_Builder::make( [
542 'is_current_user_allow_to_edit' => true,
543 'locked_by' => null,
544 ] )->build();
545 }
546 } catch ( \Exception $e ) {
547 error_log( 'Components REST API get_lock_status error: ' . $e->getMessage() );
548 return Error_Builder::make( 'get_lock_status_failed' )
549 ->set_status( 500 )
550 ->set_message( __( 'Failed to get lock status', 'elementor' ) )
551 ->build();
552 }
553 }
554
555 private function archive_components( \WP_REST_Request $request ) {
556 if ( ! Components_Access_Controller::can_delete() ) {
557 return $this->get_insufficient_permissions_error( 'delete' );
558 }
559
560 $component_ids = $request->get_param( 'componentIds' );
561 $status = $request->get_param( 'status' );
562
563 try {
564 $result = $this->get_repository()->archive( $component_ids, $status );
565 } catch ( \Exception $e ) {
566 error_log( 'Components REST API archive_components error: ' . $e->getMessage() );
567 return Error_Builder::make( 'archive_failed' )
568 ->set_meta( [ 'error' => $e->getMessage() ] )
569 ->set_status( 500 )
570 ->set_message( __( 'Failed to archive components', 'elementor' ) )
571 ->build();
572 }
573 return Response_Builder::make( $result )->build();
574 }
575
576 private function update_components_title( \WP_REST_Request $request ) {
577 if ( ! Components_Access_Controller::can_rename() ) {
578 return $this->get_insufficient_permissions_error( 'rename' );
579 }
580
581 $failed_ids = [];
582 $success_ids = [];
583 $components = $request->get_param( 'components' );
584 $status = $request->get_param( 'status' );
585
586 foreach ( $components as $component ) {
587 $is_success = $this->get_repository()->update_title( $component['componentId'], $component['title'], $status );
588
589 if ( ! $is_success ) {
590 $failed_ids[] = $component['componentId'];
591 continue;
592 }
593 $success_ids[] = $component['componentId'];
594
595 }
596 return Response_Builder::make( [
597 'failedIds' => $failed_ids,
598 'successIds' => $success_ids,
599 ] )->build();
600 }
601
602 private function create_validate_components( \WP_REST_Request $request ) {
603 if ( ! Components_Access_Controller::can_create() ) {
604 return $this->get_insufficient_permissions_error( 'create' );
605 }
606
607 $items = Collection::make( $request->get_param( 'items' ) );
608 $components = $this->get_repository()->all();
609
610 $result = Save_Components_Validator::make( $components )->validate( $items );
611
612 if ( ! $result['success'] ) {
613 return Error_Builder::make( 'components_validation_failed' )
614 ->set_status( 422 )
615 ->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
616 ->build();
617 }
618
619 $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
620
621 if ( ! $circular_result['success'] ) {
622 return Error_Builder::make( 'circular_dependency_detected' )
623 ->set_status( 422 )
624 ->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
625 ->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
626 ->build();
627 }
628
629 $non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );
630
631 if ( ! $non_atomic_result['success'] ) {
632 return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
633 ->set_status( 422 )
634 ->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
635 ->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
636 ->build();
637 }
638
639 $validation_errors = $items->map_with_keys( function ( $item ) {
640 try {
641 if ( isset( $item['settings'] ) ) {
642 $this->parse_settings( $item['settings'] );
643 }
644 } catch ( \Exception $e ) {
645 return [ $item['uid'] => $e->getMessage() ];
646 }
647
648 return [ $item['uid'] => null ];
649 } )
650 ->filter( fn( $value ) => null !== $value );
651
652 if ( ! $validation_errors->is_empty() ) {
653 return Error_Builder::make( 'settings_validation_failed' )
654 ->set_status( 422 )
655 ->set_message( 'Settings validation failed: ' . json_encode( $validation_errors->all() ) )
656 ->build();
657 }
658
659 return Response_Builder::make()
660 ->set_status( 200 )
661 ->build();
662 }
663
664 private function parse_settings( array $settings ): array {
665 $result = [];
666
667 if ( empty( $settings ) ) {
668 return $result;
669 }
670
671 if ( isset( $settings['overridable_props'] ) ) {
672 $parser = Component_Overridable_Props_Parser::make();
673 $overridable_props_result = $parser->parse( $settings['overridable_props'] );
674
675 if ( ! $overridable_props_result->is_valid() ) {
676 throw new \Exception(
677 esc_html( 'Validation failed for overridable_props: ' . $overridable_props_result->errors()->to_string() )
678 );
679 }
680
681 $result['overridable_props'] = $overridable_props_result->unwrap();
682 }
683
684 return $result;
685 }
686
687 private function route_wrapper( callable $cb ) {
688 try {
689 $response = $cb();
690 } catch ( \Exception $e ) {
691 return Error_Builder::make( 'unexpected_error' )
692 ->set_message( __( 'Something went wrong', 'elementor' ) )
693 ->build();
694 }
695
696 return $response;
697 }
698
699 private function get_insufficient_permissions_error( string $action ) {
700 return Error_Builder::make( 'insufficient_permissions' )
701 ->set_status( 403 )
702 ->set_message( __( 'You do not have permission to perform this action.', 'elementor' ) )
703 ->set_meta( [
704 'action' => $action,
705 'tier' => Components_Access_Controller::get_access_tier(),
706 ] )
707 ->build();
708 }
709 }
710