|
12 | 12 | use Psr\Log\LoggerInterface; |
13 | 13 | use Throwable; |
14 | 14 |
|
| 15 | +use function file_get_contents; |
15 | 16 | use function htmlspecialchars; |
16 | 17 | use function http_build_query; |
17 | 18 | use function in_array; |
18 | 19 | use function is_array; |
19 | 20 | use function is_scalar; |
20 | 21 | use function is_string; |
| 22 | +use function json_decode; |
21 | 23 | use function nl2br; |
22 | 24 | use function sprintf; |
| 25 | +use function stream_context_create; |
23 | 26 | use function trim; |
24 | 27 |
|
25 | 28 | use const ENT_QUOTES; |
|
35 | 38 | * query: string, |
36 | 39 | * } |
37 | 40 | */ |
38 | | -final class ContactService |
| 41 | +class ContactService |
39 | 42 | { |
| 43 | + private const string TURNSTILE_ACTION = 'contact'; |
| 44 | + |
40 | 45 | private const array REQUIRED_FIELDS = [ |
41 | 46 | 'name' => 'Name', |
42 | 47 | 'email' => 'Work email', |
43 | 48 | 'message' => 'Message', |
44 | 49 | ]; |
45 | 50 |
|
46 | | - private const array RESERVED_FIELDS = ['topic', 'name', 'email', 'company', 'stack', 'message', 'contact']; |
| 51 | + private const array RESERVED_FIELDS = [ |
| 52 | + 'topic', |
| 53 | + 'name', |
| 54 | + 'email', |
| 55 | + 'company', |
| 56 | + 'stack', |
| 57 | + 'message', |
| 58 | + 'contact', |
| 59 | + 'cf-turnstile-response', |
| 60 | + ]; |
47 | 61 |
|
| 62 | + /** |
| 63 | + * @param array{site_key: string, secret_key: string, hostname: string} $turnstile |
| 64 | + */ |
48 | 65 | #[Inject( |
49 | 66 | 'dot-mail.service.contact', |
50 | 67 | 'dot-log.default_logger', |
| 68 | + 'config.turnstile', |
51 | 69 | )] |
52 | 70 | public function __construct( |
53 | 71 | private readonly MailServiceInterface $mailService, |
54 | 72 | private readonly LoggerInterface $logger, |
| 73 | + private readonly array $turnstile = ['site_key' => '', 'secret_key' => '', 'hostname' => ''], |
55 | 74 | ) { |
56 | 75 | } |
57 | 76 |
|
| 77 | + public function turnstileSiteKey(): string |
| 78 | + { |
| 79 | + return $this->turnstile['secret_key'] !== '' ? $this->turnstile['site_key'] : ''; |
| 80 | + } |
| 81 | + |
| 82 | + public function verifyTurnstile(string $token, ?string $remoteIp): bool |
| 83 | + { |
| 84 | + if ($this->turnstileSiteKey() === '') { |
| 85 | + return true; |
| 86 | + } |
| 87 | + |
| 88 | + if ($token === '') { |
| 89 | + return false; |
| 90 | + } |
| 91 | + |
| 92 | + $data = [ |
| 93 | + 'secret' => $this->turnstile['secret_key'], |
| 94 | + 'response' => $token, |
| 95 | + ]; |
| 96 | + if ($remoteIp) { |
| 97 | + $data['remoteip'] = $remoteIp; |
| 98 | + } |
| 99 | + |
| 100 | + $response = $this->siteverify($data); |
| 101 | + if ($response === null) { |
| 102 | + return false; |
| 103 | + } |
| 104 | + |
| 105 | + $validation = json_decode($response, true); |
| 106 | + if (! is_array($validation) || ($validation['success'] ?? false) !== true) { |
| 107 | + return false; |
| 108 | + } |
| 109 | + |
| 110 | + if (($validation['metadata']['result_with_testing_key'] ?? false) === true) { |
| 111 | + return true; |
| 112 | + } |
| 113 | + |
| 114 | + $hostname = $this->turnstile['hostname']; |
| 115 | + |
| 116 | + return ($validation['action'] ?? null) === self::TURNSTILE_ACTION |
| 117 | + && ($hostname === '' || ($validation['hostname'] ?? null) === $hostname); |
| 118 | + } |
| 119 | + |
58 | 120 | /** |
59 | 121 | * @param array<array-key, mixed> $source |
60 | 122 | * @return array<string, string> |
@@ -142,6 +204,26 @@ public function send(array $data): bool |
142 | 204 | return true; |
143 | 205 | } |
144 | 206 |
|
| 207 | + /** |
| 208 | + * @param array<string, string> $data |
| 209 | + */ |
| 210 | + protected function siteverify(array $data): ?string |
| 211 | + { |
| 212 | + $options = [ |
| 213 | + 'http' => [ |
| 214 | + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", |
| 215 | + 'method' => 'POST', |
| 216 | + 'content' => http_build_query($data), |
| 217 | + 'timeout' => 5, |
| 218 | + ], |
| 219 | + ]; |
| 220 | + |
| 221 | + $context = stream_context_create($options); |
| 222 | + $response = @file_get_contents('https://challenges.cloudflare.com/turnstile/v0/siteverify', false, $context); |
| 223 | + |
| 224 | + return $response === false ? null : $response; |
| 225 | + } |
| 226 | + |
145 | 227 | /** |
146 | 228 | * @param array<array-key, mixed> $raw |
147 | 229 | */ |
|
0 commit comments