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 / FormSettings / FormSettingsDTO.php

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

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