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