PluginProbe
WebberZone Top 10 — Popular Posts / 4.4.0
WebberZone Top 10 — Popular Posts v4.4.0
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.0, at includes/admin/class-cron.php

343 lines 9.8 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 $timestamp = wp_next_scheduled( 'tptn_aggregation_cron_hook' );
196
197 if ( ! $timestamp ) {
198 self::enable_aggregation_run();
199 $this->aggregation_cron_was_missing = true;
200 return;
201 }
202
203 $crons = _get_cron_array();
204 $args_key = md5( serialize( array() ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
205 $current = isset( $crons[ $timestamp ]['tptn_aggregation_cron_hook'][ $args_key ]['schedule'] )
206 ? $crons[ $timestamp ]['tptn_aggregation_cron_hook'][ $args_key ]['schedule']
207 : '';
208
209 if ( $current !== $interval ) {
210 wp_clear_scheduled_hook( 'tptn_aggregation_cron_hook' );
211 self::enable_aggregation_run();
212 $this->aggregation_cron_interval_changed = true;
213 }
214 }
215
216 /**
217 * Show an admin notice when the aggregation cron was missing or rescheduled due to an interval change.
218 *
219 * @since 4.3.0
220 */
221 public function aggregation_cron_missing_notice() {
222 if ( $this->aggregation_cron_interval_changed ) {
223 ?>
224 <div class="notice notice-info is-dismissible">
225 <p>
226 <?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' ); ?>
227 </p>
228 </div>
229 <?php
230 } elseif ( $this->aggregation_cron_was_missing ) {
231 ?>
232 <div class="notice notice-warning is-dismissible">
233 <p>
234 <?php esc_html_e( 'Top 10: The visit aggregation cron job (tptn_aggregation_cron_hook) was missing and has been rescheduled automatically.', 'top-10' ); ?>
235 </p>
236 </div>
237 <?php
238 }
239
240 foreach ( self::TRACKED_HOOKS as $hook ) {
241 $error = self::get_reschedule_error( $hook );
242 if ( ! $error ) {
243 continue;
244 }
245 ?>
246 <div class="notice notice-warning is-dismissible">
247 <p>
248 <?php
249 printf(
250 /* translators: 1: Hook name, 2: Error message, 3: Human-readable time difference. */
251 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' ),
252 '<code>' . esc_html( $hook ) . '</code>',
253 esc_html( $error['message'] ),
254 esc_html( human_time_diff( $error['time'] ) )
255 );
256 ?>
257 </p>
258 </div>
259 <?php
260 }
261 }
262
263 /**
264 * Record a core WP-Cron reschedule failure for one of our hooks.
265 *
266 * Fired from wp-cron.php when the real cron runner fails to persist the
267 * next occurrence of a recurring event (`_set_cron_array()` failure).
268 *
269 * @since 4.4.0
270 *
271 * @param mixed $result Expected to be a WP_Error when core reports a failure.
272 * @param string $hook Hook name the error occurred for.
273 */
274 public function log_reschedule_error( $result, $hook ) {
275 if ( ! in_array( $hook, self::TRACKED_HOOKS, true ) || ! is_wp_error( $result ) ) {
276 return;
277 }
278
279 self::set_reschedule_error( $hook, $result );
280 }
281
282 /**
283 * Record a core WP-Cron unschedule failure for one of our hooks.
284 *
285 * @since 4.4.0
286 *
287 * @param mixed $result Expected to be a WP_Error when core reports a failure.
288 * @param string $hook Hook name the error occurred for.
289 */
290 public function log_unschedule_error( $result, $hook ) {
291 if ( ! in_array( $hook, self::TRACKED_HOOKS, true ) || ! is_wp_error( $result ) ) {
292 return;
293 }
294
295 self::set_reschedule_error( $hook, $result );
296 }
297
298 /**
299 * Store the last cron scheduling error for a hook.
300 *
301 * @since 4.4.0
302 *
303 * @param string $hook Hook name.
304 * @param \WP_Error $result Error returned by WordPress core.
305 */
306 private static function set_reschedule_error( $hook, $result ) {
307 set_transient(
308 self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook,
309 array(
310 'code' => $result->get_error_code(),
311 'message' => $result->get_error_message(),
312 'time' => time(),
313 ),
314 WEEK_IN_SECONDS
315 );
316 }
317
318 /**
319 * Retrieve the last recorded cron scheduling error for a hook, if any.
320 *
321 * @since 4.4.0
322 *
323 * @param string $hook Hook name.
324 * @return array{code: string, message: string, time: int}|false Error data, or false if none recorded.
325 */
326 public static function get_reschedule_error( $hook ) {
327 $error = get_transient( self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook );
328
329 return is_array( $error ) ? $error : false;
330 }
331
332 /**
333 * Clear the recorded cron scheduling errors for all tracked hooks.
334 *
335 * @since 4.4.0
336 */
337 public static function clear_reschedule_errors() {
338 foreach ( self::TRACKED_HOOKS as $hook ) {
339 delete_transient( self::RESCHEDULE_ERROR_TRANSIENT_PREFIX . $hook );
340 }
341 }
342 }
343