PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 1.6.2 All 116 releases
top-10 / includes / admin / class-cron.php

class-cron.php in WebberZone Top 10 — Popular Posts trunk, at includes/admin/class-cron.php

374 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cron class.
4 *
5 * @package WebberZone\Top_Ten\Admin
6 */
7
8 namespace WebberZone\Top_Ten\Admin;
9
10 use WebberZone\Top_Ten\Database;
11 use WebberZone\Top_Ten\Util\Hook_Registry;
12
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 /**
18 * Class Cron
19 */
20 class Cron {
21 /**
22 * Whether the aggregation cron was missing and had to be rescheduled.
23 *
24 * @var bool
25 */
26 private bool $aggregation_cron_was_missing = false;
27
28 /**
29 * Whether the aggregation cron interval changed and had to be rescheduled.
30 *
31 * @var bool
32 */
33 private bool $aggregation_cron_interval_changed = false;
34
35 /**
36 * Transient key prefix used to record core cron reschedule failures for our hooks.
37 *
38 * @since 4.4.0
39 */
40 private const RESCHEDULE_ERROR_TRANSIENT_PREFIX = 'tptn_cron_reschedule_error_';
41
42 /**
43 * Hooks that this class schedules and should track reschedule errors for.
44 *
45 * @since 4.4.0
46 */
47 private const TRACKED_HOOKS = array( 'tptn_cron_hook', 'tptn_aggregation_cron_hook' );
48
49 /**
50 * Initialize the class.
51 */
52 public function __construct() {
53 Hook_Registry::add_action( 'tptn_cron_hook', array( $this, 'run_cron' ) );
54 Hook_Registry::add_action( 'tptn_aggregation_cron_hook', array( $this, 'run_aggregation' ) );
55 Hook_Registry::add_action( 'tptn_count_updated', array( Dashboard_Widgets::class, 'clear_network_dashboard_cache' ) );
56 Hook_Registry::add_action( 'tptn_delete_counts', array( Dashboard_Widgets::class, 'clear_network_dashboard_cache' ) );
57 Hook_Registry::add_action( 'tptn_set_count', array( Dashboard_Widgets::class, 'clear_network_dashboard_cache' ) );
58 Hook_Registry::add_action( 'admin_init', array( $this, 'check_aggregation_cron' ) );
59 Hook_Registry::add_action( 'admin_notices', array( $this, 'aggregation_cron_missing_notice' ) );
60 Hook_Registry::add_action( 'cron_reschedule_event_error', array( $this, 'log_reschedule_error' ), 10, 2 );
61 Hook_Registry::add_action( 'cron_unschedule_event_error', array( $this, 'log_unschedule_error' ), 10, 2 );
62 }
63
64 /**
65 * Function to truncate daily run.
66 *
67 * @since 3.0.0
68 */
69 public function run_cron() {
70 global $wpdb;
71
72 $table_statuses = Database::get_table_installation_status( true );
73 if ( in_array( false, $table_statuses, true ) ) {
74 return;
75 }
76
77 $delete_from = TOP_TEN_STORE_DATA;
78
79 /**
80 * Override maintenance day range.
81 *
82 * @since 2.5.0
83 *
84 * @param int $delete_from Number of days before which post data is deleted from daily tables.
85 */
86 $delete_from = apply_filters( 'tptn_maintenance_days', $delete_from );
87
88 $log_retention = TOP_TEN_LOG_STORE_DATA;
89
90 /**
91 * Override retention period for the visits log table.
92 *
93 * @since 4.3.0
94 *
95 * @param int $log_retention Number of days to retain raw visit log rows.
96 */
97 $log_retention = apply_filters( 'tptn_log_retention_days', $log_retention );
98
99 $current_time = strtotime( current_time( 'mysql' ) );
100 $cutoff = strtotime( "-{$delete_from} DAY", $current_time );
101 $cutoff_log = strtotime( "-{$log_retention} DAY", $current_time );
102 $from_date = gmdate( 'Y-m-d H', $cutoff );
103 $from_date_log = gmdate( 'Y-m-d H:i:s', $cutoff_log );
104
105 // Delete old daily entries.
106 $args = array(
107 'daily' => true,
108 'to_date' => $from_date,
109 'limit' => 1000,
110 );
111
112 $deadline = microtime( true ) + 20;
113 do {
114 $deleted = Database::delete_counts( $args );
115 } while ( $deleted > 0 && microtime( true ) < $deadline );
116
117 // Prune visits log rows older than the retention period.
118 $log_table = Database::get_log_table();
119 $deadline_log = microtime( true ) + 20;
120 do {
121 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
122 $deleted_log = $wpdb->query( $wpdb->prepare( "DELETE FROM {$log_table} WHERE visited_at < %s LIMIT 1000", $from_date_log ) );
123 } while ( $deleted_log > 0 && microtime( true ) < $deadline_log );
124
125 Dashboard_Widgets::clear_network_dashboard_cache();
126 }
127
128 /**
129 * Function to enable run or actions.
130 *
131 * @since 3.0.0
132 * @param int $hour Hour.
133 * @param int $min Minute.
134 * @param string $recurrence Frequency.
135 */
136 public static function enable_run( $hour, $min, $recurrence ) {
137 // Invoke WordPress internal cron.
138 if ( ! wp_next_scheduled( 'tptn_cron_hook' ) ) {
139 wp_schedule_event( mktime( $hour, $min, 0 ), $recurrence, 'tptn_cron_hook' );
140 } else {
141 wp_clear_scheduled_hook( 'tptn_cron_hook' );
142 wp_schedule_event( mktime( $hour, $min, 0 ), $recurrence, 'tptn_cron_hook' );
143 }
144 }
145
146 /**
147 * Function to disable daily run or actions.
148 *
149 * @since 3.0.0
150 */
151 public static function disable_run() {
152 if ( wp_next_scheduled( 'tptn_cron_hook' ) ) {
153 wp_clear_scheduled_hook( 'tptn_cron_hook' );
154 }
155 }
156
157 /**
158 * Drain the visits log into the count tables and prune orphaned log rows.
159 *
160 * @since 4.3.0
161 */
162 public function run_aggregation() {
163 Database::aggregate_visit_log();
164 }
165
166 /**
167 * Schedule the aggregation cron to run at the configured interval.
168 *
169 * @since 4.3.0
170 */
171 public static function enable_aggregation_run() {
172 /**
173 * Override the aggregation cron schedule. Must be a registered WP-Cron recurrence
174 * (e.g. 'one_minute', 'two_minutes', 'three_minutes', 'five_minutes').
175 *
176 * @since 4.3.0
177 *
178 * @param string $interval Schedule name. Default 'two_minutes'.
179 */
180 $interval = (string) apply_filters( 'tptn_aggregation_cron_interval', 'two_minutes' );
181
182 if ( ! wp_next_scheduled( 'tptn_aggregation_cron_hook' ) ) {
183 wp_schedule_event( time(), $interval, 'tptn_aggregation_cron_hook' );
184 }
185 }
186
187 /**
188 * Remove the aggregation cron.
189 *
190 * @since 4.3.0
191 */
192 public static function disable_aggregation_run() {
193 wp_clear_scheduled_hook( 'tptn_aggregation_cron_hook' );
194 }
195
196 /**
197 * Check that the aggregation cron is scheduled at the correct interval; reschedule if missing or changed.
198 *
199 * @since 4.3.0
200 */
201 public function check_aggregation_cron() {
202 /** This filter is documented in includes/admin/class-cron.php */
203 $interval = (string) apply_filters( 'tptn_aggregation_cron_interval', 'two_minutes' );
204
205 if ( ! wp_next_scheduled( 'tptn_aggregation_cron_hook' ) ) {
206 self::enable_aggregation_run();
207 $this->aggregation_cron_was_missing = true;
208 return;
209 }
210
211 // Look across all scheduled occurrences of the hook, not just the earliest one:
212 // a legitimate one-off catch-up event (schedule = false) can be scheduled alongside
213 // the recurring event and must not be mistaken for a wrong/missing recurring schedule.
214 $args_key = md5( serialize( array() ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
215 $crons = _get_cron_array();
216 $has_correct_recurrence = false;
217
218 foreach ( $crons as $events ) {
219 if ( isset( $events['tptn_aggregation_cron_hook'][ $args_key ]['schedule'] )
220 && $interval === $events['tptn_aggregation_cron_hook'][ $args_key ]['schedule']
221 ) {
222 $has_correct_recurrence = true;
223 break;
224 }
225 }
226
227 if ( ! $has_correct_recurrence ) {
228 wp_clear_scheduled_hook( 'tptn_aggregation_cron_hook' );
229 self::enable_aggregation_run();
230 $this->aggregation_cron_interval_changed = true;
231 }
232 }
233
234 /**
235 * Show an admin notice when the aggregation cron was missing or rescheduled due to an interval change.
236 *
237 * @since 4.3.0
238 */
239 public function aggregation_cron_missing_notice() {
240 if ( $this->aggregation_cron_interval_changed ) {
241 ?>
242 <div class="notice notice-info is-dismissible">
243 <p>
244 <?php esc_html_e( 'Top 10: The visit aggregation cron job (tptn_aggregation_cron_hook) has been rescheduled to match the updated interval.', 'top-10' ); ?>
245 </p>
246 </div>
247 <?php
248 } elseif ( $this->aggregation_cron_was_missing ) {
249 ?>
250 <div class="notice notice-warning is-dismissible">
251 <p>
252 <?php esc_html_e( 'Top 10: The visit aggregation cron job (tptn_aggregation_cron_hook) was missing and has been rescheduled automatically.', 'top-10' ); ?>
253 </p>
254 </div>
255 <?php
256 }
257
258 foreach ( self::TRACKED_HOOKS as $hook ) {
259 $error = self::get_reschedule_error( $hook );
260 if ( ! $error ) {
261 continue;
262 }
263 ?>
264 <div class="notice notice-warning is-dismissible">
265 <p>
266 <?php
267 printf(
268 /* translators: 1: Hook name, 2: Error message, 3: Human-readable time difference. */
269 esc_html__( 'Top 10: WP-Cron reported an error rescheduling %1$s: %2$s (%3$s ago). Use the "Fix Cron Schedules" tool on the Top 10 Tools page if the job stops running.', 'top-10' ),
270 '<code>' . esc_html( $hook ) . '</code>',
271 esc_html( $error['message'] ),
272 esc_html( human_time_diff( $error['time'] ) )
273 );
274 ?>
275 </p>
276 </div>
277 <?php
278 }
279 }
280
281 /**
282 * Record a core WP-Cron reschedule failure for one of our hooks.
283 *
284 * Fired from wp-cron.php when the real cron runner fails to persist the
285 * next occurrence of a recurring event (`_set_cron_array()` failure).
286 *
287 * @since 4.4.0
288 *
289 * @param mixed $result Expected to be a WP_Error when core reports a failure.
290 * @param string $hook Hook name the error occurred for.
291 */
292 public function log_reschedule_error( $result, $hook ) {
293 if ( ! in_array( $hook, self::TRACKED_HOOKS, true ) || ! is_wp_error( $result ) ) {
294 return;
295 }
296
297 self::set_reschedule_error( $hook, $result );
298 }
299
300 /**
301 * Record a core WP-Cron unschedule failure for one of our hooks.
302 *
303 * @since 4.4.0
304 *
305 * @param mixed $result Expected to be a WP_Error when core reports a failure.
306 * @param string $hook Hook name the error occurred for.
307 */
308 public function log_unschedule_error( $result, $hook ) {
309 if ( ! in_array( $hook, self::TRACKED_HOOKS, true ) || ! is_wp_error( $result ) ) {
310 return;
311 }
312
313 self::set_reschedule_error( $hook, $result );
314 }
315
316 /**
317 * Store the last cron scheduling error for a hook.
318 *
319 * @since 4.4.0
320 *
321 * @param string $hook Hook name.
322 * @param \WP_Error $result Error returned by WordPress core.
323 */
324 private static function set_reschedule_error( $hook, $result ) {
325 set_transient(
326 self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook,
327 array(
328 'code' => $result->get_error_code(),
329 'message' => $result->get_error_message(),
330 'time' => time(),
331 ),
332 WEEK_IN_SECONDS
333 );
334 }
335
336 /**
337 * Retrieve the last recorded cron scheduling error for a hook, if any.
338 *
339 * Core reports a reschedule failure when update_option( 'cron' ) is a no-op —
340 * e.g. a concurrent cron run already saved the identical schedule. If the hook
341 * has a valid next occurrence the recorded error is stale: clear and ignore it.
342 *
343 * @since 4.4.0
344 *
345 * @param string $hook Hook name.
346 * @return array{code: string, message: string, time: int}|false Error data, or false if none recorded.
347 */
348 public static function get_reschedule_error( $hook ) {
349 $error = get_transient( self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook );
350
351 if ( ! is_array( $error ) ) {
352 return false;
353 }
354
355 if ( wp_next_scheduled( $hook ) ) {
356 delete_transient( self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook );
357 return false;
358 }
359
360 return $error;
361 }
362
363 /**
364 * Clear the recorded cron scheduling errors for all tracked hooks.
365 *
366 * @since 4.4.0
367 */
368 public static function clear_reschedule_errors() {
369 foreach ( self::TRACKED_HOOKS as $hook ) {
370 delete_transient( self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook );
371 }
372 }
373 }
374