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 +773 -141 1.7.1 → 1.8.16 View file →
@@ -32,9 +32,11 @@
32 32 case 'operators':
33 33 case 'tableaux':
34 34 case 'precheckin':
35 35 case 'revstay':
36 + case 'chat':
36 37 case 'tinyurl':
38 + case 'quote':
37 39 VikRequest::setVar('view', $view);
38 40 break;
39 41 default:
40 42 VikRequest::setVar('view', 'vikbooking');
@@ -301,8 +303,12 @@
301 303 foreach ($cfields as $cf) {
302 304 $user_inp_val = VikRequest::getString('vbf' . $cf['id'], '', 'request');
303 305 if (intval($cf['isemail']) == 1 && $emailwasfound == false) {
304 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 + }
305 311 $emailwasfound = true;
306 312 }
307 313 if ($cf['isnominative'] == 1) {
308 314 if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
@@ -534,8 +540,9 @@
534 540 $isdue = 0;
535 541 $tot_taxes = 0;
536 542 $tot_city_taxes = 0;
537 543 $tot_fees = 0;
544 + $tot_damage_dep = 0;
538 545 $rooms_costs_map = [];
539 546 $is_package = (bool)(count($pkg) > 0);
540 547 if ($is_package === true) {
541 548 foreach ($rooms as $num => $r) {
@@ -563,8 +570,37 @@
563 570 $rooms_costs_map[$num] = $tar[0]['cost'];
564 571 }
565 572 }
566 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 +
567 603 $selopt = [];
568 604 $optstr = [];
569 605 $children_age = [];
570 606 if (!empty($poptionals)) {
@@ -660,9 +696,9 @@
660 696 * Count pets, if any.
661 697 *
662 698 * @since 1.16.2 (J) - 1.6.2 (WP)
663 699 */
664 - if ($opt_params && isset($opt_params['pet_fee']) && $opt_params['pet_fee']) {
700 + if ($opt_params['pet_fee'] ?? 0) {
665 701 $tot_pets = 1;
666 702 if ($actopt[0]['hmany'] > 0 && $stept[1] > 1) {
667 703 $tot_pets = (int)$stept[1];
668 704 }
@@ -668,9 +704,35 @@
668 704 }
669 705 $arrpeople[$rnoid[0]]['pets'] = $tot_pets;
670 706 }
671 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 +
672 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 +
673 735 $opt_minus_iva = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
674 736 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
675 737 if ($actopt[0]['is_citytax'] == 1) {
676 738 $tot_city_taxes += $opt_minus_iva;
@@ -675,8 +737,10 @@
675 737 if ($actopt[0]['is_citytax'] == 1) {
676 738 $tot_city_taxes += $opt_minus_iva;
677 739 } elseif ($actopt[0]['is_fee'] == 1) {
678 740 $tot_fees += $opt_minus_iva;
741 + } elseif ($opt_params['damagedep'] ?? 0) {
742 + $tot_damage_dep += $opt_minus_iva;
679 743 }
680 744 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
681 745 if ($tmpopr == $realcost) {
682 746 $tot_taxes += ($realcost - $opt_minus_iva);
@@ -709,10 +773,10 @@
709 773 if (!empty($coupon) && $valid_customer_coupon) {
710 774 $coupondateok = true;
711 775 if (strlen((string)$coupon['datevalid'])) {
712 776 $dateparts = explode("-", $coupon['datevalid']);
713 - $pickinfo = getdate($pcheckin);
714 - $dropinfo = getdate($pcheckout);
777 + $pickinfo = $checkin_info;
778 + $dropinfo = $checkout_info;
715 779 $checkpick = mktime(0, 0, 0, $pickinfo['mon'], $pickinfo['mday'], $pickinfo['year']);
716 780 $checkdrop = mktime(0, 0, 0, $dropinfo['mon'], $dropinfo['mday'], $dropinfo['year']);
717 781 if (!($checkpick >= $dateparts[0] && $checkpick <= $dateparts[1] && $checkdrop >= $dateparts[0] && $checkdrop <= $dateparts[1])) {
718 782 $coupondateok = false;
@@ -767,13 +831,13 @@
767 831 * @since 1.16.0 (J) - 1.6.0 (WP) taxes are proportionally calculated when coupon before tax.
768 832 * @since 1.16.8 (J) - 1.6.8 (WP) with coupon before taxes, discounted amount calculation is an equal subtration.
769 833 */
770 834 $prev_isdue = $isdue;
771 - $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees);
835 + $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees - $tot_damage_dep);
772 836 $coupondiscount = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $coupon['value'] / 100;
773 837 $isdue = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $minuscoupon / 100;
774 838 $tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : $tot_taxes;
775 - $isdue += $coupon['excludetaxes'] ? ($tot_taxes + $tot_city_taxes + $tot_fees) : 0;
839 + $isdue += $coupon['excludetaxes'] ? ($tot_taxes + $tot_city_taxes + $tot_fees + $tot_damage_dep) : 0;
776 840 $coupondiscount = abs($prev_isdue - $isdue);
777 841 } else {
778 842 // total value
779 843 $coupondiscount = $coupon['value'];
@@ -792,9 +856,9 @@
792 856 }
793 857
794 858 $strisdue = number_format($isdue, 2) . 'vikbooking';
795 859 $ptotdue = number_format($ptotdue, 2) . 'vikbooking';
796 - if ($strisdue != $ptotdue) {
860 + if ($strisdue != $ptotdue && abs(round($isdue, 2) - round((float) $ptotdue, 2)) <= 0.01) {
797 861 showSelectVb(JText::translate('VBINCONGRTOT'));
798 862 return;
799 863 }
800 864
@@ -960,8 +1024,31 @@
960 1024 // push data to tracker for conversion
961 1025 $vbo_tracker = VikBooking::getTracker();
962 1026 $vbo_tracker->pushDates($pcheckin, $pcheckout, $pdays)->pushParty($arrpeople)->pushData('idcustomer', $cpin->getNewCustomerId());
963 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 +
964 1051 if (!$mod_booking && ((!empty($payment) && intval($payment['setconfirmed']) == 1) || !$must_payment || ($usedcoupon && $isdue <= 0))) {
965 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
966 1053 $arrbusy = [];
967 1054 foreach ($rooms as $num => $r) {
@@ -1011,12 +1098,22 @@
1011 1098 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1012 1099 $booking_record->tot_taxes = (float)$tot_taxes;
1013 1100 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1014 1101 $booking_record->tot_fees = (float)$tot_fees;
1102 + if ($tot_damage_dep) {
1103 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1104 + }
1015 1105 $booking_record->phone = $phone_number;
1016 1106 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1017 1107 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1018 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 +
1019 1116 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1020 1117
1021 1118 if (!isset($booking_record->id)) {
1022 1119 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1305,12 +1402,43 @@
1305 1402 if ($mod_booking['total'] < $isdue) {
1306 1403 $mod_paymcount++;
1307 1404 }
1308 1405
1309 - $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 +
1310 1431 $dbo->setQuery($q);
1311 1432 $dbo->execute();
1312 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 +
1313 1441 // remove the coupon used (should never been allowed for modifications)
1314 1442 if ($usedcoupon == true && $coupon['type'] == 2) {
1315 1443 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=".(int)$coupon['id'].";";
1316 1444 $dbo->setQuery($q);
@@ -1368,12 +1496,22 @@
1368 1496 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1369 1497 $booking_record->tot_taxes = (float)$tot_taxes;
1370 1498 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1371 1499 $booking_record->tot_fees = (float)$tot_fees;
1500 + if ($tot_damage_dep) {
1501 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1502 + }
1372 1503 $booking_record->phone = $phone_number;
1373 1504 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1374 1505 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1375 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 +
1376 1514 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1377 1515
1378 1516 if (!isset($booking_record->id)) {
1379 1517 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1499,9 +1637,20 @@
1499 1637 // VBO 1.11 - push data to tracker for conversion
1500 1638 $vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
1501 1639 $vbo_tracker->resetTrack();
1502 1640
1503 - $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));
1504 1653 }
1505 1654 }
1506 1655
1507 1656 public function vieworder()
@@ -1674,10 +1823,13 @@
1674 1823 }
1675 1824
1676 1825 public function notifypayment()
1677 1826 {
1827 + $app = JFactory::getApplication();
1678 1828 $dbo = JFactory::getDbo();
1679 1829
1830 + $session = JFactory::getSession();
1831 +
1680 1832 $config = VBOFactory::getConfig();
1681 1833 $av_helper = VikBooking::getAvailabilityInstance();
1682 1834
1683 1835 $psid = VikRequest::getString('sid', '', 'request');
@@ -1709,18 +1861,16 @@
1709 1861 $recipient_mail = $all_recipients;
1710 1862 }
1711 1863 }
1712 1864
1865 + // load booking details
1713 1866 $q = "SELECT * FROM `#__vikbooking_orders` WHERE (`sid`=" . $dbo->quote($psid) . " OR `idorderota`=" . $dbo->quote($psid) . ") AND `ts`=" . $dbo->quote($pts);
1714 1867 $dbo->setQuery($q, 0, 1);
1715 - $dbo->execute();
1716 - if (!$dbo->getNumRows()) {
1868 + $row = $dbo->loadAssoc();
1869 + if (!$row) {
1717 1870 VBOHttpDocument::getInstance()->close(404, 'Booking not found');
1718 1871 }
1719 1872
1720 - // load booking details
1721 - $row = $dbo->loadAssoc();
1722 -
1723 1873 // check if the language in use is the same as the one used during the checkout
1724 1874 if (!empty($row['lang'])) {
1725 1875 $lang = JFactory::getLanguage();
1726 1876 if ($lang->getTag() != $row['lang']) {
@@ -1735,9 +1885,12 @@
1735 1885 $vbo_tn = VikBooking::getTranslator();
1736 1886
1737 1887 if ($row['status'] == 'confirmed' && !(VikBooking::multiplePayments() && $row['paymcount'] > 0)) {
1738 1888 // booking can be paid only if not confirmed or if multiple payments are enabled and payment counter for booking greater than zero
1739 - 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 + }
1740 1893 }
1741 1894
1742 1895 /**
1743 1896 * Check split stay reservation data.
@@ -1812,12 +1965,34 @@
1812 1965 // inject values
1813 1966 $row['order_rooms'] = $orderrooms;
1814 1967 $row['fares'] = $tars;
1815 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
1816 1990 $isdue = 0;
1817 1991 $tot_taxes = 0;
1818 1992 $tot_city_taxes = 0;
1819 1993 $tot_fees = 0;
1994 + $tot_damage_dep = 0;
1820 1995 $pricestr = [];
1821 1996 $optstr = [];
1822 1997 foreach ($orderrooms as $kor => $or) {
1823 1998 $num = $kor + 1;
@@ -1866,8 +2041,13 @@
1866 2041 $dbo->setQuery($q);
1867 2042 $actopt = $dbo->loadAssocList();
1868 2043 if ($actopt) {
1869 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 +
1870 2050 $chvar = '';
1871 2051 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
1872 2052 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
1873 2053 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
@@ -1917,8 +2097,19 @@
1917 2097 }
1918 2098 if ($actopt[0]['perperson'] == 1) {
1919 2099 $realcost = $realcost * $or['adults'];
1920 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 +
1921 2112 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
1922 2113 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
1923 2114 if ($actopt[0]['is_citytax'] == 1) {
1924 2115 $tot_city_taxes += $opt_minus_tax;
@@ -1923,10 +2114,12 @@
1923 2114 if ($actopt[0]['is_citytax'] == 1) {
1924 2115 $tot_city_taxes += $opt_minus_tax;
1925 2116 } elseif ($actopt[0]['is_fee'] == 1) {
1926 2117 $tot_fees += $opt_minus_tax;
2118 + } elseif ($opt_params['damagedep'] ?? 0) {
2119 + $tot_damage_dep += $opt_minus_tax;
1927 2120 }
1928 - // 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
1929 2122 if ($tmpopr == $realcost) {
1930 2123 $tot_taxes += ($realcost - $opt_minus_tax);
1931 2124 } else {
1932 2125 $tot_taxes += ($tmpopr - $realcost);
@@ -1957,16 +2150,19 @@
1957 2150 $expcoupon = explode(";", $row['coupon']);
1958 2151 $isdue = $isdue - $expcoupon[1];
1959 2152 }
1960 2153
1961 - // invoke the payment method class
1962 - $exppay = explode('=', $row['idpayment']);
1963 - $payment = VikBooking::getPayment($exppay[0], $vbo_tn);
1964 -
1965 2154 if (empty($row['sid']) && !empty($row['idorderota']) && !empty($row['channel'])) {
1966 2155 $row['sid'] = $row['idorderota'];
1967 2156 }
1968 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 +
1969 2165 if (VBOPlatformDetection::isWordPress()) {
1970 2166 /**
1971 2167 * @wponly The payment gateway is now loaded
1972 2168 * using the apposite dispatcher.
@@ -1975,9 +2171,9 @@
1975 2171 */
1976 2172 JLoader::import('adapter.payment.dispatcher');
1977 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'];
1978 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'];
1979 - $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";
1980 2176 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
1981 2177 $itemid = $model->best(array('booking'), (!empty($row['lang']) ? $row['lang'] : null));
1982 2178 $extra_data = [];
1983 2179 if ($itemid) {
@@ -2008,9 +2204,9 @@
2008 2204 $bestitemid = VikBooking::findProperItemIdType(array('booking'));
2009 2205 $extra_data = array(
2010 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)),
2011 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)),
2012 - '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),
2013 2209 );
2014 2210 $extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();
2015 2211
2016 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
@@ -2015,9 +2211,20 @@
2015 2211
2016 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
2017 2213 }
2018 2214
2019 - $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
2020 2227 $newpaymentlog = date('c')."\n".$array_result['log']."\n----------\n".$row['paymentlog'];
2021 2228
2022 2229 /**
2023 2230 * OTA reservations containing PCI-DSS card details may receive additional payments through
@@ -2036,15 +2243,16 @@
2036 2243 * Ensure the size of the log does not make the query fail.
2037 2244 *
2038 2245 * @since 1.16.9 (J) - 1.6.9 (WP)
2039 2246 */
2040 - if (strlen($newpaymentlog) > 65000) {
2041 - $newpaymentlog = substr($newpaymentlog, 0, 65000) . '...';
2247 + if (strlen($newpaymentlog) > 55000) {
2248 + $newpaymentlog = substr($newpaymentlog, 0, 55000) . '...';
2042 2249 }
2043 2250
2044 2251 if ($array_result['verified'] == 1) {
2045 2252 // valid payment
2046 2253 $shouldpay = $isdue;
2254 +
2047 2255 if ($payment['charge'] > 0.00) {
2048 2256 if ($payment['ch_disc'] == 1) {
2049 2257 // charge
2050 2258 if ($payment['val_pcent'] == 1) {
@@ -2066,39 +2274,60 @@
2066 2274 $shouldpay -= $percent_to_pay;
2067 2275 }
2068 2276 }
2069 2277 }
2070 - //deposit may be skipped by customer choice
2278 +
2279 + // deposit may be skipped by customer choice
2071 2280 $shouldpay_befdep = $shouldpay;
2072 - //
2281 + $shouldpay_befdep_no_dd = $shouldpay - ($damage_deposit_payment['damagedep_gross'] ?? 0);
2282 +
2073 2283 if (!VikBooking::payTotal()) {
2074 2284 $percentdeposit = VikBooking::getAccPerCent();
2075 2285 if ($percentdeposit > 0) {
2076 2286 if (VikBooking::getTypeDeposit() == "fixed") {
2077 2287 $shouldpay = $percentdeposit;
2288 + $shouldpay_befdep_no_dd = $percentdeposit;
2078 2289 } else {
2079 2290 $shouldpay = $shouldpay * $percentdeposit / 100;
2291 + $shouldpay_befdep_no_dd = $shouldpay_befdep_no_dd * $percentdeposit / 100;;
2080 2292 }
2081 2293 }
2082 2294 }
2083 - //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
2084 2301 if (isset($array_result['tot_paid'])) {
2085 2302 $shouldpay = round($shouldpay, 2);
2086 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);
2087 2306 $totreceived = round($array_result['tot_paid'], 2);
2088 - if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $row['paymcount'] == 0) {
2089 - //the amount paid is different than the order total
2090 - //fares might have changed or the deposit might be different
2091 - //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
2092 2311 $vbo_app = VikBooking::getVboApplication();
2093 2312 $adsendermail = VikBooking::getSenderMail();
2094 - $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 + );
2095 2327 }
2096 - /**
2097 - * We store the amount paid before applying the charge for the transaction.
2098 - *
2099 - * @since 1.3.0
2100 - */
2328 +
2329 + // amount paid should be stored as exclusive of transaction fees/discounts
2101 2330 if ($payment['charge'] > 0.00) {
2102 2331 if ($payment['ch_disc'] == 1) {
2103 2332 // charge
2104 2333 if ($payment['val_pcent'] == 1) {
@@ -2175,9 +2404,15 @@
2175 2404 $booking_record->paymentlog = $newpaymentlog;
2176 2405 }
2177 2406 $booking_record->payable = $new_payable;
2178 2407
2179 - $dbo->updateObject('#__vikbooking_orders', $booking_record, 'id');
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 + }
2180 2415
2181 2416 // assign room specific unit
2182 2417 $set_room_indexes = VikBooking::autoRoomUnit();
2183 2418 $room_indexes_usemap = [];
@@ -2183,9 +2418,8 @@
2183 2418 $room_indexes_usemap = [];
2184 2419 if ($set_room_indexes === true) {
2185 2420 $q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
2186 2421 $dbo->setQuery($q);
2187 - $dbo->execute();
2188 2422 $orooms = $dbo->loadAssocList();
2189 2423 foreach ($orooms as $oroom) {
2190 2424 if (!empty($oroom['roomindex'])) {
2191 2425 // room specific unit has already been assigned
@@ -2218,8 +2452,13 @@
2218 2452 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
2219 2453 $dbo->setQuery($q);
2220 2454 $dbo->execute();
2221 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 +
2222 2461 // customer booking
2223 2462 $q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
2224 2463 $dbo->setQuery($q);
2225 2464 $customer_id = $dbo->loadResult();
@@ -2256,8 +2495,9 @@
2256 2495 *
2257 2496 * @since 1.14 (J) - 1.4.0 (WP)
2258 2497 * @since 1.16.2 (J) - 1.6.2 (WP) we attempt to always store the amount paid with this transaction.
2259 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.
2260 2500 */
2261 2501 $tn_data = $array_result['transaction'] ?? null;
2262 2502 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2263 2503 // check event data payload to store
@@ -2262,16 +2502,16 @@
2262 2502 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2263 2503 // check event data payload to store
2264 2504 if (is_array($tn_data)) {
2265 2505 // set key
2266 - $tn_data['amount_paid'] = (float)$array_result['tot_paid'];
2506 + $tn_data['amount_paid'] = (float) $array_result['tot_paid'];
2267 2507 } elseif (is_object($tn_data)) {
2268 2508 // set property
2269 - $tn_data->amount_paid = (float)$array_result['tot_paid'];
2509 + $tn_data->amount_paid = (float) $array_result['tot_paid'];
2270 2510 } elseif (!$tn_data) {
2271 2511 // build an array (we add the payment name because we know there is no other transaction data)
2272 2512 $tn_data = [
2273 - 'amount_paid' => (float)$array_result['tot_paid'],
2513 + 'amount_paid' => (float) $array_result['tot_paid'],
2274 2514 'payment_method' => $payment['name'],
2275 2515 ];
2276 2516 }
2277 2517 }
@@ -2278,14 +2518,23 @@
2278 2518 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
2279 2519 // check event data payload to store
2280 2520 if (is_array($tn_data)) {
2281 2521 // set key
2282 - $tn_data['processing_fees'] = (float)$array_result['tot_fees'];
2522 + $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
2283 2523 } elseif (is_object($tn_data)) {
2284 2524 // set property
2285 - $tn_data->processing_fees = (float)$array_result['tot_fees'];
2525 + $tn_data->processing_fees = (float) $array_result['tot_fees'];
2286 2526 }
2287 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 + }
2288 2537
2289 2538 // Booking History
2290 2539 VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);
2291 2540
@@ -2294,9 +2543,8 @@
2294 2543 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
2295 2544 $vcm = new SynchVikBooking($row['id']);
2296 2545 $vcm->setPushType('new')->sendRequest();
2297 2546 }
2298 - $session = JFactory::getSession();
2299 2547 $vcmchanneldata = $session->get('vcmChannelData', '');
2300 2548 if (!empty($vcmchanneldata)) {
2301 2549 $session->set('vcmChannelData', '');
2302 2550 }
@@ -2319,11 +2567,16 @@
2319 2567 $obj->afterValidation(0);
2320 2568 }
2321 2569 }
2322 2570 }
2323 -
2571 +
2324 2572 public function currencyconverter()
2325 2573 {
2574 + if (!JSession::checkToken()) {
2575 + // missing CSRF-proof token
2576 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2577 + }
2578 +
2326 2579 $session = JFactory::getSession();
2327 2580 $pprices = VikRequest::getVar('prices', array(0));
2328 2581 $pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
2329 2582 $ptocurrency = VikRequest::getString('tocurrency', '', 'request');
@@ -2367,8 +2620,13 @@
2367 2620 }
2368 2621
2369 2622 public function storesignature()
2370 2623 {
2624 + if (!JSession::checkToken()) {
2625 + // missing CSRF-proof token
2626 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2627 + }
2628 +
2371 2629 $sid = VikRequest::getString('sid', '', 'request');
2372 2630 $ts = VikRequest::getString('ts', '', 'request');
2373 2631 $psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
2374 2632 $ppad_width = VikRequest::getInt('pad_width', '', 'request');
@@ -2452,8 +2710,13 @@
2452 2710 }
2453 2711
2454 2712 public function validatepin()
2455 2713 {
2714 + if (!JSession::checkToken()) {
2715 + // missing CSRF-proof token
2716 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2717 + }
2718 +
2456 2719 $cpin = VikBooking::getCPinIstance();
2457 2720
2458 2721 $ppin = VikRequest::getString('pin', '', 'request');
2459 2722
@@ -2459,9 +2722,9 @@
2459 2722
2460 2723 $response = [];
2461 2724
2462 2725 $customer = $cpin->getCustomerByPin($ppin);
2463 - if (count($customer)) {
2726 + if ($customer) {
2464 2727 $response = $customer;
2465 2728 $response['success'] = 1;
2466 2729
2467 2730 if ($cpin->getCustomerCoupon($customer)) {
@@ -2622,8 +2885,15 @@
2622 2885 // push the transaction currency information
2623 2886 $order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();
2624 2887
2625 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 + /**
2626 2896 * @wponly The payment gateway is loaded
2627 2897 * through the apposite dispatcher.
2628 2898 */
2629 2899 JLoader::import('adapter.payment.dispatcher');
@@ -2879,16 +3149,14 @@
2879 3149 */
2880 3150 public function operatorlogin()
2881 3151 {
2882 3152 /**
2883 - * Extra security fix for the login form token.
2884 - *
2885 - * @since September 8th 2020
3153 + * Secure the login form token.
2886 3154 */
2887 - if (!JFactory::getSession()->checkToken()) {
2888 - 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);
2889 3158 }
2890 - //
2891 3159
2892 3160 $app = JFactory::getApplication();
2893 3161 $pauthcode = VikRequest::getString('authcode', '', 'request');
2894 3162 $pitemid = VikRequest::getInt('Itemid', '', 'request');
@@ -2929,39 +3197,44 @@
2929 3197 * @since 1.12
2930 3198 */
2931 3199 public function storeprecheckin()
2932 3200 {
3201 + if (!JSession::checkToken()) {
3202 + // missing CSRF-proof token
3203 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
3204 + }
3205 +
2933 3206 $dbo = JFactory::getDbo();
2934 3207 $app = JFactory::getApplication();
2935 - $sid = VikRequest::getString('sid', '', 'request');
2936 - $ts = VikRequest::getString('ts', '', 'request');
2937 - $pguests = VikRequest::getVar('guests', array());
2938 - $pitemid = VikRequest::getInt('Itemid', 0, 'request');
2939 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 +
2940 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';";
2941 3215 $dbo->setQuery($q);
2942 - $dbo->execute();
2943 - if (!$dbo->getNumRows()) {
3216 + $order = $dbo->loadAssoc();
3217 + if (!$order) {
2944 3218 throw new Exception('Booking not found', 404);
2945 3219 }
2946 - $order = $dbo->loadAssoc();
2947 3220
2948 - $orderrooms = array();
2949 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;";
2950 3222 $dbo->setQuery($q);
2951 - $dbo->execute();
2952 - if (!$dbo->getNumRows()) {
3223 + $orderrooms = $dbo->loadAssocList();
3224 + if (!$orderrooms) {
2953 3225 throw new Exception('No rooms found', 404);
2954 3226 }
2955 - $orderrooms = $dbo->loadAssocList();
2956 3227
3228 + // access the customer record
3229 + $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);
3230 +
2957 3231 $q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
2958 3232 $dbo->setQuery($q);
2959 - $dbo->execute();
2960 - if (!$dbo->getNumRows()) {
3233 + $custorder = $dbo->loadAssoc();
3234 + if (!$custorder) {
2961 3235 throw new Exception('No customer found', 404);
2962 3236 }
2963 - $custorder = $dbo->loadAssoc();
2964 3237
2965 3238 // booking details page
2966 3239 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
2967 3240 if (empty($order['sid']) && !empty($order['idorderota'])) {
@@ -2973,10 +3246,16 @@
2973 3246 $precheckin = VikBooking::precheckinEnabled();
2974 3247 if ($precheckin) {
2975 3248 // make sure the limit of days in advance is reflected
2976 3249 $precheckin_mind = VikBooking::precheckinMinOffset();
2977 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
2978 - $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 + }
2979 3258 }
2980 3259 if (!$precheckin) {
2981 3260 // raise error and redirect in case of website or OTA booking
2982 3261 VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
@@ -2992,13 +3271,18 @@
2992 3271
2993 3272 foreach ($pguests as $ind => $adults) {
2994 3273 foreach ($adults as $aduind => $details) {
2995 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 + }
2996 3280 if (!in_array($detkey, $front_keys)) {
2997 3281 // push the key of the guest details for later comparison
2998 3282 array_push($front_keys, $detkey);
2999 3283 }
3000 - if (strlen($detval)) {
3284 + if (is_scalar($detval) && strlen($detval)) {
3001 3285 // push value only if not empty
3002 3286 if (!isset($guests_details[$ind])) {
3003 3287 $guests_details[$ind] = array();
3004 3288 }
@@ -3027,8 +3311,13 @@
3027 3311 // current pax data include a guest not present, set it to not lose it
3028 3312 $guests_details[$ind][$aduind] = $details;
3029 3313 }
3030 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 + }
3031 3320 if (!in_array($detkey, $front_keys)) {
3032 3321 // merge this key probably reserved to the back-end
3033 3322 $guests_details[$ind][$aduind][$detkey] = $detval;
3034 3323 }
@@ -3044,8 +3333,20 @@
3044 3333
3045 3334 // Booking History
3046 3335 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');
3047 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 +
3048 3349 // print success message and redirect
3049 3350 $app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
3050 3351 $app->redirect($goto);
3051 3352 }
@@ -3067,24 +3368,23 @@
3067 3368 if (!$paddopt) {
3068 3369 throw new Exception('No extra services selected', 404);
3069 3370 }
3070 3371
3372 + // find the involved reservation, direct or OTA
3071 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) . ";";
3072 3374 $dbo->setQuery($q);
3073 - $dbo->execute();
3074 - if (!$dbo->getNumRows()) {
3375 + $order = $dbo->loadAssoc();
3376 + if (!$order) {
3075 3377 throw new Exception('Booking not found', 404);
3076 3378 }
3077 - $order = $dbo->loadAssoc();
3078 3379
3079 - $orderrooms = [];
3080 - $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;";
3081 3382 $dbo->setQuery($q);
3082 - $dbo->execute();
3083 - if (!$dbo->getNumRows()) {
3383 + $orderrooms = $dbo->loadAssocList();
3384 + if (!$orderrooms) {
3084 3385 throw new Exception('No rooms found', 404);
3085 3386 }
3086 - $orderrooms = $dbo->loadAssocList();
3087 3387
3088 3388 // availability helper
3089 3389 $av_helper = VikBooking::getAvailabilityInstance();
3090 3390
@@ -3101,20 +3401,56 @@
3101 3401 // load all valid and existing options as a security measure
3102 3402 $alloptions = [];
3103 3403 $q = "SELECT * FROM `#__vikbooking_optionals`;";
3104 3404 $dbo->setQuery($q);
3105 - $dbo->execute();
3106 - if (!$dbo->getNumRows()) {
3405 + $records = $dbo->loadAssocList();
3406 + if (!$records) {
3107 3407 throw new Exception('No options found', 404);
3108 3408 }
3109 - $records = $dbo->loadAssocList();
3110 3409 foreach ($records as $v) {
3111 - $alloptions[(int)$v['id']] = $v;
3410 + $alloptions[$v['id']] = $v;
3112 3411 }
3113 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
3114 3450 $extras_booked = [];
3115 3451 foreach ($orderrooms as $kor => $or) {
3116 - if (!isset($paddopt[$kor]) || !count($paddopt[$kor])) {
3452 + if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
3117 3453 continue;
3118 3454 }
3119 3455
3120 3456 // determine proper nights of stay
@@ -3130,27 +3466,65 @@
3130 3466 if (strpos($or['optionals'], $optid . ':') === 0 || strpos($or['optionals'], ';' . $optid . ':') > 0) {
3131 3467 // this option has already been booked, skip it
3132 3468 continue;
3133 3469 }
3134 - if (!isset($alloptions[(int)$optid])) {
3470 + if (!isset($alloptions[$optid])) {
3135 3471 // this option ID does not exist, skip it
3136 3472 continue;
3137 3473 }
3138 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 +
3139 3512 // push option booked
3140 - array_push($extras_booked, array(
3141 - 'id' => $optid,
3142 - 'idroom' => $or['idroom'],
3143 - 'name' => $alloptions[(int)$optid]['name'],
3144 - 'quant' => $quant,
3513 + $extras_booked[] = [
3514 + 'id' => $optid,
3515 + 'idroom' => $or['idroom'],
3516 + 'name' => $alloptions[$optid]['name'],
3517 + 'quant' => $quant,
3145 3518 'room_cost' => (!empty($or['cust_cost']) ? $or['cust_cost'] : $or['room_cost']),
3146 3519 'room_name' => $or['room_name'],
3147 - 'optcost' => $alloptions[(int)$optid]['cost'],
3148 - 'adults' => $or['adults'],
3149 - 'children' => $or['children'],
3150 - 'nights' => $room_stay_nights,
3151 - ));
3520 + 'optcost' => $alloptions[$optid]['cost'],
3521 + 'adults' => $or['adults'],
3522 + 'children' => $or['children'],
3523 + 'nights' => $room_stay_nights,
3524 + ];
3152 3525 }
3526 +
3153 3527 // update options for this room record
3154 3528 $newoptstr = $or['optionals'] . $extraoptstr;
3155 3529 $q = "UPDATE `#__vikbooking_ordersrooms` SET `optionals`=" . $dbo->quote($newoptstr) . " WHERE `id`={$or['id']};";
3156 3530 $dbo->setQuery($q);
@@ -3160,8 +3534,9 @@
3160 3534 // increase booking total amount and build event log for the history
3161 3535 $currency = VikBooking::getCurrencySymb();
3162 3536 $totrooms = count($orderrooms);
3163 3537 $increase = 0;
3538 + $future_dd = 0;
3164 3539 $add_tax = 0;
3165 3540 $extraslog = [];
3166 3541 foreach ($extras_booked as $extra) {
3167 3542 $o = $alloptions[(int)$extra['id']];
@@ -3179,34 +3554,100 @@
3179 3554 if ($o['perperson'] == 1) {
3180 3555 $optcost = $optcost * $extra['adults'];
3181 3556 }
3182 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 +
3183 3569 $floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
3184 3570 $netoptprice = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
3185 3571 $increase += $floatoptprice;
3186 3572 $add_tax += $floatoptprice - $netoptprice;
3187 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 + }
3188 3579 }
3189 3580
3190 3581 $newtotbooking = $order['total'] + $increase;
3191 3582 $new_tot_taxes = $order['tot_taxes'] + $add_tax;
3583 +
3192 3584 /**
3193 3585 * Important: the 'paymcount' should be increased only if the status is
3194 3586 * "confirmed" or no rooms may be occupied when receiving a payment.
3195 3587 */
3196 - $q = "UPDATE `#__vikbooking_orders` SET `total`=" . $dbo->quote($newtotbooking) . ", `paymcount`=" . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']) . ", `tot_taxes`=" . $dbo->q($new_tot_taxes) . ", `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
3197 3624 $dbo->setQuery($q);
3198 3625 $dbo->execute();
3199 3626
3200 3627 // Booking History
3201 - VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('UE', implode("\n", $extraslog));
3628 + VikBooking::getBookingHistoryInstance($order['id'])->store('UE', implode("\n", $extraslog));
3202 3629
3203 - // send email notification to guest and admin
3204 - 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 + }
3205 3640
3641 + if ($send_notifications) {
3642 + // send email notification to guest and admin
3643 + VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
3644 + }
3645 +
3206 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);
3207 3647 $app->enqueueMessage(JText::translate('VBOUPSELLRESULTOK'));
3208 3648 $app->redirect($goto);
3649 + $app->close();
3209 3650 }
3210 3651
3211 3652 /**
3212 3653 * Submits a new review.
@@ -3329,9 +3770,9 @@
3329 3770
3330 3771 // reviewer information
3331 3772 $review_content->reviewer = new stdClass;
3332 3773 $customer_name = '';
3333 - if (count($customer)) {
3774 + if ($customer) {
3334 3775 $review_content->reviewer->name = $customer['first_name'];
3335 3776 $review_content->reviewer->country_code = $customer['country'];
3336 3777 $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
3337 3778 } else {
@@ -3855,8 +4296,13 @@
3855 4296 * @since 1.13.5
3856 4297 */
3857 4298 public function add_roomdaynote()
3858 4299 {
4300 + if (!JSession::checkToken()) {
4301 + // missing CSRF-proof token
4302 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4303 + }
4304 +
3859 4305 $dt = VikRequest::getString('dt', '', 'request');
3860 4306 $idroom = VikRequest::getInt('idroom', 0, 'request');
3861 4307 $subunit = VikRequest::getInt('subunit', 0, 'request');
3862 4308 $type = VikRequest::getString('type', '', 'request');
@@ -3921,21 +4367,26 @@
3921 4367
3922 4368 /**
3923 4369 * AJAX endpoint to upload customer documents during the pre-checkin.
3924 4370 *
3925 - * @throws Exception
3926 - *
3927 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.
3928 4373 */
3929 4374 public function precheckin_upload_docs()
3930 4375 {
3931 - $dbo = JFactory::getDbo();
3932 - $app = JFactory::getApplication();
3933 - $input = $app->input;
4376 + $app = JFactory::getApplication();
4377 + $dbo = JFactory::getDbo();
4378 + $input = $app->input;
3934 4379
3935 - // get request values
3936 - $order_sid = $input->getString('sid', '');
3937 - $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);
3938 4389 /**
3939 4390 * This is a simple file uploading process, but if
3940 4391 * we wanted to automatically update the pax_data,
3941 4392 * the request vars below would be necessary.
@@ -3941,31 +4392,29 @@
3941 4392 * the request vars below would be necessary.
3942 4393 */
3943 4394 $room_index = $input->getInt('room_index', 0);
3944 4395 $guest_index = $input->getInt('guest_index', 0);
3945 - $pax_index = $input->getString('pax_index', '');
4396 + $pax_index = $input->getString('pax_index', '');
3946 4397
3947 4398 if (empty($order_sid) || empty($order_ts)) {
3948 - throw new Exception('Missing booking details', 404);
4399 + VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
3949 4400 }
3950 4401
3951 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';";
3952 4403 $dbo->setQuery($q);
3953 - $dbo->execute();
3954 - if (!$dbo->getNumRows()) {
3955 - throw new Exception('Booking not found', 404);
4404 + $order = $dbo->loadAssoc();
4405 + if (!$order) {
4406 + VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
3956 4407 }
3957 - $order = $dbo->loadAssoc();
3958 4408
3959 4409 $customer = array();
3960 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'].";";
3961 4411 $dbo->setQuery($q);
3962 - $dbo->execute();
3963 - if (!$dbo->getNumRows()) {
4412 + $customer = $dbo->loadObject();
4413 + if (!$customer) {
3964 4414 // one customer must be assigned to this booking for the pre-checkin
3965 - throw new Exception('No customers associated to this booking', 404);
4415 + VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
3966 4416 }
3967 - $customer = $dbo->loadObject();
3968 4417
3969 4418 // make sure pre-checkin is allowed
3970 4419 $precheckin = VikBooking::precheckinEnabled();
3971 4420 if ($precheckin) {
@@ -3970,25 +4419,31 @@
3970 4419 $precheckin = VikBooking::precheckinEnabled();
3971 4420 if ($precheckin) {
3972 4421 // make sure the limit of days in advance is reflected
3973 4422 $precheckin_mind = VikBooking::precheckinMinOffset();
3974 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3975 - $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 + }
3976 4431 }
3977 4432 if (!$precheckin) {
3978 - throw new Exception('Pre-checkin not allowed at this time', 403);
4433 + VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
3979 4434 }
3980 4435
3981 4436 // get uploaded files array (use "raw" to avoid filtering the file to upload)
3982 4437 $files = $input->files->get('docs', array(), 'raw');
3983 - if (!count($files)) {
3984 - throw new Exception('No files to be uploaded', 500);
4438 + if (!$files) {
4439 + VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
3985 4440 }
3986 4441
3987 4442 if (isset($files['name'])) {
3988 4443 // we have a single associative array, we need to push it within a list,
3989 4444 // because the upload iterates the $files array
3990 - $files = array($files);
4445 + $files = [$files];
3991 4446 }
3992 4447
3993 4448 // fetch documents folder path
3994 4449 $dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
@@ -3998,28 +4453,26 @@
3998 4453 // randomize string
3999 4454 $customer->seed = uniqid();
4000 4455
4001 4456 // create blocks for hashed folder
4002 - $parts = array(
4457 + $parts = [
4003 4458 $customer->first_name,
4004 4459 $customer->last_name,
4005 4460 md5(serialize($customer)),
4006 - );
4461 + ];
4007 4462
4008 4463 // join fetched parts
4009 4464 $customer->docsfolder = strtolower(implode('-', array_filter($parts)));
4010 4465
4011 4466 if (strlen($customer->docsfolder) < 16) {
4012 - 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.');
4013 4468 }
4014 4469
4015 - jimport('joomla.filesystem.folder');
4016 -
4017 4470 // create a folder for this customer
4018 4471 $created = JFolder::create($dirpath . $customer->docsfolder);
4019 4472
4020 4473 if (!$created) {
4021 - 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));
4022 4475 }
4023 4476
4024 4477 unset($customer->seed);
4025 4478
@@ -4029,10 +4482,12 @@
4029 4482 $record->docsfolder = $customer->docsfolder;
4030 4483 $dbo->updateObject('#__vikbooking_customers', $record, 'id');
4031 4484 }
4032 4485
4033 - // prepare the response array of uploaded-file objects
4034 - $response = array();
4486 + // prepare the response with the uploaded-file objects
4487 + $response = [
4488 + 'uploads' => [],
4489 + ];
4035 4490 $upload_err = null;
4036 4491
4037 4492 // compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
4038 4493 $file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';
@@ -4050,13 +4505,13 @@
4050 4505 }
4051 4506 // always prepend "pre check-in" to the original file name
4052 4507 $file['name'] = strtolower($file_prefix . $file['name']);
4053 4508 // try to upload the file
4054 - $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');
4055 4510 // set a valid URL for the uploaded file
4056 4511 $result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
4057 4512 // push uploaded file
4058 - array_push($response, $result);
4513 + array_push($response['uploads'], $result);
4059 4514 }
4060 4515
4061 4516 } catch (Exception $e) {
4062 4517 // do nothing, but catch the error
@@ -4062,15 +4517,56 @@
4062 4517 // do nothing, but catch the error
4063 4518 $upload_err = $e;
4064 4519 }
4065 4520
4066 - if (!count($response)) {
4067 - 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');
4068 4527 }
4069 4528
4070 - // output the response
4071 - echo json_encode($response);
4072 - 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);
4073 4569 }
4074 4570
4075 4571 /**
4076 4572 * AJAX endpoint to submit an inquiry/information request.
@@ -4096,11 +4592,11 @@
4096 4592 $checkout_h = $input->getInt('checkouth', 0);
4097 4593 $checkout_m = $input->getInt('checkoutm', 0);
4098 4594 $categories = $input->getString('categories', '');
4099 4595 $roomsnum = $input->getInt('roomsnum', 1);
4100 - $adults = $input->get('adults', array(), 'int');
4101 - $children = $input->get('children', array(), 'int');
4102 - $inquiry = $input->get('inquiry', array(), 'raw');
4596 + $adults = $input->get('adults', [], 'int');
4597 + $children = $input->get('children', [], 'int');
4598 + $inquiry = $input->get('inquiry', [], 'array');
4103 4599 $ulang = $input->getString('ulang', '');
4104 4600
4105 4601 $timeopst = VikBooking::getTimeOpenStore();
4106 4602 if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
@@ -4145,8 +4641,10 @@
4145 4641 if (!is_scalar($info_val) || !strlen($info_val)) {
4146 4642 // empty or invalid field
4147 4643 continue;
4148 4644 }
4645 + // accept only text from submitted value
4646 + $info_val = strip_tags($info_val);
4149 4647 if ($info_type == 'nominative') {
4150 4648 if (empty($t_first_name)) {
4151 4649 $t_first_name = $info_val;
4152 4650 $res_custdata[JText::translate('VBNAME')] = $info_val;
@@ -4575,6 +5073,140 @@
4575 5073 public function crontab()
4576 5074 {
4577 5075 VBOFactory::getCrontabSimulator()->run();
4578 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();
4579 5211 }
4580 5212 }