PluginProbe
Code Snippets / 3.9.6
Code Snippets v3.9.6
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / migration / importers / files / file-upload-importer.php

file-upload-importer.php in Code Snippets 3.9.6, at php/migration/importers/files/file-upload-importer.php

407 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets;
4
5 use WP_REST_Server;
6 use WP_REST_Request;
7 use WP_Error;
8 use const Code_Snippets\REST_API_NAMESPACE;
9
10 class Files_Import_Manager {
11
12 const VERSION = 1;
13
14 public function __construct() {
15 add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
16 }
17
18 public function register_rest_routes() {
19 $namespace = REST_API_NAMESPACE . self::VERSION;
20
21 register_rest_route( $namespace, 'file-upload/parse', [
22 'methods' => WP_REST_Server::CREATABLE,
23 'callback' => [ $this, 'parse_uploaded_files' ],
24 'permission_callback' => function() {
25 return current_user_can( 'manage_options' );
26 },
27 ] );
28
29 register_rest_route( $namespace, 'file-upload/import', [
30 'methods' => WP_REST_Server::CREATABLE,
31 'callback' => [ $this, 'import_selected_snippets' ],
32 'permission_callback' => function() {
33 return current_user_can( 'manage_options' );
34 },
35 'args' => [
36 'snippets' => [
37 'description' => __( 'Array of snippet data to import', 'code-snippets' ),
38 'type' => 'array',
39 'required' => true,
40 ],
41 'duplicate_action' => [
42 'description' => __( 'Action to take when duplicate snippets are found', 'code-snippets' ),
43 'type' => 'string',
44 'enum' => [ 'ignore', 'replace', 'skip' ],
45 'default' => 'ignore',
46 ],
47 'network' => [
48 'description' => __( 'Whether to import to network table', 'code-snippets' ),
49 'type' => 'boolean',
50 'default' => false,
51 ],
52 ],
53 ] );
54 }
55
56 public function parse_uploaded_files( WP_REST_Request $request ) {
57 // Verify nonce for security
58 $nonce = $request->get_header( 'X-WP-Nonce' );
59 if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
60 return new WP_Error(
61 'rest_cookie_invalid_nonce',
62 __( 'Cookie check failed', 'code-snippets' ),
63 [ 'status' => 403 ]
64 );
65 }
66
67 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above via REST API header
68 if ( empty( $_FILES['files'] ) ) {
69 return new WP_Error(
70 'no_files',
71 __( 'No files were uploaded.', 'code-snippets' ),
72 [ 'status' => 400 ]
73 );
74 }
75
76 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above, file data validated below
77 $files = $_FILES['files'];
78
79 if ( ! isset( $files['name'], $files['type'], $files['tmp_name'], $files['error'] ) ) {
80 return new WP_Error(
81 'invalid_file_data',
82 __( 'Invalid file upload data.', 'code-snippets' ),
83 [ 'status' => 400 ]
84 );
85 }
86
87 $all_snippets = [];
88 $errors = [];
89
90 $file_count = is_array( $files['name'] ) ? count( $files['name'] ) : 1;
91
92 for ( $i = 0; $i < $file_count; $i++ ) {
93 $file_name = is_array( $files['name'] ) ? $files['name'][ $i ] : $files['name'];
94 $file_type = is_array( $files['type'] ) ? $files['type'][ $i ] : $files['type'];
95 $file_tmp = is_array( $files['tmp_name'] ) ? $files['tmp_name'][ $i ] : $files['tmp_name'];
96 $file_error = is_array( $files['error'] ) ? $files['error'][ $i ] : $files['error'];
97
98 if ( UPLOAD_ERR_OK !== $file_error ) {
99 $errors[] = sprintf(
100 /* translators: %1$s: file name, %2$s: error message */
101 __( 'Upload error for file %1$s: %2$s', 'code-snippets' ),
102 $file_name,
103 $this->get_upload_error_message( $file_error )
104 );
105 continue;
106 }
107
108 $file_info = pathinfo( $file_name );
109 $extension = strtolower( $file_info['extension'] ?? '' );
110 $mime_type = sanitize_mime_type( $file_type );
111
112 if ( ! $this->is_valid_file_type( $extension, $mime_type ) ) {
113 $errors[] = sprintf(
114 /* translators: %s: file name */
115 __( 'Invalid file type for %s. Only JSON and XML files are allowed.', 'code-snippets' ),
116 $file_name,
117 );
118 continue;
119 }
120
121 $snippets = $this->parse_file_content( $file_tmp, $extension, $mime_type, $file_name );
122
123 if ( is_wp_error( $snippets ) ) {
124 $errors[] = sprintf(
125 /* translators: %1$s: file name, %2$s: error message */
126 __( 'Error parsing %1$s: %2$s', 'code-snippets' ),
127 $file_name,
128 $snippets->get_error_message(),
129 );
130 } else {
131 $all_snippets = array_merge( $all_snippets, $snippets );
132 }
133 }
134
135 if ( empty( $all_snippets ) ) {
136 return new WP_Error(
137 'no_snippets_found',
138 __( 'No valid snippets found in the uploaded files.', 'code-snippets' ),
139 [
140 'status' => 400,
141 'errors' => $errors,
142 ],
143 );
144 }
145
146 $response = [
147 'snippets' => $all_snippets,
148 'total_count' => count( $all_snippets ),
149 'message' => sprintf(
150 /* translators: %d: number of snippets */
151 _n(
152 'Found %d snippet ready for import.',
153 'Found %d snippets ready for import.',
154 count( $all_snippets ),
155 'code-snippets',
156 ),
157 count( $all_snippets )
158 ),
159 ];
160
161 if ( ! empty( $errors ) ) {
162 $response['warnings'] = $errors;
163 }
164
165 return rest_ensure_response( $response );
166 }
167
168 public function import_selected_snippets( WP_REST_Request $request ) {
169 $snippets_data = $request->get_param( 'snippets' );
170 $duplicate_action = $request->get_param( 'duplicate_action' ) ?? 'ignore';
171 $network = $request->get_param( 'network' ) ?? false;
172
173 if ( empty( $snippets_data ) || ! is_array( $snippets_data ) ) {
174 return new WP_Error(
175 'no_snippets',
176 __( 'No snippet data provided for import.', 'code-snippets' ),
177 [ 'status' => 400 ]
178 );
179 }
180
181 $snippets = [];
182 foreach ( $snippets_data as $snippet_data ) {
183 $snippet = new Snippet();
184 $snippet->network = $network;
185
186 $import_fields = [
187 'name',
188 'desc',
189 'description',
190 'code',
191 'tags',
192 'scope',
193 'priority',
194 'shared_network',
195 'modified',
196 'cloud_id',
197 ];
198
199 foreach ( $import_fields as $field ) {
200 if ( isset( $snippet_data[ $field ] ) ) {
201 $snippet->set_field( $field, $snippet_data[ $field ] );
202 }
203 }
204
205 $snippets[] = $snippet;
206 }
207
208 $imported = $this->save_snippets( $snippets, $duplicate_action, $network );
209
210 $response = [
211 'imported' => count( $imported ),
212 'imported_ids' => $imported,
213 'message' => sprintf(
214 /* translators: %d: number of snippets */
215 _n(
216 'Successfully imported %d snippet.',
217 'Successfully imported %d snippets.',
218 count( $imported ),
219 'code-snippets',
220 ),
221 count( $imported )
222 ),
223 ];
224
225 return rest_ensure_response( $response );
226 }
227
228 private function parse_file_content( string $file_path, string $extension, string $mime_type, string $file_name ) {
229 if ( ! file_exists( $file_path ) || ! is_file( $file_path ) ) {
230 return new WP_Error(
231 'file_not_found',
232 __( 'File not found or is not a valid file.', 'code-snippets' )
233 );
234 }
235
236 if ( 'json' === $extension || 'application/json' === $mime_type ) {
237 return $this->parse_json_file( $file_path, $file_name );
238 } elseif ( 'xml' === $extension || in_array( $mime_type, [ 'text/xml', 'application/xml' ], true ) ) {
239 return $this->parse_xml_file( $file_path, $file_name );
240 }
241
242 return new WP_Error(
243 'unsupported_file_type',
244 __( 'Unsupported file type.', 'code-snippets' )
245 );
246 }
247
248 private function parse_json_file( string $file_path, string $file_name ) {
249 $raw_data = file_get_contents( $file_path );
250 $data = json_decode( $raw_data, true );
251
252 if ( json_last_error() !== JSON_ERROR_NONE ) {
253 return new WP_Error(
254 'invalid_json',
255 sprintf(
256 /* translators: %1$s: file name, %2$s: error message */
257 __( 'Invalid JSON in file %1$s: %2$s', 'code-snippets' ),
258 $file_name,
259 json_last_error_msg()
260 )
261 );
262 }
263
264 if ( ! isset( $data['snippets'] ) || ! is_array( $data['snippets'] ) ) {
265 return new WP_Error(
266 'no_snippets_in_file',
267 sprintf(
268 /* translators: %s: file name */
269 __( 'No snippets found in file %s', 'code-snippets' ),
270 $file_name
271 )
272 );
273 }
274
275 $snippets = [];
276 foreach ( $data['snippets'] as $snippet_data ) {
277 $snippet_data['source_file'] = $file_name;
278
279 $snippet_data['table_data'] = [
280 'id' => $snippet_data['id'] ?? uniqid(),
281 'title' => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
282 'scope' => $snippet_data['scope'] ?? 'global',
283 'tags' => is_array( $snippet_data['tags'] ?? null ) ? implode( ', ', $snippet_data['tags'] ) : '',
284 'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
285 'type' => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' )
286 ];
287
288 $snippets[] = $snippet_data;
289 }
290
291 return $snippets;
292 }
293
294 private function parse_xml_file( string $file_path, string $file_name ) {
295 $dom = new \DOMDocument( '1.0', get_bloginfo( 'charset' ) );
296
297 if ( ! $dom->load( $file_path ) ) {
298 return new WP_Error(
299 'invalid_xml',
300 sprintf(
301 /* translators: %s: file name */
302 __( 'Invalid XML in file %s', 'code-snippets' ),
303 $file_name
304 )
305 );
306 }
307
308 $snippets_xml = $dom->getElementsByTagName( 'snippet' );
309 $fields = [ 'name', 'description', 'desc', 'code', 'tags', 'scope' ];
310
311 $snippets = [];
312 $index = 0;
313
314 foreach ( $snippets_xml as $snippet_xml ) {
315 $snippet_data = [];
316
317 foreach ( $fields as $field_name ) {
318 $field = $snippet_xml->getElementsByTagName( $field_name )->item( 0 );
319
320 if ( isset( $field->nodeValue ) ) {
321 $snippet_data[ $field_name ] = $field->nodeValue;
322 }
323 }
324
325 $scope = $snippet_xml->getAttribute( 'scope' );
326 if ( ! empty( $scope ) ) {
327 $snippet_data['scope'] = $scope;
328 }
329
330 $snippet_data['source_file'] = $file_name;
331
332 $snippet_data['table_data'] = [
333 'id' => ++$index,
334 'title' => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
335 'scope' => $snippet_data['scope'] ?? 'global',
336 'tags' => $snippet_data['tags'] ?? '',
337 'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
338 'type' => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' ),
339 ];
340
341 $snippets[] = $snippet_data;
342 }
343
344 return $snippets;
345 }
346
347 private function save_snippets( array $snippets, string $duplicate_action, bool $network ): array {
348 $existing_snippets = [];
349
350 if ( 'replace' === $duplicate_action || 'skip' === $duplicate_action ) {
351 $all_snippets = get_snippets( [], $network );
352
353 foreach ( $all_snippets as $snippet ) {
354 if ( $snippet->name ) {
355 $existing_snippets[ $snippet->name ] = $snippet->id;
356 }
357 }
358 }
359
360 $imported = [];
361
362 foreach ( $snippets as $snippet ) {
363 if ( 'ignore' !== $duplicate_action && isset( $existing_snippets[ $snippet->name ] ) ) {
364 if ( 'replace' === $duplicate_action ) {
365 $snippet->id = $existing_snippets[ $snippet->name ];
366 } elseif ( 'skip' === $duplicate_action ) {
367 continue;
368 }
369 }
370
371 $saved_snippet = save_snippet( $snippet );
372
373 $snippet_id = $saved_snippet->id;
374
375 if ( $snippet_id ) {
376 $imported[] = $snippet_id;
377 }
378 }
379
380 return $imported;
381 }
382
383 private function is_valid_file_type( string $extension, string $mime_type ): bool {
384 $valid_extensions = [ 'json', 'xml' ];
385 $valid_mime_types = [ 'application/json', 'text/xml', 'application/xml' ];
386
387 return in_array( $extension, $valid_extensions, true ) ||
388 in_array( $mime_type, $valid_mime_types, true );
389 }
390
391
392
393 private function get_upload_error_message( int $error_code ): string {
394 $error_messages = [
395 UPLOAD_ERR_INI_SIZE => __( 'File exceeds the upload_max_filesize directive.', 'code-snippets' ),
396 UPLOAD_ERR_FORM_SIZE => __( 'File exceeds the MAX_FILE_SIZE directive.', 'code-snippets' ),
397 UPLOAD_ERR_PARTIAL => __( 'File was only partially uploaded.', 'code-snippets' ),
398 UPLOAD_ERR_NO_FILE => __( 'No file was uploaded.', 'code-snippets' ),
399 UPLOAD_ERR_NO_TMP_DIR => __( 'Missing a temporary folder.', 'code-snippets' ),
400 UPLOAD_ERR_CANT_WRITE => __( 'Failed to write file to disk.', 'code-snippets' ),
401 UPLOAD_ERR_EXTENSION => __( 'A PHP extension stopped the file upload.', 'code-snippets' ),
402 ];
403
404 return $error_messages[ $error_code ] ?? __( 'Unknown upload error.', 'code-snippets' );
405 }
406 }
407