diff --git a/newspack-custom-content-migrator.php b/newspack-custom-content-migrator.php index 3fa227e2b..412bf42b2 100644 --- a/newspack-custom-content-migrator.php +++ b/newspack-custom-content-migrator.php @@ -84,5 +84,6 @@ Command\PublisherSpecific\CarsonNowMigrator::class, Command\PublisherSpecific\ArkansasTimesMigrator::class, Command\PublisherSpecific\ZocaloMigrator::class, + Command\PublisherSpecific\StandardMigrationTestMigrator::class, ) ); diff --git a/src/Command/PublisherSpecific/StandardMigrationTestMigrator.php b/src/Command/PublisherSpecific/StandardMigrationTestMigrator.php new file mode 100644 index 000000000..210028a77 --- /dev/null +++ b/src/Command/PublisherSpecific/StandardMigrationTestMigrator.php @@ -0,0 +1,109 @@ + 'Executes the user migration.', + 'synopsis' => [ + [ + 'type' => 'assoc', + 'name' => 'path-to-json', + 'description' => 'The JSON to migrate', + 'optional' => false, + ], + ], + ] + ); + } + + public function execute_user_migration( $args, $assoc_args ) { + ( new class( $args, $assoc_args ) extends AbstractMigrationRun { + + public function __construct( $args, $assoc_args ) { + $this->args = $args; + $this->assoc_args = $assoc_args; + + $this->set_name( 'User Migration' ); + } + + /** + * Returns the migration objects. + * + * @return MigrationObjects + */ + public function get_migration_objects(): MigrationObjects { + return new JSONMigrationObjectsClass( $this->get_run_key(), $this->assoc_args['path-to-json'] ); + } + + /** + * This function houses the logic for the command. + * + * @param MigrationObjects $migration_objects The objects to perform the migration on. + * + * @return bool|\WP_Error + * @throws \Exception If an error occurs. + */ + public function command( MigrationObjects $migration_objects ): bool|\WP_Error { + foreach ( $migration_objects->get_unprocessed() as $migration_object ) { + $user_data = [ + 'user_pass' => wp_generate_password( 12 ), + ]; + + if ( is_email( $migration_object->get()['user_login'] ) ) { + $user_data['user_login'] = substr( $migration_object->get()['user_login'], 0, strpos( $migration_object->get()['user_login'], '@' ) ); + } else { + $user_data['user_login'] = $migration_object->get()['user_login']; + } + + $user_data['user_email'] = $migration_object->get()['user_email']; + + $user_data['display_name'] = $migration_object->get()['user_name'] . ' ' . $migration_object->get()['user_lastname']; + + $user_data['user_nicename'] = sanitize_title( $user_data['display_name'] ); + + $user_id = wp_insert_user( $user_data ); + + if ( is_wp_error( $user_id ) ) { + \WP_CLI::error( 'Failed to insert user: ' . $user_id->get_error_message() ); + return false; + } else { + \WP_CLI::success( 'Inserted user: ' . $user_data['user_login'] ); + $migration_object->record_source( 'wp_users', 'user_login', $user_id, 'user_login' ); + $migration_object->record_source( 'wp_users', 'user_email', $user_id, 'user_email' ); + $migration_object->record_source( 'wp_users', 'display_name', $user_id, 'user_name' ); + $migration_object->record_source( 'wp_users', 'display_name', $user_id, 'user_lastname' ); + $migration_object->record_source( 'wp_users', 'user_nicename', $user_id, 'wp_users.display_name' ); + $migration_object->store_processed_marker(); + } + } + + return true; + } + } )->start(); + } +} diff --git a/src/Migrator/AbstractMigrationObject.php b/src/Migrator/AbstractMigrationObject.php new file mode 100644 index 000000000..24454a5b2 --- /dev/null +++ b/src/Migrator/AbstractMigrationObject.php @@ -0,0 +1,159 @@ +run_key = $run_key; + global $wpdb; + $this->wpdb = $wpdb; + } + + /** + * @inheritDoc + */ + public function set_run_key( MigrationRunKey $run_key ): void { + // TODO: Implement set_run_key() method. + } + + /** + * @inheritDoc + */ + public function get_run_key(): MigrationRunKey { + return $this->run_key; + } + + /** + * @inheritDoc + */ + public function set( object|array $data, string $pointer_to_identifier = 'id' ): void { + $this->data = $data; + $this->pointer_to_identifier = $pointer_to_identifier; + // TODO setting of $processed could be improved by using a cache + $this->has_been_processed(); + } + + /** + * @inheritDoc + */ + public function get(): array|object { + return $this->data; + } + + /** + * @return string + */ + public function get_pointer_to_identifier(): string { + return $this->pointer_to_identifier; + } + + /** + * @inheritDoc + */ + public function store(): bool { + $insert = $this->wpdb->insert( + $this->wpdb->options, + [ + 'option_name' => $this->get_run_key()->get() . '_migration_object_' . $this->get() [ $this->get_pointer_to_identifier() ], + 'option_value' => wp_json_encode( $this->get() ), + 'autoload' => 'no', + ] + ); + + if ( ! is_bool( $insert ) ) { + return false; + } + + return $insert; + } + + /** + * @inheritDoc + */ + public function store_processed_marker(): bool { + $insert = $this->wpdb->insert( + $this->wpdb->options, + [ + 'option_name' => $this->get_run_key()->get() . '_migration_object_' . $this->get()[ $this->get_pointer_to_identifier() ] . '_processed', + 'option_value' => '1', + 'autoload' => 'no', + ] + ); + + if ( ! is_bool( $insert ) ) { + return false; + } + + if ( $insert ) { + $this->processed = true; + } + + return $insert; + } + + /** + * @inheritDoc + */ + public function has_been_processed(): bool { + if ( ! isset( $this->processed ) ) { + // phpcs:disable + $options_table = $this->wpdb->options; + $this->processed = (bool) $this->wpdb->get_var( + $this->wpdb->prepare( + "SELECT option_value FROM {$options_table} WHERE option_name = %s", + $this->get_run_key()->get() . '_migration_object_' . $this->get()[ $this->get_pointer_to_identifier() ] . '_processed' + ) + ); + // phpcs:enable + } + + return $this->processed; + } + + /** + * @inheritDoc + */ + public function record_source( string $table, string $column, int $id, string $source ): bool { + $meta_table = match ( $table ) { + 'wp_users' => $this->wpdb->usermeta, + 'wp_posts' => $this->wpdb->postmeta, + 'wp_terms' => $this->wpdb->termmeta, + default => $this->wpdb->options, + }; + + $option_name = $this->get_run_key()->get() . '_migration_object_source_' . $this->get()[ $this->get_pointer_to_identifier() ] . "_{$table}_{$column}_{$id}"; + $insert = $this->wpdb->insert( + $meta_table, + [ + 'meta_key' => $option_name, + 'meta_value' => wp_json_encode( + [ + 'table' => $table, + 'column' => $column, + 'id' => $id, + 'source' => $source, + ] + ), + ] + ); + + if ( ! is_bool( $insert ) ) { + return false; + } + + return $insert; + } +} diff --git a/src/Migrator/AbstractMigrationObjects.php b/src/Migrator/AbstractMigrationObjects.php new file mode 100644 index 000000000..75f358665 --- /dev/null +++ b/src/Migrator/AbstractMigrationObjects.php @@ -0,0 +1,57 @@ +data = $data; + $this->run_key = $run_key; + } + + /** + * @inheritDoc + */ + public function set_run_key( MigrationRunKey $run_key ): void { + $this->run_key = $run_key; + } + + /** + * @inheritDoc + */ + public function get_run_key(): MigrationRunKey { + return $this->run_key; + } + + /** + * Returns all processed migration objects. + * + * @return MigrationObject[] + */ + public function get_processed(): iterable { + foreach ( $this->get_all() as $migration_object ) { + if ( $migration_object->has_been_processed() ) { + yield $migration_object; + } + } + } + + /** + * Returns all unprocessed migration objects. + * + * @return MigrationObject[] + */ + public function get_unprocessed(): iterable { + foreach ( $this->get_all() as $migration_object ) { + if ( ! $migration_object->has_been_processed() ) { + yield $migration_object; + } + } + } +} \ No newline at end of file diff --git a/src/Migrator/AbstractMigrationRun.php b/src/Migrator/AbstractMigrationRun.php new file mode 100644 index 000000000..156dbc93f --- /dev/null +++ b/src/Migrator/AbstractMigrationRun.php @@ -0,0 +1,73 @@ +name = sanitize_title( $name ); + } + + /** + * @inheritDoc + */ + public function get_name(): string { + return $this->name; + } + + /** + * @inheritDoc + * + * @throws \Exception If an error occurs. + */ + public function start(): void { + $this->command( $this->get_migration_objects() ); + } + + /** + * @inheritDoc + */ + public function resume(): void { + // TODO: Implement resume() method. + } + + /** + * @inheritDoc + * + * @throws \Exception If an error occurs. + */ + public function restart(): void { + $this->cancel( false ); + $this->start(); + } + + /** + * @inheritDoc + */ + public function cancel( bool $delete_data ): void { + // TODO: Implement cancel() method. + } + + /** + * @inheritDoc + */ + public function get_run_key(): FinalMigrationRunKey { + return new FinalMigrationRunKey( $this->get_name() . '1' ); + } +} \ No newline at end of file diff --git a/src/Migrator/FinalMigrationRunKey.php b/src/Migrator/FinalMigrationRunKey.php new file mode 100644 index 000000000..d6beb1846 --- /dev/null +++ b/src/Migrator/FinalMigrationRunKey.php @@ -0,0 +1,19 @@ +run_key = $run_key; + } + + /** + * @inheritDoc + */ + public function get(): string { + return $this->run_key; + } +} \ No newline at end of file diff --git a/src/Migrator/JSONMigrationObject.php b/src/Migrator/JSONMigrationObject.php new file mode 100644 index 000000000..57db16876 --- /dev/null +++ b/src/Migrator/JSONMigrationObject.php @@ -0,0 +1,13 @@ +save( $path_to_json_file, $pointer_to_identifier ); + + if ( ! $result ) { + throw new Exception( 'Failed to save JSON migration objects.' ); + } + } + + /** + * Saves the migration JSON to DB. + * + * @param string $full_path The full local path to the migration JSON. + * @param string $pointer_to_identifier The JSON attribute pointer that can be used to uniquely identify a JSON object. + * + * @return bool + */ + public function save( string $full_path, string $pointer_to_identifier = 'id' ): bool { + if ( ! file_exists( $full_path ) ) { + return false; + } + + $meta = [ + [ + 'option_name' => $this->get_run_key()->get() . '_' . self::MIGRATION_JSON_FILE_PATH_KEY, + 'option_value' => $full_path, + 'autoload' => 'no', + ], + [ + 'option_name' => $this->get_run_key()->get() . '_' . self::MIGRATION_JSON_FILE_POINTER_TO_IDENTIFIER_KEY, + 'option_value' => $pointer_to_identifier, + 'autoload' => 'no', + ], + ]; + + global $wpdb; + + foreach ( $meta as $meta_item ) { + extract( $meta_item ); + $option_exists = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM $wpdb->options WHERE option_name = %s", + $option_name + ) + ); + + if ( $option_exists ) { + if ( $option_exists->option_value !== $option_value ) { + // TODO - trying to reuse migration run with new params. Either new params should be ignored or new migration run is required. + return false; + } else { + continue; + } + } else { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->insert( $wpdb->options, $meta_item ); + } + } + + $this->path_to_json_file = $full_path; + $this->pointer_to_identifier = $pointer_to_identifier; + + return true; + } + + /** + * Gets the JSON attribute pointer that can be used to uniquely identify a JSON object. + * + * @return string + */ + public function get_pointer_to_identifier(): string { + if ( ! isset( $this->pointer_to_identifier ) ) { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $pointer_to_identifier = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", + $this->get_run_key()->get() . '_' . self::MIGRATION_JSON_FILE_POINTER_TO_IDENTIFIER_KEY + ) + ); + + if ( ! $pointer_to_identifier ) { + $pointer_to_identifier = 'id'; + } + + $this->pointer_to_identifier = $pointer_to_identifier; + } + + return $this->pointer_to_identifier; + } + + /** + * Gets all migration objects. + * + * @return MigrationObject[] + */ + public function get_all(): iterable { + if ( ! isset( $this->path_to_json_file ) ) { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $path_to_json_file = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", + $this->get_run_key()->get() . '_' . self::MIGRATION_JSON_FILE_PATH_KEY + ) + ); + + if ( ! $path_to_json_file ) { + return []; + } + + $this->path_to_json_file = $path_to_json_file; + } + + foreach ( ( new FileImportFactory() )->get_file( $this->path_to_json_file )->getIterator() as $json_object ) { + yield new MigrationObjectClass( $this->get_run_key(), $json_object, $this->get_pointer_to_identifier() ); + } + } +} diff --git a/src/Migrator/MigrationCommand.php b/src/Migrator/MigrationCommand.php new file mode 100644 index 000000000..7e686b671 --- /dev/null +++ b/src/Migrator/MigrationCommand.php @@ -0,0 +1,39 @@ +set( $data, $pointer_to_identifier ); + } +} \ No newline at end of file diff --git a/src/Migrator/MigrationObjects.php b/src/Migrator/MigrationObjects.php new file mode 100644 index 000000000..920f1d415 --- /dev/null +++ b/src/Migrator/MigrationObjects.php @@ -0,0 +1,41 @@ +