PluginProbe
TablePress – Tables in WordPress made easy / 3.1.1
TablePress – Tables in WordPress made easy v3.1.1
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / classes / class-import.php

class-import.php in TablePress – Tables in WordPress made easy 3.1.1, at classes/class-import.php

849 lines 31.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TablePress Table Import Class
4 *
5 * @package TablePress
6 * @subpackage Export/Import
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 use TablePress\Import\File;
12
13 // Prohibit direct script loading.
14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
15
16 TablePress::load_file( 'class-import-file.php', 'classes' );
17
18 /**
19 * TablePress Table Import Class
20 *
21 * @package TablePress
22 * @subpackage Export/Import
23 * @author Tobias Bäthge
24 * @since 1.0.0
25 */
26 class TablePress_Import {
27
28 /**
29 * Instance of the TablePress Legacy or PHPSpreadsheet Importer.
30 *
31 * @since 1.0.0
32 * @var TablePress_Import_Legacy|TablePress_Import_PHPSpreadsheet
33 */
34 protected object $importer;
35
36 /**
37 * Import configuration (mainly the data from the Import form).
38 *
39 * @since 2.0.0
40 * @var array<string, mixed>
41 */
42 protected array $import_config = array();
43
44 /**
45 * Whether ZIP archive support is available (which it always is, as PclZip is used as a fallback).
46 *
47 * @since 1.0.0
48 * @deprecated 2.3.0 ZIP support is now always available, either through `ZipArchive` or through `PclZip`.
49 */
50 public bool $zip_support_available = true;
51
52 /**
53 * List of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
54 *
55 * @since 2.0.0
56 * @var array<string, string[]>
57 */
58 protected array $table_names_ids = array();
59
60 /**
61 * Runs the import process for a given import configuration.
62 *
63 * @since 2.0.0
64 *
65 * @param array<string, mixed> $import_config Import configuration.
66 * @return array{tables: array<int, array<string, mixed>>, errors: File[]}|WP_Error List of imported tables on success, WP_Error on failure.
67 */
68 public function run( array $import_config ) /* : array|WP_Error */ {
69 // Unziping can use a lot of memory and execution time, but not this much hopefully.
70 wp_raise_memory_limit( 'admin' );
71 if ( function_exists( 'set_time_limit' ) ) {
72 @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
73 }
74
75 $this->import_config = $import_config;
76
77 $import_files = $this->get_files_to_import();
78 if ( is_wp_error( $import_files ) ) {
79 return $import_files;
80 }
81
82 $import_files = $this->convert_zip_files( $import_files );
83
84 if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) ) {
85 $this->table_names_ids = $this->get_list_of_table_names();
86 }
87
88 return $this->import_files( $import_files );
89 }
90
91 /**
92 * Extracts the files that shall be imported from the import configuration.
93 *
94 * @since 2.0.0
95 *
96 * @return File[]|WP_Error Array of files that shall be imported or WP_Error on failure.
97 */
98 protected function get_files_to_import() /* : array|WP_Error */ {
99 $import_files = array();
100
101 switch ( $this->import_config['source'] ) {
102 case 'file-upload':
103 foreach ( $this->import_config['file-upload']['error'] as $key => $error ) {
104 $file = new File( array(
105 'location' => $this->import_config['file-upload']['tmp_name'][ $key ],
106 'name' => $this->import_config['file-upload']['name'][ $key ],
107 ) );
108 if ( UPLOAD_ERR_OK !== $error ) {
109 @unlink( $this->import_config['file-upload']['tmp_name'][ $key ] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
110 $file->error = new WP_Error( 'table_import_file-upload_error', '', $error );
111 }
112 $import_files[] = $file;
113 }
114 break;
115 case 'url':
116 $host = wp_parse_url( $this->import_config['url'], PHP_URL_HOST );
117
118 if ( empty( $host ) ) {
119 return new WP_Error( 'table_import_url_host_invalid', '', $this->import_config['url'] );
120 }
121
122 // Check the IP address of the host against a blocklist of hosts which should not be accessible, e.g. for security considerations.
123 $ip = gethostbyname( $host ); // If no IP address can be found, this will return the host name, which will then be checked against the blocklist.
124 $blocked_ips = array(
125 '169.254.169.254', // Meta-data API for various cloud providers.
126 '169.254.170.2', // AWS task metadata endpoint.
127 '192.0.0.192', // Oracle Cloud endpoint.
128 '100.100.100.200', // Alibaba Cloud endpoint.
129 );
130 if ( in_array( $ip, $blocked_ips, true ) ) {
131 return new WP_Error( 'table_import_url_host_blocked', '', array( 'url' => $this->import_config['url'], 'ip' => $ip ) );
132 }
133
134 /**
135 * Load WP file functions to be sure that `download_url()` exists, in particular during Cron requests.
136 */
137 require_once ABSPATH . 'wp-admin/includes/file.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
138
139 // Download URL to local file.
140 $location = download_url( $this->import_config['url'] );
141 if ( is_wp_error( $location ) ) {
142 $error = new WP_Error( 'table_import_url_download_failed', '', $this->import_config['url'] );
143 $error->merge_from( $location );
144 return $error;
145 }
146
147 $import_files[] = new File( array(
148 'location' => $location,
149 'name' => $this->import_config['url'],
150 ) );
151 break;
152 case 'server':
153 if ( ABSPATH === $this->import_config['server'] ) {
154 return new WP_Error( 'table_import_server_invalid', '', $this->import_config['server'] );
155 }
156
157 if ( ! is_readable( $this->import_config['server'] ) ) {
158 return new WP_Error( 'table_import_server_not_readable', '', $this->import_config['server'] );
159 }
160
161 $import_files[] = new File( array(
162 'location' => $this->import_config['server'],
163 'name' => pathinfo( $this->import_config['server'], PATHINFO_BASENAME ),
164 'keep_file' => true, // Files on the server must not be deleted.
165 ) );
166 break;
167 case 'form-field':
168 $location = wp_tempnam();
169 $num_written_bytes = file_put_contents( $location, $this->import_config['form-field'] );
170 if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
171 @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
172 return new WP_Error( 'table_import_form-field_temp_file_not_written' );
173 }
174
175 $import_files[] = new File( array(
176 'location' => $location,
177 'name' => __( 'Imported from Manual Input', 'tablepress' ),
178 ) );
179 break;
180 default:
181 return new WP_Error( 'table_import_invalid_source', '', $this->import_config['source'] );
182 }
183
184 return $import_files;
185 }
186
187 /**
188 * Replaces ZIP archives in the import files with a list of their contents.
189 *
190 * ZIP files are removed from the list and their contents are added to the end of the list.
191 *
192 * @since 2.0.0
193 *
194 * @param File[] $import_files Files that shall be imported, including ZIP archives.
195 * @return File[] Files that shall be imported, with all ZIP archives recursively replaced by their contents.
196 */
197 protected function convert_zip_files( array $import_files ): array {
198 foreach ( $import_files as $key => &$file ) {
199 // $file has to be used by reference, so that $key points to the correct element, due to array modification with `unset()` and `array_push()`.
200
201 // Skip files that already have an error.
202 if ( is_wp_error( $file->error ) ) {
203 continue;
204 }
205
206 $file->extension = strtolower( pathinfo( $file->name, PATHINFO_EXTENSION ) );
207
208 if ( function_exists( 'mime_content_type' ) ) {
209 $mime_type = mime_content_type( $file->location );
210 if ( false !== $mime_type ) {
211 $file->mime_type = $mime_type;
212 }
213 }
214
215 // Detect ZIP files from their file extension or MIME type.
216 if ( 'zip' === $file->extension || 'application/zip' === $file->mime_type ) {
217 $extracted_files = $this->extract_zip_file( $file );
218 if ( is_wp_error( $extracted_files ) ) {
219 $file->error = $extracted_files;
220 $this->maybe_unlink_file( $file );
221 continue;
222 }
223
224 if ( empty( $extracted_files ) ) {
225 $file->error = new WP_Error( 'table_import_zip_file_empty', '', $file->name );
226 $this->maybe_unlink_file( $file );
227 continue;
228 }
229
230 /*
231 * Remove the ZIP file from the list and instead append its contents.
232 * Appending ensures recursiveness, as the appended files will be checked again.
233 */
234 unset( $import_files[ $key ] );
235 array_push( $import_files, ...$extracted_files );
236
237 $this->maybe_unlink_file( $file );
238 }
239 }
240 unset( $file ); // Unset use-by-reference parameter of foreach loop.
241
242 $import_files = array_merge( $import_files ); // Re-index.
243
244 return $import_files;
245 }
246
247 /**
248 * Extracts the files of a ZIP file and returns a list of files and their location.
249 *
250 * Depending on availability, either the PHP's ZipArchive class or WordPress' PclZip class is used.
251 *
252 * @since 2.0.0
253 *
254 * @param File $zip_file File data of a ZIP file (likely in a temporary folder).
255 * @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
256 */
257 protected function extract_zip_file( File $zip_file ) /* : array|WP_Error */ {
258 if ( class_exists( 'ZipArchive', false ) ) {
259 $ziparchive_result = $this->extract_zip_file_ziparchive( $zip_file );
260 if ( is_array( $ziparchive_result ) ) {
261 return $ziparchive_result;
262 }
263 } else {
264 $ziparchive_result = new WP_Error( 'table_import_error_zip_open', '', array( 'ziparchive_error' => 'Class ZipArchive not available' ) );
265 }
266
267 // Fall through to PclZip if ZipArchive is not available or encountered an error opening the file.
268 $pclzip_result = $this->extract_zip_file_pclzip( $zip_file );
269 if ( is_wp_error( $pclzip_result ) ) {
270 // Append the WP_Error from ZipArchive, to have all error information available.
271 $pclzip_result->merge_from( $ziparchive_result );
272 }
273
274 return $pclzip_result;
275 }
276
277 /**
278 * Extracts the files of a ZIP file using the PHP ZipArchive class.
279 *
280 * The ZIP file is extracted to a temporary folder and a list of files and their location is returned.
281 *
282 * @since 2.3.0
283 *
284 * @param File $zip_file File data of a ZIP file (likely in a temporary folder).
285 * @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
286 */
287 protected function extract_zip_file_ziparchive( File $zip_file ) /* : array|WP_Error */ {
288 $archive = new ZipArchive();
289 $archive_opened = $archive->open( $zip_file->location, ZipArchive::CHECKCONS );
290
291 // If the ZIP file can't be opened with ZipArchive::CHECKCONS, try again without.
292 if ( true !== $archive_opened ) {
293 $archive_opened = $archive->open( $zip_file->location );
294 }
295
296 // If the ZIP file can't even be opened without ZipArchive::CHECKCONS, bail.
297 if ( true !== $archive_opened ) {
298 return new WP_Error( 'table_import_error_zip_open', '', array( 'ziparchive_error' => $archive_opened ) );
299 }
300
301 $files = array();
302
303 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
304 for ( $file_idx = 0; $file_idx < $archive->numFiles; $file_idx++ ) {
305 $file_name = $archive->getNameIndex( $file_idx );
306
307 if ( false === $file_name ) {
308 $files[] = new File( array(
309 'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
310 ) );
311 continue;
312 }
313
314 // Skip directories.
315 if ( str_ends_with( $file_name, '/' ) ) {
316 continue;
317 }
318
319 // Skip the __MACOSX directory that macOS adds to archives.
320 if ( str_starts_with( $file_name, '__MACOSX/' ) ) {
321 continue;
322 }
323
324 // Don't extract invalid files.
325 if ( 0 !== validate_file( $file_name ) ) {
326 continue;
327 }
328
329 $file_data = $archive->getFromIndex( $file_idx );
330 if ( false === $file_data ) {
331 $files[] = new File( array(
332 'name' => $file_name,
333 'error' => new WP_Error( 'table_import_error_zip_get_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
334 ) );
335 continue;
336 }
337
338 $location = wp_tempnam();
339 $num_written_bytes = file_put_contents( $location, $file_data );
340 if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
341 @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
342 $files[] = new File( array(
343 'name' => $file_name,
344 'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
345 ) );
346 continue;
347 }
348
349 $files[] = new File( array(
350 'location' => $location,
351 'name' => $file_name,
352 ) );
353 }
354
355 $archive->close();
356
357 return $files;
358 }
359
360 /**
361 * Extracts the files of a ZIP file using WordPress' PclZip class.
362 *
363 * The ZIP file is extracted to a temporary folder and a list of files and their location is returned.
364 *
365 * @since 2.3.0
366 *
367 * @param File $zip_file File data of a ZIP file (likely in a temporary folder).
368 * @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
369 */
370 protected function extract_zip_file_pclzip( File $zip_file ) /* : array|WP_Error */ {
371 mbstring_binary_safe_encoding();
372
373 require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
374
375 $archive = new PclZip( $zip_file->location );
376 $archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING ); // @phpstan-ignore arguments.count (PclZip::extract() uses `func_get_args()` to handle optional arguments.)
377
378 reset_mbstring_encoding();
379
380 // If the ZIP file can't be opened, bail.
381 if ( ! is_array( $archive_files ) ) {
382 return new WP_Error( 'table_import_error_zip_open', '', array( 'pclzip_error' => $archive->errorInfo( true ) ) );
383 }
384
385 $files = array();
386
387 foreach ( $archive_files as $file ) {
388 // Skip directories.
389 if ( $file['folder'] ) {
390 continue;
391 }
392
393 // Skip the __MACOSX directory that macOS adds to archives.
394 if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) {
395 continue;
396 }
397
398 // Don't extract invalid files.
399 if ( 0 !== validate_file( $file['filename'] ) ) {
400 continue;
401 }
402
403 $location = wp_tempnam();
404 $num_written_bytes = file_put_contents( $location, $file['content'] );
405 if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
406 @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
407 $files[] = new File( array(
408 'name' => $file['filename'],
409 'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file['index'], 'ziparchive_file_name' => $file['filename'] ) ),
410 ) );
411 continue;
412 }
413
414 $files[] = new File( array(
415 'location' => $location,
416 'name' => $file['filename'],
417 ) );
418 }
419
420 return $files;
421 }
422
423 /**
424 * Deletes a file unless the `keep_file` property is set to `true`.
425 *
426 * @since 2.0.0
427 *
428 * @param File $file File that should maybe be deleted.
429 */
430 protected function maybe_unlink_file( File $file ): void {
431 if ( ! $file->keep_file && file_exists( $file->location ) ) {
432 @unlink( $file->location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
433 }
434 }
435
436 /**
437 * Prepares a list of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
438 *
439 * @since 2.0.0
440 *
441 * @return array<string, string[]> List of table names and IDs.
442 */
443 protected function get_list_of_table_names(): array {
444 $existing_tables = array();
445 // Load all table IDs and names for a comparison with the file name.
446 $table_ids = TablePress::$model_table->load_all( false );
447 foreach ( $table_ids as $table_id ) {
448 // Load table, without table data, options, and visibility settings.
449 $table = TablePress::$model_table->load( $table_id, false, false );
450 if ( ! is_wp_error( $table ) ) {
451 $existing_tables[ (string) $table['name'] ][] = $table_id; // Attention: The table name is not unique!
452 }
453 }
454 return $existing_tables;
455 }
456
457 /**
458 * Checks whether the requirements for the PHPSpreadsheet import class are fulfilled or if the legacy import class should be used.
459 *
460 * @since 2.0.0
461 *
462 * @return bool Whether the legacy import class should be used.
463 */
464 protected function should_use_legacy_import_class(): bool {
465 // Allow overriding in the import config (coming e.g. from the import form UI).
466 if ( $this->import_config['legacy_import'] ) {
467 return true;
468 }
469
470 /**
471 * Filters whether the Legacy Table Import class shall be used.
472 *
473 * @since 2.0.0
474 *
475 * @param bool $use_legacy_class Whether to use the legacy table import class. Default false.
476 */
477 if ( apply_filters( 'tablepress_use_legacy_table_import_class', false ) ) {
478 return true;
479 }
480
481 // Use the legacy import class, if the requirements for PHPSpreadsheet are not fulfilled.
482 $phpspreadsheet_requirements_fulfilled = extension_loaded( 'mbstring' )
483 && class_exists( 'ZipArchive', false )
484 && class_exists( 'DOMDocument', false )
485 && function_exists( 'simplexml_load_string' )
486 && ( function_exists( 'libxml_disable_entity_loader' ) || PHP_VERSION_ID >= 80000 ); // This function is only needed for older versions of PHP.
487 if ( ! $phpspreadsheet_requirements_fulfilled ) {
488 return true;
489 }
490
491 return false;
492 }
493
494 /**
495 * Imports all found/extracted/configured files into TablePress.
496 *
497 * @since 2.0.0
498 *
499 * @param File[] $import_files Files that shall be imported.
500 * @return array{tables: array<int, array<string, mixed>>, errors: File[]} Imported tables and files that caused errors.
501 */
502 protected function import_files( array $import_files ): array {
503 $tables = array();
504 $errors = array();
505
506 $use_legacy_import_class = $this->should_use_legacy_import_class();
507
508 // Load Import Base Class.
509 TablePress::load_file( 'class-import-base.php', 'classes' );
510
511 // Choose the Table Import library based on the PHP version and the filter hook value.
512 if ( $use_legacy_import_class ) {
513 // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
514 $this->importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
515 } else {
516 // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
517 $this->importer = TablePress::load_class( 'TablePress_Import_PHPSpreadsheet', 'class-import-phpspreadsheet.php', 'classes' );
518 }
519
520 // If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
521 if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) && '' !== $this->import_config['existing_table'] ) {
522 $valid_import_files = 0;
523 foreach ( $import_files as $file ) {
524 if ( ! is_wp_error( $file->error ) ) {
525 ++$valid_import_files;
526 if ( $valid_import_files > 1 ) {
527 $this->import_config['existing_table'] = '';
528 break;
529 }
530 }
531 }
532 }
533
534 // Loop through all import files and import them.
535 foreach ( $import_files as $file ) {
536 if ( is_wp_error( $file->error ) ) {
537 $errors[] = $file;
538 continue;
539 }
540
541 // Use import method depending on chosen import class.
542 if ( $use_legacy_import_class ) {
543 $table = $this->load_table_from_file_legacy( $file );
544 } else {
545 $table = $this->load_table_from_file_phpspreadsheet( $file );
546 }
547
548 $this->maybe_unlink_file( $file );
549
550 if ( is_wp_error( $table ) ) {
551 $file->error = $table;
552 $errors[] = $file;
553 continue;
554 }
555
556 $table = $this->save_imported_table( $table, $file );
557 if ( is_wp_error( $table ) ) {
558 $file->error = $table;
559 $errors[] = $file;
560 continue;
561 }
562
563 $tables[] = $table;
564 }
565
566 return array(
567 'tables' => $tables,
568 'errors' => $errors,
569 );
570 }
571
572 /**
573 * Loads a table from a file via the legacy import class.
574 *
575 * @since 2.0.0
576 *
577 * @param File $file File with the table data.
578 * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
579 */
580 protected function load_table_from_file_legacy( File $file ) /* : array|WP_Error */ {
581 // Guess the import format from the file extension.
582 switch ( $file->extension ) {
583 case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet.
584 case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded).
585 case 'xltx': // Excel (OfficeOpenXML) Template.
586 case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded).
587 $format = 'xlsx';
588 break;
589 case 'xls': // Excel (BIFF) Spreadsheet.
590 case 'xlt': // Excel (BIFF) Template.
591 $format = 'xls';
592 break;
593 case 'htm':
594 case 'html':
595 $format = 'html';
596 break;
597 case 'csv':
598 case 'tsv':
599 $format = 'csv';
600 break;
601 case 'json':
602 $format = 'json';
603 break;
604 default:
605 // If no format was found, try finding the format from the first character below.
606 $format = '';
607 }
608
609 $data = file_get_contents( $file->location );
610 if ( false === $data ) {
611 return new WP_Error( 'table_import_legacy_data_read', '', $file->location );
612 }
613 if ( '' === $data ) {
614 return new WP_Error( 'table_import_legacy_data_empty', '', $file->location );
615 }
616
617 // If no format could be determined from the file extension, try guessing from the file content.
618 if ( '' === $format ) {
619 $data = trim( $data );
620 $first_character = $data[0];
621 $last_character = $data[-1];
622
623 if ( '<' === $first_character && '>' === $last_character ) {
624 $format = 'html';
625 } elseif ( ( '[' === $first_character && ']' === $last_character ) || ( '{' === $first_character && '}' === $last_character ) ) {
626 $json_table = json_decode( $data, true );
627 if ( ! is_null( $json_table ) ) {
628 $format = 'json';
629 }
630 }
631 }
632
633 // Fall back to CSV if no file format could be determined.
634 if ( '' === $format ) {
635 $format = 'csv';
636 }
637
638 if ( ! in_array( $format, $this->importer->import_formats, true ) ) { // @phpstan-ignore property.notFound (`$this->importer` is an instance of `TablePress_Import_Legacy` which has the property `import_formats`.)
639 return new WP_Error( 'table_import_legacy_unknown_format', '', $file->name );
640 }
641
642 $table = $this->importer->import_table( $format, $data );
643
644 if ( false === $table ) {
645 return new WP_Error( 'table_import_legacy_importer_failed', '', array( 'file_name' => $file->name, 'file_format' => $format ) );
646 }
647
648 return $table;
649 }
650
651 /**
652 * Loads a table from a file via the PHPSpreadsheet import class.
653 *
654 * @since 2.0.0
655 *
656 * @param File $file File with the table data.
657 * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
658 */
659 protected function load_table_from_file_phpspreadsheet( File $file ) /* : array|WP_Error */ {
660 // Convert File object to array, as those are not yet used outside of this class.
661 return $this->importer->import_table( $file ); // @phpstan-ignore return.type (This is an instance of TablePress_Import_PHPSpreadsheet which does not return false.)
662 }
663
664 /**
665 * Imports a loaded table into TablePress.
666 *
667 * @since 2.0.0
668 *
669 * @param array<string, mixed> $table The table to be imported, either with properties or just the $table['data'] property set.
670 * @param File $file File with the table data.
671 * @return array<string, mixed>|WP_Error Imported table on success, WP_Error on failure.
672 */
673 protected function save_imported_table( array $table, File $file ) /* : array|WP_Error */ {
674 // If name and description are imported from a new table, use those.
675 if ( ! isset( $table['name'] ) ) {
676 $table['name'] = $file->name;
677 }
678 if ( ! isset( $table['description'] ) ) {
679 $table['description'] = $file->name;
680 }
681
682 $import_type = $this->import_config['type'];
683 $existing_table_id = $this->import_config['existing_table'];
684
685 // If no existing table ID has been set (or if we are importing multiple tables), try to find a potential existing table from the table ID in the import data or by comparing the file name with the table name.
686 if ( in_array( $import_type, array( 'replace', 'append' ), true ) && '' === $existing_table_id ) {
687 if ( isset( $table['id'] ) ) {
688 // If the table already contained a table ID (e.g. for the JSON format), use that.
689 $existing_table_id = $table['id'];
690 } elseif ( isset( $this->table_names_ids[ $file->name ] ) && 1 === count( $this->table_names_ids[ $file->name ] ) ) {
691 // Use the replace/append ID of tables where the table name matches the file name, but only if there was exactly one file name match.
692 $existing_table_id = $this->table_names_ids[ $file->name ][0];
693 }
694 }
695
696 // If the table that is to be replaced or appended to does not exist, add the new table instead.
697 if ( ! TablePress::$model_table->table_exists( $existing_table_id ) ) {
698 $existing_table_id = '';
699 $import_type = 'add';
700 }
701
702 $table = $this->import_tablepress_table( $table, $import_type, $existing_table_id );
703
704 return $table;
705 }
706
707 /**
708 * Imports a table by either replacing or appending to an existing table or by adding it as a new table.
709 *
710 * @since 1.0.0
711 *
712 * @param array<string, mixed> $imported_table The table to be imported, either with properties or just the `name`, `description`, and `data` property set.
713 * @param string $import_type What to do with the imported data: "add", "replace", "append".
714 * @param string $existing_table_id Empty string if table shall be added as a new table, ID of the table to be replaced or appended to otherwise.
715 * @return array<string, mixed>|WP_Error Table on success, WP_Error on error.
716 */
717 protected function import_tablepress_table( array $imported_table, string $import_type, string $existing_table_id ) /* : array|WP_Error */ {
718 // Full JSON format table can contain a table ID, try to keep that, by later changing the imported table ID to this.
719 $table_id_in_import = $imported_table['id'] ?? '';
720
721 // To be able to replace or append to a table, the user must be able to edit the table, or it must be a request via the Automatic Periodic Table Import module.
722 if ( in_array( $import_type, array( 'replace', 'append' ), true )
723 && ! ( current_user_can( 'tablepress_edit_table', $existing_table_id ) || doing_action( 'tablepress_automatic_periodic_table_import_action' ) ) ) {
724 return new WP_Error( 'table_import_replace_append_capability_check_failed', '', $existing_table_id );
725 }
726
727 switch ( $import_type ) {
728 case 'add':
729 $existing_table = TablePress::$model_table->get_table_template();
730 // Import visibility information if it exists, usually only for the JSON format.
731 if ( isset( $imported_table['visibility'] ) ) {
732 $existing_table['visibility'] = $imported_table['visibility'];
733 }
734 break;
735 case 'replace':
736 // Load table, without table data, but with options and visibility settings.
737 $existing_table = TablePress::$model_table->load( $existing_table_id, false, true );
738 if ( is_wp_error( $existing_table ) ) {
739 $error = new WP_Error( 'table_import_replace_table_load', '', $existing_table_id );
740 $error->merge_from( $existing_table );
741 return $error;
742 }
743 // Don't change name and description when a table is replaced.
744 $imported_table['name'] = $existing_table['name'];
745 $imported_table['description'] = $existing_table['description'];
746 // Replace visibility information if it exists.
747 if ( isset( $imported_table['visibility'] ) ) {
748 $existing_table['visibility'] = $imported_table['visibility'];
749 }
750 break;
751 case 'append':
752 // Load table, with table data, options, and visibility settings.
753 $existing_table = TablePress::$model_table->load( $existing_table_id, true, true );
754 if ( is_wp_error( $existing_table ) ) {
755 $error = new WP_Error( 'table_import_append_table_load', '', $existing_table_id );
756 $error->merge_from( $existing_table );
757 return $error;
758 }
759 if ( isset( $existing_table['is_corrupted'] ) && $existing_table['is_corrupted'] ) {
760 return new WP_Error( 'table_import_append_table_load_corrupted', '', $existing_table_id );
761 }
762 // Don't change name and description when a table is appended to.
763 $imported_table['name'] = $existing_table['name'];
764 $imported_table['description'] = $existing_table['description'];
765 // Actual appending:.
766 $imported_table['data'] = array_merge( $existing_table['data'], $imported_table['data'] );
767 $this->importer->pad_array_to_max_cols( $imported_table['data'] );
768 // Append visibility information for rows.
769 if ( isset( $imported_table['visibility']['rows'] ) ) {
770 $existing_table['visibility']['rows'] = array_merge( $existing_table['visibility']['rows'], $imported_table['visibility']['rows'] );
771 }
772 // When appending, do not overwrite options, e.g. coming from a JSON file.
773 unset( $imported_table['options'] );
774 break;
775 default:
776 return new WP_Error( 'table_import_import_type_invalid', '', $import_type );
777 }
778
779 // Merge new or existing table with information from the imported table.
780 $imported_table['id'] = $existing_table['id']; // Will be false for new table or the existing table ID.
781 // Cut visibility array (if the imported table is smaller), and pad correctly if imported table is bigger than existing table (or new template).
782 $num_rows = count( $imported_table['data'] );
783 $num_columns = count( $imported_table['data'][0] );
784 $imported_table['visibility'] = array(
785 'rows' => array_pad( array_slice( $existing_table['visibility']['rows'], 0, $num_rows ), $num_rows, 1 ),
786 'columns' => array_pad( array_slice( $existing_table['visibility']['columns'], 0, $num_columns ), $num_columns, 1 ),
787 );
788
789 // Check if the new table data is valid and consistent.
790 $table = TablePress::$model_table->prepare_table( $existing_table, $imported_table, false );
791 if ( is_wp_error( $table ) ) {
792 $error = new WP_Error( 'table_import_table_prepare', '', $imported_table['id'] );
793 $error->merge_from( $table );
794 return $error;
795 }
796
797 // DataTables Custom Commands can only be edit by trusted users.
798 if ( ! current_user_can( 'unfiltered_html' ) ) {
799 $table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
800 }
801
802 // Replace existing table or add new table.
803 if ( in_array( $import_type, array( 'replace', 'append' ), true ) ) {
804 // Replace existing table with imported/appended table.
805 $table_id = TablePress::$model_table->save( $table );
806 } else {
807 // Add the imported table (and get its first ID).
808 $table_id = TablePress::$model_table->add( $table );
809 }
810
811 if ( is_wp_error( $table_id ) ) {
812 $error = new WP_Error( 'table_import_table_save_or_add', '', $table['id'] );
813 $error->merge_from( $table_id );
814 return $error;
815 }
816
817 // Try to use ID from imported file (e.g. in full JSON format table).
818 if ( '' !== $table_id_in_import && $table_id !== $table_id_in_import && current_user_can( 'tablepress_edit_table_id', $table_id ) ) {
819 $id_changed = TablePress::$model_table->change_table_id( $table_id, $table_id_in_import );
820 if ( ! is_wp_error( $id_changed ) ) {
821 $table_id = $table_id_in_import;
822 }
823 }
824
825 $table['id'] = $table_id;
826
827 return $table;
828 }
829
830 /**
831 * Imports a table in legacy versions of the Table Auto Update Extension.
832 *
833 * This method is deprecated and is only left for backward compatibility reasons. Do not use this in new code!
834 *
835 * @since 1.0.0
836 * @deprecated 2.0.0 Use `run()` instead.
837 *
838 * @param string $format Import format.
839 * @param string $data Data to import.
840 * @return array<string, mixed>|WP_Error|false Table array on success, WP_Error or false on error.
841 */
842 public function import_table( string $format, string $data ) /* : array|false */ {
843 TablePress::load_file( 'class-import-base.php', 'classes' );
844 $importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
845 return $importer->import_table( $format, $data );
846 }
847
848 } // class TablePress_Import
849