PluginProbe
Elementor Website Builder – more than just a page builder / 3.32.2
Elementor Website Builder – more than just a page builder v3.32.2
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
← All changes | modules/components/components-rest-api.php +45 -599 4.1.43.32.2 View file →
@@ -1,14 +1,10 @@
1 1 <?php
2 2
3 3 namespace Elementor\Modules\Components;
4 4
5 -use Elementor\Core\Base\Document;
6 5 use Elementor\Core\Utils\Api\Error_Builder;
7 6 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 7
12 8 if ( ! defined( 'ABSPATH' ) ) {
13 9 exit; // Exit if accessed directly.
14 10 }
@@ -15,13 +11,13 @@
15 11
16 12 class Components_REST_API {
17 13 const API_NAMESPACE = 'elementor/v1';
18 14 const API_BASE = 'components';
19 - const LOCK_DOCUMENT_TYPE_NAME = 'components';
20 15 const STYLES_ROUTE = 'styles';
21 - const MAX_COMPONENTS = 100;
16 + const MAX_COMPONENTS = 50;
22 17
23 18 private $repository = null;
19 +
24 20 public function register_hooks() {
25 21 add_action( 'rest_api_init', fn() => $this->register_routes() );
26 22 }
27 23
@@ -32,21 +28,14 @@
32 28
33 29 return $this->repository;
34 30 }
35 31
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 32 private function register_routes() {
44 33 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
45 34 [
46 35 'methods' => 'GET',
47 36 'callback' => fn() => $this->route_wrapper( fn() => $this->get_components() ),
48 - 'permission_callback' => fn() => current_user_can( 'edit_posts' ),
37 + 'permission_callback' => fn() => is_user_logged_in(),
49 38 ],
50 39 ] );
51 40
52 41 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/' . self::STYLES_ROUTE, [
@@ -52,9 +41,9 @@
52 41 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/' . self::STYLES_ROUTE, [
53 42 [
54 43 'methods' => 'GET',
55 44 'callback' => fn() => $this->route_wrapper( fn() => $this->get_styles() ),
56 - 'permission_callback' => fn() => current_user_can( 'edit_posts' ),
45 + 'permission_callback' => fn() => is_user_logged_in(),
57 46 ],
58 47 ] );
59 48
60 49 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
@@ -59,246 +48,34 @@
59 48
60 49 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
61 50 [
62 51 'methods' => 'POST',
63 - 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_components( $request ) ),
52 + 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_component( $request ) ),
64 53 'permission_callback' => fn() => current_user_can( 'manage_options' ),
65 54 'args' => [
66 - 'status' => [
55 + 'name' => [
67 56 'type' => 'string',
68 - 'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
69 57 'required' => true,
70 58 ],
71 - 'items' => [
59 + 'content' => [
72 60 'type' => 'array',
73 61 'required' => true,
74 62 'items' => [
75 63 '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 64 ],
100 65 ],
101 66 ],
102 67 ],
103 68 ] );
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 69 }
289 70
290 71 private function get_components() {
291 72 $components = $this->get_repository()->all();
292 73
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() );
74 + $components_list = $components->get_components()->map( fn( $component ) => [
75 + 'id' => $component['id'],
76 + 'name' => $component['name'],
77 + ])->all();
301 78
302 79 return Response_Builder::make( $components_list )->build();
303 80 }
304 81
@@ -305,405 +82,74 @@
305 82 private function get_styles() {
306 83 $components = $this->get_repository()->all();
307 84
308 85 $styles = [];
309 - $components->each( function( $component ) use ( &$styles ) {
86 + $components->get_components()->each( function( $component ) use ( &$styles ) {
310 87 $styles[ $component['id'] ] = $component['styles'];
311 88 } );
312 89
313 90 return Response_Builder::make( $styles )->build();
314 91 }
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' ) );
92 + private function create_component( \WP_REST_Request $request ) {
361 93 $components = $this->get_repository()->all();
94 + $components_count = $components->get_components()->count();
362 95
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'] ) )
96 + if ( $components_count >= static::MAX_COMPONENTS ) {
97 + return Error_Builder::make( 'components_limit_exceeded' )
98 + ->set_status( 400 )
99 + ->set_message( sprintf(
100 + /* translators: %d: maximum components limit. */
101 + __( 'Components limit exceeded. Maximum allowed: %d', 'elementor' ),
102 + static::MAX_COMPONENTS
103 + ) )
369 104 ->build();
370 105 }
371 106
372 - $circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );
107 + $parser = Components_Parser::make();
373 108
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 - }
109 + $name_result = $parser->parse_name( $request->get_param( 'name' ), $components->get_components()->map( fn( $component ) => $component['name'] )->all() );
381 110
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'] ] )
111 + if ( ! $name_result->is_valid() ) {
112 + return Error_Builder::make( 'invalid_name' )
113 + ->set_status( 400 )
114 + ->set_message( 'Invalid component name: ' . $name_result->errors()->to_string() )
389 115 ->build();
390 116 }
391 117
392 - $validation_errors = [];
118 + $name = $name_result->unwrap();
119 + // The content is validated & sanitized in the document save process.
120 + $content = $request->get_param( 'content' );
393 121
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 122 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 - }
123 + $component_id = $this->get_repository()->create( $name, $content );
472 124
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 );
125 + return Response_Builder::make( [ 'component_id' => $component_id ] )->set_status( 201 )->build();
491 126 } 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 - }
127 + $error_message = $e->getMessage();
498 128
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 - }
129 + $invalid_elements_structure_error = str_contains( $error_message, 'Invalid data' );
130 + $atomic_styles_validation_error = str_contains( $error_message, 'Styles validation failed' );
131 + $atomic_settings_validation_error = str_contains( $error_message, 'Settings validation failed' );
507 132
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 );
133 + if ( $invalid_elements_structure_error || $atomic_styles_validation_error || $atomic_settings_validation_error ) {
134 + return Error_Builder::make( 'content_validation_failed' )
135 + ->set_status( 400 )
136 + ->set_message( $error_message )
137 + ->build();
518 138 }
519 139
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();
140 + throw $e;
552 141 }
553 142 }
554 143
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 144 private function route_wrapper( callable $cb ) {
688 145 try {
689 146 $response = $cb();
690 147 } catch ( \Exception $e ) {
691 148 return Error_Builder::make( 'unexpected_error' )
692 - ->set_message( __( 'Something went wrong', 'elementor' ) )
693 - ->build();
149 + ->set_message( __( 'Something went wrong', 'elementor' ) )
150 + ->build();
694 151 }
695 152
696 153 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 154 }
709 155 }