PluginProbe
TablePress – Tables in WordPress made easy / 1.14
TablePress – Tables in WordPress made easy v1.14
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
tablepress / classes / class-import.php

class-import.php in TablePress – Tables in WordPress made easy 1.14, at classes/class-import.php

468 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TablePress Table Import Class
4 *
5 * @package TablePress
6 * @subpackage Export/Import
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 // Prohibit direct script loading.
12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13
14 /**
15 * TablePress Table Import Class
16 * @package TablePress
17 * @subpackage Export/Import
18 * @author Tobias Bäthge
19 * @since 1.0.0
20 */
21 class TablePress_Import {
22
23 /**
24 * File/Data Formats that are available for import.
25 *
26 * @since 1.0.0
27 * @var array
28 */
29 public $import_formats = array();
30
31 /**
32 * Whether ZIP archive support is available in the PHP installation on the server.
33 *
34 * @since 1.0.0
35 * @var bool
36 */
37 public $zip_support_available = false;
38
39 /**
40 * Whether HTML import support is available in the PHP installation on the server.
41 *
42 * @since 1.0.0
43 * @var bool
44 */
45 public $html_import_support_available = false;
46
47 /**
48 * Data to be imported.
49 *
50 * @since 1.0.0
51 * @var string
52 */
53 protected $import_data;
54
55 /**
56 * Imported table.
57 *
58 * @since 1.0.0
59 * @var array|false
60 */
61 protected $imported_table = false;
62
63 /**
64 * Initialize the Import class.
65 *
66 * @since 1.0.0
67 */
68 public function __construct() {
69 /** This filter is documented in the WordPress function unzip_file() in wp-admin/includes/file.php */
70 if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
71 $this->zip_support_available = true;
72 }
73
74 if ( class_exists( 'DOMDocument', false ) && function_exists( 'simplexml_import_dom' ) && function_exists( 'libxml_use_internal_errors' ) ) {
75 $this->html_import_support_available = true;
76 }
77
78 // Initiate here, because function call not possible outside a class method.
79 $this->import_formats = array();
80 $this->import_formats['csv'] = __( 'CSV - Character-Separated Values', 'tablepress' );
81 if ( $this->html_import_support_available ) {
82 $this->import_formats['html'] = __( 'HTML - Hypertext Markup Language', 'tablepress' );
83 }
84 $this->import_formats['json'] = __( 'JSON - JavaScript Object Notation', 'tablepress' );
85 $this->import_formats['xls'] = __( 'XLS - Microsoft Excel 97-2003 (experimental)', 'tablepress' );
86 $this->import_formats['xlsx'] = __( 'XLSX - Microsoft Excel 2007-2019 (experimental)', 'tablepress' );
87 }
88
89 /**
90 * Import a table.
91 *
92 * @since 1.0.0
93 *
94 * @param string $format Import format.
95 * @param string $data Data to import.
96 * @return array|false Table array on success, false on error.
97 */
98 public function import_table( $format, $data ) {
99 $this->import_data = apply_filters( 'tablepress_import_table_data', $data, $format );
100
101 if ( ! in_array( $format, array( 'xlsx', 'xls' ), true ) ) {
102 $this->fix_table_encoding();
103 }
104
105 switch ( $format ) {
106 case 'csv':
107 $this->import_csv();
108 break;
109 case 'html':
110 $this->import_html();
111 break;
112 case 'json':
113 $this->import_json();
114 break;
115 case 'xlsx':
116 $this->import_xlsx();
117 break;
118 case 'xls':
119 $this->import_xls();
120 break;
121 default:
122 return false;
123 }
124
125 return $this->imported_table;
126 }
127
128 /**
129 * Import CSV data.
130 *
131 * @since 1.0.0
132 */
133 protected function import_csv() {
134 $csv_parser = TablePress::load_class( 'CSV_Parser', 'csv-parser.class.php', 'libraries' );
135 $csv_parser->load_data( $this->import_data );
136 $delimiter = $csv_parser->find_delimiter();
137 $data = $csv_parser->parse( $delimiter );
138 $this->imported_table = array( 'data' => $this->pad_array_to_max_cols( $data ) );
139 }
140
141 /**
142 * Import HTML data.
143 *
144 * @since 1.0.0
145 */
146 protected function import_html() {
147 if ( ! $this->html_import_support_available ) {
148 return false;
149 }
150
151 if ( false === stripos( $this->import_data, '<table' ) || false === stripos( $this->import_data, '</table>' ) ) {
152 $this->imported_table = false;
153 return;
154 }
155
156 // Prepend XML declaration, for better encoding support.
157 $full_html = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . $this->import_data;
158 if ( function_exists( 'libxml_disable_entity_loader' ) ) {
159 /*
160 * Don't expand external entities, see https://websec.io/2012/08/27/Preventing-XXE-in-PHP.html.
161 * Silence warnings as the function is deprecated in PHP 8, but can be necessary with LIBXML_NOENT being defined, see https://core.trac.wordpress.org/changeset/50714.
162 */
163 @libxml_disable_entity_loader( true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
164 }
165 // No warnings/errors raised, but stored internally.
166 libxml_use_internal_errors( true );
167 $dom = new DOMDocument( '1.0', 'UTF-8' );
168 // No strict checking for invalid HTML.
169 $dom->strictErrorChecking = false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
170 $dom->loadHTML( $full_html );
171 if ( false === $dom ) {
172 $this->imported_table = false;
173 return;
174 }
175 $dom_tables = $dom->getElementsByTagName( 'table' );
176 if ( 0 === count( $dom_tables ) ) {
177 $this->imported_table = false;
178 return;
179 }
180 libxml_clear_errors(); // Clear errors so that we only catch those inside the table in the next line.
181 $table = simplexml_import_dom( $dom_tables->item( 0 ) );
182 if ( false === $table ) {
183 $this->imported_table = false;
184 return;
185 }
186
187 $errors = libxml_get_errors();
188 libxml_clear_errors();
189 if ( ! empty( $errors ) ) {
190 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />';
191 foreach ( $errors as $error ) {
192 switch ( $error->level ) {
193 case LIBXML_ERR_WARNING:
194 $output .= "Warning {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
195 break;
196 case LIBXML_ERR_ERROR:
197 $output .= "Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
198 break;
199 case LIBXML_ERR_FATAL:
200 $output .= "Fatal Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
201 break;
202 }
203 }
204 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
205 }
206
207 $html_table = array(
208 'data' => array(),
209 'options' => array(),
210 );
211 if ( isset( $table->thead ) ) {
212 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->thead[0]->tr ) );
213 $html_table['options']['table_head'] = true;
214 }
215 if ( isset( $table->tbody ) ) {
216 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tbody[0]->tr ) );
217 }
218 if ( isset( $table->tr ) ) {
219 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tr ) );
220 }
221 if ( isset( $table->tfoot ) ) {
222 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tfoot[0]->tr ) );
223 $html_table['options']['table_foot'] = true;
224 }
225
226 $html_table['data'] = $this->pad_array_to_max_cols( $html_table['data'] );
227 $this->imported_table = $html_table;
228 }
229
230 /**
231 * Helper for HTML import.
232 *
233 * @since 1.0.0
234 *
235 * @param SimpleXMLElement $element XMLElement.
236 * @return array SimpleXMLElement exported to an array.
237 */
238 protected function _import_html_rows( $element ) {
239 $rows = array();
240 foreach ( $element as $row ) {
241 $new_row = array();
242 foreach ( $row as $cell ) {
243 // Get text between <td>...</td>, or <th>...</th>, possibly with attributes.
244 if ( 1 === preg_match( '#<t(?:d|h).*?>(.*)</t(?:d|h)>#is', $cell->asXML(), $matches ) ) {
245 /*
246 * Decode HTML entities again, as there might be some left especially in attributes of HTML tags in the cells,
247 * see https://secure.php.net/manual/en/simplexmlelement.asxml.php#107137.
248 */
249 $matches[1] = html_entity_decode( $matches[1], ENT_NOQUOTES, 'UTF-8' );
250 $new_row[] = $matches[1];
251
252 // Look for colspan and add correct number of cells.
253 if ( 1 === preg_match( '#<t(?:d|h).*colspan="(\d+)".*?>#is', $cell->asXml(), $matches ) ) {
254 for ( $i = 1; $i < (int) $matches[1]; $i++ ) {
255 $new_row[] = '#colspan#';
256 }
257 }
258 } else {
259 $new_row[] = '';
260 }
261 }
262 $rows[] = $new_row;
263 }
264 return $rows;
265 }
266
267 /**
268 * Import JSON data.
269 *
270 * @since 1.0.0
271 */
272 protected function import_json() {
273 $json_table = json_decode( $this->import_data, true );
274
275 // Check if JSON could be decoded.
276 if ( is_null( $json_table ) ) {
277 // If possible, try to find out what error prevented the JSON from being decoded.
278 $json_error = 'The error could not be determined.';
279 $json_error_msg = json_last_error_msg();
280 if ( false !== $json_error_msg ) {
281 $json_error = $json_error_msg;
282 }
283 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . "</strong><br /><br />JSON error: {$json_error}<br />";
284 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
285 }
286
287 // Specifically cast to an array again.
288 $json_table = (array) $json_table;
289
290 if ( isset( $json_table['data'] ) ) {
291 // JSON data contained a full export.
292 $table = $json_table;
293 } else {
294 // JSON data contained only the data of a table, but no options.
295 $table = array( 'data' => array() );
296 foreach ( $json_table as $row ) {
297 $table['data'][] = array_values( (array) $row );
298 }
299 }
300
301 $table['data'] = $this->pad_array_to_max_cols( $table['data'] );
302 $this->imported_table = $table;
303 }
304
305 /**
306 * Import Microsoft Excel 97-2003 data.
307 *
308 * @since 1.1.0
309 */
310 protected function import_xls() {
311 $excel_reader = TablePress::load_class( 'Spreadsheet_Excel_Reader', 'excel-reader.class.php', 'libraries', $this->import_data );
312
313 // Loop through Excel file and retrieve value and colspan/rowspan properties for each cell.
314 $sheet = 0; // 0 means first sheet of the Workbook
315 $table = array();
316 for ( $row = 1; $row <= $excel_reader->rowcount( $sheet ); $row++ ) {
317 $table_row = array();
318 for ( $col = 1; $col <= $excel_reader->colcount( $sheet ); $col++ ) {
319 $cell = array();
320 $cell['rowspan'] = $excel_reader->rowspan( $row, $col, $sheet );
321 $cell['colspan'] = $excel_reader->colspan( $row, $col, $sheet );
322 $cell['val'] = $excel_reader->val( $row, $col, $sheet );
323 $table_row[] = $cell;
324 }
325 $table[] = $table_row;
326 }
327
328 // Transform colspan/rowspan properties to TablePress equivalent (cell content).
329 foreach ( $table as $row_idx => $row ) {
330 foreach ( $row as $col_idx => $cell ) {
331 if ( 1 === $cell['rowspan'] && 1 === $cell['colspan'] ) {
332 continue;
333 }
334
335 if ( 1 < $cell['colspan'] ) {
336 for ( $i = 1; $i < $cell['colspan']; $i++ ) {
337 $table[ $row_idx ][ $col_idx + $i ]['val'] = '#colspan#';
338 }
339 }
340 if ( 1 < $cell['rowspan'] ) {
341 for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
342 $table[ $row_idx + $i ][ $col_idx ]['val'] = '#rowspan#';
343 }
344 }
345
346 if ( 1 < $cell['rowspan'] && 1 < $cell['colspan'] ) {
347 for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
348 for ( $j = 1; $j < $cell['colspan']; $j++ ) {
349 $table[ $row_idx + $i ][ $col_idx + $j ]['val'] = '#span#';
350 }
351 }
352 }
353 }
354 }
355
356 // Flatten value property to two-dimensional array.
357 $result_table = array();
358 foreach ( $table as $row_idx => $row ) {
359 $table_row = array();
360 foreach ( $row as $col_idx => $cell ) {
361 $table_row[] = (string) $cell['val'];
362 }
363 $result_table[] = $table_row;
364 }
365
366 $this->imported_table = array( 'data' => $this->pad_array_to_max_cols( $result_table ) );
367 }
368
369 /**
370 * Import Microsoft Excel 2007-2019 data.
371 *
372 * @since 1.1.0
373 */
374 protected function import_xlsx() {
375 TablePress::load_file( 'simplexlsx.class.php', 'libraries' );
376 $xlsx_file = SimpleXLSX::parse( $this->import_data, true );
377
378 if ( ! $xlsx_file ) {
379 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />' . SimpleXLSX::parseError() . '<br />';
380 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
381 }
382
383 $this->imported_table = array( 'data' => $xlsx_file->rows() );
384 }
385
386 /**
387 * Make sure array is rectangular with $max_cols columns in every row.
388 *
389 * @since 1.0.0
390 *
391 * @param array $array Two-dimensional array to be padded.
392 * @return array Padded array.
393 */
394 public function pad_array_to_max_cols( array $array ) {
395 $rows = count( $array );
396 $rows = ( $rows > 0 ) ? $rows : 1;
397 $max_columns = $this->count_max_columns( $array );
398 $max_columns = ( $max_columns > 0 ) ? $max_columns : 1;
399 // array_map wants arrays as additional parameters, so we create one with the max_columns to pad to and one with the value to use (empty string).
400 $max_columns_array = array_fill( 1, $rows, $max_columns );
401 $pad_values_array = array_fill( 1, $rows, '' );
402 return array_map( 'array_pad', $array, $max_columns_array, $pad_values_array );
403 }
404
405 /**
406 * Get the highest number of columns in the rows.
407 *
408 * @since 1.0.0
409 *
410 * @param array $array Two-dimensional array.
411 * @return int Highest number of columns in the rows of the array.
412 */
413 protected function count_max_columns( array $array ) {
414 $max_columns = 0;
415 foreach ( $array as $row_idx => $row ) {
416 $num_columns = count( $row );
417 $max_columns = max( $num_columns, $max_columns );
418 }
419 return $max_columns;
420 }
421
422 /**
423 * Fixes the encoding to UTF-8 for the entire string that is to be imported.
424 *
425 * @since 1.0.0
426 *
427 * @link http://stevephillips.me/blog/dealing-php-and-character-encoding
428 */
429 protected function fix_table_encoding() {
430 // Check and remove possible UTF-8 Byte-Order Mark (BOM).
431 $bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
432 if ( 0 === strncmp( $this->import_data, $bom, 3 ) ) {
433 $this->import_data = substr( $this->import_data, 3 );
434 // If data has a BOM, it's UTF-8, so further checks unnecessary.
435 return;
436 }
437
438 // Require the iconv() function for the following checks.
439 if ( ! function_exists( 'iconv' ) ) {
440 return;
441 }
442
443 // Check for possible UTF-16 BOMs ("little endian" and "big endian") and try to convert the data to UTF-8.
444 if ( "\xFF\xFE" === substr( $this->import_data, 0, 2 ) || "\xFE\xFF" === substr( $this->import_data, 0, 2 ) ) {
445 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
446 $data = @iconv( 'UTF-16', 'UTF-8', $this->import_data );
447 if ( false !== $data ) {
448 $this->import_data = $data;
449 return;
450 }
451 }
452
453 // Detect the character encoding and convert to UTF-8, if it's different.
454 if ( function_exists( 'mb_detect_encoding' ) ) {
455 $current_encoding = mb_detect_encoding( $this->import_data, 'ASCII, UTF-8, ISO-8859-1' );
456 if ( 'UTF-8' !== $current_encoding ) {
457 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
458 $data = @iconv( $current_encoding, 'UTF-8', $this->import_data );
459 if ( false !== $data ) {
460 $this->import_data = $data;
461 return;
462 }
463 }
464 }
465 }
466
467 } // class TablePress_Import
468