PluginProbe
Elementor Website Builder – more than just a page builder / 3.35.3
Elementor Website Builder – more than just a page builder v3.35.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 / global-classes / global-classes-rest-api.php

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

340 lines 9.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\GlobalClasses;
4
5 use Elementor\Modules\GlobalClasses\Usage\Applied_Global_Classes_Usage;
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
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 class Global_Classes_REST_API {
15 const API_NAMESPACE = 'elementor/v1';
16 const API_BASE = 'global-classes';
17 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;
22
23 public function register_hooks() {
24 add_action( 'rest_api_init', fn() => $this->register_routes() );
25 }
26
27 private function get_repository() {
28 if ( ! $this->repository ) {
29 $this->repository = new Global_Classes_Repository();
30 }
31
32 return $this->repository;
33 }
34
35 private function register_routes() {
36 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
37 [
38 'methods' => 'GET',
39 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->all( $request ) ),
40 'permission_callback' => fn() => is_user_logged_in(),
41 'args' => [
42 'context' => [
43 'type' => 'string',
44 'required' => false,
45 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
46 'enum' => [
47 Global_Classes_Repository::CONTEXT_FRONTEND,
48 Global_Classes_Repository::CONTEXT_PREVIEW,
49 ],
50 ],
51 ],
52 ],
53 ] );
54
55 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE_USAGE, [
56 [
57 'callback' => fn() => $this->route_wrapper( fn() => $this->get_usage() ),
58 'permission_callback' => fn() => current_user_can( 'manage_options' ),
59 'args' => [
60 'context' => [
61 'type' => 'string',
62 'required' => false,
63 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
64 'enum' => [
65 Global_Classes_Repository::CONTEXT_FRONTEND,
66 Global_Classes_Repository::CONTEXT_PREVIEW,
67 ],
68 ],
69 ],
70 ],
71 ] );
72
73 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
74 [
75 'methods' => 'PUT',
76 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->put( $request ) ),
77 'permission_callback' => fn() => current_user_can( Add_Capabilities::UPDATE_CLASS ),
78 'args' => [
79 'context' => [
80 'type' => 'string',
81 'required' => false,
82 'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
83 'enum' => [
84 Global_Classes_Repository::CONTEXT_FRONTEND,
85 Global_Classes_Repository::CONTEXT_PREVIEW,
86 ],
87 ],
88 'changes' => [
89 'type' => 'object',
90 'required' => true,
91 'additionalProperties' => false,
92 'properties' => [
93 'added' => [
94 'type' => 'array',
95 'required' => true,
96 'items' => [ 'type' => 'string' ],
97 ],
98 'deleted' => [
99 'type' => 'array',
100 'required' => true,
101 'items' => [ 'type' => 'string' ],
102 ],
103 'modified' => [
104 'type' => 'array',
105 'required' => true,
106 'items' => [ 'type' => 'string' ],
107 ],
108 ],
109 ],
110 'items' => [
111 'required' => true,
112 'type' => 'object',
113 'additionalProperties' => [
114 'type' => 'object',
115 'properties' => [
116 'id' => [
117 'type' => 'string',
118 'required' => true,
119 ],
120 'variants' => [
121 'type' => 'array',
122 'required' => true,
123 ],
124 'type' => [
125 'type' => 'string',
126 'enum' => [ 'class' ],
127 'required' => true,
128 ],
129 'label' => [
130 'type' => 'string',
131 'required' => true,
132 ],
133 ],
134 ],
135 ],
136 'order' => [
137 'required' => true,
138 'type' => 'array',
139 'items' => [
140 'type' => 'string',
141 ],
142 ],
143 ],
144 ],
145 ] );
146 }
147
148 private function all( \WP_REST_Request $request ) {
149 $context = $request->get_param( 'context' );
150
151 $classes = $this->get_repository()->context( $context )->all();
152
153 return Response_Builder::make( (object) $classes->get_items()->all() )
154 ->set_meta( [ 'order' => $classes->get_order()->all() ] )
155 ->build();
156 }
157
158 private function get_usage() {
159 $classes_usage = ( new Applied_Global_Classes_Usage() )->get_detailed_usage();
160
161 return Response_Builder::make( (object) $classes_usage )->build();
162 }
163
164 private function put( \WP_REST_Request $request ) {
165 $context = $request->get_param( 'context' );
166 $changes = $request->get_param( 'changes' ) ?? [];
167 $new_added_items_ids = $changes['added'] ?? [];
168 $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();
177
178 $items_result = $parser->parse_items(
179 $request->get_param( 'items' )
180 );
181
182 $items_count = count( $items_result->unwrap() );
183
184 if ( $items_count > self::MAX_ITEMS ) {
185 return Error_Builder::make( 'global_classes_limit_exceeded' )
186 ->set_status( 400 )
187 ->set_meta([
188 'current_count' => $items_count,
189 'max_allowed' => self::MAX_ITEMS,
190 ])
191 ->set_message(sprintf(
192 /* translators: %d: Maximum allowed items. */
193 __( 'Global classes limit exceeded. Maximum allowed: %d', 'elementor' ),
194 self::MAX_ITEMS
195 ))
196 ->build();
197 }
198
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();
204 }
205
206 $order_result = $parser->parse_order(
207 $request->get_param( 'order' ),
208 $items_result->unwrap()
209 );
210
211 if ( ! $order_result->is_valid() ) {
212 return Error_Builder::make( 'invalid_order' )
213 ->set_status( 400 )
214 ->set_message( 'Invalid order: ' . $order_result->errors()->to_string() )
215 ->build();
216 }
217
218 $repository = $this->get_repository()
219 ->context( $request->get_param( 'context' ) );
220
221 $changes_resolver = Global_Classes_Changes_Resolver::make(
222 $repository,
223 $changes,
224 );
225
226 $duplicated_labels = Global_Classes_Parser::check_for_duplicate_labels(
227 $existing_labels,
228 $items_result->unwrap(),
229 $new_added_items_ids
230 );
231
232 $final_items = $items_result->unwrap();
233 $duplicate_validation_result = null;
234
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'];
240 }
241 }
242
243 $repository->put(
244 $changes_resolver->resolve_items( $final_items ),
245 $changes_resolver->resolve_order( $order_result->unwrap() ),
246 );
247
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();
254 }
255
256 return Response_Builder::make()->no_content()->build();
257 }
258
259 private function route_wrapper( callable $cb ) {
260 try {
261 $response = $cb();
262 } catch ( \Exception $e ) {
263 return Error_Builder::make( 'unexpected_error' )
264 ->set_message( __( 'Something went wrong', 'elementor' ) )
265 ->build();
266 }
267
268 return $response;
269 }
270
271 private function handle_duplicates( array $duplicate_labels, array $existing_labels ) {
272
273 $modified_labels = [];
274
275 foreach ( $duplicate_labels as $duplicate_label ) {
276 $item_id = $duplicate_label['item_id'];
277 $original_label = $duplicate_label['label'];
278
279 $modified_label = $this->generate_unique_label( $original_label, $existing_labels );
280
281 $modified_labels[ $item_id ] = [
282 'original' => $original_label,
283 'modified' => $modified_label,
284 ];
285 }
286
287 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 }
339 }
340