PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.9.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.9.0
5.9.0 5.8.0 5.8.1 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 All 42 releases
double-opt-in / src / Frontend / ConfirmationShortcodes.php

ConfirmationShortcodes.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.9.0, at src/Frontend/ConfirmationShortcodes.php

268 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shortcodes for the confirmation and error pages.
4 *
5 * - [doi_confirmation_status] says what happened to the link the visitor
6 * just clicked: confirmed, already confirmed, expired or invalid.
7 * - [doi_error_message] explains the `?doi_error=<code>` that a refused
8 * submission is redirected with. Up to 5.7 nothing read that parameter.
9 * - [doi_field name="…"] greets with a value from the form, only in the
10 * request that confirmed it.
11 * - [doi_if status="…"]…[/doi_if] shows its content only for the listed
12 * validation statuses, so one page can carry a text per outcome.
13 *
14 * Without a shortcode on the page, a failed link still gets its message in
15 * front of the content — the new integration system set the status but
16 * showed it nowhere.
17 *
18 * @package Forge12\DoubleOptIn\Frontend
19 * @since 5.8.0
20 */
21
22 declare( strict_types=1 );
23
24 namespace Forge12\DoubleOptIn\Frontend;
25
26 use Forge12\DoubleOptIn\Integration\AbstractFormIntegration;
27 use Forge12\DoubleOptIn\Integration\OptInError;
28
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit;
31 }
32
33 class ConfirmationShortcodes {
34
35 /** @var callable(): string */
36 private $status;
37
38 /**
39 * Form values of the opt-in confirmed in this request.
40 *
41 * @var array<string, mixed>|null
42 */
43 private $confirmedFields = null;
44
45 /** @var bool */
46 private $statusShown = false;
47
48 /**
49 * @param callable|null $status Current validation status; defaults to the integration system's.
50 */
51 public function __construct( ?callable $status = null ) {
52 $this->status = $status ?? array( AbstractFormIntegration::class, 'getValidationStatus' );
53 }
54
55 public function register(): void {
56 add_shortcode( 'doi_confirmation_status', array( $this, 'renderStatus' ) );
57 add_shortcode( 'doi_error_message', array( $this, 'renderError' ) );
58 add_shortcode( 'doi_field', array( $this, 'renderField' ) );
59 add_shortcode( 'doi_if', array( $this, 'renderIf' ) );
60
61 add_action( 'f12_cf7_doubleoptin_after_confirm', array( $this, 'rememberConfirmed' ), 1, 2 );
62 // After do_shortcode (11): by then a [doi_confirmation_status] on the page has run.
63 add_filter( 'the_content', array( $this, 'prependFallbackNotice' ), 12 );
64 }
65
66 /**
67 * @param mixed $hash Confirmed hash (unused).
68 * @param mixed $optIn The confirmed opt-in (legacy wrapper or entity).
69 */
70 public function rememberConfirmed( $hash, $optIn ): void {
71 if ( is_object( $optIn ) && method_exists( $optIn, 'getEntity' ) ) {
72 $optIn = $optIn->getEntity();
73 }
74 if ( is_object( $optIn ) && method_exists( $optIn, 'getContentArray' ) ) {
75 $this->confirmedFields = (array) $optIn->getContentArray();
76 }
77 }
78
79 /**
80 * Default texts per validation status.
81 *
82 * @return array<string, string>
83 */
84 public static function statusMessages(): array {
85 return array(
86 'confirmed' => __( 'Thank you! Your email address has been confirmed.', 'double-opt-in' ),
87 'already_confirmed' => __( 'Your opt-in has already been confirmed.', 'double-opt-in' ),
88 'expired' => __( 'This confirmation link has expired. Please submit the form again.', 'double-opt-in' ),
89 'not_found' => __( 'This confirmation link is invalid.', 'double-opt-in' ),
90 );
91 }
92
93 /**
94 * [doi_confirmation_status confirmed="…" already_confirmed="…" expired="…" not_found="…" none="…"]
95 *
96 * @param mixed $atts Shortcode attributes.
97 */
98 public function renderStatus( $atts = array() ): string {
99 $status = (string) call_user_func( $this->status );
100 $atts = shortcode_atts( array_merge( self::statusMessages(), array( 'none' => '' ) ), is_array( $atts ) ? $atts : array(), 'doi_confirmation_status' );
101
102 $this->statusShown = true;
103
104 $key = $status !== '' && isset( $atts[ $status ] ) ? $status : 'none';
105 $message = (string) $atts[ $key ];
106 if ( $message === '' ) {
107 return '';
108 }
109
110 return self::notice( $key === 'none' ? 'none' : $status, $message );
111 }
112
113 /**
114 * Visitor-facing texts per OptInError code.
115 *
116 * @return array<string, string>
117 */
118 public static function errorMessages(): array {
119 $messages = array(
120 OptInError::RATE_LIMIT_IP => __( 'Too many sign-ups from your connection in a short time. Please try again later.', 'double-opt-in' ),
121 OptInError::RATE_LIMIT_EMAIL => __( 'This email address was signed up several times in a short time. Please check your inbox for the confirmation mail or try again later.', 'double-opt-in' ),
122 OptInError::RECIPIENT_INVALID => __( 'This email address cannot receive mail. Please check it and try again.', 'double-opt-in' ),
123 OptInError::NO_RECIPIENT => __( 'No valid email address was entered. Please try again.', 'double-opt-in' ),
124 OptInError::UNIQUE_EMAIL_DUPLICATE => __( 'This email address is already signed up.', 'double-opt-in' ),
125 OptInError::CONSENT_NOT_GIVEN => __( 'Please agree to the consent text to sign up.', 'double-opt-in' ),
126 OptInError::SAVE_FAILED => __( 'Your sign-up could not be saved. Please try again later.', 'double-opt-in' ),
127 OptInError::SUBMISSION_CANCELLED => __( 'Your sign-up could not be completed. Please try again later.', 'double-opt-in' ),
128 );
129
130 /**
131 * Texts shown by [doi_error_message] per error code.
132 *
133 * @since 5.8.0
134 *
135 * @param array<string, string> $messages code => text.
136 */
137 $filtered = apply_filters( 'f12_doi_error_messages', $messages );
138
139 return is_array( $filtered ) ? $filtered : $messages;
140 }
141
142 /**
143 * [doi_error_message default="…"]
144 *
145 * @param mixed $atts Shortcode attributes.
146 */
147 public function renderError( $atts = array() ): string {
148 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display of a redirect parameter.
149 $code = isset( $_GET['doi_error'] ) ? sanitize_key( wp_unslash( (string) $_GET['doi_error'] ) ) : '';
150 if ( $code === '' ) {
151 return '';
152 }
153
154 $atts = shortcode_atts(
155 array( 'default' => __( 'Your sign-up could not be completed. Please try again later.', 'double-opt-in' ) ),
156 is_array( $atts ) ? $atts : array(),
157 'doi_error_message'
158 );
159 $messages = self::errorMessages();
160 $message = isset( $messages[ $code ] ) ? (string) $messages[ $code ] : (string) $atts['default'];
161
162 return $message === '' ? '' : self::notice( 'error-' . $code, $message );
163 }
164
165 /**
166 * [doi_field name="first-name" default="…"]
167 *
168 * @param mixed $atts Shortcode attributes.
169 */
170 public function renderField( $atts = array() ): string {
171 $atts = shortcode_atts(
172 array(
173 'name' => '',
174 'default' => '',
175 ),
176 is_array( $atts ) ? $atts : array(),
177 'doi_field'
178 );
179
180 $default = esc_html( (string) $atts['default'] );
181 if ( $this->confirmedFields === null || (string) call_user_func( $this->status ) !== 'confirmed' ) {
182 return $default;
183 }
184
185 $value = $this->confirmedFields[ (string) $atts['name'] ] ?? null;
186 if ( is_array( $value ) ) {
187 $value = implode( ', ', array_map( 'strval', array_filter( $value, 'is_scalar' ) ) );
188 }
189 if ( ! is_scalar( $value ) || trim( (string) $value ) === '' ) {
190 return $default;
191 }
192
193 return esc_html( (string) $value );
194 }
195
196 /**
197 * [doi_if status="confirmed,already_confirmed"]…[/doi_if]
198 *
199 * Statuses are those of [doi_confirmation_status]; "none" matches a visit
200 * without a confirmation link. Content shown for a real status counts as
201 * the page's status message, so the fallback notice stays away.
202 *
203 * @param mixed $atts Shortcode attributes.
204 * @param string|null $content Enclosed content.
205 */
206 public function renderIf( $atts = array(), $content = null ): string {
207 if ( ! is_string( $content ) || $content === '' ) {
208 return '';
209 }
210
211 $atts = shortcode_atts( array( 'status' => '' ), is_array( $atts ) ? $atts : array(), 'doi_if' );
212 $wanted = array_filter( array_map( 'trim', explode( ',', strtolower( (string) $atts['status'] ) ) ) );
213 $status = (string) call_user_func( $this->status );
214 $key = $status === '' ? 'none' : $status;
215
216 if ( ! in_array( $key, $wanted, true ) ) {
217 return '';
218 }
219
220 if ( $key !== 'none' ) {
221 $this->statusShown = true;
222 }
223
224 return do_shortcode( $content );
225 }
226
227 /**
228 * A failed link on a page without [doi_confirmation_status]: say so first.
229 *
230 * @param mixed $content Post content.
231 *
232 * @return mixed
233 */
234 public function prependFallbackNotice( $content ) {
235 if ( $this->statusShown || ! is_string( $content ) || ! is_main_query() || ! in_the_loop() ) {
236 return $content;
237 }
238
239 $status = (string) call_user_func( $this->status );
240 if ( ! in_array( $status, array( 'already_confirmed', 'expired', 'not_found' ), true ) ) {
241 return $content;
242 }
243
244 /**
245 * Whether a failed confirmation link gets its message in front of the
246 * page content when the page has no [doi_confirmation_status].
247 *
248 * @since 5.8.0
249 *
250 * @param bool $show Default true.
251 * @param string $status Validation status.
252 */
253 if ( ! apply_filters( 'f12_doi_validation_notice_auto', true, $status ) ) {
254 return $content;
255 }
256
257 $this->statusShown = true;
258
259 return self::notice( $status, self::statusMessages()[ $status ] ) . $content;
260 }
261
262 private static function notice( string $status, string $message ): string {
263 return '<div class="doi-validation-notice doi-notice-' . esc_attr( $status ) . '" role="status"><p>'
264 . esc_html( $message )
265 . '</p></div>';
266 }
267 }
268