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 +104 -52 2.0.4 → 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,34 +39,34 @@
35 39 * Imports a table from a file.
36 40 *
37 41 * @since 2.0.0
38 42 *
39 - * @param array $file File to import.
40 - * @return array|WP_Error Table array on success, WP_Error on error.
43 + * @param File $file File to import.
44 + * @return array<string, mixed>|WP_Error Table array on success, WP_Error on error.
41 45 */
42 - public function import_table( array $file ) {
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 );
50 - if ( 0 === strncmp( $data, $bom, 3 ) ) {
54 + if ( str_starts_with( $data, $bom ) ) {
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 );
@@ -73,15 +77,17 @@
73 77 *
74 78 * @since 2.0.0
75 79 *
76 80 * @param string $data Data to import.
77 - * @return array|WP_Error|false Table array on success, WP_Error on error, false if the file is not a JSON file.
81 + * @return array<string, mixed>|false Table array on success, false if the file is not a JSON file.
78 82 */
79 - protected function _maybe_import_json( $data ) {
80 - // If the first non-whitespace character is not a { or [, the file is not a supported JSON file.
81 - $data = ltrim( $data );
83 + protected function _maybe_import_json( string $data ) /* : array|false */ {
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 );
@@ -101,9 +107,19 @@
101 107 // JSON data contained only the data of a table, but no options.
102 108 $table = array( 'data' => array() );
103 109 foreach ( $json_table as $row ) {
104 110 // Turn row into indexed arrays with numeric keys.
105 - $table['data'][] = array_values( (array) $row );
111 + $row = array_values( (array) $row );
112 +
113 + // Remove entries of multi-dimensional arrays.
114 + foreach ( $row as &$cell ) {
115 + if ( is_array( $cell ) ) {
116 + $cell = '';
117 + }
118 + }
119 + unset( $cell ); // Unset use-by-reference parameter of foreach loop.
120 +
121 + $table['data'][] = $row;
106 122 }
107 123 }
108 124
109 125 $this->pad_array_to_max_cols( $table['data'] );
@@ -115,17 +131,17 @@
115 131 *
116 132 * @since 2.0.0
117 133 *
118 134 * @param string $data Data to import.
119 - * @return array|WP_Error|false Table array on success, WP_Error on error, false if the file is not a JSON file.
135 + * @return array<string, mixed>|WP_Error Table array on success, WP_Error if the file is not an HTML file.
120 136 */
121 - protected function _maybe_import_html( $data ) {
137 + protected function _maybe_import_html( string $data ) /* : array|false */ {
122 138 TablePress::load_file( 'html-parser.class.php', 'libraries' );
123 139 $table = HTML_Parser::parse( $data );
124 140
125 141 // Check if the HTML code could be parsed. If not, this is probably not an HTML file.
126 142 if ( is_wp_error( $table ) ) {
127 - return false;
143 + return $table;
128 144 }
129 145
130 146 $this->pad_array_to_max_cols( $table['data'] );
131 147 return $table;
@@ -135,19 +151,28 @@
135 151 * Tries to import a table via PHPSpreadsheet.
136 152 *
137 153 * @since 2.0.0
138 154 *
139 - * @param array $file File to import.
140 - * @return array|WP_Error Table array on success, WP_Error on error.
155 + * @param File $file File to import.
156 + * @return array<string, mixed>|WP_Error Table array on success, WP_Error on error.
141 157 */
142 - protected function _import_phpspreadsheet( array $file ) {
158 + protected function _import_phpspreadsheet( File $file ) /* : array|WP_Error */ {
143 159 // Rename the temporary file, as PHPSpreadsheet tries to infer the format from the file's extension.
144 - if ( '' !== $file['extension'] ) {
145 - $temp_file = pathinfo( $file['location'] );
146 - if ( ! isset( $temp_file['extension'] ) || $file['extension'] !== $temp_file['extension'] ) {
147 - $new_location = "{$temp_file['dirname']}/{$temp_file['filename']}.{$file['extension']}";
148 - if ( rename( $file['location'], $new_location ) ) {
149 - $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 + }
150 175 }
151 176 }
152 177 }
153 178
@@ -153,9 +178,9 @@
153 178
154 179 try {
155 180 // Treat all cell values as strings, except for formulas (due to recognition of quoted/escaped formulas like `'=A2`).
156 181 \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \TablePress\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder() );
157 - \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false );
182 + \TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false ); // @phpstan-ignore method.notFound
158 183
159 184 /*
160 185 * Try to detect a reader from the file extension and MIME type.
161 186 * Fall back to CSV if no reader could be determined.
@@ -160,15 +185,24 @@
160 185 * Try to detect a reader from the file extension and MIME type.
161 186 * Fall back to CSV if no reader could be determined.
162 187 */
163 188 try {
164 - $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file['location'] );
189 + $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file->location );
165 190 } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
166 191 $reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReader( 'Csv' );
167 - // Append .csv to the file name, so that \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::canRead() returns true.
168 - $new_location = $file['location'] . '.csv';
169 - if ( rename( $file['location'], $new_location ) ) {
170 - $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 + }
171 205 }
172 206 }
173 207
174 208 $class_name = get_class( $reader );
@@ -175,9 +209,10 @@
175 209 $class_type = explode( '\\', $class_name );
176 210 $detected_format = strtolower( array_pop( $class_type ) );
177 211
178 212 if ( 'csv' === $detected_format ) {
179 - $reader->setInputEncoding( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING );
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.)
180 215 $reader->setEscapeCharacter( ( PHP_VERSION_ID < 70400 ) ? "\x0" : '' ); // Disable the proprietary escape mechanism of PHP's fgetcsv() in PHP >= 7.4.
181 216 }
182 217
183 218 $reader->setIncludeCharts( false );
@@ -189,12 +224,12 @@
189 224 }
190 225
191 226 // For formats where it's supported, import only the first sheet.
192 227 if ( in_array( $detected_format, array( 'csv', 'html', 'slk' ), true ) ) {
193 - $reader->setSheetIndex( 0 );
228 + $reader->setSheetIndex( 0 ); // @phpstan-ignore method.notFound
194 229 }
195 230
196 - $spreadsheet = $reader->load( $file['location'] );
231 + $spreadsheet = $reader->load( $file->location );
197 232 $worksheet = $spreadsheet->getActiveSheet();
198 233 $cell_collection = $worksheet->getCellCollection();
199 234 $comments = $worksheet->getComments();
200 235
@@ -207,12 +242,12 @@
207 242 $max_col = $worksheet->getHighestColumn();
208 243 $max_row = $worksheet->getHighestRow();
209 244
210 245 // Adapted from \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::rangeToArray().
211 - ++$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.
212 247 for ( $row = $min_row; $row <= $max_row; $row++ ) {
213 248 $row_data = array();
214 - for ( $col = $min_col; $col !== $max_col; $col++ ) {
249 + for ( $col = $min_col; $col !== $max_col; \TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper::stringIncrement( $col ) ) {
215 250 $cell_reference = $col . $row;
216 251 if ( ! $cell_collection->has( $cell_reference ) ) {
217 252 $row_data[] = '';
218 253 continue;
@@ -224,10 +259,12 @@
224 259 $row_data[] = '';
225 260 continue;
226 261 }
227 262
263 + $cell_has_hyperlink = $worksheet->hyperlinkExists( $cell_reference ) && ! $worksheet->getHyperlink( $cell_reference )->isInternal();
264 +
228 265 if ( $value instanceof \TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText ) {
229 - $cell_data = $this->parse_rich_text( $value );
266 + $cell_data = $this->parse_rich_text( $value, $cell_has_hyperlink );
230 267 } else {
231 268 $cell_data = (string) $value;
232 269 }
233 270
@@ -232,16 +269,31 @@
232 269 }
233 270
234 271 // Apply data type formatting.
235 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 + }
236 288 $cell_data = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::toFormattedString(
237 289 $cell_data,
238 - $style->getNumberFormat() ? $style->getNumberFormat()->getFormatCode() : \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL,
239 - array( $this, 'format_color' )
290 + $format,
291 + array( $this, 'format_color' ),
240 292 );
241 293
242 294 if ( strlen( $cell_data ) > 1 && '=' === $cell_data[0] ) {
243 - if ( $style->getQuotePrefix() ) {
295 + if ( 'xlsx' === $detected_format && $style->getQuotePrefix() ) {
244 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.
245 297 $cell_data = "'{$cell_data}";
246 298 } else {
247 299 // Bail early, to not add inline HTML styling around formulas, as they won't work anymore then.
@@ -249,10 +301,8 @@
249 301 continue;
250 302 }
251 303 }
252 304
253 - $cell_has_hyperlink = $worksheet->hyperlinkExists( $cell_reference ) && ! $worksheet->getHyperlink( $cell_reference )->isInternal();
254 -
255 305 $font = $style->getFont();
256 306
257 307 if ( $font->getSuperscript() ) {
258 308 $cell_data = "<sup>{$cell_data}</sup>";
@@ -280,9 +330,9 @@
280 330 }
281 331
282 332 // Convert Hyperlinks to HTML code.
283 333 if ( $cell_has_hyperlink ) {
284 - $url = esc_url( $worksheet->getHyperlink( $cell_reference )->getUrl() );
334 + $url = $worksheet->getHyperlink( $cell_reference )->getUrl();
285 335 if ( '' !== $url ) {
286 336 $title = $worksheet->getHyperlink( $cell_reference )->getTooltip();
287 337 if ( '' !== $title ) {
288 338 $title = ' title="' . esc_attr( $title ) . '"';
@@ -330,9 +380,9 @@
330 380 $spreadsheet->disconnectWorksheets();
331 381 unset( $comments, $cell_collection, $worksheet, $spreadsheet );
332 382
333 383 return $table;
334 - } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
384 + } catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception | \TablePress\PhpOffice\PhpSpreadsheet\Exception $exception ) {
335 385 return new WP_Error( 'table_import_phpspreadsheet_failed', '', 'Exception: ' . $exception->getMessage() );
336 386 }
337 387 }
338 388
@@ -338,12 +388,13 @@
338 388
339 389 /**
340 390 * Parses PHPSpreadsheet RichText elements and converts formatting to HTML tags.
341 391 *
342 - * @param RichText $value RichText element.
392 + * @param \TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText $value RichText element.
393 + * @param bool $cell_has_hyperlink Whether the cell has a hyperlink.
343 394 * @return string Cell value with HTML formatting.
344 395 */
345 - protected function parse_rich_text( $value ) {
396 + protected function parse_rich_text( \TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText $value, bool $cell_has_hyperlink ): string {
346 397 $cell_data = '';
347 398 $elements = $value->getRichTextElements();
348 399 foreach ( $elements as $element ) {
349 400 $element_data = $element->getText();
@@ -370,9 +421,10 @@
370 421 if ( $font->getItalic() ) {
371 422 $element_data = "<em>{$element_data}</em>";
372 423 }
373 424 $color = $font->getColor()->getRGB();
374 - if ( '' !== $color ) {
425 + if ( '' !== $color && '000000' !== $color && ! $cell_has_hyperlink ) {
426 + // Don't add the span if the color is black, as that's the default, or if it's in a hyperlink.
375 427 $color_css = esc_attr( "color:#{$color};" );
376 428 $element_data = "<span style=\"{$color_css}\">{$element_data}</span>";
377 429 }
378 430 }
@@ -388,9 +440,9 @@
388 440 * @param string $value Plain formatted value without color.
389 441 * @param string $format_code Format code.
390 442 * @return string Value with color format applied.
391 443 */
392 - public function format_color( $value, $format_code ) {
444 + public function format_color( string $value, string $format_code ): string {
393 445 // Color information, e.g. [Red] is always at the beginning of the format code.
394 446 $color = '';
395 447 if ( 1 === preg_match( '/^\\[[a-zA-Z]+\\]/', $format_code, $matches ) ) {
396 448 $color = str_replace( array( '[', ']' ), '', $matches[0] );