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
elementor / modules / mcp / abilities / manage-default-styles-ability.php

manage-default-styles-ability.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/mcp/abilities/manage-default-styles-ability.php

325 lines 10.3 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\AtomicWidgets\CssConverter\Converter_Registry_Factory;
6 use Elementor\Modules\AtomicWidgets\CssConverter\Css_Converter;
7 use Elementor\Modules\AtomicWidgets\CssConverter\Expander_Registry_Factory;
8 use Elementor\Modules\AtomicWidgets\CssConverter\Metrics\Null_Failure_Reporter;
9 use Elementor\Modules\AtomicWidgets\CssConverter\Variable_Prop_Value_Transformer;
10 use Elementor\Modules\AtomicWidgets\Module as AtomicWidgetsModule;
11 use Elementor\Modules\DefaultStyles\Default_Styles_Repository;
12 use Elementor\Modules\Mcp\Abilities\Utils\Bulk_Operations_Result;
13 use Elementor\Modules\Mcp\Abilities\Utils\Style_Variants_Merger;
14 use Elementor\Modules\Variables\Module as Variables_Module;
15 use Elementor\Modules\Variables\Services\Batch_Operations\Batch_Processor;
16 use Elementor\Modules\Variables\Services\Variables_Service;
17 use Elementor\Modules\Variables\Storage\Variables_Repository;
18 use Elementor\Plugin;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 class Manage_Default_Styles_Ability extends Abstract_Ability {
25
26 const CLASS_TYPE = 'class';
27 const MAX_BATCH_SIZE = 20;
28
29 private ?Default_Styles_Repository $repository;
30 private ?Css_Converter $css_converter;
31
32 public function __construct( ?Default_Styles_Repository $repository = null, ?Css_Converter $css_converter = null ) {
33 $this->repository = $repository;
34 $this->css_converter = $css_converter;
35 }
36
37 protected function get_ability_id(): string {
38 return 'elementor/manage-default-styles';
39 }
40
41 protected function get_definition(): Ability_Definition {
42 return new Ability_Definition(
43 __( 'Manage Default Styles (Site-Wide)', 'elementor' ),
44 __( 'Bulk manage the active kit\'s site-wide default styles, keyed by HTML wrapper tag (h1..h6, p, a, section, div, ...). These styles apply to every V4 atomic element that renders that tag on the whole site, sitting on top of each widget\'s built-in base_styles and beneath any inline or global class overrides. Use action=update to upsert (patch or replace) a tag\'s variants via a raw CSS string (supports @media(--breakpoint) + &:hover/&:focus/&:active), and action=delete to remove a tag\'s default style entirely.', 'elementor' ),
45 'elementor',
46 [
47 'type' => 'object',
48 'required' => [ 'status', 'results' ],
49 'properties' => [
50 'status' => [ 'type' => 'string' ],
51 'results' => [ 'type' => 'array' ],
52 ],
53 ],
54 [
55 'annotations' => [
56 'readonly' => false,
57 'idempotent' => false,
58 'destructive' => true,
59 ],
60 ],
61 fn() => current_user_can( 'manage_options' ),
62 [
63 'type' => 'object',
64 'required' => [ 'operations' ],
65 'properties' => [
66 'operations' => [
67 'type' => 'array',
68 'description' => 'Bulk operations (1–20). Each item requires action and tag. update needs css (raw CSS string, same format as manage-classes) and applies site-wide to that HTML tag. Use mode to control merge behaviour on update (patch = upsert variants, replace = overwrite variants for the affected breakpoints). delete removes the tag\'s default style entirely.',
69 'items' => [
70 'type' => 'object',
71 'required' => [ 'action', 'tag' ],
72 'properties' => [
73 'action' => [
74 'type' => 'string',
75 'enum' => [ 'update', 'delete' ],
76 ],
77 'tag' => [
78 'type' => 'string',
79 'description' => 'HTML wrapper tag to target (e.g. h1, h2, p, a). Must be one of Elementor\'s allowed wrapper tags.',
80 ],
81 'css' => [
82 'type' => 'string',
83 'description' => 'Plain CSS string. Supports &:hover/&:focus/&:active nesting and @media(--breakpoint) blocks. In patch mode: "prop: null" removes that prop; "all: null" wipes the variant.',
84 ],
85 'mode' => [
86 'type' => 'string',
87 'enum' => [ 'patch', 'replace' ],
88 'default' => 'patch',
89 'description' => 'patch (default): upsert variants, preserving untouched ones; null/all:null deletions apply. replace: discard all variants for the affected breakpoints, then store new ones; null values have no effect.',
90 ],
91 ],
92 ],
93 ],
94 ],
95 ]
96 );
97 }
98
99 public function execute( $input = [] ) {
100 $input = is_array( $input ) ? $input : [];
101 $operations = $input['operations'] ?? null;
102
103 if ( ! is_array( $operations ) ) {
104 return $this->bad_request( __( 'operations array is required.', 'elementor' ) );
105 }
106
107 if ( empty( $operations ) ) {
108 return $this->bad_request( __( 'operations must not be empty.', 'elementor' ) );
109 }
110
111 if ( count( $operations ) > self::MAX_BATCH_SIZE ) {
112 return new \WP_Error(
113 'batch_size_exceeded',
114 sprintf(
115 /* translators: %d: maximum operations per request */
116 __( 'Maximum %d operations per request.', 'elementor' ),
117 self::MAX_BATCH_SIZE
118 ),
119 [
120 'status' => \WP_Http::BAD_REQUEST,
121 'max_allowed' => self::MAX_BATCH_SIZE,
122 ]
123 );
124 }
125
126 return $this->handle_bulk( $operations );
127 }
128
129 private function handle_bulk( array $operations ): array {
130 $results = new Bulk_Operations_Result();
131
132 foreach ( $operations as $index => $operation ) {
133 $this->process_operation( (int) $index, $operation, $results );
134 }
135
136 return $results->to_array();
137 }
138
139 private function process_operation( int $index, $operation, Bulk_Operations_Result $results ): void {
140 if ( ! is_array( $operation ) ) {
141 $results->add_error( $index, '', 'invalid_input', __( 'Invalid operation.', 'elementor' ) );
142 return;
143 }
144
145 $action = $operation['action'] ?? '';
146 $tag = isset( $operation['tag'] ) ? (string) $operation['tag'] : '';
147
148 if ( '' === $tag ) {
149 $results->add_error( $index, (string) $action, 'invalid_input', __( 'tag is required.', 'elementor' ) );
150 return;
151 }
152
153 if ( ! Default_Styles_Repository::is_allowed_tag( $tag ) ) {
154 $results->add_error( $index, (string) $action, 'invalid_tag', sprintf(
155 /* translators: %s: HTML tag */
156 __( 'Invalid HTML tag: %s.', 'elementor' ),
157 $tag
158 ) );
159 return;
160 }
161
162 switch ( $action ) {
163 case 'update':
164 $this->handle_update( $index, $tag, $operation, $results );
165 return;
166
167 case 'delete':
168 $this->handle_delete( $index, $tag, $results );
169 return;
170
171 default:
172 $results->add_error( $index, (string) $action, 'invalid_input', sprintf(
173 /* translators: %s: action name */
174 __( 'Unknown action: %s.', 'elementor' ),
175 $action
176 ) );
177 }
178 }
179
180 private function handle_update( int $index, string $tag, array $operation, Bulk_Operations_Result $results ): void {
181 $css_string = $operation['css'] ?? null;
182
183 if ( ! is_string( $css_string ) || '' === trim( $css_string ) ) {
184 $results->add_error( $index, 'update', 'invalid_input', __( 'update requires css.', 'elementor' ) );
185 return;
186 }
187
188 $mode = $operation['mode'] ?? 'patch';
189 if ( ! in_array( $mode, [ 'patch', 'replace' ], true ) ) {
190 $results->add_error( $index, 'update', 'invalid_input', sprintf( 'Unknown mode: %s. Valid modes: patch, replace.', $mode ) );
191 return;
192 }
193
194 $parsed = Style_Variants_Merger::parse_css_string(
195 $css_string,
196 $this->get_active_breakpoint_keys(),
197 $index,
198 'update',
199 $results,
200 fn() => $this->get_css_converter()
201 );
202
203 if ( null === $parsed ) {
204 return;
205 }
206
207 $new_variants = Style_Variants_Merger::build_variants( $parsed['breakpoint_blocks'], $this->get_css_converter() );
208
209 $existing = $this->get_repository()->get( $tag );
210 $existing_variants = is_array( $existing['variants'] ?? null ) ? $existing['variants'] : [];
211
212 $existing_after_removal = array_values(
213 array_filter(
214 $existing_variants,
215 fn( $v ) => ! in_array( $v['meta']['breakpoint'] ?? null, $parsed['removal_breakpoints'], true )
216 )
217 );
218
219 $merged = Style_Variants_Merger::apply_mode(
220 $existing_after_removal,
221 $new_variants,
222 $mode,
223 array_column( $parsed['breakpoint_blocks'], 'breakpoint' )
224 );
225
226 $merged = $this->strip_null_props( $merged );
227
228 $persisted = $this->get_repository()->put( $tag, [
229 'type' => self::CLASS_TYPE,
230 'variants' => $merged,
231 ] );
232
233 if ( ! $persisted ) {
234 $results->add_error( $index, 'update', 'persist_failed', __( 'Failed to save default style.', 'elementor' ) );
235 return;
236 }
237
238 $results->add_success( $index, 'update', [ 'tag' => $tag ] );
239 }
240
241 private function handle_delete( int $index, string $tag, Bulk_Operations_Result $results ): void {
242 $existing = $this->get_repository()->get( $tag );
243
244 if ( null === $existing ) {
245 $results->add_error( $index, 'delete', 'not_found', __( 'Default style not found for this tag.', 'elementor' ) );
246 return;
247 }
248
249 $this->get_repository()->delete( $tag );
250
251 $results->add_success( $index, 'delete', [ 'tag' => $tag ] );
252 }
253
254 private function strip_null_props( array $variants ): array {
255 foreach ( $variants as &$variant ) {
256 unset( $variant['null_props'] );
257 }
258
259 return $variants;
260 }
261
262 private function bad_request( string $message ): \WP_Error {
263 return new \WP_Error( 'invalid_input', $message, [ 'status' => \WP_Http::BAD_REQUEST ] );
264 }
265
266 protected function get_active_breakpoint_keys(): array {
267 return Plugin::$instance->breakpoints->get_active_devices_list();
268 }
269
270 private function get_repository(): Default_Styles_Repository {
271 if ( $this->repository ) {
272 return $this->repository;
273 }
274
275 $kit = Plugin::$instance->kits_manager->get_active_kit();
276
277 return Default_Styles_Repository::make( $kit );
278 }
279
280 private function get_css_converter(): Css_Converter {
281 if ( $this->css_converter ) {
282 return $this->css_converter;
283 }
284
285 $variables_service = $this->create_variables_service();
286
287 $variable_transformer = $variables_service
288 ? new Variable_Prop_Value_Transformer( $variables_service )
289 : null;
290
291 $this->css_converter = new Css_Converter(
292 Converter_Registry_Factory::create( $variables_service ),
293 new Null_Failure_Reporter(),
294 Expander_Registry_Factory::create( $variables_service ),
295 $variable_transformer
296 );
297
298 return $this->css_converter;
299 }
300
301 private function create_variables_service(): ?Variables_Service {
302 if ( ! $this->is_variables_active() ) {
303 return null;
304 }
305
306 $kit = Plugin::$instance->kits_manager->get_active_kit();
307
308 if ( ! $kit ) {
309 return null;
310 }
311
312 return new Variables_Service(
313 new Variables_Repository( $kit ),
314 new Batch_Processor()
315 );
316 }
317
318 private function is_variables_active(): bool {
319 $experiments = Plugin::$instance->experiments;
320
321 return $experiments->is_feature_active( Variables_Module::EXPERIMENT_NAME )
322 && $experiments->is_feature_active( AtomicWidgetsModule::EXPERIMENT_NAME );
323 }
324 }
325