PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.3.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.3.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / seo / class-email-report-scheduler.php

class-email-report-scheduler.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.3.0, at includes/seo/class-email-report-scheduler.php

121 lines 4.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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_scheduled_at is a site-local wall clock (written with wp_date()).
82 // strtotime() would resolve it against PHP's default timezone (UTC
83 // under WordPress) and shift the send by the site's offset — early on
84 // a negative offset, late on a positive one. Interpret it in the site
85 // timezone, the same way next_run_iso() reports it to the UI.
86 $next_ts = (int) get_gmt_from_date((string) $config['next_scheduled_at'], 'U');
87 if ($next_ts > 0 && $next_ts > time()) {
88 return;
89 }
90
91 $this->generator->generate_for_due();
92 }
93
94 /**
95 * Convenience used by REST + admin UI to surface the next-send timestamp
96 * to the user. Defends against an empty or stale next_scheduled_at.
97 *
98 * next_scheduled_at is stored as a site-local `Y-m-d H:i:s` string with
99 * no zone information. Handing that to the browser meant `new Date()`
100 * read it in the *viewer's* timezone, so any admin whose browser zone
101 * differed from the site's saw the wrong time. Emit a real ISO 8601
102 * string carrying the site's UTC offset instead, which parses to one
103 * unambiguous instant everywhere.
104 */
105 public function next_run_iso(): ?string {
106 $config = $this->config->get();
107 if (empty($config['enabled']) || empty($config['next_scheduled_at'])) {
108 return null;
109 }
110
111 // Interpret the stored wall clock in the site timezone, then render
112 // that same instant with its offset attached.
113 $timestamp = (int) get_gmt_from_date((string) $config['next_scheduled_at'], 'U');
114 if ($timestamp <= 0) {
115 return null;
116 }
117
118 return wp_date('c', $timestamp);
119 }
120 }
121