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

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