PluginProbe
TablePress – Tables in WordPress made easy / 1.9.2
TablePress – Tables in WordPress made easy v1.9.2
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.9.2, at classes/class-import.php

463 lines 14.0 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
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-2013 (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 bool|array False on error, table array on success.
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' ) ) ) {
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 // Don't expand external entities, see https://websec.io/2012/08/27/Preventing-XXE-in-PHP.html.
160 libxml_disable_entity_loader( true );
161 }
162 // No warnings/errors raised, but stored internally.
163 libxml_use_internal_errors( true );
164 $dom = new DOMDocument( '1.0', 'UTF-8' );
165 // No strict checking for invalid HTML.
166 $dom->strictErrorChecking = false;
167 $dom->loadHTML( $full_html );
168 if ( false === $dom ) {
169 $this->imported_table = false;
170 return;
171 }
172 $dom_tables = $dom->getElementsByTagName( 'table' );
173 if ( 0 === count( $dom_tables ) ) {
174 $this->imported_table = false;
175 return;
176 }
177 libxml_clear_errors(); // Clear errors so that we only catch those inside the table in the next line.
178 $table = simplexml_import_dom( $dom_tables->item( 0 ) );
179 if ( false === $table ) {
180 $this->imported_table = false;
181 return;
182 }
183
184 $errors = libxml_get_errors();
185 libxml_clear_errors();
186 if ( ! empty( $errors ) ) {
187 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />';
188 foreach ( $errors as $error ) {
189 switch ( $error->level ) {
190 case LIBXML_ERR_WARNING:
191 $output .= "Warning {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
192 break;
193 case LIBXML_ERR_ERROR:
194 $output .= "Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
195 break;
196 case LIBXML_ERR_FATAL:
197 $output .= "Fatal Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
198 break;
199 }
200 }
201 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
202 }
203
204 $html_table = array(
205 'data' => array(),
206 'options' => array(),
207 );
208 if ( isset( $table->thead ) ) {
209 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->thead[0]->tr ) );
210 $html_table['options']['table_head'] = true;
211 }
212 if ( isset( $table->tbody ) ) {
213 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tbody[0]->tr ) );
214 }
215 if ( isset( $table->tr ) ) {
216 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tr ) );
217 }
218 if ( isset( $table->tfoot ) ) {
219 $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tfoot[0]->tr ) );
220 $html_table['options']['table_foot'] = true;
221 }
222
223 $html_table['data'] = $this->pad_array_to_max_cols( $html_table['data'] );
224 $this->imported_table = $html_table;
225 }
226
227 /**
228 * Helper for HTML import.
229 *
230 * @since 1.0.0
231 *
232 * @param SimpleXMLElement $element XMLElement.
233 * @return array SimpleXMLElement exported to an array.
234 */
235 protected function _import_html_rows( $element ) {
236 $rows = array();
237 foreach ( $element as $row ) {
238 $new_row = array();
239 foreach ( $row as $cell ) {
240 // Get text between <td>...</td>, or <th>...</th>, possibly with attributes.
241 if ( 1 === preg_match( '#<t(?:d|h).*?>(.*)</t(?:d|h)>#is', $cell->asXML(), $matches ) ) {
242 /*
243 * Decode HTML entities again, as there might be some left especially in attributes of HTML tags in the cells,
244 * see https://secure.php.net/manual/en/simplexmlelement.asxml.php#107137.
245 */
246 $matches[1] = html_entity_decode( $matches[1], ENT_NOQUOTES, 'UTF-8' );
247 $new_row[] = $matches[1];
248
249 // Look for colspan and add correct number of cells.
250 if ( 1 === preg_match( '#<t(?:d|h).*colspan="(\d+)".*?>#is', $cell->asXml(), $matches ) ) {
251 for ( $i = 1; $i < (int) $matches[1]; $i++ ) {
252 $new_row[] = '#colspan#';
253 }
254 }
255 } else {
256 $new_row[] = '';
257 }
258 }
259 $rows[] = $new_row;
260 }
261 return $rows;
262 }
263
264 /**
265 * Import JSON data.
266 *
267 * @since 1.0.0
268 */
269 protected function import_json() {
270 $json_table = json_decode( $this->import_data, true );
271
272 // Check if JSON could be decoded.
273 if ( is_null( $json_table ) ) {
274 // If possible, try to find out what error prevented the JSON from being decoded.
275 $json_error = 'The error could not be determined.';
276 $json_error_msg = json_last_error_msg();
277 if ( false !== $json_error_msg ) {
278 $json_error = $json_error_msg;
279 }
280 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . "</strong><br /><br />JSON error: {$json_error}<br />";
281 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
282 } else {
283 // Specifically cast to an array again.
284 $json_table = (array) $json_table;
285 }
286
287 if ( isset( $json_table['data'] ) ) {
288 // JSON data contained a full export.
289 $table = $json_table;
290 } else {
291 // JSON data contained only the data of a table, but no options.
292 $table = array( 'data' => array() );
293 foreach ( $json_table as $row ) {
294 $table['data'][] = array_values( (array) $row );
295 }
296 }
297
298 $table['data'] = $this->pad_array_to_max_cols( $table['data'] );
299 $this->imported_table = $table;
300 }
301
302 /**
303 * Import Microsoft Excel 97-2003 data.
304 *
305 * @since 1.1.0
306 */
307 protected function import_xls() {
308 $excel_reader = TablePress::load_class( 'Spreadsheet_Excel_Reader', 'excel-reader.class.php', 'libraries', $this->import_data );
309
310 // Loop through Excel file and retrieve value and colspan/rowspan properties for each cell.
311 $sheet = 0; // 0 means first sheet of the Workbook
312 $table = array();
313 for ( $row = 1; $row <= $excel_reader->rowcount( $sheet ); $row++ ) {
314 $table_row = array();
315 for ( $col = 1; $col <= $excel_reader->colcount( $sheet ); $col++ ) {
316 $cell = array();
317 $cell['rowspan'] = $excel_reader->rowspan( $row, $col, $sheet );
318 $cell['colspan'] = $excel_reader->colspan( $row, $col, $sheet );
319 $cell['val'] = $excel_reader->val( $row, $col, $sheet );
320 $table_row[] = $cell;
321 }
322 $table[] = $table_row;
323 }
324
325 // Transform colspan/rowspan properties to TablePress equivalent (cell content).
326 foreach ( $table as $row_idx => $row ) {
327 foreach ( $row as $col_idx => $cell ) {
328 if ( 1 === $cell['rowspan'] && 1 === $cell['colspan'] ) {
329 continue;
330 }
331
332 if ( 1 < $cell['colspan'] ) {
333 for ( $i = 1; $i < $cell['colspan']; $i++ ) {
334 $table[ $row_idx ][ $col_idx + $i ]['val'] = '#colspan#';
335 }
336 }
337 if ( 1 < $cell['rowspan'] ) {
338 for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
339 $table[ $row_idx + $i ][ $col_idx ]['val'] = '#rowspan#';
340 }
341 }
342
343 if ( 1 < $cell['rowspan'] && 1 < $cell['colspan'] ) {
344 for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
345 for ( $j = 1; $j < $cell['colspan']; $j++ ) {
346 $table[ $row_idx + $i ][ $col_idx + $j ]['val'] = '#span#';
347 }
348 }
349 }
350 }
351 }
352
353 // Flatten value property to two-dimensional array.
354 $result_table = array();
355 foreach ( $table as $row_idx => $row ) {
356 $table_row = array();
357 foreach ( $row as $col_idx => $cell ) {
358 $table_row[] = (string) $cell['val'];
359 }
360 $result_table[] = $table_row;
361 }
362
363 $this->imported_table = array( 'data' => $this->pad_array_to_max_cols( $result_table ) );
364 }
365
366 /**
367 * Import Microsoft Excel 2007-2013 data.
368 *
369 * @since 1.1.0
370 */
371 protected function import_xlsx() {
372 TablePress::load_file( 'simplexlsx.class.php', 'libraries' );
373 $xlsx_file = SimpleXLSX::parse( $this->import_data, true );
374
375 if ( $xlsx_file ) {
376 $this->imported_table = array( 'data' => $xlsx_file->rows() );
377 } else {
378 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />' . SimpleXLSX::parse_error() . '<br />';
379 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
380 }
381 }
382
383 /**
384 * Make sure array is rectangular with $max_cols columns in every row.
385 *
386 * @since 1.0.0
387 *
388 * @param array $array Two-dimensional array to be padded.
389 * @return array Padded array.
390 */
391 public function pad_array_to_max_cols( array $array ) {
392 $rows = count( $array );
393 $rows = ( $rows > 0 ) ? $rows : 1;
394 $max_columns = $this->count_max_columns( $array );
395 $max_columns = ( $max_columns > 0 ) ? $max_columns : 1;
396 // 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).
397 $max_columns_array = array_fill( 1, $rows, $max_columns );
398 $pad_values_array = array_fill( 1, $rows, '' );
399 return array_map( 'array_pad', $array, $max_columns_array, $pad_values_array );
400 }
401
402 /**
403 * Get the highest number of columns in the rows.
404 *
405 * @since 1.0.0
406 *
407 * @param array $array Two-dimensional array.
408 * @return int Highest number of columns in the rows of the array.
409 */
410 protected function count_max_columns( array $array ) {
411 $max_columns = 0;
412 foreach ( $array as $row_idx => $row ) {
413 $num_columns = count( $row );
414 $max_columns = max( $num_columns, $max_columns );
415 }
416 return $max_columns;
417 }
418
419 /**
420 * Fixes the encoding to UTF-8 for the entire string that is to be imported.
421 *
422 * @since 1.0.0
423 *
424 * @link http://stevephillips.me/blog/dealing-php-and-character-encoding
425 */
426 protected function fix_table_encoding() {
427 // Check and remove possible UTF-8 Byte-Order Mark (BOM).
428 $bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
429 if ( 0 === strncmp( $this->import_data, $bom, 3 ) ) {
430 $this->import_data = substr( $this->import_data, 3 );
431 // If data has a BOM, it's UTF-8, so further checks unnecessary.
432 return;
433 }
434
435 // Require the iconv() function for the following checks.
436 if ( ! function_exists( 'iconv' ) ) {
437 return;
438 }
439
440 // Check for possible UTF-16 BOMs ("little endian" and "big endian") and try to convert the data to UTF-8.
441 if ( "\xFF\xFE" === substr( $this->import_data, 0, 2 ) || "\xFE\xFF" === substr( $this->import_data, 0, 2 ) ) {
442 $data = @iconv( 'UTF-16', 'UTF-8', $this->import_data );
443 if ( false !== $data ) {
444 $this->import_data = $data;
445 return;
446 }
447 }
448
449 // Detect the character encoding and convert to UTF-8, if it's different.
450 if ( function_exists( 'mb_detect_encoding' ) ) {
451 $current_encoding = mb_detect_encoding( $this->import_data, 'ASCII, UTF-8, ISO-8859-1' );
452 if ( 'UTF-8' !== $current_encoding ) {
453 $data = @iconv( $current_encoding, 'UTF-8', $this->import_data );
454 if ( false !== $data ) {
455 $this->import_data = $data;
456 return;
457 }
458 }
459 }
460 }
461
462 } // class TablePress_Import
463