PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.3.2
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.3.2
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 3.0.60 3.0.61 3.0.62 All 38 releases
double-opt-in / compatibility / OptInFrontend.class.php

OptInFrontend.class.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.3.2, at compatibility/OptInFrontend.class.php

1,343 lines 40.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace forge12\contactform7\CF7DoubleOptIn;
4
5
6 use Forge12\DoubleOptIn\Container\Container;
7 use Forge12\DoubleOptIn\EmailTemplates\PlaceholderMapper;
8 use Forge12\DoubleOptIn\EventSystem\EventDispatcherInterface;
9 use Forge12\DoubleOptIn\Events\Lifecycle\OptInConfirmedEvent;
10 use Forge12\DoubleOptIn\Events\Lifecycle\OptInCreatedEvent;
11 use Forge12\DoubleOptIn\Frontend\ErrorNotification;
12 use Forge12\DoubleOptIn\Integration\OptInError;
13 use Forge12\DoubleOptIn\Service\RateLimiter;
14 use Forge12\Shared\Logger;
15 use Forge12\Shared\LoggerInterface;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * @deprecated 4.0.0 Use \Forge12\DoubleOptIn\Integration\AbstractFormIntegration instead.
23 *
24 * This class is maintained for backward compatibility only.
25 * New integrations should extend AbstractFormIntegration and implement FormIntegrationInterface.
26 *
27 * @see \Forge12\DoubleOptIn\Integration\AbstractFormIntegration
28 * @see \Forge12\DoubleOptIn\Integration\FormIntegrationInterface
29 */
30 abstract class OptInFrontend {
31 /**
32 * The Type of the OptIn Form System
33 *
34 * @var string
35 */
36 protected string $type = '';
37
38 private LoggerInterface $logger;
39
40 /**
41 * Stored hCaptcha CF7 instance for restore after mail send.
42 *
43 * @var object|null
44 */
45 private $hcaptchaCf7Instance = null;
46
47 /**
48 * Stored hCaptcha CF7 filter priority for restore after mail send.
49 *
50 * @var int
51 */
52 private int $hcaptchaCf7Priority = 20;
53
54 /**
55 * Validation status from the last validateOptIn() call.
56 *
57 * @var string
58 */
59 private static string $validationStatus = '';
60
61 /**
62 * Last error from maybeCreateOptIn(), if any.
63 *
64 * @var OptInError|null
65 */
66 protected ?OptInError $lastCreationError = null;
67
68 /**
69 * Get the validation status from the last validateOptIn() call.
70 *
71 * @return string One of: '', 'confirmed', 'already_confirmed', 'expired', 'not_found'.
72 */
73 public static function getValidationStatus(): string {
74 return self::$validationStatus;
75 }
76
77 /**
78 * Set the validation status.
79 *
80 * @param string $status The validation status.
81 */
82 private function setValidationStatus( string $status ): void {
83 self::$validationStatus = $status;
84 }
85
86 /**
87 * Constructor for the class.
88 *
89 * This constructor registers the necessary actions to be performed,
90 * by hooking methods of the current class, for the following events:
91 * - 'f12_cf7_doubleoptin_before_send_default_mail'
92 * - 'f12_cf7_doubleoptin_after_send_default_mail'
93 * - 'f12_cf7_doubleoptin_trigger_default_mail'
94 * - 'shutdown'
95 *
96 * @return void
97 */
98 public function __construct( LoggerInterface $logger, string $type ) {
99 $this->logger = $logger;
100 $this->type = $type;
101
102 $this->get_logger()->debug( 'Base integration constructor called', [
103 'plugin' => 'double-opt-in',
104 'class' => __CLASS__,
105 'method' => __METHOD__,
106 'type' => $type,
107 ] );
108
109 add_action( 'f12_cf7_doubleoptin_before_send_default_mail', [ $this, 'beforeSendDefaultMail' ], 10, 1 );
110 $this->get_logger()->debug( 'Hook f12_cf7_doubleoptin_before_send_default_mail registered', [
111 'plugin' => 'double-opt-in',
112 ] );
113
114 add_action( 'f12_cf7_doubleoptin_after_send_default_mail', [ $this, 'afterSendDefaultMail' ], 10, 1 );
115 $this->get_logger()->debug( 'Hook f12_cf7_doubleoptin_after_send_default_mail registered', [
116 'plugin' => 'double-opt-in',
117 ] );
118
119 add_action( 'f12_cf7_doubleoptin_trigger_default_mail', [ $this, 'sendDefaultMail' ], 10, 1 );
120 $this->get_logger()->debug( 'Hook f12_cf7_doubleoptin_trigger_default_mail registered', [
121 'plugin' => 'double-opt-in',
122 ] );
123
124 add_action( 'shutdown', [ $this, 'removeFiles' ] );
125 $this->get_logger()->debug( 'Hook shutdown registered for removeFiles', [
126 'plugin' => 'double-opt-in',
127 ] );
128
129 add_action( 'init', [ $this, 'validateOptIn' ] );
130 $this->get_logger()->debug( 'Hook init registered for validateOptIn', [
131 'plugin' => 'double-opt-in',
132 ] );
133
134 add_action( 'wp_footer', [ $this, 'renderValidationFeedback' ] );
135
136 // Default feedback handler for validation statuses
137 add_action( 'f12_cf7_doubleoptin_validation_feedback', function ( $status ) {
138 if ( $status === 'confirmed' ) {
139 return;
140 }
141
142 $messages = [
143 'already_confirmed' => __( 'Your opt-in has already been confirmed.', 'double-opt-in' ),
144 'expired' => __( 'This confirmation link has expired. Please submit the form again.', 'double-opt-in' ),
145 'not_found' => __( 'This confirmation link is invalid.', 'double-opt-in' ),
146 ];
147
148 $message = $messages[ $status ] ?? '';
149 if ( ! empty( $message ) ) {
150 echo '<div class="doi-validation-notice doi-notice-' . esc_attr( $status ) . '">';
151 echo '<p>' . esc_html( $message ) . '</p>';
152 echo '</div>';
153 }
154 }, 10, 1 );
155
156 add_filter( 'f12_cf7_doubleoptin_get_recipient_' . $this->type, [ $this, 'getRecipient' ], 10, 3 );
157 $this->get_logger()->debug( 'Filter f12_cf7_doubleoptin_get_recipient_' . $this->type . ' registered', [
158 'plugin' => 'double-opt-in',
159 ] );
160 }
161
162
163 public function get_logger() {
164 return $this->logger;
165 }
166
167 /**
168 * Retrieves the recipient for a given recipient name, form parameters, and post parameters.
169 *
170 * @param string $recipient The name of the recipient to retrieve.
171 * @param array $formParameter The array of form parameters.
172 * @param array $postParameter The array of post parameters.
173 *
174 * @return string The recipient for the given parameters.
175 */
176 abstract public function getRecipient( string $recipient, array $formParameter, array $postParameter ): string;
177
178 /**
179 * Sends a default mail for the given OptIn object.
180 *
181 * This method is declared as abstract, meaning that it must be implemented
182 * by any child class that extends the current class. The method takes
183 * a single parameter, $OptIn, of type OptIn. This parameter represents
184 * the OptIn object for which the default mail needs to be sent.
185 *
186 * This method should be overridden by child classes to define the specific
187 * logic for sending the default mail for the given OptIn object.
188 *
189 * Note that the implementation details of this method vary depending on the
190 * specific subclass. Therefore, the implementation code is not provided here.
191 *
192 * @param OptIn $OptIn The OptIn object for which the default mail needs to be sent.
193 *
194 * @return void
195 */
196 abstract public function sendDefaultMail( OptIn $OptIn ): void;
197
198 public function disable_contact_form_7_captcha($is_active){
199 if($is_active){
200 $this->get_logger()->debug( 'Forge12 CF7Captcha filters and actions removed', ['plugin' => 'double-opt-in']);
201 return false;
202 }
203 $this->get_logger()->debug( 'Forge12 CF7Captcha filters and actions removed', ['plugin' => 'double-opt-in']);
204 return $is_active;
205 }
206
207 /**
208 * This method is used to perform necessary actions before sending the default mail.
209 *
210 * This method removes the filter for the forge12 spam captcha if the class
211 * '\forge12\contactform7\CF7Captcha\TimerValidatorCF7' exists. It removes the filters 'wpcf7_spam' for the methods
212 * '\forge12\contactform7\CF7Captcha::isSpam' and
213 * '\forge12\contactform7\CF7Captcha\CF7IPLog::isSpam', and also removes the action 'wpcf7_mail_sent' for the
214 * method
215 * '\forge12\contactform7\CF7Captcha\CF7IPLog::doLogIP', if these filters and action exist.
216 *
217 * Additionally, this method removes the filter 'wpcf7_spam' for the method 'wpcf7_recaptcha_verify_response' with
218 * a priority of 9.
219 *
220 * @return void
221 */
222 public function beforeSendDefaultMail() {
223 $this->get_logger()->debug( 'beforeSendDefaultMail called', [
224 'plugin' => 'double-opt-in',
225 'class' => __CLASS__,
226 'method' => __METHOD__,
227 ] );
228
229 // Remove the filter for the Forge12 spam captcha
230 add_filter('f12_cf7_captcha_is_installed_cf7', [$this, 'disable_contact_form_7_captcha'], 999, 1);
231 $this->get_logger()->debug( 'Forge12 CF7Captcha filters and actions removed', [
232 'plugin' => 'double-opt-in',
233 ] );
234
235 // Remove the filter for the Google reCAPTCHA validation
236 remove_filter( 'wpcf7_spam', 'wpcf7_recaptcha_verify_response', 9 );
237
238 $this->get_logger()->debug( 'Google reCAPTCHA filter removed', [
239 'plugin' => 'double-opt-in',
240 ] );
241
242 // Remove hCaptcha validation filter
243 $this->removeHCaptchaFilter();
244 }
245
246
247 /**
248 * Performs actions after sending the default mail.
249 *
250 * In this method, two filters and an action are added to the WordPress hooks system.
251 *
252 * The first filter is added with the hook name 'wpcf7_spam' and the callback
253 * function is set to 'wpcf7_recaptcha_verify_response'. The priority is set to 9
254 * and the number of accepted arguments for the callback function is 2.
255 * This filter is added only if the function 'wpcf7_recaptcha_verifiy_response'
256 * exists.
257 *
258 * The second filter is added with the hook name 'wpcf7_spam' and the callback
259 * function is set to '\forge12\contactform7\CF7Captcha\TimerValidatorCF7::isSpam'.
260 * The priority is set to 100 and the number of accepted arguments for the callback
261 * function is 2. This filter is added only if the class '\forge12\contactform7\CF7Captcha\TimerValidatorCF7'
262 * exists.
263 *
264 * The third filter is added with the hook name 'wpcf7_spam' and the callback
265 * function is set to '\forge12\contactform7\CF7Captcha\CF7IPLog::isSpam'.
266 * The priority is set to 100 and the number of accepted arguments for the callback
267 * function is 2. This filter is added only if the class '\forge12\contactform7\CF7Captcha\CF7IPLog'
268 * exists.
269 *
270 * An action is added with the hook name 'wpcf7_mail_sent' and the callback function
271 * is set to '\forge12\contactform7\CF7Captcha\CF7IPLog::doLogIP'. The priority is set
272 * to 100 and the number of accepted arguments for the callback function is 1.
273 * This action is added only if the class '\forge12\contactform7\CF7Captcha\CF7IPLog' exists.
274 *
275 * @return void
276 */
277 public function afterSendDefaultMail() {
278 $this->get_logger()->debug( 'afterSendDefaultMail called', [
279 'plugin' => 'double-opt-in',
280 'class' => __CLASS__,
281 'method' => __METHOD__,
282 ] );
283
284 // re-add the filter to ensure for all other forms the reCAPTCHA is used
285 if ( function_exists( 'wpcf7_recaptcha_verify_response' ) ) {
286 add_filter( 'wpcf7_spam', 'wpcf7_recaptcha_verify_response', 9, 2 );
287 $this->get_logger()->debug( 'Google reCAPTCHA filter re-added', [
288 'plugin' => 'double-opt-in',
289 ] );
290 }
291
292 // re-add the filter for the Forge12 spam captcha
293 if ( class_exists( '\forge12\contactform7\CF7Captcha\TimerValidatorCF7' ) ) {
294 add_filter( 'wpcf7_spam', '\forge12\contactform7\CF7Captcha\TimerValidatorCF7::isSpam', 100, 2 );
295 add_filter( 'wpcf7_spam', '\forge12\contactform7\CF7Captcha\CF7IPLog::isSpam', 100, 2 );
296 add_action( 'wpcf7_mail_sent', '\forge12\contactform7\CF7Captcha\CF7IPLog::doLogIP', 100, 1 );
297
298 $this->get_logger()->debug( 'Forge12 CF7Captcha filters and actions re-added', [
299 'plugin' => 'double-opt-in',
300 ] );
301 }
302
303 // re-add hCaptcha validation filter
304 $this->restoreHCaptchaFilter();
305 }
306
307 /**
308 * Remove hCaptcha CF7 validation filter and store the instance for later restore.
309 *
310 * @return void
311 */
312 private function removeHCaptchaFilter(): void {
313 if ( ! class_exists( '\HCaptcha\CF7\CF7' ) ) {
314 return;
315 }
316
317 global $wp_filter;
318
319 if ( ! isset( $wp_filter['wpcf7_validate'] ) ) {
320 return;
321 }
322
323 foreach ( $wp_filter['wpcf7_validate']->callbacks as $priority => $hooks ) {
324 foreach ( $hooks as $key => $hook ) {
325 if ( is_array( $hook['function'] ) && $hook['function'][0] instanceof \HCaptcha\CF7\CF7 ) {
326 $this->hcaptchaCf7Instance = $hook['function'][0];
327 $this->hcaptchaCf7Priority = $priority;
328 remove_filter( 'wpcf7_validate', $hook['function'], $priority );
329 $this->get_logger()->debug( 'hCaptcha CF7 validation filter removed', [
330 'plugin' => 'double-opt-in',
331 ] );
332
333 return;
334 }
335 }
336 }
337 }
338
339 /**
340 * Re-add hCaptcha CF7 validation filter if it was previously removed.
341 *
342 * @return void
343 */
344 private function restoreHCaptchaFilter(): void {
345 if ( isset( $this->hcaptchaCf7Instance ) ) {
346 add_filter( 'wpcf7_validate', [ $this->hcaptchaCf7Instance, 'verify_hcaptcha' ], $this->hcaptchaCf7Priority, 2 );
347 $this->get_logger()->debug( 'hCaptcha CF7 validation filter re-added', [
348 'plugin' => 'double-opt-in',
349 ] );
350 $this->hcaptchaCf7Instance = null;
351 }
352 }
353
354 /**
355 * Updates the opt-in status by hash.
356 *
357 * @param string $hash The opt-in hash.
358 * @param int $value The opt-in value to set.
359 * @param OptIn|null $OptIn The opt-in object. Optional.
360 *
361 * @return int Returns 0 if the opt-in is already confirmed, 1 if the opt-in is successfully updated.
362 */
363 protected function updateOptInByHash( string $hash, int $value, ?OptIn $OptIn = null ): int {
364 $this->get_logger()->debug( 'updateOptInByHash called', [
365 'plugin' => 'double-opt-in',
366 'class' => __CLASS__,
367 'method' => __METHOD__,
368 'hash' => $hash,
369 'value' => $value,
370 ] );
371
372 $OptIn = $OptIn ?? OptIn::get_by_hash( $hash );
373
374 if ( ! $OptIn ) {
375 $this->get_logger()->warning( 'No OptIn found for hash', [
376 'plugin' => 'double-opt-in',
377 'hash' => $hash,
378 ] );
379 return 0;
380 }
381
382 if ( $OptIn->is_confirmed() ) {
383 $this->get_logger()->info( 'OptIn already confirmed, skipping update', [
384 'plugin' => 'double-opt-in',
385 'hash' => $hash,
386 'optin_id' => $OptIn->get_id(),
387 ] );
388
389 do_action( 'f12_cf7_doubleoptin_already_confirmed', $hash, $OptIn );
390 return 0;
391 }
392
393 do_action( 'f12_cf7_doubleoptin_before_confirm', $hash, $OptIn );
394
395 $OptIn->set_doubleoptin( $value );
396 $OptIn->set_updatetime( time() );
397 $OptIn->set_ipaddr_confirmation( IPHelper::getIPAdress() );
398
399 $telemetry = new Telemetry( $this->get_logger() );
400
401 $result = $OptIn->save();
402
403 if ( $result ) {
404 if ( (int)$value === 1 ) {
405 $telemetry->increment( 'confirmed_optins' );
406
407 // Dispatch typed event for new event-driven architecture
408 $this->dispatchOptInConfirmedEvent( $OptIn, $hash );
409 }
410
411 $this->get_logger()->info( 'OptIn updated successfully', [
412 'plugin' => 'double-opt-in',
413 'hash' => $hash,
414 'optin_id' => $OptIn->get_id(),
415 ] );
416 do_action( 'f12_cf7_doubleoptin_after_confirm', $hash, $OptIn );
417 } else {
418 $this->get_logger()->error( 'Failed to update OptIn', [
419 'plugin' => 'double-opt-in',
420 'hash' => $hash,
421 'optin_id' => $OptIn->get_id(),
422 ] );
423 }
424
425 return (int) $result;
426 }
427
428
429 /**
430 * Add additional placeholder like time, date, subject
431 *
432 * @formatter:off
433 *
434 * @param string $body The content containing the placeholder that will be replaced.
435 * @param OptIn $OptIn The OptIn Object.
436 *
437 * @param array $parameter {
438 * @type string $formUrl The URL where the form is displayed.
439 * @type string $subject The Subject of the Form
440 * }
441 * #
442 * @formatter:on
443 */
444 protected function addPlaceholders( string $body, OptIn $OptIn, array $parameter ): string {
445 $this->get_logger()->debug( 'addPlaceholders called', [
446 'plugin' => 'double-opt-in',
447 'class' => __CLASS__,
448 'method' => __METHOD__,
449 'optin_id' => $OptIn->get_id(),
450 'parameter' => $parameter,
451 ] );
452
453 $placeholder = [
454 // User-influenced submit URL — sanitise so a crafted query string
455 // can't reflect into the mail HTML (esc_url_raw keeps text-mail intact).
456 'doubleoptin_form_url' => esc_url_raw( (string) ( $parameter['formUrl'] ?? '' ) ),
457 'doubleoptin_form_subject' => $parameter['subject'] ?? '',
458 // wp_date() formats in the site's timezone without mutating PHP's
459 // global timezone (the old date() + date_default_timezone_set() did).
460 'doubleoptin_form_date' => wp_date( get_option( 'date_format' ) ),
461 'doubleoptin_form_time' => wp_date( get_option( 'time_format' ) ),
462 'doubleoptin_form_email' => get_option( 'admin_email' ),
463 'doubleoptinlink' => $OptIn->get_link_optin( $parameter ),
464 'doubleoptoutlink' => $OptIn->get_link_optout(),
465 'doubleoptin_privacy_url' => $this->getPrivacyPolicyUrl(),
466 ];
467
468
469 foreach ( $placeholder as $key => $value ) {
470 if ( is_array( $value ) || is_object( $value ) ) {
471 // Array/Objekte serialisieren oder in JSON umwandeln
472 $value = wp_json_encode( $value );
473 }
474
475 $replacement = (string) ( $value ?? '' );
476
477 $body = str_replace( '[' . $key . ']', $replacement, $body );
478
479 // Logging je Platzhalter
480 $this->get_logger()->debug( 'Placeholder replaced', [
481 'plugin' => 'double-opt-in',
482 'placeholder' => '[' . $key . ']',
483 'value' => $replacement,
484 ] );
485 }
486
487 // Logging nach allen Ersetzungen
488 $this->get_logger()->debug( 'System placeholders replaced in body', [
489 'plugin' => 'double-opt-in',
490 'placeholders' => array_keys( $placeholder ),
491 'body_length' => strlen( $body ),
492 ] );
493
494 // Replace standard placeholders (doi_email, doi_name, etc.)
495 $formData = maybe_unserialize( $OptIn->get_content() );
496 if ( is_array( $formData ) ) {
497 // Per-integration nesting unwrap. The serialized OptIn content
498 // is whatever each frontend stored, and that differs by
499 // integration — see SubmittedContent for the table.
500 // Without this, PlaceholderMapper::replacePlaceholders looks
501 // for `$formData[$mappedField]` at the top level and finds
502 // nothing for Elementor — every `[doi_*]` placeholder renders
503 // as an empty string in the confirmation mail.
504 //
505 // The chain used to be spelled out here. It now lives in
506 // SubmittedContent because the audit reader needs the same
507 // knowledge, and the copy it had was one shape short — every
508 // Elementor opt-in reported its consent checkbox as unticked
509 // (customer report 2026-08-27).
510 $fieldData = \Forge12\DoubleOptIn\Integration\SubmittedContent::unwrapFields( $formData );
511
512 $body = PlaceholderMapper::replacePlaceholders(
513 $body,
514 $fieldData,
515 $OptIn->get_cf_form_id(),
516 [],
517 $this->type
518 );
519
520 $this->get_logger()->debug( 'Standard placeholders replaced', [
521 'plugin' => 'double-opt-in',
522 'form_id' => $OptIn->get_cf_form_id(),
523 'field_data_keys' => array_keys( $fieldData ),
524 'unwrapped_from' => \Forge12\DoubleOptIn\Integration\SubmittedContent::describeShape( $formData ),
525 ] );
526 }
527
528 return $body;
529 }
530
531
532 /**
533 * Add Stylesheets
534 */
535 public function validateOptIn(): bool {
536 $this->get_logger()->debug( 'validateOptIn started', [
537 'plugin' => 'double-opt-in',
538 ] );
539
540 /**
541 * Skip if the hash has not been submitted.
542 */
543 if ( ! isset( $_GET['optin'] ) ) {
544 $this->get_logger()->debug( 'No optin hash found in request, skipping', [
545 'plugin' => 'double-opt-in',
546 ] );
547 return false;
548 }
549
550 /**
551 * Get the Hash
552 */
553 $hash = sanitize_text_field( $_GET['optin'] );
554
555 /**
556 * Load the OptIn
557 */
558 $OptIn = OptIn::get_by_hash( $hash );
559
560 /**
561 * Skip if the OptIn does not exist
562 */
563 if ( null == $OptIn ) {
564 $this->get_logger()->warning( 'OptIn not found for hash', [
565 'plugin' => 'double-opt-in',
566 'hash' => $hash,
567 ] );
568 $this->setValidationStatus( 'not_found' );
569 return false;
570 }
571
572 /**
573 * Skip if the OptIn is not from Type cf7.
574 */
575 if ( ! $OptIn->isType( $this->type ) ) {
576 $this->get_logger()->warning( 'OptIn type mismatch', [
577 'plugin' => 'double-opt-in',
578 'hash' => $hash,
579 'type' => $this->type,
580 'optin_id' => $OptIn->get_id(),
581 ] );
582 return false;
583 }
584
585 $this->get_logger()->debug( 'OptIn type found', [
586 'plugin' => 'double-opt-in',
587 'hash' => $hash,
588 'type' => $this->type,
589 'optin_id' => $OptIn->get_id(),
590 ] );
591
592 /**
593 * Check if the token has expired.
594 */
595 $settings = CF7DoubleOptIn::getInstance()->getSettings();
596 $expiryHours = (int) ( $settings['token_expiry_hours'] ?? 48 );
597 if ( $expiryHours > 0 && ( time() - (int) $OptIn->get_createtime() ) > ( $expiryHours * 3600 ) ) {
598 $this->get_logger()->info( 'OptIn token expired', [
599 'plugin' => 'double-opt-in',
600 'hash' => $hash,
601 'optin_id' => $OptIn->get_id(),
602 'expiry_hours' => $expiryHours,
603 ] );
604 do_action( 'f12_cf7_doubleoptin_token_expired', $hash, $OptIn );
605 $this->setValidationStatus( 'expired' );
606 return false;
607 }
608
609 /**
610 * Check if already confirmed (before calling updateOptInByHash).
611 */
612 if ( $OptIn->is_confirmed() ) {
613 $this->get_logger()->info( 'OptIn already confirmed', [
614 'plugin' => 'double-opt-in',
615 'hash' => $hash,
616 'optin_id' => $OptIn->get_id(),
617 ] );
618 do_action( 'f12_cf7_doubleoptin_already_confirmed', $hash, $OptIn );
619 $this->setValidationStatus( 'already_confirmed' );
620 return false;
621 }
622
623 /**
624 * Confirm the OptIn.
625 */
626 if ( $this->updateOptInByHash( $hash, 1, $OptIn ) <= 0 ) {
627 $this->get_logger()->info( 'OptIn update failed', [
628 'plugin' => 'double-opt-in',
629 'hash' => $hash,
630 'optin_id' => $OptIn->get_id(),
631 ] );
632 return false;
633 }
634
635 $this->setValidationStatus( 'confirmed' );
636
637 /**
638 * Enable / Disable default mail.
639 *
640 * @param bool $status Enable (true) or disable (false) the default mail.
641 * @param int $postId The ID of the Post / Form.
642 *
643 * @since 2.3.3
644 */
645 if ( ! apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $OptIn->get_cf_form_id() ) ) {
646 $this->get_logger()->info( 'Default mail disabled for OptIn', [
647 'plugin' => 'double-opt-in',
648 'form_id' => $OptIn->get_cf_form_id(),
649 'optin_id' => $OptIn->get_id(),
650 ] );
651 return false;
652 }
653
654 $this->get_logger()->debug( 'Triggering before_send_default_mail hook', [
655 'plugin' => 'double-opt-in',
656 'optin_id' => $OptIn->get_id(),
657 ] );
658 do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $OptIn );
659
660 $this->get_logger()->info( 'Triggering send_default_mail hook', [
661 'plugin' => 'double-opt-in',
662 'optin_id' => $OptIn->get_id(),
663 ] );
664 do_action( 'f12_cf7_doubleoptin_trigger_default_mail', $OptIn );
665
666 $this->get_logger()->debug( 'Triggering after_send_default_mail hook', [
667 'plugin' => 'double-opt-in',
668 'optin_id' => $OptIn->get_id(),
669 ] );
670 do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $OptIn );
671
672 return true;
673 }
674
675
676
677 /**
678 * Store the files
679 *
680 * @param array $inFiles
681 *
682 * @return array
683 */
684 private function maybeStoreFiles( array $inFiles ): array {
685 $this->get_logger()->debug( 'maybeStoreFiles called', [
686 'plugin' => 'double-opt-in',
687 'class' => __CLASS__,
688 'method' => __METHOD__,
689 'files_in' => $inFiles,
690 ] );
691
692 $outFiles = [];
693
694 if ( empty( $inFiles ) ) {
695 $this->get_logger()->debug( 'No files provided to maybeStoreFiles', [
696 'plugin' => 'double-opt-in',
697 ] );
698 return $outFiles;
699 }
700
701 foreach ( $inFiles as $key => $subfiles ) {
702 foreach ( $subfiles as $file ) {
703 $newFile = $this->copyAndRenameFile( $file );
704 if ( $newFile ) {
705 $outFiles[] = $newFile;
706 $this->get_logger()->debug( 'File stored successfully', [
707 'plugin' => 'double-opt-in',
708 'original' => $file,
709 'new' => $newFile,
710 ] );
711 } else {
712 $this->get_logger()->warning( 'File could not be stored', [
713 'plugin' => 'double-opt-in',
714 'original' => $file,
715 ] );
716 }
717 }
718 }
719
720 $this->get_logger()->debug( 'maybeStoreFiles completed', [
721 'plugin' => 'double-opt-in',
722 'files_out' => $outFiles,
723 ] );
724
725 return $outFiles;
726 }
727
728
729 /**
730 * Copy and rename a file
731 *
732 * @param string $file The path to the file to copy and rename
733 *
734 * @return string|null The path to the copied and renamed file, or null if the copy operation failed
735 */
736 private function copyAndRenameFile( string $file ): ?string {
737 $this->get_logger()->debug( 'copyAndRenameFile called', [
738 'plugin' => 'double-opt-in',
739 'class' => __CLASS__,
740 'method' => __METHOD__,
741 'file' => $file,
742 ] );
743
744 $newFile = explode( '/', $file );
745 $name = $newFile[ count( $newFile ) - 1 ];
746 $name = time() . '_' . $name;
747 $newFile[ count( $newFile ) - 1 ] = $name;
748 $newFile = implode( "/", $newFile );
749
750 if ( copy( $file, $newFile ) ) {
751 $this->get_logger()->info( 'File copied and renamed successfully', [
752 'plugin' => 'double-opt-in',
753 'original' => $file,
754 'new_file' => $newFile,
755 ] );
756 return $newFile;
757 }
758
759 $this->get_logger()->error( 'Failed to copy and rename file', [
760 'plugin' => 'double-opt-in',
761 'original' => $file,
762 'target' => $newFile,
763 ] );
764
765 return null;
766 }
767
768
769 /**
770 * Create the OptIn
771 *
772 * @param int $formId The identifier of the form
773 * @param string $formHtml The HTML code of the form
774 * @param array $parameter The Post Parameter of the form.
775 * @param array $files The Files attached to the form.
776 *
777 * @return OptIn|null
778 */
779 protected function maybeCreateOptIn( int $formId, string $formHtml, array $parameter, array $files = array() ): ?OptIn {
780 $this->get_logger()->debug( 'maybeCreateOptIn called', [
781 'plugin' => 'double-opt-in',
782 'class' => __CLASS__,
783 'method' => __METHOD__,
784 'formId' => $formId,
785 'formHtml' => substr( $formHtml, 0, 200 ),
786 'files' => $files,
787 ] );
788
789 /**
790 * Mögliche Dateien speichern, um sie während der Opt-In-Bestätigung vorzuhalten
791 */
792 $files = $this->maybeStoreFiles( $files );
793 $this->get_logger()->debug( 'Files checked and possibly stored', [
794 'plugin' => 'double-opt-in',
795 'files' => $files,
796 ] );
797
798 /**
799 * Filter, um die Parameter vor dem Speichern in der Datenbank zu manipulieren
800 */
801 $parameter = \apply_filters( 'f12_cf7_doubleoptin_add_request_parameter', $parameter );
802 $this->get_logger()->debug( 'Request parameters filtered', [
803 'plugin' => 'double-opt-in',
804 'parameter' => $parameter,
805 ] );
806
807 /**
808 * Globale Einstellungen für das Formular abrufen
809 */
810 $formParameter = CF7DoubleOptIn::getInstance()->getParameter( $formId );
811
812 /**
813 * Filter, um den Empfänger zu ermitteln, bevor das OptIn-Objekt erstellt wird
814 */
815 $recipient = \apply_filters( 'f12_cf7_doubleoptin_get_recipient_' . $this->type, '', $formParameter, $parameter );
816
817 /**
818 * Wenn keine E-Mail-Adresse gefunden wurde, Abbruch
819 */
820 if ( empty( $recipient ) ) {
821 $this->get_logger()->warning( 'No recipient found, skipping OptIn creation', [
822 'plugin' => 'double-opt-in',
823 ] );
824 ErrorNotification::store(
825 OptInError::fromCode( OptInError::NO_RECIPIENT, [ 'form_id' => $formId ] ),
826 $formId
827 );
828 return null;
829 }
830
831 /**
832 * Rate-Limiting: Check IP and email limits before creating OptIn.
833 */
834 $rateLimiter = new RateLimiter();
835 $ratSettings = CF7DoubleOptIn::getInstance()->getSettings();
836 $rateLimitIp = (int) ( $ratSettings['rate_limit_ip'] ?? 5 );
837 $rateLimitEmail = (int) ( $ratSettings['rate_limit_email'] ?? 3 );
838 $rateLimitWindow = (int) ( $ratSettings['rate_limit_window'] ?? 60 );
839
840 $ip = IPHelper::getIPAdress();
841 if ( ! $rateLimiter->isAllowed( 'ip', $ip, $rateLimitIp, $rateLimitWindow ) ) {
842 $this->get_logger()->warning( 'Rate limit exceeded for IP', [
843 'plugin' => 'double-opt-in',
844 'ip' => $ip,
845 'formId' => $formId,
846 ] );
847 do_action( 'f12_cf7_doubleoptin_rate_limited', 'ip', $ip, $formId );
848 ErrorNotification::store(
849 OptInError::fromCode( OptInError::RATE_LIMIT_IP, [ 'ip' => $ip, 'form_id' => $formId ] ),
850 $formId
851 );
852 return null;
853 }
854
855 if ( ! $rateLimiter->isAllowed( 'email', $recipient, $rateLimitEmail, $rateLimitWindow ) ) {
856 $this->get_logger()->warning( 'Rate limit exceeded for email', [
857 'plugin' => 'double-opt-in',
858 'email' => $recipient,
859 'formId' => $formId,
860 ] );
861 do_action( 'f12_cf7_doubleoptin_rate_limited', 'email', $recipient, $formId );
862 ErrorNotification::store(
863 OptInError::fromCode( OptInError::RATE_LIMIT_EMAIL, [ 'email' => $recipient, 'form_id' => $formId ] ),
864 $formId
865 );
866 return null;
867 }
868
869 /**
870 * Validate recipient (extensible via Pro plugin: MX check, unique
871 * email, etc.). Uses a lightweight proxy so filters that expect
872 * FormDataInterface::getFormId() keep working.
873 *
874 * IMPORTANT: `getFormType()` must be set to `$this->type` — that's
875 * the integration identifier ("elementor", "cf7" via legacy path,
876 * "avada" via legacy path). The Unique Email validator's
877 * resolver matches conditions on the (integration, form_id) pair;
878 * a proxy without getFormType returns '' and breaks the
879 * `selected` and `all_except` modes for every form that flows
880 * through this legacy path. User-reported 2026-05-13. Pinned by
881 * `LegacyFormDataProxyTest` in core tests.
882 */
883 $formDataProxy = new class( $formId, $this->type ) {
884 private int $formId;
885 private string $formType;
886 public function __construct( int $formId, string $formType ) {
887 $this->formId = $formId;
888 $this->formType = $formType;
889 }
890 public function getFormId(): int { return $this->formId; }
891 public function getFormType(): string { return $this->formType; }
892 };
893
894 $recipientValid = \apply_filters(
895 'f12_cf7_doubleoptin_validate_recipient',
896 true,
897 $recipient,
898 $formDataProxy
899 );
900
901 if ( $recipientValid !== true ) {
902 $errorMsg = is_string( $recipientValid ) ? $recipientValid : '';
903 $errorCode = OptInError::RECIPIENT_INVALID;
904
905 if ( $errorMsg === 'unique_email_rejected' ) {
906 $errorCode = OptInError::UNIQUE_EMAIL_DUPLICATE;
907 $errorMsg = OptInError::fromCode( OptInError::UNIQUE_EMAIL_DUPLICATE )->getMessage();
908 }
909
910 $this->get_logger()->warning( 'Recipient validation failed', [
911 'plugin' => 'double-opt-in',
912 'email' => $recipient,
913 'form_id' => $formId,
914 'reason' => $errorMsg,
915 ] );
916
917 $this->lastCreationError = new OptInError(
918 $errorCode,
919 ! empty( $errorMsg ) ? $errorMsg : OptInError::fromCode( OptInError::RECIPIENT_INVALID )->getMessage(),
920 [ 'email' => $recipient, 'form_id' => $formId ]
921 );
922
923 ErrorNotification::store( $this->lastCreationError, $formId );
924 return null;
925 }
926
927 /**
928 * Consent-Snapshot aus den Form-Settings laden. Both the wording
929 * shown to the user (`consent_text`) AND the form-field key the
930 * user had to tick (`consent_field`) need to be persisted, or
931 * the Consent Evidence panel reports "No acknowledgment field
932 * configured — consent text is stored but not legally provable"
933 * even when the admin wired up the checkbox. The modern
934 * AbstractFormIntegration::buildOptInProperties() path includes
935 * both; this legacy path (used by Elementor + the CF7/Avada
936 * legacy compat shims) used to only carry consent_text.
937 */
938 $consentText = '';
939 $consentField = '';
940 try {
941 $container = \Forge12\DoubleOptIn\Container\Container::getInstance();
942 $settingsService = $container->get( \Forge12\DoubleOptIn\FormSettings\FormSettingsService::class );
943 $formSettings = $settingsService->getSettings( $formId );
944 $consentText = $formSettings->consentText ?? '';
945 $consentField = $formSettings->consentField ?? '';
946 } catch ( \Exception $e ) {
947 $this->get_logger()->debug( 'Could not load consent snapshot from FormSettings', [
948 'plugin' => 'double-opt-in',
949 'error' => $e->getMessage(),
950 ] );
951 }
952
953 /**
954 * Eigenschaften des OptIn-Objekts festlegen
955 */
956 $properties = $this->buildLegacyOptInProperties(
957 $formId,
958 $formHtml,
959 $parameter,
960 $files,
961 $formParameter,
962 $recipient,
963 $consentText,
964 $consentField
965 );
966
967 $this->get_logger()->debug( 'OptIn properties created', [
968 'plugin' => 'double-opt-in',
969 'properties' => $properties,
970 ] );
971
972 /**
973 * OptIn-Objekt erstellen
974 */
975 $OptIn = new OptIn($this->get_logger(), $properties );
976 $this->get_logger()->debug( 'OptIn object instantiated', [
977 'plugin' => 'double-opt-in',
978 'OptIn' => $OptIn,
979 ] );
980
981 /**
982 * OptIn speichern
983 */
984 if ( $OptIn->save() ) {
985 $this->get_logger()->info( 'OptIn object saved successfully', [
986 'plugin' => 'double-opt-in',
987 'OptIn' => $OptIn,
988 ] );
989
990 // Dispatch typed event for new event-driven architecture
991 $this->dispatchOptInCreatedEvent( $OptIn, $formId );
992
993 return $OptIn;
994 }
995
996 $this->get_logger()->error( 'Failed to save OptIn object', [
997 'plugin' => 'double-opt-in',
998 ] );
999
1000 do_action( 'f12_cf7_doubleoptin_creation_failed', $formId, $recipient );
1001
1002 ErrorNotification::store(
1003 OptInError::fromCode( OptInError::SAVE_FAILED, [ 'form_id' => $formId ] ),
1004 $formId
1005 );
1006
1007 return null;
1008 }
1009
1010
1011 /**
1012 * Validate if the optin is enabled.
1013 */
1014 protected function isOptinEnabled( int $formId ): bool {
1015 // Disable optin sending if the optin flag is set.
1016 if ( isset( $_GET['optin'] ) ) {
1017 $this->get_logger()->debug( 'Optin disabled due to optin flag in GET request', [
1018 'plugin' => 'double-opt-in',
1019 'class' => __CLASS__,
1020 'method' => __METHOD__,
1021 ] );
1022 return false;
1023 }
1024
1025 $parameter = CF7DoubleOptIn::getInstance()->getParameter( $formId );
1026
1027 if ( (int) $parameter['enable'] != 1 ) {
1028 $this->get_logger()->debug( 'Optin not enabled in form parameter', [
1029 'plugin' => 'double-opt-in',
1030 'class' => __CLASS__,
1031 'method' => __METHOD__,
1032 'parameter' => $parameter,
1033 ] );
1034 return false;
1035 }
1036
1037 // Check the custom condition
1038 if ( isset( $parameter['conditions'] ) ) {
1039 $condition = sanitize_text_field( $parameter['conditions'] );
1040
1041 if ( ( $condition != 'disable' && $condition !== 'disabled' ) && ( ! isset( $_POST[ $condition ] ) || empty( $_POST[ $condition ] ) ) ) {
1042 $this->get_logger()->debug( 'Optin disabled due to unmet custom condition', [
1043 'plugin' => 'double-opt-in',
1044 'class' => __CLASS__,
1045 'method' => __METHOD__,
1046 'condition' => $condition,
1047 'post_keys' => array_keys( $_POST ),
1048 ] );
1049 return false;
1050 }
1051 }
1052
1053 $this->get_logger()->debug( 'Optin enabled', [
1054 'plugin' => 'double-opt-in',
1055 'class' => __CLASS__,
1056 'method' => __METHOD__,
1057 ] );
1058
1059 return true;
1060 }
1061
1062
1063 /**
1064 * Render validation feedback in the frontend via wp_footer.
1065 *
1066 * Fires the 'f12_cf7_doubleoptin_validation_feedback' action for customization,
1067 * and provides a default inline notice for non-confirmed statuses.
1068 *
1069 * @return void
1070 */
1071 public function renderValidationFeedback(): void {
1072 $status = self::getValidationStatus();
1073 if ( empty( $status ) ) {
1074 return;
1075 }
1076
1077 /**
1078 * Allow themes/plugins to handle the validation feedback display.
1079 *
1080 * @param string $status The validation status.
1081 *
1082 * @since 3.3.0
1083 */
1084 do_action( 'f12_cf7_doubleoptin_validation_feedback', $status );
1085 }
1086
1087 /**
1088 * Removes files associated with the optin parameter.
1089 *
1090 * This method checks if the optin parameter is set and
1091 * loads the OptIn object based on the hash value. If
1092 * the OptIn does not exist or no files are found,
1093 * the method will return. Otherwise, it will iterate
1094 * through the files and delete each one.
1095 *
1096 * @return void
1097 */
1098 public function removeFiles(): void {
1099 /**
1100 * Skip if the optin parameter is not set.
1101 */
1102 if ( ! isset( $_GET['optin'] ) ) {
1103 $this->get_logger()->debug( 'No optin parameter found, skipping file removal', [
1104 'plugin' => 'double-opt-in',
1105 'class' => __CLASS__,
1106 'method' => __METHOD__,
1107 ] );
1108 return;
1109 }
1110
1111 $hash = sanitize_text_field( wp_unslash( $_GET['optin'] ) );
1112
1113 /**
1114 * Load the OptIn
1115 */
1116 $OptIn = OptIn::get_by_hash( $hash );
1117
1118 /**
1119 * Skip if the OptIn does not exist
1120 */
1121 if ( null == $OptIn ) {
1122 $this->get_logger()->warning( 'OptIn not found, skipping file removal', [
1123 'plugin' => 'double-opt-in',
1124 'class' => __CLASS__,
1125 'method' => __METHOD__,
1126 'hash' => $hash,
1127 ] );
1128 return;
1129 }
1130
1131 /**
1132 * Load all files
1133 */
1134 $files = maybe_unserialize( $OptIn->get_files() );
1135
1136 /**
1137 * Skip if no files found
1138 */
1139 if ( empty( $files ) ) {
1140 $this->get_logger()->debug( 'No files found in OptIn, skipping removal', [
1141 'plugin' => 'double-opt-in',
1142 'class' => __CLASS__,
1143 'method' => __METHOD__,
1144 'hash' => $hash,
1145 ] );
1146 return;
1147 }
1148
1149 foreach ( $files as $file ) {
1150 /**
1151 * Skip if empty
1152 */
1153 if ( empty( $file ) ) {
1154 continue;
1155 }
1156
1157 /**
1158 * Skip if no file found
1159 */
1160 if ( ! is_file( $file ) ) {
1161 $this->get_logger()->warning( 'File not found, skipping', [
1162 'plugin' => 'double-opt-in',
1163 'class' => __CLASS__,
1164 'method' => __METHOD__,
1165 'file' => $file,
1166 ] );
1167 continue;
1168 }
1169
1170 /**
1171 * Delete the file
1172 */
1173 if ( unlink( $file ) ) {
1174 $this->get_logger()->info( 'File deleted successfully', [
1175 'plugin' => 'double-opt-in',
1176 'class' => __CLASS__,
1177 'method' => __METHOD__,
1178 'file' => $file,
1179 ] );
1180 } else {
1181 $this->get_logger()->error( 'Could not delete file', [
1182 'plugin' => 'double-opt-in',
1183 'class' => __CLASS__,
1184 'method' => __METHOD__,
1185 'file' => $file,
1186 ] );
1187 }
1188 }
1189 }
1190
1191 /**
1192 * Get the privacy policy URL.
1193 *
1194 * Fallback chain: Plugin setting → WordPress Privacy Policy page → empty string.
1195 *
1196 * @return string
1197 */
1198 private function getPrivacyPolicyUrl(): string {
1199 $settings = CF7DoubleOptIn::getInstance()->getSettings();
1200 $pageId = (int) ( $settings['privacy_policy_page'] ?? 0 );
1201
1202 if ( $pageId > 0 ) {
1203 $url = get_permalink( $pageId );
1204 if ( $url ) {
1205 return $url;
1206 }
1207 }
1208
1209 // Fallback to WordPress privacy policy page
1210 $wpPrivacyPageId = (int) get_option( 'wp_page_for_privacy_policy', 0 );
1211 if ( $wpPrivacyPageId > 0 ) {
1212 $url = get_permalink( $wpPrivacyPageId );
1213 if ( $url ) {
1214 return $url;
1215 }
1216 }
1217
1218 return '';
1219 }
1220
1221 /**
1222 * Dispatch OptInCreatedEvent via the new event system.
1223 *
1224 * @param OptIn $optIn The created OptIn object.
1225 * @param int $formId The form ID.
1226 *
1227 * @since 4.0.0
1228 */
1229 protected function dispatchOptInCreatedEvent( OptIn $optIn, int $formId ): void {
1230 try {
1231 $container = Container::getInstance();
1232 if ( $container->has( EventDispatcherInterface::class ) ) {
1233 $dispatcher = $container->get( EventDispatcherInterface::class );
1234 $event = new OptInCreatedEvent(
1235 $optIn->get_id(),
1236 $formId,
1237 $this->type,
1238 $optIn->get_email(),
1239 $optIn->get_hash()
1240 );
1241 $dispatcher->dispatch( $event );
1242
1243 $this->get_logger()->debug( 'OptInCreatedEvent dispatched', [
1244 'plugin' => 'double-opt-in',
1245 'optin_id' => $optIn->get_id(),
1246 'form_id' => $formId,
1247 ] );
1248 }
1249 } catch ( \Exception $e ) {
1250 $this->get_logger()->warning( 'Failed to dispatch OptInCreatedEvent', [
1251 'plugin' => 'double-opt-in',
1252 'error' => $e->getMessage(),
1253 ] );
1254 }
1255 }
1256
1257 /**
1258 * Dispatch OptInConfirmedEvent via the new event system.
1259 *
1260 * @param OptIn $optIn The confirmed OptIn object.
1261 * @param string $hash The opt-in hash.
1262 *
1263 * @since 4.0.0
1264 */
1265 protected function dispatchOptInConfirmedEvent( OptIn $optIn, string $hash ): void {
1266 try {
1267 $container = Container::getInstance();
1268 if ( $container->has( EventDispatcherInterface::class ) ) {
1269 $dispatcher = $container->get( EventDispatcherInterface::class );
1270
1271 $formData = maybe_unserialize( $optIn->get_content() );
1272
1273 $event = new OptInConfirmedEvent(
1274 $optIn->get_id(),
1275 $hash,
1276 $optIn->get_email(),
1277 $optIn->get_ipaddr_confirmation(),
1278 (int) $optIn->get_cf_form_id(),
1279 is_array( $formData ) ? $formData : []
1280 );
1281 $dispatcher->dispatch( $event );
1282
1283 $this->get_logger()->debug( 'OptInConfirmedEvent dispatched', [
1284 'plugin' => 'double-opt-in',
1285 'optin_id' => $optIn->get_id(),
1286 'hash' => $hash,
1287 ] );
1288 }
1289 } catch ( \Exception $e ) {
1290 $this->get_logger()->warning( 'Failed to dispatch OptInConfirmedEvent', [
1291 'plugin' => 'double-opt-in',
1292 'error' => $e->getMessage(),
1293 ] );
1294 }
1295 }
1296
1297 /**
1298 * Assemble the legacy OptIn properties array.
1299 *
1300 * Extracted from {@see maybeCreateOptIn()} so the column inventory
1301 * is unit-testable without standing up the whole submit pipeline
1302 * (rate limiter, FormSettingsService, $wpdb save, etc.). The
1303 * modern {@see \Forge12\DoubleOptIn\Integration\AbstractFormIntegration::buildOptInProperties()}
1304 * has a sibling helper; the two MUST stay in sync — every column
1305 * the modern path snapshots, this legacy path also has to snapshot,
1306 * otherwise integrations on the legacy compat path (Elementor +
1307 * the CF7/Avada legacy shims) lose the column silently.
1308 *
1309 * The 2026-05-13 incident this guards against: `consent_field`
1310 * was missing here, so Elementor opt-ins shipped with an empty
1311 * acknowledgment field even when the admin configured one. The
1312 * Consent Evidence panel then rendered "No acknowledgment field
1313 * configured — consent text is stored but not legally provable"
1314 * on a record that was, in fact, ticked through a configured
1315 * checkbox.
1316 *
1317 * @return array<string, mixed>
1318 */
1319 protected function buildLegacyOptInProperties(
1320 int $formId,
1321 string $formHtml,
1322 array $parameter,
1323 array $files,
1324 array $formParameter,
1325 string $recipient,
1326 string $consentText,
1327 string $consentField
1328 ): array {
1329 return [
1330 'cf_form_id' => $formId,
1331 'doubleoptin' => 0,
1332 'createtime' => time(),
1333 'content' => maybe_serialize( $parameter ),
1334 'files' => maybe_serialize( $files ),
1335 'ipaddr_register' => IPHelper::getIPAdress(),
1336 'category' => (int) ( $formParameter['category'] ?? 0 ),
1337 'form' => $formHtml,
1338 'email' => $recipient,
1339 'consent_text' => $consentText,
1340 'consent_field' => $consentField,
1341 ];
1342 }
1343 }