| 1 |
<?php |
| 2 |
/** |
| 3 |
* Email Report Scheduler |
| 4 |
* |
| 5 |
* Owns the cron lifecycle for Email Reporting: |
| 6 |
* - registers the recurring `thinkrank_email_report_tick` event |
| 7 |
* - on each tick, asks the config whether the site is due |
| 8 |
* - delegates to Email_Report_Generator::generate_for_due() |
| 9 |
* |
| 10 |
* The cron event is cleared on plugin deactivation by ThinkRank\Core\Deactivator |
| 11 |
* (see its clear_scheduled_hooks()). |
| 12 |
* |
| 13 |
* Cron cadence: hourly. Per-site frequency (7/15/30 days) is enforced |
| 14 |
* by checking next_scheduled_at against now — not by scheduling N |
| 15 |
* different cron events. One event for all sites, simple to reason about. |
| 16 |
* |
| 17 |
* @package ThinkRank |
| 18 |
* @subpackage SEO |
| 19 |
* @since 1.9.0 |
| 20 |
*/ |
| 21 |
|
| 22 |
declare(strict_types=1); |
| 23 |
|
| 24 |
namespace ThinkRank\SEO; |
| 25 |
|
| 26 |
if (!defined('ABSPATH')) { |
| 27 |
exit; |
| 28 |
} |
| 29 |
|
| 30 |
/** |
| 31 |
* Email_Report_Scheduler |
| 32 |
* |
| 33 |
* @since 1.9.0 |
| 34 |
*/ |
| 35 |
final class Email_Report_Scheduler { |
| 36 |
|
| 37 |
public const CRON_HOOK = 'thinkrank_email_report_tick'; |
| 38 |
|
| 39 |
private Email_Report_Config $config; |
| 40 |
private Email_Report_Generator $generator; |
| 41 |
|
| 42 |
public function __construct(Email_Report_Config $config, Email_Report_Generator $generator) { |
| 43 |
$this->config = $config; |
| 44 |
$this->generator = $generator; |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Register cron + handler. Called once at plugin boot. |
| 49 |
*/ |
| 50 |
public function register(): void { |
| 51 |
add_action(self::CRON_HOOK, [$this, 'on_tick']); |
| 52 |
|
| 53 |
if (!wp_next_scheduled(self::CRON_HOOK)) { |
| 54 |
wp_schedule_event(time() + 60, 'hourly', self::CRON_HOOK); |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Cron tick handler. Runs every hour. |
| 60 |
* |
| 61 |
* Decision tree: |
| 62 |
* 1. Feature disabled → skip |
| 63 |
* 2. No next_scheduled_at on file → set one and skip this tick |
| 64 |
* (gives the user a fresh cycle starting from "now" rather than |
| 65 |
* firing immediately on first enable) |
| 66 |
* 3. next_scheduled_at is in the future → skip |
| 67 |
* 4. Otherwise → generate. |
| 68 |
*/ |
| 69 |
public function on_tick(): void { |
| 70 |
$config = $this->config->get(); |
| 71 |
if (empty($config['enabled'])) { |
| 72 |
return; |
| 73 |
} |
| 74 |
|
| 75 |
if (empty($config['next_scheduled_at'])) { |
| 76 |
$next = wp_date('Y-m-d H:i:s', strtotime('+' . max(1, (int) $config['frequency_days']) . ' days')); |
| 77 |
$this->config->update_schedule(null, $next); |
| 78 |
return; |
| 79 |
} |
| 80 |
|
| 81 |
$next_ts = strtotime((string) $config['next_scheduled_at']); |
| 82 |
if ($next_ts && $next_ts > time()) { |
| 83 |
return; |
| 84 |
} |
| 85 |
|
| 86 |
$this->generator->generate_for_due(); |
| 87 |
} |
| 88 |
|
| 89 |
/** |
| 90 |
* Convenience used by REST + admin UI to surface the next-send timestamp |
| 91 |
* to the user. Defends against an empty or stale next_scheduled_at. |
| 92 |
* |
| 93 |
* next_scheduled_at is stored as a site-local `Y-m-d H:i:s` string with |
| 94 |
* no zone information. Handing that to the browser meant `new Date()` |
| 95 |
* read it in the *viewer's* timezone, so any admin whose browser zone |
| 96 |
* differed from the site's saw the wrong time. Emit a real ISO 8601 |
| 97 |
* string carrying the site's UTC offset instead, which parses to one |
| 98 |
* unambiguous instant everywhere. |
| 99 |
*/ |
| 100 |
public function next_run_iso(): ?string { |
| 101 |
$config = $this->config->get(); |
| 102 |
if (empty($config['enabled']) || empty($config['next_scheduled_at'])) { |
| 103 |
return null; |
| 104 |
} |
| 105 |
|
| 106 |
// Interpret the stored wall clock in the site timezone, then render |
| 107 |
// that same instant with its offset attached. |
| 108 |
$timestamp = (int) get_gmt_from_date((string) $config['next_scheduled_at'], 'U'); |
| 109 |
if ($timestamp <= 0) { |
| 110 |
return null; |
| 111 |
} |
| 112 |
|
| 113 |
return wp_date('c', $timestamp); |
| 114 |
} |
| 115 |
} |
| 116 |
|