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