PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.2
Elementor Website Builder – more than just a page builder v4.2.2
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 4.0.7 All 451 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.2.2, at modules/global-classes/import-export-utils/import-utils.php

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