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
tablepress / libraries / html-parser.class.php

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

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