PluginProbe
TablePress – Tables in WordPress made easy / 2.2.1
TablePress – Tables in WordPress made easy v2.2.1
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 2.2.1, at libraries/html-parser.class.php

199 lines 7.6 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-line
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 $html_table['data'] = array_merge( $html_table['data'], self::_import_html_rows( $table->thead[0]->tr ) ); // @phpstan-ignore-line
91 $html_table['options']['table_head'] = true;
92 }
93 if ( isset( $table->tbody ) ) {
94 $html_table['data'] = array_merge( $html_table['data'], self::_import_html_rows( $table->tbody[0]->tr ) ); // @phpstan-ignore-line
95 }
96 if ( isset( $table->tr ) ) {
97 $html_table['data'] = array_merge( $html_table['data'], self::_import_html_rows( $table->tr ) );
98 }
99 if ( isset( $table->tfoot ) ) {
100 $html_table['data'] = array_merge( $html_table['data'], self::_import_html_rows( $table->tfoot[0]->tr ) ); // @phpstan-ignore-line
101 $html_table['options']['table_foot'] = true;
102 }
103
104 return $html_table;
105 }
106
107 /**
108 * Converts table HTML rows to an array.
109 *
110 * @since 2.0.0
111 *
112 * @param SimpleXMLElement $element XMLElement.
113 * @return array<int, array<int, string>> SimpleXMLElement exported to an array.
114 */
115 protected static function _import_html_rows( SimpleXMLElement $element ): array {
116 $rows = array(); // Container for the table data.
117 $rowspans = array(); // Container for information about rowspans in rows that follow the currently processed row.
118
119 $row_idx = 0;
120 foreach ( $element as $row ) {
121 // 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).
122 while ( isset( $rowspans[ $row_idx ] ) && count( $rowspans[ $row_idx ] ) === count( $rows[ $row_idx - 1 ] ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
123 $rows[] = $rowspans[ $row_idx ];
124 ++$row_idx;
125 }
126
127 $new_row = array();
128 $column_idx = 0;
129 foreach ( $row as $cell ) {
130 // If a cell in a row should be merged with the cell above it, add the trigger word to it.
131 while ( isset( $rowspans[ $row_idx ][ $column_idx ] ) ) {
132 $new_row[] = $rowspans[ $row_idx ][ $column_idx ];
133 ++$column_idx;
134 }
135
136 $cell_xml = $cell->asXml();
137
138 // Get content between <td>...</td>, or <th>...</th>, possibly with HTML.
139 if ( false !== $cell_xml && 1 === preg_match( '#<t[d|h].*?>(.*)</t[d|h]>#is', $cell_xml, $matches ) ) {
140 /*
141 * Decode HTML entities again, as there might be some left especially in attributes of HTML tags in the cells,
142 * see https://secure.php.net/manual/en/simplexmlelement.asxml.php#107137.
143 */
144 $new_row[] = html_entity_decode( $matches[1], ENT_NOQUOTES, 'UTF-8' );
145
146 // Search for colspan and rowspan attributes in the cell's HTML tag.
147 $colspan = 1;
148 $rowspan = 1;
149 if ( 1 === preg_match( '#<t[d|h].*colspan=["\']?(\d+)["\']?.*?>#is', $cell_xml, $matches ) ) {
150 $colspan = (int) $matches[1];
151 }
152 if ( 1 === preg_match( '#<t[d|h].*rowspan=["\']?(\d+)["\']?.*?>#is', $cell_xml, $matches ) ) {
153 $rowspan = (int) $matches[1];
154 }
155
156 // Add cells with the colspan trigger word, if merged cells across columns were found.
157 for ( $i = 1; $i < $colspan; $i++ ) {
158 $new_row[] = '#colspan#';
159 }
160
161 // If merged cells across rows were found, add trigger words to a temporary variable.
162 for ( $i = 1; $i < $rowspan; $i++ ) {
163 if ( ! isset( $rowspans[ $row_idx + $i ] ) ) {
164 $rowspans[ $row_idx + $i ] = array();
165 }
166 $rowspans[ $row_idx + $i ][ $column_idx ] = '#rowspan#';
167 for ( $j = 1; $j < $colspan; $j++ ) {
168 $rowspans[ $row_idx + $i ][ $column_idx + $j ] = '#span#';
169 }
170 }
171 } else {
172 // Add an empty cell if no content could be extracted from the cell's HTML tag.
173 $new_row[] = '';
174 }
175
176 ++$column_idx;
177 }
178
179 // 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.
180 while ( isset( $rowspans[ $row_idx ][ $column_idx ] ) ) {
181 $new_row[] = $rowspans[ $row_idx ][ $column_idx ];
182 ++$column_idx;
183 }
184
185 $rows[] = $new_row;
186 ++$row_idx;
187 }
188
189 // 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).
190 while ( isset( $rowspans[ $row_idx ] ) && count( $rowspans[ $row_idx ] ) === count( $rows[ $row_idx - 1 ] ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
191 $rows[] = $rowspans[ $row_idx ];
192 ++$row_idx;
193 }
194
195 return $rows;
196 }
197
198 } // class HTML_Parser
199