| @@ -12,8 +12,9 @@ | ||
| 12 | 12 | defined( 'ABSPATH' ) || die( 'No direct script access allowed!' ); |
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | 15 | * TablePress Table Export Class |
| 16 | + * | |
| 16 | 17 | * @package TablePress |
| 17 | 18 | * @subpackage Export/Import |
| 18 | 19 | * @author Tobias Bäthge |
| 19 | 20 | * @since 1.0.0 |
| @@ -23,27 +24,26 @@ | ||
| 23 | 24 | /** |
| 24 | 25 | * File/Data Formats that are available for the export. |
| 25 | 26 | * |
| 26 | 27 | * @since 1.0.0 |
| 27 | - * @var array | |
| 28 | + * @var array<string, string> | |
| 28 | 29 | */ |
| 29 | - public $export_formats = array(); | |
| 30 | + public array $export_formats = array(); | |
| 30 | 31 | |
| 31 | 32 | /** |
| 32 | 33 | * Delimiters for the CSV export. |
| 33 | 34 | * |
| 34 | 35 | * @since 1.0.0 |
| 35 | - * @var array | |
| 36 | + * @var array<string, string> | |
| 36 | 37 | */ |
| 37 | - public $csv_delimiters = array(); | |
| 38 | + public array $csv_delimiters = array(); | |
| 38 | 39 | |
| 39 | 40 | /** |
| 40 | 41 | * Whether ZIP archive support is available in the PHP installation on the server. |
| 41 | 42 | * |
| 42 | 43 | * @since 1.0.0 |
| 43 | - * @var bool | |
| 44 | 44 | */ |
| 45 | - public $zip_support_available = false; | |
| 45 | + public bool $zip_support_available = false; | |
| 46 | 46 | |
| 47 | 47 | /** |
| 48 | 48 | * Initialize the Export class. |
| 49 | 49 | * |
| @@ -61,10 +61,9 @@ | ||
| 61 | 61 | ',' => __( ', (comma)', 'tablepress' ), |
| 62 | 62 | 'tab' => __( '\t (tabulator)', 'tablepress' ), |
| 63 | 63 | ); |
| 64 | 64 | |
| 65 | - /** This filter is documented in the WordPress function unzip_file() in wp-admin/includes/file.php */ | |
| 66 | - if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) { | |
| 65 | + if ( class_exists( 'ZipArchive', false ) ) { | |
| 67 | 66 | $this->zip_support_available = true; |
| 68 | 67 | } |
| 69 | 68 | } |
| 70 | 69 | |
| @@ -72,14 +71,14 @@ | ||
| 72 | 71 | * Export a table. |
| 73 | 72 | * |
| 74 | 73 | * @since 1.0.0 |
| 75 | 74 | * |
| 76 | - * @param array $table Table to be exported. | |
| 77 | - * @param string $export_format Format for the export ('csv', 'html', 'json'). | |
| 78 | - * @param string $csv_delimiter Delimiter for CSV export. | |
| 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. | |
| 79 | 78 | * @return string Exported table (only data for CSV and HTML, full tables (including options) for JSON). |
| 80 | 79 | */ |
| 81 | - public function export_table( array $table, $export_format, $csv_delimiter ) { | |
| 80 | + public function export_table( array $table, string $export_format, string $csv_delimiter ): string { | |
| 82 | 81 | switch ( $export_format ) { |
| 83 | 82 | case 'csv': |
| 84 | 83 | $output = ''; |
| 85 | 84 | if ( 'tab' === $csv_delimiter ) { |
| @@ -101,15 +100,15 @@ | ||
| 101 | 100 | $tfoot = ''; |
| 102 | 101 | $tbody = array(); |
| 103 | 102 | |
| 104 | 103 | foreach ( $table['data'] as $row_idx => $row ) { |
| 105 | - // First row, need to check for head (but only if at least two rows). | |
| 106 | - if ( 0 === $row_idx && $table['options']['table_head'] && $num_rows > 1 ) { | |
| 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'] ) { | |
| 107 | 106 | $thead = $this->html_render_row( $row, 'th' ); |
| 108 | 107 | continue; |
| 109 | 108 | } |
| 110 | - // Last row, need to check for footer (but only if at least two rows). | |
| 111 | - if ( $last_row_idx === $row_idx && $table['options']['table_foot'] && $num_rows > 1 ) { | |
| 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'] ) { | |
| 112 | 111 | $tfoot = $this->html_render_row( $row, 'th' ); |
| 113 | 112 | continue; |
| 114 | 113 | } |
| 115 | 114 | // Neither first nor last row (with respective head/foot enabled), so render as body row. |
| @@ -128,8 +127,11 @@ | ||
| 128 | 127 | $output = "<table>\n" . $thead . $tfoot . $tbody . "</table>\n"; |
| 129 | 128 | break; |
| 130 | 129 | case 'json': |
| 131 | 130 | $output = wp_json_encode( $table, TABLEPRESS_JSON_OPTIONS ); |
| 131 | + if ( false === $output ) { | |
| 132 | + $output = ''; | |
| 133 | + } | |
| 132 | 134 | break; |
| 133 | 135 | default: |
| 134 | 136 | $output = ''; |
| 135 | 137 | } |
| @@ -141,22 +143,52 @@ | ||
| 141 | 143 | * Wrap and escape a cell for CSV export. |
| 142 | 144 | * |
| 143 | 145 | * @since 1.0.0 |
| 144 | 146 | * |
| 145 | - * @param string $string Content of a cell. | |
| 146 | - * @param string $delimiter CSV delimiter character. | |
| 147 | - * @return string Wrapped string for CSV export | |
| 147 | + * @param string $cell_content Content of a cell. | |
| 148 | + * @param string $delimiter CSV delimiter character. | |
| 149 | + * @return string Wrapped string for CSV export. | |
| 148 | 150 | */ |
| 149 | - protected function csv_wrap_and_escape( $string, $delimiter ) { | |
| 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 | + $functions_to_escape = array( | |
| 161 | + 'cmd|', | |
| 162 | + 'rundll32', | |
| 163 | + 'DDE(', | |
| 164 | + 'IMPORTXML(', | |
| 165 | + 'IMPORTFEED(', | |
| 166 | + 'IMPORTHTML(', | |
| 167 | + 'IMPORTRANGE(', | |
| 168 | + 'IMPORTDATA(', | |
| 169 | + 'IMAGE(', | |
| 170 | + 'HYPERLINK(', | |
| 171 | + 'WEBSERVICE(', | |
| 172 | + ); | |
| 173 | + foreach ( $functions_to_escape as $function ) { | |
| 174 | + if ( false !== stripos( $cell_content, $function ) ) { | |
| 175 | + $cell_content = "'" . $cell_content; // Prepend a ' to indicate that the cell format is a text string. | |
| 176 | + break; | |
| 177 | + } | |
| 178 | + } | |
| 179 | + } | |
| 180 | + | |
| 150 | 181 | // Escape CSV delimiter for RegExp (e.g. '|'). |
| 151 | 182 | $delimiter = preg_quote( $delimiter, '#' ); |
| 152 | - if ( 1 === preg_match( '#' . $delimiter . '|"|\n|\r#i', $string ) || ' ' === substr( $string, 0, 1 ) || ' ' === substr( $string, -1 ) ) { | |
| 183 | + if ( 1 === preg_match( '#' . $delimiter . '|"|\n|\r#i', $cell_content ) || str_starts_with( $cell_content, ' ' ) || str_ends_with( $cell_content, ' ' ) ) { | |
| 153 | 184 | // Escape single " as double "". |
| 154 | - $string = str_replace( '"', '""', $string ); | |
| 185 | + $cell_content = str_replace( '"', '""', $cell_content ); | |
| 155 | 186 | // Wrap string in "". |
| 156 | - $string = '"' . $string . '"'; | |
| 187 | + $cell_content = '"' . $cell_content . '"'; | |
| 157 | 188 | } |
| 158 | - return $string; | |
| 189 | + | |
| 190 | + return $cell_content; | |
| 159 | 191 | } |
| 160 | 192 | |
| 161 | 193 | /** |
| 162 | 194 | * Generate the HTML of a row. |
| @@ -162,13 +194,13 @@ | ||
| 162 | 194 | * Generate the HTML of a row. |
| 163 | 195 | * |
| 164 | 196 | * @since 1.0.0 |
| 165 | 197 | * |
| 166 | - * @param array $row Cells of the row to be rendered. | |
| 167 | - * @param string $tag HTML tag to use for the cells (td or th). | |
| 198 | + * @param string[] $row Cells of the row to be rendered. | |
| 199 | + * @param string $tag HTML tag to use for the cells (td or th). | |
| 168 | 200 | * @return string HTML code for the row. |
| 169 | 201 | */ |
| 170 | - protected function html_render_row( array $row, $tag ) { | |
| 202 | + protected function html_render_row( array $row, string $tag ): string { | |
| 171 | 203 | $output = "\t\t<tr>\n"; |
| 172 | 204 | array_walk( $row, array( $this, 'html_wrap_and_escape' ), $tag ); |
| 173 | 205 | $output .= implode( '', $row ); |
| 174 | 206 | $output .= "\t\t</tr>\n"; |
| @@ -179,18 +211,18 @@ | ||
| 179 | 211 | * Wrap and escape a cell for HTML export. |
| 180 | 212 | * |
| 181 | 213 | * @since 1.0.0 |
| 182 | 214 | * |
| 183 | - * @param string $cell_content Content of a cell. | |
| 184 | - * @param int|null $column_idx Column index, or null if omitted. Unused, but defined to be able to use function as callback in array_walk(). | |
| 185 | - * @param string $html_tag HTML tag that shall be used for the cell. | |
| 215 | + * @param string $cell_content Content of a cell. | |
| 216 | + * @param int $column_idx Column index, or -1 if omitted. Unused, but defined to be able to use function as callback in array_walk(). | |
| 217 | + * @param string $html_tag HTML tag that shall be used for the cell. | |
| 186 | 218 | */ |
| 187 | - protected function html_wrap_and_escape( &$cell_content, $column_idx, $html_tag ) { | |
| 219 | + protected function html_wrap_and_escape( string &$cell_content, int $column_idx, string $html_tag ): void { | |
| 188 | 220 | /* |
| 189 | 221 | * Replace any & with & that is not already an encoded entity (from function htmlentities2 in WP 2.8). |
| 190 | 222 | * A complete htmlentities2() or htmlspecialchars() would encode <HTML> tags, which we don't want. |
| 191 | 223 | */ |
| 192 | - $cell_content = preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&', $cell_content ); | |
| 224 | + $cell_content = (string) preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&', $cell_content ); | |
| 193 | 225 | $cell_content = "\t\t\t<{$html_tag}>{$cell_content}</{$html_tag}>\n"; |
| 194 | 226 | } |
| 195 | 227 | |
| 196 | 228 | } // class TablePress_Export |