PluginProbe
Elementor Website Builder – more than just a page builder / 4.1.0-dev1
Elementor Website Builder – more than just a page builder v4.1.0-dev1
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.1.0-dev1, at modules/global-classes/import-export-utils/import-utils.php

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