| 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 |
|