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 +80 -24 4.5.4 → 4.9.3 View file →
@@ -1,6 +1,8 @@
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;
@@ -297,13 +299,12 @@
297 299 $action = $this->args['action'];
298 300
299 301 $import_data = $this->parse( $file );
300 302
301 - if ( isset( $import_data['type'] ) && $import_data['type'] === 'sample/csv' ) {
302 - $this->import_sample_data( $import_data['posts'], $action );
303 - return true;
304 - }
305 -
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.
306 307 if ( is_wp_error( $import_data ) ) {
307 308 /**
308 309 * @var WP_Error $import_data ;
309 310 */
@@ -311,8 +312,13 @@
311 312
312 313 return false;
313 314 }
314 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 +
315 321 $posts = $import_data['posts'];
316 322 // Use array_map to apply the callback function to each item in the array
317 323 $posts = array_map( [ $this, 'modify_post_type' ], $posts );
318 324
@@ -317,9 +323,9 @@
317 323 $posts = array_map( [ $this, 'modify_post_type' ], $posts );
318 324
319 325 if ( ! empty( $action ) ) {
320 326 $existing_posts = $this->args['existing_slug'];
321 - $existing_posts_array = explode( ',', $existing_posts );
327 + $existing_posts_array = is_array( $existing_posts ) ? $existing_posts : explode( ',', (string) $existing_posts );
322 328
323 329 if ( $existing_posts && $action == 'ignore' ) {
324 330 // Filter out posts with slugs in $existing_slugs_array
325 331 $filtered_posts = array_filter(
@@ -366,8 +372,16 @@
366 372
367 373 wp_defer_term_counting( true );
368 374 wp_defer_comment_counting( true );
369 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 +
370 384 do_action( 'import_start', $this );
371 385
372 386 return true;
373 387 }
@@ -379,8 +393,29 @@
379 393 wp_import_cleanup( $this->id );
380 394
381 395 wp_cache_flush();
382 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 +
383 418 foreach ( get_taxonomies() as $tax ) {
384 419 delete_option( "{$tax}_children" );
385 420 _get_term_hierarchy( $tax );
386 421 }
@@ -386,10 +421,8 @@
386 421 }
387 422
388 423 wp_defer_term_counting( false );
389 424 wp_defer_comment_counting( false );
390 -
391 - do_action( 'import_end' );
392 425 }
393 426
394 427 /**
395 428 * Retrieve authors from parsed WXR data and set it to `$this->>authors`.
@@ -645,9 +678,9 @@
645 678 }
646 679
647 680 public function existing_slug_action( $posts, $action ) {
648 681 $existing_posts = $this->args['existing_slug'];
649 - $existing_posts_array = explode( ',', $existing_posts );
682 + $existing_posts_array = is_array( $existing_posts ) ? $existing_posts : explode( ',', (string) $existing_posts );
650 683
651 684 if ( $existing_posts && $action == 'ignore' ) {
652 685 // Filter out posts with slugs in $existing_slugs_array
653 686 $filtered_posts = array_filter(
@@ -1355,9 +1388,9 @@
1355 1388 private function fetch_remote_file( $url, $post ) {
1356 1389 include_once ABSPATH . '/wp-admin/includes/file.php';
1357 1390
1358 1391 // Extract the file name from the URL.
1359 - $file_name = basename( parse_url( $url, PHP_URL_PATH ) );
1392 + $file_name = basename( (string) wp_parse_url( $url, PHP_URL_PATH ) );
1360 1393
1361 1394 if ( ! $file_name ) {
1362 1395 $file_name = md5( $url );
1363 1396 }
@@ -1380,9 +1413,9 @@
1380 1413 ]
1381 1414 );
1382 1415
1383 1416 if ( is_wp_error( $remote_response ) ) {
1384 - @unlink( $tmp_file_name );
1417 + wp_delete_file( $tmp_file_name );
1385 1418
1386 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() ) ) );
1387 1420 }
1388 1421
@@ -1389,9 +1422,9 @@
1389 1422 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1390 1423
1391 1424 // Make sure the fetch was successful.
1392 1425 if ( 200 !== $remote_response_code ) {
1393 - @unlink( $tmp_file_name );
1426 + wp_delete_file( $tmp_file_name );
1394 1427
1395 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 ) ) );
1396 1429 }
1397 1430
@@ -1398,9 +1431,9 @@
1398 1431 $headers = wp_remote_retrieve_headers( $remote_response );
1399 1432
1400 1433 // Request failed.
1401 1434 if ( ! $headers ) {
1402 - @unlink( $tmp_file_name );
1435 + wp_delete_file( $tmp_file_name );
1403 1436
1404 1437 return new WP_Error( 'import_file_error', esc_html__( 'Remote server did not respond', 'betterdocs' ) );
1405 1438 }
1406 1439
@@ -1406,15 +1439,15 @@
1406 1439
1407 1440 $filesize = (int) filesize( $tmp_file_name );
1408 1441
1409 1442 if ( 0 === $filesize ) {
1410 - @unlink( $tmp_file_name );
1443 + wp_delete_file( $tmp_file_name );
1411 1444
1412 1445 return new WP_Error( 'import_file_error', esc_html__( 'Zero size file downloaded', 'betterdocs' ) );
1413 1446 }
1414 1447
1415 1448 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1416 - @unlink( $tmp_file_name );
1449 + wp_delete_file( $tmp_file_name );
1417 1450
1418 1451 return new WP_Error( 'import_file_error', esc_html__( 'Downloaded file has incorrect size', 'betterdocs' ) );
1419 1452 }
1420 1453
@@ -1419,9 +1452,9 @@
1419 1452 }
1420 1453
1421 1454 $max_size = (int) apply_filters( 'import_attachment_size_limit', self::DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT );
1422 1455 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1423 - @unlink( $tmp_file_name );
1456 + wp_delete_file( $tmp_file_name );
1424 1457
1425 1458 /* translators: %s: Max file size. */
1426 1459
1427 1460 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'betterdocs' ), size_format( $max_size ) ) );
@@ -1469,16 +1502,17 @@
1469 1502 $new_file = $uploads['path'] . "/$file_name";
1470 1503 $move_new_file = copy( $tmp_file_name, $new_file );
1471 1504
1472 1505 if ( ! $move_new_file ) {
1473 - @unlink( $tmp_file_name );
1506 + wp_delete_file( $tmp_file_name );
1474 1507
1475 1508 return new WP_Error( 'import_file_error', esc_html__( 'The uploaded file could not be moved', 'betterdocs' ) );
1476 1509 }
1477 1510
1478 - // Set correct file permissions.
1511 + // Set correct file permissions to match parent directory.
1479 1512 $stat = stat( dirname( $new_file ) );
1480 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.
1481 1515 chmod( $new_file, $perms );
1482 1516
1483 1517 $upload = [
1484 1518 'file' => $new_file,
@@ -1587,14 +1621,29 @@
1587 1621 * @param string $file Path to WXR file for parsing
1588 1622 *
1589 1623 * @return array Information gathered from the WXR file
1590 1624 */
1591 - private function parse( $file ): array {
1592 - 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 {
1593 1643 $parser = new WXR_Parser();
1594 - } elseif ( $this->file_type == 'text/csv' ) {
1595 - $parser = new CSV_Parser();
1596 1644 }
1645 +
1597 1646 return $parser->parse( $file );
1598 1647 }
1599 1648
1600 1649 /**
@@ -1672,9 +1721,16 @@
1672 1721 * @param $file
1673 1722 * @param array $args
1674 1723 */
1675 1724 public function __construct( $file, array $args = [] ) {
1676 - 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 + }
1677 1733
1678 1734 $this->requested_file_path = $file;
1679 1735 $this->args = $args;
1680 1736