PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | site/controller.php +914 -177 1.6.6 → 1.8.16 View file →
@@ -32,8 +32,11 @@
32 32 case 'operators':
33 33 case 'tableaux':
34 34 case 'precheckin':
35 35 case 'revstay':
36 + case 'chat':
37 + case 'tinyurl':
38 + case 'quote':
36 39 VikRequest::setVar('view', $view);
37 40 break;
38 41 default:
39 42 VikRequest::setVar('view', 'vikbooking');
@@ -300,8 +303,12 @@
300 303 foreach ($cfields as $cf) {
301 304 $user_inp_val = VikRequest::getString('vbf' . $cf['id'], '', 'request');
302 305 if (intval($cf['isemail']) == 1 && $emailwasfound == false) {
303 306 $useremail = trim($user_inp_val);
307 + if (!preg_match('/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/', $useremail)) {
308 + showSelectVb('Invalid email address provided.');
309 + return;
310 + }
304 311 $emailwasfound = true;
305 312 }
306 313 if ($cf['isnominative'] == 1) {
307 314 if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
@@ -533,8 +540,9 @@
533 540 $isdue = 0;
534 541 $tot_taxes = 0;
535 542 $tot_city_taxes = 0;
536 543 $tot_fees = 0;
544 + $tot_damage_dep = 0;
537 545 $rooms_costs_map = [];
538 546 $is_package = (bool)(count($pkg) > 0);
539 547 if ($is_package === true) {
540 548 foreach ($rooms as $num => $r) {
@@ -562,8 +570,37 @@
562 570 $rooms_costs_map[$num] = $tar[0]['cost'];
563 571 }
564 572 }
565 573
574 + /**
575 + * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
576 + *
577 + * @since 1.17.2 (J) - 1.7.2 (WP)
578 + * @since 1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
579 + */
580 + $custom_checkinout = [];
581 + if (count($rooms) === 1) {
582 + $listing_custom_checkin = VikBooking::getRoomParam('checkin', ($rooms[(key($rooms))]['params'] ?? ''));
583 + $listing_custom_checkout = VikBooking::getRoomParam('checkout', ($rooms[(key($rooms))]['params'] ?? ''));
584 + if ($listing_custom_checkin) {
585 + // set listing-level check-in time in seconds
586 + $listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
587 + $listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
588 + }
589 + if ($listing_custom_checkout) {
590 + // set listing-level check-out time in seconds
591 + $listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
592 + $listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
593 + }
594 + if ($listing_custom_checkin || $listing_custom_checkout) {
595 + // set listing-level check-in/out times in seconds
596 + $custom_checkinout = [
597 + (int) $listing_custom_checkin,
598 + (int) $listing_custom_checkout,
599 + ];
600 + }
601 + }
602 +
566 603 $selopt = [];
567 604 $optstr = [];
568 605 $children_age = [];
569 606 if (!empty($poptionals)) {
@@ -659,9 +696,9 @@
659 696 * Count pets, if any.
660 697 *
661 698 * @since 1.16.2 (J) - 1.6.2 (WP)
662 699 */
663 - if ($opt_params && isset($opt_params['pet_fee']) && $opt_params['pet_fee']) {
700 + if ($opt_params['pet_fee'] ?? 0) {
664 701 $tot_pets = 1;
665 702 if ($actopt[0]['hmany'] > 0 && $stept[1] > 1) {
666 703 $tot_pets = (int)$stept[1];
667 704 }
@@ -667,9 +704,35 @@
667 704 }
668 705 $arrpeople[$rnoid[0]]['pets'] = $tot_pets;
669 706 }
670 707
708 + /**
709 + * Custom check-in/out times due to late check-out/early check-in options.
710 + *
711 + * @since 1.17.2 (J) - 1.7.2 (WP)
712 + */
713 + if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
714 + $custom_checkinout = [
715 + ($opt_params['set_checkin'] ?? 0),
716 + ($opt_params['set_checkout'] ?? 0),
717 + ];
718 + }
719 +
671 720 $realcost = ($actopt[0]['perperson'] == 1 ? ($realcost * $arrpeople[$rnoid[0]]['adults']) : $realcost);
721 +
722 + /**
723 + * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
724 + *
725 + * @since 1.17.7 (J) - 1.7.7 (WP)
726 + */
727 + $use_room_cost = $is_package === true ? $pkg['cost'] : ($tars[$rnoid[0]][0]['cost'] ?? 0);
728 + $custom_calc_booking = ['days' => $use_los];
729 + $custom_calc_booking_room = array_merge($arrpeople[$rnoid[0]], ['room_cost' => $use_room_cost]);
730 + $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $custom_calc_booking, $custom_calc_booking_room]);
731 + if ($custom_calculation) {
732 + $realcost = (float) $custom_calculation[0];
733 + }
734 +
672 735 $opt_minus_iva = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
673 736 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
674 737 if ($actopt[0]['is_citytax'] == 1) {
675 738 $tot_city_taxes += $opt_minus_iva;
@@ -674,8 +737,10 @@
674 737 if ($actopt[0]['is_citytax'] == 1) {
675 738 $tot_city_taxes += $opt_minus_iva;
676 739 } elseif ($actopt[0]['is_fee'] == 1) {
677 740 $tot_fees += $opt_minus_iva;
741 + } elseif ($opt_params['damagedep'] ?? 0) {
742 + $tot_damage_dep += $opt_minus_iva;
678 743 }
679 744 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
680 745 if ($tmpopr == $realcost) {
681 746 $tot_taxes += ($realcost - $opt_minus_iva);
@@ -708,10 +773,10 @@
708 773 if (!empty($coupon) && $valid_customer_coupon) {
709 774 $coupondateok = true;
710 775 if (strlen((string)$coupon['datevalid'])) {
711 776 $dateparts = explode("-", $coupon['datevalid']);
712 - $pickinfo = getdate($pcheckin);
713 - $dropinfo = getdate($pcheckout);
777 + $pickinfo = $checkin_info;
778 + $dropinfo = $checkout_info;
714 779 $checkpick = mktime(0, 0, 0, $pickinfo['mon'], $pickinfo['mday'], $pickinfo['year']);
715 780 $checkdrop = mktime(0, 0, 0, $dropinfo['mon'], $dropinfo['mday'], $dropinfo['year']);
716 781 if (!($checkpick >= $dateparts[0] && $checkpick <= $dateparts[1] && $checkdrop >= $dateparts[0] && $checkdrop <= $dateparts[1])) {
717 782 $coupondateok = false;
@@ -739,9 +804,22 @@
739 804 }
740 805 if ($coupon['maxtotord'] > 0 && $isdue > $coupon['maxtotord']) {
741 806 $coupontotok = false;
742 807 }
808 +
809 + /**
810 + * Trigger event to allow third-party plugins to implement additional coupon validations or manipulation.
811 + *
812 + * @since 1.16.7 (J) - 1.6.7 (WP)
813 + */
743 814 if ($coupontotok) {
815 + $coupon_validation = VBOFactory::getPlatform()->getDispatcher()->filter('onValidateCouponCode', [&$coupon]);
816 + if (is_array($coupon_validation) && in_array(false, $coupon_validation, true)) {
817 + $coupontotok = false;
818 + }
819 + }
820 +
821 + if ($coupontotok) {
744 822 $usedcoupon = true;
745 823 if ($coupon['percentot'] == 1) {
746 824 // percent value
747 825 $minuscoupon = 100 - $coupon['value'];
@@ -750,14 +828,17 @@
750 828 *
751 829 * @since 1.13.5 (J) - 1.3.5 (WP)
752 830 * @since 1.14.3 (J) - 1.4.3 (WP) we also exclude the amount of taxes beside the mandatory fees.
753 831 * @since 1.16.0 (J) - 1.6.0 (WP) taxes are proportionally calculated when coupon before tax.
832 + * @since 1.16.8 (J) - 1.6.8 (WP) with coupon before taxes, discounted amount calculation is an equal subtration.
754 833 */
755 - $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees);
834 + $prev_isdue = $isdue;
835 + $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees - $tot_damage_dep);
756 836 $coupondiscount = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $coupon['value'] / 100;
757 837 $isdue = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $minuscoupon / 100;
758 - $tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : ($tot_taxes * $minuscoupon / 100);
759 - $isdue += $coupon['excludetaxes'] ? ($tot_taxes + $tot_city_taxes + $tot_fees) : 0;
838 + $tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : $tot_taxes;
839 + $isdue += $coupon['excludetaxes'] ? ($tot_taxes + $tot_city_taxes + $tot_fees + $tot_damage_dep) : 0;
840 + $coupondiscount = abs($prev_isdue - $isdue);
760 841 } else {
761 842 // total value
762 843 $coupondiscount = $coupon['value'];
763 844 // isdue : taxes = coupon_discount : x
@@ -775,9 +856,9 @@
775 856 }
776 857
777 858 $strisdue = number_format($isdue, 2) . 'vikbooking';
778 859 $ptotdue = number_format($ptotdue, 2) . 'vikbooking';
779 - if ($strisdue != $ptotdue) {
860 + if ($strisdue != $ptotdue && abs(round($isdue, 2) - round((float) $ptotdue, 2)) <= 0.01) {
780 861 showSelectVb(JText::translate('VBINCONGRTOT'));
781 862 return;
782 863 }
783 864
@@ -943,8 +1024,31 @@
943 1024 // push data to tracker for conversion
944 1025 $vbo_tracker = VikBooking::getTracker();
945 1026 $vbo_tracker->pushDates($pcheckin, $pcheckout, $pdays)->pushParty($arrpeople)->pushData('idcustomer', $cpin->getNewCustomerId());
946 1027
1028 + /**
1029 + * Custom check-in/out times due to late check-out/early check-in options.
1030 + *
1031 + * @since 1.17.2 (J) - 1.7.2 (WP)
1032 + */
1033 + if ($custom_checkinout) {
1034 + // overwrite check-in and/or check-out timestamp(s)
1035 + if ($custom_checkinout[0] >= 3600) {
1036 + // overwrite check-in timestamp
1037 + $time_hours = floor($custom_checkinout[0] / 3600);
1038 + $time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
1039 + $pcheckin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
1040 + }
1041 + if ($custom_checkinout[1] >= 3600) {
1042 + // overwrite check-out timestamp
1043 + $time_hours = floor($custom_checkinout[1] / 3600);
1044 + $time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
1045 + $pcheckout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
1046 + // overwrite "realback" timestamp as well
1047 + $realback = $turnover_secs + $pcheckout;
1048 + }
1049 + }
1050 +
947 1051 if (!$mod_booking && ((!empty($payment) && intval($payment['setconfirmed']) == 1) || !$must_payment || ($usedcoupon && $isdue <= 0))) {
948 1052 // we enter this statement to set the booking to Confirmed when: no booking modification and, payment selected sets status to confirmed or no payments enabled or 100% coupon
949 1053 $arrbusy = [];
950 1054 foreach ($rooms as $num => $r) {
@@ -987,19 +1091,29 @@
987 1091 $booking_record->idpayment = !empty($payment) ? ($payment['id'] . '=' . $payment['name']) : null;
988 1092 $booking_record->ujid = $currentUser->id;
989 1093 $booking_record->coupon = $usedcoupon === true ? $strcouponeff : null;
990 1094 $booking_record->roomsnum = count($rooms);
991 - $booking_record->total = $isdue;
1095 + $booking_record->total = (float)$isdue;
992 1096 $booking_record->channel = is_array($vcmchanneldata) && !empty($vcmchanneldata['name']) ? $vcmchanneldata['name'] : null;
993 1097 $booking_record->lang = $langtag;
994 1098 $booking_record->country = !empty($usercountry) ? $usercountry : null;
995 - $booking_record->tot_taxes = $tot_taxes;
996 - $booking_record->tot_city_taxes = $tot_city_taxes;
997 - $booking_record->tot_fees = $tot_fees;
1099 + $booking_record->tot_taxes = (float)$tot_taxes;
1100 + $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1101 + $booking_record->tot_fees = (float)$tot_fees;
1102 + if ($tot_damage_dep) {
1103 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1104 + }
998 1105 $booking_record->phone = $phone_number;
999 1106 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1000 1107 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1001 1108
1109 + /**
1110 + * Trigger event to allow third party plugins to overwrite any booking property before it gets created.
1111 + *
1112 + * @since 1.18.3 (J) - 1.8.3 (WP)
1113 + */
1114 + VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateBookingRecord', [$booking_record, $rooms, $tars, $selopt, $arrpeople]);
1115 +
1002 1116 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1003 1117
1004 1118 if (!isset($booking_record->id)) {
1005 1119 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1288,12 +1402,43 @@
1288 1402 if ($mod_booking['total'] < $isdue) {
1289 1403 $mod_paymcount++;
1290 1404 }
1291 1405
1292 - $q = "UPDATE `#__vikbooking_orders` SET `custdata`=".$dbo->quote($custdata).",`ts`='".$nowts."',`days`=".$dbo->quote($pdays).",`checkin`=".$dbo->quote($pcheckin).",`checkout`=".$dbo->quote($pcheckout).",`custmail`=".$dbo->quote($useremail).",`ujid`='".$currentUser->id."',`coupon`=".($usedcoupon === true ? $dbo->quote($strcouponeff) : "NULL").",`roomsnum`='".count($rooms)."',`total`='".$isdue."',`channel`=".(is_array($vcmchanneldata) ? $dbo->quote($vcmchanneldata['name']) : (!empty($mod_booking['channel']) ? $dbo->quote($mod_booking['channel']) : 'NULL')).",`paymcount`=".$mod_paymcount.",`adminnotes`=".$dbo->quote($mod_notes).",`lang`=".$dbo->quote($langtag).",`country`=".(!empty($usercountry) ? $dbo->quote($usercountry) : 'NULL').",`tot_taxes`='".$tot_taxes."',`tot_city_taxes`='".$tot_city_taxes."',`tot_fees`='".$tot_fees."',`phone`=".$dbo->quote($phone_number).",`pkg`=".($is_package === true ? (int)$pkg['id'] : "NULL")." WHERE `id`=".(int)$mod_booking['id'].";";
1406 + $q = $dbo->getQuery(true)
1407 + ->update($dbo->qn('#__vikbooking_orders'))
1408 + ->set($dbo->qn('custdata') . ' = ' . $dbo->q($custdata))
1409 + ->set($dbo->qn('ts') . ' = ' . $nowts)
1410 + ->set($dbo->qn('days') . ' = ' . $pdays)
1411 + ->set($dbo->qn('checkin') . ' = ' . $dbo->q($pcheckin))
1412 + ->set($dbo->qn('checkout') . ' = ' . $dbo->q($pcheckout))
1413 + ->set($dbo->qn('custmail') . ' = ' . $dbo->q($useremail))
1414 + ->set($dbo->qn('ujid') . ' = ' . (int) $currentUser->id)
1415 + ->set($dbo->qn('coupon') . ' = ' . ($usedcoupon === true ? $dbo->q($strcouponeff) : 'NULL'))
1416 + ->set($dbo->qn('roomsnum') . ' = ' . count($rooms))
1417 + ->set($dbo->qn('total') . ' = ' . $isdue)
1418 + ->set($dbo->qn('channel') . ' = ' . (is_array($vcmchanneldata) ? $dbo->q($vcmchanneldata['name']) : (!empty($mod_booking['channel']) ? $dbo->q($mod_booking['channel']) : 'NULL')))
1419 + ->set($dbo->qn('paymcount') . ' = ' . $mod_paymcount)
1420 + ->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($mod_notes))
1421 + ->set($dbo->qn('lang') . ' = ' . $dbo->q($langtag))
1422 + ->set($dbo->qn('country') . ' = ' . (!empty($usercountry) ? $dbo->q($usercountry) : 'NULL'))
1423 + ->set($dbo->qn('tot_taxes') . ' = ' . $tot_taxes)
1424 + ->set($dbo->qn('tot_city_taxes') . ' = ' . $tot_city_taxes)
1425 + ->set($dbo->qn('tot_fees') . ' = ' . $tot_fees)
1426 + ->set($dbo->qn('tot_damage_dep') . ' = ' . $tot_damage_dep)
1427 + ->set($dbo->qn('phone') . ' = ' . $dbo->q($phone_number))
1428 + ->set($dbo->qn('pkg') . ' = ' . ($is_package === true ? (int) $pkg['id'] : 'NULL'))
1429 + ->where($dbo->qn('id') . ' = ' . (int) $mod_booking['id']);
1430 +
1293 1431 $dbo->setQuery($q);
1294 1432 $dbo->execute();
1295 1433
1434 + /**
1435 + * Trigger event to allow third party plugins to run after the booking is modified.
1436 + *
1437 + * @since 1.18.3 (J) - 1.8.3 (WP)
1438 + */
1439 + VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterModifyBookingRecord', [$mod_booking, $rooms, $tars, $selopt, $arrpeople]);
1440 +
1296 1441 // remove the coupon used (should never been allowed for modifications)
1297 1442 if ($usedcoupon == true && $coupon['type'] == 2) {
1298 1443 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=".(int)$coupon['id'].";";
1299 1444 $dbo->setQuery($q);
@@ -1305,9 +1450,9 @@
1305 1450 // check if some of the rooms booked have shared calendars
1306 1451 VikBooking::updateSharedCalendars($mod_booking['id'], array(), $pcheckin, $pcheckout);
1307 1452
1308 1453 //send email messages (admin and customer) and invoke SMS send
1309 - VikBooking::sendBookingEmail($mod_booking['id'], array('guest', 'admin'));
1454 + VikBooking::sendBookingEmail($mod_booking['id'], array('guest', 'admin'), true, false, $type = 'modified');
1310 1455
1311 1456 //SMS
1312 1457 VikBooking::sendBookingSMS($mod_booking['id']);
1313 1458
@@ -1344,19 +1489,29 @@
1344 1489 $booking_record->idpayment = !empty($payment) ? ($payment['id'] . '=' . $payment['name']) : null;
1345 1490 $booking_record->ujid = $currentUser->id;
1346 1491 $booking_record->coupon = $usedcoupon === true ? $strcouponeff : null;
1347 1492 $booking_record->roomsnum = count($rooms);
1348 - $booking_record->total = $isdue;
1493 + $booking_record->total = (float)$isdue;
1349 1494 $booking_record->channel = is_array($vcmchanneldata) && !empty($vcmchanneldata['name']) ? $vcmchanneldata['name'] : null;
1350 1495 $booking_record->lang = $langtag;
1351 1496 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1352 - $booking_record->tot_taxes = $tot_taxes;
1353 - $booking_record->tot_city_taxes = $tot_city_taxes;
1354 - $booking_record->tot_fees = $tot_fees;
1497 + $booking_record->tot_taxes = (float)$tot_taxes;
1498 + $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1499 + $booking_record->tot_fees = (float)$tot_fees;
1500 + if ($tot_damage_dep) {
1501 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1502 + }
1355 1503 $booking_record->phone = $phone_number;
1356 1504 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1357 1505 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1358 1506
1507 + /**
1508 + * Trigger event to allow third party plugins to overwrite any booking property before it gets created.
1509 + *
1510 + * @since 1.18.3 (J) - 1.8.3 (WP)
1511 + */
1512 + VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateBookingRecord', [$booking_record, $rooms, $tars, $selopt, $arrpeople]);
1513 +
1359 1514 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1360 1515
1361 1516 if (!isset($booking_record->id)) {
1362 1517 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1482,9 +1637,20 @@
1482 1637 // VBO 1.11 - push data to tracker for conversion
1483 1638 $vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
1484 1639 $vbo_tracker->resetTrack();
1485 1640
1486 - $app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : ""), false));
1641 + // redirect URI to pending booking details
1642 + $booking_details_uri = "index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : "");
1643 +
1644 + /**
1645 + * Trigger event to allow third-party plugins to manipulate the redirect URI.
1646 + *
1647 + * @since 1.17.2 (J) - 1.7.2 (WP)
1648 + */
1649 + VBOFactory::getPlatform()->getDispatcher()->trigger('onRedirectOrder', [&$booking_details_uri, $neworderid]);
1650 +
1651 + // redirect to booking details page
1652 + $app->redirect(JRoute::rewrite($booking_details_uri, false));
1487 1653 }
1488 1654 }
1489 1655
1490 1656 public function vieworder()
@@ -1494,18 +1660,24 @@
1494 1660 }
1495 1661
1496 1662 public function cancelrequest()
1497 1663 {
1664 + if (!JSession::checkToken()) {
1665 + // missing CSRF-proof token
1666 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
1667 + }
1668 +
1498 1669 $dbo = JFactory::getDbo();
1499 1670 $mainframe = JFactory::getApplication();
1671 +
1500 1672 $psid = VikRequest::getString('sid', '', 'request');
1501 1673 $pidorder = VikRequest::getString('idorder', '', 'request');
1674 +
1502 1675 if (!empty($psid) && !empty($pidorder)) {
1503 1676 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid).";";
1504 1677 $dbo->setQuery($q);
1505 - $dbo->execute();
1506 - if ($dbo->getNumRows() == 1) {
1507 - $order = $dbo->loadAssocList();
1678 + $order = $dbo->loadAssocList();
1679 + if ($order) {
1508 1680 $pemail = VikRequest::getString('email', '', 'request');
1509 1681 $preason = VikRequest::getString('reason', '', 'request');
1510 1682 if (!empty($pemail) && !empty($preason)) {
1511 1683 $to = VikBooking::getAdminMail();
@@ -1651,10 +1823,13 @@
1651 1823 }
1652 1824
1653 1825 public function notifypayment()
1654 1826 {
1827 + $app = JFactory::getApplication();
1655 1828 $dbo = JFactory::getDbo();
1656 1829
1830 + $session = JFactory::getSession();
1831 +
1657 1832 $config = VBOFactory::getConfig();
1658 1833 $av_helper = VikBooking::getAvailabilityInstance();
1659 1834
1660 1835 $psid = VikRequest::getString('sid', '', 'request');
@@ -1686,18 +1861,16 @@
1686 1861 $recipient_mail = $all_recipients;
1687 1862 }
1688 1863 }
1689 1864
1865 + // load booking details
1690 1866 $q = "SELECT * FROM `#__vikbooking_orders` WHERE (`sid`=" . $dbo->quote($psid) . " OR `idorderota`=" . $dbo->quote($psid) . ") AND `ts`=" . $dbo->quote($pts);
1691 1867 $dbo->setQuery($q, 0, 1);
1692 - $dbo->execute();
1693 - if (!$dbo->getNumRows()) {
1868 + $row = $dbo->loadAssoc();
1869 + if (!$row) {
1694 1870 VBOHttpDocument::getInstance()->close(404, 'Booking not found');
1695 1871 }
1696 1872
1697 - // load booking details
1698 - $row = $dbo->loadAssoc();
1699 -
1700 1873 // check if the language in use is the same as the one used during the checkout
1701 1874 if (!empty($row['lang'])) {
1702 1875 $lang = JFactory::getLanguage();
1703 1876 if ($lang->getTag() != $row['lang']) {
@@ -1712,9 +1885,12 @@
1712 1885 $vbo_tn = VikBooking::getTranslator();
1713 1886
1714 1887 if ($row['status'] == 'confirmed' && !(VikBooking::multiplePayments() && $row['paymcount'] > 0)) {
1715 1888 // booking can be paid only if not confirmed or if multiple payments are enabled and payment counter for booking greater than zero
1716 - VBOHttpDocument::getInstance()->close(409, 'Conflicting and unexpected payment validation for this reservation');
1889 + if (empty($row['idorderota']) || !empty($row['totpaid'])) {
1890 + // accept only the first payment for an OTA reservation
1891 + VBOHttpDocument::getInstance()->close(409, 'Conflicting and unexpected payment validation for this reservation');
1892 + }
1717 1893 }
1718 1894
1719 1895 /**
1720 1896 * Check split stay reservation data.
@@ -1789,12 +1965,34 @@
1789 1965 // inject values
1790 1966 $row['order_rooms'] = $orderrooms;
1791 1967 $row['fares'] = $tars;
1792 1968
1969 + // invoke the payment method class
1970 + $exppay = explode('=', ($row['idpayment'] ?? ''));
1971 + $payment = VikBooking::getPayment($exppay[0], $vbo_tn);
1972 +
1973 + /**
1974 + * Scan the booking and related rooms for damage deposit payment data.
1975 + *
1976 + * @since 1.17.6 (J) - 1.7.6 (WP)
1977 + */
1978 + $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($row, $orderrooms);
1979 +
1980 + if ($app->input->getBool('dd') && !empty($damage_deposit_payment['payment_window']['pay_id'])) {
1981 + // load the proper payment driver
1982 + $payment = VikBooking::getPayment($damage_deposit_payment['payment_window']['pay_id']) ?: $payment;
1983 + }
1984 +
1985 + if (!$payment) {
1986 + VBOHttpDocument::getInstance()->close(500, 'Could not load payment processor for validation.');
1987 + }
1988 +
1989 + // calculate booking totals
1793 1990 $isdue = 0;
1794 1991 $tot_taxes = 0;
1795 1992 $tot_city_taxes = 0;
1796 1993 $tot_fees = 0;
1994 + $tot_damage_dep = 0;
1797 1995 $pricestr = [];
1798 1996 $optstr = [];
1799 1997 foreach ($orderrooms as $kor => $or) {
1800 1998 $num = $kor + 1;
@@ -1843,8 +2041,13 @@
1843 2041 $dbo->setQuery($q);
1844 2042 $actopt = $dbo->loadAssocList();
1845 2043 if ($actopt) {
1846 2044 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
2045 +
2046 + // option params
2047 + $opt_params = !empty($actopt[0]['oparams']) ? json_decode($actopt[0]['oparams'], true) : [];
2048 + $opt_params = is_array($opt_params) ? $opt_params : [];
2049 +
1847 2050 $chvar = '';
1848 2051 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
1849 2052 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
1850 2053 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
@@ -1894,8 +2097,19 @@
1894 2097 }
1895 2098 if ($actopt[0]['perperson'] == 1) {
1896 2099 $realcost = $realcost * $or['adults'];
1897 2100 }
2101 +
2102 + /**
2103 + * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
2104 + *
2105 + * @since 1.17.7 (J) - 1.7.7 (WP)
2106 + */
2107 + $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $row, $or]);
2108 + if ($custom_calculation) {
2109 + $realcost = (float) $custom_calculation[0];
2110 + }
2111 +
1898 2112 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
1899 2113 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
1900 2114 if ($actopt[0]['is_citytax'] == 1) {
1901 2115 $tot_city_taxes += $opt_minus_tax;
@@ -1900,10 +2114,12 @@
1900 2114 if ($actopt[0]['is_citytax'] == 1) {
1901 2115 $tot_city_taxes += $opt_minus_tax;
1902 2116 } elseif ($actopt[0]['is_fee'] == 1) {
1903 2117 $tot_fees += $opt_minus_tax;
2118 + } elseif ($opt_params['damagedep'] ?? 0) {
2119 + $tot_damage_dep += $opt_minus_tax;
1904 2120 }
1905 - // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
2121 + // always calculate the amount of tax no matter if this is already a tax or a fee
1906 2122 if ($tmpopr == $realcost) {
1907 2123 $tot_taxes += ($realcost - $opt_minus_tax);
1908 2124 } else {
1909 2125 $tot_taxes += ($tmpopr - $realcost);
@@ -1934,16 +2150,19 @@
1934 2150 $expcoupon = explode(";", $row['coupon']);
1935 2151 $isdue = $isdue - $expcoupon[1];
1936 2152 }
1937 2153
1938 - // invoke the payment method class
1939 - $exppay = explode('=', $row['idpayment']);
1940 - $payment = VikBooking::getPayment($exppay[0], $vbo_tn);
1941 -
1942 2154 if (empty($row['sid']) && !empty($row['idorderota']) && !empty($row['channel'])) {
1943 2155 $row['sid'] = $row['idorderota'];
1944 2156 }
1945 2157
2158 + /**
2159 + * Trigger event to allow third-party plugins to manipulate the transaction data.
2160 + *
2161 + * @since 1.18.5 (J) - 1.8.5 (WP)
2162 + */
2163 + VBOFactory::getPlatform()->getDispatcher()->trigger('onInitPaymentTransaction', [&$row, &$payment['params'], []]);
2164 +
1946 2165 if (VBOPlatformDetection::isWordPress()) {
1947 2166 /**
1948 2167 * @wponly The payment gateway is now loaded
1949 2168 * using the apposite dispatcher.
@@ -1952,9 +2171,9 @@
1952 2171 */
1953 2172 JLoader::import('adapter.payment.dispatcher');
1954 2173 $return_url = JUri::root() . "index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'];
1955 2174 $error_url = JUri::root() . "index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'];
1956 - $notify_url = JUri::root() . "index.php?option=com_vikbooking&task=notifypayment&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts']."&tmpl=component";
2175 + $notify_url = JUri::root() . "index.php?option=com_vikbooking&task=notifypayment" . ($app->input->getBool('dd') ? '&dd=1' : '') . "&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'] . "&tmpl=component";
1957 2176 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
1958 2177 $itemid = $model->best(array('booking'), (!empty($row['lang']) ? $row['lang'] : null));
1959 2178 $extra_data = [];
1960 2179 if ($itemid) {
@@ -1985,9 +2204,9 @@
1985 2204 $bestitemid = VikBooking::findProperItemIdType(array('booking'));
1986 2205 $extra_data = array(
1987 2206 'return_url' => VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'], false, (!empty($bestitemid) ? $bestitemid : null)),
1988 2207 'error_url' => VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'], false, (!empty($bestitemid) ? $bestitemid : null)),
1989 - 'notify_url' => VikBooking::externalroute("index.php?option=com_vikbooking&task=notifypayment&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts']."&tmpl=component", false, null),
2208 + 'notify_url' => VikBooking::externalroute("index.php?option=com_vikbooking&task=notifypayment" . ($app->input->getBool('dd') ? '&dd=1' : '') . "&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'] . "&tmpl=component", false, null),
1990 2209 );
1991 2210 $extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();
1992 2211
1993 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
@@ -1992,9 +2211,20 @@
1992 2211
1993 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
1994 2213 }
1995 2214
1996 - $array_result = $obj->validatePayment();
2215 + try {
2216 + // let the gateway validate the payment transaction
2217 + $array_result = $obj->validatePayment();
2218 + } catch (Throwable $e) {
2219 + // silently catch the error and set the log
2220 + $array_result = [
2221 + 'verified' => 0,
2222 + 'log' => $e->getMessage() ?: 'Transaction error exception.',
2223 + ];
2224 + }
2225 +
2226 + // build payment log
1997 2227 $newpaymentlog = date('c')."\n".$array_result['log']."\n----------\n".$row['paymentlog'];
1998 2228
1999 2229 /**
2000 2230 * OTA reservations containing PCI-DSS card details may receive additional payments through
@@ -2008,11 +2238,21 @@
2008 2238 $newpaymentlog = $row['paymentlog'] . "\n----------\n" . date('c') . "\n" . $array_result['log'];
2009 2239 }
2010 2240 }
2011 2241
2242 + /**
2243 + * Ensure the size of the log does not make the query fail.
2244 + *
2245 + * @since 1.16.9 (J) - 1.6.9 (WP)
2246 + */
2247 + if (strlen($newpaymentlog) > 55000) {
2248 + $newpaymentlog = substr($newpaymentlog, 0, 55000) . '...';
2249 + }
2250 +
2012 2251 if ($array_result['verified'] == 1) {
2013 2252 // valid payment
2014 2253 $shouldpay = $isdue;
2254 +
2015 2255 if ($payment['charge'] > 0.00) {
2016 2256 if ($payment['ch_disc'] == 1) {
2017 2257 // charge
2018 2258 if ($payment['val_pcent'] == 1) {
@@ -2034,39 +2274,60 @@
2034 2274 $shouldpay -= $percent_to_pay;
2035 2275 }
2036 2276 }
2037 2277 }
2038 - //deposit may be skipped by customer choice
2278 +
2279 + // deposit may be skipped by customer choice
2039 2280 $shouldpay_befdep = $shouldpay;
2040 - //
2281 + $shouldpay_befdep_no_dd = $shouldpay - ($damage_deposit_payment['damagedep_gross'] ?? 0);
2282 +
2041 2283 if (!VikBooking::payTotal()) {
2042 2284 $percentdeposit = VikBooking::getAccPerCent();
2043 2285 if ($percentdeposit > 0) {
2044 2286 if (VikBooking::getTypeDeposit() == "fixed") {
2045 2287 $shouldpay = $percentdeposit;
2288 + $shouldpay_befdep_no_dd = $percentdeposit;
2046 2289 } else {
2047 2290 $shouldpay = $shouldpay * $percentdeposit / 100;
2291 + $shouldpay_befdep_no_dd = $shouldpay_befdep_no_dd * $percentdeposit / 100;;
2048 2292 }
2049 2293 }
2050 2294 }
2051 - //check if the total amount paid is the same as the order total
2295 +
2296 + // check if a damage deposit was allowed to be paid
2297 + $shouldpay_dd = $damage_deposit_payment['damagedep_gross'] ?? 0;
2298 + $shouldpay_befdd = $shouldpay - $shouldpay_dd;
2299 +
2300 + // check if the total amount paid is the same as the order total
2052 2301 if (isset($array_result['tot_paid'])) {
2053 2302 $shouldpay = round($shouldpay, 2);
2054 2303 $shouldpay_befdep = round($shouldpay_befdep, 2);
2304 + $shouldpay_befdep_no_dd = round($shouldpay_befdep_no_dd, 2);
2305 + $shouldpay_less_damagedep = round(($row['total'] - $row['tot_damage_dep']), 2);
2055 2306 $totreceived = round($array_result['tot_paid'], 2);
2056 - if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $row['paymcount'] == 0) {
2057 - //the amount paid is different than the order total
2058 - //fares might have changed or the deposit might be different
2059 - //Sending just an email to the admin that will check
2307 + if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $shouldpay_befdep_no_dd != $totreceived && $shouldpay_befdd != $totreceived && $shouldpay_less_damagedep != $totreceived && $shouldpay_dd != $totreceived && $row['paymcount'] == 0) {
2308 + // the amount paid is different than the order total
2309 + // fares might have changed or the deposit might be different
2310 + // Sending just an email to the admin that will check
2060 2311 $vbo_app = VikBooking::getVboApplication();
2061 2312 $adsendermail = VikBooking::getSenderMail();
2062 - $vbo_app->sendMail($adsendermail, $adsendermail, $recipient_mail, $adsendermail, JText::translate('VBTOTPAYMENTINVALID'), JText::sprintf('VBTOTPAYMENTINVALIDTXT', $row['id'], $totreceived." (".$array_result['tot_paid'].")", $shouldpay), false);
2313 + $vbo_app->sendMail(
2314 + $adsendermail,
2315 + $adsendermail,
2316 + $recipient_mail,
2317 + $adsendermail,
2318 + JText::translate('VBTOTPAYMENTINVALID'),
2319 + JText::sprintf(
2320 + 'VBTOTPAYMENTINVALIDTXT',
2321 + $row['id'],
2322 + $totreceived . " (" . $array_result['tot_paid'] . ")",
2323 + $shouldpay
2324 + ),
2325 + false
2326 + );
2063 2327 }
2064 - /**
2065 - * We store the amount paid before applying the charge for the transaction.
2066 - *
2067 - * @since 1.3.0
2068 - */
2328 +
2329 + // amount paid should be stored as exclusive of transaction fees/discounts
2069 2330 if ($payment['charge'] > 0.00) {
2070 2331 if ($payment['ch_disc'] == 1) {
2071 2332 // charge
2072 2333 if ($payment['val_pcent'] == 1) {
@@ -2130,12 +2391,29 @@
2130 2391 // update payable amount in case of up-sells or simply in case of another payment received
2131 2392 $new_payable = isset($array_result['tot_paid']) && $array_result['tot_paid'] ? ($row['payable'] - $array_result['tot_paid']) : 0;
2132 2393 $new_payable = $new_payable < 0 ? 0 : $new_payable;
2133 2394
2134 - $q = "UPDATE `#__vikbooking_orders` SET `status`='confirmed'" . (isset($array_result['tot_paid']) && $array_result['tot_paid'] ? ", `totpaid`='" . ($array_result['tot_paid'] + $row['totpaid']) . "', `paymcount`=".($row['paymcount'] + 1) : "") . (!empty($array_result['log']) ? ", `paymentlog`=".$dbo->quote($newpaymentlog) : "") . ", `payable`=" . $dbo->quote($new_payable) . " WHERE `id`='" . $row['id'] . "';";
2135 - $dbo->setQuery($q);
2136 - $dbo->execute();
2395 + // update booking record
2396 + $booking_record = new stdClass;
2397 + $booking_record->id = $row['id'];
2398 + $booking_record->status = 'confirmed';
2399 + if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2400 + $booking_record->totpaid = ($array_result['tot_paid'] + $row['totpaid']);
2401 + }
2402 + $booking_record->paymcount = ($row['paymcount'] + 1);
2403 + if (!empty($array_result['log'])) {
2404 + $booking_record->paymentlog = $newpaymentlog;
2405 + }
2406 + $booking_record->payable = $new_payable;
2137 2407
2408 + // update booking record
2409 + if (!$dbo->updateObject('#__vikbooking_orders', $booking_record, 'id')) {
2410 + // payments log content may break the update
2411 + unset($booking_record->paymentlog);
2412 + // try once more
2413 + $dbo->updateObject('#__vikbooking_orders', $booking_record, 'id');
2414 + }
2415 +
2138 2416 // assign room specific unit
2139 2417 $set_room_indexes = VikBooking::autoRoomUnit();
2140 2418 $room_indexes_usemap = [];
2141 2419 if ($set_room_indexes === true) {
@@ -2140,9 +2418,8 @@
2140 2418 $room_indexes_usemap = [];
2141 2419 if ($set_room_indexes === true) {
2142 2420 $q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
2143 2421 $dbo->setQuery($q);
2144 - $dbo->execute();
2145 2422 $orooms = $dbo->loadAssocList();
2146 2423 foreach ($orooms as $oroom) {
2147 2424 if (!empty($oroom['roomindex'])) {
2148 2425 // room specific unit has already been assigned
@@ -2175,8 +2452,13 @@
2175 2452 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
2176 2453 $dbo->setQuery($q);
2177 2454 $dbo->execute();
2178 2455
2456 + if (!empty($row['idquote'])) {
2457 + // let the quote model handle the release of other locked room records (OTAs included), if any
2458 + VBOMvcModel::getInstance('quote')->releaseUnconfirmedSolutions((int) $row['idquote'], (int) $row['id']);
2459 + }
2460 +
2179 2461 // customer booking
2180 2462 $q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
2181 2463 $dbo->setQuery($q);
2182 2464 $customer_id = $dbo->loadResult();
@@ -2187,14 +2469,27 @@
2187 2469
2188 2470 // check if some of the rooms booked have shared calendars
2189 2471 VikBooking::updateSharedCalendars($row['id'], array(), $row['checkin'], $row['checkout']);
2190 2472
2191 - // send email notification to guest and admin
2192 - VikBooking::sendBookingEmail($row['id'], array('guest', 'admin'));
2473 + /**
2474 + * Trigger event to allow third-party plugins to choose whether payment notifications should be sent
2475 + *
2476 + * @since 1.16.10 (J) - 1.6.10 (WP)
2477 + */
2478 + $send_notifications = true;
2479 + $should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onPaymentReceivedShouldSendNotifications', [$row]);
2480 + if (is_array($should_send) && in_array(false, $should_send, true)) {
2481 + $send_notifications = false;
2482 + }
2193 2483
2194 - // SMS
2195 - VikBooking::sendBookingSMS($row['id']);
2484 + if ($send_notifications) {
2485 + // send email notification to guest and admin
2486 + VikBooking::sendBookingEmail($row['id'], array('guest', 'admin'));
2196 2487
2488 + // SMS
2489 + VikBooking::sendBookingSMS($row['id']);
2490 + }
2491 +
2197 2492 /**
2198 2493 * Payment gateways may set and return the transaction information
2199 2494 * to eventually support a later transaction of type refund.
2200 2495 *
@@ -2199,26 +2494,47 @@
2199 2494 * to eventually support a later transaction of type refund.
2200 2495 *
2201 2496 * @since 1.14 (J) - 1.4.0 (WP)
2202 2497 * @since 1.16.2 (J) - 1.6.2 (WP) we attempt to always store the amount paid with this transaction.
2498 + * @since 1.16.9 (J) - 1.6.9 (WP) we attempt to store the amount of payment processing fees for the transaction.
2499 + * @since 1.17.6 (J) - 1.7.6 (WP) added support to separate payment for damage deposit.
2203 2500 */
2204 - $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
2501 + $tn_data = $array_result['transaction'] ?? null;
2205 2502 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2206 2503 // check event data payload to store
2207 2504 if (is_array($tn_data)) {
2208 2505 // set key
2209 - $tn_data['amount_paid'] = (float)$array_result['tot_paid'];
2506 + $tn_data['amount_paid'] = (float) $array_result['tot_paid'];
2210 2507 } elseif (is_object($tn_data)) {
2211 2508 // set property
2212 - $tn_data->amount_paid = (float)$array_result['tot_paid'];
2509 + $tn_data->amount_paid = (float) $array_result['tot_paid'];
2213 2510 } elseif (!$tn_data) {
2214 2511 // build an array (we add the payment name because we know there is no other transaction data)
2215 2512 $tn_data = [
2216 - 'amount_paid' => (float)$array_result['tot_paid'],
2513 + 'amount_paid' => (float) $array_result['tot_paid'],
2217 2514 'payment_method' => $payment['name'],
2218 2515 ];
2219 2516 }
2220 2517 }
2518 + if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
2519 + // check event data payload to store
2520 + if (is_array($tn_data)) {
2521 + // set key
2522 + $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
2523 + } elseif (is_object($tn_data)) {
2524 + // set property
2525 + $tn_data->processing_fees = (float) $array_result['tot_fees'];
2526 + }
2527 + }
2528 + if ($app->input->getBool('dd') && ($damage_deposit_payment['damagedep_gross'] ?? null)) {
2529 + if ($tn_data) {
2530 + $tn_data = (array) $tn_data;
2531 + } else {
2532 + $tn_data = [];
2533 + }
2534 + // damage deposit may be authorized without an amount paid returned
2535 + $tn_data['damage_deposit'] = (float) $damage_deposit_payment['damagedep_gross'];
2536 + }
2221 2537
2222 2538 // Booking History
2223 2539 VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);
2224 2540
@@ -2227,9 +2543,8 @@
2227 2543 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
2228 2544 $vcm = new SynchVikBooking($row['id']);
2229 2545 $vcm->setPushType('new')->sendRequest();
2230 2546 }
2231 - $session = JFactory::getSession();
2232 2547 $vcmchanneldata = $session->get('vcmChannelData', '');
2233 2548 if (!empty($vcmchanneldata)) {
2234 2549 $session->set('vcmChannelData', '');
2235 2550 }
@@ -2252,11 +2567,16 @@
2252 2567 $obj->afterValidation(0);
2253 2568 }
2254 2569 }
2255 2570 }
2256 -
2571 +
2257 2572 public function currencyconverter()
2258 2573 {
2574 + if (!JSession::checkToken()) {
2575 + // missing CSRF-proof token
2576 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2577 + }
2578 +
2259 2579 $session = JFactory::getSession();
2260 2580 $pprices = VikRequest::getVar('prices', array(0));
2261 2581 $pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
2262 2582 $ptocurrency = VikRequest::getString('tocurrency', '', 'request');
@@ -2300,8 +2620,13 @@
2300 2620 }
2301 2621
2302 2622 public function storesignature()
2303 2623 {
2624 + if (!JSession::checkToken()) {
2625 + // missing CSRF-proof token
2626 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2627 + }
2628 +
2304 2629 $sid = VikRequest::getString('sid', '', 'request');
2305 2630 $ts = VikRequest::getString('ts', '', 'request');
2306 2631 $psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
2307 2632 $ppad_width = VikRequest::getInt('pad_width', '', 'request');
@@ -2385,8 +2710,13 @@
2385 2710 }
2386 2711
2387 2712 public function validatepin()
2388 2713 {
2714 + if (!JSession::checkToken()) {
2715 + // missing CSRF-proof token
2716 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2717 + }
2718 +
2389 2719 $cpin = VikBooking::getCPinIstance();
2390 2720
2391 2721 $ppin = VikRequest::getString('pin', '', 'request');
2392 2722
@@ -2392,9 +2722,9 @@
2392 2722
2393 2723 $response = [];
2394 2724
2395 2725 $customer = $cpin->getCustomerByPin($ppin);
2396 - if (count($customer)) {
2726 + if ($customer) {
2397 2727 $response = $customer;
2398 2728 $response['success'] = 1;
2399 2729
2400 2730 if ($cpin->getCustomerCoupon($customer)) {
@@ -2408,18 +2738,24 @@
2408 2738 }
2409 2739
2410 2740 public function docancelbooking()
2411 2741 {
2742 + if (!JSession::checkToken()) {
2743 + // missing CSRF-proof token
2744 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2745 + }
2746 +
2747 + $dbo = JFactory::getDbo();
2748 + $mainframe = JFactory::getApplication();
2749 +
2412 2750 $psid = VikRequest::getString('sid', '', 'request');
2413 2751 $pidorder = VikRequest::getString('idorder', '', 'request');
2414 - $dbo = JFactory::getDBO();
2415 - $mainframe = JFactory::getApplication();
2752 +
2416 2753 if (!empty($psid) && !empty($pidorder)) {
2417 2754 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
2418 2755 $dbo->setQuery($q);
2419 - $dbo->execute();
2420 - if ($dbo->getNumRows() == 1) {
2421 - $order = $dbo->loadAssocList();
2756 + $order = $dbo->loadAssocList();
2757 + if ($order) {
2422 2758 $pemail = VikRequest::getString('email', '', 'request');
2423 2759 $preason = VikRequest::getString('reason', '', 'request');
2424 2760 if (!empty($pemail) && !empty($preason)) {
2425 2761 $to = VikBooking::getAdminMail();
@@ -2454,11 +2790,10 @@
2454 2790 $is_package = !empty($order[0]['pkg']) ? true : false;
2455 2791 $orderrooms = array();
2456 2792 $q = "SELECT `or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`roomindex`,`or`.`pkg_id`,`or`.`pkg_name`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`or`.`otarplan`,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`img`,`r`.`idcarat`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`='".$order[0]['id']."' AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
2457 2793 $dbo->setQuery($q);
2458 - $dbo->execute();
2459 - if ($dbo->getNumRows() > 0) {
2460 - $orderrooms = $dbo->loadAssocList();
2794 + $orderrooms = $dbo->loadAssocList();
2795 + if ($orderrooms) {
2461 2796 foreach($orderrooms as $kor => $or) {
2462 2797 $num = $kor + 1;
2463 2798 if($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2464 2799 //package or custom cost set from the back-end
@@ -2465,11 +2800,10 @@
2465 2800 continue;
2466 2801 }
2467 2802 $q = "SELECT `t`.*,`p`.`name`,`p`.`free_cancellation`,`p`.`canc_deadline`,`p`.`canc_policy` FROM `#__vikbooking_dispcost` AS `t` LEFT JOIN `#__vikbooking_prices` AS `p` ON `t`.`idprice`=`p`.`id` WHERE `t`.`id`='" . $or['idtar'] . "';";
2468 2803 $dbo->setQuery($q);
2469 - $dbo->execute();
2470 - if ($dbo->getNumRows() == 1) {
2471 - $tar = $dbo->loadAssocList();
2804 + $tar = $dbo->loadAssocList();
2805 + if ($tar) {
2472 2806 $tars[$num] = $tar[0];
2473 2807 }
2474 2808 }
2475 2809 }
@@ -2551,8 +2885,15 @@
2551 2885 // push the transaction currency information
2552 2886 $order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();
2553 2887
2554 2888 /**
2889 + * Trigger event to allow third-party plugins to manipulate the transaction data.
2890 + *
2891 + * @since 1.18.5 (J) - 1.8.5 (WP)
2892 + */
2893 + VBOFactory::getPlatform()->getDispatcher()->trigger('onInitRefundTransaction', [&$order[0], &$payment['params']]);
2894 +
2895 + /**
2555 2896 * @wponly The payment gateway is loaded
2556 2897 * through the apposite dispatcher.
2557 2898 */
2558 2899 JLoader::import('adapter.payment.dispatcher');
@@ -2570,16 +2911,29 @@
2570 2911 $admin_refund_error .= !empty($array_result['log']) && is_string($array_result['log']) ? $array_result['log'] : '';
2571 2912 } else {
2572 2913 // refund was successful
2573 2914
2915 + /**
2916 + * The history event extra data will contain the "amount_paid" (refunded).
2917 + *
2918 + * @since 1.16.9 (J) - 1.6.9 (WP)
2919 + */
2920 + if (!empty($array_result['tot_paid'])) {
2921 + // overwrite the requested amount with the returned one
2922 + $refund_amount = (float)$array_result['tot_paid'];
2923 + }
2924 + $history_obj->setExtraData([
2925 + 'amount_paid' => $refund_amount,
2926 + ]);
2927 +
2574 2928 // update total paid, total and refund columns for the booking
2575 2929 $booking = new stdClass;
2576 2930 $booking->id = $order[0]['id'];
2577 2931 if ($order[0]['totpaid'] > 0) {
2578 - $booking->totpaid = $order[0]['totpaid'] - $refund_amount;
2932 + $booking->totpaid = (float)($order[0]['totpaid'] - $refund_amount);
2579 2933 }
2580 2934 if ($order[0]['total'] > 0) {
2581 - $booking->total = $order[0]['total'] - $refund_amount;
2935 + $booking->total = (float)($order[0]['total'] - $refund_amount);
2582 2936 }
2583 2937 $booking->refund = (float)$order[0]['refund'] + $refund_amount;
2584 2938 // update record in db
2585 2939 $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
@@ -2584,12 +2938,12 @@
2584 2938 // update record in db
2585 2939 $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
2586 2940
2587 2941 // store the refund event
2588 - $event_descr = array(
2942 + $event_descr = [
2589 2943 '(' . $payment['name'] . ')',
2590 2944 $currencysymb . ' ' . VikBooking::numberFormat($refund_amount),
2591 - );
2945 + ];
2592 2946 $history_obj->store('RF', implode("\n", $event_descr));
2593 2947 }
2594 2948 }
2595 2949 }
@@ -2678,9 +3032,13 @@
2678 3032 VikRequest::setVar('start_date', $start_date);
2679 3033 VikRequest::setVar('end_date', $end_date);
2680 3034 //make call to get the result
2681 3035 TACVBO::$getArray = true;
2682 - $website_rates = TACVBO::tac_av_l();
3036 + $website_rates = TACVBO::tac_av_l([
3037 + // always force adults and children to be injected as arguments to avoid request conflicts
3038 + 'adults' => VikRequest::getVar('adults', array()),
3039 + 'children' => VikRequest::getVar('children', array()) ?: [0],
3040 + ]);
2683 3041 //validate response
2684 3042 if (!is_array($website_rates)) {
2685 3043 //error returned
2686 3044 echo json_encode(array('e4j.error' => $website_rates));
@@ -2791,16 +3149,14 @@
2791 3149 */
2792 3150 public function operatorlogin()
2793 3151 {
2794 3152 /**
2795 - * Extra security fix for the login form token.
2796 - *
2797 - * @since September 8th 2020
3153 + * Secure the login form token.
2798 3154 */
2799 - if (!JFactory::getSession()->checkToken()) {
2800 - throw new Exception("The security token did not match.", 403);
3155 + if (!JSession::checkToken()) {
3156 + // missing CSRF-proof token
3157 + throw new Exception('The security token did not match.', 403);
2801 3158 }
2802 - //
2803 3159
2804 3160 $app = JFactory::getApplication();
2805 3161 $pauthcode = VikRequest::getString('authcode', '', 'request');
2806 3162 $pitemid = VikRequest::getInt('Itemid', '', 'request');
@@ -2841,39 +3197,44 @@
2841 3197 * @since 1.12
2842 3198 */
2843 3199 public function storeprecheckin()
2844 3200 {
3201 + if (!JSession::checkToken()) {
3202 + // missing CSRF-proof token
3203 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
3204 + }
3205 +
2845 3206 $dbo = JFactory::getDbo();
2846 3207 $app = JFactory::getApplication();
2847 - $sid = VikRequest::getString('sid', '', 'request');
2848 - $ts = VikRequest::getString('ts', '', 'request');
2849 - $pguests = VikRequest::getVar('guests', array());
2850 - $pitemid = VikRequest::getInt('Itemid', 0, 'request');
2851 3208
3209 + $sid = $app->input->getString('sid', '');
3210 + $ts = $app->input->getString('ts', '');
3211 + $pguests = $app->input->get('guests', [], 'array');
3212 + $pitemid = $app->input->getInt('Itemid', 0);
3213 +
2852 3214 $q = "SELECT `o`.* FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($sid) . " OR `o`.`idorderota`=" . $dbo->quote($sid) . ") AND `o`.`ts`=" . $dbo->quote($ts) . " AND `o`.`status`='confirmed';";
2853 3215 $dbo->setQuery($q);
2854 - $dbo->execute();
2855 - if (!$dbo->getNumRows()) {
3216 + $order = $dbo->loadAssoc();
3217 + if (!$order) {
2856 3218 throw new Exception('Booking not found', 404);
2857 3219 }
2858 - $order = $dbo->loadAssoc();
2859 3220
2860 - $orderrooms = array();
2861 3221 $q = "SELECT `or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`childrenage`,`or`.`t_first_name`,`or`.`t_last_name`,`or`.`roomindex`,`or`.`pkg_id`,`or`.`pkg_name`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`or`.`otarplan`,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`img`,`r`.`idcarat`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$order['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
2862 3222 $dbo->setQuery($q);
2863 - $dbo->execute();
2864 - if (!$dbo->getNumRows()) {
3223 + $orderrooms = $dbo->loadAssocList();
3224 + if (!$orderrooms) {
2865 3225 throw new Exception('No rooms found', 404);
2866 3226 }
2867 - $orderrooms = $dbo->loadAssocList();
2868 3227
3228 + // access the customer record
3229 + $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);
3230 +
2869 3231 $q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
2870 3232 $dbo->setQuery($q);
2871 - $dbo->execute();
2872 - if (!$dbo->getNumRows()) {
3233 + $custorder = $dbo->loadAssoc();
3234 + if (!$custorder) {
2873 3235 throw new Exception('No customer found', 404);
2874 3236 }
2875 - $custorder = $dbo->loadAssoc();
2876 3237
2877 3238 // booking details page
2878 3239 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
2879 3240 if (empty($order['sid']) && !empty($order['idorderota'])) {
@@ -2885,10 +3246,16 @@
2885 3246 $precheckin = VikBooking::precheckinEnabled();
2886 3247 if ($precheckin) {
2887 3248 // make sure the limit of days in advance is reflected
2888 3249 $precheckin_mind = VikBooking::precheckinMinOffset();
2889 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
2890 - $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
3250 + if ($precheckin_mind < 0) {
3251 + // validation made prior to check-out date and time
3252 + $precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
3253 + } else {
3254 + // classic validation prior to check-in date and time
3255 + $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3256 + $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
3257 + }
2891 3258 }
2892 3259 if (!$precheckin) {
2893 3260 // raise error and redirect in case of website or OTA booking
2894 3261 VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
@@ -2904,13 +3271,18 @@
2904 3271
2905 3272 foreach ($pguests as $ind => $adults) {
2906 3273 foreach ($adults as $aduind => $details) {
2907 3274 foreach ($details as $detkey => $detval) {
3275 + // accept no raw HTML tags as keys
3276 + $detkey = htmlspecialchars(strip_tags($detkey));
3277 + if (!$detkey) {
3278 + continue;
3279 + }
2908 3280 if (!in_array($detkey, $front_keys)) {
2909 3281 // push the key of the guest details for later comparison
2910 3282 array_push($front_keys, $detkey);
2911 3283 }
2912 - if (strlen($detval)) {
3284 + if (is_scalar($detval) && strlen($detval)) {
2913 3285 // push value only if not empty
2914 3286 if (!isset($guests_details[$ind])) {
2915 3287 $guests_details[$ind] = array();
2916 3288 }
@@ -2939,8 +3311,13 @@
2939 3311 // current pax data include a guest not present, set it to not lose it
2940 3312 $guests_details[$ind][$aduind] = $details;
2941 3313 }
2942 3314 foreach ($details as $detkey => $detval) {
3315 + // accept no raw HTML tags as keys
3316 + $detkey = htmlspecialchars(strip_tags($detkey));
3317 + if (!$detkey) {
3318 + continue;
3319 + }
2943 3320 if (!in_array($detkey, $front_keys)) {
2944 3321 // merge this key probably reserved to the back-end
2945 3322 $guests_details[$ind][$aduind][$detkey] = $detval;
2946 3323 }
@@ -2956,8 +3333,20 @@
2956 3333
2957 3334 // Booking History
2958 3335 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');
2959 3336
3337 + /**
3338 + * Invoke the callback on the pax registration driver.
3339 + *
3340 + * @since 1.17.5 (J) - 1.7.5 (WP)
3341 + */
3342 + VBOCheckinPax::callbackPrecheckinDataStored(
3343 + VBOFactory::getConfig()->getString('checkindata', 'basic'),
3344 + (array) $guests_details,
3345 + (array) $order,
3346 + (array) $customer
3347 + );
3348 +
2960 3349 // print success message and redirect
2961 3350 $app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
2962 3351 $app->redirect($goto);
2963 3352 }
@@ -2975,28 +3364,27 @@
2975 3364 $ts = VikRequest::getString('ts', '', 'request');
2976 3365 $pitemid = VikRequest::getInt('Itemid', 0, 'request');
2977 3366 $paddopt = VikRequest::getVar('addopt', array());
2978 3367
2979 - if (!count($paddopt)) {
3368 + if (!$paddopt) {
2980 3369 throw new Exception('No extra services selected', 404);
2981 3370 }
2982 3371
3372 + // find the involved reservation, direct or OTA
2983 3373 $q = "SELECT `o`.* FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($sid) . " OR `o`.`idorderota`=" . $dbo->quote($sid) . ") AND `o`.`ts`=" . $dbo->quote($ts) . ";";
2984 3374 $dbo->setQuery($q);
2985 - $dbo->execute();
2986 - if (!$dbo->getNumRows()) {
3375 + $order = $dbo->loadAssoc();
3376 + if (!$order) {
2987 3377 throw new Exception('Booking not found', 404);
2988 3378 }
2989 - $order = $dbo->loadAssoc();
2990 3379
2991 - $orderrooms = [];
2992 - $q = "SELECT `or`.*,`r`.`name` AS `room_name` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$order['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
3380 + // obtain the involved rooms (include room "params")
3381 + $q = "SELECT `or`.*,`r`.`name` AS `room_name`,`r`.`params` AS `room_params` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$order['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
2993 3382 $dbo->setQuery($q);
2994 - $dbo->execute();
2995 - if (!$dbo->getNumRows()) {
3383 + $orderrooms = $dbo->loadAssocList();
3384 + if (!$orderrooms) {
2996 3385 throw new Exception('No rooms found', 404);
2997 3386 }
2998 - $orderrooms = $dbo->loadAssocList();
2999 3387
3000 3388 // availability helper
3001 3389 $av_helper = VikBooking::getAvailabilityInstance();
3002 3390
@@ -3013,20 +3401,56 @@
3013 3401 // load all valid and existing options as a security measure
3014 3402 $alloptions = [];
3015 3403 $q = "SELECT * FROM `#__vikbooking_optionals`;";
3016 3404 $dbo->setQuery($q);
3017 - $dbo->execute();
3018 - if (!$dbo->getNumRows()) {
3405 + $records = $dbo->loadAssocList();
3406 + if (!$records) {
3019 3407 throw new Exception('No options found', 404);
3020 3408 }
3021 - $records = $dbo->loadAssocList();
3022 3409 foreach ($records as $v) {
3023 - $alloptions[(int)$v['id']] = $v;
3410 + $alloptions[$v['id']] = $v;
3024 3411 }
3025 3412
3413 + /**
3414 + * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
3415 + *
3416 + * @since 1.17.2 (J) - 1.7.2 (WP)
3417 + * @since 1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
3418 + */
3419 + $custom_checkinout = [];
3420 + if (count($orderrooms) === 1) {
3421 + $listing_custom_checkin = VikBooking::getRoomParam('checkin', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
3422 + $listing_custom_checkout = VikBooking::getRoomParam('checkout', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
3423 + if ($listing_custom_checkin) {
3424 + // set listing-level check-in time in seconds
3425 + $listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
3426 + $listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
3427 + }
3428 + if ($listing_custom_checkout) {
3429 + // set listing-level check-out time in seconds
3430 + $listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
3431 + $listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
3432 + }
3433 + if ($listing_custom_checkin || $listing_custom_checkout) {
3434 + // set listing-level check-in/out times in seconds
3435 + $custom_checkinout = [
3436 + (int) $listing_custom_checkin,
3437 + (int) $listing_custom_checkout,
3438 + ];
3439 + }
3440 + }
3441 +
3442 + /**
3443 + * Gather the damage deposit options to be paid separately.
3444 + *
3445 + * @since 1.18.6 (J) - 1.8.6 (WP)
3446 + */
3447 + $separate_dd_options = [];
3448 +
3449 + // build the extras booked
3026 3450 $extras_booked = [];
3027 3451 foreach ($orderrooms as $kor => $or) {
3028 - if (!isset($paddopt[$kor]) || !count($paddopt[$kor])) {
3452 + if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
3029 3453 continue;
3030 3454 }
3031 3455
3032 3456 // determine proper nights of stay
@@ -3042,27 +3466,65 @@
3042 3466 if (strpos($or['optionals'], $optid . ':') === 0 || strpos($or['optionals'], ';' . $optid . ':') > 0) {
3043 3467 // this option has already been booked, skip it
3044 3468 continue;
3045 3469 }
3046 - if (!isset($alloptions[(int)$optid])) {
3470 + if (!isset($alloptions[$optid])) {
3047 3471 // this option ID does not exist, skip it
3048 3472 continue;
3049 3473 }
3050 3474 $extraoptstr .= $optid . ':' . (int)$quant . ';';
3475 +
3476 + // option params
3477 + $opt_params = !empty($alloptions[$optid]['oparams']) ? (array) json_decode($alloptions[$optid]['oparams'], true) : [];
3478 +
3479 + /**
3480 + * Custom check-in/out times due to late check-out/early check-in options.
3481 + *
3482 + * @since 1.17.2 (J) - 1.7.2 (WP)
3483 + */
3484 + if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
3485 + $custom_checkinout = [
3486 + ($opt_params['set_checkin'] ?? 0),
3487 + ($opt_params['set_checkout'] ?? 0),
3488 + ];
3489 + }
3490 +
3491 + /**
3492 + * Check if this is a damage deposit with a separate payment window.
3493 + *
3494 + * @since 1.18.6 (J) - 1.8.6 (WP)
3495 + */
3496 + if (($opt_params['damagedep'] ?? 0) && !empty($opt_params['damagedep_settings']['paywhen'])) {
3497 + // damage deposit option with separate payment defined
3498 + $future_payable_dd = true;
3499 + if (!empty($opt_params['damagedep_settings']['bmaxlos']) && ($order['days'] ?? 1) > $opt_params['damagedep_settings']['bmaxlos']) {
3500 + // maximum nights of stay validation failed
3501 + $future_payable_dd = false;
3502 + } elseif (empty($opt_params['damagedep_settings']['payid'])) {
3503 + // separate payment method ID validation failed
3504 + $future_payable_dd = false;
3505 + }
3506 + if ($future_payable_dd === true) {
3507 + // push damage deposit option ID to be paid separately
3508 + $separate_dd_options[] = (int) $optid;
3509 + }
3510 + }
3511 +
3051 3512 // push option booked
3052 - array_push($extras_booked, array(
3053 - 'id' => $optid,
3054 - 'idroom' => $or['idroom'],
3055 - 'name' => $alloptions[(int)$optid]['name'],
3056 - 'quant' => $quant,
3513 + $extras_booked[] = [
3514 + 'id' => $optid,
3515 + 'idroom' => $or['idroom'],
3516 + 'name' => $alloptions[$optid]['name'],
3517 + 'quant' => $quant,
3057 3518 'room_cost' => (!empty($or['cust_cost']) ? $or['cust_cost'] : $or['room_cost']),
3058 3519 'room_name' => $or['room_name'],
3059 - 'optcost' => $alloptions[(int)$optid]['cost'],
3060 - 'adults' => $or['adults'],
3061 - 'children' => $or['children'],
3062 - 'nights' => $room_stay_nights,
3063 - ));
3520 + 'optcost' => $alloptions[$optid]['cost'],
3521 + 'adults' => $or['adults'],
3522 + 'children' => $or['children'],
3523 + 'nights' => $room_stay_nights,
3524 + ];
3064 3525 }
3526 +
3065 3527 // update options for this room record
3066 3528 $newoptstr = $or['optionals'] . $extraoptstr;
3067 3529 $q = "UPDATE `#__vikbooking_ordersrooms` SET `optionals`=" . $dbo->quote($newoptstr) . " WHERE `id`={$or['id']};";
3068 3530 $dbo->setQuery($q);
@@ -3072,8 +3534,10 @@
3072 3534 // increase booking total amount and build event log for the history
3073 3535 $currency = VikBooking::getCurrencySymb();
3074 3536 $totrooms = count($orderrooms);
3075 3537 $increase = 0;
3538 + $future_dd = 0;
3539 + $add_tax = 0;
3076 3540 $extraslog = [];
3077 3541 foreach ($extras_booked as $extra) {
3078 3542 $o = $alloptions[(int)$extra['id']];
3079 3543 if ((int)$o['pcentroom']) {
@@ -3090,31 +3554,100 @@
3090 3554 if ($o['perperson'] == 1) {
3091 3555 $optcost = $optcost * $extra['adults'];
3092 3556 }
3093 3557 $optcost *= $extra['quant'];
3558 +
3559 + /**
3560 + * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
3561 + *
3562 + * @since 1.17.7 (J) - 1.7.7 (WP)
3563 + */
3564 + $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$optcost, &$o, $order, $extra]);
3565 + if ($custom_calculation) {
3566 + $optcost = (float) $custom_calculation[0];
3567 + }
3568 +
3094 3569 $floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
3570 + $netoptprice = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
3095 3571 $increase += $floatoptprice;
3572 + $add_tax += $floatoptprice - $netoptprice;
3096 3573 array_push($extraslog, ($totrooms > 1 ? $extra['room_name'] . ': ' : '') . $extra['name'] . ($extra['quant'] > 1 ? ' (x' . $extra['quant'] . ')' : '') . ' ' . $currency . ' ' . VikBooking::numberFormat($floatoptprice));
3574 +
3575 + if (in_array((int) $extra['id'], $separate_dd_options)) {
3576 + // this is a damage deposit option that will be paid separately
3577 + $future_dd += $floatoptprice;
3578 + }
3097 3579 }
3098 3580
3099 3581 $newtotbooking = $order['total'] + $increase;
3582 + $new_tot_taxes = $order['tot_taxes'] + $add_tax;
3583 +
3100 3584 /**
3101 3585 * Important: the 'paymcount' should be increased only if the status is
3102 3586 * "confirmed" or no rooms may be occupied when receiving a payment.
3103 3587 */
3104 - $q = "UPDATE `#__vikbooking_orders` SET `total`=" . $dbo->quote($newtotbooking) . ", `paymcount`=" . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']) . ", `payable`=" . $dbo->quote(((float)$order['payable'] + $increase)) . " WHERE `id`={$order['id']};";
3588 + $q = $dbo->getQuery(true)
3589 + ->update($dbo->qn('#__vikbooking_orders'))
3590 + ->set($dbo->qn('total') . ' = ' . $dbo->q($newtotbooking))
3591 + ->set($dbo->qn('paymcount') . ' = ' . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']))
3592 + ->set($dbo->qn('tot_taxes') . ' = ' . $dbo->q($new_tot_taxes))
3593 + ->set($dbo->qn('payable') . ' = ' . $dbo->q(((float)$order['payable'] + $increase - $future_dd)))
3594 + ->where($dbo->qn('id') . ' = ' . (int) $order['id']);
3595 +
3596 + /**
3597 + * Custom check-in/out times due to late check-out/early check-in options.
3598 + *
3599 + * @since 1.17.2 (J) - 1.7.2 (WP)
3600 + */
3601 + if ($custom_checkinout) {
3602 + $checkin_info = getdate($order['checkin']);
3603 + $checkout_info = getdate($order['checkout']);
3604 + // overwrite check-in and/or check-out timestamp(s)
3605 + if ($custom_checkinout[0] >= 3600) {
3606 + // overwrite check-in timestamp
3607 + $time_hours = floor($custom_checkinout[0] / 3600);
3608 + $time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
3609 + $new_booking_checkin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
3610 + // update db record field
3611 + $q->set($dbo->qn('checkin') . ' = ' . $dbo->q($new_booking_checkin));
3612 + }
3613 + if ($custom_checkinout[1] >= 3600) {
3614 + // overwrite check-out timestamp
3615 + $time_hours = floor($custom_checkinout[1] / 3600);
3616 + $time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
3617 + $new_booking_checkout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
3618 + // update db record field
3619 + $q->set($dbo->qn('checkout') . ' = ' . $dbo->q($new_booking_checkout));
3620 + }
3621 + }
3622 +
3623 + // update booking record on db
3105 3624 $dbo->setQuery($q);
3106 3625 $dbo->execute();
3107 3626
3108 3627 // Booking History
3109 - VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('UE', implode("\n", $extraslog));
3628 + VikBooking::getBookingHistoryInstance($order['id'])->store('UE', implode("\n", $extraslog));
3110 3629
3111 - // send email notification to guest and admin
3112 - VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
3630 + /**
3631 + * Trigger event to allow third-party plugins to choose whether upselling notifications should be sent.
3632 + *
3633 + * @since 1.17.2 (J) - 1.7.2 (WP)
3634 + */
3635 + $send_notifications = true;
3636 + $should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onUpsellingReceivedShouldSendNotifications', [$order]);
3637 + if (is_array($should_send) && in_array(false, $should_send, true)) {
3638 + $send_notifications = false;
3639 + }
3113 3640
3641 + if ($send_notifications) {
3642 + // send email notification to guest and admin
3643 + VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
3644 + }
3645 +
3114 3646 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . (empty($order['sid']) && !empty($order['idorderota']) ? $order['idorderota'] : $order['sid']) . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3115 3647 $app->enqueueMessage(JText::translate('VBOUPSELLRESULTOK'));
3116 3648 $app->redirect($goto);
3649 + $app->close();
3117 3650 }
3118 3651
3119 3652 /**
3120 3653 * Submits a new review.
@@ -3237,12 +3770,12 @@
3237 3770
3238 3771 // reviewer information
3239 3772 $review_content->reviewer = new stdClass;
3240 3773 $customer_name = '';
3241 - if (count($customer)) {
3774 + if ($customer) {
3242 3775 $review_content->reviewer->name = $customer['first_name'];
3243 3776 $review_content->reviewer->country_code = $customer['country'];
3244 - $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];;
3777 + $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
3245 3778 } else {
3246 3779 $revuname = '';
3247 3780 if (!empty($order['custdata'])) {
3248 3781 $uinfos = explode("\n", $order['custdata']);
@@ -3763,8 +4296,13 @@
3763 4296 * @since 1.13.5
3764 4297 */
3765 4298 public function add_roomdaynote()
3766 4299 {
4300 + if (!JSession::checkToken()) {
4301 + // missing CSRF-proof token
4302 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4303 + }
4304 +
3767 4305 $dt = VikRequest::getString('dt', '', 'request');
3768 4306 $idroom = VikRequest::getInt('idroom', 0, 'request');
3769 4307 $subunit = VikRequest::getInt('subunit', 0, 'request');
3770 4308 $type = VikRequest::getString('type', '', 'request');
@@ -3829,21 +4367,26 @@
3829 4367
3830 4368 /**
3831 4369 * AJAX endpoint to upload customer documents during the pre-checkin.
3832 4370 *
3833 - * @throws Exception
3834 - *
3835 4371 * @since 1.14 (J) - 1.4.0 (WP)
4372 + * @since 1.18.6 (J) - 1.8.6 (WP) added support for MRZ detection through Channel Manager.
3836 4373 */
3837 4374 public function precheckin_upload_docs()
3838 4375 {
3839 - $dbo = JFactory::getDbo();
3840 - $app = JFactory::getApplication();
3841 - $input = $app->input;
4376 + $app = JFactory::getApplication();
4377 + $dbo = JFactory::getDbo();
4378 + $input = $app->input;
3842 4379
3843 - // get request values
3844 - $order_sid = $input->getString('sid', '');
3845 - $order_ts = $input->getString('ts', '');
4380 + if (!JSession::checkToken()) {
4381 + // missing CSRF-proof token
4382 + VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
4383 + }
4384 +
4385 + // gather request values
4386 + $order_sid = $input->getString('sid', '');
4387 + $order_ts = $input->getString('ts', '');
4388 + $use_mrz = $input->getBool('mrz', false);
3846 4389 /**
3847 4390 * This is a simple file uploading process, but if
3848 4391 * we wanted to automatically update the pax_data,
3849 4392 * the request vars below would be necessary.
@@ -3849,31 +4392,29 @@
3849 4392 * the request vars below would be necessary.
3850 4393 */
3851 4394 $room_index = $input->getInt('room_index', 0);
3852 4395 $guest_index = $input->getInt('guest_index', 0);
3853 - $pax_index = $input->getString('pax_index', '');
4396 + $pax_index = $input->getString('pax_index', '');
3854 4397
3855 4398 if (empty($order_sid) || empty($order_ts)) {
3856 - throw new Exception('Missing booking details', 404);
4399 + VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
3857 4400 }
3858 4401
3859 4402 $q = "SELECT `o`.*,(SELECT SUM(`or`.`adults`) FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idorder`=`o`.`id`) AS `tot_adults` FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($order_sid) . " OR `o`.`idorderota`=" . $dbo->quote($order_sid) . ") AND `o`.`ts`=" . $dbo->quote($order_ts) . " AND `o`.`status`='confirmed';";
3860 4403 $dbo->setQuery($q);
3861 - $dbo->execute();
3862 - if (!$dbo->getNumRows()) {
3863 - throw new Exception('Booking not found', 404);
4404 + $order = $dbo->loadAssoc();
4405 + if (!$order) {
4406 + VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
3864 4407 }
3865 - $order = $dbo->loadAssoc();
3866 4408
3867 4409 $customer = array();
3868 4410 $q = "SELECT `c`.*,`co`.`idorder`,`co`.`signature`,`co`.`pax_data`,`co`.`comments`,`co`.`checkindoc` FROM `#__vikbooking_customers` AS `c` LEFT JOIN `#__vikbooking_customers_orders` `co` ON `c`.`id`=`co`.`idcustomer` WHERE `co`.`idorder`=".$order['id'].";";
3869 4411 $dbo->setQuery($q);
3870 - $dbo->execute();
3871 - if (!$dbo->getNumRows()) {
4412 + $customer = $dbo->loadObject();
4413 + if (!$customer) {
3872 4414 // one customer must be assigned to this booking for the pre-checkin
3873 - throw new Exception('No customers associated to this booking', 404);
4415 + VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
3874 4416 }
3875 - $customer = $dbo->loadObject();
3876 4417
3877 4418 // make sure pre-checkin is allowed
3878 4419 $precheckin = VikBooking::precheckinEnabled();
3879 4420 if ($precheckin) {
@@ -3878,25 +4419,31 @@
3878 4419 $precheckin = VikBooking::precheckinEnabled();
3879 4420 if ($precheckin) {
3880 4421 // make sure the limit of days in advance is reflected
3881 4422 $precheckin_mind = VikBooking::precheckinMinOffset();
3882 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3883 - $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
4423 + if ($precheckin_mind < 0) {
4424 + // validation made prior to check-out date and time
4425 + $precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
4426 + } else {
4427 + // classic validation prior to check-in date and time
4428 + $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
4429 + $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
4430 + }
3884 4431 }
3885 4432 if (!$precheckin) {
3886 - throw new Exception('Pre-checkin not allowed at this time', 403);
4433 + VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
3887 4434 }
3888 4435
3889 4436 // get uploaded files array (use "raw" to avoid filtering the file to upload)
3890 4437 $files = $input->files->get('docs', array(), 'raw');
3891 - if (!count($files)) {
3892 - throw new Exception('No files to be uploaded', 500);
4438 + if (!$files) {
4439 + VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
3893 4440 }
3894 4441
3895 4442 if (isset($files['name'])) {
3896 4443 // we have a single associative array, we need to push it within a list,
3897 4444 // because the upload iterates the $files array
3898 - $files = array($files);
4445 + $files = [$files];
3899 4446 }
3900 4447
3901 4448 // fetch documents folder path
3902 4449 $dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
@@ -3906,28 +4453,26 @@
3906 4453 // randomize string
3907 4454 $customer->seed = uniqid();
3908 4455
3909 4456 // create blocks for hashed folder
3910 - $parts = array(
4457 + $parts = [
3911 4458 $customer->first_name,
3912 4459 $customer->last_name,
3913 4460 md5(serialize($customer)),
3914 - );
4461 + ];
3915 4462
3916 4463 // join fetched parts
3917 4464 $customer->docsfolder = strtolower(implode('-', array_filter($parts)));
3918 4465
3919 4466 if (strlen($customer->docsfolder) < 16) {
3920 - throw new Exception('Possible security breach. Please specify as many details as possible.', 400);
4467 + VBOHttpDocument::getInstance($app)->close(400, 'Possible security breach. Please specify as many details as possible.');
3921 4468 }
3922 4469
3923 - jimport('joomla.filesystem.folder');
3924 -
3925 4470 // create a folder for this customer
3926 4471 $created = JFolder::create($dirpath . $customer->docsfolder);
3927 4472
3928 4473 if (!$created) {
3929 - throw new Exception(sprintf('Unable to create the folder [%s]', $dirpath . $customer->docsfolder), 403);
4474 + VBOHttpDocument::getInstance($app)->close(403, sprintf('Unable to create the folder [%s]', $dirpath . $customer->docsfolder));
3930 4475 }
3931 4476
3932 4477 unset($customer->seed);
3933 4478
@@ -3937,10 +4482,12 @@
3937 4482 $record->docsfolder = $customer->docsfolder;
3938 4483 $dbo->updateObject('#__vikbooking_customers', $record, 'id');
3939 4484 }
3940 4485
3941 - // prepare the response array of uploaded-file objects
3942 - $response = array();
4486 + // prepare the response with the uploaded-file objects
4487 + $response = [
4488 + 'uploads' => [],
4489 + ];
3943 4490 $upload_err = null;
3944 4491
3945 4492 // compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
3946 4493 $file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';
@@ -3958,13 +4505,13 @@
3958 4505 }
3959 4506 // always prepend "pre check-in" to the original file name
3960 4507 $file['name'] = strtolower($file_prefix . $file['name']);
3961 4508 // try to upload the file
3962 - $result = VikBooking::uploadFileFromRequest($file, $dirpath . $customer->docsfolder, "/(image\/.+)|(application\/(zip|pdf|msword|vnd.*?))|(text\/(plain|markdown|csv))$/i");
4509 + $result = VikBooking::uploadFileFromRequest($file, $dirpath . $customer->docsfolder, 'png,jpg,jpeg,bmp,heic,zip,rar,pdf,doc,docx,rtf,odt,pages,xls,xlsx,csv,ods,numbers,txt,md');
3963 4510 // set a valid URL for the uploaded file
3964 4511 $result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
3965 4512 // push uploaded file
3966 - array_push($response, $result);
4513 + array_push($response['uploads'], $result);
3967 4514 }
3968 4515
3969 4516 } catch (Exception $e) {
3970 4517 // do nothing, but catch the error
@@ -3970,15 +4517,56 @@
3970 4517 // do nothing, but catch the error
3971 4518 $upload_err = $e;
3972 4519 }
3973 4520
3974 - if (!count($response)) {
3975 - throw ($upload_err !== null ? $upload_err : (new Exception('No files could actually be uploaded', 500)));
4521 + if (!$response['uploads']) {
4522 + // raise an error
4523 + if ($upload_err instanceof Exception) {
4524 + VBOHttpDocument::getInstance($app)->close($upload_err->getCode() ?: 500, $upload_err->getMessage());
4525 + }
4526 + VBOHttpDocument::getInstance($app)->close(500, 'No files could actually be uploaded');
3976 4527 }
3977 4528
3978 - // output the response
3979 - echo json_encode($response);
3980 - exit;
4529 + if ($use_mrz) {
4530 + // file upload completed, detect MRZ codes from uploaded documents
4531 + $mrzImageUrls = array_values(array_filter(array_map(function($uploaded) {
4532 + return $uploaded->url ?? '';
4533 + }, $response['uploads'])));
4534 +
4535 + // set response properties for MRZ detection result
4536 + $response['mrz'] = [
4537 + 'data' => null,
4538 + 'raw' => null,
4539 + 'verified' => null,
4540 + 'error' => null,
4541 + ];
4542 +
4543 + try {
4544 + // let the AI model service analyse the uploaded documents
4545 + $mrzResult = (new VCMAiModelService)->mrzDetection($mrzImageUrls);
4546 +
4547 + // let the current pax-fields data collector parse the extracted MRZ data
4548 + $mrzMappedFields = VBOCheckinPax::getInstance()
4549 + ->getMRZMapper()
4550 + ->setVerified((bool) ($mrzResult->valid ?? false))
4551 + ->mapDetectedProperties((array) ($mrzResult->data ?? null))
4552 + ->getMappedFields();
4553 +
4554 + // set MRZ validation result within the response
4555 + $response['mrz']['data'] = $mrzMappedFields;
4556 + $response['mrz']['raw'] = ($mrzResult->data ?? null);
4557 + $response['mrz']['verified'] = (bool) $mrzResult->valid ?? false;
4558 + } catch (Exception $e) {
4559 + // catch the error message
4560 + $response['mrz']['error'] = $e->getMessage();
4561 + } catch (Throwable $e) {
4562 + // catch the PHP error
4563 + $response['mrz']['error'] = sprintf('Fatal error caught: %s', $e->getMessage());
4564 + }
4565 + }
4566 +
4567 + // send response to output
4568 + VBOHttpDocument::getInstance($app)->json($response);
3981 4569 }
3982 4570
3983 4571 /**
3984 4572 * AJAX endpoint to submit an inquiry/information request.
@@ -4004,11 +4592,11 @@
4004 4592 $checkout_h = $input->getInt('checkouth', 0);
4005 4593 $checkout_m = $input->getInt('checkoutm', 0);
4006 4594 $categories = $input->getString('categories', '');
4007 4595 $roomsnum = $input->getInt('roomsnum', 1);
4008 - $adults = $input->get('adults', array(), 'int');
4009 - $children = $input->get('children', array(), 'int');
4010 - $inquiry = $input->get('inquiry', array(), 'raw');
4596 + $adults = $input->get('adults', [], 'int');
4597 + $children = $input->get('children', [], 'int');
4598 + $inquiry = $input->get('inquiry', [], 'array');
4011 4599 $ulang = $input->getString('ulang', '');
4012 4600
4013 4601 $timeopst = VikBooking::getTimeOpenStore();
4014 4602 if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
@@ -4053,8 +4641,10 @@
4053 4641 if (!is_scalar($info_val) || !strlen($info_val)) {
4054 4642 // empty or invalid field
4055 4643 continue;
4056 4644 }
4645 + // accept only text from submitted value
4646 + $info_val = strip_tags($info_val);
4057 4647 if ($info_type == 'nominative') {
4058 4648 if (empty($t_first_name)) {
4059 4649 $t_first_name = $info_val;
4060 4650 $res_custdata[JText::translate('VBNAME')] = $info_val;
@@ -4447,10 +5037,10 @@
4447 5037 // update booking total amounts as first
4448 5038 if ($upgrade_difference > 0) {
4449 5039 $upd_booking = new stdClass;
4450 5040 $upd_booking->id = $booking['id'];
4451 - $upd_booking->total = $booking['total'] + $upgrade_difference;
4452 - $upd_booking->tot_taxes = $booking['tot_taxes'] + $upgtax_difference;
5041 + $upd_booking->total = (float)($booking['total'] + $upgrade_difference);
5042 + $upd_booking->tot_taxes = (float)($booking['tot_taxes'] + $upgtax_difference);
4453 5043
4454 5044 $dbo->updateObject('#__vikbooking_orders', $upd_booking, 'id');
4455 5045 }
4456 5046
@@ -4470,6 +5060,153 @@
4470 5060 VikBooking::getBookingHistoryInstance()->setBid($booking['id'])->store('UR', $hist_descr);
4471 5061
4472 5062 // output the JSON encoded successful response
4473 5063 VBOHttpDocument::getInstance()->json(['success' => 1]);
5064 + }
5065 +
5066 + /**
5067 + * End-point used to ping the execution of the scheduled crontab runners.
5068 + *
5069 + * @return void
5070 + *
5071 + * @since 1.17 (J) - 1.7 (WP)
5072 + */
5073 + public function crontab()
5074 + {
5075 + VBOFactory::getCrontabSimulator()->run();
5076 + exit;
5077 + }
5078 +
5079 + /**
5080 + * End-point used to route at runtime the link to the requested room details page.
5081 + * Originally introduced to improve landing page URL accuracy with Google VR.
5082 + *
5083 + * @return void
5084 + *
5085 + * @since 1.18.3 (J) - 1.8.3 (WP)
5086 + */
5087 + public function route_listing_details()
5088 + {
5089 + $app = JFactory::getApplication();
5090 +
5091 + $listing_id = $app->input->getUint('listing_id', 0);
5092 +
5093 + if (!$listing_id) {
5094 + VBOHttpDocument::getInstance($app)->close(400, 'Missing listing ID.');
5095 + }
5096 +
5097 + // make sure the requested listing exists
5098 + $listing_data = VikBooking::getRoomInfo($listing_id);
5099 +
5100 + if (!$listing_data) {
5101 + VBOHttpDocument::getInstance($app)->close(404, 'Listing not found.');
5102 + }
5103 +
5104 + if (empty($listing_data['avail'])) {
5105 + VBOHttpDocument::getInstance($app)->close(404, 'Listing currently unavailable.');
5106 + }
5107 +
5108 + // get user country and locale (preferred language), if available, to find the best language
5109 + $user_country = $app->input->getString('country', '');
5110 + $user_lang = $app->input->getString('ulang', '');
5111 + $user_country = empty($user_country) && !empty($user_lang) ? $user_lang : $user_country;
5112 + $best_lang = (!empty($user_country) || !empty($user_lang)) && class_exists('VikChannelManager') ? VikChannelManager::guessBookingLangFromCountry($user_country, $user_lang) : '';
5113 +
5114 + /**
5115 + * Adjust best language in case the default website lang is different than English and
5116 + * the visitor speaks a foreign language. This way we make English the best language.
5117 + * I.E. Website default lang is IT, visitor's lang is DE, we make it land on EN because DE isn't available.
5118 + */
5119 + $current_lang = JFactory::getLanguage()->getTag();
5120 + if (empty($best_lang) && !empty($user_lang) && substr(strtolower($user_lang), 0, 2) != 'en' && substr(strtolower($current_lang), 0, 2) != 'en') {
5121 + // check if English is available
5122 + foreach (VikBooking::getVboApplication()->getKnownLanguages() as $ltag => $ldet) {
5123 + if (substr(strtolower($ltag), 0, 2) == 'en') {
5124 + // grab this English-esque language
5125 + $best_lang = $ltag;
5126 + break;
5127 + }
5128 + }
5129 + }
5130 +
5131 + // build query arguments for routing
5132 + $route_query_args = [
5133 + 'option' => 'com_vikbooking',
5134 + 'view' => 'roomdetails',
5135 + 'roomid' => $listing_id,
5136 + ];
5137 +
5138 + // route the best URI (if possible)
5139 + try {
5140 + // find the best page id for the URL according to the CMS
5141 + $itemid = null;
5142 +
5143 + if (VBOPlatformDetection::isWordPress()) {
5144 + /**
5145 + * @wponly route the best Shortcode for the booking process ("room details" or "search form").
5146 + * the best booking language is passed over the model to find the best shortcode.
5147 + */
5148 + $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5149 +
5150 + // grab all Shortcodes and parse them to find the best one for this room, if any
5151 + $shortcodes = $model->all();
5152 + foreach ($shortcodes as $shortcode) {
5153 + if ($shortcode->type != 'roomdetails' || empty($shortcode->post_id)) {
5154 + continue;
5155 + }
5156 + $page_params = json_decode($shortcode->json);
5157 + if (!is_object($page_params) || !isset($page_params->roomid) || (int) $page_params->roomid != $listing_id) {
5158 + continue;
5159 + }
5160 + if (!empty($best_lang) && $shortcode->lang == $best_lang) {
5161 + // always give higher priority to the exact lang
5162 + $itemid = $shortcode->post_id;
5163 + }
5164 + if (empty($itemid)) {
5165 + // in case no perfect lang found, use this post id for this room-type
5166 + $itemid = $shortcode->post_id;
5167 + }
5168 + }
5169 +
5170 + if (empty($itemid)) {
5171 + // default to the best shortcode for this lang of type "search form"
5172 + $itemid = $model->best(['vikbooking'], $best_lang);
5173 + if (!$itemid) {
5174 + // if we really get to this point, try to fetch the first non-empty shortcode, if any
5175 + $shortcodes = $model->all($columns = 'post_id', $full = true);
5176 + if ($shortcodes) {
5177 + // grab the very first active shortcode, no matter what's the type of it
5178 + $itemid = $shortcodes[0]->post_id;
5179 + }
5180 + }
5181 + }
5182 + } else {
5183 + /**
5184 + * @joomlaonly inject the language to the query arguments list before routing
5185 + */
5186 + if (!empty($best_lang)) {
5187 + $route_query_args['lang'] = $best_lang;
5188 + }
5189 +
5190 + if (class_exists('VCMFactory')) {
5191 + $best_menuitem_id = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['roomdetails'], ['roomid' => $room_type_id, 'lang' => $best_lang]);
5192 +
5193 + if ($best_menuitem_id) {
5194 + $itemid = $best_menuitem_id;
5195 + } else {
5196 + $itemid = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['vikbooking'], $best_lang);
5197 + }
5198 + }
5199 + }
5200 +
5201 + // route final URL with all arguments
5202 + $routed_url = VikBooking::externalroute($route_query_args, false, $itemid);
5203 + } catch (Throwable $e) {
5204 + // raise an error
5205 + VBOHttpDocument::getInstance($app)->close(500, 'Could not route to the requested page.');
5206 + }
5207 +
5208 + // redirect the request to the proper page
5209 + $app->redirect($routed_url);
5210 + $app->close();
4474 5211 }
4475 5212 }