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