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 +226 -143 2.2.53.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;
@@ -141,9 +133,9 @@
141 133
142 134 /**
143 135 * Load WP file functions to be sure that `download_url()` exists, in particular during Cron requests.
144 136 */
145 - 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.)
146 138
147 139 // Download URL to local file.
148 140 $location = download_url( $this->import_config['url'] );
149 141 if ( is_wp_error( $location ) ) {
@@ -151,12 +143,12 @@
151 143 $error->merge_from( $location );
152 144 return $error;
153 145 }
154 146
155 - $import_files[] = array(
147 + $import_files[] = new File( array(
156 148 'location' => $location,
157 149 'name' => $this->import_config['url'],
158 - );
150 + ) );
159 151 break;
160 152 case 'server':
161 153 if ( ABSPATH === $this->import_config['server'] ) {
162 154 return new WP_Error( 'table_import_server_invalid', '', $this->import_config['server'] );
@@ -165,13 +157,13 @@
165 157 if ( ! is_readable( $this->import_config['server'] ) ) {
166 158 return new WP_Error( 'table_import_server_not_readable', '', $this->import_config['server'] );
167 159 }
168 160
169 - $import_files[] = array(
161 + $import_files[] = new File( array(
170 162 'location' => $this->import_config['server'],
171 163 'name' => pathinfo( $this->import_config['server'], PATHINFO_BASENAME ),
172 164 'keep_file' => true, // Files on the server must not be deleted.
173 - );
165 + ) );
174 166 break;
175 167 case 'form-field':
176 168 $location = wp_tempnam();
177 169 $num_written_bytes = file_put_contents( $location, $this->import_config['form-field'] );
@@ -179,12 +171,12 @@
179 171 @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
180 172 return new WP_Error( 'table_import_form-field_temp_file_not_written' );
181 173 }
182 174
183 - $import_files[] = array(
175 + $import_files[] = new File( array(
184 176 'location' => $location,
185 177 'name' => __( 'Imported from Manual Input', 'tablepress' ),
186 - );
178 + ) );
187 179 break;
188 180 default:
189 181 return new WP_Error( 'table_import_invalid_source', '', $this->import_config['source'] );
190 182 }
@@ -198,55 +190,52 @@
198 190 * ZIP files are removed from the list and their contents are added to the end of the list.
199 191 *
200 192 * @since 2.0.0
201 193 *
202 - * @param array<int, array<string, mixed>> $import_files Files that shall be imported, including ZIP archives.
203 - * @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.
204 196 */
205 - protected function _convert_zip_files( array $import_files ): array {
197 + protected function convert_zip_files( array $import_files ): array {
206 198 foreach ( $import_files as $key => &$file ) {
207 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()`.
208 - 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 ) ) {
209 203 continue;
210 204 }
211 205
212 - $file['extension'] = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
206 + $file->extension = strtolower( pathinfo( $file->name, PATHINFO_EXTENSION ) );
213 207
214 208 if ( function_exists( 'mime_content_type' ) ) {
215 - $file['mime_type'] = mime_content_type( $file['location'] );
216 - if ( false === $file['mime_type'] ) {
217 - $file['mime_type'] = '';
209 + $mime_type = mime_content_type( $file->location );
210 + if ( false !== $mime_type ) {
211 + $file->mime_type = $mime_type;
218 212 }
219 - } else {
220 - $file['mime_type'] = '';
221 213 }
222 214
223 215 // Detect ZIP files from their file extension or MIME type.
224 - if ( 'zip' === $file['extension'] || 'application/zip' === $file['mime_type'] ) {
225 - if ( ! $this->zip_support_available ) {
226 - $file['error'] = new WP_Error( 'table_import_no_zip_support', '', $file['name'] );
227 - $this->_maybe_unlink_file( $file );
228 - continue;
229 - }
230 -
231 - $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 );
232 218 if ( is_wp_error( $extracted_files ) ) {
233 - $file['error'] = $extracted_files;
234 - $this->_maybe_unlink_file( $file );
219 + $file->error = $extracted_files;
220 + $this->maybe_unlink_file( $file );
235 221 continue;
236 222 }
237 223
238 224 if ( empty( $extracted_files ) ) {
239 - $file['error'] = new WP_Error( 'table_import_zip_file_empty', '', $file['name'] );
240 - $this->_maybe_unlink_file( $file );
225 + $file->error = new WP_Error( 'table_import_zip_file_empty', '', $file->name );
226 + $this->maybe_unlink_file( $file );
241 227 continue;
242 228 }
243 229
244 - // 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 + */
245 234 unset( $import_files[ $key ] );
246 235 array_push( $import_files, ...$extracted_files );
247 236
248 - $this->_maybe_unlink_file( $file );
237 + $this->maybe_unlink_file( $file );
249 238 }
250 239 }
251 240 unset( $file ); // Unset use-by-reference parameter of foreach loop.
252 241
@@ -255,41 +244,71 @@
255 244 return $import_files;
256 245 }
257 246
258 247 /**
259 - * 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.
260 249 *
250 + * Depending on availability, either the PHP's ZipArchive class or WordPress' PclZip class is used.
251 + *
261 252 * @since 2.0.0
262 253 *
263 - * @param array<string, mixed> $zip_file File data of a ZIP file (likely in a temporary folder).
264 - * @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.
265 256 */
266 - protected function _extract_zip_file( array $zip_file ) /* : array|WP_Error */ {
267 - $zip = new ZipArchive();
268 - $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 + }
269 266
270 - // If the ZIP file can't be opened with ZIPARCHIVE::CHECKCONS, try again without.
271 - if ( true !== $zip_opened ) {
272 - $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 );
273 272 }
274 273
275 - // If the ZIP file can't even be opened without ZIPARCHIVE::CHECKCONS, bail.
276 - if ( true !== $zip_opened ) {
277 - 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 );
278 294 }
279 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 +
280 301 $files = array();
281 302
282 303 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
283 - for ( $file_idx = 0; $file_idx < $zip->numFiles; $file_idx++ ) {
284 - $file_name = $zip->getNameIndex( $file_idx );
304 + for ( $file_idx = 0; $file_idx < $archive->numFiles; $file_idx++ ) {
305 + $file_name = $archive->getNameIndex( $file_idx );
285 306
286 307 if ( false === $file_name ) {
287 - $files[] = array(
288 - 'location' => '',
289 - 'name' => '',
290 - 'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
291 - );
308 + $files[] = new File( array(
309 + 'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
310 + ) );
292 311 continue;
293 312 }
294 313
295 314 // Skip directories.
@@ -301,15 +320,19 @@
301 320 if ( str_starts_with( $file_name, '__MACOSX/' ) ) {
302 321 continue;
303 322 }
304 323
305 - $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 );
306 330 if ( false === $file_data ) {
307 - $files[] = array(
308 - 'location' => '',
309 - 'name' => $file_name,
310 - 'error' => new WP_Error( 'table_import_error_zip_get_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
311 - );
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 + ) );
312 335 continue;
313 336 }
314 337
315 338 $location = wp_tempnam();
@@ -315,38 +338,99 @@
315 338 $location = wp_tempnam();
316 339 $num_written_bytes = file_put_contents( $location, $file_data );
317 340 if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
318 341 @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
319 - $files[] = array(
320 - 'location' => '',
321 - 'name' => $file_name,
322 - 'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
323 - );
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 + ) );
324 346 continue;
325 347 }
326 348
327 - $files[] = array(
349 + $files[] = new File( array(
328 350 'location' => $location,
329 351 'name' => $file_name,
330 - );
352 + ) );
331 353 }
332 354
333 - $zip->close();
355 + $archive->close();
334 356
335 357 return $files;
336 358 }
337 359
338 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 + /**
339 424 * Deletes a file unless the `keep_file` property is set to `true`.
340 425 *
341 426 * @since 2.0.0
342 427 *
343 - * @param array<string, string|WP_Error> $file File that should maybe be deleted.
428 + * @param File $file File that should maybe be deleted.
344 429 */
345 - protected function _maybe_unlink_file( array $file ): void {
346 - if ( ! ( isset( $file['keep_file'] ) && $file['keep_file'] ) && file_exists( $file['location'] ) ) { // @phpstan-ignore-line
347 - // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
348 - @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
349 433 }
350 434 }
351 435
352 436 /**
@@ -355,9 +439,9 @@
355 439 * @since 2.0.0
356 440 *
357 441 * @return array<string, string[]> List of table names and IDs.
358 442 */
359 - protected function _get_list_of_table_names(): array {
443 + protected function get_list_of_table_names(): array {
360 444 $existing_tables = array();
361 445 // Load all table IDs and names for a comparison with the file name.
362 446 $table_ids = TablePress::$model_table->load_all( false );
363 447 foreach ( $table_ids as $table_id ) {
@@ -363,12 +447,12 @@
363 447 foreach ( $table_ids as $table_id ) {
364 448 // Load table, without table data, options, and visibility settings.
365 449 $table = TablePress::$model_table->load( $table_id, false, false );
366 450 if ( ! is_wp_error( $table ) ) {
367 - $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!
368 452 }
369 453 }
370 - return $existing_tables; // @phpstan-ignore-line
454 + return $existing_tables;
371 455 }
372 456
373 457 /**
374 458 * Checks whether the requirements for the PHPSpreadsheet import class are fulfilled or if the legacy import class should be used.
@@ -376,9 +460,9 @@
376 460 * @since 2.0.0
377 461 *
378 462 * @return bool Whether the legacy import class should be used.
379 463 */
380 - protected function _should_use_legacy_import_class(): bool {
464 + protected function should_use_legacy_import_class(): bool {
381 465 // Allow overriding in the import config (coming e.g. from the import form UI).
382 466 if ( $this->import_config['legacy_import'] ) {
383 467 return true;
384 468 }
@@ -403,13 +487,8 @@
403 487 if ( ! $phpspreadsheet_requirements_fulfilled ) {
404 488 return true;
405 489 }
406 490
407 - // Use the legacy import class, if the PHPSpreadsheet files do not exist (e.g. because `composer install` was not run).
408 - if ( ! file_exists( TABLEPRESS_ABSPATH . 'libraries/autoload.php' ) ) {
409 - return true;
410 - }
411 -
412 491 return false;
413 492 }
414 493
415 494 /**
@@ -416,16 +495,16 @@
416 495 * Imports all found/extracted/configured files into TablePress.
417 496 *
418 497 * @since 2.0.0
419 498 *
420 - * @param array<int, array<string, mixed>> $import_files Files that shall be imported.
421 - * @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.
422 501 */
423 - protected function _import_files( array $import_files ): array {
502 + protected function import_files( array $import_files ): array {
424 503 $tables = array();
425 504 $errors = array();
426 505
427 - $use_legacy_import_class = $this->_should_use_legacy_import_class();
506 + $use_legacy_import_class = $this->should_use_legacy_import_class();
428 507
429 508 // Load Import Base Class.
430 509 TablePress::load_file( 'class-import-base.php', 'classes' );
431 510
@@ -430,10 +509,12 @@
430 509 TablePress::load_file( 'class-import-base.php', 'classes' );
431 510
432 511 // Choose the Table Import library based on the PHP version and the filter hook value.
433 512 if ( $use_legacy_import_class ) {
513 + // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
434 514 $this->importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
435 515 } else {
516 + // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
436 517 $this->importer = TablePress::load_class( 'TablePress_Import_PHPSpreadsheet', 'class-import-phpspreadsheet.php', 'classes' );
437 518 }
438 519
439 520 // If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
@@ -439,9 +520,9 @@
439 520 // If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
440 521 if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) && '' !== $this->import_config['existing_table'] ) {
441 522 $valid_import_files = 0;
442 523 foreach ( $import_files as $file ) {
443 - if ( ! isset( $file['error'] ) || ! is_wp_error( $file['error'] ) ) {
524 + if ( ! is_wp_error( $file->error ) ) {
444 525 ++$valid_import_files;
445 526 if ( $valid_import_files > 1 ) {
446 527 $this->import_config['existing_table'] = '';
447 528 break;
@@ -451,9 +532,9 @@
451 532 }
452 533
453 534 // Loop through all import files and import them.
454 535 foreach ( $import_files as $file ) {
455 - if ( isset( $file['error'] ) && is_wp_error( $file['error'] ) ) {
536 + if ( is_wp_error( $file->error ) ) {
456 537 $errors[] = $file;
457 538 continue;
458 539 }
459 540
@@ -458,24 +539,24 @@
458 539 }
459 540
460 541 // Use import method depending on chosen import class.
461 542 if ( $use_legacy_import_class ) {
462 - $table = $this->_load_table_from_file_legacy( $file );
543 + $table = $this->load_table_from_file_legacy( $file );
463 544 } else {
464 - $table = $this->_load_table_from_file_phpspreadsheet( $file );
545 + $table = $this->load_table_from_file_phpspreadsheet( $file );
465 546 }
466 547
467 - $this->_maybe_unlink_file( $file );
548 + $this->maybe_unlink_file( $file );
468 549
469 550 if ( is_wp_error( $table ) ) {
470 - $file['error'] = $table;
551 + $file->error = $table;
471 552 $errors[] = $file;
472 553 continue;
473 554 }
474 555
475 - $table = $this->_import_table( $table, $file );
556 + $table = $this->save_imported_table( $table, $file );
476 557 if ( is_wp_error( $table ) ) {
477 - $file['error'] = $table;
558 + $file->error = $table;
478 559 $errors[] = $file;
479 560 continue;
480 561 }
481 562
@@ -492,14 +573,14 @@
492 573 * Loads a table from a file via the legacy import class.
493 574 *
494 575 * @since 2.0.0
495 576 *
496 - * @param array<string, string|WP_Error> $file File with the table data.
577 + * @param File $file File with the table data.
497 578 * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
498 579 */
499 - 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 */ {
500 581 // Guess the import format from the file extension.
501 - switch ( $file['extension'] ) {
582 + switch ( $file->extension ) {
502 583 case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet.
503 584 case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded).
504 585 case 'xltx': // Excel (OfficeOpenXML) Template.
505 586 case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded).
@@ -524,14 +605,14 @@
524 605 // If no format was found, try finding the format from the first character below.
525 606 $format = '';
526 607 }
527 608
528 - $data = file_get_contents( $file['location'] ); // @phpstan-ignore-line
609 + $data = file_get_contents( $file->location );
529 610 if ( false === $data ) {
530 - return new WP_Error( 'table_import_legacy_data_read', '', $file['location'] );
611 + return new WP_Error( 'table_import_legacy_data_read', '', $file->location );
531 612 }
532 613 if ( '' === $data ) {
533 - return new WP_Error( 'table_import_legacy_data_empty', '', $file['location'] );
614 + return new WP_Error( 'table_import_legacy_data_empty', '', $file->location );
534 615 }
535 616
536 617 // If no format could be determined from the file extension, try guessing from the file content.
537 618 if ( '' === $format ) {
@@ -554,15 +635,15 @@
554 635 $format = 'csv';
555 636 }
556 637
557 638 if ( ! isset( $this->importer->import_formats[ $format ] ) ) {
558 - return new WP_Error( 'table_import_legacy_unknown_format', '', $file['name'] );
639 + return new WP_Error( 'table_import_legacy_unknown_format', '', $file->name );
559 640 }
560 641
561 642 $table = $this->importer->import_table( $format, $data );
562 643
563 644 if ( false === $table ) {
564 - 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 ) );
565 646 }
566 647
567 648 return $table;
568 649 }
@@ -571,13 +652,14 @@
571 652 * Loads a table from a file via the PHPSpreadsheet import class.
572 653 *
573 654 * @since 2.0.0
574 655 *
575 - * @param array<string, string|WP_Error> $file File with the table data.
656 + * @param File $file File with the table data.
576 657 * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
577 658 */
578 - protected function _load_table_from_file_phpspreadsheet( array $file ) /* : array|WP_Error */ {
579 - 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.)
580 662 }
581 663
582 664 /**
583 665 * Imports a loaded table into TablePress.
@@ -583,19 +665,19 @@
583 665 * Imports a loaded table into TablePress.
584 666 *
585 667 * @since 2.0.0
586 668 *
587 - * @param array<string, mixed> $table The table to be imported, either with properties or just the $table['data'] property set.
588 - * @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.
589 671 * @return array<string, mixed>|WP_Error Imported table on success, WP_Error on failure.
590 672 */
591 - protected function _import_table( array $table, array $file ) /* : array|WP_Error */ {
673 + protected function save_imported_table( array $table, File $file ) /* : array|WP_Error */ {
592 674 // If name and description are imported from a new table, use those.
593 675 if ( ! isset( $table['name'] ) ) {
594 - $table['name'] = $file['name'];
676 + $table['name'] = $file->name;
595 677 }
596 678 if ( ! isset( $table['description'] ) ) {
597 - $table['description'] = $file['name'];
679 + $table['description'] = $file->name;
598 680 }
599 681
600 682 $import_type = $this->import_config['type'];
601 683 $existing_table_id = $this->import_config['existing_table'];
@@ -604,11 +686,11 @@
604 686 if ( in_array( $import_type, array( 'replace', 'append' ), true ) && '' === $existing_table_id ) {
605 687 if ( isset( $table['id'] ) ) {
606 688 // If the table already contained a table ID (e.g. for the JSON format), use that.
607 689 $existing_table_id = $table['id'];
608 - } 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 ] ) ) {
609 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.
610 - $existing_table_id = $this->table_names_ids[ $file['name'] ][0]; // @phpstan-ignore-line
692 + $existing_table_id = $this->table_names_ids[ $file->name ][0];
611 693 }
612 694 }
613 695
614 696 // If the table that is to be replaced or appended to does not exist, add the new table instead.
@@ -616,9 +698,9 @@
616 698 $existing_table_id = '';
617 699 $import_type = 'add';
618 700 }
619 701
620 - $table = $this->_import_tablepress_table( $table, $import_type, $existing_table_id );
702 + $table = $this->import_tablepress_table( $table, $import_type, $existing_table_id );
621 703
622 704 return $table;
623 705 }
624 706
@@ -631,14 +713,15 @@
631 713 * @param string $import_type What to do with the imported data: "add", "replace", "append".
632 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.
633 715 * @return array<string, mixed>|WP_Error Table on success, WP_Error on error.
634 716 */
635 - 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 */ {
636 718 // Full JSON format table can contain a table ID, try to keep that, by later changing the imported table ID to this.
637 719 $table_id_in_import = $imported_table['id'] ?? '';
638 720
639 - // 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).
640 - 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' ) ) ) {
641 724 return new WP_Error( 'table_import_replace_append_capability_check_failed', '', $existing_table_id );
642 725 }
643 726
644 727 switch ( $import_type ) {