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>
549 lines
22 KiB
PHP
Executable File
549 lines
22 KiB
PHP
Executable File
<div class="bookingContainer">
|
|
<div class="bookingTitle">Foglalási folyamat</div>
|
|
<ul class="steps-indicator steps-3">
|
|
<li>
|
|
<div class="stepTitleContainer">
|
|
<div class="stepLine"></div>
|
|
<div class="stepCircle activeStep" id="stepCircle1"></div>
|
|
<div class="stepTitle">LÉPÉS 1</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="stepTitleContainer">
|
|
<div class="stepLine"></div>
|
|
<div class="stepCircle" id="stepCircle2"></div>
|
|
<div class="stepTitle">LÉPÉS 2</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="stepTitleContainer">
|
|
<div class="stepCircle" id="stepCircle3"></div>
|
|
<div class="stepTitle">LÉPÉS 3</div>
|
|
</div>
|
|
<li>
|
|
</ul>
|
|
<div class="totalPanelContainer">
|
|
<div id="totalPanel">
|
|
<div class="serviceBookingDateContainer">
|
|
<div id="serviceBookingDate"></div>
|
|
<div id="serviceBookingTime"></div>
|
|
</div>
|
|
<div class="serviceBookingWorkerContainer">
|
|
</div>
|
|
<div class="serviceContent">
|
|
</div>
|
|
<div class="totalSelectedTime"></div>
|
|
<div class="totalPrice"></div>
|
|
</div>
|
|
<div id="totalPanelBtnContainer">
|
|
<div id="totalPanelBtn"><i class="fas fa-caret-right"></i></div>
|
|
</div>
|
|
|
|
</div>
|
|
<form method="post" action="<?php echo SITEURL;?>booking-process">
|
|
<div class="BookingStepContainer" id="bookingStep1">
|
|
|
|
<div class="totalTime"><input type="time" id="servicelength" name="servicelength" value="00:00"></div>
|
|
|
|
<div class="bookingTable">
|
|
<div class="panel panel-primary">
|
|
<div class="panel-heading">Szolgáltatások</div>
|
|
</div>
|
|
</div>
|
|
<div class="serviceTable">
|
|
<div class="serviceHeader">
|
|
<div class="serviceCol serviceNameCol">Szolgáltatás neve</div>
|
|
<div class="serviceCol"></div>
|
|
<div class="serviceCol servicePriceCol">Ára</div>
|
|
<div class="serviceCol serviceInfoCol"></div>
|
|
<div class="serviceCol serviceCheckCol"></div>
|
|
</div>
|
|
<?php
|
|
function canon_cat($s) {
|
|
$s = html_entity_decode((string)$s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$s = strip_tags($s); // removes "<p class=" junk etc.
|
|
return trim($s); // removes trailing/leading spaces
|
|
}
|
|
|
|
if (is_array($services) && count($services) > 0) {
|
|
|
|
// Build groups in first-seen order
|
|
$groups = []; // label => [items]
|
|
$order = []; // labels in order of first appearance
|
|
|
|
foreach ($services as $serviceItem) {
|
|
$label = canon_cat($serviceItem->service_category ?? '');
|
|
if ($label === '') $label = 'Other';
|
|
|
|
if (!isset($groups[$label])) {
|
|
$groups[$label] = [];
|
|
$order[] = $label;
|
|
}
|
|
$groups[$label][] = $serviceItem;
|
|
}
|
|
|
|
// Render
|
|
foreach ($order as $label) {
|
|
?>
|
|
<div class="serviceRow">
|
|
<div class="serviceCol" style="width: 100%">
|
|
<h4><?php echo $label; ?></h4>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
|
|
foreach ($groups[$label] as $serviceItem) {
|
|
?>
|
|
<div class="serviceRow">
|
|
<div class="serviceCol serviceNameCol" id="serviceName_<?php echo $serviceItem->service_id; ?>" data-service-name="<?php echo htmlspecialchars($serviceItem->service_name, ENT_QUOTES, 'UTF-8'); ?>">
|
|
<?php echo vertical_service_label(isset($vertical) ? $vertical : NULL, $serviceItem); ?>
|
|
</div>
|
|
|
|
<div class="serviceCol">
|
|
<input type="time" class="serviceTime" id="time_<?php echo $serviceItem->service_id; ?>" value="<?php echo $serviceItem->service_time; ?>"/>
|
|
</div>
|
|
|
|
<div class="serviceCol servicePriceCol" id="servicePrice_<?php echo $serviceItem->service_id; ?>">
|
|
<?php echo $serviceItem->service_price; ?>
|
|
</div>
|
|
|
|
<div class="serviceCol serviceInfoCol">
|
|
<div class="serviceInfoIcon">
|
|
<i class="fas fa-info-circle"></i>
|
|
<div class="serviceInfoLabel"><?php echo $serviceItem->service_description; ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="serviceCol serviceCheckCol">
|
|
<input type="hidden" name="service_<?php echo $serviceItem->service_id; ?>" value="0"/>
|
|
<input type="checkbox"
|
|
class="service <?php echo $serviceItem->serv_cat_slug; ?>"
|
|
id="<?php echo $serviceItem->service_id; ?>"
|
|
name="service_<?php echo $serviceItem->service_id; ?>"
|
|
value="1"/>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="bookingButtonContainer">
|
|
<input type="button" class="rightButton bookingButton" onclick="goToStep(2);" value="Következő"/>
|
|
</div>
|
|
</div>
|
|
<div class="BookingStepContainer" id="bookingStep2">
|
|
<input type="hidden" name="worker_id" id="worker_id" value="">
|
|
<input type="hidden" name="subpage" id="subpage" value="<?php echo $subpage;?>">
|
|
<input type="hidden" name="lang" id="lang" value="<?php echo $selectedLang;?>">
|
|
<div class="bookingHeader">Válassz dolgozót</div>
|
|
<div class="workerTable" id="workerTable">
|
|
|
|
</div>
|
|
<div class="bookingButtonContainer">
|
|
<input type="button" class="leftButton bookingButton" onclick="goToStep(1);" value="Vissza"/>
|
|
<input type="button" class="rightButton bookingButton" onclick="goToStep(3);" value="Következő"/>
|
|
</div>
|
|
</div>
|
|
<div class="BookingStepContainer" id="bookingStep3">
|
|
<div class="bookingCalendar">
|
|
<div id="datepicker"></div>
|
|
</div>
|
|
<div class="bookingResultsWrapper">
|
|
<div class="availableTimesContainerTitle"></div>
|
|
<div class="availableTimesContainer" style="padding:5px;">
|
|
<input type="hidden" name="booking_date" id="booking_date" value="<?php echo date('Y-m-d');?>">
|
|
<input type="hidden" name="booking_start_time" id="booking_time" value="">
|
|
|
|
<div class="availableTimes">
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bookingButtonContainer">
|
|
<input type="button" class="leftButton bookingButton" onclick="goToStep(2);" value="Vissza"/>
|
|
<input type="button" class="rightButton bookingButton" id="isTimeSelectedBtn" onclick="goToStep(4);" value="Tovább" disabled/>
|
|
|
|
</div>
|
|
</div>
|
|
<div class="BookingStepContainer" id="bookingStep4">
|
|
|
|
<div class="formRow">
|
|
<label class="bookingEmailTitle">Név:</label>
|
|
<input type="text" class="bookingName" name="guest_name" value="" required>
|
|
</div>
|
|
<div class="formRow">
|
|
<label class="bookingEmailTitle">E-mail:</label>
|
|
<input type="email" class="bookingEmail" name="guest_email" value="" required>
|
|
</div>
|
|
<div class="formRow">
|
|
<label class="bookingPhoneTitle">Telefon:</label>
|
|
<input type="text" class="bookingPhone" name="guest_phone" value="" required>
|
|
</div>
|
|
|
|
<div class="bookingButtonContainer">
|
|
<input type="button" class="leftButton bookingButton" onclick="goToStep(3);" value="Vissza"/>
|
|
<input type="hidden" name="subpage" value="<?php echo $subpage;?>">
|
|
<input type="hidden" name="sendBooking" value="1">
|
|
<input type="submit" id="sendBooking" class="rightButton bookingSubmit" for="bookingStepForm" name="sendBooking" value="Elküld"/>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
$(document).ready(function(){
|
|
|
|
// Prevent double submission. The booking POST stays open for several
|
|
// seconds (Google Calendar + confirmation e-mail), so an impatient
|
|
// second tap used to hit the server as a separate booking attempt.
|
|
var bookingSubmitInProgress = false;
|
|
$('#sendBooking').closest('form').on('submit', function(e){
|
|
if(bookingSubmitInProgress){
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
bookingSubmitInProgress = true;
|
|
$('#sendBooking').prop('disabled', true).val('Küldés...');
|
|
});
|
|
|
|
$('#booking_time').change(function(){
|
|
if($('#booking_time').val() != ''){
|
|
$('#isTimeSelectedBtn').prop('disabled', false);
|
|
}
|
|
});
|
|
|
|
$('#totalPanelBtn').click(function(){
|
|
if($('#totalPanel').is(':visible')){
|
|
$('#totalPanelBtn').html('<i class="fas fa-caret-left"></i>');
|
|
$('#totalPanel').hide();
|
|
$('#totalPanelBtn').css('border-top-left-radius','5px');
|
|
$('#totalPanelBtn').css('border-bottom-left-radius','5px');
|
|
|
|
}
|
|
else{
|
|
$('#totalPanelBtn').html('<i class="fas fa-caret-right"></i>');
|
|
$('#totalPanel').show();
|
|
$('#totalPanelBtn').css('border-top-left-radius','0px');
|
|
$('#totalPanelBtn').css('border-bottom-left-radius','0px');
|
|
}
|
|
});
|
|
|
|
<?php if ( ! empty($vertical['single_service_booking'])): ?>
|
|
/* Single-service verticals (massage): a guest books one treatment at a
|
|
time, so selecting one clears and disables the rest. Clicking the
|
|
selected one again releases everything - the escape hatch for a
|
|
mis-click. Bound BEFORE the totals handler below, so that handler
|
|
always sees the corrected state. */
|
|
$('.service').click(function(){
|
|
if($(this).is(':checked')){
|
|
$('.service').not(this)
|
|
.prop('checked', false)
|
|
.prop('disabled', true)
|
|
.closest('.serviceRow').addClass('serviceRowDisabled');
|
|
}
|
|
else{
|
|
$('.service').prop('disabled', false);
|
|
$('.serviceRow').removeClass('serviceRowDisabled');
|
|
}
|
|
});
|
|
<?php endif; ?>
|
|
|
|
$('.service').click(function(){
|
|
var totalPrice = 0;
|
|
var selectedServices = '<div class="selectedServiceTable">';
|
|
selectedServices += '<div class="totalServiceRow serviceHeader"><div class="selectedServiceCol totalSelectedServiceName">Service name</div><div class="selectedServiceCol totalSelectedServicePrice">Price</div><div class="selectedServiceCol serviceInfoCol"></div><div class="selectedServiceCol serviceCheckCol"></div></div>';
|
|
|
|
var classIsChecked = 0;
|
|
$('#servicelength').val('00:00');
|
|
$('.service').each(function(i, obj) {
|
|
if($(this).is(':checked')){
|
|
classIsChecked = 1;
|
|
var selected_id = $(this).attr('id');
|
|
selectedPriceElement = document.getElementById('servicePrice_'+selected_id).innerHTML;
|
|
totalPrice = totalPrice + parseInt(selectedPriceElement);
|
|
var nameNode = document.getElementById('serviceName_'+selected_id);
|
|
// Prefer the full name: the visible label may have had its
|
|
// category prefix stripped for readability.
|
|
selectedNameElement = nameNode.getAttribute('data-service-name') || nameNode.innerHTML;
|
|
selectedServices = selectedServices + '<div class="totalServiceRow"><div class="selectedServiceCol totalSelectedServiceName">'+selectedNameElement+'</div><div class="selectedServiceCol totalSelectedServicePrice">'+selectedPriceElement+' kr</div></div>';
|
|
|
|
var selectedTotalTime = $('#servicelength').val();
|
|
const selectedServiceDatetime = new Date('<?php echo date('Y-m-d');?> ' + $('#time_'+selected_id).val());
|
|
|
|
var subTotalTimeHours = moment.utc(selectedTotalTime,'HH:mm').add(selectedServiceDatetime.getHours(),'hour').format('HH:mm');
|
|
var subTotalTimeMinutes = moment.utc(subTotalTimeHours,'HH:mm').add(selectedServiceDatetime.getMinutes(),'minutes').format('HH:mm');
|
|
//console.log(subTotalTimeMinutes);
|
|
$('#servicelength').val(subTotalTimeMinutes);
|
|
}
|
|
});
|
|
|
|
selectedServices +='</div></div></div>';
|
|
$('.serviceContent').html(selectedServices);
|
|
$('.totalPrice').html('<span class="totalTitle">Total:</span> <span>' + totalPrice + '</span><span class="totalTitle"> kr</span>');
|
|
$('.totalSelectedTime').html('<span class="totalTitle">Total time:</span> <span>' + $('#servicelength').val() + '</span>');
|
|
|
|
if(classIsChecked){
|
|
$('#totalPanelBtn').show();
|
|
$('#totalPanel').show();
|
|
}
|
|
else{
|
|
$('#totalPanelBtn').hide();
|
|
$('#totalPanel').hide();
|
|
}
|
|
});
|
|
|
|
$(".workers").each(function() {
|
|
var worker = $(this).attr('id');
|
|
worker_id = worker.split("_");
|
|
setWorker(worker_id[1])
|
|
});
|
|
|
|
|
|
$('.service').click(function(){
|
|
if($('.service').is(':checked')){
|
|
var selectedClasses = $(this).attr('class');
|
|
var selectedclassesArray = selectedClasses.split(" ");
|
|
|
|
$('.service').each(function(i, obj) {
|
|
var actualElement = $(this).attr('class');
|
|
var actualElementArray = actualElement.split(" ");
|
|
if(selectedclassesArray[1] !== actualElementArray[1]){
|
|
$(this).attr("disabled", true);
|
|
}
|
|
});
|
|
getAvailableWorkersByServiceCategorySlug(selectedclassesArray[1]);
|
|
}
|
|
else{
|
|
$('.service').attr("disabled", false);
|
|
}
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
function recaptcha_callback(){
|
|
|
|
selectedDate = $('#booking_date').val();
|
|
selectedTime = $('#booking_time').val();
|
|
var response = 0;
|
|
|
|
$.ajax({
|
|
url: '<?php echo base_url();?>ajax',
|
|
type: 'POST',
|
|
data: {
|
|
action:'isTimeAvailable',
|
|
worker_id:$('#worker_id').val(),
|
|
servicelength:$('#servicelength').val(),
|
|
selectedDate:selectedDate,
|
|
selectedTime:selectedTime
|
|
},
|
|
error: function() {
|
|
},
|
|
//dataType: 'json',
|
|
success: function(data) {
|
|
},
|
|
}).done(function(result){
|
|
if(result == '1'){
|
|
$('#sendBooking').prop("disabled", false);
|
|
}
|
|
else{
|
|
alert('A választott időpont már foglalt, kérem válasszon másik időpontot!');
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
function getAvailableWorkersByServiceCategorySlug(categorySlug){
|
|
$.ajax({
|
|
url: '<?php echo base_url();?>ajax',
|
|
type: 'POST',
|
|
data: {
|
|
action:'getAvailableWorkersByServiceCategory',
|
|
serv_cat_slug:categorySlug
|
|
},
|
|
error: function() {
|
|
},
|
|
dataType: 'json',
|
|
success: function(data) {
|
|
},
|
|
}).done(function(result){
|
|
$('#workerTable').html(result.workerListShow);
|
|
if(result.workers && result.workers.length > 0){
|
|
setWorker(result.workers[0].worker_id);
|
|
}
|
|
else{
|
|
$('#workerTable').html('<div class="noWorkerMessage">Nincs elérhető munkatárs ehhez a szolgáltatáshoz.</div>');
|
|
setWorker('');
|
|
}
|
|
});
|
|
}
|
|
|
|
$(function(){
|
|
|
|
$("#datepicker").datepicker({
|
|
firstDay: 1,
|
|
defaultDate: new Date(),
|
|
dateFormat: 'yy-mm-dd',
|
|
onSelect: function(dateText) {
|
|
$('#booking_date').val(this.value);
|
|
getAvaliableTimesOfTheDay(this.value);
|
|
//console.log("Selected date: " + dateText + "; input's current value: " + this.value);
|
|
}
|
|
});
|
|
|
|
|
|
});
|
|
|
|
function getAvaliableTimesOfTheDay(selectedDate){
|
|
$('.bookingResultsWrapper').show();
|
|
$('#serviceBookingDate').html(selectedDate);
|
|
$('#serviceBookingDate').show();
|
|
//console.log(selectedDate);
|
|
|
|
$.ajax({
|
|
url: '<?php echo base_url();?>ajax',
|
|
type: 'POST',
|
|
data: {
|
|
action:'getAvailableTimes',
|
|
worker_id:$('#worker_id').val(),
|
|
servicelength:$('#servicelength').val(),
|
|
selectedDate:selectedDate
|
|
},
|
|
error: function() {
|
|
},
|
|
//dataType: 'json',
|
|
success: function(data) {
|
|
},
|
|
}).done(function(result){
|
|
if(result != 0){
|
|
$('.availableTimes').html(result);
|
|
}
|
|
else{
|
|
$('.availableTimes').html('There is not available time on this day!');
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
function setSelectedTime(time, id){
|
|
$('#booking_time').val(time).trigger('change');
|
|
$('#serviceBookingTime').html(time);
|
|
$('#serviceBookingTime').show();
|
|
|
|
$('.availableBookingTime').each(function(i, obj) {
|
|
$(this).removeClass('selectedTime');
|
|
});
|
|
|
|
|
|
//$('#bookingStep4').show();
|
|
$('#bookingTime_'+id).addClass('selectedTime');
|
|
}
|
|
|
|
function goToStep(stepNumber){
|
|
|
|
if(canGotoNextStep(stepNumber)){
|
|
$('.stepCircle').each(function(i, obj) {
|
|
$(this).removeClass('activeStep');
|
|
});
|
|
|
|
//$('#stepCircle'+stepNumber).addClass('activeStep');
|
|
|
|
$('.BookingStepContainer').each(function(i, obj) {
|
|
$(this).hide();
|
|
});
|
|
|
|
$('#bookingStep'+stepNumber).show();
|
|
|
|
if(stepNumber == 4){
|
|
recaptcha_callback();
|
|
$('#stepCircle1').addClass('activeStep');
|
|
$('#stepCircle2').addClass('activeStep');
|
|
$('#stepCircle3').addClass('activeStep');
|
|
}
|
|
else if(stepNumber == 3){
|
|
if($("#datepicker").val() != ''){
|
|
getAvaliableTimesOfTheDay($("#datepicker").val());
|
|
}
|
|
|
|
$('.bookingSummaryContainer').show();
|
|
$('#stepCircle1').addClass('activeStep');
|
|
$('#stepCircle2').addClass('activeStep');
|
|
$('#stepCircle3').addClass('activeStep');
|
|
}
|
|
else{
|
|
if(stepNumber == 1){
|
|
$('#stepCircle1').addClass('activeStep');
|
|
}
|
|
if(stepNumber == 2){
|
|
$('#stepCircle1').addClass('activeStep');
|
|
$('#stepCircle2').addClass('activeStep');
|
|
}
|
|
$('.bookingSummaryContainer').hide();
|
|
$('.bookingResultsWrapper').hide();
|
|
|
|
}
|
|
$('html').scrollTop(0);
|
|
}
|
|
else{
|
|
alert('Please fill the required fields!');
|
|
}
|
|
}
|
|
|
|
function canGotoNextStep(stepNumber){
|
|
|
|
$RequiredFieldSelected = 0;
|
|
if(stepNumber == 2){
|
|
$('.service').each(function(i, obj) {
|
|
if($(this).is(':checked')){
|
|
$RequiredFieldSelected = 1;
|
|
}
|
|
});
|
|
}
|
|
else if(stepNumber == 1){
|
|
$RequiredFieldSelected = 1;
|
|
}
|
|
else if(stepNumber == 4){
|
|
recaptcha_callback();
|
|
$RequiredFieldSelected = 1;
|
|
}
|
|
else if(stepNumber == 3){
|
|
$('.workers').each(function(i, obj) {
|
|
if($(this).is(':checked')){
|
|
$RequiredFieldSelected = 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
else if(stepNumber == 4){
|
|
recaptcha_callback();
|
|
if($('#booking_date').val() != '' && $('#booking_time').val() != ''){
|
|
$RequiredFieldSelected = 1;
|
|
}
|
|
}
|
|
|
|
|
|
return $RequiredFieldSelected;
|
|
}
|
|
|
|
function setWorker(worker_id){
|
|
// csak az aktuális worker ID-t állítjuk be a formban
|
|
$('#worker_id').val(worker_id);
|
|
|
|
// ne jelenjen meg worker név/kép a jobb oldali panelen
|
|
$('.serviceBookingWorkerContainer').hide().html('');
|
|
}
|
|
|
|
|
|
</script>
|