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 +856 -149 1.6.8 → 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;
@@ -766,13 +831,13 @@
766 831 * @since 1.16.0 (J) - 1.6.0 (WP) taxes are proportionally calculated when coupon before tax.
767 832 * @since 1.16.8 (J) - 1.6.8 (WP) with coupon before taxes, discounted amount calculation is an equal subtration.
768 833 */
769 834 $prev_isdue = $isdue;
770 - $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees);
835 + $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees - $tot_damage_dep);
771 836 $coupondiscount = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $coupon['value'] / 100;
772 837 $isdue = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $minuscoupon / 100;
773 838 $tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : $tot_taxes;
774 - $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;
775 840 $coupondiscount = abs($prev_isdue - $isdue);
776 841 } else {
777 842 // total value
778 843 $coupondiscount = $coupon['value'];
@@ -791,9 +856,9 @@
791 856 }
792 857
793 858 $strisdue = number_format($isdue, 2) . 'vikbooking';
794 859 $ptotdue = number_format($ptotdue, 2) . 'vikbooking';
795 - if ($strisdue != $ptotdue) {
860 + if ($strisdue != $ptotdue && abs(round($isdue, 2) - round((float) $ptotdue, 2)) <= 0.01) {
796 861 showSelectVb(JText::translate('VBINCONGRTOT'));
797 862 return;
798 863 }
799 864
@@ -959,8 +1024,31 @@
959 1024 // push data to tracker for conversion
960 1025 $vbo_tracker = VikBooking::getTracker();
961 1026 $vbo_tracker->pushDates($pcheckin, $pcheckout, $pdays)->pushParty($arrpeople)->pushData('idcustomer', $cpin->getNewCustomerId());
962 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 +
963 1051 if (!$mod_booking && ((!empty($payment) && intval($payment['setconfirmed']) == 1) || !$must_payment || ($usedcoupon && $isdue <= 0))) {
964 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
965 1053 $arrbusy = [];
966 1054 foreach ($rooms as $num => $r) {
@@ -1010,12 +1098,22 @@
1010 1098 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1011 1099 $booking_record->tot_taxes = (float)$tot_taxes;
1012 1100 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1013 1101 $booking_record->tot_fees = (float)$tot_fees;
1102 + if ($tot_damage_dep) {
1103 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1104 + }
1014 1105 $booking_record->phone = $phone_number;
1015 1106 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1016 1107 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1017 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 +
1018 1116 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1019 1117
1020 1118 if (!isset($booking_record->id)) {
1021 1119 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1304,12 +1402,43 @@
1304 1402 if ($mod_booking['total'] < $isdue) {
1305 1403 $mod_paymcount++;
1306 1404 }
1307 1405
1308 - $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 +
1309 1431 $dbo->setQuery($q);
1310 1432 $dbo->execute();
1311 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 +
1312 1441 // remove the coupon used (should never been allowed for modifications)
1313 1442 if ($usedcoupon == true && $coupon['type'] == 2) {
1314 1443 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=".(int)$coupon['id'].";";
1315 1444 $dbo->setQuery($q);
@@ -1367,12 +1496,22 @@
1367 1496 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1368 1497 $booking_record->tot_taxes = (float)$tot_taxes;
1369 1498 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1370 1499 $booking_record->tot_fees = (float)$tot_fees;
1500 + if ($tot_damage_dep) {
1501 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1502 + }
1371 1503 $booking_record->phone = $phone_number;
1372 1504 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1373 1505 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1374 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 +
1375 1514 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1376 1515
1377 1516 if (!isset($booking_record->id)) {
1378 1517 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1498,9 +1637,20 @@
1498 1637 // VBO 1.11 - push data to tracker for conversion
1499 1638 $vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
1500 1639 $vbo_tracker->resetTrack();
1501 1640
1502 - $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));
1503 1653 }
1504 1654 }
1505 1655
1506 1656 public function vieworder()
@@ -1673,10 +1823,13 @@
1673 1823 }
1674 1824
1675 1825 public function notifypayment()
1676 1826 {
1827 + $app = JFactory::getApplication();
1677 1828 $dbo = JFactory::getDbo();
1678 1829
1830 + $session = JFactory::getSession();
1831 +
1679 1832 $config = VBOFactory::getConfig();
1680 1833 $av_helper = VikBooking::getAvailabilityInstance();
1681 1834
1682 1835 $psid = VikRequest::getString('sid', '', 'request');
@@ -1708,18 +1861,16 @@
1708 1861 $recipient_mail = $all_recipients;
1709 1862 }
1710 1863 }
1711 1864
1865 + // load booking details
1712 1866 $q = "SELECT * FROM `#__vikbooking_orders` WHERE (`sid`=" . $dbo->quote($psid) . " OR `idorderota`=" . $dbo->quote($psid) . ") AND `ts`=" . $dbo->quote($pts);
1713 1867 $dbo->setQuery($q, 0, 1);
1714 - $dbo->execute();
1715 - if (!$dbo->getNumRows()) {
1868 + $row = $dbo->loadAssoc();
1869 + if (!$row) {
1716 1870 VBOHttpDocument::getInstance()->close(404, 'Booking not found');
1717 1871 }
1718 1872
1719 - // load booking details
1720 - $row = $dbo->loadAssoc();
1721 -
1722 1873 // check if the language in use is the same as the one used during the checkout
1723 1874 if (!empty($row['lang'])) {
1724 1875 $lang = JFactory::getLanguage();
1725 1876 if ($lang->getTag() != $row['lang']) {
@@ -1734,9 +1885,12 @@
1734 1885 $vbo_tn = VikBooking::getTranslator();
1735 1886
1736 1887 if ($row['status'] == 'confirmed' && !(VikBooking::multiplePayments() && $row['paymcount'] > 0)) {
1737 1888 // booking can be paid only if not confirmed or if multiple payments are enabled and payment counter for booking greater than zero
1738 - 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 + }
1739 1893 }
1740 1894
1741 1895 /**
1742 1896 * Check split stay reservation data.
@@ -1811,12 +1965,34 @@
1811 1965 // inject values
1812 1966 $row['order_rooms'] = $orderrooms;
1813 1967 $row['fares'] = $tars;
1814 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
1815 1990 $isdue = 0;
1816 1991 $tot_taxes = 0;
1817 1992 $tot_city_taxes = 0;
1818 1993 $tot_fees = 0;
1994 + $tot_damage_dep = 0;
1819 1995 $pricestr = [];
1820 1996 $optstr = [];
1821 1997 foreach ($orderrooms as $kor => $or) {
1822 1998 $num = $kor + 1;
@@ -1865,8 +2041,13 @@
1865 2041 $dbo->setQuery($q);
1866 2042 $actopt = $dbo->loadAssocList();
1867 2043 if ($actopt) {
1868 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 +
1869 2050 $chvar = '';
1870 2051 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
1871 2052 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
1872 2053 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
@@ -1916,8 +2097,19 @@
1916 2097 }
1917 2098 if ($actopt[0]['perperson'] == 1) {
1918 2099 $realcost = $realcost * $or['adults'];
1919 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 +
1920 2112 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
1921 2113 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
1922 2114 if ($actopt[0]['is_citytax'] == 1) {
1923 2115 $tot_city_taxes += $opt_minus_tax;
@@ -1922,10 +2114,12 @@
1922 2114 if ($actopt[0]['is_citytax'] == 1) {
1923 2115 $tot_city_taxes += $opt_minus_tax;
1924 2116 } elseif ($actopt[0]['is_fee'] == 1) {
1925 2117 $tot_fees += $opt_minus_tax;
2118 + } elseif ($opt_params['damagedep'] ?? 0) {
2119 + $tot_damage_dep += $opt_minus_tax;
1926 2120 }
1927 - // 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
1928 2122 if ($tmpopr == $realcost) {
1929 2123 $tot_taxes += ($realcost - $opt_minus_tax);
1930 2124 } else {
1931 2125 $tot_taxes += ($tmpopr - $realcost);
@@ -1956,16 +2150,19 @@
1956 2150 $expcoupon = explode(";", $row['coupon']);
1957 2151 $isdue = $isdue - $expcoupon[1];
1958 2152 }
1959 2153
1960 - // invoke the payment method class
1961 - $exppay = explode('=', $row['idpayment']);
1962 - $payment = VikBooking::getPayment($exppay[0], $vbo_tn);
1963 -
1964 2154 if (empty($row['sid']) && !empty($row['idorderota']) && !empty($row['channel'])) {
1965 2155 $row['sid'] = $row['idorderota'];
1966 2156 }
1967 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 +
1968 2165 if (VBOPlatformDetection::isWordPress()) {
1969 2166 /**
1970 2167 * @wponly The payment gateway is now loaded
1971 2168 * using the apposite dispatcher.
@@ -1974,9 +2171,9 @@
1974 2171 */
1975 2172 JLoader::import('adapter.payment.dispatcher');
1976 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'];
1977 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'];
1978 - $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";
1979 2176 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
1980 2177 $itemid = $model->best(array('booking'), (!empty($row['lang']) ? $row['lang'] : null));
1981 2178 $extra_data = [];
1982 2179 if ($itemid) {
@@ -2007,9 +2204,9 @@
2007 2204 $bestitemid = VikBooking::findProperItemIdType(array('booking'));
2008 2205 $extra_data = array(
2009 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)),
2010 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)),
2011 - '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),
2012 2209 );
2013 2210 $extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();
2014 2211
2015 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
@@ -2014,9 +2211,20 @@
2014 2211
2015 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
2016 2213 }
2017 2214
2018 - $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
2019 2227 $newpaymentlog = date('c')."\n".$array_result['log']."\n----------\n".$row['paymentlog'];
2020 2228
2021 2229 /**
2022 2230 * OTA reservations containing PCI-DSS card details may receive additional payments through
@@ -2030,11 +2238,21 @@
2030 2238 $newpaymentlog = $row['paymentlog'] . "\n----------\n" . date('c') . "\n" . $array_result['log'];
2031 2239 }
2032 2240 }
2033 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 +
2034 2251 if ($array_result['verified'] == 1) {
2035 2252 // valid payment
2036 2253 $shouldpay = $isdue;
2254 +
2037 2255 if ($payment['charge'] > 0.00) {
2038 2256 if ($payment['ch_disc'] == 1) {
2039 2257 // charge
2040 2258 if ($payment['val_pcent'] == 1) {
@@ -2056,39 +2274,60 @@
2056 2274 $shouldpay -= $percent_to_pay;
2057 2275 }
2058 2276 }
2059 2277 }
2060 - //deposit may be skipped by customer choice
2278 +
2279 + // deposit may be skipped by customer choice
2061 2280 $shouldpay_befdep = $shouldpay;
2062 - //
2281 + $shouldpay_befdep_no_dd = $shouldpay - ($damage_deposit_payment['damagedep_gross'] ?? 0);
2282 +
2063 2283 if (!VikBooking::payTotal()) {
2064 2284 $percentdeposit = VikBooking::getAccPerCent();
2065 2285 if ($percentdeposit > 0) {
2066 2286 if (VikBooking::getTypeDeposit() == "fixed") {
2067 2287 $shouldpay = $percentdeposit;
2288 + $shouldpay_befdep_no_dd = $percentdeposit;
2068 2289 } else {
2069 2290 $shouldpay = $shouldpay * $percentdeposit / 100;
2291 + $shouldpay_befdep_no_dd = $shouldpay_befdep_no_dd * $percentdeposit / 100;;
2070 2292 }
2071 2293 }
2072 2294 }
2073 - //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
2074 2301 if (isset($array_result['tot_paid'])) {
2075 2302 $shouldpay = round($shouldpay, 2);
2076 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);
2077 2306 $totreceived = round($array_result['tot_paid'], 2);
2078 - if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $row['paymcount'] == 0) {
2079 - //the amount paid is different than the order total
2080 - //fares might have changed or the deposit might be different
2081 - //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
2082 2311 $vbo_app = VikBooking::getVboApplication();
2083 2312 $adsendermail = VikBooking::getSenderMail();
2084 - $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 + );
2085 2327 }
2086 - /**
2087 - * We store the amount paid before applying the charge for the transaction.
2088 - *
2089 - * @since 1.3.0
2090 - */
2328 +
2329 + // amount paid should be stored as exclusive of transaction fees/discounts
2091 2330 if ($payment['charge'] > 0.00) {
2092 2331 if ($payment['ch_disc'] == 1) {
2093 2332 // charge
2094 2333 if ($payment['val_pcent'] == 1) {
@@ -2152,12 +2391,29 @@
2152 2391 // update payable amount in case of up-sells or simply in case of another payment received
2153 2392 $new_payable = isset($array_result['tot_paid']) && $array_result['tot_paid'] ? ($row['payable'] - $array_result['tot_paid']) : 0;
2154 2393 $new_payable = $new_payable < 0 ? 0 : $new_payable;
2155 2394
2156 - $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'] . "';";
2157 - $dbo->setQuery($q);
2158 - $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;
2159 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 +
2160 2416 // assign room specific unit
2161 2417 $set_room_indexes = VikBooking::autoRoomUnit();
2162 2418 $room_indexes_usemap = [];
2163 2419 if ($set_room_indexes === true) {
@@ -2162,9 +2418,8 @@
2162 2418 $room_indexes_usemap = [];
2163 2419 if ($set_room_indexes === true) {
2164 2420 $q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
2165 2421 $dbo->setQuery($q);
2166 - $dbo->execute();
2167 2422 $orooms = $dbo->loadAssocList();
2168 2423 foreach ($orooms as $oroom) {
2169 2424 if (!empty($oroom['roomindex'])) {
2170 2425 // room specific unit has already been assigned
@@ -2197,8 +2452,13 @@
2197 2452 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
2198 2453 $dbo->setQuery($q);
2199 2454 $dbo->execute();
2200 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 +
2201 2461 // customer booking
2202 2462 $q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
2203 2463 $dbo->setQuery($q);
2204 2464 $customer_id = $dbo->loadResult();
@@ -2209,14 +2469,27 @@
2209 2469
2210 2470 // check if some of the rooms booked have shared calendars
2211 2471 VikBooking::updateSharedCalendars($row['id'], array(), $row['checkin'], $row['checkout']);
2212 2472
2213 - // send email notification to guest and admin
2214 - 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 + }
2215 2483
2216 - // SMS
2217 - VikBooking::sendBookingSMS($row['id']);
2484 + if ($send_notifications) {
2485 + // send email notification to guest and admin
2486 + VikBooking::sendBookingEmail($row['id'], array('guest', 'admin'));
2218 2487
2488 + // SMS
2489 + VikBooking::sendBookingSMS($row['id']);
2490 + }
2491 +
2219 2492 /**
2220 2493 * Payment gateways may set and return the transaction information
2221 2494 * to eventually support a later transaction of type refund.
2222 2495 *
@@ -2221,26 +2494,47 @@
2221 2494 * to eventually support a later transaction of type refund.
2222 2495 *
2223 2496 * @since 1.14 (J) - 1.4.0 (WP)
2224 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.
2225 2500 */
2226 - $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
2501 + $tn_data = $array_result['transaction'] ?? null;
2227 2502 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2228 2503 // check event data payload to store
2229 2504 if (is_array($tn_data)) {
2230 2505 // set key
2231 - $tn_data['amount_paid'] = (float)$array_result['tot_paid'];
2506 + $tn_data['amount_paid'] = (float) $array_result['tot_paid'];
2232 2507 } elseif (is_object($tn_data)) {
2233 2508 // set property
2234 - $tn_data->amount_paid = (float)$array_result['tot_paid'];
2509 + $tn_data->amount_paid = (float) $array_result['tot_paid'];
2235 2510 } elseif (!$tn_data) {
2236 2511 // build an array (we add the payment name because we know there is no other transaction data)
2237 2512 $tn_data = [
2238 - 'amount_paid' => (float)$array_result['tot_paid'],
2513 + 'amount_paid' => (float) $array_result['tot_paid'],
2239 2514 'payment_method' => $payment['name'],
2240 2515 ];
2241 2516 }
2242 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 + }
2243 2537
2244 2538 // Booking History
2245 2539 VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);
2246 2540
@@ -2249,9 +2543,8 @@
2249 2543 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
2250 2544 $vcm = new SynchVikBooking($row['id']);
2251 2545 $vcm->setPushType('new')->sendRequest();
2252 2546 }
2253 - $session = JFactory::getSession();
2254 2547 $vcmchanneldata = $session->get('vcmChannelData', '');
2255 2548 if (!empty($vcmchanneldata)) {
2256 2549 $session->set('vcmChannelData', '');
2257 2550 }
@@ -2274,11 +2567,16 @@
2274 2567 $obj->afterValidation(0);
2275 2568 }
2276 2569 }
2277 2570 }
2278 -
2571 +
2279 2572 public function currencyconverter()
2280 2573 {
2574 + if (!JSession::checkToken()) {
2575 + // missing CSRF-proof token
2576 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2577 + }
2578 +
2281 2579 $session = JFactory::getSession();
2282 2580 $pprices = VikRequest::getVar('prices', array(0));
2283 2581 $pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
2284 2582 $ptocurrency = VikRequest::getString('tocurrency', '', 'request');
@@ -2322,8 +2620,13 @@
2322 2620 }
2323 2621
2324 2622 public function storesignature()
2325 2623 {
2624 + if (!JSession::checkToken()) {
2625 + // missing CSRF-proof token
2626 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2627 + }
2628 +
2326 2629 $sid = VikRequest::getString('sid', '', 'request');
2327 2630 $ts = VikRequest::getString('ts', '', 'request');
2328 2631 $psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
2329 2632 $ppad_width = VikRequest::getInt('pad_width', '', 'request');
@@ -2407,8 +2710,13 @@
2407 2710 }
2408 2711
2409 2712 public function validatepin()
2410 2713 {
2714 + if (!JSession::checkToken()) {
2715 + // missing CSRF-proof token
2716 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2717 + }
2718 +
2411 2719 $cpin = VikBooking::getCPinIstance();
2412 2720
2413 2721 $ppin = VikRequest::getString('pin', '', 'request');
2414 2722
@@ -2414,9 +2722,9 @@
2414 2722
2415 2723 $response = [];
2416 2724
2417 2725 $customer = $cpin->getCustomerByPin($ppin);
2418 - if (count($customer)) {
2726 + if ($customer) {
2419 2727 $response = $customer;
2420 2728 $response['success'] = 1;
2421 2729
2422 2730 if ($cpin->getCustomerCoupon($customer)) {
@@ -2577,8 +2885,15 @@
2577 2885 // push the transaction currency information
2578 2886 $order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();
2579 2887
2580 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 + /**
2581 2896 * @wponly The payment gateway is loaded
2582 2897 * through the apposite dispatcher.
2583 2898 */
2584 2899 JLoader::import('adapter.payment.dispatcher');
@@ -2596,8 +2911,21 @@
2596 2911 $admin_refund_error .= !empty($array_result['log']) && is_string($array_result['log']) ? $array_result['log'] : '';
2597 2912 } else {
2598 2913 // refund was successful
2599 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 +
2600 2928 // update total paid, total and refund columns for the booking
2601 2929 $booking = new stdClass;
2602 2930 $booking->id = $order[0]['id'];
2603 2931 if ($order[0]['totpaid'] > 0) {
@@ -2610,12 +2938,12 @@
2610 2938 // update record in db
2611 2939 $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
2612 2940
2613 2941 // store the refund event
2614 - $event_descr = array(
2942 + $event_descr = [
2615 2943 '(' . $payment['name'] . ')',
2616 2944 $currencysymb . ' ' . VikBooking::numberFormat($refund_amount),
2617 - );
2945 + ];
2618 2946 $history_obj->store('RF', implode("\n", $event_descr));
2619 2947 }
2620 2948 }
2621 2949 }
@@ -2704,9 +3032,13 @@
2704 3032 VikRequest::setVar('start_date', $start_date);
2705 3033 VikRequest::setVar('end_date', $end_date);
2706 3034 //make call to get the result
2707 3035 TACVBO::$getArray = true;
2708 - $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 + ]);
2709 3041 //validate response
2710 3042 if (!is_array($website_rates)) {
2711 3043 //error returned
2712 3044 echo json_encode(array('e4j.error' => $website_rates));
@@ -2817,16 +3149,14 @@
2817 3149 */
2818 3150 public function operatorlogin()
2819 3151 {
2820 3152 /**
2821 - * Extra security fix for the login form token.
2822 - *
2823 - * @since September 8th 2020
3153 + * Secure the login form token.
2824 3154 */
2825 - if (!JFactory::getSession()->checkToken()) {
2826 - 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);
2827 3158 }
2828 - //
2829 3159
2830 3160 $app = JFactory::getApplication();
2831 3161 $pauthcode = VikRequest::getString('authcode', '', 'request');
2832 3162 $pitemid = VikRequest::getInt('Itemid', '', 'request');
@@ -2867,39 +3197,44 @@
2867 3197 * @since 1.12
2868 3198 */
2869 3199 public function storeprecheckin()
2870 3200 {
3201 + if (!JSession::checkToken()) {
3202 + // missing CSRF-proof token
3203 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
3204 + }
3205 +
2871 3206 $dbo = JFactory::getDbo();
2872 3207 $app = JFactory::getApplication();
2873 - $sid = VikRequest::getString('sid', '', 'request');
2874 - $ts = VikRequest::getString('ts', '', 'request');
2875 - $pguests = VikRequest::getVar('guests', array());
2876 - $pitemid = VikRequest::getInt('Itemid', 0, 'request');
2877 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 +
2878 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';";
2879 3215 $dbo->setQuery($q);
2880 - $dbo->execute();
2881 - if (!$dbo->getNumRows()) {
3216 + $order = $dbo->loadAssoc();
3217 + if (!$order) {
2882 3218 throw new Exception('Booking not found', 404);
2883 3219 }
2884 - $order = $dbo->loadAssoc();
2885 3220
2886 - $orderrooms = array();
2887 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;";
2888 3222 $dbo->setQuery($q);
2889 - $dbo->execute();
2890 - if (!$dbo->getNumRows()) {
3223 + $orderrooms = $dbo->loadAssocList();
3224 + if (!$orderrooms) {
2891 3225 throw new Exception('No rooms found', 404);
2892 3226 }
2893 - $orderrooms = $dbo->loadAssocList();
2894 3227
3228 + // access the customer record
3229 + $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);
3230 +
2895 3231 $q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
2896 3232 $dbo->setQuery($q);
2897 - $dbo->execute();
2898 - if (!$dbo->getNumRows()) {
3233 + $custorder = $dbo->loadAssoc();
3234 + if (!$custorder) {
2899 3235 throw new Exception('No customer found', 404);
2900 3236 }
2901 - $custorder = $dbo->loadAssoc();
2902 3237
2903 3238 // booking details page
2904 3239 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
2905 3240 if (empty($order['sid']) && !empty($order['idorderota'])) {
@@ -2911,10 +3246,16 @@
2911 3246 $precheckin = VikBooking::precheckinEnabled();
2912 3247 if ($precheckin) {
2913 3248 // make sure the limit of days in advance is reflected
2914 3249 $precheckin_mind = VikBooking::precheckinMinOffset();
2915 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
2916 - $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 + }
2917 3258 }
2918 3259 if (!$precheckin) {
2919 3260 // raise error and redirect in case of website or OTA booking
2920 3261 VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
@@ -2930,13 +3271,18 @@
2930 3271
2931 3272 foreach ($pguests as $ind => $adults) {
2932 3273 foreach ($adults as $aduind => $details) {
2933 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 + }
2934 3280 if (!in_array($detkey, $front_keys)) {
2935 3281 // push the key of the guest details for later comparison
2936 3282 array_push($front_keys, $detkey);
2937 3283 }
2938 - if (strlen($detval)) {
3284 + if (is_scalar($detval) && strlen($detval)) {
2939 3285 // push value only if not empty
2940 3286 if (!isset($guests_details[$ind])) {
2941 3287 $guests_details[$ind] = array();
2942 3288 }
@@ -2965,8 +3311,13 @@
2965 3311 // current pax data include a guest not present, set it to not lose it
2966 3312 $guests_details[$ind][$aduind] = $details;
2967 3313 }
2968 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 + }
2969 3320 if (!in_array($detkey, $front_keys)) {
2970 3321 // merge this key probably reserved to the back-end
2971 3322 $guests_details[$ind][$aduind][$detkey] = $detval;
2972 3323 }
@@ -2982,8 +3333,20 @@
2982 3333
2983 3334 // Booking History
2984 3335 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');
2985 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 +
2986 3349 // print success message and redirect
2987 3350 $app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
2988 3351 $app->redirect($goto);
2989 3352 }
@@ -3001,28 +3364,27 @@
3001 3364 $ts = VikRequest::getString('ts', '', 'request');
3002 3365 $pitemid = VikRequest::getInt('Itemid', 0, 'request');
3003 3366 $paddopt = VikRequest::getVar('addopt', array());
3004 3367
3005 - if (!count($paddopt)) {
3368 + if (!$paddopt) {
3006 3369 throw new Exception('No extra services selected', 404);
3007 3370 }
3008 3371
3372 + // find the involved reservation, direct or OTA
3009 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) . ";";
3010 3374 $dbo->setQuery($q);
3011 - $dbo->execute();
3012 - if (!$dbo->getNumRows()) {
3375 + $order = $dbo->loadAssoc();
3376 + if (!$order) {
3013 3377 throw new Exception('Booking not found', 404);
3014 3378 }
3015 - $order = $dbo->loadAssoc();
3016 3379
3017 - $orderrooms = [];
3018 - $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;";
3019 3382 $dbo->setQuery($q);
3020 - $dbo->execute();
3021 - if (!$dbo->getNumRows()) {
3383 + $orderrooms = $dbo->loadAssocList();
3384 + if (!$orderrooms) {
3022 3385 throw new Exception('No rooms found', 404);
3023 3386 }
3024 - $orderrooms = $dbo->loadAssocList();
3025 3387
3026 3388 // availability helper
3027 3389 $av_helper = VikBooking::getAvailabilityInstance();
3028 3390
@@ -3039,20 +3401,56 @@
3039 3401 // load all valid and existing options as a security measure
3040 3402 $alloptions = [];
3041 3403 $q = "SELECT * FROM `#__vikbooking_optionals`;";
3042 3404 $dbo->setQuery($q);
3043 - $dbo->execute();
3044 - if (!$dbo->getNumRows()) {
3405 + $records = $dbo->loadAssocList();
3406 + if (!$records) {
3045 3407 throw new Exception('No options found', 404);
3046 3408 }
3047 - $records = $dbo->loadAssocList();
3048 3409 foreach ($records as $v) {
3049 - $alloptions[(int)$v['id']] = $v;
3410 + $alloptions[$v['id']] = $v;
3050 3411 }
3051 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
3052 3450 $extras_booked = [];
3053 3451 foreach ($orderrooms as $kor => $or) {
3054 - if (!isset($paddopt[$kor]) || !count($paddopt[$kor])) {
3452 + if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
3055 3453 continue;
3056 3454 }
3057 3455
3058 3456 // determine proper nights of stay
@@ -3068,27 +3466,65 @@
3068 3466 if (strpos($or['optionals'], $optid . ':') === 0 || strpos($or['optionals'], ';' . $optid . ':') > 0) {
3069 3467 // this option has already been booked, skip it
3070 3468 continue;
3071 3469 }
3072 - if (!isset($alloptions[(int)$optid])) {
3470 + if (!isset($alloptions[$optid])) {
3073 3471 // this option ID does not exist, skip it
3074 3472 continue;
3075 3473 }
3076 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 +
3077 3512 // push option booked
3078 - array_push($extras_booked, array(
3079 - 'id' => $optid,
3080 - 'idroom' => $or['idroom'],
3081 - 'name' => $alloptions[(int)$optid]['name'],
3082 - 'quant' => $quant,
3513 + $extras_booked[] = [
3514 + 'id' => $optid,
3515 + 'idroom' => $or['idroom'],
3516 + 'name' => $alloptions[$optid]['name'],
3517 + 'quant' => $quant,
3083 3518 'room_cost' => (!empty($or['cust_cost']) ? $or['cust_cost'] : $or['room_cost']),
3084 3519 'room_name' => $or['room_name'],
3085 - 'optcost' => $alloptions[(int)$optid]['cost'],
3086 - 'adults' => $or['adults'],
3087 - 'children' => $or['children'],
3088 - 'nights' => $room_stay_nights,
3089 - ));
3520 + 'optcost' => $alloptions[$optid]['cost'],
3521 + 'adults' => $or['adults'],
3522 + 'children' => $or['children'],
3523 + 'nights' => $room_stay_nights,
3524 + ];
3090 3525 }
3526 +
3091 3527 // update options for this room record
3092 3528 $newoptstr = $or['optionals'] . $extraoptstr;
3093 3529 $q = "UPDATE `#__vikbooking_ordersrooms` SET `optionals`=" . $dbo->quote($newoptstr) . " WHERE `id`={$or['id']};";
3094 3530 $dbo->setQuery($q);
@@ -3098,8 +3534,9 @@
3098 3534 // increase booking total amount and build event log for the history
3099 3535 $currency = VikBooking::getCurrencySymb();
3100 3536 $totrooms = count($orderrooms);
3101 3537 $increase = 0;
3538 + $future_dd = 0;
3102 3539 $add_tax = 0;
3103 3540 $extraslog = [];
3104 3541 foreach ($extras_booked as $extra) {
3105 3542 $o = $alloptions[(int)$extra['id']];
@@ -3117,34 +3554,100 @@
3117 3554 if ($o['perperson'] == 1) {
3118 3555 $optcost = $optcost * $extra['adults'];
3119 3556 }
3120 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 +
3121 3569 $floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
3122 3570 $netoptprice = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
3123 3571 $increase += $floatoptprice;
3124 3572 $add_tax += $floatoptprice - $netoptprice;
3125 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 + }
3126 3579 }
3127 3580
3128 3581 $newtotbooking = $order['total'] + $increase;
3129 3582 $new_tot_taxes = $order['tot_taxes'] + $add_tax;
3583 +
3130 3584 /**
3131 3585 * Important: the 'paymcount' should be increased only if the status is
3132 3586 * "confirmed" or no rooms may be occupied when receiving a payment.
3133 3587 */
3134 - $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
3135 3624 $dbo->setQuery($q);
3136 3625 $dbo->execute();
3137 3626
3138 3627 // Booking History
3139 - VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('UE', implode("\n", $extraslog));
3628 + VikBooking::getBookingHistoryInstance($order['id'])->store('UE', implode("\n", $extraslog));
3140 3629
3141 - // send email notification to guest and admin
3142 - 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 + }
3143 3640
3641 + if ($send_notifications) {
3642 + // send email notification to guest and admin
3643 + VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
3644 + }
3645 +
3144 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);
3145 3647 $app->enqueueMessage(JText::translate('VBOUPSELLRESULTOK'));
3146 3648 $app->redirect($goto);
3649 + $app->close();
3147 3650 }
3148 3651
3149 3652 /**
3150 3653 * Submits a new review.
@@ -3267,12 +3770,12 @@
3267 3770
3268 3771 // reviewer information
3269 3772 $review_content->reviewer = new stdClass;
3270 3773 $customer_name = '';
3271 - if (count($customer)) {
3774 + if ($customer) {
3272 3775 $review_content->reviewer->name = $customer['first_name'];
3273 3776 $review_content->reviewer->country_code = $customer['country'];
3274 - $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];;
3777 + $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
3275 3778 } else {
3276 3779 $revuname = '';
3277 3780 if (!empty($order['custdata'])) {
3278 3781 $uinfos = explode("\n", $order['custdata']);
@@ -3793,8 +4296,13 @@
3793 4296 * @since 1.13.5
3794 4297 */
3795 4298 public function add_roomdaynote()
3796 4299 {
4300 + if (!JSession::checkToken()) {
4301 + // missing CSRF-proof token
4302 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4303 + }
4304 +
3797 4305 $dt = VikRequest::getString('dt', '', 'request');
3798 4306 $idroom = VikRequest::getInt('idroom', 0, 'request');
3799 4307 $subunit = VikRequest::getInt('subunit', 0, 'request');
3800 4308 $type = VikRequest::getString('type', '', 'request');
@@ -3859,21 +4367,26 @@
3859 4367
3860 4368 /**
3861 4369 * AJAX endpoint to upload customer documents during the pre-checkin.
3862 4370 *
3863 - * @throws Exception
3864 - *
3865 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.
3866 4373 */
3867 4374 public function precheckin_upload_docs()
3868 4375 {
3869 - $dbo = JFactory::getDbo();
3870 - $app = JFactory::getApplication();
3871 - $input = $app->input;
4376 + $app = JFactory::getApplication();
4377 + $dbo = JFactory::getDbo();
4378 + $input = $app->input;
3872 4379
3873 - // get request values
3874 - $order_sid = $input->getString('sid', '');
3875 - $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);
3876 4389 /**
3877 4390 * This is a simple file uploading process, but if
3878 4391 * we wanted to automatically update the pax_data,
3879 4392 * the request vars below would be necessary.
@@ -3879,31 +4392,29 @@
3879 4392 * the request vars below would be necessary.
3880 4393 */
3881 4394 $room_index = $input->getInt('room_index', 0);
3882 4395 $guest_index = $input->getInt('guest_index', 0);
3883 - $pax_index = $input->getString('pax_index', '');
4396 + $pax_index = $input->getString('pax_index', '');
3884 4397
3885 4398 if (empty($order_sid) || empty($order_ts)) {
3886 - throw new Exception('Missing booking details', 404);
4399 + VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
3887 4400 }
3888 4401
3889 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';";
3890 4403 $dbo->setQuery($q);
3891 - $dbo->execute();
3892 - if (!$dbo->getNumRows()) {
3893 - throw new Exception('Booking not found', 404);
4404 + $order = $dbo->loadAssoc();
4405 + if (!$order) {
4406 + VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
3894 4407 }
3895 - $order = $dbo->loadAssoc();
3896 4408
3897 4409 $customer = array();
3898 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'].";";
3899 4411 $dbo->setQuery($q);
3900 - $dbo->execute();
3901 - if (!$dbo->getNumRows()) {
4412 + $customer = $dbo->loadObject();
4413 + if (!$customer) {
3902 4414 // one customer must be assigned to this booking for the pre-checkin
3903 - throw new Exception('No customers associated to this booking', 404);
4415 + VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
3904 4416 }
3905 - $customer = $dbo->loadObject();
3906 4417
3907 4418 // make sure pre-checkin is allowed
3908 4419 $precheckin = VikBooking::precheckinEnabled();
3909 4420 if ($precheckin) {
@@ -3908,25 +4419,31 @@
3908 4419 $precheckin = VikBooking::precheckinEnabled();
3909 4420 if ($precheckin) {
3910 4421 // make sure the limit of days in advance is reflected
3911 4422 $precheckin_mind = VikBooking::precheckinMinOffset();
3912 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3913 - $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 + }
3914 4431 }
3915 4432 if (!$precheckin) {
3916 - throw new Exception('Pre-checkin not allowed at this time', 403);
4433 + VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
3917 4434 }
3918 4435
3919 4436 // get uploaded files array (use "raw" to avoid filtering the file to upload)
3920 4437 $files = $input->files->get('docs', array(), 'raw');
3921 - if (!count($files)) {
3922 - throw new Exception('No files to be uploaded', 500);
4438 + if (!$files) {
4439 + VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
3923 4440 }
3924 4441
3925 4442 if (isset($files['name'])) {
3926 4443 // we have a single associative array, we need to push it within a list,
3927 4444 // because the upload iterates the $files array
3928 - $files = array($files);
4445 + $files = [$files];
3929 4446 }
3930 4447
3931 4448 // fetch documents folder path
3932 4449 $dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
@@ -3936,28 +4453,26 @@
3936 4453 // randomize string
3937 4454 $customer->seed = uniqid();
3938 4455
3939 4456 // create blocks for hashed folder
3940 - $parts = array(
4457 + $parts = [
3941 4458 $customer->first_name,
3942 4459 $customer->last_name,
3943 4460 md5(serialize($customer)),
3944 - );
4461 + ];
3945 4462
3946 4463 // join fetched parts
3947 4464 $customer->docsfolder = strtolower(implode('-', array_filter($parts)));
3948 4465
3949 4466 if (strlen($customer->docsfolder) < 16) {
3950 - 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.');
3951 4468 }
3952 4469
3953 - jimport('joomla.filesystem.folder');
3954 -
3955 4470 // create a folder for this customer
3956 4471 $created = JFolder::create($dirpath . $customer->docsfolder);
3957 4472
3958 4473 if (!$created) {
3959 - 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));
3960 4475 }
3961 4476
3962 4477 unset($customer->seed);
3963 4478
@@ -3967,10 +4482,12 @@
3967 4482 $record->docsfolder = $customer->docsfolder;
3968 4483 $dbo->updateObject('#__vikbooking_customers', $record, 'id');
3969 4484 }
3970 4485
3971 - // prepare the response array of uploaded-file objects
3972 - $response = array();
4486 + // prepare the response with the uploaded-file objects
4487 + $response = [
4488 + 'uploads' => [],
4489 + ];
3973 4490 $upload_err = null;
3974 4491
3975 4492 // compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
3976 4493 $file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';
@@ -3988,13 +4505,13 @@
3988 4505 }
3989 4506 // always prepend "pre check-in" to the original file name
3990 4507 $file['name'] = strtolower($file_prefix . $file['name']);
3991 4508 // try to upload the file
3992 - $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');
3993 4510 // set a valid URL for the uploaded file
3994 4511 $result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
3995 4512 // push uploaded file
3996 - array_push($response, $result);
4513 + array_push($response['uploads'], $result);
3997 4514 }
3998 4515
3999 4516 } catch (Exception $e) {
4000 4517 // do nothing, but catch the error
@@ -4000,15 +4517,56 @@
4000 4517 // do nothing, but catch the error
4001 4518 $upload_err = $e;
4002 4519 }
4003 4520
4004 - if (!count($response)) {
4005 - 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');
4006 4527 }
4007 4528
4008 - // output the response
4009 - echo json_encode($response);
4010 - 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);
4011 4569 }
4012 4570
4013 4571 /**
4014 4572 * AJAX endpoint to submit an inquiry/information request.
@@ -4034,11 +4592,11 @@
4034 4592 $checkout_h = $input->getInt('checkouth', 0);
4035 4593 $checkout_m = $input->getInt('checkoutm', 0);
4036 4594 $categories = $input->getString('categories', '');
4037 4595 $roomsnum = $input->getInt('roomsnum', 1);
4038 - $adults = $input->get('adults', array(), 'int');
4039 - $children = $input->get('children', array(), 'int');
4040 - $inquiry = $input->get('inquiry', array(), 'raw');
4596 + $adults = $input->get('adults', [], 'int');
4597 + $children = $input->get('children', [], 'int');
4598 + $inquiry = $input->get('inquiry', [], 'array');
4041 4599 $ulang = $input->getString('ulang', '');
4042 4600
4043 4601 $timeopst = VikBooking::getTimeOpenStore();
4044 4602 if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
@@ -4083,8 +4641,10 @@
4083 4641 if (!is_scalar($info_val) || !strlen($info_val)) {
4084 4642 // empty or invalid field
4085 4643 continue;
4086 4644 }
4645 + // accept only text from submitted value
4646 + $info_val = strip_tags($info_val);
4087 4647 if ($info_type == 'nominative') {
4088 4648 if (empty($t_first_name)) {
4089 4649 $t_first_name = $info_val;
4090 4650 $res_custdata[JText::translate('VBNAME')] = $info_val;
@@ -4500,6 +5060,153 @@
4500 5060 VikBooking::getBookingHistoryInstance()->setBid($booking['id'])->store('UR', $hist_descr);
4501 5061
4502 5062 // output the JSON encoded successful response
4503 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();
4504 5211 }
4505 5212 }