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

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