admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ailm_url_migrator_nonce' ),
'i18n' => array(
'analyzing' => __( 'Analyzing...', 'ailinkmaster-internal-auto-manager' ),
'running' => __( 'Migrating...', 'ailinkmaster-internal-auto-manager' ),
'confirm_run' => __( 'Run the URL migration? This will create 301 redirects and update the link database.', 'ailinkmaster-internal-auto-manager' ),
'confirm_rollback' => __( 'Roll back this migration batch? Redirects will be archived and content restored (unless edited).', 'ailinkmaster-internal-auto-manager' ),
'error' => __( 'Error: ', 'ailinkmaster-internal-auto-manager' ),
'no_change' => __( 'No permalink structure change detected.', 'ailinkmaster-internal-auto-manager' ),
'settings_saved' => __( 'Settings saved.', 'ailinkmaster-internal-auto-manager' ),
),
) );
}
/**
* Render the full tab UI.
*
* @return void
*/
public static function render_tab() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$snapshot = AILM_URL_Snapshot::get();
$changed = AILM_URL_Snapshot::detect_change();
$live_structure = (string) get_option( 'permalink_structure' );
$old_structure = $snapshot ? $snapshot['structure'] : '';
?>
⚠️
404 Scout module.', 'ailinkmaster-internal-auto-manager' ),
esc_url( admin_url( 'admin.php?page=ailinkmaster-internal-auto-manager&tab=scout' ) )
),
array( 'a' => array( 'href' => array(), 'target' => array() ) )
);
?>
Settings → Permalinks in WordPress. After saving there, return here — the change will be detected automatically and you can run a migration.', 'ailinkmaster-internal-auto-manager' ),
esc_url( admin_url( 'options-permalink.php' ) )
),
array( 'a' => array( 'href' => array() ) )
);
?>
(bool) get_option( 'ailm_url_migrator_rewrite_content', 0 ),
'process_terms' => (bool) get_option( 'ailm_url_migrator_process_terms', 1 ),
'redirect_type' => (int) get_option( 'ailm_url_migrator_redirect_type', 301 ),
);
$result = AILM_URL_Migrator_Engine::run_migration( $args );
wp_send_json_success( $result );
}
/**
* AJAX: get migration/rewrite progress AND process one batch.
*
* The browser polls this endpoint every 2s during a content rewrite. Each
* poll runs one batch (process_batch() self-guards against concurrent
* execution via the batch lock). Without this call the progress bar would
* stay at 0% forever — the rewrite only advances when a batch is processed.
*
* @return void
*/
public static function ajax_status() {
check_ajax_referer( 'ailm_url_migrator_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ailinkmaster-internal-auto-manager' ) );
}
// If a content rewrite is running, advance it by one batch.
// process_batch() acquires its own lock and is a no-op if already locked.
if ( AILM_URL_Content_Rewriter::is_running() ) {
AILM_URL_Content_Rewriter::process_batch();
}
$status = AILM_URL_Content_Rewriter::get_status();
wp_send_json_success( $status );
}
/**
* AJAX: list redirects (paginated).
*
* @return void
*/
public static function ajax_list() {
check_ajax_referer( 'ailm_url_migrator_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ailinkmaster-internal-auto-manager' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
$per_page = 20;
$offset = ( $page - 1 ) * $per_page;
$redirects = AILM_URL_Redirect_DB::get_redirects_paginated( $offset, $per_page, 'active' );
$total = AILM_URL_Redirect_DB::count_redirects( 'active' );
$rows = array();
foreach ( $redirects as $r ) {
$rows[] = array(
'id' => (int) $r->id,
'from_url' => $r->from_url,
'to_url' => $r->to_url,
'type' => (int) $r->redirect_type,
'hits' => (int) $r->hit_count,
'last_hit' => $r->last_hit,
'created_at' => $r->created_at,
);
}
wp_send_json_success( array(
'rows' => $rows,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil( $total / $per_page ),
) );
}
/**
* AJAX: list migration batches (for the rollback UI).
*
* Returns distinct migration batches from the backup/log table, newest first,
* each with its creation time and row count. This lets the user pick a batch
* to roll back instead of typing the ID manually.
*
* @return void
*/
public static function ajax_batches() {
check_ajax_referer( 'ailm_url_migrator_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ailinkmaster-internal-auto-manager' ) );
}
$batches = AILM_URL_Redirect_DB::get_batch_list();
$rows = array();
if ( is_array( $batches ) ) {
foreach ( $batches as $b ) {
$rows[] = array(
'batch_id' => $b->migration_batch,
'created_at' => $b->created_at,
'row_count' => (int) $b->row_count,
);
}
}
wp_send_json_success( array( 'batches' => $rows ) );
}
/**
* AJAX: roll back a migration batch.
*
* @return void
*/
public static function ajax_rollback() {
check_ajax_referer( 'ailm_url_migrator_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ailinkmaster-internal-auto-manager' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$batch_id = isset( $_POST['batch_id'] ) ? sanitize_text_field( wp_unslash( $_POST['batch_id'] ) ) : '';
if ( empty( $batch_id ) ) {
wp_send_json_error( __( 'Missing batch ID.', 'ailinkmaster-internal-auto-manager' ) );
}
$result = AILM_URL_Rollback::rollback( $batch_id );
wp_send_json_success( $result );
}
/**
* AJAX: save settings.
*
* @return void
*/
public static function ajax_settings() {
check_ajax_referer( 'ailm_url_migrator_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ailinkmaster-internal-auto-manager' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$rewrite_content = isset( $_POST['rewrite_content'] ) ? 1 : 0;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$process_postmeta = isset( $_POST['process_postmeta'] ) ? 1 : 0;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$process_terms = isset( $_POST['process_terms'] ) ? 1 : 0;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$redirect_type = isset( $_POST['redirect_type'] ) ? absint( $_POST['redirect_type'] ) : 301;
update_option( 'ailm_url_migrator_rewrite_content', $rewrite_content );
update_option( 'ailm_url_migrator_process_postmeta', $process_postmeta );
update_option( 'ailm_url_migrator_process_terms', $process_terms );
update_option( 'ailm_url_migrator_redirect_type', $redirect_type );
wp_send_json_success( array( 'message' => __( 'Settings saved.', 'ailinkmaster-internal-auto-manager' ) ) );
}
/**
* AJAX: delete a single redirect by ID.
*
* @return void
*/
public static function ajax_delete_redirect() {
check_ajax_referer( 'ailm_url_migrator_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ailinkmaster-internal-auto-manager' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$redirect_id = isset( $_POST['redirect_id'] ) ? absint( $_POST['redirect_id'] ) : 0;
if ( ! $redirect_id ) {
wp_send_json_error( __( 'Missing redirect ID.', 'ailinkmaster-internal-auto-manager' ) );
}
$deleted = AILM_URL_Redirect_DB::delete_redirect( $redirect_id );
if ( ! $deleted ) {
wp_send_json_error( __( 'Redirect not found or could not be deleted.', 'ailinkmaster-internal-auto-manager' ) );
}
wp_send_json_success( array( 'message' => __( 'Redirect deleted.', 'ailinkmaster-internal-auto-manager' ) ) );
}
}