PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
4.3.0-beta3 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 All 452 releases
← All changes | modules/global-classes/global-classes-rest-api.php +250 -117 4.0.94.3.0-beta3 View file →
@@ -1,12 +1,14 @@
1 1 <?php
2 2
3 3 namespace Elementor\Modules\GlobalClasses;
4 4
5 -use Elementor\Modules\GlobalClasses\Usage\Applied_Global_Classes_Usage;
5 +use Elementor\Core\Kits\Documents\Kit;
6 6 use Elementor\Core\Utils\Api\Error_Builder;
7 7 use Elementor\Core\Utils\Api\Response_Builder;
8 8 use Elementor\Modules\GlobalClasses\Database\Migrations\Add_Capabilities;
9 +use Elementor\Modules\GlobalClasses\Usage\Applied_Global_Classes_Usage;
10 +use Elementor\Plugin;
9 11
10 12 if ( ! defined( 'ABSPATH' ) ) {
11 13 exit; // Exit if accessed directly.
12 14 }
@@ -14,26 +16,53 @@
14 16 class Global_Classes_REST_API {
15 17 const API_NAMESPACE = 'elementor/v1';
16 18 const API_BASE = 'global-classes';
17 19 const API_BASE_USAGE = self::API_BASE . '/usage';
18 - const MAX_ITEMS = 100;
19 - const LABEL_PREFIX = 'DUP_';
20 - const MAX_LABEL_LENGTH = 50;
21 - private $repository = null;
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;
22 26
23 27 public function register_hooks() {
24 28 add_action( 'rest_api_init', fn() => $this->register_routes() );
25 29 }
26 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 +
27 45 private function get_repository() {
28 46 if ( ! $this->repository ) {
29 - $this->repository = new Global_Classes_Repository();
47 + $this->repository = new Global_Classes_Repository( $this->get_kit() );
30 48 }
31 49
32 50 return $this->repository;
33 51 }
34 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 +
35 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 +
36 65 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
37 66 [
38 67 'methods' => 'GET',
39 68 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->all( $request ) ),
@@ -51,10 +80,58 @@
51 80 ],
52 81 ],
53 82 ] );
54 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 +
55 131 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE_USAGE, [
56 132 [
133 + 'methods' => 'GET',
57 134 'callback' => fn() => $this->route_wrapper( fn() => $this->get_usage() ),
58 135 'permission_callback' => fn() => current_user_can( 'manage_options' ),
59 136 'args' => [
60 137 'context' => [
@@ -104,8 +181,12 @@
104 181 'type' => 'array',
105 182 'required' => true,
106 183 'items' => [ 'type' => 'string' ],
107 184 ],
185 + 'order' => [
186 + 'type' => 'boolean',
187 + 'required' => false,
188 + ],
108 189 ],
109 190 ],
110 191 'items' => [
111 192 'required' => true,
@@ -146,16 +227,77 @@
146 227 }
147 228
148 229 private function all( \WP_REST_Request $request ) {
149 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 = [];
150 234
151 - $classes = $this->get_repository()->context( $context )->all();
235 + foreach ( $label_by_id as $id => $label ) {
236 + $list[] = [
237 + 'id' => $id,
238 + 'label' => $label,
239 + ];
240 + }
152 241
153 - return Response_Builder::make( (object) $classes->get_items()->all() )
154 - ->set_meta( [ 'order' => $classes->get_order()->all() ] )
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 ] )
155 269 ->build();
156 270 }
157 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 +
158 300 private function get_usage() {
159 301 $classes_usage = ( new Applied_Global_Classes_Usage() )->get_detailed_usage();
160 302
161 303 return Response_Builder::make( (object) $classes_usage )->build();
@@ -162,53 +304,75 @@
162 304 }
163 305
164 306 private function put( \WP_REST_Request $request ) {
165 307 $context = $request->get_param( 'context' );
308 + $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
166 309 $changes = $request->get_param( 'changes' ) ?? [];
167 - $new_added_items_ids = $changes['added'] ?? [];
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 +
168 319 $parser = Global_Classes_Parser::make();
169 - $existing_labels = Global_Classes_Repository::make()
170 - ->context( $context )
171 - ->all()
172 - ->get_items()
173 - ->map( function ( $item ) {
174 - return $item['label'];
175 - } )
176 - ->all();
320 + $items_result = $parser->parse_items( $request->get_param( 'items' ) ?? [] );
177 321
178 - $items_result = $parser->parse_items(
179 - $request->get_param( 'items' )
180 - );
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 + }
181 328
182 - $items_count = count( $items_result->unwrap() );
329 + $touched_items = $items_result->unwrap();
183 330
184 - if ( $items_count > self::MAX_ITEMS ) {
331 + if ( $total_count > self::MAX_ITEMS ) {
185 332 return Error_Builder::make( 'global_classes_limit_exceeded' )
186 333 ->set_status( 400 )
187 - ->set_meta([
188 - 'current_count' => $items_count,
334 + ->set_meta( [
335 + 'current_count' => $total_count,
189 336 'max_allowed' => self::MAX_ITEMS,
190 - ])
191 - ->set_message(sprintf(
337 + ] )
338 + ->set_message( sprintf(
192 339 /* translators: %d: Maximum allowed items. */
193 340 __( 'Global classes limit exceeded. Maximum allowed: %d', 'elementor' ),
194 341 self::MAX_ITEMS
195 - ))
342 + ) )
196 343 ->build();
197 344 }
198 345
199 - if ( ! $items_result->is_valid() ) {
200 - return Error_Builder::make( 'invalid_items' )
201 - ->set_status( 400 )
202 - ->set_message( 'Invalid items: ' . $items_result->errors()->to_string() )
203 - ->build();
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 + }
204 362 }
205 363
206 - $order_result = $parser->parse_order(
207 - $request->get_param( 'order' ),
208 - $items_result->unwrap()
209 - );
364 + $final_item_ids = array_keys( $this->merge_touched_with_existing_labels( $all_label_by_id, $touched_items, $deleted_ids ) );
210 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 +
211 375 if ( ! $order_result->is_valid() ) {
212 376 return Error_Builder::make( 'invalid_order' )
213 377 ->set_status( 400 )
214 378 ->set_message( 'Invalid order: ' . $order_result->errors()->to_string() )
@@ -214,47 +378,66 @@
214 378 ->set_message( 'Invalid order: ' . $order_result->errors()->to_string() )
215 379 ->build();
216 380 }
217 381
218 - $repository = $this->get_repository()
219 - ->context( $request->get_param( 'context' ) );
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() );
220 388
221 - $changes_resolver = Global_Classes_Changes_Resolver::make(
222 - $repository,
223 - $changes,
224 - );
389 + if ( $duplicate_validation_result ) {
390 + return Response_Builder::make( [
391 + 'code' => 'DUPLICATED_LABEL',
392 + 'modifiedLabels' => $duplicate_validation_result,
393 + ] )->build();
394 + }
225 395
226 - $duplicated_labels = Global_Classes_Parser::check_for_duplicate_labels(
227 - $existing_labels,
228 - $items_result->unwrap(),
229 - $new_added_items_ids
230 - );
396 + return Response_Builder::make()->no_content()->build();
397 + }
231 398
232 - $final_items = $items_result->unwrap();
233 - $duplicate_validation_result = null;
399 + private function global_classes_existing_label_list( array $label_by_id, array $deleted_ids ): array {
400 + $labels = [];
234 401
235 - if ( ! empty( $duplicated_labels ) ) {
236 - $modified_labels = $this->handle_duplicates( $duplicated_labels, $existing_labels );
237 - $duplicate_validation_result = $modified_labels;
238 - foreach ( $modified_labels as $item_id => $labels ) {
239 - $final_items[ $item_id ]['label'] = $labels['modified'];
402 + foreach ( $label_by_id as $id => $label ) {
403 + if ( in_array( $id, $deleted_ids, true ) ) {
404 + continue;
240 405 }
406 +
407 + $labels[] = $label;
241 408 }
242 409
243 - $repository->put(
244 - $changes_resolver->resolve_items( $final_items ),
245 - $changes_resolver->resolve_order( $order_result->unwrap() ),
246 - );
410 + return $labels;
411 + }
247 412
248 - if ( $duplicate_validation_result ) {
249 - $response_data = [
250 - 'code' => 'DUPLICATED_LABEL',
251 - 'modifiedLabels' => $duplicate_validation_result,
252 - ];
253 - return Response_Builder::make( $response_data )->build();
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 + }
254 431 }
255 432
256 - return Response_Builder::make()->no_content()->build();
433 + foreach ( $touched_items as $id => $item ) {
434 + if ( ! isset( $final[ $id ] ) ) {
435 + $final[ $id ] = $item;
436 + }
437 + }
438 +
439 + return $final;
257 440 }
258 441
259 442 private function route_wrapper( callable $cb ) {
260 443 try {
@@ -275,9 +458,9 @@
275 458 foreach ( $duplicate_labels as $duplicate_label ) {
276 459 $item_id = $duplicate_label['item_id'];
277 460 $original_label = $duplicate_label['label'];
278 461
279 - $modified_label = $this->generate_unique_label( $original_label, $existing_labels );
462 + $modified_label = Global_Classes_Labels::generate_unique_label( $original_label, $existing_labels );
280 463
281 464 $modified_labels[ $item_id ] = [
282 465 'original' => $original_label,
283 466 'modified' => $modified_label,
@@ -284,56 +467,6 @@
284 467 ];
285 468 }
286 469
287 470 return $modified_labels;
288 - }
289 -
290 -
291 - private function generate_unique_label( $original_label, $existing_labels ) {
292 - $prefix = self::LABEL_PREFIX;
293 - $max_length = self::MAX_LABEL_LENGTH;
294 -
295 - $has_prefix = strpos( $original_label, $prefix ) === 0;
296 -
297 - if ( $has_prefix ) {
298 - $base_label = substr( $original_label, strlen( $prefix ) );
299 -
300 - $counter = 1;
301 - $new_label = $prefix . $base_label . $counter;
302 -
303 - while ( in_array( $new_label, $existing_labels, true ) ) {
304 - ++$counter;
305 - $new_label = $prefix . $base_label . $counter;
306 - }
307 -
308 - if ( strlen( $new_label ) > $max_length ) {
309 - $available_length = $max_length - strlen( $prefix . $counter );
310 - $base_label = substr( $base_label, 0, $available_length );
311 - $new_label = $prefix . $base_label . $counter;
312 - }
313 - } else {
314 - $new_label = $prefix . $original_label;
315 -
316 - if ( strlen( $new_label ) > $max_length ) {
317 - $available_length = $max_length - strlen( $prefix );
318 - $new_label = $prefix . substr( $original_label, 0, $available_length );
319 - }
320 -
321 - $counter = 1;
322 - $base_label = substr( $original_label, 0, $available_length ?? strlen( $original_label ) );
323 -
324 - while ( in_array( $new_label, $existing_labels, true ) ) {
325 - $new_label = $prefix . $base_label . $counter;
326 -
327 - if ( strlen( $new_label ) > $max_length ) {
328 - $available_length = $max_length - strlen( $prefix . $counter );
329 - $base_label = substr( $original_label, 0, $available_length );
330 - $new_label = $prefix . $base_label . $counter;
331 - }
332 -
333 - ++$counter;
334 - }
335 - }
336 -
337 - return $new_label;
338 471 }
339 472 }