PluginProbe
TablePress – Tables in WordPress made easy / 3.3.2
TablePress – Tables in WordPress made easy v3.3.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 / libraries / html-parser.class.php

html-parser.class.php in TablePress – Tables in WordPress made easy 3.3.2, at libraries/html-parser.class.php

201 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * HTML Parsing class for TablePress, used for import of HTML files.
4 *
5 * @package TablePress
6 * @subpackage Import
7 * @author Tobias Bäthge
8 * @since 2.0.0
9 */
10
11 // Prohibit direct script loading.
12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13
14 /**
15 * HTML Parsing class
16 *
17 * @package TablePress
18 * @subpackage Import
19 * @author Tobias Bäthge
20 * @since 2.0.0
21 */
22 abstract class HTML_Parser {
23
24 /**
25 * Parses HTML string into a two-dimensional array, maybe with options.
26 *
27 * @since 2.0.0
28 *
29 * @param string $html Data to be parsed.
30 * @return array<string, mixed>|WP_Error Array with table data and options (current table head and foot row) on success, WP_Error on error.
31 */
32 public static function parse( string $html ) /* : array|WP_Error */ {
33 if ( false === stripos( $html, '<table' ) || false === stripos( $html, '</table>' ) ) {
34 return new WP_Error( 'table_import_html_no_table_found' );
35 }
36
37 // Prepend XML declaration, for better encoding support.
38 $full_html = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . $html;
39 if ( function_exists( 'libxml_disable_entity_loader' ) ) {
40 /*
41 * Don't expand external entities, see https://websec.io/2012/08/27/Preventing-XXE-in-PHP.html.
42 * 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.
43 */
44 @libxml_disable_entity_loader( true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Generic.PHP.DeprecatedFunctions.Deprecated
45 }
46 // No warnings/errors raised, but stored internally.
47 libxml_use_internal_errors( true );
48 $dom = new DOMDocument( '1.0', 'UTF-8' );
49 // No strict checking for invalid HTML.
50 $dom->strictErrorChecking = false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
51 $result = $dom->loadHTML( $full_html );
52 if ( ! $result ) {
53 return new WP_Error( 'table_import_html_dom_load_html_failed' );
54 }
55 $dom_tables = $dom->getElementsByTagName( 'table' );
56 if ( 0 === count( $dom_tables ) ) {
57 return new WP_Error( 'table_import_html_dom_get_tables' );
58 }
59 libxml_clear_errors(); // Clear errors so that we only catch those inside the table in the next line.
60 $table = simplexml_import_dom( $dom_tables->item( 0 ) ); // @phpstan-ignore argument.type
61 if ( is_null( $table ) ) {
62 return new WP_Error( 'table_import_html_simplexml_import_dom_failed' );
63 }
64
65 $errors = libxml_get_errors();
66 libxml_clear_errors();
67 if ( ! empty( $errors ) ) {
68 $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br><br>';
69 foreach ( $errors as $error ) {
70 switch ( $error->level ) {
71 case LIBXML_ERR_WARNING:
72 $output .= "Warning {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br>";
73 break;
74 case LIBXML_ERR_ERROR:
75 $output .= "Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br>";
76 break;
77 case LIBXML_ERR_FATAL:
78 $output .= "Fatal Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br>";
79 break;
80 }
81 }
82 wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
83 }
84
85 $html_table = array(
86 'data' => array(),
87 'options' => array(),
88 );
89 if ( isset( $table->thead ) ) {
90 $head_rows = self::_import_html_rows( $table->thead[0]->tr ); // @phpstan-ignore property.nonObject
91 $html_table['data'] = array_merge( $html_table['data'], $head_rows );
92 $html_table['options']['table_head'] = count( $head_rows );
93 }
94 if ( isset( $table->tbody ) ) {
95 $html_table['data'] = array_merge( $html_table['data'], self::_import_html_rows( $table->tbody[0]->tr ) ); // @phpstan-ignore property.nonObject
96 }
97 if ( isset( $table->tr ) ) {
98 $html_table['data'] = array_merge( $html_table['data'], self::_import_html_rows( $table->tr ) );
99 }
100 if ( isset( $table->tfoot ) ) {
101 $foot_rows = self::_import_html_rows( $table->tfoot[0]->tr ); // @phpstan-ignore property.nonObject
102 $html_table['data'] = array_merge( $html_table['data'], $foot_rows );
103 $html_table['options']['table_foot'] = count( $foot_rows );
104 }
105
106 return $html_table;
107 }
108
109 /**
110 * Converts table HTML rows to an array.
111 *
112 * @since 2.0.0
113 *
114 * @param SimpleXMLElement $element XMLElement.
115 * @return array<int, array<int, string>> SimpleXMLElement exported to an array.
116 */
117 protected static function _import_html_rows( SimpleXMLElement $element ): array {
118 $rows = array(); // Container for the table data.
119 $rowspans = array(); // Container for information about rowspans in rows that follow the currently processed row.
120
121 $row_idx = 0;
122 foreach ( $element as $row ) {
123 // If all cells in a row should be merged with the cells in the row above, add the trigger word to each of them (should be very rare).
124 while ( isset( $rowspans[ $row_idx ] ) && count( $rowspans[ $row_idx ] ) === count( $rows[ $row_idx - 1 ] ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
125 $rows[] = $rowspans[ $row_idx ];
126 ++$row_idx;
127 }
128
129 $new_row = array();
130 $column_idx = 0;
131 foreach ( $row as $cell ) {
132 // If a cell in a row should be merged with the cell above it, add the trigger word to it.
133 while ( isset( $rowspans[ $row_idx ][ $column_idx ] ) ) {
134 $new_row[] = $rowspans[ $row_idx ][ $column_idx ];
135 ++$column_idx;
136 }
137
138 $cell_xml = $cell->asXML();
139
140 // Get content between <td>...</td>, or <th>...</th>, possibly with HTML.
141 if ( false !== $cell_xml && 1 === preg_match( '#<t[d|h].*?>(.*)</t[d|h]>#is', $cell_xml, $matches ) ) {
142 /*
143 * Decode HTML entities again, as there might be some left especially in attributes of HTML tags in the cells,
144 * see https://www.php.net/manual/en/simplexmlelement.asxml.php#107137.
145 */
146 $new_row[] = html_entity_decode( $matches[1], ENT_NOQUOTES, 'UTF-8' );
147
148 // Search for colspan and rowspan attributes in the cell's HTML tag.
149 $colspan = 1;
150 $rowspan = 1;
151 if ( 1 === preg_match( '#<t[d|h].*colspan=["\']?(\d+)["\']?.*?>#is', $cell_xml, $matches ) ) {
152 $colspan = (int) $matches[1];
153 }
154 if ( 1 === preg_match( '#<t[d|h].*rowspan=["\']?(\d+)["\']?.*?>#is', $cell_xml, $matches ) ) {
155 $rowspan = (int) $matches[1];
156 }
157
158 // Add cells with the colspan trigger word, if merged cells across columns were found.
159 for ( $i = 1; $i < $colspan; $i++ ) {
160 $new_row[] = '#colspan#';
161 }
162
163 // If merged cells across rows were found, add trigger words to a temporary variable.
164 for ( $i = 1; $i < $rowspan; $i++ ) {
165 if ( ! isset( $rowspans[ $row_idx + $i ] ) ) {
166 $rowspans[ $row_idx + $i ] = array();
167 }
168 $rowspans[ $row_idx + $i ][ $column_idx ] = '#rowspan#';
169 for ( $j = 1; $j < $colspan; $j++ ) {
170 $rowspans[ $row_idx + $i ][ $column_idx + $j ] = '#span#';
171 }
172 }
173 } else {
174 // Add an empty cell if no content could be extracted from the cell's HTML tag.
175 $new_row[] = '';
176 }
177
178 ++$column_idx;
179 }
180
181 // After the last cell in a row: If a cell in a row should be merged with the cell above it, add the trigger word to it.
182 while ( isset( $rowspans[ $row_idx ][ $column_idx ] ) ) {
183 $new_row[] = $rowspans[ $row_idx ][ $column_idx ];
184 ++$column_idx;
185 }
186
187 $rows[] = $new_row;
188 ++$row_idx;
189 }
190
191 // After the last data row: If all cells in a row should be merged with the cells in the row above, add the trigger word to each of them (should be very rare).
192 while ( isset( $rowspans[ $row_idx ] ) && count( $rowspans[ $row_idx ] ) === count( $rows[ $row_idx - 1 ] ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
193 $rows[] = $rowspans[ $row_idx ];
194 ++$row_idx;
195 }
196
197 return $rows;
198 }
199
200 } // class HTML_Parser
201