Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/cron api endpoint #5324

Merged
merged 1 commit into from
Nov 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions json-endpoints/jetpack/class.jetpack-json-api-cron-endpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
<?php

// GET /sites/%s/cron
class Jetpack_JSON_API_Cron_Endpoint extends Jetpack_JSON_API_Endpoint {
protected $needed_capabilities = 'manage_options';

protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
parent::validate_call( $_blog_id, $capability, false );
}

protected function result() {
return array(
'cron_array' => _get_cron_array(),
'current_timestamp' => time()
);
}

protected function sanitize_hook( $hook ) {
return preg_replace( '/[^A-Za-z0-9-_]/', '', $hook );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we sync any hooks with uppercase characters?

Can we use sanitize_key() instead of rolling our own regex? The difference is that sanitize_key() converts uppercase characters to lowercase and also allows dashes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing I haven't come across uppercase keys but it doesn't meant that and there are not many actions that user uppercase. I would keep things as they are. Since it is a bit more liberal then the current sanitize_key() implementation. Maybe WP needs to add sanitize_action().

}

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

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;
}

protected function maybe_unlock_cron( $doing_wp_cron ) {
if ( $this->get_cron_lock() == $doing_wp_cron ) {
delete_transient( 'doing_cron' );
}
}

protected function lock_cron() {
$lock = sprintf( '%.22F', microtime( true ) );
set_transient( 'doing_cron', $lock );
return $lock;
}

protected function get_schedules( $hook, $args ) {
$crons = _get_cron_array();
$key = md5(serialize($args));
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;
}
}

// POST /sites/%s/cron
class Jetpack_JSON_API_Cron_Post_Endpoint extends Jetpack_JSON_API_Cron_Endpoint {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to try set_time_limit(0) at the start of this method, or your jobs could get killed with impunity. Often there are other timeouts too though, need to be mindful of that (e.g. Apache will often set a 5 minute response packet timeout)


protected function result() {
define( 'DOING_CRON', true );
set_time_limit( 0 );
$args = $this->input();

if ( false === $crons = _get_cron_array() ) {
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we're adding a status code here since we're returning this WP_Error directly.

}

$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'] ) ) {
continue;
}

foreach ( $hook_data as $hash => $hook_item ) {

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

if ( $schedule != false ) {
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have an empty newline above this comment to separate it from code above.

// 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 );
}
}

// POST /sites/%s/cron/schedule
class Jetpack_JSON_API_Cron_Schedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint {

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 ( 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' => is_null( $next ) ? true : false );
}
}

// POST /sites/%s/cron/unschedule
class Jetpack_JSON_API_Cron_Unschedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint {

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 ) ) {
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 );
}
}
108 changes: 108 additions & 0 deletions json-endpoints/jetpack/json-api-jetpack-endpoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -903,3 +903,111 @@
),
),
) );


require_once( $json_jetpack_endpoints_dir . 'class.jetpack-json-api-cron-endpoint.php' );

// GET /sites/%s/cron
new Jetpack_JSON_API_Cron_Endpoint( array(
'description' => 'Fetches the cron array',
'group' => '__do_not_document',
'method' => 'GET',
'path' => '/sites/%s/cron',
'stat' => 'cron-get',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain'
),
'response_format' => array(
'cron_array' => '(array) The cron array',
'current_timestamp' => '(int) Current server timestamp'
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
),
) );

// POST /sites/%s/cron
new Jetpack_JSON_API_Cron_Post_Endpoint( array(
'description' => 'Process items in the cron',
'group' => '__do_not_document',
'method' => 'POST',
'path' => '/sites/%s/cron',
'stat' => 'cron-run',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain'
),
'request_format' => array(
'hooks' => '(array) List of hooks to run if they have been scheduled (optional)',
),
'response_format' => array(
'success' => '(array) Of processed hooks with their arguments'
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
'body' => array(
'hooks' => array( 'jetpack_sync_cron' )
),
),
) );

// POST /sites/%s/cron/schedule
new Jetpack_JSON_API_Cron_Schedule_Endpoint( array(
'description' => 'Schedule one or a recurring hook to fire at a particular time',
'group' => '__do_not_document',
'method' => 'POST',
'path' => '/sites/%s/cron/schedule',
'stat' => 'cron-schedule',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain'
),
'request_format' => array(
'hook' => '(string) Hook name that should run when the event is scheduled',
'timestamp' => '(int) Timestamp when the event should take place, has to be in the future',
'arguments' => '(string) JSON Object of arguments that the hook will use (optional)',
'recurrence' => '(string) How often the event should take place. If empty only one event will be scheduled. Possible values 1min, hourly, twicedaily, daily (optional) '
),
'response_format' => array(
'success' => '(bool) Was the event scheduled?'
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron/schedule',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
'body' => array(
'hook' => 'jetpack_sync_cron',
'arguments' => '[]',
'recurrence'=> '1min',
'timestamp' => 1476385523
),
),
) );

// POST /sites/%s/cron/unschedule
new Jetpack_JSON_API_Cron_Unschedule_Endpoint( array(
'description' => 'Unschedule one or all events with a particular hook and arguments',
'group' => '__do_not_document',
'method' => 'POST',
'path' => '/sites/%s/cron/unschedule',
'stat' => 'cron-unschedule',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain'
),
'request_format' => array(
'hook' => '(string) Name of the hook that should be unscheduled',
'timestamp' => '(int) Timestamp of the hook that you want to unschedule. This will unschedule only 1 event. (optional)',
'arguments' => '(string) JSON Object of arguments that the hook has been scheduled with (optional)',
),
'response_format' => array(
'success' => '(bool) Was the event unscheduled?'
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron/unschedule',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
'body' => array(
'hook' => 'jetpack_sync_cron',
'arguments' => '[]',
'timestamp' => 1476385523
),
),
) );