PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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-config.php

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

255 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email Report Config
4 *
5 * Persistence layer for the per-site Email Reporting state. Stores a single
6 * associative array under the `thinkrank_email_report_config` option.
7 *
8 * The free report is fixed: every 30 days, to the site admin email, with every
9 * section. What is stored is only whether it is on and when it last and next
10 * runs. ThinkRank Pro owns the schedule, recipient and branding settings and
11 * supplies them through the `thinkrank_email_report_config` filter (#673).
12 *
13 * Keys a save does not own are left in the stored array untouched, so values an
14 * earlier release wrote there (recipients, branding) survive for Pro to pick up.
15 *
16 * @package ThinkRank
17 * @subpackage SEO
18 * @since 1.9.0
19 */
20
21 declare(strict_types=1);
22
23 namespace ThinkRank\SEO;
24
25 if (!defined('ABSPATH')) {
26 exit;
27 }
28
29 /**
30 * Email_Report_Config
31 *
32 * @since 1.9.0
33 */
34 final class Email_Report_Config {
35
36 private const OPTION_KEY = 'thinkrank_email_report_config';
37
38 /**
39 * Days between reports when nothing filters the schedule.
40 */
41 public const FREQUENCY_DAYS = 30;
42
43 /**
44 * Load the procedural defaults file, which lives outside the autoloader.
45 */
46 private function load_defaults_file(): void {
47 if (!function_exists('thinkrank_get_default_email_report_config')) {
48 require_once THINKRANK_PLUGIN_DIR . 'includes/config/email-report-settings-config.php';
49 }
50 }
51
52 /**
53 * The stored option, as an array.
54 */
55 private function stored(): array {
56 $stored = get_option(self::OPTION_KEY, []);
57
58 return is_array($stored) ? $stored : [];
59 }
60
61 /**
62 * The resolved config every consumer reads.
63 *
64 * @return array{enabled: bool, frequency_days: int, recipients: string[], sections_enabled: string[], next_scheduled_at: ?string, last_sent_at: ?string}
65 */
66 public function get(): array {
67 $this->load_defaults_file();
68
69 $stored = $this->stored();
70 $state = [
71 'enabled' => !empty($stored['enabled']),
72 'next_scheduled_at' => $stored['next_scheduled_at'] ?? null,
73 'last_sent_at' => $stored['last_sent_at'] ?? null,
74 ];
75
76 $report = [
77 'frequency_days' => self::FREQUENCY_DAYS,
78 'recipients' => [(string) get_option('admin_email')],
79 'sections_enabled' => array_keys(thinkrank_get_email_report_default_sections()),
80 ];
81
82 /**
83 * Filter what the report covers and who receives it.
84 *
85 * ThinkRank Pro returns its own schedule, recipients and sections here.
86 * Extra keys are passed through to the renderer and mailer filters.
87 * The on/off switch and schedule timestamps are not filterable.
88 *
89 * @since 2.6.0
90 *
91 * @param array $report {
92 * @type int $frequency_days Days between reports.
93 * @type string[] $recipients Recipient addresses.
94 * @type string[] $sections_enabled Section keys, in render order.
95 * }
96 * @param array $state Stored on/off switch and schedule timestamps.
97 */
98 $filtered = apply_filters('thinkrank_email_report_config', $report, $state);
99 $filtered = is_array($filtered) ? $filtered : $report;
100
101 return array_merge(
102 $filtered,
103 [
104 'frequency_days' => max(1, (int) ($filtered['frequency_days'] ?? self::FREQUENCY_DAYS)),
105 'recipients' => $this->normalize_recipients($filtered['recipients'] ?? []),
106 'sections_enabled' => $this->normalize_section_keys($filtered['sections_enabled'] ?? []),
107 ],
108 $state
109 );
110 }
111
112 /**
113 * Save the on/off switch. Returns the resolved config after the write.
114 *
115 * The first enable seeds `next_scheduled_at` so the UI shows a real "Next
116 * report" date immediately. The scheduler still re-seeds on its first tick
117 * for any other path that flips enable on.
118 */
119 public function save(array $input): array {
120 $this->load_defaults_file();
121
122 $stored = $this->stored() + thinkrank_get_default_email_report_config();
123
124 if (array_key_exists('enabled', $input)) {
125 $stored['enabled'] = (bool) filter_var($input['enabled'], FILTER_VALIDATE_BOOLEAN);
126 }
127
128 if ($stored['enabled'] && empty($stored['next_scheduled_at'])) {
129 $next = strtotime('+' . $this->get()['frequency_days'] . ' days');
130
131 $stored['next_scheduled_at'] = wp_date('Y-m-d H:i:s', max($next ?: time(), time()));
132 }
133
134 update_option(self::OPTION_KEY, $stored, false);
135
136 $config = $this->get();
137
138 /**
139 * Fires after Email Report config is saved.
140 *
141 * @since 1.9.0
142 *
143 * @param array $config The resolved config.
144 */
145 do_action('thinkrank_email_report_settings_saved', $config);
146
147 return $config;
148 }
149
150 /**
151 * Move the next send after the report's frequency changed.
152 *
153 * Called by whatever changed the frequency (ThinkRank Pro) with the value
154 * it had before. Carrying the old timestamp through meant switching 30 → 7
155 * days still waited out the original 30-day window. The new date anchors
156 * off the last send when there is one, so shortening the cadence brings the
157 * next report forward instead of adding a full period on top of time
158 * already elapsed.
159 *
160 * @param int $previous_frequency_days Frequency before the change.
161 * @return array The resolved config.
162 */
163 public function reschedule(int $previous_frequency_days): array {
164 $config = $this->get();
165
166 if (empty($config['enabled']) || $previous_frequency_days === (int) $config['frequency_days']) {
167 return $config;
168 }
169
170 // last_sent_at is a site-local wall clock (current_time('mysql')).
171 // strtotime() would read it as UTC and skew the whole cadence by the
172 // site's offset, so resolve it in the site timezone instead.
173 $anchor = !empty($config['last_sent_at'])
174 ? (int) get_gmt_from_date((string) $config['last_sent_at'], 'U')
175 : time();
176 $anchor = $anchor ?: time();
177
178 $next = strtotime('+' . (int) $config['frequency_days'] . ' days', $anchor);
179
180 // Never schedule into the past — a big cadence cut on an old
181 // last_sent_at means "due now", which the next tick picks up.
182 return $this->update_schedule(
183 $config['last_sent_at'],
184 wp_date('Y-m-d H:i:s', max($next ?: time(), time()))
185 );
186 }
187
188 /**
189 * Update only the schedule timestamps. Called from the scheduler after
190 * a send.
191 */
192 public function update_schedule(?string $last_sent_at, ?string $next_scheduled_at): array {
193 $stored = $this->stored();
194 $stored['last_sent_at'] = $last_sent_at;
195 $stored['next_scheduled_at'] = $next_scheduled_at;
196 update_option(self::OPTION_KEY, $stored, false);
197
198 return $this->get();
199 }
200
201 /**
202 * Normalize a recipient list that might arrive as a string
203 * ("a@x.com, b@x.com") or as an array.
204 *
205 * @param mixed $raw
206 * @return string[]
207 */
208 private function normalize_recipients($raw): array {
209 if (is_string($raw)) {
210 $raw = preg_split('/[\s,;]+/', $raw) ?: [];
211 }
212 if (!is_array($raw)) {
213 return [];
214 }
215 $emails = [];
216 foreach ($raw as $candidate) {
217 if (!is_string($candidate)) {
218 continue;
219 }
220 $candidate = sanitize_email(trim($candidate));
221 if ($candidate !== '' && is_email($candidate)) {
222 $emails[] = strtolower($candidate);
223 }
224 }
225 return array_values(array_unique($emails));
226 }
227
228 /**
229 * Keep known section keys, in the order given.
230 *
231 * @param mixed $raw
232 * @return string[]
233 */
234 private function normalize_section_keys($raw): array {
235 if (!is_array($raw)) {
236 return [];
237 }
238 $allowed = array_unique(array_merge(
239 array_keys(thinkrank_get_email_report_default_sections()),
240 (array) apply_filters('thinkrank_email_report_section_keys', [])
241 ));
242 $clean = [];
243 foreach ($raw as $key) {
244 if (!is_string($key)) {
245 continue;
246 }
247 $key = sanitize_key($key);
248 if (in_array($key, $allowed, true)) {
249 $clean[] = $key;
250 }
251 }
252 return array_values(array_unique($clean));
253 }
254 }
255