PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.3
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.3
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
← All changes | src/Integration/CF7Integration.php +101 -27 5.1.5 → 5.6.3 View file →
@@ -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 }