PluginProbe
TablePress – Tables in WordPress made easy / 3.0.4
TablePress – Tables in WordPress made easy v3.0.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 2.4.4 All 44 releases
← All changes | classes/class-import.php +687 -306 1.143.0.4 View file →
@@ -7,13 +7,18 @@
7 7 * @author Tobias Bäthge
8 8 * @since 1.0.0
9 9 */
10 10
11 +use TablePress\Import\File;
12 +
11 13 // Prohibit direct script loading.
12 14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13 15
16 +TablePress::load_file( 'class-import-file.php', 'classes' );
17 +
14 18 /**
15 19 * TablePress Table Import Class
20 + *
16 21 * @package TablePress
17 22 * @subpackage Export/Import
18 23 * @author Tobias Bäthge
19 24 * @since 1.0.0
@@ -20,448 +25,824 @@
20 25 */
21 26 class TablePress_Import {
22 27
23 28 /**
24 - * File/Data Formats that are available for import.
29 + * Instance of the TablePress Legacy or PHPSpreadsheet Importer.
25 30 *
26 31 * @since 1.0.0
27 - * @var array
32 + * @var TablePress_Import_Legacy|TablePress_Import_PHPSpreadsheet
28 33 */
29 - public $import_formats = array();
34 + protected object $importer;
30 35
31 36 /**
32 - * Whether ZIP archive support is available in the PHP installation on the server.
37 + * Import configuration (mainly the data from the Import form).
33 38 *
34 - * @since 1.0.0
35 - * @var bool
39 + * @since 2.0.0
40 + * @var array<string, mixed>
36 41 */
37 - public $zip_support_available = false;
42 + protected array $import_config = array();
38 43
39 44 /**
40 - * Whether HTML import support is available in the PHP installation on the server.
45 + * Whether ZIP archive support is available (which it always is, as PclZip is used as a fallback).
41 46 *
42 47 * @since 1.0.0
43 - * @var bool
48 + * @deprecated 2.3.0 ZIP support is now always available, either through `ZipArchive` or through `PclZip`.
44 49 */
45 - public $html_import_support_available = false;
50 + public bool $zip_support_available = true;
46 51
47 52 /**
48 - * Data to be imported.
53 + * List of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
49 54 *
50 - * @since 1.0.0
51 - * @var string
55 + * @since 2.0.0
56 + * @var array<string, string[]>
52 57 */
53 - protected $import_data;
58 + protected array $table_names_ids = array();
54 59
55 60 /**
56 - * Imported table.
61 + * Runs the import process for a given import configuration.
57 62 *
58 - * @since 1.0.0
59 - * @var array|false
60 - */
61 - protected $imported_table = false;
62 -
63 - /**
64 - * Initialize the Import class.
63 + * @since 2.0.0
65 64 *
66 - * @since 1.0.0
65 + * @param array<string, mixed> $import_config Import configuration.
66 + * @return array{tables: array<int, array<string, mixed>>, errors: File[]}|WP_Error List of imported tables on success, WP_Error on failure.
67 67 */
68 - public function __construct() {
69 - /** This filter is documented in the WordPress function unzip_file() in wp-admin/includes/file.php */
70 - if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
71 - $this->zip_support_available = true;
68 + public function run( array $import_config ) /* : array|WP_Error */ {
69 + // Unziping can use a lot of memory and execution time, but not this much hopefully.
70 + wp_raise_memory_limit( 'admin' );
71 + if ( function_exists( 'set_time_limit' ) ) {
72 + @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
72 73 }
73 74
74 - if ( class_exists( 'DOMDocument', false ) && function_exists( 'simplexml_import_dom' ) && function_exists( 'libxml_use_internal_errors' ) ) {
75 - $this->html_import_support_available = true;
75 + $this->import_config = $import_config;
76 +
77 + $import_files = $this->get_files_to_import();
78 + if ( is_wp_error( $import_files ) ) {
79 + return $import_files;
76 80 }
77 81
78 - // Initiate here, because function call not possible outside a class method.
79 - $this->import_formats = array();
80 - $this->import_formats['csv'] = __( 'CSV - Character-Separated Values', 'tablepress' );
81 - if ( $this->html_import_support_available ) {
82 - $this->import_formats['html'] = __( 'HTML - Hypertext Markup Language', 'tablepress' );
82 + $import_files = $this->convert_zip_files( $import_files );
83 +
84 + if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) ) {
85 + $this->table_names_ids = $this->get_list_of_table_names();
83 86 }
84 - $this->import_formats['json'] = __( 'JSON - JavaScript Object Notation', 'tablepress' );
85 - $this->import_formats['xls'] = __( 'XLS - Microsoft Excel 97-2003 (experimental)', 'tablepress' );
86 - $this->import_formats['xlsx'] = __( 'XLSX - Microsoft Excel 2007-2019 (experimental)', 'tablepress' );
87 +
88 + return $this->import_files( $import_files );
87 89 }
88 90
89 91 /**
90 - * Import a table.
92 + * Extracts the files that shall be imported from the import configuration.
91 93 *
92 - * @since 1.0.0
94 + * @since 2.0.0
93 95 *
94 - * @param string $format Import format.
95 - * @param string $data Data to import.
96 - * @return array|false Table array on success, false on error.
96 + * @return File[]|WP_Error Array of files that shall be imported or WP_Error on failure.
97 97 */
98 - public function import_table( $format, $data ) {
99 - $this->import_data = apply_filters( 'tablepress_import_table_data', $data, $format );
98 + protected function get_files_to_import() /* : array|WP_Error */ {
99 + $import_files = array();
100 100
101 - if ( ! in_array( $format, array( 'xlsx', 'xls' ), true ) ) {
102 - $this->fix_table_encoding();
103 - }
101 + switch ( $this->import_config['source'] ) {
102 + case 'file-upload':
103 + foreach ( $this->import_config['file-upload']['error'] as $key => $error ) {
104 + $file = new File( array(
105 + 'location' => $this->import_config['file-upload']['tmp_name'][ $key ],
106 + 'name' => $this->import_config['file-upload']['name'][ $key ],
107 + ) );
108 + if ( UPLOAD_ERR_OK !== $error ) {
109 + @unlink( $this->import_config['file-upload']['tmp_name'][ $key ] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
110 + $file->error = new WP_Error( 'table_import_file-upload_error', '', $error );
111 + }
112 + $import_files[] = $file;
113 + }
114 + break;
115 + case 'url':
116 + $host = wp_parse_url( $this->import_config['url'], PHP_URL_HOST );
104 117
105 - switch ( $format ) {
106 - case 'csv':
107 - $this->import_csv();
118 + if ( empty( $host ) ) {
119 + return new WP_Error( 'table_import_url_host_invalid', '', $this->import_config['url'] );
120 + }
121 +
122 + // Check the IP address of the host against a blocklist of hosts which should not be accessible, e.g. for security considerations.
123 + $ip = gethostbyname( $host ); // If no IP address can be found, this will return the host name, which will then be checked against the blocklist.
124 + $blocked_ips = array(
125 + '169.254.169.254', // Meta-data API for various cloud providers.
126 + '169.254.170.2', // AWS task metadata endpoint.
127 + '192.0.0.192', // Oracle Cloud endpoint.
128 + '100.100.100.200', // Alibaba Cloud endpoint.
129 + );
130 + if ( in_array( $ip, $blocked_ips, true ) ) {
131 + return new WP_Error( 'table_import_url_host_blocked', '', array( 'url' => $this->import_config['url'], 'ip' => $ip ) );
132 + }
133 +
134 + /**
135 + * Load WP file functions to be sure that `download_url()` exists, in particular during Cron requests.
136 + */
137 + require_once ABSPATH . 'wp-admin/includes/file.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
138 +
139 + // Download URL to local file.
140 + $location = download_url( $this->import_config['url'] );
141 + if ( is_wp_error( $location ) ) {
142 + $error = new WP_Error( 'table_import_url_download_failed', '', $this->import_config['url'] );
143 + $error->merge_from( $location );
144 + return $error;
145 + }
146 +
147 + $import_files[] = new File( array(
148 + 'location' => $location,
149 + 'name' => $this->import_config['url'],
150 + ) );
108 151 break;
109 - case 'html':
110 - $this->import_html();
152 + case 'server':
153 + if ( ABSPATH === $this->import_config['server'] ) {
154 + return new WP_Error( 'table_import_server_invalid', '', $this->import_config['server'] );
155 + }
156 +
157 + if ( ! is_readable( $this->import_config['server'] ) ) {
158 + return new WP_Error( 'table_import_server_not_readable', '', $this->import_config['server'] );
159 + }
160 +
161 + $import_files[] = new File( array(
162 + 'location' => $this->import_config['server'],
163 + 'name' => pathinfo( $this->import_config['server'], PATHINFO_BASENAME ),
164 + 'keep_file' => true, // Files on the server must not be deleted.
165 + ) );
111 166 break;
112 - case 'json':
113 - $this->import_json();
167 + case 'form-field':
168 + $location = wp_tempnam();
169 + $num_written_bytes = file_put_contents( $location, $this->import_config['form-field'] );
170 + if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
171 + @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
172 + return new WP_Error( 'table_import_form-field_temp_file_not_written' );
173 + }
174 +
175 + $import_files[] = new File( array(
176 + 'location' => $location,
177 + 'name' => __( 'Imported from Manual Input', 'tablepress' ),
178 + ) );
114 179 break;
115 - case 'xlsx':
116 - $this->import_xlsx();
117 - break;
118 - case 'xls':
119 - $this->import_xls();
120 - break;
121 180 default:
122 - return false;
181 + return new WP_Error( 'table_import_invalid_source', '', $this->import_config['source'] );
123 182 }
124 183
125 - return $this->imported_table;
184 + return $import_files;
126 185 }
127 186
128 187 /**
129 - * Import CSV data.
188 + * Replaces ZIP archives in the import files with a list of their contents.
130 189 *
131 - * @since 1.0.0
190 + * ZIP files are removed from the list and their contents are added to the end of the list.
191 + *
192 + * @since 2.0.0
193 + *
194 + * @param File[] $import_files Files that shall be imported, including ZIP archives.
195 + * @return File[] Files that shall be imported, with all ZIP archives recursively replaced by their contents.
132 196 */
133 - protected function import_csv() {
134 - $csv_parser = TablePress::load_class( 'CSV_Parser', 'csv-parser.class.php', 'libraries' );
135 - $csv_parser->load_data( $this->import_data );
136 - $delimiter = $csv_parser->find_delimiter();
137 - $data = $csv_parser->parse( $delimiter );
138 - $this->imported_table = array( 'data' => $this->pad_array_to_max_cols( $data ) );
197 + protected function convert_zip_files( array $import_files ): array {
198 + foreach ( $import_files as $key => &$file ) {
199 + // $file has to be used by reference, so that $key points to the correct element, due to array modification with `unset()` and `array_push()`.
200 +
201 + // Skip files that already have an error.
202 + if ( is_wp_error( $file->error ) ) {
203 + continue;
204 + }
205 +
206 + $file->extension = strtolower( pathinfo( $file->name, PATHINFO_EXTENSION ) );
207 +
208 + if ( function_exists( 'mime_content_type' ) ) {
209 + $mime_type = mime_content_type( $file->location );
210 + if ( false !== $mime_type ) {
211 + $file->mime_type = $mime_type;
212 + }
213 + }
214 +
215 + // Detect ZIP files from their file extension or MIME type.
216 + if ( 'zip' === $file->extension || 'application/zip' === $file->mime_type ) {
217 + $extracted_files = $this->extract_zip_file( $file );
218 + if ( is_wp_error( $extracted_files ) ) {
219 + $file->error = $extracted_files;
220 + $this->maybe_unlink_file( $file );
221 + continue;
222 + }
223 +
224 + if ( empty( $extracted_files ) ) {
225 + $file->error = new WP_Error( 'table_import_zip_file_empty', '', $file->name );
226 + $this->maybe_unlink_file( $file );
227 + continue;
228 + }
229 +
230 + /*
231 + * Remove the ZIP file from the list and instead append its contents.
232 + * Appending ensures recursiveness, as the appended files will be checked again.
233 + */
234 + unset( $import_files[ $key ] );
235 + array_push( $import_files, ...$extracted_files );
236 +
237 + $this->maybe_unlink_file( $file );
238 + }
239 + }
240 + unset( $file ); // Unset use-by-reference parameter of foreach loop.
241 +
242 + $import_files = array_merge( $import_files ); // Re-index.
243 +
244 + return $import_files;
139 245 }
140 246
141 247 /**
142 - * Import HTML data.
248 + * Extracts the files of a ZIP file and returns a list of files and their location.
143 249 *
144 - * @since 1.0.0
250 + * Depending on availability, either the PHP's ZipArchive class or WordPress' PclZip class is used.
251 + *
252 + * @since 2.0.0
253 + *
254 + * @param File $zip_file File data of a ZIP file (likely in a temporary folder).
255 + * @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
145 256 */
146 - protected function import_html() {
147 - if ( ! $this->html_import_support_available ) {
148 - return false;
257 + protected function extract_zip_file( File $zip_file ) /* : array|WP_Error */ {
258 + if ( class_exists( 'ZipArchive', false ) ) {
259 + $ziparchive_result = $this->extract_zip_file_ziparchive( $zip_file );
260 + if ( is_array( $ziparchive_result ) ) {
261 + return $ziparchive_result;
262 + }
263 + } else {
264 + $ziparchive_result = new WP_Error( 'table_import_error_zip_open', '', array( 'ziparchive_error' => 'Class ZipArchive not available' ) );
149 265 }
150 266
151 - if ( false === stripos( $this->import_data, '<table' ) || false === stripos( $this->import_data, '</table>' ) ) {
152 - $this->imported_table = false;
153 - return;
267 + // Fall through to PclZip if ZipArchive is not available or encountered an error opening the file.
268 + $pclzip_result = $this->extract_zip_file_pclzip( $zip_file );
269 + if ( is_wp_error( $pclzip_result ) ) {
270 + // Append the WP_Error from ZipArchive, to have all error information available.
271 + $pclzip_result->merge_from( $ziparchive_result );
154 272 }
155 273
156 - // Prepend XML declaration, for better encoding support.
157 - $full_html = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . $this->import_data;
158 - if ( function_exists( 'libxml_disable_entity_loader' ) ) {
159 - /*
160 - * Don't expand external entities, see https://websec.io/2012/08/27/Preventing-XXE-in-PHP.html.
161 - * 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.
162 - */
163 - @libxml_disable_entity_loader( true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
274 + return $pclzip_result;
275 + }
276 +
277 + /**
278 + * Extracts the files of a ZIP file using the PHP ZipArchive class.
279 + *
280 + * The ZIP file is extracted to a temporary folder and a list of files and their location is returned.
281 + *
282 + * @since 2.3.0
283 + *
284 + * @param File $zip_file File data of a ZIP file (likely in a temporary folder).
285 + * @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
286 + */
287 + protected function extract_zip_file_ziparchive( File $zip_file ) /* : array|WP_Error */ {
288 + $archive = new ZipArchive();
289 + $archive_opened = $archive->open( $zip_file->location, ZipArchive::CHECKCONS );
290 +
291 + // If the ZIP file can't be opened with ZipArchive::CHECKCONS, try again without.
292 + if ( true !== $archive_opened ) {
293 + $archive_opened = $archive->open( $zip_file->location );
164 294 }
165 - // No warnings/errors raised, but stored internally.
166 - libxml_use_internal_errors( true );
167 - $dom = new DOMDocument( '1.0', 'UTF-8' );
168 - // No strict checking for invalid HTML.
169 - $dom->strictErrorChecking = false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
170 - $dom->loadHTML( $full_html );
171 - if ( false === $dom ) {
172 - $this->imported_table = false;
173 - return;
295 +
296 + // If the ZIP file can't even be opened without ZipArchive::CHECKCONS, bail.
297 + if ( true !== $archive_opened ) {
298 + return new WP_Error( 'table_import_error_zip_open', '', array( 'ziparchive_error' => $archive_opened ) );
174 299 }
175 - $dom_tables = $dom->getElementsByTagName( 'table' );
176 - if ( 0 === count( $dom_tables ) ) {
177 - $this->imported_table = false;
178 - return;
300 +
301 + $files = array();
302 +
303 + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
304 + for ( $file_idx = 0; $file_idx < $archive->numFiles; $file_idx++ ) {
305 + $file_name = $archive->getNameIndex( $file_idx );
306 +
307 + if ( false === $file_name ) {
308 + $files[] = new File( array(
309 + 'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
310 + ) );
311 + continue;
312 + }
313 +
314 + // Skip directories.
315 + if ( str_ends_with( $file_name, '/' ) ) {
316 + continue;
317 + }
318 +
319 + // Skip the __MACOSX directory that macOS adds to archives.
320 + if ( str_starts_with( $file_name, '__MACOSX/' ) ) {
321 + continue;
322 + }
323 +
324 + // Don't extract invalid files.
325 + if ( 0 !== validate_file( $file_name ) ) {
326 + continue;
327 + }
328 +
329 + $file_data = $archive->getFromIndex( $file_idx );
330 + if ( false === $file_data ) {
331 + $files[] = new File( array(
332 + 'name' => $file_name,
333 + 'error' => new WP_Error( 'table_import_error_zip_get_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
334 + ) );
335 + continue;
336 + }
337 +
338 + $location = wp_tempnam();
339 + $num_written_bytes = file_put_contents( $location, $file_data );
340 + if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
341 + @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
342 + $files[] = new File( array(
343 + 'name' => $file_name,
344 + 'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
345 + ) );
346 + continue;
347 + }
348 +
349 + $files[] = new File( array(
350 + 'location' => $location,
351 + 'name' => $file_name,
352 + ) );
179 353 }
180 - libxml_clear_errors(); // Clear errors so that we only catch those inside the table in the next line.
181 - $table = simplexml_import_dom( $dom_tables->item( 0 ) );
182 - if ( false === $table ) {
183 - $this->imported_table = false;
184 - return;
354 +
355 + $archive->close();
356 +
357 + return $files;
358 + }
359 +
360 + /**
361 + * Extracts the files of a ZIP file using WordPress' PclZip class.
362 + *
363 + * The ZIP file is extracted to a temporary folder and a list of files and their location is returned.
364 + *
365 + * @since 2.3.0
366 + *
367 + * @param File $zip_file File data of a ZIP file (likely in a temporary folder).
368 + * @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
369 + */
370 + protected function extract_zip_file_pclzip( File $zip_file ) /* : array|WP_Error */ {
371 + mbstring_binary_safe_encoding();
372 +
373 + require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
374 +
375 + $archive = new PclZip( $zip_file->location );
376 + $archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING ); // @phpstan-ignore arguments.count (PclZip::extract() uses `func_get_args()` to handle optional arguments.)
377 +
378 + reset_mbstring_encoding();
379 +
380 + // If the ZIP file can't be opened, bail.
381 + if ( ! is_array( $archive_files ) ) {
382 + return new WP_Error( 'table_import_error_zip_open', '', array( 'pclzip_error' => $archive->errorInfo( true ) ) );
185 383 }
186 384
187 - $errors = libxml_get_errors();
188 - libxml_clear_errors();
189 - if ( ! empty( $errors ) ) {
190 - $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />';
191 - foreach ( $errors as $error ) {
192 - switch ( $error->level ) {
193 - case LIBXML_ERR_WARNING:
194 - $output .= "Warning {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
195 - break;
196 - case LIBXML_ERR_ERROR:
197 - $output .= "Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
198 - break;
199 - case LIBXML_ERR_FATAL:
200 - $output .= "Fatal Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
201 - break;
202 - }
385 + $files = array();
386 +
387 + foreach ( $archive_files as $file ) {
388 + // Skip directories.
389 + if ( $file['folder'] ) {
390 + continue;
203 391 }
204 - wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
205 - }
206 392
207 - $html_table = array(
208 - 'data' => array(),
209 - 'options' => array(),
210 - );
211 - if ( isset( $table->thead ) ) {
212 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->thead[0]->tr ) );
213 - $html_table['options']['table_head'] = true;
393 + // Skip the __MACOSX directory that macOS adds to archives.
394 + if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) {
395 + continue;
396 + }
397 +
398 + // Don't extract invalid files.
399 + if ( 0 !== validate_file( $file['filename'] ) ) {
400 + continue;
401 + }
402 +
403 + $location = wp_tempnam();
404 + $num_written_bytes = file_put_contents( $location, $file['content'] );
405 + if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
406 + @unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
407 + $files[] = new File( array(
408 + 'name' => $file['filename'],
409 + 'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file['index'], 'ziparchive_file_name' => $file['filename'] ) ),
410 + ) );
411 + continue;
412 + }
413 +
414 + $files[] = new File( array(
415 + 'location' => $location,
416 + 'name' => $file['filename'],
417 + ) );
214 418 }
215 - if ( isset( $table->tbody ) ) {
216 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tbody[0]->tr ) );
217 - }
218 - if ( isset( $table->tr ) ) {
219 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tr ) );
220 - }
221 - if ( isset( $table->tfoot ) ) {
222 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tfoot[0]->tr ) );
223 - $html_table['options']['table_foot'] = true;
224 - }
225 419
226 - $html_table['data'] = $this->pad_array_to_max_cols( $html_table['data'] );
227 - $this->imported_table = $html_table;
420 + return $files;
228 421 }
229 422
230 423 /**
231 - * Helper for HTML import.
424 + * Deletes a file unless the `keep_file` property is set to `true`.
232 425 *
233 - * @since 1.0.0
426 + * @since 2.0.0
234 427 *
235 - * @param SimpleXMLElement $element XMLElement.
236 - * @return array SimpleXMLElement exported to an array.
428 + * @param File $file File that should maybe be deleted.
237 429 */
238 - protected function _import_html_rows( $element ) {
239 - $rows = array();
240 - foreach ( $element as $row ) {
241 - $new_row = array();
242 - foreach ( $row as $cell ) {
243 - // Get text between <td>...</td>, or <th>...</th>, possibly with attributes.
244 - if ( 1 === preg_match( '#<t(?:d|h).*?>(.*)</t(?:d|h)>#is', $cell->asXML(), $matches ) ) {
245 - /*
246 - * Decode HTML entities again, as there might be some left especially in attributes of HTML tags in the cells,
247 - * see https://secure.php.net/manual/en/simplexmlelement.asxml.php#107137.
248 - */
249 - $matches[1] = html_entity_decode( $matches[1], ENT_NOQUOTES, 'UTF-8' );
250 - $new_row[] = $matches[1];
430 + protected function maybe_unlink_file( File $file ): void {
431 + if ( ! $file->keep_file && file_exists( $file->location ) ) {
432 + @unlink( $file->location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
433 + }
434 + }
251 435
252 - // Look for colspan and add correct number of cells.
253 - if ( 1 === preg_match( '#<t(?:d|h).*colspan="(\d+)".*?>#is', $cell->asXml(), $matches ) ) {
254 - for ( $i = 1; $i < (int) $matches[1]; $i++ ) {
255 - $new_row[] = '#colspan#';
256 - }
257 - }
258 - } else {
259 - $new_row[] = '';
260 - }
436 + /**
437 + * Prepares a list of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
438 + *
439 + * @since 2.0.0
440 + *
441 + * @return array<string, string[]> List of table names and IDs.
442 + */
443 + protected function get_list_of_table_names(): array {
444 + $existing_tables = array();
445 + // Load all table IDs and names for a comparison with the file name.
446 + $table_ids = TablePress::$model_table->load_all( false );
447 + foreach ( $table_ids as $table_id ) {
448 + // Load table, without table data, options, and visibility settings.
449 + $table = TablePress::$model_table->load( $table_id, false, false );
450 + if ( ! is_wp_error( $table ) ) {
451 + $existing_tables[ (string) $table['name'] ][] = $table_id; // Attention: The table name is not unique!
261 452 }
262 - $rows[] = $new_row;
263 453 }
264 - return $rows;
454 + return $existing_tables;
265 455 }
266 456
267 457 /**
268 - * Import JSON data.
458 + * Checks whether the requirements for the PHPSpreadsheet import class are fulfilled or if the legacy import class should be used.
269 459 *
270 - * @since 1.0.0
460 + * @since 2.0.0
461 + *
462 + * @return bool Whether the legacy import class should be used.
271 463 */
272 - protected function import_json() {
273 - $json_table = json_decode( $this->import_data, true );
464 + protected function should_use_legacy_import_class(): bool {
465 + // Allow overriding in the import config (coming e.g. from the import form UI).
466 + if ( $this->import_config['legacy_import'] ) {
467 + return true;
468 + }
274 469
275 - // Check if JSON could be decoded.
276 - if ( is_null( $json_table ) ) {
277 - // If possible, try to find out what error prevented the JSON from being decoded.
278 - $json_error = 'The error could not be determined.';
279 - $json_error_msg = json_last_error_msg();
280 - if ( false !== $json_error_msg ) {
281 - $json_error = $json_error_msg;
282 - }
283 - $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . "</strong><br /><br />JSON error: {$json_error}<br />";
284 - wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
470 + /**
471 + * Filters whether the Legacy Table Import class shall be used.
472 + *
473 + * @since 2.0.0
474 + *
475 + * @param bool $use_legacy_class Whether to use the legacy table import class. Default false.
476 + */
477 + if ( apply_filters( 'tablepress_use_legacy_table_import_class', false ) ) {
478 + return true;
285 479 }
286 480
287 - // Specifically cast to an array again.
288 - $json_table = (array) $json_table;
289 -
290 - if ( isset( $json_table['data'] ) ) {
291 - // JSON data contained a full export.
292 - $table = $json_table;
293 - } else {
294 - // JSON data contained only the data of a table, but no options.
295 - $table = array( 'data' => array() );
296 - foreach ( $json_table as $row ) {
297 - $table['data'][] = array_values( (array) $row );
298 - }
481 + // Use the legacy import class, if the requirements for PHPSpreadsheet are not fulfilled.
482 + $phpspreadsheet_requirements_fulfilled = extension_loaded( 'mbstring' )
483 + && class_exists( 'ZipArchive', false )
484 + && class_exists( 'DOMDocument', false )
485 + && function_exists( 'simplexml_load_string' )
486 + && ( function_exists( 'libxml_disable_entity_loader' ) || PHP_VERSION_ID >= 80000 ); // This function is only needed for older versions of PHP.
487 + if ( ! $phpspreadsheet_requirements_fulfilled ) {
488 + return true;
299 489 }
300 490
301 - $table['data'] = $this->pad_array_to_max_cols( $table['data'] );
302 - $this->imported_table = $table;
491 + return false;
303 492 }
304 493
305 494 /**
306 - * Import Microsoft Excel 97-2003 data.
495 + * Imports all found/extracted/configured files into TablePress.
307 496 *
308 - * @since 1.1.0
497 + * @since 2.0.0
498 + *
499 + * @param File[] $import_files Files that shall be imported.
500 + * @return array{tables: array<int, array<string, mixed>>, errors: File[]} Imported tables and files that caused errors.
309 501 */
310 - protected function import_xls() {
311 - $excel_reader = TablePress::load_class( 'Spreadsheet_Excel_Reader', 'excel-reader.class.php', 'libraries', $this->import_data );
502 + protected function import_files( array $import_files ): array {
503 + $tables = array();
504 + $errors = array();
312 505
313 - // Loop through Excel file and retrieve value and colspan/rowspan properties for each cell.
314 - $sheet = 0; // 0 means first sheet of the Workbook
315 - $table = array();
316 - for ( $row = 1; $row <= $excel_reader->rowcount( $sheet ); $row++ ) {
317 - $table_row = array();
318 - for ( $col = 1; $col <= $excel_reader->colcount( $sheet ); $col++ ) {
319 - $cell = array();
320 - $cell['rowspan'] = $excel_reader->rowspan( $row, $col, $sheet );
321 - $cell['colspan'] = $excel_reader->colspan( $row, $col, $sheet );
322 - $cell['val'] = $excel_reader->val( $row, $col, $sheet );
323 - $table_row[] = $cell;
324 - }
325 - $table[] = $table_row;
326 - }
506 + $use_legacy_import_class = $this->should_use_legacy_import_class();
327 507
328 - // Transform colspan/rowspan properties to TablePress equivalent (cell content).
329 - foreach ( $table as $row_idx => $row ) {
330 - foreach ( $row as $col_idx => $cell ) {
331 - if ( 1 === $cell['rowspan'] && 1 === $cell['colspan'] ) {
332 - continue;
333 - }
508 + // Load Import Base Class.
509 + TablePress::load_file( 'class-import-base.php', 'classes' );
334 510
335 - if ( 1 < $cell['colspan'] ) {
336 - for ( $i = 1; $i < $cell['colspan']; $i++ ) {
337 - $table[ $row_idx ][ $col_idx + $i ]['val'] = '#colspan#';
338 - }
339 - }
340 - if ( 1 < $cell['rowspan'] ) {
341 - for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
342 - $table[ $row_idx + $i ][ $col_idx ]['val'] = '#rowspan#';
343 - }
344 - }
511 + // Choose the Table Import library based on the PHP version and the filter hook value.
512 + if ( $use_legacy_import_class ) {
513 + // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
514 + $this->importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
515 + } else {
516 + // @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
517 + $this->importer = TablePress::load_class( 'TablePress_Import_PHPSpreadsheet', 'class-import-phpspreadsheet.php', 'classes' );
518 + }
345 519
346 - if ( 1 < $cell['rowspan'] && 1 < $cell['colspan'] ) {
347 - for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
348 - for ( $j = 1; $j < $cell['colspan']; $j++ ) {
349 - $table[ $row_idx + $i ][ $col_idx + $j ]['val'] = '#span#';
350 - }
520 + // If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
521 + if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) && '' !== $this->import_config['existing_table'] ) {
522 + $valid_import_files = 0;
523 + foreach ( $import_files as $file ) {
524 + if ( ! is_wp_error( $file->error ) ) {
525 + ++$valid_import_files;
526 + if ( $valid_import_files > 1 ) {
527 + $this->import_config['existing_table'] = '';
528 + break;
351 529 }
352 530 }
353 531 }
354 532 }
355 533
356 - // Flatten value property to two-dimensional array.
357 - $result_table = array();
358 - foreach ( $table as $row_idx => $row ) {
359 - $table_row = array();
360 - foreach ( $row as $col_idx => $cell ) {
361 - $table_row[] = (string) $cell['val'];
534 + // Loop through all import files and import them.
535 + foreach ( $import_files as $file ) {
536 + if ( is_wp_error( $file->error ) ) {
537 + $errors[] = $file;
538 + continue;
362 539 }
363 - $result_table[] = $table_row;
540 +
541 + // Use import method depending on chosen import class.
542 + if ( $use_legacy_import_class ) {
543 + $table = $this->load_table_from_file_legacy( $file );
544 + } else {
545 + $table = $this->load_table_from_file_phpspreadsheet( $file );
546 + }
547 +
548 + $this->maybe_unlink_file( $file );
549 +
550 + if ( is_wp_error( $table ) ) {
551 + $file->error = $table;
552 + $errors[] = $file;
553 + continue;
554 + }
555 +
556 + $table = $this->save_imported_table( $table, $file );
557 + if ( is_wp_error( $table ) ) {
558 + $file->error = $table;
559 + $errors[] = $file;
560 + continue;
561 + }
562 +
563 + $tables[] = $table;
364 564 }
365 565
366 - $this->imported_table = array( 'data' => $this->pad_array_to_max_cols( $result_table ) );
566 + return array(
567 + 'tables' => $tables,
568 + 'errors' => $errors,
569 + );
367 570 }
368 571
369 572 /**
370 - * Import Microsoft Excel 2007-2019 data.
573 + * Loads a table from a file via the legacy import class.
371 574 *
372 - * @since 1.1.0
575 + * @since 2.0.0
576 + *
577 + * @param File $file File with the table data.
578 + * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
373 579 */
374 - protected function import_xlsx() {
375 - TablePress::load_file( 'simplexlsx.class.php', 'libraries' );
376 - $xlsx_file = SimpleXLSX::parse( $this->import_data, true );
580 + protected function load_table_from_file_legacy( File $file ) /* : array|WP_Error */ {
581 + // Guess the import format from the file extension.
582 + switch ( $file->extension ) {
583 + case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet.
584 + case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded).
585 + case 'xltx': // Excel (OfficeOpenXML) Template.
586 + case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded).
587 + $format = 'xlsx';
588 + break;
589 + case 'xls': // Excel (BIFF) Spreadsheet.
590 + case 'xlt': // Excel (BIFF) Template.
591 + $format = 'xls';
592 + break;
593 + case 'htm':
594 + case 'html':
595 + $format = 'html';
596 + break;
597 + case 'csv':
598 + case 'tsv':
599 + $format = 'csv';
600 + break;
601 + case 'json':
602 + $format = 'json';
603 + break;
604 + default:
605 + // If no format was found, try finding the format from the first character below.
606 + $format = '';
607 + }
377 608
378 - if ( ! $xlsx_file ) {
379 - $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />' . SimpleXLSX::parseError() . '<br />';
380 - wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
609 + $data = file_get_contents( $file->location );
610 + if ( false === $data ) {
611 + return new WP_Error( 'table_import_legacy_data_read', '', $file->location );
381 612 }
613 + if ( '' === $data ) {
614 + return new WP_Error( 'table_import_legacy_data_empty', '', $file->location );
615 + }
382 616
383 - $this->imported_table = array( 'data' => $xlsx_file->rows() );
617 + // If no format could be determined from the file extension, try guessing from the file content.
618 + if ( '' === $format ) {
619 + $data = trim( $data );
620 + $first_character = $data[0];
621 + $last_character = $data[-1];
622 +
623 + if ( '<' === $first_character && '>' === $last_character ) {
624 + $format = 'html';
625 + } elseif ( ( '[' === $first_character && ']' === $last_character ) || ( '{' === $first_character && '}' === $last_character ) ) {
626 + $json_table = json_decode( $data, true );
627 + if ( ! is_null( $json_table ) ) {
628 + $format = 'json';
629 + }
630 + }
631 + }
632 +
633 + // Fall back to CSV if no file format could be determined.
634 + if ( '' === $format ) {
635 + $format = 'csv';
636 + }
637 +
638 + if ( ! isset( $this->importer->import_formats[ $format ] ) ) {
639 + return new WP_Error( 'table_import_legacy_unknown_format', '', $file->name );
640 + }
641 +
642 + $table = $this->importer->import_table( $format, $data );
643 +
644 + if ( false === $table ) {
645 + return new WP_Error( 'table_import_legacy_importer_failed', '', array( 'file_name' => $file->name, 'file_format' => $format ) );
646 + }
647 +
648 + return $table;
384 649 }
385 650
386 651 /**
387 - * Make sure array is rectangular with $max_cols columns in every row.
652 + * Loads a table from a file via the PHPSpreadsheet import class.
388 653 *
389 - * @since 1.0.0
654 + * @since 2.0.0
390 655 *
391 - * @param array $array Two-dimensional array to be padded.
392 - * @return array Padded array.
656 + * @param File $file File with the table data.
657 + * @return array<string, mixed>|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
393 658 */
394 - public function pad_array_to_max_cols( array $array ) {
395 - $rows = count( $array );
396 - $rows = ( $rows > 0 ) ? $rows : 1;
397 - $max_columns = $this->count_max_columns( $array );
398 - $max_columns = ( $max_columns > 0 ) ? $max_columns : 1;
399 - // array_map wants arrays as additional parameters, so we create one with the max_columns to pad to and one with the value to use (empty string).
400 - $max_columns_array = array_fill( 1, $rows, $max_columns );
401 - $pad_values_array = array_fill( 1, $rows, '' );
402 - return array_map( 'array_pad', $array, $max_columns_array, $pad_values_array );
659 + protected function load_table_from_file_phpspreadsheet( File $file ) /* : array|WP_Error */ {
660 + // Convert File object to array, as those are not yet used outside of this class.
661 + return $this->importer->import_table( $file ); // @phpstan-ignore return.type (This is an instance of TablePress_Import_PHPSpreadsheet which does not return false.)
403 662 }
404 663
405 664 /**
406 - * Get the highest number of columns in the rows.
665 + * Imports a loaded table into TablePress.
407 666 *
408 - * @since 1.0.0
667 + * @since 2.0.0
409 668 *
410 - * @param array $array Two-dimensional array.
411 - * @return int Highest number of columns in the rows of the array.
669 + * @param array<string, mixed> $table The table to be imported, either with properties or just the $table['data'] property set.
670 + * @param File $file File with the table data.
671 + * @return array<string, mixed>|WP_Error Imported table on success, WP_Error on failure.
412 672 */
413 - protected function count_max_columns( array $array ) {
414 - $max_columns = 0;
415 - foreach ( $array as $row_idx => $row ) {
416 - $num_columns = count( $row );
417 - $max_columns = max( $num_columns, $max_columns );
673 + protected function save_imported_table( array $table, File $file ) /* : array|WP_Error */ {
674 + // If name and description are imported from a new table, use those.
675 + if ( ! isset( $table['name'] ) ) {
676 + $table['name'] = $file->name;
418 677 }
419 - return $max_columns;
678 + if ( ! isset( $table['description'] ) ) {
679 + $table['description'] = $file->name;
680 + }
681 +
682 + $import_type = $this->import_config['type'];
683 + $existing_table_id = $this->import_config['existing_table'];
684 +
685 + // If no existing table ID has been set (or if we are importing multiple tables), try to find a potential existing table from the table ID in the import data or by comparing the file name with the table name.
686 + if ( in_array( $import_type, array( 'replace', 'append' ), true ) && '' === $existing_table_id ) {
687 + if ( isset( $table['id'] ) ) {
688 + // If the table already contained a table ID (e.g. for the JSON format), use that.
689 + $existing_table_id = $table['id'];
690 + } elseif ( isset( $this->table_names_ids[ $file->name ] ) && 1 === count( $this->table_names_ids[ $file->name ] ) ) {
691 + // Use the replace/append ID of tables where the table name matches the file name, but only if there was exactly one file name match.
692 + $existing_table_id = $this->table_names_ids[ $file->name ][0];
693 + }
694 + }
695 +
696 + // If the table that is to be replaced or appended to does not exist, add the new table instead.
697 + if ( ! TablePress::$model_table->table_exists( $existing_table_id ) ) {
698 + $existing_table_id = '';
699 + $import_type = 'add';
700 + }
701 +
702 + $table = $this->import_tablepress_table( $table, $import_type, $existing_table_id );
703 +
704 + return $table;
420 705 }
421 706
422 707 /**
423 - * Fixes the encoding to UTF-8 for the entire string that is to be imported.
708 + * Imports a table by either replacing or appending to an existing table or by adding it as a new table.
424 709 *
425 710 * @since 1.0.0
426 711 *
427 - * @link http://stevephillips.me/blog/dealing-php-and-character-encoding
712 + * @param array<string, mixed> $imported_table The table to be imported, either with properties or just the `name`, `description`, and `data` property set.
713 + * @param string $import_type What to do with the imported data: "add", "replace", "append".
714 + * @param string $existing_table_id Empty string if table shall be added as a new table, ID of the table to be replaced or appended to otherwise.
715 + * @return array<string, mixed>|WP_Error Table on success, WP_Error on error.
428 716 */
429 - protected function fix_table_encoding() {
430 - // Check and remove possible UTF-8 Byte-Order Mark (BOM).
431 - $bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
432 - if ( 0 === strncmp( $this->import_data, $bom, 3 ) ) {
433 - $this->import_data = substr( $this->import_data, 3 );
434 - // If data has a BOM, it's UTF-8, so further checks unnecessary.
435 - return;
717 + protected function import_tablepress_table( array $imported_table, string $import_type, string $existing_table_id ) /* : array|WP_Error */ {
718 + // Full JSON format table can contain a table ID, try to keep that, by later changing the imported table ID to this.
719 + $table_id_in_import = $imported_table['id'] ?? '';
720 +
721 + // To be able to replace or append to a table, the user must be able to edit the table, or it must be a request via the Automatic Periodic Table Import module.
722 + if ( in_array( $import_type, array( 'replace', 'append' ), true )
723 + && ! ( current_user_can( 'tablepress_edit_table', $existing_table_id ) || doing_action( 'tablepress_automatic_periodic_table_import_action' ) ) ) {
724 + return new WP_Error( 'table_import_replace_append_capability_check_failed', '', $existing_table_id );
436 725 }
437 726
438 - // Require the iconv() function for the following checks.
439 - if ( ! function_exists( 'iconv' ) ) {
440 - return;
727 + switch ( $import_type ) {
728 + case 'add':
729 + $existing_table = TablePress::$model_table->get_table_template();
730 + // Import visibility information if it exists, usually only for the JSON format.
731 + if ( isset( $imported_table['visibility'] ) ) {
732 + $existing_table['visibility'] = $imported_table['visibility'];
733 + }
734 + break;
735 + case 'replace':
736 + // Load table, without table data, but with options and visibility settings.
737 + $existing_table = TablePress::$model_table->load( $existing_table_id, false, true );
738 + if ( is_wp_error( $existing_table ) ) {
739 + $error = new WP_Error( 'table_import_replace_table_load', '', $existing_table_id );
740 + $error->merge_from( $existing_table );
741 + return $error;
742 + }
743 + // Don't change name and description when a table is replaced.
744 + $imported_table['name'] = $existing_table['name'];
745 + $imported_table['description'] = $existing_table['description'];
746 + // Replace visibility information if it exists.
747 + if ( isset( $imported_table['visibility'] ) ) {
748 + $existing_table['visibility'] = $imported_table['visibility'];
749 + }
750 + break;
751 + case 'append':
752 + // Load table, with table data, options, and visibility settings.
753 + $existing_table = TablePress::$model_table->load( $existing_table_id, true, true );
754 + if ( is_wp_error( $existing_table ) ) {
755 + $error = new WP_Error( 'table_import_append_table_load', '', $existing_table_id );
756 + $error->merge_from( $existing_table );
757 + return $error;
758 + }
759 + if ( isset( $existing_table['is_corrupted'] ) && $existing_table['is_corrupted'] ) {
760 + return new WP_Error( 'table_import_append_table_load_corrupted', '', $existing_table_id );
761 + }
762 + // Don't change name and description when a table is appended to.
763 + $imported_table['name'] = $existing_table['name'];
764 + $imported_table['description'] = $existing_table['description'];
765 + // Actual appending:.
766 + $imported_table['data'] = array_merge( $existing_table['data'], $imported_table['data'] );
767 + $this->importer->pad_array_to_max_cols( $imported_table['data'] );
768 + // Append visibility information for rows.
769 + if ( isset( $imported_table['visibility']['rows'] ) ) {
770 + $existing_table['visibility']['rows'] = array_merge( $existing_table['visibility']['rows'], $imported_table['visibility']['rows'] );
771 + }
772 + // When appending, do not overwrite options, e.g. coming from a JSON file.
773 + unset( $imported_table['options'] );
774 + break;
775 + default:
776 + return new WP_Error( 'table_import_import_type_invalid', '', $import_type );
441 777 }
442 778
443 - // Check for possible UTF-16 BOMs ("little endian" and "big endian") and try to convert the data to UTF-8.
444 - if ( "\xFF\xFE" === substr( $this->import_data, 0, 2 ) || "\xFE\xFF" === substr( $this->import_data, 0, 2 ) ) {
445 - // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
446 - $data = @iconv( 'UTF-16', 'UTF-8', $this->import_data );
447 - if ( false !== $data ) {
448 - $this->import_data = $data;
449 - return;
450 - }
779 + // Merge new or existing table with information from the imported table.
780 + $imported_table['id'] = $existing_table['id']; // Will be false for new table or the existing table ID.
781 + // Cut visibility array (if the imported table is smaller), and pad correctly if imported table is bigger than existing table (or new template).
782 + $num_rows = count( $imported_table['data'] );
783 + $num_columns = count( $imported_table['data'][0] );
784 + $imported_table['visibility'] = array(
785 + 'rows' => array_pad( array_slice( $existing_table['visibility']['rows'], 0, $num_rows ), $num_rows, 1 ),
786 + 'columns' => array_pad( array_slice( $existing_table['visibility']['columns'], 0, $num_columns ), $num_columns, 1 ),
787 + );
788 +
789 + // Check if the new table data is valid and consistent.
790 + $table = TablePress::$model_table->prepare_table( $existing_table, $imported_table, false );
791 + if ( is_wp_error( $table ) ) {
792 + $error = new WP_Error( 'table_import_table_prepare', '', $imported_table['id'] );
793 + $error->merge_from( $table );
794 + return $error;
451 795 }
452 796
453 - // Detect the character encoding and convert to UTF-8, if it's different.
454 - if ( function_exists( 'mb_detect_encoding' ) ) {
455 - $current_encoding = mb_detect_encoding( $this->import_data, 'ASCII, UTF-8, ISO-8859-1' );
456 - if ( 'UTF-8' !== $current_encoding ) {
457 - // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
458 - $data = @iconv( $current_encoding, 'UTF-8', $this->import_data );
459 - if ( false !== $data ) {
460 - $this->import_data = $data;
461 - return;
462 - }
797 + // DataTables Custom Commands can only be edit by trusted users.
798 + if ( ! current_user_can( 'unfiltered_html' ) ) {
799 + $table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
800 + }
801 +
802 + // Replace existing table or add new table.
803 + if ( in_array( $import_type, array( 'replace', 'append' ), true ) ) {
804 + // Replace existing table with imported/appended table.
805 + $table_id = TablePress::$model_table->save( $table );
806 + } else {
807 + // Add the imported table (and get its first ID).
808 + $table_id = TablePress::$model_table->add( $table );
809 + }
810 +
811 + if ( is_wp_error( $table_id ) ) {
812 + $error = new WP_Error( 'table_import_table_save_or_add', '', $table['id'] );
813 + $error->merge_from( $table_id );
814 + return $error;
815 + }
816 +
817 + // Try to use ID from imported file (e.g. in full JSON format table).
818 + if ( '' !== $table_id_in_import && $table_id !== $table_id_in_import && current_user_can( 'tablepress_edit_table_id', $table_id ) ) {
819 + $id_changed = TablePress::$model_table->change_table_id( $table_id, $table_id_in_import );
820 + if ( ! is_wp_error( $id_changed ) ) {
821 + $table_id = $table_id_in_import;
463 822 }
464 823 }
824 +
825 + $table['id'] = $table_id;
826 +
827 + return $table;
828 + }
829 +
830 + /**
831 + * Imports a table in legacy versions of the Table Auto Update Extension.
832 + *
833 + * This method is deprecated and is only left for backward compatibility reasons. Do not use this in new code!
834 + *
835 + * @since 1.0.0
836 + * @deprecated 2.0.0 Use `run()` instead.
837 + *
838 + * @param string $format Import format.
839 + * @param string $data Data to import.
840 + * @return array<string, mixed>|WP_Error|false Table array on success, WP_Error or false on error.
841 + */
842 + public function import_table( string $format, string $data ) /* : array|false */ {
843 + TablePress::load_file( 'class-import-base.php', 'classes' );
844 + $importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
845 + return $importer->import_table( $format, $data );
465 846 }
466 847
467 848 } // class TablePress_Import