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 +77 -38 2.2.1 → 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 );
@@ -76,12 +80,14 @@
76 80 * @param string $data Data to import.
77 81 * @return array<string, mixed>|false Table array on success, false if the file is not a JSON file.
78 82 */
79 83 protected function _maybe_import_json( string $data ) /* : array|false */ {
80 - // If the first non-whitespace character is not a { or [, the file is not a supported JSON file.
81 - $data = ltrim( $data );
84 + $data = trim( $data );
85 +
86 + // If the file does not begin / end with [ / ] or { / }, it's not a supported JSON file.
82 87 $first_character = $data[0];
83 - if ( '{' !== $first_character && '[' !== $first_character ) {
88 + $last_character = $data[-1];
89 + if ( ! ( '[' === $first_character && ']' === $last_character ) && ! ( '{' === $first_character && '}' === $last_character ) ) {
84 90 return false;
85 91 }
86 92
87 93 $json_table = json_decode( $data, true );
@@ -125,9 +131,9 @@
125 131 *
126 132 * @since 2.0.0
127 133 *
128 134 * @param string $data Data to import.
129 - * @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.
130 136 */
131 137 protected function _maybe_import_html( string $data ) /* : array|false */ {
132 138 TablePress::load_file( 'html-parser.class.php', 'libraries' );
133 139 $table = HTML_Parser::parse( $data );
@@ -133,9 +139,9 @@
133 139 $table = HTML_Parser::parse( $data );
134 140
135 141 // Check if the HTML code could be parsed. If not, this is probably not an HTML file.
136 142 if ( is_wp_error( $table ) ) {
137 - return false;
143 + return $table;
138 144 }
139 145
140 146 $this->pad_array_to_max_cols( $table['data'] );
141 147 return $table;
@@ -145,19 +151,28 @@
145 151 * Tries to import a table via PHPSpreadsheet.
146 152 *
147 153 * @since 2.0.0
148 154 *
149 - * @param array<string, mixed> $file File to import.
155 + * @param File $file File to import.
150 156 * @return array<string, mixed>|WP_Error Table array on success, WP_Error on error.
151 157 */
152 - protected function _import_phpspreadsheet( array $file ) /* : array|WP_Error */ {
158 + protected function _import_phpspreadsheet( File $file ) /* : array|WP_Error */ {
153 159 // Rename the temporary file, as PHPSpreadsheet tries to infer the format from the file's extension.
154 - if ( '' !== $file['extension'] ) {
155 - $temp_file = pathinfo( $file['location'] );
156 - if ( ! isset( $temp_file['extension'] ) || $file['extension'] !== $temp_file['extension'] ) {
157 - $new_location = "{$temp_file['dirname']}/{$temp_file['filename']}.{$file['extension']}"; // @phpstan-ignore-line
158 - if ( rename( $file['location'], $new_location ) ) {
159 - $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 + }
160 175 }
161 176 }
162 177 }
163 178
@@ -163,9 +178,9 @@
163 178
164 179 try {
165 180 // Treat all cell values as strings, except for formulas (due to recognition of quoted/escaped formulas like `'=A2`).
166 181 \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \TablePress\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder() );
167 - \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false ); // @phpstan-ignore-line
182 + \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false ); // @phpstan-ignore method.notFound
168 183
169 184 /*
170 185 * Try to detect a reader from the file extension and MIME type.
171 186 * Fall back to CSV if no reader could be determined.
@@ -170,15 +185,24 @@
170 185 * Try to detect a reader from the file extension and MIME type.
171 186 * Fall back to CSV if no reader could be determined.
172 187 */
173 188 try {
174 - $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file['location'] );
189 + $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file->location );
175 190 } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
176 191 $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReader( 'Csv' );
177 - // Append .csv to the file name, so that \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::canRead() returns true.
178 - $new_location = $file['location'] . '.csv';
179 - if ( rename( $file['location'], $new_location ) ) {
180 - $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 + }
181 205 }
182 206 }
183 207
184 208 $class_name = get_class( $reader );
@@ -185,10 +209,10 @@
185 209 $class_type = explode( '\\', $class_name );
186 210 $detected_format = strtolower( array_pop( $class_type ) );
187 211
188 212 if ( 'csv' === $detected_format ) {
189 - $reader->setInputEncoding( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING ); // @phpstan-ignore-line
190 - // @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.)
191 215 $reader->setEscapeCharacter( ( PHP_VERSION_ID < 70400 ) ? "\x0" : '' ); // Disable the proprietary escape mechanism of PHP's fgetcsv() in PHP >= 7.4.
192 216 }
193 217
194 218 $reader->setIncludeCharts( false );
@@ -200,12 +224,12 @@
200 224 }
201 225
202 226 // For formats where it's supported, import only the first sheet.
203 227 if ( in_array( $detected_format, array( 'csv', 'html', 'slk' ), true ) ) {
204 - $reader->setSheetIndex( 0 ); // @phpstan-ignore-line
228 + $reader->setSheetIndex( 0 ); // @phpstan-ignore method.notFound
205 229 }
206 230
207 - $spreadsheet = $reader->load( $file['location'] );
231 + $spreadsheet = $reader->load( $file->location );
208 232 $worksheet = $spreadsheet->getActiveSheet();
209 233 $cell_collection = $worksheet->getCellCollection();
210 234 $comments = $worksheet->getComments();
211 235
@@ -218,12 +242,12 @@
218 242 $max_col = $worksheet->getHighestColumn();
219 243 $max_row = $worksheet->getHighestRow();
220 244
221 245 // Adapted from \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::rangeToArray().
222 - ++$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.
223 247 for ( $row = $min_row; $row <= $max_row; $row++ ) {
224 248 $row_data = array();
225 - for ( $col = $min_col; $col !== $max_col; $col++ ) {
249 + for ( $col = $min_col; $col !== $max_col; \TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper::stringIncrement( $col ) ) {
226 250 $cell_reference = $col . $row;
227 251 if ( ! $cell_collection->has( $cell_reference ) ) {
228 252 $row_data[] = '';
229 253 continue;
@@ -245,16 +269,31 @@
245 269 }
246 270
247 271 // Apply data type formatting.
248 272 $style = $spreadsheet->getCellXfByIndex( $cell->getXfIndex() );
273 +
274 + $format = $style->getNumberFormat()->getFormatCode() ?? \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL;
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 +
284 + // Fix floating point precision issues with numbers in the "General" Excel .xlsx format.
285 + if ( 'xlsx' === $detected_format && \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL === $format && is_numeric( $cell_data ) ) {
286 + $cell_data = (string) (float) $cell_data; // Type-cast strings to float and back.
287 + }
249 288 $cell_data = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::toFormattedString(
250 289 $cell_data,
251 - $style->getNumberFormat() ? $style->getNumberFormat()->getFormatCode() : \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL, // @phpstan-ignore-line
252 - array( $this, 'format_color' )
290 + $format,
291 + array( $this, 'format_color' ),
253 292 );
254 293
255 294 if ( strlen( $cell_data ) > 1 && '=' === $cell_data[0] ) {
256 - if ( $style->getQuotePrefix() ) {
295 + if ( 'xlsx' === $detected_format && $style->getQuotePrefix() ) {
257 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.
258 297 $cell_data = "'{$cell_data}";
259 298 } else {
260 299 // Bail early, to not add inline HTML styling around formulas, as they won't work anymore then.
@@ -341,9 +380,9 @@
341 380 $spreadsheet->disconnectWorksheets();
342 381 unset( $comments, $cell_collection, $worksheet, $spreadsheet );
343 382
344 383 return $table;
345 - } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
384 + } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception | \TablePress\PhpOffice\PhpSpreadsheet\Exception $exception ) {
346 385 return new WP_Error( 'table_import_phpspreadsheet_failed', '', 'Exception: ' . $exception->getMessage() );
347 386 }
348 387 }
349 388