PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 All 138 releases
wpforo / admin / tools-tabs / cron-jobs.php

cron-jobs.php in wpForo Forum 3.1.6, at admin/tools-tabs/cron-jobs.php

636 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * wpForo Tools — Cron Jobs tab
4 *
5 * Lists every WP-Cron event registered in this WordPress install with
6 * wpForo events ordered first. Each row exposes Run Now and Delete
7 * buttons. Helps diagnose `wp_options.cron` bloat and verify whether
8 * AI / Email Queue / other wpForo crons are scheduled correctly.
9 */
10
11 // Exit if accessed directly
12 if ( ! defined( 'ABSPATH' ) ) exit;
13 if ( ! current_user_can( 'administrator' ) ) exit;
14
15 $wpf_cron_nonce_action = 'wpforo_cron_jobs';
16
17 // -----------------------------------------------------------------------
18 // POST handler — run / delete / run-all-wpforo
19 // Tools page emits HTML before including this tab file, so a redirect
20 // here would land after "headers already sent". We process inline,
21 // add notices via WPF()->notice, and fall through to render — matching
22 // the existing Email Queue tab pattern. Actions are idempotent
23 // enough (re-running a cron just fires it again; re-deleting is a
24 // silent no-op) that a browser refresh re-submitting is safe.
25 // -----------------------------------------------------------------------
26 if (
27 'POST' === ( $_SERVER['REQUEST_METHOD'] ?? '' )
28 && isset( $_POST['wpforo_cron_action'] )
29 && wp_verify_nonce( wpfval( $_POST, '_wpnonce' ), $wpf_cron_nonce_action )
30 ) {
31 $action = sanitize_text_field( wp_unslash( $_POST['wpforo_cron_action'] ) );
32 $timestamp = absint( wpfval( $_POST, 'timestamp' ) );
33 $hook = sanitize_text_field( wpfval( $_POST, 'hook' ) );
34 $md5 = sanitize_text_field( wpfval( $_POST, 'md5' ) );
35
36 $events = function_exists( '_get_cron_array' ) ? _get_cron_array() : [];
37 if ( ! is_array( $events ) ) $events = [];
38
39 switch ( $action ) {
40
41 case 'run':
42 if ( $timestamp && $hook && $md5 && isset( $events[ $timestamp ][ $hook ][ $md5 ] ) ) {
43 $entry = $events[ $timestamp ][ $hook ][ $md5 ];
44 $args = isset( $entry['args'] ) ? (array) $entry['args'] : [];
45 $schedule = $entry['schedule'] ?? false;
46
47 // For single events: unschedule before firing so the queued one
48 // doesn't run a second time. For recurring events: leave the
49 // schedule alone — the regular cadence resumes after we fire.
50 if ( false === $schedule ) {
51 wp_unschedule_event( $timestamp, $hook, $args );
52 }
53
54 try {
55 do_action_ref_array( $hook, $args );
56 WPF()->notice->add(
57 sprintf(
58 /* translators: %s = cron hook name */
59 __( 'Cron event "%s" executed.', 'wpforo' ),
60 $hook
61 ),
62 'success'
63 );
64 } catch ( \Throwable $e ) {
65 WPF()->notice->add(
66 sprintf(
67 /* translators: 1: hook name, 2: error message */
68 __( 'Cron event "%1$s" raised an exception: %2$s', 'wpforo' ),
69 $hook,
70 $e->getMessage()
71 ),
72 'error'
73 );
74 }
75 } else {
76 WPF()->notice->add(
77 __( 'Cron event no longer exists (likely already executed or rescheduled).', 'wpforo' ),
78 'warning'
79 );
80 }
81 break;
82
83 case 'delete':
84 if ( $timestamp && $hook && $md5 && isset( $events[ $timestamp ][ $hook ][ $md5 ] ) ) {
85 $args = (array) ( $events[ $timestamp ][ $hook ][ $md5 ]['args'] ?? [] );
86 $schedule = $events[ $timestamp ][ $hook ][ $md5 ]['schedule'] ?? false;
87
88 wp_unschedule_event( $timestamp, $hook, $args );
89
90 // Recurring event: also clear any future occurrences with the
91 // same args so the row truly goes away from the listing.
92 if ( false !== $schedule ) {
93 wp_clear_scheduled_hook( $hook, $args );
94 }
95
96 WPF()->notice->add(
97 sprintf(
98 /* translators: %s = cron hook name */
99 __( 'Cron event "%s" deleted.', 'wpforo' ),
100 $hook
101 ),
102 'success'
103 );
104 }
105 break;
106
107 case 'run_all_wpforo':
108 $ran = 0;
109 foreach ( $events as $ts => $hooks ) {
110 if ( ! is_array( $hooks ) ) continue;
111 foreach ( $hooks as $h => $items ) {
112 if ( strpos( $h, 'wpforo_' ) !== 0 ) continue;
113 if ( ! is_array( $items ) ) continue;
114 foreach ( $items as $item ) {
115 $args = isset( $item['args'] ) ? (array) $item['args'] : [];
116 $schedule = $item['schedule'] ?? false;
117 if ( false === $schedule ) {
118 wp_unschedule_event( $ts, $h, $args );
119 }
120 try {
121 do_action_ref_array( $h, $args );
122 $ran++;
123 } catch ( \Throwable $e ) {
124 WPF()->notice->add(
125 sprintf(
126 /* translators: 1: hook name, 2: error message */
127 __( 'Cron event "%1$s" raised an exception: %2$s', 'wpforo' ),
128 $h,
129 $e->getMessage()
130 ),
131 'error'
132 );
133 }
134 }
135 }
136 }
137 WPF()->notice->add(
138 sprintf(
139 /* translators: %d = number of cron events executed */
140 _n( '%d wpForo cron event executed.', '%d wpForo cron events executed.', $ran, 'wpforo' ),
141 $ran
142 ),
143 'success'
144 );
145 break;
146 }
147 }
148
149 // -----------------------------------------------------------------------
150 // Load + normalize + sort cron events
151 // -----------------------------------------------------------------------
152 $cron_array = function_exists( '_get_cron_array' ) ? _get_cron_array() : [];
153 if ( ! is_array( $cron_array ) ) $cron_array = [];
154
155 $rows = [];
156 $wpforo_count = 0;
157
158 foreach ( $cron_array as $timestamp => $hooks ) {
159 if ( ! is_array( $hooks ) ) continue;
160 foreach ( $hooks as $hook => $items ) {
161 if ( ! is_array( $items ) ) continue;
162 foreach ( $items as $md5 => $entry ) {
163 $is_wpforo = ( strpos( $hook, 'wpforo_' ) === 0 );
164 if ( $is_wpforo ) $wpforo_count++;
165 $rows[] = [
166 'timestamp' => (int) $timestamp,
167 'hook' => (string) $hook,
168 'md5' => (string) $md5,
169 'args' => isset( $entry['args'] ) ? (array) $entry['args'] : [],
170 'schedule' => $entry['schedule'] ?? false,
171 'interval' => isset( $entry['interval'] ) ? (int) $entry['interval'] : 0,
172 'is_wpforo' => $is_wpforo,
173 ];
174 }
175 }
176 }
177
178 // Sort: wpForo first (alphabetical by hook then by timestamp), then everything
179 // else by next-run time.
180 usort( $rows, function ( $a, $b ) {
181 if ( $a['is_wpforo'] !== $b['is_wpforo'] ) return $a['is_wpforo'] ? -1 : 1;
182 if ( $a['is_wpforo'] ) {
183 $h = strcmp( $a['hook'], $b['hook'] );
184 if ( $h !== 0 ) return $h;
185 return $a['timestamp'] <=> $b['timestamp'];
186 }
187 return $a['timestamp'] <=> $b['timestamp'];
188 } );
189
190 $now = time();
191 $total_count = count( $rows );
192 $cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
193 $cron_alt = defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON;
194 $schedules = function_exists( 'wp_get_schedules' ) ? wp_get_schedules() : [];
195
196 // Pretty-print interval helper
197 $wpf_fmt_interval = function ( $seconds ) {
198 $seconds = (int) $seconds;
199 if ( $seconds <= 0 ) return '—';
200 if ( $seconds < HOUR_IN_SECONDS ) {
201 $n = max( 1, (int) round( $seconds / MINUTE_IN_SECONDS ) );
202 /* translators: %d is the number of minutes */
203 return sprintf( _n( 'every %d minute', 'every %d minutes', $n, 'wpforo' ), $n );
204 }
205 if ( $seconds < DAY_IN_SECONDS ) {
206 $n = max( 1, (int) round( $seconds / HOUR_IN_SECONDS ) );
207 /* translators: %d is the number of hours */
208 return sprintf( _n( 'every %d hour', 'every %d hours', $n, 'wpforo' ), $n );
209 }
210 if ( $seconds < WEEK_IN_SECONDS ) {
211 $n = max( 1, (int) round( $seconds / DAY_IN_SECONDS ) );
212 /* translators: %d is the number of days */
213 return sprintf( _n( 'every %d day', 'every %d days', $n, 'wpforo' ), $n );
214 }
215 $n = max( 1, (int) round( $seconds / WEEK_IN_SECONDS ) );
216 /* translators: %d is the number of weeks */
217 return sprintf( _n( 'every %d week', 'every %d weeks', $n, 'wpforo' ), $n );
218 };
219
220 // Pretty-print relative time helper
221 $wpf_fmt_when = function ( $ts ) use ( $now ) {
222 $diff = $ts - $now;
223 if ( $diff < 0 ) return '<span style="color:#dc3232;font-weight:600;">' . esc_html__( 'past due', 'wpforo' ) . '</span>';
224 if ( $diff < MINUTE_IN_SECONDS ) return esc_html__( 'in <1 min', 'wpforo' );
225 if ( $diff < HOUR_IN_SECONDS ) {
226 $n = (int) round( $diff / MINUTE_IN_SECONDS );
227 /* translators: %d is the number of minutes — matches wpforo_ai_format_next_run_time() in ai-features-helpers.php */
228 return esc_html( sprintf( _n( 'in %d min', 'in %d mins', $n, 'wpforo' ), $n ) );
229 }
230 if ( $diff < DAY_IN_SECONDS ) {
231 $n = (int) round( $diff / HOUR_IN_SECONDS );
232 /* translators: %d is the number of hours */
233 return esc_html( sprintf( _n( 'in %d hour', 'in %d hours', $n, 'wpforo' ), $n ) );
234 }
235 $n = (int) round( $diff / DAY_IN_SECONDS );
236 /* translators: %d is the number of days */
237 return esc_html( sprintf( _n( 'in %d day', 'in %d days', $n, 'wpforo' ), $n ) );
238 };
239 ?>
240
241 <style>
242 .wpf-cron-summary {
243 display: flex;
244 gap: 15px;
245 margin: 15px 0;
246 flex-wrap: wrap;
247 align-items: center;
248 }
249 .wpf-cron-summary .wpf-cron-counter {
250 background: #fff;
251 border: 1px solid #ccd0d4;
252 border-radius: 4px;
253 padding: 8px 18px;
254 font-size: 13px;
255 }
256 .wpf-cron-summary .wpf-cron-counter strong { font-size: 18px; color: #1d2327; }
257 .wpf-cron-summary .wpf-cron-counter.wpforo strong { color: #0073aa; }
258
259 .wpf-cron-banner {
260 padding: 10px 15px;
261 border-radius: 4px;
262 margin: 10px 0;
263 }
264 .wpf-cron-banner.warning { background: #fff3cd; border: 1px solid #ffc107; color: #856404; }
265 .wpf-cron-banner.ok { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
266
267 .wpf-cron-table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #ccd0d4; }
268 .wpf-cron-table th, .wpf-cron-table td { padding: 9px 12px; text-align: left; border-bottom: 1px solid #eee; vertical-align: top; font-size: 13px; }
269 .wpf-cron-table th { background: #f6f7f7; font-weight: 600; }
270 .wpf-cron-table tr.wpforo-row td { background: #f6fbff; }
271 .wpf-cron-table tr:hover td { background: #f0f6fc; }
272
273 .wpf-cron-hook { font-family: Consolas, Monaco, monospace; word-break: break-all; }
274 .wpf-cron-hook .wpf-cron-badge { background: #0073aa; color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 3px; margin-right: 6px; font-family: -apple-system, sans-serif; font-weight: 600; vertical-align: middle; }
275
276 .wpf-cron-row-actions { display: inline-flex; gap: 6px; flex-wrap: wrap; }
277 .wpf-cron-row-actions form { margin: 0; display: inline-block; }
278 .wpf-cron-row-actions .button { font-size: 11px; height: auto; padding: 3px 9px; line-height: 1.4; }
279 .wpf-cron-row-actions .button.delete { color: #b32d2e; border-color: #b32d2e; }
280 .wpf-cron-row-actions .button.delete:hover { background: #b32d2e; color: #fff; }
281 .wpf-cron-row-actions .button.details { color: #2271b1; border-color: #2271b1; }
282 .wpf-cron-row-actions .button.details:hover { background: #2271b1; color: #fff; }
283
284 /* Details modal */
285 .wpf-cron-modal-overlay {
286 display: none;
287 position: fixed;
288 inset: 0;
289 background: rgba(0,0,0,0.55);
290 z-index: 99999;
291 align-items: flex-start;
292 justify-content: center;
293 overflow-y: auto;
294 padding: 60px 20px;
295 }
296 .wpf-cron-modal-overlay.is-open { display: flex; }
297 .wpf-cron-modal {
298 background: #fff;
299 border-radius: 6px;
300 max-width: 720px;
301 width: 100%;
302 box-shadow: 0 12px 32px rgba(0,0,0,0.25);
303 max-height: calc(100vh - 120px);
304 display: flex;
305 flex-direction: column;
306 }
307 .wpf-cron-modal-header {
308 display: flex;
309 align-items: center;
310 justify-content: space-between;
311 padding: 14px 18px;
312 border-bottom: 1px solid #e5e5e5;
313 }
314 .wpf-cron-modal-header h3 { margin: 0; font-size: 16px; }
315 .wpf-cron-modal-close {
316 background: transparent;
317 border: 0;
318 font-size: 22px;
319 line-height: 1;
320 cursor: pointer;
321 color: #555;
322 padding: 4px 8px;
323 }
324 .wpf-cron-modal-close:hover { color: #000; }
325 .wpf-cron-modal-body { padding: 16px 18px; overflow-y: auto; }
326 .wpf-cron-modal-body dl { margin: 0; }
327 .wpf-cron-modal-body dt {
328 font-weight: 600;
329 color: #1d2327;
330 margin-top: 12px;
331 margin-bottom: 4px;
332 font-size: 12px;
333 text-transform: uppercase;
334 letter-spacing: 0.04em;
335 }
336 .wpf-cron-modal-body dt:first-child { margin-top: 0; }
337 .wpf-cron-modal-body dd { margin: 0; font-size: 13px; word-break: break-all; }
338 .wpf-cron-modal-body dd code,
339 .wpf-cron-modal-body dd pre {
340 font-family: Consolas, Monaco, monospace;
341 font-size: 12px;
342 }
343 .wpf-cron-modal-body pre {
344 background: #f6f7f7;
345 border: 1px solid #e5e5e5;
346 border-radius: 3px;
347 padding: 10px;
348 max-height: 260px;
349 overflow: auto;
350 margin: 0;
351 white-space: pre-wrap;
352 word-break: break-word;
353 }
354
355 .wpf-cron-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; }
356 .wpf-cron-empty { padding: 30px; text-align: center; color: #666; background: #fff; border: 1px dashed #ccd0d4; border-radius: 4px; }
357 </style>
358
359 <div style="padding: 1%;">
360 <?php
361 // Render any notices added by the POST handler above. The tools.php
362 // wrapper already called WPF()->notice->show() before this tab was
363 // included, so anything we added since needs an explicit second flush
364 // here to surface on the current request.
365 WPF()->notice->show();
366 ?>
367 <h3 style="margin-top: 0;"><?php _e( 'WP-Cron Jobs', 'wpforo' ); ?></h3>
368 <p class="wpf-info">
369 <?php _e( 'All scheduled events in this WordPress install. wpForo events are listed first. Use Run Now to fire a single event immediately, or Delete to remove it from the schedule.', 'wpforo' ); ?>
370 </p>
371
372 <?php if ( $cron_disabled ) : ?>
373 <div class="wpf-cron-banner warning">
374 <strong><?php _e( 'WP-Cron is disabled.', 'wpforo' ); ?></strong>
375 <?php _e( '<code>DISABLE_WP_CRON</code> is set. Events stay scheduled but will not fire automatically — you need to trigger wp-cron.php via a system cron or call it manually.', 'wpforo' ); ?>
376 </div>
377 <?php elseif ( $cron_alt ) : ?>
378 <div class="wpf-cron-banner warning">
379 <strong><?php _e( 'Alternate WP-Cron in use.', 'wpforo' ); ?></strong>
380 <?php _e( '<code>ALTERNATE_WP_CRON</code> is set; events fire via redirect rather than WordPress\'s normal background spawn.', 'wpforo' ); ?>
381 </div>
382 <?php else : ?>
383 <div class="wpf-cron-banner ok">
384 <span class="dashicons dashicons-yes-alt" style="vertical-align: text-bottom;"></span>
385 <?php _e( 'WP-Cron is active. Events fire on the next page load past their scheduled time.', 'wpforo' ); ?>
386 </div>
387 <?php endif; ?>
388
389 <div class="wpf-cron-summary">
390 <div class="wpf-cron-counter">
391 <?php printf( __( '<strong>%d</strong> total scheduled events', 'wpforo' ), $total_count ); ?>
392 </div>
393 <div class="wpf-cron-counter wpforo">
394 <?php printf( __( '<strong>%d</strong> from wpForo', 'wpforo' ), $wpforo_count ); ?>
395 </div>
396 </div>
397
398 <?php if ( $wpforo_count > 0 ) : ?>
399 <div class="wpf-cron-toolbar">
400 <form method="post" style="margin: 0;">
401 <?php wp_nonce_field( $wpf_cron_nonce_action ); ?>
402 <input type="hidden" name="wpforo_cron_action" value="run_all_wpforo">
403 <button type="submit" class="button button-secondary"
404 onclick="return confirm('<?php echo esc_js( __( 'Run every scheduled wpForo cron event now? Long-running events may delay this admin page.', 'wpforo' ) ); ?>');">
405 <span class="dashicons dashicons-controls-play" style="vertical-align: text-bottom;"></span>
406 <?php _e( 'Run All wpForo Crons Now', 'wpforo' ); ?>
407 </button>
408 </form>
409 </div>
410 <?php endif; ?>
411
412 <?php if ( empty( $rows ) ) : ?>
413 <div class="wpf-cron-empty">
414 <?php _e( 'No scheduled events.', 'wpforo' ); ?>
415 </div>
416 <?php else : ?>
417 <table class="wpf-cron-table widefat striped">
418 <thead>
419 <tr>
420 <th><?php _e( 'Hook', 'wpforo' ); ?></th>
421 <th style="width: 18%;"><?php _e( 'Next run', 'wpforo' ); ?></th>
422 <th style="width: 14%;"><?php _e( 'Schedule', 'wpforo' ); ?></th>
423 <th style="width: 13%;"><?php _e( 'Interval', 'wpforo' ); ?></th>
424 <th style="width: 240px;"><?php _e( 'Actions', 'wpforo' ); ?></th>
425 </tr>
426 </thead>
427 <tbody>
428 <?php foreach ( $rows as $row ) :
429 // Schedule label: prefer the human-readable `display` field from
430 // wp_get_schedules() over the raw key. Some plugins (e.g. wpDiscuz
431 // uses `wpdiscuz_generate_thumbnails_every_3h`) namespace their
432 // schedule keys, so the display field is much more useful in the
433 // table cell.
434 $has_display = $row['schedule']
435 && isset( $schedules[ $row['schedule'] ]['display'] )
436 && $schedules[ $row['schedule'] ]['display'] !== '';
437 $schedule_lbl = $has_display
438 ? $schedules[ $row['schedule'] ]['display']
439 : ( $row['schedule'] ? $row['schedule'] : __( 'single event', 'wpforo' ) );
440 // Tooltip carries the raw key when a display was used, so it's
441 // still discoverable on hover.
442 $schedule_h = $has_display ? $row['schedule'] : $schedule_lbl;
443 $interval_lbl = $row['interval'] > 0 ? $wpf_fmt_interval( $row['interval'] ) : '—';
444 $next_abs = wp_date( 'Y-m-d H:i:s', $row['timestamp'] );
445 $row_class = $row['is_wpforo'] ? 'wpforo-row' : '';
446 ?>
447 <tr class="<?php echo esc_attr( $row_class ); ?>">
448 <td class="wpf-cron-hook">
449 <?php if ( $row['is_wpforo'] ) : ?>
450 <span class="wpf-cron-badge">wpForo</span>
451 <?php endif; ?>
452 <strong><?php echo esc_html( $row['hook'] ); ?></strong>
453 </td>
454 <td>
455 <div><?php echo $wpf_fmt_when( $row['timestamp'] ); ?></div>
456 <div style="color:#888;font-size:11px;"><?php echo esc_html( $next_abs ); ?></div>
457 </td>
458 <td>
459 <span title="<?php echo esc_attr( $schedule_h ); ?>">
460 <?php echo esc_html( $schedule_lbl ); ?>
461 </span>
462 </td>
463 <td><?php echo esc_html( $interval_lbl ); ?></td>
464 <td>
465 <div class="wpf-cron-row-actions">
466 <?php
467 // Build the full attribute payload for the Details modal.
468 // Pretty-print args JSON server-side so we don't depend on
469 // JSON.stringify formatting in JS.
470 $details_args_pretty = ( ! empty( $row['args'] ) )
471 ? wp_json_encode( $row['args'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
472 : __( '(no arguments)', 'wpforo' );
473 $details_payload = [
474 'hook' => $row['hook'],
475 'source' => $row['is_wpforo'] ? 'wpForo' : __( 'Other / WordPress core', 'wpforo' ),
476 'timestamp' => (int) $row['timestamp'],
477 'next_local' => wp_date( 'Y-m-d H:i:s T', $row['timestamp'] ),
478 'next_gmt' => gmdate( 'Y-m-d H:i:s', $row['timestamp'] ) . ' GMT',
479 'next_rel' => wp_strip_all_tags( $wpf_fmt_when( $row['timestamp'] ) ),
480 // Schedule: raw key + human-readable display (when registered)
481 'schedule' => $row['schedule'] ? $row['schedule'] : 'single_event',
482 'schedule_lbl' => $has_display ? $schedules[ $row['schedule'] ]['display'] : '',
483 'interval' => $row['interval'],
484 'interval_lbl' => $interval_lbl,
485 'args' => $details_args_pretty,
486 'md5' => $row['md5'],
487 ];
488 ?>
489 <button type="button" class="button details wpf-cron-details-btn"
490 data-payload="<?php echo esc_attr( wp_json_encode( $details_payload ) ); ?>"
491 title="<?php esc_attr_e( 'View full cron event attributes', 'wpforo' ); ?>">
492 <?php _e( 'Details', 'wpforo' ); ?>
493 </button>
494 <form method="post">
495 <?php wp_nonce_field( $wpf_cron_nonce_action ); ?>
496 <input type="hidden" name="wpforo_cron_action" value="run">
497 <input type="hidden" name="timestamp" value="<?php echo (int) $row['timestamp']; ?>">
498 <input type="hidden" name="hook" value="<?php echo esc_attr( $row['hook'] ); ?>">
499 <input type="hidden" name="md5" value="<?php echo esc_attr( $row['md5'] ); ?>">
500 <button type="submit" class="button" title="<?php esc_attr_e( 'Fire this event now', 'wpforo' ); ?>">
501 <?php _e( 'Run Now', 'wpforo' ); ?>
502 </button>
503 </form>
504 <form method="post"
505 onsubmit="return confirm('<?php echo esc_js( sprintf( __( 'Delete the scheduled event \"%s\"? This removes it from WP-Cron immediately.', 'wpforo' ), $row['hook'] ) ); ?>');">
506 <?php wp_nonce_field( $wpf_cron_nonce_action ); ?>
507 <input type="hidden" name="wpforo_cron_action" value="delete">
508 <input type="hidden" name="timestamp" value="<?php echo (int) $row['timestamp']; ?>">
509 <input type="hidden" name="hook" value="<?php echo esc_attr( $row['hook'] ); ?>">
510 <input type="hidden" name="md5" value="<?php echo esc_attr( $row['md5'] ); ?>">
511 <button type="submit" class="button delete" title="<?php esc_attr_e( 'Remove this event from the schedule', 'wpforo' ); ?>">
512 <?php _e( 'Delete', 'wpforo' ); ?>
513 </button>
514 </form>
515 </div>
516 </td>
517 </tr>
518 <?php endforeach; ?>
519 </tbody>
520 </table>
521 <?php endif; ?>
522
523 <!-- Details modal (shared, populated on click) -->
524 <div id="wpf-cron-modal-overlay" class="wpf-cron-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="wpf-cron-modal-title" aria-hidden="true">
525 <div class="wpf-cron-modal">
526 <div class="wpf-cron-modal-header">
527 <h3 id="wpf-cron-modal-title"><?php _e( 'Cron event details', 'wpforo' ); ?></h3>
528 <button type="button" class="wpf-cron-modal-close" aria-label="<?php esc_attr_e( 'Close', 'wpforo' ); ?>">&times;</button>
529 </div>
530 <div class="wpf-cron-modal-body">
531 <dl>
532 <dt><?php _e( 'Hook', 'wpforo' ); ?></dt>
533 <dd><code data-field="hook"></code></dd>
534
535 <dt><?php _e( 'Source', 'wpforo' ); ?></dt>
536 <dd data-field="source"></dd>
537
538 <dt><?php _e( 'Next run (site time)', 'wpforo' ); ?></dt>
539 <dd data-field="next_local"></dd>
540
541 <dt><?php _e( 'Next run (GMT)', 'wpforo' ); ?></dt>
542 <dd data-field="next_gmt"></dd>
543
544 <dt><?php _e( 'Relative', 'wpforo' ); ?></dt>
545 <dd data-field="next_rel"></dd>
546
547 <dt><?php _e( 'Schedule', 'wpforo' ); ?></dt>
548 <dd>
549 <code data-field="schedule"></code>
550 <span data-field="schedule_lbl" style="color:#666;margin-left:6px;"></span>
551 </dd>
552
553 <dt><?php _e( 'Interval', 'wpforo' ); ?></dt>
554 <dd>
555 <span data-field="interval_lbl"></span>
556 <span data-field="interval" style="color:#666;"></span>
557 </dd>
558
559 <dt><?php _e( 'Arguments', 'wpforo' ); ?></dt>
560 <dd><pre data-field="args"></pre></dd>
561
562 <dt><?php _e( 'Signature (md5)', 'wpforo' ); ?></dt>
563 <dd><code data-field="md5"></code></dd>
564
565 <dt><?php _e( 'Unix timestamp', 'wpforo' ); ?></dt>
566 <dd><code data-field="timestamp"></code></dd>
567 </dl>
568 </div>
569 </div>
570 </div>
571 </div>
572
573 <script>
574 (function() {
575 var overlay = document.getElementById( 'wpf-cron-modal-overlay' );
576 if ( ! overlay ) return;
577
578 function setField( name, value ) {
579 var els = overlay.querySelectorAll( '[data-field="' + name + '"]' );
580 for ( var i = 0; i < els.length; i++ ) {
581 els[ i ].textContent = ( value === null || typeof value === 'undefined' ) ? '' : String( value );
582 }
583 }
584
585 function openModal( payload ) {
586 var formattedInterval = '';
587 if ( payload.interval && payload.interval > 0 ) {
588 formattedInterval = ' (' + payload.interval + 's)';
589 }
590 setField( 'hook', payload.hook );
591 setField( 'source', payload.source );
592 setField( 'next_local', payload.next_local );
593 setField( 'next_gmt', payload.next_gmt );
594 setField( 'next_rel', payload.next_rel );
595 setField( 'schedule', payload.schedule );
596 setField( 'schedule_lbl', payload.schedule_lbl );
597 setField( 'interval_lbl', payload.interval_lbl );
598 setField( 'interval', formattedInterval );
599 setField( 'args', payload.args );
600 setField( 'md5', payload.md5 );
601 setField( 'timestamp', payload.timestamp );
602 overlay.classList.add( 'is-open' );
603 overlay.setAttribute( 'aria-hidden', 'false' );
604 }
605
606 function closeModal() {
607 overlay.classList.remove( 'is-open' );
608 overlay.setAttribute( 'aria-hidden', 'true' );
609 }
610
611 // Open on Details click (event delegation works even after future renders)
612 document.addEventListener( 'click', function ( e ) {
613 var btn = e.target.closest ? e.target.closest( '.wpf-cron-details-btn' ) : null;
614 if ( ! btn ) return;
615 e.preventDefault();
616 var raw = btn.getAttribute( 'data-payload' );
617 if ( ! raw ) return;
618 try {
619 openModal( JSON.parse( raw ) );
620 } catch ( err ) {
621 console.error( 'wpForo cron details: invalid payload', err );
622 }
623 } );
624
625 // Close on overlay click (outside the white card), the X button, or Escape
626 overlay.addEventListener( 'click', function ( e ) {
627 if ( e.target === overlay ) closeModal();
628 } );
629 var closeBtn = overlay.querySelector( '.wpf-cron-modal-close' );
630 if ( closeBtn ) closeBtn.addEventListener( 'click', closeModal );
631 document.addEventListener( 'keydown', function ( e ) {
632 if ( e.key === 'Escape' && overlay.classList.contains( 'is-open' ) ) closeModal();
633 } );
634 })();
635 </script>
636