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 / global-classes / import-export-utils / import-utils.php

import-utils.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/global-classes/import-export-utils/import-utils.php

376 lines 11.0 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\ImportExportUtils;
4
5 use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
6 use Elementor\Core\Kits\Documents\Kit;
7 use Elementor\Modules\AtomicWidgets\Parsers\Style_Parser;
8 use Elementor\Modules\AtomicWidgets\PropTypeMigrations\Migrations_Orchestrator;
9 use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
10 use Elementor\Modules\DesignSystemSync\Classes\Global_Classes_Sync_Map;
11 use Elementor\Modules\GlobalClasses\Global_Class_Post;
12 use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
13 use Elementor\Modules\GlobalClasses\Global_Classes_REST_API;
14 use Elementor\Plugin;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 class Import_Utils {
21
22 const ORDER_FILE = 'order.json';
23 const DEFAULT_CONFLICT_RESOLUTION = 'skip';
24
25 const ERROR_NOT_ARRAY = 'not_array';
26 const ERROR_MISSING_FIELDS = 'missing_fields';
27 const ERROR_FILE_NOT_FOUND = 'file_not_found';
28 const ERROR_INVALID_JSON = 'invalid_json';
29 const ERROR_INVALID_PROPS = 'invalid_props';
30 const ERROR_ID_MISMATCH = 'id_mismatch';
31 const ERROR_LIMIT_REACHED = 'limit_reached';
32
33 const EMPTY_RESULT = [
34 'created' => [],
35 'renamed' => [],
36 'replaced' => [],
37 'skipped' => [],
38 'failed' => [],
39 ];
40
41 public static function import_classes( string $classes_dir, array $options = [] ) {
42 $order_file = rtrim( $classes_dir, '/' ) . '/' . self::ORDER_FILE;
43 $active_kit = Plugin::$instance->kits_manager->get_active_kit();
44
45 if ( ! $active_kit || ! is_dir( $classes_dir ) || ! file_exists( $order_file ) ) {
46 return self::EMPTY_RESULT;
47 }
48
49 $conflict_resolution = $options['conflict_resolution'] ?? self::DEFAULT_CONFLICT_RESOLUTION;
50
51 $imported_classes_order = json_decode( file_get_contents( $order_file ), true );
52
53 if ( ! is_array( $imported_classes_order ) ) {
54 throw new \Exception( 'Invalid file: order.json is not valid JSON.' );
55 }
56
57 if ( empty( $imported_classes_order ) ) {
58 return self::EMPTY_RESULT;
59 }
60
61 global $wpdb;
62 $wpdb->query( 'START TRANSACTION' );
63
64 try {
65 [ 'result' => $result, 'changes' => $changes ] = self::do_import( $active_kit, $classes_dir, $imported_classes_order, $conflict_resolution );
66 $wpdb->query( 'COMMIT' );
67 } catch ( \Throwable $e ) {
68 $wpdb->query( 'ROLLBACK' );
69 throw $e;
70 }
71
72 if ( $changes ) {
73 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
74 wp_cache_flush_runtime();
75 }
76 do_action( 'elementor/global_classes/update', Global_Classes_Repository::CONTEXT_FRONTEND, $changes );
77 }
78
79 return $result;
80 }
81
82 private static function do_import(
83 Kit $active_kit,
84 string $classes_dir,
85 array $imported_classes_order,
86 string $conflict_resolution
87 ): array {
88 $added_classes_order = [];
89 $added_classes_labels = [];
90 $modified_classes = [];
91 $deleted_classes = [];
92 $touched_sync_items = [];
93
94 $classes_repository_preview = Global_Classes_Repository::make( $active_kit )->set_preview( true );
95 $classes_repository_frontend = Global_Classes_Repository::make( $active_kit )->set_preview( false );
96
97 $previous_preview_order = $classes_repository_preview->get_order();
98 $previous_frontend_order = $classes_repository_frontend->get_order();
99 $order_set = array_flip( array_unique( array_merge( $previous_preview_order, $previous_frontend_order ) ) );
100 $style_parser = Style_Parser::make( Style_Schema::get() );
101 $classes_dir = rtrim( $classes_dir, '/' );
102
103 if ( 'override-all' === $conflict_resolution ) {
104 $deleted_classes = array_unique( array_merge( $previous_preview_order, $previous_frontend_order ) );
105 $classes_repository_preview->delete_all();
106 $classes_repository_frontend->delete_all();
107 $previous_preview_order = [];
108 $previous_frontend_order = [];
109 $order_set = [];
110 }
111
112 $all_labels = array_merge(
113 $classes_repository_frontend->all_labels(),
114 $classes_repository_preview->all_labels()
115 );
116
117 $label_to_id_map = self::build_label_to_id_map_from_labels( $all_labels );
118
119 $result = self::EMPTY_RESULT;
120
121 foreach ( $imported_classes_order as $import_entry ) {
122 [ 'is_valid' => $is_valid, 'error' => $validation_error ] = self::validate_class_entry( $import_entry );
123 if ( ! $is_valid ) {
124 $result['failed'][] = [
125 'import_entry' => $import_entry,
126 'error' => $validation_error,
127 ];
128 continue;
129 }
130
131 $action = self::resolve_item_action( $import_entry, $label_to_id_map, $conflict_resolution );
132
133 if ( 'skip' === $action ) {
134 $result['skipped'][] = [ 'import_entry' => $import_entry ];
135 continue;
136 }
137
138 if ( 'replace' !== $action && count( $order_set ) >= Global_Classes_REST_API::MAX_ITEMS ) {
139 $result['failed'][] = [
140 'import_entry' => $import_entry,
141 'error' => self::ERROR_LIMIT_REACHED,
142 ];
143 continue;
144 }
145
146 $class_file = $classes_dir . '/' . $import_entry['id'] . '.json';
147 if ( ! file_exists( $class_file ) ) {
148 $result['failed'][] = [
149 'import_entry' => $import_entry,
150 'error' => self::ERROR_FILE_NOT_FOUND,
151 ];
152 continue;
153 }
154
155 $raw_item = json_decode( file_get_contents( $class_file ), true );
156 if ( ! is_array( $raw_item ) ) {
157 $result['failed'][] = [
158 'import_entry' => $import_entry,
159 'error' => self::ERROR_INVALID_JSON,
160 ];
161 continue;
162 }
163
164 [ 'is_valid' => $is_valid, 'error' => $sanitize_error, 'sanitized' => $sanitized_item ] = self::sanitize_item( $import_entry['id'], $raw_item, $style_parser );
165 if ( ! $is_valid ) {
166 $result['failed'][] = [
167 'import_entry' => $import_entry,
168 'error' => $sanitize_error,
169 ];
170 continue;
171 }
172
173 if ( 'replace' === $action ) {
174 $existing_id = $label_to_id_map[ strtolower( $import_entry['label'] ) ];
175 self::replace_existing_class( $existing_id, $sanitized_item );
176
177 $touched_sync_items[ $existing_id ] = [ 'sync_to_v3' => ! empty( $sanitized_item['sync_to_v3'] ) ];
178 $modified_classes[] = $existing_id;
179 $result['replaced'][] = [
180 'import_entry' => $import_entry,
181 'result_entry' => [
182 'id' => $existing_id,
183 'label' => $import_entry['label'],
184 ],
185 ];
186 continue;
187 }
188
189 if ( 'rename' === $action ) {
190 $existing_labels = array_keys( $label_to_id_map );
191
192 $new_label = ImportExportUtils::resolve_label_conflict( $import_entry['label'], $existing_labels );
193 $sanitized_item['label'] = $new_label;
194 }
195
196 $new_id = $sanitized_item['id'];
197
198 if ( isset( $order_set[ $new_id ] ) ) {
199 $new_id = self::generate_unique_id( $order_set );
200 $sanitized_item['id'] = $new_id;
201 }
202
203 self::create_new_class( $sanitized_item );
204 $touched_sync_items[ $new_id ] = [ 'sync_to_v3' => ! empty( $sanitized_item['sync_to_v3'] ) ];
205 $order_set[ $new_id ] = true;
206 $added_classes_order[] = $new_id;
207 $added_classes_labels[ $new_id ] = $sanitized_item['label'];
208 $label_to_id_map[ strtolower( $sanitized_item['label'] ) ] = $new_id;
209
210 $result_entry = [
211 'id' => $new_id,
212 'label' => $sanitized_item['label'],
213 ];
214
215 if ( 'rename' === $action ) {
216 $result['renamed'][] = [
217 'import_entry' => $import_entry,
218 'result_entry' => $result_entry,
219 ];
220 } else {
221 $result['created'][] = [
222 'import_entry' => $import_entry,
223 'result_entry' => $result_entry,
224 ];
225 }
226 }
227
228 $changes = null;
229 $has_changes = ! empty( $added_classes_order ) || ! empty( $modified_classes ) || ! empty( $deleted_classes );
230
231 if ( $has_changes ) {
232 $new_preview_order = array_merge( $added_classes_order, $previous_preview_order );
233 $new_frontend_order = array_merge( $added_classes_order, $previous_frontend_order );
234
235 $classes_repository_preview->update_order_and_labels( $new_preview_order, $added_classes_labels );
236 $classes_repository_frontend->update_order_and_labels( $new_frontend_order, $added_classes_labels );
237
238 Global_Classes_Sync_Map::make( $active_kit )->apply_changes( $touched_sync_items, $deleted_classes );
239
240 $changes = [
241 'added' => $added_classes_order,
242 'deleted' => $deleted_classes,
243 'modified' => $modified_classes,
244 'order' => count( $added_classes_order ) > 0 || count( $deleted_classes ) > 0,
245 ];
246 }
247
248 return [
249 'result' => $result,
250 'changes' => $changes,
251 ];
252 }
253
254 private static function create_new_class( array $sanitized_item ): void {
255 $created = Global_Class_Post::create( $sanitized_item['id'], $sanitized_item['label'], $sanitized_item );
256
257 if ( $created ) {
258 clean_post_cache( $created->get_post_id() );
259 } else {
260 throw new \Exception( 'Failed to create new class: ' . esc_html( $sanitized_item['id'] ) . ' with label: ' . esc_html( $sanitized_item['label'] ) );
261 }
262 }
263
264 private static function replace_existing_class( string $existing_id, array $sanitized_item ): void {
265 $post = Global_Class_Post::find_by_class_id( $existing_id );
266
267 if ( ! $post ) {
268 throw new \Exception( 'Failed to find existing class: ' . esc_html( $existing_id ) );
269 }
270
271 $post->set_preview( false );
272 $post->update_data( $sanitized_item );
273 Migrations_Orchestrator::clear_entity_migration_cache( $post->get_post_id(), Global_Classes_Repository::META_KEY_FRONTEND );
274 clean_post_cache( $post->get_post_id() );
275 }
276
277 private static function validate_class_entry( $class_entry ): array {
278 if ( ! is_array( $class_entry ) ) {
279 return [
280 'is_valid' => false,
281 'error' => self::ERROR_NOT_ARRAY,
282 ];
283 }
284
285 $missing_fields = [];
286
287 foreach ( [ 'id', 'label' ] as $field ) {
288 if ( ! isset( $class_entry[ $field ] ) || ! is_string( $class_entry[ $field ] ) ) {
289 $missing_fields[] = $field;
290 }
291 }
292
293 if ( ! empty( $missing_fields ) ) {
294 return [
295 'is_valid' => false,
296 'error' => self::ERROR_MISSING_FIELDS . ':' . implode( ',', $missing_fields ),
297 ];
298 }
299
300 return [
301 'is_valid' => true,
302 'error' => null,
303 ];
304 }
305
306 private static function resolve_item_action(
307 array $class_entry,
308 array $existing_label_to_id,
309 string $conflict_resolution
310 ): string {
311 $label_lower = strtolower( $class_entry['label'] );
312 $has_conflict = isset( $existing_label_to_id[ $label_lower ] );
313
314 if ( ! $has_conflict ) {
315 return 'new';
316 }
317
318 switch ( $conflict_resolution ) {
319 case 'skip':
320 return 'skip';
321 case 'replace':
322 return 'replace';
323 case 'merge':
324 return 'rename';
325 default:
326 return self::DEFAULT_CONFLICT_RESOLUTION;
327 }
328 }
329
330 private static function sanitize_item( string $item_id, array $item, Style_Parser $style_parser ): array {
331 $item_result = $style_parser->parse( $item );
332
333 if ( ! $item_result->is_valid() ) {
334 return [
335 'is_valid' => false,
336 'error' => self::ERROR_INVALID_PROPS,
337 'sanitized' => null,
338 ];
339 }
340
341 $sanitized_item = $item_result->unwrap();
342
343 if ( $item_id !== $sanitized_item['id'] ) {
344 return [
345 'is_valid' => false,
346 'error' => self::ERROR_ID_MISMATCH,
347 'sanitized' => null,
348 ];
349 }
350
351 return [
352 'is_valid' => true,
353 'error' => null,
354 'sanitized' => $sanitized_item,
355 ];
356 }
357
358 private static function build_label_to_id_map_from_labels( array $id_to_label ): array {
359 $map = [];
360
361 foreach ( $id_to_label as $id => $label ) {
362 $map[ strtolower( $label ) ] = $id;
363 }
364
365 return $map;
366 }
367
368 private static function generate_unique_id( array $order_set ): string {
369 do {
370 $id = 'g-' . substr( bin2hex( random_bytes( 4 ) ), 0, 7 );
371 } while ( isset( $order_set[ $id ] ) );
372
373 return $id;
374 }
375 }
376