PluginProbe
Elementor Website Builder – more than just a page builder / 3.34.3
Elementor Website Builder – more than just a page builder v3.34.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 / variables / storage / repository.php

repository.php in Elementor Website Builder – more than just a page builder 3.34.3, at modules/variables/storage/repository.php

505 lines 13.6 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\Storage;
4
5 use Elementor\Core\Kits\Documents\Kit;
6 use Elementor\Modules\AtomicWidgets\Utils\Utils;
7 use Elementor\Modules\Variables\Storage\Exceptions\DuplicatedLabel;
8 use Elementor\Modules\Variables\Storage\Exceptions\RecordNotFound;
9 use Elementor\Modules\Variables\Storage\Exceptions\VariablesLimitReached;
10 use Elementor\Modules\Variables\Storage\Exceptions\FatalError;
11 use Elementor\Modules\Variables\Storage\Exceptions\BatchOperationFailed;
12 use Exception;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17
18 class Repository {
19 // TODO: deleted this class later after this PR
20 const TOTAL_VARIABLES_COUNT = 100;
21 const FORMAT_VERSION_V1 = 1;
22 const VARIABLES_META_KEY = '_elementor_global_variables';
23 private Kit $kit;
24
25 public function __construct( Kit $kit ) {
26 $this->kit = $kit;
27 }
28
29 /**
30 * @throws VariablesLimitReached If database connection fails or query execution errors occur.
31 */
32 private function assert_if_variables_limit_reached( array $db_record ) {
33 $variables_in_use = 0;
34
35 foreach ( $db_record['data'] as $variable ) {
36 if ( isset( $variable['deleted'] ) && $variable['deleted'] ) {
37 continue;
38 }
39
40 ++$variables_in_use;
41 }
42
43 if ( self::TOTAL_VARIABLES_COUNT < $variables_in_use ) {
44 throw new VariablesLimitReached( 'Total variables count limit reached' );
45 }
46 }
47
48 /**
49 * @throws DuplicatedLabel If variable creation fails or validation errors occur.
50 */
51 private function assert_if_variable_label_is_duplicated( array $db_record, array $variable = [] ) {
52 foreach ( $db_record['data'] as $id => $existing_variable ) {
53 if ( isset( $existing_variable['deleted'] ) && $existing_variable['deleted'] ) {
54 continue;
55 }
56
57 if ( isset( $variable['id'] ) && $variable['id'] === $id ) {
58 continue;
59 }
60
61 if ( ! isset( $variable['label'] ) || ! isset( $existing_variable['label'] ) ) {
62 continue;
63 }
64
65 if ( strtolower( $existing_variable['label'] ) === strtolower( $variable['label'] ) ) {
66 throw new DuplicatedLabel( 'Variable label already exists' );
67 }
68 }
69 }
70
71 public function variables(): array {
72 $db_record = $this->load();
73
74 return $db_record['data'] ?? [];
75 }
76
77 public function load(): array {
78 $db_record = $this->kit->get_json_meta( static::VARIABLES_META_KEY );
79
80 if ( is_array( $db_record ) && ! empty( $db_record ) ) {
81 return $db_record;
82 }
83
84 return $this->get_default_meta();
85 }
86
87 /**
88 * @throws FatalError If variable update fails or validation errors occur.
89 */
90 public function create( array $variable ) {
91 $db_record = $this->load();
92
93 $list_of_variables = $db_record['data'] ?? [];
94
95 $id = $this->new_id_for( $list_of_variables );
96 $new_variable = $this->extract_from( $variable, [
97 'type',
98 'label',
99 'value',
100 'order',
101 ] );
102
103 if ( ! isset( $new_variable['order'] ) ) {
104 $new_variable['order'] = $this->get_next_order( $list_of_variables );
105 }
106
107 $this->assert_if_variable_label_is_duplicated( $db_record, $new_variable );
108
109 $list_of_variables[ $id ] = $new_variable;
110 $db_record['data'] = $list_of_variables;
111
112 $this->assert_if_variables_limit_reached( $db_record );
113
114 $watermark = $this->save( $db_record );
115
116 if ( false === $watermark ) {
117 throw new FatalError( 'Failed to create variable' );
118 }
119
120 return [
121 'variable' => array_merge( [ 'id' => $id ], $list_of_variables[ $id ] ),
122 'watermark' => $watermark,
123 ];
124 }
125
126 /**
127 * @throws RecordNotFound If variable deletion fails or database errors occur.
128 * @throws FatalError If variable deletion fails or database errors occur.
129 */
130 public function update( string $id, array $variable ) {
131 $db_record = $this->load();
132
133 $list_of_variables = $db_record['data'] ?? [];
134
135 if ( ! isset( $list_of_variables[ $id ] ) ) {
136 throw new RecordNotFound( 'Variable not found' );
137 }
138
139 $updated_variable = array_merge( $list_of_variables[ $id ], $this->extract_from( $variable, [
140 'label',
141 'value',
142 'order',
143 ] ) );
144
145 $this->assert_if_variable_label_is_duplicated( $db_record, array_merge( $updated_variable, [ 'id' => $id ] ) );
146
147 $list_of_variables[ $id ] = $updated_variable;
148 $db_record['data'] = $list_of_variables;
149
150 $watermark = $this->save( $db_record );
151
152 if ( false === $watermark ) {
153 throw new FatalError( 'Failed to update variable' );
154 }
155
156 return [
157 'variable' => array_merge( [ 'id' => $id ], $list_of_variables[ $id ] ),
158 'watermark' => $watermark,
159 ];
160 }
161
162 /**
163 * @throws RecordNotFound If bulk operation fails or validation errors occur.
164 * @throws FatalError If bulk operation fails or validation errors occur.
165 */
166 public function delete( string $id ) {
167 $db_record = $this->load();
168
169 $list_of_variables = $db_record['data'] ?? [];
170
171 if ( ! isset( $list_of_variables[ $id ] ) ) {
172 throw new RecordNotFound( 'Variable not found' );
173 }
174
175 $list_of_variables[ $id ]['deleted'] = true;
176 $list_of_variables[ $id ]['deleted_at'] = $this->now();
177
178 $db_record['data'] = $list_of_variables;
179
180 $watermark = $this->save( $db_record );
181
182 if ( false === $watermark ) {
183 throw new FatalError( 'Failed to delete variable' );
184 }
185
186 return [
187 'variable' => array_merge( [ 'id' => $id ], $list_of_variables[ $id ] ),
188 'watermark' => $watermark,
189 ];
190 }
191
192 /**
193 * @throws RecordNotFound If export operation fails or data serialization errors occur.
194 * @throws FatalError If export operation fails or data serialization errors occur.
195 */
196 public function restore( string $id, $overrides = [] ) {
197 $db_record = $this->load();
198
199 $list_of_variables = $db_record['data'] ?? [];
200
201 if ( ! isset( $list_of_variables[ $id ] ) ) {
202 throw new RecordNotFound( 'Variable not found' );
203 }
204
205 $restored_variable = $this->extract_from( $list_of_variables[ $id ], [
206 'label',
207 'value',
208 'type',
209 'order',
210 ] );
211
212 if ( array_key_exists( 'label', $overrides ) ) {
213 $restored_variable['label'] = $overrides['label'];
214 }
215
216 if ( array_key_exists( 'value', $overrides ) ) {
217 $restored_variable['value'] = $overrides['value'];
218 }
219
220 $this->assert_if_variable_label_is_duplicated( $db_record, array_merge( $restored_variable, [ 'id' => $id ] ) );
221
222 $list_of_variables[ $id ] = $restored_variable;
223 $db_record['data'] = $list_of_variables;
224
225 $this->assert_if_variables_limit_reached( $db_record );
226
227 $watermark = $this->save( $db_record );
228
229 if ( false === $watermark ) {
230 throw new FatalError( 'Failed to restore variable' );
231 }
232
233 return [
234 'variable' => array_merge( [ 'id' => $id ], $restored_variable ),
235 'watermark' => $watermark,
236 ];
237 }
238
239 /**
240 * Process multiple operations atomically
241 *
242 * @throws BatchOperationFailed If batch operation fails or validation errors occur.
243 * @throws FatalError If batch operation fails or validation errors occur.
244 */
245 public function process_atomic_batch( array $operations, int $expected_watermark ): array {
246 $db_record = $this->load();
247 $results = [];
248 $errors = [];
249
250 foreach ( $operations as $index => $operation ) {
251 try {
252 $result = $this->process_single_operation( $db_record, $operation );
253 $results[] = $result;
254 } catch ( Exception $e ) {
255 $operation_id = $this->get_operation_identifier( $operation, $index );
256 $errors[ $operation_id ] = [
257 'status' => $this->get_error_status_code( $e ),
258 'code' => $this->get_error_code( $e ),
259 'message' => $e->getMessage(),
260 ];
261 }
262 }
263
264 if ( ! empty( $errors ) ) {
265 $error_details = [];
266
267 foreach ( $errors as $operation_id => $error ) {
268 $error_details[ esc_html( $operation_id ) ] = [
269 'status' => (int) $error['status'],
270 'code' => $error['code'],
271 'message' => esc_html( $error['message'] ),
272 ];
273 }
274
275 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
276 throw new BatchOperationFailed( 'Batch operation failed', $error_details );
277 }
278
279 $watermark = $this->save( $db_record );
280
281 if ( false === $watermark ) {
282 throw new FatalError( 'Failed to save batch operations' );
283 }
284
285 return [
286 'success' => true,
287 'watermark' => $watermark,
288 'results' => $results,
289 ];
290 }
291
292 private function process_single_operation( array &$db_record, array $operation ): array {
293 switch ( $operation['type'] ) {
294 case 'create':
295 return $this->process_create_operation( $db_record, $operation );
296
297 case 'update':
298 return $this->process_update_operation( $db_record, $operation );
299
300 case 'delete':
301 return $this->process_delete_operation( $db_record, $operation );
302
303 case 'restore':
304 return $this->process_restore_operation( $db_record, $operation );
305
306 default:
307 throw new BatchOperationFailed( 'Invalid operation type: ' . esc_html( $operation['type'] ), [] );
308 }
309 }
310
311 private function process_create_operation( array &$db_record, array $operation ): array {
312 $variable_data = $operation['variable'];
313
314 $temp_id = $variable_data['id'] ?? null;
315 $new_variable = $this->extract_from( $variable_data, [ 'type', 'label', 'value', 'order' ] );
316
317 if ( ! isset( $new_variable['order'] ) ) {
318 $new_variable['order'] = $this->get_next_order( $db_record['data'] );
319 }
320
321 $this->assert_if_variable_label_is_duplicated( $db_record, $new_variable );
322
323 $this->assert_if_variables_limit_reached( $db_record );
324
325 $id = $this->new_id_for( $db_record['data'] );
326 $now = $this->now();
327
328 $new_variable['created_at'] = $now;
329 $new_variable['updated_at'] = $now;
330
331 $db_record['data'][ $id ] = $new_variable;
332
333 return [
334 'id' => $id,
335 'type' => 'create',
336 'variable' => array_merge( [ 'id' => $id ], $new_variable ),
337 'temp_id' => $temp_id,
338 ];
339 }
340
341 private function process_update_operation( array &$db_record, array $operation ): array {
342 $id = $operation['id'];
343 $variable_data = $operation['variable'];
344
345 if ( ! isset( $db_record['data'][ $id ] ) ) {
346 throw new \Elementor\Modules\Variables\Storage\Exceptions\RecordNotFound( 'Variable not found' );
347 }
348
349 $updated_fields = $this->extract_from( $variable_data, [ 'label', 'value', 'order' ] );
350 $updated_variable = array_merge( $db_record['data'][ $id ], $updated_fields );
351 $updated_variable['updated_at'] = $this->now();
352
353 $this->assert_if_variable_label_is_duplicated( $db_record, array_merge( $updated_variable, [ 'id' => $id ] ) );
354
355 $db_record['data'][ $id ] = $updated_variable;
356
357 return [
358 'id' => $id,
359 'type' => 'update',
360 'variable' => array_merge( [ 'id' => $id ], $updated_variable ),
361 ];
362 }
363
364 private function process_delete_operation( array &$db_record, array $operation ): array {
365 $id = $operation['id'];
366
367 if ( ! isset( $db_record['data'][ $id ] ) ) {
368 throw new RecordNotFound( 'Variable not found' );
369 }
370
371 $db_record['data'][ $id ]['deleted'] = true;
372 $db_record['data'][ $id ]['deleted_at'] = $this->now();
373
374 return [
375 'id' => $id,
376 'type' => 'delete',
377 'deleted' => true,
378 ];
379 }
380
381 private function process_restore_operation( array &$db_record, array $operation ): array {
382 $id = $operation['id'];
383
384 if ( ! isset( $db_record['data'][ $id ] ) ) {
385 throw new RecordNotFound( 'Variable not found' );
386 }
387
388 $overrides = [];
389
390 if ( isset( $operation['label'] ) ) {
391 $overrides['label'] = $operation['label'];
392 }
393
394 if ( isset( $operation['value'] ) ) {
395 $overrides['value'] = $operation['value'];
396 }
397
398 $restored_variable = $this->extract_from( $db_record['data'][ $id ], [ 'label', 'value', 'type' ] );
399 $restored_variable = array_merge( $restored_variable, $overrides );
400 $restored_variable['updated_at'] = $this->now();
401
402 $this->assert_if_variable_label_is_duplicated( $db_record, array_merge( $restored_variable, [ 'id' => $id ] ) );
403
404 $this->assert_if_variables_limit_reached( $db_record );
405
406 $db_record['data'][ $id ] = $restored_variable;
407
408 return [
409 'id' => $id,
410 'type' => 'restore',
411 'variable' => array_merge( [ 'id' => $id ], $restored_variable ),
412 ];
413 }
414
415 private function get_operation_identifier( array $operation, int $index ): string {
416 if ( 'create' === $operation['type'] && isset( $operation['variable']['id'] ) ) {
417 return $operation['variable']['id'];
418 }
419
420 if ( isset( $operation['id'] ) ) {
421 return $operation['id'];
422 }
423
424 return "operation_{$index}";
425 }
426
427 private function get_error_status_code( Exception $e ): int {
428 if ( $e instanceof RecordNotFound ) {
429 return 404;
430 }
431
432 if ( $e instanceof DuplicatedLabel || $e instanceof VariablesLimitReached ) {
433 return 400;
434 }
435
436 return 500;
437 }
438
439 private function get_error_code( Exception $e ): string {
440 if ( $e instanceof VariablesLimitReached ) {
441 return 'invalid_variable_limit_reached';
442 }
443
444 if ( $e instanceof DuplicatedLabel ) {
445 return 'duplicated_label';
446 }
447
448 if ( $e instanceof RecordNotFound ) {
449 return 'variable_not_found';
450 }
451
452 return 'unexpected_server_error';
453 }
454
455 private function save( array $db_record ) {
456 if ( PHP_INT_MAX === $db_record['watermark'] ) {
457 $db_record['watermark'] = 0;
458 }
459
460 ++$db_record['watermark'];
461
462 if ( $this->kit->update_json_meta( static::VARIABLES_META_KEY, $db_record ) ) {
463 return $db_record['watermark'];
464 }
465
466 return false;
467 }
468
469 private function new_id_for( array $list_of_variables ): string {
470 return Utils::generate_id( 'e-gv-', array_keys( $list_of_variables ) );
471 }
472
473 private function now(): string {
474 return gmdate( 'Y-m-d H:i:s' );
475 }
476
477 private function extract_from( array $source, array $fields ): array {
478 return array_intersect_key( $source, array_flip( $fields ) );
479 }
480
481 private function get_default_meta(): array {
482 return [
483 'data' => [],
484 'watermark' => 0,
485 'version' => self::FORMAT_VERSION_V1,
486 ];
487 }
488
489 private function get_next_order( array $list_of_variables ): int {
490 $highest_order = 0;
491
492 foreach ( $list_of_variables as $variable ) {
493 if ( isset( $variable['deleted'] ) && $variable['deleted'] ) {
494 continue;
495 }
496
497 if ( isset( $variable['order'] ) && $variable['order'] > $highest_order ) {
498 $highest_order = $variable['order'];
499 }
500 }
501
502 return $highest_order + 1;
503 }
504 }
505