Validate the guest e-mail before the booking is saved

An address like "asdf" used to be accepted: the field was type="text" with
only `required`, and there was no server-side check. createBooking() runs
long before sendEmail(), so the failure was silent rather than loud -
reproduced end to end on test:

  - the booking row WAS created, with a manage_token
  - PHPMailer's addAddress() threw, so nothing was ever sent
  - the Location header was already queued, so the guest was redirected to
    the normal "booking finished" page and saw success
  - Evelin is a CC on that same message, so the salon was not told either
  - the guest had no manage link, so they could not cancel

Fixes
- booking_process() rejects an empty or malformed address BEFORE any write,
  returning invalid_email / HTTP 400. Message added in all three languages,
  worded to say why it matters (the confirmation and the manage link go
  there). filter_var is equal-or-stricter than PHPMailer's own validator -
  checked against it on ten cases - so anything accepted here cannot throw
  later.
- The three public booking forms use type="email", so most typos never
  reach the server.
- Removed three debug echoes from User_model::sendEmail() that leaked $lang
  and Hungarian strings ("Üzenet elküldve", "Üzenetküldési hiba. Mailer
  Error: ...") into the guest-facing response.

Scope
- Public flow only. 508 existing bookings have an empty guest_email because
  admin-created block bookings legitimately have none; those go through
  Admin::booking_process(), which is untouched, and its form stays
  type="text".
- Not covered: a valid address whose SMTP delivery fails still leaves the
  booking created and the guest seeing success, logged only via
  log_message(). Different failure mode, needs a separate decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ubuntu
2026-08-15 09:55:33 +00:00
co-authored by Claude Opus 5
parent 7f51a795a5
commit 35115404be
8 changed files with 28 additions and 7 deletions
+18 -1
View File
@@ -403,7 +403,24 @@ class Pages extends CI_Controller {
'guest_confirm_code' => '1234', 'guest_confirm_code' => '1234',
'manage_token' => $manageToken, 'manage_token' => $manageToken,
); );
// Reject an unusable e-mail BEFORE anything is written. The booking is
// saved well before sendEmail() runs, so without this the row is created,
// PHPMailer's addAddress() throws, and the guest is redirected to the
// success page having received nothing - no confirmation and no manage
// link. Evelin is a CC on that same message, so the salon is not told
// either. filter_var is equal-or-stricter than PHPMailer's own check, so
// anything accepted here will not throw later.
//
// Public flow only: admin-created block bookings legitimately carry an
// empty guest_email and go through Admin::booking_process().
$guestEmailInput = isset($_POST['guest_email']) ? trim($_POST['guest_email']) : '';
if ($guestEmailInput === '' || !filter_var($guestEmailInput, FILTER_VALIDATE_EMAIL)) {
$this->_booking_error('invalid_email', 400);
return;
}
$bookingArray['guest_email'] = $guestEmailInput;
$bookingDate = $bookingArray['booking_date']; $bookingDate = $bookingArray['booking_date'];
$bookingTime = $bookingArray['booking_start_time']; $bookingTime = $bookingArray['booking_start_time'];
+4 -3
View File
@@ -132,7 +132,6 @@ class User_model extends CI_Model {
public function sendEmail($addressList, $lang, $subpage){ public function sendEmail($addressList, $lang, $subpage){
// Import PHPMailer classes into the global namespace // Import PHPMailer classes into the global namespace
// These must be at the top of your script, not inside a function // These must be at the top of your script, not inside a function
echo $lang;
if(!empty($addressList)){ if(!empty($addressList)){
foreach($addressList as $addressListItem){ foreach($addressList as $addressListItem){
// Instantiation and passing `true` enables exceptions // Instantiation and passing `true` enables exceptions
@@ -179,7 +178,6 @@ class User_model extends CI_Model {
//$mail->AltBody = strip_tags($answer_message); //$mail->AltBody = strip_tags($answer_message);
$mail->send(); $mail->send();
echo 'Üzenet elküldve';
if($subpage != ''){ if($subpage != ''){
header('location:'.base_url().$lang.'/booking-finished/'.$subpage); header('location:'.base_url().$lang.'/booking-finished/'.$subpage);
} }
@@ -187,7 +185,10 @@ class User_model extends CI_Model {
} catch (Exception $e) { } catch (Exception $e) {
//header('location:'.base_url().'manage-applicants/?email-sent=false&error='.$mail->ErrorInfo); //header('location:'.base_url().'manage-applicants/?email-sent=false&error='.$mail->ErrorInfo);
log_message('error', 'sendEmail FAILED: ' . $mail->ErrorInfo); log_message('error', 'sendEmail FAILED: ' . $mail->ErrorInfo);
echo "Üzenetküldési hiba. Mailer Error: {$mail->ErrorInfo}"; // Do NOT echo the mailer error. This runs after the booking is
// saved and after the redirect header is queued, so it only leaks
// Hungarian internals into a response the guest never reads.
// The log_message() above is the record.
} }
} }
@@ -8,6 +8,7 @@
'too_far' => 'Bookings can only be made up to 3 months in advance.', 'too_far' => 'Bookings can only be made up to 3 months in advance.',
'category_mismatch' => 'The selected staff member does not perform the chosen services. Please start again.', 'category_mismatch' => 'The selected staff member does not perform the chosen services. Please start again.',
'single_service_only' => 'Only one treatment can be booked at a time. Please select just one.', 'single_service_only' => 'Only one treatment can be booked at a time. Please select just one.',
'invalid_email' => 'That e-mail address does not look valid. Please check it - your confirmation and the link to manage your booking are sent there.',
'no_service' => 'No service was selected. Please choose at least one service.', 'no_service' => 'No service was selected. Please choose at least one service.',
'default' => 'Something went wrong with your booking. Please try again.', 'default' => 'Something went wrong with your booking. Please try again.',
); );
@@ -8,6 +8,7 @@
'too_far' => 'Foglalás legfeljebb 3 hónappal előre adható le.', 'too_far' => 'Foglalás legfeljebb 3 hónappal előre adható le.',
'category_mismatch' => 'A kiválasztott kolléga nem végzi a kiválasztott szolgáltatásokat. Kérjük, válasszon újra.', 'category_mismatch' => 'A kiválasztott kolléga nem végzi a kiválasztott szolgáltatásokat. Kérjük, válasszon újra.',
'single_service_only' => 'Egyszerre csak egy kezelés foglalható. Kérjük, csak egyet válasszon.', 'single_service_only' => 'Egyszerre csak egy kezelés foglalható. Kérjük, csak egyet válasszon.',
'invalid_email' => 'Az e-mail cím nem tűnik érvényesnek. Kérjük, ellenőrizze - a visszaigazolást és a foglalás kezelésére szolgáló linket erre a címre küldjük.',
'no_service' => 'Nem választott ki szolgáltatást. Kérjük, válasszon legalább egyet.', 'no_service' => 'Nem választott ki szolgáltatást. Kérjük, válasszon legalább egyet.',
'default' => 'Hiba történt a foglalás során. Kérjük, próbálja újra.', 'default' => 'Hiba történt a foglalás során. Kérjük, próbálja újra.',
); );
@@ -8,6 +8,7 @@
'too_far' => 'Du kan kun bestille time inntil 3 måneder frem i tid.', 'too_far' => 'Du kan kun bestille time inntil 3 måneder frem i tid.',
'category_mismatch' => 'Den valgte medarbeideren utfører ikke de valgte tjenestene. Vennligst velg på nytt.', 'category_mismatch' => 'Den valgte medarbeideren utfører ikke de valgte tjenestene. Vennligst velg på nytt.',
'single_service_only' => 'Du kan kun bestille én behandling om gangen. Vennligst velg kun én.', 'single_service_only' => 'Du kan kun bestille én behandling om gangen. Vennligst velg kun én.',
'invalid_email' => 'E-postadressen ser ikke ut til å være gyldig. Vennligst kontroller den - bekreftelsen og lenken for å endre bestillingen sendes dit.',
'no_service' => 'Du har ikke valgt noen tjeneste. Vennligst velg minst én tjeneste.', 'no_service' => 'Du har ikke valgt noen tjeneste. Vennligst velg minst én tjeneste.',
'default' => 'Noe gikk galt med bestillingen. Vennligst prøv igjen.', 'default' => 'Noe gikk galt med bestillingen. Vennligst prøv igjen.',
); );
@@ -177,7 +177,7 @@
</div> </div>
<div class="formRow"> <div class="formRow">
<label class="bookingEmailTitle">E-mail:</label> <label class="bookingEmailTitle">E-mail:</label>
<input type="text" class="bookingEmail" name="guest_email" value="" required> <input type="email" class="bookingEmail" name="guest_email" value="" required>
</div> </div>
<div class="formRow"> <div class="formRow">
<label class="bookingPhoneTitle">Phone:</label> <label class="bookingPhoneTitle">Phone:</label>
@@ -178,7 +178,7 @@
</div> </div>
<div class="formRow"> <div class="formRow">
<label class="bookingEmailTitle">E-mail:</label> <label class="bookingEmailTitle">E-mail:</label>
<input type="text" class="bookingEmail" name="guest_email" value="" required> <input type="email" class="bookingEmail" name="guest_email" value="" required>
</div> </div>
<div class="formRow"> <div class="formRow">
<label class="bookingPhoneTitle">Telefon:</label> <label class="bookingPhoneTitle">Telefon:</label>
@@ -178,7 +178,7 @@ if (is_array($services) && count($services) > 0) {
</div> </div>
<div class="formRow"> <div class="formRow">
<label class="bookingEmailTitle">E-mail:</label> <label class="bookingEmailTitle">E-mail:</label>
<input type="text" class="bookingEmail" name="guest_email" value="" required> <input type="email" class="bookingEmail" name="guest_email" value="" required>
</div> </div>
<div class="formRow"> <div class="formRow">
<label class="bookingPhoneTitle">Telefon:</label> <label class="bookingPhoneTitle">Telefon:</label>