PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.9.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.9.0
5.9.0 5.8.0 5.8.1 5.7.0 5.6.2 5.6.3 5.6.1 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 All 42 releases
double-opt-in / src / Service / PrivacyIntegration.php

PrivacyIntegration.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.9.0, at src/Service/PrivacyIntegration.php

286 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Privacy Integration
4 *
5 * Integrates with WordPress Privacy Tools (Data Export + Data Erasure).
6 *
7 * @package Forge12\DoubleOptIn\Service
8 * @since 3.2.0
9 */
10
11 namespace Forge12\DoubleOptIn\Service;
12
13 use Forge12\DoubleOptIn\Entity\OptIn;
14 use Forge12\DoubleOptIn\Repository\OptInMailStatusRepository;
15 use Forge12\DoubleOptIn\Repository\OptInRepositoryInterface;
16 use Forge12\Shared\LoggerInterface;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Class PrivacyIntegration
24 *
25 * Registers the plugin with WordPress Privacy Tools for GDPR compliance.
26 */
27 class PrivacyIntegration {
28
29 private LoggerInterface $logger;
30 private OptInRepositoryInterface $repository;
31
32 /**
33 * Delivery status of the confirmation mail (5.8.0). Optional so existing
34 * callers keep working.
35 *
36 * @var OptInMailStatusRepository|null
37 */
38 private $mailStatus;
39
40 public function __construct( LoggerInterface $logger, OptInRepositoryInterface $repository, ?OptInMailStatusRepository $mailStatus = null ) {
41 $this->logger = $logger;
42 $this->repository = $repository;
43 $this->mailStatus = $mailStatus;
44 }
45
46 /**
47 * Register privacy hooks.
48 *
49 * @return void
50 */
51 public function register(): void {
52 add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'registerExporter' ) );
53 add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'registerEraser' ) );
54
55 $this->logger->debug(
56 'Privacy integration hooks registered',
57 array(
58 'plugin' => 'double-opt-in',
59 )
60 );
61 }
62
63 /**
64 * Register the personal data exporter.
65 *
66 * @param array $exporters Existing exporters.
67 *
68 * @return array
69 */
70 public function registerExporter( array $exporters ): array {
71 $exporters['double-opt-in'] = array(
72 'exporter_friendly_name' => __( 'Double Opt-In Records', 'double-opt-in' ),
73 'callback' => array( $this, 'exportPersonalData' ),
74 );
75
76 return $exporters;
77 }
78
79 /**
80 * Register the personal data eraser.
81 *
82 * @param array $erasers Existing erasers.
83 *
84 * @return array
85 */
86 public function registerEraser( array $erasers ): array {
87 $erasers['double-opt-in'] = array(
88 'eraser_friendly_name' => __( 'Double Opt-In Records', 'double-opt-in' ),
89 'callback' => array( $this, 'erasePersonalData' ),
90 );
91
92 return $erasers;
93 }
94
95 /**
96 * Export personal data for a given email.
97 *
98 * @param string $email The email address.
99 * @param int $page The current page (pagination).
100 *
101 * @return array WordPress privacy export response.
102 */
103 public function exportPersonalData( string $email, int $page = 1 ): array {
104 $optIns = $this->repository->findByEmail( $email );
105 $items = array();
106
107 $dateFormat = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
108
109 foreach ( $optIns as $optIn ) {
110 $data = array(
111 array(
112 'name' => __( 'Email', 'double-opt-in' ),
113 'value' => $optIn->getEmail(),
114 ),
115 array(
116 'name' => __( 'Confirmed', 'double-opt-in' ),
117 'value' => $optIn->isConfirmed()
118 ? __( 'Yes', 'double-opt-in' )
119 : __( 'No', 'double-opt-in' ),
120 ),
121 array(
122 'name' => __( 'Consent Text', 'double-opt-in' ),
123 'value' => $optIn->getConsentText() ?: __( '(not recorded)', 'double-opt-in' ),
124 ),
125 array(
126 'name' => __( 'Registration Date', 'double-opt-in' ),
127 'value' => $optIn->getCreateTime() > 0
128 ? wp_date( $dateFormat, $optIn->getCreateTime() )
129 : '',
130 ),
131 array(
132 'name' => __( 'Confirmation Date', 'double-opt-in' ),
133 'value' => $optIn->getUpdateTime() > 0 && $optIn->isConfirmed()
134 ? wp_date( $dateFormat, $optIn->getUpdateTime() )
135 : '',
136 ),
137 array(
138 'name' => __( 'Opt-Out Date', 'double-opt-in' ),
139 'value' => $optIn->getOptOutTime() > 0
140 ? wp_date( $dateFormat, $optIn->getOptOutTime() )
141 : '',
142 ),
143 array(
144 'name' => __( 'Registration IP', 'double-opt-in' ),
145 'value' => $optIn->getIpRegister(),
146 ),
147 array(
148 'name' => __( 'Confirmation IP', 'double-opt-in' ),
149 'value' => $optIn->getIpConfirmation(),
150 ),
151 array(
152 'name' => __( 'Opt-Out IP', 'double-opt-in' ),
153 'value' => $optIn->getIpOptOut(),
154 ),
155 array(
156 'name' => __( 'Form ID', 'double-opt-in' ),
157 'value' => (string) $optIn->getFormId(),
158 ),
159 );
160
161 if ( $this->mailStatus !== null ) {
162 $mail = $this->mailStatus->find( $optIn->getId() );
163 if ( $mail['status'] !== '' ) {
164 // Same wording as the opt-in detail page, not the stored key.
165 $value = $mail['status'] === 'failed'
166 /* translators: %s: date and time of the attempt */
167 ? sprintf( __( 'Could not be sent (%s)', 'double-opt-in' ), $mail['at'] )
168 /* translators: %s: date and time the mail was handed over */
169 : sprintf( __( 'Handed to the mail server (%s)', 'double-opt-in' ), $mail['at'] );
170 $data[] = array(
171 'name' => __( 'Confirmation mail', 'double-opt-in' ),
172 'value' => $value . ( $mail['error'] !== '' ? ' — ' . $mail['error'] : '' ),
173 );
174 }
175 }
176
177 $items[] = array(
178 'group_id' => 'double-opt-in',
179 'group_label' => __( 'Double Opt-In Records', 'double-opt-in' ),
180 'item_id' => 'doi-' . $optIn->getId(),
181 'data' => $data,
182 );
183 }
184
185 $this->logger->info(
186 'Personal data exported',
187 array(
188 'plugin' => 'double-opt-in',
189 // No address in the log (rules/gdpr.md).
190 'count' => count( $items ),
191 )
192 );
193
194 return array(
195 'data' => $items,
196 'done' => true,
197 );
198 }
199
200 /**
201 * Erase personal data for a given email.
202 *
203 * Anonymizes PII (email, IP addresses, form content, form HTML, mail body)
204 * while retaining the consent record (timestamps, confirmed status, consent
205 * text, form ID) as proof of consent per GDPR Art. 7.
206 *
207 * @param string $email The email address.
208 * @param int $page The current page (pagination).
209 *
210 * @return array WordPress privacy eraser response.
211 */
212 public function erasePersonalData( string $email, int $page = 1 ): array {
213 $optIns = $this->repository->findByEmail( $email );
214 $retained = 0;
215 $messages = array();
216
217 foreach ( $optIns as $optIn ) {
218 $anonymized = $this->anonymizeOptIn( $optIn );
219
220 try {
221 $this->repository->save( $anonymized );
222 // The mail error text can quote the address.
223 if ( $this->mailStatus !== null ) {
224 $this->mailStatus->clearError( $optIn->getId() );
225 }
226 ++$retained;
227 } catch ( \RuntimeException $e ) {
228 $this->logger->error(
229 'Failed to anonymize OptIn',
230 array(
231 'plugin' => 'double-opt-in',
232 'id' => $optIn->getId(),
233 'error' => $e->getMessage(),
234 )
235 );
236 }
237 }
238
239 if ( $retained > 0 ) {
240 $messages[] = sprintf(
241 /* translators: %d: number of anonymized records */
242 __( '%d opt-in record(s) anonymized. Consent proof retained per GDPR Art. 7.', 'double-opt-in' ),
243 $retained
244 );
245 }
246
247 $this->logger->info(
248 'Personal data anonymized',
249 array(
250 'plugin' => 'double-opt-in',
251 // No address in the log (rules/gdpr.md).
252 'retained' => $retained,
253 )
254 );
255
256 return array(
257 'items_removed' => 0,
258 'items_retained' => $retained,
259 'messages' => $messages,
260 'done' => true,
261 );
262 }
263
264 /**
265 * Anonymize PII fields on an OptIn entity.
266 *
267 * Clears: email, IP addresses, form content, form HTML, mail body.
268 * Retains: ID, form ID, hash, timestamps, confirmed status, consent text, category.
269 *
270 * @param OptIn $optIn The original OptIn entity.
271 *
272 * @return OptIn The anonymized entity.
273 */
274 private function anonymizeOptIn( OptIn $optIn ): OptIn {
275 return $optIn
276 ->withEmail( 'anonymized-' . $optIn->getId() . '@deleted.invalid' )
277 ->withIpRegister( '0.0.0.0' )
278 ->withIpConfirmation( '0.0.0.0' )
279 ->withIpOptOut( '0.0.0.0' )
280 ->withContent( '' )
281 ->withForm( '' )
282 ->withMailOptIn( '' )
283 ->withFiles( '' );
284 }
285 }
286