342 lines
13 KiB
PHP
342 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* Wizard Step 4 — Content Relations Migration.
|
|
*
|
|
* Processes post and term relation groups in batches via AJAX.
|
|
* State (offsets, totals, completion) is persisted in Wizard_State step 4 data.
|
|
*
|
|
* @package Robotstxt_Mlp2_Mlp5
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class Robotstxt_Mlp2_Mlp5_Step_Content_Relations
|
|
*/
|
|
class Robotstxt_Mlp2_Mlp5_Step_Content_Relations {
|
|
|
|
/**
|
|
* Number of groups to process per AJAX batch.
|
|
*
|
|
* @var int
|
|
*/
|
|
const BATCH_SIZE = 50;
|
|
|
|
/**
|
|
* Run one batch of content relations.
|
|
*
|
|
* Reads the current offset from wizard state, processes BATCH_SIZE groups,
|
|
* updates state, and returns a progress snapshot.
|
|
*
|
|
* @return array Progress snapshot (see get_progress()).
|
|
*/
|
|
public static function run_batch(): array {
|
|
$state = Robotstxt_Mlp2_Mlp5_Wizard_State::get_step_data( 4 );
|
|
|
|
// Initialise totals on first call.
|
|
if ( empty( $state ) ) {
|
|
$state = array(
|
|
'post_total' => Robotstxt_Mlp2_Mlp5_Reader_Content_Relations::count_groups( 'post' ),
|
|
'term_total' => Robotstxt_Mlp2_Mlp5_Reader_Content_Relations::count_groups( 'term' ),
|
|
'post_offset' => 0,
|
|
'term_offset' => 0,
|
|
'post_done' => false,
|
|
'term_done' => false,
|
|
);
|
|
}
|
|
|
|
// Process posts first, then terms.
|
|
if ( ! $state['post_done'] ) {
|
|
$groups = Robotstxt_Mlp2_Mlp5_Reader_Content_Relations::get_groups_batch( 'post', (int) $state['post_offset'], self::BATCH_SIZE );
|
|
$state['post_offset'] += count( $groups );
|
|
self::process_groups( $groups );
|
|
|
|
if ( empty( $groups ) || (int) $state['post_offset'] >= (int) $state['post_total'] ) {
|
|
$state['post_done'] = true;
|
|
}
|
|
} elseif ( ! $state['term_done'] ) {
|
|
$groups = Robotstxt_Mlp2_Mlp5_Reader_Content_Relations::get_groups_batch( 'term', (int) $state['term_offset'], self::BATCH_SIZE );
|
|
$state['term_offset'] += count( $groups );
|
|
self::process_groups( $groups );
|
|
|
|
if ( empty( $groups ) || (int) $state['term_offset'] >= (int) $state['term_total'] ) {
|
|
$state['term_done'] = true;
|
|
}
|
|
}
|
|
|
|
Robotstxt_Mlp2_Mlp5_Wizard_State::set_step_data( 4, $state );
|
|
|
|
return self::build_progress( $state );
|
|
}
|
|
|
|
/**
|
|
* Return the current progress snapshot without running any work.
|
|
*
|
|
* @return array {
|
|
* @type int $total Total groups to process.
|
|
* @type int $processed Groups processed so far.
|
|
* @type int $migrated Successfully written to MLP5.
|
|
* @type int $skipped Skipped (orphaned, already exists, etc.).
|
|
* @type int $errors Failed writes.
|
|
* @type bool $done True when all groups (post + term) have been processed.
|
|
* }
|
|
*/
|
|
public static function get_progress(): array {
|
|
$state = Robotstxt_Mlp2_Mlp5_Wizard_State::get_step_data( 4 );
|
|
if ( empty( $state ) ) {
|
|
return array(
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
'migrated' => 0,
|
|
'skipped' => 0,
|
|
'errors' => 0,
|
|
'done' => false,
|
|
);
|
|
}
|
|
return self::build_progress( $state );
|
|
}
|
|
|
|
/**
|
|
* Render the Step 4 UI.
|
|
*/
|
|
public static function render(): void {
|
|
$progress = self::get_progress();
|
|
$done = $progress['done'];
|
|
$nonce = wp_create_nonce( Robotstxt_Mlp2_Mlp5_Ajax::NONCE_ACTION );
|
|
?>
|
|
<h2><?php esc_html_e( 'Step 4 — Content Relations', 'robotstxt-mlp2-mlp5' ); ?></h2>
|
|
<p><?php esc_html_e( 'This step migrates all post and term relationships in batches. On large sites this can take several minutes. You can pause and resume at any time.', 'robotstxt-mlp2-mlp5' ); ?></p>
|
|
|
|
<div id="mlp-progress-wrap" style="max-width:500px;margin-bottom:1.5em;">
|
|
<p id="mlp-progress-label">
|
|
<?php
|
|
printf(
|
|
/* translators: 1: processed count, 2: total count */
|
|
esc_html__( 'Processed %1$s of %2$s groups', 'robotstxt-mlp2-mlp5' ),
|
|
'<strong id="mlp-processed">' . esc_html( number_format_i18n( $progress['processed'] ) ) . '</strong>',
|
|
'<strong id="mlp-total">' . esc_html( number_format_i18n( $progress['total'] ) ) . '</strong>'
|
|
);
|
|
?>
|
|
</p>
|
|
<progress id="mlp-progress-bar" max="<?php echo esc_attr( (string) max( 1, $progress['total'] ) ); ?>" value="<?php echo esc_attr( (string) $progress['processed'] ); ?>" style="width:100%;height:20px;"></progress>
|
|
<table class="widefat striped" style="border:0;margin-top:.75em;">
|
|
<tbody>
|
|
<tr><td><?php esc_html_e( 'Migrated', 'robotstxt-mlp2-mlp5' ); ?></td><td id="mlp-migrated"><?php echo esc_html( number_format_i18n( $progress['migrated'] ) ); ?></td></tr>
|
|
<tr><td><?php esc_html_e( 'Skipped', 'robotstxt-mlp2-mlp5' ); ?></td><td id="mlp-skipped"><?php echo esc_html( number_format_i18n( $progress['skipped'] ) ); ?></td></tr>
|
|
<tr><td><?php esc_html_e( 'Errors', 'robotstxt-mlp2-mlp5' ); ?></td><td id="mlp-errors"><?php echo esc_html( number_format_i18n( $progress['errors'] ) ); ?></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ( $done ) : ?>
|
|
<p style="color:#46b450;font-weight:600;"><?php esc_html_e( 'Content relations migration complete.', 'robotstxt-mlp2-mlp5' ); ?></p>
|
|
<?php else : ?>
|
|
<p>
|
|
<button id="mlp-btn-start" class="button button-primary"><?php echo $progress['processed'] > 0 ? esc_html__( 'Resume', 'robotstxt-mlp2-mlp5' ) : esc_html__( 'Start', 'robotstxt-mlp2-mlp5' ); ?></button>
|
|
<button id="mlp-btn-pause" class="button" style="display:none;"><?php esc_html_e( 'Pause', 'robotstxt-mlp2-mlp5' ); ?></button>
|
|
<span id="mlp-status" style="margin-left:.75em;"></span>
|
|
</p>
|
|
<?php endif; ?>
|
|
|
|
<?php if ( $done ) : ?>
|
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
|
<?php wp_nonce_field( 'mlp_migrator_step_4', 'mlp_migrator_nonce' ); ?>
|
|
<input type="hidden" name="action" value="mlp_migrator_step" />
|
|
<input type="hidden" name="mlp_migrator_step_action" value="4" />
|
|
<p><button type="submit" class="button button-primary"><?php esc_html_e( 'Next: Verification', 'robotstxt-mlp2-mlp5' ); ?></button></p>
|
|
</form>
|
|
<?php endif; ?>
|
|
|
|
<script>
|
|
(function() {
|
|
var ajaxUrl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
|
|
var nonce = <?php echo wp_json_encode( $nonce ); ?>;
|
|
var running = false;
|
|
|
|
function updateUI( data ) {
|
|
document.getElementById('mlp-processed').textContent = data.processed;
|
|
document.getElementById('mlp-total').textContent = data.total;
|
|
document.getElementById('mlp-migrated').textContent = data.migrated;
|
|
document.getElementById('mlp-skipped').textContent = data.skipped;
|
|
document.getElementById('mlp-errors').textContent = data.errors;
|
|
var bar = document.getElementById('mlp-progress-bar');
|
|
if ( bar ) { bar.max = Math.max(1, data.total); bar.value = data.processed; }
|
|
}
|
|
|
|
function runBatch() {
|
|
if ( ! running ) { return; }
|
|
var body = new URLSearchParams({ action: 'mlp_migrator_run_batch', nonce: nonce });
|
|
fetch( ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' } )
|
|
.then( function(r) { return r.json(); } )
|
|
.then( function(res) {
|
|
if ( ! res.success ) {
|
|
document.getElementById('mlp-status').textContent = res.data.message || '<?php echo esc_js( __( 'Error during migration.', 'robotstxt-mlp2-mlp5' ) ); ?>';
|
|
running = false;
|
|
document.getElementById('mlp-btn-start').style.display = '';
|
|
document.getElementById('mlp-btn-pause').style.display = 'none';
|
|
return;
|
|
}
|
|
updateUI( res.data );
|
|
if ( res.data.done ) {
|
|
running = false;
|
|
document.getElementById('mlp-status').textContent = '<?php echo esc_js( __( 'Done! Reload the page to continue.', 'robotstxt-mlp2-mlp5' ) ); ?>';
|
|
document.getElementById('mlp-btn-start').style.display = 'none';
|
|
document.getElementById('mlp-btn-pause').style.display = 'none';
|
|
setTimeout( function() { window.location.reload(); }, 1500 );
|
|
} else {
|
|
runBatch();
|
|
}
|
|
} )
|
|
.catch( function() {
|
|
document.getElementById('mlp-status').textContent = '<?php echo esc_js( __( 'Network error. Click Resume to continue.', 'robotstxt-mlp2-mlp5' ) ); ?>';
|
|
running = false;
|
|
document.getElementById('mlp-btn-start').style.display = '';
|
|
document.getElementById('mlp-btn-pause').style.display = 'none';
|
|
} );
|
|
}
|
|
|
|
var btnStart = document.getElementById('mlp-btn-start');
|
|
var btnPause = document.getElementById('mlp-btn-pause');
|
|
|
|
if ( btnStart ) {
|
|
btnStart.addEventListener( 'click', function(e) {
|
|
e.preventDefault();
|
|
running = true;
|
|
btnStart.style.display = 'none';
|
|
btnPause.style.display = '';
|
|
document.getElementById('mlp-status').textContent = '<?php echo esc_js( __( 'Running…', 'robotstxt-mlp2-mlp5' ) ); ?>';
|
|
runBatch();
|
|
} );
|
|
}
|
|
if ( btnPause ) {
|
|
btnPause.addEventListener( 'click', function(e) {
|
|
e.preventDefault();
|
|
running = false;
|
|
btnPause.style.display = 'none';
|
|
btnStart.style.display = '';
|
|
document.getElementById('mlp-status').textContent = '<?php echo esc_js( __( 'Paused.', 'robotstxt-mlp2-mlp5' ) ); ?>';
|
|
} );
|
|
}
|
|
})();
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Process a set of groups and log the results.
|
|
*
|
|
* @param array[] $groups Groups from Reader_Content_Relations::get_groups_batch().
|
|
*/
|
|
private static function process_groups( array $groups ): void {
|
|
foreach ( $groups as $group ) {
|
|
$type = $group['type'];
|
|
$members = $group['members'];
|
|
|
|
// Idempotency: skip if the first member is already in MLP5.
|
|
$first = $members[0];
|
|
if ( Robotstxt_Mlp2_Mlp5_Writer_Content_Relations::pair_exists_in_mlp5( $first['site_id'], $first['element_id'] ) ) {
|
|
Robotstxt_Mlp2_Mlp5_Logger::log(
|
|
'skipped',
|
|
'post_relation' === $type || 'post' === $type ? 'post_relation' : 'term_relation',
|
|
$first['site_id'],
|
|
$first['element_id'],
|
|
__( 'Relation already exists in MLP5 (idempotent skip).', 'robotstxt-mlp2-mlp5' )
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Validate members: skip those where site or content no longer exists.
|
|
$valid_members = array();
|
|
foreach ( $members as $m ) {
|
|
if ( ! get_site( $m['site_id'] ) ) {
|
|
continue;
|
|
}
|
|
switch_to_blog( $m['site_id'] );
|
|
if ( 'term' === $type ) {
|
|
$exists = (bool) get_term( $m['element_id'] );
|
|
} else {
|
|
$exists = (bool) get_post( $m['element_id'] );
|
|
}
|
|
restore_current_blog();
|
|
if ( $exists ) {
|
|
$valid_members[] = $m;
|
|
}
|
|
}
|
|
|
|
if ( count( $valid_members ) < 2 ) {
|
|
Robotstxt_Mlp2_Mlp5_Logger::log(
|
|
'skipped',
|
|
'term' === $type ? 'term_relation' : 'post_relation',
|
|
$first['site_id'],
|
|
$first['element_id'],
|
|
__( 'Fewer than 2 valid members after orphan check. Skipped.', 'robotstxt-mlp2-mlp5' )
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Write to MLP5.
|
|
$context = 'term' === $type ? 'term_relation' : 'post_relation';
|
|
$written = Robotstxt_Mlp2_Mlp5_Writer_Content_Relations::create_group( $type, $valid_members );
|
|
|
|
if ( $written ) {
|
|
Robotstxt_Mlp2_Mlp5_Logger::log( 'success', $context, $first['site_id'], $first['element_id'], sprintf( /* translators: %d: member count */ __( 'Group migrated with %d members.', 'robotstxt-mlp2-mlp5' ), count( $valid_members ) ) );
|
|
} else {
|
|
Robotstxt_Mlp2_Mlp5_Logger::log( 'error', $context, $first['site_id'], $first['element_id'], __( 'Failed to write group to MLP5.', 'robotstxt-mlp2-mlp5' ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a progress snapshot from a state array.
|
|
*
|
|
* @param array $state Step 4 state array.
|
|
* @return array
|
|
*/
|
|
private static function build_progress( array $state ): array {
|
|
$post_total = (int) ( $state['post_total'] ?? 0 );
|
|
$term_total = (int) ( $state['term_total'] ?? 0 );
|
|
$post_offset = (int) ( $state['post_offset'] ?? 0 );
|
|
$term_offset = (int) ( $state['term_offset'] ?? 0 );
|
|
$post_done = ! empty( $state['post_done'] );
|
|
$term_done = ! empty( $state['term_done'] );
|
|
|
|
$migrated = Robotstxt_Mlp2_Mlp5_Logger::count( array( 'type' => 'success' ) );
|
|
$skipped = Robotstxt_Mlp2_Mlp5_Logger::count( array( 'type' => 'skipped' ) );
|
|
$errors = Robotstxt_Mlp2_Mlp5_Logger::count( array( 'type' => 'error' ) );
|
|
|
|
// Subtract site_relation log entries (those belong to step 3).
|
|
$migrated -= Robotstxt_Mlp2_Mlp5_Logger::count(
|
|
array(
|
|
'type' => 'success',
|
|
'context' => 'site_relation',
|
|
)
|
|
);
|
|
$skipped -= Robotstxt_Mlp2_Mlp5_Logger::count(
|
|
array(
|
|
'type' => 'skipped',
|
|
'context' => 'site_relation',
|
|
)
|
|
);
|
|
$errors -= Robotstxt_Mlp2_Mlp5_Logger::count(
|
|
array(
|
|
'type' => 'error',
|
|
'context' => 'site_relation',
|
|
)
|
|
);
|
|
|
|
return array(
|
|
'total' => $post_total + $term_total,
|
|
'processed' => $post_offset + $term_offset,
|
|
'migrated' => max( 0, $migrated ),
|
|
'skipped' => max( 0, $skipped ),
|
|
'errors' => max( 0, $errors ),
|
|
'done' => $post_done && $term_done,
|
|
);
|
|
}
|
|
}
|