diff --git a/packages/playground/data-liberation/bin/import/blueprint-import-wxr.json b/packages/playground/data-liberation/bin/import/blueprint-import-wxr.json index 55ab107921..b8ad517fae 100644 --- a/packages/playground/data-liberation/bin/import/blueprint-import-wxr.json +++ b/packages/playground/data-liberation/bin/import/blueprint-import-wxr.json @@ -11,8 +11,8 @@ "pluginPath": "data-liberation/plugin.php" }, { - "step": "runPHP", - "code": "files as $file ) {\nif ( $file->isFile() && pathinfo( $file->getPathname(), PATHINFO_EXTENSION ) === 'xml' ) {\ndata_liberation_import( $file->getPathname() );\n}\n}\n};" + "step": "wp-cli", + "command": "wp data-liberation import /wordpress/wp-content/uploads/import-wxr" } ] } diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 56d10ac191..10d602d4e9 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -64,6 +64,8 @@ require_once __DIR__ . '/src/import/WP_Stream_Importer.php'; require_once __DIR__ . '/src/import/WP_Entity_Iterator_Chain.php'; require_once __DIR__ . '/src/import/WP_Retry_Frontloading_Iterator.php'; +require_once __DIR__ . '/src/import/WP_Logger.php'; +require_once __DIR__ . '/src/import/WP_Topological_Sorter.php'; require_once __DIR__ . '/src/utf8_decoder.php'; diff --git a/packages/playground/data-liberation/phpunit.xml b/packages/playground/data-liberation/phpunit.xml index 800b55f189..54fbc00a3c 100644 --- a/packages/playground/data-liberation/phpunit.xml +++ b/packages/playground/data-liberation/phpunit.xml @@ -11,6 +11,7 @@ tests/WPXMLProcessorTests.php tests/UrldecodeNTests.php tests/WPStreamImporterTests.php + tests/WPTopologicalSorterTests.php diff --git a/packages/playground/data-liberation/plugin.php b/packages/playground/data-liberation/plugin.php index f17704ebcc..077a89fb67 100644 --- a/packages/playground/data-liberation/plugin.php +++ b/packages/playground/data-liberation/plugin.php @@ -39,40 +39,59 @@ function () { } ); -add_action( - 'init', - function () { - if ( defined( 'WP_CLI' ) && WP_CLI ) { - /** - * Import a WXR file. - * - * - * : The WXR file to import. - */ - $command = function ( $args, $assoc_args ) { - $file = $args[0]; - data_liberation_import( $file ); - }; - - // Register the WP-CLI import command. - // Example usage: wp data-liberation /path/to/file.xml - WP_CLI::add_command( 'data-liberation', $command ); - } +function data_liberation_init() { + if ( defined( 'WP_CLI' ) && WP_CLI ) { + require_once __DIR__ . '/src/cli/WP_Import_Command.php'; - register_post_status( - 'error', - array( - 'label' => _x( 'Error', 'post' ), // Label name - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => false, - 'show_in_admin_status_list' => false, - // translators: %s is the number of errors - 'label_count' => _n_noop( 'Error (%s)', 'Error (%s)' ), - ) - ); + // Register the WP-CLI import command. + WP_CLI::add_command( 'data-liberation', WP_Import_Command::class ); } -); + + register_post_status( + 'error', + array( + 'label' => _x( 'Error', 'post' ), // Label name + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => false, + 'show_in_admin_status_list' => false, + // translators: %s is the number of errors + 'label_count' => _n_noop( 'Error (%s)', 'Error (%s)' ), + ) + ); +} + +add_action( 'init', 'data_liberation_init' ); + +function data_liberation_activate() { + // Activate the topological sorter. Create tables and options. + WP_Topological_Sorter::activate(); + update_option( WP_Topological_Sorter::OPTION_NAME, WP_Topological_Sorter::DB_VERSION ); +} + +// Run when the plugin is activated. +register_activation_hook( __FILE__, 'data_liberation_activate' ); + +function data_liberation_deactivate() { + // Deactivate the topological sorter. Flush away all data. + WP_Topological_Sorter::deactivate(); + + // @TODO: Cancel any active import sessions and cleanup other data. +} + +// Run when the plugin is deactivated. +register_deactivation_hook( __FILE__, 'data_liberation_deactivate' ); + +function data_liberation_load() { + if ( WP_Topological_Sorter::DB_VERSION !== (int) get_site_option( WP_Topological_Sorter::OPTION_NAME ) ) { + // Update the database with dbDelta, if needed in the future. + WP_Topological_Sorter::activate(); + update_option( WP_Topological_Sorter::OPTION_NAME, WP_Topological_Sorter::DB_VERSION ); + } +} + +// Run when the plugin is loaded. +add_action( 'plugins_loaded', 'data_liberation_load' ); // Register admin menu add_action( diff --git a/packages/playground/data-liberation/src/cli/WP_Import_Command.php b/packages/playground/data-liberation/src/cli/WP_Import_Command.php new file mode 100644 index 0000000000..586378f746 --- /dev/null +++ b/packages/playground/data-liberation/src/cli/WP_Import_Command.php @@ -0,0 +1,246 @@ + + * : The path to the WXR file. Either a file, a directory or a URL. + * + * [--count=] + * : The number of items to import in one go. Default is 10,000. + * + * [--dry-run] + * : Perform a dry run if set. + * + * ## EXAMPLES + * + * wp data-liberation import /path/to/file.xml + * + * @param array $args + * @param array $assoc_args + * @return void + */ + public function import( $args, $assoc_args ) { + $path = $args[0]; + $this->dry_run = WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false ); + $this->count = isset( $assoc_args['count'] ) ? (int) $assoc_args['count'] : 10000; + $options = array( + 'logger' => new WP_Import_logger(), + ); + + if ( extension_loaded( 'pcntl' ) ) { + // Set the signal handler. + $this->register_handlers(); + } + + // Be sure Data Liberation is activated. + data_liberation_activate(); + + if ( filter_var( $path, FILTER_VALIDATE_URL ) ) { + // Import URL. + $this->import_wxr_url( $path, $options ); + } elseif ( is_dir( $path ) ) { + $count = 0; + // Get all the WXR files in the directory. + foreach ( wp_visit_file_tree( $path ) as $event ) { + foreach ( $event->files as $file ) { + if ( $file->isFile() && 'xml' === pathinfo( $file->getPathname(), PATHINFO_EXTENSION ) ) { + ++$count; + + // Import the WXR file. + $this->import_wxr_file( $file->getPathname(), $options ); + } + } + } + + if ( ! $count ) { + WP_CLI::error( WP_CLI::colorize( "No WXR files found in the %R{$path}%n directory" ) ); + } + } else { + if ( ! is_file( $path ) ) { + WP_CLI::error( WP_CLI::colorize( "File not found: %R{$path}%n" ) ); + } + + // Import the WXR file. + $this->import_wxr_file( $path, $options ); + } + } + + private function start_session( $args ) { + if ( $this->dry_run ) { + WP_CLI::line( 'Dry run enabled. No session created.' ); + + return; + } + + $active_session = WP_Import_Session::get_active(); + + if ( $active_session ) { + $this->import_session = $active_session; + + $id = $this->import_session->get_id(); + WP_CLI::line( WP_CLI::colorize( "Current session: %g{$id}%n" ) ); + } else { + $this->import_session = WP_Import_Session::create( $args ); + + $id = $this->import_session->get_id(); + WP_CLI::line( WP_CLI::colorize( "New session: %g{$id}%n" ) ); + } + } + + /** + * Import a WXR file. + * + * @param string $file_path The path to the WXR file. + * @return void + */ + private function import_wxr_file( $file_path, $options = array() ) { + $this->wxr_path = $file_path; + + $this->start_session( + array( + 'data_source' => 'wxr_file', + 'file_name' => $file_path, + ) + ); + + // Pass the session ID. + $options['session_id'] = $this->import_session->get_id(); + + $this->importer = WP_Stream_Importer::create_for_wxr_file( $file_path, $options ); + $this->import_wxr(); + } + + /** + * Import a WXR file from a URL. + * + * @param string $url The URL to the WXR file. + * @return void + */ + private function import_wxr_url( $url, $options = array() ) { + $this->wxr_path = $url; + + $this->start_session( + array( + 'data_source' => 'wxr_url', + 'file_name' => $url, + ) + ); + + // Pass the session ID. + $options['session_id'] = $this->import_session->get_id(); + + $this->importer = WP_Stream_Importer::create_for_wxr_url( $url, $options ); + $this->import_wxr(); + } + + /** + * Import the WXR file. + */ + private function import_wxr() { + if ( ! $this->importer ) { + WP_CLI::error( 'Could not create importer' ); + } + + if ( ! $this->import_session ) { + WP_CLI::error( 'Could not create session' ); + } + + WP_CLI::line( "Importing {$this->wxr_path}" ); + + if ( $this->dry_run ) { + // @TODO: do something with the dry run. + WP_CLI::line( 'Dry run enabled.' ); + } else { + do { + $current_stage = $this->importer->get_stage(); + WP_CLI::line( WP_CLI::colorize( "Stage %g{$current_stage}%n" ) ); + $step_count = 0; + + while ( $this->importer->next_step() ) { + ++$step_count; + WP_CLI::line( WP_CLI::colorize( "Step %g{$step_count}%n" ) ); + } + } while ( $this->importer->advance_to_next_stage() ); + } + + WP_CLI::success( 'Import finished' ); + } + + /** + * Callback function registered to `pcntl_signal` to handle signals. + * + * @param int $signal The signal number. + * @return void + */ + protected function signal_handler( $signal ) { + switch ( $signal ) { + case SIGINT: + WP_CLI::line( 'Received SIGINT signal' ); + exit( 0 ); + + case SIGTERM: + WP_CLI::line( 'Received SIGTERM signal' ); + exit( 0 ); + } + } + + /** + * Register signal handlers for the command. + * + * @return void + */ + private function register_handlers() { + // Handle the Ctrl + C signal to terminate the program. + pcntl_signal( SIGINT, array( $this, 'signal_handler' ) ); + + // Handle the `kill` command to terminate the program. + pcntl_signal( SIGTERM, array( $this, 'signal_handler' ) ); + } +} diff --git a/packages/playground/data-liberation/src/cli/WP_Import_Logger.php b/packages/playground/data-liberation/src/cli/WP_Import_Logger.php new file mode 100644 index 0000000000..103ab3d9e2 --- /dev/null +++ b/packages/playground/data-liberation/src/cli/WP_Import_Logger.php @@ -0,0 +1,51 @@ +next_step() ) { - // Output the current stage if running in WP-CLI. - if ( $is_wp_cli ) { - $current_stage = $importer->get_current_stage(); - WP_CLI::line( "Import: stage {$current_stage}" ); - } - } - - if ( $is_wp_cli ) { - WP_CLI::success( 'Import ended' ); - } - - return true; -} - function get_all_post_meta_flat( $post_id ) { return array_map( function ( $value ) { diff --git a/packages/playground/data-liberation/src/import/WP_Entity_Importer.php b/packages/playground/data-liberation/src/import/WP_Entity_Importer.php index 95ff593f6f..c04fd1685d 100644 --- a/packages/playground/data-liberation/src/import/WP_Entity_Importer.php +++ b/packages/playground/data-liberation/src/import/WP_Entity_Importer.php @@ -69,6 +69,11 @@ class=[\'"].*?\b(wp-image-\d+|attachment-[\w\-]+)\b protected $url_remap = array(); protected $featured_images = array(); + /** + * @var WP_Topological_Sorter + */ + private $topological_sorter; + /** * Constructor * @@ -95,7 +100,7 @@ public function __construct( $options = array() ) { $this->mapping['term_id'] = array(); $this->requires_remapping = $empty_types; $this->exists = $empty_types; - $this->logger = new Logger(); + $this->logger = isset( $options['logger'] ) ? $options['logger'] : new WP_Logger(); $this->options = wp_parse_args( $options, @@ -108,6 +113,9 @@ public function __construct( $options = array() ) { 'default_author' => null, ) ); + + WP_Topological_Sorter::activate(); + $this->topological_sorter = new WP_Topological_Sorter( $this->options ); } public function import_entity( WP_Imported_Entity $entity ) { @@ -126,6 +134,8 @@ public function import_entity( WP_Imported_Entity $entity ) { case WP_Imported_Entity::TYPE_TAG: case WP_Imported_Entity::TYPE_CATEGORY: return $this->import_term( $data ); + case WP_Imported_Entity::TYPE_TERM_META: + return $this->import_term_meta( $data, $data['term_id'] ); case WP_Imported_Entity::TYPE_USER: return $this->import_user( $data ); case WP_Imported_Entity::TYPE_SITE_OPTION: @@ -257,6 +267,7 @@ public function import_user( $data ) { * @param array $userdata Raw data imported for the user. */ do_action( 'wxr_importer_processed_user', $user_id, $userdata ); + // $this->topological_sorter->map_entity( 'user', $userdata, $user_id ); } public function import_term( $data ) { @@ -267,13 +278,13 @@ public function import_term( $data ) { * @param array $meta Meta data. */ $data = apply_filters( 'wxr_importer_pre_process_term', $data ); + $data = $this->topological_sorter->get_mapped_entity( 'term', $data ); if ( empty( $data ) ) { return false; } $original_id = isset( $data['id'] ) ? (int) $data['id'] : 0; - $parent_id = isset( $data['parent'] ) ? (int) $data['parent'] : 0; - + $parent = isset( $data['parent'] ) ? $data['parent'] : null; $mapping_key = sha1( $data['taxonomy'] . ':' . $data['slug'] ); $existing = $this->term_exists( $data ); if ( $existing ) { @@ -297,15 +308,17 @@ public function import_term( $data ) { $termdata = array(); $allowed = array( - 'slug' => true, 'description' => true, + 'name' => true, + 'slug' => true, + 'parent' => true, ); - // Map the parent comment, or mark it as one we need to fix - // TODO: add parent mapping and remapping - /*$requires_remapping = false; - if ( $parent_id ) { - if ( isset( $this->mapping['term'][ $parent_id ] ) ) { + // Map the parent term, or mark it as one we need to fix + if ( $parent ) { + // TODO: add parent mapping and remapping + // $requires_remapping = false; + /*if ( isset( $this->mapping['term'][ $parent_id ] ) ) { $data['parent'] = $this->mapping['term'][ $parent_id ]; } else { // Prepare for remapping later @@ -314,9 +327,30 @@ public function import_term( $data ) { // Wipe the parent for now $data['parent'] = 0; + }*/ + $parent_term = term_exists( $parent, $data['taxonomy'] ); + + if ( $parent_term ) { + $data['parent'] = $parent_term['term_id']; + } else { + // It can happens that the parent term is not imported yet in manually created WXR files. + $parent_term = wp_insert_term( $parent, $data['taxonomy'] ); + + if ( is_wp_error( $parent_term ) ) { + $this->logger->error( + sprintf( + /* translators: %s: taxonomy name */ + __( 'Failed to import parent term for "%s"', 'wordpress-importer' ), + $data['taxonomy'] + ) + ); + } else { + $data['parent'] = $parent_term['term_id']; + } } - }*/ + } + // Filter the term data to only include allowed keys. foreach ( $data as $key => $value ) { if ( ! isset( $allowed[ $key ] ) ) { continue; @@ -325,7 +359,17 @@ public function import_term( $data ) { $termdata[ $key ] = $data[ $key ]; } - $result = wp_insert_term( $data['name'], $data['taxonomy'], $termdata ); + $term = term_exists( $data['slug'], $data['taxonomy'] ); + $result = null; + + if ( is_array( $term ) ) { + // Update the existing term. + $result = wp_update_term( $term['term_id'], $data['taxonomy'], $termdata ); + } else { + // Create a new term. + $result = wp_insert_term( $data['name'], $data['taxonomy'], $termdata ); + } + if ( is_wp_error( $result ) ) { $this->logger->warning( sprintf( @@ -380,8 +424,42 @@ public function import_term( $data ) { * @param array $data Raw data imported for the term. */ do_action( 'wxr_importer_processed_term', $term_id, $data ); + $this->topological_sorter->map_entity( 'term', $data, $term_id ); } + public function import_term_meta( $meta_item, $term_id ) { + if ( empty( $meta_item ) ) { + return true; + } + + /** + * Pre-process term meta data. + * + * @param array $meta_item Meta data. (Return empty to skip.) + * @param int $term_id Term the meta is attached to. + */ + $meta_item = apply_filters( 'wxr_importer_pre_process_term_meta', $meta_item, $term_id ); + $meta_item = $this->topological_sorter->get_mapped_entity( 'term_meta', $meta_item, $term_id ); + if ( empty( $meta_item ) ) { + return false; + } + + // Have we already processed this? + if ( isset( $element['_already_mapped'] ) ) { + $this->logger->debug( 'Skipping term meta, already processed' ); + return; + } + + if ( ! isset( $meta_item['term_id'] ) ) { + $meta_item['term_id'] = $term_id; + } + + $value = maybe_unserialize( $meta_item['meta_value'] ); + $term_meta_id = add_term_meta( $meta_item['term_id'], wp_slash( $meta_item['meta_key'] ), wp_slash_strings_only( $value ) ); + + do_action( 'wxr_importer_processed_term_meta', $term_meta_id, $meta_item, $meta_item['term_id'] ); + $this->topological_sorter->map_entity( 'term_meta', $meta_item, $meta_item['meta_key'] ); + } /** * Prefill existing post data. @@ -439,6 +517,8 @@ protected function post_exists( $data ) { * Note that new/updated terms, comments and meta are imported for the last of the above. */ public function import_post( $data ) { + $parent_id = isset( $data['post_parent'] ) ? (int) $data['post_parent'] : 0; + /** * Pre-process post data. * @@ -447,17 +527,17 @@ public function import_post( $data ) { * @param array $comments Comments on the post. * @param array $terms Terms on the post. */ - $data = apply_filters( 'wxr_importer_pre_process_post', $data ); + $data = apply_filters( 'wxr_importer_pre_process_post', $data, $parent_id ); + $data = $this->topological_sorter->get_mapped_entity( 'post', $data, $parent_id ); if ( empty( $data ) ) { $this->logger->debug( 'Skipping post, empty data' ); return false; } $original_id = isset( $data['post_id'] ) ? (int) $data['post_id'] : 0; - $parent_id = isset( $data['post_parent'] ) ? (int) $data['post_parent'] : 0; // Have we already processed this? - if ( isset( $this->mapping['post'][ $original_id ] ) ) { + if ( isset( $element['_already_mapped'] ) ) { $this->logger->debug( 'Skipping post, already processed' ); return; } @@ -617,6 +697,37 @@ public function import_post( $data ) { } $this->mark_post_exists( $data, $post_id ); + // Add terms to the post + if ( ! empty( $data['terms'] ) ) { + $terms_to_set = array(); + + foreach ( $data['terms'] as $term ) { + // Back compat with WXR 1.0 map 'tag' to 'post_tag' + $taxonomy = ( 'tag' === $term['taxonomy'] ) ? 'post_tag' : $term['taxonomy']; + $term_exists = term_exists( $term['slug'], $taxonomy ); + $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists; + + if ( ! $term_id ) { + // @TODO: Add a unit test with a WXR with one post and X tags without root declated tags. + $new_term = wp_insert_term( $term['slug'], $taxonomy, $term ); + + if ( ! is_wp_error( $new_term ) ) { + $term_id = $new_term['term_id']; + + $this->topological_sorter->map_entity( 'term', $new_term, $term_id ); + } else { + continue; + } + } + $terms_to_set[ $taxonomy ][] = intval( $term_id ); + } + + foreach ( $terms_to_set as $tax => $ids ) { + // Add the post terms to the post + wp_set_post_terms( $post_id, $ids, $tax ); + } + } + $this->logger->info( sprintf( /* translators: 1: post title, 2: post type name */ @@ -644,6 +755,8 @@ public function import_post( $data ) { * @param array $terms Raw term data, already processed. */ do_action( 'wxr_importer_processed_post', $post_id, $data ); + $this->topological_sorter->map_entity( 'post', $data, $post_id ); + return $post_id; } @@ -865,7 +978,7 @@ public function import_attachment( $filepath, $post_id ) { * @return int|WP_Error Number of meta items imported on success, error otherwise. */ public function import_post_meta( $meta_item, $post_id ) { - if ( empty( $meta ) ) { + if ( empty( $meta_item ) ) { return true; } @@ -876,16 +989,17 @@ public function import_post_meta( $meta_item, $post_id ) { * @param int $post_id Post the meta is attached to. */ $meta_item = apply_filters( 'wxr_importer_pre_process_post_meta', $meta_item, $post_id ); + $meta_item = $this->topological_sorter->get_mapped_entity( 'post_meta', $meta_item, $post_id ); if ( empty( $meta_item ) ) { return false; } - $key = apply_filters( 'import_post_meta_key', $meta_item['key'], $post_id, $post ); + $key = apply_filters( 'import_post_meta_key', $meta_item['meta_key'], $post_id ); $value = false; if ( '_edit_last' === $key ) { - $value = intval( $meta_item['value'] ); - if ( ! isset( $this->mapping['user'][ $value ] ) ) { + $value = intval( $value ); + if ( ! isset( $this->mapping['user'][ $meta_item['meta_value'] ] ) ) { // Skip! _doing_it_wrong( __METHOD__, 'User ID not found in mapping', '4.7' ); return false; @@ -897,10 +1011,10 @@ public function import_post_meta( $meta_item, $post_id ) { if ( $key ) { // export gets meta straight from the DB so could have a serialized string if ( ! $value ) { - $value = maybe_unserialize( $meta_item['value'] ); + $value = maybe_unserialize( $meta_item['meta_value'] ); } - add_post_meta( $post_id, $key, $value ); + add_post_meta( $post_id, wp_slash( $key ), wp_slash_strings_only( $value ) ); do_action( 'import_post_meta', $post_id, $key, $value ); // if the post has a featured image, take note of this in case of remap @@ -909,6 +1023,9 @@ public function import_post_meta( $meta_item, $post_id ) { } } + do_action( 'wxr_importer_processed_post_meta', $post_id, $meta_item ); + $this->topological_sorter->map_entity( 'post_meta', $meta_item, $key ); + return true; } @@ -931,6 +1048,7 @@ public function import_comment( $comment, $post_id, $post_just_imported = false // Sort by ID to avoid excessive remapping later usort( $comments, array( $this, 'sort_comments_by_id' ) ); + $parent_id = isset( $comment['comment_parent'] ) ? (int) $comment['comment_parent'] : null; /** * Pre-process comment data @@ -938,13 +1056,13 @@ public function import_comment( $comment, $post_id, $post_just_imported = false * @param array $comment Comment data. (Return empty to skip.) * @param int $post_id Post the comment is attached to. */ - $comment = apply_filters( 'wxr_importer_pre_process_comment', $comment, $post_id ); + $comment = apply_filters( 'wxr_importer_pre_process_comment', $comment, $post_id, $parent_id ); + $comment = $this->topological_sorter->get_mapped_entity( 'comment', $comment, $post_id, $parent_id ); if ( empty( $comment ) ) { return false; } $original_id = isset( $comment['comment_id'] ) ? (int) $comment['comment_id'] : 0; - $parent_id = isset( $comment['comment_parent'] ) ? (int) $comment['comment_parent'] : 0; $author_id = isset( $comment['comment_user_id'] ) ? (int) $comment['comment_user_id'] : 0; // if this is a new post we can skip the comment_exists() check @@ -1001,7 +1119,10 @@ public function import_comment( $comment, $post_id, $post_just_imported = false } // Run standard core filters - $comment['comment_post_ID'] = $post_id; + if ( ! $comment['comment_post_ID'] ) { + $comment['comment_post_ID'] = $post_id; + } + // @TODO: How to handle missing fields? Use sensible defaults? What defaults? if ( ! isset( $comment['comment_author_IP'] ) ) { $comment['comment_author_IP'] = ''; @@ -1038,17 +1159,31 @@ public function import_comment( $comment, $post_id, $post_just_imported = false /** * Post processing completed. * - * @param int $post_id New post ID. + * @param int $comment_id New comment ID. * @param array $comment Raw data imported for the comment. - * @param array $meta Raw meta data, already processed by {@see process_post_meta}. * @param array $post_id Parent post ID. */ do_action( 'wxr_importer_processed_comment', $comment_id, $comment, $post_id ); + $this->topological_sorter->map_entity( 'comment', $comment, $comment_id, $post_id ); } public function import_comment_meta( $meta_item, $comment_id ) { - $value = maybe_unserialize( $meta_item['value'] ); - add_comment_meta( $comment_id, wp_slash( $meta_item['key'] ), wp_slash( $value ) ); + $meta_item = apply_filters( 'wxr_importer_pre_process_comment_meta', $meta_item, $comment_id ); + $meta_item = $this->topological_sorter->get_mapped_entity( 'comment_meta', $meta_item, $comment_id ); + if ( empty( $meta_item ) ) { + return false; + } + + if ( ! isset( $meta_item['comment_id'] ) ) { + $meta_item['comment_id'] = $comment_id; + } + + // @TODO: Check if wp_slash is correct and not wp_slash_strings_only + $value = maybe_unserialize( $meta_item['meta_value'] ); + $comment_meta_id = add_comment_meta( $meta_item['comment_id'], wp_slash( $meta_item['meta_key'] ), wp_slash( $value ) ); + + do_action( 'wxr_importer_processed_comment_meta', $comment_meta_id, $meta_item, $meta_item['comment_id'] ); + $this->topological_sorter->map_entity( 'comment_meta', $meta_item, $comment_meta_id, $meta_item['comment_id'] ); } /** @@ -1193,57 +1328,3 @@ public static function sort_comments_by_id( $a, $b ) { return $a['comment_id'] - $b['comment_id']; } } - -/** - * @TODO how to treat this? Should this class even exist? - * how does WordPress handle different levels? It - * seems useful for usage in wp-cli, Blueprints, - * and other non-web environments. - */ -// phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound -class Logger { - /** - * Log a debug message. - * - * @param string $message Message to log - */ - public function debug( $message ) { - // echo( '[DEBUG] ' . $message ); - } - - /** - * Log an info message. - * - * @param string $message Message to log - */ - public function info( $message ) { - // echo( '[INFO] ' . $message ); - } - - /** - * Log a warning message. - * - * @param string $message Message to log - */ - public function warning( $message ) { - echo( '[WARNING] ' . $message ); - } - - /** - * Log an error message. - * - * @param string $message Message to log - */ - public function error( $message ) { - echo( '[ERROR] ' . $message ); - } - - /** - * Log a notice message. - * - * @param string $message Message to log - */ - public function notice( $message ) { - // echo( '[NOTICE] ' . $message ); - } -} diff --git a/packages/playground/data-liberation/src/import/WP_Import_Session.php b/packages/playground/data-liberation/src/import/WP_Import_Session.php index a731f3f9fc..6dc21b0df1 100644 --- a/packages/playground/data-liberation/src/import/WP_Import_Session.php +++ b/packages/playground/data-liberation/src/import/WP_Import_Session.php @@ -19,6 +19,7 @@ class WP_Import_Session { 'category', 'tag', 'term', + 'term_meta', 'post', 'post_meta', 'comment', @@ -310,8 +311,8 @@ public function count_unfinished_frontloading_placeholders() { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( - "SELECT COUNT(*) FROM $wpdb->posts - WHERE post_type = 'frontloading_placeholder' + "SELECT COUNT(*) FROM $wpdb->posts + WHERE post_type = 'frontloading_placeholder' AND post_parent = %d AND post_status != %s AND post_status != %s", @@ -373,8 +374,8 @@ public function get_total_number_of_assets() { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( - "SELECT COUNT(*) FROM $wpdb->posts - WHERE post_type = 'frontloading_placeholder' + "SELECT COUNT(*) FROM $wpdb->posts + WHERE post_type = 'frontloading_placeholder' AND post_parent = %d", $this->post_id ) @@ -417,8 +418,8 @@ public function create_frontloading_placeholders( $urls ) { */ $exists = $wpdb->get_var( $wpdb->prepare( - "SELECT ID FROM $wpdb->posts - WHERE post_type = 'frontloading_placeholder' + "SELECT ID FROM $wpdb->posts + WHERE post_type = 'frontloading_placeholder' AND post_parent = %d AND guid = %s LIMIT 1", diff --git a/packages/playground/data-liberation/src/import/WP_Imported_Entity.php b/packages/playground/data-liberation/src/import/WP_Imported_Entity.php index 96c3dd3dd2..8e0dcb230e 100644 --- a/packages/playground/data-liberation/src/import/WP_Imported_Entity.php +++ b/packages/playground/data-liberation/src/import/WP_Imported_Entity.php @@ -7,6 +7,7 @@ class WP_Imported_Entity { const TYPE_COMMENT = 'comment'; const TYPE_COMMENT_META = 'comment_meta'; const TYPE_TERM = 'term'; + const TYPE_TERM_META = 'term_meta'; const TYPE_TAG = 'tag'; const TYPE_CATEGORY = 'category'; const TYPE_USER = 'user'; diff --git a/packages/playground/data-liberation/src/import/WP_Logger.php b/packages/playground/data-liberation/src/import/WP_Logger.php new file mode 100644 index 0000000000..87605336fe --- /dev/null +++ b/packages/playground/data-liberation/src/import/WP_Logger.php @@ -0,0 +1,51 @@ +stage ) { case self::STAGE_INITIAL: @@ -288,10 +298,14 @@ public function next_step() { if ( true === $this->index_next_entities() ) { return true; } + $this->next_stage = self::STAGE_TOPOLOGICAL_SORT; return false; case self::STAGE_TOPOLOGICAL_SORT: - // @TODO: Topologically sort the entities. + if ( true === $this->topological_sort_next_entity() ) { + return true; + } + $this->next_stage = self::STAGE_FRONTLOAD_ASSETS; return false; case self::STAGE_FRONTLOAD_ASSETS: @@ -307,6 +321,8 @@ public function next_step() { $this->next_stage = self::STAGE_FINISHED; return false; case self::STAGE_FINISHED: + // Flush away the topological sorter session. + $this->topological_sorter->delete_session(); return false; } } @@ -340,6 +356,10 @@ private function index_next_entities( $count = 10000 ) { $this->entity_iterator = $this->create_entity_iterator(); } + if ( null === $this->topological_sorter ) { + $this->topological_sorter = new WP_Topological_Sorter( $this->options ); + } + // Mark all mapping candidates as seen. foreach ( $this->site_url_mapping_candidates as $base_url => $status ) { $this->site_url_mapping_candidates[ $base_url ] = true; @@ -503,6 +523,51 @@ private function frontloading_advance_reentrancy_cursor() { } } + /** + * Sort the entities topologically. + * + * @param int $count The number of entities to process in one go. + */ + private function topological_sort_next_entity( $count = 10000 ) { + if ( null !== $this->next_stage ) { + return false; + } + + if ( null === $this->entity_iterator ) { + $this->entity_iterator = $this->create_entity_iterator(); + } + + if ( null === $this->topological_sorter ) { + $this->topological_sorter = new WP_Topological_Sorter( $this->options ); + } + + if ( ! $this->entity_iterator->valid() ) { + $this->entity_iterator = null; + $this->resume_at_entity = null; + return false; + } + + /** + * Internalize the loop to avoid computing the reentrancy cursor + * on every entity in the imported data stream. + */ + for ( $i = 0; $i < $count; ++$i ) { + if ( ! $this->entity_iterator->valid() ) { + break; + } + + $entity = $this->entity_iterator->current(); + $data = $entity->get_data(); + // $offset = $this->entity_iterator->get_last_xml_byte_offset_outside_of_entity(); + $this->topological_sorter->map_entity( $entity->get_type(), $data ); + $this->entity_iterator->next(); + } + + $this->resume_at_entity = $this->entity_iterator->get_reentrancy_cursor(); + + return true; + } + /** * Downloads all the assets referenced in the imported entities. * @@ -522,6 +587,10 @@ private function frontload_next_entity() { $this->downloader = new WP_Attachment_Downloader( $this->options['uploads_path'] ); } + if ( null === $this->topological_sorter ) { + $this->topological_sorter = new WP_Topological_Sorter( $this->options ); + } + // Clear the frontloading events from the previous pass. $this->frontloading_events = array(); $this->frontloading_advance_reentrancy_cursor(); @@ -627,6 +696,10 @@ private function import_next_entity() { $this->importer = new WP_Entity_Importer(); } + if ( null === $this->topological_sorter ) { + $this->topological_sorter = new WP_Topological_Sorter( $this->options ); + } + if ( ! $this->entity_iterator->valid() ) { // We're done. $this->stage = self::STAGE_FINISHED; diff --git a/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php b/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php new file mode 100644 index 0000000000..273ede6b09 --- /dev/null +++ b/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php @@ -0,0 +1,406 @@ + 1, + 'comment_meta' => 2, + 'post' => 3, + 'post_meta' => 4, + 'term' => 5, + 'term_meta' => 6, + ); + + /** + * The name of the field where the ID is saved. + */ + const ENTITY_TYPES_ID = array( + 'comment' => 'comment_id', + 'comment_meta' => 'meta_key', + 'post' => 'post_id', + 'post_meta' => 'meta_key', + 'term' => 'term_id', + 'term_meta' => 'meta_key', + ); + + /** + * Set the current session ID. + */ + public function __construct( $options = array() ) { + if ( array_key_exists( 'session_id', $options ) ) { + $this->set_session( $options['session_id'] ); + } else { + $active_session = WP_Import_Session::get_active(); + + if ( $active_session ) { + $this->set_session( $active_session->get_id() ); + } + } + } + + /** + * Get the name of the table. + * + * @return string The name of the table. + */ + public static function get_table_name() { + global $wpdb; + + // Default is wp_{TABLE_NAME} + return $wpdb->prefix . self::TABLE_NAME; + } + + /** + * Run by register_activation_hook. It creates the table if it doesn't exist. + */ + public static function activate() { + global $wpdb; + + // See wp_get_db_schema. + $max_index_length = 191; + + /** + * This is a table used to map the IDs of the imported entities. It is + * used to map all the IDs of the entities. + * + * @param int $id The ID of the entity. + * @param int $session_id The current session ID. + * @param int $entity_type The type of the entity, comment, etc. + * @param string $entity_id The ID of the entity before the import. + * @param string $mapped_id The mapped ID of the entity after the import. + * @param string $parent_id The parent ID of the entity. + * @param string $additional_id The additional ID of the entity. Used for comments and terms. Comments have a comment_parent, and the post. + * @param int $byte_offset The byte offset of the entity inside the WXR file. Not used now. + * @param int $sort_order The sort order of the entity. Not used now. + */ + $sql = $wpdb->prepare( + 'CREATE TABLE IF NOT EXISTS %i ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + session_id bigint(20) unsigned, + entity_type tinyint(1) NOT NULL, + entity_id text NOT NULL, + mapped_id text DEFAULT NULL, + parent_id text DEFAULT NULL, + additional_id text DEFAULT NULL, + byte_offset bigint(20) unsigned NOT NULL, + sort_order int DEFAULT 1, + PRIMARY KEY (id), + KEY session_id (session_id), + KEY entity_id (entity_id(%d)), + KEY parent_id (parent_id(%d)), + KEY byte_offset (byte_offset) + ) ' . $wpdb->get_charset_collate(), + self::get_table_name(), + $max_index_length, + $max_index_length + ); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( $sql ); + } + + /** + * Run by register_deactivation_hook. It drops the table and deletes the + * option. + */ + public static function deactivate() { + global $wpdb; + $table_name = self::get_table_name(); + + // Drop the table. + $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %s', $table_name ) ); + + // Delete the option. + delete_option( self::OPTION_NAME ); + } + + /** + * Reset the class. + */ + public function reset() { + $this->set_session( null ); + } + + /** + * Set the current session ID. + * + * @param int|null $session_id The session ID. + */ + public function set_session( $session_id ) { + $this->current_session = $session_id; + } + + /** + * Delete all rows for a given session ID. + * + * @param int $session_id The session ID to delete rows for. + * @return int|false The number of rows deleted, or false on error. + */ + public function delete_session( $session_id = null ) { + global $wpdb; + + return $wpdb->delete( + self::get_table_name(), + array( 'session_id' => $session_id ?? $this->current_session ), + array( '%d' ) + ); + } + + /** + * Map an entity to the index. If $id is provided, it will be used to map the entity. + * + * @param string $entity_type The type of the entity. + * @param array $data The data to map. + * @param int|null $id The ID of the entity. + * @param int|null $additional_id The additional ID of the entity. + */ + public function map_entity( $entity_type, $data, $id = null, $additional_id = null ) { + global $wpdb; + + if ( ! array_key_exists( $entity_type, self::ENTITY_TYPES ) ) { + return; + } + + $new_entity = array( + 'session_id' => $this->current_session, + 'entity_type' => self::ENTITY_TYPES[ $entity_type ], + 'entity_id' => null, + 'mapped_id' => is_null( $id ) ? null : (string) $id, + 'parent_id' => null, + 'byte_offset' => 0, + // Items with a parent has at least a sort order of 2. + 'sort_order' => 1, + ); + // Get the ID of the entity. + $entity_id = (string) $data[ self::ENTITY_TYPES_ID[ $entity_type ] ]; + + // Map the parent ID if the entity has one. + switch ( $entity_type ) { + // @TODO: missing comment parent ID. + case 'comment_meta': + if ( array_key_exists( 'comment_id', $data ) ) { + $new_entity['parent_id'] = $data['comment_id']; + } + break; + case 'post': + if ( 'post' === $data['post_type'] || 'page' === $data['post_type'] ) { + if ( array_key_exists( 'post_parent', $data ) && '0' !== $data['post_parent'] ) { + $new_entity['parent_id'] = $data['post_parent']; + } + } + break; + case 'post_meta': + if ( array_key_exists( 'post_id', $data ) ) { + $new_entity['parent_id'] = $data['post_id']; + } + break; + case 'term': + if ( array_key_exists( 'parent', $data ) ) { + $new_entity['parent_id'] = $data['parent']; + } + break; + case 'term_meta': + if ( array_key_exists( 'term_id', $data ) ) { + $new_entity['parent_id'] = $data['term_id']; + } + break; + } + + // The entity has been imported, so we can use the ID. + if ( $id ) { + $existing_entity = $this->get_mapped_ids( $entity_id, self::ENTITY_TYPES[ $entity_type ] ); + + if ( $existing_entity && is_null( $existing_entity['mapped_id'] ) ) { + $new_entity['mapped_id'] = (string) $id; + + // Update the entity if it already exists. + $wpdb->update( + self::get_table_name(), + array( 'mapped_id' => (string) $id ), + array( + 'entity_id' => (string) $entity_id, + 'entity_type' => self::ENTITY_TYPES[ $entity_type ], + 'session_id' => $this->current_session, + ), + array( '%s' ) + ); + } + } else { + // Insert the entity if it doesn't exist. + $new_entity['entity_id'] = $entity_id; + $wpdb->insert( self::get_table_name(), $new_entity ); + } + } + + /** + * Get a mapped entity. + * + * @param int $entity The entity to get the mapped ID for. + * @param int $id The ID of the entity. + * + * @return mixed|bool The mapped entity or false if the post is not found. + */ + public function get_mapped_entity( $entity_type, $entity, $id = null, $additional_id = null ) { + $already_mapped = false; + $mapped_entity = null; + + if ( ! array_key_exists( $entity_type, self::ENTITY_TYPES ) ) { + return $entity; + } + + // Get the mapped IDs of the entity. + $id_field = self::ENTITY_TYPES_ID[ $entity_type ]; + $mapped_entity = $this->get_mapped_ids( $entity[ $id_field ], self::ENTITY_TYPES[ $entity_type ] ); + + if ( $mapped_entity ) { + // Get entity parents. + switch ( $entity_type ) { + case 'comment': + // The ID is the post ID. + $mapped_ids = $this->get_mapped_ids( $id, self::ENTITY_TYPES['post'] ); + + if ( $mapped_ids && ! is_null( $mapped_ids['mapped_id'] ) ) { + // Save the mapped ID of comment parent post. + $entity['comment_post_ID'] = $mapped_ids['mapped_id']; + } + break; + case 'comment_meta': + // The ID is the comment ID. + $mapped_ids = $this->get_mapped_ids( $id, self::ENTITY_TYPES['comment'] ); + + if ( $mapped_ids && ! is_null( $mapped_ids['mapped_id'] ) ) { + // Save the mapped ID of comment meta parent comment. + $entity['comment_id'] = $mapped_ids['mapped_id']; + } + break; + case 'post': + // The ID is the parent post ID. + $mapped_ids = $this->get_mapped_ids( $id, self::ENTITY_TYPES['post'] ); + + if ( $mapped_ids && ! is_null( $mapped_ids['mapped_id'] ) ) { + // Save the mapped ID of post parent. + $entity['post_parent'] = $mapped_ids['mapped_id']; + } + break; + case 'post_meta': + // The ID is the post ID. + $mapped_ids = $this->get_mapped_ids( $id, self::ENTITY_TYPES['post'] ); + + if ( $mapped_ids ) { + // Save the mapped ID of post meta parent post. + $entity['post_id'] = $mapped_ids['mapped_id']; + } + break; + case 'term_meta': + // The ID is the term ID. + $mapped_ids = $this->get_mapped_ids( $id, self::ENTITY_TYPES['term'] ); + + if ( $mapped_ids && ! is_null( $mapped_ids['mapped_id'] ) ) { + // Save the mapped ID of term meta parent term. + $entity['term_id'] = $mapped_ids['mapped_id']; + } + } + } + + if ( $mapped_entity ) { + if ( ! is_null( $mapped_entity['mapped_id'] ) ) { + // This is used to skip an entity if it has already been mapped. + $entity[ $id_field ] = $mapped_entity['mapped_id']; + $entity['_already_mapped'] = true; + } else { + $entity['_already_mapped'] = false; + } + } + + return $entity; + } + + /** + * Get the mapped ID for an entity. + * + * @param int $id The ID of the entity. + * @param int $type The type of the entity. + * + * @return int|false The mapped ID or null if the entity is not found. + */ + private function get_mapped_ids( $id, $type ) { + global $wpdb; + + if ( ! $id ) { + return null; + } + + if ( is_null( $this->current_session ) ) { + $results = $wpdb->get_results( + $wpdb->prepare( + 'SELECT entity_id, mapped_id FROM %i WHERE entity_id = %s AND entity_type = %d AND session_id IS NULL LIMIT 1', + self::get_table_name(), + (string) $id, + $type + ), + ARRAY_A + ); + } else { + $results = $wpdb->get_results( + $wpdb->prepare( + 'SELECT entity_id, mapped_id FROM %i WHERE entity_id = %s AND entity_type = %d AND session_id = %d LIMIT 1', + self::get_table_name(), + (string) $id, + $type, + $this->current_session + ), + ARRAY_A + ); + } + + if ( $results && 1 === count( $results ) ) { + return $results[0]; + } + + return null; + } +} diff --git a/packages/playground/data-liberation/src/wxr/WP_WXR_Reader.php b/packages/playground/data-liberation/src/wxr/WP_WXR_Reader.php index 25c21ff608..4337638f47 100644 --- a/packages/playground/data-liberation/src/wxr/WP_WXR_Reader.php +++ b/packages/playground/data-liberation/src/wxr/WP_WXR_Reader.php @@ -213,6 +213,14 @@ class WP_WXR_Reader implements Iterator { */ private $last_comment_id = null; + /** + * The ID of the last processed term. + * + * @since WP_VERSION + * @var int|null + */ + private $last_term_id = null; + /** * Buffer for accumulating text content between tags. * @@ -328,6 +336,13 @@ class WP_WXR_Reader implements Iterator { 'wp:term_name' => 'name', ), ), + 'wp:termmeta' => array( + 'type' => 'term_meta', + 'fields' => array( + 'wp:meta_key' => 'meta_key', + 'wp:meta_value' => 'meta_value', + ), + ), 'wp:tag' => array( 'type' => 'tag', 'fields' => array( @@ -340,6 +355,7 @@ class WP_WXR_Reader implements Iterator { 'wp:category' => array( 'type' => 'category', 'fields' => array( + 'wp:term_id' => 'term_id', 'wp:category_nicename' => 'slug', 'wp:category_parent' => 'parent', 'wp:cat_name' => 'name', @@ -368,6 +384,7 @@ public static function create( WP_Byte_Reader $upstream = null, $cursor = null ) if ( null !== $cursor ) { $reader->last_post_id = $cursor['last_post_id']; $reader->last_comment_id = $cursor['last_comment_id']; + $reader->last_term_id = $cursor['last_term_id']; } if ( null !== $upstream ) { $reader->connect_upstream( $upstream ); @@ -396,6 +413,10 @@ protected function __construct( WP_XML_Processor $xml ) { $this->xml = $xml; } + public function get_last_xml_byte_offset_outside_of_entity() { + return $this->last_xml_byte_offset_outside_of_entity; + } + public function get_reentrancy_cursor() { /** * @TODO: Instead of adjusting the XML cursor internals, adjust the get_reentrancy_cursor() @@ -413,6 +434,7 @@ public function get_reentrancy_cursor() { 'upstream' => $this->last_xml_byte_offset_outside_of_entity, 'last_post_id' => $this->last_post_id, 'last_comment_id' => $this->last_comment_id, + 'last_term_id' => $this->last_term_id, ) ); } @@ -473,6 +495,17 @@ public function get_last_comment_id() { return $this->last_comment_id; } + /** + * Gets the ID of the last processed term. + * + * @since WP_VERSION + * + * @return int|null The term ID, or null if no terms have been processed. + */ + public function get_last_term_id() { + return $this->last_term_id; + } + /** * Appends bytes to the input stream. * @@ -867,8 +900,12 @@ private function emit_entity() { $this->entity_data['comment_id'] = $this->last_comment_id; } elseif ( $this->entity_type === 'tag' ) { $this->entity_data['taxonomy'] = 'post_tag'; + $this->last_term_id = $this->entity_data['term_id']; } elseif ( $this->entity_type === 'category' ) { $this->entity_data['taxonomy'] = 'category'; + $this->last_term_id = $this->entity_data['term_id']; + } elseif ( $this->entity_type === 'term_meta' ) { + $this->entity_data['term_id'] = $this->last_term_id; } $this->entity_finished = true; ++$this->entities_read_so_far; diff --git a/packages/playground/data-liberation/tests/PlaygroundTestCase.php b/packages/playground/data-liberation/tests/PlaygroundTestCase.php new file mode 100644 index 0000000000..9bc3ee4d39 --- /dev/null +++ b/packages/playground/data-liberation/tests/PlaygroundTestCase.php @@ -0,0 +1,51 @@ +markTestSkipped( 'Test only runs in Playground' ); + } + } + + /** + * Deletes all data from the database. Copy of _delete_all_data() from WordPress core. + * + * @see https://github.com/WordPress/wordpress-develop/blob/trunk/tests/phpunit/includes/functions.php + */ + protected function delete_all_data() { + global $wpdb; + + foreach ( array( + $wpdb->posts, + $wpdb->postmeta, + $wpdb->comments, + $wpdb->commentmeta, + $wpdb->term_relationships, + $wpdb->termmeta, + ) as $table ) { + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( "DELETE FROM {$table}" ); + } + + foreach ( array( + $wpdb->terms, + $wpdb->term_taxonomy, + ) as $table ) { + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( "DELETE FROM {$table} WHERE term_id != 1" ); + } + + $wpdb->query( "UPDATE {$wpdb->term_taxonomy} SET count = 0" ); + + $wpdb->query( "DELETE FROM {$wpdb->users} WHERE ID != 1" ); + $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE user_id != 1" ); + } +} diff --git a/packages/playground/data-liberation/tests/WPStreamImporterTests.php b/packages/playground/data-liberation/tests/WPStreamImporterTests.php index 28079e416c..70200eafd9 100644 --- a/packages/playground/data-liberation/tests/WPStreamImporterTests.php +++ b/packages/playground/data-liberation/tests/WPStreamImporterTests.php @@ -1,50 +1,37 @@ markTestSkipped( 'Test only runs in Playground' ); - } - } - - /** - * @before + /** + * @before * * TODO: Run each test in a fresh Playground instance instead of sharing the global * state like this. - */ - public function clean_up_uploads(): void - { - $files = glob( '/wordpress/wp-content/uploads/*' ); - foreach( $files as $file ) { - if( is_dir( $file ) ) { - array_map( 'unlink', glob( "$file/*.*" ) ); - rmdir( $file ); - } else { - unlink( $file ); - } - } - } - - public function test_import_simple_wxr() { - $import = data_liberation_import( __DIR__ . '/wxr/small-export.xml' ); - - $this->assertTrue( $import ); + */ + public function clean_up_uploads(): void { + $files = glob( '/wordpress/wp-content/uploads/*' ); + foreach ( $files as $file ) { + if ( is_dir( $file ) ) { + array_map( 'unlink', glob( "$file/*.*" ) ); + rmdir( $file ); + } else { + unlink( $file ); + } + } } public function test_frontloading() { $wxr_path = __DIR__ . '/wxr/frontloading-1-attachment.xml'; $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path ); $this->skip_to_stage( $importer, WP_Stream_Importer::STAGE_FRONTLOAD_ASSETS ); - while( $importer->next_step() ) { + while ( $importer->next_step() ) { // noop } $files = glob( '/wordpress/wp-content/uploads/*' ); @@ -57,17 +44,17 @@ public function test_resume_frontloading() { $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path ); $this->skip_to_stage( $importer, WP_Stream_Importer::STAGE_FRONTLOAD_ASSETS ); - $progress_url = null; + $progress_url = null; $progress_value = null; - for($i = 0; $i < 20; ++$i) { + for ( $i = 0; $i < 20; ++$i ) { $importer->next_step(); $progress = $importer->get_frontloading_progress(); - if( count( $progress ) === 0 ) { + if ( count( $progress ) === 0 ) { continue; } - $progress_url = array_keys( $progress )[0]; + $progress_url = array_keys( $progress )[0]; $progress_value = array_values( $progress )[0]; - if( null === $progress_value['received'] ) { + if ( null === $progress_value['received'] ) { continue; } break; @@ -78,22 +65,22 @@ public function test_resume_frontloading() { $this->assertEquals( 'https://wpthemetestdata.files.wordpress.com/2008/06/canola2.jpg', $progress_url ); $this->assertGreaterThan( 0, $progress_value['total'] ); - $cursor = $importer->get_reentrancy_cursor(); - $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path, [], $cursor ); + $cursor = $importer->get_reentrancy_cursor(); + $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path, array(), $cursor ); // Rewind back to the entity we were on. $this->assertTrue( $importer->next_step() ); - // Restart the download of the same entity – from scratch. - $progress_value = []; - for($i = 0; $i < 20; ++$i) { + // Restart the download of the same entity - from scratch. + $progress_value = array(); + for ( $i = 0; $i < 20; ++$i ) { $importer->next_step(); $progress = $importer->get_frontloading_progress(); - if( count( $progress ) === 0 ) { + if ( count( $progress ) === 0 ) { continue; } - $progress_url = array_keys( $progress )[0]; + $progress_url = array_keys( $progress )[0]; $progress_value = array_values( $progress )[0]; - if( null === $progress_value['received'] ) { + if ( null === $progress_value['received'] ) { continue; } break; @@ -105,17 +92,17 @@ public function test_resume_frontloading() { } /** - * + * Test resume entity import. */ public function test_resume_entity_import() { $wxr_path = __DIR__ . '/wxr/entities-options-and-posts.xml'; $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path ); $this->skip_to_stage( $importer, WP_Stream_Importer::STAGE_IMPORT_ENTITIES ); - for($i = 0; $i < 11; ++$i) { + for ( $i = 0; $i < 11; ++$i ) { $this->assertTrue( $importer->next_step() ); - $cursor = $importer->get_reentrancy_cursor(); - $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path, [], $cursor ); + $cursor = $importer->get_reentrancy_cursor(); + $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path, array(), $cursor ); // Rewind back to the entity we were on. // Note this means we may attempt to insert it twice. It's // the importer's job to detect that and skip the duplicate diff --git a/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php b/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php new file mode 100644 index 0000000000..62eb975dbd --- /dev/null +++ b/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php @@ -0,0 +1,484 @@ +delete_all_data(); + wp_cache_flush(); + WP_Topological_Sorter::activate(); + } + + protected function tearDown(): void { + WP_Topological_Sorter::deactivate(); + + parent::tearDown(); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/comment-meta.php + */ + public function test_serialized_comment_meta() { + $this->import_wxr_file( __DIR__ . '/wxr/test-serialized-comment-meta.xml' ); + + $expected_string = '¯\_(ツ)_/¯'; + $expected_array = array( 'key' => '¯\_(ツ)_/¯' ); + + $comments_count = wp_count_comments(); + // Note: using assertEquals() as the return type changes across different WP versions - numeric string vs int. + $this->assertEquals( 1, $comments_count->approved ); + + $comments = get_comments(); + $this->assertCount( 1, $comments ); + + $comment = $comments[0]; + $this->assertSame( $expected_string, get_comment_meta( $comment->comment_ID, 'string', true ) ); + $this->assertSame( $expected_array, get_comment_meta( $comment->comment_ID, 'array', true ) ); + + // Additional check for Data Liberation. + $this->assertEquals( 'A WordPress Commenter', $comments[0]->comment_author ); + $this->assertEquals( 2, $comments[0]->comment_ID ); + $this->assertEquals( 10, $comments[0]->comment_post_ID ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/import.php + */ + public function test_small_import() { + global $wpdb; + + $authors = array( + 'admin' => false, + 'editor' => false, + 'author' => false, + ); + $this->import_wxr_file( __DIR__ . '/wxr/small-export.xml' ); + + // Ensure that authors were imported correctly. + $user_count = count_users(); + $this->assertSame( 3, $user_count['total_users'] ); + $admin = get_user_by( 'login', 'admin' ); + /*$this->assertSame( 'admin', $admin->user_login ); + $this->assertSame( 'local@host.null', $admin->user_email ); + $editor = get_user_by( 'login', 'editor' ); + $this->assertSame( 'editor', $editor->user_login ); + $this->assertSame( 'editor@example.org', $editor->user_email ); + $this->assertSame( 'FirstName', $editor->user_firstname ); + $this->assertSame( 'LastName', $editor->user_lastname ); + $author = get_user_by( 'login', 'author' ); + $this->assertSame( 'author', $author->user_login ); + $this->assertSame( 'author@example.org', $author->user_email );*/ + + // Check that terms were imported correctly. + + $this->assertSame( '30', wp_count_terms( 'category' ) ); + $this->assertSame( '3', wp_count_terms( 'post_tag' ) ); + $foo = get_term_by( 'slug', 'foo', 'category' ); + $this->assertSame( 0, $foo->parent ); + $bar = get_term_by( 'slug', 'bar', 'category' ); + $foo_bar = get_term_by( 'slug', 'foo-bar', 'category' ); + $this->assertSame( $bar->term_id, $foo_bar->parent ); + + // Check that posts/pages were imported correctly. + $post_count = wp_count_posts( 'post' ); + $this->assertSame( '5', $post_count->publish ); + $this->assertSame( '1', $post_count->private ); + $page_count = wp_count_posts( 'page' ); + $this->assertSame( '4', $page_count->publish ); + $this->assertSame( '1', $page_count->draft ); + $comment_count = wp_count_comments(); + $this->assertSame( 1, $comment_count->total_comments ); + + $posts = get_posts( + array( + 'numberposts' => 20, + 'post_type' => 'any', + 'post_status' => 'any', + 'orderby' => 'ID', + ) + ); + $this->assertCount( 11, $posts ); + + $post = $posts[0]; + $this->assertSame( 'Many Categories', $post->post_title ); + $this->assertSame( 'many-categories', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'post', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $cats = wp_get_post_categories( $post->ID ); + $this->assertCount( 27, $cats ); + + $post = $posts[1]; + $this->assertSame( 'Non-standard post format', $post->post_title ); + $this->assertSame( 'non-standard-post-format', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'post', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $cats = wp_get_post_categories( $post->ID ); + $this->assertCount( 1, $cats ); + $this->assertTrue( has_post_format( 'aside', $post->ID ) ); + + $post = $posts[2]; + $this->assertSame( 'Top-level Foo', $post->post_title ); + $this->assertSame( 'top-level-foo', $post->post_name ); + //$this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'post', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $cats = wp_get_post_categories( $post->ID, array( 'fields' => 'all' ) ); + $this->assertCount( 1, $cats ); + $this->assertSame( 'foo', $cats[0]->slug ); + + $post = $posts[3]; + $this->assertSame( 'Foo-child', $post->post_title ); + $this->assertSame( 'foo-child', $post->post_name ); + // $this->assertSame( (string) $editor->ID, $post->post_author ); + $this->assertSame( 'post', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $cats = wp_get_post_categories( $post->ID, array( 'fields' => 'all' ) ); + $this->assertCount( 1, $cats ); + $this->assertSame( 'foo-bar', $cats[0]->slug ); + + $post = $posts[4]; + $this->assertSame( 'Private Post', $post->post_title ); + $this->assertSame( 'private-post', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'post', $post->post_type ); + $this->assertSame( 'private', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $cats = wp_get_post_categories( $post->ID ); + $this->assertCount( 1, $cats ); + $tags = wp_get_post_tags( $post->ID ); + $this->assertCount( 3, $tags ); + $this->assertSame( 'tag1', $tags[0]->slug ); + $this->assertSame( 'tag2', $tags[1]->slug ); + $this->assertSame( 'tag3', $tags[2]->slug ); + + $post = $posts[5]; + $this->assertSame( '1-col page', $post->post_title ); + $this->assertSame( '1-col-page', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'page', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $this->assertSame( 'onecolumn-page.php', get_post_meta( $post->ID, '_wp_page_template', true ) ); + + $post = $posts[6]; + $this->assertSame( 'Draft Page', $post->post_title ); + $this->assertSame( '', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'page', $post->post_type ); + $this->assertSame( 'draft', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $this->assertSame( 'default', get_post_meta( $post->ID, '_wp_page_template', true ) ); + + $post = $posts[7]; + $this->assertSame( 'Parent Page', $post->post_title ); + $this->assertSame( 'parent-page', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'page', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $this->assertSame( 'default', get_post_meta( $post->ID, '_wp_page_template', true ) ); + + $post = $posts[8]; + $this->assertSame( 'Child Page', $post->post_title ); + $this->assertSame( 'child-page', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'page', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( $posts[7]->ID, $post->post_parent ); + $this->assertSame( 'default', get_post_meta( $post->ID, '_wp_page_template', true ) ); + + $post = $posts[9]; + $this->assertSame( 'Sample Page', $post->post_title ); + $this->assertSame( 'sample-page', $post->post_name ); + // $this->assertSame( (string) $admin->ID, $post->post_author ); + $this->assertSame( 'page', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $this->assertSame( 'default', get_post_meta( $post->ID, '_wp_page_template', true ) ); + + $post = $posts[10]; + $this->assertSame( 'Hello world!', $post->post_title ); + $this->assertSame( 'hello-world', $post->post_name ); + // $this->assertSame( (string) $author->ID, $post->post_author ); + $this->assertSame( 'post', $post->post_type ); + $this->assertSame( 'publish', $post->post_status ); + $this->assertSame( 0, $post->post_parent ); + $cats = wp_get_post_categories( $post->ID ); + $this->assertCount( 1, $cats ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/postmeta.php + */ + public function test_serialized_postmeta_no_cdata() { + $this->import_wxr_file( __DIR__ . '/wxr/test-serialized-postmeta-no-cdata.xml' ); + + $expected = array( + 'special_post_title' => 'A special title', + 'is_calendar' => '', + ); + $this->assertSame( $expected, get_post_meta( 122, 'post-options', true ) ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/postmeta.php + */ + public function test_utw_postmeta() { + $this->import_wxr_file( __DIR__ . '/wxr/test-utw-post-meta-import.xml' ); + + $tags = array( + 'album', + 'apple', + 'art', + 'artwork', + 'dead-tracks', + 'ipod', + 'itunes', + 'javascript', + 'lyrics', + 'script', + 'tracks', + 'windows-scripting-host', + 'wscript', + ); + + $expected = array(); + foreach ( $tags as $tag ) { + $classy = new StdClass(); + $classy->tag = $tag; + $expected[] = $classy; + } + + $this->assertEquals( $expected, get_post_meta( 150, 'test', true ) ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/postmeta.php + */ + public function test_serialized_postmeta_with_cdata() { + $this->import_wxr_file( __DIR__ . '/wxr/test-serialized-postmeta-with-cdata.xml' ); + + // HTML in the CDATA should work with old WordPress version. + $this->assertSame( '
some html
', get_post_meta( 10, 'contains-html', true ) ); + // Serialised will only work with 3.0 onwards. + $expected = array( + 'special_post_title' => 'A special title', + 'is_calendar' => '', + ); + $this->assertSame( $expected, get_post_meta( 10, 'post-options', true ) ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/postmeta.php + */ + public function test_serialized_postmeta_with_evil_stuff_in_cdata() { + $this->import_wxr_file( __DIR__ . '/wxr/test-serialized-postmeta-with-cdata.xml' ); + + // Evil content in the CDATA. + $this->assertSame( 'evil', get_post_meta( 10, 'evil', true ) ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/postmeta.php + */ + public function test_serialized_postmeta_with_slashes() { + $this->import_wxr_file( __DIR__ . '/wxr/test-serialized-postmeta-with-cdata.xml' ); + + $expected_integer = '1'; + $expected_string = '¯\_(ツ)_/¯'; + $expected_array = array( 'key' => '¯\_(ツ)_/¯' ); + $expected_array_nested = array( + 'key' => array( + 'foo' => '¯\_(ツ)_/¯', + 'bar' => '\o/', + ), + ); + + $this->assertSame( $expected_string, get_post_meta( 10, 'string', true ) ); + $this->assertSame( $expected_array, get_post_meta( 10, 'array', true ) ); + $this->assertSame( $expected_array_nested, get_post_meta( 10, 'array-nested', true ) ); + $this->assertSame( $expected_integer, get_post_meta( 10, 'integer', true ) ); + } + + /** + * This is a WordPress core importer test. + * + * @see https://github.com/WordPress/wordpress-importer/blob/master/phpunit/tests/term-meta.php + */ + public function test_serialized_term_meta() { + register_taxonomy( 'custom_taxonomy', array( 'post' ) ); + + $this->import_wxr_file( __DIR__ . '/wxr/test-serialized-term-meta.xml' ); + + $expected_string = '¯\_(ツ)_/¯'; + $expected_array = array( 'key' => '¯\_(ツ)_/¯' ); + + $term = get_term_by( 'slug', 'post_tag', 'post_tag' ); + $this->assertInstanceOf( 'WP_Term', $term ); + $this->assertSame( $expected_string, get_term_meta( $term->term_id, 'string', true ) ); + $this->assertSame( $expected_array, get_term_meta( $term->term_id, 'array', true ) ); + + $term = get_term_by( 'slug', 'category', 'category' ); + $this->assertInstanceOf( 'WP_Term', $term ); + $this->assertSame( $expected_string, get_term_meta( $term->term_id, 'string', true ) ); + $this->assertSame( $expected_array, get_term_meta( $term->term_id, 'array', true ) ); + + $term = get_term_by( 'slug', 'custom_taxonomy', 'custom_taxonomy' ); + $this->assertInstanceOf( 'WP_Term', $term ); + $this->assertSame( $expected_string, get_term_meta( $term->term_id, 'string', true ) ); + $this->assertSame( $expected_array, get_term_meta( $term->term_id, 'array', true ) ); + } + + /** + * Multiple sessions tests. + */ + public function test_topological_sorter_set_session() { + $sorter = new WP_Topological_Sorter(); + $post = array( 'post_id' => 1 ); + $mapped = array( + 'post_id' => 1, + '_already_mapped' => false + ); + + // Add a first session. + $sorter->set_session( 1 ); + $sorter->map_entity( 'post', $post ); + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + // Map the same entity again but with a different ID (the real one). + $sorter->map_entity( 'post', $post, 2 ); + + $mapped['_already_mapped'] = true; + $mapped['post_id'] = '2'; + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + + $mapped = array( + 'post_id' => 1, + '_already_mapped' => false + ); + + // Add a second session. + $sorter->set_session( 2 ); + $sorter->map_entity( 'post', $post ); + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + // Map the same entity again but with a different ID (the real one). + $sorter->map_entity( 'post', $post, 3 ); + + $mapped['_already_mapped'] = true; + $mapped['post_id'] = '3'; + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + + $sorter->set_session( 1 ); + $mapped['post_id'] = '2'; + // First session should still have the old mapping. + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + + $sorter->delete_session( 1 ); + $this->assertSame( $post, $sorter->get_mapped_entity( 'post', $post ) ); + + $sorter->set_session( 2 ); + $mapped['post_id'] = '3'; + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + + $sorter->delete_session( 2 ); + $this->assertSame( $post, $sorter->get_mapped_entity( 'post', $post ) ); + } + + /** + * Null session tests. + */ + public function test_topological_sorter_no_session() { + $sorter = new WP_Topological_Sorter(); + $post = array( 'post_id' => 1 ); + $mapped = array( + 'post_id' => 1, + '_already_mapped' => false + ); + + // Add a first session. + $sorter->map_entity( 'post', $post ); + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + // Map the same entity again but with a different ID (the real one). + $sorter->map_entity( 'post', $post, 2 ); + + $mapped['_already_mapped'] = true; + $mapped['post_id'] = '2'; + $this->assertSame( $mapped, $sorter->get_mapped_entity( 'post', $post ) ); + } + + /** + * Null session tests. + */ + public function test_topological_sorter_multiple_entities() { + $sorter = new WP_Topological_Sorter(); + $post = array( 'post_id' => 1 ); + $term = array( 'term_id' => 1 ); + $mapped_post = array( + 'post_id' => 1, + '_already_mapped' => false + ); + $mapped_term = array( + 'term_id' => 1, + '_already_mapped' => false + ); + + // Add a first session. + $sorter->set_session( 1 ); + + $sorter->map_entity( 'post', $post ); + $sorter->map_entity( 'term', $term ); + + $this->assertSame( $mapped_post, $sorter->get_mapped_entity( 'post', $post ) ); + $this->assertSame( $mapped_term, $sorter->get_mapped_entity( 'term', $term ) ); + + // Map the same entity again but with a different ID (the real one). + $sorter->map_entity( 'post', $post, 2 ); + $sorter->map_entity( 'term', $term, 2 ); + + $mapped_post['_already_mapped'] = true; + $mapped_post['post_id'] = '2'; + $this->assertSame( $mapped_post, $sorter->get_mapped_entity( 'post', $post ) ); + + $mapped_term['_already_mapped'] = true; + $mapped_term['term_id'] = '2'; + $this->assertSame( $mapped_term, $sorter->get_mapped_entity( 'term', $term ) ); + } + + /** + * Import a WXR file. + */ + private function import_wxr_file( string $wxr_path ) { + $importer = WP_Stream_Importer::create_for_wxr_file( $wxr_path ); + + do { + while ( $importer->next_step( 1 ) ) { + // noop + } + } while ( $importer->advance_to_next_stage() ); + } +} diff --git a/packages/playground/data-liberation/tests/WPWXRReaderTests.php b/packages/playground/data-liberation/tests/WPWXRReaderTests.php index d9b131ce3f..23f3431b11 100644 --- a/packages/playground/data-liberation/tests/WPWXRReaderTests.php +++ b/packages/playground/data-liberation/tests/WPWXRReaderTests.php @@ -3,7 +3,7 @@ use PHPUnit\Framework\TestCase; class WPWXRReaderTests extends TestCase { - + /** * @dataProvider preexisting_wxr_files_provider */ @@ -42,7 +42,7 @@ public function test_does_not_crash_when_parsing_preexisting_wxr_files_as_stream $this->assertEquals($expected_entitys, $found_entities); } - public function preexisting_wxr_files_provider() { + public static function preexisting_wxr_files_provider() { return [ [__DIR__ . '/wxr/a11y-unit-test-data.xml', 1043], [__DIR__ . '/wxr/crazy-cdata-escaped.xml', 5], @@ -52,7 +52,7 @@ public function preexisting_wxr_files_provider() { [__DIR__ . '/wxr/slashes.xml', 9], [__DIR__ . '/wxr/small-export.xml', 68], [__DIR__ . '/wxr/test-serialized-postmeta-no-cdata.xml', 5], - [__DIR__ . '/wxr/test-serialized-postmeta-with-cdata.xml', 7], + [__DIR__ . '/wxr/test-serialized-postmeta-with-cdata.xml', 11], [__DIR__ . '/wxr/test-utw-post-meta-import.xml', 5], [__DIR__ . '/wxr/theme-unit-test-data.xml', 1146], [__DIR__ . '/wxr/valid-wxr-1.0.xml', 32], @@ -114,7 +114,7 @@ public function test_simple_wxr() { ], $importer->get_entity()->get_data() ); - + $this->assertTrue( $importer->next_entity() ); $this->assertEquals( [ diff --git a/packages/playground/data-liberation/tests/import/blueprint-import.json b/packages/playground/data-liberation/tests/import/blueprint-import.json index 4030a4d263..99e8f5037b 100644 --- a/packages/playground/data-liberation/tests/import/blueprint-import.json +++ b/packages/playground/data-liberation/tests/import/blueprint-import.json @@ -3,7 +3,8 @@ "constants": { "WP_DEBUG": true, "WP_DEBUG_DISPLAY": true, - "WP_DEBUG_LOG": true + "WP_DEBUG_LOG": true, + "PHPUNIT_FILTER": false }, "login": true, "steps": [ @@ -18,7 +19,7 @@ }, { "step": "runPHP", - "code": "run($arguments);\nif ( $res !== 0 ) {\ntrigger_error('PHPUnit failed', E_USER_ERROR);\n}\n} catch (Throwable $e) {\ntrigger_error('PHPUnit failed: ' . $e->getMessage(), E_USER_ERROR);\n};" + "code": "run($arguments);\nif ( $res !== 0 ) {\ntrigger_error('PHPUnit failed', E_USER_ERROR);\n}\n} catch (Throwable $e) {\ntrigger_error('PHPUnit failed: ' . $e->getMessage(), E_USER_ERROR);\n}\n;" } ] } diff --git a/packages/playground/data-liberation/tests/wxr/mixed-categories.xml b/packages/playground/data-liberation/tests/wxr/mixed-categories.xml new file mode 100644 index 0000000000..ae74a7530e --- /dev/null +++ b/packages/playground/data-liberation/tests/wxr/mixed-categories.xml @@ -0,0 +1,82 @@ + + + + + Mixed Categories + https://playground.wordpress.net/scope:funny-chic-valley + + Fri, 29 Nov 2024 12:36:23 +0000 + en-US + 1.2 + https://playground.wordpress.net/scope:funny-chic-valley + https://playground.wordpress.net/scope:funny-chic-valley + + + 1 + + + + + + + + + 5 + + + + + + 1 + + + + + + 3 + + + + + + 2 + + + + + + 5 + + + + + + + 1 + + + + + + + 3 + + + + + + + 2 + + + + + + + diff --git a/packages/playground/data-liberation/tests/wxr/post-content-blank-lines.xml b/packages/playground/data-liberation/tests/wxr/post-content-blank-lines.xml new file mode 100644 index 0000000000..db15df5521 --- /dev/null +++ b/packages/playground/data-liberation/tests/wxr/post-content-blank-lines.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + Export Datasets + http://localhost/ + Just another WordPress site + Sat, 16 Oct 2010 20:53:18 +0000 + en + 1.1 + http://localhost/ + http://localhost/ + + 2johnjohndoe@example.org + http://wordpress.org/?v=3.1-alpha + + + Hello world! + http://localhost/?p=1 + Sat, 16 Oct 2010 20:53:18 +0000 + john + http://localhost/?p=1 + + + 1 + 2010-10-16 20:53:18 + 2010-10-16 20:53:18 + open + open + hello-world + publish + 0 + 0 + post + + 0 + + + diff --git a/packages/playground/data-liberation/tests/wxr/slashes.xml b/packages/playground/data-liberation/tests/wxr/slashes.xml index 3e073d8121..2e0cb0d25b 100644 --- a/packages/playground/data-liberation/tests/wxr/slashes.xml +++ b/packages/playground/data-liberation/tests/wxr/slashes.xml @@ -64,14 +64,24 @@ 0 - - Post by - - _edit_last + + 1 + + + http://wordpress.org/ + + 2011-01-18 20:53:18 + 2011-01-18 20:53:18 + + 1 + + 0 + 0 + diff --git a/packages/playground/data-liberation/tests/wxr/term-formats.xml b/packages/playground/data-liberation/tests/wxr/term-formats.xml new file mode 100644 index 0000000000..602b9f0ee4 --- /dev/null +++ b/packages/playground/data-liberation/tests/wxr/term-formats.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + Export Dataset + http://localhost/ + Just another WordPress site + Fri, 15 Dec 2017 10:47:50 +0000 + en + 1.2 + http://localhost/ + http://localhost/ + + + 1 + + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + 5 + + + + + + + + + + + + 7nav_menu + + + https://wordpress.org/?v=5.0 + + + + diff --git a/packages/playground/data-liberation/tests/wxr/test-serialized-comment-meta.xml b/packages/playground/data-liberation/tests/wxr/test-serialized-comment-meta.xml new file mode 100644 index 0000000000..8cc47132c6 --- /dev/null +++ b/packages/playground/data-liberation/tests/wxr/test-serialized-comment-meta.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + Test With Serialized Comment Meta + http://test.wordpress.org/ + Just another blog + Mon, 30 Nov 2009 21:35:27 +0000 + http://wordpress.org/?v=2.8.4 + en + 1.0 + http://test.wordpress.org/ + http://test.wordpress.org/ + + + My Entry with comments and comment meta + http://test.wordpress.org/comment-meta + Tue, 30 Nov 1999 00:00:00 +0000 + + http://test.wordpress.org/comment-meta + + + + 10 + 2009-10-20 16:13:20 + 0000-00-00 00:00:00 + open + open + + draft + 0 + 0 + post + + + + 1 + + + https://wordpress.org/ + + + + Gravatar.]]> + + + 0 + 0 + + + + + + + + + + + + diff --git a/packages/playground/data-liberation/tests/wxr/test-serialized-postmeta-with-cdata.xml b/packages/playground/data-liberation/tests/wxr/test-serialized-postmeta-with-cdata.xml index 2fd3923501..38d015726f 100644 --- a/packages/playground/data-liberation/tests/wxr/test-serialized-postmeta-with-cdata.xml +++ b/packages/playground/data-liberation/tests/wxr/test-serialized-postmeta-with-cdata.xml @@ -21,57 +21,71 @@ xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:wp="http://wordpress.org/export/1.0/" -> + xmlns:wp="http://wordpress.org/export/1.0/"> - - Test With Serialized Postmeta - http://test.wordpress.org/ - Just another blog - Mon, 30 Nov 2009 21:35:27 +0000 - http://wordpress.org/?v=2.8.4 - en - 1.0 - http://test.wordpress.org/ - http://test.wordpress.org/ + + Test With Serialized Postmeta + http://test.wordpress.org/ + Just another blog + Mon, 30 Nov 2009 21:35:27 +0000 + http://wordpress.org/?v=2.8.4 + en + 1.0 + http://test.wordpress.org/ + http://test.wordpress.org/ -My Entry with Postmeta -http://test.wordpress.org/postemta -Tue, 30 Nov 1999 00:00:00 +0000 - + My Entry with Postmeta + http://test.wordpress.org/postemta + Tue, 30 Nov 1999 00:00:00 +0000 + - + - + -http://test.wordpress.org/postmeta - - - -10 -2009-10-20 16:13:20 -0000-00-00 00:00:00 -open -open - -draft -0 -0 -post - - -post-options - - - -contains-html -some html]]> - - -evil -evil]]> - - - + http://test.wordpress.org/postmeta + + + + 10 + 2009-10-20 16:13:20 + 0000-00-00 00:00:00 + open + open + + draft + 0 + 0 + post + + + post-options + + + + contains-html + some html]]> + + + evil + evil]]> + + + + + + + + + + + + + + + + + + diff --git a/packages/playground/data-liberation/tests/wxr/test-serialized-term-meta.xml b/packages/playground/data-liberation/tests/wxr/test-serialized-term-meta.xml new file mode 100644 index 0000000000..c7e942f77d --- /dev/null +++ b/packages/playground/data-liberation/tests/wxr/test-serialized-term-meta.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + Test With Serialized Term Meta + http://test.wordpress.org/ + Just another blog + Mon, 30 Nov 2009 21:35:27 +0000 + http://wordpress.org/?v=2.8.4 + en + 1.0 + http://test.wordpress.org/ + http://test.wordpress.org/ + + 1 + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + My Entry with term meta + http://test.wordpress.org/term-meta + Tue, 30 Nov 1999 00:00:00 +0000 + + + + + http://test.wordpress.org/term-meta + + + + 10 + 2009-10-20 16:13:20 + 0000-00-00 00:00:00 + open + open + + draft + 0 + 0 + post + + + + diff --git a/packages/playground/data-liberation/tests/wxr/valid-wxr-1.1.xml b/packages/playground/data-liberation/tests/wxr/valid-wxr-1.1.xml index cd039e8efd..f389741f1b 100644 --- a/packages/playground/data-liberation/tests/wxr/valid-wxr-1.1.xml +++ b/packages/playground/data-liberation/tests/wxr/valid-wxr-1.1.xml @@ -1,112 +1,112 @@ - - - - - - - - - - - - - - - - - - - - - - - Export Datasets - http://localhost/ - Just another WordPress site - Sat, 16 Oct 2010 20:53:18 +0000 - en - 1.1 - http://localhost/ - http://localhost/ - - 2johnjohndoe@example.org - - 3alpha - 22clippable - 40post_taxbieup - - http://wordpress.org/?v=3.1-alpha - - - Hello world! - http://localhost/?p=1 - Sat, 16 Oct 2010 20:53:18 +0000 - john - http://localhost/?p=1 - - - - 1 - 2010-10-16 20:53:18 - 2010-10-16 20:53:18 - open - open - hello-world - publish - 0 - 0 - post - - 0 - - - - - 1 - - - http://wordpress.org/ - - 2010-10-16 20:53:18 - 2010-10-16 20:53:18 - To delete a comment, just log in and view the post's comments. There you will have the option to edit or delete them.]]> - 1 - - 0 - 0 - - - - About - http://localhost/?page_id=2 - Sat, 16 Oct 2010 20:53:18 +0000 - john - http://localhost/?page_id=2 - - - - 2 - 2010-10-16 20:53:18 - 2010-10-16 20:53:18 - open - open - about - publish - 0 - 0 - page - - 0 - - _wp_page_template - - - - - + + + + + + + + + + + + + + + + + + + + + + + Export Datasets + http://localhost/ + Just another WordPress site + Sat, 16 Oct 2010 20:53:18 +0000 + en + 1.1 + http://localhost/ + http://localhost/ + + 2johnjohndoe@example.org + + 3alpha + 22clippable + 40post_taxbieup + + http://wordpress.org/?v=3.1-alpha + + + Hello world! + http://localhost/?p=1 + Sat, 16 Oct 2010 20:53:18 +0000 + john + http://localhost/?p=1 + + + + 1 + 2010-10-16 20:53:18 + 2010-10-16 20:53:18 + open + open + hello-world + publish + 0 + 0 + post + + 0 + + + + + 1 + + + http://wordpress.org/ + + 2010-10-16 20:53:18 + 2010-10-16 20:53:18 + To delete a comment, just log in and view the post's comments. There you will have the option to edit or delete them.]]> + 1 + + 0 + 0 + + + + About + http://localhost/?page_id=2 + Sat, 16 Oct 2010 20:53:18 +0000 + john + http://localhost/?page_id=2 + + + + 2 + 2010-10-16 20:53:18 + 2010-10-16 20:53:18 + open + open + about + publish + 0 + 0 + page + + 0 + + _wp_page_template + + + + +