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 +622 -113 1.7.2 → 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) {
@@ -564,13 +571,35 @@
564 571 }
565 572 }
566 573
567 574 /**
568 - * Custom check-in/out times due to late check-out/early check-in options.
575 + * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
569 576 *
570 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.
571 579 */
572 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 + }
573 602
574 603 $selopt = [];
575 604 $optstr = [];
576 605 $children_age = [];
@@ -688,8 +717,22 @@
688 717 ];
689 718 }
690 719
691 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 +
692 735 $opt_minus_iva = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
693 736 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
694 737 if ($actopt[0]['is_citytax'] == 1) {
695 738 $tot_city_taxes += $opt_minus_iva;
@@ -694,8 +737,10 @@
694 737 if ($actopt[0]['is_citytax'] == 1) {
695 738 $tot_city_taxes += $opt_minus_iva;
696 739 } elseif ($actopt[0]['is_fee'] == 1) {
697 740 $tot_fees += $opt_minus_iva;
741 + } elseif ($opt_params['damagedep'] ?? 0) {
742 + $tot_damage_dep += $opt_minus_iva;
698 743 }
699 744 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
700 745 if ($tmpopr == $realcost) {
701 746 $tot_taxes += ($realcost - $opt_minus_iva);
@@ -786,13 +831,13 @@
786 831 * @since 1.16.0 (J) - 1.6.0 (WP) taxes are proportionally calculated when coupon before tax.
787 832 * @since 1.16.8 (J) - 1.6.8 (WP) with coupon before taxes, discounted amount calculation is an equal subtration.
788 833 */
789 834 $prev_isdue = $isdue;
790 - $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees);
835 + $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees - $tot_damage_dep);
791 836 $coupondiscount = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $coupon['value'] / 100;
792 837 $isdue = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $minuscoupon / 100;
793 838 $tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : $tot_taxes;
794 - $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;
795 840 $coupondiscount = abs($prev_isdue - $isdue);
796 841 } else {
797 842 // total value
798 843 $coupondiscount = $coupon['value'];
@@ -811,9 +856,9 @@
811 856 }
812 857
813 858 $strisdue = number_format($isdue, 2) . 'vikbooking';
814 859 $ptotdue = number_format($ptotdue, 2) . 'vikbooking';
815 - if ($strisdue != $ptotdue) {
860 + if ($strisdue != $ptotdue && abs(round($isdue, 2) - round((float) $ptotdue, 2)) <= 0.01) {
816 861 showSelectVb(JText::translate('VBINCONGRTOT'));
817 862 return;
818 863 }
819 864
@@ -1053,12 +1098,22 @@
1053 1098 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1054 1099 $booking_record->tot_taxes = (float)$tot_taxes;
1055 1100 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1056 1101 $booking_record->tot_fees = (float)$tot_fees;
1102 + if ($tot_damage_dep) {
1103 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1104 + }
1057 1105 $booking_record->phone = $phone_number;
1058 1106 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1059 1107 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1060 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 +
1061 1116 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1062 1117
1063 1118 if (!isset($booking_record->id)) {
1064 1119 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1347,12 +1402,43 @@
1347 1402 if ($mod_booking['total'] < $isdue) {
1348 1403 $mod_paymcount++;
1349 1404 }
1350 1405
1351 - $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 +
1352 1431 $dbo->setQuery($q);
1353 1432 $dbo->execute();
1354 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 +
1355 1441 // remove the coupon used (should never been allowed for modifications)
1356 1442 if ($usedcoupon == true && $coupon['type'] == 2) {
1357 1443 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=".(int)$coupon['id'].";";
1358 1444 $dbo->setQuery($q);
@@ -1410,12 +1496,22 @@
1410 1496 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1411 1497 $booking_record->tot_taxes = (float)$tot_taxes;
1412 1498 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1413 1499 $booking_record->tot_fees = (float)$tot_fees;
1500 + if ($tot_damage_dep) {
1501 + $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1502 + }
1414 1503 $booking_record->phone = $phone_number;
1415 1504 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1416 1505 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1417 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 +
1418 1514 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1419 1515
1420 1516 if (!isset($booking_record->id)) {
1421 1517 showSelectVb('Critical error while saving the booking. Please try again');
@@ -1727,10 +1823,13 @@
1727 1823 }
1728 1824
1729 1825 public function notifypayment()
1730 1826 {
1827 + $app = JFactory::getApplication();
1731 1828 $dbo = JFactory::getDbo();
1732 1829
1830 + $session = JFactory::getSession();
1831 +
1733 1832 $config = VBOFactory::getConfig();
1734 1833 $av_helper = VikBooking::getAvailabilityInstance();
1735 1834
1736 1835 $psid = VikRequest::getString('sid', '', 'request');
@@ -1762,18 +1861,16 @@
1762 1861 $recipient_mail = $all_recipients;
1763 1862 }
1764 1863 }
1765 1864
1865 + // load booking details
1766 1866 $q = "SELECT * FROM `#__vikbooking_orders` WHERE (`sid`=" . $dbo->quote($psid) . " OR `idorderota`=" . $dbo->quote($psid) . ") AND `ts`=" . $dbo->quote($pts);
1767 1867 $dbo->setQuery($q, 0, 1);
1768 - $dbo->execute();
1769 - if (!$dbo->getNumRows()) {
1868 + $row = $dbo->loadAssoc();
1869 + if (!$row) {
1770 1870 VBOHttpDocument::getInstance()->close(404, 'Booking not found');
1771 1871 }
1772 1872
1773 - // load booking details
1774 - $row = $dbo->loadAssoc();
1775 -
1776 1873 // check if the language in use is the same as the one used during the checkout
1777 1874 if (!empty($row['lang'])) {
1778 1875 $lang = JFactory::getLanguage();
1779 1876 if ($lang->getTag() != $row['lang']) {
@@ -1788,9 +1885,12 @@
1788 1885 $vbo_tn = VikBooking::getTranslator();
1789 1886
1790 1887 if ($row['status'] == 'confirmed' && !(VikBooking::multiplePayments() && $row['paymcount'] > 0)) {
1791 1888 // booking can be paid only if not confirmed or if multiple payments are enabled and payment counter for booking greater than zero
1792 - 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 + }
1793 1893 }
1794 1894
1795 1895 /**
1796 1896 * Check split stay reservation data.
@@ -1865,12 +1965,34 @@
1865 1965 // inject values
1866 1966 $row['order_rooms'] = $orderrooms;
1867 1967 $row['fares'] = $tars;
1868 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
1869 1990 $isdue = 0;
1870 1991 $tot_taxes = 0;
1871 1992 $tot_city_taxes = 0;
1872 1993 $tot_fees = 0;
1994 + $tot_damage_dep = 0;
1873 1995 $pricestr = [];
1874 1996 $optstr = [];
1875 1997 foreach ($orderrooms as $kor => $or) {
1876 1998 $num = $kor + 1;
@@ -1919,8 +2041,13 @@
1919 2041 $dbo->setQuery($q);
1920 2042 $actopt = $dbo->loadAssocList();
1921 2043 if ($actopt) {
1922 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 +
1923 2050 $chvar = '';
1924 2051 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
1925 2052 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
1926 2053 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
@@ -1970,8 +2097,19 @@
1970 2097 }
1971 2098 if ($actopt[0]['perperson'] == 1) {
1972 2099 $realcost = $realcost * $or['adults'];
1973 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 +
1974 2112 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
1975 2113 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
1976 2114 if ($actopt[0]['is_citytax'] == 1) {
1977 2115 $tot_city_taxes += $opt_minus_tax;
@@ -1976,10 +2114,12 @@
1976 2114 if ($actopt[0]['is_citytax'] == 1) {
1977 2115 $tot_city_taxes += $opt_minus_tax;
1978 2116 } elseif ($actopt[0]['is_fee'] == 1) {
1979 2117 $tot_fees += $opt_minus_tax;
2118 + } elseif ($opt_params['damagedep'] ?? 0) {
2119 + $tot_damage_dep += $opt_minus_tax;
1980 2120 }
1981 - // 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
1982 2122 if ($tmpopr == $realcost) {
1983 2123 $tot_taxes += ($realcost - $opt_minus_tax);
1984 2124 } else {
1985 2125 $tot_taxes += ($tmpopr - $realcost);
@@ -2010,16 +2150,19 @@
2010 2150 $expcoupon = explode(";", $row['coupon']);
2011 2151 $isdue = $isdue - $expcoupon[1];
2012 2152 }
2013 2153
2014 - // invoke the payment method class
2015 - $exppay = explode('=', $row['idpayment']);
2016 - $payment = VikBooking::getPayment($exppay[0], $vbo_tn);
2017 -
2018 2154 if (empty($row['sid']) && !empty($row['idorderota']) && !empty($row['channel'])) {
2019 2155 $row['sid'] = $row['idorderota'];
2020 2156 }
2021 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 +
2022 2165 if (VBOPlatformDetection::isWordPress()) {
2023 2166 /**
2024 2167 * @wponly The payment gateway is now loaded
2025 2168 * using the apposite dispatcher.
@@ -2028,9 +2171,9 @@
2028 2171 */
2029 2172 JLoader::import('adapter.payment.dispatcher');
2030 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'];
2031 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'];
2032 - $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";
2033 2176 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
2034 2177 $itemid = $model->best(array('booking'), (!empty($row['lang']) ? $row['lang'] : null));
2035 2178 $extra_data = [];
2036 2179 if ($itemid) {
@@ -2061,9 +2204,9 @@
2061 2204 $bestitemid = VikBooking::findProperItemIdType(array('booking'));
2062 2205 $extra_data = array(
2063 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)),
2064 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)),
2065 - '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),
2066 2209 );
2067 2210 $extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();
2068 2211
2069 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
@@ -2068,9 +2211,20 @@
2068 2211
2069 2212 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
2070 2213 }
2071 2214
2072 - $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
2073 2227 $newpaymentlog = date('c')."\n".$array_result['log']."\n----------\n".$row['paymentlog'];
2074 2228
2075 2229 /**
2076 2230 * OTA reservations containing PCI-DSS card details may receive additional payments through
@@ -2089,15 +2243,16 @@
2089 2243 * Ensure the size of the log does not make the query fail.
2090 2244 *
2091 2245 * @since 1.16.9 (J) - 1.6.9 (WP)
2092 2246 */
2093 - if (strlen($newpaymentlog) > 65000) {
2094 - $newpaymentlog = substr($newpaymentlog, 0, 65000) . '...';
2247 + if (strlen($newpaymentlog) > 55000) {
2248 + $newpaymentlog = substr($newpaymentlog, 0, 55000) . '...';
2095 2249 }
2096 2250
2097 2251 if ($array_result['verified'] == 1) {
2098 2252 // valid payment
2099 2253 $shouldpay = $isdue;
2254 +
2100 2255 if ($payment['charge'] > 0.00) {
2101 2256 if ($payment['ch_disc'] == 1) {
2102 2257 // charge
2103 2258 if ($payment['val_pcent'] == 1) {
@@ -2119,39 +2274,60 @@
2119 2274 $shouldpay -= $percent_to_pay;
2120 2275 }
2121 2276 }
2122 2277 }
2123 - //deposit may be skipped by customer choice
2278 +
2279 + // deposit may be skipped by customer choice
2124 2280 $shouldpay_befdep = $shouldpay;
2125 - //
2281 + $shouldpay_befdep_no_dd = $shouldpay - ($damage_deposit_payment['damagedep_gross'] ?? 0);
2282 +
2126 2283 if (!VikBooking::payTotal()) {
2127 2284 $percentdeposit = VikBooking::getAccPerCent();
2128 2285 if ($percentdeposit > 0) {
2129 2286 if (VikBooking::getTypeDeposit() == "fixed") {
2130 2287 $shouldpay = $percentdeposit;
2288 + $shouldpay_befdep_no_dd = $percentdeposit;
2131 2289 } else {
2132 2290 $shouldpay = $shouldpay * $percentdeposit / 100;
2291 + $shouldpay_befdep_no_dd = $shouldpay_befdep_no_dd * $percentdeposit / 100;;
2133 2292 }
2134 2293 }
2135 2294 }
2136 - //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
2137 2301 if (isset($array_result['tot_paid'])) {
2138 2302 $shouldpay = round($shouldpay, 2);
2139 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);
2140 2306 $totreceived = round($array_result['tot_paid'], 2);
2141 - if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $row['paymcount'] == 0) {
2142 - //the amount paid is different than the order total
2143 - //fares might have changed or the deposit might be different
2144 - //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
2145 2311 $vbo_app = VikBooking::getVboApplication();
2146 2312 $adsendermail = VikBooking::getSenderMail();
2147 - $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 + );
2148 2327 }
2149 - /**
2150 - * We store the amount paid before applying the charge for the transaction.
2151 - *
2152 - * @since 1.3.0
2153 - */
2328 +
2329 + // amount paid should be stored as exclusive of transaction fees/discounts
2154 2330 if ($payment['charge'] > 0.00) {
2155 2331 if ($payment['ch_disc'] == 1) {
2156 2332 // charge
2157 2333 if ($payment['val_pcent'] == 1) {
@@ -2228,9 +2404,15 @@
2228 2404 $booking_record->paymentlog = $newpaymentlog;
2229 2405 }
2230 2406 $booking_record->payable = $new_payable;
2231 2407
2232 - $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 + }
2233 2415
2234 2416 // assign room specific unit
2235 2417 $set_room_indexes = VikBooking::autoRoomUnit();
2236 2418 $room_indexes_usemap = [];
@@ -2236,9 +2418,8 @@
2236 2418 $room_indexes_usemap = [];
2237 2419 if ($set_room_indexes === true) {
2238 2420 $q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
2239 2421 $dbo->setQuery($q);
2240 - $dbo->execute();
2241 2422 $orooms = $dbo->loadAssocList();
2242 2423 foreach ($orooms as $oroom) {
2243 2424 if (!empty($oroom['roomindex'])) {
2244 2425 // room specific unit has already been assigned
@@ -2271,8 +2452,13 @@
2271 2452 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
2272 2453 $dbo->setQuery($q);
2273 2454 $dbo->execute();
2274 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 +
2275 2461 // customer booking
2276 2462 $q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
2277 2463 $dbo->setQuery($q);
2278 2464 $customer_id = $dbo->loadResult();
@@ -2309,8 +2495,9 @@
2309 2495 *
2310 2496 * @since 1.14 (J) - 1.4.0 (WP)
2311 2497 * @since 1.16.2 (J) - 1.6.2 (WP) we attempt to always store the amount paid with this transaction.
2312 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.
2313 2500 */
2314 2501 $tn_data = $array_result['transaction'] ?? null;
2315 2502 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2316 2503 // check event data payload to store
@@ -2315,16 +2502,16 @@
2315 2502 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2316 2503 // check event data payload to store
2317 2504 if (is_array($tn_data)) {
2318 2505 // set key
2319 - $tn_data['amount_paid'] = (float)$array_result['tot_paid'];
2506 + $tn_data['amount_paid'] = (float) $array_result['tot_paid'];
2320 2507 } elseif (is_object($tn_data)) {
2321 2508 // set property
2322 - $tn_data->amount_paid = (float)$array_result['tot_paid'];
2509 + $tn_data->amount_paid = (float) $array_result['tot_paid'];
2323 2510 } elseif (!$tn_data) {
2324 2511 // build an array (we add the payment name because we know there is no other transaction data)
2325 2512 $tn_data = [
2326 - 'amount_paid' => (float)$array_result['tot_paid'],
2513 + 'amount_paid' => (float) $array_result['tot_paid'],
2327 2514 'payment_method' => $payment['name'],
2328 2515 ];
2329 2516 }
2330 2517 }
@@ -2331,14 +2518,23 @@
2331 2518 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
2332 2519 // check event data payload to store
2333 2520 if (is_array($tn_data)) {
2334 2521 // set key
2335 - $tn_data['processing_fees'] = (float)$array_result['tot_fees'];
2522 + $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
2336 2523 } elseif (is_object($tn_data)) {
2337 2524 // set property
2338 - $tn_data->processing_fees = (float)$array_result['tot_fees'];
2525 + $tn_data->processing_fees = (float) $array_result['tot_fees'];
2339 2526 }
2340 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 + }
2341 2537
2342 2538 // Booking History
2343 2539 VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);
2344 2540
@@ -2347,9 +2543,8 @@
2347 2543 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
2348 2544 $vcm = new SynchVikBooking($row['id']);
2349 2545 $vcm->setPushType('new')->sendRequest();
2350 2546 }
2351 - $session = JFactory::getSession();
2352 2547 $vcmchanneldata = $session->get('vcmChannelData', '');
2353 2548 if (!empty($vcmchanneldata)) {
2354 2549 $session->set('vcmChannelData', '');
2355 2550 }
@@ -2372,11 +2567,16 @@
2372 2567 $obj->afterValidation(0);
2373 2568 }
2374 2569 }
2375 2570 }
2376 -
2571 +
2377 2572 public function currencyconverter()
2378 2573 {
2574 + if (!JSession::checkToken()) {
2575 + // missing CSRF-proof token
2576 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2577 + }
2578 +
2379 2579 $session = JFactory::getSession();
2380 2580 $pprices = VikRequest::getVar('prices', array(0));
2381 2581 $pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
2382 2582 $ptocurrency = VikRequest::getString('tocurrency', '', 'request');
@@ -2420,8 +2620,13 @@
2420 2620 }
2421 2621
2422 2622 public function storesignature()
2423 2623 {
2624 + if (!JSession::checkToken()) {
2625 + // missing CSRF-proof token
2626 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2627 + }
2628 +
2424 2629 $sid = VikRequest::getString('sid', '', 'request');
2425 2630 $ts = VikRequest::getString('ts', '', 'request');
2426 2631 $psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
2427 2632 $ppad_width = VikRequest::getInt('pad_width', '', 'request');
@@ -2505,8 +2710,13 @@
2505 2710 }
2506 2711
2507 2712 public function validatepin()
2508 2713 {
2714 + if (!JSession::checkToken()) {
2715 + // missing CSRF-proof token
2716 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2717 + }
2718 +
2509 2719 $cpin = VikBooking::getCPinIstance();
2510 2720
2511 2721 $ppin = VikRequest::getString('pin', '', 'request');
2512 2722
@@ -2512,9 +2722,9 @@
2512 2722
2513 2723 $response = [];
2514 2724
2515 2725 $customer = $cpin->getCustomerByPin($ppin);
2516 - if (count($customer)) {
2726 + if ($customer) {
2517 2727 $response = $customer;
2518 2728 $response['success'] = 1;
2519 2729
2520 2730 if ($cpin->getCustomerCoupon($customer)) {
@@ -2675,8 +2885,15 @@
2675 2885 // push the transaction currency information
2676 2886 $order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();
2677 2887
2678 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 + /**
2679 2896 * @wponly The payment gateway is loaded
2680 2897 * through the apposite dispatcher.
2681 2898 */
2682 2899 JLoader::import('adapter.payment.dispatcher');
@@ -2932,16 +3149,14 @@
2932 3149 */
2933 3150 public function operatorlogin()
2934 3151 {
2935 3152 /**
2936 - * Extra security fix for the login form token.
2937 - *
2938 - * @since September 8th 2020
3153 + * Secure the login form token.
2939 3154 */
2940 - if (!JFactory::getSession()->checkToken()) {
2941 - 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);
2942 3158 }
2943 - //
2944 3159
2945 3160 $app = JFactory::getApplication();
2946 3161 $pauthcode = VikRequest::getString('authcode', '', 'request');
2947 3162 $pitemid = VikRequest::getInt('Itemid', '', 'request');
@@ -2982,39 +3197,44 @@
2982 3197 * @since 1.12
2983 3198 */
2984 3199 public function storeprecheckin()
2985 3200 {
3201 + if (!JSession::checkToken()) {
3202 + // missing CSRF-proof token
3203 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
3204 + }
3205 +
2986 3206 $dbo = JFactory::getDbo();
2987 3207 $app = JFactory::getApplication();
2988 - $sid = VikRequest::getString('sid', '', 'request');
2989 - $ts = VikRequest::getString('ts', '', 'request');
2990 - $pguests = VikRequest::getVar('guests', array());
2991 - $pitemid = VikRequest::getInt('Itemid', 0, 'request');
2992 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 +
2993 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';";
2994 3215 $dbo->setQuery($q);
2995 - $dbo->execute();
2996 - if (!$dbo->getNumRows()) {
3216 + $order = $dbo->loadAssoc();
3217 + if (!$order) {
2997 3218 throw new Exception('Booking not found', 404);
2998 3219 }
2999 - $order = $dbo->loadAssoc();
3000 3220
3001 - $orderrooms = array();
3002 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;";
3003 3222 $dbo->setQuery($q);
3004 - $dbo->execute();
3005 - if (!$dbo->getNumRows()) {
3223 + $orderrooms = $dbo->loadAssocList();
3224 + if (!$orderrooms) {
3006 3225 throw new Exception('No rooms found', 404);
3007 3226 }
3008 - $orderrooms = $dbo->loadAssocList();
3009 3227
3228 + // access the customer record
3229 + $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);
3230 +
3010 3231 $q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
3011 3232 $dbo->setQuery($q);
3012 - $dbo->execute();
3013 - if (!$dbo->getNumRows()) {
3233 + $custorder = $dbo->loadAssoc();
3234 + if (!$custorder) {
3014 3235 throw new Exception('No customer found', 404);
3015 3236 }
3016 - $custorder = $dbo->loadAssoc();
3017 3237
3018 3238 // booking details page
3019 3239 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3020 3240 if (empty($order['sid']) && !empty($order['idorderota'])) {
@@ -3026,10 +3246,16 @@
3026 3246 $precheckin = VikBooking::precheckinEnabled();
3027 3247 if ($precheckin) {
3028 3248 // make sure the limit of days in advance is reflected
3029 3249 $precheckin_mind = VikBooking::precheckinMinOffset();
3030 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3031 - $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 + }
3032 3258 }
3033 3259 if (!$precheckin) {
3034 3260 // raise error and redirect in case of website or OTA booking
3035 3261 VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
@@ -3045,13 +3271,18 @@
3045 3271
3046 3272 foreach ($pguests as $ind => $adults) {
3047 3273 foreach ($adults as $aduind => $details) {
3048 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 + }
3049 3280 if (!in_array($detkey, $front_keys)) {
3050 3281 // push the key of the guest details for later comparison
3051 3282 array_push($front_keys, $detkey);
3052 3283 }
3053 - if (strlen($detval)) {
3284 + if (is_scalar($detval) && strlen($detval)) {
3054 3285 // push value only if not empty
3055 3286 if (!isset($guests_details[$ind])) {
3056 3287 $guests_details[$ind] = array();
3057 3288 }
@@ -3080,8 +3311,13 @@
3080 3311 // current pax data include a guest not present, set it to not lose it
3081 3312 $guests_details[$ind][$aduind] = $details;
3082 3313 }
3083 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 + }
3084 3320 if (!in_array($detkey, $front_keys)) {
3085 3321 // merge this key probably reserved to the back-end
3086 3322 $guests_details[$ind][$aduind][$detkey] = $detval;
3087 3323 }
@@ -3097,8 +3333,20 @@
3097 3333
3098 3334 // Booking History
3099 3335 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');
3100 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 +
3101 3349 // print success message and redirect
3102 3350 $app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
3103 3351 $app->redirect($goto);
3104 3352 }
@@ -3120,8 +3368,9 @@
3120 3368 if (!$paddopt) {
3121 3369 throw new Exception('No extra services selected', 404);
3122 3370 }
3123 3371
3372 + // find the involved reservation, direct or OTA
3124 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) . ";";
3125 3374 $dbo->setQuery($q);
3126 3375 $order = $dbo->loadAssoc();
3127 3376 if (!$order) {
@@ -3127,9 +3376,10 @@
3127 3376 if (!$order) {
3128 3377 throw new Exception('Booking not found', 404);
3129 3378 }
3130 3379
3131 - $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;";
3132 3382 $dbo->setQuery($q);
3133 3383 $orderrooms = $dbo->loadAssocList();
3134 3384 if (!$orderrooms) {
3135 3385 throw new Exception('No rooms found', 404);
@@ -3160,14 +3410,44 @@
3160 3410 $alloptions[$v['id']] = $v;
3161 3411 }
3162 3412
3163 3413 /**
3164 - * Custom check-in/out times due to late check-out/early check-in options.
3414 + * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
3165 3415 *
3166 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.
3167 3418 */
3168 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 + }
3169 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
3170 3450 $extras_booked = [];
3171 3451 foreach ($orderrooms as $kor => $or) {
3172 3452 if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
3173 3453 continue;
@@ -3207,8 +3487,29 @@
3207 3487 ($opt_params['set_checkout'] ?? 0),
3208 3488 ];
3209 3489 }
3210 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 +
3211 3512 // push option booked
3212 3513 $extras_booked[] = [
3213 3514 'id' => $optid,
3214 3515 'idroom' => $or['idroom'],
@@ -3233,8 +3534,9 @@
3233 3534 // increase booking total amount and build event log for the history
3234 3535 $currency = VikBooking::getCurrencySymb();
3235 3536 $totrooms = count($orderrooms);
3236 3537 $increase = 0;
3538 + $future_dd = 0;
3237 3539 $add_tax = 0;
3238 3540 $extraslog = [];
3239 3541 foreach ($extras_booked as $extra) {
3240 3542 $o = $alloptions[(int)$extra['id']];
@@ -3252,13 +3554,29 @@
3252 3554 if ($o['perperson'] == 1) {
3253 3555 $optcost = $optcost * $extra['adults'];
3254 3556 }
3255 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 +
3256 3569 $floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
3257 3570 $netoptprice = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
3258 3571 $increase += $floatoptprice;
3259 3572 $add_tax += $floatoptprice - $netoptprice;
3260 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 + }
3261 3579 }
3262 3580
3263 3581 $newtotbooking = $order['total'] + $increase;
3264 3582 $new_tot_taxes = $order['tot_taxes'] + $add_tax;
@@ -3271,9 +3589,9 @@
3271 3589 ->update($dbo->qn('#__vikbooking_orders'))
3272 3590 ->set($dbo->qn('total') . ' = ' . $dbo->q($newtotbooking))
3273 3591 ->set($dbo->qn('paymcount') . ' = ' . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']))
3274 3592 ->set($dbo->qn('tot_taxes') . ' = ' . $dbo->q($new_tot_taxes))
3275 - ->set($dbo->qn('payable') . ' = ' . $dbo->q(((float)$order['payable'] + $increase)))
3593 + ->set($dbo->qn('payable') . ' = ' . $dbo->q(((float)$order['payable'] + $increase - $future_dd)))
3276 3594 ->where($dbo->qn('id') . ' = ' . (int) $order['id']);
3277 3595
3278 3596 /**
3279 3597 * Custom check-in/out times due to late check-out/early check-in options.
@@ -3452,9 +3770,9 @@
3452 3770
3453 3771 // reviewer information
3454 3772 $review_content->reviewer = new stdClass;
3455 3773 $customer_name = '';
3456 - if (count($customer)) {
3774 + if ($customer) {
3457 3775 $review_content->reviewer->name = $customer['first_name'];
3458 3776 $review_content->reviewer->country_code = $customer['country'];
3459 3777 $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
3460 3778 } else {
@@ -3978,8 +4296,13 @@
3978 4296 * @since 1.13.5
3979 4297 */
3980 4298 public function add_roomdaynote()
3981 4299 {
4300 + if (!JSession::checkToken()) {
4301 + // missing CSRF-proof token
4302 + VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4303 + }
4304 +
3982 4305 $dt = VikRequest::getString('dt', '', 'request');
3983 4306 $idroom = VikRequest::getInt('idroom', 0, 'request');
3984 4307 $subunit = VikRequest::getInt('subunit', 0, 'request');
3985 4308 $type = VikRequest::getString('type', '', 'request');
@@ -4044,21 +4367,26 @@
4044 4367
4045 4368 /**
4046 4369 * AJAX endpoint to upload customer documents during the pre-checkin.
4047 4370 *
4048 - * @throws Exception
4049 - *
4050 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.
4051 4373 */
4052 4374 public function precheckin_upload_docs()
4053 4375 {
4054 - $dbo = JFactory::getDbo();
4055 - $app = JFactory::getApplication();
4056 - $input = $app->input;
4376 + $app = JFactory::getApplication();
4377 + $dbo = JFactory::getDbo();
4378 + $input = $app->input;
4057 4379
4058 - // get request values
4059 - $order_sid = $input->getString('sid', '');
4060 - $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);
4061 4389 /**
4062 4390 * This is a simple file uploading process, but if
4063 4391 * we wanted to automatically update the pax_data,
4064 4392 * the request vars below would be necessary.
@@ -4064,31 +4392,29 @@
4064 4392 * the request vars below would be necessary.
4065 4393 */
4066 4394 $room_index = $input->getInt('room_index', 0);
4067 4395 $guest_index = $input->getInt('guest_index', 0);
4068 - $pax_index = $input->getString('pax_index', '');
4396 + $pax_index = $input->getString('pax_index', '');
4069 4397
4070 4398 if (empty($order_sid) || empty($order_ts)) {
4071 - throw new Exception('Missing booking details', 404);
4399 + VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
4072 4400 }
4073 4401
4074 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';";
4075 4403 $dbo->setQuery($q);
4076 - $dbo->execute();
4077 - if (!$dbo->getNumRows()) {
4078 - throw new Exception('Booking not found', 404);
4404 + $order = $dbo->loadAssoc();
4405 + if (!$order) {
4406 + VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
4079 4407 }
4080 - $order = $dbo->loadAssoc();
4081 4408
4082 4409 $customer = array();
4083 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'].";";
4084 4411 $dbo->setQuery($q);
4085 - $dbo->execute();
4086 - if (!$dbo->getNumRows()) {
4412 + $customer = $dbo->loadObject();
4413 + if (!$customer) {
4087 4414 // one customer must be assigned to this booking for the pre-checkin
4088 - throw new Exception('No customers associated to this booking', 404);
4415 + VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
4089 4416 }
4090 - $customer = $dbo->loadObject();
4091 4417
4092 4418 // make sure pre-checkin is allowed
4093 4419 $precheckin = VikBooking::precheckinEnabled();
4094 4420 if ($precheckin) {
@@ -4093,25 +4419,31 @@
4093 4419 $precheckin = VikBooking::precheckinEnabled();
4094 4420 if ($precheckin) {
4095 4421 // make sure the limit of days in advance is reflected
4096 4422 $precheckin_mind = VikBooking::precheckinMinOffset();
4097 - $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
4098 - $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 + }
4099 4431 }
4100 4432 if (!$precheckin) {
4101 - throw new Exception('Pre-checkin not allowed at this time', 403);
4433 + VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
4102 4434 }
4103 4435
4104 4436 // get uploaded files array (use "raw" to avoid filtering the file to upload)
4105 4437 $files = $input->files->get('docs', array(), 'raw');
4106 - if (!count($files)) {
4107 - throw new Exception('No files to be uploaded', 500);
4438 + if (!$files) {
4439 + VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
4108 4440 }
4109 4441
4110 4442 if (isset($files['name'])) {
4111 4443 // we have a single associative array, we need to push it within a list,
4112 4444 // because the upload iterates the $files array
4113 - $files = array($files);
4445 + $files = [$files];
4114 4446 }
4115 4447
4116 4448 // fetch documents folder path
4117 4449 $dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
@@ -4121,28 +4453,26 @@
4121 4453 // randomize string
4122 4454 $customer->seed = uniqid();
4123 4455
4124 4456 // create blocks for hashed folder
4125 - $parts = array(
4457 + $parts = [
4126 4458 $customer->first_name,
4127 4459 $customer->last_name,
4128 4460 md5(serialize($customer)),
4129 - );
4461 + ];
4130 4462
4131 4463 // join fetched parts
4132 4464 $customer->docsfolder = strtolower(implode('-', array_filter($parts)));
4133 4465
4134 4466 if (strlen($customer->docsfolder) < 16) {
4135 - 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.');
4136 4468 }
4137 4469
4138 - jimport('joomla.filesystem.folder');
4139 -
4140 4470 // create a folder for this customer
4141 4471 $created = JFolder::create($dirpath . $customer->docsfolder);
4142 4472
4143 4473 if (!$created) {
4144 - 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));
4145 4475 }
4146 4476
4147 4477 unset($customer->seed);
4148 4478
@@ -4152,10 +4482,12 @@
4152 4482 $record->docsfolder = $customer->docsfolder;
4153 4483 $dbo->updateObject('#__vikbooking_customers', $record, 'id');
4154 4484 }
4155 4485
4156 - // prepare the response array of uploaded-file objects
4157 - $response = array();
4486 + // prepare the response with the uploaded-file objects
4487 + $response = [
4488 + 'uploads' => [],
4489 + ];
4158 4490 $upload_err = null;
4159 4491
4160 4492 // compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
4161 4493 $file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';
@@ -4173,13 +4505,13 @@
4173 4505 }
4174 4506 // always prepend "pre check-in" to the original file name
4175 4507 $file['name'] = strtolower($file_prefix . $file['name']);
4176 4508 // try to upload the file
4177 - $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');
4178 4510 // set a valid URL for the uploaded file
4179 4511 $result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
4180 4512 // push uploaded file
4181 - array_push($response, $result);
4513 + array_push($response['uploads'], $result);
4182 4514 }
4183 4515
4184 4516 } catch (Exception $e) {
4185 4517 // do nothing, but catch the error
@@ -4185,15 +4517,56 @@
4185 4517 // do nothing, but catch the error
4186 4518 $upload_err = $e;
4187 4519 }
4188 4520
4189 - if (!count($response)) {
4190 - 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');
4191 4527 }
4192 4528
4193 - // output the response
4194 - echo json_encode($response);
4195 - 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);
4196 4569 }
4197 4570
4198 4571 /**
4199 4572 * AJAX endpoint to submit an inquiry/information request.
@@ -4219,11 +4592,11 @@
4219 4592 $checkout_h = $input->getInt('checkouth', 0);
4220 4593 $checkout_m = $input->getInt('checkoutm', 0);
4221 4594 $categories = $input->getString('categories', '');
4222 4595 $roomsnum = $input->getInt('roomsnum', 1);
4223 - $adults = $input->get('adults', array(), 'int');
4224 - $children = $input->get('children', array(), 'int');
4225 - $inquiry = $input->get('inquiry', array(), 'raw');
4596 + $adults = $input->get('adults', [], 'int');
4597 + $children = $input->get('children', [], 'int');
4598 + $inquiry = $input->get('inquiry', [], 'array');
4226 4599 $ulang = $input->getString('ulang', '');
4227 4600
4228 4601 $timeopst = VikBooking::getTimeOpenStore();
4229 4602 if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
@@ -4268,8 +4641,10 @@
4268 4641 if (!is_scalar($info_val) || !strlen($info_val)) {
4269 4642 // empty or invalid field
4270 4643 continue;
4271 4644 }
4645 + // accept only text from submitted value
4646 + $info_val = strip_tags($info_val);
4272 4647 if ($info_type == 'nominative') {
4273 4648 if (empty($t_first_name)) {
4274 4649 $t_first_name = $info_val;
4275 4650 $res_custdata[JText::translate('VBNAME')] = $info_val;
@@ -4698,6 +5073,140 @@
4698 5073 public function crontab()
4699 5074 {
4700 5075 VBOFactory::getCrontabSimulator()->run();
4701 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();
4702 5211 }
4703 5212 }