PluginProbe
TablePress – Tables in WordPress made easy / trunk
TablePress – Tables in WordPress made easy vtrunk
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-export.php

class-export.php in TablePress – Tables in WordPress made easy trunk, at classes/class-export.php

235 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TablePress Table Export 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 Export Class
16 *
17 * @package TablePress
18 * @subpackage Export/Import
19 * @author Tobias Bäthge
20 * @since 1.0.0
21 */
22 class TablePress_Export {
23
24 /**
25 * File/Data Formats that are available for the export.
26 *
27 * @since 1.0.0
28 * @var array<string, string>
29 */
30 public array $export_formats = array();
31
32 /**
33 * Delimiters for the CSV export.
34 *
35 * @since 1.0.0
36 * @var array<string, string>
37 */
38 public array $csv_delimiters = array();
39
40 /**
41 * Whether ZIP archive support is available in the PHP installation on the server.
42 *
43 * @since 1.0.0
44 */
45 public bool $zip_support_available = false;
46
47 /**
48 * Initialize the Export class.
49 *
50 * @since 1.0.0
51 */
52 public function __construct() {
53 // Initiate here, because function call not possible outside a class method.
54 $this->export_formats = array(
55 'csv' => __( 'CSV - Character-Separated Values', 'tablepress' ),
56 'html' => __( 'HTML - Hypertext Markup Language', 'tablepress' ),
57 'json' => __( 'JSON - JavaScript Object Notation', 'tablepress' ),
58 );
59 $this->csv_delimiters = array(
60 ';' => __( '; (semicolon)', 'tablepress' ),
61 ',' => __( ', (comma)', 'tablepress' ),
62 'tab' => __( '\t (tabulator)', 'tablepress' ),
63 );
64
65 if ( class_exists( 'ZipArchive', false ) ) {
66 $this->zip_support_available = true;
67 }
68 }
69
70 /**
71 * Export a table.
72 *
73 * @since 1.0.0
74 *
75 * @param array<string, mixed> $table Table to be exported.
76 * @param string $export_format Format for the export ('csv', 'html', 'json').
77 * @param string $csv_delimiter Delimiter for CSV export.
78 * @return string Exported table (only data for CSV and HTML, full tables (including options) for JSON).
79 */
80 public function export_table( array $table, string $export_format, string $csv_delimiter ): string {
81 switch ( $export_format ) {
82 case 'csv':
83 $output = '';
84 if ( 'tab' === $csv_delimiter ) {
85 $csv_delimiter = "\t";
86 }
87 foreach ( $table['data'] as $row_idx => $row ) {
88 $csv_row = array();
89 foreach ( $row as $column_idx => $cell_content ) {
90 $csv_row[] = $this->csv_wrap_and_escape( $cell_content, $csv_delimiter );
91 }
92 $output .= implode( $csv_delimiter, $csv_row );
93 $output .= "\n";
94 }
95 break;
96 case 'html':
97 $num_rows = count( $table['data'] );
98 $last_row_idx = $num_rows - 1;
99 $thead = '';
100 $tfoot = '';
101 $tbody = array();
102
103 foreach ( $table['data'] as $row_idx => $row ) {
104 // Table head rows, but only if there's at least one additional row.
105 if ( $row_idx < $table['options']['table_head'] && $num_rows > $table['options']['table_head'] ) {
106 $thead = $this->html_render_row( $row, 'th' );
107 continue;
108 }
109 // Table foot rows, but only if there's at least one additional row.
110 if ( $row_idx > $last_row_idx - $table['options']['table_foot'] && $num_rows > $table['options']['table_foot'] ) {
111 $tfoot = $this->html_render_row( $row, 'th' );
112 continue;
113 }
114 // Neither first nor last row (with respective head/foot enabled), so render as body row.
115 $tbody[] = $this->html_render_row( $row, 'td' );
116 }
117
118 // <thead>, <tfoot>, and <tbody> tags.
119 if ( ! empty( $thead ) ) {
120 $thead = "\t<thead>\n{$thead}\t</thead>\n";
121 }
122 if ( ! empty( $tfoot ) ) {
123 $tfoot = "\t<tfoot>\n{$tfoot}\t</tfoot>\n";
124 }
125 $tbody = "\t<tbody>\n" . implode( '', $tbody ) . "\t</tbody>\n";
126
127 $output = "<table>\n" . $thead . $tbody . $tfoot . "</table>\n";
128 break;
129 case 'json':
130 $output = wp_json_encode( $table, TABLEPRESS_JSON_OPTIONS );
131 if ( false === $output ) {
132 $output = '';
133 }
134 break;
135 default:
136 $output = '';
137 }
138
139 return $output;
140 }
141
142 /**
143 * Wrap and escape a cell for CSV export.
144 *
145 * @since 1.0.0
146 *
147 * @param string $cell_content Content of a cell.
148 * @param string $delimiter CSV delimiter character.
149 * @return string Wrapped string for CSV export.
150 */
151 protected function csv_wrap_and_escape( string $cell_content, string $delimiter ): string {
152 // Return early if the cell is empty. No escaping or wrapping is needed then.
153 if ( '' === $cell_content ) {
154 return $cell_content;
155 }
156
157 // Escape potentially dangerous functions that could be used for CSV injection attacks in external spreadsheet software.
158 $active_content_triggers = array( '=', '+', '-', '@' );
159 if ( in_array( $cell_content[0], $active_content_triggers, true ) ) {
160 // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found -- Avoid concatenation of function names to prevent false positives in code scanners.
161 $functions_to_escape = array(
162 'cmd|',
163 'FOR' . 'FILES|',
164 'rund' . 'll32',
165 'DD' . 'E(',
166 'IMPORT' . 'XML(',
167 'IMPORT' . 'FEED(',
168 'IMPORT' . 'HTML(',
169 'IMPORT' . 'RANGE(',
170 'IMPORT' . 'DATA(',
171 'IMAGE(',
172 'HYPERLINK(',
173 'WEBSERVICE(',
174 );
175 // phpcs:enable
176
177 $fn_stripos = function_exists( 'mb_stripos' ) ? 'mb_stripos' : 'stripos';
178
179 foreach ( $functions_to_escape as $function ) {
180 if ( false !== $fn_stripos( $cell_content, $function ) ) {
181 $cell_content = "'" . $cell_content; // Prepend a ' to indicate that the cell format is a text string.
182 break;
183 }
184 }
185 }
186
187 // Escape CSV delimiter for RegExp (e.g. '|').
188 $delimiter = preg_quote( $delimiter, '#' );
189 if ( 1 === preg_match( '#' . $delimiter . '|"|\n|\r#i', $cell_content ) || str_starts_with( $cell_content, ' ' ) || str_ends_with( $cell_content, ' ' ) ) {
190 // Escape single " as double "".
191 $cell_content = str_replace( '"', '""', $cell_content );
192 // Wrap string in "".
193 $cell_content = '"' . $cell_content . '"';
194 }
195
196 return $cell_content;
197 }
198
199 /**
200 * Generate the HTML of a row.
201 *
202 * @since 1.0.0
203 *
204 * @param string[] $row Cells of the row to be rendered.
205 * @param string $tag HTML tag to use for the cells (td or th).
206 * @return string HTML code for the row.
207 */
208 protected function html_render_row( array $row, string $tag ): string {
209 $output = "\t\t<tr>\n";
210 array_walk( $row, array( $this, 'html_wrap_and_escape' ), $tag );
211 $output .= implode( '', $row );
212 $output .= "\t\t</tr>\n";
213 return $output;
214 }
215
216 /**
217 * Wrap and escape a cell for HTML export.
218 *
219 * @since 1.0.0
220 *
221 * @param string $cell_content Content of a cell.
222 * @param int $column_idx Column index, or -1 if omitted. Unused, but defined to be able to use function as callback in array_walk().
223 * @param string $html_tag HTML tag that shall be used for the cell.
224 */
225 protected function html_wrap_and_escape( string &$cell_content, int $column_idx, string $html_tag ): void {
226 /*
227 * Replace any & with &amp; that is not already an encoded entity (from function htmlentities2 in WP 2.8).
228 * A complete htmlentities2() or htmlspecialchars() would encode <HTML> tags, which we don't want.
229 */
230 $cell_content = (string) preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&amp;', $cell_content );
231 $cell_content = "\t\t\t<{$html_tag}>{$cell_content}</{$html_tag}>\n";
232 }
233
234 } // class TablePress_Export
235