PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 All 35 releases
double-opt-in / src / Health / SiteHealthIntegration.php

SiteHealthIntegration.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.5.0, at src/Health/SiteHealthIntegration.php

279 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Surfaces health check results where operators actually look.
4 *
5 * @package Forge12\DoubleOptIn\Health
6 * @since 5.3.0
7 */
8
9 declare( strict_types=1 );
10
11 namespace Forge12\DoubleOptIn\Health;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Three surfaces, one source of truth:
19 *
20 * - **Site Health → Status** (`site_status_tests`): the canonical place
21 * a WordPress operator — or a support agent talking one through a
22 * problem — goes to ask "is anything wrong?".
23 * - **Site Health → Info** (`debug_information`): the exportable
24 * report. WordPress' own support handbook points users at it when
25 * asked to send diagnostics, which makes it the cheapest possible
26 * channel for us to receive them.
27 * - **Admin notice**: because a broken precondition should not wait
28 * for someone to think of opening Site Health.
29 *
30 * All three read the same {@see HealthCheckRegistry}, so a check that
31 * is registered once shows up everywhere without further work.
32 */
33 final class SiteHealthIntegration {
34
35 /**
36 * Site Health test ids must be globally unique across all plugins.
37 */
38 private const TEST_PREFIX = 'f12_doi_';
39
40 /**
41 * @var HealthCheckRegistry
42 */
43 private $registry;
44
45 public function __construct( HealthCheckRegistry $registry ) {
46 $this->registry = $registry;
47 }
48
49 public function register(): void {
50 add_filter( 'site_status_tests', array( $this, 'addStatusTests' ) );
51 add_filter( 'debug_information', array( $this, 'addDebugInformation' ) );
52 add_action( 'admin_notices', array( $this, 'renderCriticalNotice' ) );
53 }
54
55 /**
56 * Contribute one direct test per registered check.
57 *
58 * Direct rather than async: every check we ship is a single indexed
59 * lookup or an option read. Async would add a REST round-trip for
60 * work measured in microseconds.
61 *
62 * @param array<string,array<string,mixed>> $tests
63 *
64 * @return array<string,array<string,mixed>>
65 */
66 public function addStatusTests( $tests ) {
67 if ( ! is_array( $tests ) ) {
68 return $tests;
69 }
70 if ( ! isset( $tests['direct'] ) || ! is_array( $tests['direct'] ) ) {
71 $tests['direct'] = array();
72 }
73
74 foreach ( $this->registry->all() as $id => $check ) {
75 $testId = $this->testId( $id );
76
77 $tests['direct'][ $testId ] = array(
78 'label' => $check->getLabel(),
79 'test' => function () use ( $id ) {
80 return $this->renderTest( $id );
81 },
82 );
83 }
84
85 return $tests;
86 }
87
88 /**
89 * Map one HealthCheckResult onto the array shape WP_Site_Health
90 * expects. Status values are shared vocabulary, so this is a
91 * pass-through rather than a translation.
92 *
93 * @return array<string,mixed>
94 */
95 private function renderTest( string $id ): array {
96 $results = $this->registry->runAll();
97 $result = $results[ $id ] ?? null;
98
99 if ( ! $result instanceof HealthCheckResult ) {
100 return array(
101 'label' => __( 'Double Opt-In', 'double-opt-in' ),
102 'status' => HealthCheckResult::STATUS_RECOMMENDED,
103 'badge' => $this->badge(),
104 'test' => $this->testId( $id ),
105 );
106 }
107
108 $test = array(
109 'label' => $result->getLabel(),
110 'status' => $result->getStatus(),
111 'badge' => $this->badge(),
112 'description' => sprintf( '<p>%s</p>', esc_html( $result->getDescription() ) ),
113 'test' => $this->testId( $id ),
114 );
115
116 if ( $result->getActionUrl() !== '' && $result->getActionLabel() !== '' ) {
117 $test['actions'] = sprintf(
118 '<p><a href="%s">%s</a></p>',
119 esc_url( $result->getActionUrl() ),
120 esc_html( $result->getActionLabel() )
121 );
122 }
123
124 return $test;
125 }
126
127 /**
128 * Shared badge so every DOI test groups under one heading in the
129 * Site Health UI. Colour is WordPress' own "not performance, not
130 * security" neutral.
131 *
132 * @return array<string,string>
133 */
134 private function badge(): array {
135 return array(
136 'label' => __( 'Double Opt-In', 'double-opt-in' ),
137 'color' => 'purple',
138 );
139 }
140
141 private function testId( string $id ): string {
142 return strpos( $id, self::TEST_PREFIX ) === 0 ? $id : self::TEST_PREFIX . $id;
143 }
144
145 /**
146 * Add a "Double Opt-In" section to Site Health → Info.
147 *
148 * This is what a customer exports and pastes into a support ticket,
149 * so it carries versions alongside the check outcomes. No personal
150 * data — table names and version strings only (see rules/gdpr.md).
151 *
152 * @param array<string,mixed> $info
153 *
154 * @return array<string,mixed>
155 */
156 public function addDebugInformation( $info ) {
157 if ( ! is_array( $info ) ) {
158 return $info;
159 }
160
161 $fields = array(
162 'core_version' => array(
163 'label' => __( 'Core version', 'double-opt-in' ),
164 'value' => defined( 'FORGE12_OPTIN_VERSION' ) ? FORGE12_OPTIN_VERSION : __( 'unknown', 'double-opt-in' ),
165 ),
166 'core_api_version' => array(
167 'label' => __( 'Core API version', 'double-opt-in' ),
168 'value' => defined( 'F12_DOI_CORE_API_VERSION' ) ? F12_DOI_CORE_API_VERSION : __( 'unknown', 'double-opt-in' ),
169 ),
170 );
171
172 foreach ( $this->registry->runAll() as $id => $result ) {
173 $check = $this->registry->all()[ $id ] ?? null;
174 $name = $check instanceof HealthCheckInterface
175 ? sprintf( '%s (%s)', $check->getLabel(), $check->getPackage() )
176 : $id;
177
178 $fields[ $id ] = array(
179 'label' => $name,
180 'value' => $result->getDebugValue(),
181 'debug' => $result->getStatus(),
182 );
183 }
184
185 $info['f12-double-opt-in'] = array(
186 'label' => __( 'Double Opt-In', 'double-opt-in' ),
187 'description' => __( 'Runtime preconditions checked by the Double Opt-In plugin family. Send this section along when you contact support.', 'double-opt-in' ),
188 'fields' => $fields,
189 );
190
191 return $info;
192 }
193
194 /**
195 * Nag on the screens an operator passes through anyway.
196 *
197 * Deliberately not dismissible: the condition is a hard failure,
198 * and a dismissed notice would hide it until the next customer
199 * complains. It disappears by itself the moment the check passes.
200 */
201 public function renderCriticalNotice(): void {
202 if ( ! current_user_can( 'manage_options' ) ) {
203 return;
204 }
205 if ( ! $this->isRelevantScreen() ) {
206 return;
207 }
208
209 $criticals = $this->registry->criticals();
210 if ( empty( $criticals ) ) {
211 return;
212 }
213
214 echo '<div class="notice notice-error"><p><strong>';
215 echo esc_html__( 'Double Opt-In has detected a problem that will affect your visitors.', 'double-opt-in' );
216 echo '</strong></p><ul style="list-style:disc;margin-left:1.5em">';
217
218 $actions = array();
219
220 foreach ( $criticals as $result ) {
221 printf(
222 '<li><strong>%s</strong><br>%s</li>',
223 esc_html( $result->getLabel() ),
224 esc_html( $result->getDescription() )
225 );
226
227 // Collected rather than printed inline so the buttons end up
228 // in one row under the list instead of interrupting it.
229 if ( $result->getActionUrl() !== '' && $result->getActionLabel() !== '' ) {
230 $actions[ $result->getActionUrl() ] = $result->getActionLabel();
231 }
232 }
233
234 echo '</ul><p>';
235
236 // A check that knows how to fix itself says so here. Site Health is
237 // where the full report lives, but a fix that is one click away
238 // should not require finding that screen first — this notice runs
239 // on the dashboard and the plugins list, which is where an operator
240 // already stands when something has just gone wrong.
241 foreach ( $actions as $url => $label ) {
242 printf(
243 '<a class="button button-primary" style="margin-right:.5em" href="%s">%s</a>',
244 esc_url( $url ),
245 esc_html( $label )
246 );
247 }
248
249 printf(
250 '<a class="button" href="%s">%s</a></p></div>',
251 esc_url( admin_url( 'site-health.php' ) ),
252 esc_html__( 'Open Site Health', 'double-opt-in' )
253 );
254 }
255
256 /**
257 * Dashboard, plugins list and the plugin's own screens. Not every
258 * admin page — a site-wide red banner for a scoped problem trains
259 * people to ignore banners.
260 */
261 private function isRelevantScreen(): bool {
262 if ( ! function_exists( 'get_current_screen' ) ) {
263 return false;
264 }
265
266 $screen = get_current_screen();
267 if ( ! $screen ) {
268 return false;
269 }
270
271 if ( in_array( $screen->id, array( 'dashboard', 'plugins', 'plugins-network' ), true ) ) {
272 return true;
273 }
274
275 return strpos( $screen->id, 'f12-doi' ) !== false
276 || strpos( $screen->id, 'f12-cf7-doubleoptin' ) !== false;
277 }
278 }
279