forked from Automattic/WP-Cron-Control
-
Notifications
You must be signed in to change notification settings - Fork 0
/
wp-cron-control.php
396 lines (335 loc) · 16.3 KB
/
wp-cron-control.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
<?php
/*
Plugin Name: WP-Cron Control
Plugin URI: https://wordpress.org/plugins/wp-cron-control/
Description: Take control of wp-cron execution.
Author: Thorsten Ott, Erick Hitter, Automattic
Version: 0.7.1
Text Domain: wp-cron-control
*/
class WP_Cron_Control {
private static $__instance = NULL;
private $settings = array();
private $default_settings = array();
private $settings_texts = array();
private $plugin_prefix = 'wpcroncontrol_';
private $plugin_name = 'WP-Cron Control';
private $settings_page_name = null;
private $dashed_name = 'wp-cron-control';
private $js_version = '20110801';
private $css_version = '20110801';
private $define_global_secret = NULL; // if this is set, it's value will be used as secret instead of the option
public function __construct() {
global $blog_id;
// this allows overwriting of the default secret with a value set in the code. Useful If you don't want to give control to users.
if ( NULL <> $this->define_global_secret && !defined( 'WP_CRON_CONTROL_SECRET' ) )
define( 'WP_CRON_CONTROL_SECRET', $this->define_global_secret );
add_action( 'admin_init', array( &$this, 'register_setting' ) );
add_action( 'admin_menu', array( &$this, 'register_settings_page' ) );
/**
* Default settings that will be used for the setup. You can alter these value with a simple filter such as this
* add_filter( 'wpcroncontrol_default_settings', 'mywpcroncontrol_settings' );
* function mywpcroncontrol_settings( $settings ) {
* $settings['secret_string'] = 'i am more secret than the default';
* return $settings;
* }
*/
$this->default_settings = (array) apply_filters( $this->plugin_prefix . 'default_settings', array(
'enable' => 1,
'enable_scheduled_post_validation' => 0,
'secret_string' => md5( __FILE__ . $blog_id ),
) );
/**
* Define fields that will be used on the options page
* the array key is the field_name the array then describes the label, description and type of the field. possible values for field types are 'text' and 'yesno' for a text field or input fields or 'echo' for a simple output
* a filter similar to the default settings (ie wpcroncontrol_settings_texts) can be used to alter this values
*/
$this->settings_texts = (array) apply_filters( $this->plugin_prefix . 'settings_texts', array(
'enable' => array(
'label' => sprintf( __( 'Enable %s', 'wp-cron-control' ), $this->plugin_name ),
'desc' => sprintf( __( 'Enable this plugin and allow requests to %s only with the appended secret parameter.', 'wp-cron-control' ), '<code>wp-cron.php</code>' ),
'type' => 'yesno'
),
'secret_string' => array(
'label' => __( 'Secret string', 'wp-cron-control' ),
'desc' => sprintf( __( 'The secret parameter that needs to be appended to %s requests.', 'wp-cron-control' ), '<code>wp-cron.php</code>' ),
'type' => 'text'
),
'enable_scheduled_post_validation' => array(
'label' => __( 'Enable scheduled post validation', 'wp-cron-control' ),
'desc' => sprintf( __( 'In some rare cases, it can happen that even when running %s via a scheduled system cron job, posts miss their schedule. This feature makes sure that there is a scheduled event for each scheduled post.', 'wp-cron-control' ), '<code>wp-cron</code>' ),
'type' => 'yesno'
),
) );
$user_settings = get_option( $this->plugin_prefix . 'settings' );
if ( false === $user_settings )
$user_settings = array();
// after getting default settings make sure to parse the arguments together with the user settings
$this->settings = wp_parse_args( $user_settings, $this->default_settings );
/**
* If you define( 'WP_CRON_CONTROL_SECRET', 'my_super_secret_string' ); in your wp-config.php or your theme then
* users are not allowed to change the secret, so we output the existing secret string rather than allowing to add a new one
*/
if ( defined( 'WP_CRON_CONTROL_SECRET' ) ) {
$this->settings_texts['secret_string']['type'] = 'echo';
$this->settings_texts['secret_string']['desc'] = $this->settings_texts['secret_string']['desc'] . sprintf( __( 'Cannot be changed as it is defined via %s.', 'wp-cron-control' ), "<code>WP_CRON_CONTROL_SECRET</code>" );
$this->settings['secret_string'] = WP_CRON_CONTROL_SECRET;
}
}
public static function init() {
self::instance()->settings_page_name = sprintf( __( '%s Settings', 'wp-cron-control' ), self::instance()->plugin_name );
if ( 1 == self::instance()->settings['enable'] ) {
self::instance()->prepare();
}
}
/*
* Use this singleton to address methods
*/
public static function instance() {
if ( self::$__instance == NULL )
self::$__instance = new WP_Cron_Control;
return self::$__instance;
}
public function prepare() {
/**
* If a css file for this plugin exists in ./css/wp-cron-control.css make sure it's included
*/
if ( file_exists( dirname( __FILE__ ) . "/css/" . $this->dashed_name . ".css" ) )
wp_enqueue_style( $this->dashed_name, plugins_url( "css/" . $this->dashed_name . ".css", __FILE__ ), $deps = array(), $this->css_version );
/**
* If a js file for this plugin exists in ./js/wp-cron-control.css make sure it's included
*/
if ( file_exists( dirname( __FILE__ ) . "/js/" . $this->dashed_name . ".js" ) )
wp_enqueue_script( $this->dashed_name, plugins_url( "js/" . $this->dashed_name . ".js", __FILE__ ), array(), $this->js_version, true );
/**
* When the plugin is enabled make sure remove the default behavior for issueing wp-cron requests and add our own method
* see: http://core.trac.wordpress.org/browser/trunk/wp-includes/default-filters.php#L236
* and http://core.trac.wordpress.org/browser/trunk/wp-includes/cron.php#L258
*/
if ( 1 == $this->settings['enable'] ) {
remove_action( 'init', 'wp_cron' );
add_action( 'init', array( &$this, 'validate_cron_request' ) );
}
}
public function register_settings_page() {
add_options_page( $this->settings_page_name, $this->plugin_name, 'manage_options', $this->dashed_name, array( &$this, 'settings_page' ) );
}
public function register_setting() {
register_setting( $this->plugin_prefix . 'settings', $this->plugin_prefix . 'settings', array( &$this, 'validate_settings') );
}
public function validate_settings( $settings ) {
$validated_settings = array();
if ( !empty( $_POST[ $this->dashed_name . '-defaults'] ) ) {
// Reset to defaults
$validated_settings = $this->default_settings;
$_REQUEST['_wp_http_referer'] = add_query_arg( 'defaults', 'true', $_REQUEST['_wp_http_referer'] );
} else {
foreach ( $this->settings_texts as $setting => $setting_info ) {
switch( $setting ) {
case 'enable':
case 'enable_scheduled_post_validation':
$validated_settings[ $setting ] = intval( $settings[ $setting ] );
if ( $validated_settings[ $setting ] > 1 || $validated_settings[ $setting ] < 0 ) {
$validated_settings[ $setting ] = $this->default_settings[ $setting ];
}
break;
case 'secret_string':
$validated_settings[ $setting ] = sanitize_text_field( $settings[ $setting ] );
if ( empty( $validated_settings[ $setting ] ) ) {
$validated_settings[ $setting ] = $this->default_settings[ $setting ];
}
break;
default:
$validated_settings[ $setting ] = sanitize_text_field( $settings[ $setting ] );
break;
}
}
}
return $validated_settings;
}
public function settings_page() {
if ( !current_user_can( 'manage_options' ) ) {
wp_die( __( 'You do not permission to access this page' ) );
}
?>
<div class="wrap">
<?php if ( function_exists('screen_icon') ) screen_icon(); ?>
<h2><?php echo $this->settings_page_name; ?></h2>
<form method="post" action="options.php">
<?php settings_fields( $this->plugin_prefix . 'settings' ); ?>
<table class="form-table">
<?php foreach( $this->settings as $setting => $value): ?>
<tr valign="top">
<th scope="row"><label for="<?php echo $this->dashed_name . '-' . $setting; ?>"><?php if ( isset( $this->settings_texts[$setting]['label'] ) ) { echo $this->settings_texts[$setting]['label']; } else { echo $setting; } ?></label></th>
<td>
<?php
/**
* Implement various handlers for the different types of fields. This could be easily extended to allow for drop-down boxes, textareas and more
*/
?>
<?php switch( $this->settings_texts[$setting]['type'] ):
case 'yesno': ?>
<select name="<?php echo $this->plugin_prefix; ?>settings[<?php echo $setting; ?>]" id="<?php echo $this->dashed_name . '-' . $setting; ?>" class="postform">
<?php
$yesno = array( 0 => __( 'No', 'wp-cron-control' ), 1 => __( 'Yes', 'wp-cron-control' ) );
foreach ( $yesno as $val => $txt ) {
echo '<option value="' . esc_attr( $val ) . '"' . selected( $value, $val, false ) . '>' . esc_html( $txt ) . " </option>\n";
}
?>
</select><br />
<?php break;
case 'text': ?>
<div><input type="text" name="<?php echo $this->plugin_prefix; ?>settings[<?php echo $setting; ?>]" id="<?php echo $this->dashed_name . '-' . $setting; ?>" class="postform" value="<?php echo esc_attr( $value ); ?>" /></div>
<?php break;
case 'echo': ?>
<div><span id="<?php echo $this->dashed_name . '-' . $setting; ?>" class="postform"><?php echo esc_html( $value ); ?></span></div>
<?php break;
default: ?>
<?php echo esc_html( $this->settings_texts[$setting]['type'] ); ?>
<?php break;
endswitch; ?>
<?php if ( !empty( $this->settings_texts[$setting]['desc'] ) ) { echo wp_kses_post( $this->settings_texts[$setting]['desc'] ); } ?>
</td>
</tr>
<?php endforeach; ?>
<?php if ( 1 == $this->settings['enable'] ): ?>
<tr>
<td colspan="3">
<p><?php printf( __( 'You enabled %s. To make sure that scheduled tasks are still executed correctly, you will need to setup a system cron job that will call %s with the secret parameter defined in the settings.', 'wp-cron-control' ), $this->plugin_name, '<code>wp-cron.php</code>' ); ?></p>
<p><?php _e( 'You can use the function defined in this script and set up a cron job that calls either:', 'wp-cron-control' ); ?></p>
<p><code>php <?php echo __FILE__; ?> <?php echo get_site_url(); ?> <?php echo $this->settings['secret_string']; ?></code></p>
<p>or</p>
<p><code>wget -q "<?php echo get_site_url(); ?>/wp-cron.php?doing_wp_cron&<?php echo $this->settings['secret_string']; ?>"</code></p>
<p><?php _e( 'You can set an interval as low as one minute, but should consider a reasonable value of 5-15 minutes as well.', 'wp-cron-control' ); ?></p>
<p><?php _e( 'If you need help setting up a cron job please refer to the documentation that your provider offers.', 'wp-cron-control' ); ?></p>
<p><?php printf( __( 'Anyway, chances are high that either the %s, %s, or %s documentation will help you.', 'wp-cron-control' ), '<a href="http://docs.cpanel.net/twiki/bin/view/AllDocumentation/CpanelDocs/CronJobs#Adding a cron job" target="_blank">CPanel</a>', '<a href="http://download1.parallels.com/Plesk/PP10/10.3.1/Doc/en-US/online/plesk-administrator-guide/plesk-control-panel-user-guide/index.htm?fileName=65208.htm" target="_blank">Plesk</a>', '<a href="http://www.thegeekstuff.com/2011/07/php-cron-job/" target="_blank">crontab</a>' ); ?></p>
</td>
</tr>
<?php endif; ?>
</table>
<p class="submit">
<?php
if ( function_exists( 'submit_button' ) ) {
submit_button( null, 'primary', $this->dashed_name . '-submit', false );
echo ' ';
submit_button( __( 'Reset to Defaults', 'wp-cron-control' ), '', $this->dashed_name . '-defaults', false );
} else {
echo '<input type="submit" name="' . $this->dashed_name . '-submit" class="button-primary" value="' . __( 'Save Changes', 'wp-cron-control' ) . '" />' . "\n";
echo '<input type="submit" name="' . $this->dashed_name . '-defaults" id="' . $this->dashed_name . '-defaults" class="button-primary" value="' . __( 'Reset to Defaults', 'wp-cron-control' ) . '" />' . "\n";
}
?>
</p>
</form>
</div>
<?php
}
/**
* Alternative function to the current wp_cron function that would usually executed on sanitize_comment_cookies
*/
public function validate_cron_request() {
// make sure we're in wp-cron.php
if ( false !== strpos( $_SERVER['REQUEST_URI'], '/wp-cron.php' ) ) {
// grab the necessary secret string
if ( defined( 'WP_CRON_CONTROL_SECRET' ) )
$secret = WP_CRON_CONTROL_SECRET;
else
$secret = $this->settings['secret_string'];
// make sure a secret string is provided in the ur
if ( isset( $_GET[$secret] ) ) {
// check if there is already a cron request running
$local_time = time();
if ( function_exists( '_get_cron_lock' ) )
$flag = _get_cron_lock();
else
$flag = get_transient('doing_cron');
if ( defined( 'WP_CRON_LOCK_TIMEOUT' ) )
$timeout = WP_CRON_LOCK_TIMEOUT;
else
$timeout = 60;
if ( $flag > $local_time + 10 * $timeout )
$flag = 0;
// don't run if another process is currently running it or more than once every 60 sec.
if ( $flag + $timeout > $local_time )
die( 'another cron process running or previous not older than 60 secs' );
// set a transient to allow locking down parallel requests
set_transient( 'doing_cron', $local_time );
// make sure the request also validates in wp-cron.php
global $doing_wp_cron;
$doing_wp_cron = $local_time;
// if settings allow it validate if there are any scheduled posts without a cron event
if ( 1 == self::instance()->settings['enable_scheduled_post_validation'] ) {
$this->validate_scheduled_posts();
}
return true;
}
// something went wrong
die( 'invalid secret string' );
}
// for all other cases disable wp-cron.php and spawn_cron() by telling the system it's already running
//if ( !defined( 'DOING_CRON' ) )
// define( 'DOING_CRON', true );
// and also disable the wp_cron() call execution
if ( !defined( 'DISABLE_WP_CRON' ) )
define( 'DISABLE_WP_CRON', true );
return false;
}
public function validate_scheduled_posts() {
global $wpdb;
$return_value = true;
$offset = 0;
$limit = 30;
while ( true ) {
// grab batch of scheduled posts
// uses `post_date` and converts to GMT later, rather than pulling `post_date_gmt`, to leverage `type_status_date` index
$results = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_date FROM $wpdb->posts WHERE post_status = 'future' LIMIT %d,%d", $offset, $limit ) );
$offset += $limit;
// if none exists just return
if ( empty( $results ) ) {
return $return_value;
}
// otherwise check each of them
foreach ( $results as $r ) {
$gmt_time = strtotime( get_gmt_from_date( $r->post_date ) . ' GMT' );
// grab the scheduled job for this post
$timestamp = wp_next_scheduled( 'publish_future_post', array( (int) $r->ID ) );
if ( false === $timestamp ) {
// if none exists issue one
wp_schedule_single_event( $gmt_time, 'publish_future_post', array( (int) $r->ID ) );
$return_value = false;
} elseif ( (int) $timestamp !== $gmt_time ) {
wp_clear_scheduled_hook( 'publish_future_post', array( (int) $r->ID ) );
wp_schedule_single_event( $gmt_time, 'publish_future_post', array( (int) $r->ID ) );
$return_value = false;
}
}
}
return $return_value;
}
}
/**
* This method can be used to initiate a cron call via cli
*/
function wp_cron_control_call_cron( $blog_address, $secret ) {
$cron_url = $blog_address . '/wp-cron.php?doing_wp_cron&' . $secret;
$ch = curl_init( $cron_url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 0 );
curl_setopt( $ch, CURLOPT_TIMEOUT, '3' );
$result = curl_exec( $ch );
curl_close( $ch );
return $result;
}
// if we loaded wp-config then ABSPATH is defined and we know the script was not called directly to issue a cli call
if ( defined('ABSPATH') ) {
WP_Cron_Control::init();
} else {
// otherwise parse the arguments and call the cron.
if ( !empty( $argv ) && $argv[0] == basename( __FILE__ ) || $argv[0] == __FILE__ ) {
if ( isset( $argv[1] ) && isset( $argv[2] ) ) {
wp_cron_control_call_cron( $argv[1], $argv[2] );
} else {
echo "Usage: php " . __FILE__ . " <blog_address> <secret_string>\n";
echo "Example: php " . __FILE__ . " http://my.blog.com efe18b0e53498e737da9b91cf4ca3d25\n";
exit;
}
}
}