1313 *
1414 * @author Raspgot <contact@raspgot.fr>
1515 * @link https://github.com/raspgot/AjaxForm-PHPMailer-reCAPTCHA
16- * @version 1.7.2
16+ * @version 1.7.3
1717 * @see https://github.com/PHPMailer/PHPMailer
1818 * @see https://developers.google.com/recaptcha/docs/v3
1919 */
3333use PHPMailer \PHPMailer \Exception ;
3434
3535// Load PHPMailer manually (no-Composer project)
36- require_once ' PHPMailer/PHPMailer.php ' ;
37- require_once ' PHPMailer/SMTP.php ' ;
38- require_once ' PHPMailer/Exception.php ' ;
36+ require_once __DIR__ . ' / PHPMailer/PHPMailer.php ' ;
37+ require_once __DIR__ . ' / PHPMailer/SMTP.php ' ;
38+ require_once __DIR__ . ' / PHPMailer/Exception.php ' ;
3939
4040// Configuration constants (must be customized)
4141const SECRET_KEY = '' ; // Your reCAPTCHA secret key
4848const FROM_NAME = 'Raspgot ' ; // Name displayed as sender
4949const EMAIL_SUBJECT_DEFAULT = '[GitHub] New message received ' ; // Default subject if none provided
5050const EMAIL_SUBJECT_AUTOREPLY = 'We have received your message ' ; // Subject of auto-reply email
51- const MAX_ATTEMPTS = 55 ; // Maximum allowed submissions per session
52- const RATE_LIMIT_DURATION = 3600 ; // 30 minutes
51+ const MAX_ATTEMPTS = 5 ; // Maximum allowed submissions per session
52+ const RATE_LIMIT_DURATION = 3600 ; // 1 hour rate limit (in seconds)
5353
5454// User-facing response messages
5555const RESPONSES = [
127127 $ mail ->addReplyTo ($ email , $ name );
128128 $ mail ->Subject = $ subject ?: EMAIL_SUBJECT_DEFAULT ;
129129 $ mail ->Body = $ emailBody ;
130- $ mail ->AltBody = strip_tags ($ emailBody );
130+ // First convert <br> tags into \n (strip_tags removes <br> without adding line breaks)
131+ $ alt = preg_replace ('/<br\s*\/?>/i ' , "\n" , $ emailBody );
132+ $ alt = trim (strip_tags ($ alt ));
133+ $ mail ->AltBody = $ alt ;
131134 $ mail ->send ();
132135
133136 // Send confirmation auto-reply to user
137140 $ autoReply ->Subject = EMAIL_SUBJECT_AUTOREPLY . ' — ' . $ subject ;
138141 $ autoReply ->Body = '
139142 <p>Hello ' . htmlspecialchars ($ name ) . ',</p>
140- <p>Thank you for reaching out. Here is a copy of your message:</p>
143+ <p>Thank you for reaching out, here is a copy of your message :</p>
141144 <hr> ' . $ emailBody ;
142- $ autoReply ->AltBody = strip_tags ( $ emailBody ) ;
145+ $ autoReply ->AltBody = $ alt ;
143146 $ autoReply ->send ();
144147
145148 respond (true , RESPONSES ['success ' ]);
148151}
149152
150153/**
151- * Verifies reCAPTCHA token with Google API and checks score.
154+ * Verifies reCAPTCHA token with Google API and checks score, action & hostname
152155 *
153- * @param string $token reCAPTCHA token submitted by the form.
156+ * @param string $token reCAPTCHA token submitted by the form
154157 * @return void
155158 */
156159function validateRecaptcha (string $ token ): void
@@ -161,6 +164,7 @@ function validateRecaptcha(string $token): void
161164 $ postFields = http_build_query ([
162165 'secret ' => SECRET_KEY ,
163166 'response ' => $ token ,
167+ 'remoteip ' => $ _SERVER ['REMOTE_ADDR ' ] ?? null ,
164168 ]);
165169
166170 curl_setopt_array ($ ch , [
@@ -170,34 +174,47 @@ function validateRecaptcha(string $token): void
170174 CURLOPT_TIMEOUT => 10 ,
171175 ]);
172176
173- $ response = curl_exec ($ ch );
174- $ httpCode = curl_getinfo ($ ch , CURLINFO_HTTP_CODE );
177+ $ response = curl_exec ($ ch );
178+ $ httpCode = curl_getinfo ($ ch , CURLINFO_HTTP_CODE );
175179 $ curlError = curl_error ($ ch );
176180 curl_close ($ ch );
177181
178- // Handle HTTP or cURL error
182+ // If cURL execution failed (network error, timeout, etc.)
179183 if ($ response === false ) {
180184 respond (false , '❌ reCAPTCHA request failed : ' . ($ curlError ?: 'Unknown cURL error. ' ));
181185 }
182186
187+ // If Google did not respond with a 200 OK
183188 if ($ httpCode !== 200 ) {
184189 respond (false , '❌ reCAPTCHA HTTP error : ' . $ httpCode );
185190 }
186191
187- // Parse JSON response
188192 $ data = json_decode ($ response , true );
189193
194+ // If response cannot be decoded as valid JSON
190195 if (json_last_error () !== JSON_ERROR_NONE || !is_array ($ data )) {
191196 respond (false , '❌ Invalid JSON response from reCAPTCHA. ' );
192197 }
193198
194- // Check success flag
199+ // If Google says " success" flag is false (invalid token, wrong secret, expired, etc.)
195200 if (empty ($ data ['success ' ])) {
196201 $ errors = isset ($ data ['error-codes ' ]) ? implode (', ' , $ data ['error-codes ' ]) : 'Unknown error. ' ;
197202 respond (false , '❌ reCAPTCHA verification failed : ' . $ errors );
198203 }
199204
200- // Check score (threshold configurable if needed)
205+ // If the "action" does not match the one expected (mitigates token reuse across forms)
206+ $ expectedAction = 'submit ' ;
207+ if (($ data ['action ' ] ?? '' ) !== $ expectedAction ) {
208+ respond (false , '❌ reCAPTCHA action mismatch. ' );
209+ }
210+
211+ // If the hostname returned by Google does not match our server's hostname (prevents token theft)
212+ $ expectedHost = $ _SERVER ['SERVER_NAME ' ] ?? '' ;
213+ if (!empty ($ expectedHost ) && ($ data ['hostname ' ] ?? '' ) !== $ expectedHost ) {
214+ respond (false , '❌ reCAPTCHA hostname mismatch. ' );
215+ }
216+
217+ // If the score is below threshold (Google thinks the request looks like a bot)
201218 $ score = $ data ['score ' ] ?? 1.0 ;
202219 if ($ score < 0.6 ) {
203220 respond (false , '❌ Low reCAPTCHA score ( ' . $ score . '). You might be a robot. ' );
@@ -220,13 +237,13 @@ function sanitize(string $data): string
220237}
221238
222239/**
223- * Sends a JSON response and terminates the script.
240+ * Sends a JSON response and terminates the script
224241 *
225- * @param bool $success Whether the operation was successful.
226- * @param string $message Message to be displayed to the user.
227- * @param string|null $field Optional field name to mark as invalid.
242+ * @param bool $success Whether the operation was successful
243+ * @param string $message Message to be displayed to the user
244+ * @param string|null $field Optional field name to mark as invalid
228245 *
229- * @return never This function does not return; it ends execution with exit().
246+ * @return never This function does not return; it ends execution with exit()
230247 */
231248function respond (bool $ success , string $ message , ?string $ field = null ): never
232249{
0 commit comments