PluginProbe
Elementor Website Builder – more than just a page builder / 4.1.2
Elementor Website Builder – more than just a page builder v4.1.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
elementor / modules / global-classes / global-classes-rest-api.php

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

473 lines 13.5 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\GlobalClasses;
4
5 use Elementor\Core\Kits\Documents\Kit;
6 use Elementor\Core\Utils\Api\Error_Builder;
7 use Elementor\Core\Utils\Api\Response_Builder;
8 use Elementor\Modules\GlobalClasses\Database\Migrations\Add_Capabilities;
9 use Elementor\Modules\GlobalClasses\Usage\Applied_Global_Classes_Usage;
10 use Elementor\Plugin;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 class Global_Classes_REST_API {
17 const API_NAMESPACE = 'elementor/v1';
18 const API_BASE = 'global-classes';
19 const API_BASE_USAGE = self::API_BASE . '/usage';
20 const API_BASE_POST = self::API_BASE . '/post';
21 const API_BASE_STYLES = self::API_BASE . '/styles';
22 const MAX_ITEMS = 1000;
23 private ?Global_Classes_Repository $repository = null;
24 private ?Global_Classes_Relations $relations = null;
25 private ?Kit $kit = null;
26
27 public function register_hooks() {
28 add_action( 'rest_api_init', fn() => $this->register_routes() );
29 }
30
31 public function invalidate_cache() {
32 $this->kit = null;
33 $this->repository = null;
34 $this->relations = null;
35 }
36
37 private function get_kit(): ?Kit {
38 if ( ! $this->kit ) {
39 $this->kit = Plugin::$instance->kits_manager->get_active_kit();
40 }
41
42 return $this->kit;
43 }
44
45 private function get_repository() {
46 if ( ! $this->repository ) {
47 $this->repository = new Global_Classes_Repository( $this->get_kit() );
48 }
49
50 return $this->repository;
51 }
52
53 private function get_classes_relations(): Global_Classes_Relations {
54 if ( ! $this->relations ) {
55 $this->relations = new Global_Classes_Relations();
56 }
57
58 return $this->relations;
59 }
60
61 private function register_routes() {
62 // cache invalidation at this point is solely for tests, in particular - Test_Global_Classes_Rest_Api
63 $this->invalidate_cache();
64
65 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
66 [
67 'methods' => 'GET',
68 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->all( $request ) ),
69 'permission_callback' => fn() => is_user_logged_in(),
70 'args' => [
71 'context' => [
72 'type' => 'string',
73 'required' => false,
74 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
75 'enum' => [
76 Global_Classes_Repository::CONTEXT_FRONTEND,
77 Global_Classes_Repository::CONTEXT_PREVIEW,
78 ],
79 ],
80 ],
81 ],
82 ] );
83
84 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE_POST, [
85 [
86 'methods' => 'GET',
87 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->styles_for_post( $request ) ),
88 'permission_callback' => fn() => is_user_logged_in(),
89 'args' => [
90 'context' => [
91 'type' => 'string',
92 'required' => false,
93 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
94 'enum' => [
95 Global_Classes_Repository::CONTEXT_FRONTEND,
96 Global_Classes_Repository::CONTEXT_PREVIEW,
97 ],
98 ],
99 'post_id' => [
100 'type' => 'integer',
101 'required' => true,
102 ],
103 ],
104 ],
105 ] );
106
107 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE_STYLES, [
108 [
109 'methods' => 'GET',
110 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->styles_by_ids( $request ) ),
111 'permission_callback' => fn() => is_user_logged_in(),
112 'args' => [
113 'context' => [
114 'type' => 'string',
115 'required' => false,
116 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
117 'enum' => [
118 Global_Classes_Repository::CONTEXT_FRONTEND,
119 Global_Classes_Repository::CONTEXT_PREVIEW,
120 ],
121 ],
122 'ids' => [
123 'type' => 'string',
124 'required' => true,
125 'description' => 'Comma-separated list of global class IDs',
126 ],
127 ],
128 ],
129 ] );
130
131 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE_USAGE, [
132 [
133 'methods' => 'GET',
134 'callback' => fn() => $this->route_wrapper( fn() => $this->get_usage() ),
135 'permission_callback' => fn() => current_user_can( 'manage_options' ),
136 'args' => [
137 'context' => [
138 'type' => 'string',
139 'required' => false,
140 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
141 'enum' => [
142 Global_Classes_Repository::CONTEXT_FRONTEND,
143 Global_Classes_Repository::CONTEXT_PREVIEW,
144 ],
145 ],
146 ],
147 ],
148 ] );
149
150 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
151 [
152 'methods' => 'PUT',
153 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->put( $request ) ),
154 'permission_callback' => fn() => current_user_can( Add_Capabilities::UPDATE_CLASS ),
155 'args' => [
156 'context' => [
157 'type' => 'string',
158 'required' => false,
159 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
160 'enum' => [
161 Global_Classes_Repository::CONTEXT_FRONTEND,
162 Global_Classes_Repository::CONTEXT_PREVIEW,
163 ],
164 ],
165 'changes' => [
166 'type' => 'object',
167 'required' => true,
168 'additionalProperties' => false,
169 'properties' => [
170 'added' => [
171 'type' => 'array',
172 'required' => true,
173 'items' => [ 'type' => 'string' ],
174 ],
175 'deleted' => [
176 'type' => 'array',
177 'required' => true,
178 'items' => [ 'type' => 'string' ],
179 ],
180 'modified' => [
181 'type' => 'array',
182 'required' => true,
183 'items' => [ 'type' => 'string' ],
184 ],
185 'order' => [
186 'type' => 'boolean',
187 'required' => false,
188 ],
189 ],
190 ],
191 'items' => [
192 'required' => true,
193 'type' => 'object',
194 'additionalProperties' => [
195 'type' => 'object',
196 'properties' => [
197 'id' => [
198 'type' => 'string',
199 'required' => true,
200 ],
201 'variants' => [
202 'type' => 'array',
203 'required' => true,
204 ],
205 'type' => [
206 'type' => 'string',
207 'enum' => [ 'class' ],
208 'required' => true,
209 ],
210 'label' => [
211 'type' => 'string',
212 'required' => true,
213 ],
214 ],
215 ],
216 ],
217 'order' => [
218 'required' => true,
219 'type' => 'array',
220 'items' => [
221 'type' => 'string',
222 ],
223 ],
224 ],
225 ],
226 ] );
227 }
228
229 private function all( \WP_REST_Request $request ) {
230 $context = $request->get_param( 'context' );
231 $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
232 $label_by_id = $this->get_repository()->set_preview( $is_preview )->all_labels();
233 $list = [];
234
235 foreach ( $label_by_id as $id => $label ) {
236 $list[] = [
237 'id' => $id,
238 'label' => $label,
239 ];
240 }
241
242 return Response_Builder::make( $list )->build();
243 }
244
245 private function styles_for_post( \WP_REST_Request $request ) {
246 $context = $request->get_param( 'context' );
247 $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
248 $post_id = (int) $request->get_param( 'post_id' );
249
250 $document_class_ids = $this->get_classes_relations()->set_preview( $is_preview )->get_styles_by_post( $post_id );
251
252 if ( empty( $document_class_ids ) ) {
253 return Response_Builder::make( (object) [] )->set_meta( [ 'order' => [] ] )->build();
254 }
255
256 $repository = $this->get_repository()->set_preview( $is_preview );
257 $global_order = array_keys( $repository->all_labels() );
258 $filtered_order = array_values( array_intersect( $global_order, $document_class_ids ) );
259 $items = $repository->get_by_ids( $document_class_ids );
260
261 $result = [];
262
263 foreach ( $document_class_ids as $id ) {
264 $result[ $id ] = $items[ $id ] ?? null;
265 }
266
267 return Response_Builder::make( (object) $result )
268 ->set_meta( [ 'order' => $filtered_order ] )
269 ->build();
270 }
271
272 private function styles_by_ids( \WP_REST_Request $request ) {
273 $context = $request->get_param( 'context' );
274 $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
275 $ids_param = $request->get_param( 'ids' );
276
277 $requested_ids = array_map( 'trim', explode( ',', $ids_param ) );
278 $requested_ids = array_filter( $requested_ids );
279
280 if ( empty( $requested_ids ) ) {
281 return Response_Builder::make( (object) [] )->set_meta( [ 'order' => [] ] )->build();
282 }
283
284 $repository = $this->get_repository()->set_preview( $is_preview );
285 $global_order = array_keys( $repository->all_labels() );
286 $filtered_order = array_values( array_intersect( $global_order, $requested_ids ) );
287 $items = $repository->get_by_ids( $requested_ids );
288
289 $result = [];
290
291 foreach ( $requested_ids as $id ) {
292 $result[ $id ] = $items[ $id ] ?? null;
293 }
294
295 return Response_Builder::make( (object) $result )
296 ->set_meta( [ 'order' => $filtered_order ] )
297 ->build();
298 }
299
300 private function get_usage() {
301 $classes_usage = ( new Applied_Global_Classes_Usage() )->get_detailed_usage();
302
303 return Response_Builder::make( (object) $classes_usage )->build();
304 }
305
306 private function put( \WP_REST_Request $request ) {
307 $context = $request->get_param( 'context' );
308 $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
309 $changes = $request->get_param( 'changes' ) ?? [];
310 $added_ids = $changes['added'] ?? [];
311 $deleted_ids = $changes['deleted'] ?? [];
312 $order = $request->get_param( 'order' ) ?? [];
313
314 $repository = $this->get_repository()->set_preview( $is_preview );
315 $all_label_by_id = $repository->all_labels();
316 $existing_label_list = $this->global_classes_existing_label_list( $all_label_by_id, $deleted_ids );
317 $total_count = count( $all_label_by_id ) - count( $deleted_ids ) + count( $added_ids );
318
319 $parser = Global_Classes_Parser::make();
320 $items_result = $parser->parse_items( $request->get_param( 'items' ) ?? [] );
321
322 if ( ! $items_result->is_valid() ) {
323 return Error_Builder::make( 'invalid_items' )
324 ->set_status( 400 )
325 ->set_message( 'Invalid items: ' . $items_result->errors()->to_string() )
326 ->build();
327 }
328
329 $touched_items = $items_result->unwrap();
330
331 if ( $total_count > self::MAX_ITEMS ) {
332 return Error_Builder::make( 'global_classes_limit_exceeded' )
333 ->set_status( 400 )
334 ->set_meta( [
335 'current_count' => $total_count,
336 'max_allowed' => self::MAX_ITEMS,
337 ] )
338 ->set_message( sprintf(
339 /* translators: %d: Maximum allowed items. */
340 __( 'Global classes limit exceeded. Maximum allowed: %d', 'elementor' ),
341 self::MAX_ITEMS
342 ) )
343 ->build();
344 }
345
346 $duplicated_labels = Global_Classes_Parser::check_for_duplicate_labels(
347 $all_label_by_id,
348 $deleted_ids,
349 $touched_items,
350 $added_ids
351 );
352
353 $duplicate_validation_result = null;
354
355 if ( ! empty( $duplicated_labels ) ) {
356 $modified_labels = $this->handle_duplicates( $duplicated_labels, $existing_label_list );
357 $duplicate_validation_result = $modified_labels;
358
359 foreach ( $modified_labels as $item_id => $labels ) {
360 $touched_items[ $item_id ]['label'] = $labels['modified'];
361 }
362 }
363
364 $final_item_ids = array_keys( $this->merge_touched_with_existing_labels( $all_label_by_id, $touched_items, $deleted_ids ) );
365
366 $final_item_ids_set = array_flip( $final_item_ids );
367 $order_set = array_flip( $order );
368
369 $order = array_values( array_filter( $order, fn( $id ) => isset( $final_item_ids_set[ $id ] ) ) );
370 $missing_from_order = array_values( array_filter( $final_item_ids, fn( $id ) => ! isset( $order_set[ $id ] ) ) );
371 $order = array_merge( $order, $missing_from_order );
372
373 $order_result = $parser->parse_order( $order, $final_item_ids );
374
375 if ( ! $order_result->is_valid() ) {
376 return Error_Builder::make( 'invalid_order' )
377 ->set_status( 400 )
378 ->set_message( 'Invalid order: ' . $order_result->errors()->to_string() )
379 ->build();
380 }
381
382 $repository->apply_changes( $touched_items, [
383 'added' => $added_ids,
384 'deleted' => $changes['deleted'] ?? [],
385 'modified' => $changes['modified'] ?? [],
386 'order' => isset( $changes['order'] ) && $changes['order'], // boolean indicating if the order has changed
387 ], $order_result->unwrap() );
388
389 if ( $duplicate_validation_result ) {
390 return Response_Builder::make( [
391 'code' => 'DUPLICATED_LABEL',
392 'modifiedLabels' => $duplicate_validation_result,
393 ] )->build();
394 }
395
396 return Response_Builder::make()->no_content()->build();
397 }
398
399 private function global_classes_existing_label_list( array $label_by_id, array $deleted_ids ): array {
400 $labels = [];
401
402 foreach ( $label_by_id as $id => $label ) {
403 if ( in_array( $id, $deleted_ids, true ) ) {
404 continue;
405 }
406
407 $labels[] = $label;
408 }
409
410 return $labels;
411 }
412
413 private function merge_touched_with_existing_labels( array $label_by_id, array $touched_items, array $deleted_ids ): array {
414 $final = [];
415
416 foreach ( $label_by_id as $id => $label ) {
417 if ( in_array( $id, $deleted_ids, true ) ) {
418 continue;
419 }
420
421 if ( isset( $touched_items[ $id ] ) ) {
422 $final[ $id ] = $touched_items[ $id ];
423 } else {
424 $final[ $id ] = [
425 'id' => $id,
426 'label' => $label,
427 'type' => 'class',
428 'variants' => [],
429 ];
430 }
431 }
432
433 foreach ( $touched_items as $id => $item ) {
434 if ( ! isset( $final[ $id ] ) ) {
435 $final[ $id ] = $item;
436 }
437 }
438
439 return $final;
440 }
441
442 private function route_wrapper( callable $cb ) {
443 try {
444 $response = $cb();
445 } catch ( \Exception $e ) {
446 return Error_Builder::make( 'unexpected_error' )
447 ->set_message( __( 'Something went wrong', 'elementor' ) )
448 ->build();
449 }
450
451 return $response;
452 }
453
454 private function handle_duplicates( array $duplicate_labels, array $existing_labels ) {
455
456 $modified_labels = [];
457
458 foreach ( $duplicate_labels as $duplicate_label ) {
459 $item_id = $duplicate_label['item_id'];
460 $original_label = $duplicate_label['label'];
461
462 $modified_label = Global_Classes_Labels::generate_unique_label( $original_label, $existing_labels );
463
464 $modified_labels[ $item_id ] = [
465 'original' => $original_label,
466 'modified' => $modified_label,
467 ];
468 }
469
470 return $modified_labels;
471 }
472 }
473