Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 4
Jetpack_JSON_API_Cron_Endpoint
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 9
342
0.00% covered (danger)
0.00%
0 / 1
 validate_call
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 result
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 sanitize_hook
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resolve_arguments
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 is_cron_locked
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_unlock_cron
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 lock_cron
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_schedules
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 get_cron_lock
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
Jetpack_JSON_API_Cron_Post_Endpoint
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
240
0.00% covered (danger)
0.00%
0 / 1
 result
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
240
Jetpack_JSON_API_Cron_Schedule_Endpoint
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 1
 result
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
Jetpack_JSON_API_Cron_Unschedule_Endpoint
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 1
 result
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3if ( ! defined( 'ABSPATH' ) ) {
4    exit( 0 );
5}
6
7/**
8 * Cron endpoint class.
9 *
10 * GET /sites/%s/cron
11 *
12 * @phan-constructor-used-for-side-effects
13 */
14class Jetpack_JSON_API_Cron_Endpoint extends Jetpack_JSON_API_Endpoint {
15
16    /**
17     * Needed capabilities.
18     *
19     * @var string
20     */
21    protected $needed_capabilities = 'manage_options';
22
23    /**
24     * Validate the call.
25     *
26     * @param int   $_blog_id - the blog ID.
27     * @param array $capability - the capabilities of the user.
28     * @param bool  $check_manage_active - parameter is for making the method signature compatible with its parent class method.
29     */
30    protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
31        return parent::validate_call( $_blog_id, $capability, false );
32    }
33
34    /**
35     * Return the result of current timestamp.
36     */
37    protected function result() {
38        return array(
39            'cron_array'        => _get_cron_array(),
40            'current_timestamp' => time(),
41        );
42    }
43
44    /**
45     * Sanitize the hook.
46     *
47     * @param string $hook - the hook.
48     *
49     * @return string
50     */
51    protected function sanitize_hook( $hook ) {
52        return preg_replace( '/[^A-Za-z0-9-_]/', '', $hook );
53    }
54
55    /**
56     * Resolve arguments.
57     *
58     * @return array
59     */
60    protected function resolve_arguments() {
61        $args = $this->input();
62        return isset( $args['arguments'] ) ? json_decode( $args['arguments'] ) : array();
63    }
64
65    /**
66     * Check the cron lock.
67     *
68     * @param float $gmt_time - the time in GMT.
69     *
70     * @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.
71     */
72    protected function is_cron_locked( $gmt_time ) {
73        // The cron lock: a unix timestamp from when the cron was spawned.
74        $doing_cron_transient = $this->get_cron_lock();
75        if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) ) {
76            return new WP_Error( 'cron-is-locked', 'Current there is a cron already happening.', 403 );
77        }
78        return $doing_cron_transient;
79    }
80
81    /**
82     * Check if we can unlock the cron transient.
83     *
84     * @param string $doing_wp_cron - if we're doing the wp_cron.
85     */
86    protected function maybe_unlock_cron( $doing_wp_cron ) {
87        if ( $this->get_cron_lock() === $doing_wp_cron ) {
88            delete_transient( 'doing_cron' );
89        }
90    }
91
92    /**
93     * Set the cron lock.
94     *
95     * @return string
96     */
97    protected function lock_cron() {
98        $lock = sprintf( '%.22F', microtime( true ) );
99        set_transient( 'doing_cron', $lock );
100        return $lock;
101    }
102
103    /**
104     * Get scheduled.
105     *
106     * @param string $hook - the hook.
107     * @param array  $args - the arguments.
108     *
109     * @return array
110     */
111    protected function get_schedules( $hook, $args ) {
112        $crons = _get_cron_array();
113        $key   = md5( serialize( $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
114        if ( empty( $crons ) ) {
115            return array();
116        }
117        $found = array();
118        foreach ( $crons as $timestamp => $cron ) {
119            if ( isset( $cron[ $hook ][ $key ] ) ) {
120                $found[] = $timestamp;
121            }
122        }
123
124        return $found;
125    }
126
127    /**
128     * This function is based on the one found in wp-cron.php with a similar name
129     *
130     * @return int
131     */
132    protected function get_cron_lock() {
133        global $wpdb;
134
135        $value = 0;
136        if ( wp_using_ext_object_cache() ) {
137            /*
138             * Skip local cache and force re-fetch of doing_cron transient
139             * in case another process updated the cache.
140             */
141            $value = wp_cache_get( 'doing_cron', 'transient', true );
142        } else {
143            $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) );
144            if ( is_object( $row ) ) {
145                $value = $row->option_value;
146            }
147        }
148        return $value;
149    }
150}
151
152/**
153 * Cron post endpoint class.
154 *
155 * POST /sites/%s/cron
156 *
157 * @phan-constructor-used-for-side-effects
158 */
159class Jetpack_JSON_API_Cron_Post_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
160
161    /**
162     * The result.
163     *
164     * @return array|WP_Error
165     */
166    protected function result() {
167        define( 'DOING_CRON', true );
168        set_time_limit( 0 );
169        $args  = $this->input();
170        $crons = _get_cron_array();
171        if ( false === $crons ) {
172            return new WP_Error( 'no-cron-event', 'Currently there are no cron events', 400 );
173        }
174
175        $timestamps_to_run = array_keys( $crons );
176        $gmt_time          = microtime( true );
177
178        if ( isset( $timestamps_to_run[0] ) && $timestamps_to_run[0] > $gmt_time ) {
179            return new WP_Error( 'no-cron-event', 'Currently there are no cron events ready to be run', 400 );
180        }
181
182        $locked = $this->is_cron_locked( $gmt_time );
183        if ( is_wp_error( $locked ) ) {
184            return $locked;
185        }
186
187        $lock             = $this->lock_cron();
188        $processed_events = array();
189
190        foreach ( $crons as $timestamp => $cronhooks ) {
191            if ( $timestamp > $gmt_time && ! isset( $args['hook'] ) ) {
192                break;
193            }
194
195            foreach ( $cronhooks as $hook => $hook_data ) {
196                if ( isset( $args['hook'] ) && ! in_array( $hook, $args['hook'], true ) ) {
197                    continue;
198                }
199
200                foreach ( $hook_data as $hook_item ) {
201
202                    $schedule  = $hook_item['schedule'];
203                    $arguments = $hook_item['args'];
204
205                    if ( ! $schedule ) {
206                        wp_reschedule_event( $timestamp, $schedule, $hook, $arguments );
207                    }
208
209                    wp_unschedule_event( $timestamp, $hook, $arguments );
210
211                    do_action_ref_array( $hook, $arguments );
212                    $processed_events[] = array( $hook => $arguments );
213
214                    // If the hook ran too long and another cron process stole the lock,
215                    // or if we things are taking longer then 20 seconds then quit.
216                    if ( ( $this->get_cron_lock() !== $lock ) || ( $gmt_time + 20 > microtime( true ) ) ) {
217                        $this->maybe_unlock_cron( $lock );
218                        return array( 'success' => $processed_events );
219                    }
220                }
221            }
222        }
223
224        $this->maybe_unlock_cron( $lock );
225        return array( 'success' => $processed_events );
226    }
227}
228
229/**
230 * Schedule endpoint class.
231 *
232 * POST /sites/%s/cron/schedule
233 *
234 * @phan-constructor-used-for-side-effects
235 */
236class Jetpack_JSON_API_Cron_Schedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
237
238    /**
239     * The result.
240     *
241     * @return array|WP_Error
242     */
243    protected function result() {
244        $args = $this->input();
245        if ( ! isset( $args['timestamp'] ) ) {
246            return new WP_Error( 'missing_argument', 'Please provide the timestamp argument', 400 );
247        }
248
249        if ( ! is_int( $args['timestamp'] ) || $args['timestamp'] < time() ) {
250            return new WP_Error( 'timestamp-invalid', 'Please provide timestamp that is an integer and set in the future', 400 );
251        }
252
253        if ( ! isset( $args['hook'] ) ) {
254            return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 );
255        }
256
257        $hook = $this->sanitize_hook( $args['hook'] );
258
259        $locked = $this->is_cron_locked( microtime( true ) );
260        if ( is_wp_error( $locked ) ) {
261            return $locked;
262        }
263
264        $arguments      = $this->resolve_arguments();
265        $next_scheduled = $this->get_schedules( $hook, $arguments );
266
267        if ( isset( $args['recurrence'] ) ) {
268            $schedules = wp_get_schedules();
269            if ( ! isset( $schedules[ $args['recurrence'] ] ) ) {
270                return new WP_Error( 'invalid-recurrence', 'Please provide a valid recurrence argument', 400 );
271            }
272
273            if ( is_countable( $next_scheduled ) && count( $next_scheduled ) > 0 ) {
274                return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 );
275            }
276            $lock = $this->lock_cron();
277            wp_schedule_event( $args['timestamp'], $args['recurrence'], $hook, $arguments );
278            $this->maybe_unlock_cron( $lock );
279            return array( 'success' => true );
280        }
281
282        foreach ( $next_scheduled as $scheduled_time ) {
283            if ( abs( $scheduled_time - $args['timestamp'] ) <= 10 * MINUTE_IN_SECONDS ) {
284                return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 );
285            }
286        }
287        $lock = $this->lock_cron();
288        $next = wp_schedule_single_event( $args['timestamp'], $hook, $arguments );
289        $this->maybe_unlock_cron( $lock );
290        return array( 'success' => $next );
291    }
292}
293
294/**
295 * The cron unschedule ednpoint class.
296 *
297 * POST /sites/%s/cron/unschedule
298 *
299 * @phan-constructor-used-for-side-effects
300 */
301class Jetpack_JSON_API_Cron_Unschedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
302
303    /**
304     * The result.
305     *
306     * @return array|WP_Error
307     */
308    protected function result() {
309        $args = $this->input();
310
311        if ( ! isset( $args['hook'] ) ) {
312            return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 );
313        }
314
315        $hook = $this->sanitize_hook( $args['hook'] );
316
317        $locked = $this->is_cron_locked( microtime( true ) );
318        if ( is_wp_error( $locked ) ) {
319            return $locked;
320        }
321
322        $crons = _get_cron_array();
323        if ( empty( $crons ) ) {
324            return new WP_Error( 'cron-not-present', 'Unable to unschedule an event, no events in the cron', 400 );
325        }
326
327        $arguments = $this->resolve_arguments();
328
329        if ( isset( $args['timestamp'] ) ) {
330            $next_schedulded = $this->get_schedules( $hook, $arguments );
331            if ( in_array( $args['timestamp'], $next_schedulded, true ) ) {
332                return new WP_Error( 'event-not-present', 'Unable to unschedule the event, the event doesn\'t exist', 400 );
333            }
334
335            $lock = $this->lock_cron();
336            wp_unschedule_event( $args['timestamp'], $hook, $arguments );
337            $this->maybe_unlock_cron( $lock );
338            return array( 'success' => true );
339        }
340        $lock = $this->lock_cron();
341        wp_clear_scheduled_hook( $hook, $arguments );
342        $this->maybe_unlock_cron( $lock );
343        return array( 'success' => true );
344    }
345}