| @@ -67,8 +67,34 @@ | ||
| 67 | 67 | */ |
| 68 | 68 | protected ?OptInError $lastCreationError = null; |
| 69 | 69 | |
| 70 | 70 | /** |
| 71 | + * Why the last maybeCreateOptIn() returned null, or null if it did not. | |
| 72 | + * | |
| 73 | + * Callers use it to answer in the form plugin's own words — Elementor | |
| 74 | + * adds the message to its AJAX response when the error must reach the | |
| 75 | + * visitor (OptInError::shouldShowToVisitor()). | |
| 76 | + * | |
| 77 | + * @return OptInError|null | |
| 78 | + */ | |
| 79 | + public function getLastCreationError(): ?OptInError { | |
| 80 | + return $this->lastCreationError; | |
| 81 | + } | |
| 82 | + | |
| 83 | + /** | |
| 84 | + * Remember the refusal and hand it to the frontend toast. | |
| 85 | + * | |
| 86 | + * @param OptInError $error The reason. | |
| 87 | + * @param int $formId The form. | |
| 88 | + * | |
| 89 | + * @return void | |
| 90 | + */ | |
| 91 | + protected function storeCreationError( OptInError $error, int $formId ): void { | |
| 92 | + $this->lastCreationError = $error; | |
| 93 | + ErrorNotification::store( $error, $formId ); | |
| 94 | + } | |
| 95 | + | |
| 96 | + /** | |
| 71 | 97 | * Get the validation status from the last validateOptIn() call. |
| 72 | 98 | * |
| 73 | 99 | * @return string One of: '', 'confirmed', 'already_confirmed', 'expired', 'not_found'. |
| 74 | 100 | */ |
| @@ -622,8 +648,26 @@ | ||
| 622 | 648 | return false; |
| 623 | 649 | } |
| 624 | 650 | |
| 625 | 651 | /** |
| 652 | + * Enable / Disable default mail. | |
| 653 | + * | |
| 654 | + * @param bool $status Enable (true) or disable (false) the default mail. | |
| 655 | + * @param int $postId The ID of the Post / Form. | |
| 656 | + * | |
| 657 | + * @since 2.3.3 | |
| 658 | + */ | |
| 659 | + $sendDefaultMail = (bool) apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $OptIn->get_cf_form_id() ); | |
| 660 | + | |
| 661 | + /** | |
| 662 | + * Bind the follow-up plan before the confirmation is saved (see | |
| 663 | + * AbstractFormIntegration::validateOptIn). False when no adapter | |
| 664 | + * handles this integration → previous behaviour below. | |
| 665 | + */ | |
| 666 | + $coordinator = \Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator::instance(); | |
| 667 | + $managed = $coordinator !== null && $coordinator->plan( $OptIn, $sendDefaultMail ); | |
| 668 | + | |
| 669 | + /** | |
| 626 | 670 | * Confirm the OptIn. |
| 627 | 671 | */ |
| 628 | 672 | if ( $this->updateOptInByHash( $hash, 1, $OptIn ) <= 0 ) { |
| 629 | 673 | $this->get_logger()->info( 'OptIn update failed', [ |
| @@ -635,17 +679,23 @@ | ||
| 635 | 679 | } |
| 636 | 680 | |
| 637 | 681 | $this->setValidationStatus( 'confirmed' ); |
| 638 | 682 | |
| 639 | - /** | |
| 640 | - * Enable / Disable default mail. | |
| 641 | - * | |
| 642 | - * @param bool $status Enable (true) or disable (false) the default mail. | |
| 643 | - * @param int $postId The ID of the Post / Form. | |
| 644 | - * | |
| 645 | - * @since 2.3.3 | |
| 646 | - */ | |
| 647 | - if ( ! apply_filters( 'f12_cf7_doubleoptin_send_default_mail', true, $OptIn->get_cf_form_id() ) ) { | |
| 683 | + if ( $managed ) { | |
| 684 | + // Planned actions (skipped ones included) are recorded by the | |
| 685 | + // coordinator; trigger_default_mail is not fired for managed | |
| 686 | + // opt-ins, so no listener can replay them a second time. | |
| 687 | + if ( $sendDefaultMail ) { | |
| 688 | + do_action( 'f12_cf7_doubleoptin_before_send_default_mail', $OptIn ); | |
| 689 | + } | |
| 690 | + $coordinator->run( $OptIn, \Forge12\DoubleOptIn\FollowUp\FollowUpAttempt::TRIGGER_CONFIRM ); | |
| 691 | + if ( $sendDefaultMail ) { | |
| 692 | + do_action( 'f12_cf7_doubleoptin_after_send_default_mail', $OptIn ); | |
| 693 | + } | |
| 694 | + return true; | |
| 695 | + } | |
| 696 | + | |
| 697 | + if ( ! $sendDefaultMail ) { | |
| 648 | 698 | $this->get_logger()->info( 'Default mail disabled for OptIn', [ |
| 649 | 699 | 'plugin' => 'double-opt-in', |
| 650 | 700 | 'form_id' => $OptIn->get_cf_form_id(), |
| 651 | 701 | 'optin_id' => $OptIn->get_id(), |
| @@ -788,8 +838,10 @@ | ||
| 788 | 838 | * |
| 789 | 839 | * @return OptIn|null |
| 790 | 840 | */ |
| 791 | 841 | protected function maybeCreateOptIn( int $formId, string $formHtml, array $parameter, array $files = array(), array $knownFields = array() ): ?OptIn { |
| 842 | + $this->lastCreationError = null; | |
| 843 | + | |
| 792 | 844 | $this->get_logger()->debug( 'maybeCreateOptIn called', [ |
| 793 | 845 | 'plugin' => 'double-opt-in', |
| 794 | 846 | 'class' => __CLASS__, |
| 795 | 847 | 'method' => __METHOD__, |
| @@ -832,9 +884,9 @@ | ||
| 832 | 884 | if ( empty( $recipient ) ) { |
| 833 | 885 | $this->get_logger()->warning( 'No recipient found, skipping OptIn creation', [ |
| 834 | 886 | 'plugin' => 'double-opt-in', |
| 835 | 887 | ] ); |
| 836 | - ErrorNotification::store( | |
| 888 | + $this->storeCreationError( | |
| 837 | 889 | OptInError::fromCode( OptInError::NO_RECIPIENT, [ 'form_id' => $formId ] ), |
| 838 | 890 | $formId |
| 839 | 891 | ); |
| 840 | 892 | return null; |
| @@ -881,9 +933,9 @@ | ||
| 881 | 933 | 'integration' => $this->type, |
| 882 | 934 | 'consent_field' => $consentField, |
| 883 | 935 | ] ); |
| 884 | 936 | do_action( 'f12_cf7_doubleoptin_consent_not_given', $formId, $consentField ); |
| 885 | - ErrorNotification::store( | |
| 937 | + $this->storeCreationError( | |
| 886 | 938 | OptInError::fromCode( |
| 887 | 939 | OptInError::CONSENT_NOT_GIVEN, |
| 888 | 940 | [ 'form_id' => $formId, 'consent_field' => $consentField ] |
| 889 | 941 | ), |
| @@ -918,9 +970,9 @@ | ||
| 918 | 970 | 'ip' => $ip, |
| 919 | 971 | 'formId' => $formId, |
| 920 | 972 | ] ); |
| 921 | 973 | do_action( 'f12_cf7_doubleoptin_rate_limited', 'ip', $ip, $formId ); |
| 922 | - ErrorNotification::store( | |
| 974 | + $this->storeCreationError( | |
| 923 | 975 | OptInError::fromCode( OptInError::RATE_LIMIT_IP, [ 'ip' => $ip, 'form_id' => $formId ] ), |
| 924 | 976 | $formId |
| 925 | 977 | ); |
| 926 | 978 | return null; |
| @@ -932,9 +984,9 @@ | ||
| 932 | 984 | 'email' => $recipient, |
| 933 | 985 | 'formId' => $formId, |
| 934 | 986 | ] ); |
| 935 | 987 | do_action( 'f12_cf7_doubleoptin_rate_limited', 'email', $recipient, $formId ); |
| 936 | - ErrorNotification::store( | |
| 988 | + $this->storeCreationError( | |
| 937 | 989 | OptInError::fromCode( OptInError::RATE_LIMIT_EMAIL, [ 'email' => $recipient, 'form_id' => $formId ] ), |
| 938 | 990 | $formId |
| 939 | 991 | ); |
| 940 | 992 | return null; |
| @@ -993,9 +1045,9 @@ | ||
| 993 | 1045 | ! empty( $errorMsg ) ? $errorMsg : OptInError::fromCode( OptInError::RECIPIENT_INVALID )->getMessage(), |
| 994 | 1046 | [ 'email' => $recipient, 'form_id' => $formId ] |
| 995 | 1047 | ); |
| 996 | 1048 | |
| 997 | - ErrorNotification::store( $this->lastCreationError, $formId ); | |
| 1049 | + $this->storeCreationError( $this->lastCreationError, $formId ); | |
| 998 | 1050 | return null; |
| 999 | 1051 | } |
| 1000 | 1052 | |
| 1001 | 1053 | /** |
| @@ -1060,9 +1112,9 @@ | ||
| 1060 | 1112 | ] ); |
| 1061 | 1113 | |
| 1062 | 1114 | do_action( 'f12_cf7_doubleoptin_creation_failed', $formId, $recipient ); |
| 1063 | 1115 | |
| 1064 | - ErrorNotification::store( | |
| 1116 | + $this->storeCreationError( | |
| 1065 | 1117 | OptInError::fromCode( OptInError::SAVE_FAILED, [ 'form_id' => $formId ] ), |
| 1066 | 1118 | $formId |
| 1067 | 1119 | ); |
| 1068 | 1120 | |
| @@ -1073,11 +1125,13 @@ | ||
| 1073 | 1125 | /** |
| 1074 | 1126 | * Validate if the optin is enabled. |
| 1075 | 1127 | */ |
| 1076 | 1128 | protected function isOptinEnabled( int $formId ): bool { |
| 1077 | - // Disable optin sending if the optin flag is set. | |
| 1078 | - if ( isset( $_GET['optin'] ) ) { | |
| 1079 | - $this->get_logger()->debug( 'Optin disabled due to optin flag in GET request', [ | |
| 1129 | + // Disable optin sending while our own post-confirmation replay runs. | |
| 1130 | + // Not `isset( $_GET['optin'] )`: that is client input and let a | |
| 1131 | + // submitter switch the double opt-in off. | |
| 1132 | + if ( \Forge12\DoubleOptIn\Integration\AbstractFormIntegration::isReplaying() ) { | |
| 1133 | + $this->get_logger()->debug( 'Optin disabled during post-confirmation replay', [ | |
| 1080 | 1134 | 'plugin' => 'double-opt-in', |
| 1081 | 1135 | 'class' => __CLASS__, |
| 1082 | 1136 | 'method' => __METHOD__, |
| 1083 | 1137 | ] ); |
| @@ -1186,8 +1240,23 @@ | ||
| 1186 | 1240 | 'class' => __CLASS__, |
| 1187 | 1241 | 'method' => __METHOD__, |
| 1188 | 1242 | 'hash' => $hash, |
| 1189 | 1243 | ] ); |
| 1244 | + return; | |
| 1245 | + } | |
| 1246 | + | |
| 1247 | + /** | |
| 1248 | + * Only for this integration's own opt-in, only right after it was | |
| 1249 | + * confirmed in this request, and only when no follow-up adapter | |
| 1250 | + * owns the files. Previously any `?optin=` request — an expired | |
| 1251 | + * link, a second click, another integration's hash — deleted the | |
| 1252 | + * stored files, including ones a pending action still needed. | |
| 1253 | + */ | |
| 1254 | + if ( ! $OptIn->isType( $this->type ) || self::$validationStatus !== 'confirmed' ) { | |
| 1255 | + return; | |
| 1256 | + } | |
| 1257 | + $coordinator = \Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator::instance(); | |
| 1258 | + if ( $coordinator !== null && $coordinator->adapterFor( $OptIn ) !== null ) { | |
| 1190 | 1259 | return; |
| 1191 | 1260 | } |
| 1192 | 1261 | |
| 1193 | 1262 | /** |