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 / reorder-classes-ability.php

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

293 lines 7.7 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\GlobalClasses\Database\Migrations\Add_Capabilities;
6 use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
7 use Elementor\Plugin;
8 use WP_Http;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 class Reorder_Classes_Ability extends Abstract_Ability {
15
16 const MAX_MOVES = 50;
17
18 private ?Global_Classes_Repository $repository;
19
20 public function __construct( ?Global_Classes_Repository $repository = null ) {
21 $this->repository = $repository;
22 }
23
24 protected function get_ability_id(): string {
25 return 'elementor/reorder-classes';
26 }
27
28 protected function get_definition(): Ability_Definition {
29 return new Ability_Definition(
30 __( 'Reorder Global Classes', 'elementor' ),
31 __( 'Change the priority of V4 global CSS classes on the active kit. Classes earlier in the order override conflicting CSS declarations from classes later in the order.', 'elementor' ),
32 'elementor',
33 [
34 'type' => 'object',
35 'required' => [ 'changed', 'order' ],
36 'properties' => [
37 'changed' => [ 'type' => 'boolean' ],
38 'order' => [
39 'type' => 'array',
40 'items' => [ 'type' => 'string' ],
41 ],
42 'appended_ids' => [
43 'type' => 'array',
44 'items' => [ 'type' => 'string' ],
45 ],
46 'moves' => [
47 'type' => 'array',
48 'description' => 'Applied moves in order. Each from/to is relative to the array state when that move ran, not the original input order.',
49 'items' => [
50 'type' => 'object',
51 'properties' => [
52 'id' => [ 'type' => 'string' ],
53 'from' => [ 'type' => 'integer' ],
54 'to' => [ 'type' => 'integer' ],
55 ],
56 ],
57 ],
58 ],
59 ],
60 [
61 'annotations' => [
62 'readonly' => false,
63 'idempotent' => true,
64 'destructive' => false,
65 ],
66 ],
67 fn() => current_user_can( Add_Capabilities::UPDATE_CLASS ),
68 [
69 'type' => 'object',
70 'properties' => [
71 'moves' => [
72 'type' => 'array',
73 'description' => 'One to fifty sequential moves. Each move is applied to the result of the preceding move.',
74 'items' => [
75 'type' => 'object',
76 'required' => [ 'id', 'position' ],
77 'properties' => [
78 'id' => [
79 'type' => 'string',
80 'description' => 'Global class ID from the global-classes resource.',
81 ],
82 'position' => [
83 'type' => 'string',
84 'enum' => [ 'before', 'after', 'start', 'end' ],
85 ],
86 'ref' => [
87 'type' => 'string',
88 'description' => 'Reference class ID. Required when position is before or after.',
89 ],
90 ],
91 ],
92 ],
93 'order' => [
94 'type' => 'array',
95 'description' => 'Optional complete or partial priority order. Omitted existing class IDs are appended in their current relative order.',
96 'items' => [ 'type' => 'string' ],
97 ],
98 ],
99 ]
100 );
101 }
102
103 public function execute( $input = [] ) {
104 $input = is_array( $input ) ? $input : [];
105 $has_moves = array_key_exists( 'moves', $input );
106 $has_order = array_key_exists( 'order', $input );
107
108 if ( $has_moves === $has_order ) {
109 return $this->bad_request( __( 'Provide exactly one of moves or order.', 'elementor' ) );
110 }
111
112 $current_order = $this->get_repository()->get_order();
113
114 if ( $has_moves ) {
115 if ( ! is_array( $input['moves'] ) || empty( $input['moves'] ) ) {
116 return $this->bad_request( __( 'moves must be a non-empty array.', 'elementor' ) );
117 }
118
119 if ( count( $input['moves'] ) > self::MAX_MOVES ) {
120 return $this->bad_request( sprintf(
121 /* translators: %d: maximum moves per request */
122 __( 'Maximum %d moves per request.', 'elementor' ),
123 self::MAX_MOVES
124 ) );
125 }
126
127 $result = $this->apply_moves( $current_order, $input['moves'] );
128 if ( is_wp_error( $result ) ) {
129 return $result;
130 }
131
132 $new_order = $result['order'];
133 $moves = $result['moves'];
134 $appended_ids = [];
135 } else {
136 if ( ! is_array( $input['order'] ) ) {
137 return $this->bad_request( __( 'order must be an array.', 'elementor' ) );
138 }
139
140 $result = $this->build_order( $current_order, $input['order'] );
141 if ( is_wp_error( $result ) ) {
142 return $result;
143 }
144
145 $new_order = $result['order'];
146 $appended_ids = $result['appended_ids'];
147 $moves = [];
148 }
149
150 $changed = $new_order !== $current_order;
151 if ( $changed ) {
152 $this->get_repository()->apply_changes(
153 [],
154 [
155 'added' => [],
156 'deleted' => [],
157 'modified' => [],
158 'order' => true,
159 ],
160 $new_order
161 );
162 $this->clear_cache();
163 }
164
165 return [
166 'changed' => $changed,
167 'order' => $new_order,
168 'appended_ids' => $appended_ids,
169 'moves' => $moves,
170 ];
171 }
172
173 private function apply_moves( array $order, array $moves ) {
174 $applied_moves = [];
175
176 foreach ( $moves as $index => $move ) {
177 if ( ! is_array( $move ) ) {
178 return $this->bad_request( sprintf(
179 /* translators: %d: move index */
180 __( 'Move %d must be an object.', 'elementor' ),
181 $index
182 ) );
183 }
184
185 $id = $move['id'] ?? '';
186 $position = $move['position'] ?? '';
187 $ref = $move['ref'] ?? '';
188
189 if ( ! is_string( $id ) || ! in_array( $id, $order, true ) ) {
190 return $this->class_not_found( is_string( $id ) ? $id : '' );
191 }
192
193 if ( ! in_array( $position, [ 'before', 'after', 'start', 'end' ], true ) ) {
194 return $this->bad_request( __( 'position must be before, after, start, or end.', 'elementor' ) );
195 }
196
197 if ( in_array( $position, [ 'before', 'after' ], true ) ) {
198 if ( ! is_string( $ref ) || ! in_array( $ref, $order, true ) ) {
199 return $this->class_not_found( is_string( $ref ) ? $ref : '' );
200 }
201
202 if ( $id === $ref ) {
203 return $this->bad_request( __( 'A class cannot be moved relative to itself.', 'elementor' ) );
204 }
205 }
206
207 $from = array_search( $id, $order, true );
208 $order = array_values( array_filter( $order, fn( $item ) => $item !== $id ) );
209
210 if ( 'start' === $position ) {
211 $to = 0;
212 } elseif ( 'end' === $position ) {
213 $to = count( $order );
214 } else {
215 $to = array_search( $ref, $order, true );
216 if ( 'after' === $position ) {
217 ++$to;
218 }
219 }
220
221 array_splice( $order, $to, 0, [ $id ] );
222 $applied_moves[] = [
223 'id' => $id,
224 'from' => $from,
225 'to' => $to,
226 ];
227 }
228
229 return [
230 'order' => $order,
231 'moves' => $applied_moves,
232 ];
233 }
234
235 private function build_order( array $current_order, array $requested_order ) {
236 $known_ids = array_flip( $current_order );
237 $order = [];
238
239 foreach ( $requested_order as $id ) {
240 if ( ! is_string( $id ) || ! isset( $known_ids[ $id ] ) ) {
241 return $this->class_not_found( is_string( $id ) ? $id : '' );
242 }
243
244 if ( in_array( $id, $order, true ) ) {
245 return $this->bad_request( __( 'order must not contain duplicate class IDs.', 'elementor' ) );
246 }
247
248 $order[] = $id;
249 }
250
251 $appended_ids = array_values( array_diff( $current_order, $order ) );
252
253 return [
254 'order' => array_merge( $order, $appended_ids ),
255 'appended_ids' => $appended_ids,
256 ];
257 }
258
259 private function get_repository(): Global_Classes_Repository {
260 if ( $this->repository ) {
261 return $this->repository;
262 }
263
264 $kit = Plugin::$instance->kits_manager->get_active_kit();
265
266 return Global_Classes_Repository::make( $kit );
267 }
268
269 private function clear_cache(): void {
270 if ( ! class_exists( Plugin::class ) || ! isset( Plugin::$instance ) ) {
271 return;
272 }
273
274 Plugin::$instance->files_manager->clear_cache();
275 }
276
277 private function bad_request( string $message ): \WP_Error {
278 return new \WP_Error( 'invalid_input', $message, [ 'status' => WP_Http::BAD_REQUEST ] );
279 }
280
281 private function class_not_found( string $id ): \WP_Error {
282 return new \WP_Error(
283 'class_not_found',
284 sprintf(
285 /* translators: %s: global class ID */
286 __( 'Global class not found: %s.', 'elementor' ),
287 $id
288 ),
289 [ 'status' => WP_Http::BAD_REQUEST ]
290 );
291 }
292 }
293