| @@ -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( |