PluginProbe
TablePress – Tables in WordPress made easy / 2.3
TablePress – Tables in WordPress made easy v2.3
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 2.3, at classes/class-import.php

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