PluginProbe
Elementor Website Builder – more than just a page builder / 3.27.4
Elementor Website Builder – more than just a page builder v3.27.4
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 +111 -250 4.0.73.27.4 View file →
@@ -1,12 +1,12 @@
1 1 <?php
2 2
3 3 namespace Elementor\Modules\GlobalClasses;
4 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;
5 +use Elementor\Core\Utils\Collection;
6 +use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
7 +use Elementor\Modules\AtomicWidgets\Parsers\Style_Parser;
8 +use Elementor\Plugin;
9 9
10 10 if ( ! defined( 'ABSPATH' ) ) {
11 11 exit; // Exit if accessed directly.
12 12 }
@@ -13,12 +13,9 @@
13 13
14 14 class Global_Classes_REST_API {
15 15 const API_NAMESPACE = 'elementor/v1';
16 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;
17 +
21 18 private $repository = null;
22 19
23 20 public function register_hooks() {
24 21 add_action( 'rest_api_init', fn() => $this->register_routes() );
@@ -31,309 +28,173 @@
31 28
32 29 return $this->repository;
33 30 }
34 31
32 + // TODO: Add sanitization when implemented on prop types [EDS-574]
35 33 private function register_routes() {
36 34 register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
37 35 [
38 36 'methods' => 'GET',
39 - 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->all( $request ) ),
40 - 'permission_callback' => fn() => is_user_logged_in(),
37 + 'callback' => fn() => $this->route_wrapper( fn() => $this->all() ),
38 + 'permission_callback' => fn() => current_user_can( 'manage_options' ),
39 + ],
40 + ] );
41 +
42 + register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/(?P<id>[\w-]+)', [
43 + [
44 + 'methods' => 'GET',
45 + 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->get( $request ) ),
41 46 'args' => [
42 - 'context' => [
47 + 'id' => [
43 48 '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 - ],
49 + 'required' => true,
50 50 ],
51 51 ],
52 + 'permission_callback' => fn() => current_user_can( 'manage_options' ),
52 53 ],
53 54 ] );
54 55
55 - register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE_USAGE, [
56 + register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/(?P<id>[\w-]+)', [
56 57 [
57 - 'callback' => fn() => $this->route_wrapper( fn() => $this->get_usage() ),
58 - 'permission_callback' => fn() => current_user_can( 'manage_options' ),
58 + 'methods' => 'DELETE',
59 + 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->delete( $request ) ),
59 60 'args' => [
60 - 'context' => [
61 + 'id' => [
61 62 '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 - ],
63 + 'required' => true,
68 64 ],
69 65 ],
66 + 'permission_callback' => fn() => current_user_can( 'manage_options' ),
70 67 ],
71 68 ] );
72 69
73 - register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
70 + register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/(?P<id>[\w-]+)', [
74 71 [
75 72 'methods' => 'PUT',
76 73 '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 - ],
74 + 'permission_callback' => fn() => current_user_can( 'manage_options' ),
144 75 ],
145 76 ] );
146 - }
147 77
148 - private function all( \WP_REST_Request $request ) {
149 - $context = $request->get_param( 'context' );
78 + register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
79 + [
80 + 'methods' => 'POST',
81 + 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create( $request ) ),
82 + 'permission_callback' => fn() => current_user_can( 'manage_options' ),
83 + ],
84 + ] );
150 85
151 - $classes = $this->get_repository()->context( $context )->all();
86 + register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '-order', [
87 + [
88 + 'methods' => 'PUT',
89 + 'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->arrange( $request ) ),
90 + 'validate_callback' => function( \WP_REST_Request $request ) {
91 + $order = $request->get_params();
152 92
153 - return Response_Builder::make( (object) $classes->get_items()->all() )
154 - ->set_meta( [ 'order' => $classes->get_order()->all() ] )
155 - ->build();
156 - }
93 + if ( ! is_array( $order ) ) {
94 + return false;
95 + }
157 96
158 - private function get_usage() {
159 - $classes_usage = ( new Applied_Global_Classes_Usage() )->get_detailed_usage();
97 + $classes = $this->get_repository()->all();
160 98
161 - return Response_Builder::make( (object) $classes_usage )->build();
99 + $missing_items = Collection::make( $classes->get_items()->keys() )->diff( $order );
100 + $extra_items = Collection::make( $order )->diff( $classes->get_items()->keys() );
101 +
102 + return $missing_items->is_empty() && $extra_items->is_empty();
103 + },
104 + 'permission_callback' => fn() => current_user_can( 'manage_options' ),
105 + ],
106 + ] );
162 107 }
163 108
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();
109 + private function all() {
110 + $classes = $this->get_repository()->all();
177 111
178 - $items_result = $parser->parse_items(
179 - $request->get_param( 'items' )
180 - );
112 + return $classes->get();
113 + }
181 114
182 - $items_count = count( $items_result->unwrap() );
115 + private function get( \WP_REST_Request $request ) {
116 + $id = $request->get_param( 'id' );
117 + $class = $this->get_repository()->get( $id );
183 118
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();
119 + if ( null === $class ) {
120 + return new \WP_Error( 'entity_not_found', __( 'Global class not found', 'elementor' ), [ 'status' => 404 ] );
197 121 }
198 122
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 - }
123 + return $class;
124 + }
205 125
206 - $order_result = $parser->parse_order(
207 - $request->get_param( 'order' ),
208 - $items_result->unwrap()
209 - );
126 + private function delete( \WP_REST_Request $request ) {
127 + $id = $request->get_param( 'id' );
128 + $class = $this->get_repository()->get( $id );
210 129
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();
130 + if ( null === $class ) {
131 + return new \WP_Error( 'entity_not_found', __( 'Global class not found', 'elementor' ), [ 'status' => 404 ] );
216 132 }
217 133
218 - $repository = $this->get_repository()
219 - ->context( $request->get_param( 'context' ) );
134 + $this->get_repository()->delete( $id );
220 135
221 - $changes_resolver = Global_Classes_Changes_Resolver::make(
222 - $repository,
223 - $changes,
224 - );
136 + return new \WP_REST_Response( null, 204 );
137 + }
225 138
226 - $duplicated_labels = Global_Classes_Parser::check_for_duplicate_labels(
227 - $existing_labels,
228 - $items_result->unwrap(),
229 - $new_added_items_ids
230 - );
139 + private function put( \WP_REST_Request $request ) {
140 + $id = $request->get_param( 'id' );
141 + $values = $request->get_params();
231 142
232 - $final_items = $items_result->unwrap();
233 - $duplicate_validation_result = null;
143 + // Ignore id to simplify the patch, and allow passing the entity as it is
144 + unset( $values['id'] );
234 145
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 - }
146 + $class = $this->get_repository()->get( $id );
147 +
148 + if ( null === $class ) {
149 + return new \WP_Error( 'entity_not_found', __( 'Global class not found', 'elementor' ), [ 'status' => 404 ] );
241 150 }
242 151
243 - $repository->put(
244 - $changes_resolver->resolve_items( $final_items ),
245 - $changes_resolver->resolve_order( $order_result->unwrap() ),
246 - );
152 + [$is_valid, $parsed, $errors] = Style_Parser::make( Style_Schema::get() )
153 + ->without_id()
154 + ->parse( $values );
247 155
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();
156 + if ( ! $is_valid ) {
157 + return $this->fail_with_validation_errors( $errors );
254 158 }
255 159
256 - return Response_Builder::make()->no_content()->build();
160 + $values = $this->get_repository()->put( $id, $parsed );
161 +
162 + return new \WP_REST_Response( $values, 200 );
257 163 }
258 164
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();
165 + private function create( \WP_REST_Request $request ) {
166 + $class = $request->get_params();
167 + [$is_valid, $parsed, $errors] = Style_Parser::make( Style_Schema::get() )
168 + ->without_id()
169 + ->parse( $class );
170 +
171 + if ( ! $is_valid ) {
172 + return $this->fail_with_validation_errors( $errors );
266 173 }
267 174
268 - return $response;
175 + $new = $this->get_repository()->create( $parsed );
176 +
177 + return new \WP_REST_Response( $new, 201 );
269 178 }
270 179
271 - private function handle_duplicates( array $duplicate_labels, array $existing_labels ) {
180 + private function arrange( \WP_REST_Request $request ) {
181 + $order = $request->get_params();
182 + $updated = $this->get_repository()->arrange( $order );
272 183
273 - $modified_labels = [];
184 + return new \WP_REST_Response( $updated, 200 );
185 + }
274 186
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 - ];
187 + private function route_wrapper( callable $cb ) {
188 + try {
189 + $response = $cb();
190 + } catch ( \Exception $e ) {
191 + return new \WP_Error( 'unexpected_error', __( 'Something went wrong', 'elementor' ), [ 'status' => 500 ] );
285 192 }
286 193
287 - return $modified_labels;
194 + return $response;
288 195 }
289 196
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;
197 + private function fail_with_validation_errors( array $errors ) {
198 + return new \WP_Error( 'Invalid data: ', join( ', ', $errors ), [ 'status' => 400 ] );
338 199 }
339 200 }