PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.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 / StaleConsentFieldCheck.php

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

191 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Health check: every configured acceptance field still exists.
4 *
5 * @package Forge12\DoubleOptIn\Health
6 * @since 5.4.0
7 */
8
9 declare( strict_types=1 );
10
11 namespace Forge12\DoubleOptIn\Health;
12
13 use Forge12\DoubleOptIn\Consent\ConsentGate;
14 use Forge12\DoubleOptIn\Container\Container;
15 use Forge12\DoubleOptIn\FormSettings\FormSettingsService;
16 use Forge12\DoubleOptIn\Integration\FormIntegrationRegistry;
17 use Forge12\DoubleOptIn\Integration\SubmittedContent;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Finds forms whose `consent_field` points at a field that is not on the
25 * form any more.
26 *
27 * Such a form keeps accepting registrations — {@see ConsentGate} refuses
28 * to punish a visitor for a settings mistake — but every opt-in it
29 * creates carries a `consent_text` that nobody was ever required to
30 * confirm. The record looks like proof and is not one, which is the worst
31 * of the three possible states, and nothing in the admin surfaced it
32 * before this check: the banner on the settings tab only appears if
33 * someone opens that particular form.
34 *
35 * A field goes stale by being renamed or deleted in the form builder
36 * while the opt-in settings keep the old name — nothing warns the admin
37 * at that moment, because the two live in different plugins.
38 *
39 * Cost: one `getForms()` + `getFormFields()` per integration and one
40 * post-meta read per form. That is too much for every admin page load, so
41 * the outcome is cached for six hours and flushed whenever form settings
42 * are saved ({@see self::flush()}).
43 */
44 final class StaleConsentFieldCheck implements HealthCheckInterface {
45
46 private const CACHE_KEY = 'f12_doi_stale_consent_fields';
47 private const CACHE_TTL = 21600; // 6h.
48
49 /**
50 * How many offending forms to name in the description before
51 * summarising the rest.
52 */
53 private const NAME_LIMIT = 3;
54
55 public function getId(): string {
56 return 'f12_doi_consent_field_exists';
57 }
58
59 public function getLabel(): string {
60 return __( 'Consent acceptance fields', 'double-opt-in' );
61 }
62
63 public function getPackage(): string {
64 return 'core';
65 }
66
67 /**
68 * Drop the cached scan. Called after form settings change, because
69 * the most common reason to open those settings is to fix exactly
70 * this — and a six-hour-old "still broken" is a bad answer then.
71 */
72 public static function flush(): void {
73 delete_transient( self::CACHE_KEY );
74 }
75
76 public function run(): HealthCheckResult {
77 $stale = $this->findStaleForms();
78
79 if ( $stale === array() ) {
80 return new HealthCheckResult(
81 HealthCheckResult::STATUS_GOOD,
82 __( 'Every configured acceptance field exists', 'double-opt-in' ),
83 __( 'Each form that requires a consent checkbox points at a field that is actually on the form, so the stored consent is backed by a confirmation.', 'double-opt-in' ),
84 'ok'
85 );
86 }
87
88 $names = array();
89 foreach ( array_slice( $stale, 0, self::NAME_LIMIT ) as $form ) {
90 /* translators: 1: form title, 2: configured field name */
91 $names[] = sprintf( __( '“%1$s” (field “%2$s”)', 'double-opt-in' ), $form['title'], $form['field'] );
92 }
93
94 $listed = implode( ', ', $names );
95 if ( count( $stale ) > self::NAME_LIMIT ) {
96 /* translators: 1: comma-separated form list, 2: number of further forms */
97 $listed = sprintf( __( '%1$s and %2$d more', 'double-opt-in' ), $listed, count( $stale ) - self::NAME_LIMIT );
98 }
99
100 return new HealthCheckResult(
101 HealthCheckResult::STATUS_RECOMMENDED,
102 /* translators: %d: number of affected forms */
103 sprintf( _n( '%d form has an acceptance field that no longer exists', '%d forms have an acceptance field that no longer exists', count( $stale ), 'double-opt-in' ), count( $stale ) ),
104 sprintf(
105 /* translators: %s: list of affected forms */
106 __( 'These forms store a consent text as proof, but the field the visitor was supposed to tick is not on the form: %s. Submissions are still accepted — a settings mistake must not take your registrations offline — but the stored consent is not provable. Pick a field that exists, or clear the setting.', 'double-opt-in' ),
107 $listed
108 ),
109 'stale:' . count( $stale ),
110 __( 'Review form settings', 'double-opt-in' ),
111 admin_url( 'admin.php?page=f12-doi-admin#/forms' )
112 );
113 }
114
115 /**
116 * @return array<int,array{title:string,field:string}>
117 */
118 private function findStaleForms(): array {
119 $cached = get_transient( self::CACHE_KEY );
120 if ( is_array( $cached ) ) {
121 return $cached;
122 }
123
124 $stale = $this->scan();
125 set_transient( self::CACHE_KEY, $stale, self::CACHE_TTL );
126
127 return $stale;
128 }
129
130 /**
131 * @return array<int,array{title:string,field:string}>
132 */
133 private function scan(): array {
134 $stale = array();
135
136 try {
137 $registry = FormIntegrationRegistry::getInstance();
138 $settings = Container::getInstance()->get( FormSettingsService::class );
139 } catch ( \Throwable $e ) {
140 return $stale;
141 }
142
143 if ( ! $settings instanceof FormSettingsService ) {
144 return $stale;
145 }
146
147 foreach ( $registry->getAvailable() as $integration ) {
148 try {
149 $forms = $integration->getForms();
150 } catch ( \Throwable $e ) {
151 continue;
152 }
153
154 foreach ( $forms as $form ) {
155 $formId = isset( $form['id'] ) ? (int) $form['id'] : 0;
156 if ( $formId <= 0 ) {
157 continue;
158 }
159
160 try {
161 $field = (string) ( $settings->getSettings( $formId )->consentField ?? '' );
162 if ( $field === '' ) {
163 continue;
164 }
165
166 $known = ConsentGate::normalizeFieldNames( $integration->getFormFields( $formId ) );
167 // An integration that cannot list its fields tells us
168 // nothing — reporting those would flood the check with
169 // forms that are perfectly fine.
170 if ( $known === array() ) {
171 continue;
172 }
173
174 if ( SubmittedContent::matchFieldName( $field, $known ) !== '' ) {
175 continue;
176 }
177 } catch ( \Throwable $e ) {
178 continue;
179 }
180
181 $stale[] = array(
182 'title' => (string) ( $form['title'] ?? ( '#' . $formId ) ),
183 'field' => $field,
184 );
185 }
186 }
187
188 return $stale;
189 }
190 }
191