PluginProbe
WebberZone Top 10 — Popular Posts / 4.4.3
WebberZone Top 10 — Popular Posts v4.4.3
4.5.1 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 All 117 releases
top-10 / includes / admin / class-cron.php

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

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