File "AnalyticsAggregationTask.php"

Full Path: /home/buyiwexj/public_html/wp-content/plugins/wpforms-lite/src/Tasks/Actions/AnalyticsAggregationTask.php
File size: 5.71 KB
MIME-type: text/x-php
Charset: utf-8

<?php

namespace WPForms\Tasks\Actions;

use DateTimeImmutable;
use WPForms\Analytics\Aggregation;
use WPForms\Analytics\Analytics;
use WPForms\Db\Analytics\DB as AnalyticsDB;
use WPForms\Tasks\Task;
use WPForms\Tasks\Tasks; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement

/**
 * Nightly aggregation task for Form Analytics.
 *
 * Schedules itself recurring at site-local midnight + abandonment grace
 * (default 1h after midnight). Delegates work to the Loader-resolved
 * Analytics\Aggregation (Lite) or Pro\Analytics\Aggregation (Pro).
 *
 * @since 2.0.0
 */
class AnalyticsAggregationTask extends Task {

	/**
	 * Action Scheduler action name.
	 *
	 * @since 2.0.0
	 */
	public const ACTION = 'wpforms_analytics_aggregate';

	/**
	 * Option key storing the last-applied interval (seconds).
	 *
	 * Used by reconcile_schedule() to detect cadence drift between the
	 * filter's current return value and the cadence the recurring action
	 * was created with. Autoloaded so init() stays on the cheap path.
	 *
	 * @since 2.0.0
	 */
	public const INTERVAL_OPTION = 'wpforms_analytics_aggregation_interval';

	/**
	 * Interval in seconds. 0 cancels schedule; non-zero floored at DAY_IN_SECONDS.
	 *
	 * @since 2.0.0
	 *
	 * @var int
	 */
	private $interval;

	/**
	 * Tasks instance.
	 *
	 * @since 2.0.0
	 *
	 * @var Tasks|null
	 */
	private $tasks;

	/**
	 * Log title.
	 *
	 * @since 2.0.0
	 *
	 * @var string
	 */
	protected $log_title = 'Analytics Aggregation';

	/**
	 * Class constructor.
	 *
	 * @since 2.0.0
	 */
	public function __construct() {

		parent::__construct( self::ACTION );

		$this->init();
		$this->hooks();
	}

	/**
	 * Register the recurring schedule.
	 *
	 * @since 2.0.0
	 */
	private function init(): void {

		$this->tasks = wpforms()->obj( 'tasks' );

		if ( ! $this->tasks ) {
			return;
		}

		// Schedule add/remove is an admin/cron concern. Skip it on anonymous
		// front-end requests, where the schedule never needs adjusting — this
		// avoids an option read and the interval filter on every page view.
		if ( ! is_admin() && ! wp_doing_cron() ) {
			return;
		}

		if ( ! Analytics::is_enabled() ) {
			$this->remove_task();

			return;
		}

		// Don't schedule until the migration has created the analytics tables.
		if ( ! AnalyticsDB::tables_exist() ) {
			return;
		}

		/**
		 * Filter the aggregation task interval (seconds).
		 *
		 * Return 0 to cancel the schedule entirely. Non-zero values below
		 * DAY_IN_SECONDS are floored to DAY_IN_SECONDS to preserve the
		 * abandonment-grace correctness boundary.
		 *
		 * @since 2.0.0
		 *
		 * @param int $interval Interval in seconds. Default DAY_IN_SECONDS.
		 */
		$raw = (int) apply_filters( 'wpforms_tasks_actions_analytics_aggregation_task_init_interval', DAY_IN_SECONDS );

		$this->interval = $raw <= 0 ? 0 : max( DAY_IN_SECONDS, $raw );

		$this->reconcile_schedule();
	}

	/**
	 * Cancel any scheduled aggregation action and clear the recorded interval.
	 *
	 * Called from init() when the analytics kill switch is on so the documented
	 * "no scheduled task" contract holds even if analytics was disabled after a
	 * recurring action had already been registered.
	 *
	 * @since 2.0.0
	 */
	private function remove_task(): void {

		if ( $this->tasks->is_scheduled( self::ACTION ) !== false ) {
			$this->cancel();
		}

		delete_option( self::INTERVAL_OPTION );
	}

	/**
	 * Converge the scheduled action to the filtered desired interval.
	 *
	 * Self-healing reconciliation across plugin boots:
	 *  - Not scheduled + interval > 0 → schedule + record interval.
	 *  - Scheduled + interval == 0    → cancel + drop recorded interval.
	 *  - Scheduled + interval changed → cancel + reschedule + update record.
	 *  - Scheduled + interval same    → no-op (steady state).
	 *
	 * The recorded interval lives in an autoloaded option, so the
	 * steady-state path is one cached lookup with no DB write.
	 *
	 * @since 2.0.0
	 */
	private function reconcile_schedule(): void {

		$scheduled = $this->tasks->is_scheduled( self::ACTION ) !== false;

		// Cancellation requested.
		if ( $this->interval <= 0 ) {

			if ( $scheduled ) {
				$this->cancel();
				delete_option( self::INTERVAL_OPTION );
			}

			return;
		}

		// First-time scheduling — no existing recurring action.
		if ( ! $scheduled ) {

			$this->add_task();
			update_option( self::INTERVAL_OPTION, $this->interval );

			return;
		}

		// Already scheduled — re-arm only if cadence changed.
		if ( (int) get_option( self::INTERVAL_OPTION, 0 ) === $this->interval ) {
			return;
		}

		$this->cancel();
		$this->add_task();
		update_option( self::INTERVAL_OPTION, $this->interval );
	}

	/**
	 * Bind the recurring action to process().
	 *
	 * @since 2.0.0
	 */
	private function hooks(): void {

		add_action( self::ACTION, [ $this, 'process' ] );
	}

	/**
	 * Schedule the first run and recurring cadence.
	 *
	 * @since 2.0.0
	 */
	private function add_task(): void {

		if ( $this->interval <= 0 ) {
			return;
		}

		$this->tasks->create( self::ACTION )
			->recurring( $this->next_run_timestamp(), $this->interval )
			->params()
			->register();
	}

	/**
	 * Compute the first-run timestamp: next site-local midnight + grace.
	 *
	 * @since 2.0.0
	 *
	 * @return int Unix timestamp.
	 */
	private function next_run_timestamp(): int {

		$midnight = ( new DateTimeImmutable( 'tomorrow', wp_timezone() ) )->getTimestamp();

		return $midnight + Aggregation::ABANDONMENT_GRACE_SECONDS;
	}

	/**
	 * Recurring callback. Delegates to the resolved Aggregation instance.
	 *
	 * @since 2.0.0
	 */
	public function process(): void {

		$aggregator = wpforms()->obj( 'analytics_aggregation' );

		if ( ! $aggregator || ! method_exists( $aggregator, 'run' ) ) {
			return;
		}

		$aggregator->run();

		$this->log( 'Analytics aggregation completed.' );
	}
}