<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName

if ( ! defined( 'ABSPATH' ) ) {
	exit( 0 );
}

/**
 * Cron endpoint class.
 *
 * GET /sites/%s/cron
 *
 * @phan-constructor-used-for-side-effects
 */
class Jetpack_JSON_API_Cron_Endpoint extends Jetpack_JSON_API_Endpoint {

	/**
	 * Needed capabilities.
	 *
	 * @var string
	 */
	protected $needed_capabilities = 'manage_options';

	/**
	 * Validate the call.
	 *
	 * @param int   $_blog_id - the blog ID.
	 * @param array $capability - the capabilities of the user.
	 * @param bool  $check_manage_active - parameter is for making the method signature compatible with its parent class method.
	 */
	protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return parent::validate_call( $_blog_id, $capability, false );
	}

	/**
	 * Return the result of current timestamp.
	 */
	protected function result() {
		return array(
			'cron_array'        => _get_cron_array(),
			'current_timestamp' => time(),
		);
	}

	/**
	 * Sanitize the hook.
	 *
	 * @param string $hook - the hook.
	 *
	 * @return string
	 */
	protected function sanitize_hook( $hook ) {
		return preg_replace( '/[^A-Za-z0-9-_]/', '', $hook );
	}

	/**
	 * Resolve arguments.
	 *
	 * @return array
	 */
	protected function resolve_arguments() {
		$args = $this->input();
		return isset( $args['arguments'] ) ? json_decode( $args['arguments'] ) : array();
	}

	/**
	 * Check the cron lock.
	 *
	 * @param float $gmt_time - the time in GMT.
	 *
	 * @return string|int|WP_Error WP_Error if cron was locked in the `WP_CRON_LOCK_TIMEOUT` seconds before `gmt_time`, int or string otherwise.
	 */
	protected function is_cron_locked( $gmt_time ) {
		// The cron lock: a unix timestamp from when the cron was spawned.
		$doing_cron_transient = $this->get_cron_lock();
		if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) ) {
			return new WP_Error( 'cron-is-locked', 'Current there is a cron already happening.', 403 );
		}
		return $doing_cron_transient;
	}

	/**
	 * Check if we can unlock the cron transient.
	 *
	 * @param string $doing_wp_cron - if we're doing the wp_cron.
	 */
	protected function maybe_unlock_cron( $doing_wp_cron ) {
		if ( $this->get_cron_lock() === $doing_wp_cron ) {
			delete_transient( 'doing_cron' );
		}
	}

	/**
	 * Set the cron lock.
	 *
	 * @return string
	 */
	protected function lock_cron() {
		$lock = sprintf( '%.22F', microtime( true ) );
		set_transient( 'doing_cron', $lock );
		return $lock;
	}

	/**
	 * Get scheduled.
	 *
	 * @param string $hook - the hook.
	 * @param array  $args - the arguments.
	 *
	 * @return array
	 */
	protected function get_schedules( $hook, $args ) {
		$crons = _get_cron_array();
		$key   = md5( serialize( $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
		if ( empty( $crons ) ) {
			return array();
		}
		$found = array();
		foreach ( $crons as $timestamp => $cron ) {
			if ( isset( $cron[ $hook ][ $key ] ) ) {
				$found[] = $timestamp;
			}
		}

		return $found;
	}

	/**
	 * This function is based on the one found in wp-cron.php with a similar name
	 *
	 * @return int
	 */
	protected function get_cron_lock() {
		global $wpdb;

		$value = 0;
		if ( wp_using_ext_object_cache() ) {
			/*
			 * Skip local cache and force re-fetch of doing_cron transient
			 * in case another process updated the cache.
			 */
			$value = wp_cache_get( 'doing_cron', 'transient', true );
		} else {
			$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) );
			if ( is_object( $row ) ) {
				$value = $row->option_value;
			}
		}
		return $value;
	}
}

/**
 * Cron post endpoint class.
 *
 * POST /sites/%s/cron
 *
 * @phan-constructor-used-for-side-effects
 */
class Jetpack_JSON_API_Cron_Post_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace

	/**
	 * The result.
	 *
	 * @return array|WP_Error
	 */
	protected function result() {
		define( 'DOING_CRON', true );
		set_time_limit( 0 );
		$args  = $this->input();
		$crons = _get_cron_array();
		if ( false === $crons ) {
			return new WP_Error( 'no-cron-event', 'Currently there are no cron events', 400 );
		}

		$timestamps_to_run = array_keys( $crons );
		$gmt_time          = microtime( true );

		if ( isset( $timestamps_to_run[0] ) && $timestamps_to_run[0] > $gmt_time ) {
			return new WP_Error( 'no-cron-event', 'Currently there are no cron events ready to be run', 400 );
		}

		$locked = $this->is_cron_locked( $gmt_time );
		if ( is_wp_error( $locked ) ) {
			return $locked;
		}

		$lock             = $this->lock_cron();
		$processed_events = array();

		foreach ( $crons as $timestamp => $cronhooks ) {
			if ( $timestamp > $gmt_time && ! isset( $args['hook'] ) ) {
				break;
			}

			foreach ( $cronhooks as $hook => $hook_data ) {
				if ( isset( $args['hook'] ) && ! in_array( $hook, $args['hook'], true ) ) {
					continue;
				}

				foreach ( $hook_data as $hook_item ) {

					$schedule  = $hook_item['schedule'];
					$arguments = $hook_item['args'];

					if ( ! $schedule ) {
						wp_reschedule_event( $timestamp, $schedule, $hook, $arguments );
					}

					wp_unschedule_event( $timestamp, $hook, $arguments );

					do_action_ref_array( $hook, $arguments );
					$processed_events[] = array( $hook => $arguments );

					// If the hook ran too long and another cron process stole the lock,
					// or if we things are taking longer then 20 seconds then quit.
					if ( ( $this->get_cron_lock() !== $lock ) || ( $gmt_time + 20 > microtime( true ) ) ) {
						$this->maybe_unlock_cron( $lock );
						return array( 'success' => $processed_events );
					}
				}
			}
		}

		$this->maybe_unlock_cron( $lock );
		return array( 'success' => $processed_events );
	}
}

/**
 * Schedule endpoint class.
 *
 * POST /sites/%s/cron/schedule
 *
 * @phan-constructor-used-for-side-effects
 */
class Jetpack_JSON_API_Cron_Schedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace

	/**
	 * The result.
	 *
	 * @return array|WP_Error
	 */
	protected function result() {
		$args = $this->input();
		if ( ! isset( $args['timestamp'] ) ) {
			return new WP_Error( 'missing_argument', 'Please provide the timestamp argument', 400 );
		}

		if ( ! is_int( $args['timestamp'] ) || $args['timestamp'] < time() ) {
			return new WP_Error( 'timestamp-invalid', 'Please provide timestamp that is an integer and set in the future', 400 );
		}

		if ( ! isset( $args['hook'] ) ) {
			return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 );
		}

		$hook = $this->sanitize_hook( $args['hook'] );

		$locked = $this->is_cron_locked( microtime( true ) );
		if ( is_wp_error( $locked ) ) {
			return $locked;
		}

		$arguments      = $this->resolve_arguments();
		$next_scheduled = $this->get_schedules( $hook, $arguments );

		if ( isset( $args['recurrence'] ) ) {
			$schedules = wp_get_schedules();
			if ( ! isset( $schedules[ $args['recurrence'] ] ) ) {
				return new WP_Error( 'invalid-recurrence', 'Please provide a valid recurrence argument', 400 );
			}

			if ( is_countable( $next_scheduled ) && count( $next_scheduled ) > 0 ) {
				return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 );
			}
			$lock = $this->lock_cron();
			wp_schedule_event( $args['timestamp'], $args['recurrence'], $hook, $arguments );
			$this->maybe_unlock_cron( $lock );
			return array( 'success' => true );
		}

		foreach ( $next_scheduled as $scheduled_time ) {
			if ( abs( $scheduled_time - $args['timestamp'] ) <= 10 * MINUTE_IN_SECONDS ) {
				return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 );
			}
		}
		$lock = $this->lock_cron();
		$next = wp_schedule_single_event( $args['timestamp'], $hook, $arguments );
		$this->maybe_unlock_cron( $lock );
		return array( 'success' => $next );
	}
}

/**
 * The cron unschedule ednpoint class.
 *
 * POST /sites/%s/cron/unschedule
 *
 * @phan-constructor-used-for-side-effects
 */
class Jetpack_JSON_API_Cron_Unschedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace

	/**
	 * The result.
	 *
	 * @return array|WP_Error
	 */
	protected function result() {
		$args = $this->input();

		if ( ! isset( $args['hook'] ) ) {
			return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 );
		}

		$hook = $this->sanitize_hook( $args['hook'] );

		$locked = $this->is_cron_locked( microtime( true ) );
		if ( is_wp_error( $locked ) ) {
			return $locked;
		}

		$crons = _get_cron_array();
		if ( empty( $crons ) ) {
			return new WP_Error( 'cron-not-present', 'Unable to unschedule an event, no events in the cron', 400 );
		}

		$arguments = $this->resolve_arguments();

		if ( isset( $args['timestamp'] ) ) {
			$next_schedulded = $this->get_schedules( $hook, $arguments );
			if ( in_array( $args['timestamp'], $next_schedulded, true ) ) {
				return new WP_Error( 'event-not-present', 'Unable to unschedule the event, the event doesn\'t exist', 400 );
			}

			$lock = $this->lock_cron();
			wp_unschedule_event( $args['timestamp'], $hook, $arguments );
			$this->maybe_unlock_cron( $lock );
			return array( 'success' => true );
		}
		$lock = $this->lock_cron();
		wp_clear_scheduled_hook( $hook, $arguments );
		$this->maybe_unlock_cron( $lock );
		return array( 'success' => true );
	}
}
