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 +271 -202 2.1.73.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,60 +25,48 @@
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 *
35 39 * @since 2.0.0
36 - * @var array
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 *
51 55 * @since 2.0.0
52 - * @var array
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 - * @param array $import_config Import configuration.
74 - * @return array|WP_Error List of imported tables on success, WP_Error on failure.
65 + * @param array<string, mixed> $import_config Import configuration.
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 - public function run( array $import_config ) {
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' );
79 71 if ( function_exists( 'set_time_limit' ) ) {
80 72 @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@@ -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|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() {
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 - @unlink( $this->import_config['file-upload']['tmp_name'][ $key ] );
118 - $file['error'] = new WP_Error( 'table_import_file-upload_error', '', $error );
109 + @unlink( $this->import_config['file-upload']['tmp_name'][ $key ] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
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,26 +157,26 @@
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'] );
174 170 if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
175 - @unlink( $location );
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,77 +190,56 @@
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 $import_files Files that shall be imported, including ZIP archives.
199 - * @return array 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 ) {
202 - /*
203 - * Here, a for loop is used over a foreach loop, as the array is modified while being iterated over.
204 - * The foreach approach works in PHP 7+ (via https://www.php.net/manual/en/migration70.incompatible.php#migration70.incompatible.foreach.by-ref), but not in PHP 5.6.
205 - * Once PHP 7.x is required, this can be adjusted again.
206 - */
207 - $num_files = count( $import_files ); // This number is growing inside the loop, if files are extracted from a ZIP file and appended to the list.
208 - for ( $key = 0; $key < $num_files; $key++ ) {
209 - $file = $import_files[ $key ];
197 + protected function convert_zip_files( array $import_files ): array {
198 + foreach ( $import_files as $key => &$file ) {
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()`.
210 200
211 - if ( isset( $file['error'] ) && is_wp_error( $file['error'] ) ) {
201 + // Skip files that already have an error.
202 + if ( is_wp_error( $file->error ) ) {
212 203 continue;
213 204 }
214 205
215 - $file['extension'] = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
206 + $file->extension = strtolower( pathinfo( $file->name, PATHINFO_EXTENSION ) );
216 207
217 208 if ( function_exists( 'mime_content_type' ) ) {
218 - $file['mime_type'] = mime_content_type( $file['location'] );
219 - if ( false === $file['mime_type'] ) {
220 - $file['mime_type'] = '';
209 + $mime_type = mime_content_type( $file->location );
210 + if ( false !== $mime_type ) {
211 + $file->mime_type = $mime_type;
221 212 }
222 - } else {
223 - $file['mime_type'] = '';
224 213 }
225 214
226 215 // Detect ZIP files from their file extension or MIME type.
227 - if ( 'zip' === $file['extension'] || 'application/zip' === $file['mime_type'] ) {
228 - if ( ! $this->zip_support_available ) {
229 - $file['error'] = new WP_Error( 'table_import_no_zip_support', '', $file['name'] );
230 - $this->_maybe_unlink_file( $file );
231 - continue;
232 - }
233 -
234 - $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 );
235 218 if ( is_wp_error( $extracted_files ) ) {
236 - $file['error'] = $extracted_files->get_error_code();
237 - $this->_maybe_unlink_file( $file );
219 + $file->error = $extracted_files;
220 + $this->maybe_unlink_file( $file );
238 221 continue;
239 222 }
240 223
241 224 if ( empty( $extracted_files ) ) {
242 - $file['error'] = new WP_Error( 'table_import_zip_file_empty', '', $file['name'] );
243 - $this->_maybe_unlink_file( $file );
225 + $file->error = new WP_Error( 'table_import_zip_file_empty', '', $file->name );
226 + $this->maybe_unlink_file( $file );
244 227 continue;
245 228 }
246 229
247 - $this->_maybe_unlink_file( $file );
248 -
249 - // Mark the ZIP file as removed from the list (null), append its contents to the end, and increase the number of files counter.
250 - $file = null;
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 + */
234 + unset( $import_files[ $key ] );
251 235 array_push( $import_files, ...$extracted_files );
252 - $num_files += count( $extracted_files );
253 236
237 + $this->maybe_unlink_file( $file );
254 238 }
255 -
256 - $import_files[ $key ] = $file;
257 239 }
240 + unset( $file ); // Unset use-by-reference parameter of foreach loop.
258 241
259 - // Actually remove files that are marked as removed (null).
260 - $import_files = array_filter(
261 - $import_files,
262 - static function( $file ) {
263 - return ! is_null( $file );
264 - }
265 - );
266 -
267 242 $import_files = array_merge( $import_files ); // Re-index.
268 243
269 244 return $import_files;
270 245 }
@@ -269,61 +244,95 @@
269 244 return $import_files;
270 245 }
271 246
272 247 /**
273 - * 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.
274 249 *
250 + * Depending on availability, either the PHP's ZipArchive class or WordPress' PclZip class is used.
251 + *
275 252 * @since 2.0.0
276 253 *
277 - * @param array $zip_file File data of a ZIP file (likely in a temporary folder).
278 - * @return array|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.
279 256 */
280 - protected function _extract_zip_file( array $zip_file ) {
281 - $zip = new ZipArchive();
282 - $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 + }
283 266
284 - // If the ZIP file can't be opened with ZIPARCHIVE::CHECKCONS, try again without.
285 - if ( true !== $zip_opened ) {
286 - $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 );
287 272 }
288 273
289 - // If the ZIP file can't even be opened without ZIPARCHIVE::CHECKCONS, bail.
290 - if ( true !== $zip_opened ) {
291 - 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 );
292 294 }
293 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 +
294 301 $files = array();
295 302
296 303 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
297 - for ( $file_idx = 0; $file_idx < $zip->numFiles; $file_idx++ ) {
298 - $file_name = $zip->getNameIndex( $file_idx );
304 + for ( $file_idx = 0; $file_idx < $archive->numFiles; $file_idx++ ) {
305 + $file_name = $archive->getNameIndex( $file_idx );
299 306
300 307 if ( false === $file_name ) {
301 - $files[] = array(
302 - 'location' => '',
303 - 'name' => '',
304 - 'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
305 - );
308 + $files[] = new File( array(
309 + 'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
310 + ) );
306 311 continue;
307 312 }
308 313
309 314 // Skip directories.
310 - if ( '/' === substr( $file_name, -1 ) ) {
315 + if ( str_ends_with( $file_name, '/' ) ) {
311 316 continue;
312 317 }
313 318
314 319 // Skip the __MACOSX directory that macOS adds to archives.
315 - if ( '__MACOSX/' === substr( $file_name, 0, 9 ) ) {
320 + if ( str_starts_with( $file_name, '__MACOSX/' ) ) {
316 321 continue;
317 322 }
318 323
319 - $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 );
320 330 if ( false === $file_data ) {
321 - $files[] = array(
322 - 'location' => '',
323 - 'name' => $file_name,
324 - 'error' => new WP_Error( 'table_import_error_zip_get_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
325 - );
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 + ) );
326 335 continue;
327 336 }
328 337
329 338 $location = wp_tempnam();
@@ -328,38 +337,100 @@
328 337
329 338 $location = wp_tempnam();
330 339 $num_written_bytes = file_put_contents( $location, $file_data );
331 340 if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
332 - @unlink( $location );
333 - $files[] = array(
334 - 'location' => '',
335 - 'name' => $file_name,
336 - 'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
337 - );
341 + @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
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 + ) );
338 346 continue;
339 347 }
340 348
341 - $files[] = array(
349 + $files[] = new File( array(
342 350 'location' => $location,
343 351 'name' => $file_name,
344 - );
352 + ) );
345 353 }
346 354
347 - $zip->close();
355 + $archive->close();
348 356
349 357 return $files;
350 358 }
351 359
352 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 + /**
353 424 * Deletes a file unless the `keep_file` property is set to `true`.
354 425 *
355 426 * @since 2.0.0
356 427 *
357 - * @param array $file File that should maybe be deleted.
428 + * @param File $file File that should maybe be deleted.
358 429 */
359 - protected function _maybe_unlink_file( array $file ) {
360 - if ( ! ( isset( $file['keep_file'] ) && $file['keep_file'] ) && file_exists( $file['location'] ) ) {
361 - @unlink( $file['location'] );
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
362 433 }
363 434 }
364 435
365 436 /**
@@ -366,11 +437,11 @@
366 437 * Prepares a list of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
367 438 *
368 439 * @since 2.0.0
369 440 *
370 - * @return array List of table names and IDs.
441 + * @return array<string, string[]> List of table names and IDs.
371 442 */
372 - protected function _get_list_of_table_names() {
443 + protected function get_list_of_table_names(): array {
373 444 $existing_tables = array();
374 445 // Load all table IDs and names for a comparison with the file name.
375 446 $table_ids = TablePress::$model_table->load_all( false );
376 447 foreach ( $table_ids as $table_id ) {
@@ -376,9 +447,9 @@
376 447 foreach ( $table_ids as $table_id ) {
377 448 // Load table, without table data, options, and visibility settings.
378 449 $table = TablePress::$model_table->load( $table_id, false, false );
379 450 if ( ! is_wp_error( $table ) ) {
380 - $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!
381 452 }
382 453 }
383 454 return $existing_tables;
384 455 }
@@ -389,9 +460,9 @@
389 460 * @since 2.0.0
390 461 *
391 462 * @return bool Whether the legacy import class should be used.
392 463 */
393 - protected function _should_use_legacy_import_class() {
464 + protected function should_use_legacy_import_class(): bool {
394 465 // Allow overriding in the import config (coming e.g. from the import form UI).
395 466 if ( $this->import_config['legacy_import'] ) {
396 467 return true;
397 468 }
@@ -400,9 +471,9 @@
400 471 * Filters whether the Legacy Table Import class shall be used.
401 472 *
402 473 * @since 2.0.0
403 474 *
404 - * @param bool Whether to use the legacy table import class. Default false.
475 + * @param bool $use_legacy_class Whether to use the legacy table import class. Default false.
405 476 */
406 477 if ( apply_filters( 'tablepress_use_legacy_table_import_class', false ) ) {
407 478 return true;
408 479 }
@@ -407,23 +478,17 @@
407 478 return true;
408 479 }
409 480
410 481 // Use the legacy import class, if the requirements for PHPSpreadsheet are not fulfilled.
411 - $phpspreadsheet_requirements_fulfilled = PHP_VERSION_ID >= 70200
412 - && extension_loaded( 'mbstring' )
482 + $phpspreadsheet_requirements_fulfilled = extension_loaded( 'mbstring' )
413 483 && class_exists( 'ZipArchive', false )
414 484 && class_exists( 'DOMDocument', false )
415 485 && function_exists( 'simplexml_load_string' )
416 - && 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.
417 487 if ( ! $phpspreadsheet_requirements_fulfilled ) {
418 488 return true;
419 489 }
420 490
421 - // Use the legacy import class, if the PHPSpreadsheet files do not exist (e.g. because `composer install` was not run).
422 - if ( ! file_exists( TABLEPRESS_ABSPATH . 'libraries/autoload.php' ) ) {
423 - return true;
424 - }
425 -
426 491 return false;
427 492 }
428 493
429 494 /**
@@ -430,16 +495,16 @@
430 495 * Imports all found/extracted/configured files into TablePress.
431 496 *
432 497 * @since 2.0.0
433 498 *
434 - * @param array $import_files Files that shall be imported.
435 - * @return array 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.
436 501 */
437 - protected function _import_files( array $import_files ) {
502 + protected function import_files( array $import_files ): array {
438 503 $tables = array();
439 504 $errors = array();
440 505
441 - $use_legacy_import_class = $this->_should_use_legacy_import_class();
506 + $use_legacy_import_class = $this->should_use_legacy_import_class();
442 507
443 508 // Load Import Base Class.
444 509 TablePress::load_file( 'class-import-base.php', 'classes' );
445 510
@@ -444,10 +509,12 @@
444 509 TablePress::load_file( 'class-import-base.php', 'classes' );
445 510
446 511 // Choose the Table Import library based on the PHP version and the filter hook value.
447 512 if ( $use_legacy_import_class ) {
513 + // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
448 514 $this->importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
449 515 } else {
516 + // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
450 517 $this->importer = TablePress::load_class( 'TablePress_Import_PHPSpreadsheet', 'class-import-phpspreadsheet.php', 'classes' );
451 518 }
452 519
453 520 // If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
@@ -453,9 +520,9 @@
453 520 // If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
454 521 if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) && '' !== $this->import_config['existing_table'] ) {
455 522 $valid_import_files = 0;
456 523 foreach ( $import_files as $file ) {
457 - if ( ! isset( $file['error'] ) || ! is_wp_error( $file['error'] ) ) {
524 + if ( ! is_wp_error( $file->error ) ) {
458 525 ++$valid_import_files;
459 526 if ( $valid_import_files > 1 ) {
460 527 $this->import_config['existing_table'] = '';
461 528 break;
@@ -465,9 +532,9 @@
465 532 }
466 533
467 534 // Loop through all import files and import them.
468 535 foreach ( $import_files as $file ) {
469 - if ( isset( $file['error'] ) && is_wp_error( $file['error'] ) ) {
536 + if ( is_wp_error( $file->error ) ) {
470 537 $errors[] = $file;
471 538 continue;
472 539 }
473 540
@@ -472,24 +539,24 @@
472 539 }
473 540
474 541 // Use import method depending on chosen import class.
475 542 if ( $use_legacy_import_class ) {
476 - $table = $this->_load_table_from_file_legacy( $file );
543 + $table = $this->load_table_from_file_legacy( $file );
477 544 } else {
478 - $table = $this->_load_table_from_file_phpspreadsheet( $file );
545 + $table = $this->load_table_from_file_phpspreadsheet( $file );
479 546 }
480 547
481 - $this->_maybe_unlink_file( $file );
548 + $this->maybe_unlink_file( $file );
482 549
483 550 if ( is_wp_error( $table ) ) {
484 - $file['error'] = $table;
551 + $file->error = $table;
485 552 $errors[] = $file;
486 553 continue;
487 554 }
488 555
489 - $table = $this->_import_table( $table, $file );
556 + $table = $this->save_imported_table( $table, $file );
490 557 if ( is_wp_error( $table ) ) {
491 - $file['error'] = $table;
558 + $file->error = $table;
492 559 $errors[] = $file;
493 560 continue;
494 561 }
495 562
@@ -506,14 +573,14 @@
506 573 * Loads a table from a file via the legacy import class.
507 574 *
508 575 * @since 2.0.0
509 576 *
510 - * @param array $file File with the table data.
511 - * @return array|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
577 + * @param File $file File with the table data.
578 + * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
512 579 */
513 - protected function _load_table_from_file_legacy( array $file ) {
580 + protected function load_table_from_file_legacy( File $file ) /* : array|WP_Error */ {
514 581 // Guess the import format from the file extension.
515 - switch ( $file['extension'] ) {
582 + switch ( $file->extension ) {
516 583 case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet.
517 584 case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded).
518 585 case 'xltx': // Excel (OfficeOpenXML) Template.
519 586 case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded).
@@ -534,27 +601,33 @@
534 601 case 'json':
535 602 $format = 'json';
536 603 break;
537 604 default:
538 - // If no format was found, pass the extension (which will likely result in an error).
539 - $format = $file['extension'];
605 + // If no format was found, try finding the format from the first character below.
606 + $format = '';
540 607 }
541 608
542 - $data = file_get_contents( $file['location'] );
609 + $data = file_get_contents( $file->location );
543 610 if ( false === $data ) {
544 - return new WP_Error( 'table_import_legacy_data_read', '', $file['location'] );
611 + return new WP_Error( 'table_import_legacy_data_read', '', $file->location );
545 612 }
546 613 if ( '' === $data ) {
547 - return new WP_Error( 'table_import_legacy_data_empty', '', $file['location'] );
614 + return new WP_Error( 'table_import_legacy_data_empty', '', $file->location );
548 615 }
549 616
550 617 // If no format could be determined from the file extension, try guessing from the file content.
551 618 if ( '' === $format ) {
619 + $data = trim( $data );
552 620 $first_character = $data[0];
553 - if ( '<' === $first_character ) {
621 + $last_character = $data[-1];
622 +
623 + if ( '<' === $first_character && '>' === $last_character ) {
554 624 $format = 'html';
555 - } elseif ( '{' === $first_character || '[' === $first_character ) {
556 - $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 + }
557 630 }
558 631 }
559 632
560 633 // Fall back to CSV if no file format could be determined.
@@ -562,15 +635,15 @@
562 635 $format = 'csv';
563 636 }
564 637
565 638 if ( ! isset( $this->importer->import_formats[ $format ] ) ) {
566 - return new WP_Error( 'table_import_legacy_unknown_format', '', $file['name'] );
639 + return new WP_Error( 'table_import_legacy_unknown_format', '', $file->name );
567 640 }
568 641
569 642 $table = $this->importer->import_table( $format, $data );
570 643
571 644 if ( false === $table ) {
572 - 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 ) );
573 646 }
574 647
575 648 return $table;
576 649 }
@@ -579,19 +652,14 @@
579 652 * Loads a table from a file via the PHPSpreadsheet import class.
580 653 *
581 654 * @since 2.0.0
582 655 *
583 - * @param array $file File with the table data.
584 - * @return array|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
656 + * @param File $file File with the table data.
657 + * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
585 658 */
586 - protected function _load_table_from_file_phpspreadsheet( array $file ) {
587 - $table = $this->importer->import_table( $file );
588 -
589 - if ( is_wp_error( $table ) ) {
590 - return $table;
591 - }
592 -
593 - return $table;
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.)
594 662 }
595 663
596 664 /**
597 665 * Imports a loaded table into TablePress.
@@ -597,19 +665,19 @@
597 665 * Imports a loaded table into TablePress.
598 666 *
599 667 * @since 2.0.0
600 668 *
601 - * @param array $table The table to be imported, either with properties or just the $table['data'] property set.
602 - * @param array $file File with the table data.
603 - * @return array|WP_Error Imported table on success, WP_Error on failure.
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.
671 + * @return array<string, mixed>|WP_Error Imported table on success, WP_Error on failure.
604 672 */
605 - protected function _import_table( array $table, array $file ) {
673 + protected function save_imported_table( array $table, File $file ) /* : array|WP_Error */ {
606 674 // If name and description are imported from a new table, use those.
607 675 if ( ! isset( $table['name'] ) ) {
608 - $table['name'] = $file['name'];
676 + $table['name'] = $file->name;
609 677 }
610 678 if ( ! isset( $table['description'] ) ) {
611 - $table['description'] = $file['name'];
679 + $table['description'] = $file->name;
612 680 }
613 681
614 682 $import_type = $this->import_config['type'];
615 683 $existing_table_id = $this->import_config['existing_table'];
@@ -618,11 +686,11 @@
618 686 if ( in_array( $import_type, array( 'replace', 'append' ), true ) && '' === $existing_table_id ) {
619 687 if ( isset( $table['id'] ) ) {
620 688 // If the table already contained a table ID (e.g. for the JSON format), use that.
621 689 $existing_table_id = $table['id'];
622 - } elseif ( isset( $this->table_names_ids[ $file['name'] ] ) && 1 === count( $this->table_names_ids[ $file['name'] ] ) ) {
690 + } elseif ( isset( $this->table_names_ids[ $file->name ] ) && 1 === count( $this->table_names_ids[ $file->name ] ) ) {
623 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.
624 - $existing_table_id = $this->table_names_ids[ $file['name'] ][0];
692 + $existing_table_id = $this->table_names_ids[ $file->name ][0];
625 693 }
626 694 }
627 695
628 696 // If the table that is to be replaced or appended to does not exist, add the new table instead.
@@ -630,9 +698,9 @@
630 698 $existing_table_id = '';
631 699 $import_type = 'add';
632 700 }
633 701
634 - $table = $this->_import_tablepress_table( $table, $import_type, $existing_table_id );
702 + $table = $this->import_tablepress_table( $table, $import_type, $existing_table_id );
635 703
636 704 return $table;
637 705 }
638 706
@@ -640,19 +708,20 @@
640 708 * Imports a table by either replacing or appending to an existing table or by adding it as a new table.
641 709 *
642 710 * @since 1.0.0
643 711 *
644 - * @param array $imported_table The table to be imported, either with properties or just the `name`, `description`, and `data` property set.
645 - * @param string $import_type What to do with the imported data: "add", "replace", "append".
646 - * @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.
647 - * @return array|WP_Error Table on success, WP_Error on error.
712 + * @param array<string, mixed> $imported_table The table to be imported, either with properties or just the `name`, `description`, and `data` property set.
713 + * @param string $import_type What to do with the imported data: "add", "replace", "append".
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.
715 + * @return array<string, mixed>|WP_Error Table on success, WP_Error on error.
648 716 */
649 - protected function _import_tablepress_table( array $imported_table, $import_type, $existing_table_id ) {
717 + protected function import_tablepress_table( array $imported_table, string $import_type, string $existing_table_id ) /* : array|WP_Error */ {
650 718 // Full JSON format table can contain a table ID, try to keep that, by later changing the imported table ID to this.
651 - $table_id_in_import = isset( $imported_table['id'] ) ? $imported_table['id'] : '';
719 + $table_id_in_import = $imported_table['id'] ?? '';
652 720
653 - // 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).
654 - 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' ) ) ) {
655 724 return new WP_Error( 'table_import_replace_append_capability_check_failed', '', $existing_table_id );
656 725 }
657 726
658 727 switch ( $import_type ) {
@@ -767,11 +836,11 @@
767 836 * @deprecated 2.0.0 Use `run()` instead.
768 837 *
769 838 * @param string $format Import format.
770 839 * @param string $data Data to import.
771 - * @return array|false Table array on success, false on error.
840 + * @return array<string, mixed>|WP_Error|false Table array on success, WP_Error or false on error.
772 841 */
773 - public function import_table( $format, $data ) {
842 + public function import_table( string $format, string $data ) /* : array|false */ {
774 843 TablePress::load_file( 'class-import-base.php', 'classes' );
775 844 $importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
776 845 return $importer->import_table( $format, $data );
777 846 }