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-phpspreadsheet.php +64 -34 2.2.5 → 3.4 View file →
@@ -7,8 +7,12 @@
7 7 * @author Tobias Bäthge
8 8 * @since 2.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
14 18 /**
@@ -26,10 +30,10 @@
26 30 *
27 31 * @since 2.0.0
28 32 */
29 33 public function __construct() {
30 - // Load PHPSpreadsheet via the Composer autoloading mechanism.
31 - TablePress::load_file( 'autoload.php', 'libraries' );
34 + // Load PHPSpreadsheet via its autoloading mechanism.
35 + TablePress::load_file( 'autoload.php', 'libraries/vendor' );
32 36 }
33 37
34 38 /**
35 39 * Imports a table from a file.
@@ -35,15 +39,15 @@
35 39 * Imports a table from a file.
36 40 *
37 41 * @since 2.0.0
38 42 *
39 - * @param array<string, mixed> $file File to import.
43 + * @param File $file File to import.
40 44 * @return array<string, mixed>|WP_Error Table array on success, WP_Error on error.
41 45 */
42 - public function import_table( array $file ) /* : array|WP_Error */ {
43 - $data = file_get_contents( $file['location'] );
46 + public function import_table( File $file ) /* : array|WP_Error */ {
47 + $data = file_get_contents( $file->location );
44 48 if ( false === $data ) {
45 - return new WP_Error( 'table_import_phpspreadsheet_data_read', '', $file['location'] );
49 + return new WP_Error( 'table_import_phpspreadsheet_data_read', '', $file->location );
46 50 }
47 51
48 52 // Remove a possible UTF-8 Byte-Order Mark (BOM).
49 53 $bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
@@ -51,18 +55,18 @@
51 55 $data = substr( $data, 3 );
52 56 }
53 57
54 58 if ( '' === $data ) {
55 - return new WP_Error( 'table_import_phpspreadsheet_data_empty', '', $file['location'] );
59 + return new WP_Error( 'table_import_phpspreadsheet_data_empty', '', $file->location );
56 60 }
57 61
58 62 $table = $this->_maybe_import_json( $data );
59 - if ( false !== $table ) {
63 + if ( is_array( $table ) ) {
60 64 return $table;
61 65 }
62 66
63 67 $table = $this->_maybe_import_html( $data );
64 - if ( false !== $table ) {
68 + if ( is_array( $table ) ) {
65 69 return $table;
66 70 }
67 71
68 72 return $this->_import_phpspreadsheet( $file );
@@ -127,9 +131,9 @@
127 131 *
128 132 * @since 2.0.0
129 133 *
130 134 * @param string $data Data to import.
131 - * @return array<string, mixed>|false Table array on success, false if the file is not an HTML file.
135 + * @return array<string, mixed>|WP_Error Table array on success, WP_Error if the file is not an HTML file.
132 136 */
133 137 protected function _maybe_import_html( string $data ) /* : array|false */ {
134 138 TablePress::load_file( 'html-parser.class.php', 'libraries' );
135 139 $table = HTML_Parser::parse( $data );
@@ -135,9 +139,9 @@
135 139 $table = HTML_Parser::parse( $data );
136 140
137 141 // Check if the HTML code could be parsed. If not, this is probably not an HTML file.
138 142 if ( is_wp_error( $table ) ) {
139 - return false;
143 + return $table;
140 144 }
141 145
142 146 $this->pad_array_to_max_cols( $table['data'] );
143 147 return $table;
@@ -147,19 +151,28 @@
147 151 * Tries to import a table via PHPSpreadsheet.
148 152 *
149 153 * @since 2.0.0
150 154 *
151 - * @param array<string, mixed> $file File to import.
155 + * @param File $file File to import.
152 156 * @return array<string, mixed>|WP_Error Table array on success, WP_Error on error.
153 157 */
154 - protected function _import_phpspreadsheet( array $file ) /* : array|WP_Error */ {
158 + protected function _import_phpspreadsheet( File $file ) /* : array|WP_Error */ {
155 159 // Rename the temporary file, as PHPSpreadsheet tries to infer the format from the file's extension.
156 - if ( '' !== $file['extension'] ) {
157 - $temp_file = pathinfo( $file['location'] );
158 - if ( ! isset( $temp_file['extension'] ) || $file['extension'] !== $temp_file['extension'] ) {
159 - $new_location = "{$temp_file['dirname']}/{$temp_file['filename']}.{$file['extension']}"; // @phpstan-ignore-line
160 - if ( rename( $file['location'], $new_location ) ) {
161 - $file['location'] = $new_location;
160 + if ( '' !== $file->extension ) {
161 + $file_data = pathinfo( $file->location );
162 + if ( ! isset( $file_data['extension'] ) || $file->extension !== $file_data['extension'] ) {
163 + $temp_file = wp_tempnam();
164 + $new_location = "{$temp_file}.{$file->extension}";
165 + if ( $file->keep_file ) {
166 + // Copy the file, as the original should be kept.
167 + if ( copy( $file->location, $new_location ) ) {
168 + $file->location = $new_location;
169 + $file->keep_file = false; // Delete the newly created file after the import.
170 + }
171 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
172 + if ( rename( $file->location, $new_location ) ) {
173 + $file->location = $new_location;
174 + }
162 175 }
163 176 }
164 177 }
165 178
@@ -165,9 +178,9 @@
165 178
166 179 try {
167 180 // Treat all cell values as strings, except for formulas (due to recognition of quoted/escaped formulas like `'=A2`).
168 181 \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \TablePress\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder() );
169 - \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false ); // @phpstan-ignore-line
182 + \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false ); // @phpstan-ignore method.notFound
170 183
171 184 /*
172 185 * Try to detect a reader from the file extension and MIME type.
173 186 * Fall back to CSV if no reader could be determined.
@@ -172,15 +185,24 @@
172 185 * Try to detect a reader from the file extension and MIME type.
173 186 * Fall back to CSV if no reader could be determined.
174 187 */
175 188 try {
176 - $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file['location'] );
189 + $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file->location );
177 190 } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
178 191 $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReader( 'Csv' );
179 - // Append .csv to the file name, so that \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::canRead() returns true.
180 - $new_location = $file['location'] . '.csv';
181 - if ( rename( $file['location'], $new_location ) ) {
182 - $file['location'] = $new_location;
192 + // Change the file extension to .csv, so that \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::canRead() returns true.
193 + $temp_file = wp_tempnam();
194 + $new_location = "{$temp_file}.csv";
195 + if ( $file->keep_file ) {
196 + // Copy the file, as the original should be kept.
197 + if ( copy( $file->location, $new_location ) ) {
198 + $file->location = $new_location;
199 + $file->keep_file = false; // Delete the newly created file after the import.
200 + }
201 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
202 + if ( rename( $file->location, $new_location ) ) {
203 + $file->location = $new_location;
204 + }
183 205 }
184 206 }
185 207
186 208 $class_name = get_class( $reader );
@@ -187,10 +209,10 @@
187 209 $class_type = explode( '\\', $class_name );
188 210 $detected_format = strtolower( array_pop( $class_type ) );
189 211
190 212 if ( 'csv' === $detected_format ) {
191 - $reader->setInputEncoding( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING ); // @phpstan-ignore-line
192 - // @phpstan-ignore-next-line
213 + $reader->setInputEncoding( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING ); // @phpstan-ignore method.notFound
214 + // @phpstan-ignore method.notFound, smaller.alwaysFalse (PHPStan thinks that the Composer minimum version will always be fulfilled.)
193 215 $reader->setEscapeCharacter( ( PHP_VERSION_ID < 70400 ) ? "\x0" : '' ); // Disable the proprietary escape mechanism of PHP's fgetcsv() in PHP >= 7.4.
194 216 }
195 217
196 218 $reader->setIncludeCharts( false );
@@ -202,12 +224,12 @@
202 224 }
203 225
204 226 // For formats where it's supported, import only the first sheet.
205 227 if ( in_array( $detected_format, array( 'csv', 'html', 'slk' ), true ) ) {
206 - $reader->setSheetIndex( 0 ); // @phpstan-ignore-line
228 + $reader->setSheetIndex( 0 ); // @phpstan-ignore method.notFound
207 229 }
208 230
209 - $spreadsheet = $reader->load( $file['location'] );
231 + $spreadsheet = $reader->load( $file->location );
210 232 $worksheet = $spreadsheet->getActiveSheet();
211 233 $cell_collection = $worksheet->getCellCollection();
212 234 $comments = $worksheet->getComments();
213 235
@@ -220,12 +242,12 @@
220 242 $max_col = $worksheet->getHighestColumn();
221 243 $max_row = $worksheet->getHighestRow();
222 244
223 245 // Adapted from \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::rangeToArray().
224 - ++$max_col; // Due to for-loop with characters for columns.
246 + \TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper::stringIncrement( $max_col ); // Due to for-loop with characters for columns.
225 247 for ( $row = $min_row; $row <= $max_row; $row++ ) {
226 248 $row_data = array();
227 - for ( $col = $min_col; $col !== $max_col; $col++ ) {
249 + for ( $col = $min_col; $col !== $max_col; \TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper::stringIncrement( $col ) ) {
228 250 $cell_reference = $col . $row;
229 251 if ( ! $cell_collection->has( $cell_reference ) ) {
230 252 $row_data[] = '';
231 253 continue;
@@ -250,8 +272,16 @@
250 272 $style = $spreadsheet->getCellXfByIndex( $cell->getXfIndex() );
251 273
252 274 $format = $style->getNumberFormat()->getFormatCode() ?? \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL;
253 275
276 + /*
277 + * When cells in Excel files are formatted as "Text", quotation marks are removed, due to https://github.com/PHPOffice/PhpSpreadsheet/pull/3344.
278 + * Setting the format to "General" seems to prevent that.
279 + */
280 + if ( \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT === $format && ! is_numeric( $cell_data ) ) {
281 + $format = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL;
282 + }
283 +
254 284 // Fix floating point precision issues with numbers in the "General" Excel .xlsx format.
255 285 if ( 'xlsx' === $detected_format && \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL === $format && is_numeric( $cell_data ) ) {
256 286 $cell_data = (string) (float) $cell_data; // Type-cast strings to float and back.
257 287 }
@@ -257,13 +287,13 @@
257 287 }
258 288 $cell_data = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::toFormattedString(
259 289 $cell_data,
260 290 $format,
261 - array( $this, 'format_color' )
291 + array( $this, 'format_color' ),
262 292 );
263 293
264 294 if ( strlen( $cell_data ) > 1 && '=' === $cell_data[0] ) {
265 - if ( $style->getQuotePrefix() ) {
295 + if ( 'xlsx' === $detected_format && $style->getQuotePrefix() ) {
266 296 // Prepend a ' to quoted/escaped formulas (so that they are shown as text). This is currently not supported (at least) for the XLS format.
267 297 $cell_data = "'{$cell_data}";
268 298 } else {
269 299 // Bail early, to not add inline HTML styling around formulas, as they won't work anymore then.
@@ -350,9 +380,9 @@
350 380 $spreadsheet->disconnectWorksheets();
351 381 unset( $comments, $cell_collection, $worksheet, $spreadsheet );
352 382
353 383 return $table;
354 - } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
384 + } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception | \TablePress\PhpOffice\PhpSpreadsheet\Exception $exception ) {
355 385 return new WP_Error( 'table_import_phpspreadsheet_failed', '', 'Exception: ' . $exception->getMessage() );
356 386 }
357 387 }
358 388