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.12.0 5.13.0 5.13.1 5.11.0 5.10.0 5.9.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 All 47 releases
← All changes | src/Integration/AbstractFormIntegration.php +13 -163 5.11.0 → 5.5.0 View file →
@@ -15,10 +15,8 @@
15 15 use Forge12\DoubleOptIn\Events\Integration\FormSubmissionEvent;
16 16 use Forge12\DoubleOptIn\Events\Lifecycle\OptInConfirmedEvent;
17 17 use Forge12\DoubleOptIn\Events\Lifecycle\OptInCreatedEvent;
18 18 use Forge12\DoubleOptIn\Files\FileStorage;
19 -use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt;
20 -use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator;
21 19 use Forge12\DoubleOptIn\Frontend\ErrorNotification;
22 20 use Forge12\DoubleOptIn\Service\RateLimiter;
23 21 use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn;
24 22 use forge12\contactform7\CF7DoubleOptIn\IPHelper;
@@ -83,47 +81,8 @@
83 81 * Get the validation status from the last validateOptIn() call.
84 82 *
85 83 * @return string One of: '', 'confirmed', 'already_confirmed', 'expired', 'not_found'.
86 84 */
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 -
126 85 public static function getValidationStatus(): string {
127 86 return self::$validationStatus;
128 87 }
129 88
@@ -212,19 +171,12 @@
212 171 /**
213 172 * {@inheritdoc}
214 173 */
215 174 public function isOptInEnabled( int $formId ): bool {
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() ) {
175 + // Disable if opt-in confirmation is in progress
176 + if ( isset( $_GET['optin'] ) ) {
225 177 $this->getLogger()->debug(
226 - 'Opt-in disabled during post-confirmation replay',
178 + 'Opt-in disabled due to optin flag in GET request',
227 179 array(
228 180 'plugin' => 'double-opt-in',
229 181 'class' => static::class,
230 182 )
@@ -360,8 +312,9 @@
360 312 $this->getLogger()->warning(
361 313 'Rate limit exceeded for IP',
362 314 array(
363 315 'plugin' => 'double-opt-in',
316 + 'ip' => $ip,
364 317 'form_id' => $formData->getFormId(),
365 318 )
366 319 );
367 320 do_action( 'f12_cf7_doubleoptin_rate_limited', 'ip', $ip, $formData->getFormId() );
@@ -382,8 +335,9 @@
382 335 $this->getLogger()->warning(
383 336 'Rate limit exceeded for email',
384 337 array(
385 338 'plugin' => 'double-opt-in',
339 + 'email' => $recipient,
386 340 'form_id' => $formData->getFormId(),
387 341 )
388 342 );
389 343 do_action( 'f12_cf7_doubleoptin_rate_limited', 'email', $recipient, $formData->getFormId() );
@@ -421,8 +375,9 @@
421 375 $this->getLogger()->warning(
422 376 'Recipient validation failed',
423 377 array(
424 378 'plugin' => 'double-opt-in',
379 + 'email' => $recipient,
425 380 'form_id' => $formData->getFormId(),
426 381 'reason' => $errorMsg,
427 382 )
428 383 );
@@ -484,81 +439,8 @@
484 439 return null;
485 440 }
486 441
487 442 /**
488 - * The consent gate, asked at the form plugin's own validation stage.
489 - *
490 - * For integrations whose submit hook runs after the form plugin has
491 - * already accepted the submission (WPForms `wpforms_process_complete`,
492 - * Gravity Forms `gform_after_submission`). By then the form has been
493 - * replaced by its confirmation, and a refused consent could only be
494 - * reported in a toast over an empty page (5.6.2 click test). Asked from
495 - * `wpforms_process` / `gform_validation` instead, the form plugin marks
496 - * the checkbox like a missed required field and keeps the input.
497 - *
498 - * Only a refusal that would stand is returned: DOI on for the form, not
499 - * skipped by `f12_cf7_doubleoptin_skip_option`, verdict NOT_GIVEN, gate
500 - * enforced. Everything else — a stale field name, the gate switched off
501 - * by filter — is left to createOptIn(), which logs it as before.
502 - *
503 - * @param FormDataInterface $formData The submission, normalized the same
504 - * way the submit hook will normalize it.
505 - * @param mixed $rawFields What the skip filter receives in the
506 - * submit hook of this integration.
507 - *
508 - * @return OptInError|null The refusal, or null to let the form through.
509 - *
510 - * @since 5.6.2
511 - */
512 - public function refusedConsentBeforeSubmit( FormDataInterface $formData, $rawFields = array() ): ?OptInError {
513 - $formId = $formData->getFormId();
514 -
515 - if ( ! $this->isOptInEnabled( $formId ) ) {
516 - return null;
517 - }
518 -
519 - if ( apply_filters( 'f12_cf7_doubleoptin_skip_option', false, $formId, $rawFields, $this->getIdentifier() ) ) {
520 - return null;
521 - }
522 -
523 - $consentField = (string) ( $this->getFormParameter( $formId )['consent_field'] ?? '' );
524 - if ( $consentField === '' ) {
525 - return null;
526 - }
527 -
528 - $verdict = ConsentGate::evaluate(
529 - $consentField,
530 - $formData->getFields(),
531 - $this->getKnownFieldNames( $formId )
532 - );
533 -
534 - if ( $verdict !== ConsentGate::NOT_GIVEN || ! ConsentGate::isEnforced( $formId, $this->getIdentifier() ) ) {
535 - return null;
536 - }
537 -
538 - $this->getLogger()->info(
539 - 'Consent acceptance not given, rejecting submission at validation',
540 - array(
541 - 'plugin' => 'double-opt-in',
542 - 'form_id' => $formId,
543 - 'integration' => $this->getIdentifier(),
544 - 'consent_field' => $consentField,
545 - )
546 - );
547 -
548 - /** This action is documented in createOptIn(). */
549 - do_action( 'f12_cf7_doubleoptin_consent_not_given', $formId, $consentField );
550 -
551 - return OptInError::fromCode(
552 - OptInError::CONSENT_NOT_GIVEN,
553 - array(
554 - 'form_id' => $formId,
555 - 'consent_field' => $consentField,
556 - )
557 - );
558 - }
559 -
560 - /**
561 443 * Validate the consent-acceptance gate (GDPR Art. 7).
562 444 *
563 445 * The decision itself lives in {@see ConsentGate} — this method only
564 446 * turns it into the return value `createOptIn()` expects and writes
@@ -1149,24 +1031,8 @@
1149 1031 self::setValidationStatus( 'already_confirmed' );
1150 1032 return false;
1151 1033 }
1152 1034
1153 - /**
1154 - * Enable / Disable default mail.
1155 - *
1156 - * @param bool $status Enable (true) or disable (false) the default mail.
1157 - * @param int $postId The ID of the Post / Form.
1158 - *
1159 - * @since 2.3.3
1160 - */
1161 - $sendDefaultMail = (bool) apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $optIn->get_cf_form_id() );
1162 -
1163 - // Bind the follow-up plan BEFORE the confirmation is saved, so a
1164 - // request that dies in between leaves rows the sweep can finish.
1165 - // False = no adapter for this integration → previous behaviour.
1166 - $coordinator = FollowUpCoordinator::instance();
1167 - $managed = $coordinator !== null && $coordinator->plan( $optIn, $sendDefaultMail );
1168 -
1169 1035 // Confirm the opt-in
1170 1036 do_action( 'f12_cf7_doubleoptin_before_confirm', $hash, $optIn );
1171 1037
1172 1038 $optIn->set_doubleoptin( 1 );
@@ -1192,32 +1058,16 @@
1192 1058
1193 1059 // Dispatch event
1194 1060 $this->dispatchOptInConfirmedEvent( $optIn, $hash );
1195 1061
1196 - // Everything from here on re-processes the stored submission.
1197 - self::runAsReplay(
1198 - function () use ( $hash, $optIn, $managed, $coordinator, $sendDefaultMail ) {
1199 - do_action( 'f12_cf7_doubleoptin_after_confirm', $hash, $optIn );
1062 + do_action( 'f12_cf7_doubleoptin_after_confirm', $hash, $optIn );
1200 1063
1201 - if ( $managed ) {
1202 - // The coordinator runs every planned action — entry and
1203 - // mail — and records a result per action. The before/after
1204 - // hooks keep firing for listeners that depend on them.
1205 - if ( $sendDefaultMail ) {
1206 - do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $optIn );
1207 - }
1208 - $coordinator->run( $optIn, FollowUpAttempt::TRIGGER_CONFIRM );
1209 - if ( $sendDefaultMail ) {
1210 - do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $optIn );
1211 - }
1212 - } elseif ( $sendDefaultMail ) {
1213 - // Send the original mail if enabled
1214 - do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $optIn );
1215 - $this->sendConfirmationMail( $optIn );
1216 - do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $optIn );
1217 - }
1218 - }
1219 - );
1064 + // Send the original mail if enabled
1065 + if ( apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $optIn->get_cf_form_id() ) ) {
1066 + do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $optIn );
1067 + $this->sendConfirmationMail( $optIn );
1068 + do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $optIn );
1069 + }
1220 1070
1221 1071 $this->getLogger()->info(
1222 1072 'OptIn confirmed successfully',
1223 1073 array(