PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.7.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.7.0
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 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 All 39 releases
← All changes | src/Integration/CF7Integration.php +6 -271 trunk → 5.7.0 View file →
@@ -13,10 +13,8 @@
13 13 use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt;
14 14 use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator;
15 15 use Forge12\DoubleOptIn\FollowUp\FollowUpResult;
16 16 use Forge12\DoubleOptIn\Frontend\ErrorNotification;
17 -use Forge12\DoubleOptIn\Frontend\SubmitNotice;
18 -use Forge12\DoubleOptIn\Spam\SubmissionTrap;
19 17 use forge12\contactform7\CF7DoubleOptIn\Category;
20 18 use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn;
21 19 use forge12\contactform7\CF7DoubleOptIn\HTMLSelect;
22 20 use forge12\contactform7\CF7DoubleOptIn\OptIn;
@@ -42,23 +40,8 @@
42 40 */
43 41 private ?OptIn $currentOptIn = null;
44 42
45 43 /**
46 - * The confirmation mail sent in this request, for the feedback response
47 - * (5.8.0): recipient, resolved sender and subject, and the outcome.
48 - *
49 - * @var array{email: string, sender: string, subject: string, sent: bool}|null
50 - */
51 - private $lastOptInMail = null;
52 -
53 - /**
54 - * The DOI submission of this request, keyed to its form (5.8.0).
55 - *
56 - * @var array{form_id: int, email: string, sender: string, subject: string, sent: bool}|null
57 - */
58 - private $submitted = null;
59 -
60 - /**
61 44 * {@inheritdoc}
62 45 */
63 46 public function getIdentifier(): string {
64 47 return 'cf7';
@@ -99,16 +82,8 @@
99 82 // Frontend hooks
100 83 add_action( 'wpcf7_before_send_mail', array( $this, 'onSubmit' ), $this->getHookPriority(), 3 );
101 84 add_action( 'init', array( $this, 'handleOptInConfirmation' ) );
102 85
103 - // Honest success message + "open your inbox" (5.8.0).
104 - add_filter( 'wpcf7_feedback_response', array( $this, 'filterFeedbackResponse' ), 10, 2 );
105 - add_action( 'wpcf7_enqueue_scripts', array( $this, 'enqueueSubmitNotice' ) );
106 -
107 - // Honeypot + minimum fill time, through CF7's own spam flow (5.8.0).
108 - add_filter( 'wpcf7_form_elements', array( $this, 'addSubmissionTrap' ) );
109 - add_filter( 'wpcf7_spam', array( $this, 'checkSubmissionTrap' ), 9, 2 );
110 -
111 86 // Register recipient filter
112 87 add_filter( 'f12_cf7_doubleoptin_get_recipient_cf7', array( $this, 'getRecipientFilter' ), 10, 3 );
113 88
114 89 // Confirmation mail hooks
@@ -328,13 +303,9 @@
328 303 return;
329 304 }
330 305
331 306 // Send opt-in mail
332 - $this->lastOptInMail = null;
333 307 $this->sendOptInMail( $optIn, $formData, $formParameter );
334 - if ( $this->lastOptInMail !== null ) {
335 - $this->submitted = array( 'form_id' => (int) $formId ) + $this->lastOptInMail;
336 - }
337 308
338 309 // Skip original mail
339 310 add_filter( 'wpcf7_skip_mail', '__return_true' );
340 311 do_action( 'f12_cf7_doubleoptin_sent', $form, $formId );
@@ -380,257 +351,21 @@
380 351 if ( ! empty( $args['sender_name'] ) ) {
381 352 $args['additional_headers'] .= 'From: ' . $args['sender_name'] . ' <' . $args['sender'] . '>';
382 353 }
383 354
384 - // Send via CF7 mail system. Up to 5.7 the result was dropped and a
385 - // failed send looked exactly like a successful one.
386 - $sent = (bool) \WPCF7_Mail::send( $args, 'mail' );
355 + // Send via CF7 mail system
356 + \WPCF7_Mail::send( $args, 'mail' );
387 357
388 - // Record the outcome on the opt-in (OptInMailTracker). No address in
389 - // the log: the id identifies the record.
390 - do_action( 'f12_doi_optin_mail_result', (int) $optIn->get_id(), $sent, '' );
391 -
392 - // What the visitor is told to look for — with CF7's mail tags resolved,
393 - // as WPCF7_Mail::send() did for the mail itself.
394 - $sender = (string) $args['sender'];
395 - $subject = (string) $args['subject'];
396 - if ( function_exists( 'wpcf7_mail_replace_tags' ) ) {
397 - $sender = (string) wpcf7_mail_replace_tags( $sender );
398 - $subject = (string) wpcf7_mail_replace_tags( $subject );
399 - }
400 - $this->lastOptInMail = array(
401 - 'optin_id' => (int) $optIn->get_id(),
402 - 'email' => (string) $optIn->get_email(),
403 - 'sender' => SubmitNotice::senderAddress( $sender ),
404 - 'subject' => trim( wp_strip_all_tags( $subject ) ),
405 - 'sent' => $sent,
406 - );
407 -
408 358 $this->getLogger()->info(
409 - $sent ? 'OptIn mail handed to the mail server via CF7' : 'OptIn mail could not be sent via CF7',
359 + 'OptIn mail sent via CF7',
410 360 array(
411 - 'plugin' => 'double-opt-in',
412 - 'form_id' => $formData->getFormId(),
413 - 'optin_id' => (int) $optIn->get_id(),
361 + 'plugin' => 'double-opt-in',
362 + 'form_id' => $formData->getFormId(),
363 + 'recipient' => $args['recipient'],
414 364 )
415 365 );
416 366
417 - return $sent;
418 - }
419 -
420 - /**
421 - * Make CF7's answer to a double opt-in submission tell the truth (5.8.0).
422 - *
423 - * CF7 answers "Thank you for your message. It has been sent." — but
424 - * nothing is sent until the address is confirmed, and when the
425 - * confirmation mail itself failed CF7 still said so and cleared the form.
426 - * CF7 overwrites any response set during the submission, so this runs on
427 - * the finished feedback response.
428 - *
429 - * @param mixed $response CF7 feedback response.
430 - * @param mixed $result CF7 submission result.
431 - *
432 - * @return mixed
433 - */
434 - public function filterFeedbackResponse( $response, $result = null ) {
435 - if ( ! is_array( $response ) || $this->submitted === null ) {
436 - return $response;
437 - }
438 -
439 - $formId = (int) ( $response['contact_form_id'] ?? 0 );
440 - if ( $formId !== $this->submitted['form_id'] || ( $response['status'] ?? '' ) !== 'mail_sent' ) {
441 - return $response;
442 - }
443 -
444 - $submitted = $this->submitted;
445 - $this->submitted = null;
446 - $form = class_exists( '\WPCF7_ContactForm' ) ? \WPCF7_ContactForm::get_instance( $formId ) : null;
447 -
448 - if ( ! $submitted['sent'] ) {
449 - // CF7 then keeps the visitor's input and fires wpcf7mailfailed.
450 - $response['status'] = 'mail_failed';
451 - $response['message'] = $form ? (string) $form->message( 'mail_sent_ng' ) : __( 'The confirmation mail could not be sent. Please try again later.', 'double-opt-in' );
452 - return $response;
453 - }
454 -
455 - /**
456 - * Whether to show the confirmation hint (masked address, what to look
457 - * for, "open your inbox" link) after a double opt-in submission.
458 - *
459 - * @since 5.8.0
460 - *
461 - * @param bool $show Default true.
462 - * @param int $formId Form ID.
463 - */
464 - if ( ! apply_filters( 'f12_doi_submit_notice', true, $formId ) ) {
465 - return $response;
466 - }
467 -
468 - $notice = SubmitNotice::extend(
469 - SubmitNotice::build( $submitted['email'], $submitted['sender'], $submitted['subject'] ),
470 - array(
471 - 'form_id' => $formId,
472 - 'optin_id' => (int) ( $submitted['optin_id'] ?? 0 ),
473 - 'integration' => 'cf7',
474 - )
475 - );
476 -
477 - // A message the site owner wrote stays; only CF7's own default is wrong.
478 - if ( $form && self::isDefaultSentMessage( (string) $form->message( 'mail_sent_ok', false ) ) ) {
479 - $response['message'] = SubmitNotice::message( $notice['masked'] );
480 - }
481 -
482 - $response['doi'] = $notice;
483 -
484 - return $response;
485 - }
486 -
487 - /**
488 - * Whether a form's success message is still CF7's default, in English or
489 - * in the current language.
490 - */
491 - public static function isDefaultSentMessage( string $message ): bool {
492 - $defaults = array( 'Thank you for your message. It has been sent.' );
493 - if ( function_exists( 'wpcf7_messages' ) ) {
494 - $messages = wpcf7_messages();
495 - $defaults[] = (string) ( $messages['mail_sent_ok']['default'] ?? '' );
496 - }
497 -
498 - return in_array( trim( $message ), array_filter( $defaults ), true );
499 - }
500 -
501 - /**
502 - * Whether a form gets the honeypot and the minimum fill time.
503 - */
504 - private function trapEnabled( int $formId ): bool {
505 - /**
506 - * Whether a double opt-in form gets the honeypot and the minimum
507 - * fill time.
508 - *
509 - * @since 5.8.0
510 - *
511 - * @param bool $enabled Default true.
512 - * @param int $formId Form ID.
513 - */
514 - return $formId > 0 && $this->isOptInEnabled( $formId ) && (bool) apply_filters( 'f12_doi_spam_trap', true, $formId );
515 - }
516 -
517 - /**
518 - * Add the trap fields to a double opt-in form.
519 - *
520 - * @param mixed $elements Form HTML.
521 - *
522 - * @return mixed
523 - */
524 - public function addSubmissionTrap( $elements ) {
525 - if ( ! is_string( $elements ) || ! function_exists( 'wpcf7_get_current_contact_form' ) ) {
526 - return $elements;
527 - }
528 - $form = wpcf7_get_current_contact_form();
529 - if ( ! $form || ! $this->trapEnabled( (int) $form->id() ) ) {
530 - return $elements;
531 - }
532 -
533 - return $elements . SubmissionTrap::markup( time() );
534 - }
535 -
536 - /**
537 - * Mark a bot submission as spam before any opt-in or mail exists.
538 - *
539 - * @param mixed $spam CF7's verdict so far.
540 - * @param mixed $submission The submission.
541 - *
542 - * @return mixed
543 - */
544 - public function checkSubmissionTrap( $spam, $submission = null ) {
545 - if ( $spam || self::isReplaying() || ! is_object( $submission ) || ! method_exists( $submission, 'get_contact_form' ) ) {
546 - return $spam;
547 - }
548 - $form = $submission->get_contact_form();
549 - $formId = $form ? (int) $form->id() : 0;
550 - if ( ! $this->trapEnabled( $formId ) ) {
551 - return $spam;
552 - }
553 -
554 - /**
555 - * Minimum seconds between rendering a double opt-in form and
556 - * submitting it; faster submissions are treated as bots.
557 - *
558 - * @since 5.8.0
559 - *
560 - * @param int $seconds Default 2.
561 - * @param int $formId Form ID.
562 - */
563 - $minSeconds = (int) apply_filters( 'f12_doi_min_fill_seconds', SubmissionTrap::DEFAULT_MIN_SECONDS, $formId );
564 -
565 - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- CF7 verified the submission; only our two trap fields are read.
566 - $reason = SubmissionTrap::check( wp_unslash( $_POST ), time(), $minSeconds );
567 - if ( $reason === '' ) {
568 - return $spam;
569 - }
570 -
571 - if ( method_exists( $submission, 'add_spam_log' ) ) {
572 - $submission->add_spam_log(
573 - array(
574 - 'agent' => 'double-opt-in',
575 - 'reason' => $reason,
576 - )
577 - );
578 - }
579 -
580 - /**
581 - * A double opt-in submission was stopped by the honeypot or the
582 - * minimum fill time. No personal data is passed.
583 - *
584 - * @since 5.8.0
585 - *
586 - * @param string $reason 'honeypot', 'stamp_invalid' or 'too_fast'.
587 - * @param int $formId Form ID.
588 - * @param string $integration Integration identifier.
589 - */
590 - do_action( 'f12_doi_spam_trap_hit', $reason, $formId, 'cf7' );
591 -
592 - $this->getLogger()->info(
593 - 'Submission stopped by the spam trap',
594 - array(
595 - 'plugin' => 'double-opt-in',
596 - 'form_id' => $formId,
597 - 'reason' => $reason,
598 - )
599 - );
600 -
601 367 return true;
602 - }
603 -
604 - /**
605 - * The script that renders the confirmation hint under the CF7 message.
606 - * Runs only where CF7 enqueues its own scripts.
607 - *
608 - * @return void
609 - */
610 - public function enqueueSubmitNotice(): void {
611 - $version = defined( 'FORGE12_OPTIN_VERSION' ) ? FORGE12_OPTIN_VERSION : false;
612 -
613 - wp_enqueue_script(
614 - 'f12-doi-submit-notice',
615 - plugins_url( 'assets/js/doi-submit-notice.js', F12_DOUBLEOPTIN_PLUGIN_FILE ),
616 - array(),
617 - $version,
618 - true
619 - );
620 -
621 - wp_register_style( 'f12-doi-submit-notice', false, array(), $version );
622 - wp_enqueue_style( 'f12-doi-submit-notice' );
623 - wp_add_inline_style(
624 - 'f12-doi-submit-notice',
625 - '.f12-doi-notice{margin:.5em 0 1em;padding:0 1em}'
626 - . '.f12-doi-notice p{margin:.4em 0}'
627 - . '.f12-doi-notice .f12-doi-inbox{display:inline-block;margin-top:.4em;padding:.5em 1em;border:1px solid currentColor;border-radius:4px;text-decoration:none;font-weight:600}'
628 - // Buttons and links of add-ons (f12_doi_submit_notice_data) look like
629 - // the inbox link instead of a bare browser button.
630 - . '.f12-doi-notice .f12-doi-action{display:inline-block;margin-top:.4em;padding:.5em 1em;border:1px solid currentColor;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;text-decoration:none;cursor:pointer}'
631 - . '.f12-doi-notice button.f12-doi-action:disabled{opacity:.5;cursor:default}'
632 - );
633 368 }
634 369
635 370 /**
636 371 * {@inheritdoc}