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

698 lines 20.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\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 $document = $this->get_repository()->get( $component_id );
324
325 if ( ! $document ) {
326 return Error_Builder::make( 'component_not_found' )
327 ->set_status( 404 )
328 ->set_message( __( 'Component not found', 'elementor' ) )
329 ->build();
330 }
331
332 $overridable = $document->get_json_meta( Component::OVERRIDABLE_PROPS_META_KEY ) ?? null;
333
334 if ( empty( $overridable ) ) {
335 $overridable = null;
336 }
337
338 return Response_Builder::make( $overridable )->build();
339 }
340
341 private function create_components( \WP_REST_Request $request ) {
342 if ( ! Components_Access_Controller::can_create() ) {
343 return $this->get_insufficient_permissions_error( 'create' );
344 }
345
346 $save_status = $request->get_param( 'status' );
347
348 $items = Collection::make( $request->get_param( 'items' ) );
349 $components = $this->get_repository()->all();
350
351 $result = Save_Components_Validator::make( $components )->validate( $items );
352
353 if ( ! $result['success'] ) {
354 return Error_Builder::make( 'components_validation_failed' )
355 ->set_status( 422 )
356 ->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
357 ->build();
358 }
359
360 $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
361
362 if ( ! $circular_result['success'] ) {
363 return Error_Builder::make( 'circular_dependency_detected' )
364 ->set_status( 422 )
365 ->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
366 ->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
367 ->build();
368 }
369
370 $non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );
371
372 if ( ! $non_atomic_result['success'] ) {
373 return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
374 ->set_status( 422 )
375 ->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
376 ->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
377 ->build();
378 }
379
380 $validation_errors = [];
381
382 $created = $items->map_with_keys( function ( $item ) use ( $save_status, &$validation_errors ) {
383 $title = sanitize_text_field( $item['title'] );
384 $content = $item['elements'];
385 $uid = $item['uid'];
386
387 try {
388 $settings = isset( $item['settings'] ) ? $this->parse_settings( $item['settings'] ) : [];
389
390 $status = Document::STATUS_AUTOSAVE === $save_status
391 ? Document::STATUS_DRAFT
392 : $save_status;
393
394 $component_id = $this->get_repository()->create( $title, $content, $status, $uid, $settings );
395
396 return [ $uid => $component_id ];
397 } catch ( \Exception $e ) {
398 $validation_errors[ $uid ] = $e->getMessage();
399 return [ $uid => null ];
400 }
401 } );
402
403 if ( ! empty( $validation_errors ) ) {
404 return Error_Builder::make( 'settings_validation_failed' )
405 ->set_status( 422 )
406 ->set_message( 'Settings validation failed: ' . json_encode( $validation_errors ) )
407 ->build();
408 }
409
410 return Response_Builder::make( $created->all() )
411 ->set_status( 201 )
412 ->build();
413 }
414
415 private function update_statuses( \WP_REST_Request $request ) {
416 if ( ! Components_Access_Controller::can_publish() ) {
417 return $this->get_insufficient_permissions_error( 'publish' );
418 }
419
420 $result = Collection::make( $request->get_param( 'ids' ) )
421 ->reduce(
422 function ( $result, int $component_id ) {
423 $component = $this->get_repository()->get( $component_id );
424
425 if ( ! $component ) {
426 $result['failed'][] = $component_id;
427 return $result;
428 }
429
430 $publish_result = $this->get_repository()->publish_component( $component );
431
432 $result[ $publish_result ? 'success' : 'failed' ][] = $component_id;
433
434 return $result;
435 },
436 [
437 'success' => [],
438 'failed' => [],
439 ]
440 );
441
442 return Response_Builder::make( $result )->build();
443 }
444
445 private function lock_component( \WP_REST_Request $request ) {
446 if ( ! Components_Access_Controller::can_lock() ) {
447 return $this->get_insufficient_permissions_error( 'lock' );
448 }
449
450 $component_id = $request->get_param( 'componentId' );
451 try {
452 $success = $this->get_component_lock_manager()->lock( $component_id );
453 } catch ( \Exception $e ) {
454 error_log( 'Components REST API lock_component error: ' . $e->getMessage() );
455 return Error_Builder::make( 'lock_failed' )
456 ->set_status( 500 )
457 ->set_message( __( 'Failed to lock component', 'elementor' ) )
458 ->build();
459 }
460
461 if ( ! $success ) {
462 return Error_Builder::make( 'lock_failed' )
463 ->set_status( 500 )
464 ->set_message( __( 'Failed to lock component', 'elementor' ) )
465 ->build();
466 }
467
468 return Response_Builder::make( [ 'locked' => $success ] )->build();
469 }
470
471 private function unlock_component( \WP_REST_Request $request ) {
472 if ( ! Components_Access_Controller::can_lock() ) {
473 return $this->get_insufficient_permissions_error( 'unlock' );
474 }
475
476 $component_id = $request->get_param( 'componentId' );
477 try {
478 $success = $this->get_component_lock_manager()->unlock( $component_id );
479 } catch ( \Exception $e ) {
480 error_log( 'Components REST API unlock_component error: ' . $e->getMessage() );
481 return Error_Builder::make( 'unlock_failed' )
482 ->set_status( 500 )
483 ->set_message( __( 'Failed to unlock component', 'elementor' ) )
484 ->build();
485 }
486
487 if ( ! $success ) {
488 return Error_Builder::make( 'unlock_failed' )
489 ->set_status( 500 )
490 ->set_message( __( 'Failed to unlock component', 'elementor' ) )
491 ->build();
492 }
493 return Response_Builder::make( [ 'unlocked' => $success ] )->build();
494 }
495
496 private function get_lock_status( \WP_REST_Request $request ) {
497 if ( ! Components_Access_Controller::can_lock() ) {
498 return $this->get_insufficient_permissions_error( 'lock_status' );
499 }
500
501 $component_id = (int) $request->get_param( 'componentId' );
502 try {
503 $lock_manager = $this->get_component_lock_manager();
504 if ( $lock_manager->is_lock_expired( $component_id ) ) {
505 $lock_manager->unlock( $component_id );
506 }
507
508 $lock_data = $lock_manager->get_lock_data( $component_id );
509 $current_user_id = get_current_user_id();
510
511 // if current user is the lock user, return true
512 if ( $lock_data['locked_by'] && $lock_data['locked_by'] === $current_user_id ) {
513 return Response_Builder::make( [
514 'is_current_user_allow_to_edit' => true,
515 'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
516 ] )->build();
517 }
518
519 // if the user is not the lock user, return false
520 if ( $lock_data['locked_by'] && $lock_data['locked_by'] !== $current_user_id ) {
521 return Response_Builder::make( [
522 'is_current_user_allow_to_edit' => false,
523 'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
524 ] )->build();
525 }
526
527 // if the component is not locked, return true
528 if ( ! $lock_data['locked_by'] ) {
529 return Response_Builder::make( [
530 'is_current_user_allow_to_edit' => true,
531 'locked_by' => null,
532 ] )->build();
533 }
534 } catch ( \Exception $e ) {
535 error_log( 'Components REST API get_lock_status error: ' . $e->getMessage() );
536 return Error_Builder::make( 'get_lock_status_failed' )
537 ->set_status( 500 )
538 ->set_message( __( 'Failed to get lock status', 'elementor' ) )
539 ->build();
540 }
541 }
542
543 private function archive_components( \WP_REST_Request $request ) {
544 if ( ! Components_Access_Controller::can_delete() ) {
545 return $this->get_insufficient_permissions_error( 'delete' );
546 }
547
548 $component_ids = $request->get_param( 'componentIds' );
549 $status = $request->get_param( 'status' );
550
551 try {
552 $result = $this->get_repository()->archive( $component_ids, $status );
553 } catch ( \Exception $e ) {
554 error_log( 'Components REST API archive_components error: ' . $e->getMessage() );
555 return Error_Builder::make( 'archive_failed' )
556 ->set_meta( [ 'error' => $e->getMessage() ] )
557 ->set_status( 500 )
558 ->set_message( __( 'Failed to archive components', 'elementor' ) )
559 ->build();
560 }
561 return Response_Builder::make( $result )->build();
562 }
563
564 private function update_components_title( \WP_REST_Request $request ) {
565 if ( ! Components_Access_Controller::can_rename() ) {
566 return $this->get_insufficient_permissions_error( 'rename' );
567 }
568
569 $failed_ids = [];
570 $success_ids = [];
571 $components = $request->get_param( 'components' );
572 $status = $request->get_param( 'status' );
573
574 foreach ( $components as $component ) {
575 $is_success = $this->get_repository()->update_title( $component['componentId'], $component['title'], $status );
576
577 if ( ! $is_success ) {
578 $failed_ids[] = $component['componentId'];
579 continue;
580 }
581 $success_ids[] = $component['componentId'];
582
583 }
584 return Response_Builder::make( [
585 'failedIds' => $failed_ids,
586 'successIds' => $success_ids,
587 ] )->build();
588 }
589
590 private function create_validate_components( \WP_REST_Request $request ) {
591 if ( ! Components_Access_Controller::can_create() ) {
592 return $this->get_insufficient_permissions_error( 'create' );
593 }
594
595 $items = Collection::make( $request->get_param( 'items' ) );
596 $components = $this->get_repository()->all();
597
598 $result = Save_Components_Validator::make( $components )->validate( $items );
599
600 if ( ! $result['success'] ) {
601 return Error_Builder::make( 'components_validation_failed' )
602 ->set_status( 422 )
603 ->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
604 ->build();
605 }
606
607 $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
608
609 if ( ! $circular_result['success'] ) {
610 return Error_Builder::make( 'circular_dependency_detected' )
611 ->set_status( 422 )
612 ->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
613 ->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
614 ->build();
615 }
616
617 $non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );
618
619 if ( ! $non_atomic_result['success'] ) {
620 return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
621 ->set_status( 422 )
622 ->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
623 ->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
624 ->build();
625 }
626
627 $validation_errors = $items->map_with_keys( function ( $item ) {
628 try {
629 if ( isset( $item['settings'] ) ) {
630 $this->parse_settings( $item['settings'] );
631 }
632 } catch ( \Exception $e ) {
633 return [ $item['uid'] => $e->getMessage() ];
634 }
635
636 return [ $item['uid'] => null ];
637 } )
638 ->filter( fn( $value ) => null !== $value );
639
640 if ( ! $validation_errors->is_empty() ) {
641 return Error_Builder::make( 'settings_validation_failed' )
642 ->set_status( 422 )
643 ->set_message( 'Settings validation failed: ' . json_encode( $validation_errors->all() ) )
644 ->build();
645 }
646
647 return Response_Builder::make()
648 ->set_status( 200 )
649 ->build();
650 }
651
652 private function parse_settings( array $settings ): array {
653 $result = [];
654
655 if ( empty( $settings ) ) {
656 return $result;
657 }
658
659 if ( isset( $settings['overridable_props'] ) ) {
660 $parser = Component_Overridable_Props_Parser::make();
661 $overridable_props_result = $parser->parse( $settings['overridable_props'] );
662
663 if ( ! $overridable_props_result->is_valid() ) {
664 throw new \Exception(
665 esc_html( 'Validation failed for overridable_props: ' . $overridable_props_result->errors()->to_string() )
666 );
667 }
668
669 $result['overridable_props'] = $overridable_props_result->unwrap();
670 }
671
672 return $result;
673 }
674
675 private function route_wrapper( callable $cb ) {
676 try {
677 $response = $cb();
678 } catch ( \Exception $e ) {
679 return Error_Builder::make( 'unexpected_error' )
680 ->set_message( __( 'Something went wrong', 'elementor' ) )
681 ->build();
682 }
683
684 return $response;
685 }
686
687 private function get_insufficient_permissions_error( string $action ) {
688 return Error_Builder::make( 'insufficient_permissions' )
689 ->set_status( 403 )
690 ->set_message( __( 'You do not have permission to perform this action.', 'elementor' ) )
691 ->set_meta( [
692 'action' => $action,
693 'tier' => Components_Access_Controller::get_access_tier(),
694 ] )
695 ->build();
696 }
697 }
698