PluginProbe
Elementor Website Builder – more than just a page builder / 3.33.0-dev1
Elementor Website Builder – more than just a page builder v3.33.0-dev1
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 / variables / classes / rest-api.php

rest-api.php in Elementor Website Builder – more than just a page builder 3.33.0-dev1, at modules/variables/classes/rest-api.php

584 lines 16.1 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\Variables\Classes;
4
5 use Exception;
6 use WP_Error;
7 use WP_REST_Response;
8 use WP_REST_Request;
9 use WP_REST_Server;
10 use Elementor\Plugin;
11 use Elementor\Modules\Variables\Module as Variables_Module;
12 use Elementor\Modules\Variables\Storage\Repository as Variables_Repository;
13 use Elementor\Modules\Variables\Storage\Exceptions\VariablesLimitReached;
14 use Elementor\Modules\Variables\Storage\Exceptions\RecordNotFound;
15 use Elementor\Modules\Variables\Storage\Exceptions\DuplicatedLabel;
16 use Elementor\Modules\Variables\Storage\Exceptions\BatchOperationFailed;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 class Rest_Api {
23 const API_NAMESPACE = 'elementor/v1';
24 const API_BASE = 'variables';
25 const HTTP_OK = 200;
26 const HTTP_CREATED = 201;
27 const HTTP_BAD_REQUEST = 400;
28 const HTTP_NOT_FOUND = 404;
29 const HTTP_SERVER_ERROR = 500;
30 const MAX_ID_LENGTH = 64;
31 const MAX_LABEL_LENGTH = 50;
32 const MAX_VALUE_LENGTH = 512;
33 private Variables_Repository $variables_repository;
34
35 public function __construct( Variables_Repository $variables_repository ) {
36 $this->variables_repository = $variables_repository;
37 }
38
39 public function enough_permissions_to_perform_ro_action() {
40 return current_user_can( 'edit_posts' );
41 }
42
43 public function enough_permissions_to_perform_rw_action() {
44 return current_user_can( 'manage_options' );
45 }
46
47 public function register_routes() {
48 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/list', [
49 'methods' => WP_REST_Server::READABLE,
50 'callback' => [ $this, 'get_variables' ],
51 'permission_callback' => [ $this, 'enough_permissions_to_perform_ro_action' ],
52 ] );
53
54 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/create', [
55 'methods' => WP_REST_Server::CREATABLE,
56 'callback' => [ $this, 'create_variable' ],
57 'permission_callback' => [ $this, 'enough_permissions_to_perform_rw_action' ],
58 'args' => [
59 'type' => [
60 'required' => true,
61 'type' => 'string',
62 'validate_callback' => [ $this, 'is_valid_variable_type' ],
63 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
64 ],
65 'label' => [
66 'required' => true,
67 'type' => 'string',
68 'validate_callback' => [ $this, 'is_valid_variable_label' ],
69 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
70 ],
71 'value' => [
72 'required' => true,
73 'type' => 'string',
74 'validate_callback' => [ $this, 'is_valid_variable_value' ],
75 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
76 ],
77 ],
78 ] );
79
80 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/update', [
81 'methods' => WP_REST_Server::EDITABLE,
82 'callback' => [ $this, 'update_variable' ],
83 'permission_callback' => [ $this, 'enough_permissions_to_perform_rw_action' ],
84 'args' => [
85 'id' => [
86 'required' => true,
87 'type' => 'string',
88 'validate_callback' => [ $this, 'is_valid_variable_id' ],
89 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
90 ],
91 'label' => [
92 'required' => true,
93 'type' => 'string',
94 'validate_callback' => [ $this, 'is_valid_variable_label' ],
95 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
96 ],
97 'value' => [
98 'required' => true,
99 'type' => 'string',
100 'validate_callback' => [ $this, 'is_valid_variable_value' ],
101 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
102 ],
103 'order' => [
104 'required' => false,
105 'type' => 'integer',
106 'validate_callback' => [ $this, 'is_valid_order' ],
107 ],
108 ],
109 ] );
110
111 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/delete', [
112 'methods' => WP_REST_Server::EDITABLE,
113 'callback' => [ $this, 'delete_variable' ],
114 'permission_callback' => [ $this, 'enough_permissions_to_perform_rw_action' ],
115 'args' => [
116 'id' => [
117 'required' => true,
118 'type' => 'string',
119 'validate_callback' => [ $this, 'is_valid_variable_id' ],
120 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
121 ],
122 ],
123 ] );
124
125 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/restore', [
126 'methods' => WP_REST_Server::EDITABLE,
127 'callback' => [ $this, 'restore_variable' ],
128 'permission_callback' => [ $this, 'enough_permissions_to_perform_rw_action' ],
129 'args' => [
130 'id' => [
131 'required' => true,
132 'type' => 'string',
133 'validate_callback' => [ $this, 'is_valid_variable_id' ],
134 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
135 ],
136 'label' => [
137 'required' => false,
138 'type' => 'string',
139 'validate_callback' => [ $this, 'is_valid_variable_label' ],
140 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
141 ],
142 'value' => [
143 'required' => false,
144 'type' => 'string',
145 'validate_callback' => [ $this, 'is_valid_variable_value' ],
146 'sanitize_callback' => [ $this, 'trim_and_sanitize_text_field' ],
147 ],
148 ],
149 ] );
150
151 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/batch', [
152 'methods' => WP_REST_Server::CREATABLE,
153 'callback' => [ $this, 'process_batch' ],
154 'permission_callback' => [ $this, 'enough_permissions_to_perform_rw_action' ],
155 'args' => [
156 'watermark' => [
157 'required' => true,
158 'type' => 'integer',
159 'validate_callback' => [ $this, 'is_valid_watermark' ],
160 ],
161 'operations' => [
162 'required' => true,
163 'type' => 'array',
164 'validate_callback' => [ $this, 'is_valid_operations_array' ],
165 ],
166 ],
167 ] );
168 }
169
170 public function trim_and_sanitize_text_field( $value ) {
171 return trim( sanitize_text_field( $value ) );
172 }
173
174 public function is_valid_variable_id( $id ) {
175 $id = trim( $id );
176
177 if ( empty( $id ) ) {
178 return new WP_Error(
179 'invalid_variable_id_empty',
180 __( 'ID cannot be empty', 'elementor' )
181 );
182 }
183
184 if ( self::MAX_ID_LENGTH < strlen( $id ) ) {
185 return new WP_Error( 'invalid_variable_id_length', sprintf(
186 /* translators: %d: Maximum ID length. */
187 __( 'ID cannot exceed %d characters', 'elementor' ),
188 self::MAX_ID_LENGTH
189 ) );
190 }
191
192 return true;
193 }
194
195 public function is_valid_variable_type( $type ) {
196 $allowed_types = array_keys( Variables_Module::instance()->get_variable_types_registry()->all() );
197
198 return in_array( $type, $allowed_types, true );
199 }
200
201 public function is_valid_variable_label( $label ) {
202 $label = trim( $label );
203
204 if ( empty( $label ) ) {
205 return new WP_Error(
206 'invalid_variable_label_empty',
207 __( 'Label cannot be empty', 'elementor' )
208 );
209 }
210
211 if ( self::MAX_LABEL_LENGTH < strlen( $label ) ) {
212 return new WP_Error( 'invalid_variable_label_length', sprintf(
213 /* translators: %d: Maximum label length. */
214 __( 'Label cannot exceed %d characters', 'elementor' ),
215 self::MAX_LABEL_LENGTH
216 ) );
217 }
218
219 return true;
220 }
221
222 public function is_valid_order( $order ) {
223 if ( ! is_numeric( $order ) || $order < 0 ) {
224 return new WP_Error(
225 'invalid_order',
226 __( 'Order must be a non-negative integer', 'elementor' )
227 );
228 }
229
230 return true;
231 }
232
233 public function is_valid_variable_value( $value ) {
234 $value = trim( $value );
235
236 if ( empty( $value ) ) {
237 return new WP_Error(
238 'invalid_variable_value_empty',
239 __( 'Value cannot be empty', 'elementor' )
240 );
241 }
242
243 if ( self::MAX_VALUE_LENGTH < strlen( $value ) ) {
244 return new WP_Error( 'invalid_variable_value_length', sprintf(
245 /* translators: %d: Maximum value length. */
246 __( 'Value cannot exceed %d characters', 'elementor' ),
247 self::MAX_VALUE_LENGTH
248 ) );
249 }
250
251 return true;
252 }
253
254 public function create_variable( WP_REST_Request $request ) {
255 try {
256 return $this->create_new_variable( $request );
257 } catch ( Exception $e ) {
258 return $this->error_response( $e );
259 }
260 }
261
262 protected function clear_cache() {
263 Plugin::$instance->files_manager->clear_cache();
264 }
265
266 private function create_new_variable( WP_REST_Request $request ) {
267 $type = $request->get_param( 'type' );
268 $label = $request->get_param( 'label' );
269 $value = $request->get_param( 'value' );
270
271 $result = $this->variables_repository->create( [
272 'type' => $type,
273 'label' => $label,
274 'value' => $value,
275 ] );
276
277 $this->clear_cache();
278
279 return $this->success_response( [
280 'variable' => $result['variable'],
281 'watermark' => $result['watermark'],
282 ], self::HTTP_CREATED );
283 }
284
285 public function update_variable( WP_REST_Request $request ) {
286 try {
287 return $this->update_existing_variable( $request );
288 } catch ( Exception $e ) {
289 return $this->error_response( $e );
290 }
291 }
292
293 private function update_existing_variable( WP_REST_Request $request ) {
294 $id = $request->get_param( 'id' );
295 $label = $request->get_param( 'label' );
296 $value = $request->get_param( 'value' );
297 $order = $request->get_param( 'order' );
298
299 $update_data = [
300 'label' => $label,
301 'value' => $value,
302 ];
303
304 if ( null !== $order ) {
305 $update_data['order'] = $order;
306 }
307
308 $result = $this->variables_repository->update( $id, $update_data );
309
310 $this->clear_cache();
311
312 return $this->success_response( [
313 'variable' => $result['variable'],
314 'watermark' => $result['watermark'],
315 ] );
316 }
317
318 public function delete_variable( WP_REST_Request $request ) {
319 try {
320 return $this->delete_existing_variable( $request );
321 } catch ( Exception $e ) {
322 return $this->error_response( $e );
323 }
324 }
325
326 private function delete_existing_variable( WP_REST_Request $request ) {
327 $id = $request->get_param( 'id' );
328
329 $result = $this->variables_repository->delete( $id );
330
331 $this->clear_cache();
332
333 return $this->success_response( [
334 'variable' => $result['variable'],
335 'watermark' => $result['watermark'],
336 ] );
337 }
338
339 public function restore_variable( WP_REST_Request $request ) {
340 try {
341 return $this->restore_existing_variable( $request );
342 } catch ( Exception $e ) {
343 return $this->error_response( $e );
344 }
345 }
346
347 private function restore_existing_variable( WP_REST_Request $request ) {
348 $id = $request->get_param( 'id' );
349
350 $overrides = [];
351
352 $label = $request->get_param( 'label' );
353
354 if ( $label ) {
355 $overrides['label'] = $label;
356 }
357
358 $value = $request->get_param( 'value' );
359
360 if ( $value ) {
361 $overrides['value'] = $value;
362 }
363
364 $result = $this->variables_repository->restore( $id, $overrides );
365
366 $this->clear_cache();
367
368 return $this->success_response( [
369 'variable' => $result['variable'],
370 'watermark' => $result['watermark'],
371 ] );
372 }
373
374 public function get_variables() {
375 try {
376 return $this->list_of_variables();
377 } catch ( Exception $e ) {
378 return $this->error_response( $e );
379 }
380 }
381
382 private function list_of_variables() {
383 $db_record = $this->variables_repository->load();
384
385 return $this->success_response( [
386 'variables' => $db_record['data'] ?? [],
387 'total' => count( $db_record['data'] ),
388 'watermark' => $db_record['watermark'],
389 ] );
390 }
391
392 private function success_response( $payload, $status_code = null ) {
393 return new WP_REST_Response( [
394 'success' => true,
395 'data' => $payload,
396 ], $status_code ?? self::HTTP_OK );
397 }
398
399 private function error_response( Exception $e ) {
400 if ( $e instanceof VariablesLimitReached ) {
401 return $this->prepare_error_response(
402 self::HTTP_BAD_REQUEST,
403 'invalid_variable_limit_reached',
404 __( 'Reached the maximum number of variables', 'elementor' )
405 );
406 }
407
408 if ( $e instanceof DuplicatedLabel ) {
409 return $this->prepare_error_response(
410 self::HTTP_BAD_REQUEST,
411 'duplicated_label',
412 __( 'Variable label already exists', 'elementor' )
413 );
414 }
415
416 if ( $e instanceof RecordNotFound ) {
417 return $this->prepare_error_response(
418 self::HTTP_NOT_FOUND,
419 'variable_not_found',
420 __( 'Variable not found', 'elementor' )
421 );
422 }
423
424 return $this->prepare_error_response(
425 self::HTTP_SERVER_ERROR,
426 'unexpected_server_error',
427 __( 'Unexpected server error', 'elementor' )
428 );
429 }
430
431 private function prepare_error_response( $status_code, $error, $message ) {
432 return new WP_REST_Response( [
433 'code' => $error,
434 'message' => $message,
435 'data' => [
436 'status' => $status_code,
437 ],
438 ], $status_code );
439 }
440
441 public function is_valid_watermark( $watermark ) {
442 if ( ! is_numeric( $watermark ) || $watermark < 0 ) {
443 return new WP_Error(
444 'invalid_watermark',
445 __( 'Watermark must be a non-negative integer', 'elementor' )
446 );
447 }
448
449 return true;
450 }
451
452
453 public function is_valid_operations_array( $operations ) {
454 if ( ! is_array( $operations ) || empty( $operations ) ) {
455 return new WP_Error(
456 'invalid_operations_empty',
457 __( 'Operations array cannot be empty', 'elementor' )
458 );
459 }
460
461 foreach ( $operations as $index => $operation ) {
462 if ( ! is_array( $operation ) || ! isset( $operation['type'] ) ) {
463 return new WP_Error(
464 'invalid_operation_structure',
465 sprintf(
466 /* translators: %d: operation index */
467 __( 'Invalid operation structure at index %d', 'elementor' ),
468 $index
469 )
470 );
471 }
472
473 $allowed_types = [ 'create', 'update', 'delete', 'restore', 'reorder' ];
474
475 if ( ! in_array( $operation['type'], $allowed_types, true ) ) {
476 return new WP_Error(
477 'invalid_operation_type',
478 sprintf(
479 /* translators: %d: operation index */
480 __( 'Invalid operation type at index %d', 'elementor' ),
481 $index
482 )
483 );
484 }
485 }
486
487 return true;
488 }
489
490 public function process_batch( WP_REST_Request $request ) {
491 try {
492 return $this->process_batch_operations( $request );
493 } catch ( Exception $e ) {
494 return $this->batch_error_response( $e );
495 }
496 }
497
498 private function process_batch_operations( WP_REST_Request $request ) {
499 $watermark = $request->get_param( 'watermark' );
500 $operations = $request->get_param( 'operations' );
501
502 $result = $this->variables_repository->process_atomic_batch( $operations, $watermark );
503
504 $this->clear_cache();
505
506 return $this->success_response( $result );
507 }
508
509
510 private function batch_error_response( Exception $e ) {
511 if ( $e instanceof BatchOperationFailed ) {
512 $error_details = $e->getErrorDetails();
513 $batch_error_context = $this->determine_batch_error_context( $error_details );
514
515 return new WP_REST_Response( [
516 'success' => false,
517 'code' => $batch_error_context['code'],
518 'message' => $batch_error_context['message'],
519 'data' => $batch_error_context['filtered_errors'],
520 ], self::HTTP_BAD_REQUEST );
521 }
522
523 return $this->error_response( $e );
524 }
525
526 private function determine_batch_error_context( array $error_details ) {
527 $error_config = [
528 'invalid_variable_limit_reached' => [
529 'batch_code' => 'batch_variables_limit_reached',
530 'batch_message' => __( 'Batch operation failed: Reached the maximum number of variables', 'elementor' ),
531 'status' => self::HTTP_BAD_REQUEST,
532 'message' => __( 'Reached the maximum number of variables', 'elementor' ),
533 ],
534 'duplicated_label' => [
535 'batch_code' => 'batch_duplicated_label',
536 'batch_message' => __( 'Batch operation failed: Variable labels already exist', 'elementor' ),
537 'status' => self::HTTP_BAD_REQUEST,
538 'message' => __( 'Variable label already exists', 'elementor' ),
539 ],
540 'variable_not_found' => [
541 'batch_code' => 'batch_variables_not_found',
542 'batch_message' => __( 'Batch operation failed: Variables not found', 'elementor' ),
543 'status' => self::HTTP_NOT_FOUND,
544 'message' => __( 'Variable not found', 'elementor' ),
545 ],
546 ];
547
548 $grouped_errors = [];
549
550 foreach ( $error_details as $id => $error_detail ) {
551 $error_code = $error_detail['code'] ?? '';
552
553 if ( isset( $error_config[ $error_code ] ) ) {
554 $config = $error_config[ $error_code ];
555 $grouped_errors[ $error_code ][ $id ] = [
556 'status' => $config['status'],
557 'message' => $config['message'],
558 ];
559 } else {
560 $grouped_errors['unknown'][ $id ] = [
561 'status' => self::HTTP_SERVER_ERROR,
562 'message' => $error_detail['message'] ?? __( 'Unexpected error', 'elementor' ),
563 ];
564 }
565 }
566
567 foreach ( $error_config as $error_code => $config ) {
568 if ( ! empty( $grouped_errors[ $error_code ] ) ) {
569 return [
570 'code' => $config['batch_code'],
571 'message' => $config['batch_message'],
572 'filtered_errors' => $grouped_errors[ $error_code ],
573 ];
574 }
575 }
576
577 return [
578 'code' => 'batch_operation_failed',
579 'message' => __( 'Batch operation failed', 'elementor' ),
580 'filtered_errors' => $grouped_errors['unknown'] ?? [],
581 ];
582 }
583 }
584