HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/globfdxw/www/wp-content/plugins/wpforms-entry-automation/src/Tasks/ProcessActionTask.php
<?php

namespace WPFormsEntryAutomation\Tasks;

use WPForms\Tasks\Task;
use WPForms\Tasks\Meta;
use WPFormsEntryAutomation\Plugin;

/**
 * Class ProcessActionTask.
 *
 * @since 1.0.0
 */
class ProcessActionTask extends Task {

	/**
	 * Task action.
	 *
	 * @since 1.0.0
	 */
	public const ACTION = 'wpforms_entry_automation_process_action_task';

	/**
	 * Task to create tasks.
	 *
	 * @since 1.0.0
	 */
	public const CREATE_TASKS_ACTION = 'wpforms_entry_automation_action_create_tasks';

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		parent::__construct( self::ACTION );
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.0.0
	 */
	public function hooks(): void {

		// Register the task to recreate all entry automation tasks only after ActionScheduler is usable.
		add_action( 'action_scheduler/migration_complete', [ $this, 'add_recreate_task' ] );

		add_action( self::ACTION, [ $this, 'process' ] );
		add_action( self::CREATE_TASKS_ACTION, [ $this, 'create_tasks' ] );
		add_filter( 'wpforms_entry_handler_get_entries_where', [ $this, 'set_correct_dates' ], 10, 2 );
	}

	/**
	 * Process the task.
	 *
	 * @since 1.0.0
	 *
	 * @param int $meta_id Task meta ID.
	 */
	public function process( int $meta_id ): void {

		// Retrieve AS task meta data.
		$task_data = $this->get_task_meta_data( $meta_id );

		// Check if the task is allowed to run based on the schedule.
		if ( ! $this->process_allowed( $task_data ) ) {
			return;
		}

		$task_id = $task_data['task_id'] ?? 0;

		// Get the Entry Automation task from custom table.
		$task = wpforms_entry_automation()->get( 'tasks' )->get_by_id( $task_id );

		if ( ! $this->is_active( $task ) ) {
			$this->log_inactive( $task );

			return;
		}

		$form_id       = $task['form_id'] ?? 0;
		$connection_id = $task['connection_id'] ?? 0;

		// Get the form data.
		$form_data = wpforms()->obj( 'form' )->get( $form_id, [ 'content_only' => true ] );

		// Get the entry automation task from the form data.
		$connection = $this->get_connection( $connection_id, $form_data );

		// Process the action.
		$this->process_action( $form_id, $task_id, $connection, $form_data );
	}

	/**
	 * Log inactive task.
	 *
	 * @since 1.0.0
	 *
	 * @param array $task Task data.
	 */
	private function log_inactive( array $task ): void {

		wpforms_log(
			'Entry Automation Task: Process stopped',
			[
				'form_id'       => $task['form_id'] ?? 0,
				'connection_id' => $task['connection_id'] ?? 0,
				'task_id'       => $task['id'] ?? 0,
				'action_id'     => $task['action_id'] ?? 0,
				'status'        => $task['status'] ?? 'unknown',
			],
			[ 'type' => 'log' ]
		);
	}

	/**
	 * Check if the task is active.
	 *
	 * @since 1.0.0
	 *
	 * @param array $task Task data.
	 *
	 * @return bool True if the task is active or failed, false otherwise.
	 */
	private function is_active( $task ): bool {

		if ( empty( $task ) ) {
			return false;
		}

		// Allow the task to run if it is active or failed.
		return in_array( $task['status'], [ 'active', 'failed' ], true );
	}

	/**
	 * Get task meta data.
	 *
	 * @since 1.0.0
	 *
	 * @param int $meta_id Task meta ID.
	 *
	 * @return array Task meta data.
	 */
	private function get_task_meta_data( int $meta_id ): array {

		$task_meta = new Meta();
		$meta      = $task_meta->get( $meta_id );

		// We should actually receive something.
		if ( empty( $meta ) || empty( $meta->data ) || ! is_array( $meta->data ) ) {
			return [];
		}

		return $meta->data[0] ?? [];
	}

	/**
	 * Get the entry automation task from the form data.
	 *
	 * @since 1.0.0
	 *
	 * @param string $connection_id Connection ID.
	 * @param array  $form_data     Form data.
	 *
	 * @return array Entry automation task.
	 */
	private function get_connection( string $connection_id, array $form_data ): array {

		$entry_automation = $form_data['settings'][ Plugin::SLUG ] ?? [];

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

		$connection = $entry_automation[ $connection_id ] ?? [];

		$status = $connection['status'] ?? '1';

		// Stop if the task is not active.
		if ( ! (int) $status ) {
			return [];
		}

		return $connection;
	}

	/**
	 * Process the action.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $form_id    Form ID.
	 * @param int   $task_id    Task ID.
	 * @param array $connection Connection data.
	 * @param array $form_data  Form data.
	 */
	private function process_action( int $form_id, int $task_id, array $connection, array $form_data ): void {

		if ( empty( $connection ) ) {
			return;
		}

		$action       = $connection['action'] ?? 'export';
		$task_manager = wpforms_entry_automation()->get( 'tasks' );

		try {
			$result = $this->execute( $form_id, $task_id, $action, $connection, $form_data );
		} catch ( \Throwable $e ) {
			wpforms_log(
				'Entry Automation Task: Execution error',
				[
					'form_id' => $form_id,
					'task_id' => $task_id,
					'error'   => $e->getMessage(),
				],
				[ 'type' => 'error' ]
			);

			$result = false;
		}

		// If the task was successful, update the status to active and process related tasks.
		if ( $result ) {
			$task_manager->update_status( $task_id, 'active' );

			// Get related tasks.
			$related_tasks = $task_manager->get_all(
				[
					'form_id'   => $form_id,
					'parent_id' => $connection['id'],
				]
			);

			foreach ( $related_tasks as $related_task ) {
				// Retrieve the related task data from the form data.
				$related_connection = $this->get_connection( $related_task['connection_id'], $form_data );

				$this->process_action(
					$form_id,
					$related_task['id'],
					$related_connection,
					$form_data
				);
			}
		} else {
			$task_manager->update_status( $task_id, 'failed' );
		}

		// Update the last run time.
		$task_manager->update_last_run( $task_id );
	}

	/**
	 * Execute the task based on the action.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $form_id    Form ID.
	 * @param int    $task_id    Task ID.
	 * @param string $action     Action type (export or delete).
	 * @param array  $connection Connection data.
	 * @param array  $form_data  Form data.
	 *
	 * @return bool
	 */
	private function execute( int $form_id, int $task_id, string $action, array $connection, array $form_data ): bool {

		$filters = $connection['filters'] ?? [];

		$task = wpforms_entry_automation()->get( 'tasks' )->get_by_id( $task_id );

		if ( empty( $task ) || ! $this->is_active( $task ) ) {
			return false;
		}

		$args = $this->prepare_args( $form_id, $filters, $connection, $task );

		wpforms_entry_automation()->get( 'tasks' )->update_status( $task_id, 'running' );

		if ( $action === 'export' ) {
			$connection['task_id'] = $task_id;

			return ( new ExportTask() )->execute( $connection, $form_data, $args, $task );
		}

		if ( $action === 'delete' ) {
			return ( new DeleteTask() )->execute( $args );
		}

		return false;
	}

	/**
	 * Prepare arguments for the export.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $form_id    Form ID.
	 * @param array $filters    Filters.
	 * @param array $connection Connection data.
	 * @param array $task       Task data.
	 *
	 * @return array Arguments.
	 */
	private function prepare_args( int $form_id, array $filters, array $connection, array $task ): array {

		$statuses = $filters['statuses'] ?? [];

		// Replace `published` with empty string.
		$statuses = array_map(
			function ( $status ) {
				return $status === 'published' ? '' : $status;
			},
			$statuses
		);

		$args = [
			'form_id'         => $form_id,
			'entry_id'        => 0,
			'additional_info' => $connection['additional_field'] ?? [],
			'status'          => $statuses,
			'dates'           => $this->get_dates( $connection, $task ),
			'fields'          => $connection['form_field'] ?? [],
			'search'          => [ 'term' => '' ],
			'number'          => -1,
		];

		if ( $filters['value'] !== '' ) {
			$args['search']['term']       = $filters['value'];
			$args['search']['comparison'] = $filters['operator'];
			$args['search']['field']      = $filters['field'];
		}

		return $args;
	}

	/**
	 * Get the dates for the task.
	 *
	 * @since 1.0.0
	 *
	 * @param array $connection            Connection data.
	 * @param array $entry_automation_task Entry automation task.
	 *
	 * @return array Dates.
	 */
	private function get_dates( array $connection, array $entry_automation_task ): array {

		$entries_number = $connection['entries_number'] ?? false;

		if ( $entries_number === 'new' && ! empty( $entry_automation_task['last_run'] ) ) {
			return [
				$entry_automation_task['last_run'],
				gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), // Any date in the future.
				/**
				 * Additional parameter to prevent the date from being rounded to the start of the day.
				 * Default method check if the date array contains only two elements.
				 */
				'period' => false,
			];
		}

		return [];
	}

	/**
	 * Set the correct dates for the query.
	 * Default request using `wpforms_get_day_period_date` function.
	 * This means that the date round to the start of the day.
	 * We need to set the correct date range for the query.
	 *
	 * @since 1.0.0
	 *
	 * @param array $where Query where clause.
	 * @param array $args  Query arguments.
	 *
	 * @return array Modified where clause.
	 */
	public function set_correct_dates( $where, $args ): array {

		if ( empty( $args['date'] ) ) {
			return $where;
		}

		if ( is_array( $args['date'] ) && count( $args['date'] ) === 3 && isset( $args['date']['period'] ) && $args['date']['period'] === false ) {
			$table_name = wpforms()->obj( 'entry' )->table_name;

			$date_start = gmdate( 'Y-m-d H:i:s', strtotime( $args['date'][0] ) );
			$date_end   = gmdate( 'Y-m-d H:i:s', strtotime( $args['date'][1] ) );

			if ( ! empty( $date_start ) && ! empty( $date_end ) ) {
				$where['arg_date_start'] = "$table_name.date >= '$date_start'";
				$where['arg_date_end']   = "$table_name.date <= '$date_end'";
			}
		}

		return $where;
	}

	/**
	 * Check if the task is allowed to run based on the schedule.
	 *
	 * @since 1.0.0
	 *
	 * @param array $data Task data.
	 *
	 * @return bool
	 */
	private function process_allowed( array $data ): bool {

		if ( empty( $data ) ) {
			return false;
		}

		$current_date = strtotime( gmdate( 'Y-m-d' ) );

		if ( ! $this->is_in_date_range( $data, $current_date ) ) {
			return false;
		}

		$frequency = $data['frequency'] ?? '';
		$days      = $data['days'] ?? [];

		return $this->check_frequency_condition( $frequency, $days );
	}

	/**
	 * Check if current date is within the configured date range.
	 *
	 * @since 1.0.0
	 *
	 * @param array $data         Task data.
	 * @param int   $current_date Current date timestamp.
	 *
	 * @return bool
	 */
	private function is_in_date_range( array $data, int $current_date ): bool {

		if ( ! $this->is_after_start_date( $data['start'] ?? '', $current_date ) ) {
			return false;
		}

		if ( ! $this->is_before_end_date( $data['end'] ?? '', $current_date ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if current date is after the start date.
	 *
	 * @since 1.0.0
	 *
	 * @param string $start_date   Start date string.
	 * @param int    $current_date Current date timestamp.
	 *
	 * @return bool
	 */
	private function is_after_start_date( string $start_date, int $current_date ): bool {

		if ( empty( $start_date ) ) {
			return true;
		}

		$start_timestamp = strtotime( $start_date );

		return $start_timestamp <= $current_date;
	}

	/**
	 * Check if current date is before the end date.
	 *
	 * @since 1.0.0
	 *
	 * @param string $end_date     End date string.
	 * @param int    $current_date Current date timestamp.
	 *
	 * @return bool
	 */
	private function is_before_end_date( string $end_date, int $current_date ): bool {

		if ( empty( $end_date ) ) {
			return true;
		}

		$end_timestamp = strtotime( $end_date );

		return $end_timestamp >= $current_date;
	}

	/**
	 * Check if the current day matches the frequency condition.
	 *
	 * @since 1.0.0
	 *
	 * @param string $frequency Frequency setting (week, month, first, last).
	 * @param array  $days      Days configuration.
	 *
	 * @return bool
	 */
	private function check_frequency_condition( string $frequency, array $days ): bool {

		switch ( $frequency ) {
			case 'week':
				$condition = $this->is_matching_weekday( $days );
				break;

			case 'month':
				$condition = $this->is_matching_day_of_month( $days );
				break;

			case 'first':
				$condition = $this->is_first_day_of_month();
				break;

			case 'last':
				$condition = $this->is_last_day_of_month();
				break;

			default:
				$condition = false;
		}

		return $condition;
	}

	/**
	 * Check if current day is in the list of allowed weekdays.
	 *
	 * @since 1.0.0
	 *
	 * @param array $days List of allowed weekdays.
	 *
	 * @return bool
	 */
	private function is_matching_weekday( array $days ): bool {

		$current_week_day = strtolower( gmdate( 'D' ) );

		return in_array( $current_week_day, $days, true );
	}

	/**
	 * Check if current day is in the list of allowed days of month.
	 *
	 * @since 1.0.0
	 *
	 * @param array $days List of allowed days of month.
	 *
	 * @return bool
	 */
	private function is_matching_day_of_month( array $days ): bool {

		$current_day = strtolower( gmdate( 'd' ) );

		return in_array( (int) $current_day, $days, true );
	}

	/**
	 * Check if today is the first day of the month.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	private function is_first_day_of_month(): bool {

		$today              = gmdate( 'Y-m-d' );
		$first_day_of_month = gmdate( 'Y-m-01' );

		return $today === $first_day_of_month;
	}

	/**
	 * Check if today is the last day of the month.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	private function is_last_day_of_month(): bool {

		$today             = gmdate( 'Y-m-d' );
		$last_day_of_month = gmdate( 'Y-m-t' );

		return $today === $last_day_of_month;
	}

	/**
	 * Add a task to the queue.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $task_id  Task ID.
	 * @param array $schedule Task schedule.
	 */
	public function add( $task_id, $schedule ): void {

		$params = array_merge( $schedule, [ 'task_id' => $task_id ] );

		$time      = $schedule['time'];
		$timestamp = date_create( 'today ' . $time, wp_timezone() )->format( 'U' );

		// If the time has already passed today, schedule for tomorrow.
		if ( $timestamp < time() ) {
			$timestamp = date_create( 'tomorrow ' . $time, wp_timezone() )->format( 'U' );
		}

		$action_id = wpforms()->obj( 'tasks' )
			->create( self::ACTION )
			->recurring( $timestamp, DAY_IN_SECONDS )
			->params( $params )
			->register();

		wpforms_entry_automation()->get( 'tasks' )->update( $task_id, [ 'action_id' => $action_id ] );
	}

	/**
	 * Create tasks for all entry automation tasks.
	 *
	 * @since 1.0.0
	 */
	public function create_tasks(): void {

		$tasks = wpforms_entry_automation()->get( 'tasks' )->get_all_parents();

		if ( empty( $tasks ) ) {
			return;
		}

		foreach ( $tasks as $task ) {
			$task_id = (int) ( $task['id'] ?? 0 );

			if ( empty( $task_id ) ) {
				continue;
			}

			$schedule = json_decode( $task['schedule'], true );

			if ( empty( $schedule ) ) {
				continue;
			}

			$this->add( $task_id, $schedule );
		}
	}

	/**
	 * Schedule a one-time task to recreate all entry automation tasks.
	 *
	 * @since 1.0.0
	 */
	public function add_recreate_task(): void {

		wpforms()->obj( 'tasks' )
			->create( self::CREATE_TASKS_ACTION )
			->once( time() )
			->register();
	}

	/**
	 * Unschedule a task.
	 *
	 * @since 1.0.0
	 *
	 * @param int $action_id Action ID.
	 */
	public function unschedule( $action_id ): void {

		$action = wpforms()->obj( 'tasks' )->fetch_action( $action_id );

		if ( $action === null ) {
			return;
		}

		as_unschedule_action( self::ACTION, $action->get_args() );
	}
}