PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.8.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.8.0
2.8.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 All 49 releases
← All changes | includes/seo/class-email-report-generator.php +109 -1 2.0.02.8.0 View file →
@@ -99,8 +99,16 @@
99 99 private function run(array $config, bool $is_test): array {
100 100 $frequency = (int) ($config['frequency_days'] ?? 30);
101 101
102 102 try {
103 + // Is there a report to build? Asked before anything is fetched
104 + // (#742). A site without Search Console used to be fetched,
105 + // rendered and — before #611 — sent as a column of blanks.
106 + $readiness = $this->data_provider->readiness();
107 + if (empty($readiness['ready'])) {
108 + return $this->handle_not_ready($config, $readiness, $is_test);
109 + }
110 +
103 111 $shared = $this->data_provider->fetch($frequency);
104 112
105 113 $context = [
106 114 'period_start' => $shared['period_start'] ?? '',
@@ -156,9 +164,33 @@
156 164 }
157 165
158 166 $html = $this->renderer->render($config, $context);
159 167
160 - $tokens = ['%period%' => $context['period_label']];
168 + // Every section fell back to its "no data" notice, so this report
169 + // is a header, a footer and a column of placeholders — the empty
170 + // send the pre-check above was meant to stop, but could not: that
171 + // check only knows what is *enabled*, and emptiness is only known
172 + // once the sections have run.
173 + //
174 + // A test send still goes out. "Send Test Email" exists to prove
175 + // delivery works, and it has to do that on a site with no data.
176 + if (!$is_test && $this->renderer->sections_with_data() === 0) {
177 + // record_attempt() already claimed this period's log row.
178 + // Leaving it 'pending' would accumulate rows for periods that
179 + // were deliberately never sent, so close it out honestly.
180 + $this->mark_log_skipped($config, $context);
181 +
182 + // Don't re-evaluate this every hour — wait out a period.
183 + $this->config->update_schedule(
184 + $config['last_sent_at'] ?? null,
185 + $this->compute_next_run($frequency)
186 + );
187 + $this->config->record_skip('no_data');
188 +
189 + return ['success' => false, 'skipped' => 'no_data'];
190 + }
191 +
192 + $tokens = Email_Report_Data_Provider::subject_tokens($shared, $frequency);
161 193 $result = $this->mailer->send($config, $html, $tokens);
162 194
163 195 if (!$is_test) {
164 196 $this->finalize_log($config, $context, $result);
@@ -167,8 +199,9 @@
167 199 $this->config->update_schedule(
168 200 current_time('mysql'),
169 201 $this->compute_next_run($frequency)
170 202 );
203 + $this->config->clear_skip();
171 204 } else {
172 205 // A transient mail failure must not cost the user a whole
173 206 // period, and it must not stamp last_sent_at with a send
174 207 // that never happened. Retry soon; give up after
@@ -202,8 +235,58 @@
202 235 }
203 236 }
204 237
205 238 /**
239 + * The report cannot be built — Search Console is not connected.
240 + *
241 + * A scheduled run records the reason for the panel and leaves the
242 + * schedule exactly where it is: `next_scheduled_at` stays in the past,
243 + * so the hourly tick keeps asking and the first tick after the
244 + * connection is made sends the report. No log row is written — there
245 + * was no attempt.
246 + *
247 + * A test send still goes out, as the one-card "connect Search Console"
248 + * email, so "Send Test Email" proves delivery and shows the recipient
249 + * what to do next instead of a report full of blanks.
250 + *
251 + * @return array{success:bool,skipped?:string,reason?:string,result?:array,not_connected?:bool}
252 + */
253 + private function handle_not_ready(array $config, array $readiness, bool $is_test): array {
254 + $reason = (string) ($readiness['reason'] ?? 'not_ready');
255 +
256 + if (!$is_test) {
257 + $this->config->record_skip($reason);
258 + return ['success' => false, 'skipped' => 'not_ready', 'reason' => $reason];
259 + }
260 +
261 + $context = [
262 + 'period_start' => '',
263 + 'period_end' => '',
264 + 'period_label' => '',
265 + 'is_test' => true,
266 + 'not_connected' => true,
267 + 'readiness' => $readiness,
268 + 'shared' => [],
269 + ];
270 +
271 + $html = $this->renderer->render($config, $context);
272 + $tokens = [
273 + '%period%' => '',
274 + '%headline%' => __('Connect Google Search Console to start your SEO reports', 'thinkrank'),
275 + ];
276 + $result = $this->mailer->send($config, $html, $tokens);
277 +
278 + do_action('thinkrank_email_report_after_send', $config, $result, true);
279 +
280 + return [
281 + 'success' => (bool) ($result['success'] ?? false),
282 + 'result' => $result,
283 + 'not_connected' => true,
284 + 'reason' => $reason,
285 + ];
286 + }
287 +
288 + /**
206 289 * Claim this period's send by inserting a `pending` row in
207 290 * email_report_logs. The unique key on (site_id, period_start,
208 291 * recipient_hash) is the dedupe gate — a conflicting insert means the
209 292 * period is already accounted for.
@@ -332,8 +415,33 @@
332 415 'log_id' => (int) $existing['id'],
333 416 'attempts' => $attempts,
334 417 'retry' => true,
335 418 ];
419 + }
420 +
421 + /**
422 + * Close out the row inserted by record_attempt() for a period that was
423 + * deliberately not sent, so it doesn't linger as 'pending'.
424 + */
425 + private function mark_log_skipped(array $config, array $context): void {
426 + global $wpdb;
427 + $table = $wpdb->prefix . 'thinkrank_email_report_logs';
428 +
429 + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
430 + $table,
431 + [
432 + 'status' => 'skipped',
433 + 'sent_at' => null,
434 + 'error_message' => null,
435 + ],
436 + [
437 + 'site_id' => get_current_blog_id(),
438 + 'period_start' => $context['period_start'] ?: current_time('mysql'),
439 + 'recipient_hash' => $this->recipient_hash((array) $config['recipients']),
440 + ],
441 + ['%s','%s','%s'],
442 + ['%d','%s','%s']
443 + );
336 444 }
337 445
338 446 /**
339 447 * Update the row inserted by record_attempt() with the send outcome.