├── README.md ├── readme.txt ├── screenshot-1.jpg └── wp-cron-control.php /README.md: -------------------------------------------------------------------------------- 1 | This repository had been archived in benefit of https://github.com/Automattic/Cron-Control. 2 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === WP-Cron Control === 2 | Contributors: tott, ethitter, automattic, batmoo 3 | Tags: wp-cron, cron, cron jobs, post missed schedule, scheduled posts 4 | Requires at least: 3.4 5 | Tested up to: 4.6 6 | Stable tag: 0.7.1 7 | 8 | This plugin allows you to take control over the execution of cron jobs. 9 | 10 | == Description == 11 | 12 | This plugin allows you to take control over the execution of cron jobs. It's mainly useful for sites that either don't get enough comments to ensure a frequent execution of wp-cron or for sites where the execution of cron via regular methods can cause race conditions resulting in multiple execution of wp-cron at the same time. It can also help when you run into posts that missed their schedule. 13 | 14 | This plugin implements a secret parameter and ensures that cron jobs are only executed when this parameter is existing. 15 | 16 | == Installation == 17 | 18 | * Install either via the WordPress.org plugin directory, or by uploading the files to your server. 19 | * Activate the Plugin and ensure that you enable the feature in the plugins' settings screen 20 | * Follow the instructions on the plugins' settings screen in order to set up a cron job that either calls `php wp-cron-control.php http://blog.address secret_string` or `wget -q "http://blog.address/wp-cron.php?doing_wp_cron&secret_string"` 21 | * If you like to have a global secret string you can define it in your wp-config.php by adding `define( 'WP_CRON_CONTROL_SECRET', my_secret_string' );` 22 | 23 | == Limitations == 24 | 25 | This plugin performs a `remove_action( 'sanitize_comment_cookies', 'wp_cron' );` call in order to disable the spawning of new cron processes via the regular WordPress method. If `wp_cron` is hooked in an other action or called directly this might cause trouble. 26 | 27 | == Screenshots == 28 | 29 | 1. Settings screen to enable/disable various features. 30 | 31 | == ChangeLog == 32 | 33 | = Version 0.7.1 = 34 | 35 | * Security hardening (better escaping, sanitization of saved values) 36 | * Update plugin to use core's updated cron hook 37 | 38 | = Version 0.7 = 39 | 40 | * Remove unneeded use of `$wpdb->prepare()` that triggered PHP warnings because a second argument wasn't provided. 41 | * Update interface text to be translatable. 42 | 43 | = Version 0.6 = 44 | 45 | * Make sure that validated wp-cron-control requests also are valid in wp-cron.php by setting the global $doing_wp_cron value 46 | 47 | = Version 0.5 = 48 | 49 | * Adjustments for improved cron locking introduced in WordPress 3.3 http://core.trac.wordpress.org/changeset/18659 50 | 51 | = Version 0.4 = 52 | 53 | * Implementing feedback from Yoast http://yoast.com/wp-plugin-review/wp-cron-control/, fixing button classes, more inline comments 54 | 55 | = Version 0.3 = 56 | 57 | * Added option to enable extra check that would search for missing jobs for scheduled posts and add them if necessary. 58 | 59 | = Version 0.2 = 60 | 61 | * Added capability check in settings page 62 | 63 | = Version 0.1 = 64 | 65 | * Initial version of this plugin. 66 | -------------------------------------------------------------------------------- /screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/WP-Cron-Control/d8d27c53f8d7199fda03a6aeca4794df6f8199f9/screenshot-1.jpg -------------------------------------------------------------------------------- /wp-cron-control.php: -------------------------------------------------------------------------------- 1 | $this->define_global_secret && !defined( 'WP_CRON_CONTROL_SECRET' ) ) 33 | define( 'WP_CRON_CONTROL_SECRET', $this->define_global_secret ); 34 | 35 | add_action( 'admin_init', array( &$this, 'register_setting' ) ); 36 | add_action( 'admin_menu', array( &$this, 'register_settings_page' ) ); 37 | 38 | /** 39 | * Default settings that will be used for the setup. You can alter these value with a simple filter such as this 40 | * add_filter( 'wpcroncontrol_default_settings', 'mywpcroncontrol_settings' ); 41 | * function mywpcroncontrol_settings( $settings ) { 42 | * $settings['secret_string'] = 'i am more secret than the default'; 43 | * return $settings; 44 | * } 45 | */ 46 | $this->default_settings = (array) apply_filters( $this->plugin_prefix . 'default_settings', array( 47 | 'enable' => 1, 48 | 'enable_scheduled_post_validation' => 0, 49 | 'secret_string' => md5( __FILE__ . $blog_id ), 50 | ) ); 51 | 52 | /** 53 | * Define fields that will be used on the options page 54 | * 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 55 | * a filter similar to the default settings (ie wpcroncontrol_settings_texts) can be used to alter this values 56 | */ 57 | $this->settings_texts = (array) apply_filters( $this->plugin_prefix . 'settings_texts', array( 58 | 'enable' => array( 59 | 'label' => sprintf( __( 'Enable %s', 'wp-cron-control' ), $this->plugin_name ), 60 | 'desc' => sprintf( __( 'Enable this plugin and allow requests to %s only with the appended secret parameter.', 'wp-cron-control' ), 'wp-cron.php' ), 61 | 'type' => 'yesno' 62 | ), 63 | 'secret_string' => array( 64 | 'label' => __( 'Secret string', 'wp-cron-control' ), 65 | 'desc' => sprintf( __( 'The secret parameter that needs to be appended to %s requests.', 'wp-cron-control' ), 'wp-cron.php' ), 66 | 'type' => 'text' 67 | ), 68 | 'enable_scheduled_post_validation' => array( 69 | 'label' => __( 'Enable scheduled post validation', 'wp-cron-control' ), 70 | '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' ), 'wp-cron' ), 71 | 'type' => 'yesno' 72 | ), 73 | ) ); 74 | 75 | $user_settings = get_option( $this->plugin_prefix . 'settings' ); 76 | if ( false === $user_settings ) 77 | $user_settings = array(); 78 | 79 | // after getting default settings make sure to parse the arguments together with the user settings 80 | $this->settings = wp_parse_args( $user_settings, $this->default_settings ); 81 | 82 | /** 83 | * If you define( 'WP_CRON_CONTROL_SECRET', 'my_super_secret_string' ); in your wp-config.php or your theme then 84 | * users are not allowed to change the secret, so we output the existing secret string rather than allowing to add a new one 85 | */ 86 | if ( defined( 'WP_CRON_CONTROL_SECRET' ) ) { 87 | $this->settings_texts['secret_string']['type'] = 'echo'; 88 | $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' ), "WP_CRON_CONTROL_SECRET" ); 89 | $this->settings['secret_string'] = WP_CRON_CONTROL_SECRET; 90 | } 91 | 92 | } 93 | 94 | public static function init() { 95 | self::instance()->settings_page_name = sprintf( __( '%s Settings', 'wp-cron-control' ), self::instance()->plugin_name ); 96 | 97 | if ( 1 == self::instance()->settings['enable'] ) { 98 | self::instance()->prepare(); 99 | } 100 | } 101 | 102 | /* 103 | * Use this singleton to address methods 104 | */ 105 | public static function instance() { 106 | if ( self::$__instance == NULL ) 107 | self::$__instance = new WP_Cron_Control; 108 | return self::$__instance; 109 | } 110 | 111 | public function prepare() { 112 | /** 113 | * If a css file for this plugin exists in ./css/wp-cron-control.css make sure it's included 114 | */ 115 | if ( file_exists( dirname( __FILE__ ) . "/css/" . $this->dashed_name . ".css" ) ) 116 | wp_enqueue_style( $this->dashed_name, plugins_url( "css/" . $this->dashed_name . ".css", __FILE__ ), $deps = array(), $this->css_version ); 117 | /** 118 | * If a js file for this plugin exists in ./js/wp-cron-control.css make sure it's included 119 | */ 120 | if ( file_exists( dirname( __FILE__ ) . "/js/" . $this->dashed_name . ".js" ) ) 121 | wp_enqueue_script( $this->dashed_name, plugins_url( "js/" . $this->dashed_name . ".js", __FILE__ ), array(), $this->js_version, true ); 122 | 123 | /** 124 | * When the plugin is enabled make sure remove the default behavior for issueing wp-cron requests and add our own method 125 | * see: http://core.trac.wordpress.org/browser/trunk/wp-includes/default-filters.php#L236 126 | * and http://core.trac.wordpress.org/browser/trunk/wp-includes/cron.php#L258 127 | */ 128 | if ( 1 == $this->settings['enable'] ) { 129 | remove_action( 'init', 'wp_cron' ); 130 | add_action( 'init', array( &$this, 'validate_cron_request' ) ); 131 | } 132 | 133 | } 134 | 135 | public function register_settings_page() { 136 | add_options_page( $this->settings_page_name, $this->plugin_name, 'manage_options', $this->dashed_name, array( &$this, 'settings_page' ) ); 137 | } 138 | 139 | public function register_setting() { 140 | register_setting( $this->plugin_prefix . 'settings', $this->plugin_prefix . 'settings', array( &$this, 'validate_settings') ); 141 | } 142 | 143 | public function validate_settings( $settings ) { 144 | $validated_settings = array(); 145 | 146 | if ( !empty( $_POST[ $this->dashed_name . '-defaults'] ) ) { 147 | // Reset to defaults 148 | $validated_settings = $this->default_settings; 149 | $_REQUEST['_wp_http_referer'] = add_query_arg( 'defaults', 'true', $_REQUEST['_wp_http_referer'] ); 150 | } else { 151 | foreach ( $this->settings_texts as $setting => $setting_info ) { 152 | switch( $setting ) { 153 | case 'enable': 154 | case 'enable_scheduled_post_validation': 155 | $validated_settings[ $setting ] = intval( $settings[ $setting ] ); 156 | if ( $validated_settings[ $setting ] > 1 || $validated_settings[ $setting ] < 0 ) { 157 | $validated_settings[ $setting ] = $this->default_settings[ $setting ]; 158 | } 159 | break; 160 | 161 | case 'secret_string': 162 | $validated_settings[ $setting ] = sanitize_text_field( $settings[ $setting ] ); 163 | if ( empty( $validated_settings[ $setting ] ) ) { 164 | $validated_settings[ $setting ] = $this->default_settings[ $setting ]; 165 | } 166 | break; 167 | 168 | default: 169 | $validated_settings[ $setting ] = sanitize_text_field( $settings[ $setting ] ); 170 | break; 171 | } 172 | } 173 | } 174 | 175 | return $validated_settings; 176 | } 177 | 178 | public function settings_page() { 179 | if ( !current_user_can( 'manage_options' ) ) { 180 | wp_die( __( 'You do not permission to access this page' ) ); 181 | } 182 | ?> 183 |
184 | 185 |

settings_page_name; ?>

186 | 187 |
188 | 189 | plugin_prefix . 'settings' ); ?> 190 | 191 | 192 | settings as $setting => $value): ?> 193 | 194 | 195 | 224 | 225 | 226 | settings['enable'] ): ?> 227 | 228 | 243 | 244 | 245 |
196 | 201 | settings_texts[$setting]['type'] ): 202 | case 'yesno': ?> 203 |
211 | 213 |
214 | 216 |
217 | 219 | settings_texts[$setting]['type'] ); ?> 220 | 222 | settings_texts[$setting]['desc'] ) ) { echo wp_kses_post( $this->settings_texts[$setting]['desc'] ); } ?> 223 |
229 |

plugin_name, 'wp-cron.php' ); ?>

230 | 231 |

232 | 233 |

php settings['secret_string']; ?>

234 |

or

235 |

wget -q "/wp-cron.php?doing_wp_cron&settings['secret_string']; ?>"

236 | 237 |

238 | 239 |

240 | 241 |

CPanel', 'Plesk', 'crontab' ); ?>

242 |
246 | 247 |

248 | dashed_name . '-submit', false ); 251 | echo ' '; 252 | submit_button( __( 'Reset to Defaults', 'wp-cron-control' ), '', $this->dashed_name . '-defaults', false ); 253 | } else { 254 | echo '' . "\n"; 255 | echo '' . "\n"; 256 | } 257 | ?> 258 |

259 | 260 |
261 |
262 | 263 | settings['secret_string']; 277 | 278 | // make sure a secret string is provided in the ur 279 | if ( isset( $_GET[$secret] ) ) { 280 | 281 | // check if there is already a cron request running 282 | $local_time = time(); 283 | if ( function_exists( '_get_cron_lock' ) ) 284 | $flag = _get_cron_lock(); 285 | else 286 | $flag = get_transient('doing_cron'); 287 | 288 | if ( defined( 'WP_CRON_LOCK_TIMEOUT' ) ) 289 | $timeout = WP_CRON_LOCK_TIMEOUT; 290 | else 291 | $timeout = 60; 292 | 293 | if ( $flag > $local_time + 10 * $timeout ) 294 | $flag = 0; 295 | 296 | // don't run if another process is currently running it or more than once every 60 sec. 297 | if ( $flag + $timeout > $local_time ) 298 | die( 'another cron process running or previous not older than 60 secs' ); 299 | 300 | // set a transient to allow locking down parallel requests 301 | set_transient( 'doing_cron', $local_time ); 302 | 303 | // make sure the request also validates in wp-cron.php 304 | global $doing_wp_cron; 305 | $doing_wp_cron = $local_time; 306 | 307 | // if settings allow it validate if there are any scheduled posts without a cron event 308 | if ( 1 == self::instance()->settings['enable_scheduled_post_validation'] ) { 309 | $this->validate_scheduled_posts(); 310 | } 311 | return true; 312 | } 313 | // something went wrong 314 | die( 'invalid secret string' ); 315 | } 316 | 317 | // for all other cases disable wp-cron.php and spawn_cron() by telling the system it's already running 318 | //if ( !defined( 'DOING_CRON' ) ) 319 | // define( 'DOING_CRON', true ); 320 | 321 | // and also disable the wp_cron() call execution 322 | if ( !defined( 'DISABLE_WP_CRON' ) ) 323 | define( 'DISABLE_WP_CRON', true ); 324 | return false; 325 | } 326 | 327 | public function validate_scheduled_posts() { 328 | global $wpdb; 329 | 330 | $return_value = true; 331 | 332 | $offset = 0; 333 | $limit = 30; 334 | 335 | while ( true ) { 336 | // grab batch of scheduled posts 337 | // uses `post_date` and converts to GMT later, rather than pulling `post_date_gmt`, to leverage `type_status_date` index 338 | $results = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_date FROM $wpdb->posts WHERE post_status = 'future' LIMIT %d,%d", $offset, $limit ) ); 339 | $offset += $limit; 340 | 341 | // if none exists just return 342 | if ( empty( $results ) ) { 343 | return $return_value; 344 | } 345 | 346 | // otherwise check each of them 347 | foreach ( $results as $r ) { 348 | $gmt_time = strtotime( get_gmt_from_date( $r->post_date ) . ' GMT' ); 349 | 350 | // grab the scheduled job for this post 351 | $timestamp = wp_next_scheduled( 'publish_future_post', array( (int) $r->ID ) ); 352 | if ( false === $timestamp ) { 353 | // if none exists issue one 354 | wp_schedule_single_event( $gmt_time, 'publish_future_post', array( (int) $r->ID ) ); 355 | $return_value = false; 356 | } elseif ( (int) $timestamp !== $gmt_time ) { 357 | wp_clear_scheduled_hook( 'publish_future_post', array( (int) $r->ID ) ); 358 | wp_schedule_single_event( $gmt_time, 'publish_future_post', array( (int) $r->ID ) ); 359 | $return_value = false; 360 | } 361 | } 362 | } 363 | 364 | return $return_value; 365 | } 366 | } 367 | 368 | /** 369 | * This method can be used to initiate a cron call via cli 370 | */ 371 | function wp_cron_control_call_cron( $blog_address, $secret ) { 372 | $cron_url = $blog_address . '/wp-cron.php?doing_wp_cron&' . $secret; 373 | $ch = curl_init( $cron_url ); 374 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 0 ); 375 | curl_setopt( $ch, CURLOPT_TIMEOUT, '3' ); 376 | $result = curl_exec( $ch ); 377 | curl_close( $ch ); 378 | return $result; 379 | } 380 | 381 | // if we loaded wp-config then ABSPATH is defined and we know the script was not called directly to issue a cli call 382 | if ( defined('ABSPATH') ) { 383 | WP_Cron_Control::init(); 384 | } else { 385 | // otherwise parse the arguments and call the cron. 386 | if ( !empty( $argv ) && $argv[0] == basename( __FILE__ ) || $argv[0] == __FILE__ ) { 387 | if ( isset( $argv[1] ) && isset( $argv[2] ) ) { 388 | wp_cron_control_call_cron( $argv[1], $argv[2] ); 389 | } else { 390 | echo "Usage: php " . __FILE__ . " \n"; 391 | echo "Example: php " . __FILE__ . " http://my.blog.com efe18b0e53498e737da9b91cf4ca3d25\n"; 392 | exit; 393 | } 394 | } 395 | } 396 | 397 | --------------------------------------------------------------------------------