PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 All 201 releases
← All changes | includes/Admin/Importer/WPImport.php +104 -28 4.5.2 → 4.9.3 View file →
@@ -1,11 +1,14 @@
1 1 <?php
2 -
2 +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- view template receives variables via extract(); prefixing is impractical.
3 +// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bulk-import write paths from upstream WP Importer; caching unwanted.
4 +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- inherited WP Importer hook contract (import_*, wp_import_*) preserved for compat with external listeners.
3 5 namespace WPDeveloper\BetterDocs\Admin\Importer;
4 6
5 7 use WP_Error;
6 8 use WP_Importer;
7 9 use WPDeveloper\BetterDocs\Admin\Importer\Parsers\CSV_Parser;
10 +use WPDeveloper\BetterDocs\Admin\WPMLSupport;
8 11
9 12 if ( ! defined( 'ABSPATH' ) ) {
10 13 exit; // Exit if accessed directly.
11 14 }
@@ -296,13 +299,12 @@
296 299 $action = $this->args['action'];
297 300
298 301 $import_data = $this->parse( $file );
299 302
300 - if ( isset( $import_data['type'] ) && $import_data['type'] === 'sample/csv' ) {
301 - $this->import_sample_data( $import_data['posts'], $action );
302 - return true;
303 - }
304 -
303 + // Check for a parser error first: on an invalid/unsupported file the
304 + // parser returns a WP_Error, and reading $import_data['type'] on it
305 + // would fatal ("Cannot use object of type WP_Error as array"). Report
306 + // it cleanly instead.
305 307 if ( is_wp_error( $import_data ) ) {
306 308 /**
307 309 * @var WP_Error $import_data ;
308 310 */
@@ -310,8 +312,13 @@
310 312
311 313 return false;
312 314 }
313 315
316 + if ( isset( $import_data['type'] ) && $import_data['type'] === 'sample/csv' ) {
317 + $this->import_sample_data( $import_data['posts'], $action );
318 + return true;
319 + }
320 +
314 321 $posts = $import_data['posts'];
315 322 // Use array_map to apply the callback function to each item in the array
316 323 $posts = array_map( [ $this, 'modify_post_type' ], $posts );
317 324
@@ -316,9 +323,9 @@
316 323 $posts = array_map( [ $this, 'modify_post_type' ], $posts );
317 324
318 325 if ( ! empty( $action ) ) {
319 326 $existing_posts = $this->args['existing_slug'];
320 - $existing_posts_array = explode( ',', $existing_posts );
327 + $existing_posts_array = is_array( $existing_posts ) ? $existing_posts : explode( ',', (string) $existing_posts );
321 328
322 329 if ( $existing_posts && $action == 'ignore' ) {
323 330 // Filter out posts with slugs in $existing_slugs_array
324 331 $filtered_posts = array_filter(
@@ -365,8 +372,16 @@
365 372
366 373 wp_defer_term_counting( true );
367 374 wp_defer_comment_counting( true );
368 375
376 + // Safety net for an aborted run: a PHP timeout, memory limit, fatal, or a
377 + // cut-off request can end the import before import_end() runs, which would
378 + // strand wp_defer_term_counting(true) and a stale {taxonomy}_children cache
379 + // and silently drop nested-category counts site-wide. A shutdown handler
380 + // guarantees the restore runs on the way out; it is idempotent, so the normal
381 + // import_end() path (which also calls it) makes this a no-op. (#167)
382 + register_shutdown_function( array( $this, 'restore_counting_and_hierarchy' ) );
383 +
369 384 do_action( 'import_start', $this );
370 385
371 386 return true;
372 387 }
@@ -378,8 +393,29 @@
378 393 wp_import_cleanup( $this->id );
379 394
380 395 wp_cache_flush();
381 396
397 + $this->restore_counting_and_hierarchy();
398 +
399 + do_action( 'import_end' );
400 + }
401 +
402 + /**
403 + * Turn term/comment counting back on and rebuild the term hierarchy.
404 + *
405 + * import_start() registers this as a shutdown handler so an aborted run (timeout,
406 + * memory, fatal, cut-off request) that never reaches import_end() cannot strand
407 + * wp_defer_term_counting(true) or a stale {taxonomy}_children cache. Idempotent,
408 + * so it is safe to call from both import_end() and the shutdown handler. (#167)
409 + */
410 + public function restore_counting_and_hierarchy() {
411 + static $restored = false;
412 +
413 + if ( $restored ) {
414 + return;
415 + }
416 + $restored = true;
417 +
382 418 foreach ( get_taxonomies() as $tax ) {
383 419 delete_option( "{$tax}_children" );
384 420 _get_term_hierarchy( $tax );
385 421 }
@@ -385,10 +421,8 @@
385 421 }
386 422
387 423 wp_defer_term_counting( false );
388 424 wp_defer_comment_counting( false );
389 -
390 - do_action( 'import_end' );
391 425 }
392 426
393 427 /**
394 428 * Retrieve authors from parsed WXR data and set it to `$this->>authors`.
@@ -644,9 +678,9 @@
644 678 }
645 679
646 680 public function existing_slug_action( $posts, $action ) {
647 681 $existing_posts = $this->args['existing_slug'];
648 - $existing_posts_array = explode( ',', $existing_posts );
682 + $existing_posts_array = is_array( $existing_posts ) ? $existing_posts : explode( ',', (string) $existing_posts );
649 683
650 684 if ( $existing_posts && $action == 'ignore' ) {
651 685 // Filter out posts with slugs in $existing_slugs_array
652 686 $filtered_posts = array_filter(
@@ -1219,13 +1253,28 @@
1219 1253 }
1220 1254
1221 1255 $post['postmeta'] = apply_filters( 'wp_import_post_meta', $post['postmeta'], $post_id, $post );
1222 1256
1257 + // WPML language metadata transported via synthetic postmeta — captured
1258 + // here so the keys never persist on the post (WPML's icl_translations
1259 + // is the source of truth on the target site).
1260 + $wpml_lang = '';
1261 + $wpml_source_slug = '';
1262 +
1223 1263 // Add/update post meta.
1224 1264 if ( ! empty( $post['postmeta'] ) ) {
1225 1265 $imported_meta_keys = []; // Track imported meta keys to prevent duplicates
1226 -
1266 +
1227 1267 foreach ( $post['postmeta'] as $meta ) {
1268 + if ( WPMLSupport::META_LANG === $meta['key'] ) {
1269 + $wpml_lang = (string) $meta['value'];
1270 + continue;
1271 + }
1272 + if ( WPMLSupport::META_SOURCE_SLUG === $meta['key'] ) {
1273 + $wpml_source_slug = (string) $meta['value'];
1274 + continue;
1275 + }
1276 +
1228 1277 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
1229 1278 $value = false;
1230 1279
1231 1280 if ( '_edit_last' === $key ) {
@@ -1240,12 +1289,12 @@
1240 1289 // Skip if this meta key has already been imported for this post
1241 1290 if ( isset( $imported_meta_keys[ $key ] ) ) {
1242 1291 continue;
1243 1292 }
1244 -
1293 +
1245 1294 // Mark this meta key as imported
1246 1295 $imported_meta_keys[ $key ] = true;
1247 -
1296 +
1248 1297 // Export gets meta straight from the DB so could have a serialized string.
1249 1298 if ( ! $value ) {
1250 1299 $value = maybe_unserialize( $meta['value'] );
1251 1300 }
@@ -1260,9 +1309,13 @@
1260 1309 }
1261 1310 }
1262 1311 }
1263 1312 }
1264 -
1313 +
1314 + if ( $wpml_lang !== '' ) {
1315 + WPMLSupport::assign_post_language( (int) $post_id, $wpml_lang, $wpml_source_slug );
1316 + }
1317 +
1265 1318 do_action( 'templately_import.process_post', $post, $this, $result );
1266 1319 }
1267 1320
1268 1321 unset( $this->posts );
@@ -1335,9 +1388,9 @@
1335 1388 private function fetch_remote_file( $url, $post ) {
1336 1389 include_once ABSPATH . '/wp-admin/includes/file.php';
1337 1390
1338 1391 // Extract the file name from the URL.
1339 - $file_name = basename( parse_url( $url, PHP_URL_PATH ) );
1392 + $file_name = basename( (string) wp_parse_url( $url, PHP_URL_PATH ) );
1340 1393
1341 1394 if ( ! $file_name ) {
1342 1395 $file_name = md5( $url );
1343 1396 }
@@ -1360,9 +1413,9 @@
1360 1413 ]
1361 1414 );
1362 1415
1363 1416 if ( is_wp_error( $remote_response ) ) {
1364 - @unlink( $tmp_file_name );
1417 + wp_delete_file( $tmp_file_name );
1365 1418
1366 1419 return new WP_Error( 'import_file_error', sprintf( /* translators: 1: WordPress error message, 2: WordPress error code. */esc_html__( 'Request failed due to an error: %1$s (%2$s)', 'betterdocs' ), esc_html( $remote_response->get_error_message() ), esc_html( $remote_response->get_error_code() ) ) );
1367 1420 }
1368 1421
@@ -1369,9 +1422,9 @@
1369 1422 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1370 1423
1371 1424 // Make sure the fetch was successful.
1372 1425 if ( 200 !== $remote_response_code ) {
1373 - @unlink( $tmp_file_name );
1426 + wp_delete_file( $tmp_file_name );
1374 1427
1375 1428 return new WP_Error( 'import_file_error', sprintf( /* translators: 1: HTTP error message, 2: HTTP error code. */esc_html__( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'betterdocs' ), get_status_header_desc( $remote_response_code ), esc_html( $remote_response_code ) ) );
1376 1429 }
1377 1430
@@ -1378,9 +1431,9 @@
1378 1431 $headers = wp_remote_retrieve_headers( $remote_response );
1379 1432
1380 1433 // Request failed.
1381 1434 if ( ! $headers ) {
1382 - @unlink( $tmp_file_name );
1435 + wp_delete_file( $tmp_file_name );
1383 1436
1384 1437 return new WP_Error( 'import_file_error', esc_html__( 'Remote server did not respond', 'betterdocs' ) );
1385 1438 }
1386 1439
@@ -1386,15 +1439,15 @@
1386 1439
1387 1440 $filesize = (int) filesize( $tmp_file_name );
1388 1441
1389 1442 if ( 0 === $filesize ) {
1390 - @unlink( $tmp_file_name );
1443 + wp_delete_file( $tmp_file_name );
1391 1444
1392 1445 return new WP_Error( 'import_file_error', esc_html__( 'Zero size file downloaded', 'betterdocs' ) );
1393 1446 }
1394 1447
1395 1448 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1396 - @unlink( $tmp_file_name );
1449 + wp_delete_file( $tmp_file_name );
1397 1450
1398 1451 return new WP_Error( 'import_file_error', esc_html__( 'Downloaded file has incorrect size', 'betterdocs' ) );
1399 1452 }
1400 1453
@@ -1399,9 +1452,9 @@
1399 1452 }
1400 1453
1401 1454 $max_size = (int) apply_filters( 'import_attachment_size_limit', self::DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT );
1402 1455 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1403 - @unlink( $tmp_file_name );
1456 + wp_delete_file( $tmp_file_name );
1404 1457
1405 1458 /* translators: %s: Max file size. */
1406 1459
1407 1460 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'betterdocs' ), size_format( $max_size ) ) );
@@ -1449,16 +1502,17 @@
1449 1502 $new_file = $uploads['path'] . "/$file_name";
1450 1503 $move_new_file = copy( $tmp_file_name, $new_file );
1451 1504
1452 1505 if ( ! $move_new_file ) {
1453 - @unlink( $tmp_file_name );
1506 + wp_delete_file( $tmp_file_name );
1454 1507
1455 1508 return new WP_Error( 'import_file_error', esc_html__( 'The uploaded file could not be moved', 'betterdocs' ) );
1456 1509 }
1457 1510
1458 - // Set correct file permissions.
1511 + // Set correct file permissions to match parent directory.
1459 1512 $stat = stat( dirname( $new_file ) );
1460 1513 $perms = $stat['mode'] & 0000666;
1514 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- WP_Filesystem chmod requires init; mirrors WP core media handler.
1461 1515 chmod( $new_file, $perms );
1462 1516
1463 1517 $upload = [
1464 1518 'file' => $new_file,
@@ -1567,14 +1621,29 @@
1567 1621 * @param string $file Path to WXR file for parsing
1568 1622 *
1569 1623 * @return array Information gathered from the WXR file
1570 1624 */
1571 - private function parse( $file ): array {
1572 - if ( $this->file_type == 'text/xml' ) {
1625 + private function parse( $file ) {
1626 + $type = strtolower( (string) $this->file_type );
1627 +
1628 + // The uploaded temp file has no extension, and browsers report a .xml
1629 + // upload inconsistently (text/xml, application/xml, text/plain, or
1630 + // nothing) — so prefer the original file name's extension when it was
1631 + // passed through, fall back to the MIME, and default anything that is
1632 + // not clearly CSV to the WordPress export parser. WXR_Parser validates
1633 + // the file and returns a WP_Error for a non-WXR file, which
1634 + // import_start() already reports cleanly; previously an unrecognised
1635 + // MIME left $parser null and fataled with "Call to a member function
1636 + // parse() on null".
1637 + $name = isset( $this->args['file_name'] ) ? (string) $this->args['file_name'] : (string) $this->requested_file_path;
1638 + $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
1639 +
1640 + if ( 'csv' === $ext || ( '' === $ext && strpos( $type, 'csv' ) !== false ) ) {
1641 + $parser = new CSV_Parser();
1642 + } else {
1573 1643 $parser = new WXR_Parser();
1574 - } elseif ( $this->file_type == 'text/csv' ) {
1575 - $parser = new CSV_Parser();
1576 1644 }
1645 +
1577 1646 return $parser->parse( $file );
1578 1647 }
1579 1648
1580 1649 /**
@@ -1652,9 +1721,16 @@
1652 1721 * @param $file
1653 1722 * @param array $args
1654 1723 */
1655 1724 public function __construct( $file, array $args = [] ) {
1656 - parent::__construct();
1725 + // WP core's WP_Importer has no constructor, and on PHP 8 calling
1726 + // parent::__construct() on a constructor-less parent throws
1727 + // "Error: Cannot call constructor" — which aborted every import. Only
1728 + // call the parent when it actually defines one (future-proof if WP adds
1729 + // it back).
1730 + if ( method_exists( get_parent_class( $this ), '__construct' ) ) {
1731 + parent::__construct();
1732 + }
1657 1733
1658 1734 $this->requested_file_path = $file;
1659 1735 $this->args = $args;
1660 1736