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' ) ) ); } }