# suredonation/1.6.0/inc/import/charitable/session.php

SureDonation – Donation Forms, Fundraising Campaigns &amp; Donor Management, version 1.6.0. 249 lines.

- Page: https://pluginprobe.com/plugins/suredonation/1.6.0/code/inc/import/charitable/session.php
- Raw: https://pluginprobe.com/plugins/suredonation/1.6.0/raw/inc/import/charitable/session.php
- Modified: 2026-09-09T06:00:02+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/suredonation/1.6.0/code/inc/import/charitable/session.php#L10-L20`.

```php
<?php
/**
 * Migration session store.
 *
 * Wraps a transient with typed accessors so the rest of the importer never
 * touches transient keys directly. Each session is keyed by a UUID that the
 * REST layer hands back to the client and expects back on subsequent /batch
 * calls.
 *
 * @package SureDonation
 */

namespace SureDonation\Inc\Import\Charitable;

use SureDonation\Inc\Traits\Get_Instance;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * Session class.
 *
 * @phpstan-type PhaseResults array{imported: int, skipped: int, errors: int, error_log: array<int, array{source_id: int, message: string}>, gateway_breakdown?: array<string, int>, imported_live?: int, imported_historical?: int}
 * @phpstan-type ImportProgress array{import_id: string, started_at: string, started_by: int, options: array{campaign_ids: array<int, int>, include_standalone_donors: bool}, phases: array<int, string>, current_phase: int, offset: int, campaign_map: array<int, int>, donor_map: array<string, int>, status: string, results: array<string, PhaseResults>, error?: string, completed_at?: string}
 *
 * @since 1.0.0
 */
class Session {
	use Get_Instance;

	/**
	 * Transient key prefix.
	 */
	public const TRANSIENT_PREFIX = 'suredonation_charitable_import_';

	/**
	 * Session TTL.
	 *
	 * Long enough to survive batch gaps and brief page reloads, short enough
	 * that abandoned sessions don't pile up indefinitely.
	 */
	public const TTL = HOUR_IN_SECONDS;

	/**
	 * Create a new import session and return its progress payload.
	 *
	 * Options shape:
	 *   - campaign_ids: int[] — Charitable form IDs selected in the preview UI.
	 *   - include_standalone_donors: bool — whether to also import donors
	 *     that have no associated payments (Pro phase, off by default).
	 *
	 * Campaigns + donations phases are always registered (the user picked
	 * at least one campaign on the preview step). Subscriptions is injected
	 * by the Pro plugin via the phases filter when recurring_donation plans
	 * exist. Standalone donors is opt-in via the option above.
	 *
	 * @param  array<string, mixed> $options Session options.
	 * @return array<string, mixed> Newly created progress payload (including generated import_id) or empty array if no phases are selected.
	 * @since  1.0.0
	 */
	public function create( $options ) {
		$options = is_array( $options ) ? $options : [];

		$defaults = [
			'campaign_ids'              => [],
			'include_standalone_donors' => false,
		];

		$options = wp_parse_args( $options, $defaults );

		$options['campaign_ids']              = is_array( $options['campaign_ids'] )
			? array_values( array_unique( array_filter( array_map( 'absint', $options['campaign_ids'] ) ) ) )
			: [];
		$options['include_standalone_donors'] = (bool) $options['include_standalone_donors'];

		if ( empty( $options['campaign_ids'] ) ) {
			return [];
		}

		// Campaigns + donations always run; the user opted in to those by
		// picking campaigns on the preview step. Pro injects subscriptions
		// and (conditionally) standalone_donors via the filter below.
		$phases = [ 'campaigns', 'donations' ];

		/**
		 * Filter the phases that a Charitable migration session will run.
		 *
		 * Pro reads $options['include_standalone_donors'] when deciding
		 * whether to inject the standalone_donors phase. The subscriptions
		 * phase is injected unconditionally when recurring_donation plans
		 * exist — it's still campaign-scoped because the mapper
		 * filters by $progress['options']['campaign_ids'].
		 *
		 * @param array $phases  Default phases (campaigns + donations).
		 * @param array $options Resolved options for this session.
		 * @since 1.0.0
		 */
		$phases = apply_filters( 'suredonation_import_charitable_phases', $phases, $options );
		$phases = is_array( $phases ) ? array_values( array_unique( array_filter( array_map( 'strval', $phases ) ) ) ) : [];

		if ( empty( $phases ) ) {
			return [];
		}

		$progress = [
			'import_id'     => wp_generate_uuid4(),
			'started_at'    => current_time( 'mysql', true ),
			'started_by'    => get_current_user_id(),
			'options'       => $options,
			'phases'        => $phases,
			'current_phase' => 0,
			'offset'        => 0,
			'campaign_map'  => [],
			'donor_map'     => [],
			'status'        => 'running',
			'results'       => $this->empty_results( $phases ),
		];

		$this->put( $progress['import_id'], $progress );

		/**
		 * Fires after a new Charitable migration session has been created.
		 *
		 * @param string $import_id Session UUID.
		 * @param array  $progress  Full progress payload.
		 * @since 1.0.0
		 */
		do_action( 'suredonation_import_charitable_session_created', $progress['import_id'], $progress );

		return $progress;
	}

	/**
	 * Load progress for an existing import session.
	 *
	 * @param  string $import_id Session UUID.
	 * @return array<string, mixed>|false Progress payload, or false if the transient is gone.
	 * @since  1.0.0
	 */
	public function get( $import_id ) {
		$import_id = $this->sanitize_id( $import_id );
		if ( '' === $import_id ) {
			return false;
		}

		$progress = get_transient( self::TRANSIENT_PREFIX . $import_id );
		return is_array( $progress ) ? $progress : false;
	}

	/**
	 * Persist updated progress to its transient.
	 *
	 * @param  string               $import_id Session UUID.
	 * @param  array<string, mixed> $progress  Progress payload.
	 * @return bool
	 * @since  1.0.0
	 */
	public function put( $import_id, $progress ) {
		$import_id = $this->sanitize_id( $import_id );
		if ( '' === $import_id || ! is_array( $progress ) ) {
			return false;
		}

		return (bool) set_transient( self::TRANSIENT_PREFIX . $import_id, $progress, self::TTL );
	}

	/**
	 * Delete a session.
	 *
	 * @param  string $import_id Session UUID.
	 * @return bool
	 * @since  1.0.0
	 */
	public function delete( $import_id ) {
		$import_id = $this->sanitize_id( $import_id );
		if ( '' === $import_id ) {
			return false;
		}

		return (bool) delete_transient( self::TRANSIENT_PREFIX . $import_id );
	}

	/**
	 * Build a zeroed results structure for the given phases.
	 *
	 * @param  array<int, string> $phases Phase names.
	 * @return array<string, array<string, mixed>>
	 * @since  1.0.0
	 */
	private function empty_results( $phases ) {
		$schema = [
			'campaigns'         => [
				'imported'  => 0,
				'skipped'   => 0,
				'errors'    => 0,
				'error_log' => [],
			],
			'donations'         => [
				'imported'          => 0,
				'skipped'           => 0,
				'errors'            => 0,
				'error_log'         => [],
				'gateway_breakdown' => [],
			],
			'subscriptions'     => [
				'imported_live'       => 0,
				'imported_historical' => 0,
				'skipped'             => 0,
				'errors'              => 0,
				'error_log'           => [],
			],
			'standalone_donors' => [
				'imported'  => 0,
				'skipped'   => 0,
				'errors'    => 0,
				'error_log' => [],
			],
		];

		$out = [];
		foreach ( $phases as $phase ) {
			if ( isset( $schema[ $phase ] ) ) {
				$out[ $phase ] = $schema[ $phase ];
			}
		}
		return $out;
	}

	/**
	 * Sanitize an incoming import-id string.
	 *
	 * @param  mixed $import_id Raw input.
	 * @return string Empty string if not a plausible UUID.
	 * @since  1.0.0
	 */
	private function sanitize_id( $import_id ) {
		if ( ! is_string( $import_id ) ) {
			return '';
		}
		$import_id = sanitize_text_field( $import_id );

		if ( ! preg_match( '/^[a-f0-9-]{8,64}$/i', $import_id ) ) {
			return '';
		}

		return $import_id;
	}
}

```
