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