PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.5
Elementor Website Builder – more than just a page builder v4.3.5
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 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 All 458 releases
elementor / modules / mcp / abilities / manage-variable-ability.php

manage-variable-ability.php in Elementor Website Builder – more than just a page builder 4.3.5, at modules/mcp/abilities/manage-variable-ability.php

433 lines 12.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\Mcp\Abilities;
4
5 use Elementor\Modules\Mcp\Abilities\Utils\Bulk_Operations_Result;
6 use Elementor\Modules\Mcp\Abilities\Utils\Tool_Performance_Metrics;
7 use Elementor\Modules\Mcp\Events\Mcp_Event_Dispatcher;
8 use Elementor\Modules\Variables\Services\Batch_Operations\Batch_Processor;
9 use Elementor\Modules\Variables\Services\Variables_Service;
10 use Elementor\Modules\Variables\Storage\Exceptions\FatalError;
11 use Elementor\Modules\Variables\Storage\Variables_Repository;
12 use Elementor\Plugin;
13 use Elementor\Utils;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 class Manage_Variable_Ability extends Abstract_Ability {
20
21 const TYPE_COLOR = 'global-color-variable';
22 const TYPE_FONT = 'global-font-variable';
23 const TYPE_SIZE = 'global-size-variable';
24 const TYPE_CUSTOM_SIZE = 'global-custom-size-variable';
25 const MAX_BATCH_SIZE = 50;
26
27 private ?Variables_Service $service;
28
29 public function __construct( ?Variables_Service $service = null ) {
30 $this->service = $service;
31 }
32
33 protected function get_ability_id(): string {
34 return 'elementor/manage-global-variable';
35 }
36
37 protected function get_definition(): Ability_Definition {
38 return new Ability_Definition(
39 __( 'Manage Global Variable', 'elementor' ),
40 __( 'Manage V4 global variables (color, font, size, custom-size). Bulk create, update, or delete on the active kit (up to 50 operations).', 'elementor' ),
41 'elementor',
42 [
43 'type' => 'object',
44 'required' => [ 'status', 'results' ],
45 'properties' => [
46 'status' => [ 'type' => 'string' ],
47 'results' => [ 'type' => 'array' ],
48 'watermark' => [ 'type' => 'integer' ],
49 ],
50 ],
51 [
52 'annotations' => [
53 'readonly' => false,
54 'idempotent' => false,
55 'destructive' => false,
56 ],
57 ],
58 fn() => current_user_can( 'manage_options' ),
59 [
60 'type' => 'object',
61 'required' => [ 'operations' ],
62 'properties' => [
63 'operations' => [
64 'type' => 'array',
65 'description' => 'Bulk operations (1–50). Each item requires action; create needs type/label/value, update needs id/label/value, delete needs id.',
66 'items' => [
67 'type' => 'object',
68 'required' => [ 'action' ],
69 'properties' => [
70 'action' => [
71 'type' => 'string',
72 'enum' => [ 'create', 'update', 'delete' ],
73 ],
74 'id' => [ 'type' => 'string' ],
75 'type' => [
76 'type' => 'string',
77 'enum' => $this->get_allowed_types(),
78 ],
79 'label' => [ 'type' => 'string' ],
80 'value' => [ 'type' => 'string' ],
81 ],
82 ],
83 ],
84 ],
85 ]
86 );
87 }
88
89 public function execute( $input = [] ) {
90 $started_at = hrtime( true );
91 $input = is_array( $input ) ? $input : [];
92 $operations = $input['operations'] ?? null;
93
94 if ( ! is_array( $operations ) ) {
95 $error = $this->bad_request( __( 'operations array is required.', 'elementor' ) );
96 $this->emit_mcp_manage_global_variable_executed( $started_at, [], $error );
97 return $error;
98 }
99
100 if ( empty( $operations ) ) {
101 $error = $this->bad_request( __( 'operations must not be empty.', 'elementor' ) );
102 $this->emit_mcp_manage_global_variable_executed( $started_at, [], $error );
103 return $error;
104 }
105
106 if ( count( $operations ) > self::MAX_BATCH_SIZE ) {
107 $error = new \WP_Error(
108 'batch_size_exceeded',
109 sprintf(
110 /* translators: %d: maximum operations per request */
111 __( 'Maximum %d operations per request.', 'elementor' ),
112 self::MAX_BATCH_SIZE
113 ),
114 [
115 'status' => \WP_Http::BAD_REQUEST,
116 'max_allowed' => self::MAX_BATCH_SIZE,
117 ]
118 );
119 $this->emit_mcp_manage_global_variable_executed( $started_at, $operations, $error );
120 return $error;
121 }
122
123 $response = $this->handle_bulk( $operations );
124 if ( is_wp_error( $response ) ) {
125 $this->emit_mcp_manage_global_variable_executed( $started_at, $operations, $response );
126 } else {
127 $this->emit_mcp_manage_global_variable_executed( $started_at, $operations, null, $response );
128 }
129 return $response;
130 }
131
132 private function handle_bulk( array $operations ) {
133 $results = new Bulk_Operations_Result();
134 $batch_operations = [];
135 $index_map = [];
136
137 foreach ( $operations as $index => $operation ) {
138 if ( ! is_array( $operation ) ) {
139 $results->add_error( $index, '', 'invalid_input', __( 'Invalid operation.', 'elementor' ) );
140 continue;
141 }
142
143 $translated = $this->translate_operation( $operation );
144
145 if ( is_wp_error( $translated ) ) {
146 $results->add_error(
147 $index,
148 $operation['action'] ?? '',
149 $translated->get_error_code(),
150 $translated->get_error_message()
151 );
152 continue;
153 }
154
155 $index_map[ count( $batch_operations ) ] = $index;
156 $batch_operations[] = $translated;
157 }
158
159 $watermark = null;
160
161 if ( ! empty( $batch_operations ) ) {
162 try {
163 $batch_result = $this->get_service()->process_batch( $batch_operations, true );
164 $watermark = $batch_result['watermark'] ?? null;
165
166 $this->merge_batch_results( $batch_result['results'], $index_map, $results );
167 $this->emit_variable_events( $batch_result['results'] );
168 } catch ( FatalError $e ) {
169 return new \WP_Error(
170 'unexpected_server_error',
171 __( 'Unexpected server error', 'elementor' ),
172 [ 'status' => \WP_Http::INTERNAL_SERVER_ERROR ]
173 );
174 }
175
176 $this->clear_cache();
177 }
178
179 $response = $results->to_array();
180
181 if ( null !== $watermark ) {
182 $response['watermark'] = $watermark;
183 }
184
185 return $response;
186 }
187
188 private function translate_operation( array $operation ) {
189 $action = $operation['action'] ?? '';
190
191 switch ( $action ) {
192 case 'create':
193 $type = $operation['type'] ?? '';
194 $label = $operation['label'] ?? '';
195 $value = $operation['value'] ?? '';
196
197 if ( '' === $type || '' === $label || '' === $value ) {
198 return $this->bad_request( __( 'Create requires type, label, and value.', 'elementor' ) );
199 }
200
201 if ( $this->is_size_type( $type ) && ! $this->can_manage_size_variables() ) {
202 return $this->pro_license_required();
203 }
204
205 return [
206 'type' => 'create',
207 'variable' => [
208 'type' => $type,
209 'label' => $label,
210 'value' => $value,
211 ],
212 ];
213
214 case 'update':
215 $id = $operation['id'] ?? '';
216 $label = $operation['label'] ?? '';
217 $value = $operation['value'] ?? '';
218
219 if ( '' === $id || '' === $label || '' === $value ) {
220 return $this->bad_request( __( 'Update requires id, label, and value.', 'elementor' ) );
221 }
222
223 if ( ! $this->can_manage_size_variables() && $this->is_size_variable( $id ) ) {
224 return $this->pro_license_required();
225 }
226
227 return [
228 'type' => 'update',
229 'id' => $id,
230 'variable' => [
231 'label' => $label,
232 'value' => $value,
233 ],
234 ];
235
236 case 'delete':
237 $id = $operation['id'] ?? '';
238
239 if ( '' === $id ) {
240 return $this->bad_request( __( 'Delete requires id.', 'elementor' ) );
241 }
242
243 return [
244 'type' => 'delete',
245 'id' => $id,
246 ];
247
248 default:
249 return $this->bad_request( sprintf(
250 /* translators: %s: action name */
251 __( 'Unknown action: %s.', 'elementor' ),
252 $action
253 ) );
254 }
255 }
256
257 private function merge_batch_results( array $batch_results, array $index_map, Bulk_Operations_Result $results ): void {
258 foreach ( $batch_results as $batch_index => $result ) {
259 $original_index = $index_map[ $batch_index ];
260
261 if ( 'ok' === ( $result['status'] ?? '' ) ) {
262 $extra = array_diff_key( $result, array_flip( [ 'index', 'status' ] ) );
263 $results->add_success( $original_index, $result['action'] ?? '', $extra );
264 } else {
265 $results->add_error(
266 $original_index,
267 $result['action'] ?? '',
268 $result['code'] ?? 'unknown_error',
269 $result['message'] ?? ''
270 );
271 }
272 }
273 }
274
275 private function bad_request( string $message ): \WP_Error {
276 return new \WP_Error( 'invalid_input', $message, [ 'status' => \WP_Http::BAD_REQUEST ] );
277 }
278
279 private function pro_license_required(): \WP_Error {
280 return new \WP_Error(
281 'pro_license_required',
282 __( 'Size variables require an active Elementor Pro license, so this operation was not applied. Do not retry it — tell the user that size variables are a Pro feature.', 'elementor' ),
283 [ 'status' => \WP_Http::FORBIDDEN ]
284 );
285 }
286
287 private function is_size_type( $type ): bool {
288 return in_array( $type, [ self::TYPE_SIZE, self::TYPE_CUSTOM_SIZE ], true );
289 }
290
291 private function is_size_variable( $id ): bool {
292 return is_string( $id ) && $this->is_size_type( $this->get_service()->find_type( $id ) );
293 }
294
295 private function can_manage_size_variables(): bool {
296 return Utils::is_license_active();
297 }
298
299 private function get_allowed_types(): array {
300 $types = [ self::TYPE_COLOR, self::TYPE_FONT ];
301
302 if ( $this->can_manage_size_variables() ) {
303 $types[] = self::TYPE_SIZE;
304 $types[] = self::TYPE_CUSTOM_SIZE;
305 }
306
307 return $types;
308 }
309
310 private function clear_cache(): void {
311 if ( ! class_exists( Plugin::class ) || ! isset( Plugin::$instance ) ) {
312 return;
313 }
314
315 Plugin::$instance->files_manager->clear_cache();
316 $this->flush_runtime_cache();
317 }
318
319 private function flush_runtime_cache(): void {
320 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
321 wp_cache_flush_runtime();
322 }
323 }
324
325 private function get_service(): Variables_Service {
326 if ( $this->service ) {
327 return $this->service;
328 }
329
330 $kit = Plugin::$instance->kits_manager->get_active_kit();
331
332 return new Variables_Service(
333 new Variables_Repository( $kit ),
334 new Batch_Processor()
335 );
336 }
337
338 private function emit_variable_events( array $batch_results ): void {
339 $event_by_action = [
340 'create' => 'variable_created',
341 'update' => 'variable_updated',
342 ];
343
344 $var_type_labels = [
345 self::TYPE_COLOR => 'color',
346 self::TYPE_FONT => 'font',
347 self::TYPE_SIZE => 'size',
348 self::TYPE_CUSTOM_SIZE => 'size',
349 ];
350
351 foreach ( $batch_results as $result ) {
352 if ( 'ok' !== ( $result['status'] ?? '' ) ) {
353 continue;
354 }
355
356 $action = $result['action'] ?? '';
357
358 if ( ! isset( $event_by_action[ $action ] ) ) {
359 continue;
360 }
361
362 $raw_type = $result['type'] ?? '';
363 $var_type = $var_type_labels[ $raw_type ] ?? $raw_type;
364
365 Mcp_Event_Dispatcher::emit( $event_by_action[ $action ], [
366 'id' => $result['id'] ?? '',
367 'name' => $result['label'] ?? '',
368 'var_type' => $var_type,
369 ] );
370 }
371 }
372
373 private function emit_mcp_manage_global_variable_executed(
374 int $started_at,
375 array $operations,
376 ?\WP_Error $top_level_error = null,
377 array $response = []
378 ): void {
379 $duration_ms = Tool_Performance_Metrics::duration_ms_since( $started_at );
380
381 [ 'status' => $status, 'error_code' => $error_code ] = Tool_Performance_Metrics::resolve_status( $response, $top_level_error );
382
383 $failed_results = array_filter( $response['results'] ?? [], fn( $r ) => 'error' === ( $r['status'] ?? '' ) );
384 $failed_count = count( $failed_results );
385 $failed_codes = array_values( array_unique( array_column( $failed_results, 'code' ) ) );
386
387 $ops_count = count( $operations );
388 $by_action = [];
389 $variable_types = [];
390
391 foreach ( $operations as $op ) {
392 $action = $op['action'] ?? '';
393 if ( is_string( $action ) && '' !== $action ) {
394 $by_action[ $action ] = ( $by_action[ $action ] ?? 0 ) + 1;
395 }
396 }
397
398 $ok_results = array_filter( $response['results'] ?? [], fn( $r ) => 'ok' === ( $r['status'] ?? '' ) );
399 foreach ( $ok_results as $row ) {
400 $type = $row['type'] ?? '';
401 if ( is_string( $type ) && '' !== $type ) {
402 $variable_types[ $type ] = ( $variable_types[ $type ] ?? 0 ) + 1;
403 }
404 }
405
406 $dominant_action = '';
407 if ( ! empty( $by_action ) ) {
408 arsort( $by_action );
409 $dominant_action = (string) array_key_first( $by_action );
410 }
411
412 $payload = [
413 'tool_name' => $this->get_ability_id(),
414 'status' => $status,
415 'duration_ms' => $duration_ms,
416 'action' => $dominant_action,
417 'operations_count' => $ops_count,
418 'operations_by_type' => $by_action,
419 'variable_types' => $variable_types,
420 'failed_operations_count' => $failed_count,
421 'failed_operation_codes' => $failed_codes,
422 'warning_count' => 0,
423 'warning_types' => [],
424 ];
425
426 if ( null !== $error_code ) {
427 $payload['error_code'] = $error_code;
428 }
429
430 Mcp_Event_Dispatcher::emit( 'mcp_manage_global_variable_executed', $payload );
431 }
432 }
433