PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.8.1
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.8.1
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 3.0.51 All 41 releases
double-opt-in / src / Frontend / SubmitNotice.php

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

311 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * What the visitor sees right after submitting a double opt-in form.
4 *
5 * The form plugin's own success message says the message "has been sent",
6 * which is wrong until the address is confirmed. This builds the honest
7 * version: where the confirmation mail went (masked), what to look for, and
8 * — for the big webmail providers — a link straight into the inbox.
9 *
10 * Plain links from a local list; nothing is requested from anyone.
11 *
12 * @package Forge12\DoubleOptIn\Frontend
13 * @since 5.8.0
14 */
15
16 declare( strict_types=1 );
17
18 namespace Forge12\DoubleOptIn\Frontend;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 class SubmitNotice {
25
26 /**
27 * Webmail providers by mail domain. `search` (optional) takes the
28 * url-encoded query in place of `%s`.
29 *
30 * @return array<string, array{name: string, url: string, search?: string}>
31 */
32 public static function providers(): array {
33 $gmail = array(
34 'name' => 'Gmail',
35 'url' => 'https://mail.google.com/mail/u/0/',
36 'search' => 'https://mail.google.com/mail/u/0/#search/%s',
37 );
38 $outlook = array(
39 'name' => 'Outlook',
40 'url' => 'https://outlook.live.com/mail/0/',
41 );
42 $gmx = array(
43 'name' => 'GMX',
44 'url' => 'https://www.gmx.net/',
45 );
46 $yahoo = array(
47 'name' => 'Yahoo Mail',
48 'url' => 'https://mail.yahoo.com/',
49 );
50 $icloud = array(
51 'name' => 'iCloud Mail',
52 'url' => 'https://www.icloud.com/mail',
53 );
54
55 $providers = array(
56 'gmail.com' => $gmail,
57 'googlemail.com' => $gmail,
58 'outlook.com' => $outlook,
59 'outlook.de' => $outlook,
60 'hotmail.com' => $outlook,
61 'hotmail.de' => $outlook,
62 'live.com' => $outlook,
63 'live.de' => $outlook,
64 'msn.com' => $outlook,
65 'gmx.de' => $gmx,
66 'gmx.net' => $gmx,
67 'gmx.at' => $gmx,
68 'gmx.ch' => $gmx,
69 'web.de' => array(
70 'name' => 'WEB.DE',
71 'url' => 'https://web.de/',
72 ),
73 't-online.de' => array(
74 'name' => 'T-Online',
75 'url' => 'https://email.t-online.de/',
76 ),
77 'yahoo.com' => $yahoo,
78 'yahoo.de' => $yahoo,
79 'ymail.com' => $yahoo,
80 'icloud.com' => $icloud,
81 'me.com' => $icloud,
82 'mac.com' => $icloud,
83 );
84
85 /**
86 * Webmail providers offered as "open your inbox" links after a
87 * double opt-in submission, keyed by lower-case mail domain.
88 *
89 * @since 5.8.0
90 *
91 * @param array $providers domain => array{name, url, search?}.
92 */
93 $filtered = apply_filters( 'f12_doi_webmail_providers', $providers );
94
95 return is_array( $filtered ) ? $filtered : $providers;
96 }
97
98 /**
99 * `[email protected]` → `a***@gmx.de`. The visitor sees which address
100 * they typed without the page echoing it in full.
101 */
102 public static function mask( string $email ): string {
103 $at = strrpos( $email, '@' );
104 if ( $at === false || $at === 0 ) {
105 return '';
106 }
107
108 return substr( $email, 0, 1 ) . '***' . substr( $email, $at );
109 }
110
111 /**
112 * The provider for an address, with a search link when the provider
113 * supports one and the sender is known.
114 *
115 * @return array{name: string, url: string}|null
116 */
117 public static function inboxFor( string $email, string $sender = '' ): ?array {
118 $at = strrpos( $email, '@' );
119 if ( $at === false ) {
120 return null;
121 }
122
123 $domain = strtolower( trim( substr( $email, $at + 1 ) ) );
124 $providers = self::providers();
125 if ( ! isset( $providers[ $domain ] ) || ! is_array( $providers[ $domain ] ) ) {
126 return null;
127 }
128
129 $provider = $providers[ $domain ];
130 $name = (string) ( $provider['name'] ?? '' );
131 $url = (string) ( $provider['url'] ?? '' );
132 if ( $name === '' || strpos( $url, 'https://' ) !== 0 ) {
133 return null;
134 }
135
136 $search = (string) ( $provider['search'] ?? '' );
137 if ( $search !== '' && is_email( $sender ) ) {
138 // in:anywhere — Gmail's plain search skips Spam, where these mails end up.
139 $url = sprintf( $search, rawurlencode( 'from:' . $sender . ' in:anywhere' ) );
140 }
141
142 return array(
143 'name' => $name,
144 'url' => $url,
145 );
146 }
147
148 /**
149 * The bare address out of a From value such as `Shop <[email protected]>`.
150 */
151 public static function senderAddress( string $from ): string {
152 if ( preg_match( '/<([^>]+)>/', $from, $m ) ) {
153 $from = $m[1];
154 }
155 $from = trim( $from );
156
157 return is_email( $from ) ? $from : '';
158 }
159
160 /**
161 * Everything the frontend script renders, translated here.
162 *
163 * @return array{masked: string, lines: string[], inbox: array{name: string, url: string, label: string}|null}
164 */
165 public static function build( string $email, string $sender = '', string $subject = '' ): array {
166 $masked = self::mask( $email );
167 $lines = array();
168
169 if ( $sender !== '' && $subject !== '' ) {
170 $lines[] = sprintf(
171 /* translators: 1: sender address, 2: mail subject */
172 __( 'Look for a mail from %1$s with the subject “%2$s”. If it is not in your inbox, check the spam folder.', 'double-opt-in' ),
173 $sender,
174 $subject
175 );
176 } else {
177 $lines[] = __( 'If it is not in your inbox, check the spam folder.', 'double-opt-in' );
178 }
179 $lines[] = __( 'Typo in the address? Just fill in the form again.', 'double-opt-in' );
180
181 $inbox = self::inboxFor( $email, $sender );
182 if ( $inbox !== null ) {
183 $inbox['label'] = sprintf(
184 /* translators: %s: webmail provider, e.g. Gmail */
185 __( 'Open %s', 'double-opt-in' ),
186 $inbox['name']
187 );
188 }
189
190 return array(
191 'masked' => $masked,
192 'lines' => $lines,
193 'inbox' => $inbox,
194 );
195 }
196
197 /**
198 * Hand the notice to extensions, then keep only what the script can
199 * render safely.
200 *
201 * The context names the opt-in so an add-on can act on it (addon-reminder
202 * offers "send it again"); the visitor's browser never sees the id or
203 * the confirmation hash — an add-on that needs to refer back to the
204 * opt-in signs its own token into the action's `data`.
205 *
206 * @param array{masked: string, lines: string[], inbox: array<string, string>|null} $notice From build().
207 * @param array{form_id: int, optin_id: int, integration: string} $context Who submitted what.
208 *
209 * @return array{masked: string, lines: string[], inbox: array<string, string>|null, actions: array<int, array<string, mixed>>}
210 */
211 public static function extend( array $notice, array $context ): array {
212 $notice['actions'] = array();
213
214 /**
215 * The confirmation hint after a double opt-in submission, before it
216 * goes to the browser.
217 *
218 * `actions` takes links (`type` link, `url` https) and buttons
219 * (`type` button); a button fires the DOM event
220 * `f12-doi-notice-action` with its `id` and `data` when clicked and
221 * can stay disabled for `wait` seconds.
222 *
223 * @since 5.8.0
224 *
225 * @param array $notice masked, lines, inbox, actions.
226 * @param array $context form_id, optin_id, integration — server side only.
227 */
228 $filtered = apply_filters( 'f12_doi_submit_notice_data', $notice, $context );
229 if ( ! is_array( $filtered ) ) {
230 return $notice;
231 }
232
233 $lines = array();
234 foreach ( (array) ( $filtered['lines'] ?? array() ) as $line ) {
235 if ( is_scalar( $line ) && (string) $line !== '' ) {
236 $lines[] = (string) $line;
237 }
238 }
239
240 return array(
241 'masked' => $notice['masked'],
242 'lines' => $lines,
243 'inbox' => $notice['inbox'],
244 'actions' => self::actions( $filtered['actions'] ?? array() ),
245 );
246 }
247
248 /**
249 * @param mixed $actions As returned by the filter.
250 *
251 * @return array<int, array<string, mixed>>
252 */
253 private static function actions( $actions ): array {
254 $clean = array();
255
256 foreach ( is_array( $actions ) ? $actions : array() as $action ) {
257 if ( ! is_array( $action ) ) {
258 continue;
259 }
260
261 $id = sanitize_key( (string) ( $action['id'] ?? '' ) );
262 $label = is_scalar( $action['label'] ?? null ) ? trim( (string) $action['label'] ) : '';
263 $type = ( $action['type'] ?? '' ) === 'link' ? 'link' : 'button';
264 if ( $id === '' || $label === '' ) {
265 continue;
266 }
267
268 $entry = array(
269 'id' => $id,
270 'type' => $type,
271 'label' => $label,
272 );
273
274 if ( $type === 'link' ) {
275 $url = (string) ( $action['url'] ?? '' );
276 if ( strpos( $url, 'https://' ) !== 0 ) {
277 continue;
278 }
279 $entry['url'] = $url;
280 } else {
281 $entry['wait'] = max( 0, min( 600, (int) ( $action['wait'] ?? 0 ) ) );
282 $entry['data'] = array();
283 foreach ( (array) ( $action['data'] ?? array() ) as $key => $value ) {
284 if ( is_scalar( $value ) ) {
285 $entry['data'][ sanitize_key( (string) $key ) ] = $value;
286 }
287 }
288 }
289
290 $clean[] = $entry;
291 }
292
293 return $clean;
294 }
295
296 /**
297 * The success message that replaces the form plugin's default "sent".
298 */
299 public static function message( string $masked ): string {
300 if ( $masked === '' ) {
301 return __( 'Almost done: please confirm your address. We have sent you a mail with a confirmation link.', 'double-opt-in' );
302 }
303
304 return sprintf(
305 /* translators: %s: masked email address, e.g. a***@gmx.de */
306 __( 'Almost done: please confirm your address. We have sent a confirmation link to %s.', 'double-opt-in' ),
307 $masked
308 );
309 }
310 }
311