| @@ -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 | } |