Skip to content

Commit 4ae22cb

Browse files
author
gauthier witkowski
committed
Improve docblocks & typing
1 parent 2f4e8cb commit 4ae22cb

File tree

1 file changed

+63
-34
lines changed

1 file changed

+63
-34
lines changed

AjaxForm.php

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<?php
22

33
/**
4-
* Secure Contact Form using PHPMailer & reCAPTCHA v3 with autoreply
4+
* This script processes AJAX form submissions, validates input, applies
5+
* anti-spam measures (honeypot, rate limiting, DNS & reCAPTCHA checks), and
6+
* sends both an admin notification and an optional autoreply to the user
57
*
6-
* @author Raspgot <contact@raspgot.fr>
7-
* @link https://github.com/raspgot/AjaxForm-PHPMailer-reCAPTCHA
8-
* @version 1.7.3
9-
* @see https://github.com/PHPMailer/PHPMailer
10-
* @see https://developers.google.com/recaptcha/docs/v3
8+
* @author Raspgot <contact@raspgot.fr>
9+
* @link https://github.com/raspgot/AjaxForm-PHPMailer-reCAPTCHA
10+
* @version 1.7.4
11+
* @see https://github.com/PHPMailer/PHPMailer
12+
* @see https://developers.google.com/recaptcha/docs/v3
1113
*/
1214

1315
declare(strict_types=1);
@@ -101,7 +103,7 @@
101103
// Verify reCAPTCHA token authenticity and score
102104
validateRecaptcha($token);
103105

104-
// Build email body from template
106+
// Build email body (HTML) from template
105107
$emailBody = renderEmail([
106108
'subject' => $subject,
107109
'date' => $date->format('Y-m-d H:i:s'),
@@ -112,39 +114,50 @@
112114
]);
113115

114116
try {
117+
// Build minimal plain text alternative part from HTML
118+
$altText = buildAltBody($emailBody);
119+
115120
// Send notification email to site owner
116-
$mail = new PHPMailer(true);
117-
configureMailer($mail);
121+
$mail = configureMailer(new PHPMailer(true));
118122
$mail->addAddress(SMTP_USERNAME, 'Admin');
119123
$mail->addReplyTo($email, $name);
120124
$mail->Subject = $subject ?: EMAIL_SUBJECT_DEFAULT;
121125
$mail->Body = $emailBody;
122-
$alt = preg_replace('/<br\s*\/?>/i', "\n", $emailBody); // First convert <br> tags into \n (strip_tags removes <br> without adding line breaks)
123-
$alt = trim(strip_tags($alt));
124-
$mail->AltBody = $alt;
126+
$mail->AltBody = $altText;
125127
$mail->send();
126128

127129
// Send autoreply confirmation to user
128-
$autoReply = new PHPMailer(true);
129-
configureMailer($autoReply);
130+
$autoReply = configureMailer(new PHPMailer(true));
130131
$autoReply->addAddress($email, $name);
131132
$autoReply->Subject = EMAIL_SUBJECT_AUTOREPLY . '' . $subject;
132-
$autoReply->Body = '
133-
<p>Hello ' . htmlspecialchars($name) . ',</p>
134-
<p>Thank you for reaching out, here is a copy of your message :</p>
135-
<hr>' . $emailBody;
136-
$autoReply->AltBody = $alt;
133+
$autoReply->Body = '<p>Hello ' . htmlspecialchars($name) . ',</p>' .
134+
'<p>Thank you for reaching out. Here is a copy of your message:</p>' .
135+
'<hr>' . $emailBody;
136+
$autoReply->AltBody = $altText;
137137
$autoReply->send();
138138

139139
respond(true, RESPONSES['success']);
140140
} catch (Exception $e) {
141141
respond(false, '❌ Mail error: ' . $e->getMessage(), 'email');
142142
}
143143

144+
/**
145+
* Create a plain-text alternative body from an HTML email fragment
146+
*
147+
* @param string $html HTML email body
148+
* @return string Plain text version
149+
*/
150+
function buildAltBody(string $html): string
151+
{
152+
$text = preg_replace('/<br\s*\/??>/i', "\n", $html) ?? $html;
153+
$text = strip_tags($text);
154+
return html_entity_decode($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
155+
}
156+
144157
/**
145158
* Verify reCAPTCHA token with Google API and validate score, action, and hostname
146159
*
147-
* @param string $token reCAPTCHA token submitted by the form
160+
* @param string $token The reCAPTCHA token received from the frontend
148161
* @return void
149162
*/
150163
function validateRecaptcha(string $token): void
@@ -215,24 +228,27 @@ function validateRecaptcha(string $token): void
215228
/**
216229
* Sanitize user input to prevent XSS and header injection
217230
*
218-
* @param string $data Raw input string
219-
* @return string Cleaned string
231+
* @param string $data Raw user-supplied input
232+
* @return string Sanitized value
220233
*/
221234
function sanitize(string $data): string
222235
{
223236
// Remove control characters and null bytes
224-
$data = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', '', $data);
237+
$filtered = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', '', $data);
238+
if ($filtered === null) {
239+
$filtered = $data; // Fallback to original if regex engine fails
240+
}
225241

226242
// Escape HTML entities (UTF-8 safe)
227-
return trim(htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true));
243+
return trim(htmlspecialchars($filtered, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true));
228244
}
229245

230246
/**
231247
* Send JSON response and terminate execution
232248
*
233249
* @param bool $success Success flag
234250
* @param string $message Message to display
235-
* @param string|null $field Optional field to highlight as invalid
251+
* @param string|null $field Optional field to highlight as invalid
236252
* @return never
237253
*/
238254
function respond(bool $success, string $message, ?string $field = null): never
@@ -246,10 +262,22 @@ function respond(bool $success, string $message, ?string $field = null): never
246262
}
247263

248264
/**
249-
* Render the email body from template file
265+
* Render the HTML email body using the external template file
266+
*
267+
* @param array{
268+
* subject: string,
269+
* date: string,
270+
* name: string,
271+
* email: string,
272+
* message: string,
273+
* ip: string
274+
* } $data Strictly typed template variables
250275
*
251-
* @param array $data Template variables
252-
* @return string HTML email content
276+
* Extraction uses EXTR_SKIP to avoid overwriting existing variables inside the closure scope
277+
* Output buffering captures the template output as a string
278+
*
279+
* @return string Fully rendered HTML fragment
280+
* @throws RuntimeException If the template file cannot be found
253281
*/
254282
function renderEmail(array $data): string
255283
{
@@ -269,12 +297,12 @@ function renderEmail(array $data): string
269297
}
270298

271299
/**
272-
* Configures a PHPMailer instance with SMTP settings
300+
* Configure a PHPMailer instance with project SMTP defaults
273301
*
274-
* @param PHPMailer $mailer Instance to configure
275-
* @return void
302+
* @param PHPMailer $mailer The PHPMailer instance to configure
303+
* @return PHPMailer Configured PHPMailer instance
276304
*/
277-
function configureMailer(PHPMailer $mailer): void
305+
function configureMailer(PHPMailer $mailer): PHPMailer
278306
{
279307
$mailer->isSMTP();
280308
$mailer->Host = SMTP_HOST;
@@ -287,12 +315,13 @@ function configureMailer(PHPMailer $mailer): void
287315
$mailer->Sender = SMTP_USERNAME;
288316
$mailer->isHTML(true);
289317
$mailer->CharSet = 'UTF-8';
318+
return $mailer;
290319
}
291320

292321
/**
293-
* Enforce session-based rate limiting
322+
* Enforce a simple session-based rate limit
294323
*
295-
* @param int $max Max submissions allowed
324+
* @param int $max Max submissions allowed
296325
* @param int $window Time window in seconds
297326
* @return void
298327
*/

0 commit comments

Comments
 (0)