oont-contents/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-cron-endpoint.php
2025-02-08 15:10:23 +01:00

333 lines
9.4 KiB
PHP

<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Cron endpoint class.
*
* GET /sites/%s/cron
*/
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
*/
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
*/
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
*/
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 );
}
}