PluginProbe
TablePress – Tables in WordPress made easy / 3.0
TablePress – Tables in WordPress made easy v3.0
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 +688 -302 1.123.0 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,443 +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
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 bool|array False on error, table array on success.
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' ) ) ) {
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 - // Don't expand external entities, see https://websec.io/2012/08/27/Preventing-XXE-in-PHP.html.
160 - libxml_disable_entity_loader( true );
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 );
161 294 }
162 - // No warnings/errors raised, but stored internally.
163 - libxml_use_internal_errors( true );
164 - $dom = new DOMDocument( '1.0', 'UTF-8' );
165 - // No strict checking for invalid HTML.
166 - $dom->strictErrorChecking = false;
167 - $dom->loadHTML( $full_html );
168 - if ( false === $dom ) {
169 - $this->imported_table = false;
170 - 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 ) );
171 299 }
172 - $dom_tables = $dom->getElementsByTagName( 'table' );
173 - if ( 0 === count( $dom_tables ) ) {
174 - $this->imported_table = false;
175 - 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 + ) );
176 353 }
177 - libxml_clear_errors(); // Clear errors so that we only catch those inside the table in the next line.
178 - $table = simplexml_import_dom( $dom_tables->item( 0 ) );
179 - if ( false === $table ) {
180 - $this->imported_table = false;
181 - 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 ) ) );
182 383 }
183 384
184 - $errors = libxml_get_errors();
185 - libxml_clear_errors();
186 - if ( ! empty( $errors ) ) {
187 - $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />';
188 - foreach ( $errors as $error ) {
189 - switch ( $error->level ) {
190 - case LIBXML_ERR_WARNING:
191 - $output .= "Warning {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
192 - break;
193 - case LIBXML_ERR_ERROR:
194 - $output .= "Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
195 - break;
196 - case LIBXML_ERR_FATAL:
197 - $output .= "Fatal Error {$error->code}: {$error->message} in line {$error->line}, column {$error->column}<br />";
198 - break;
199 - }
385 + $files = array();
386 +
387 + foreach ( $archive_files as $file ) {
388 + // Skip directories.
389 + if ( $file['folder'] ) {
390 + continue;
200 391 }
201 - wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
202 - }
203 392
204 - $html_table = array(
205 - 'data' => array(),
206 - 'options' => array(),
207 - );
208 - if ( isset( $table->thead ) ) {
209 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->thead[0]->tr ) );
210 - $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 + ) );
211 418 }
212 - if ( isset( $table->tbody ) ) {
213 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tbody[0]->tr ) );
214 - }
215 - if ( isset( $table->tr ) ) {
216 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tr ) );
217 - }
218 - if ( isset( $table->tfoot ) ) {
219 - $html_table['data'] = array_merge( $html_table['data'], $this->_import_html_rows( $table->tfoot[0]->tr ) );
220 - $html_table['options']['table_foot'] = true;
221 - }
222 419
223 - $html_table['data'] = $this->pad_array_to_max_cols( $html_table['data'] );
224 - $this->imported_table = $html_table;
420 + return $files;
225 421 }
226 422
227 423 /**
228 - * Helper for HTML import.
424 + * Deletes a file unless the `keep_file` property is set to `true`.
229 425 *
230 - * @since 1.0.0
426 + * @since 2.0.0
231 427 *
232 - * @param SimpleXMLElement $element XMLElement.
233 - * @return array SimpleXMLElement exported to an array.
428 + * @param File $file File that should maybe be deleted.
234 429 */
235 - protected function _import_html_rows( $element ) {
236 - $rows = array();
237 - foreach ( $element as $row ) {
238 - $new_row = array();
239 - foreach ( $row as $cell ) {
240 - // Get text between <td>...</td>, or <th>...</th>, possibly with attributes.
241 - if ( 1 === preg_match( '#<t(?:d|h).*?>(.*)</t(?:d|h)>#is', $cell->asXML(), $matches ) ) {
242 - /*
243 - * Decode HTML entities again, as there might be some left especially in attributes of HTML tags in the cells,
244 - * see https://secure.php.net/manual/en/simplexmlelement.asxml.php#107137.
245 - */
246 - $matches[1] = html_entity_decode( $matches[1], ENT_NOQUOTES, 'UTF-8' );
247 - $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 + }
248 435
249 - // Look for colspan and add correct number of cells.
250 - if ( 1 === preg_match( '#<t(?:d|h).*colspan="(\d+)".*?>#is', $cell->asXml(), $matches ) ) {
251 - for ( $i = 1; $i < (int) $matches[1]; $i++ ) {
252 - $new_row[] = '#colspan#';
253 - }
254 - }
255 - } else {
256 - $new_row[] = '';
257 - }
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!
258 452 }
259 - $rows[] = $new_row;
260 453 }
261 - return $rows;
454 + return $existing_tables;
262 455 }
263 456
264 457 /**
265 - * Import JSON data.
458 + * Checks whether the requirements for the PHPSpreadsheet import class are fulfilled or if the legacy import class should be used.
266 459 *
267 - * @since 1.0.0
460 + * @since 2.0.0
461 + *
462 + * @return bool Whether the legacy import class should be used.
268 463 */
269 - protected function import_json() {
270 - $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 + }
271 469
272 - // Check if JSON could be decoded.
273 - if ( is_null( $json_table ) ) {
274 - // If possible, try to find out what error prevented the JSON from being decoded.
275 - $json_error = 'The error could not be determined.';
276 - $json_error_msg = json_last_error_msg();
277 - if ( false !== $json_error_msg ) {
278 - $json_error = $json_error_msg;
279 - }
280 - $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . "</strong><br /><br />JSON error: {$json_error}<br />";
281 - wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
282 - } else {
283 - // Specifically cast to an array again.
284 - $json_table = (array) $json_table;
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 - if ( isset( $json_table['data'] ) ) {
288 - // JSON data contained a full export.
289 - $table = $json_table;
290 - } else {
291 - // JSON data contained only the data of a table, but no options.
292 - $table = array( 'data' => array() );
293 - foreach ( $json_table as $row ) {
294 - $table['data'][] = array_values( (array) $row );
295 - }
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;
296 489 }
297 490
298 - $table['data'] = $this->pad_array_to_max_cols( $table['data'] );
299 - $this->imported_table = $table;
491 + return false;
300 492 }
301 493
302 494 /**
303 - * Import Microsoft Excel 97-2003 data.
495 + * Imports all found/extracted/configured files into TablePress.
304 496 *
305 - * @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.
306 501 */
307 - protected function import_xls() {
308 - $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();
309 505
310 - // Loop through Excel file and retrieve value and colspan/rowspan properties for each cell.
311 - $sheet = 0; // 0 means first sheet of the Workbook
312 - $table = array();
313 - for ( $row = 1; $row <= $excel_reader->rowcount( $sheet ); $row++ ) {
314 - $table_row = array();
315 - for ( $col = 1; $col <= $excel_reader->colcount( $sheet ); $col++ ) {
316 - $cell = array();
317 - $cell['rowspan'] = $excel_reader->rowspan( $row, $col, $sheet );
318 - $cell['colspan'] = $excel_reader->colspan( $row, $col, $sheet );
319 - $cell['val'] = $excel_reader->val( $row, $col, $sheet );
320 - $table_row[] = $cell;
321 - }
322 - $table[] = $table_row;
323 - }
506 + $use_legacy_import_class = $this->should_use_legacy_import_class();
324 507
325 - // Transform colspan/rowspan properties to TablePress equivalent (cell content).
326 - foreach ( $table as $row_idx => $row ) {
327 - foreach ( $row as $col_idx => $cell ) {
328 - if ( 1 === $cell['rowspan'] && 1 === $cell['colspan'] ) {
329 - continue;
330 - }
508 + // Load Import Base Class.
509 + TablePress::load_file( 'class-import-base.php', 'classes' );
331 510
332 - if ( 1 < $cell['colspan'] ) {
333 - for ( $i = 1; $i < $cell['colspan']; $i++ ) {
334 - $table[ $row_idx ][ $col_idx + $i ]['val'] = '#colspan#';
335 - }
336 - }
337 - if ( 1 < $cell['rowspan'] ) {
338 - for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
339 - $table[ $row_idx + $i ][ $col_idx ]['val'] = '#rowspan#';
340 - }
341 - }
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 + }
342 519
343 - if ( 1 < $cell['rowspan'] && 1 < $cell['colspan'] ) {
344 - for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
345 - for ( $j = 1; $j < $cell['colspan']; $j++ ) {
346 - $table[ $row_idx + $i ][ $col_idx + $j ]['val'] = '#span#';
347 - }
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;
348 529 }
349 530 }
350 531 }
351 532 }
352 533
353 - // Flatten value property to two-dimensional array.
354 - $result_table = array();
355 - foreach ( $table as $row_idx => $row ) {
356 - $table_row = array();
357 - foreach ( $row as $col_idx => $cell ) {
358 - $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;
359 539 }
360 - $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;
361 564 }
362 565
363 - $this->imported_table = array( 'data' => $this->pad_array_to_max_cols( $result_table ) );
566 + return array(
567 + 'tables' => $tables,
568 + 'errors' => $errors,
569 + );
364 570 }
365 571
366 572 /**
367 - * Import Microsoft Excel 2007-2019 data.
573 + * Loads a table from a file via the legacy import class.
368 574 *
369 - * @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.
370 579 */
371 - protected function import_xlsx() {
372 - TablePress::load_file( 'simplexlsx.class.php', 'libraries' );
373 - $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 + }
374 608
375 - if ( $xlsx_file ) {
376 - $this->imported_table = array( 'data' => $xlsx_file->rows() );
377 - } else {
378 - $output = '<strong>' . __( 'The imported file contains errors:', 'tablepress' ) . '</strong><br /><br />' . SimpleXLSX::parseError() . '<br />';
379 - 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 );
380 612 }
613 + if ( '' === $data ) {
614 + return new WP_Error( 'table_import_legacy_data_empty', '', $file->location );
615 + }
616 +
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;
381 649 }
382 650
383 651 /**
384 - * Make sure array is rectangular with $max_cols columns in every row.
652 + * Loads a table from a file via the PHPSpreadsheet import class.
385 653 *
386 - * @since 1.0.0
654 + * @since 2.0.0
387 655 *
388 - * @param array $array Two-dimensional array to be padded.
389 - * @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.
390 658 */
391 - public function pad_array_to_max_cols( array $array ) {
392 - $rows = count( $array );
393 - $rows = ( $rows > 0 ) ? $rows : 1;
394 - $max_columns = $this->count_max_columns( $array );
395 - $max_columns = ( $max_columns > 0 ) ? $max_columns : 1;
396 - // 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).
397 - $max_columns_array = array_fill( 1, $rows, $max_columns );
398 - $pad_values_array = array_fill( 1, $rows, '' );
399 - 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.)
400 662 }
401 663
402 664 /**
403 - * Get the highest number of columns in the rows.
665 + * Imports a loaded table into TablePress.
404 666 *
405 - * @since 1.0.0
667 + * @since 2.0.0
406 668 *
407 - * @param array $array Two-dimensional array.
408 - * @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.
409 672 */
410 - protected function count_max_columns( array $array ) {
411 - $max_columns = 0;
412 - foreach ( $array as $row_idx => $row ) {
413 - $num_columns = count( $row );
414 - $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;
415 677 }
416 - 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;
417 705 }
418 706
419 707 /**
420 - * 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.
421 709 *
422 710 * @since 1.0.0
423 711 *
424 - * @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.
425 716 */
426 - protected function fix_table_encoding() {
427 - // Check and remove possible UTF-8 Byte-Order Mark (BOM).
428 - $bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
429 - if ( 0 === strncmp( $this->import_data, $bom, 3 ) ) {
430 - $this->import_data = substr( $this->import_data, 3 );
431 - // If data has a BOM, it's UTF-8, so further checks unnecessary.
432 - 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 );
433 725 }
434 726
435 - // Require the iconv() function for the following checks.
436 - if ( ! function_exists( 'iconv' ) ) {
437 - 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 );
438 777 }
439 778
440 - // Check for possible UTF-16 BOMs ("little endian" and "big endian") and try to convert the data to UTF-8.
441 - if ( "\xFF\xFE" === substr( $this->import_data, 0, 2 ) || "\xFE\xFF" === substr( $this->import_data, 0, 2 ) ) {
442 - $data = @iconv( 'UTF-16', 'UTF-8', $this->import_data );
443 - if ( false !== $data ) {
444 - $this->import_data = $data;
445 - return;
446 - }
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;
447 795 }
448 796
449 - // Detect the character encoding and convert to UTF-8, if it's different.
450 - if ( function_exists( 'mb_detect_encoding' ) ) {
451 - $current_encoding = mb_detect_encoding( $this->import_data, 'ASCII, UTF-8, ISO-8859-1' );
452 - if ( 'UTF-8' !== $current_encoding ) {
453 - $data = @iconv( $current_encoding, 'UTF-8', $this->import_data );
454 - if ( false !== $data ) {
455 - $this->import_data = $data;
456 - return;
457 - }
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;
458 822 }
459 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 );
460 846 }
461 847
462 848 } // class TablePress_Import