'404ops-activity-log', 'tab' => 'import', self::RESULT_ARG => $result, ), admin_url( 'admin.php' ) ) ); exit; } /** * Start or cancel a run. * * @param array $request The posted fields, unslashed. * @return string A RESULTS code. */ public static function perform( array $request ): string { $op = isset( $request['op'] ) && is_string( $request['op'] ) ? sanitize_key( $request['op'] ) : ''; $key = isset( $request['source'] ) && is_string( $request['source'] ) ? sanitize_key( $request['source'] ) : ''; $nonce = isset( $request['_wpnonce'] ) && is_string( $request['_wpnonce'] ) ? sanitize_text_field( $request['_wpnonce'] ) : ''; $source = Importer::source( $key ); if ( ! in_array( $op, array( self::OP_START, self::OP_CANCEL ), true ) || null === $source ) { return 'bad_request'; } if ( ! wp_verify_nonce( $nonce, self::nonce_action( $op, $key ) ) ) { return 'bad_nonce'; } if ( ! current_user_can( Importer::capability( $key ) ) ) { return 'forbidden'; } if ( self::OP_CANCEL === $op ) { wp_clear_scheduled_hook( self::CRON_HOOK, array( $key ) ); return Importer::cancel( $key ) ? 'cancelled' : 'not_running'; } $result = Importer::start( $source, get_current_user_id() ); if ( 'started' === $result ) { self::schedule( $key ); } return $result; } /** * AJAX handler for the tab's progress loop. */ public static function handle_step(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- step() verifies the nonce first thing. $response = self::step( wp_unslash( $_POST ) ); if ( isset( $response['error'] ) ) { wp_send_json_error( $response, 'forbidden' === $response['error'] ? 403 : 400 ); } wp_send_json_success( $response ); } /** * Run one step of a source's import and report where it stands. * * @param array $request The posted fields, unslashed. * @return array Progress, or `error` with a code. */ public static function step( array $request ): array { $key = isset( $request['source'] ) && is_string( $request['source'] ) ? sanitize_key( $request['source'] ) : ''; $nonce = isset( $request['_wpnonce'] ) && is_string( $request['_wpnonce'] ) ? sanitize_text_field( $request['_wpnonce'] ) : ''; if ( null === Importer::source( $key ) ) { return array( 'error' => 'bad_request' ); } if ( ! wp_verify_nonce( $nonce, self::nonce_action( 'step', $key ) ) ) { return array( 'error' => 'bad_nonce' ); } if ( ! current_user_can( Importer::capability( $key ) ) ) { return array( 'error' => 'forbidden' ); } return self::progress( Importer::run( $key, self::STEP_SECONDS ) ); } /** * WP-Cron handler: run for a while, and come back while there is more. * * @param mixed $key Source key, the event's one argument. */ public static function cron( $key ): void { $key = is_string( $key ) ? $key : ''; if ( null === Importer::source( $key ) ) { return; } $state = Importer::run( $key, self::CRON_SECONDS ); if ( Importer::RUNNING === $state['status'] ) { self::schedule( $key ); } } /** * @param string $key Source key. */ private static function schedule( string $key ): void { if ( ! wp_next_scheduled( self::CRON_HOOK, array( $key ) ) ) { wp_schedule_single_event( time(), self::CRON_HOOK, array( $key ) ); } } /** * A run's state as the progress bar reads it. * * @param array $state Importer::state(). * @return array */ public static function progress( array $state ): array { $total = (int) $state['total']; $processed = min( $total, (int) $state['processed'] ); return array( 'status' => (string) $state['status'], 'total' => $total, 'processed' => $processed, 'imported' => (int) $state['imported'], 'generic' => (int) $state['generic'], 'skipped' => array_map( 'intval', (array) $state['skipped'] ), 'percent' => $total > 0 ? (int) floor( 100 * $processed / $total ) : 100, ); } }