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.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 3.1.0 All 34 releases
double-opt-in / src / FormSettings / FormSettingsDTO.php

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

361 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Form Settings Data Transfer Object
4 *
5 * @package Forge12\DoubleOptIn\FormSettings
6 * @since 4.1.0
7 */
8
9 namespace Forge12\DoubleOptIn\FormSettings;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class FormSettingsDTO
17 *
18 * Data Transfer Object for form settings.
19 * Provides a type-safe way to handle form configuration data.
20 */
21 class FormSettingsDTO {
22
23 /**
24 * Whether double opt-in is enabled for this form.
25 *
26 * @var bool
27 */
28 public bool $enabled = false;
29
30 /**
31 * The sender email address.
32 *
33 * @var string
34 */
35 public string $sender = '';
36
37 /**
38 * The sender name.
39 *
40 * @var string
41 */
42 public string $senderName = '';
43
44 /**
45 * The email subject.
46 *
47 * @var string
48 */
49 public string $subject = '';
50
51 /**
52 * The email body content.
53 *
54 * @var string
55 */
56 public string $body = '';
57
58 /**
59 * The recipient field name.
60 *
61 * @var string
62 */
63 public string $recipient = '';
64
65 /**
66 * The confirmation page ID.
67 *
68 * @var int
69 */
70 public int $confirmationPage = -1;
71
72 /**
73 * The error redirect page ID.
74 * When set to a valid page ID, users are redirected there on OptIn errors
75 * instead of seeing the toast notification.
76 *
77 * @var int
78 */
79 public int $errorRedirectPage = -1;
80
81 /**
82 * The dynamic condition field.
83 *
84 * @var string
85 */
86 public string $conditions = 'disabled';
87
88 /**
89 * The email template name.
90 *
91 * @var string
92 */
93 public string $template = '';
94
95 /**
96 * The category ID.
97 *
98 * @var int
99 */
100 public int $category = 0;
101
102 /**
103 * The consent text (GDPR). Wording the user is asked to agree to.
104 *
105 * @var string
106 */
107 public string $consentText = '';
108
109 /**
110 * Form-field name that captures the user's explicit consent
111 * acknowledgment (typically a CF7 [acceptance] checkbox). Together
112 * with `consentText` this forms a GDPR Art. 7 evidence chain:
113 *
114 * - `consentText` — the wording the user was shown
115 * - `consentField` — which form field they had to actively confirm
116 * - `content[consentField]` — the truthy/falsy value at submit time
117 *
118 * Empty string means "no acknowledgment gate" — the consent_text is
119 * stored without a corresponding acknowledgment field. Backward-compat
120 * default; not GDPR-compliant on its own.
121 *
122 * @var string
123 */
124 public string $consentField = '';
125
126 /**
127 * Field mapping for standard placeholders.
128 * Maps doi_* placeholders to actual form field names.
129 * Example: ['doi_email' => 'your-email', 'doi_name' => 'full-name']
130 *
131 * @var array
132 */
133 public array $fieldMapping = array();
134
135 /**
136 * Additional settings from extensions (e.g., Pro version).
137 *
138 * @var array
139 */
140 public array $extensionData = array();
141
142 /**
143 * Create a DTO from an array.
144 *
145 * @param array $data The data array.
146 *
147 * @return self
148 */
149 public static function fromArray( array $data ): self {
150 $dto = new self();
151
152 $dto->enabled = (bool) ( $data['enable'] ?? $data['enabled'] ?? false );
153 $dto->sender = (string) ( $data['sender'] ?? '' );
154 $dto->senderName = (string) ( $data['sender_name'] ?? $data['senderName'] ?? '' );
155 $dto->subject = (string) ( $data['subject'] ?? '' );
156 $dto->body = (string) ( $data['body'] ?? '' );
157 $dto->recipient = (string) ( $data['recipient'] ?? '' );
158 $dto->confirmationPage = (int) ( $data['page'] ?? $data['confirmationPage'] ?? -1 );
159 $dto->errorRedirectPage = (int) ( $data['error_page'] ?? $data['errorRedirectPage'] ?? -1 );
160 $dto->conditions = (string) ( $data['conditions'] ?? 'disabled' );
161 $dto->template = (string) ( $data['template'] ?? '' );
162 $dto->category = (int) ( $data['category'] ?? 0 );
163 $dto->consentText = (string) ( $data['consent_text'] ?? $data['consentText'] ?? '' );
164 $dto->fieldMapping = (array) ( $data['field_mapping'] ?? $data['fieldMapping'] ?? array() );
165 $dto->consentField = (string) ( $data['consent_field'] ?? $data['consentField'] ?? '' );
166
167 // Addon-contributed extension fields land here via the filter
168 // below. Each Pro addon hooks `f12_doi_settings_dto_from_array`
169 // and writes its own keys into `$dto->extensionData[...]`. Core
170 // itself ships zero hardcoded extension fields — addons that
171 // aren't loaded leave their keys absent, and the React form
172 // renderers fall back to defaults via `?? value` patterns.
173 //
174 // Example consumer: addon-unique-email contributes
175 // `unique_email_enabled`, `unique_email_behavior`,
176 // `unique_email_scope`, `unique_email_message`,
177 // `unique_email_redirect_page` from its UniqueEmailAddon::boot().
178
179 /**
180 * Filter to allow addons to add additional fields to the DTO.
181 *
182 * @param FormSettingsDTO $dto The DTO instance.
183 * @param array $data The raw data array.
184 *
185 * @since 4.1.0
186 */
187 $dto = apply_filters( 'f12_doi_settings_dto_from_array', $dto, $data );
188
189 return $dto;
190 }
191
192 /**
193 * Convert the DTO to an array.
194 *
195 * Uses the legacy key names for backward compatibility with post_meta storage.
196 *
197 * @return array
198 */
199 public function toArray(): array {
200 $array = array(
201 'enable' => $this->enabled ? 1 : 0,
202 'sender' => $this->sender,
203 'sender_name' => $this->senderName,
204 'subject' => $this->subject,
205 'body' => $this->body,
206 'recipient' => $this->recipient,
207 'page' => $this->confirmationPage,
208 'error_page' => $this->errorRedirectPage,
209 'conditions' => $this->conditions,
210 'template' => $this->template,
211 'category' => $this->category,
212 'consent_text' => $this->consentText,
213 'consent_field' => $this->consentField,
214 'field_mapping' => $this->fieldMapping,
215 );
216
217 // Merge extension data
218 $array = array_merge( $array, $this->extensionData );
219
220 /**
221 * Filter to allow extensions (e.g., Pro version) to add additional fields to the array.
222 *
223 * @param array $array The array representation.
224 * @param FormSettingsDTO $dto The DTO instance.
225 *
226 * @since 4.1.0
227 */
228 return apply_filters( 'f12_doi_settings_dto_to_array', $array, $this );
229 }
230
231 /**
232 * Convert the DTO to a camelCase array.
233 *
234 * Useful for JSON API responses.
235 *
236 * @return array
237 */
238 public function toCamelCaseArray(): array {
239 $array = array(
240 'enabled' => $this->enabled,
241 'sender' => $this->sender,
242 'senderName' => $this->senderName,
243 'subject' => $this->subject,
244 'body' => $this->body,
245 'recipient' => $this->recipient,
246 'confirmationPage' => $this->confirmationPage,
247 'errorRedirectPage' => $this->errorRedirectPage,
248 'conditions' => $this->conditions,
249 'template' => $this->template,
250 'category' => $this->category,
251 'consentText' => $this->consentText,
252 'consentField' => $this->consentField,
253 'fieldMapping' => $this->fieldMapping,
254 );
255
256 // Merge extension data
257 $array = array_merge( $array, $this->extensionData );
258
259 /**
260 * Filter to allow extensions (e.g., Pro version) to add additional fields to the camelCase array.
261 *
262 * @param array $array The camelCase array representation.
263 * @param FormSettingsDTO $dto The DTO instance.
264 *
265 * @since 4.1.0
266 */
267 return apply_filters( 'f12_doi_settings_dto_to_camel_case_array', $array, $this );
268 }
269
270 /**
271 * Create a DTO with default values.
272 *
273 * @return self
274 */
275 public static function createDefault(): self {
276 $dto = new self();
277 $dto->sender = get_bloginfo( 'admin_email' );
278 return $dto;
279 }
280
281 /**
282 * Return the list of required-field IDs that are missing or invalid
283 * for DOI to function end-to-end.
284 *
285 * Distinct from {@see FormSettingsValidator::validate()} — the Validator
286 * runs at REST-save time on raw input. This method answers a different
287 * question: "if this form is enabled right now, will DOI actually
288 * work?" — the answer drives the UI completeness-gate that auto-disables
289 * misconfigured forms and surfaces a "configuration incomplete" badge.
290 *
291 * Hart-required set (decisions locked 2026-05-12, see
292 * plan/doi-completeness-gate.md §5):
293 * - `recipient` — without it the integration aborts with NO_RECIPIENT
294 * (Avada/GF/WPForms) or silently falls back to the literal string
295 * 'email' (Elementor's hardcoded default).
296 * - `subject` — empty subjects are accepted by wp_mail but mark the
297 * mail as spam in most clients.
298 * - `body_or_template` — either the body contains the
299 * `[doubleoptinlink]` placeholder OR a non-empty `template` is set.
300 * If both are missing the user has no clickable confirmation link
301 * and DOI is unusable.
302 *
303 * NOT in this set:
304 * - `enabled` — incomplete-AND-enabled is the exact bug we want to
305 * detect, not part of the "incomplete" definition.
306 * - `sender` — soft-required (per §5.3): leaving it empty falls back
307 * to the WP admin email, which is functional, just not ideal.
308 *
309 * Pro addons can append integration-specific entries via the
310 * `f12_doi_form_missing_required_fields` filter (per §5.5).
311 *
312 * @return array<int,string> Stable string IDs ('recipient', 'subject',
313 * 'body_or_template', or addon-contributed).
314 * Empty array = configuration is complete.
315 *
316 * @since 4.5.0
317 */
318 public function getMissingRequiredFields(): array {
319 $missing = array();
320
321 if ( empty( $this->recipient ) ) {
322 $missing[] = 'recipient';
323 }
324
325 if ( empty( $this->subject ) ) {
326 $missing[] = 'subject';
327 }
328
329 $hasBodyWithLink = ! empty( $this->body )
330 && strpos( $this->body, '[doubleoptinlink]' ) !== false;
331 $hasTemplate = ! empty( $this->template );
332 if ( ! $hasBodyWithLink && ! $hasTemplate ) {
333 $missing[] = 'body_or_template';
334 }
335
336 /**
337 * Filter to let addons append integration-specific
338 * required-field IDs.
339 *
340 * @since 4.5.0
341 *
342 * @param array<int,string> $missing Already-collected required-field IDs.
343 * @param FormSettingsDTO $dto The DTO being inspected.
344 */
345 return apply_filters( 'f12_doi_form_missing_required_fields', $missing, $this );
346 }
347
348 /**
349 * Convenience wrapper. True iff {@see getMissingRequiredFields()} is empty.
350 *
351 * Does NOT check `$this->enabled` — "complete but disabled" is a
352 * legitimate state for a just-created form. The completeness-gate
353 * uses this to decide whether `enabled` may flip to true.
354 *
355 * @since 4.5.0
356 */
357 public function isFunctionallyComplete(): bool {
358 return empty( $this->getMissingRequiredFields() );
359 }
360 }
361