PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.8.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.8.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 3.0.51 All 41 releases
double-opt-in / src / Service / PrivacyIntegration.php

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

280 lines 7.3 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 $data[] = array(
165 'name' => __( 'Confirmation mail', 'double-opt-in' ),
166 'value' => trim( $mail['status'] . ' ' . $mail['at'] . ( $mail['error'] !== '' ? ' — ' . $mail['error'] : '' ) ),
167 );
168 }
169 }
170
171 $items[] = array(
172 'group_id' => 'double-opt-in',
173 'group_label' => __( 'Double Opt-In Records', 'double-opt-in' ),
174 'item_id' => 'doi-' . $optIn->getId(),
175 'data' => $data,
176 );
177 }
178
179 $this->logger->info(
180 'Personal data exported',
181 array(
182 'plugin' => 'double-opt-in',
183 // No address in the log (rules/gdpr.md).
184 'count' => count( $items ),
185 )
186 );
187
188 return array(
189 'data' => $items,
190 'done' => true,
191 );
192 }
193
194 /**
195 * Erase personal data for a given email.
196 *
197 * Anonymizes PII (email, IP addresses, form content, form HTML, mail body)
198 * while retaining the consent record (timestamps, confirmed status, consent
199 * text, form ID) as proof of consent per GDPR Art. 7.
200 *
201 * @param string $email The email address.
202 * @param int $page The current page (pagination).
203 *
204 * @return array WordPress privacy eraser response.
205 */
206 public function erasePersonalData( string $email, int $page = 1 ): array {
207 $optIns = $this->repository->findByEmail( $email );
208 $retained = 0;
209 $messages = array();
210
211 foreach ( $optIns as $optIn ) {
212 $anonymized = $this->anonymizeOptIn( $optIn );
213
214 try {
215 $this->repository->save( $anonymized );
216 // The mail error text can quote the address.
217 if ( $this->mailStatus !== null ) {
218 $this->mailStatus->clearError( $optIn->getId() );
219 }
220 ++$retained;
221 } catch ( \RuntimeException $e ) {
222 $this->logger->error(
223 'Failed to anonymize OptIn',
224 array(
225 'plugin' => 'double-opt-in',
226 'id' => $optIn->getId(),
227 'error' => $e->getMessage(),
228 )
229 );
230 }
231 }
232
233 if ( $retained > 0 ) {
234 $messages[] = sprintf(
235 /* translators: %d: number of anonymized records */
236 __( '%d opt-in record(s) anonymized. Consent proof retained per GDPR Art. 7.', 'double-opt-in' ),
237 $retained
238 );
239 }
240
241 $this->logger->info(
242 'Personal data anonymized',
243 array(
244 'plugin' => 'double-opt-in',
245 // No address in the log (rules/gdpr.md).
246 'retained' => $retained,
247 )
248 );
249
250 return array(
251 'items_removed' => 0,
252 'items_retained' => $retained,
253 'messages' => $messages,
254 'done' => true,
255 );
256 }
257
258 /**
259 * Anonymize PII fields on an OptIn entity.
260 *
261 * Clears: email, IP addresses, form content, form HTML, mail body.
262 * Retains: ID, form ID, hash, timestamps, confirmed status, consent text, category.
263 *
264 * @param OptIn $optIn The original OptIn entity.
265 *
266 * @return OptIn The anonymized entity.
267 */
268 private function anonymizeOptIn( OptIn $optIn ): OptIn {
269 return $optIn
270 ->withEmail( 'anonymized-' . $optIn->getId() . '@deleted.invalid' )
271 ->withIpRegister( '0.0.0.0' )
272 ->withIpConfirmation( '0.0.0.0' )
273 ->withIpOptOut( '0.0.0.0' )
274 ->withContent( '' )
275 ->withForm( '' )
276 ->withMailOptIn( '' )
277 ->withFiles( '' );
278 }
279 }
280