| @@ -9,8 +9,12 @@ | ||
| 9 | 9 | namespace Forge12\DoubleOptIn\Integration; |
| 10 | 10 | |
| 11 | 11 | use Forge12\DoubleOptIn\Container\Container; |
| 12 | 12 | use Forge12\DoubleOptIn\EmailTemplates\PlaceholderMapper; |
| 13 | +use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt; | |
| 14 | +use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator; | |
| 15 | +use Forge12\DoubleOptIn\FollowUp\FollowUpResult; | |
| 16 | +use Forge12\DoubleOptIn\Frontend\ErrorNotification; | |
| 13 | 17 | use forge12\contactform7\CF7DoubleOptIn\Category; |
| 14 | 18 | use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn; |
| 15 | 19 | use forge12\contactform7\CF7DoubleOptIn\HTMLSelect; |
| 16 | 20 | use forge12\contactform7\CF7DoubleOptIn\OptIn; |
| @@ -84,9 +88,9 @@ | ||
| 84 | 88 | |
| 85 | 89 | // Confirmation mail hooks |
| 86 | 90 | add_action( 'f12_cf7_doubleoptin_before_send_default_mail', array( $this, 'beforeSendDefaultMail' ) ); |
| 87 | 91 | add_action( 'f12_cf7_doubleoptin_after_send_default_mail', array( $this, 'afterSendDefaultMail' ) ); |
| 88 | - add_action( 'f12_cf7_doubleoptin_trigger_default_mail', array( $this, 'sendConfirmationMail' ) ); | |
| 92 | + add_action( 'f12_cf7_doubleoptin_trigger_default_mail', array( $this, 'onTriggerDefaultMail' ) ); | |
| 89 | 93 | |
| 90 | 94 | // File hand-off + pending-cleanup. CF7 attaches files to the |
| 91 | 95 | // confirmation mail in attachExtraAttachments (hooked on |
| 92 | 96 | // wpcf7_before_send_mail during sendConfirmationMail). Cleanup |
| @@ -245,10 +249,10 @@ | ||
| 245 | 249 | ) |
| 246 | 250 | ); |
| 247 | 251 | |
| 248 | 252 | if ( ! $this->isOptInEnabled( $formId ) ) { |
| 249 | - // Handle file attachments for confirmation | |
| 250 | - if ( isset( $_GET['optin'] ) ) { | |
| 253 | + // Our own post-confirmation replay: attach the stored files. | |
| 254 | + if ( self::isReplaying() ) { | |
| 251 | 255 | $this->attachStoredFiles( $submission ); |
| 252 | 256 | } |
| 253 | 257 | return; |
| 254 | 258 | } |
| @@ -283,14 +287,19 @@ | ||
| 283 | 287 | // Always prevent the original CF7 mail from being sent when opt-in creation fails |
| 284 | 288 | add_filter( 'wpcf7_skip_mail', '__return_true' ); |
| 285 | 289 | |
| 286 | 290 | $error = self::getLastError(); |
| 287 | - if ( $error && apply_filters( 'f12_cf7_doubleoptin_show_validation_error', false ) ) { | |
| 291 | + // Abort with the reason instead of CF7's "sent": CF7 then keeps the | |
| 292 | + // visitor's input and shows the message in the form. A refused | |
| 293 | + // consent always takes this path (OptInError::isAlwaysShown()). | |
| 294 | + if ( $error && $error->shouldShowToVisitor( (int) $formId ) ) { | |
| 288 | 295 | $message = apply_filters( 'f12_cf7_doubleoptin_error_message', $error->getMessage(), $error, $formId ); |
| 289 | 296 | if ( method_exists( $submission, 'set_response' ) ) { |
| 290 | 297 | $submission->set_response( $message ); |
| 291 | 298 | } |
| 292 | 299 | $abort = true; |
| 300 | + // The form shows it now — no second copy in the toast. | |
| 301 | + ErrorNotification::forget(); | |
| 293 | 302 | } |
| 294 | 303 | return; |
| 295 | 304 | } |
| 296 | 305 | |
| @@ -361,9 +370,51 @@ | ||
| 361 | 370 | /** |
| 362 | 371 | * {@inheritdoc} |
| 363 | 372 | */ |
| 364 | 373 | public function sendConfirmationMail( OptIn $optIn ): void { |
| 365 | - if ( ! $this->isAvailable() ) { | |
| 374 | + self::runAsReplay( | |
| 375 | + function () use ( $optIn ) { | |
| 376 | + $this->replaySubmission( $optIn ); | |
| 377 | + } | |
| 378 | + ); | |
| 379 | + } | |
| 380 | + | |
| 381 | + /** | |
| 382 | + * Listener on the global `f12_cf7_doubleoptin_trigger_default_mail`. | |
| 383 | + * That action fires for every integration's opt-in; this used to run | |
| 384 | + * the CF7 submission for Elementor opt-ins too, overwriting $_POST. | |
| 385 | + * Managed opt-ins go through the follow-up coordinator, so a second | |
| 386 | + * trigger never sends twice. | |
| 387 | + * | |
| 388 | + * @param OptIn $optIn The confirmed opt-in. | |
| 389 | + * | |
| 390 | + * @since 5.6.0 | |
| 391 | + */ | |
| 392 | + public function onTriggerDefaultMail( OptIn $optIn ): void { | |
| 393 | + if ( ! $optIn->isType( $this->getIdentifier() ) ) { | |
| 394 | + return; | |
| 395 | + } | |
| 396 | + | |
| 397 | + $coordinator = FollowUpCoordinator::instance(); | |
| 398 | + if ( $coordinator !== null && $coordinator->plan( $optIn, true ) ) { | |
| 399 | + $coordinator->run( $optIn, FollowUpAttempt::TRIGGER_LEGACY ); | |
| 400 | + return; | |
| 401 | + } | |
| 402 | + | |
| 403 | + $this->sendConfirmationMail( $optIn ); | |
| 404 | + } | |
| 405 | + | |
| 406 | + /** | |
| 407 | + * Re-run the stored submission through CF7 and report what CF7 says. | |
| 408 | + * | |
| 409 | + * Must run inside {@see runAsReplay()} so our own | |
| 410 | + * `wpcf7_before_send_mail` listener processes it as the confirmed | |
| 411 | + * submission (attachments) instead of creating a new opt-in. | |
| 412 | + * | |
| 413 | + * @since 5.6.0 | |
| 414 | + */ | |
| 415 | + public function replaySubmission( OptIn $optIn ): FollowUpResult { | |
| 416 | + if ( ! $this->isAvailable() || ! class_exists( '\\WPCF7_ContactForm' ) || ! class_exists( '\\WPCF7_Submission' ) ) { | |
| 366 | 417 | $this->getLogger()->warning( |
| 367 | 418 | 'CF7 not available for confirmation mail', |
| 368 | 419 | array( |
| 369 | 420 | 'plugin' => 'double-opt-in', |
| @@ -368,18 +419,11 @@ | ||
| 368 | 419 | array( |
| 369 | 420 | 'plugin' => 'double-opt-in', |
| 370 | 421 | ) |
| 371 | 422 | ); |
| 372 | - return; | |
| 423 | + return FollowUpResult::failedRetryable( 'integration_unavailable' ); | |
| 373 | 424 | } |
| 374 | 425 | |
| 375 | - $this->currentOptIn = $optIn; | |
| 376 | - | |
| 377 | - // Restore POST data | |
| 378 | - $data = maybe_unserialize( $optIn->get_content() ); | |
| 379 | - $_POST = SanitizeHelper::sanitize_array( $data ); | |
| 380 | - | |
| 381 | - // Get CF7 form | |
| 382 | 426 | $contactForm = \WPCF7_ContactForm::get_instance( $optIn->get_cf_form_id() ); |
| 383 | 427 | if ( ! $contactForm ) { |
| 384 | 428 | $this->getLogger()->warning( |
| 385 | 429 | 'CF7 form not found for confirmation mail', |
| @@ -387,31 +431,52 @@ | ||
| 387 | 431 | 'plugin' => 'double-opt-in', |
| 388 | 432 | 'form_id' => $optIn->get_cf_form_id(), |
| 389 | 433 | ) |
| 390 | 434 | ); |
| 391 | - return; | |
| 435 | + return FollowUpResult::failedPermanent( 'form_missing' ); | |
| 392 | 436 | } |
| 393 | 437 | |
| 394 | - // Add attachment hook | |
| 395 | - add_action( 'wpcf7_before_send_mail', array( $this, 'attachExtraAttachments' ), 10, 3 ); | |
| 438 | + $data = maybe_unserialize( $optIn->get_content() ); | |
| 439 | + if ( ! is_array( $data ) ) { | |
| 440 | + return FollowUpResult::failedPermanent( 'payload_missing' ); | |
| 441 | + } | |
| 396 | 442 | |
| 397 | - // Disable validation and spam checks before creating submission | |
| 398 | - $this->beforeSendConfirmationMail(); | |
| 443 | + $previousPost = $_POST; | |
| 444 | + $previousOptIn = $this->currentOptIn; | |
| 445 | + $this->currentOptIn = $optIn; | |
| 446 | + $status = ''; | |
| 399 | 447 | |
| 400 | - // Create submission and send mail | |
| 401 | - $submission = \WPCF7_Submission::get_instance( $contactForm ); | |
| 448 | + try { | |
| 449 | + $_POST = SanitizeHelper::sanitize_array( $data ); | |
| 402 | 450 | |
| 403 | - // Re-enable validation and spam checks | |
| 404 | - $this->afterSendConfirmationMail(); | |
| 451 | + // Disable validation and spam checks before creating submission | |
| 452 | + $this->beforeSendConfirmationMail(); | |
| 405 | 453 | |
| 454 | + // Create submission and send mail. Attachments are added by | |
| 455 | + // onSubmit() → attachStoredFiles() while isReplaying(). | |
| 456 | + $submission = \WPCF7_Submission::get_instance( $contactForm ); | |
| 457 | + | |
| 458 | + if ( is_object( $submission ) && method_exists( $submission, 'get_status' ) ) { | |
| 459 | + $status = (string) $submission->get_status(); | |
| 460 | + } | |
| 461 | + } finally { | |
| 462 | + // Re-enable validation and spam checks, restore request state. | |
| 463 | + $this->afterSendConfirmationMail(); | |
| 464 | + $_POST = $previousPost; | |
| 465 | + $this->currentOptIn = $previousOptIn; | |
| 466 | + } | |
| 467 | + | |
| 406 | 468 | $this->getLogger()->info( |
| 407 | 469 | 'Confirmation mail triggered via CF7', |
| 408 | 470 | array( |
| 409 | - 'plugin' => 'double-opt-in', | |
| 410 | - 'form_id' => $optIn->get_cf_form_id(), | |
| 411 | - 'optin_id' => $optIn->get_id(), | |
| 471 | + 'plugin' => 'double-opt-in', | |
| 472 | + 'form_id' => $optIn->get_cf_form_id(), | |
| 473 | + 'optin_id' => $optIn->get_id(), | |
| 474 | + 'cf7_status' => $status, | |
| 412 | 475 | ) |
| 413 | 476 | ); |
| 477 | + | |
| 478 | + return CF7FollowUpAdapter::mapStatus( $status ); | |
| 414 | 479 | } |
| 415 | 480 | |
| 416 | 481 | /** |
| 417 | 482 | * Handle opt-in confirmation from URL. |
| @@ -472,10 +537,11 @@ | ||
| 472 | 537 | * |
| 473 | 538 | * @return void |
| 474 | 539 | */ |
| 475 | 540 | private function attachStoredFiles( $submission ): void { |
| 476 | - $hash = sanitize_text_field( $_GET['optin'] ); | |
| 477 | - $optIn = OptIn::get_by_hash( $hash ); | |
| 541 | + // The opt-in being replayed — not the one named in the URL, which | |
| 542 | + // a cron or admin retry does not have (and a visitor controls). | |
| 543 | + $optIn = $this->currentOptIn; | |
| 478 | 544 | |
| 479 | 545 | if ( ! $optIn ) { |
| 480 | 546 | return; |
| 481 | 547 | } |
| @@ -537,8 +603,16 @@ | ||
| 537 | 603 | * |
| 538 | 604 | * @since 4.3.0 |
| 539 | 605 | */ |
| 540 | 606 | public function cleanupPendingAfterMail( OptIn $optIn ): void { |
| 607 | + // Managed opt-ins: CF7FollowUpAdapter::onSettled() cleans up only | |
| 608 | + // once the mail was actually handed over. This hook fires after | |
| 609 | + // the attempt regardless of its outcome. | |
| 610 | + $coordinator = FollowUpCoordinator::instance(); | |
| 611 | + if ( $coordinator !== null && $coordinator->adapterFor( $optIn ) !== null ) { | |
| 612 | + return; | |
| 613 | + } | |
| 614 | + | |
| 541 | 615 | // Template-method's $hash arg is unused inside processFilesOnConfirm; |
| 542 | 616 | // passing an empty string keeps the contract tight. |
| 543 | 617 | $this->processFilesOnConfirm( '', $optIn ); |
| 544 | 618 | } |