| @@ -7,8 +7,9 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | namespace Forge12\DoubleOptIn\Integration; |
| 10 | 10 | |
| 11 | +use Forge12\DoubleOptIn\Consent\ConsentGate; | |
| 11 | 12 | use Forge12\DoubleOptIn\Container\Container; |
| 12 | 13 | use Forge12\DoubleOptIn\EmailTemplates\PlaceholderMapper; |
| 13 | 14 | use Forge12\DoubleOptIn\EventSystem\EventDispatcherInterface; |
| 14 | 15 | use Forge12\DoubleOptIn\Events\Integration\FormSubmissionEvent; |
| @@ -14,8 +15,10 @@ | ||
| 14 | 15 | use Forge12\DoubleOptIn\Events\Integration\FormSubmissionEvent; |
| 15 | 16 | use Forge12\DoubleOptIn\Events\Lifecycle\OptInConfirmedEvent; |
| 16 | 17 | use Forge12\DoubleOptIn\Events\Lifecycle\OptInCreatedEvent; |
| 17 | 18 | use Forge12\DoubleOptIn\Files\FileStorage; |
| 19 | +use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt; | |
| 20 | +use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator; | |
| 18 | 21 | use Forge12\DoubleOptIn\Frontend\ErrorNotification; |
| 19 | 22 | use Forge12\DoubleOptIn\Service\RateLimiter; |
| 20 | 23 | use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn; |
| 21 | 24 | use forge12\contactform7\CF7DoubleOptIn\IPHelper; |
| @@ -80,8 +83,47 @@ | ||
| 80 | 83 | * Get the validation status from the last validateOptIn() call. |
| 81 | 84 | * |
| 82 | 85 | * @return string One of: '', 'confirmed', 'already_confirmed', 'expired', 'not_found'. |
| 83 | 86 | */ |
| 87 | + /** | |
| 88 | + * Nesting depth of post-confirmation replays in this request. | |
| 89 | + * | |
| 90 | + * @var int | |
| 91 | + */ | |
| 92 | + private static $replayDepth = 0; | |
| 93 | + | |
| 94 | + /** | |
| 95 | + * True while a post-confirmation replay (re-submitting the stored | |
| 96 | + * data to the form plugin) runs in this request. Set only by server | |
| 97 | + * code via {@see runAsReplay()} — never derived from request input. | |
| 98 | + * | |
| 99 | + * @since 5.6.0 | |
| 100 | + */ | |
| 101 | + public static function isReplaying(): bool { | |
| 102 | + return self::$replayDepth > 0; | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 106 | + * Run $fn as a post-confirmation replay: form-submit hooks that fire | |
| 107 | + * inside it (wpcf7_before_send_mail, gform_after_submission, …) see | |
| 108 | + * {@see isReplaying()} and do not create a new opt-in. | |
| 109 | + * | |
| 110 | + * @template T | |
| 111 | + * @param callable():T $fn | |
| 112 | + * | |
| 113 | + * @return T | |
| 114 | + * | |
| 115 | + * @since 5.6.0 | |
| 116 | + */ | |
| 117 | + public static function runAsReplay( callable $fn ) { | |
| 118 | + self::$replayDepth++; | |
| 119 | + try { | |
| 120 | + return $fn(); | |
| 121 | + } finally { | |
| 122 | + self::$replayDepth--; | |
| 123 | + } | |
| 124 | + } | |
| 125 | + | |
| 84 | 126 | public static function getValidationStatus(): string { |
| 85 | 127 | return self::$validationStatus; |
| 86 | 128 | } |
| 87 | 129 | |
| @@ -170,12 +212,19 @@ | ||
| 170 | 212 | /** |
| 171 | 213 | * {@inheritdoc} |
| 172 | 214 | */ |
| 173 | 215 | public function isOptInEnabled( int $formId ): bool { |
| 174 | - // Disable if opt-in confirmation is in progress | |
| 175 | - if ( isset( $_GET['optin'] ) ) { | |
| 216 | + // Disable while our own post-confirmation replay runs — the stored | |
| 217 | + // submission must be processed, not turned into a new opt-in. | |
| 218 | + // | |
| 219 | + // This used to test `isset( $_GET['optin'] )`. That flag is set by | |
| 220 | + // whoever sends the request: `…/feedback?optin=1` on the CF7 REST | |
| 221 | + // route submitted a DOI form with every mail and no confirmation. | |
| 222 | + // It also broke every replay outside the confirmation request | |
| 223 | + // (cron, admin retry), which has no `?optin` in its URL. | |
| 224 | + if ( self::isReplaying() ) { | |
| 176 | 225 | $this->getLogger()->debug( |
| 177 | - 'Opt-in disabled due to optin flag in GET request', | |
| 226 | + 'Opt-in disabled during post-confirmation replay', | |
| 178 | 227 | array( |
| 179 | 228 | 'plugin' => 'double-opt-in', |
| 180 | 229 | 'class' => static::class, |
| 181 | 230 | ) |
| @@ -438,26 +487,102 @@ | ||
| 438 | 487 | return null; |
| 439 | 488 | } |
| 440 | 489 | |
| 441 | 490 | /** |
| 491 | + * The consent gate, asked at the form plugin's own validation stage. | |
| 492 | + * | |
| 493 | + * For integrations whose submit hook runs after the form plugin has | |
| 494 | + * already accepted the submission (WPForms `wpforms_process_complete`, | |
| 495 | + * Gravity Forms `gform_after_submission`). By then the form has been | |
| 496 | + * replaced by its confirmation, and a refused consent could only be | |
| 497 | + * reported in a toast over an empty page (5.6.2 click test). Asked from | |
| 498 | + * `wpforms_process` / `gform_validation` instead, the form plugin marks | |
| 499 | + * the checkbox like a missed required field and keeps the input. | |
| 500 | + * | |
| 501 | + * Only a refusal that would stand is returned: DOI on for the form, not | |
| 502 | + * skipped by `f12_cf7_doubleoptin_skip_option`, verdict NOT_GIVEN, gate | |
| 503 | + * enforced. Everything else — a stale field name, the gate switched off | |
| 504 | + * by filter — is left to createOptIn(), which logs it as before. | |
| 505 | + * | |
| 506 | + * @param FormDataInterface $formData The submission, normalized the same | |
| 507 | + * way the submit hook will normalize it. | |
| 508 | + * @param mixed $rawFields What the skip filter receives in the | |
| 509 | + * submit hook of this integration. | |
| 510 | + * | |
| 511 | + * @return OptInError|null The refusal, or null to let the form through. | |
| 512 | + * | |
| 513 | + * @since 5.6.2 | |
| 514 | + */ | |
| 515 | + public function refusedConsentBeforeSubmit( FormDataInterface $formData, $rawFields = array() ): ?OptInError { | |
| 516 | + $formId = $formData->getFormId(); | |
| 517 | + | |
| 518 | + if ( ! $this->isOptInEnabled( $formId ) ) { | |
| 519 | + return null; | |
| 520 | + } | |
| 521 | + | |
| 522 | + if ( apply_filters( 'f12_cf7_doubleoptin_skip_option', false, $formId, $rawFields, $this->getIdentifier() ) ) { | |
| 523 | + return null; | |
| 524 | + } | |
| 525 | + | |
| 526 | + $consentField = (string) ( $this->getFormParameter( $formId )['consent_field'] ?? '' ); | |
| 527 | + if ( $consentField === '' ) { | |
| 528 | + return null; | |
| 529 | + } | |
| 530 | + | |
| 531 | + $verdict = ConsentGate::evaluate( | |
| 532 | + $consentField, | |
| 533 | + $formData->getFields(), | |
| 534 | + $this->getKnownFieldNames( $formId ) | |
| 535 | + ); | |
| 536 | + | |
| 537 | + if ( $verdict !== ConsentGate::NOT_GIVEN || ! ConsentGate::isEnforced( $formId, $this->getIdentifier() ) ) { | |
| 538 | + return null; | |
| 539 | + } | |
| 540 | + | |
| 541 | + $this->getLogger()->info( | |
| 542 | + 'Consent acceptance not given, rejecting submission at validation', | |
| 543 | + array( | |
| 544 | + 'plugin' => 'double-opt-in', | |
| 545 | + 'form_id' => $formId, | |
| 546 | + 'integration' => $this->getIdentifier(), | |
| 547 | + 'consent_field' => $consentField, | |
| 548 | + ) | |
| 549 | + ); | |
| 550 | + | |
| 551 | + /** This action is documented in createOptIn(). */ | |
| 552 | + do_action( 'f12_cf7_doubleoptin_consent_not_given', $formId, $consentField ); | |
| 553 | + | |
| 554 | + return OptInError::fromCode( | |
| 555 | + OptInError::CONSENT_NOT_GIVEN, | |
| 556 | + array( | |
| 557 | + 'form_id' => $formId, | |
| 558 | + 'consent_field' => $consentField, | |
| 559 | + ) | |
| 560 | + ); | |
| 561 | + } | |
| 562 | + | |
| 563 | + /** | |
| 442 | 564 | * Validate the consent-acceptance gate (GDPR Art. 7). |
| 443 | 565 | * |
| 444 | - * When the form has a configured `consent_field`, the user must | |
| 445 | - * have actively confirmed it. Otherwise we'd be storing a | |
| 446 | - * `consent_text` snapshot the user never saw — fabricated audit | |
| 447 | - * evidence. Empty `consent_field` means "no gate" | |
| 448 | - * (backward-compat; admins who haven't migrated yet keep working). | |
| 566 | + * The decision itself lives in {@see ConsentGate} — this method only | |
| 567 | + * turns it into the return value `createOptIn()` expects and writes | |
| 568 | + * the log lines. Both halves of the plugin share that class now: the | |
| 569 | + * integrations that extend this base, and the legacy path in | |
| 570 | + * `OptInFrontend::maybeCreateOptIn()` that serves Elementor and the | |
| 571 | + * CF7/Avada shims. | |
| 449 | 572 | * |
| 450 | - * Extracted into its own method so the gate behavior is testable | |
| 451 | - * in isolation — the full createOptIn() flow has too many | |
| 452 | - * side-effects (rate-limiting, file storage, container access) | |
| 453 | - * for clean unit testing of just this branch. | |
| 573 | + * `ConsentGate::FIELD_UNKNOWN` — the configured field is not on this | |
| 574 | + * form — deliberately does NOT reject. Until 5.4.0 it did, which took | |
| 575 | + * a site's registrations offline over a settings mistake the visitor | |
| 576 | + * could neither see nor fix (customer report 2026-08-27). The admin | |
| 577 | + * now hears about it through the log, the Site Health check and the | |
| 578 | + * banner on the form's settings tab instead. | |
| 454 | 579 | * |
| 455 | 580 | * @param FormDataInterface $formData The submitted form data. |
| 456 | 581 | * @param array<string, mixed> $formParameter The form-settings snapshot. |
| 457 | 582 | * |
| 458 | - * @return OptInError|null Error when the gate fails; null when it | |
| 459 | - * passes (gate disabled or value truthy). | |
| 583 | + * @return OptInError|null Error when the gate rejects; null when the | |
| 584 | + * submission may proceed. | |
| 460 | 585 | */ |
| 461 | 586 | protected function validateConsentAcceptance( FormDataInterface $formData, array $formParameter ): ?OptInError { |
| 462 | 587 | $consentField = (string) ( $formParameter['consent_field'] ?? '' ); |
| 463 | 588 | if ( $consentField === '' ) { |
| @@ -463,87 +588,65 @@ | ||
| 463 | 588 | if ( $consentField === '' ) { |
| 464 | 589 | return null; |
| 465 | 590 | } |
| 466 | 591 | |
| 467 | - // Reconcile the configured name with what the form actually | |
| 468 | - // submitted. Settings saved before 5.3.2 ran through | |
| 469 | - // sanitize_key(), which lowercased them — a CF7 field named | |
| 470 | - // `Datenschutz` was stored as `datenschutz`, was never found | |
| 471 | - // here, and the gate then rejected EVERY submission with | |
| 472 | - // "consent not given" (customer report 2026-08-27). | |
| 473 | - // | |
| 474 | - // An exact match always wins, so a form carrying both spellings | |
| 475 | - // still resolves to the one the admin configured. When nothing | |
| 476 | - // matches at all the original name is kept and the rejection | |
| 477 | - // below carries it into the log — that case is a genuinely | |
| 478 | - // misconfigured form and has to stay visible. | |
| 479 | - $resolvedField = SubmittedContent::matchFieldName( $consentField, array_keys( $formData->getFields() ) ); | |
| 480 | - if ( $resolvedField !== '' ) { | |
| 481 | - $consentField = $resolvedField; | |
| 592 | + $verdict = ConsentGate::evaluate( | |
| 593 | + $consentField, | |
| 594 | + $formData->getFields(), | |
| 595 | + $this->getKnownFieldNames( $formData->getFormId() ) | |
| 596 | + ); | |
| 597 | + | |
| 598 | + if ( $verdict === ConsentGate::PASSED ) { | |
| 599 | + return null; | |
| 482 | 600 | } |
| 483 | 601 | |
| 484 | - $consentValue = $formData->getField( $consentField ); | |
| 602 | + if ( $verdict === ConsentGate::FIELD_UNKNOWN ) { | |
| 603 | + // Never a rejection — see the ConsentGate docblock. Logged as | |
| 604 | + // a warning all the same: until someone fixes the setting, the | |
| 605 | + // consent proof stored with every opt-in of this form is | |
| 606 | + // worthless. | |
| 607 | + $this->getLogger()->warning( | |
| 608 | + 'Consent field is not on this form — opt-in accepted without provable consent', | |
| 609 | + array( | |
| 610 | + 'plugin' => 'double-opt-in', | |
| 611 | + 'form_id' => $formData->getFormId(), | |
| 612 | + 'integration' => $this->getIdentifier(), | |
| 613 | + 'consent_field' => $consentField, | |
| 614 | + ) | |
| 615 | + ); | |
| 485 | 616 | |
| 486 | - // Diagnostic — 2026-05-13 user report: WPForms Checkbox field | |
| 487 | - // ticked, gate still rejects. Need to see the actual shape of | |
| 488 | - // `$consentValue` to know whether `! empty()` is the wrong | |
| 489 | - // predicate for WPForms checkbox payloads (e.g. array with | |
| 490 | - // empty string, scalar 0, etc.). | |
| 491 | - $this->getLogger()->info( | |
| 492 | - 'Consent gate evaluation', | |
| 493 | - array( | |
| 494 | - 'plugin' => 'double-opt-in', | |
| 495 | - 'form_id' => $formData->getFormId(), | |
| 496 | - 'consent_field' => $consentField, | |
| 497 | - 'value_type' => gettype( $consentValue ), | |
| 498 | - 'value_preview' => is_scalar( $consentValue ) | |
| 499 | - ? (string) $consentValue | |
| 500 | - : wp_json_encode( $consentValue ), | |
| 501 | - 'is_empty' => empty( $consentValue ), | |
| 502 | - ) | |
| 503 | - ); | |
| 617 | + /** | |
| 618 | + * Fires when a form's configured acceptance field cannot be | |
| 619 | + * found on the form itself. The submission is accepted. | |
| 620 | + * | |
| 621 | + * @since 5.4.0 | |
| 622 | + * | |
| 623 | + * @param int $formId The form the submission came from. | |
| 624 | + * @param string $consentField The configured field name. | |
| 625 | + * @param string $integration The integration identifier. | |
| 626 | + */ | |
| 627 | + do_action( | |
| 628 | + 'f12_doi_consent_field_unknown', | |
| 629 | + $formData->getFormId(), | |
| 630 | + $consentField, | |
| 631 | + $this->getIdentifier() | |
| 632 | + ); | |
| 504 | 633 | |
| 505 | - if ( ! empty( $consentValue ) ) { | |
| 506 | 634 | return null; |
| 507 | 635 | } |
| 508 | 636 | |
| 509 | - // Fallback for WPForms checkbox shape: when the user ticked | |
| 510 | - // the box, the bare-id key may carry the joined-string `value` | |
| 511 | - // while the truthful "did the user actually tick anything" | |
| 512 | - // signal lives in the `field_{id}` mirror's `value_raw` | |
| 513 | - // (array of internal slugs). If `value_raw` is a non-empty | |
| 514 | - // array with at least one non-empty entry, treat as consent | |
| 515 | - // given — `empty()` over the joined string is a false | |
| 516 | - // negative when the checkbox's display labels are empty. | |
| 517 | - $mirror = $formData->getField( 'field_' . $consentField ); | |
| 518 | - if ( is_array( $mirror ) ) { | |
| 519 | - $valueRaw = $mirror['value_raw'] ?? null; | |
| 520 | - $value = $mirror['value'] ?? null; | |
| 521 | - $hasTicked = false; | |
| 522 | - foreach ( array( $valueRaw, $value ) as $candidate ) { | |
| 523 | - if ( is_array( $candidate ) ) { | |
| 524 | - foreach ( $candidate as $entry ) { | |
| 525 | - if ( is_scalar( $entry ) && (string) $entry !== '' ) { | |
| 526 | - $hasTicked = true; | |
| 527 | - break 2; | |
| 528 | - } | |
| 529 | - } | |
| 530 | - } elseif ( is_scalar( $candidate ) && (string) $candidate !== '' ) { | |
| 531 | - $hasTicked = true; | |
| 532 | - break; | |
| 533 | - } | |
| 534 | - } | |
| 535 | - if ( $hasTicked ) { | |
| 536 | - $this->getLogger()->info( | |
| 537 | - 'Consent gate passed via field_{id} mirror fallback', | |
| 538 | - array( | |
| 539 | - 'plugin' => 'double-opt-in', | |
| 540 | - 'form_id' => $formData->getFormId(), | |
| 541 | - 'consent_field' => $consentField, | |
| 542 | - ) | |
| 543 | - ); | |
| 544 | - return null; | |
| 545 | - } | |
| 637 | + if ( ! ConsentGate::isEnforced( $formData->getFormId(), $this->getIdentifier() ) ) { | |
| 638 | + $this->getLogger()->warning( | |
| 639 | + 'Consent gate disabled by filter — accepting an unconfirmed submission', | |
| 640 | + array( | |
| 641 | + 'plugin' => 'double-opt-in', | |
| 642 | + 'form_id' => $formData->getFormId(), | |
| 643 | + 'integration' => $this->getIdentifier(), | |
| 644 | + 'consent_field' => $consentField, | |
| 645 | + ) | |
| 646 | + ); | |
| 647 | + | |
| 648 | + return null; | |
| 546 | 649 | } |
| 547 | 650 | |
| 548 | 651 | return OptInError::fromCode( |
| 549 | 652 | OptInError::CONSENT_NOT_GIVEN, |
| @@ -554,8 +657,38 @@ | ||
| 554 | 657 | ); |
| 555 | 658 | } |
| 556 | 659 | |
| 557 | 660 | /** |
| 661 | + * The names of the fields this form actually declares. | |
| 662 | + * | |
| 663 | + * Needed to tell an unticked checkbox — which the browser leaves out | |
| 664 | + * of the payload entirely — from a `consent_field` pointing at a | |
| 665 | + * field the admin has since renamed or deleted. An integration that | |
| 666 | + * cannot answer yields an empty list, and the gate then stays on the | |
| 667 | + * cautious side and rejects nothing it cannot prove. | |
| 668 | + * | |
| 669 | + * @param int $formId The form to inspect. | |
| 670 | + * | |
| 671 | + * @return array<int,string> | |
| 672 | + */ | |
| 673 | + protected function getKnownFieldNames( int $formId ): array { | |
| 674 | + try { | |
| 675 | + return ConsentGate::normalizeFieldNames( $this->getFormFields( $formId ) ); | |
| 676 | + } catch ( \Throwable $e ) { | |
| 677 | + $this->getLogger()->warning( | |
| 678 | + 'Could not read the form field inventory for the consent gate', | |
| 679 | + array( | |
| 680 | + 'plugin' => 'double-opt-in', | |
| 681 | + 'form_id' => $formId, | |
| 682 | + 'error' => $e->getMessage(), | |
| 683 | + ) | |
| 684 | + ); | |
| 685 | + | |
| 686 | + return array(); | |
| 687 | + } | |
| 688 | + } | |
| 689 | + | |
| 690 | + /** | |
| 558 | 691 | * Build the OptIn properties array that will be persisted on |
| 559 | 692 | * record creation. Extracted from {@see createOptIn()} so the |
| 560 | 693 | * field-coverage contract is testable in isolation — the full |
| 561 | 694 | * createOptIn() flow has too many side-effects (rate-limiting, |
| @@ -1019,8 +1152,24 @@ | ||
| 1019 | 1152 | self::setValidationStatus( 'already_confirmed' ); |
| 1020 | 1153 | return false; |
| 1021 | 1154 | } |
| 1022 | 1155 | |
| 1156 | + /** | |
| 1157 | + * Enable / Disable default mail. | |
| 1158 | + * | |
| 1159 | + * @param bool $status Enable (true) or disable (false) the default mail. | |
| 1160 | + * @param int $postId The ID of the Post / Form. | |
| 1161 | + * | |
| 1162 | + * @since 2.3.3 | |
| 1163 | + */ | |
| 1164 | + $sendDefaultMail = (bool) apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $optIn->get_cf_form_id() ); | |
| 1165 | + | |
| 1166 | + // Bind the follow-up plan BEFORE the confirmation is saved, so a | |
| 1167 | + // request that dies in between leaves rows the sweep can finish. | |
| 1168 | + // False = no adapter for this integration → previous behaviour. | |
| 1169 | + $coordinator = FollowUpCoordinator::instance(); | |
| 1170 | + $managed = $coordinator !== null && $coordinator->plan( $optIn, $sendDefaultMail ); | |
| 1171 | + | |
| 1023 | 1172 | // Confirm the opt-in |
| 1024 | 1173 | do_action( 'f12_cf7_doubleoptin_before_confirm', $hash, $optIn ); |
| 1025 | 1174 | |
| 1026 | 1175 | $optIn->set_doubleoptin( 1 ); |
| @@ -1046,16 +1195,32 @@ | ||
| 1046 | 1195 | |
| 1047 | 1196 | // Dispatch event |
| 1048 | 1197 | $this->dispatchOptInConfirmedEvent( $optIn, $hash ); |
| 1049 | 1198 | |
| 1050 | - do_action( 'f12_cf7_doubleoptin_after_confirm', $hash, $optIn ); | |
| 1199 | + // Everything from here on re-processes the stored submission. | |
| 1200 | + self::runAsReplay( | |
| 1201 | + function () use ( $hash, $optIn, $managed, $coordinator, $sendDefaultMail ) { | |
| 1202 | + do_action( 'f12_cf7_doubleoptin_after_confirm', $hash, $optIn ); | |
| 1051 | 1203 | |
| 1052 | - // Send the original mail if enabled | |
| 1053 | - if ( apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $optIn->get_cf_form_id() ) ) { | |
| 1054 | - do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $optIn ); | |
| 1055 | - $this->sendConfirmationMail( $optIn ); | |
| 1056 | - do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $optIn ); | |
| 1057 | - } | |
| 1204 | + if ( $managed ) { | |
| 1205 | + // The coordinator runs every planned action — entry and | |
| 1206 | + // mail — and records a result per action. The before/after | |
| 1207 | + // hooks keep firing for listeners that depend on them. | |
| 1208 | + if ( $sendDefaultMail ) { | |
| 1209 | + do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $optIn ); | |
| 1210 | + } | |
| 1211 | + $coordinator->run( $optIn, FollowUpAttempt::TRIGGER_CONFIRM ); | |
| 1212 | + if ( $sendDefaultMail ) { | |
| 1213 | + do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $optIn ); | |
| 1214 | + } | |
| 1215 | + } elseif ( $sendDefaultMail ) { | |
| 1216 | + // Send the original mail if enabled | |
| 1217 | + do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $optIn ); | |
| 1218 | + $this->sendConfirmationMail( $optIn ); | |
| 1219 | + do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $optIn ); | |
| 1220 | + } | |
| 1221 | + } | |
| 1222 | + ); | |
| 1058 | 1223 | |
| 1059 | 1224 | $this->getLogger()->info( |
| 1060 | 1225 | 'OptIn confirmed successfully', |
| 1061 | 1226 | array( |