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