v1.0.0
This commit is contained in:
parent
8caa140991
commit
5cdf7f48e5
29 changed files with 6526 additions and 0 deletions
390
includes/class-robotstxt-mlp2-mlp5-step-analysis.php
Normal file
390
includes/class-robotstxt-mlp2-mlp5-step-analysis.php
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
<?php
|
||||
/**
|
||||
* Wizard Step 2 — Analysis.
|
||||
*
|
||||
* Scans MLP2 backup tables and MLP5 tables to count records, detect
|
||||
* orphaned content, and validate schema compatibility before migration.
|
||||
*
|
||||
* @package Robotstxt_Mlp2_Mlp5
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Robotstxt_Mlp2_Mlp5_Step_Analysis
|
||||
*/
|
||||
class Robotstxt_Mlp2_Mlp5_Step_Analysis {
|
||||
|
||||
/**
|
||||
* Run the full analysis and return a structured result.
|
||||
*
|
||||
* @return array {
|
||||
* @type int $site_relations_count MLP2 site relation rows.
|
||||
* @type int $post_groups_count Distinct MLP2 post-relation groups (by source).
|
||||
* @type int $term_groups_count Distinct MLP2 term-relation groups (by source).
|
||||
* @type int[] $orphaned_site_ids Site IDs referenced in MLP2 that no longer exist.
|
||||
* @type int $orphaned_posts_count Count of orphaned post references (sampled).
|
||||
* @type int $orphaned_terms_count Count of orphaned term references (sampled).
|
||||
* @type int $duplicate_relations Count of duplicate content relation rows.
|
||||
* @type bool $mlp5_schema_ok True if MLP5 tables have expected columns.
|
||||
* @type string $mlp5_schema_error Human-readable schema error, or empty string.
|
||||
* @type string $mlp2_content_table Backup content relations table name.
|
||||
* @type string $mlp2_site_table Backup site relations table name.
|
||||
* @type string $mlp5_content_table MLP5 content relations table name.
|
||||
* @type string $mlp5_site_table MLP5 site relations table name.
|
||||
* @type string $mlp5_relationships_table MLP5 relationships group table name.
|
||||
* }
|
||||
*/
|
||||
public static function run(): array {
|
||||
global $wpdb;
|
||||
|
||||
$mlp2 = new Robotstxt_Mlp2_Mlp5_Detector_Mlp2();
|
||||
$mlp5 = new Robotstxt_Mlp2_Mlp5_Detector_Mlp5();
|
||||
|
||||
$mlp2_site_table = $mlp2->get_site_relations_table();
|
||||
$mlp2_content_table = $mlp2->get_content_relations_table();
|
||||
$mlp5_site_table = $mlp5->get_site_relations_table();
|
||||
$mlp5_content_table = $mlp5->get_content_relations_table();
|
||||
$mlp5_rel_table = $mlp5->get_relationships_table();
|
||||
|
||||
// --- Counts ---
|
||||
$site_count = '';
|
||||
if ( '' !== $mlp2_site_table ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$site_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$mlp2_site_table}`" );
|
||||
}
|
||||
|
||||
$post_count = 0;
|
||||
$term_count = 0;
|
||||
if ( '' !== $mlp2_content_table ) {
|
||||
$col = self::source_blog_col( $mlp2, $mlp2_content_table );
|
||||
$id = self::source_element_col( $mlp2, $mlp2_content_table );
|
||||
|
||||
if ( '' !== $col && '' !== $id ) {
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$post_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT {$col}, {$id}) FROM `{$mlp2_content_table}` WHERE ml_type = 'post'" );
|
||||
$term_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT {$col}, {$id}) FROM `{$mlp2_content_table}` WHERE ml_type = 'term'" );
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
||||
|
||||
// --- Orphaned sites ---
|
||||
$orphaned_sites = self::detect_orphaned_sites( $mlp2_site_table );
|
||||
|
||||
// --- Orphaned posts/terms (sampled) ---
|
||||
$orphaned_posts = self::detect_orphaned_content( $mlp2_content_table, 'post', 100 );
|
||||
$orphaned_terms = self::detect_orphaned_content( $mlp2_content_table, 'term', 100 );
|
||||
|
||||
// --- Duplicates ---
|
||||
$duplicates = self::count_duplicate_relations( $mlp2_content_table );
|
||||
|
||||
// --- MLP5 schema validation ---
|
||||
list( $schema_ok, $schema_error ) = self::validate_mlp5_schema( $mlp5_site_table, $mlp5_content_table, $mlp5_rel_table, $mlp2, $mlp2_site_table, $mlp2_content_table );
|
||||
|
||||
return array(
|
||||
'site_relations_count' => (int) $site_count,
|
||||
'post_groups_count' => $post_count,
|
||||
'term_groups_count' => $term_count,
|
||||
'orphaned_site_ids' => $orphaned_sites,
|
||||
'orphaned_posts_count' => $orphaned_posts,
|
||||
'orphaned_terms_count' => $orphaned_terms,
|
||||
'duplicate_relations' => $duplicates,
|
||||
'mlp5_schema_ok' => $schema_ok,
|
||||
'mlp5_schema_error' => $schema_error,
|
||||
'mlp2_content_table' => $mlp2_content_table,
|
||||
'mlp2_site_table' => $mlp2_site_table,
|
||||
'mlp5_content_table' => $mlp5_content_table,
|
||||
'mlp5_site_table' => $mlp5_site_table,
|
||||
'mlp5_relationships_table' => $mlp5_rel_table,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Step 2 UI.
|
||||
*/
|
||||
public static function render(): void {
|
||||
$data = Robotstxt_Mlp2_Mlp5_Wizard_State::get_step_data( 2 );
|
||||
$ran = ! empty( $data );
|
||||
$ran_ok = $ran && ! empty( $data['mlp5_schema_ok'] );
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Step 2 — Analysis', 'robotstxt-mlp2-mlp5' ); ?></h2>
|
||||
|
||||
<?php if ( ! $ran ) : ?>
|
||||
<p><?php esc_html_e( 'Click "Run Analysis" to scan the MLP2 backup database and validate MLP5 schema compatibility before migrating.', 'robotstxt-mlp2-mlp5' ); ?></p>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'mlp_migrator_analysis', 'mlp_migrator_nonce' ); ?>
|
||||
<input type="hidden" name="action" value="mlp_migrator_step" />
|
||||
<input type="hidden" name="mlp_migrator_step_action" value="2_run" />
|
||||
<p><button type="submit" class="button button-primary"><?php esc_html_e( 'Run Analysis', 'robotstxt-mlp2-mlp5' ); ?></button></p>
|
||||
</form>
|
||||
<?php else : ?>
|
||||
|
||||
<?php if ( ! $data['mlp5_schema_ok'] ) : ?>
|
||||
<div class="notice notice-error inline" style="margin:0 0 1em;">
|
||||
<p><strong><?php esc_html_e( 'MLP5 schema incompatibility detected:', 'robotstxt-mlp2-mlp5' ); ?></strong> <?php echo esc_html( $data['mlp5_schema_error'] ); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="widefat striped" style="border:0;margin-bottom:1.5em;">
|
||||
<thead><tr><th><?php esc_html_e( 'Category', 'robotstxt-mlp2-mlp5' ); ?></th><th><?php esc_html_e( 'Records found', 'robotstxt-mlp2-mlp5' ); ?></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><?php esc_html_e( 'Site relations', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( number_format_i18n( (int) $data['site_relations_count'] ) ); ?></td></tr>
|
||||
<tr><td><?php esc_html_e( 'Post relation groups', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( number_format_i18n( (int) $data['post_groups_count'] ) ); ?></td></tr>
|
||||
<tr><td><?php esc_html_e( 'Term relation groups', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( number_format_i18n( (int) $data['term_groups_count'] ) ); ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ( ! empty( $data['orphaned_site_ids'] ) || $data['orphaned_posts_count'] > 0 || $data['orphaned_terms_count'] > 0 || $data['duplicate_relations'] > 0 ) : ?>
|
||||
<h3><?php esc_html_e( 'Conflicts (will be skipped and logged)', 'robotstxt-mlp2-mlp5' ); ?></h3>
|
||||
<table class="widefat striped" style="border:0;margin-bottom:1.5em;">
|
||||
<tbody>
|
||||
<?php if ( ! empty( $data['orphaned_site_ids'] ) ) : ?>
|
||||
<tr><td><?php esc_html_e( 'Orphaned sites', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( implode( ', ', array_map( 'intval', $data['orphaned_site_ids'] ) ) ); ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if ( $data['orphaned_posts_count'] > 0 ) : ?>
|
||||
<tr><td><?php esc_html_e( 'Orphaned post references', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( number_format_i18n( (int) $data['orphaned_posts_count'] ) ); ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if ( $data['orphaned_terms_count'] > 0 ) : ?>
|
||||
<tr><td><?php esc_html_e( 'Orphaned term references', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( number_format_i18n( (int) $data['orphaned_terms_count'] ) ); ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if ( $data['duplicate_relations'] > 0 ) : ?>
|
||||
<tr><td><?php esc_html_e( 'Duplicate relation rows', 'robotstxt-mlp2-mlp5' ); ?></td><td><?php echo esc_html( number_format_i18n( (int) $data['duplicate_relations'] ) ); ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline;">
|
||||
<?php wp_nonce_field( 'mlp_migrator_analysis', 'mlp_migrator_nonce' ); ?>
|
||||
<input type="hidden" name="action" value="mlp_migrator_step" />
|
||||
<input type="hidden" name="mlp_migrator_step_action" value="2_run" />
|
||||
<button type="submit" class="button button-secondary"><?php esc_html_e( 'Re-run Analysis', 'robotstxt-mlp2-mlp5' ); ?></button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline;">
|
||||
<?php wp_nonce_field( 'mlp_migrator_step_2', 'mlp_migrator_nonce' ); ?>
|
||||
<input type="hidden" name="action" value="mlp_migrator_step" />
|
||||
<input type="hidden" name="mlp_migrator_step_action" value="2" />
|
||||
<button type="submit" class="button button-primary"<?php echo $ran_ok ? '' : ' disabled'; ?>><?php esc_html_e( 'Next: Migrate Site Relations', 'robotstxt-mlp2-mlp5' ); ?></button>
|
||||
</form>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect orphaned site IDs in the MLP2 site relations table.
|
||||
*
|
||||
* @param string $table MLP2 backup site relations table name.
|
||||
* @return int[] Site IDs that no longer exist in the network.
|
||||
*/
|
||||
private static function detect_orphaned_sites( string $table ): array {
|
||||
if ( '' === $table ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$rows = $wpdb->get_results( "SELECT site_1, site_2 FROM `{$table}`", ARRAY_A );
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$ids = array();
|
||||
foreach ( $rows as $row ) {
|
||||
$ids[] = (int) $row['site_1'];
|
||||
$ids[] = (int) $row['site_2'];
|
||||
}
|
||||
$ids = array_unique( $ids );
|
||||
|
||||
$existing = get_sites(
|
||||
array(
|
||||
'site__in' => $ids,
|
||||
'fields' => 'ids',
|
||||
'number' => count( $ids ),
|
||||
)
|
||||
);
|
||||
$existing = array_map( 'intval', $existing );
|
||||
|
||||
return array_values( array_diff( $ids, $existing ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample up to $sample content relation rows and count orphaned elements.
|
||||
*
|
||||
* @param string $table MLP2 backup content relations table name.
|
||||
* @param string $type 'post' or 'term'.
|
||||
* @param int $sample Maximum rows to inspect.
|
||||
* @return int Count of orphaned references found in the sample.
|
||||
*/
|
||||
private static function detect_orphaned_content( string $table, string $type, int $sample ): int {
|
||||
if ( '' === $table ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$type_safe = ( 'term' === $type ) ? 'term' : 'post';
|
||||
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT ml_blogid, ml_elementid FROM `{$table}` WHERE ml_type = %s LIMIT %d",
|
||||
$type_safe,
|
||||
$sample
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$orphans = 0;
|
||||
foreach ( $rows as $row ) {
|
||||
$site_id = (int) $row['ml_blogid'];
|
||||
$element_id = (int) $row['ml_elementid'];
|
||||
|
||||
if ( ! get_site( $site_id ) ) {
|
||||
++$orphans;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch_to_blog( $site_id );
|
||||
if ( 'term' === $type_safe ) {
|
||||
$exists = (bool) get_term( $element_id );
|
||||
} else {
|
||||
$exists = (bool) get_post( $element_id );
|
||||
}
|
||||
restore_current_blog();
|
||||
|
||||
if ( ! $exists ) {
|
||||
++$orphans;
|
||||
}
|
||||
}
|
||||
|
||||
return $orphans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count duplicate rows in the content relations table.
|
||||
*
|
||||
* @param string $table MLP2 backup content relations table name.
|
||||
* @return int Count of duplicate rows.
|
||||
*/
|
||||
private static function count_duplicate_relations( string $table ): int {
|
||||
if ( '' === $table ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$distinct = (int) $wpdb->get_var( "SELECT COUNT(*) FROM (SELECT DISTINCT ml_source_blogid, ml_source_elementid, ml_blogid, ml_elementid, ml_type FROM `{$table}`) AS d" );
|
||||
|
||||
return max( 0, $total - $distinct );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that MLP5 tables have the expected columns.
|
||||
*
|
||||
* @param string $mlp5_site MLP5 site relations table.
|
||||
* @param string $mlp5_content MLP5 content relations table.
|
||||
* @param string $mlp5_rel MLP5 relationships table.
|
||||
* @param Robotstxt_Mlp2_Mlp5_Detector_Mlp2 $mlp2 MLP2 detector.
|
||||
* @param string $mlp2_site MLP2 backup site relations table.
|
||||
* @param string $mlp2_content MLP2 backup content relations table.
|
||||
* @return array [ bool $ok, string $error_message ]
|
||||
*/
|
||||
private static function validate_mlp5_schema(
|
||||
string $mlp5_site,
|
||||
string $mlp5_content,
|
||||
string $mlp5_rel,
|
||||
Robotstxt_Mlp2_Mlp5_Detector_Mlp2 $mlp2,
|
||||
string $mlp2_site,
|
||||
string $mlp2_content
|
||||
): array {
|
||||
if ( '' === $mlp5_site || '' === $mlp5_content ) {
|
||||
return array( false, __( 'MLP5 tables not found. Ensure MultilingualPress 5 is active and has been initialized.', 'robotstxt-mlp2-mlp5' ) );
|
||||
}
|
||||
|
||||
// Site relations: expect site_1 and site_2 columns.
|
||||
$site_cols = $mlp2->describe_table( $mlp5_site );
|
||||
if ( ! isset( $site_cols['site_1'], $site_cols['site_2'] ) ) {
|
||||
return array( false, sprintf( /* translators: %s: table name */ __( 'Table %s does not have expected columns (site_1, site_2).', 'robotstxt-mlp2-mlp5' ), $mlp5_site ) );
|
||||
}
|
||||
|
||||
// Content relations: expect relationship_id, site_id, content_id.
|
||||
$content_cols = $mlp2->describe_table( $mlp5_content );
|
||||
if ( ! isset( $content_cols['relationship_id'], $content_cols['site_id'], $content_cols['content_id'] ) ) {
|
||||
return array( false, sprintf( /* translators: %s: table name */ __( 'Table %s does not have expected columns (relationship_id, site_id, content_id).', 'robotstxt-mlp2-mlp5' ), $mlp5_content ) );
|
||||
}
|
||||
|
||||
// Relationships group table: expect id and type columns.
|
||||
if ( '' !== $mlp5_rel ) {
|
||||
$rel_cols = $mlp2->describe_table( $mlp5_rel );
|
||||
if ( ! isset( $rel_cols['id'], $rel_cols['type'] ) ) {
|
||||
return array( false, sprintf( /* translators: %s: table name */ __( 'Table %s does not have expected columns (id, type).', 'robotstxt-mlp2-mlp5' ), $mlp5_rel ) );
|
||||
}
|
||||
}
|
||||
|
||||
// MLP2 backup: check the content table has the known columns.
|
||||
if ( '' !== $mlp2_content ) {
|
||||
$mlp2_cols = $mlp2->describe_table( $mlp2_content );
|
||||
$required = array( 'ml_blogid', 'ml_elementid', 'ml_type' );
|
||||
foreach ( $required as $col ) {
|
||||
if ( ! isset( $mlp2_cols[ $col ] ) ) {
|
||||
return array( false, sprintf( /* translators: 1: column name, 2: table name */ __( 'MLP2 backup table %2$s is missing expected column "%1$s". Schema may differ from expected.', 'robotstxt-mlp2-mlp5' ), $col, $mlp2_content ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( true, '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the source-blog column name for the MLP2 content relations table.
|
||||
*
|
||||
* @param Robotstxt_Mlp2_Mlp5_Detector_Mlp2 $mlp2 MLP2 detector.
|
||||
* @param string $table Content relations table name.
|
||||
* @return string Column name or empty string.
|
||||
*/
|
||||
private static function source_blog_col( Robotstxt_Mlp2_Mlp5_Detector_Mlp2 $mlp2, string $table ): string {
|
||||
$cols = $mlp2->describe_table( $table );
|
||||
foreach ( array( 'ml_source_blogid', 'ml_blogid' ) as $candidate ) {
|
||||
if ( isset( $cols[ $candidate ] ) ) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the source-element column name for the MLP2 content relations table.
|
||||
*
|
||||
* @param Robotstxt_Mlp2_Mlp5_Detector_Mlp2 $mlp2 MLP2 detector.
|
||||
* @param string $table Content relations table name.
|
||||
* @return string Column name or empty string.
|
||||
*/
|
||||
private static function source_element_col( Robotstxt_Mlp2_Mlp5_Detector_Mlp2 $mlp2, string $table ): string {
|
||||
$cols = $mlp2->describe_table( $table );
|
||||
foreach ( array( 'ml_source_elementid', 'ml_elementid' ) as $candidate ) {
|
||||
if ( isset( $cols[ $candidate ] ) ) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue