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
+
+
+
+
+