PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.1
VikBooking Hotel Booking Engine & PMS v1.7.1
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
vikbooking / site / helpers / lib.vikbooking.php

lib.vikbooking.php in VikBooking Hotel Booking Engine & PMS 1.7.1, at site/helpers/lib.vikbooking.php

13,329 lines 448.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 if (!class_exists('VikBookingIcons')) {
14 // require the Icons class
15 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "icons.php");
16 }
17
18 if (!function_exists('showSelectVb')) {
19 function showSelectVb($err, $err_code_info = array()) {
20 include(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'error_form.php');
21 }
22 }
23
24 class VikBooking
25 {
26 public static function addJoomlaUser($name, $username, $email, $password) {
27 //new method
28 jimport('joomla.application.component.helper');
29 $params = JComponentHelper::getParams('com_users');
30 $user = new JUser;
31 $data = array();
32 //Get the default new user group, Registered if not specified.
33 $system = $params->get('new_usertype', 2);
34 $data['groups'] = array();
35 $data['groups'][] = $system;
36 $data['name'] = $name;
37 $data['username'] = $username;
38 $data['email'] = self::getVboApplication()->emailToPunycode($email);
39 $data['password'] = $password;
40 $data['password2'] = $password;
41 $data['sendEmail'] = 0; //should the user receive system mails?
42 //$data['block'] = 0;
43 if (!$user->bind($data)) {
44 VikError::raiseWarning('', JText::translate($user->getError()));
45 return false;
46 }
47 if (!$user->save()) {
48 VikError::raiseWarning('', JText::translate($user->getError()));
49 return false;
50 }
51 return $user->id;
52 }
53
54 public static function userIsLogged() {
55 $user = JFactory::getUser();
56
57 return !$user->guest;
58 }
59
60 public static function prepareViewContent()
61 {
62 /**
63 * @wponly JApplication::getMenu() cannot be adapted to WP
64 */
65 }
66
67 public static function isFontAwesomeEnabled()
68 {
69 return VBOFactory::getConfig()->getBool('usefa', true);
70 }
71
72 public static function loadFontAwesome($force_load = false)
73 {
74 if (!self::isFontAwesomeEnabled() && !$force_load) {
75 return false;
76 }
77
78 /**
79 * We let the class VikBookingIcons load the proper FontAwesome libraries.
80 *
81 * @since 1.11
82 */
83 VikBookingIcons::loadAssets();
84
85 return true;
86 }
87
88 /**
89 * Checks if modifications or cancellations via front-end are allowed.
90 * 0 = everything is Disabled.
91 * 1 = Disabled, with request message (default).
92 * 2 = Modification Enabled, Cancellation Disabled.
93 * 3 = Cancellation Enabled, Modification Disabled.
94 * 4 = everything is Enabled.
95 *
96 * @return int
97 */
98 public static function getReservationModCanc()
99 {
100 return VBOFactory::getConfig()->getInt('resmodcanc', 1);
101 }
102
103 public static function getReservationModCancMin()
104 {
105 return VBOFactory::getConfig()->getInt('resmodcancmin', 1);
106 }
107
108 public static function getDefaultDistinctiveFeatures()
109 {
110 return [
111 'VBODEFAULTDISTFEATUREONE' => '',
112 // Below is the default feature for 'Room Code'. One default feature is sufficient
113 // 'VBODEFAULTDISTFEATURETWO' => '',
114 ];
115 }
116
117 /**
118 * Given the room's parameters and index, tries to take the first distinctive feature.
119 *
120 * @param mixed $rparams string to be decoded, or decoded array/object params
121 * @param int $rindex room index to look for, starting from 1.
122 *
123 * @return mixed false on failure, array otherwise [feature name, feature value]
124 *
125 * @since 1.14 (J) - 1.4.0 (WP)
126 */
127 public static function getRoomUnitDistinctiveFeature($rparams, $rindex) {
128 $rindex = (int)$rindex;
129 if ($rindex < 1 || empty($rparams)) {
130 return false;
131 }
132
133 if (is_string($rparams)) {
134 // decode params
135 $rparams = self::getRoomParam('features', $rparams);
136 }
137
138 if (is_object($rparams)) {
139 // typecast to array
140 $rparams = (array)$rparams;
141 }
142
143 if (!is_array($rparams) || !count($rparams)) {
144 return false;
145 }
146
147 if (isset($rparams['features'])) {
148 $rparams = $rparams['features'];
149 }
150
151 $feature = array();
152 foreach ($rparams as $param_index => $rfeatures) {
153 if ((int)$param_index != $rindex || !is_array($rfeatures) || !count($rfeatures)) {
154 continue;
155 }
156 foreach ($rfeatures as $featname => $featval) {
157 if (empty($featval)) {
158 continue;
159 }
160 // use the first distinctive feature
161 $tn_featname = JText::translate($featname);
162 if ($tn_featname == $featname) {
163 // no translation was applied
164 if (VBOPlatformDetection::isWordPress()) {
165 // try to apply a translation through Gettext even if we have to pass a variable
166 $tn_featname = __($featname);
167 } else {
168 // convert the string to a hypothetical INI constant
169 $ini_constant = str_replace(' ', '_', strtoupper($featname));
170 $tn_featname = JText::translate($ini_constant);
171 $tn_featname = $tn_featname == $ini_constant ? $featname : $tn_featname;
172 }
173 }
174 // store values and break loop
175 $feature = array($tn_featname, $featval);
176 break;
177 }
178 }
179
180 return count($feature) ? $feature : false;
181 }
182
183 public static function getRoomUnitNumsUnavailable($order, $idroom) {
184 $dbo = JFactory::getDbo();
185 $unavailable_indexes = array();
186 $first = $order['checkin'];
187 $second = $order['checkout'];
188 $secdiff = $second - $first;
189 $daysdiff = $secdiff / 86400;
190 if (is_int($daysdiff)) {
191 if ($daysdiff < 1) {
192 $daysdiff = 1;
193 }
194 } else {
195 if ($daysdiff < 1) {
196 $daysdiff = 1;
197 } else {
198 $sum = floor($daysdiff) * 86400;
199 $newdiff = $secdiff - $sum;
200 $maxhmore = self::getHoursMoreRb() * 3600;
201 if ($maxhmore >= $newdiff) {
202 $daysdiff = floor($daysdiff);
203 } else {
204 $daysdiff = ceil($daysdiff);
205 }
206 }
207 }
208 $groupdays = self::getGroupDays($first, $second, $daysdiff);
209 $q = "SELECT `b`.`id`,`b`.`checkin`,`b`.`checkout`,`b`.`realback`,`ob`.`idorder`,`ob`.`idbusy`,`or`.`id` AS `or_id`,`or`.`idroom`,`or`.`roomindex`,`o`.`status` ".
210 "FROM `#__vikbooking_busy` AS `b` ".
211 "LEFT JOIN `#__vikbooking_ordersbusy` `ob` ON `ob`.`idbusy`=`b`.`id` ".
212 "LEFT JOIN `#__vikbooking_ordersrooms` `or` ON `or`.`idorder`=`ob`.`idorder` AND `or`.`idorder`!=".(int)$order['id']." ".
213 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` AND `o`.`id`=`ob`.`idorder` AND `o`.`id`!=".(int)$order['id']." ".
214 "WHERE `or`.`idroom`=".(int)$idroom." AND `b`.`checkout` > ".time()." AND `o`.`status`='confirmed' AND `ob`.`idorder`!=".(int)$order['id']." AND `ob`.`idorder` > 0;";
215 $dbo->setQuery($q);
216 $busy = $dbo->loadAssocList();
217 if ($busy) {
218 foreach ($groupdays as $gday) {
219 foreach ($busy as $bu) {
220 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
221 continue;
222 }
223 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
224 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
225 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
226 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
227 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
228 }
229 }
230 }
231 }
232 }
233
234 return $unavailable_indexes;
235 }
236
237 public static function getRoomUnitNumsAvailable($order, $idroom) {
238 $dbo = JFactory::getDbo();
239 $unavailable_indexes = array();
240 $available_indexes = array();
241 $first = $order['checkin'];
242 $second = $order['checkout'];
243 $secdiff = $second - $first;
244 $daysdiff = $secdiff / 86400;
245 if (is_int($daysdiff)) {
246 if ($daysdiff < 1) {
247 $daysdiff = 1;
248 }
249 } else {
250 if ($daysdiff < 1) {
251 $daysdiff = 1;
252 } else {
253 $sum = floor($daysdiff) * 86400;
254 $newdiff = $secdiff - $sum;
255 $maxhmore = self::getHoursMoreRb() * 3600;
256 if ($maxhmore >= $newdiff) {
257 $daysdiff = floor($daysdiff);
258 } else {
259 $daysdiff = ceil($daysdiff);
260 }
261 }
262 }
263 $groupdays = self::getGroupDays($first, $second, $daysdiff);
264 $q = "SELECT `b`.`id`,`b`.`checkin`,`b`.`checkout`,`b`.`realback`,`ob`.`idorder`,`ob`.`idbusy`,`or`.`id` AS `or_id`,`or`.`idroom`,`or`.`roomindex`,`o`.`status` ".
265 "FROM `#__vikbooking_busy` AS `b` ".
266 "LEFT JOIN `#__vikbooking_ordersbusy` `ob` ON `ob`.`idbusy`=`b`.`id` ".
267 "LEFT JOIN `#__vikbooking_ordersrooms` `or` ON `or`.`idorder`=`ob`.`idorder` AND `or`.`idorder`!=".(int)$order['id']." ".
268 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` AND `o`.`id`=`ob`.`idorder` AND `o`.`id`!=".(int)$order['id']." ".
269 "WHERE `or`.`idroom`=".(int)$idroom." AND `b`.`checkout` > ".time()." AND `o`.`status`='confirmed' AND `ob`.`idorder`!=".(int)$order['id']." AND `ob`.`idorder` > 0;";
270 $dbo->setQuery($q);
271 $busy = $dbo->loadAssocList();
272 if ($busy) {
273 foreach ($groupdays as $gday) {
274 foreach ($busy as $bu) {
275 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
276 continue;
277 }
278 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
279 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
280 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
281 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
282 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
283 }
284 }
285 }
286 }
287 }
288 $q = "SELECT `params` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
289 $dbo->setQuery($q);
290 $room_params = $dbo->loadResult();
291 if ($room_params) {
292 $room_params_arr = json_decode($room_params, true);
293 if (array_key_exists('features', $room_params_arr) && is_array($room_params_arr['features']) && count($room_params_arr['features'])) {
294 foreach ($room_params_arr['features'] as $rind => $rfeatures) {
295 if (in_array($rind, $unavailable_indexes)) {
296 continue;
297 }
298 $available_indexes[] = $rind;
299 }
300 }
301 }
302
303 return $available_indexes;
304 }
305
306 /**
307 * Load the restrictions applying the given filters to the passed rooms.
308 * The ordering of the query SHOULD remain unchanged, because it's required
309 * to have it by ascending order of the ID. So the older records first.
310 * Even when no filters, we always exclude expired restrictions.
311 *
312 * @param boolean $filters whether to apply filters
313 * @param array $rooms the list of rooms to filter
314 *
315 * @return array the list of restrictions found or an empty array.
316 */
317 public static function loadRestrictions($filters = true, $rooms = [])
318 {
319 $dbo = JFactory::getDbo();
320
321 $restrictions = [];
322 $limts = strtotime(date('Y-m-d'));
323
324 if (!$filters) {
325 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
326 } else {
327 if (!$rooms) {
328 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
329 } else {
330 $clause = [];
331 foreach ($rooms as $idr) {
332 if (empty($idr)) {
333 continue;
334 }
335 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
336 }
337 if ($clause) {
338 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE (`dto` = 0 OR `dto` >= " . $limts . ") AND (`allrooms` = 1 OR (`allrooms` = 0 AND (" . implode(" OR ", $clause) . "))) ORDER BY `id` ASC;";
339 } else {
340 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
341 }
342 }
343 }
344
345 $dbo->setQuery($q);
346 $allrestrictions = $dbo->loadAssocList();
347
348 foreach ($allrestrictions as $k => $res) {
349 if (!empty($res['month'])) {
350 $restrictions[$res['month']] = $res;
351 } else {
352 if (!isset($restrictions['range'])) {
353 $restrictions['range'] = array();
354 }
355 $restrictions['range'][$k] = $res;
356 }
357 }
358
359 return $restrictions;
360 }
361
362 public static function globalRestrictions($restrictions = [])
363 {
364 $ret = [];
365 foreach ($restrictions as $kr => $rr) {
366 if ($kr == 'range') {
367 foreach ($rr as $kd => $dr) {
368 if ($dr['allrooms'] == 1) {
369 $ret['range'][$kd] = $restrictions[$kr][$kd];
370 }
371 }
372 continue;
373 }
374 if ($rr['allrooms'] == 1) {
375 $ret[$kr] = $restrictions[$kr];
376 }
377 }
378 return $ret;
379 }
380
381 /**
382 * From the given restrictions, check-in, check-out and nights, looks for
383 * a restriction to be returned and applied over these stays.
384 * In order to give priority to newer Restriction IDs, the order of the
385 * $restrictions array should be ascending, so that the newer restrictions
386 * will overwrite the array with the last ID (more recent). Only for date ranges.
387 * Returns an array with the record of the restriction found. The loop is not
388 * broken when the first valid restriction is found to give priority to
389 * newer restriction IDs to overwrite older records.
390 *
391 * @param int $first the unix timestamp for the check-in date
392 * @param int $second the unix timestamp for the check-out date
393 * @param int $daysdiff the number of nights of stay
394 * @param array $restrictions the list of restrictions loaded
395 *
396 * @return array the restriction found, or an empty array.
397 */
398 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
399 $season_restrictions = array();
400 $restrcheckin = getdate($first);
401 $restrcheckout = getdate($second);
402 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
403 //restriction found for this month, checking:
404 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
405 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
406 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
407 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
408 //Week Day Arrival Restriction
409 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
410 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
411 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
412 }
413 $season_restrictions['wdays'] = $rvalidwdays;
414 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
415 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
416 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
417 }
418 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
419 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
420 }
421 }
422 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
423 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
424 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
425 $season_restrictions['allowed'] = false;
426 }
427 }
428 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
429 $season_restrictions['allowed'] = false;
430 }
431 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
432 } elseif (array_key_exists('range', $restrictions)) {
433 foreach ($restrictions['range'] as $restr) {
434 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
435 //restriction found for this date range, checking:
436 $season_restrictions['id'] = $restr['id'];
437 $season_restrictions['name'] = $restr['name'];
438 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
439 if (strlen((string)$restr['wday']) > 0) {
440 //Week Day Arrival Restriction
441 $rvalidwdays = array($restr['wday']);
442 if (strlen((string)$restr['wdaytwo']) > 0) {
443 $rvalidwdays[] = $restr['wdaytwo'];
444 }
445 $season_restrictions['wdays'] = $rvalidwdays;
446 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
447 if (!empty($restr['ctad'])) {
448 $season_restrictions['cta'] = explode(',', $restr['ctad']);
449 }
450 if (!empty($restr['ctdd'])) {
451 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
452 }
453 }
454 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] > $restr['minlos']) {
455 $season_restrictions['maxlos'] = $restr['maxlos'];
456 if ($daysdiff > $restr['maxlos']) {
457 $season_restrictions['allowed'] = false;
458 }
459 }
460 if ($daysdiff < $restr['minlos']) {
461 $season_restrictions['allowed'] = false;
462 }
463 $season_restrictions['minlos'] = $restr['minlos'];
464 }
465 }
466 }
467
468 return $season_restrictions;
469 }
470
471 public static function compareSeasonRestrictionsNights($restrictions)
472 {
473 $base_compare = array();
474 $base_nights = 0;
475 foreach ($restrictions as $nights => $restr) {
476 $base_compare = $restr;
477 $base_nights = $nights;
478 break;
479 }
480
481 /**
482 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
483 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
484 *
485 * @since 1.15.0 (J) - 1.5.0 (WP)
486 */
487 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
488
489 foreach ($restrictions as $nights => $restr) {
490 if ($nights == $base_nights) {
491 continue;
492 }
493
494 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
495 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
496
497 // get associative array of differences
498 $diff = array_diff($use_base_compare, $use_restr);
499
500 if (count($diff) > 0 && array_key_exists('id', $diff)) {
501 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
502 // only one Restriction per time should be applied to certain Season Dates but check just in case.
503 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
504 }
505 }
506
507 return array();
508 }
509
510 /**
511 * Methods using array_diff() may need one-dimension arrays, because this
512 * native functions applies a string comparison, and so casting an array
513 * value to a string would generate a Notice message. This method simply
514 * converts any sub-array into a restorable string into a non-scalar var.
515 * This only works when comparing 2 array variables.
516 *
517 * @param array $arr the hypothetical multi dimension array.
518 *
519 * @return array the one dimension array to be passed to array_diff()
520 * and the list of keys converted to strings for comparison.
521 *
522 * @since 1.15.0 (J) - 1.5.0 (WP)
523 */
524 public static function prepareMultiDimArrayDiff($arr)
525 {
526 $casted_keys = array();
527
528 if (!is_array($arr) || !count($arr)) {
529 return array($casted_keys, $arr);
530 }
531
532 foreach ($arr as $key => $val) {
533 if (!is_scalar($val)) {
534 // memorize original value
535 $casted_keys[$key] = $val;
536 // cast to string must be applied
537 $arr[$key] = json_encode($val);
538 }
539 }
540
541 return array($casted_keys, $arr);
542 }
543
544 /**
545 * Methods using array_diff() may need one-dimension arrays, because this
546 * native functions applies a string comparison, and so casting an array
547 * value to a string would generate a Notice message. This method restores
548 * the original values of the "prepared" arrays by getting the list of the
549 * modified keys from non-scalar to scalar values.
550 * This only works when comparing 2 array variables.
551 *
552 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
553 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
554 * @param array $arr the array result of array_diff().
555 *
556 * @return array the array result of array_diff() with its original values.
557 *
558 * @since 1.15.0 (J) - 1.5.0 (WP)
559 */
560 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
561 {
562 if (!is_array($arr) || !count($arr)) {
563 return $arr;
564 }
565
566 foreach ($arr as $key => $val) {
567 // keys returned by array_diff can be present in both $keys_one and $keys_two
568 if (isset($keys_one[$key])) {
569 // always give higher priority to the 1st argument keys for array_diff()
570 $arr[$key] = json_decode($keys_one[$key]);
571 } elseif (isset($keys_two[$key])) {
572 // fallback to 2nd argument keys
573 $arr[$key] = json_decode($keys_two[$key]);
574 }
575 }
576
577 return $arr;
578 }
579
580 public static function roomRestrictions($roomid, $restrictions) {
581 $ret = array();
582 if (!empty($roomid) && count($restrictions) > 0) {
583 foreach ($restrictions as $kr => $rr) {
584 if ($kr == 'range') {
585 foreach ($rr as $kd => $dr) {
586 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
587 $allrooms = explode(';', $dr['idrooms']);
588 if (in_array('-'.$roomid.'-', $allrooms)) {
589 $ret['range'][$kd] = $restrictions[$kr][$kd];
590 }
591 }
592 }
593 } else {
594 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
595 $allrooms = explode(';', $rr['idrooms']);
596 if (in_array('-'.$roomid.'-', $allrooms)) {
597 $ret[$kr] = $restrictions[$kr];
598 }
599 }
600 }
601 }
602 }
603 return $ret;
604 }
605
606 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
607 {
608 // default states
609 $restrictionerrmsg = '';
610 $restrictions_affcount = 0;
611
612 // check for month-level or range-level restrictions
613 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
614 //restriction found for this month, checking:
615 $restrictions_affcount++;
616 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
617 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
618 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
619 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
620 }
621 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
622 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVAL', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wday']).(strlen($roomrestr[$restrcheckin['mon']]['wdaytwo']) > 0 ? '/'.self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wdaytwo']) : ''));
623 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
624 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
625 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
626 }
627 }
628 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
629 if (count($comborestr) > 0) {
630 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
631 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
632 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBO', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
633 }
634 }
635 }
636 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
637 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
638 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
639 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
640 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
641 }
642 }
643 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
644 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
645 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
646 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
647 }
648 }
649 }
650 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
651 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
652 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
653 }
654 }
655 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
656 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
657 }
658 } elseif (array_key_exists('range', $roomrestr)) {
659 // start valid flag
660 $restrictionsvalid = true;
661
662 /**
663 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
664 * The most recent restrictions will have a higher priority over the oldest ones.
665 *
666 * @since 1.12.1
667 */
668 $minlos_priority = array(
669 'ok' => array(),
670 'nok' => array()
671 );
672
673 /**
674 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
675 *
676 * @since 1.16.10 (J) - 1.6.10 (WP)
677 */
678 $ctad_priority = [
679 'ok' => [],
680 'nok' => [],
681 ];
682
683 foreach ($roomrestr['range'] as $restr) {
684 /**
685 * We should not always add 82799 seconds to the end date of the restriction
686 * because if they only last for one day (like a Saturday), then $restr['dto']
687 * will be already set to the time 23:59:59.
688 *
689 * @since 1.13 (J) - 1.2.18 (WP)
690 */
691 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) ? 82799 : 0;
692
693 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
694 // restriction found for this date range based on arrival date, check if compliant
695 $restrictions_affcount++;
696
697 // set flag for CTA/CTD compliance
698 $cta_ctd_passed = true;
699
700 if (strlen((string)$restr['wday']) > 0) {
701 $rvalidwdays = array($restr['wday']);
702 if (strlen((string)$restr['wdaytwo']) > 0) {
703 $rvalidwdays[] = $restr['wdaytwo'];
704 }
705 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
706 $restrictionsvalid = false;
707 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
708 } elseif ($restr['multiplyminlos'] == 1) {
709 if (($daysdiff % $restr['minlos']) != 0) {
710 $restrictionsvalid = false;
711 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
712 }
713 }
714 $comborestr = self::parseJsDrangeWdayCombo($restr);
715 if ($comborestr) {
716 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
717 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
718 $restrictionsvalid = false;
719 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBORANGE', self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
720 }
721 }
722 }
723 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
724 if (!empty($restr['ctad'])) {
725 $ctarestrictions = explode(',', $restr['ctad']);
726 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
727 $restrictionsvalid = false;
728 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
729 $cta_ctd_passed = false;
730 }
731 }
732 if (!empty($restr['ctdd'])) {
733 $ctdrestrictions = explode(',', $restr['ctdd']);
734 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
735 $restrictionsvalid = false;
736 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
737 $cta_ctd_passed = false;
738 }
739 }
740 }
741
742 // check CTA/CTD compliance
743 if (!$cta_ctd_passed) {
744 array_push($ctad_priority['nok'], (int)$restr['id']);
745 } else {
746 array_push($ctad_priority['ok'], (int)$restr['id']);
747 }
748
749 // max LOS validation
750 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] > $restr['minlos']) {
751 if ($daysdiff > $restr['maxlos']) {
752 $restrictionsvalid = false;
753 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
754 }
755 }
756
757 // min LOS validation
758 if ($daysdiff < $restr['minlos']) {
759 $restrictionsvalid = false;
760 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
761 array_push($minlos_priority['nok'], (int)$restr['id']);
762 } else {
763 array_push($minlos_priority['ok'], (int)$restr['id']);
764 }
765 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
766 /**
767 * We validate the CTD restrictions depending on the check-out date.
768 *
769 * @since 1.16.3 (J) - 1.6.3 (WP)
770 */
771 $ctdrestrictions = explode(',', $restr['ctdd']);
772 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
773 $restrictions_affcount++;
774 $restrictionsvalid = false;
775 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
776 }
777 }
778 }
779
780 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
781 // a more recent restriction is allowing this MinLOS
782 // ensure there are no recent CTA/CTD rules not compliant
783 $cta_ctd_priority_ok = true;
784
785 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
786 // a more recent restriction is not compliant with the CTA/CTD rules
787 $cta_ctd_priority_ok = false;
788 }
789
790 if ($cta_ctd_priority_ok) {
791 // we unset the error message because more recent restriction(s) are allowing this stay dates
792 $restrictionerrmsg = '';
793 }
794 }
795 }
796
797 // check global restriction of Min LOS for TAC functions in VBO and VCM
798 if (empty($restrictionerrmsg) && count($roomrestr) && $restrictions_affcount <= 0) {
799 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
800 $globminlos = self::getDefaultNightsCalendar();
801 if ($globminlos > 1 && $daysdiff < $globminlos) {
802 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
803 }
804 }
805
806 // return the restriction error message string, if anything wrong was found
807 return $restrictionerrmsg;
808 }
809
810 public static function parseJsDrangeWdayCombo($drestr) {
811 $combo = array();
812 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
813 $cparts = explode(':', $drestr['wdaycombo']);
814 foreach ($cparts as $kc => $cw) {
815 if (!empty($cw)) {
816 $nowcombo = explode('-', $cw);
817 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
818 }
819 }
820 }
821 return $combo;
822 }
823
824 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
825 $dbo = JFactory::getDbo();
826 $pkg = array();
827 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
828 $dbo->setQuery($q);
829 $dbo->execute();
830 if ($dbo->getNumRows() == 1) {
831 $pkg = $dbo->loadAssoc();
832 $vbo_tn = self::getTranslator();
833 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
834 } else {
835 return JText::translate('VBOPKGERRNOTFOUND');
836 }
837 $rooms_req = array();
838 foreach ($rooms as $num => $room) {
839 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
840 $rooms_req[] = $room['id'];
841 }
842 }
843 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
844 $dbo->setQuery($q);
845 $dbo->execute();
846 if ($dbo->getNumRows() != count($rooms_req)) {
847 //error, not all the rooms requested are available for this package
848 return JText::translate('VBOPKGERRNOTROOM');
849 }
850 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
851 return JText::translate('VBOPKGERRNUMNIGHTS');
852 }
853 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
854 return JText::translate('VBOPKGERRCHECKIND');
855 }
856 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
857 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
858 return JText::translate('VBOPKGERRCHECKOUTD');
859 }
860 if (!empty($pkg['excldates'])) {
861 //this would check if any stay date is excluded
862 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
863 //check just the arrival and departure dates
864 $bookdates_ts = array($checkints, $checkoutts);
865 $bookdates = array();
866 foreach ($bookdates_ts as $bookdate_ts) {
867 $info_d = getdate($bookdate_ts);
868 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
869 }
870 $edates = explode(';', $pkg['excldates']);
871 foreach ($edates as $edate) {
872 if (!empty($edate) && in_array($edate, $bookdates)) {
873 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
874 }
875 }
876 }
877 return $pkg;
878 }
879
880 public static function getPackage($pkg_id) {
881 $dbo = JFactory::getDbo();
882 $pkg = array();
883 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
884 $dbo->setQuery($q);
885 $dbo->execute();
886 if ($dbo->getNumRows() == 1) {
887 $pkg = $dbo->loadAssoc();
888 }
889 return $pkg;
890 }
891
892 public static function getRoomParam($paramname, $paramstr)
893 {
894 if (empty($paramstr)) {
895 return '';
896 }
897
898 $paramarr = json_decode($paramstr, true);
899
900 if (is_array($paramarr) && isset($paramarr[$paramname])) {
901 return $paramarr[$paramname];
902 }
903
904 return '';
905 }
906
907 public static function filterNightsSeasonsCal($arr_nights) {
908 $nights = array();
909 foreach ($arr_nights as $night) {
910 if (intval(trim($night)) > 0) {
911 $nights[] = intval(trim($night));
912 }
913 }
914 sort($nights);
915 return array_unique($nights);
916 }
917
918 public static function getSeasonRangeTs($from, $to, $year) {
919 $sfrom = 0;
920 $sto = 0;
921 $tsbase = mktime(0, 0, 0, 1, 1, $year);
922 $curyear = $year;
923 $tsbasetwo = $tsbase;
924 $curyeartwo = $year;
925 if ($from > $to) {
926 //between two years
927 $curyeartwo += 1;
928 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
929 }
930 $sfrom = ($tsbase + $from);
931 $sto = ($tsbasetwo + $to);
932 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
933 //leap years
934 $infoseason = getdate($sfrom);
935 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
936 if ($infoseason[0] > $leapts) {
937 /**
938 * Timestamp must be greater than the leap-day of Feb 29th.
939 * It used to be checked for >= $leapts.
940 *
941 * @since July 3rd 2019
942 */
943 $sfrom += 86400;
944 if ($curyear == $curyeartwo) {
945 $sto += 86400;
946 }
947 }
948 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
949 //leap years
950 $infoseason = getdate($sto);
951 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
952 if ($infoseason[0] > $leapts) {
953 /**
954 * Timestamp must be greater than the leap-day of Feb 29th.
955 * It used to be checked for >= $leapts.
956 *
957 * @since July 3rd 2019
958 */
959 $sto += 86400;
960 }
961 }
962 return array($sfrom, $sto);
963 }
964
965 public static function sortSeasonsRangeTs ($all_seasons) {
966 $sorted = array();
967 $map = array();
968 foreach ($all_seasons as $key => $season) {
969 $map[$key] = $season['from_ts'];
970 }
971 asort($map);
972 foreach ($map as $key => $s) {
973 $sorted[] = $all_seasons[$key];
974 }
975 return $sorted;
976 }
977
978 public static function formatSeasonDates ($from_ts, $to_ts) {
979 $one = getdate($from_ts);
980 $two = getdate($to_ts);
981 $months_map = array(
982 1 => JText::translate('VBSHORTMONTHONE'),
983 2 => JText::translate('VBSHORTMONTHTWO'),
984 3 => JText::translate('VBSHORTMONTHTHREE'),
985 4 => JText::translate('VBSHORTMONTHFOUR'),
986 5 => JText::translate('VBSHORTMONTHFIVE'),
987 6 => JText::translate('VBSHORTMONTHSIX'),
988 7 => JText::translate('VBSHORTMONTHSEVEN'),
989 8 => JText::translate('VBSHORTMONTHEIGHT'),
990 9 => JText::translate('VBSHORTMONTHNINE'),
991 10 => JText::translate('VBSHORTMONTHTEN'),
992 11 => JText::translate('VBSHORTMONTHELEVEN'),
993 12 => JText::translate('VBSHORTMONTHTWELVE')
994 );
995 $mday_map = array(
996 1 => JText::translate('VBMDAYFRIST'),
997 2 => JText::translate('VBMDAYSECOND'),
998 3 => JText::translate('VBMDAYTHIRD'),
999 'generic' => JText::translate('VBMDAYNUMGEN')
1000 );
1001 if ($one['year'] == $two['year']) {
1002 return $one['year'].' '.$months_map[(int)$one['mon']].' '.$one['mday'].'<sup>'.(array_key_exists((int)substr($one['mday'], -1), $mday_map) && ($one['mday'] < 10 || $one['mday'] > 20) ? $mday_map[(int)substr($one['mday'], -1)] : $mday_map['generic']).'</sup> - '.$months_map[(int)$two['mon']].' '.$two['mday'].'<sup>'.(array_key_exists((int)substr($two['mday'], -1), $mday_map) && ($two['mday'] < 10 || $two['mday'] > 20) ? $mday_map[(int)substr($two['mday'], -1)] : $mday_map['generic']).'</sup>';
1003 }
1004 return $months_map[(int)$one['mon']].' '.$one['mday'].'<sup>'.(array_key_exists((int)substr($one['mday'], -1), $mday_map) && ($one['mday'] < 10 || $one['mday'] > 20) ? $mday_map[(int)substr($one['mday'], -1)] : $mday_map['generic']).'</sup> '.$one['year'].' - '.$months_map[(int)$two['mon']].' '.$two['mday'].'<sup>'.(array_key_exists((int)substr($two['mday'], -1), $mday_map) && ($two['mday'] < 10 || $two['mday'] > 20) ? $mday_map[(int)substr($two['mday'], -1)] : $mday_map['generic']).'</sup> '.$two['year'];
1005 }
1006
1007 public static function getFirstCustDataField($custdata) {
1008 $first_field = '';
1009 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1010 //Room is closed with this booking
1011 return '----';
1012 }
1013 $parts = explode("\n", $custdata);
1014 foreach ($parts as $part) {
1015 if (!empty($part)) {
1016 $field = explode(':', trim($part));
1017 if (!empty($field[1])) {
1018 return trim($field[1]);
1019 }
1020 }
1021 }
1022 return $first_field;
1023 }
1024
1025 /**
1026 * This method composes a string to be logged for the admin
1027 * to keep track of what was inside the booking before the
1028 * modification. Returns a string and it uses language definitions
1029 * that should be available on the front-end and back-end INI files.
1030 *
1031 * @param array $old_booking the array of the booking prior to the modification
1032 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1033 *
1034 * @return string text describing the situation with the booking before the changes.
1035 *
1036 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1037 */
1038 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1039 {
1040 $vbo_df = self::getDateFormat();
1041 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1042 $wdays_map = array(
1043 JText::translate('VBWEEKDAYZERO'),
1044 JText::translate('VBWEEKDAYONE'),
1045 JText::translate('VBWEEKDAYTWO'),
1046 JText::translate('VBWEEKDAYTHREE'),
1047 JText::translate('VBWEEKDAYFOUR'),
1048 JText::translate('VBWEEKDAYFIVE'),
1049 JText::translate('VBWEEKDAYSIX'),
1050 );
1051 $now_info = getdate();
1052 $checkin_info = getdate($old_booking['checkin']);
1053 $checkout_info = getdate($old_booking['checkout']);
1054
1055 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1056 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1057 $prev_dates = $prev_nights.' - '.$wdays_map[$checkin_info['wday']].', '.date($df.' H:i', $checkin_info[0]).' - '.$wdays_map[$checkout_info['wday']].', '.date($df.' H:i', $checkout_info[0]);
1058 $prev_rooms = '';
1059
1060 $orooms_map = [];
1061 $orooms_arr = [];
1062
1063 if (isset($old_booking['rooms_info'])) {
1064 foreach ($old_booking['rooms_info'] as $oroom) {
1065 $orooms_arr[] = $oroom['name'].', '.JText::translate('VBMAILADULTS').': '.$oroom['adults'].', '.JText::translate('VBMAILCHILDREN').': '.$oroom['children'];
1066 if (!empty($oroom['idroom'])) {
1067 $orooms_map[$oroom['idroom']] = $oroom['name'];
1068 }
1069 }
1070 $prev_rooms = implode("\n", $orooms_arr);
1071 }
1072
1073 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && count($orooms_map)) {
1074 $split_stay_prev_infos = [];
1075 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1076 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1077 continue;
1078 }
1079 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1080 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1081 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1082
1083 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1084 if (!empty($room_nights)) {
1085 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1086 }
1087 $split_stay_descr .= ', ';
1088 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1089
1090 // push split stay description string
1091 $split_stay_prev_infos[] = $split_stay_descr;
1092 }
1093 if (count($split_stay_prev_infos)) {
1094 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1095 }
1096 }
1097
1098 $currencyname = self::getCurrencyName();
1099 $prev_total = $currencyname.' '.self::numberFormat($old_booking['total']);
1100
1101 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1102 }
1103
1104 /**
1105 * This method invokes the class
1106 * VikChannelManagerLogos (new in VCM 1.6.4) to
1107 * map the name of a channel to its corresponding logo.
1108 * The method can also be used to get an istance of the class.
1109 *
1110 * @param mixed $provenience either a string or an array with main and sub channels.
1111 * @param boolean $get_istance
1112 *
1113 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1114 */
1115 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1116 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1117 return false;
1118 }
1119 if (!class_exists('VikChannelManagerLogos')) {
1120 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1121 }
1122
1123 /**
1124 * Due to the new iCal channel we now support main and sub channels.
1125 *
1126 * @since 1.13 - VCM 1.7.0
1127 */
1128 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1129 $provenience = explode('_', $provenience);
1130 }
1131 $main_channel = '';
1132 $full_channel = '';
1133 if (is_array($provenience)) {
1134 if (count($provenience) > 1) {
1135 $main_channel = $provenience[1];
1136 } else {
1137 $main_channel = $provenience[0];
1138 }
1139 if (stripos($provenience[0], 'ical') !== false) {
1140 $full_channel = implode('_', $provenience);
1141 }
1142 } else {
1143 $main_channel = $provenience;
1144 }
1145
1146 // get object instance by passing the main provenience
1147 $obj = new VikChannelManagerLogos($main_channel);
1148
1149 // update provenience with main and full channel
1150 if (!empty($full_channel)) {
1151 $obj->setProvenience($main_channel, $full_channel);
1152 }
1153
1154 // return either the instance or the logo URL for this channel source
1155 return $get_istance ? $obj : $obj->getLogoURL();
1156 }
1157
1158 public static function vcmAutoUpdate()
1159 {
1160 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1161 return -1;
1162 }
1163
1164 $vcm_auto_upd = (int)VBOFactory::getConfig()->get('vcmautoupd', 0);
1165
1166 return $vcm_auto_upd ? 1 : 0;
1167 }
1168
1169 public static function getVcmInvoker()
1170 {
1171 if (!class_exists('VboVcmInvoker')) {
1172 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1173 }
1174
1175 return new VboVcmInvoker();
1176 }
1177
1178 /**
1179 * Returns an instance of VikBooking History object.
1180 *
1181 * @param int $bid optional booking ID to bind.
1182 *
1183 * @return VboBookingHistory
1184 *
1185 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1186 */
1187 public static function getBookingHistoryInstance($bid = 0)
1188 {
1189 if (!class_exists('VboBookingHistory')) {
1190 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1191 }
1192
1193 return new VboBookingHistory($bid);
1194 }
1195
1196 /**
1197 * Returns an instance of the VCMChatHandler class to handle
1198 * the messaging/chat for the given reservation ID.
1199 *
1200 * @param int $oid the ID of the booking in VBO
1201 * @param string $channel the name of the source channel
1202 *
1203 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1204 *
1205 * @since 1.11.2 (J) - 1.1.2 (WP)
1206 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1207 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1208 */
1209 public static function getVcmChatInstance($oid, $channel = null)
1210 {
1211 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1212 if (!is_file($vcm_messaging_helper)) {
1213 // VCM is not available
1214 return null;
1215 }
1216
1217 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1218 // VCM must be inactive, prevent the loading of the instance
1219 return null;
1220 }
1221
1222 // make sure the channel name is correct as it may contain the sub-network
1223 if (!empty($channel)) {
1224 $segments = explode('_', $channel);
1225 if (count($segments) > 1) {
1226 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1227 $channel = $segments[0];
1228 }
1229 }
1230
1231 // always require main file of the abstract class even if arguments are empty/invalid
1232 require_once $vcm_messaging_helper;
1233
1234 // return the instance of the class for this channel handler
1235 return VCMChatHandler::getInstance($oid, $channel);
1236 }
1237
1238 /**
1239 * Returns an instance of the VCMOpportunityHandler class to handle
1240 * the various opportunities through VCM.
1241 *
1242 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1243 *
1244 * @since 1.2.0
1245 */
1246 public static function getVcmOpportunityInstance() {
1247 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1248 if (!is_file($vcm_opp_helper)) {
1249 // VCM is not available
1250 return null;
1251 }
1252 // require main file of the class
1253 require_once $vcm_opp_helper;
1254 // return the instance of the class
1255 return VCMOpportunityHandler::getInstance();
1256 }
1257
1258 /**
1259 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1260 *
1261 * @param array $data optional data to bind to the object.
1262 *
1263 * @return bool
1264 *
1265 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1266 *
1267 * @requires VCM >= 1.8.24
1268 */
1269 public static function vcmBcomReportingSupported(array $data = [])
1270 {
1271 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1272 }
1273
1274 /**
1275 * Gets a list of all channels supporting the promotions.
1276 *
1277 * @param string $key the key of the handler.
1278 *
1279 * @return mixed false if VCM is not installed, empty array otherwise.
1280 *
1281 * @requires Vik Channel Manager 1.7.1
1282 *
1283 * @since 1.3.0
1284 */
1285 public static function getPromotionHandlers($key = null)
1286 {
1287 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1288 // VCM not installed
1289 return false;
1290 }
1291
1292 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1293 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1294 return [];
1295 }
1296
1297 return VikChannelManager::getPromotionHandlers($key);
1298 }
1299
1300 /**
1301 * Gets the factors for suggesting the application of the promotions.
1302 *
1303 * @param mixed $data some optional instructions to be passed as argument.
1304 *
1305 * @return mixed false if VCM is not installed, associative array otherwise.
1306 *
1307 * @requires Vik Channel Manager 1.7.1
1308 *
1309 * @since 1.3.0
1310 */
1311 public static function getPromotionFactors($data = null)
1312 {
1313 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1314 // VCM not installed
1315 return false;
1316 }
1317
1318 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1319 // VCM is outdated
1320 return false;
1321 }
1322
1323 return VikChannelManager::getPromotionFactors($data);
1324 }
1325
1326 public static function getTheme()
1327 {
1328 return VBOFactory::getConfig()->get('theme', '');
1329 }
1330
1331 public static function getFooterOrdMail($vbo_tn = null)
1332 {
1333 $dbo = JFactory::getDbo();
1334
1335 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1336 $dbo->setQuery($q);
1337 $ft = $dbo->loadAssocList();
1338
1339 if (!is_object($vbo_tn)) {
1340 $vbo_tn = self::getTranslator();
1341 }
1342
1343 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1344
1345 return $ft ? $ft[0]['setting'] : '';
1346 }
1347
1348 public static function requireLogin()
1349 {
1350 return VBOFactory::getConfig()->getBool('requirelogin', false);
1351 }
1352
1353 public static function autoRoomUnit()
1354 {
1355 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1356 }
1357
1358 public static function todayBookings()
1359 {
1360 static $todayBookings = null;
1361
1362 if ($todayBookings === null) {
1363 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1364 }
1365
1366 return $todayBookings;
1367 }
1368
1369 public static function couponsEnabled()
1370 {
1371 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1372 }
1373
1374 public static function customersPinEnabled()
1375 {
1376 return VBOFactory::getConfig()->getBool('enablepin', false);
1377 }
1378
1379 /**
1380 * Detects the type of visitor from the user agent.
1381 * Known types are: computer, smartphone, tablet.
1382 *
1383 * @param boolean $returnua whether the type of visitor should be returned. If false
1384 * boolean is returned in case of mobile device detected.
1385 * @param boolean $loadassets whether the system should load an apposite CSS file.
1386 *
1387 * @return mixed string for the type of visitor or boolean if mobile detected.
1388 *
1389 * @since 1.0.13 - Revision September 2018
1390 */
1391 public static function detectUserAgent($returnua = false, $loadassets = true) {
1392 $session = JFactory::getSession();
1393 $sval = $session->get('vbuseragent', '');
1394 $mobiles = array('tablet', 'smartphone');
1395 if (!empty($sval)) {
1396 if ($loadassets) {
1397 self::userAgentStyleSheet($sval);
1398 }
1399 return $returnua ? $sval : in_array($sval, $mobiles);
1400 }
1401
1402 // detect visitor (device) type
1403 $visitoris = 'computer';
1404 try {
1405 if (!class_exists('MobileDetector')) {
1406 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1407 }
1408 $detector = new MobileDetector;
1409 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1410 } catch (Exception $e) {
1411 // do nothing
1412 }
1413
1414 $session->set('vbuseragent', $visitoris);
1415 if ($loadassets) {
1416 self::userAgentStyleSheet($visitoris);
1417 }
1418
1419 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1420 }
1421
1422 public static function userAgentStyleSheet($ua)
1423 {
1424 /**
1425 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1426 */
1427 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1428 return;
1429 }
1430 //
1431
1432 $document = JFactory::getDocument();
1433 /**
1434 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1435 */
1436 if ($ua == 'smartphone') {
1437 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1438 } elseif ($ua == 'tablet') {
1439 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1440 }
1441 return true;
1442 }
1443
1444 public static function loadJquery()
1445 {
1446 return VBOFactory::getConfig()->getBool('loadjquery', true);
1447 }
1448
1449 public static function loadBootstrap()
1450 {
1451 return VBOFactory::getConfig()->getBool('bootstrap', true);
1452 }
1453
1454 public static function allowMultiLanguage()
1455 {
1456 return VBOFactory::getConfig()->getBool('multilang', true);
1457 }
1458
1459 public static function getTranslator()
1460 {
1461 if (!class_exists('VikBookingTranslator')) {
1462 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1463 }
1464
1465 return new VikBookingTranslator();
1466 }
1467
1468 /**
1469 * New method's name spelled correctly. The object will be cached by default.
1470 *
1471 * @return VikBookingCustomersPin
1472 *
1473 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1474 */
1475 public static function getCPinInstance()
1476 {
1477 static $cpin_instance = null;
1478
1479 if (!$cpin_instance) {
1480 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1481
1482 $cpin_instance = new VikBookingCustomersPin();
1483 }
1484
1485 return $cpin_instance;
1486 }
1487
1488 /**
1489 * Old method's name spelled wrongly for BC.
1490 *
1491 * @return VikBookingCustomersPin
1492 */
1493 public static function getCPinIstance()
1494 {
1495 return self::getCPinInstance();
1496 }
1497
1498 /**
1499 * Returns an instance of the VikBookingTracker Class.
1500 * It is also possible to call this method to just require the library.
1501 * This is useful for the back-end to access some static methods
1502 * without tracking any data.
1503 *
1504 * @param boolean $require_only whether to return the object.
1505 *
1506 * @return VikBookingTracker
1507 *
1508 * @since 1.11
1509 */
1510 public static function getTracker($require_only = false) {
1511 if (!class_exists('VikBookingTracker')) {
1512 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1513 }
1514 return $require_only ? true : VikBookingTracker::getInstance();
1515 }
1516
1517 /**
1518 * Returns an instance of the VikBookingOperator Class.
1519 *
1520 * @return VikBookingOperator
1521 *
1522 * @since 1.11
1523 */
1524 public static function getOperatorInstance() {
1525 if (!class_exists('VikBookingOperator')) {
1526 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1527 }
1528 return VikBookingOperator::getInstance();
1529 }
1530
1531 /**
1532 * Returns an instance of the VikBookingFestivities Class.
1533 *
1534 * @return VikBookingFestivities
1535 *
1536 * @since 1.12
1537 */
1538 public static function getFestivitiesInstance() {
1539 if (!class_exists('VikBookingFestivities')) {
1540 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1541 }
1542 return VikBookingFestivities::getInstance();
1543 }
1544
1545 /**
1546 * Returns an instance of the VikBookingCriticalDates Class.
1547 *
1548 * @return VikBookingCriticalDates
1549 *
1550 * @since 1.13.5
1551 */
1552 public static function getCriticalDatesInstance() {
1553 if (!class_exists('VikBookingCriticalDates')) {
1554 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1555 }
1556 return VikBookingCriticalDates::getInstance();
1557 }
1558
1559 /**
1560 * Checks whether the chat is enabled.
1561 *
1562 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1563 *
1564 * @since 1.12
1565 */
1566 public static function chatEnabled() {
1567 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1568 return -1;
1569 }
1570 $dbo = JFactory::getDbo();
1571 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1572 $dbo->setQuery($q);
1573 $dbo->execute();
1574 if (!$dbo->getNumRows()) {
1575 // enable the chat by default if VCM is installed
1576 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1577 $dbo->setQuery($q);
1578 $dbo->execute();
1579 return 1;
1580 }
1581 $s = $dbo->loadResult();
1582 return intval($s);
1583 }
1584
1585 /**
1586 * Loads the chat parameters from the configuration.
1587 *
1588 * @return object stdClass object from decoded JSON string
1589 *
1590 * @since 1.12
1591 */
1592 public static function getChatParams() {
1593 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1594 return new stdClass;
1595 }
1596 $dbo = JFactory::getDbo();
1597 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1598 $dbo->setQuery($q);
1599 $dbo->execute();
1600 if (!$dbo->getNumRows()) {
1601 // compose default and basic chat params for the first time
1602 $basic_params = new stdClass;
1603 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1604 $basic_params->av_type = 'checkin';
1605 $basic_params->av_days = 0;
1606
1607 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1608 $dbo->setQuery($q);
1609 $dbo->execute();
1610
1611 return $basic_params;
1612 }
1613 return json_decode($dbo->loadResult());
1614 }
1615
1616 /**
1617 * Checks whether the pre-checkin is enabled.
1618 *
1619 * @return int 0 if disabled, 1 otherwise
1620 *
1621 * @since 1.12
1622 */
1623 public static function precheckinEnabled() {
1624 $dbo = JFactory::getDbo();
1625 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinenabled';";
1626 $dbo->setQuery($q);
1627 $dbo->execute();
1628 if (!$dbo->getNumRows()) {
1629 // enable the pre-checkin by default
1630 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinenabled', '1');";
1631 $dbo->setQuery($q);
1632 $dbo->execute();
1633 return 1;
1634 }
1635 $s = $dbo->loadResult();
1636 return intval($s);
1637 }
1638
1639 /**
1640 * Returns the minimum number of days in advance to
1641 * enable the pre-checkin via front-end.
1642 *
1643 * @return int the min number of days in advance for pre-checkin.
1644 *
1645 * @since 1.12
1646 */
1647 public static function precheckinMinOffset() {
1648 $dbo = JFactory::getDbo();
1649 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinminoffset';";
1650 $dbo->setQuery($q);
1651 $dbo->execute();
1652 if (!$dbo->getNumRows()) {
1653 // set the limit to 1 day before arrival by default
1654 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinminoffset', '1');";
1655 $dbo->setQuery($q);
1656 $dbo->execute();
1657 return 1;
1658 }
1659 $s = $dbo->loadResult();
1660 return intval($s);
1661 }
1662
1663 /**
1664 * Whether upselling extra services is enabled.
1665 *
1666 * @return boolean true if enabled, false otherwise.
1667 *
1668 * @since 1.13
1669 */
1670 public static function upsellingEnabled() {
1671 $dbo = JFactory::getDbo();
1672 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='upselling';";
1673 $dbo->setQuery($q);
1674 $dbo->execute();
1675 if (!$dbo->getNumRows()) {
1676 // enable the upselling feature by default
1677 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('upselling', '1');";
1678 $dbo->setQuery($q);
1679 $dbo->execute();
1680 return true;
1681 }
1682 return (intval($dbo->loadResult()) > 0);
1683 }
1684
1685 /**
1686 * Collects a list of options/extra that can be upsold for the
1687 * the rooms booked.
1688 *
1689 * @param array $upsell_data list of stdClass objects for each room booked.
1690 * @param array $info some booking details (id, checkin, checkout).
1691 * @param object $vbo_tn the translation object.
1692 *
1693 * @return array list of upsellable options for each room booked.
1694 *
1695 * @since 1.3.0
1696 */
1697 public static function loadUpsellingData($upsell_data, $info, $vbo_tn) {
1698 $dbo = JFactory::getDbo();
1699 // get all options for all rooms booked
1700 $all_room_ids = array();
1701 foreach ($upsell_data as $v) {
1702 if (!in_array($v->id, $all_room_ids)) {
1703 array_push($all_room_ids, $v->id);
1704 }
1705 }
1706 $q = "SELECT `id`, `idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(', ', $all_room_ids) . ");";
1707 $dbo->setQuery($q);
1708 $dbo->execute();
1709 if (!$dbo->getNumRows()) {
1710 // no rooms found
1711 return array();
1712 }
1713 $records = $dbo->loadAssocList();
1714 $all_options = array();
1715 $rooms_options = array();
1716 foreach ($records as $v) {
1717 $allopts = explode(';', $v['idopt']);
1718 $room_opt = array();
1719 foreach ($allopts as $o) {
1720 if (empty($o)) {
1721 continue;
1722 }
1723 if (!in_array((int)$o, $all_options)) {
1724 array_push($all_options, (int)$o);
1725 }
1726 array_push($room_opt, (int)$o);
1727 }
1728 $rooms_options[$v['id']] = $room_opt;
1729 }
1730 if (!count($all_options)) {
1731 // no options found
1732 return array();
1733 }
1734 // load all options that could be used by the booked rooms no matter what was already booked
1735 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', $all_options) . ") AND `forcesel`=0 AND `ifchildren`=0 AND `is_citytax`=0 AND `is_fee`=0;";
1736 $dbo->setQuery($q);
1737 $dbo->execute();
1738 if (!$dbo->getNumRows()) {
1739 // no upsell-able options found
1740 return array();
1741 }
1742 $records = $dbo->loadAssocList();
1743 // filter options by available date and translate records
1744 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1745 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1746 $records = !is_array($records) ? array() : $records;
1747 //
1748 $tot_upsellable = 0;
1749 foreach ($upsell_data as $k => $rdata) {
1750 if (!isset($upsell_data[$k]->upsellable)) {
1751 $upsell_data[$k]->upsellable = array();
1752 }
1753 foreach ($records as $opt) {
1754 if (!empty($opt['ageintervals'])) {
1755 // upsellable options should not contain age intervals for children
1756 continue;
1757 }
1758 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
1759 // this option is not assigned to this room
1760 continue;
1761 }
1762 // check if the option is suited for this room party
1763 $clone_opt = array($opt);
1764 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
1765 if (!is_array($clone_opt) || !count($clone_opt)) {
1766 // this option is not suited for this room party
1767 continue;
1768 }
1769 //
1770 if (in_array($opt['id'], $rdata->options)) {
1771 // this option has already been booked
1772 continue;
1773 }
1774 // push this option and increase counter
1775 array_push($upsell_data[$k]->upsellable, $opt);
1776 $tot_upsellable++;
1777 }
1778 }
1779
1780 // if no upsellable options were found, we return an empty array
1781 return $tot_upsellable > 0 ? $upsell_data : array();
1782 }
1783
1784 /**
1785 * Returns the minimum days in advance for booking, by considering by default
1786 * also the property closing dates. If the property is currently closed, then
1787 * the minimum number of days in advance will be increased.
1788 *
1789 * @param bool $no_closing_dates whether to skip checking closing dates.
1790 *
1791 * @return int the number of days in advance for booking.
1792 *
1793 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
1794 * getMinDaysAdvance($skipsession = false) to:
1795 * getMinDaysAdvance($no_closing_dates = false)
1796 * This was made to return the proper number of days in advance
1797 * in case the property is currently closed.
1798 */
1799 public static function getMinDaysAdvance($no_closing_dates = false) {
1800 // cache value in static var
1801 static $getMinDaysAdvance = null;
1802
1803 if ($getMinDaysAdvance) {
1804 return $getMinDaysAdvance;
1805 }
1806 //
1807
1808 $dbo = JFactory::getDbo();
1809
1810 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='mindaysadvance';";
1811 $dbo->setQuery($q);
1812 $dbo->execute();
1813 $mind = $dbo->getNumRows() ? (int)$dbo->loadResult() : 0;
1814
1815 // update cached var
1816 $getMinDaysAdvance = $mind;
1817
1818 if ($no_closing_dates) {
1819 // do not check the closing dates
1820 return $mind;
1821 }
1822
1823 // check if the property is currently closed
1824 $cur_closed_dates = self::getClosingDates();
1825 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
1826 $today_midnight = mktime(0, 0, 0);
1827 $closed_until = null;
1828 foreach ($cur_closed_dates as $kcd => $vcd) {
1829 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
1830 // closing period found
1831 $closed_until = $vcd['to'];
1832 break;
1833 }
1834 }
1835 if ($closed_until !== null) {
1836 // count the number of days until property is closed
1837 $mind = 0;
1838 $today_info = getdate($today_midnight);
1839 while ($today_info[0] <= $closed_until) {
1840 $mind++;
1841 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
1842 }
1843 }
1844 }
1845
1846 // update cached var
1847 $getMinDaysAdvance = $mind;
1848
1849 return $mind;
1850 }
1851
1852 public static function getDefaultNightsCalendar($skipsession = false) {
1853 if ($skipsession) {
1854 $dbo = JFactory::getDbo();
1855 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1856 $dbo->setQuery($q);
1857 $dbo->execute();
1858 $s = $dbo->loadAssocList();
1859 return (int)$s[0]['setting'];
1860 } else {
1861 $session = JFactory::getSession();
1862 $sval = $session->get('vbdefaultNightsCalendar', '');
1863 if (!empty($sval)) {
1864 return (int)$sval;
1865 } else {
1866 $dbo = JFactory::getDbo();
1867 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1868 $dbo->setQuery($q);
1869 $dbo->execute();
1870 $s = $dbo->loadAssocList();
1871 $session->set('vbdefaultNightsCalendar', $s[0]['setting']);
1872 return (int)$s[0]['setting'];
1873 }
1874 }
1875 }
1876
1877 public static function getSearchNumRooms($skipsession = true)
1878 {
1879 $dbo = JFactory::getDbo();
1880 if ($skipsession) {
1881 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1882 $dbo->setQuery($q);
1883 $dbo->execute();
1884 $s = $dbo->loadAssocList();
1885 return (int)$s[0]['setting'];
1886 }
1887 $session = JFactory::getSession();
1888 $sval = $session->get('vbsearchNumRooms', '');
1889 if (!empty($sval)) {
1890 return (int)$sval;
1891 }
1892 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1893 $dbo->setQuery($q);
1894 $dbo->execute();
1895 $s = $dbo->loadAssocList();
1896 $session->set('vbsearchNumRooms', $s[0]['setting']);
1897 return (int)$s[0]['setting'];
1898 }
1899
1900 public static function getSearchNumAdults($skipsession = true)
1901 {
1902 $dbo = JFactory::getDbo();
1903 if ($skipsession) {
1904 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1905 $dbo->setQuery($q);
1906 $dbo->execute();
1907 $s = $dbo->loadAssocList();
1908 return $s[0]['setting'];
1909 }
1910 $session = JFactory::getSession();
1911 $sval = $session->get('vbsearchNumAdults', '');
1912 if (!empty($sval)) {
1913 return $sval;
1914 }
1915 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1916 $dbo->setQuery($q);
1917 $dbo->execute();
1918 $s = $dbo->loadAssocList();
1919 $session->set('vbsearchNumAdults', $s[0]['setting']);
1920 return $s[0]['setting'];
1921 }
1922
1923 public static function getSearchNumChildren($skipsession = true)
1924 {
1925 $dbo = JFactory::getDbo();
1926 if ($skipsession) {
1927 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1928 $dbo->setQuery($q);
1929 $dbo->execute();
1930 $s = $dbo->loadAssocList();
1931 return $s[0]['setting'];
1932 }
1933 $session = JFactory::getSession();
1934 $sval = $session->get('vbsearchNumChildren', '');
1935 if (!empty($sval)) {
1936 return $sval;
1937 }
1938 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1939 $dbo->setQuery($q);
1940 $dbo->execute();
1941 $s = $dbo->loadAssocList();
1942 $session->set('vbsearchNumChildren', $s[0]['setting']);
1943 return $s[0]['setting'];
1944 }
1945
1946 public static function getSmartSearchType($skipsession = true)
1947 {
1948 $dbo = JFactory::getDbo();
1949 if ($skipsession) {
1950 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1951 $dbo->setQuery($q);
1952 $dbo->execute();
1953 $s = $dbo->loadAssocList();
1954 return $s[0]['setting'];
1955 }
1956 $session = JFactory::getSession();
1957 $sval = $session->get('vbsmartSearchType', '');
1958 if (!empty($sval)) {
1959 return $sval;
1960 }
1961 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1962 $dbo->setQuery($q);
1963 $dbo->execute();
1964 $s = $dbo->loadAssocList();
1965 $session->set('vbsmartSearchType', $s[0]['setting']);
1966 return $s[0]['setting'];
1967 }
1968
1969 /**
1970 * Returns the maximum advance booking notice (offset) as a period.
1971 * It either returns the global configuration setting, or the value
1972 * defined at room-level, if any and if requested.
1973 *
1974 * @param int $idroom optional room ID for room-level setting.
1975 *
1976 * @return string the maximum booking notice period (i.e. +1Y).
1977 *
1978 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
1979 * $idroom to support room-level settings.
1980 */
1981 public static function getMaxDateFuture($idroom = 0)
1982 {
1983 static $max_adv_book_pool = [];
1984
1985 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
1986
1987 if (isset($max_adv_book_pool[$pool_id])) {
1988 return $max_adv_book_pool[$pool_id];
1989 }
1990
1991 $config = VBOFactory::getConfig();
1992
1993 if ($pool_id === 'global') {
1994 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
1995 } else {
1996 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
1997 if (!empty($room_level_max_notice)) {
1998 // setting is defined at room-level
1999 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2000 } else {
2001 // recurse for global setting
2002 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2003 }
2004 }
2005
2006 return $max_adv_book_pool[$pool_id];
2007 }
2008
2009 /**
2010 * Validates the maximum advance booking notice against the requested check-in
2011 * timestamp. The method supports the validation at both room and property levels.
2012 *
2013 * @param int $checkints the check-in date timestamp.
2014 * @param int $idroom the optional room ID for room-level validation.
2015 *
2016 * @return string error date string in case of validation failure.
2017 *
2018 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2019 */
2020 public static function validateMaxDateBookings($checkints, $idroom = 0)
2021 {
2022 $datelim = self::getMaxDateFuture($idroom);
2023 $datelim = empty($datelim) ? '+2y' : $datelim;
2024 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2025 $quantlim = substr($datelim, -1, 1);
2026
2027 $now = getdate();
2028 if ($quantlim == 'w') {
2029 $until_ts = strtotime("+$numlim weeks") + 86399;
2030 } else {
2031 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2032 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2033 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2034 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2035 }
2036
2037 if ($until_ts > $now[0] && $checkints > $until_ts) {
2038 $vbo_df = self::getDateFormat();
2039 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2040
2041 // error, return the maximum date in the future allowed
2042 return date($df, $until_ts);
2043 }
2044
2045 // validation passed
2046 return '';
2047 }
2048
2049 public static function validateMinDaysAdvance($checkints) {
2050 $mindadv = self::getMinDaysAdvance(true);
2051 if ($mindadv > 0) {
2052 $tsinfo = getdate($checkints);
2053 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2054 if ($checkints < $limit_ts) {
2055 return $mindadv;
2056 }
2057 }
2058
2059 return '';
2060 }
2061
2062 /**
2063 * The only supported calendar type has been changed to jQuery UI.
2064 *
2065 * @since 1.13
2066 */
2067 public static function calendarType($skipsession = false)
2068 {
2069 return 'jqueryui';
2070 }
2071
2072 public static function getSiteLogo()
2073 {
2074 static $siteLogo = null;
2075
2076 if ($siteLogo !== null) {
2077 return $siteLogo;
2078 }
2079
2080 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2081
2082 return $siteLogo;
2083 }
2084
2085 public static function getBackendLogo()
2086 {
2087 static $backLogo = null;
2088
2089 if ($backLogo !== null) {
2090 return $backLogo;
2091 }
2092
2093 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2094
2095 return $backLogo;
2096 }
2097
2098 public static function numCalendars()
2099 {
2100 static $numCalendars = null;
2101
2102 if ($numCalendars !== null) {
2103 return $numCalendars;
2104 }
2105
2106 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2107
2108 return $numCalendars;
2109 }
2110
2111 public static function getFirstWeekDay()
2112 {
2113 static $firstWeekDay = null;
2114
2115 if ($firstWeekDay !== null) {
2116 return $firstWeekDay;
2117 }
2118
2119 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2120
2121 return $firstWeekDay;
2122 }
2123
2124 public static function showPartlyReserved()
2125 {
2126 return VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2127 }
2128
2129 public static function showStatusCheckinoutOnly()
2130 {
2131 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2132 }
2133
2134 public static function getDisclaimer($vbo_tn = null)
2135 {
2136 $dbo = JFactory::getDbo();
2137
2138 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2139 $dbo->setQuery($q);
2140 $ft = $dbo->loadAssocList();
2141 if (!$ft) {
2142 return '';
2143 }
2144
2145 if (!is_object($vbo_tn)) {
2146 $vbo_tn = self::getTranslator();
2147 }
2148 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2149
2150 return $ft[0]['setting'];
2151 }
2152
2153 public static function showFooter()
2154 {
2155 return VBOFactory::getConfig()->getBool('showfooter', false);
2156 }
2157
2158 public static function getPriceName($idp, $vbo_tn = null)
2159 {
2160 $dbo = JFactory::getDbo();
2161
2162 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2163 $dbo->setQuery($q, 0, 1);
2164 $n = $dbo->loadAssocList();
2165 if ($n) {
2166 if (is_object($vbo_tn)) {
2167 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2168 }
2169 return $n[0]['name'];
2170 }
2171
2172 return "";
2173 }
2174
2175 public static function getPriceAttr($idp, $vbo_tn = null)
2176 {
2177 $dbo = JFactory::getDbo();
2178
2179 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2180 $dbo->setQuery($q, 0, 1);
2181 $n = $dbo->loadAssocList();
2182 if ($n) {
2183 if (is_object($vbo_tn)) {
2184 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2185 }
2186 return $n[0]['attr'];
2187 }
2188
2189 return "";
2190 }
2191
2192 /**
2193 * Fetches the requested rate plan information.
2194 *
2195 * @param int $idp the rate plan ID.
2196 * @param object $vbo_tn the Vik Booking translator object.
2197 *
2198 * @return array empty array or associative record.
2199 */
2200 public static function getPriceInfo($idp, $vbo_tn = null)
2201 {
2202 // cache values
2203 static $priceInfos = [];
2204
2205 $idp = (int)$idp;
2206
2207 if ($priceInfos && isset($priceInfos[$idp])) {
2208 return $priceInfos[$idp];
2209 }
2210
2211 $dbo = JFactory::getDbo();
2212
2213 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2214 $dbo->setQuery($q, 0, 1);
2215 $rplan = $dbo->loadAssocList();
2216 if (!$rplan) {
2217 return [];
2218 }
2219
2220 if (is_object($vbo_tn)) {
2221 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2222 }
2223
2224 // set cached value
2225 $priceInfos[$idp] = $rplan[0];
2226
2227 return $rplan[0];
2228 }
2229
2230 public static function getAliq($idal)
2231 {
2232 $dbo = JFactory::getDbo();
2233
2234 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2235 $dbo->setQuery($q);
2236 $aliq = $dbo->loadResult();
2237 if ($aliq) {
2238 return $aliq;
2239 }
2240
2241 return 0;
2242 }
2243
2244 public static function getTimeOpenStore($skipsession = false)
2245 {
2246 // cache value in static var
2247 static $getTimeOpenStore = null;
2248
2249 if ($getTimeOpenStore) {
2250 return $getTimeOpenStore;
2251 }
2252 //
2253
2254 $dbo = JFactory::getDbo();
2255 $session = JFactory::getSession();
2256
2257 if ($skipsession) {
2258 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2259 if (empty($setting) && $setting != "0") {
2260 return false;
2261 } else {
2262 $x = explode("-", $setting);
2263 if (!empty($x[1]) && $x[1] != "0") {
2264 $getTimeOpenStore = $x;
2265 return $x;
2266 }
2267 }
2268 } else {
2269 $sval = $session->get('vbgetTimeOpenStore', '');
2270 if (!empty($sval)) {
2271 return $sval;
2272 } else {
2273 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2274 if (empty($setting) && $setting != "0") {
2275 return false;
2276 } else {
2277 $x = explode("-", $setting);
2278 if (!empty($x[1]) && $x[1] != "0") {
2279 $session->set('vbgetTimeOpenStore', $x);
2280 $getTimeOpenStore = $x;
2281 return $x;
2282 }
2283 }
2284 }
2285 }
2286 return false;
2287 }
2288
2289 public static function getHoursMinutes($secs) {
2290 if ($secs >= 3600) {
2291 $op = $secs / 3600;
2292 $hours = floor($op);
2293 $less = $hours * 3600;
2294 $newsec = $secs - $less;
2295 $optwo = $newsec / 60;
2296 $minutes = floor($optwo);
2297 } else {
2298 $hours = "0";
2299 $optwo = $secs / 60;
2300 $minutes = floor($optwo);
2301 }
2302 $x[] = $hours;
2303 $x[] = $minutes;
2304 return $x;
2305 }
2306
2307 public static function getClosingDates()
2308 {
2309 // cache value in static var
2310 static $getClosingDates = null;
2311
2312 if (is_array($getClosingDates)) {
2313 return $getClosingDates;
2314 }
2315
2316 $dbo = JFactory::getDbo();
2317
2318 $allcd = [];
2319
2320 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2321 $dbo->setQuery($q);
2322 $s = $dbo->loadAssocList();
2323 if ($s && !empty($s[0]['setting'])) {
2324 $allcd = json_decode($s[0]['setting'], true);
2325 $allcd = is_array($allcd) ? $allcd : array();
2326 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2327 foreach ($allcd as $k => $v) {
2328 if ($v['to'] < $base_ts) {
2329 unset($allcd[$k]);
2330 }
2331 }
2332 $allcd = array_values($allcd);
2333 }
2334
2335 $getClosingDates = $allcd;
2336
2337 return $allcd;
2338 }
2339
2340 public static function parseJsClosingDates() {
2341 $cd = self::getClosingDates();
2342 if (count($cd) > 0) {
2343 $cdjs = array();
2344 foreach ($cd as $k => $v) {
2345 $cdjs[] = array(date('Y-m-d', $v['from']), date('Y-m-d', $v['to']));
2346 }
2347 return $cdjs;
2348 }
2349 return array();
2350 }
2351
2352 public static function validateClosingDates($checkints, $checkoutts, $df = null)
2353 {
2354 $cd = self::getClosingDates();
2355 if (!count($cd)) {
2356 return '';
2357 }
2358 $df = empty($df) ? 'Y-m-d' : $df;
2359 $margin_seconds = 22 * 60 * 60;
2360 foreach ($cd as $k => $v) {
2361 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2362 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2363 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2364 if ($inner_closed || $outer_closed || $middle_closed) {
2365 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2366 }
2367 }
2368 return '';
2369 }
2370
2371 /**
2372 * Whether the categories dropdown filter menu should be displayed.
2373 *
2374 * @param boolean $skipsession [optional] re-read the configuration setting.
2375 *
2376 * @return boolean
2377 */
2378 public static function showCategoriesFront($skipsession = true)
2379 {
2380 $dbo = JFactory::getDbo();
2381 if ($skipsession) {
2382 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2383 $dbo->setQuery($q);
2384 $dbo->execute();
2385 if ($dbo->getNumRows() > 0) {
2386 $s = $dbo->loadAssocList();
2387 return (intval($s[0]['setting']) == 1);
2388 }
2389 return false;
2390 }
2391 $session = JFactory::getSession();
2392 $sval = $session->get('vbshowCategoriesFront', '');
2393 if (strlen($sval)) {
2394 return (intval($sval) == 1 ? true : false);
2395 }
2396 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2397 $dbo->setQuery($q);
2398 $dbo->execute();
2399 if ($dbo->getNumRows() > 0) {
2400 $s = $dbo->loadAssocList();
2401 $session->set('vbshowCategoriesFront', $s[0]['setting']);
2402 return (intval($s[0]['setting']) == 1);
2403 }
2404 return false;
2405 }
2406
2407 /**
2408 * Whether the number of children dropdown menu should be displayed.
2409 * Defaults to skip the session values and to re-read the configuration setting.
2410 *
2411 * @param boolean $skipsession [optional] re-read the configuration setting.
2412 *
2413 * @return boolean
2414 */
2415 public static function showChildrenFront($skipsession = true)
2416 {
2417 $dbo = JFactory::getDbo();
2418 if ($skipsession) {
2419 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2420 $dbo->setQuery($q);
2421 $dbo->execute();
2422 if ($dbo->getNumRows() > 0) {
2423 $s = $dbo->loadAssocList();
2424 return (intval($s[0]['setting']) == 1);
2425 }
2426 return false;
2427 }
2428 $session = JFactory::getSession();
2429 $sval = $session->get('vbshowChildrenFront', '');
2430 if (strlen($sval)) {
2431 return (intval($sval) == 1 ? true : false);
2432 }
2433 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2434 $dbo->setQuery($q);
2435 $dbo->execute();
2436 if ($dbo->getNumRows() > 0) {
2437 $s = $dbo->loadAssocList();
2438 $session->set('vbshowChildrenFront', $s[0]['setting']);
2439 return (intval($s[0]['setting']) == 1);
2440 }
2441 return false;
2442 }
2443
2444 public static function allowBooking() {
2445 $dbo = JFactory::getDbo();
2446 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='allowbooking';";
2447 $dbo->setQuery($q);
2448 $dbo->execute();
2449 if ($dbo->getNumRows() > 0) {
2450 $s = $dbo->loadAssocList();
2451 return (intval($s[0]['setting']) == 1 ? true : false);
2452 } else {
2453 return false;
2454 }
2455 }
2456
2457 public static function getDisabledBookingMsg($vbo_tn = null) {
2458 $dbo = JFactory::getDbo();
2459 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2460 $dbo->setQuery($q);
2461 $dbo->execute();
2462 $s = $dbo->loadAssocList();
2463 if (!is_object($vbo_tn)) {
2464 $vbo_tn = self::getTranslator();
2465 }
2466 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2467 return $s[0]['setting'];
2468 }
2469
2470 /**
2471 * Returns the text for the guests allowed policy.
2472 *
2473 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2474 *
2475 * @return string either the raw or the translated policy text.
2476 *
2477 * @since 1.16.5 (WP) - 1.6.5 (WP)
2478 */
2479 public static function getGuestsAllowedPolicy($vbo_tn = null)
2480 {
2481 $dbo = JFactory::getDbo();
2482
2483 $q = $dbo->getQuery(true)
2484 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2485 ->from($dbo->qn('#__vikbooking_texts'))
2486 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2487
2488 $dbo->setQuery($q);
2489 $text_record = $dbo->loadAssocList();
2490
2491 if (!$text_record) {
2492 // missing record, insert it with empty values
2493 if (self::isAdmin()) {
2494 $record = new stdClass;
2495 $record->param = 'guests_allowed_policy';
2496 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2497 $record->setting = '';
2498 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2499 }
2500
2501 return '';
2502 }
2503
2504 if (is_object($vbo_tn)) {
2505 // translate the record only if requested
2506 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2507 }
2508
2509 return $text_record[0]['setting'];
2510 }
2511
2512 public static function getDateFormat($skipsession = true)
2513 {
2514 // cache value in static var
2515 static $getDateFormat = null;
2516
2517 if ($getDateFormat) {
2518 return $getDateFormat;
2519 }
2520
2521 $dbo = JFactory::getDbo();
2522
2523 if ($skipsession) {
2524 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2525 $dbo->setQuery($q, 0, 1);
2526 $getDateFormat = $dbo->loadResult();
2527
2528 return $getDateFormat;
2529 }
2530
2531 $session = JFactory::getSession();
2532 $sval = $session->get('vbgetDateFormat', '');
2533 if (!empty($sval)) {
2534 return $sval;
2535 }
2536
2537 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2538 $dbo->setQuery($q, 0, 1);
2539 $getDateFormat = $dbo->loadResult();
2540
2541 $session->set('vbgetDateFormat', $getDateFormat);
2542
2543 return $getDateFormat;
2544 }
2545
2546 public static function getDateSeparator($skipsession = true)
2547 {
2548 // cache value in static var
2549 static $getDateSeparator = null;
2550
2551 if ($getDateSeparator) {
2552 return $getDateSeparator;
2553 }
2554
2555 $dbo = JFactory::getDbo();
2556
2557 if ($skipsession) {
2558 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2559 $dbo->setQuery($q, 0, 1);
2560 $val = $dbo->loadResult();
2561
2562 $getDateSeparator = empty($val) ? "/" : $val;
2563
2564 return $getDateSeparator;
2565 }
2566
2567 $session = JFactory::getSession();
2568 $sval = $session->get('vbgetDateSep', '');
2569 if (!empty($sval)) {
2570 return $sval;
2571 }
2572
2573 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2574 $dbo->setQuery($q, 0, 1);
2575 $val = $dbo->loadResult();
2576
2577 $getDateSeparator = empty($val) ? "/" : $val;
2578
2579 $session->set('vbgetDateSep', $getDateSeparator);
2580
2581 return $getDateSeparator;
2582 }
2583
2584 public static function getHoursMoreRb($skipsession = false)
2585 {
2586 // cache value in static var
2587 static $getHoursMoreRb = null;
2588
2589 if ($getHoursMoreRb !== null) {
2590 return $getHoursMoreRb;
2591 }
2592
2593 $dbo = JFactory::getDbo();
2594
2595 if ($skipsession) {
2596 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2597 $dbo->setQuery($q, 0, 1);
2598 $getHoursMoreRb = (int)$dbo->loadResult();
2599
2600 return $getHoursMoreRb;
2601 }
2602
2603 $session = JFactory::getSession();
2604 $sval = $session->get('getHoursMoreRb', '');
2605 if (strlen($sval)) {
2606 return $sval;
2607 }
2608
2609 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2610 $dbo->setQuery($q, 0, 1);
2611 $getHoursMoreRb = (int)$dbo->loadResult();
2612
2613 $session->set('getHoursMoreRb', $getHoursMoreRb);
2614
2615 return $getHoursMoreRb;
2616 }
2617
2618 public static function getHoursRoomAvail()
2619 {
2620 $dbo = JFactory::getDbo();
2621
2622 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2623 $dbo->setQuery($q, 0, 1);
2624
2625 return (int)$dbo->loadResult();
2626 }
2627
2628 public static function getFrontTitle($vbo_tn = null) {
2629 // cache value in static var
2630 static $getFrontTitle = null;
2631
2632 if ($getFrontTitle) {
2633 return $getFrontTitle;
2634 }
2635 //
2636
2637 $dbo = JFactory::getDbo();
2638 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2639 $dbo->setQuery($q);
2640 $dbo->execute();
2641 $ft = $dbo->loadAssocList();
2642 if (!is_object($vbo_tn)) {
2643 $vbo_tn = self::getTranslator();
2644 }
2645 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2646
2647 $getFrontTitle = $ft[0]['setting'];
2648
2649 return $ft[0]['setting'];
2650 }
2651
2652 public static function getFrontTitleTag()
2653 {
2654 return VBOFactory::getConfig()->get('fronttitletag', '');
2655 }
2656
2657 public static function getFrontTitleTagClass()
2658 {
2659 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2660 }
2661
2662 public static function getCurrencyName()
2663 {
2664 static $currencyName = null;
2665
2666 if ($currencyName !== null) {
2667 return $currencyName;
2668 }
2669
2670 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2671
2672 return $currencyName;
2673 }
2674
2675 public static function getCurrencySymb($skipsession = false) {
2676 // cache value in static var
2677 static $getCurrencySymb = null;
2678
2679 if ($getCurrencySymb) {
2680 return $getCurrencySymb;
2681 }
2682
2683 $dbo = JFactory::getDbo();
2684
2685 if ($skipsession) {
2686 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2687 $dbo->setQuery($q, 0, 1);
2688 $csymb = $dbo->loadResult();
2689
2690 $getCurrencySymb = $csymb;
2691
2692 return $csymb;
2693 }
2694
2695 $session = JFactory::getSession();
2696 $sval = $session->get('vbgetCurrencySymb', '');
2697 if (!empty($sval)) {
2698 return $sval;
2699 }
2700
2701 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2702 $dbo->setQuery($q, 0, 1);
2703 $csymb = $dbo->loadResult();
2704
2705 // update session value, cache it and return it
2706 $session->set('vbgetCurrencySymb', $csymb);
2707 $getCurrencySymb = $csymb;
2708
2709 return $csymb;
2710 }
2711
2712 public static function getNumberFormatData($skipsession = false) {
2713 // cache value in static var
2714 static $getNumberFormatData = null;
2715
2716 if ($getNumberFormatData) {
2717 return $getNumberFormatData;
2718 }
2719 //
2720
2721 $dbo = JFactory::getDbo();
2722
2723 if ($skipsession) {
2724 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2725 $dbo->setQuery($q, 0, 1);
2726 $dbo->execute();
2727 $numfdata = $dbo->loadResult();
2728
2729 // cache value and return it
2730 $getNumberFormatData = $numfdata;
2731
2732 return $numfdata;
2733 }
2734
2735 $session = JFactory::getSession();
2736 $sval = $session->get('getNumberFormatData', '');
2737 if (!empty($sval)) {
2738 return $sval;
2739 }
2740
2741 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2742 $dbo->setQuery($q, 0, 1);
2743 $dbo->execute();
2744 $numfdata = $dbo->loadResult();
2745
2746 // update session value, cache it and return it
2747 $session->set('getNumberFormatData', $numfdata);
2748 $getNumberFormatData = $numfdata;
2749
2750 return $numfdata;
2751 }
2752
2753 /**
2754 * It is possible to hide the decimals if they are like "N.00".
2755 *
2756 * @return int 0 if disabled, 1 if enabled (default).
2757 *
2758 * @since 1.14 (J) - 1.4.0 (WP)
2759 */
2760 public static function hideEmptyDecimals()
2761 {
2762 // cache value in static var
2763 static $hideEmptyDecimals = null;
2764
2765 if ($hideEmptyDecimals !== null) {
2766 return $hideEmptyDecimals;
2767 }
2768 //
2769
2770 $dbo = JFactory::getDbo();
2771
2772 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='noemptydecimals';";
2773 $dbo->setQuery($q);
2774 $hideEmptyDecimals = $dbo->loadResult();
2775 if ($hideEmptyDecimals !== null) {
2776 $hideEmptyDecimals = (int)$hideEmptyDecimals;
2777 return $hideEmptyDecimals;
2778 }
2779
2780 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('noemptydecimals', '1');";
2781 $dbo->setQuery($q);
2782 $dbo->execute();
2783
2784 $hideEmptyDecimals = 1;
2785
2786 return $hideEmptyDecimals;
2787 }
2788
2789 public static function numberFormat($num, $skipsession = false)
2790 {
2791 $formatvals = self::getNumberFormatData($skipsession);
2792 $formatparts = explode(':', $formatvals);
2793
2794 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
2795 // number has got no decimals
2796 $formatparts[0] = 0;
2797 }
2798
2799 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
2800 }
2801
2802 /**
2803 * Given a date timestamp, returns the formatted date.
2804 *
2805 * @param int|string $ts the date representation in Unix timestamp.
2806 * @param bool $wtime if true, a formatted date-time will be returned.
2807 *
2808 * @return string the formatted date according to settings.
2809 *
2810 * @since 1.16.0 (J) - 1.6.0 (WP)
2811 */
2812 public static function formatDateTs($ts, $wtime = false)
2813 {
2814 $nowdf = self::getDateFormat();
2815 if ($nowdf == "%d/%m/%Y") {
2816 $df = 'd/m/Y';
2817 } elseif ($nowdf == "%m/%d/%Y") {
2818 $df = 'm/d/Y';
2819 } else {
2820 $df = 'Y/m/d';
2821 }
2822 $datesep = self::getDateSeparator();
2823
2824 $time_f = $wtime ? ' H:i' : '';
2825
2826 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
2827 }
2828
2829 /**
2830 * Given a country name, 3-char or 2-char code, this method attempts
2831 * to guess the best language to assign to the booking according to
2832 * what languages are installed on the website. This way, cron jobs
2833 * and any other email notification will be correctly and automatically
2834 * sent to the guest in the proper language without any manual action.
2835 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
2836 *
2837 * @param string $country the country name, 3-char or 2-char code.
2838 * @param string $locale optional locale lang tag of the guest.
2839 *
2840 * @return mixed the best lang-tag string to use or null.
2841 *
2842 * @since 1.16.0 (J) - 1.6.0 (WP)
2843 */
2844 public static function guessBookingLangFromCountry($country, $locale = null)
2845 {
2846 if (empty($country)) {
2847 return null;
2848 }
2849
2850 // get all the available languages
2851 $known_langs = self::getVboApplication()->getKnownLanguages();
2852 if (!is_array($known_langs) || !count($known_langs)) {
2853 return null;
2854 }
2855
2856 // check if the booking included a supported "locale" for the guest
2857 if (!empty($locale)) {
2858 // make the locale compatible with the language tags format
2859 $locale = str_replace('_', '-', $locale);
2860 foreach ($known_langs as $ltag => $ldet) {
2861 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
2862 // we support this language tag, so we just use it
2863 return $ltag;
2864 }
2865 }
2866 }
2867
2868 // build similarities with country-languages
2869 $similarities = array(
2870 'AU' => 'en',
2871 'GB' => 'en',
2872 'IE' => 'en',
2873 'NZ' => 'en',
2874 'US' => 'en',
2875 'CA' => array(
2876 'en',
2877 'fr',
2878 ),
2879 'CL' => 'es',
2880 'AR' => 'es',
2881 'PE' => 'es',
2882 'MX' => 'es',
2883 'CR' => 'es',
2884 'CO' => 'es',
2885 'EC' => 'es',
2886 'BO' => 'es',
2887 'CU' => 'es',
2888 'VE' => 'es',
2889 'BE' => 'fr',
2890 'LU' => 'fr',
2891 'CH' => array(
2892 'de',
2893 'it',
2894 'fr',
2895 ),
2896 'AT' => 'de',
2897 'GR' => 'el',
2898 'GL' => 'dk',
2899 );
2900
2901 // fetch values from db
2902 $dbo = JFactory::getDbo();
2903 $q = "SELECT `country_name`, `country_3_code`, `country_2_code` FROM `#__vikbooking_countries` WHERE `country_name`=" . $dbo->quote($country) . " OR `country_3_code`=" . $dbo->quote($country) . " OR `country_2_code`=" . $dbo->quote($country);
2904 $dbo->setQuery($q, 0, 1);
2905 $dbo->execute();
2906 if (!$dbo->getNumRows()) {
2907 return null;
2908 }
2909 $country_record = $dbo->loadAssoc();
2910
2911 // assign country name/code versions
2912 $country_name = $country_record['country_name'];
2913 $country_3char = strtoupper($country_record['country_3_code']);
2914 $country_2char = strtoupper($country_record['country_2_code']);
2915
2916 // build an associative array of language tags and related match-score
2917 $langtags_score = array();
2918 foreach ($known_langs as $ltag => $ldet) {
2919 // default language tag score is 0 for no matches
2920 $langtags_score[$ltag] = 0;
2921 // get language and country codes
2922 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
2923
2924 // check matches with the installed language details
2925 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
2926 // increase language tag score
2927 $langtags_score[$ltag]++;
2928 }
2929 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
2930 // increase language tag score
2931 $langtags_score[$ltag]++;
2932 }
2933 if (!empty($ldet['locale'])) {
2934 // sanitize locale for matching the 2-char code safely
2935 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
2936 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
2937 // increase language tag score
2938 $langtags_score[$ltag]++;
2939 }
2940 }
2941 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
2942 // increase language tag score
2943 $langtags_score[$ltag]++;
2944 }
2945 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
2946 // increase language tag score
2947 $langtags_score[$ltag]++;
2948 }
2949
2950 // check language similarities between countries
2951 if (isset($similarities[$country_2char])) {
2952 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
2953 // check if language tag(s) is available for this spoken language
2954 foreach ($spoken_tags as $spoken_tag) {
2955 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
2956 // increase language tag score
2957 $langtags_score[$ltag]++;
2958 }
2959 }
2960 }
2961 }
2962
2963 // make sure at least one language tag has got some points
2964 if (max($langtags_score) === 0) {
2965 // no languages installed to honor this country
2966 return null;
2967 }
2968
2969 // sort language tag scores
2970 arsort($langtags_score);
2971
2972 // reset array pointer to the first (highest) element
2973 reset($langtags_score);
2974
2975 // return the language tag with the highest score
2976 return key($langtags_score);
2977 }
2978
2979 public static function getCurrencyCodePp() {
2980 $dbo = JFactory::getDbo();
2981 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencycodepp';";
2982 $dbo->setQuery($q);
2983 $dbo->execute();
2984 $ft = $dbo->loadAssocList();
2985 return $ft[0]['setting'];
2986 }
2987
2988 public static function getIntroMain($vbo_tn = null) {
2989 $dbo = JFactory::getDbo();
2990 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
2991 $dbo->setQuery($q);
2992 $dbo->execute();
2993 $ft = $dbo->loadAssocList();
2994 if (!is_object($vbo_tn)) {
2995 $vbo_tn = self::getTranslator();
2996 }
2997 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2998 return $ft[0]['setting'];
2999 }
3000
3001 public static function getClosingMain($vbo_tn = null) {
3002 $dbo = JFactory::getDbo();
3003 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3004 $dbo->setQuery($q);
3005 $dbo->execute();
3006 $ft = $dbo->loadAssocList();
3007 if (!is_object($vbo_tn)) {
3008 $vbo_tn = self::getTranslator();
3009 }
3010 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3011 return $ft[0]['setting'];
3012 }
3013
3014 public static function getFullFrontTitle($vbo_tn = null) {
3015 $dbo = JFactory::getDbo();
3016 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3017 $dbo->setQuery($q);
3018 $dbo->execute();
3019 $ft = $dbo->loadAssocList();
3020 if (!is_object($vbo_tn)) {
3021 $vbo_tn = self::getTranslator();
3022 }
3023 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3024 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3025 $dbo->setQuery($q);
3026 $dbo->execute();
3027 $fttag = $dbo->loadAssocList();
3028 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3029 $dbo->setQuery($q);
3030 $dbo->execute();
3031 $fttagclass = $dbo->loadAssocList();
3032 if (empty($ft[0]['setting'])) {
3033 return "";
3034 } else {
3035 if (empty($fttag[0]['setting'])) {
3036 return $ft[0]['setting'] . "<br/>\n";
3037 } else {
3038 $tag = str_replace("<", "", $fttag[0]['setting']);
3039 $tag = str_replace(">", "", $tag);
3040 $tag = str_replace("/", "", $tag);
3041 $tag = trim($tag);
3042 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3043 }
3044 }
3045 }
3046
3047 public static function dateIsValid($date) {
3048 $df = self::getDateFormat();
3049 $datesep = self::getDateSeparator();
3050 if (strlen($date) != 10) {
3051 return false;
3052 }
3053 $cur_dsep = "/";
3054 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3055 $cur_dsep = $datesep;
3056 }
3057 $x = explode($cur_dsep, $date);
3058 if ($df == "%d/%m/%Y") {
3059 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3060 return false;
3061 }
3062 } elseif ($df == "%m/%d/%Y") {
3063 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3064 return false;
3065 }
3066 } else {
3067 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3068 return false;
3069 }
3070 }
3071 return true;
3072 }
3073
3074 public static function sayDateFormat() {
3075 $dbo = JFactory::getDbo();
3076 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3077 $dbo->setQuery($q);
3078 $dbo->execute();
3079 $s = $dbo->loadAssocList();
3080 if ($s[0]['setting'] == "%d/%m/%Y") {
3081 return JText::translate('VBCONFIGONETWELVE');
3082 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3083 return JText::translate('VBCONFIGONEMDY');
3084 } else {
3085 return JText::translate('VBCONFIGONETENTHREE');
3086 }
3087 }
3088
3089 /**
3090 * Calculates the Unix timestamp from the given date and
3091 * time. Avoids DST issues thanks to mktime. With prior releases,
3092 * DST issues may occur due to the sum of seconds.
3093 *
3094 * @param string $date the date string formatted with the current settings
3095 * @param int $h hours from 0 to 23 for check-in/check-out
3096 * @param int $m minutes from 0 to 59 for check-in/check-out
3097 * @param int $s seconds from 0 to 59 for check-in/check-out
3098 *
3099 * @return int the Unix timestamp of the date
3100 *
3101 * @since 1.0.14
3102 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3103 * signature modified for $h and $m that now take 0 as default value.
3104 */
3105 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3106 $df = self::getDateFormat();
3107 $datesep = self::getDateSeparator();
3108 $cur_dsep = "/";
3109 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3110 $cur_dsep = $datesep;
3111 }
3112 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3113 // date is in Y-m-d format (with no time)
3114 $cur_dsep = '-';
3115 $df = "%Y/%m/%d";
3116 }
3117 $x = explode($cur_dsep, $date);
3118 if (!(count($x) > 2)) {
3119 return 0;
3120 }
3121 if ($df == "%d/%m/%Y") {
3122 $month = (int)$x[1];
3123 $mday = (int)$x[0];
3124 $year = (int)$x[2];
3125 } elseif ($df == "%m/%d/%Y") {
3126 $month = (int)$x[0];
3127 $mday = (int)$x[1];
3128 $year = (int)$x[2];
3129 } else {
3130 $month = (int)$x[1];
3131 $mday = (int)$x[2];
3132 $year = (int)$x[0];
3133 }
3134 $h = empty($h) ? 0 : (int)$h;
3135 $m = empty($m) ? 0 : (int)$m;
3136 $s = $s > 0 && $s <= 59 ? $s : 0;
3137
3138 return mktime($h, $m, $s, $month, $mday, $year);
3139 }
3140
3141 public static function ivaInclusa($skipsession = false) {
3142 // cache value in static var
3143 static $getTaxIncluded = null;
3144
3145 if ($getTaxIncluded !== null) {
3146 return (bool)$getTaxIncluded;
3147 }
3148
3149 $dbo = JFactory::getDbo();
3150
3151 if ($skipsession) {
3152 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3153 $dbo->setQuery($q, 0, 1);
3154 $dbo->execute();
3155 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3156
3157 // cache value and return it
3158 $getTaxIncluded = $vat_incl_data;
3159
3160 return (bool)$vat_incl_data;
3161 }
3162
3163 $session = JFactory::getSession();
3164 $sval = $session->get('getTaxIncluded', '');
3165 if (strlen($sval)) {
3166 return (bool)$sval;
3167 }
3168
3169 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3170 $dbo->setQuery($q, 0, 1);
3171 $dbo->execute();
3172 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3173
3174 // update session value, cache it and return it
3175 $session->set('getTaxIncluded', $vat_incl_data);
3176 $getTaxIncluded = $vat_incl_data;
3177
3178 return (bool)$vat_incl_data;
3179 }
3180
3181 public static function showTaxOnSummaryOnly($skipsession = false) {
3182 if ($skipsession) {
3183 $dbo = JFactory::getDbo();
3184 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3185 $dbo->setQuery($q);
3186 $dbo->execute();
3187 $s = $dbo->loadAssocList();
3188 return (intval($s[0]['setting']) == 1 ? true : false);
3189 } else {
3190 $session = JFactory::getSession();
3191 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3192 if (strlen($sval) > 0) {
3193 return (intval($sval) == 1 ? true : false);
3194 } else {
3195 $dbo = JFactory::getDbo();
3196 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3197 $dbo->setQuery($q);
3198 $dbo->execute();
3199 $s = $dbo->loadAssocList();
3200 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3201 return (intval($s[0]['setting']) == 1 ? true : false);
3202 }
3203 }
3204 }
3205
3206 public static function tokenForm() {
3207 $dbo = JFactory::getDbo();
3208 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3209 $dbo->setQuery($q);
3210 $dbo->execute();
3211 $s = $dbo->loadAssocList();
3212 return (intval($s[0]['setting']) == 1 ? true : false);
3213 }
3214
3215 public static function getPaypalAcc() {
3216 $dbo = JFactory::getDbo();
3217 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3218 $dbo->setQuery($q);
3219 $dbo->execute();
3220 $s = $dbo->loadAssocList();
3221 return $s[0]['setting'];
3222 }
3223
3224 public static function getAccPerCent() {
3225 $dbo = JFactory::getDbo();
3226 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3227 $dbo->setQuery($q);
3228 $dbo->execute();
3229 $s = $dbo->loadAssocList();
3230 return $s[0]['setting'];
3231 }
3232
3233 public static function getTypeDeposit($skipsession = false) {
3234 if ($skipsession) {
3235 $dbo = JFactory::getDbo();
3236 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3237 $dbo->setQuery($q);
3238 $dbo->execute();
3239 $s = $dbo->loadAssocList();
3240 return $s[0]['setting'];
3241 } else {
3242 $session = JFactory::getSession();
3243 $sval = $session->get('getTypeDeposit', '');
3244 if (strlen($sval) > 0) {
3245 return $sval;
3246 } else {
3247 $dbo = JFactory::getDbo();
3248 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3249 $dbo->setQuery($q);
3250 $dbo->execute();
3251 $s = $dbo->loadAssocList();
3252 $session->set('getTypeDeposit', $s[0]['setting']);
3253 return $s[0]['setting'];
3254 }
3255 }
3256 }
3257
3258 public static function multiplePayments()
3259 {
3260 return VBOFactory::getConfig()->getBool('multipay', false);
3261 }
3262
3263 public static function getAdminMail()
3264 {
3265 return VBOFactory::getConfig()->getString('adminemail', '');
3266 }
3267
3268 public static function getSenderMail()
3269 {
3270 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3271
3272 return empty($sender) ? self::getAdminMail() : $sender;
3273 }
3274
3275 public static function getPaymentName($vbo_tn = null) {
3276 $dbo = JFactory::getDbo();
3277 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3278 $dbo->setQuery($q);
3279 $dbo->execute();
3280 $s = $dbo->loadAssocList();
3281 if (!is_object($vbo_tn)) {
3282 $vbo_tn = self::getTranslator();
3283 }
3284 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3285 return $s[0]['setting'];
3286 }
3287
3288 public static function getTermsConditions($vbo_tn = null) {
3289 //VBO 1.10
3290 $dbo = JFactory::getDbo();
3291 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3292 $dbo->setQuery($q);
3293 $dbo->execute();
3294 if ($dbo->getNumRows() > 0) {
3295 $s = $dbo->loadAssocList();
3296 if (!is_object($vbo_tn)) {
3297 $vbo_tn = self::getTranslator();
3298 }
3299 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3300 } else {
3301 //the record has never been saved. Compose it with the default lang definition
3302 $timeopst = self::getTimeOpenStore(true);
3303 if (is_array($timeopst)) {
3304 $openat = self::getHoursMinutes($timeopst[0]);
3305 $closeat = self::getHoursMinutes($timeopst[1]);
3306 } else {
3307 $openat = array(12, 0);
3308 $closeat = array(10, 0);
3309 }
3310 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3311 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3312 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3313 }
3314
3315 return $s[0]['setting'];
3316 }
3317
3318 public static function getMinutesLock($conv = false)
3319 {
3320 static $minutesLock = null;
3321
3322 if ($minutesLock === null) {
3323 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3324 }
3325
3326 if (!$minutesLock) {
3327 return 0;
3328 }
3329
3330 if ($conv) {
3331 return (time() + ($minutesLock * 60));
3332 }
3333
3334 return $minutesLock;
3335 }
3336
3337 /**
3338 * Checks if the room units are temporarily locked on the given dates.
3339 *
3340 * @param int $idroom the room ID.
3341 * @param int $units the total number of room units.
3342 * @param int $first the check-in timestamp.
3343 * @param int $second the check-out timestamp.
3344 * @param bool $occupied if true, the system will also consider the booked units.
3345 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3346 *
3347 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3348 *
3349 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3350 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3351 */
3352 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3353 {
3354 $dbo = JFactory::getDbo();
3355
3356 if ($units < 1) {
3357 return false;
3358 }
3359
3360 $actnow = time();
3361
3362 // clean up expired records
3363 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3364 $dbo->setQuery($q);
3365 $dbo->execute();
3366
3367 $secdiff = $second - $first;
3368 $daysdiff = $secdiff / 86400;
3369 if (is_int($daysdiff)) {
3370 if ($daysdiff < 1) {
3371 $daysdiff = 1;
3372 }
3373 } else {
3374 if ($daysdiff < 1) {
3375 $daysdiff = 1;
3376 } else {
3377 $sum = floor($daysdiff) * 86400;
3378 $newdiff = $secdiff - $sum;
3379 $maxhmore = self::getHoursMoreRb() * 3600;
3380 if ($maxhmore >= $newdiff) {
3381 $daysdiff = floor($daysdiff);
3382 } else {
3383 $daysdiff = ceil($daysdiff);
3384 }
3385 }
3386 }
3387 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3388 $tmplock_map = [];
3389
3390 // check temporarily locked records
3391 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3392 $dbo->setQuery($check);
3393 $busy = $dbo->loadAssocList();
3394 if ($busy) {
3395 foreach ($groupdays as $kg => $gday) {
3396 $bfound = 0;
3397 foreach ($busy as $bu) {
3398 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3399 $bfound++;
3400 }
3401 }
3402 $tmplock_map[$kg] = $bfound;
3403 if ($bfound >= $units) {
3404 return false;
3405 }
3406 }
3407 }
3408
3409 if ($occupied) {
3410 // check also the busy records and some the occupied units to the temporarily locked ones
3411 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3412 $dbo->setQuery($check);
3413 $busy = $dbo->loadAssocList();
3414 if (!$busy) {
3415 return true;
3416 }
3417
3418 foreach ($groupdays as $kg => $gday) {
3419 $bfound = 0;
3420 foreach ($busy as $bu) {
3421 if (in_array($bu['id'], $skip_busy_ids)) {
3422 // Booking modification
3423 continue;
3424 }
3425 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3426 $bfound++;
3427 }
3428 }
3429 // sum units occupied to the units temporarily locked
3430 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3431 if ($bfound >= $units) {
3432 return false;
3433 }
3434 }
3435 }
3436
3437 return true;
3438 }
3439
3440 public static function getGroupDays($first, $second, $daysdiff) {
3441 $ret = array();
3442 $ret[] = $first;
3443 if ($daysdiff > 1) {
3444 $start = getdate($first);
3445 $end = getdate($second);
3446 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3447 for($i = 1; $i < $daysdiff; $i++) {
3448 $checkday = $start['mday'] + $i;
3449 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3450 if ($dayts != $endcheck) {
3451 $ret[] = $dayts;
3452 }
3453 }
3454 }
3455 $ret[] = $second;
3456 return $ret;
3457 }
3458
3459 /**
3460 * Counts the hours of difference between the current
3461 * server time and the selected check-in date and time.
3462 *
3463 * @param int $checkin_ts
3464 * @param [int] $now_ts
3465 *
3466 * @return int
3467 */
3468 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3469 $hoursdiff = 0;
3470
3471 if (empty($now_ts)) {
3472 $now_ts = time();
3473 }
3474
3475 if ($now_ts >= $checkin_ts) {
3476 return $hoursdiff;
3477 }
3478
3479 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3480
3481 return $hoursdiff;
3482 }
3483
3484 /**
3485 * Loads all the occupied records for the given rooms and offset.
3486 *
3487 * @param array $roomids the optional list of room IDs to filter.
3488 * @param int $from_ts the optional base date to use.
3489 * @param int $max_ts the optional max date to use.
3490 * @param bool $closures whether to ignore, include or exclude closures.
3491 *
3492 * @return array
3493 *
3494 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3495 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3496 */
3497 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3498 {
3499 $base_ts = empty($from_ts) ? time() : $from_ts;
3500 $busy = [];
3501 $clauses = [];
3502
3503 if (is_array($roomids) && count($roomids)) {
3504 // filter busy records only by the requested room ids
3505 $roomids = array_map(function($rid) {
3506 return (int)$rid;
3507 }, $roomids);
3508 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3509 }
3510
3511 // exclude past reservations
3512 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3513
3514 if (!empty($max_ts)) {
3515 // exclude future reservations (recommended for large datasets)
3516 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3517 }
3518
3519 if (is_bool($closures)) {
3520 // include or exclude closures
3521 if ($closures) {
3522 $clauses[] = "`o`.`closure` = 1";
3523 } else {
3524 $clauses[] = "`o`.`closure` != 1";
3525 }
3526 }
3527
3528 $dbo = JFactory::getDbo();
3529
3530 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3531 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3532 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3533 WHERE " . implode(' AND ', $clauses) . ";";
3534 $dbo->setQuery($q);
3535 $allbusy = $dbo->loadAssocList();
3536 if ($allbusy) {
3537 foreach ($allbusy as $br) {
3538 if (!isset($busy[$br['idroom']])) {
3539 $busy[$br['idroom']] = [];
3540 }
3541 $busy[$br['idroom']][] = $br;
3542 }
3543 }
3544
3545 return $busy;
3546 }
3547
3548 /**
3549 * Loads all the busy records by excluding the closures.
3550 * Built for the page dashboard to calculate the actual
3551 * rooms occupancy by excluding the rooms closed.
3552 *
3553 * @param array $roomids the optional list of room IDs to filter.
3554 * @param int $from_ts the optional base date to use.
3555 * @param int $max_ts the optional max date to use.
3556 *
3557 * @return array the list of busy records.
3558 *
3559 * @since 1.12
3560 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3561 */
3562 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3563 {
3564 if (!is_array($roomids) || !count($roomids)) {
3565 return [];
3566 }
3567
3568 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3569 }
3570
3571 public static function loadBookingBusyIds($idorder) {
3572 $busy = array();
3573 if (empty($idorder)) {
3574 return $busy;
3575 }
3576 $dbo = JFactory::getDbo();
3577 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$idorder.";";
3578 $dbo->setQuery($q);
3579 $allbusy = $dbo->loadAssocList();
3580 if ($allbusy) {
3581 foreach ($allbusy as $b) {
3582 array_push($busy, $b['idbusy']);
3583 }
3584 }
3585 return $busy;
3586 }
3587
3588 public static function loadLockedRecords($roomids, $ts = 0) {
3589 $dbo = JFactory::getDbo();
3590 $actnow = empty($ts) ? time() : $ts;
3591 $locked = array();
3592 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3593 $dbo->setQuery($q);
3594 $dbo->execute();
3595 if (!is_array($roomids) || !(count($roomids) > 0)) {
3596 return $locked;
3597 }
3598 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3599 $dbo->setQuery($check);
3600 $all_locked = $dbo->loadAssocList();
3601 if ($all_locked) {
3602 foreach ($all_locked as $kb => $br) {
3603 $locked[$br['idroom']][$kb] = $br;
3604 }
3605 }
3606 return $locked;
3607 }
3608
3609 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3610 $bookings = [];
3611 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3612 return $bookings;
3613 }
3614 $dbo = JFactory::getDbo();
3615 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3616 $dbo->setQuery($q);
3617 $all_booking_ids = $dbo->loadAssocList();
3618 if ($all_booking_ids) {
3619 $oids = array();
3620 foreach ($all_booking_ids as $bid) {
3621 $oids[] = $bid['idorder'];
3622 }
3623 $q = "SELECT `or`.`idorder`,CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`) AS `nominative`,`or`.`roomindex`,`o`.`status`,`o`.`days`,`o`.`checkout`,`o`.`custdata`,`o`.`country`,`o`.`closure`,`o`.`checked` ".
3624 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3625 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3626 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3627 "ORDER BY `o`.`checkout` ASC;";
3628 $dbo->setQuery($q);
3629 $bookings = $dbo->loadAssocList();
3630 if (!$bookings) {
3631 $bookings = [];
3632 }
3633 }
3634 return $bookings;
3635 }
3636
3637 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3638 {
3639 $dbo = JFactory::getDbo();
3640
3641 if ($units < 1) {
3642 return false;
3643 }
3644
3645 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3646 if (!$room_info) {
3647 return false;
3648 }
3649
3650 $secdiff = $second - $first;
3651 $daysdiff = $secdiff / 86400;
3652 if (is_int($daysdiff)) {
3653 if ($daysdiff < 1) {
3654 $daysdiff = 1;
3655 }
3656 } else {
3657 if ($daysdiff < 1) {
3658 $daysdiff = 1;
3659 } else {
3660 $sum = floor($daysdiff) * 86400;
3661 $newdiff = $secdiff - $sum;
3662 $maxhmore = self::getHoursMoreRb() * 3600;
3663 if ($maxhmore >= $newdiff) {
3664 $daysdiff = floor($daysdiff);
3665 } else {
3666 $daysdiff = ceil($daysdiff);
3667 }
3668 }
3669 }
3670
3671 $q = $dbo->getQuery(true)
3672 ->select($dbo->qn([
3673 'id',
3674 'checkin',
3675 'realback',
3676 ]))
3677 ->from($dbo->qn('#__vikbooking_busy'))
3678 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
3679 ->where($dbo->qn('realback') . ' >= ' . $first);
3680
3681 $dbo->setQuery($q);
3682 $busy = $dbo->loadAssocList();
3683 if (!$busy) {
3684 return ($units <= ($room_info['units'] ?? 1));
3685 }
3686
3687 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3688 foreach ($groupdays as $gday) {
3689 $bfound = 0;
3690 foreach ($busy as $bu) {
3691 if (in_array($bu['id'], $skip_busy_ids)) {
3692 // booking modification
3693 continue;
3694 }
3695 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3696 $bfound++;
3697 }
3698 }
3699 if ($bfound >= $units) {
3700 return false;
3701 }
3702 }
3703
3704 return ($units <= ($room_info['units'] ?? 1));
3705 }
3706
3707 public static function payTotal()
3708 {
3709 return VBOFactory::getConfig()->getBool('paytotal', true);
3710 }
3711
3712 public static function getDepositIfDays()
3713 {
3714 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
3715 }
3716
3717 public static function depositAllowedDaysAdv($checkints) {
3718 $days_adv = self::getDepositIfDays();
3719 if (!($days_adv > 0) || !($checkints > 0)) {
3720 return true;
3721 }
3722 $now_info = getdate();
3723 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
3724 return $maxts > $checkints ? false : true;
3725 }
3726
3727 public static function depositCustomerChoice() {
3728 $dbo = JFactory::getDbo();
3729 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
3730 $dbo->setQuery($q);
3731 $dbo->execute();
3732 $s = $dbo->loadAssocList();
3733 return (intval($s[0]['setting']) == 1 ? true : false);
3734 }
3735
3736 public static function getDepositOverrides($getjson = false) {
3737 $dbo = JFactory::getDbo();
3738 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
3739 $dbo->setQuery($q);
3740 $dbo->execute();
3741 if ($dbo->getNumRows() > 0) {
3742 $s = $dbo->loadResult();
3743 return $getjson ? $s : json_decode($s, true);
3744 }
3745 //count of this array will be at least 1 to store the "more" property
3746 $def_arr = array('more' => '');
3747 $def_val = json_encode($def_arr);
3748 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
3749 $dbo->setQuery($q);
3750 $dbo->execute();
3751 return $getjson ? $def_val : $def_arr;
3752 }
3753
3754 public static function calcDepositOverride($amount_deposit, $nights) {
3755 $overrides = self::getDepositOverrides();
3756 $nights = intval($nights);
3757 $andmore = intval($overrides['more']);
3758 if (!(count($overrides) > 1)) {
3759 //no overrides
3760 return $amount_deposit;
3761 }
3762 foreach ($overrides as $k => $v) {
3763 if ((int)$k == $nights && strlen($v) > 0) {
3764 //exact override found
3765 return (float)$v;
3766 }
3767 }
3768 if ($andmore > 0 && $andmore <= $nights) {
3769 foreach ($overrides as $k => $v) {
3770 if ((int)$k == $andmore && strlen($v) > 0) {
3771 //"and more" nights found
3772 return (float)$v;
3773 }
3774 }
3775 }
3776 //nothing was found
3777 return $amount_deposit;
3778 }
3779
3780 public static function noDepositForNonRefund() {
3781 $dbo = JFactory::getDbo();
3782 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
3783 $dbo->setQuery($q);
3784 $dbo->execute();
3785 if ($dbo->getNumRows() > 0) {
3786 $s = $dbo->loadResult();
3787 return ((int)$s == 1);
3788 }
3789 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
3790 $dbo->setQuery($q);
3791 $dbo->execute();
3792 //default to false
3793 return false;
3794 }
3795
3796 /**
3797 * This method returns the room-rate array with the lowest price
3798 * that matches the preferred rate plan parameter (if available).
3799 * The array $website_rates could not be an array, or it could be
3800 * an array with the error string (response from the TACVBO Class).
3801 * The method has been introduced in VBO 1.10 and it's mainly used
3802 * by the module mod_vikbooking_channelrates and its ajax requests.
3803 *
3804 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
3805 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
3806 *
3807 * @return array
3808 */
3809 public static function getBestRoomRate($website_rates, $def_rplan) {
3810 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
3811 return array();
3812 }
3813 $best_room_rate = array();
3814 foreach ($website_rates as $rid => $tars) {
3815 foreach ($tars as $tar) {
3816 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
3817 //the array $website_rates is already sorted by price ASC, so we take the first useful array
3818 $best_room_rate = $tar;
3819 break 2;
3820 }
3821 }
3822 }
3823 if (!(count($best_room_rate) > 0)) {
3824 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
3825 foreach ($website_rates as $rid => $tars) {
3826 foreach ($tars as $tar) {
3827 $best_room_rate = $tar;
3828 break 2;
3829 }
3830 }
3831 }
3832
3833 return $best_room_rate;
3834 }
3835
3836 /**
3837 * This method returns an array with the details
3838 * of all channels in VCM that supports AV requests,
3839 * and that have at least one room type mapped.
3840 * The method invokes the Logos Class to return details
3841 * about the name and logo URL of the channel.
3842 * The method has been introduced in VBO 1.10 and it's mainly used
3843 * by the module mod_vikbooking_channelrates and its ajax requests.
3844 *
3845 * @param array $channels an array of channel IDs to be mapped on the VCM relations
3846 *
3847 * @return array
3848 */
3849 public static function getChannelsMap($channels) {
3850 if (!is_array($channels) || !(count($channels))) {
3851 return array();
3852 }
3853 $vcm_logos = self::getVcmChannelsLogo('', true);
3854 if (!is_object($vcm_logos)) {
3855 return array();
3856 }
3857 $channels_ids = array();
3858 foreach ($channels as $chid) {
3859 $ichid = (int)$chid;
3860 if ($ichid < 1) {
3861 continue;
3862 }
3863 array_push($channels_ids, $ichid);
3864 }
3865 if (!(count($channels_ids))) {
3866 return array();
3867 }
3868 $dbo = JFactory::getDbo();
3869 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
3870 $dbo->setQuery($q);
3871 $dbo->execute();
3872 if ($dbo->getNumRows() < 1) {
3873 return array();
3874 }
3875 $channels_names = $dbo->loadAssocList();
3876 $channels_map = array();
3877 foreach ($channels_names as $ch) {
3878 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
3879 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
3880 $chdata = array(
3881 'id' => $ch['idchannel'],
3882 'name' => ucwords($ch['channel']),
3883 'logo' => $ota_logo_url
3884 );
3885 array_push($channels_map, $chdata);
3886 }
3887 return $channels_map;
3888 }
3889
3890 /**
3891 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
3892 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
3893 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
3894 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
3895 * module/widget and its ajax requests.
3896 *
3897 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
3898 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
3899 *
3900 * @return mixed string (empty or indicating the alteration), or associative array per channel.
3901 *
3902 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
3903 */
3904 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
3905 {
3906 $otas_rates_val = '';
3907 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
3908 return $otas_rates_val;
3909 }
3910 if (!class_exists('VikChannelManager')) {
3911 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
3912 }
3913 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
3914 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
3915 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
3916 // the Bulk Rates Cache contains data for this room type and rate plan
3917 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
3918 // build rates modification string
3919 if ((int)$data_cont['rmod'] > 0) {
3920 // rates were modified for the OTAs, check how
3921 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
3922 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
3923 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
3924 // check alterations per channel, if requested
3925 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
3926 // we compose an associative array with the alteration for each channel (0 = generic rule)
3927 $channel_alterations = array($otas_rates_val);
3928 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
3929 if ((int)$ch_mods['rmod'] > 0) {
3930 // custom rate alteration for this channel
3931 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
3932 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
3933 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
3934 } else {
3935 // no rates alteration for this channel identifier
3936 $ch_alter_string = '+0%';
3937 }
3938 $channel_alterations[$ch_id] = $ch_alter_string;
3939 }
3940 // overwrite return value with associative array
3941 $otas_rates_val = $channel_alterations;
3942 }
3943 }
3944 }
3945 }
3946
3947 return $otas_rates_val;
3948 }
3949
3950 /**
3951 * This method checks if some non-refundable rates were selected
3952 * (`free_cancellation`=0), the only argument is an array of tariffs.
3953 * The property 'idprice' must be defined on each sub-array.
3954 *
3955 * @param $tars array
3956 *
3957 * @return boolean
3958 **/
3959 public static function findNonRefundableRates($tars = [])
3960 {
3961 $id_prices = [];
3962
3963 foreach ($tars as $tar) {
3964 if (isset($tar['idprice'])) {
3965 if (!in_array($tar['idprice'], $id_prices)) {
3966 array_push($id_prices, (int)$tar['idprice']);
3967 }
3968 continue;
3969 }
3970 foreach ($tar as $t) {
3971 if (isset($t['idprice'])) {
3972 if (!in_array($t['idprice'], $id_prices)) {
3973 array_push($id_prices, (int)$t['idprice']);
3974 }
3975 }
3976 }
3977 }
3978
3979 if (!count($id_prices)) {
3980 // no price IDs found (probably a package)
3981 return false;
3982 }
3983
3984 $dbo = JFactory::getDbo();
3985
3986 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
3987 $dbo->setQuery($q);
3988 $dbo->execute();
3989
3990 return (bool)($dbo->getNumRows() > 0);
3991 }
3992
3993 /**
3994 * This method checks if the deposit is allowed depending on
3995 * the selected rate plans (idprice) for the rooms reserved.
3996 * If the configuration setting is enabled, and if some
3997 * non-refundable rates were selected (`free_cancellation`=0),
3998 * the method will return false, because the deposit is not allowed.
3999 * The only argument is an array of tariffs. The property 'idprice'
4000 * must be defined on each sub-array (multi-dimension supported)
4001 * throgh the method findNonRefundableRates();
4002 *
4003 * @param $tars array
4004 *
4005 * @return boolean
4006 **/
4007 public static function allowDepositFromRates($tars) {
4008 if (!self::noDepositForNonRefund()) {
4009 //deposit can be paid also if non-refundable rates
4010 return true;
4011 }
4012 return !self::findNonRefundableRates($tars);
4013 }
4014
4015 public static function showSearchSuggestions() {
4016 $dbo = JFactory::getDbo();
4017 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4018 $dbo->setQuery($q);
4019 $dbo->execute();
4020 if ($dbo->getNumRows() > 0) {
4021 return (int)$dbo->loadResult();
4022 }
4023 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4024 $dbo->setQuery($q);
4025 $dbo->execute();
4026 return 1;
4027 }
4028
4029 /**
4030 * Given a code, returns the coupon information array.
4031 *
4032 * @param string $code the coupon code.
4033 *
4034 * @return array empty array or coupon record.
4035 *
4036 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4037 */
4038 public static function getCouponInfo($code)
4039 {
4040 $dbo = JFactory::getDbo();
4041
4042 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4043 $dbo->setQuery($q, 0, 1);
4044 $coupon = $dbo->loadAssoc();
4045 if (!$coupon) {
4046 return [];
4047 }
4048
4049 // check for customers assigned
4050 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4051 $dbo->setQuery($q);
4052 $coupon_customers = $dbo->loadAssocList();
4053 if ($coupon_customers) {
4054 // set customers and automatic properties
4055 $coupon['customers'] = [];
4056 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4057 foreach ($coupon_customers as $coupon_customer) {
4058 // push customer ID
4059 $coupon['customers'][] = $coupon_customer['idcustomer'];
4060 }
4061 }
4062
4063 return $coupon;
4064 }
4065
4066 /**
4067 * Reads the details of a given room record ID.
4068 *
4069 * @param int $idroom The room ID to fetch.
4070 * @param array $columns The optional room columns to fetch.
4071 * @param bool $no_cache Whether to ignore a previously cached record.
4072 *
4073 * @return array
4074 *
4075 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4076 */
4077 public static function getRoomInfo($idroom, $columns = [], $no_cache = false)
4078 {
4079 static $room_infos = [];
4080
4081 if (isset($room_infos[$idroom]) && !$no_cache) {
4082 return $room_infos[$idroom];
4083 }
4084
4085 $dbo = JFactory::getDbo();
4086
4087 if ($columns) {
4088 $columns = array_map([$dbo, 'qn'], $columns);
4089 }
4090
4091 $q = $dbo->getQuery(true)
4092 ->select($columns ? $columns : '*')
4093 ->from($dbo->qn('#__vikbooking_rooms'))
4094 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4095
4096 $dbo->setQuery($q);
4097 $room = $dbo->loadAssoc();
4098
4099 if ($no_cache) {
4100 return $room ?: [];
4101 }
4102
4103 $room_infos[$idroom] = $room ?: [];
4104
4105 return $room_infos[$idroom];
4106 }
4107
4108 public static function loadOrdersRoomsData($idorder)
4109 {
4110 $dbo = JFactory::getDbo();
4111
4112 $q = $dbo->getQuery(true)
4113 ->select($dbo->qn('or') . '.*')
4114 ->select($dbo->qn('r.name', 'room_name'))
4115 ->select($dbo->qn('r.params'))
4116 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4117 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4118 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4119 ->order($dbo->qn('or.id') . ' ASC');
4120
4121 $dbo->setQuery($q);
4122
4123 return $dbo->loadAssocList();
4124 }
4125
4126 public static function sayCategory($ids, $vbo_tn = null)
4127 {
4128 $dbo = JFactory::getDbo();
4129 $split = explode(";", $ids);
4130 $say = "";
4131 foreach ($split as $k => $s) {
4132 if (!strlen($s)) {
4133 continue;
4134 }
4135 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4136 $dbo->setQuery($q);
4137 $nam = $dbo->loadAssocList();
4138 if (!$nam) {
4139 continue;
4140 }
4141 if (is_object($vbo_tn)) {
4142 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4143 }
4144 $say .= $nam[0]['name'];
4145 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4146 }
4147 return $say;
4148 }
4149
4150 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4151 {
4152 $dbo = JFactory::getDbo();
4153 $split = explode(";", $idc);
4154 $carat = "";
4155 $arr = array();
4156 $where = array();
4157 foreach ($split as $s) {
4158 if (!empty($s)) {
4159 $where[] = $s;
4160 }
4161 }
4162 if ($where) {
4163 $q = "SELECT `c`.* FROM `#__vikbooking_characteristics` AS `c` WHERE `c`.`id` IN (" . implode(",", $where) . ") ORDER BY `c`.`ordering` ASC;";
4164 $dbo->setQuery($q);
4165 $arr = $dbo->loadAssocList();
4166 if ($arr && is_object($vbo_tn)) {
4167 $vbo_tn->translateContents($arr, '#__vikbooking_characteristics');
4168 }
4169 }
4170 if ($arr) {
4171 $carat .= "<div class=\"vbo-room-carats\">\n";
4172 foreach ($arr as $a) {
4173 if (!empty($a['textimg'])) {
4174 //tooltip icon text is not empty
4175 if (!empty($a['icon'])) {
4176 //an icon has been uploaded: display the image
4177 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"".$a['textimg']."\"><img src=\"".VBO_SITE_URI."resources/uploads/".$a['icon']."\" alt=\"" . $a['name'] . "\" /></span></span>\n";
4178 } else {
4179 if (strpos($a['textimg'], '</i>') !== false) {
4180 //the tooltip icon text is a font-icon, we can use the name as tooltip
4181 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"".$a['name']."\">".$a['textimg']."</span></span>\n";
4182 } else {
4183 //display just the text
4184 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4185 }
4186 }
4187 } else {
4188 $carat .= (!empty($a['icon']) ? "<span class=\"vbo-room-carat\"><img src=\"".VBO_SITE_URI."resources/uploads/" . $a['icon'] . "\" alt=\"" . $a['name'] . "\" title=\"" . $a['name'] . "\"/></span>\n" : "<span class=\"vbo-room-carat\">".$a['name']."</span>\n");
4189 }
4190 }
4191 $carat .= "</div>\n";
4192 }
4193 return $carat;
4194 }
4195
4196 public static function getRoomOptionals($idopts, $vbo_tn = null)
4197 {
4198 $dbo = JFactory::getDbo();
4199 $split = explode(";", $idopts);
4200 $fetch = array();
4201 foreach ($split as $s) {
4202 if (!empty($s)) {
4203 $fetch[] = $s;
4204 }
4205 }
4206 if ($fetch) {
4207 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $fetch).") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4208 $dbo->setQuery($q);
4209 $arr = $dbo->loadAssocList();
4210 if ($arr) {
4211 if (is_object($vbo_tn)) {
4212 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4213 }
4214 return $arr;
4215 }
4216 }
4217 return [];
4218 }
4219
4220 public static function getSingleOption($idopt, $vbo_tn = null)
4221 {
4222 $dbo = JFactory::getDbo();
4223 $opt = array();
4224 if (!empty($idopt)) {
4225 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4226 $dbo->setQuery($q);
4227 $opt = $dbo->loadAssoc();
4228 if ($opt && is_object($vbo_tn)) {
4229 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4230 }
4231 }
4232 return $opt;
4233 }
4234
4235 /**
4236 * Unsets the options that are not available in the requested dates.
4237 *
4238 * @param array $optionals the array of options passed by reference
4239 * @param int $checkin the timestamp of the check-in date and time
4240 * @param int $checkout the timestamp of the check-out date and time
4241 *
4242 * @return void
4243 *
4244 * @since 1.11
4245 */
4246 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4247 {
4248 if (!$optionals || empty($checkin) || empty($checkout)) {
4249 return;
4250 }
4251
4252 foreach ($optionals as $k => $v) {
4253 if (!empty($v['alwaysav'])) {
4254 $dates = explode(';', $v['alwaysav']);
4255 if (empty($dates[0]) || empty($dates[1])) {
4256 continue;
4257 }
4258 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4259 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4260 unset($optionals[$k]);
4261 }
4262 }
4263 }
4264 }
4265
4266 /**
4267 * Unsets the options that are not suited for the requested room party.
4268 *
4269 * @param array $optionals the array of options passed by reference.
4270 * @param int $adults the number of adults in the room party.
4271 * @param int $children the number of children in the room party.
4272 *
4273 * @return void
4274 *
4275 * @since 1.13.5
4276 */
4277 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4278 {
4279 if (!$optionals || (empty($adults) && empty($children))) {
4280 return;
4281 }
4282
4283 foreach ($optionals as $k => $v) {
4284 if (empty($v['oparams'])) {
4285 continue;
4286 }
4287 $v['oparams'] = json_decode($v['oparams'], true);
4288 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4289 continue;
4290 }
4291 if (!empty($v['oparams']['minguestsnum'])) {
4292 // filter by minimum adults/guests
4293 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4294 // minimum number of adults not sufficient, unset option
4295 unset($optionals[$k]);
4296 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4297 // minimum number of total guests not sufficient, unset option
4298 unset($optionals[$k]);
4299 }
4300 }
4301 if (!empty($v['oparams']['maxguestsnum'])) {
4302 // filter by maximum adults/guests
4303 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4304 // maximum number of adults exceeded, unset option
4305 unset($optionals[$k]);
4306 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4307 // maximum number of total guests exceeded, unset option
4308 unset($optionals[$k]);
4309 }
4310 }
4311 }
4312 }
4313
4314 /**
4315 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4316 *
4317 * @param array $id_rooms list of room IDs.
4318 * @param int $num_adults number of adults.
4319 * @param int $num_nights number of nights of stay.
4320 * @param int $checkin optional check-in timestamp.
4321 * @param int $checkout optional check-out timestamp.
4322 *
4323 * @return array associative information of city taxes, fees and options.
4324 *
4325 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4326 */
4327 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4328 {
4329 $dbo = JFactory::getDbo();
4330
4331 $taxes = 0;
4332 $fees = 0;
4333
4334 $options_data = [];
4335 $id_options = [];
4336
4337 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4338 $dbo->setQuery($q);
4339 $assocs = $dbo->loadAssocList();
4340 if ($assocs) {
4341 foreach ($assocs as $opts) {
4342 if (!empty($opts['idopt'])) {
4343 $r_ido = explode(';', rtrim($opts['idopt']));
4344 foreach ($r_ido as $ido) {
4345 if (!empty($ido) && !in_array($ido, $id_options)) {
4346 $id_options[] = (int)$ido;
4347 }
4348 }
4349 }
4350 }
4351 }
4352
4353 if ($id_options) {
4354 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(", ", $id_options) . ") AND `forcesel`=1 AND `ifchildren`=0 AND (`is_citytax`=1 OR `is_fee`=1);";
4355 $dbo->setQuery($q);
4356 $alltaxesfees = $dbo->loadAssocList();
4357
4358 if ($checkin && $checkout) {
4359 // filter the records by stay dates to support, for example, seasonal city taxes
4360 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4361 }
4362
4363 foreach ($alltaxesfees as $tf) {
4364 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4365 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4366 $realcost = $tf['maxprice'];
4367 }
4368 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4369 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4370 if ($tf['is_citytax'] == 1) {
4371 $taxes += $realcost;
4372 } elseif ($tf['is_fee'] == 1) {
4373 $fees += $realcost;
4374 }
4375 $optsett = explode('-', $tf['forceval']);
4376 $options_data[] = $tf['id'].':'.$optsett[0];
4377 }
4378 }
4379
4380 return [
4381 'city_taxes' => $taxes,
4382 'fees' => $fees,
4383 'options' => $options_data
4384 ];
4385 }
4386
4387 /**
4388 * From a list of option/extras records, it returns an array with the options/extras
4389 * that do not support age intervals, and options for the children age intervals.
4390 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4391 *
4392 * @param array $optionals the full list of options/extras records.
4393 * @param int $adults the number of adults in the room requested.
4394 * @param int $children the number of children in the room requested.
4395 *
4396 * @return array the filtered regular and children age intervals options.
4397 */
4398 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4399 {
4400 // container for age intervals
4401 $ageintervals = [];
4402
4403 // regular options should start as an array
4404 if (!is_array($optionals)) {
4405 $optionals = [];
4406 }
4407
4408 // check for the first valid age intervals option
4409 foreach ($optionals as $kopt => $opt) {
4410 if (!empty($opt['ageintervals'])) {
4411 $intervals = explode(';;', $opt['ageintervals']);
4412 foreach ($intervals as $intv) {
4413 if (empty($intv)) {
4414 continue;
4415 }
4416 $parts = explode('_', $intv);
4417 if (count($parts) >= 3) {
4418 // age intervals option found, get the first and only one
4419 $ageintervals = $optionals[$kopt];
4420 break 2;
4421 }
4422 }
4423 }
4424 }
4425
4426 if ($ageintervals) {
4427 /**
4428 * We allow price adjustments for a minimum total guests by overriding the costs
4429 * for each age interval for specific child numbers when the party covers some children.
4430 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4431 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4432 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4433 *
4434 * @since 1.13.5
4435 */
4436 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4437 $oparams = !is_array($oparams) ? array() : $oparams;
4438 $adults = (int)$adults;
4439 $children = (int)$children;
4440 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4441 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4442 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4443 if ($children_to_pay < $children) {
4444 // compose string for free age intervals
4445 $intervals = explode(';;', $ageintervals['ageintervals']);
4446 foreach ($intervals as $k => $v) {
4447 $parts = explode('_', $v);
4448 // set cost to 0 (key = 2)
4449 $parts[2] = '0';
4450 $intervals[$k] = implode('_', $parts);
4451 }
4452 $free_ageintervals = implode(';;', $intervals);
4453 for ($i = 1; $i <= $children; $i++) {
4454 if (($children - $i + 1) <= $children_to_pay) {
4455 // costs for this Nth last child should be regular as this child should pay
4456 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4457 } else {
4458 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4459 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4460 }
4461 }
4462 }
4463 }
4464
4465 // remove age intervals from regular options
4466 foreach ($optionals as $kopt => $opt) {
4467 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4468 // unset the option of type age intervals from the regular options
4469 unset($optionals[$kopt]);
4470 }
4471 }
4472 }
4473
4474 // return the filtered list of regular options and age intervals options
4475 return [$optionals, $ageintervals];
4476 }
4477
4478 /**
4479 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4480 * This is because some children in the party may not need to pay anything due to the min guests.
4481 *
4482 * @param array $optional the option full record to parse.
4483 * @param int $adults the number of adults in the room requested.
4484 * @param int $children the number of children in the room requested.
4485 *
4486 * @return array associative array with default age intervals and override strings.
4487 *
4488 * @since 1.13.5
4489 */
4490 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4491 {
4492 if (!is_array($optional)) {
4493 $optional = array();
4494 }
4495
4496 $overrides = array(
4497 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4498 );
4499
4500 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4501 $oparams = !is_array($oparams) ? array() : $oparams;
4502 $adults = (int)$adults;
4503 $children = (int)$children;
4504
4505 if (!count($oparams)) {
4506 return $overrides;
4507 }
4508
4509 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4510 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4511 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4512 if ($children_to_pay < $children) {
4513 // compose string for free age intervals
4514 $intervals = explode(';;', $optional['ageintervals']);
4515 foreach ($intervals as $k => $v) {
4516 $parts = explode('_', $v);
4517 // set cost to 0 (key = 2)
4518 $parts[2] = '0';
4519 $intervals[$k] = implode('_', $parts);
4520 }
4521 $free_ageintervals = implode(';;', $intervals);
4522 for ($i = 1; $i <= $children; $i++) {
4523 if (($children - $i + 1) <= $children_to_pay) {
4524 // costs for this Nth last child should be regular as this child should pay
4525 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4526 } else {
4527 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4528 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4529 }
4530 }
4531 }
4532 }
4533
4534 return $overrides;
4535 }
4536
4537 /**
4538 * Returns the number/index of the children being parsed given the full list of room options string,
4539 * the current key in the array of the split room options string, the ID of the ageintervals option,
4540 * and the total number of children. This is useful for later applying the cost overrides for the
4541 * children ages through the method getOptionIntervalChildOverrides().
4542 *
4543 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4544 * @param int $optid the ID of the option of type age intervals to check.
4545 * @param int $roptkey the current position in the loop of the room options string.
4546 * @param int $children the number of children in the room requested.
4547 *
4548 * @return int the position/number of the children being parsed, -1 if not found.
4549 *
4550 * @since 1.13.5
4551 */
4552 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4553 {
4554 // default to -1 for not found
4555 $child_num = -1;
4556
4557 $valid_opt_counter = 0;
4558
4559 $roptions = explode(";", $roptstr);
4560 foreach ($roptions as $k => $opt) {
4561 if (empty($opt)) {
4562 continue;
4563 }
4564 $optvals = explode(":", $opt);
4565 /**
4566 * In some cases, like the "saveorder" task, the room option string may contain
4567 * the room number beside the option ID, separated with an underscore.
4568 * If an underscore is present, we need to split it to find the option ID.
4569 */
4570 if (strpos($optvals[0], '_') !== false) {
4571 // underscore found in room number portion, extract the option ID
4572 $rn_parts = explode('_', $optvals[0]);
4573 // 0th element is the room number in the party, 1st elem is the option ID
4574 $optvals[0] = $rn_parts[1];
4575 }
4576 //
4577 if ((int)$optvals[0] != (int)$optid) {
4578 // we are not interested into this option ID
4579 continue;
4580 }
4581 // increase counter for this option ID
4582 $valid_opt_counter++;
4583
4584 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4585 // children position found, we need to return it as a 0th base
4586 $child_num = ($valid_opt_counter - 1);
4587 break;
4588 }
4589 }
4590
4591 return $child_num;
4592 }
4593
4594 public static function getOptionIntervalsCosts($intvstr) {
4595 $optcosts = array();
4596 $intervals = explode(';;', $intvstr);
4597 foreach ($intervals as $kintv => $intv) {
4598 if (empty($intv)) continue;
4599 $parts = explode('_', $intv);
4600 if (count($parts) >= 3) {
4601 $optcosts[$kintv] = (float)$parts[2];
4602 }
4603 }
4604 return $optcosts;
4605 }
4606
4607 public static function getOptionIntervalsAges($intvstr) {
4608 $optages = array();
4609 $intervals = explode(';;', $intvstr);
4610 foreach ($intervals as $kintv => $intv) {
4611 if (empty($intv)) continue;
4612 $parts = explode('_', $intv);
4613 if (count($parts) >= 3) {
4614 $optages[$kintv] = $parts[0].' - '.$parts[1];
4615 }
4616 }
4617 return $optages;
4618 }
4619
4620 public static function getOptionIntervalsPercentage($intvstr) {
4621 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4622 $optcostspcent = array();
4623 $intervals = explode(';;', $intvstr);
4624 foreach ($intervals as $kintv => $intv) {
4625 if (empty($intv)) continue;
4626 $parts = explode('_', $intv);
4627 if (count($parts) >= 3) {
4628 //fixed amount
4629 $setval = 0;
4630 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
4631 //percentage value of the room base cost (VBO 1.10)
4632 $setval = 2;
4633 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
4634 //percentage value of the adults tariff
4635 $setval = 1;
4636 }
4637 $optcostspcent[$kintv] = $setval;
4638 }
4639 }
4640 return $optcostspcent;
4641 }
4642
4643 public static function dayValidTs($days, $first, $second)
4644 {
4645 $secdiff = $second - $first;
4646 $daysdiff = $secdiff / 86400;
4647 if (is_int($daysdiff)) {
4648 if ($daysdiff < 1) {
4649 $daysdiff = 1;
4650 }
4651 } else {
4652 if ($daysdiff < 1) {
4653 $daysdiff = 1;
4654 } else {
4655 $sum = floor($daysdiff) * 86400;
4656 $newdiff = $secdiff - $sum;
4657 $maxhmore = self::getHoursMoreRb() * 3600;
4658 if ($maxhmore >= $newdiff) {
4659 $daysdiff = floor($daysdiff);
4660 } else {
4661 $daysdiff = ceil($daysdiff);
4662 }
4663 }
4664 }
4665
4666 return ($daysdiff == $days);
4667 }
4668
4669 /**
4670 * Calculates the stay fee inclusive of taxes.
4671 *
4672 * @param float $cost the cost to evaluate.
4673 * @param int $idprice the rate plan ID associated.
4674 *
4675 * @return float the given cost after tax, if taxes had to be applied.
4676 */
4677 public static function sayCostPlusIva($cost, $idprice)
4678 {
4679 $ivainclusa = self::ivaInclusa();
4680 if ($ivainclusa) {
4681 return $cost;
4682 }
4683
4684 static $tax_rplans_map = [];
4685
4686 if (array_key_exists($idprice, $tax_rplans_map)) {
4687 $tax_rate = $tax_rplans_map[$idprice];
4688 } else {
4689 $dbo = JFactory::getDbo();
4690 $q = "SELECT `p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` `i` ON `i`.`id`=`p`.`idiva` WHERE `p`.`id`=" . (int)$idprice . ";";
4691 $dbo->setQuery($q);
4692 $tax_rate = $dbo->loadAssoc();
4693
4694 // cache value
4695 $tax_rplans_map[$idprice] = $tax_rate;
4696 }
4697
4698 if (!$tax_rate || empty($tax_rate['aliq'])) {
4699 return $cost;
4700 }
4701
4702 $subt = 100 + $tax_rate['aliq'];
4703 $op = ($cost * $subt / 100);
4704
4705 /**
4706 * Tax Cap implementation for prices tax excluded (most common).
4707 *
4708 * @since 1.12
4709 */
4710 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4711 $op = ($cost + $tax_rate['taxcap']);
4712 }
4713
4714 // apply rounding to avoid issues with multiple tax rates when tax excluded
4715 $formatvals = self::getNumberFormatData();
4716 $formatparts = explode(':', $formatvals);
4717 $rounded_op = round($op, (int)$formatparts[0]);
4718 if ($rounded_op > $op) {
4719 return $rounded_op;
4720 }
4721
4722 /**
4723 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4724 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4725 *
4726 * @since 1.11.1
4727 */
4728 return round($op, (int)$formatparts[0]);
4729 }
4730
4731 /**
4732 * Calculates the stay fee exclusive of taxes.
4733 *
4734 * @param float $cost the cost to evaluate.
4735 * @param int $idprice the rate plan ID associated.
4736 *
4737 * @return float the given cost before tax.
4738 */
4739 public static function sayCostMinusIva($cost, $idprice)
4740 {
4741 $ivainclusa = self::ivaInclusa();
4742 if (!$ivainclusa) {
4743 return $cost;
4744 }
4745
4746 static $tax_rplans_map = [];
4747
4748 if (array_key_exists($idprice, $tax_rplans_map)) {
4749 $tax_rate = $tax_rplans_map[$idprice];
4750 } else {
4751 $dbo = JFactory::getDbo();
4752 $q = "SELECT `p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` `i` ON `i`.`id`=`p`.`idiva` WHERE `p`.`id`=" . (int)$idprice . ";";
4753 $dbo->setQuery($q);
4754 $tax_rate = $dbo->loadAssoc();
4755
4756 // cache value
4757 $tax_rplans_map[$idprice] = $tax_rate;
4758 }
4759
4760 if (!$tax_rate || empty($tax_rate['aliq'])) {
4761 return $cost;
4762 }
4763
4764 $subt = 100 + $tax_rate['aliq'];
4765 $op = ($cost * 100 / $subt);
4766
4767 /**
4768 * Tax Cap implementation also when prices tax included.
4769 *
4770 * @since 1.12
4771 */
4772 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4773 $op = ($cost - $tax_rate['taxcap']);
4774 }
4775
4776 // apply rounding to avoid issues with multiple tax rates when tax included
4777 $formatvals = self::getNumberFormatData();
4778 $formatparts = explode(':', $formatvals);
4779 $rounded_op = round($op, (int)$formatparts[0]);
4780 if ($rounded_op < $op) {
4781 return $rounded_op;
4782 }
4783
4784 /**
4785 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4786 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4787 *
4788 * @since 1.11.1
4789 */
4790 return round($op, (int)$formatparts[0]);
4791 }
4792
4793 /**
4794 * Given an option/extra cost, determines whether taxes should be applied over it.
4795 * Can also be used to calculate taxes on the extra costs per room in the bookings
4796 *
4797 * @param float $cost the cost to evaluate, option or extra service fee.
4798 * @param int $idiva the ID of the tax rate to use to apply taxes.
4799 * @param bool $force if true, taxes will always be applied.
4800 *
4801 * @return float the given option/extra cost plus tax, if taxes had to be applied.
4802 *
4803 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4804 */
4805 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
4806 {
4807 $ivainclusa = self::ivaInclusa();
4808 if ($ivainclusa && $force !== true) {
4809 return $cost;
4810 }
4811
4812 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4813 if (!$tax_rate) {
4814 return $cost;
4815 }
4816
4817 $subt = 100 + $tax_rate['aliq'];
4818 $op = ($cost * $subt / 100);
4819
4820 /**
4821 * Tax Cap implementation for prices tax excluded (most common).
4822 *
4823 * @since 1.12
4824 */
4825 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4826 $op = ($cost + $tax_rate['taxcap']);
4827 }
4828
4829 // apply rounding to avoid issues with multiple tax rates when tax excluded
4830 $formatvals = self::getNumberFormatData();
4831 $formatparts = explode(':', $formatvals);
4832 $rounded_op = round($op, (int)$formatparts[0]);
4833 if ($rounded_op > $op) {
4834 return $rounded_op;
4835 }
4836
4837 return $op;
4838 }
4839
4840 /**
4841 * Calculates the net amount for an option/extra service.
4842 *
4843 * @param float $cost the cost to evaluate, option or extra service fee.
4844 * @param int $idiva the ID of the tax rate to use to apply taxes.
4845 *
4846 * @return float the given option/extra cost before tax, if taxes were inclusive.
4847 */
4848 public static function sayOptionalsMinusIva($cost, $idiva)
4849 {
4850 if (empty($idiva)) {
4851 return $cost;
4852 }
4853
4854 $ivainclusa = self::ivaInclusa();
4855 if (!$ivainclusa) {
4856 return $cost;
4857 }
4858
4859 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4860 if (!$tax_rate) {
4861 return $cost;
4862 }
4863
4864 $subt = 100 + $tax_rate['aliq'];
4865 $op = ($cost * 100 / $subt);
4866
4867 /**
4868 * Tax Cap implementation also when prices tax included.
4869 *
4870 * @since 1.12
4871 */
4872 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4873 $op = ($cost - $tax_rate['taxcap']);
4874 }
4875
4876 // apply rounding to avoid issues with multiple tax rates when tax included
4877 $formatvals = self::getNumberFormatData();
4878 $formatparts = explode(':', $formatvals);
4879 $rounded_op = round($op, (int)$formatparts[0]);
4880 if ($rounded_op < $op) {
4881 return $rounded_op;
4882 }
4883
4884 return $op;
4885 }
4886
4887 /**
4888 * Given a cost, determines whether taxes should be applied over it.
4889 *
4890 * @param float $cost the cost to evaluate, could be a room custom price.
4891 * @param int $idiva the ID of the tax rate to use to apply taxes.
4892 * @param bool $force if true, taxes will always be applied.
4893 *
4894 * @return float the given cost plus tax, if taxes had to be applied.
4895 *
4896 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4897 */
4898 public static function sayPackagePlusIva($cost, $idiva, $force = false)
4899 {
4900 $ivainclusa = self::ivaInclusa();
4901 if ($ivainclusa && $force !== true) {
4902 return $cost;
4903 }
4904
4905 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4906 if (!$tax_rate) {
4907 return $cost;
4908 }
4909
4910 $subt = 100 + $tax_rate['aliq'];
4911 $op = ($cost * $subt / 100);
4912
4913 /**
4914 * Tax Cap implementation for prices tax excluded (most common).
4915 *
4916 * @since 1.12
4917 */
4918 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4919 $op = ($cost + $tax_rate['taxcap']);
4920 }
4921
4922 // apply rounding to avoid issues with multiple tax rates when tax excluded
4923 $formatvals = self::getNumberFormatData();
4924 $formatparts = explode(':', $formatvals);
4925 $rounded_op = round($op, (int)$formatparts[0]);
4926 if ($rounded_op > $op) {
4927 return $rounded_op;
4928 }
4929
4930 return $op;
4931 }
4932
4933 /**
4934 * Calculates the net amount for a package.
4935 *
4936 * @param float $cost the cost to evaluate, option or extra service fee.
4937 * @param int $idiva the ID of the tax rate to use to apply taxes.
4938 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
4939 *
4940 * @return float the given cost before tax.
4941 */
4942 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
4943 {
4944 $ivainclusa = self::ivaInclusa();
4945 if (!$ivainclusa && $force_invoice_excltax !== true) {
4946 return $cost;
4947 }
4948
4949 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4950 if (!$tax_rate) {
4951 return $cost;
4952 }
4953
4954 $subt = 100 + $tax_rate['aliq'];
4955 $op = ($cost * 100 / $subt);
4956
4957 /**
4958 * Tax Cap implementation also when prices tax included.
4959 *
4960 * @since 1.12
4961 */
4962 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4963 $op = ($cost - $tax_rate['taxcap']);
4964 }
4965
4966 // apply rounding to avoid issues with multiple tax rates when tax included
4967 $formatvals = self::getNumberFormatData();
4968 $formatparts = explode(':', $formatvals);
4969 $rounded_op = round($op, (int)$formatparts[0]);
4970 if ($rounded_op < $op) {
4971 return $rounded_op;
4972 }
4973
4974 return $op;
4975 }
4976
4977 public static function getSecretLink()
4978 {
4979 $dbo = JFactory::getDbo();
4980
4981 $sid = mt_rand();
4982
4983 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
4984 $dbo->setQuery($q);
4985 $all = $dbo->loadAssocList();
4986 if ($all) {
4987 $list = [];
4988 foreach ($all as $s) {
4989 $list[] = $s['sid'];
4990 }
4991 if (in_array($sid, $list)) {
4992 while(in_array($sid, $list)) {
4993 $sid++;
4994 }
4995 }
4996 }
4997
4998 return $sid;
4999 }
5000
5001 /**
5002 * Generates a confirmation number for the given booking.
5003 * Modified the way random string-numbers are obtained to
5004 * avoid any possible brute-force attempt.
5005 *
5006 * @since 1.15.1 (J) - 1.5.4 (WP)
5007 */
5008 public static function generateConfirmNumber($oid, $update = true)
5009 {
5010 $dbo = JFactory::getDbo();
5011
5012 $confirmnumb = date('y') . $oid;
5013 for ($i = 0; $i < 8; $i++) {
5014 $confirmnumb .= rand(0, 9);
5015 }
5016
5017 if ($update) {
5018 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5019 $dbo->setQuery($q);
5020 $dbo->execute();
5021 }
5022
5023 return $confirmnumb;
5024 }
5025
5026 public static function buildCustData($arr, $sep) {
5027 $cdata = "";
5028 foreach ($arr as $k => $e) {
5029 if (strlen($e)) {
5030 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5031 }
5032 }
5033 return $cdata;
5034 }
5035
5036 /**
5037 * This method parses the Joomla menu object to see if a menu item of a
5038 * specific type is available, to get its ID. Useful when links should be
5039 * displayed in pages where there is no Itemid set (booking details pages).
5040 *
5041 * @param array $viewtypes list of accepted menu items
5042 * @param string $lang the optional language to use.
5043 *
5044 * @return int
5045 *
5046 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5047 */
5048 public static function findProperItemIdType($viewtypes, $lang = null)
5049 {
5050 if (VBOPlatformDetection::isWordPress()) {
5051 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5052
5053 $itemid = $model->best($viewtypes, $lang);
5054
5055 if ($itemid) {
5056 return $itemid;
5057 }
5058
5059 return 0;
5060 }
5061
5062 $bestitemid = 0;
5063
5064 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5065
5066 $app = JFactory::getApplication();
5067
5068 $menu = $app->getMenu('site');
5069
5070 if (!$menu) {
5071 return 0;
5072 }
5073
5074 $menu_items = $menu->getMenu();
5075
5076 if (!$menu_items) {
5077 return 0;
5078 }
5079
5080 foreach ($menu_items as $itemid => $item) {
5081 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5082 // proper menu item type found
5083 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5084
5085 if (isset($item->language) && $item->language == $current_lang) {
5086 // we found the exact menu item type for the given language
5087 return $itemid;
5088 }
5089 }
5090 }
5091
5092 return $bestitemid;
5093 }
5094
5095 /**
5096 * Rewrites an internal URI that needs to be used outside of the website.
5097 * This means that the routed URI MUST start with the base path of the site.
5098 *
5099 * @param mixed $query The query string or an associative array of data.
5100 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5101 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5102 *
5103 * @return string The complete routed URI.
5104 *
5105 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5106 * routing from back-end (if available on the CMS version).
5107 */
5108 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5109 {
5110 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5111 }
5112
5113 /**
5114 * Generates an iCal file to be attached to the email message for the
5115 * customer or the administrator with some basic booking details.
5116 *
5117 * @param string $recip either admin or customer.
5118 * @param array $booking the booking array or some keys.
5119 *
5120 * @return mixed string in case of success, false otherwise.
5121 *
5122 * @since 1.12.0
5123 */
5124 public static function getEmailIcal($recip, $booking) {
5125 // load configuration setting
5126 $attachical = self::attachIcal();
5127
5128 if ($attachical === 0) {
5129 // do not attach any iCal file
5130 return false;
5131 }
5132
5133 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5134 // skip the iCal for the admin
5135 return false;
5136 }
5137
5138 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5139 // skip the iCal for the customer
5140 return false;
5141 }
5142
5143 if (strpos($recip, 'admin') !== false) {
5144 // prepare event description and summary for the admin
5145 $description = $booking['custdata'];
5146 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5147 $fname = $booking['ts'] . '.ics';
5148 } else {
5149 // event description and summary for the customer
5150 $description = '';
5151 $summary = self::getFrontTitle();
5152 $fname = 'reservation_reminder.ics';
5153 }
5154
5155 // prepare iCal head
5156 $company_name = self::getFrontTitle();
5157 $ics_str = "BEGIN:VCALENDAR\r\n" .
5158 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5159 "CALSCALE:GREGORIAN\r\n" .
5160 "VERSION:2.0\r\n";
5161 // compose iCal body
5162 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5163 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5164 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5165 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5166 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5167 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5168 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5169 $ics_str .= 'END:VEVENT'."\r\n";
5170 // close iCal file content
5171 $ics_str .= "END:VCALENDAR";
5172
5173 /**
5174 * Trigger event to allow third party plugins to overwrite the iCal file.
5175 *
5176 * @since 1.16.0 (J) - 1.6.0 (WP)
5177 */
5178 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5179
5180 if (empty($ics_str)) {
5181 return false;
5182 }
5183
5184 // store the event onto a .ics file. We use the resources folder in back-end.
5185 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5186 $fp = fopen($fpath, 'w+');
5187 $bytes = fwrite($fp, $ics_str);
5188 fclose($fp);
5189
5190 return $bytes ? $fpath : false;
5191 }
5192
5193 public static function loadEmailTemplate($booking_info = array()) {
5194 define('_VIKBOOKINGEXEC', '1');
5195 ob_start();
5196 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5197 $content = ob_get_contents();
5198 ob_end_clean();
5199 return $content;
5200 }
5201
5202 /**
5203 * Parses the raw HTML content of the booking email template.
5204 *
5205 * @param string $tmpl the raw content of the template.
5206 * @param mixed $bid int for the booking ID or booking array.
5207 * @param array $rooms list of rooms booked and translated.
5208 * @param array $rates list of translated rates for the booked rooms.
5209 * @param array $options list of translated options booked.
5210 * @param float [$total] the booking total amount (in case it has changed).
5211 * @param string [$link] the booking link can be passed for the no-deposit.
5212 *
5213 * @return string the HTML content of the parsed email template.
5214 *
5215 * @since 1.13 with different arguments.
5216 */
5217 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5218 {
5219 $app = JFactory::getApplication();
5220 $dbo = JFactory::getDbo();
5221 $vbo_tn = self::getTranslator();
5222
5223 // availability helper
5224 $av_helper = self::getAvailabilityInstance();
5225
5226 // get necessary values
5227 if (is_array($bid)) {
5228 // we got the full booking record
5229 $order_info = $bid;
5230 $bid = $order_info['id'];
5231 } else {
5232 $order_info = array();
5233 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5234 $dbo->setQuery($q);
5235 $order_info = $dbo->loadAssoc();
5236 if (!$order_info) {
5237 throw new Exception('Booking not found', 404);
5238 }
5239 }
5240
5241 $tars_info = array();
5242 $q = "SELECT `or`.`id`,`or`.`idroom`,`or`.`idtar`,`d`.`idprice` FROM `#__vikbooking_ordersrooms` AS `or` LEFT JOIN `#__vikbooking_dispcost` AS `d` ON `or`.`idtar`=`d`.`id` WHERE `or`.`idorder`=" . (int)$order_info['id'] . ";";
5243 $dbo->setQuery($q);
5244 $tars_info = $dbo->loadAssocList();
5245 if (!$tars_info) {
5246 throw new Exception('No rooms found', 404);
5247 }
5248
5249 /**
5250 * Split stay reservation.
5251 *
5252 * @since 1.16.0 (J) - 1.6.0 (WP)
5253 */
5254 $room_stay_dates = [];
5255 if ($order_info['split_stay']) {
5256 if ($order_info['status'] == 'confirmed') {
5257 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5258 } else {
5259 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5260 }
5261 // immediately count the number of nights of stay for each split room
5262 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5263 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5264 // overwrite values for compatibility with non-confirmed bookings
5265 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5266 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5267 }
5268 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5269 // overwrite the whole array
5270 $room_stay_dates[$sps_r_k] = $sps_r_v;
5271 }
5272 }
5273
5274 // check if the language in use is the same as the one used during the checkout
5275 $lang = JFactory::getLanguage();
5276 if (!empty($order_info['lang'])) {
5277 if ($lang->getTag() != $order_info['lang']) {
5278 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5279 if (VBOPlatformDetection::isJoomla()) {
5280 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5281 }
5282 }
5283 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5284 // force the translation to start because contents should be translated
5285 $vbo_tn::$force_tolang = $order_info['lang'];
5286 }
5287 }
5288
5289 // values for replacements
5290 $company_name = self::getFrontTitle();
5291 $currencyname = self::getCurrencyName();
5292 $sitelogo = self::getSiteLogo();
5293 $footermess = self::getFooterOrdMail($vbo_tn);
5294 $dateformat = self::getDateFormat();
5295 $datesep = self::getDateSeparator();
5296 if ($dateformat == "%d/%m/%Y") {
5297 $df = 'd/m/Y';
5298 } elseif ($dateformat == "%m/%d/%Y") {
5299 $df = 'm/d/Y';
5300 } else {
5301 $df = 'Y/m/d';
5302 }
5303 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5304 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5305 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5306 $customer_info = nl2br($order_info['custdata']);
5307 $company_logo = '';
5308 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5309 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5310 }
5311 if ($order_info['status'] == 'cancelled') {
5312 $confirmnumber = '';
5313 $status_str = JText::translate('VBCANCELLED');
5314 } elseif ($order_info['status'] == 'standby') {
5315 $confirmnumber = '';
5316 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5317 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5318 // this is an inquiry reservation
5319 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5320 }
5321 } else {
5322 $confirmnumber = $order_info['confirmnumber'];
5323 $status_str = JText::translate('VBCOMPLETED');
5324 }
5325 // booking total amount
5326 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5327 // booking link
5328 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5329 if (is_null($link)) {
5330 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5331 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5332 }
5333
5334 // raw HTML content
5335 $parsed = $tmpl;
5336
5337 /**
5338 * Trigger event to allow third-party plugins to manipulate the template string.
5339 *
5340 * @since 1.16.10 (J) - 1.6.10 (WP)
5341 */
5342 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5343
5344 /**
5345 * Parse all conditional text rules.
5346 *
5347 * @since 1.14 (J) - 1.4.0 (WP)
5348 */
5349 self::getConditionalRulesInstance()
5350 ->set(array('booking', 'rooms'), array($order_info, $rooms))
5351 ->parseTokens($parsed);
5352
5353 // special tokens (tags) replacement
5354 $parsed = str_replace("{logo}", $company_logo, $parsed);
5355 $parsed = str_replace("{company_name}", $company_name, $parsed);
5356 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5357 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5358 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5359 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5360 $parsed = str_replace("{order_status}", $status_str, $parsed);
5361 $parsed = str_replace("{order_date}", $create_date, $parsed);
5362
5363 // customer record
5364 $cpin = self::getCPinInstance();
5365 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5366 $customer_name = '';
5367 $customer_pin = '';
5368 if ($customer_record) {
5369 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5370 $customer_pin = $customer_record['pin'];
5371 }
5372 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5373 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5374
5375 // PIN Code
5376 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5377 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5378 if (!empty($customer_pin)) {
5379 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5380 }
5381 }
5382
5383 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5384
5385 // Confirmation Number
5386 if ($confirmnumber) {
5387 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5388 } else {
5389 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5390 }
5391 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5392 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5393
5394 $roomsnum = count($rooms);
5395 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5396 $roomstr = "";
5397 $tot_adults = 0;
5398 $tot_children = 0;
5399 $tot_guests = 0;
5400
5401 // Rooms Distinctive Features
5402 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5403
5404 foreach ($rooms as $num => $r) {
5405 // guests
5406 $tot_adults += (int) $r['adults'];
5407 $tot_children += (int) $r['children'];
5408 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5409
5410 // room info
5411 $roomstr .= "<strong>".$r['name']."</strong> ".$r['adults']." ".($r['adults'] > 1 ? JText::translate('VBMAILADULTS') : JText::translate('VBMAILADULT')).($r['children'] > 0 ? ", ".$r['children']." ".($r['children'] > 1 ? JText::translate('VBMAILCHILDREN') : JText::translate('VBMAILCHILD')) : "")."<br/>";
5412 // Rooms Distinctive Features
5413 if (is_array($matches[1] ?? []) && $matches[1]) {
5414 $distinctive_features = array();
5415 $rparams = (array)json_decode($r['params'], true);
5416 if (array_key_exists('features', $rparams) && count($rparams['features']) > 0 && array_key_exists('roomindex', $r) && !empty($r['roomindex']) && array_key_exists($r['roomindex'], $rparams['features'])) {
5417 $distinctive_features = $rparams['features'][$r['roomindex']];
5418 }
5419 $docheck = (count($distinctive_features) > 0);
5420 foreach ($matches[1] as $reqf) {
5421 $feature_found = false;
5422 if ($docheck) {
5423 foreach ($distinctive_features as $dfk => $dfv) {
5424 if (stripos($dfk, $reqf) !== false) {
5425 $feature_found = $dfk;
5426 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5427 break;
5428 }
5429 }
5430 }
5431 }
5432 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5433 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5434 }
5435 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5436 }
5437 }
5438 }
5439
5440 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5441 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5442 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5443
5444 // custom fields replace
5445 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5446 if (is_array($cmatches[1]) && count($cmatches[1])) {
5447 $cfids = array();
5448 foreach ($cmatches[1] as $cfid ) {
5449 $cfids[] = $cfid;
5450 }
5451 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5452 $dbo->setQuery($q);
5453 $cfields = $dbo->loadAssocList();
5454 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5455 $cfmap = array();
5456 foreach ($cfields as $cf) {
5457 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5458 }
5459 $cfmapreplace = array();
5460 $partsreceived = explode("\n", $order_info['custdata']);
5461 if (count($partsreceived) > 0) {
5462 foreach ($partsreceived as $pst) {
5463 if (!empty($pst)) {
5464 $tmpdata = explode(":", $pst);
5465 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5466 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5467 }
5468 }
5469 }
5470 }
5471 foreach ($cmatches[1] as $cfid ) {
5472 if (array_key_exists($cfid, $cfmapreplace)) {
5473 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5474 } else {
5475 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5476 }
5477 }
5478 }
5479
5480 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5481 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5482 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5483
5484 // order details
5485 $orderdetails = "";
5486 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5487 foreach ($rooms as $num => $r) {
5488 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5489 $use_room_ind = $num - $room_loop_ind;
5490 $split_stay_str = '';
5491 $split_stay_info = [];
5492 if ($order_info['split_stay'] && !empty($room_stay_dates) && isset($room_stay_dates[$use_room_ind]) && $room_stay_dates[$use_room_ind]['idroom'] == $room_id) {
5493 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5494 $split_stay_info[] = date(str_replace("/", $datesep, $df), $room_stay_dates[$use_room_ind]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$use_room_ind]['checkout']);
5495 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5496 }
5497
5498 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5499 $faredets = explode(":", $expdet[0]);
5500 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5501 if (!empty($expdet[1])) {
5502 $attrfaredets = explode(":", $expdet[1]);
5503 if (strlen($attrfaredets[1]) > 0) {
5504 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5505 }
5506 }
5507 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5508 $orderdetails .= '<div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($fareprice).'</span></div></div>';
5509 // options
5510 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5511 foreach ($options[$num] as $oo) {
5512 $expopts = explode("\n", $oo);
5513 foreach ($expopts as $optinfo) {
5514 if (!empty($optinfo)) {
5515 $splitopt = explode(":", $optinfo);
5516 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5517 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($optprice).'</span></div></div>';
5518 }
5519 }
5520 }
5521 }
5522 //
5523 if ($roomsnum > 1 && $num < $roomsnum) {
5524 $orderdetails .= '<br/>';
5525 }
5526 }
5527
5528 // coupon
5529 if (!empty($order_info['coupon'])) {
5530 $expcoupon = explode(";", $order_info['coupon']);
5531 $orderdetails .= '<br/><div class="discount"><span>'.JText::translate('VBCOUPON').' '.$expcoupon[2].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($expcoupon[1]).'</span></div></div>';
5532 }
5533
5534 // discount and payment method
5535 $payment_info = [];
5536 $payment_name = '';
5537 if (!empty($order_info['idpayment'])) {
5538 $exppay = explode('=', $order_info['idpayment']);
5539 $payment = self::getPayment($exppay[0], $vbo_tn);
5540 if ((array) $payment) {
5541 $payment_name = $payment['name'];
5542 }
5543 }
5544
5545 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5546
5547 if ($order_info['status'] != 'cancelled') {
5548 if ($payment_info) {
5549 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5550 // Discount (not charge)
5551 if ($payment_info['val_pcent'] == 1) {
5552 // fixed value
5553 $total -= $payment_info['charge'];
5554 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($payment_info['charge']).'</span></div></div>';
5555 } else {
5556 // percent value
5557 $percent_disc = $total * $payment_info['charge'] / 100;
5558 $total -= $percent_disc;
5559 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($percent_disc).'</span></div></div>';
5560 }
5561 }
5562 }
5563 }
5564
5565 // booking details string
5566 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5567
5568 // additional information
5569 $parsed = str_replace("{order_total}", $currencyname.' '.self::numberFormat($total), $parsed);
5570 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5571 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5572 $parsed = str_replace("{booking_link}", $link, $parsed);
5573
5574 // deposit
5575 $deposit_str = '';
5576 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5577 $percentdeposit = self::getAccPerCent();
5578 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5579 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5580 if (self::getTypeDeposit() == "fixed") {
5581 $deposit_amount = $percentdeposit;
5582 } else {
5583 $deposit_amount = $total * $percentdeposit / 100;
5584 }
5585 if ($deposit_amount > 0) {
5586 $deposit_str = '<div class="deposit"><span>'.JText::translate('VBLEAVEDEPOSIT').'</span><div class="service-amount" style="float: right;"><strong>'.$currencyname.' '.self::numberFormat($deposit_amount).'</strong></div></div>';
5587 }
5588 }
5589 }
5590 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5591 //
5592 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5593 $totpaid_str = '';
5594 if ($order_info['refund'] > 0) {
5595 $totpaid_str .= '<div class="amountpaid amountrefunded"><span>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</span><div class="service-amount" style="float: right;"><strong>' . $currencyname . ' ' . self::numberFormat($order_info['refund']) . '</strong></div></div>';
5596 }
5597 if ($order_info['status'] != 'cancelled') {
5598 $tot_paid = $order_info['totpaid'];
5599 $diff_topay = (float)$total - (float)$tot_paid;
5600 if ((float)$tot_paid > 0) {
5601 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBAMOUNTPAID').'</span><div class="service-amount" style="float: right;"><strong>'.$currencyname.' '.self::numberFormat($tot_paid).'</strong></div></div>';
5602 // only in case the remaining balance is greater than 1 to avoid commissions issues
5603 if ($diff_topay > 1) {
5604 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBTOTALREMAINING').'</span><div class="service-amount" style="float: right;"><strong>'.$currencyname.' '.self::numberFormat($diff_topay).'</strong></div></div>';
5605 }
5606 }
5607 }
5608 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5609 $totpaid_str .= '<div class="amountpaid amountcancfee"><span>' . JText::translate('VBO_CANC_FEE') . '</span><div class="service-amount" style="float: right;"><strong>' . $currencyname . ' ' . self::numberFormat($order_info['canc_fee']) . '</strong></div></div>';
5610 }
5611 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
5612
5613 /**
5614 * Language direction (LTR or RTL).
5615 *
5616 * @since 1.16.4 (J) - 1.6.4 (WP)
5617 */
5618 $lang_direction = 'ltr';
5619 if (!empty($order_info['lang'])) {
5620 // attempt to define the language direction and replace the special tag
5621 try {
5622 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
5623 } catch (Throwable $e) {
5624 // do nothing, default to ltr
5625 $lang_direction = 'ltr';
5626 }
5627 // static map for Arabic and Hebrew
5628 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
5629 }
5630 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
5631 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
5632 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
5633
5634 /**
5635 * Replace static floating styles depending on language direction (LTR or RTL).
5636 * For BC with older email template sources, we need to keep the inline style
5637 * for the floating of certain elements, which looks good only on LTR.
5638 *
5639 * @since 1.16.4 (J) - 1.6.4 (WP)
5640 */
5641 if ($lang_direction === 'rtl') {
5642 // get rid of inline static style declaration because the template uses CSS classes
5643 $parsed = str_replace("float: right;", '', $parsed);
5644 }
5645
5646 return $parsed;
5647 }
5648
5649 /**
5650 * New method for sending booking email messages
5651 * to the guest or to the administrator(s).
5652 *
5653 * @param int $bid the booking ID.
5654 * @param array $for guest, admin or a custom email address.
5655 * @param bool $send whether to send or return the HTML message.
5656 * @param bool $no_config if true, no configuration settings will be used for the status.
5657 * @param string $type optional type to indicate a booking modification.
5658 *
5659 * @return mixed True or False depending on the result or HTML string for the preview.
5660 *
5661 * @since 1.13 (J) - 1.3 (WP)
5662 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
5663 * the reservation status is pending and against the configuration.
5664 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
5665 */
5666 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
5667 {
5668 $dbo = JFactory::getDbo();
5669 $app = JFactory::getApplication();
5670
5671 $vbo_tn = self::getTranslator();
5672 $av_helper = self::getAvailabilityInstance();
5673
5674 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5675 $dbo->setQuery($q);
5676 $booking = $dbo->loadAssoc();
5677 if (!$booking) {
5678 return false;
5679 }
5680
5681 $result = false;
5682
5683 // check if the language in use is the same as the one used during the checkout
5684 $lang = JFactory::getLanguage();
5685 if (!empty($booking['lang'])) {
5686 if ($lang->getTag() != $booking['lang']) {
5687 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
5688 if (VBOPlatformDetection::isJoomla()) {
5689 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
5690 }
5691 }
5692 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
5693 // force the translation to start because contents should be translated
5694 $vbo_tn::$force_tolang = $booking['lang'];
5695 }
5696 }
5697
5698 // load rooms booked
5699 $q = "SELECT `or`.*,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`units`,`r`.`fromadult`,`r`.`toadult`,`r`.`params` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=" . $booking['id'] . " AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
5700 $dbo->setQuery($q);
5701 $ordersrooms = $dbo->loadAssocList();
5702 if (!$ordersrooms) {
5703 return false;
5704 }
5705 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
5706
5707 /**
5708 * Split stay reservation.
5709 *
5710 * @since 1.16.0 (J) - 1.6.0 (WP)
5711 */
5712 $room_stay_dates = [];
5713 if ($booking['split_stay']) {
5714 if ($booking['status'] == 'confirmed') {
5715 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
5716 } else {
5717 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
5718 }
5719 // immediately count the number of nights of stay for each split room
5720 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5721 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5722 // overwrite values for compatibility with non-confirmed bookings
5723 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5724 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5725 }
5726 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5727 // overwrite the whole array
5728 $room_stay_dates[$sps_r_k] = $sps_r_v;
5729 }
5730 }
5731
5732 $rooms = array();
5733 $tars = array();
5734 $is_package = !empty($booking['pkg']) ? true : false;
5735 $ftitle = self::getFrontTitle();
5736 $currencyname = self::getCurrencyName();
5737 foreach ($ordersrooms as $kor => $or) {
5738 $num = $kor + 1;
5739 $rooms[$num] = $or;
5740 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5741 // package or custom cost set from the back-end
5742 continue;
5743 }
5744
5745 // determine the days to consider for the count of the availability
5746 $room_nights = $booking['days'];
5747 $room_checkin = $booking['checkin'];
5748 $room_checkout = $booking['checkout'];
5749 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5750 $room_nights = $room_stay_dates[$kor]['nights'];
5751 $room_checkin = $room_stay_dates[$kor]['checkin'];
5752 $room_checkout = $room_stay_dates[$kor]['checkout'];
5753 }
5754
5755 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
5756 $dbo->setQuery($q);
5757 $tar = $dbo->loadAssocList();
5758 if (!$tar) {
5759 // tariff not found
5760 if (self::isAdmin()) {
5761 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
5762 }
5763 continue;
5764 }
5765
5766 // apply seasonal rates
5767 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
5768
5769 // different usage
5770 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
5771 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
5772 // Occupancy Override
5773 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
5774 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
5775 //
5776 if (is_array($diffusageprice)) {
5777 // set a charge or discount to the price(s) for the different usage of the room
5778 foreach ($tar as $kpr => $vpr) {
5779 $tar[$kpr]['diffusage'] = $or['adults'];
5780 if ($diffusageprice['chdisc'] == 1) {
5781 // charge
5782 if ($diffusageprice['valpcent'] == 1) {
5783 // fixed value
5784 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5785 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5786 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
5787 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5788 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
5789 } else {
5790 // percentage value
5791 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5792 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
5793 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
5794 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5795 $tar[$kpr]['cost'] = $aduseval;
5796 }
5797 } else {
5798 // discount
5799 if ($diffusageprice['valpcent'] == 1) {
5800 // fixed value
5801 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5802 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5803 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
5804 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5805 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
5806 } else {
5807 // percentage value
5808 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5809 $aduseval = $diffusageprice['pernight'] == 1 ? round($vpr['cost'] - ((($vpr['cost'] / $tar[$kpr]['days']) * $diffusageprice['value'] / 100) * $tar[$kpr]['days']), 2) : round(($vpr['cost'] * (100 - $diffusageprice['value']) / 100), 2);
5810 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
5811 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5812 $tar[$kpr]['cost'] = $aduseval;
5813 }
5814 }
5815 }
5816 }
5817 }
5818 //
5819 $tars[$num] = $tar[0];
5820 }
5821
5822 $secdiff = $booking['checkout'] - $booking['checkin'];
5823 $daysdiff = $secdiff / 86400;
5824 if (is_int($daysdiff)) {
5825 if ($daysdiff < 1) {
5826 $daysdiff = 1;
5827 }
5828 } else {
5829 if ($daysdiff < 1) {
5830 $daysdiff = 1;
5831 } else {
5832 $sum = floor($daysdiff) * 86400;
5833 $newdiff = $secdiff - $sum;
5834 $maxhmore = self::getHoursMoreRb() * 3600;
5835 if ($maxhmore >= $newdiff) {
5836 $daysdiff = floor($daysdiff);
5837 } else {
5838 $daysdiff = ceil($daysdiff);
5839 }
5840 }
5841 }
5842
5843 $isdue = 0;
5844 $pricestr = array();
5845 $optstr = array();
5846 foreach ($ordersrooms as $kor => $or) {
5847 $num = $kor + 1;
5848
5849 // determine the days to consider for the count of the availability
5850 $room_nights = $booking['days'];
5851 $room_checkin = $booking['checkin'];
5852 $room_checkout = $booking['checkout'];
5853 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5854 $room_nights = $room_stay_dates[$kor]['nights'];
5855 $room_checkin = $room_stay_dates[$kor]['checkin'];
5856 $room_checkout = $room_stay_dates[$kor]['checkout'];
5857 }
5858
5859 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5860 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
5861 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
5862 $isdue += $calctar;
5863 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
5864 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
5865 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5866 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
5867 $tars[$num]['calctar'] = $calctar;
5868 $isdue += $calctar;
5869 $pricestr[$num] = self::getPriceName($tars[$num]['idprice'], $vbo_tn) . ": " . $calctar . " " . $currencyname . (!empty($tars[$num]['attrdata']) ? "\n" . self::getPriceAttr($tars[$num]['idprice'], $vbo_tn) . ": " . $tars[$num]['attrdata'] : "");
5870 }
5871
5872 if (!empty($or['optionals'])) {
5873 $stepo = explode(";", $or['optionals']);
5874 foreach ($stepo as $roptkey => $oo) {
5875 if (empty($oo)) {
5876 continue;
5877 }
5878 $stept = explode(":", $oo);
5879 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
5880 $dbo->setQuery($q);
5881 $actopt = $dbo->loadAssocList();
5882 if ($actopt) {
5883 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
5884 $chvar = '';
5885 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
5886 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
5887 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
5888 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
5889 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
5890 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
5891 $agestept = explode('-', $stept[1]);
5892 $stept[1] = $agestept[0];
5893 $chvar = $agestept[1];
5894 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
5895 // percentage value of the adults tariff
5896 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5897 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5898 } else {
5899 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5900 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5901 }
5902 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
5903 // VBO 1.10 - percentage value of room base cost
5904 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5905 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5906 } else {
5907 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
5908 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5909 }
5910 }
5911 $actopt[0]['chageintv'] = $chvar;
5912 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
5913 $actopt[0]['quan'] = $stept[1];
5914 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
5915 } else {
5916 $actopt[0]['quan'] = $stept[1];
5917 // VBO 1.11 - options percentage cost of the room total fee
5918 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5919 $deftar_basecosts = $or['cust_cost'];
5920 } else {
5921 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5922 }
5923 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
5924 //
5925 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
5926 }
5927 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
5928 $realcost = $actopt[0]['maxprice'];
5929 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
5930 $realcost = $actopt[0]['maxprice'] * $stept[1];
5931 }
5932 }
5933 if ($actopt[0]['perperson'] == 1) {
5934 $realcost = $realcost * $or['adults'];
5935 }
5936 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
5937 $isdue += $tmpopr;
5938 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
5939 }
5940 }
5941 }
5942
5943 // custom extra costs
5944 if (!empty($or['extracosts'])) {
5945 $cur_extra_costs = json_decode($or['extracosts'], true);
5946 foreach ($cur_extra_costs as $eck => $ecv) {
5947 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
5948 $isdue += $ecplustax;
5949 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
5950 }
5951 }
5952 }
5953
5954 // force the original total amount if rates have changed
5955 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
5956 $isdue = $booking['total'];
5957 }
5958
5959 // mail subject
5960 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
5961 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
5962
5963 // inject the recipient of the message for the template
5964 $booking['for'] = $for;
5965
5966 // load template file that will get $booking as variable
5967 $tmpl = self::loadEmailTemplate($booking);
5968
5969 // parse email template
5970 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
5971 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
5972
5973 if ($send !== true) {
5974 // return the content of the email message parsed
5975 return $hmess;
5976 }
5977
5978 // when the message can be sent
5979 $sendwhen = self::getSendEmailWhen();
5980
5981 if ($no_config) {
5982 // force the configuration setting to send the message no matter what's the booking status
5983 $sendwhen = 1;
5984 }
5985
5986 // send the message
5987 foreach ($for as $who) {
5988 $use_subject = $subject;
5989 $recipients = array();
5990 $attachments = self::addEmailAttachment(null);
5991 $attach_ical = false;
5992 $force_replyto = null;
5993 if (strpos($who, '@') !== false) {
5994 // send email to custom email address
5995 array_push($recipients, trim($who));
5996 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
5997 // send email to the customer
5998 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5999 continue;
6000 }
6001 array_push($recipients, $booking['custmail']);
6002 /**
6003 * Check whether an iCal should be attached for the customer.
6004 *
6005 * @since 1.12.0
6006 */
6007 $attach_ical = self::getEmailIcal('customer', $booking);
6008 } elseif (stripos($who, 'admin') !== false) {
6009 // send email to the administrator(s)
6010 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6011 continue;
6012 }
6013 $use_subject = $subject . ' #' . $booking['id'];
6014 if (!strcasecmp($type, 'modified')) {
6015 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6016 }
6017 $adminemail = self::getAdminMail();
6018 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6019 if (empty($adminemail) && empty($extra_admin_recipients)) {
6020 // Prevent Joomla Exceptions that would stop the script execution
6021 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6022 continue;
6023 }
6024 if (strpos($adminemail, ',') !== false) {
6025 // multiple addresses
6026 $adminemails = explode(',', $adminemail);
6027 foreach ($adminemails as $am) {
6028 if (strpos($am, '@') !== false) {
6029 array_push($recipients, trim($am));
6030 }
6031 }
6032 } else {
6033 // single address
6034 array_push($recipients, trim($adminemail));
6035 }
6036
6037 // merge extra recipients
6038 $recipients = array_merge($recipients, $extra_admin_recipients);
6039
6040 // admin should reply to the customer
6041 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6042
6043 /**
6044 * Check whether an iCal should be attached for the admin.
6045 *
6046 * @since 1.2.0
6047 */
6048 $attach_ical = self::getEmailIcal('admin', array(
6049 'ts' => $booking['ts'],
6050 'custdata' => $booking['custdata'],
6051 'checkin' => $booking['checkin'],
6052 'checkout' => $booking['checkout'],
6053 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6054 ));
6055 }
6056
6057 // send the message, recipients should always be an array to support multiple admin addresses
6058
6059 // get sender e-mail
6060 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6061
6062 // init mail data
6063 $mail = new VBOMailWrapper([
6064 'sender' => [$adsendermail, $ftitle],
6065 'recipient' => $recipients,
6066 'bcc' => self::addAdminEmailRecipient(null, true),
6067 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6068 'subject' => $use_subject,
6069 'content' => $hmess,
6070 'attachments' => $attachments,
6071 ]);
6072
6073 if ($attach_ical !== false && $booking['status'] != 'standby') {
6074 // attach iCal file
6075 $mail->addAttachment($attach_ical);
6076 }
6077
6078 /**
6079 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6080 *
6081 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6082 *
6083 * @since 1.15.4 (J) - 1.5.10 (WP)
6084 */
6085 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6086
6087 // send e-mail
6088 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6089
6090 // unlink iCal file
6091 if ($attach_ical !== false) {
6092 @unlink($attach_ical);
6093 }
6094 }
6095
6096 return $result;
6097 }
6098
6099 /**
6100 * This method allows to add one or more recipient email
6101 * addresses for the next queue of email sending for the admin.
6102 * This method can be used in the template file for the customer
6103 * email to register an additional email address, maybe when a
6104 * specific room-type is booked.
6105 * The methods sending the email messages are supposed to call this
6106 * method by passing no arguments to obtain the extra addresses set.
6107 *
6108 * @param mixed $email null, string or array of email address(es).
6109 * @param bool $bcc if true, addresses will be used as bcc.
6110 * @param bool $reset if true, the queues will be emptied.
6111 *
6112 * @return array the current extra recipients or bcc addresses set.
6113 *
6114 * @since 1.13 (J) - 1.3.0 (WP)
6115 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6116 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6117 */
6118 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6119 {
6120 static $extra_recipients = [];
6121 static $extra_bcc = [];
6122
6123 if ($reset) {
6124 $extra_recipients = [];
6125 $extra_bcc = [];
6126 }
6127
6128 if (!empty($email)) {
6129 if (is_scalar($email)) {
6130 if ($bcc) {
6131 array_push($extra_bcc, $email);
6132 } else {
6133 array_push($extra_recipients, $email);
6134 }
6135 } else {
6136 if ($bcc) {
6137 $extra_bcc = array_merge($extra_bcc, $email);
6138 } else {
6139 $extra_recipients = array_merge($extra_recipients, $email);
6140 }
6141 }
6142 }
6143
6144 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6145 }
6146
6147 /**
6148 * This method serves to add one or more attachments to the
6149 * next queue of email sending for the admin.
6150 * The methods sending the email messages are supposed to call this
6151 * method by passing a null argument to obtain the attachments set.
6152 *
6153 * @param mixed $file null or string with path to file to attach.
6154 * @param bool $reset if true, the queue will be emptied.
6155 *
6156 * @return array the current attachments set.
6157 *
6158 * @since 1.14
6159 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6160 */
6161 public static function addEmailAttachment($file, $reset = false)
6162 {
6163 static $extra_attachments = [];
6164
6165 if ($reset) {
6166 $extra_attachments = [];
6167 }
6168
6169 if (!empty($file)) {
6170 if (is_scalar($file)) {
6171 array_push($extra_attachments, $file);
6172 } else {
6173 $extra_attachments = array_merge($extra_attachments, $file);
6174 }
6175 }
6176
6177 return array_unique($extra_attachments);
6178 }
6179
6180 /**
6181 * This method is called whenever some rooms get booked.
6182 * It checks whether the rooms involved have shared calendars.
6183 * If some are found, then also such rooms will be occupied.
6184 *
6185 * @param int $bid the booking ID.
6186 * @param array $roomids the list of rooms booked.
6187 * @param int $checkin checkin timestamp.
6188 * @param int $checkout checkout timestamp.
6189 *
6190 * @return boolean true if some other cals were occupied, false otherwise.
6191 *
6192 * @since 1.13 (J) - 1.3.0 (WP)
6193 *
6194 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6195 */
6196 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6197 {
6198 $dbo = JFactory::getDbo();
6199 $bid = (int)$bid;
6200
6201 // availability helper
6202 $av_helper = self::getAvailabilityInstance();
6203
6204 // check for split stay rooms reservation
6205 $known_split_stay = false;
6206 $is_split_stay = 0;
6207
6208 if (empty($checkin) || empty($checkout)) {
6209 // get checkin and checkout timestamps from booking
6210 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6211 $dbo->setQuery($q);
6212 $dbo->execute();
6213 if (!$dbo->getNumRows()) {
6214 // booking not found
6215 return false;
6216 }
6217 $bdata = $dbo->loadAssoc();
6218 $checkin = $bdata['checkin'];
6219 $checkout = $bdata['checkout'];
6220 $is_split_stay = $bdata['split_stay'];
6221 // turn flag on
6222 $known_split_stay = true;
6223 }
6224
6225 if (!$known_split_stay) {
6226 // we need to query the db to access this information
6227 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6228 $dbo->setQuery($q, 0, 1);
6229 $dbo->execute();
6230 if ($dbo->getNumRows()) {
6231 $is_split_stay = (int)$dbo->loadResult();
6232 }
6233 // turn flag on
6234 $known_split_stay = true;
6235 }
6236
6237 // check split stay room records
6238 $room_stay_dates = [];
6239 if ($is_split_stay) {
6240 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6241 // immediately count the number of nights of stay for each split room
6242 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6243 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6244 // overwrite values for compatibility with non-confirmed bookings
6245 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6246 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6247 }
6248 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6249 // overwrite the whole array
6250 $room_stay_dates[$sps_r_k] = $sps_r_v;
6251 }
6252 }
6253
6254 // get the rooms booked
6255 if (!count($roomids)) {
6256 // get the IDs of all rooms booked
6257 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6258 $dbo->setQuery($q);
6259 $orr = $dbo->loadAssocList();
6260 if ($orr) {
6261 foreach ($orr as $or) {
6262 array_push($roomids, $or['idroom']);
6263 }
6264 }
6265 }
6266 if (!count($roomids) || empty($bid)) {
6267 // unable to proceed
6268 return false;
6269 }
6270 $roomids = array_unique($roomids);
6271
6272 // build room stay dates map in case of split stay
6273 $room_split_stay_map = [];
6274 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6275 // we have unique room IDs in a split stay reservation for more than one room
6276 foreach ($room_stay_dates as $room_stay_date) {
6277 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6278 }
6279 }
6280
6281 // get rooms involved
6282 $involved = [];
6283 $involved_stay_map = [];
6284 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6285 $dbo->setQuery($q);
6286 $rooms_found = $dbo->loadAssocList();
6287 if (!$rooms_found) {
6288 // no rooms involved that need their calendars updated
6289 return false;
6290 }
6291 foreach ($rooms_found as $rf) {
6292 if (!in_array($rf['mainroom'], $roomids)) {
6293 // push room ID
6294 array_push($involved, $rf['mainroom']);
6295 // check for specific stay dates in connected room
6296 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6297 // split stay information available
6298 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6299 }
6300 }
6301 if (!in_array($rf['childroom'], $roomids)) {
6302 // push room ID
6303 array_push($involved, $rf['childroom']);
6304 // check for specific stay dates in connected room
6305 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6306 // split stay information available
6307 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6308 }
6309 }
6310 }
6311 $involved = array_unique($involved);
6312 if (!count($involved)) {
6313 // no rooms involved
6314 return false;
6315 }
6316
6317 // turnover seconds
6318 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6319
6320 // occupy the calendars for the involved rooms found
6321 $bids_generated = [];
6322 foreach ($involved as $rid) {
6323 // determine the values to use
6324 $room_checkin = (int)$checkin;
6325 $room_checkout = (int)$checkout;
6326 $room_realback = ($checkout + $turnover_secs);
6327 if (isset($involved_stay_map[$rid])) {
6328 // split stay dates available
6329 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6330 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6331 $room_realback = ($room_checkout + $turnover_secs);
6332 }
6333
6334 // occupy record in room with shared calendar
6335 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6336 $dbo->setQuery($q);
6337 $dbo->execute();
6338 array_push($bids_generated, $dbo->insertid());
6339 }
6340
6341 // store busy relations created
6342 foreach ($bids_generated as $busyid) {
6343 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6344 $dbo->setQuery($q);
6345 $dbo->execute();
6346 }
6347
6348 return true;
6349 }
6350
6351 /**
6352 * This method is needed whenever a booking gets modified by
6353 * adding or removing rooms. This way we reset (remove) all
6354 * busy records that were previously stored due to a shared
6355 * calendar. The correct relations should then be re-created
6356 * by calling updateSharedCalendars().
6357 *
6358 * @param int $bid the booking ID.
6359 *
6360 * @return boolean true if some records were cleaned.
6361 *
6362 * @see updateSharedCalendars() should be called after.
6363 *
6364 * @since 1.3.0
6365 */
6366 public static function cleanSharedCalendarsBusy($bid) {
6367 $dbo = JFactory::getDbo();
6368 $bid = (int)$bid;
6369 // get all the occupied records due to shared calendars for this booking
6370 $q = "SELECT `b`.`id`, `b`.`idroom`, `b`.`sharedcal`, `ob`.`idorder`, `ob`.`idbusy` FROM `#__vikbooking_busy` AS `b` LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id` WHERE `ob`.`idorder`={$bid} AND `b`.`sharedcal`=1;";
6371 $dbo->setQuery($q);
6372 $allbusy = $dbo->loadAssocList();
6373 if (!$allbusy) {
6374 return false;
6375 }
6376
6377 $busy_ids = array();
6378 foreach ($allbusy as $b) {
6379 // push busy ID to be removed later
6380 array_push($busy_ids, $b['id']);
6381 // delete the current busy ID-booking ID relation for this shared calendar
6382 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6383 $dbo->setQuery($q);
6384 $dbo->execute();
6385 }
6386 if (count($busy_ids)) {
6387 // delete all busy records due to shared calendars
6388 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6389 $dbo->setQuery($q);
6390 $dbo->execute();
6391 }
6392
6393 return true;
6394 }
6395
6396 public static function sendJutility() {
6397 //deprecated in VBO 1.10
6398 $dbo = JFactory::getDbo();
6399 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6400 $dbo->setQuery($q);
6401 $dbo->execute();
6402 $s = $dbo->loadAssocList();
6403 return (intval($s[0]['setting']) == 1 ? true : false);
6404 }
6405
6406 public static function getCategoryName($idcat, $vbo_tn = null) {
6407 $dbo = JFactory::getDbo();
6408 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6409 $dbo->setQuery($q);
6410 $dbo->execute();
6411 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6412 if (is_object($vbo_tn) && count($p) > 0) {
6413 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6414 }
6415 return count($p) > 0 ? $p[0]['name'] : '';
6416 }
6417
6418 public static function loadAdultsDiff($idroom, $adults)
6419 {
6420 static $obp_map = [];
6421
6422 $map_sign = "{$idroom}:{$adults}";
6423
6424 if (array_key_exists($map_sign, $obp_map)) {
6425 return $obp_map[$map_sign];
6426 }
6427
6428 $dbo = JFactory::getDbo();
6429
6430 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6431 $dbo->setQuery($q, 0, 1);
6432 $obp_diff = $dbo->loadAssoc();
6433 if ($obp_diff) {
6434 // cache value and return it
6435 $obp_map[$map_sign] = $obp_diff;
6436
6437 return $obp_diff;
6438 }
6439
6440 // cache value and return it
6441 $obp_map[$map_sign] = null;
6442
6443 return null;
6444 }
6445
6446 public static function loadRoomAdultsDiff($idroom)
6447 {
6448 $dbo = JFactory::getDbo();
6449
6450 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6451 $dbo->setQuery($q);
6452 $diff = $dbo->loadAssocList();
6453 if ($diff) {
6454 $roomdiff = [];
6455 foreach ($diff as $v) {
6456 $roomdiff[$v['adults']] = $v;
6457 }
6458
6459 return $roomdiff;
6460 }
6461
6462 return [];
6463 }
6464
6465 public static function occupancyOverrideExists($tar, $adults) {
6466 foreach ($tar as $k => $v) {
6467 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6468 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6469 return $v['occupancy_ovr'][$adults];
6470 }
6471 }
6472 }
6473 return false;
6474 }
6475
6476 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6477 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6478 $charges = array();
6479 if (!($children > 0) || !(count($ages) > 0)) {
6480 return $charges;
6481 }
6482 $dbo = JFactory::getDbo();
6483 $id_options = array();
6484 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6485 $dbo->setQuery($q);
6486 $dbo->execute();
6487 if ($dbo->getNumRows() > 0) {
6488 $assocs = $dbo->loadAssocList();
6489 foreach ($assocs as $opts) {
6490 if (!empty($opts['idopt'])) {
6491 $r_ido = explode(';', rtrim($opts['idopt']));
6492 foreach ($r_ido as $ido) {
6493 if (!empty($ido) && !in_array($ido, $id_options)) {
6494 $id_options[] = $ido;
6495 }
6496 }
6497 }
6498 }
6499 }
6500 if (count($id_options) > 0) {
6501 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $id_options).") AND `ifchildren`=1 AND (LENGTH(`ageintervals`) > 0 OR `ageintervals` IS NOT NULL) LIMIT 1;";
6502 $dbo->setQuery($q);
6503 $dbo->execute();
6504 if ($dbo->getNumRows() > 0) {
6505 $ageintervals = $dbo->loadAssocList();
6506 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6507 $age_range = array();
6508 foreach ($split_ages as $kg => $spage) {
6509 if (empty($spage)) {
6510 continue;
6511 }
6512 $parts = explode('_', $spage);
6513 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6514 $ind = count($age_range);
6515 $age_range[$ind]['from'] = intval($parts[0]);
6516 $age_range[$ind]['to'] = intval($parts[1]);
6517 //taxes are calculated later in VCM
6518 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6519 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6520 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6521 }
6522 }
6523 if (count($age_range) > 0) {
6524 $tot_charge = 0;
6525 $affected = array();
6526 $option_str = '';
6527 foreach ($ages as $age) {
6528 if (strlen($age) == 0) {
6529 continue;
6530 }
6531 foreach ($age_range as $range) {
6532 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6533 $tot_charge += $range['cost'];
6534 $affected[] = $age;
6535 $option_str .= $range['option_str'].';';
6536 break;
6537 }
6538 }
6539 }
6540 if ($tot_charge > 0) {
6541 $charges['total'] = $tot_charge;
6542 $charges['affected'] = $affected;
6543 $charges['options'] = $option_str;
6544 }
6545 }
6546 }
6547 }
6548
6549 return $charges;
6550 }
6551
6552 public static function sortRoomPrices($arr) {
6553 $newarr = array();
6554 foreach ($arr as $k => $v) {
6555 $newarr[$k] = $v['cost'];
6556 }
6557 asort($newarr);
6558 $sorted = array();
6559 foreach ($newarr as $k => $v) {
6560 $sorted[$k] = $arr[$k];
6561 }
6562 return $sorted;
6563 }
6564
6565 public static function sortResults($arr) {
6566 $newarr = array();
6567 foreach ($arr as $k => $v) {
6568 $newarr[$k] = $v[0]['cost'];
6569 }
6570 asort($newarr);
6571 $sorted = array();
6572 foreach ($newarr as $k => $v) {
6573 $sorted[$k] = $arr[$k];
6574 }
6575 return $sorted;
6576 }
6577
6578 public static function sortMultipleResults($arr) {
6579 foreach ($arr as $k => $v) {
6580 $newarr = array();
6581 foreach ($v as $subk => $subv) {
6582 $newarr[$subk] = $subv[0]['cost'];
6583 }
6584 asort($newarr);
6585 $sorted = array();
6586 foreach ($newarr as $nk => $v) {
6587 $sorted[$nk] = $arr[$k][$nk];
6588 }
6589 $arr[$k] = $sorted;
6590 }
6591 return $arr;
6592 }
6593
6594 /**
6595 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6596 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
6597 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
6598 *
6599 * @param array $promos optional list of promotion IDs to register in the execution flow.
6600 *
6601 * @return array list of cached promotion IDs (if any).
6602 *
6603 * @since 1.16.4 (J) - 1.6.4 (WP)
6604 */
6605 public static function registerPromotionIds(array $promos = null)
6606 {
6607 static $flagged_promos = [];
6608
6609 if ($promos) {
6610 // set cached value
6611 $flagged_promos = $promos;
6612 }
6613
6614 return $flagged_promos;
6615 }
6616
6617 /**
6618 * Getter and setter for seasons cache allowed.
6619 *
6620 * @param bool $enabled true or false for setter, null for getter.
6621 *
6622 * @return bool|null
6623 *
6624 * @since 1.16.5 (J) - 1.6.5 (WP)
6625 */
6626 public static function setSeasonsCache($enabled = null)
6627 {
6628 static $seasons_cache = null;
6629
6630 if ($enabled !== null) {
6631 // set value
6632 $seasons_cache = (bool)$enabled;
6633 }
6634
6635 return $seasons_cache;
6636 }
6637
6638 /**
6639 * Fetches all season records affecting a range of date timestamps.
6640 * Useful to pre-cache season records in case of hundreds of thousands
6641 * of records, but it can use up several MBs of server's memory.
6642 *
6643 * @param int $from unix timestamp for start date.
6644 * @param int $to unix timestamp for end date.
6645 * @param array $rooms optional list of involved room IDs.
6646 *
6647 * @return array
6648 *
6649 * @since 1.16.10 (J) - 1.6.10 (WP)
6650 */
6651 public static function getDateSeasonRecords($from, $to, array $rooms = [])
6652 {
6653 $dbo = JFactory::getDbo();
6654
6655 $one = getdate($from);
6656
6657 // leap years
6658 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6659 $isleap = true;
6660 } else {
6661 $isleap = false;
6662 }
6663
6664 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6665 $tomidnightone = intval($one['hours']) * 3600;
6666 $tomidnightone += intval($one['minutes']) * 60;
6667 $sfrom = $from - $baseone - $tomidnightone;
6668 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6669 $two = getdate($to);
6670 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6671 $tomidnighttwo = intval($two['hours']) * 3600;
6672 $tomidnighttwo += intval($two['minutes']) * 60;
6673 $sto = $to - $basetwo - $tomidnighttwo;
6674
6675 // leap years, check what dates to manipulate
6676 if ($isleap) {
6677 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
6678 if ($one[0] > $leapts) {
6679 /**
6680 * Timestamp must be greater than the leap-day of Feb 29th.
6681 * It used to be checked for >= $leapts.
6682 *
6683 * @since July 3rd 2019
6684 */
6685 $sfrom -= 86400;
6686 }
6687 if ($two[0] > $leapts && $one['year'] == $two['year']) {
6688 // lower checkin date when in leap year but not for checkout
6689 $sto -= 86400;
6690 }
6691 }
6692
6693 // check for DST changes to adjust the query values to fetch
6694 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6695 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6696 // we are parsing the day when the DST changed (probably a Sunday)
6697 if (!date('I', $from)) {
6698 // DST was just turned off
6699 $sfrom -= 3600;
6700 }
6701 }
6702 }
6703
6704 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6705 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6706 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6707 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6708 ($sto > $sfrom ? "OR (`from` >= " . $sfrom . " AND `from` <= " . $sto . " AND `to` >= " . $sfrom . " AND `to` <= " . $sto . ")" : "OR (`from` >= " . $sfrom . " AND `from`>" . $sto . " AND `to`<" . $sfrom . " AND `to` <= " . $sto . " AND `from` > `to`)") .
6709 ($sto > $sfrom ? " OR (`from` <= " . $sfrom . " AND `from` <= " . $sto . " AND `to`<" . $sfrom . " AND `to`<" . $sto . " AND `from` > `to`) OR (`from`>" . $sfrom . " AND `from`>" . $sto . " AND `to` >= " . $sfrom . " AND `to` >= " . $sto . " AND `from` > `to`)" : " OR (`from` <= " . $sfrom . " AND `to` >=" . $sfrom . " AND `from` >= " . $sto . " AND `to` > " . $sto . " AND `from` < `to`)") .
6710 ($sto > $sfrom ? " OR (`from` >= " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` < " . $sfrom . " AND `to` >=" . $sto . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `from` < `to`)") .
6711 ($sto > $sfrom ? " OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >=" . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` > " . $sfrom . " AND `to` > " . $sto . " AND `from` < `to`) OR (`from` < " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` < `to`)") .
6712 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6713 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6714
6715 $dbo->setQuery($q);
6716 $seasons = $dbo->loadAssocList();
6717
6718 if ($rooms) {
6719 // filter records by affected room IDs
6720 $seasons = array_filter($seasons, function($s) use ($rooms) {
6721 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
6722 foreach ($rooms as $idroom) {
6723 if (in_array("-" . $idroom . "-", $allrooms)) {
6724 return true;
6725 }
6726 }
6727 return false;
6728 });
6729 }
6730
6731 return $seasons;
6732 }
6733
6734 /**
6735 * Applies the seasonal rates over a list of room rate records.
6736 *
6737 * @param array $arr list of room rate records.
6738 * @param int $from unix timestamp for start date.
6739 * @param int $to unix timestamp for end date.
6740 *
6741 * @return array list of manipulated room rate records.
6742 */
6743 public static function applySeasonalPrices(array $arr, $from, $to)
6744 {
6745 $dbo = JFactory::getDbo();
6746 $vbo_tn = self::getTranslator();
6747
6748 $roomschange = [];
6749 $one = getdate($from);
6750
6751 // leap years
6752 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6753 $isleap = true;
6754 } else {
6755 $isleap = false;
6756 }
6757
6758 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6759 $tomidnightone = intval($one['hours']) * 3600;
6760 $tomidnightone += intval($one['minutes']) * 60;
6761 $sfrom = $from - $baseone - $tomidnightone;
6762 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6763 $two = getdate($to);
6764 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6765 $tomidnighttwo = intval($two['hours']) * 3600;
6766 $tomidnighttwo += intval($two['minutes']) * 60;
6767 $sto = $to - $basetwo - $tomidnighttwo;
6768
6769 // leap years, check what dates to manipulate
6770 if ($isleap) {
6771 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
6772 if ($one[0] > $leapts) {
6773 /**
6774 * Timestamp must be greater than the leap-day of Feb 29th.
6775 * It used to be checked for >= $leapts.
6776 *
6777 * @since July 3rd 2019
6778 */
6779 $sfrom -= 86400;
6780 }
6781 if ($two[0] > $leapts && $one['year'] == $two['year']) {
6782 // lower checkin date when in leap year but not for checkout
6783 $sto -= 86400;
6784 }
6785 }
6786
6787 // check for DST changes to adjust the query values to fetch
6788 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6789 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6790 // we are parsing the day when the DST changed (probably a Sunday)
6791 if (!date('I', $from)) {
6792 // DST was just turned off
6793 $sfrom -= 3600;
6794 }
6795 }
6796 }
6797
6798 // count nights requested
6799 $booking_nights = 1;
6800 foreach ($arr as $k => $a) {
6801 if (!isset($a[0])) {
6802 continue;
6803 }
6804 if (isset($a[0]['booking_nights'])) {
6805 // this value may be set when displaying pricing calendars
6806 $booking_nights = $a[0]['booking_nights'];
6807 break;
6808 }
6809 if (isset($a[0]['days'])) {
6810 $booking_nights = $a[0]['days'];
6811 break;
6812 }
6813 }
6814
6815 /**
6816 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
6817 *
6818 * @since 1.16.4 (J) - 1.6.4 (WP)
6819 */
6820 $skip_promo_ids = self::registerPromotionIds();
6821
6822 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6823 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6824 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6825 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6826 ($sto > $sfrom ? "OR (`from` >= " . $sfrom . " AND `from` <= " . $sto . " AND `to` >= " . $sfrom . " AND `to` <= " . $sto . ")" : "OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` > `to`)") .
6827 ($sto > $sfrom ? " OR (`from` <= " . $sfrom . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`) OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` >= " . $sto . " AND `from` > `to`)" : " OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . " AND `from` >= " . $sto . " AND `to` > " . $sto . " AND `from` < `to`)") .
6828 ($sto > $sfrom ? " OR (`from` >= " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` < " . $sfrom . " AND `to` >= " . $sto . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `from` < `to`)") .
6829 ($sto > $sfrom ? " OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` > " . $sfrom . " AND `to` > " . $sto . " AND `from` < `to`) OR (`from` < " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` < `to`)") .
6830 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6831 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6832
6833 $dbo->setQuery($q);
6834 $seasons = $dbo->loadAssocList();
6835
6836 if ($seasons) {
6837 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
6838 $applyseasons = false;
6839 $mem = [];
6840 foreach ($arr as $k => $a) {
6841 $mem[$k]['daysused'] = 0;
6842 $mem[$k]['sum'] = [];
6843 /**
6844 * The keys below are all needed to apply the promotions on the room's final cost.
6845 *
6846 * @since 1.13.5
6847 */
6848 $mem[$k]['diffs'] = [];
6849 $mem[$k]['trans_keys'] = [];
6850 $mem[$k]['trans_factors'] = [];
6851 $mem[$k]['trans_affdays'] = [];
6852 }
6853 foreach ($seasons as $s) {
6854 // check if this is a promotion registered for skipping
6855 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
6856 continue;
6857 }
6858
6859 // Special Price tied to the year
6860 if (!empty($s['year']) && $s['year'] > 0) {
6861 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
6862 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
6863 //VBO 1.9 - tied to the year can be set for prev year (Dec 27 to Jan 3) and booking can be Jan 1 to Jan 3 - do not skip in this case
6864 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
6865 continue;
6866 }
6867 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
6868 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
6869 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
6870 if ($calc_ends < ($from - $tomidnightone)) {
6871 continue;
6872 }
6873 }
6874 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
6875 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
6876 if ($s['from'] > $s['to']) {
6877 continue;
6878 }
6879 if (($basetwo + $s['from'] + 86399) > $to) {
6880 /**
6881 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
6882 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
6883 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
6884 *
6885 * @since 1.14.3 (J) - 1.4.3 (WP)
6886 */
6887 continue;
6888 }
6889 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
6890 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
6891 /**
6892 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
6893 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
6894 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
6895 *
6896 * @since 1.14.3 (J) - 1.4.3 (WP)
6897 */
6898 continue;
6899 }
6900 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
6901 // season tied to the year 2017 accross 2018 and we are parsing dates at the beginning of 2017 due to beginning loop in 2016 (Rates Overview)
6902 if (($baseone + $s['from']) > $to) {
6903 continue;
6904 }
6905 }
6906 }
6907 //
6908 $allrooms = explode(",", $s['idrooms']);
6909 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
6910 $inits = $baseone + $s['from'];
6911 if ($s['from'] < $s['to']) {
6912 $ends = $basetwo + $s['to'];
6913 // check if the inits must be set to the year after
6914 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
6915 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
6916 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6917 $inits = $tmpbase + $s['from'];
6918 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6919 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
6920 $ends = $baseone + $s['to'];
6921 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6922 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
6923 $ends = $baseone + $s['to'];
6924 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
6925 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
6926 $inits = $basetwo + $s['from'];
6927 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
6928 // booking dates across two years for a season tied to a specific year
6929 if ($one['year'] == $s['year']) {
6930 $inits = $baseone + $s['from'];
6931 } else {
6932 $inits = $basetwo + $s['from'];
6933 }
6934 if ($two['year'] == $s['year']) {
6935 $ends = $basetwo + $s['to'];
6936 } else {
6937 $ends = $baseone + $s['to'];
6938 }
6939 }
6940 } else {
6941 // between 2 years
6942 if ($baseone < $basetwo) {
6943 // ex. 29/12/2012 - 14/01/2013
6944 $ends = $basetwo + $s['to'];
6945 } else {
6946 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
6947 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
6948 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6949 $ends = $tmpbase + $s['to'];
6950 } else {
6951 // ex. 03/01 - 09/01
6952 $ends = $basetwo + $s['to'];
6953 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
6954 $inits = $tmpbase + $s['from'];
6955 }
6956 }
6957 }
6958
6959 // leap years
6960 if ($isleap == true) {
6961 $infoseason = getdate($inits);
6962 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
6963 // leap years, check what dates to manipulate
6964 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
6965 /**
6966 * Timestamp must be greater than the leap-day of Feb 29th.
6967 * It used to be checked for >= $leapts.
6968 *
6969 * @since July 3rd 2019
6970 */
6971 $inits += 86400;
6972 if ($s['from'] < $s['to']) {
6973 // increase season end date only if still on the same leap year
6974 $ends += 86400;
6975 }
6976 }
6977 }
6978
6979 // promotions
6980 $promotion = [];
6981 if ($s['promo'] == 1) {
6982 $daysadv = (($inits - time()) / 86400);
6983 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
6984 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
6985 continue;
6986 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
6987 $secstocheckin = ($from - time());
6988 if ($s['promolastmin'] < $secstocheckin) {
6989 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
6990 continue;
6991 }
6992 }
6993 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
6994 /**
6995 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
6996 *
6997 * @since 1.13.5
6998 */
6999 continue;
7000 }
7001 $promotion['todaydaysadv'] = $daysadv;
7002 $promotion['promodaysadv'] = $s['promodaysadv'];
7003 $promotion['promotxt'] = $s['promotxt'];
7004 }
7005
7006 // occupancy override
7007 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7008
7009 // week days
7010 $filterwdays = !empty($s['wdays']) ? true : false;
7011 $wdays = $filterwdays ? explode(';', $s['wdays']) : '';
7012 if (is_array($wdays) && $wdays) {
7013 foreach ($wdays as $kw => $wd) {
7014 if (strlen($wd) == 0) {
7015 unset($wdays[$kw]);
7016 }
7017 }
7018 }
7019
7020 // checkin must be after the beginning of the season
7021 $checkininclok = true;
7022 if ($s['checkinincl'] == 1) {
7023 $checkininclok = false;
7024 if ($s['from'] < $s['to']) {
7025 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7026 $checkininclok = true;
7027 }
7028 } else {
7029 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7030 $checkininclok = true;
7031 }
7032 }
7033 }
7034 if ($checkininclok !== true) {
7035 continue;
7036 }
7037
7038 foreach ($arr as $k => $a) {
7039 // applied only to some types of price
7040 if ($allprices && !empty($allprices[0])) {
7041 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7042 continue;
7043 }
7044 }
7045 // applied only to some room types
7046 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7047 continue;
7048 }
7049
7050 // count affected nights of stay
7051 $affdays = 0;
7052 $season_fromdayts = $fromdayts;
7053 $is_dst = date('I', $season_fromdayts);
7054 for ($i = 0; $i < $a[0]['days']; $i++) {
7055 $todayts = $season_fromdayts + ($i * 86400);
7056 $is_now_dst = date('I', $todayts);
7057 if ($is_dst != $is_now_dst) {
7058 // Daylight Saving Time has changed, check how
7059 if ((bool)$is_dst === true) {
7060 $todayts += 3600;
7061 $season_fromdayts += 3600;
7062 } else {
7063 $todayts -= 3600;
7064 $season_fromdayts -= 3600;
7065 }
7066 $is_dst = $is_now_dst;
7067 }
7068 if ($todayts >= $inits && $todayts <= $ends) {
7069 // week days
7070 if ($filterwdays == true) {
7071 $checkwday = getdate($todayts);
7072 if (in_array($checkwday['wday'], $wdays)) {
7073 $affdays++;
7074 }
7075 } else {
7076 $affdays++;
7077 }
7078 //
7079 }
7080 }
7081 if (!($affdays > 0)) {
7082 // no nights affected
7083 continue;
7084 }
7085
7086 // apply the rule
7087 $applyseasons = true;
7088 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7089
7090 // modification factor object
7091 $factor = new stdClass;
7092
7093 // calculate new price progressively
7094 if (intval($s['val_pcent']) == 2) {
7095 // percentage value
7096 $factor->pcent = 1;
7097 $pctval = $s['diffcost'];
7098 if (strlen((string)$s['losoverride'])) {
7099 // values overrides
7100 $arrvaloverrides = [];
7101 $valovrparts = explode('_', $s['losoverride']);
7102 foreach ($valovrparts as $valovr) {
7103 if (!empty($valovr)) {
7104 $ovrinfo = explode(':', $valovr);
7105 if (strstr($ovrinfo[0], '-i') != false) {
7106 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7107 if ((int)$ovrinfo[0] < $a[0]['days']) {
7108 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7109 }
7110 }
7111 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7112 }
7113 }
7114 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7115 $pctval = $arrvaloverrides[$a[0]['days']];
7116 }
7117 }
7118 if (intval($s['type']) == 1) {
7119 // charge
7120 $factor->type = '+';
7121 $cpercent = 100 + $pctval;
7122 } else {
7123 // discount
7124 $factor->type = '-';
7125 $cpercent = 100 - $pctval;
7126 }
7127 $factor->amount = $pctval;
7128 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7129 } else {
7130 // absolute value
7131 $factor->pcent = 0;
7132 $absval = $s['diffcost'];
7133 if (strlen((string)$s['losoverride'])) {
7134 // values overrides
7135 $arrvaloverrides = [];
7136 $valovrparts = explode('_', $s['losoverride']);
7137 foreach ($valovrparts as $valovr) {
7138 if (!empty($valovr)) {
7139 $ovrinfo = explode(':', $valovr);
7140 if (strstr($ovrinfo[0], '-i') != false) {
7141 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7142 if ((int)$ovrinfo[0] < $a[0]['days']) {
7143 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7144 }
7145 }
7146 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7147 }
7148 }
7149 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7150 $absval = $arrvaloverrides[$a[0]['days']];
7151 }
7152 }
7153 if (intval($s['type']) == 1) {
7154 // charge
7155 $factor->type = '+';
7156 $newprice = ($dailyprice + $absval) * $affdays;
7157 } else {
7158 // discount
7159 $factor->type = '-';
7160 $newprice = ($dailyprice - $absval) * $affdays;
7161 }
7162 $factor->amount = $absval;
7163 }
7164
7165 // apply rounding
7166 $factor->roundmode = $s['roundmode'];
7167 if (!empty($s['roundmode'])) {
7168 $newprice = round($newprice, 0, constant($s['roundmode']));
7169 } else {
7170 $newprice = round($newprice, 2);
7171 }
7172
7173 // define the promotion (only if no value overrides set the amount to 0)
7174 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
7175 /**
7176 * Include the discount information (if any). The cost re-calculated may not be
7177 * precise if multiple special prices were applied over the same dates.
7178 *
7179 * @since 1.13
7180 */
7181 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7182 $promotion['discount'] = [
7183 'amount' => $s['diffcost'],
7184 'pcent' => (int)($s['val_pcent'] == 2),
7185 ];
7186 }
7187 //
7188 $mem[$k]['promotion'] = $promotion;
7189 }
7190
7191 // define the occupancy override
7192 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7193 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7194 }
7195
7196 // push difference generated only if to be applied progressively
7197 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
7198 /**
7199 * Push the difference generated by this special price for later transliteration of final price,
7200 * only if the special price is calculated progressively and not on the final price.
7201 *
7202 * @since 1.13.5
7203 */
7204 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7205 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7206 /**
7207 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7208 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7209 * all differences applied by the previous special pricing rules.
7210 *
7211 * @since 1.13.5
7212 */
7213 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7214 array_push($mem[$k]['trans_factors'], $factor);
7215 array_push($mem[$k]['trans_affdays'], $affdays);
7216 }
7217
7218 // push values in memory array
7219 array_push($mem[$k]['sum'], $newprice);
7220 $mem[$k]['daysused'] += $affdays;
7221 array_push($roomschange, $a[0]['idroom']);
7222 }
7223 }
7224 if ($applyseasons) {
7225 foreach ($mem as $k => $v) {
7226 if ($v['daysused'] > 0 && $v['sum']) {
7227 $newprice = 0;
7228 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7229 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7230 $addrest = $restdays * $dailyprice;
7231 $newprice += $addrest;
7232
7233 // calculate new final cost
7234 $redo_rounding = null;
7235 foreach ($v['sum'] as $sum_index => $add) {
7236 /**
7237 * The application of the various special pricing rules is made in a progressive and cumulative way
7238 * by always starting from the room base cost or its average daily cost. However, promotions may need
7239 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7240 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7241 * onto the differences generated by all the regular and progressively applied special pricing rules.
7242 *
7243 * @since 1.13.5
7244 */
7245 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7246 /**
7247 * This progressive price difference must be applied on the room final cost, so we need to
7248 * apply the transliteration over the other differences applied by other special prices.
7249 */
7250 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7251 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7252 // this is the % promotion we are looking for applying it on the final cost
7253 $factor = $v['trans_factors'][$transliterate_key];
7254 if (is_object($factor) && $factor->pcent) {
7255 $final_factor = 0;
7256 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7257 $final_factor += $prog_diff * $factor->amount / 100;
7258 }
7259 // update rounding
7260 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7261
7262 /**
7263 * Leverage the final factor depending on how many nights this affected.
7264 *
7265 * @since 1.15.0 (J) - 1.5.0 (WP)
7266 */
7267 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7268 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7269 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7270 }
7271
7272 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7273 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7274 }
7275 }
7276 }
7277
7278 // apply new price progressively
7279 $newprice += $add;
7280 }
7281
7282 // apply rounding from factor
7283 if (!empty($redo_rounding)) {
7284 $newprice = round($newprice, 0, constant($redo_rounding));
7285 }
7286
7287 // set promotion (if any)
7288 if (isset($v['promotion'])) {
7289 $arr[$k][0]['promotion'] = $v['promotion'];
7290 }
7291
7292 // set occupancy overrides (if any)
7293 if (isset($v['occupancy_ovr'])) {
7294 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7295 }
7296
7297 // set new final cost and update nights affected
7298 $arr[$k][0]['cost'] = $newprice;
7299 $arr[$k][0]['affdays'] = $v['daysused'];
7300 }
7301 }
7302 }
7303 }
7304
7305 // week days with no season
7306 $roomschange = array_unique($roomschange);
7307 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7308 $dbo->setQuery($q);
7309 $specials = $dbo->loadAssocList();
7310 if ($specials) {
7311 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7312 $applyseasons = false;
7313 unset($mem);
7314 $mem = [];
7315 foreach ($arr as $k => $a) {
7316 $mem[$k]['daysused'] = 0;
7317 $mem[$k]['sum'] = [];
7318 }
7319 foreach ($specials as $s) {
7320 // check if this is a promotion registered for skipping
7321 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7322 continue;
7323 }
7324
7325 // Special Price tied to the year
7326 if (!empty($s['year']) && $s['year'] > 0) {
7327 if ($one['year'] != $s['year']) {
7328 continue;
7329 }
7330 }
7331
7332 $allrooms = explode(",", $s['idrooms']);
7333 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7334 // week days
7335 $filterwdays = !empty($s['wdays']) ? true : false;
7336 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7337 if (is_array($wdays) && $wdays) {
7338 foreach ($wdays as $kw => $wd) {
7339 if (strlen($wd) == 0) {
7340 unset($wdays[$kw]);
7341 }
7342 }
7343 }
7344
7345 foreach ($arr as $k => $a) {
7346 // only rooms with no price modifications from seasons
7347
7348 // applied only to some types of price
7349 if ($allprices && !empty($allprices[0])) {
7350 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7351 continue;
7352 }
7353 }
7354
7355 /**
7356 * We should not exclude the rooms that already had a modification of the price through a season
7357 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7358 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7359 *
7360 * @since 1.11
7361 */
7362 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7363 continue;
7364 }
7365
7366 $affdays = 0;
7367 $season_fromdayts = $fromdayts;
7368 $is_dst = date('I', $season_fromdayts);
7369 for ($i = 0; $i < $a[0]['days']; $i++) {
7370 $todayts = $season_fromdayts + ($i * 86400);
7371 $is_now_dst = date('I', $todayts);
7372 if ($is_dst != $is_now_dst) {
7373 // Daylight Saving Time has changed, check how
7374 if ((bool)$is_dst === true) {
7375 $todayts += 3600;
7376 $season_fromdayts += 3600;
7377 } else {
7378 $todayts -= 3600;
7379 $season_fromdayts -= 3600;
7380 }
7381 $is_dst = $is_now_dst;
7382 }
7383 // week days
7384 if ($filterwdays == true) {
7385 $checkwday = getdate($todayts);
7386 if (in_array($checkwday['wday'], $wdays)) {
7387 $affdays++;
7388 }
7389 }
7390 }
7391 if (!($affdays > 0)) {
7392 // no nights affected
7393 continue;
7394 }
7395
7396 // apply the rule
7397 $applyseasons = true;
7398 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7399
7400 if (intval($s['val_pcent']) == 2) {
7401 // percentage value
7402 $pctval = $s['diffcost'];
7403 if (strlen((string)$s['losoverride'])) {
7404 // values overrides
7405 $arrvaloverrides = [];
7406 $valovrparts = explode('_', $s['losoverride']);
7407 foreach ($valovrparts as $valovr) {
7408 if (!empty($valovr)) {
7409 $ovrinfo = explode(':', $valovr);
7410 if (strstr($ovrinfo[0], '-i') != false) {
7411 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7412 if ((int)$ovrinfo[0] < $a[0]['days']) {
7413 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7414 }
7415 }
7416 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7417 }
7418 }
7419 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7420 $pctval = $arrvaloverrides[$a[0]['days']];
7421 }
7422 }
7423 if (intval($s['type']) == 1) {
7424 // charge
7425 $cpercent = 100 + $pctval;
7426 } else {
7427 // discount
7428 $cpercent = 100 - $pctval;
7429 }
7430 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7431 } else {
7432 // absolute value
7433 $absval = $s['diffcost'];
7434 if (strlen((string)$s['losoverride'])) {
7435 // values overrides
7436 $arrvaloverrides = [];
7437 $valovrparts = explode('_', $s['losoverride']);
7438 foreach ($valovrparts as $valovr) {
7439 if (!empty($valovr)) {
7440 $ovrinfo = explode(':', $valovr);
7441 if (strstr($ovrinfo[0], '-i') != false) {
7442 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7443 if ((int)$ovrinfo[0] < $a[0]['days']) {
7444 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7445 }
7446 }
7447 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7448 }
7449 }
7450 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7451 $absval = $arrvaloverrides[$a[0]['days']];
7452 }
7453 }
7454 if (intval($s['type']) == 1) {
7455 // charge
7456 $newprice = ($dailyprice + $absval) * $affdays;
7457 } else {
7458 // discount
7459 $newprice = ($dailyprice - $absval) * $affdays;
7460 }
7461 }
7462
7463 // apply rounding
7464 if (!empty($s['roundmode'])) {
7465 $newprice = round($newprice, 0, constant($s['roundmode']));
7466 } else {
7467 $newprice = round($newprice, 2);
7468 }
7469
7470 // push values in memory array
7471 array_push($mem[$k]['sum'], $newprice);
7472 $mem[$k]['daysused'] += $affdays;
7473 }
7474 }
7475 if ($applyseasons) {
7476 foreach ($mem as $k => $v) {
7477 if ($v['daysused'] > 0 && $v['sum']) {
7478 $newprice = 0;
7479 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7480 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7481 $addrest = $restdays * $dailyprice;
7482 $newprice += $addrest;
7483 foreach ($v['sum'] as $add) {
7484 $newprice += $add;
7485 }
7486 $arr[$k][0]['cost'] = $newprice;
7487 $arr[$k][0]['affdays'] = $v['daysused'];
7488 }
7489 }
7490 }
7491 }
7492 // end week days with no season
7493
7494 return $arr;
7495 }
7496
7497 /**
7498 * Applies the special prices over an array of tariffs.
7499 * The function is also used by VCM (>= 1.6.5) with specific arguments.
7500 *
7501 * @param array $arr array of tariffs taken from the DB
7502 * @param int $from start timestamp
7503 * @param int $to end timestamp
7504 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
7505 * @param array $seasons_dates (VBO 1.10) array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7506 * @param array $seasons_wdays (VBO 1.10) array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7507 *
7508 * @return array
7509 */
7510 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], array $seasons_wdays = [])
7511 {
7512 static $cached_seasons = [];
7513
7514 $cache_signature = '';
7515 $cache_signature_wdays = '';
7516 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
7517 // enable season records caching
7518 $cache_signature = "{$from}_{$to}";
7519 }
7520 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
7521 // enable week-day season records caching
7522 $cache_signature_wdays = "wdays";
7523 }
7524
7525 $dbo = JFactory::getDbo();
7526 $vbo_tn = self::getTranslator();
7527
7528 $roomschange = [];
7529 $one = getdate($from);
7530
7531 // leap years
7532 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7533 $isleap = true;
7534 } else {
7535 $isleap = false;
7536 }
7537
7538 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7539 $tomidnightone = intval($one['hours']) * 3600;
7540 $tomidnightone += intval($one['minutes']) * 60;
7541 $sfrom = $from - $baseone - $tomidnightone;
7542 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7543 $two = getdate($to);
7544 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7545 $tomidnighttwo = intval($two['hours']) * 3600;
7546 $tomidnighttwo += intval($two['minutes']) * 60;
7547 $sto = $to - $basetwo - $tomidnighttwo;
7548
7549 // leap years, check what dates to manipulate
7550 if ($isleap) {
7551 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7552 if ($one[0] > $leapts) {
7553 /**
7554 * Timestamp must be greater than the leap-day of Feb 29th.
7555 * It used to be checked for >= $leapts.
7556 *
7557 * @since July 3rd 2019
7558 */
7559 $sfrom -= 86400;
7560 }
7561 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7562 // lower checkin date when in leap year but not for checkout
7563 $sto -= 86400;
7564 }
7565 }
7566
7567 // check for DST changes to adjust the query values to fetch
7568 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7569 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7570 // we are parsing the day when the DST changed (probably a Sunday)
7571 if (!date('I', $from)) {
7572 // DST was just turned off
7573 $sfrom -= 3600;
7574 }
7575 }
7576 }
7577
7578 // count nights requested
7579 $booking_nights = 1;
7580 foreach ($arr as $k => $a) {
7581 if (isset($a['booking_nights'])) {
7582 // this value may be set when displaying pricing calendars
7583 $booking_nights = $a['booking_nights'];
7584 break;
7585 }
7586 if (isset($a['days'])) {
7587 $booking_nights = $a['days'];
7588 break;
7589 }
7590 }
7591
7592 /**
7593 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7594 *
7595 * @since 1.16.4 (J) - 1.6.4 (WP)
7596 */
7597 $skip_promo_ids = self::registerPromotionIds();
7598
7599 $totseasons = 0;
7600 if (!$parsed_season && !$seasons_dates) {
7601 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7602 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7603 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7604 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7605 ($sto > $sfrom ? "OR (`from` >= " . $sfrom . " AND `from` <= " . $sto . " AND `to` >= " . $sfrom . " AND `to` <= " . $sto . ")" : "OR (`from` >= " . $sfrom . " AND `from`>" . $sto . " AND `to`<" . $sfrom . " AND `to` <= " . $sto . " AND `from` > `to`)") .
7606 ($sto > $sfrom ? " OR (`from` <= " . $sfrom . " AND `from` <= " . $sto . " AND `to`<" . $sfrom . " AND `to`<" . $sto . " AND `from` > `to`) OR (`from`>" . $sfrom . " AND `from`>" . $sto . " AND `to` >= " . $sfrom . " AND `to` >= " . $sto . " AND `from` > `to`)" : " OR (`from` <= " . $sfrom . " AND `to` >=" . $sfrom . " AND `from` >= " . $sto . " AND `to` > " . $sto . " AND `from` < `to`)") .
7607 ($sto > $sfrom ? " OR (`from` >= " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` < " . $sfrom . " AND `to` >=" . $sto . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `from` < `to`)") .
7608 ($sto > $sfrom ? " OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >=" . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` > " . $sfrom . " AND `to` > " . $sto . " AND `from` < `to`) OR (`from` < " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` < `to`)") .
7609 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7610 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7611
7612 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
7613 // avoid making a query
7614 $seasons = $cached_seasons[$cache_signature];
7615 } else {
7616 // get the season records by running the query
7617 $dbo->setQuery($q);
7618 $seasons = $dbo->loadAssocList();
7619 }
7620
7621 // count total seasons
7622 $totseasons = $seasons ? count($seasons) : 0;
7623
7624 if ($cache_signature) {
7625 // cache season records
7626 $cached_seasons[$cache_signature] = $seasons;
7627 }
7628 }
7629
7630 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
7631 if ($totseasons > 0) {
7632 $seasons = $seasons;
7633 } elseif ($parsed_season) {
7634 $seasons = [$parsed_season];
7635 } else {
7636 $seasons = $seasons_dates;
7637 }
7638 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7639 $applyseasons = false;
7640 $mem = [];
7641 foreach ($arr as $k => $a) {
7642 $mem[$k]['daysused'] = 0;
7643 $mem[$k]['sum'] = [];
7644 $mem[$k]['spids'] = [];
7645 /**
7646 * The keys below are all needed to apply the promotions on the room's final cost.
7647 *
7648 * @since 1.13.5
7649 */
7650 $mem[$k]['diffs'] = [];
7651 $mem[$k]['trans_keys'] = [];
7652 $mem[$k]['trans_factors'] = [];
7653 $mem[$k]['trans_affdays'] = [];
7654 }
7655 $affdayslistless = [];
7656 foreach ($seasons as $s) {
7657 // double check that the 'from' and 'to' properties are not empty (dates filter), in case VCM passes an array of seasons already taken from the DB
7658 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
7659 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
7660 continue;
7661 }
7662
7663 /**
7664 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
7665 * We need to skip such fake seasons as they do not need any parsing.
7666 *
7667 * @since 1.13
7668 */
7669 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
7670 continue;
7671 }
7672
7673 // check if this is a promotion registered for skipping
7674 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7675 continue;
7676 }
7677
7678 // Special Price tied to the year
7679 if (!empty($s['year']) && $s['year'] > 0) {
7680 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7681 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7682 //VBO 1.9 - tied to the year can be set for prev year (Dec 27 to Jan 3) and booking can be Jan 1 to Jan 3 - do not skip in this case
7683 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7684 continue;
7685 }
7686 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7687 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7688 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7689 if ($calc_ends < ($from - $tomidnightone)) {
7690 continue;
7691 }
7692 }
7693 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7694 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7695 if ($s['from'] > $s['to']) {
7696 continue;
7697 }
7698 if (($basetwo + $s['from'] + 86399) > $to) {
7699 /**
7700 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7701 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7702 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7703 *
7704 * @since 1.14.3 (J) - 1.4.3 (WP)
7705 */
7706 continue;
7707 }
7708 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7709 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7710 /**
7711 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7712 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7713 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7714 *
7715 * @since 1.14.3 (J) - 1.4.3 (WP)
7716 */
7717 continue;
7718 }
7719 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7720 // season tied to the year 2017 accross 2018 and we are parsing dates at the beginning of 2017 due to beginning loop in 2016 (Rates Overview)
7721 if (($baseone + $s['from']) > $to) {
7722 continue;
7723 }
7724 }
7725 }
7726 //
7727 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7728 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7729 $inits = $baseone + $s['from'];
7730 if ($s['from'] < $s['to']) {
7731 $ends = $basetwo + $s['to'];
7732 // check if the inits must be set to the year after
7733 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7734 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7735 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7736 $inits = $tmpbase + $s['from'];
7737 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7738 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7739 $ends = $baseone + $s['to'];
7740 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7741 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7742 $ends = $baseone + $s['to'];
7743 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7744 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7745 $inits = $basetwo + $s['from'];
7746 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7747 // booking dates across two years for a season tied to a specific year
7748 if ($one['year'] == $s['year']) {
7749 $inits = $baseone + $s['from'];
7750 } else {
7751 $inits = $basetwo + $s['from'];
7752 }
7753 if ($two['year'] == $s['year']) {
7754 $ends = $basetwo + $s['to'];
7755 } else {
7756 $ends = $baseone + $s['to'];
7757 }
7758 }
7759 } else {
7760 // between 2 years
7761 if ($baseone < $basetwo) {
7762 // ex. 29/12/2012 - 14/01/2013
7763 $ends = $basetwo + $s['to'];
7764 } else {
7765 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7766 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7767 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7768 $ends = $tmpbase + $s['to'];
7769 } else {
7770 // ex. 03/01 - 09/01
7771 $ends = $basetwo + $s['to'];
7772 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7773 $inits = $tmpbase + $s['from'];
7774 }
7775 }
7776 }
7777 // leap years
7778 if ($isleap == true) {
7779 $infoseason = getdate($inits);
7780 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7781 // leap years, check what dates to manipulate
7782 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7783 /**
7784 * Timestamp must be greater than the leap-day of Feb 29th.
7785 * It used to be checked for >= $leapts.
7786 *
7787 * @since July 3rd 2019
7788 */
7789 $inits += 86400;
7790 if ($s['from'] < $s['to']) {
7791 // increase season end date only if still on the same leap year
7792 $ends += 86400;
7793 }
7794 }
7795 }
7796
7797 // promotions
7798 $promotion = [];
7799 if ($s['promo'] == 1) {
7800 $daysadv = (($inits - time()) / 86400);
7801 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7802 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7803 continue;
7804 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7805 $secstocheckin = ($from - time());
7806 if ($s['promolastmin'] < $secstocheckin) {
7807 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7808 continue;
7809 }
7810 }
7811 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7812 /**
7813 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7814 *
7815 * @since 1.13.5
7816 */
7817 continue;
7818 }
7819 $promotion['todaydaysadv'] = $daysadv;
7820 $promotion['promodaysadv'] = $s['promodaysadv'];
7821 $promotion['promotxt'] = $s['promotxt'];
7822 }
7823
7824 // occupancy override
7825 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7826
7827 //week days
7828 $filterwdays = !empty($s['wdays']) ? true : false;
7829 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7830 if (is_array($wdays) && $wdays) {
7831 foreach ($wdays as $kw => $wd) {
7832 if (strlen($wd) == 0) {
7833 unset($wdays[$kw]);
7834 }
7835 }
7836 }
7837
7838 // checkin must be after the begin of the season
7839 $checkininclok = true;
7840 if ($s['checkinincl'] == 1) {
7841 $checkininclok = false;
7842 if ($s['from'] < $s['to']) {
7843 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7844 $checkininclok = true;
7845 }
7846 } else {
7847 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7848 $checkininclok = true;
7849 }
7850 }
7851 }
7852 if ($checkininclok !== true) {
7853 continue;
7854 }
7855
7856 foreach ($arr as $k => $a) {
7857 // applied only to some types of price
7858 if ($allprices && !empty($allprices[0])) {
7859 // VikBooking 1.6: Price Calendar sets the idprice to -1
7860 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
7861 continue;
7862 }
7863 }
7864 // applied only to some room types
7865 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
7866 continue;
7867 }
7868
7869 if (isset($a['days']) && is_float($a['days'])) {
7870 // prevent any possible warning with array_key_exists()
7871 $a['days'] = (int)$a['days'];
7872 }
7873
7874 $affdays = 0;
7875 $season_fromdayts = $fromdayts;
7876 $is_dst = date('I', $season_fromdayts);
7877 for ($i = 0; $i < $a['days']; $i++) {
7878 $todayts = $season_fromdayts + ($i * 86400);
7879 $is_now_dst = date('I', $todayts);
7880 if ($is_dst != $is_now_dst) {
7881 // Daylight Saving Time has changed, check how
7882 if ((bool)$is_dst === true) {
7883 $todayts += 3600;
7884 $season_fromdayts += 3600;
7885 } else {
7886 $todayts -= 3600;
7887 $season_fromdayts -= 3600;
7888 }
7889 $is_dst = $is_now_dst;
7890 }
7891 if ($todayts >= $inits && $todayts <= $ends) {
7892 $checkwday = getdate($todayts);
7893 // week days
7894 if ($filterwdays == true) {
7895 if (in_array($checkwday['wday'], $wdays)) {
7896 if (!isset($arr[$k]['affdayslist'])) {
7897 $arr[$k]['affdayslist'] = [];
7898 }
7899 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = isset($arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']]) && $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] > 0 ? $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] : 0;
7900 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7901 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7902 $affdays++;
7903 }
7904 } else {
7905 if (!isset($arr[$k]['affdayslist'])) {
7906 $arr[$k]['affdayslist'] = [];
7907 }
7908 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = isset($arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']]) && $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] > 0 ? $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] : 0;
7909 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7910 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7911 $affdays++;
7912 }
7913 //
7914 }
7915 }
7916
7917 if (!($affdays > 0)) {
7918 // no nights affected
7919 continue;
7920 }
7921
7922 // apply the rule
7923 $applyseasons = true;
7924 $dailyprice = $a['cost'] / $a['days'];
7925
7926 // modification factor object
7927 $factor = new stdClass;
7928
7929 // calculate new price progressively
7930 if (intval($s['val_pcent']) == 2) {
7931 // percentage value
7932 $factor->pcent = 1;
7933 $pctval = $s['diffcost'];
7934 if (strlen((string)$s['losoverride'])) {
7935 // values overrides
7936 $arrvaloverrides = [];
7937 $valovrparts = explode('_', $s['losoverride']);
7938 foreach ($valovrparts as $valovr) {
7939 if (!empty($valovr)) {
7940 $ovrinfo = explode(':', $valovr);
7941 if (strstr($ovrinfo[0], '-i') != false) {
7942 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7943 if ((int)$ovrinfo[0] < $a['days']) {
7944 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7945 }
7946 }
7947 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7948 }
7949 }
7950 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7951 $pctval = $arrvaloverrides[$a['days']];
7952 }
7953 }
7954 if (intval($s['type']) == 1) {
7955 // charge
7956 $factor->type = '+';
7957 $cpercent = 100 + $pctval;
7958 } else {
7959 // discount
7960 $factor->type = '-';
7961 $cpercent = 100 - $pctval;
7962 }
7963 $factor->amount = $pctval;
7964 $dailysum = ($dailyprice * $cpercent / 100);
7965 $newprice = $dailysum * $affdays;
7966 } else {
7967 // absolute value
7968 $factor->pcent = 0;
7969 $absval = $s['diffcost'];
7970 if (strlen((string)$s['losoverride'])) {
7971 // values overrides
7972 $arrvaloverrides = [];
7973 $valovrparts = explode('_', $s['losoverride']);
7974 foreach ($valovrparts as $valovr) {
7975 if (!empty($valovr)) {
7976 $ovrinfo = explode(':', $valovr);
7977 if (strstr($ovrinfo[0], '-i') != false) {
7978 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7979 if ((int)$ovrinfo[0] < $a['days']) {
7980 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7981 }
7982 }
7983 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7984 }
7985 }
7986 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7987 $absval = $arrvaloverrides[$a['days']];
7988 }
7989 }
7990 if (intval($s['type']) == 1) {
7991 // charge
7992 $factor->type = '+';
7993 $dailysum = ($dailyprice + $absval);
7994 $newprice = $dailysum * $affdays;
7995 } else {
7996 // discount
7997 $factor->type = '-';
7998 $dailysum = ($dailyprice - $absval);
7999 $newprice = $dailysum * $affdays;
8000 }
8001 $factor->amount = $absval;
8002 }
8003
8004 // apply rounding
8005 $factor->roundmode = $s['roundmode'];
8006 if (!empty($s['roundmode'])) {
8007 $newprice = round($newprice, 0, constant($s['roundmode']));
8008 } else {
8009 $newprice = round($newprice, 2);
8010 }
8011
8012 // define the promotion (only if no value overrides set the amount to 0)
8013 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
8014 /**
8015 * Include the discount information (if any). The cost re-calculated may not be
8016 * precise if multiple special prices were applied over the same dates.
8017 *
8018 * @since 1.13
8019 */
8020 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8021 $promotion['discount'] = [
8022 'amount' => $s['diffcost'],
8023 'pcent' => (int)($s['val_pcent'] == 2),
8024 ];
8025 }
8026 //
8027 $mem[$k]['promotion'] = $promotion;
8028 }
8029
8030 // define the occupancy override
8031 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8032 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8033 }
8034
8035 // affected days list
8036 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8037 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8038 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8039 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8040 }
8041 }
8042 }
8043
8044 // push special price ID
8045 if (!in_array($s['id'], $mem[$k]['spids'])) {
8046 array_push($mem[$k]['spids'], $s['id']);
8047 }
8048
8049 // push difference generated only if to be applied progressively
8050 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
8051 /**
8052 * Push the difference generated by this special price for later transliteration of final price,
8053 * only if the special price is calculated progressively and not on the final price.
8054 *
8055 * @since 1.13.5
8056 */
8057 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8058 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8059 /**
8060 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8061 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8062 * all differences applied by the previous special pricing rules.
8063 *
8064 * @since 1.13.5
8065 */
8066 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8067 array_push($mem[$k]['trans_factors'], $factor);
8068 array_push($mem[$k]['trans_affdays'], $affdays);
8069 }
8070
8071 // push values in memory array
8072 array_push($mem[$k]['sum'], $newprice);
8073 $mem[$k]['daysused'] += $affdays;
8074 array_push($roomschange, $a['idroom']);
8075 }
8076 }
8077 if ($applyseasons) {
8078 foreach ($mem as $k => $v) {
8079 if ($v['daysused'] > 0 && $v['sum']) {
8080 $newprice = 0;
8081 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8082 $restdays = $arr[$k]['days'] - $v['daysused'];
8083 $addrest = $restdays * $dailyprice;
8084 $newprice += $addrest;
8085
8086 // calculate new final cost
8087 $redo_rounding = null;
8088 foreach ($v['sum'] as $sum_index => $add) {
8089 /**
8090 * The application of the various special pricing rules is made in a progressive and cumulative way
8091 * by always starting from the room base cost or its average daily cost. However, promotions may need
8092 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8093 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8094 * onto the differences generated by all the regular and progressively applied special pricing rules.
8095 *
8096 * @since 1.13.5
8097 */
8098 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8099 /**
8100 * This progressive price difference must be applied on the room final cost, so we need to
8101 * apply the transliteration over the other differences applied by other special prices.
8102 */
8103 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8104 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8105 // this is the % promotion we are looking for applying it on the final cost
8106 $factor = $v['trans_factors'][$transliterate_key];
8107 if (is_object($factor) && $factor->pcent) {
8108 $final_factor = 0;
8109 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8110 $final_factor += $prog_diff * $factor->amount / 100;
8111 }
8112 // update rounding
8113 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8114
8115 /**
8116 * Leverage the final factor depending on how many nights this affected.
8117 *
8118 * @since 1.15.0 (J) - 1.5.0 (WP)
8119 */
8120 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8121 $avg_final_factor = $final_factor / $arr[$k]['days'];
8122 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8123 }
8124
8125 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8126 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8127 }
8128 }
8129 }
8130
8131 // apply new price progressively
8132 $newprice += $add;
8133 }
8134
8135 // apply rounding from factor
8136 if (!empty($redo_rounding)) {
8137 $newprice = round($newprice, 0, constant($redo_rounding));
8138 }
8139
8140 // set promotion (if any)
8141 if (isset($v['promotion'])) {
8142 $arr[$k]['promotion'] = $v['promotion'];
8143 }
8144
8145 // set occupancy overrides (if any)
8146 if (isset($v['occupancy_ovr'])) {
8147 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8148 }
8149
8150 // set new final cost and update nights affected
8151 $arr[$k]['cost'] = $newprice;
8152 $arr[$k]['affdays'] = $v['daysused'];
8153 if (array_key_exists('spids', $v) && $v['spids']) {
8154 $arr[$k]['spids'] = $v['spids'];
8155 }
8156 }
8157 }
8158 }
8159 }
8160
8161 // week days with no season
8162 $roomschange = array_unique($roomschange);
8163 $totspecials = 0;
8164 if (!$seasons_wdays) {
8165 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
8166
8167 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8168 // avoid making a query
8169 $specials = $cached_seasons[$cache_signature_wdays];
8170 } else {
8171 // get the week-days-with-no-season records by running the query
8172 $dbo->setQuery($q);
8173 $specials = $dbo->loadAssocList();
8174 }
8175
8176 // count records
8177 $totspecials = $specials ? count($specials) : 0;
8178
8179 if ($cache_signature_wdays) {
8180 // cache week-days-with-no-season records
8181 $cached_seasons[$cache_signature_wdays] = $specials;
8182 }
8183 }
8184 if ($totspecials > 0 || $seasons_wdays) {
8185 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8186 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8187 $applyseasons = false;
8188 /**
8189 * We no longer unset the previous memory of the seasons with dates filters
8190 * because we need the responses to be merged. We do it only if not set.
8191 * We only keep the property 'spids' but the others should be unset.
8192 *
8193 * @since 1.11
8194 */
8195 if (!isset($mem)) {
8196 $mem = [];
8197 foreach ($arr as $k => $a) {
8198 $mem[$k]['daysused'] = 0;
8199 $mem[$k]['sum'] = [];
8200 $mem[$k]['spids'] = [];
8201 }
8202 } else {
8203 foreach ($arr as $k => $a) {
8204 $mem[$k]['daysused'] = 0;
8205 $mem[$k]['sum'] = [];
8206 }
8207 }
8208 //
8209 foreach ($specials as $s) {
8210 // double check that the 'from' and 'to' properties are empty (only weekdays), in case VCM passes an array of seasons already taken from the DB
8211 if (!empty($s['from']) || !empty($s['to'])) {
8212 continue;
8213 }
8214
8215 // check if this is a promotion registered for skipping
8216 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8217 continue;
8218 }
8219
8220 // Special Price tied to the year
8221 if (!empty($s['year']) && $s['year'] > 0) {
8222 if ($one['year'] != $s['year']) {
8223 continue;
8224 }
8225 }
8226
8227 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8228 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8229 // week days
8230 $filterwdays = !empty($s['wdays']) ? true : false;
8231 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8232 if (is_array($wdays) && $wdays) {
8233 foreach ($wdays as $kw => $wd) {
8234 if (strlen($wd) == 0) {
8235 unset($wdays[$kw]);
8236 }
8237 }
8238 }
8239
8240 foreach ($arr as $k => $a) {
8241 // only rooms with no price modifications from seasons
8242
8243 // applied only to some types of price
8244 if ($allprices && !empty($allprices[0])) {
8245 // VikBooking 1.6: Price Calendar sets the idprice to -1
8246 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8247 continue;
8248 }
8249 }
8250
8251 /**
8252 * We should not exclude the rooms that already had a modification of the price through a season
8253 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8254 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8255 *
8256 * @since 1.11
8257 */
8258 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8259 continue;
8260 }
8261
8262 if (isset($a['days']) && is_float($a['days'])) {
8263 // prevent any possible warning with array_key_exists()
8264 $a['days'] = (int)$a['days'];
8265 }
8266
8267 $affdays = 0;
8268 $season_fromdayts = $fromdayts;
8269 $is_dst = date('I', $season_fromdayts);
8270 for ($i = 0; $i < $a['days']; $i++) {
8271 $todayts = $season_fromdayts + ($i * 86400);
8272 $is_now_dst = date('I', $todayts);
8273 if ($is_dst != $is_now_dst) {
8274 // Daylight Saving Time has changed, check how
8275 if ((bool)$is_dst === true) {
8276 $todayts += 3600;
8277 $season_fromdayts += 3600;
8278 } else {
8279 $todayts -= 3600;
8280 $season_fromdayts -= 3600;
8281 }
8282 $is_dst = $is_now_dst;
8283 }
8284 // week days
8285 if ($filterwdays == true) {
8286 $checkwday = getdate($todayts);
8287 if (in_array($checkwday['wday'], $wdays)) {
8288 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8289 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8290 $affdays++;
8291 }
8292 }
8293 //
8294 }
8295
8296 if (!($affdays > 0)) {
8297 // no nights affected
8298 continue;
8299 }
8300
8301 // apply the rule
8302 $applyseasons = true;
8303 $dailyprice = $a['cost'] / $a['days'];
8304
8305 if (intval($s['val_pcent']) == 2) {
8306 // percentage value
8307 $pctval = $s['diffcost'];
8308 if (strlen((string)$s['losoverride'])) {
8309 // values overrides
8310 $arrvaloverrides = [];
8311 $valovrparts = explode('_', $s['losoverride']);
8312 foreach ($valovrparts as $valovr) {
8313 if (!empty($valovr)) {
8314 $ovrinfo = explode(':', $valovr);
8315 if (strstr($ovrinfo[0], '-i') != false) {
8316 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8317 if ((int)$ovrinfo[0] < $a['days']) {
8318 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8319 }
8320 }
8321 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8322 }
8323 }
8324 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8325 $pctval = $arrvaloverrides[$a['days']];
8326 }
8327 }
8328 if (intval($s['type']) == 1) {
8329 // charge
8330 $cpercent = 100 + $pctval;
8331 } else {
8332 // discount
8333 $cpercent = 100 - $pctval;
8334 }
8335 $dailysum = ($dailyprice * $cpercent / 100);
8336 $newprice = $dailysum * $affdays;
8337 } else {
8338 // absolute value
8339 $absval = $s['diffcost'];
8340 if (strlen((string)$s['losoverride'])) {
8341 // values overrides
8342 $arrvaloverrides = [];
8343 $valovrparts = explode('_', $s['losoverride']);
8344 foreach ($valovrparts as $valovr) {
8345 if (!empty($valovr)) {
8346 $ovrinfo = explode(':', $valovr);
8347 if (strstr($ovrinfo[0], '-i') != false) {
8348 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8349 if ((int)$ovrinfo[0] < $a['days']) {
8350 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8351 }
8352 }
8353 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8354 }
8355 }
8356 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8357 $absval = $arrvaloverrides[$a['days']];
8358 }
8359 }
8360 if (intval($s['type']) == 1) {
8361 // charge
8362 $dailysum = ($dailyprice + $absval);
8363 $newprice = $dailysum * $affdays;
8364 } else {
8365 // discount
8366 $dailysum = ($dailyprice - $absval);
8367 $newprice = $dailysum * $affdays;
8368 }
8369 }
8370
8371 // apply rounding
8372 if (!empty($s['roundmode'])) {
8373 $newprice = round($newprice, 0, constant($s['roundmode']));
8374 } else {
8375 $newprice = round($newprice, 2);
8376 }
8377
8378 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8379 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8380 }
8381 if (!in_array($s['id'], $mem[$k]['spids'])) {
8382 $mem[$k]['spids'][] = $s['id'];
8383 }
8384 $mem[$k]['sum'][] = $newprice;
8385 $mem[$k]['daysused'] += $affdays;
8386 }
8387 }
8388 if ($applyseasons) {
8389 foreach ($mem as $k => $v) {
8390 if ($v['daysused'] > 0 && $v['sum']) {
8391 $newprice = 0;
8392 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8393 $restdays = $arr[$k]['days'] - $v['daysused'];
8394 $addrest = $restdays * $dailyprice;
8395 $newprice += $addrest;
8396 foreach ($v['sum'] as $add) {
8397 $newprice += $add;
8398 }
8399 $arr[$k]['cost'] = $newprice;
8400 $arr[$k]['affdays'] = $v['daysused'];
8401 if (array_key_exists('spids', $v) && $v['spids']) {
8402 $arr[$k]['spids'] = $v['spids'];
8403 }
8404 }
8405 }
8406 }
8407 }
8408 // end week days with no season
8409
8410 return $arr;
8411 }
8412
8413 public static function getRoomRplansClosingDates($idroom)
8414 {
8415 $dbo = JFactory::getDbo();
8416
8417 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8418 $dbo->setQuery($q);
8419 $price_records = $dbo->loadAssocList();
8420 if (!$price_records) {
8421 return [];
8422 }
8423
8424 $closingd = [];
8425 foreach ($price_records as $prec) {
8426 if (empty($prec['closingd'])) {
8427 continue;
8428 }
8429 $price_closing = json_decode($prec['closingd'], true);
8430 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8431 continue;
8432 }
8433 // check expired dates and clean up
8434 $today_midnight = mktime(0, 0, 0);
8435 $cleaned = false;
8436 foreach ($price_closing[$idroom] as $k => $v) {
8437 if (strtotime($v) < $today_midnight) {
8438 $cleaned = true;
8439 unset($price_closing[$idroom][$k]);
8440 }
8441 }
8442 if (!$price_closing[$idroom]) {
8443 unset($price_closing[$idroom]);
8444 } elseif ($cleaned === true) {
8445 // reset array keys for smaller JSON size
8446 $price_closing[$idroom] = array_values($price_closing[$idroom]);
8447 }
8448 if ($cleaned === true) {
8449 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
8450 $dbo->setQuery($q);
8451 $dbo->execute();
8452 }
8453 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8454 continue;
8455 }
8456 $closingd[$prec['id']] = $price_closing[$idroom];
8457 }
8458
8459 return $closingd;
8460 }
8461
8462 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
8463 {
8464 $dbo = JFactory::getDbo();
8465
8466 $closingd = [];
8467
8468 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8469 $dbo->setQuery($q);
8470 $price_records = $dbo->loadAssocList();
8471 if ($price_records && $roomids) {
8472 $info_start = getdate($checkints);
8473 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
8474 $all_nights = [];
8475 for ($i = 0; $i < (int)$numnights; $i++) {
8476 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
8477 $all_nights[] = date('Y-m-d', $next_midnight);
8478 }
8479 foreach ($price_records as $prec) {
8480 if (empty($prec['closingd'])) {
8481 continue;
8482 }
8483 $price_closing = json_decode($prec['closingd'], true);
8484 if (!$price_closing) {
8485 continue;
8486 }
8487 foreach ($price_closing as $idroom => $rclosedd) {
8488 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
8489 continue;
8490 }
8491 if (!array_key_exists($idroom, $closingd)) {
8492 $closingd[$idroom] = [];
8493 }
8494 foreach ($all_nights as $night) {
8495 if (in_array($night, $rclosedd)) {
8496 if (array_key_exists($prec['id'], $closingd[$idroom])) {
8497 $closingd[$idroom][$prec['id']][] = $night;
8498 } else {
8499 $closingd[$idroom][$prec['id']] = [$night];
8500 }
8501 }
8502 }
8503 }
8504 }
8505 }
8506
8507 return $closingd;
8508 }
8509
8510 /**
8511 * Tells whether at least one payment method must be selected to check-out.
8512 *
8513 * @param array $rooms_involved optional list of room IDs being booked.
8514 *
8515 * @return bool true if at least one payment method is applicable.
8516 *
8517 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
8518 */
8519 public static function areTherePayments($rooms_involved = [])
8520 {
8521 $dbo = JFactory::getDbo();
8522
8523 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
8524 $dbo->setQuery($q);
8525 $dbo->execute();
8526
8527 if (!$dbo->getNumRows()) {
8528 // no published payment options found
8529 return false;
8530 }
8531
8532 $pay_options = $dbo->loadAssocList();
8533
8534 if (!count($rooms_involved)) {
8535 // if no filters given, return true
8536 return true;
8537 }
8538
8539 // validate room associations
8540 foreach ($pay_options as $pay_k => $pay_v) {
8541 if (empty($pay_v['idrooms'])) {
8542 continue;
8543 }
8544 $rooms_supported = json_decode($pay_v['idrooms']);
8545 if (!is_array($rooms_supported) || !count($rooms_supported)) {
8546 continue;
8547 }
8548 // make sure all rooms being booked are supported
8549 foreach ($rooms_involved as $room_id_booked) {
8550 if (!in_array($room_id_booked, $rooms_supported)) {
8551 // we need to unset this payment option as soon as we find one non-matching room ID
8552 unset($pay_options[$pay_k]);
8553 break;
8554 }
8555 }
8556 }
8557
8558 return (bool)count($pay_options);
8559 }
8560
8561 public static function getPayment($idp, $vbo_tn = null)
8562 {
8563 if (empty($idp)) {
8564 return false;
8565 }
8566
8567 if (strstr((string)$idp, '=') !== false) {
8568 $parts = explode('=', $idp);
8569 $idp = $parts[0];
8570 }
8571
8572 $dbo = JFactory::getDbo();
8573
8574 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
8575 $dbo->setQuery($q);
8576 $payment = $dbo->loadAssocList();
8577 if (!$payment) {
8578 return false;
8579 }
8580
8581 if (is_object($vbo_tn)) {
8582 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
8583 }
8584
8585 return $payment[0];
8586 }
8587
8588 public static function getCronKey()
8589 {
8590 return VBOFactory::getConfig()->get('cronkey', '');
8591 }
8592
8593 /**
8594 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
8595 *
8596 * @return int
8597 *
8598 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
8599 */
8600 public static function getNextInvoiceNumber()
8601 {
8602 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
8603 }
8604
8605 public static function getInvoiceNumberSuffix()
8606 {
8607 return VBOFactory::getConfig()->get('invoicesuffix', '');
8608 }
8609
8610 public static function getInvoiceCompanyInfo()
8611 {
8612 return VBOFactory::getConfig()->get('invcompanyinfo', '');
8613 }
8614
8615 /**
8616 * Gets the number for the next booking receipt generation,
8617 * updates the last receipt number used for the later iterations,
8618 * stores a new receipt record to keep track of the receipts issued.
8619 *
8620 * @param int $bid the Booking ID for which we are/want to generating the receipt.
8621 * @param [int] $newnum the last number used for generating the receipt.
8622 *
8623 * @return int
8624 */
8625 public static function getNextReceiptNumber ($bid, $newnum = false) {
8626 $dbo = JFactory::getDbo();
8627 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
8628 $dbo->setQuery($q);
8629 $dbo->execute();
8630 if ($dbo->getNumRows() > 0) {
8631 $s = $dbo->loadResult();
8632 //check if this booking has already a receipt, and return that number
8633 $prev_receipt = array();
8634 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
8635 $dbo->setQuery($q);
8636 $dbo->execute();
8637 if ($dbo->getNumRows() > 0) {
8638 $prev_receipt = $dbo->loadAssoc();
8639 }
8640 //update value (receipt generated)
8641 if ($newnum !== false && $newnum > 0) {
8642 $s = (int)$newnum;
8643 if (!(count($prev_receipt) > 0)) {
8644 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
8645 $dbo->setQuery($q);
8646 $dbo->execute();
8647 //insert the new receipt record
8648 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
8649 $dbo->setQuery($q);
8650 $dbo->execute();
8651 } else {
8652 //update receipt record
8653 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
8654 $dbo->setQuery($q);
8655 $dbo->execute();
8656 }
8657 }
8658 //
8659 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
8660 }
8661 //first execution of the method should create the configuration record
8662 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
8663 $dbo->setQuery($q);
8664 $dbo->execute();
8665 return 1;
8666 }
8667
8668 public static function getReceiptNotes ($newnotes = false) {
8669 $dbo = JFactory::getDbo();
8670 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
8671 $dbo->setQuery($q);
8672 $dbo->execute();
8673 if ($dbo->getNumRows() > 0) {
8674 $s = $dbo->loadResult();
8675 //update value
8676 if ($newnotes !== false) {
8677 $s = $newnotes;
8678 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
8679 $dbo->setQuery($q);
8680 $dbo->execute();
8681 }
8682 //
8683 return $s;
8684 }
8685 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
8686 $dbo->setQuery($q);
8687 $dbo->execute();
8688 return "";
8689 }
8690
8691 public static function loadColorTagsRules()
8692 {
8693 $color_tag_rules = [
8694 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
8695 'pend1' => 'VBWAITINGFORPAYMENT',
8696 'conf1' => 'VBDBTEXTROOMCLOSED',
8697 'conf2' => 'VBOCOLORTAGRULECONFTWO',
8698 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
8699 'inv1' => 'VBOCOLORTAGRULEINVONE',
8700 'rcp1' => 'VBOCOLORTAGRULERCPONE',
8701 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
8702 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
8703 'inv2' => 'VBOCOLORTAGRULEINVTWO',
8704 ];
8705
8706 /**
8707 * Trigger event to let third-party plugins override the default booking color tag rules.
8708 *
8709 * @since 1.16.4 (J) - 1.6.4 (WP)
8710 */
8711 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
8712
8713 return $color_tag_rules;
8714 }
8715
8716 public static function loadDefaultColorTags()
8717 {
8718 $default_color_tags = [
8719 [
8720 'color' => '#9b9b9b',
8721 'name' => 'VBWAITINGFORPAYMENT',
8722 'rule' => 'pend1'
8723 ],
8724 [
8725 'color' => '#333333',
8726 'name' => 'VBDBTEXTROOMCLOSED',
8727 'rule' => 'conf1'
8728 ],
8729 [
8730 'color' => '#ff8606',
8731 'name' => 'VBOCOLORTAGRULECONFTWO',
8732 'rule' => 'conf2'
8733 ],
8734 [
8735 'color' => '#0418c9',
8736 'name' => 'VBOCOLORTAGRULECONFTHREE',
8737 'rule' => 'conf3'
8738 ],
8739 [
8740 'color' => '#bed953',
8741 'name' => 'VBOCOLORTAGRULEINVONE',
8742 'rule' => 'inv1'
8743 ],
8744 [
8745 'color' => '#67f5b5',
8746 'name' => 'VBOCOLORTAGRULERCPONE',
8747 'rule' => 'rcp1'
8748 ],
8749 [
8750 'color' => '#04d2c2',
8751 'name' => 'VBOCOLORTAGRULECONFFOUR',
8752 'rule' => 'conf4'
8753 ],
8754 [
8755 'color' => '#00b316',
8756 'name' => 'VBOCOLORTAGRULECONFFIVE',
8757 'rule' => 'conf5'
8758 ],
8759 [
8760 'color' => '#00f323',
8761 'name' => 'VBOCOLORTAGRULEINVTWO',
8762 'rule' => 'inv2'
8763 ],
8764 ];
8765
8766 /**
8767 * Trigger event to let third-party plugins override the default color tags.
8768 *
8769 * @since 1.16.4 (J) - 1.6.4 (WP)
8770 */
8771 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
8772
8773 return $default_color_tags;
8774 }
8775
8776 public static function loadBookingsColorTags()
8777 {
8778 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
8779
8780 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
8781 }
8782
8783 public static function getBestColorContrast($hexcolor)
8784 {
8785 $hexcolor = str_replace('#', '', $hexcolor);
8786
8787 if (empty($hexcolor) || strlen($hexcolor) != 6) {
8788 return '#000000';
8789 }
8790
8791 $r = hexdec(substr($hexcolor, 0, 2));
8792 $g = hexdec(substr($hexcolor, 2, 2));
8793 $b = hexdec(substr($hexcolor, 4, 2));
8794
8795 // Counting the perceptive luminance - human eye favors green color
8796 // < 0.5 bright colors
8797 // > 0.5 dark colors
8798
8799 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
8800 }
8801
8802 public static function applyBookingColorTag($booking, $tags = [])
8803 {
8804 if (!is_array($tags) || !$tags) {
8805 $tags = self::loadBookingsColorTags();
8806 }
8807
8808 if (!empty($booking['colortag'])) {
8809 $color_tag_arr = json_decode($booking['colortag'], true);
8810 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
8811 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
8812 return $color_tag_arr;
8813 }
8814 }
8815
8816 $dbo = JFactory::getDbo();
8817
8818 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
8819 $docs_data = [];
8820 $invoice_numb = false;
8821 $receipt_numb = false;
8822
8823 if ($booking['status'] == 'confirmed') {
8824 $q = "SELECT `b`.`id` AS `o_id`, `i`.`id` AS `inv_id`, `i`.`number` AS `inv_number`, `i`.`file_name` AS `inv_file_name`, `r`.`id` AS `rcp_id`, `r`.`number` AS `rcp_number`
8825 FROM `#__vikbooking_orders` AS `b`
8826 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
8827 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
8828 WHERE `b`.`id`=" . (int)$bid . ";";
8829 $dbo->setQuery($q);
8830 $docs_data = $dbo->loadAssoc();
8831 if ($docs_data) {
8832 $invoice_numb = (!empty($docs_data['inv_id']));
8833 $receipt_numb = (!empty($docs_data['rcp_id']));
8834
8835 /**
8836 * We inject the invoice number or receipt number into the color tag rules.
8837 * This is useful for the Tableaux page to show the invoice/receipt number.
8838 *
8839 * @since 1.12 (J) - 1.1.7 (WP)
8840 */
8841 if ($invoice_numb || $receipt_numb) {
8842 foreach ($tags as &$tval) {
8843 if ($invoice_numb) {
8844 $tval['invoice_number'] = $docs_data['inv_number'];
8845 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
8846 }
8847 if ($receipt_numb) {
8848 $tval['receipt_number'] = $docs_data['rcp_number'];
8849 }
8850 }
8851 // unset last reference
8852 unset($tval);
8853 }
8854 }
8855 }
8856
8857 /**
8858 * Some OTA bookings using an OTA-Collect business model may look as not entirely
8859 * paid when they actually are, due to commissions and pass through taxes. We also
8860 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
8861 *
8862 * @since 1.16.3 (J) - 1.6.3 (WP)
8863 */
8864 $ota_collected_booking = false;
8865 $hotel_collect_vcc_unpaid = false;
8866 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
8867 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
8868 $ota_collected_booking = true;
8869 }
8870 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
8871 // VCC detected, check the history for a payment event
8872 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
8873 // list of history types to fetch
8874 $h_payment_types = [];
8875 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
8876 if ($h_payment_group && $h_payment_group['types']) {
8877 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
8878 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
8879 }
8880 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
8881 if (!$payment_ev_datas) {
8882 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
8883 $hotel_collect_vcc_unpaid = true;
8884 }
8885 }
8886 }
8887
8888 foreach ($tags as $tkey => $tval) {
8889 if (empty($tval['rule'])) {
8890 continue;
8891 }
8892
8893 /**
8894 * Trigger event to let third-party plugins validate the custom rule.
8895 *
8896 * @since 1.16.4 (J) - 1.6.4 (WP)
8897 */
8898 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
8899 if (!empty($custom_rule)) {
8900 return (array) $custom_rule[0];
8901 }
8902
8903 switch ($tval['rule']) {
8904 case 'pend1':
8905 // Room is waiting for the payment (locked record)
8906 if ($booking['status'] == 'standby') {
8907 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
8908 $dbo->setQuery($q);
8909 $tmp_lock = $dbo->loadAssoc();
8910 if ($tmp_lock) {
8911 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8912 return $tval;
8913 }
8914 }
8915 break;
8916 case 'conf1':
8917 // Confirmed (Room Closed)
8918 if ($booking['status'] == 'confirmed' && $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED')) {
8919 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8920 return $tval;
8921 }
8922 break;
8923 case 'conf2':
8924 // Confirmed (No Rate 0.00/NULL Total)
8925 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
8926 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8927 return $tval;
8928 }
8929 break;
8930 case 'conf3':
8931 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
8932 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null || $hotel_collect_vcc_unpaid) && $invoice_numb === false && $receipt_numb === false) {
8933 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8934 return $tval;
8935 }
8936 break;
8937 case 'inv1':
8938 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
8939 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
8940 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8941 return $tval;
8942 }
8943 break;
8944 case 'rcp1':
8945 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
8946 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
8947 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8948 return $tval;
8949 }
8950 break;
8951 case 'conf4':
8952 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
8953 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
8954 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8955 return $tval;
8956 }
8957 break;
8958 case 'conf5':
8959 // Confirmed (Total > 0 && Total Paid >= Total)
8960 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
8961 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8962 return $tval;
8963 }
8964 break;
8965 case 'inv2':
8966 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
8967 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
8968 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8969 return $tval;
8970 }
8971 break;
8972 default:
8973 // unsupported rule
8974 break;
8975 }
8976 }
8977
8978 return [];
8979 }
8980
8981 public static function getBookingInfoFromID($bid)
8982 {
8983 $dbo = JFactory::getDbo();
8984
8985 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
8986 $dbo->setQuery($q);
8987 $booking_info = $dbo->loadAssoc();
8988 if ($booking_info) {
8989 return $booking_info;
8990 }
8991
8992 return [];
8993 }
8994
8995 /**
8996 * Load booking details for a given list of IDs.
8997 *
8998 * @param int $roomid the room integer number.
8999 * @param array $room_bids_pool list of booking IDs involved.
9000 *
9001 * @return array
9002 */
9003 public static function loadRoomIndexesBookings($roomid, array $room_bids_pool)
9004 {
9005 $dbo = JFactory::getDbo();
9006
9007 if (empty($roomid) || !$room_bids_pool) {
9008 return [];
9009 }
9010
9011 $room_features_bookings = [];
9012 $roomid = (int)$roomid;
9013
9014 $all_bids = [];
9015 foreach ($room_bids_pool as $day => $bids) {
9016 $all_bids = array_merge($all_bids, $bids);
9017 }
9018 $all_bids = array_map('intval', array_unique($all_bids));
9019 $all_bids_str = implode(', ', $all_bids);
9020
9021 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9022 $dbo->setQuery($q);
9023 $rbookings = $dbo->loadAssocList();
9024 if (!$rbookings) {
9025 return [];
9026 }
9027
9028 foreach ($rbookings as $k => $v) {
9029 if (empty($v['roomindex'])) {
9030 continue;
9031 }
9032 if (!isset($room_features_bookings[$v['roomindex']])) {
9033 $room_features_bookings[$v['roomindex']] = [];
9034 }
9035 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9036 }
9037
9038 return $room_features_bookings;
9039 }
9040
9041 public static function getSendEmailWhen()
9042 {
9043 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9044 }
9045
9046 public static function getMinutesAutoRemove()
9047 {
9048 return VBOFactory::getConfig()->getInt('minautoremove', 0);
9049 }
9050
9051 public static function getSMSAPIClass()
9052 {
9053 return VBOFactory::getConfig()->getString('smsapi', '');
9054 }
9055
9056 public static function autoSendSMSEnabled()
9057 {
9058 return VBOFactory::getConfig()->getBool('smsautosend', false);
9059 }
9060
9061 public static function getSendSMSTo()
9062 {
9063 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9064 }
9065
9066 public static function getSendSMSWhen()
9067 {
9068 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9069 }
9070
9071 public static function getSMSAdminPhone()
9072 {
9073 return VBOFactory::getConfig()->getString('smsadminphone', '');
9074 }
9075
9076 public static function getSMSParams($as_array = true)
9077 {
9078 if (!$as_array) {
9079 return VBOFactory::getConfig()->getString('smsparams', '');
9080 }
9081
9082 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9083 }
9084
9085 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9086 $dbo = JFactory::getDbo();
9087 switch (strtolower($booking_status)) {
9088 case 'standby':
9089 $status = 'pend';
9090 break;
9091 case 'cancelled':
9092 $status = 'canc';
9093 break;
9094 default:
9095 $status = '';
9096 break;
9097 }
9098 $paramtype = 'sms'.$type.'tpl'.$status;
9099 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9100 $dbo->setQuery($q);
9101 $dbo->execute();
9102 if ($dbo->getNumRows() < 1) {
9103 if ($status == 'canc') {
9104 //Type cancelled is used by VCM since v1.6.6
9105 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9106 $dbo->setQuery($q);
9107 $dbo->execute();
9108 }
9109 return '';
9110 }
9111 $ft = $dbo->loadAssocList();
9112 if (is_object($vbo_tn)) {
9113 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9114 }
9115 return $ft[0]['setting'];
9116 }
9117
9118 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9119 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9120 }
9121
9122 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9123 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9124 }
9125
9126 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9127 $dbo = JFactory::getDbo();
9128 $phone = str_replace(" ", '', trim($phone));
9129 $cprefix = '';
9130 if (!empty($country_threecode)) {
9131 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9132 $dbo->setQuery($q);
9133 $dbo->execute();
9134 if ($dbo->getNumRows() > 0) {
9135 $cprefix = $dbo->loadResult();
9136 $cprefix = str_replace(" ", '', trim($cprefix));
9137 }
9138 }
9139 if (!empty($cprefix)) {
9140 if (substr($phone, 0, 1) != '+') {
9141 if (substr($phone, 0, 2) == '00') {
9142 $phone = '+'.substr($phone, 2);
9143 } else {
9144 $phone = $cprefix.$phone;
9145 }
9146 }
9147 }
9148 return $phone;
9149 }
9150
9151 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9152 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9153
9154 /**
9155 * Trigger event to allow third-party plugins to manipulate the template string.
9156 *
9157 * @since 1.16.10 (J) - 1.6.10 (WP)
9158 */
9159 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9160
9161 /**
9162 * Parse all conditional text rules.
9163 *
9164 * @since 1.14 (J) - 1.4.0 (WP)
9165 */
9166 self::getConditionalRulesInstance()
9167 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9168 ->parseTokens($tpl);
9169
9170 $vbo_df = self::getDateFormat();
9171 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9172 $datesep = self::getDateSeparator();
9173
9174 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9175 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9176 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9177 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9178 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9179 if (!empty($booking['confirmnumber'])) {
9180 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9181 } else {
9182 $tpl = str_replace("{confirmnumb}", '', $tpl);
9183 }
9184 if (!empty($booking['idorderota'])) {
9185 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9186 } else {
9187 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9188 }
9189
9190 $rooms_booked = array();
9191 $rooms_names = array();
9192 $tot_adults = 0;
9193 $tot_children = 0;
9194 $tot_guests = 0;
9195 foreach ($booking_rooms as $broom) {
9196 $rooms_names[] = $broom['room_name'];
9197 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9198 $rooms_booked[$broom['room_name']] += 1;
9199 } else {
9200 $rooms_booked[$broom['room_name']] = 1;
9201 }
9202 $tot_adults += (int)$broom['adults'];
9203 $tot_children += (int)$broom['children'];
9204 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9205 }
9206 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9207 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9208 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9209 $rooms_booked_quant = array();
9210 foreach ($rooms_booked as $rname => $quant) {
9211 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9212 }
9213 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9214 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9215 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9216 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9217 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9218 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9219 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9220 $remaining_bal = $booking['total'] - $booking['totpaid'];
9221 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9222
9223 return $tpl;
9224 }
9225
9226 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9227 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9228
9229 /**
9230 * Trigger event to allow third-party plugins to manipulate the template string.
9231 *
9232 * @since 1.16.10 (J) - 1.6.10 (WP)
9233 */
9234 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9235
9236 /**
9237 * Parse all conditional text rules.
9238 *
9239 * @since 1.14 (J) - 1.4.0 (WP)
9240 */
9241 self::getConditionalRulesInstance()
9242 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9243 ->parseTokens($tpl);
9244
9245 $vbo_df = self::getDateFormat();
9246 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9247 $datesep = self::getDateSeparator();
9248
9249 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9250 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9251 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9252 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9253 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9254 if (!empty($booking['confirmnumber'])) {
9255 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9256 } else {
9257 $tpl = str_replace("{confirmnumb}", '', $tpl);
9258 }
9259 if (!empty($booking['idorderota'])) {
9260 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9261 } else {
9262 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9263 }
9264
9265 $rooms_booked = array();
9266 $rooms_names = array();
9267 $tot_adults = 0;
9268 $tot_children = 0;
9269 $tot_guests = 0;
9270 foreach ($booking_rooms as $broom) {
9271 $rooms_names[] = $broom['room_name'];
9272 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9273 $rooms_booked[$broom['room_name']] += 1;
9274 } else {
9275 $rooms_booked[$broom['room_name']] = 1;
9276 }
9277 $tot_adults += (int)$broom['adults'];
9278 $tot_children += (int)$broom['children'];
9279 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9280 }
9281 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9282 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9283 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9284 $rooms_booked_quant = array();
9285 foreach ($rooms_booked as $rname => $quant) {
9286 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9287 }
9288 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9289 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9290 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9291 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9292 $remaining_bal = $booking['total'] - $booking['totpaid'];
9293 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9294 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9295
9296 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9297 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9298
9299 if (VBOPlatformDetection::isWordPress()) {
9300 /**
9301 * @wponly Rewrite URI for front-end
9302 */
9303 $book_link = str_replace(JUri::root(), '', $book_link);
9304 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9305 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9306 if ($itemid) {
9307 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9308 }
9309 } else {
9310 /**
9311 * @joomlaonly Rewrite URI for front-end
9312 */
9313 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9314 $uri_parts = [
9315 'option' => 'com_vikbooking',
9316 'view' => 'booking',
9317 'sid' => $use_sid,
9318 'ts' => $booking['ts'],
9319 ];
9320 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9321 // inject lang in query string for proper link routing
9322 $uri_parts['lang'] = $booking['lang'];
9323 }
9324 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9325 }
9326
9327 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9328 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9329 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9330
9331 return $tpl;
9332 }
9333
9334 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9335 $dbo = JFactory::getDbo();
9336 if (!class_exists('VboApplication')) {
9337 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9338 }
9339 $vbo_app = new VboApplication;
9340 if (empty($oid)) {
9341 return false;
9342 }
9343 $sms_api = self::getSMSAPIClass();
9344 if (empty($sms_api)) {
9345 return false;
9346 }
9347 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
9348 return false;
9349 }
9350 $sms_api_params = self::getSMSParams();
9351 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
9352 return false;
9353 }
9354 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
9355 return false;
9356 }
9357 $send_sms_to = self::getSendSMSTo();
9358 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
9359 return false;
9360 }
9361 $booking = array();
9362 $q = "SELECT `o`.*,`co`.`idcustomer`,CONCAT_WS(' ',`c`.`first_name`,`c`.`last_name`) AS `customer_name`,`c`.`pin` AS `customer_pin`,`nat`.`country_name` FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_customers_orders` `co` ON `co`.`idorder`=`o`.`id` AND `co`.`idorder`=".(int)$oid." LEFT JOIN `#__vikbooking_customers` `c` ON `c`.`id`=`co`.`idcustomer` LEFT JOIN `#__vikbooking_countries` `nat` ON `nat`.`country_3_code`=`o`.`country` WHERE `o`.`id`=".(int)$oid.";";
9363 $dbo->setQuery($q);
9364 $dbo->execute();
9365 if ($dbo->getNumRows() > 0) {
9366 $booking = $dbo->loadAssoc();
9367 }
9368 if (!(count($booking) > 0)) {
9369 return false;
9370 }
9371 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
9372 return false;
9373 }
9374 $booking_rooms = array();
9375 $q = "SELECT `or`.*,`r`.`name` AS `room_name` FROM `#__vikbooking_ordersrooms` AS `or` LEFT JOIN `#__vikbooking_rooms` `r` ON `r`.`id`=`or`.`idroom` WHERE `or`.`idorder`=".(int)$booking['id'].";";
9376 $dbo->setQuery($q);
9377 $dbo->execute();
9378 if ($dbo->getNumRows() > 0) {
9379 $booking_rooms = $dbo->loadAssocList();
9380 }
9381 $admin_phone = self::getSMSAdminPhone();
9382 $admin_sendermail = self::getSenderMail();
9383 $admin_email = self::getAdminMail();
9384 $f_result = false;
9385 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
9386 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
9387 //SMS for the administrator
9388 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
9389 if (!empty($sms_text)) {
9390 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9391 //administrator phone can contain multiple numbers separated by comma or semicolon
9392 $admin_phones = array();
9393 if (strpos($admin_phone, ',') !== false) {
9394 $all_phones = explode(',', $admin_phone);
9395 foreach ($all_phones as $aph) {
9396 if (!empty($aph)) {
9397 $admin_phones[] = trim($aph);
9398 }
9399 }
9400 } elseif (strpos($admin_phone, ';') !== false) {
9401 $all_phones = explode(';', $admin_phone);
9402 foreach ($all_phones as $aph) {
9403 if (!empty($aph)) {
9404 $admin_phones[] = trim($aph);
9405 }
9406 }
9407 } else {
9408 $admin_phones[] = $admin_phone;
9409 }
9410 foreach ($admin_phones as $admphone) {
9411 $response_obj = $sms_obj->sendMessage($admphone, $sms_text);
9412 if ( !$sms_obj->validateResponse($response_obj) ) {
9413 //notify the administrator via email with the error of the SMS sending
9414 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9415 } else {
9416 $f_result = true;
9417 }
9418 }
9419 }
9420 }
9421 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
9422 //SMS for the Customer
9423 $vbo_tn = self::getTranslator();
9424 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
9425 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
9426 if (!empty($sms_text)) {
9427 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9428 $response_obj = $sms_obj->sendMessage($booking['phone'], $sms_text);
9429 if ( !$sms_obj->validateResponse($response_obj) ) {
9430 //notify the administrator via email with the error of the SMS sending
9431 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9432 } else {
9433 $f_result = true;
9434 }
9435 }
9436 }
9437 return $f_result;
9438 }
9439
9440 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
9441 {
9442 if (!defined('_VIKBOOKINGEXEC')) {
9443 define('_VIKBOOKINGEXEC', '1');
9444 }
9445
9446 ob_start();
9447 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
9448 $content = ob_get_contents();
9449 ob_end_clean();
9450
9451 $default_params = array(
9452 'show_header' => 0,
9453 'header_data' => array(),
9454 'show_footer' => 0,
9455 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9456 'pdf_unit' => 'PDF_UNIT',
9457 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9458 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9459 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9460 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9461 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9462 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9463 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9464 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9465 'header_font_size' => '10',
9466 'body_font_size' => '10',
9467 'footer_font_size' => '8',
9468 'show_lines_taxrate_col' => 0,
9469 );
9470
9471 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
9472 $default_params = array_merge($default_params, $invoice_params);
9473 }
9474
9475 return array($content, $default_params);
9476 }
9477
9478 /**
9479 * Includes within a buffer the template file for the custom (manual) invoice.
9480 *
9481 * @param array $invoice the record of the custom invoice
9482 * @param array $customer the customer record
9483 *
9484 * @return string the HTML content of the template file
9485 *
9486 * @since 1.11.1
9487 */
9488 public static function loadCustomInvoiceTmpl($invoice, $customer) {
9489 if (!defined('_VIKBOOKINGEXEC')) {
9490 define('_VIKBOOKINGEXEC', '1');
9491 }
9492 ob_start();
9493 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
9494 $content = ob_get_contents();
9495 ob_end_clean();
9496 $default_params = array(
9497 'show_header' => 0,
9498 'header_data' => array(),
9499 'show_footer' => 0,
9500 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9501 'pdf_unit' => 'PDF_UNIT',
9502 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9503 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9504 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9505 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9506 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9507 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9508 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9509 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9510 'header_font_size' => '10',
9511 'body_font_size' => '10',
9512 'footer_font_size' => '8'
9513 );
9514 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
9515 $default_params = array_merge($default_params, $invoice_params);
9516 }
9517 return array($content, $default_params);
9518 }
9519
9520 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
9521 {
9522 // clone the original template file source
9523 $parsed = $invoicetpl;
9524
9525 /**
9526 * Parse all conditional text rules.
9527 *
9528 * @since 1.14 (J) - 1.4.0 (WP)
9529 */
9530 self::getConditionalRulesInstance()
9531 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9532 ->parseTokens($parsed);
9533 //
9534
9535 $dbo = JFactory::getDbo();
9536 if (is_null($vbo_tn)) {
9537 $vbo_tn = self::getTranslator();
9538 }
9539 // availability helper
9540 $av_helper = self::getAvailabilityInstance();
9541
9542 $nowdf = self::getDateFormat();
9543 if ($nowdf == "%d/%m/%Y") {
9544 $df = 'd/m/Y';
9545 } elseif ($nowdf == "%m/%d/%Y") {
9546 $df = 'm/d/Y';
9547 } else {
9548 $df = 'Y/m/d';
9549 }
9550 $datesep = self::getDateSeparator();
9551
9552 /**
9553 * Split stay reservation.
9554 *
9555 * @since 1.16.0 (J) - 1.6.0 (WP)
9556 */
9557 $room_stay_dates = [];
9558 if ($booking['split_stay']) {
9559 if ($booking['status'] == 'confirmed') {
9560 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
9561 } else {
9562 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
9563 }
9564 // immediately count the number of nights of stay for each split room
9565 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
9566 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
9567 // overwrite values for compatibility with non-confirmed bookings
9568 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
9569 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
9570 }
9571 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
9572 // overwrite the whole array
9573 $room_stay_dates[$sps_r_k] = $sps_r_v;
9574 }
9575 }
9576
9577 $companylogo = self::getSiteLogo();
9578 $uselogo = '';
9579 if (!empty($companylogo)) {
9580 /**
9581 * Let's try to prevent TCPDF errors.
9582 *
9583 * @since August 2nd 2019
9584 */
9585 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9586 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9587 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9588 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9589 }
9590 }
9591 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
9592 $parsed = str_replace("{company_info}", $company_info, $parsed);
9593 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
9594 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
9595 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
9596 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
9597
9598 // custom fields replacement
9599 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
9600 if (is_array($cmatches[1]) && $cmatches[1]) {
9601 $cfids = array();
9602 foreach ($cmatches[1] as $cfid) {
9603 $cfids[] = $cfid;
9604 }
9605 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
9606 $dbo->setQuery($q);
9607 $cfields = $dbo->loadAssocList();
9608 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
9609 $cfmap = array();
9610 foreach ($cfields as $cf) {
9611 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
9612 }
9613 $cfmapreplace = array();
9614 $partsreceived = explode("\n", $booking['custdata']);
9615 if (count($partsreceived) > 0) {
9616 foreach ($partsreceived as $pst) {
9617 if (!empty($pst)) {
9618 $tmpdata = explode(":", $pst);
9619 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
9620 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
9621 }
9622 }
9623 }
9624 }
9625 foreach ($cmatches[1] as $cfid) {
9626 if (array_key_exists($cfid, $cfmapreplace)) {
9627 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
9628 } else {
9629 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
9630 }
9631 }
9632 }
9633
9634 // invoice price description - Start
9635 $rooms = array();
9636 $tars = array();
9637 $arrpeople = array();
9638 $is_package = !empty($booking['pkg']) ? true : false;
9639 $tot_adults = 0;
9640 $tot_children = 0;
9641 $tot_guests = 0;
9642 foreach ($booking_rooms as $kor => $or) {
9643 $num = $kor + 1;
9644 $rooms[$num] = $or;
9645
9646 // determine the days to consider for the count of the availability
9647 $room_nights = $booking['days'];
9648 $room_checkin = $booking['checkin'];
9649 $room_checkout = $booking['checkout'];
9650 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9651 $room_nights = $room_stay_dates[$kor]['nights'];
9652 $room_checkin = $room_stay_dates[$kor]['checkin'];
9653 $room_checkout = $room_stay_dates[$kor]['checkout'];
9654 }
9655
9656 $arrpeople[$num]['adults'] = $or['adults'];
9657 $arrpeople[$num]['children'] = $or['children'];
9658 $tot_adults += $or['adults'];
9659 $tot_children += $or['children'];
9660 $tot_guests += ($or['adults'] + $or['children']);
9661 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9662 //package or custom cost set from the back-end
9663 continue;
9664 }
9665 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
9666 $dbo->setQuery($q);
9667 $tar = $dbo->loadAssocList();
9668 if ($tar) {
9669 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
9670 //different usage
9671 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
9672 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
9673 //Occupancy Override
9674 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
9675 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
9676 //
9677 if (is_array($diffusageprice)) {
9678 //set a charge or discount to the price(s) for the different usage of the room
9679 foreach ($tar as $kpr => $vpr) {
9680 $tar[$kpr]['diffusage'] = $or['adults'];
9681 if ($diffusageprice['chdisc'] == 1) {
9682 //charge
9683 if ($diffusageprice['valpcent'] == 1) {
9684 //fixed value
9685 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9686 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9687 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
9688 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9689 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
9690 } else {
9691 //percentage value
9692 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9693 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
9694 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
9695 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9696 $tar[$kpr]['cost'] = $aduseval;
9697 }
9698 } else {
9699 //discount
9700 if ($diffusageprice['valpcent'] == 1) {
9701 //fixed value
9702 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9703 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9704 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
9705 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9706 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
9707 } else {
9708 //percentage value
9709 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9710 $aduseval = $diffusageprice['pernight'] == 1 ? round($vpr['cost'] - ((($vpr['cost'] / $tar[$kpr]['days']) * $diffusageprice['value'] / 100) * $tar[$kpr]['days']), 2) : round(($vpr['cost'] * (100 - $diffusageprice['value']) / 100), 2);
9711 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
9712 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9713 $tar[$kpr]['cost'] = $aduseval;
9714 }
9715 }
9716 }
9717 }
9718 }
9719 //
9720 $tars[$num] = $tar[0];
9721 }
9722 }
9723 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
9724 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
9725 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
9726 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
9727 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
9728 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
9729
9730 $isdue = 0;
9731 $tot_taxes = 0;
9732 $tot_city_taxes = 0;
9733 $tot_fees = 0;
9734 $pricestr = array();
9735 $optstr = array();
9736 // start building the tax summary
9737 VBOTaxonomySummary::start();
9738 foreach ($booking_rooms as $kor => $or) {
9739 $num = $kor + 1;
9740
9741 // determine the days to consider for the count of the availability
9742 $room_nights = $booking['days'];
9743 $room_checkin = $booking['checkin'];
9744 $room_checkout = $booking['checkout'];
9745 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9746 $room_nights = $room_stay_dates[$kor]['nights'];
9747 $room_checkin = $room_stay_dates[$kor]['checkin'];
9748 $room_checkout = $room_stay_dates[$kor]['checkout'];
9749 }
9750
9751 $pricestr[$num] = array();
9752 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9753 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
9754 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
9755 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
9756 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
9757 $pricestr[$num]['tot'] = $calctar;
9758 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
9759 $tot_taxes += ($calctar - $cost_minus_tax);
9760 $isdue += $calctar;
9761 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
9762 $pricestr[$num]['tax_rate'] = $tax_rate;
9763 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
9764 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9765 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
9766 $pricestr[$num]['name'] = self::getPriceName($tars[$num]['idprice'], $vbo_tn) . (!empty($tars[$num]['attrdata']) ? "\n" . self::getPriceAttr($tars[$num]['idprice'], $vbo_tn) . ": " . $tars[$num]['attrdata'] : "");
9767 $pricestr[$num]['tot'] = $calctar;
9768 $tars[$num]['calctar'] = $calctar;
9769 $isdue += $calctar;
9770 if ($calctar == $display_rate) {
9771 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
9772 $tot_taxes += ($display_rate - $cost_minus_tax);
9773 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
9774 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
9775 $pricestr[$num]['tax_rate'] = $tax_rate;
9776 } else {
9777 $tot_taxes += ($calctar - $display_rate);
9778 $pricestr[$num]['tax'] = ($calctar - $display_rate);
9779 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
9780 $pricestr[$num]['tax_rate'] = $tax_rate;
9781 }
9782 }
9783 $optstr[$num] = array();
9784 $opt_ind = 0;
9785 if (!empty($or['optionals'])) {
9786 $stepo = explode(";", $or['optionals']);
9787 foreach ($stepo as $roptkey => $oo) {
9788 if (!empty($oo)) {
9789 $stept = explode(":", $oo);
9790 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
9791 $dbo->setQuery($q);
9792 $actopt = $dbo->loadAssocList();
9793 if ($actopt) {
9794 if (is_object($vbo_tn)) {
9795 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
9796 }
9797 $optstr[$num][$opt_ind] = array();
9798 $chvar = '';
9799 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
9800 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
9801 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
9802 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
9803 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
9804 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
9805 $agestept = explode('-', $stept[1]);
9806 $stept[1] = $agestept[0];
9807 $chvar = $agestept[1];
9808 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
9809 //percentage value of the adults tariff
9810 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9811 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9812 } else {
9813 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9814 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9815 }
9816 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
9817 //VBO 1.10 - percentage value of room base cost
9818 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9819 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9820 } else {
9821 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
9822 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9823 }
9824 }
9825 $actopt[0]['chageintv'] = $chvar;
9826 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
9827 $actopt[0]['quan'] = $stept[1];
9828 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
9829 } else {
9830 $actopt[0]['quan'] = $stept[1];
9831 // VBO 1.11 - options percentage cost of the room total fee
9832 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9833 $deftar_basecosts = $or['cust_cost'];
9834 } else {
9835 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9836 }
9837 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
9838 //
9839 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
9840 }
9841 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
9842 $realcost = $actopt[0]['maxprice'];
9843 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
9844 $realcost = $actopt[0]['maxprice'] * $stept[1];
9845 }
9846 }
9847 if ($actopt[0]['perperson'] == 1) {
9848 $realcost = $realcost * $or['adults'];
9849 }
9850 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
9851 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
9852 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
9853 $optstr[$num][$opt_ind]['tax'] = 0;
9854 if ($actopt[0]['is_citytax'] == 1) {
9855 $tot_city_taxes += $tmpopr;
9856 } elseif ($actopt[0]['is_fee'] == 1) {
9857 $tot_fees += $tmpopr;
9858 }
9859 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
9860 if ($tmpopr == $realcost) {
9861 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
9862 $tot_taxes += ($realcost - $opt_minus_tax);
9863 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
9864 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
9865 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9866 } else {
9867 $tot_taxes += ($tmpopr - $realcost);
9868 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
9869 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
9870 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9871 }
9872 //
9873 $opt_ind++;
9874 $isdue += $tmpopr;
9875 }
9876 }
9877 }
9878 }
9879
9880 // custom extra costs
9881 if (!empty($or['extracosts'])) {
9882 $cur_extra_costs = json_decode($or['extracosts'], true);
9883 foreach ($cur_extra_costs as $eck => $ecv) {
9884 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9885 $isdue += $ecplustax;
9886 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
9887 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
9888 $optstr[$num][$opt_ind]['tax'] = 0;
9889 if ($ecplustax == $ecv['cost']) {
9890 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9891 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
9892 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
9893 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
9894 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9895 } else {
9896 $tot_taxes += ($ecplustax - $ecv['cost']);
9897 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
9898 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
9899 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9900 }
9901 $opt_ind++;
9902 }
9903 }
9904 }
9905
9906 $usedcoupon = false;
9907 if (strlen($booking['coupon']) > 0) {
9908 $orig_isdue = $isdue;
9909 $expcoupon = explode(";", $booking['coupon']);
9910 $usedcoupon = $expcoupon;
9911 $isdue = $isdue - (float)$expcoupon[1];
9912 if ($isdue != $orig_isdue) {
9913 //lower taxes proportionally
9914 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9915 }
9916 }
9917 if ($booking['refund'] > 0) {
9918 $orig_isdue = $isdue;
9919 $isdue -= $booking['refund'];
9920 if ($isdue != $orig_isdue) {
9921 //lower taxes proportionally
9922 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9923 }
9924 }
9925 $rows_written = 0;
9926 $inv_rows = '';
9927 foreach ($pricestr as $num => $price_descr) {
9928 // add room split stay information, if any
9929 $split_stay_str = '';
9930 $kor = $num - 1;
9931 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
9932 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
9933 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
9934 }
9935
9936 $inv_rows .= '<tr>'."\n";
9937 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
9938 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($price_descr['tot'] - $price_descr['tax'])).'</td>'."\n";
9939 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tax']).'</td>'."\n";
9940 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tot']).'</td>'."\n";
9941 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9942 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
9943 }
9944 $inv_rows .= '</tr>'."\n";
9945 $rows_written++;
9946 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
9947 foreach ($optstr[$num] as $optk => $optv) {
9948 $inv_rows .= '<tr>'."\n";
9949 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
9950 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($optv['tot'] - $optv['tax'])).'</td>'."\n";
9951 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tax']).'</td>'."\n";
9952 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tot']).'</td>'."\n";
9953 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9954 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
9955 }
9956 $inv_rows .= '</tr>'."\n";
9957 $rows_written++;
9958 }
9959 }
9960 }
9961
9962 // if discount print row
9963 if ($usedcoupon !== false) {
9964 $inv_rows .= '<tr>'."\n";
9965 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9966 $inv_rows .= '</tr>'."\n";
9967 $inv_rows .= '<tr>'."\n";
9968 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
9969 $inv_rows .= '<td></td>'."\n";
9970 $inv_rows .= '<td></td>'."\n";
9971 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($usedcoupon[1]).'</td>'."\n";
9972 $inv_rows .= '</tr>'."\n";
9973 $rows_written += 2;
9974 }
9975 // if refunded amount, print row
9976 if ($booking['refund'] > 0) {
9977 $inv_rows .= '<tr>'."\n";
9978 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9979 $inv_rows .= '</tr>'."\n";
9980 $inv_rows .= '<tr>'."\n";
9981 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
9982 $inv_rows .= '<td></td>'."\n";
9983 $inv_rows .= '<td></td>'."\n";
9984 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($booking['refund']).'</td>'."\n";
9985 $inv_rows .= '</tr>'."\n";
9986 $rows_written += 2;
9987 }
9988 //
9989 $min_records = 8;
9990 if ($rows_written < $min_records) {
9991 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
9992 $inv_rows .= '<tr>'."\n";
9993 $inv_rows .= '<td></td>'."\n";
9994 $inv_rows .= '<td></td>'."\n";
9995 $inv_rows .= '<td></td>'."\n";
9996 $inv_rows .= '</tr>'."\n";
9997 }
9998 }
9999 //invoice price description - End
10000 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10001 $parsed = str_replace("{invoice_totalnet}", $booking['currencyname'].' '.self::numberFormat(($isdue - $tot_taxes)), $parsed);
10002 $parsed = str_replace("{invoice_totaltax}", $booking['currencyname'].' '.self::numberFormat($tot_taxes), $parsed);
10003 $parsed = str_replace("{invoice_grandtotal}", $booking['currencyname'].' '.self::numberFormat($isdue), $parsed);
10004 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10005
10006 // invoice tax summary
10007 $tax_summary = VBOTaxonomySummary::get();
10008 $tax_names = VBOTaxonomySummary::getNames();
10009 $taxsum_rows = "";
10010 foreach ($tax_summary as $tax_rate => $tax_amount) {
10011 if (!$tax_rate && !$tax_amount) {
10012 continue;
10013 }
10014 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10015 $taxsum_rows .= '<tr>' . "\n";
10016 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10017 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10018 $taxsum_rows .= '<td>' . $booking['currencyname'] . ' ' . self::numberFormat($tax_amount) . '</td>' . "\n";
10019 $taxsum_rows .= '</tr>' . "\n";
10020 }
10021 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10022
10023 return $parsed;
10024 }
10025
10026 /**
10027 * Specific method for parsing the template file for the custom (manual) invoices.
10028 *
10029 * @param string $invoicetpl the plain custom invoice template file before parsing
10030 * @param array $invoice the invoice record
10031 * @param array $customer the customer record
10032 *
10033 * @return string the HTML content of the parsed custom invoice template
10034 *
10035 * @since 1.11.1
10036 */
10037 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10038 $nowdf = self::getDateFormat(true);
10039 if ($nowdf == "%d/%m/%Y") {
10040 $df = 'd/m/Y';
10041 } elseif ($nowdf == "%m/%d/%Y") {
10042 $df = 'm/d/Y';
10043 } else {
10044 $df = 'Y/m/d';
10045 }
10046 $datesep = self::getDateSeparator(true);
10047 $companylogo = self::getSiteLogo();
10048 $uselogo = '';
10049 if (!empty($companylogo)) {
10050 /**
10051 * Let's try to prevent TCPDF errors by checking if the file exists.
10052 *
10053 * @since August 2nd 2019
10054 */
10055 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10056 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10057 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10058 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10059 }
10060 }
10061 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10062 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10063 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10064 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10065 // customer information
10066 $custinfo = '';
10067 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10068 $custinfo .= $customer['email'] . "\n";
10069 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10070 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10071 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10072 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10073 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10074 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10075 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10076 // invoice notes
10077 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10078
10079 return $invoicetpl;
10080 }
10081
10082 /**
10083 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10084 *
10085 * @param array $booking the booking information.
10086 * @param int $invoice_num the invoice number.
10087 * @param string $invoice_suff the invoice suffix.
10088 * @param string $invoice_date the invoice date.
10089 * @param string $company_info optional company information.
10090 * @param bool $translate whether the invoice contents should be translated in the booking language.
10091 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10092 *
10093 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10094 *
10095 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $refresh_pdf to be used by electronic invoicing drivers after a succesful invoice transmission.
10096 */
10097 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10098 {
10099 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10100 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10101 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10102
10103 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10104 return false;
10105 }
10106
10107 /**
10108 * Trigger event to allow third party plugins to override some values for the invoice generation.
10109 *
10110 * @since 1.16.3 (J) - 1.6.3 (WP)
10111 */
10112 $event_args = [
10113 'invoice_num' => $invoice_num,
10114 'invoice_suff' => $invoice_suff,
10115 'invoice_date' => $invoice_date,
10116 'company_info' => $company_info,
10117 'translate' => $translate,
10118 ];
10119 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10120 if (is_array($event_args) && $event_args) {
10121 // extract the new values to be used
10122 extract($event_args);
10123 }
10124
10125 $dbo = JFactory::getDbo();
10126 $vbo_tn = self::getTranslator();
10127
10128 // inject the currency name value
10129 $currencyname = self::getCurrencyName();
10130 $booking['currencyname'] = $currencyname;
10131
10132 // make sure we've got a customer ID assigned to the reservation record
10133 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10134 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10135 if ($booking_customer) {
10136 $booking['idcustomer'] = $booking_customer['id'];
10137 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10138 $booking['customer_pin'] = $booking_customer['pin'];
10139 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10140 }
10141 }
10142
10143 $nowdf = self::getDateFormat(true);
10144 if ($nowdf == "%d/%m/%Y") {
10145 $df = 'd/m/Y';
10146 } elseif ($nowdf == "%m/%d/%Y") {
10147 $df = 'm/d/Y';
10148 } else {
10149 $df = 'Y/m/d';
10150 }
10151 $datesep = self::getDateSeparator(true);
10152
10153 if (empty($invoice_date)) {
10154 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10155 $used_date = $booking['ts'];
10156 } else {
10157 /**
10158 * We could be re-generating an invoice for a booking that already had an invoice.
10159 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10160 * It is now possible to keep the same invoice date as the previous one, so check
10161 * what value contains $invoice_date to see if it's different from today's date.
10162 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10163 * to also check the length of the string $invoice_date before using that date.
10164 *
10165 * @since 1.10 - August 2018
10166 */
10167 $base_ts = time();
10168 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10169 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10170 }
10171 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10172 $used_date = $base_ts;
10173 }
10174
10175 // load room reservation records
10176 $q = $dbo->getQuery(true)
10177 ->select($dbo->qn('or') . '.*')
10178 ->select($dbo->qn('r.name', 'room_name'))
10179 ->select($dbo->qn('r.fromadult'))
10180 ->select($dbo->qn('r.toadult'))
10181 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10182 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10183 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10184 ->order($dbo->qn('or.id') . ' ASC');
10185
10186 $dbo->setQuery($q);
10187 $booking_rooms = $dbo->loadAssocList();
10188
10189 if (!$booking_rooms) {
10190 return false;
10191 }
10192
10193 // check if an invoice for this reservation exists
10194 $invoice_id = 0;
10195 $dbo->setQuery(
10196 $dbo->getQuery(true)
10197 ->select('*')
10198 ->from($dbo->qn('#__vikbooking_invoices'))
10199 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10200 ->order($dbo->qn('id') . ' DESC')
10201 );
10202 $invoice_data = $dbo->loadAssoc();
10203 if ($invoice_data) {
10204 $invoice_id = $invoice_data['id'];
10205 }
10206
10207 // if requested, prepare data to refresh the PDF invoice
10208 if ($refresh_pdf === true) {
10209 if (!$invoice_id || !$invoice_data) {
10210 // cannot refresh the PDF if an invoice was never created before
10211 return false;
10212 }
10213
10214 // load the same exact previous information to refresh the invoice
10215 $invoice_num = $invoice_data['number'];
10216 $invoice_suff = $invoice_suff;
10217 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10218 $used_date = $invoice_data['for_date'];
10219
10220 // parse the previous (current) invoice number
10221 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10222 if ($matches) {
10223 // overwrite values
10224 $invoice_num = $matches[1] ?: $invoice_num;
10225 $invoice_suff = $matches[2] ?: $invoice_suff;
10226 }
10227 }
10228
10229 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10230 if ($translate === true) {
10231 if (!empty($booking['lang'])) {
10232 $lang = JFactory::getLanguage();
10233 if ($lang->getTag() != $booking['lang']) {
10234 if (VBOPlatformDetection::isWordPress()) {
10235 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10236 } else {
10237 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10238 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10239 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10240 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10241 }
10242 }
10243 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10244 // force the translation to start because contents should be translated
10245 $vbo_tn::$force_tolang = $booking['lang'];
10246 }
10247 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10248 }
10249 }
10250
10251 // the PDF file name
10252 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10253
10254 /**
10255 * If an analogic invoice is not available, make sure to create the record
10256 * before letting the e-invoicing drivers run to avoid a loop.
10257 *
10258 * @since 1.16.7 (J) - 1.6.7 (WP)
10259 */
10260 $retval = null;
10261 if (!$refresh_pdf && !$invoice_id) {
10262 // prepare record object
10263 $invoice_record = new stdClass;
10264 $invoice_record->number = $invoice_num . $invoice_suff;
10265 $invoice_record->file_name = $pdffname;
10266 $invoice_record->idorder = (int)$booking['id'];
10267 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10268 $invoice_record->created_on = time();
10269 $invoice_record->for_date = (int)$used_date;
10270
10271 // insert record
10272 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10273 return false;
10274 }
10275
10276 $retval = $invoice_record->id ?? false;
10277
10278 if (!$retval) {
10279 return false;
10280 }
10281 }
10282
10283 /**
10284 * The generation of the analogic invoices can trigger the drivers for the
10285 * generation of the e-Invoices if they are set to automatically run.
10286 * However, the e-Invoicing classes may be calling this method, so we need
10287 * to make sure the eInvoicing class is not running before proceeding.
10288 * This method could be called within a loop, and so the second iterations
10289 * may already have loaded the eInvocing class. For this we use a static variable.
10290 *
10291 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10292 * though additional information may be available only after the transmission. On top
10293 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10294 */
10295 static $einvocing_can_run = false;
10296
10297 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10298 // allow second iterations calling this method to run
10299 $einvocing_can_run = true;
10300 }
10301
10302 // always load all drivers, db and plugin based ones
10303 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10304
10305 foreach ($drivers as $driver_obj) {
10306 // set the flag of the external call
10307 $driver_obj->externalCall = __METHOD__;
10308
10309 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10310 $driver_obj->externalData['einvnum'] = $invoice_num;
10311
10312 // let the driver eventually elaborate the booking information before invoicing
10313 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10314
10315 if ($einvocing_can_run === true && !$refresh_pdf) {
10316 // generate the e-Invoice
10317 $driver_err = '';
10318 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10319 $driver_err = $driver_obj->getError();
10320 VikError::raiseWarning('', $driver_err);
10321 }
10322
10323 // Booking History - store log for the e-Invoice result
10324 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10325 }
10326 }
10327
10328 /**
10329 * Start the generation of the analogic (courtesy) PDF invoice.
10330 */
10331
10332 // load dependencies
10333 if (!class_exists('TCPDF')) {
10334 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10335 }
10336 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10337
10338 /**
10339 * Trigger event to allow third party plugins to return a specific font name.
10340 *
10341 * @since 1.16.0 (J) - 1.6.0 (WP)
10342 */
10343 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10344 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10345 $usepdffont = $custom_pdf_font[0];
10346 }
10347
10348 // set array variable to the template file
10349 $booking_info = self::getBookingInfoFromID($booking['id']);
10350
10351 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
10352
10353 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
10354
10355 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10356
10357 if (is_file($pathpdf)) {
10358 @unlink($pathpdf);
10359 }
10360
10361 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10362
10363 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10364 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
10365
10366 // header for each page of the pdf
10367 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
10368 $pdf->SetHeaderData($pdfparams['header_data'][0], $pdfparams['header_data'][1], $pdfparams['header_data'][2], $pdfparams['header_data'][3], $pdfparams['header_data'][4], $pdfparams['header_data'][5]);
10369 }
10370
10371 // change some currencies to their unicode (decimal) value
10372 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10373 if (array_key_exists($booking['currencyname'], $unichr_map)) {
10374 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
10375 }
10376
10377 // header and footer fonts
10378 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10379 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10380
10381 // margins
10382 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10383 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10384 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10385
10386 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10387 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10388 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10389
10390 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10391 $pdf->SetPrintHeader(false);
10392 }
10393 if ($pdfparams['show_footer'] == 0) {
10394 $pdf->SetPrintFooter(false);
10395 }
10396
10397 $pdf->AddPage();
10398 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10399 $pdf->lastPage();
10400 $pdf->Output($pathpdf, 'F');
10401
10402 if (!is_file($pathpdf)) {
10403 return false;
10404 }
10405
10406 if (VBOPlatformDetection::isWordPress()) {
10407 /**
10408 * @wponly - trigger files mirroring
10409 */
10410 VikBookingLoader::import('update.manager');
10411 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10412 }
10413
10414 // Booking History
10415 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
10416
10417 if ($retval) {
10418 // a new invoice ID was inserted above
10419 return $retval;
10420 }
10421
10422 // prepare record object to be updated (if insert needed, it's done at first)
10423 $invoice_record = new stdClass;
10424 $invoice_record->id = (int)$invoice_id;
10425 $invoice_record->number = $invoice_num . $invoice_suff;
10426 $invoice_record->file_name = $pdffname;
10427 $invoice_record->idorder = (int)$booking['id'];
10428 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10429 $invoice_record->created_on = time();
10430 $invoice_record->for_date = (int)$used_date;
10431
10432 // update record
10433 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
10434
10435 // return the PDF invoice ID
10436 return $invoice_id;
10437 }
10438
10439 /**
10440 * Generates an analogic invoice in PDF format for a custom list of services.
10441 * No bookings are assigned to this custom invoice. The method parses the same
10442 * invoice template as for the regular process with real booking IDs.
10443 * The invoice number must be stored before calling this method.
10444 *
10445 * @param int $invoice_id the ID of the custom invoice record
10446 *
10447 * @return bool
10448 *
10449 * @since 1.11.1 (J) - 1.1.1 (WP)
10450 */
10451 public static function generateCustomInvoice($invoice_id)
10452 {
10453 $dbo = JFactory::getDbo();
10454
10455 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
10456 $dbo->setQuery($q);
10457 $invoice = $dbo->loadAssoc();
10458 if (!$invoice) {
10459 return false;
10460 }
10461
10462 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
10463 $rawcont = is_array($rawcont) ? $rawcont : array();
10464 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
10465 $invoice['rawcont'] = $rawcont;
10466 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
10467 if (!$rawcont || !$rows || !$customer) {
10468 // at least one invoice raw is mandatory as well as the customer
10469 return false;
10470 }
10471
10472 /**
10473 * Start the generation of the electronic invoice, if any driver was enabled.
10474 *
10475 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10476 * though additional information would be available only after the transmission.
10477 */
10478 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10479
10480 if ($drivers) {
10481 // make sure the invoice number is just a number
10482 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
10483
10484 // invoke all drivers that should run automatically
10485 foreach ($drivers as $driver_obj) {
10486 // set the flag of the external call
10487 $driver_obj->externalCall = __METHOD__;
10488
10489 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
10490 $driver_obj->externalData['einvnum'] = $invoice['number'];
10491 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
10492 $driver_obj->externalData['einvcustom'] = $invoice;
10493
10494 // prepare data array for the generation of the e-Invoice
10495 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
10496
10497 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
10498 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
10499 if ($preveinv !== false) {
10500 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
10501 }
10502
10503 // generate the e-Invoice
10504 $driver_err = '';
10505 if (!$driver_obj->generateEInvoice($einvdata)) {
10506 $driver_err = $driver_obj->getError();
10507 VikError::raiseWarning('', $driver_err);
10508 }
10509 }
10510 }
10511
10512 /**
10513 * Start the generation of the analogic (courtesy) PDF invoice.
10514 */
10515
10516 // load dependencies
10517 if (!class_exists('TCPDF')) {
10518 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10519 }
10520 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10521
10522 /**
10523 * Trigger event to allow third party plugins to return a specific font name.
10524 *
10525 * @since 1.16.0 (J) - 1.6.0 (WP)
10526 */
10527 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10528 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10529 $usepdffont = $custom_pdf_font[0];
10530 }
10531
10532 // load invoice template file
10533 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
10534
10535 // parse invoice template file
10536 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
10537
10538 $pdffname = $invoice['file_name'];
10539 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10540
10541 if (is_file($pathpdf)) {
10542 @unlink($pathpdf);
10543 }
10544
10545 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10546
10547 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10548 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
10549
10550 // header for each page of the pdf
10551 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
10552 $pdf->SetHeaderData($pdfparams['header_data'][0], $pdfparams['header_data'][1], $pdfparams['header_data'][2], $pdfparams['header_data'][3], $pdfparams['header_data'][4], $pdfparams['header_data'][5]);
10553 }
10554
10555 // change some currencies to their unicode (decimal) value
10556 $currencyname = self::getCurrencyName();
10557 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10558 if (array_key_exists($currencyname, $unichr_map)) {
10559 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
10560 }
10561
10562 // header and footer fonts
10563 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10564 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10565
10566 // margins
10567 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10568 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10569 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10570
10571 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10572 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10573 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10574
10575 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10576 $pdf->SetPrintHeader(false);
10577 }
10578 if ($pdfparams['show_footer'] == 0) {
10579 $pdf->SetPrintFooter(false);
10580 }
10581
10582 $pdf->AddPage();
10583 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10584 $pdf->lastPage();
10585 $pdf->Output($pathpdf, 'F');
10586
10587 if (!is_file($pathpdf)) {
10588 return false;
10589 }
10590
10591 if (VBOPlatformDetection::isWordPress()) {
10592 /**
10593 * @wponly - trigger files mirroring
10594 */
10595 VikBookingLoader::import('update.manager');
10596 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10597 }
10598
10599 // return the result of the generation of the PDF invoice
10600 return true;
10601 }
10602
10603 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
10604 {
10605 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
10606 return false;
10607 }
10608
10609 $dbo = JFactory::getDbo();
10610
10611 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
10612 $dbo->setQuery($q);
10613 $invoice_data = $dbo->loadAssoc();
10614
10615 if (!$invoice_data) {
10616 return false;
10617 }
10618
10619 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
10620 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
10621 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
10622 if (!file_exists($invoice_file_path)) {
10623 return false;
10624 }
10625
10626 $vbo_app = self::getVboApplication();
10627 $admin_sendermail = self::getSenderMail();
10628
10629 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $booking['custmail'], $admin_sendermail, $mail_subject, $mail_text, (strpos($mail_text, '<') !== false && strpos($mail_text, '>') !== false ? true : false), 'base64', $invoice_file_path);
10630
10631 // update record
10632 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
10633 $dbo->setQuery($q);
10634 $dbo->execute();
10635
10636 return true;
10637 }
10638
10639 public static function loadCheckinDocTmpl($booking_info = array(), $booking_rooms = array(), $customer = array()) {
10640 if (!defined('_VIKBOOKINGEXEC')) {
10641 define('_VIKBOOKINGEXEC', '1');
10642 }
10643 ob_start();
10644 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "checkins" . DIRECTORY_SEPARATOR . "checkin_tmpl.php";
10645 $content = ob_get_contents();
10646 ob_end_clean();
10647 $default_params = array(
10648 'show_header' => 0,
10649 'header_data' => array(),
10650 'show_footer' => 0,
10651 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10652 'pdf_unit' => 'PDF_UNIT',
10653 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10654 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10655 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10656 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10657 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10658 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10659 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10660 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10661 'header_font_size' => '10',
10662 'body_font_size' => '10',
10663 'footer_font_size' => '8'
10664 );
10665 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
10666 $default_params = array_merge($default_params, $checkin_params);
10667 }
10668 return array($content, $default_params);
10669 }
10670
10671 public static function parseCheckinDocTemplate($checkintpl, $booking, $booking_rooms, $customer) {
10672 $parsed = $checkintpl;
10673 $dbo = JFactory::getDbo();
10674 $app = JFactory::getApplication();
10675 $nowdf = self::getDateFormat();
10676 if ($nowdf == "%d/%m/%Y") {
10677 $df='d/m/Y';
10678 } elseif ($nowdf == "%m/%d/%Y") {
10679 $df='m/d/Y';
10680 } else {
10681 $df='Y/m/d';
10682 }
10683 $datesep = self::getDateSeparator();
10684 $companylogo = self::getSiteLogo();
10685 $uselogo = '';
10686 if (!empty($companylogo)) {
10687 /**
10688 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
10689 *
10690 * @since March 16th 2021
10691 */
10692 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10693 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10694 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
10695 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10696 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10697 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
10698 }
10699 }
10700 $company_name = self::getFrontTitle();
10701 $company_info = self::getInvoiceCompanyInfo();
10702 $parsed = str_replace("{company_name}", $company_name, $parsed);
10703 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10704 $parsed = str_replace("{company_info}", $company_info, $parsed);
10705 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10706 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10707 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10708 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10709 $tot_guests = 0;
10710 $tot_adults = 0;
10711 $tot_children = 0;
10712 foreach ($booking_rooms as $kor => $or) {
10713 $tot_guests += ($or['adults'] + $or['children']);
10714 $tot_adults += $or['adults'];
10715 $tot_children += $or['children'];
10716 }
10717 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10718 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10719 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10720 if (count($customer) && isset($customer['comments'])) {
10721 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
10722 }
10723 $termsconds = self::getTermsConditions();
10724 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
10725 //custom fields replace
10726 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10727 if (is_array($cmatches[1]) && $cmatches[1]) {
10728 $cfids = array();
10729 foreach ($cmatches[1] as $cfid) {
10730 $cfids[] = $cfid;
10731 }
10732 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10733 $dbo->setQuery($q);
10734 $cfields = $dbo->loadAssocList();
10735 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10736 $cfmap = array();
10737 foreach ($cfields as $cf) {
10738 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10739 }
10740 $cfmapreplace = array();
10741 $partsreceived = explode("\n", $booking['custdata']);
10742 if (count($partsreceived) > 0) {
10743 foreach ($partsreceived as $pst) {
10744 if (!empty($pst)) {
10745 $tmpdata = explode(":", $pst);
10746 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10747 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10748 }
10749 }
10750 }
10751 }
10752 foreach ($cmatches[1] as $cfid) {
10753 if (array_key_exists($cfid, $cfmapreplace)) {
10754 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10755 } else {
10756 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10757 }
10758 }
10759 }
10760 //end custom fields replace
10761
10762 return $parsed;
10763 }
10764
10765 /**
10766 * Returns an array of key-value pairs to be used for building the Guest Details
10767 * in the Check-in process. The keys will be compared to the fields of the table
10768 * #__customers to see if some values already exist. The values can use lang defs
10769 * of both front-end or back-end. To be called as list(fields, attributes).
10770 *
10771 * @param boolean $precheckin true if requested for front-end pre check-in.
10772 * @param string $type optional type of custom pax fields to force.
10773 *
10774 * @return array key-value pairs for showing and collecting guest details.
10775 *
10776 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
10777 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
10778 */
10779 public static function getPaxFields($precheckin = false, $type = null)
10780 {
10781 if (!$precheckin) {
10782 // back-end check-in ("registration") key-value pairs
10783
10784 // check the type of pax fields collection data
10785 $collection_type = !empty($type) ? $type : VBOFactory::getConfig()->getString('checkindata', 'basic');
10786
10787 // return the requested pax fields collection list
10788 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
10789
10790 if (is_array($custom_pax_fields) && count($custom_pax_fields)) {
10791 // requested driver returned a list of fields
10792 return $custom_pax_fields;
10793 }
10794
10795 // in case the driver is unsupported, fallback to "basic" (default) driver
10796 return VBOCheckinPax::getFields('basic');
10797 }
10798
10799 // front-end key-value pairs for pre check-in
10800 $precheckin_pax_fields = [
10801 [
10802 'first_name' => JText::translate('VBCCFIRSTNAME'),
10803 'last_name' => JText::translate('VBCCLASTNAME'),
10804 'date_birth' => JText::translate('ORDER_DBIRTH'),
10805 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
10806 'country' => JText::translate('ORDER_STATE'),
10807 'city' => JText::translate('ORDER_CITY'),
10808 'zip' => JText::translate('ORDER_ZIP'),
10809 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
10810 'gender' => JText::translate('VBOCUSTGENDER'),
10811 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
10812 'docnum' => JText::translate('VBOCUSTDOCNUM'),
10813 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
10814 ],
10815 [
10816 'first_name' => 'text',
10817 'last_name' => 'text',
10818 'date_birth' => 'calendar',
10819 'place_birth' => 'text',
10820 'country' => 'country',
10821 'city' => 'text',
10822 'zip' => 'text',
10823 'nationality' => 'country',
10824 'gender' => [
10825 'M' => JText::translate('VBOCUSTGENDERM'),
10826 'F' => JText::translate('VBOCUSTGENDERF'),
10827 ],
10828 'doctype' => 'text',
10829 'docnum' => 'text',
10830 'documents' => 'file',
10831 ],
10832 ];
10833
10834 // make a safe copy of the default precheckin fields
10835 $def_precheckin_pax_fields = $precheckin_pax_fields;
10836
10837 /**
10838 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
10839 *
10840 * @since 1.16.3 (J) - 1.6.3 (WP)
10841 */
10842 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
10843
10844 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
10845 // use the filtered pax fields
10846 $def_precheckin_pax_fields = $precheckin_pax_fields;
10847 }
10848
10849 // return the front-end key-value pairs for pre check-in
10850 return $def_precheckin_pax_fields;
10851 }
10852
10853 /**
10854 * Returns the associative list of countries from the DB.
10855 *
10856 * @param bool $tn whether to translate the country name.
10857 * @param bool $no_id whether to unset the ID of the country.
10858 *
10859 * @return array associative or empty array.
10860 *
10861 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
10862 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
10863 */
10864 public static function getCountriesArray($tn = true, $no_id = true)
10865 {
10866 $dbo = JFactory::getDbo();
10867 $all_countries = [];
10868
10869 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
10870 $dbo->setQuery($q);
10871 $dbo->execute();
10872 if (!$dbo->getNumRows()) {
10873 return [];
10874 }
10875 $countries = $dbo->loadAssocList();
10876
10877 if ($tn === true) {
10878 $vbo_tn = self::getTranslator();
10879 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
10880 // re-apply sorting by country name
10881 $sorting = [];
10882 foreach ($countries as $country) {
10883 $sorting[$country['country_name']] = $country;
10884 }
10885 ksort($sorting);
10886 $sorted = [];
10887 foreach ($sorting as $country) {
10888 $sorted[] = $country;
10889 }
10890 $countries = $sorted;
10891 unset($sorting, $sorted);
10892 }
10893
10894 foreach ($countries as $v) {
10895 if ($no_id) {
10896 // keep the original structure by unsetting the ID only needed for translation
10897 unset($v['id']);
10898 }
10899 $all_countries[$v['country_3_code']] = $v;
10900 }
10901
10902 return $all_countries;
10903 }
10904
10905 public static function getCountriesSelect($name, $all_countries = array(), $current_value = '', $empty_value = ' ')
10906 {
10907 if (!count($all_countries)) {
10908 $all_countries = self::getCountriesArray();
10909 }
10910
10911 $countries = '<select name="'.$name.'">'."\n";
10912 if (strlen($empty_value)) {
10913 $countries .= '<option value="">'.$empty_value.'</option>'."\n";
10914 }
10915 foreach ($all_countries as $v) {
10916 $countries .= '<option value="'.$v['country_3_code'].'"'.($v['country_3_code'] == $current_value ? ' selected="selected"' : '').'>'.$v['country_name'].'</option>'."\n";
10917 }
10918 $countries .= '</select>';
10919
10920 return $countries;
10921 }
10922
10923 public static function getThumbSize($skipsession = false)
10924 {
10925 static $thumb_size = null;
10926
10927 if ($thumb_size) {
10928 return $thumb_size;
10929 }
10930
10931 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
10932
10933 return $thumb_size;
10934 }
10935
10936 /**
10937 * Checks whether an iCal file for the reservation should be
10938 * attached to the confirmation email for customer and/or admin.
10939 *
10940 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
10941 *
10942 * @since 1.12.0 (J) - 1.2.0 (WP)
10943 */
10944 public static function attachIcal()
10945 {
10946 return VBOFactory::getConfig()->getInt('attachical', 1);
10947 }
10948
10949 /**
10950 * How the calculation of the orphan dates should take place.
10951 *
10952 * @return string "next" for only checking the bookings ahead, "prevnext" if
10953 * also the previous bookings should be checked.
10954 *
10955 * @since 1.13 (J) - 1.3.0 (WP)
10956 */
10957 public static function orphansCalculation()
10958 {
10959 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
10960 }
10961
10962 /**
10963 * Returns the name of the template file to use for the front-end View "search".
10964 *
10965 * @return string the name of the template file. New and upgraded user will both use the classic file.
10966 *
10967 * @since 1.13 (J) - 1.3.0 (WP)
10968 */
10969 public static function searchResultsTmpl()
10970 {
10971 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
10972 }
10973
10974 /**
10975 * Returns a string without any new-line characters
10976 * to be used for JavaScript values without facing
10977 * errors like 'unterminated string literal'.
10978 * By passing nl2br($str) as argument, we can keep
10979 * the wanted new-line HTML tags for PRE tags.
10980 * We use implode() with just one argument to
10981 * not use an empty string as glue for the string.
10982 *
10983 * @param $str string
10984 *
10985 * @return string
10986 */
10987 public static function strTrimLiteral($str) {
10988 $str = str_replace(array("\r\n", "\r"), "\n", $str);
10989 $lines = explode("\n", $str);
10990 $new_lines = array();
10991 foreach ($lines as $i => $line) {
10992 if (strlen($line)) {
10993 $new_lines[] = trim($line);
10994 }
10995 }
10996 return implode($new_lines);
10997 }
10998
10999 /**
11000 * Returns an instance of the VboApplication class.
11001 *
11002 * @return VboApplication
11003 */
11004 public static function getVboApplication()
11005 {
11006 // require library
11007 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11008
11009 // return a new instance
11010 return new VboApplication();
11011 }
11012
11013 /**
11014 * Returns the translation of the given 0-indexed week day.
11015 *
11016 * @param int $wd the 0-indexed day of the week.
11017 * @param bool $short whether to get the short version of the weekday.
11018 *
11019 * @return string the name of the week day.
11020 *
11021 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11022 */
11023 public static function sayWeekDay($wd, $short = false)
11024 {
11025 switch ($wd) {
11026 case '6' :
11027 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11028 break;
11029 case '5' :
11030 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11031 break;
11032 case '4' :
11033 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11034 break;
11035 case '3' :
11036 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11037 break;
11038 case '2' :
11039 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11040 break;
11041 case '1' :
11042 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11043 break;
11044 default :
11045 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11046 break;
11047 }
11048 return $ret;
11049 }
11050
11051 /**
11052 * Returns the translation of the given 1-indexed month of the year.
11053 *
11054 * @param int $idm the 1-indexed month of the year.
11055 * @param bool $short whether to get the short version of the month.
11056 *
11057 * @return string the name of the month.
11058 *
11059 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11060 */
11061 public static function sayMonth($idm, $short = false)
11062 {
11063 switch ($idm) {
11064 case '12' :
11065 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11066 break;
11067 case '11' :
11068 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11069 break;
11070 case '10' :
11071 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11072 break;
11073 case '9' :
11074 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11075 break;
11076 case '8' :
11077 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11078 break;
11079 case '7' :
11080 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11081 break;
11082 case '6' :
11083 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11084 break;
11085 case '5' :
11086 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11087 break;
11088 case '4' :
11089 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11090 break;
11091 case '3' :
11092 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11093 break;
11094 case '2' :
11095 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11096 break;
11097 default :
11098 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11099 break;
11100 }
11101 return $ret;
11102 }
11103
11104 public static function sayDayMonth($d) {
11105 switch ($d) {
11106 case '31' :
11107 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11108 break;
11109 case '30' :
11110 $ret = JText::translate('VBDAYMONTHTHIRTY');
11111 break;
11112 case '29' :
11113 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11114 break;
11115 case '28' :
11116 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11117 break;
11118 case '27' :
11119 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11120 break;
11121 case '26' :
11122 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11123 break;
11124 case '25' :
11125 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11126 break;
11127 case '24' :
11128 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11129 break;
11130 case '23' :
11131 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11132 break;
11133 case '22' :
11134 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11135 break;
11136 case '21' :
11137 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11138 break;
11139 case '20' :
11140 $ret = JText::translate('VBDAYMONTHTWENTY');
11141 break;
11142 case '19' :
11143 $ret = JText::translate('VBDAYMONTHNINETEEN');
11144 break;
11145 case '18' :
11146 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11147 break;
11148 case '17' :
11149 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11150 break;
11151 case '16' :
11152 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11153 break;
11154 case '15' :
11155 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11156 break;
11157 case '14' :
11158 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11159 break;
11160 case '13' :
11161 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11162 break;
11163 case '12' :
11164 $ret = JText::translate('VBDAYMONTHTWELVE');
11165 break;
11166 case '11' :
11167 $ret = JText::translate('VBDAYMONTHELEVEN');
11168 break;
11169 case '10' :
11170 $ret = JText::translate('VBDAYMONTHTEN');
11171 break;
11172 case '9' :
11173 $ret = JText::translate('VBDAYMONTHNINE');
11174 break;
11175 case '8' :
11176 $ret = JText::translate('VBDAYMONTHEIGHT');
11177 break;
11178 case '7' :
11179 $ret = JText::translate('VBDAYMONTHSEVEN');
11180 break;
11181 case '6' :
11182 $ret = JText::translate('VBDAYMONTHSIX');
11183 break;
11184 case '5' :
11185 $ret = JText::translate('VBDAYMONTHFIVE');
11186 break;
11187 case '4' :
11188 $ret = JText::translate('VBDAYMONTHFOUR');
11189 break;
11190 case '3' :
11191 $ret = JText::translate('VBDAYMONTHTHREE');
11192 break;
11193 case '2' :
11194 $ret = JText::translate('VBDAYMONTHTWO');
11195 break;
11196 default :
11197 $ret = JText::translate('VBDAYMONTHONE');
11198 break;
11199 }
11200 return $ret;
11201 }
11202
11203 public static function totElements($arr) {
11204 $n = 0;
11205 if (is_array($arr)) {
11206 foreach ($arr as $a) {
11207 if (!empty($a)) {
11208 $n++;
11209 }
11210 }
11211 return $n;
11212 }
11213 return false;
11214 }
11215
11216 /**
11217 * Returns a list of documents that were uploaded
11218 * for the specified customer.
11219 *
11220 * @param integer $id The customer ID.
11221 *
11222 * @return array A list of documents.
11223 *
11224 * @since 1.3.0
11225 */
11226 public static function getCustomerDocuments($id)
11227 {
11228 $dbo = JFactory::getDbo();
11229
11230 $q = $dbo->getQuery(true)
11231 ->select($dbo->qn('docsfolder'))
11232 ->from($dbo->qn('#__vikbooking_customers'))
11233 ->where($dbo->qn('id') . ' = ' . (int) $id);
11234
11235 $dbo->setQuery($q, 0, 1);
11236 $dbo->execute();
11237
11238 if (!$dbo->getNumRows())
11239 {
11240 // customer not found
11241 return array();
11242 }
11243
11244 // retrieve customer documents directory name
11245 $dirname = $dbo->loadResult();
11246
11247 if (empty($dirname))
11248 {
11249 // no available directory
11250 return array();
11251 }
11252
11253 // build documents folder path
11254 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11255
11256 if (!is_dir($dirname))
11257 {
11258 // the customer directory doesn't exist
11259 return array();
11260 }
11261
11262 // read all files from customer directory
11263 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11264
11265 $files = array();
11266
11267 foreach ($glob as $path)
11268 {
11269 // skip "index.html"
11270 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11271 {
11272 // extract name and extension from file path
11273 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11274 {
11275 $name = $match[1];
11276 $ext = $match[2];
11277 }
11278 else
11279 {
11280 $name = basename($path);
11281 $ext = '';
11282 }
11283
11284 $file = new stdClass;
11285 $file->path = $path;
11286 $file->name = $name;
11287 $file->ext = $ext;
11288 $file->basename = $file->name . '.' . $file->ext;
11289 $file->size = filesize($path);
11290 $file->date = filemtime($path);
11291 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11292
11293 $files[] = $file;
11294 }
11295 }
11296
11297 // sort files by creation date
11298 usort($files, function($a, $b)
11299 {
11300 return $b->date - $a->date;
11301 });
11302
11303 return $files;
11304 }
11305
11306 public static function displayPaymentParameters($pfile, $pparams = '') {
11307 $html = '<p>---------</p>';
11308
11309 /**
11310 * @wponly The payment gateway is now loaded
11311 * using the apposite dispatcher.
11312 *
11313 * @since 1.0.5
11314 */
11315 JLoader::import('adapter.payment.dispatcher');
11316
11317 try
11318 {
11319 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
11320 }
11321 catch (Exception $e)
11322 {
11323 // payment not found
11324 $html = $e->getMessage();
11325
11326 if ($code = $e->getCode())
11327 {
11328 $html = '<b>' . $code . '</b> : ' . $html;
11329 }
11330
11331 return $html;
11332 }
11333 //
11334
11335 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
11336 $arrparams = !is_array($arrparams) ? array() : $arrparams;
11337
11338 // get admin parameters
11339 $pconfig = $payment->getAdminParameters();
11340
11341 if (count($pconfig) > 0) {
11342 $html = '';
11343 foreach ($pconfig as $value => $cont) {
11344 if (empty($value)) {
11345 continue;
11346 }
11347 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11348 $label = $labelparts[0];
11349 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11350 if (!empty($cont['help'])) {
11351 $labelhelp = $cont['help'];
11352 }
11353 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11354 $html .= '<div class="vbo-param-container">';
11355 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11356 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11357 }
11358 $html .= '<div class="vbo-param-setting">';
11359 switch ($cont['type']) {
11360 case 'custom':
11361 $html .= $cont['html'];
11362 break;
11363 case 'select':
11364 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11365 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11366 if (isset($cont['multiple']) && $cont['multiple']) {
11367 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
11368 } else {
11369 $html .= '<select name="vikpaymentparams['.$value.']">';
11370 }
11371 foreach ($options as $optkey => $poption) {
11372 $checkval = $is_assoc ? $optkey : $poption;
11373 $selected = false;
11374 if (isset($arrparams[$value])) {
11375 if (is_array($arrparams[$value])) {
11376 $selected = in_array($checkval, $arrparams[$value]);
11377 } else {
11378 $selected = ($checkval == $arrparams[$value]);
11379 }
11380 } elseif (isset($default_paramv)) {
11381 if (is_array($default_paramv)) {
11382 $selected = in_array($checkval, $default_paramv);
11383 } else {
11384 $selected = ($checkval == $default_paramv);
11385 }
11386 }
11387 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11388 }
11389 $html .= '</select>';
11390 break;
11391 case 'password':
11392 $html .= '<div class="btn-wrapper input-append">';
11393 $html .= '<input type="password" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"/>';
11394 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11395 $html .= '</div>';
11396 break;
11397 case 'number':
11398 $number_attr = array();
11399 if (isset($cont['min'])) {
11400 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11401 }
11402 if (isset($cont['max'])) {
11403 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11404 }
11405 if (isset($cont['step'])) {
11406 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11407 }
11408 $html .= '<input type="number" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" ' . implode(' ', $number_attr) . '/>';
11409 break;
11410 case 'textarea':
11411 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11412 break;
11413 case 'hidden':
11414 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
11415 break;
11416 case 'checkbox':
11417 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11418 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
11419 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
11420 break;
11421 default:
11422 $html .= '<input type="text" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"/>';
11423 break;
11424 }
11425 if (strlen($labelhelp) > 0) {
11426 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11427 }
11428 $html .= '</div>';
11429 $html .= '</div>';
11430 }
11431 // JS helper function to toggle the password fields
11432 $html .= "\n" . '<script>' . "\n";
11433 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
11434 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11435 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11436 $html .= ' var inp_type = inp.attr("type");' . "\n";
11437 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11438 $html .= '}' . "\n";
11439 $html .= "\n" . '</script>' . "\n";
11440 }
11441
11442 return $html;
11443 }
11444
11445 public static function displaySMSParameters($pfile, $params = null)
11446 {
11447 $html = '---------';
11448
11449 $params = is_string($params) ? json_decode($params, true) : $params;
11450 $params = is_array($params) ? $params : [];
11451
11452 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
11453 return $html;
11454 }
11455
11456 // attempt to load the class file
11457 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
11458
11459 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
11460 return $html;
11461 }
11462
11463 // load the gateway parameters
11464 $config = VikSmsApi::getAdminParameters();
11465
11466 if (!is_array($config) || !count($config)) {
11467 return $html;
11468 }
11469
11470 // flags for JS helpers
11471 $js_helpers = array();
11472
11473 $html = '';
11474 foreach ($config as $value => $cont) {
11475 if (empty($value)) {
11476 continue;
11477 }
11478 $inp_attr = '';
11479 if (isset($cont['attributes'])) {
11480 foreach ($cont['attributes'] as $inpk => $inpv) {
11481 $inp_attr .= $inpk.'="'.$inpv.'" ';
11482 }
11483 $inp_attr = ' ' . rtrim($inp_attr);
11484 }
11485 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11486 $label = $labelparts[0];
11487 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11488 if (!empty($cont['help'])) {
11489 $labelhelp = $cont['help'];
11490 }
11491 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11492 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
11493 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11494 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11495 }
11496 $html .= '<div class="vbo-param-setting">';
11497 switch ($cont['type']) {
11498 case 'custom':
11499 $html .= $cont['html'];
11500 break;
11501 case 'select':
11502 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11503 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11504 if (isset($cont['multiple']) && $cont['multiple']) {
11505 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11506 } else {
11507 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
11508 }
11509 foreach ($options as $optkey => $poption) {
11510 $checkval = $is_assoc ? $optkey : $poption;
11511 $selected = false;
11512 if (isset($params[$value])) {
11513 if (is_array($params[$value])) {
11514 $selected = in_array($checkval, $params[$value]);
11515 } else {
11516 $selected = ($checkval == $params[$value]);
11517 }
11518 } elseif (isset($default_paramv)) {
11519 if (is_array($default_paramv)) {
11520 $selected = in_array($checkval, $default_paramv);
11521 } else {
11522 $selected = ($checkval == $default_paramv);
11523 }
11524 }
11525 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11526 }
11527 $html .= '</select>';
11528 break;
11529 case 'password':
11530 $html .= '<div class="btn-wrapper input-append">';
11531 $html .= '<input type="password" name="viksmsparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"' . $inp_attr . '/>';
11532 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11533 $html .= '</div>';
11534 // set flag for JS helper
11535 $js_helpers[] = $cont['type'];
11536 break;
11537 case 'number':
11538 $number_attr = array();
11539 if (isset($cont['min'])) {
11540 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11541 }
11542 if (isset($cont['max'])) {
11543 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11544 }
11545 if (isset($cont['step'])) {
11546 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11547 }
11548 $html .= '<input type="number" name="viksmsparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" ' . implode(' ', $number_attr) . $inp_attr . '/>';
11549 break;
11550 case 'textarea':
11551 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11552 break;
11553 case 'visual_html':
11554 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11555 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
11556 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
11557 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
11558 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11559 break;
11560 case 'hidden':
11561 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"' . $inp_attr . '/>';
11562 break;
11563 case 'checkbox':
11564 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11565 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
11566 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11567 break;
11568 default:
11569 $html .= '<input type="text" name="viksmsparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"' . $inp_attr . '/>';
11570 break;
11571 }
11572 if (strlen($labelhelp) > 0) {
11573 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11574 }
11575 $html .= '</div>';
11576 $html .= '</div>';
11577 }
11578
11579 // JS helper functions
11580 if (in_array('password', $js_helpers)) {
11581 // toggle the password fields
11582 $html .= "\n" . '<script>' . "\n";
11583 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
11584 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11585 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11586 $html .= ' var inp_type = inp.attr("type");' . "\n";
11587 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11588 $html .= '}' . "\n";
11589 $html .= "\n" . '</script>' . "\n";
11590 }
11591
11592 return $html;
11593 }
11594
11595 /**
11596 * Renders the params of the requested cron job file-class.
11597 *
11598 * @param string $file the name of the cron job driver file.
11599 * @param array $params the parameters array.
11600 *
11601 * @return string the necessary HTML content to render.
11602 *
11603 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
11604 */
11605 public static function displayCronParameters($file, $params = [])
11606 {
11607 try
11608 {
11609 // attempt to create a new instance
11610 $job = VBOFactory::getCronFactory()->createInstance($file);
11611 }
11612 catch (Exception $e)
11613 {
11614 // something went wrong, display error message
11615 return '<p>' . $e->getMessage() . '<p>';
11616 }
11617
11618 // get admin parameters
11619 $config = $job->getForm();
11620
11621 if (!is_array($config) || !$config) {
11622 return '<p>---------</p>';
11623 }
11624
11625 /**
11626 * Render the cron parameters through the global params rendering class.
11627 *
11628 * @since 1.16.9 (J) - 1.6.9 (WP)
11629 */
11630 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
11631 }
11632
11633 public static function invokeChannelManager($skiporder = true, $order = array()) {
11634 $task = VikRequest::getString('task', '', 'request');
11635 $view = VikRequest::getString('view', '', 'request');
11636 $tmpl = VikRequest::getString('tmpl', '', 'request');
11637 $noimpression = array('vieworder', 'booking');
11638 if ($tmpl != 'component' && (!$skiporder || (!in_array($task, $noimpression) && !in_array($view, $noimpression))) && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
11639 //VCM Channel Impression
11640 if (!class_exists('VikChannelManagerConfig')) {
11641 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11642 }
11643 if (!class_exists('VikChannelManager')) {
11644 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11645 }
11646 VikChannelManager::invokeChannelImpression();
11647 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
11648 //VCM Channel Conversion-Impression
11649 if (!class_exists('VikChannelManagerConfig')) {
11650 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11651 }
11652 if (!class_exists('VikChannelManager')) {
11653 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11654 }
11655 VikChannelManager::invokeChannelConversionImpression($order);
11656 }
11657 }
11658
11659 public static function validEmail($email) {
11660 $isValid = true;
11661 $atIndex = strrpos($email, "@");
11662 if (is_bool($atIndex) && !$atIndex) {
11663 $isValid = false;
11664 } else {
11665 $domain = substr($email, $atIndex +1);
11666 $local = substr($email, 0, $atIndex);
11667 $localLen = strlen($local);
11668 $domainLen = strlen($domain);
11669 if ($localLen < 1 || $localLen > 64) {
11670 // local part length exceeded
11671 $isValid = false;
11672 } else
11673 if ($domainLen < 1 || $domainLen > 255) {
11674 // domain part length exceeded
11675 $isValid = false;
11676 } else
11677 if ($local[0] == '.' || $local[$localLen -1] == '.') {
11678 // local part starts or ends with '.'
11679 $isValid = false;
11680 } else
11681 if (preg_match('/\\.\\./', $local)) {
11682 // local part has two consecutive dots
11683 $isValid = false;
11684 } else
11685 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
11686 // character not valid in domain part
11687 $isValid = false;
11688 } else
11689 if (preg_match('/\\.\\./', $domain)) {
11690 // domain part has two consecutive dots
11691 $isValid = false;
11692 } else
11693 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
11694 // character not valid in local part unless
11695 // local part is quoted
11696 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
11697 $isValid = false;
11698 }
11699 }
11700 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
11701 // domain not found in DNS
11702 $isValid = false;
11703 }
11704 }
11705 return $isValid;
11706 }
11707
11708 public static function caniWrite($path) {
11709 if ($path[strlen($path) - 1] == '/') {
11710 // ricorsivo return a temporary file path
11711 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
11712 }
11713 if (is_dir($path)) {
11714 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
11715 }
11716 // check tmp file for read/write capabilities
11717 $rm = file_exists($path);
11718 $f = @fopen($path, 'a');
11719 if ($f === false) {
11720 return false;
11721 }
11722 fclose($f);
11723 if (!$rm) {
11724 unlink($path);
11725 }
11726 return true;
11727 }
11728
11729 /**
11730 * Alias method of JFile::upload to unify any
11731 * upload function into one.
11732 *
11733 * @param string $src The name of the php (temporary) uploaded file.
11734 * @param string $dest The path (including filename) to move the uploaded file to.
11735 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
11736 *
11737 * @return boolean True on success.
11738 *
11739 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
11740 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
11741 */
11742 public static function uploadFile($src, $dest, $copy_only = false) {
11743 // always attempt to include the File class
11744 jimport('joomla.filesystem.file');
11745
11746 // upload the file
11747 if (!$copy_only) {
11748 $result = JFile::upload($src, $dest);
11749 } else {
11750 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
11751 $result = JFile::copy($src, $dest);
11752 }
11753
11754 /**
11755 * @wponly in order to not lose uploaded files after installing an update,
11756 * we need to move any uploaded file onto a recovery folder.
11757 */
11758 if ($result) {
11759 VikBookingLoader::import('update.manager');
11760 VikBookingUpdateManager::triggerUploadBackup($dest);
11761 }
11762 //
11763
11764 // return upload result
11765 return $result;
11766 }
11767
11768 /**
11769 * Helper method used to upload the given file (retrieved from $_FILES)
11770 * into the specified destination.
11771 *
11772 * @param array $file An associative array with the file details.
11773 * @param string $dest The destination path.
11774 * @param string $filters A string (or a regex) containing the allowed extensions.
11775 *
11776 * @return array The uploading result.
11777 *
11778 * @throws RuntimeException
11779 *
11780 * @since 1.3.0
11781 */
11782 public static function uploadFileFromRequest($file, $dest, $filters = '*')
11783 {
11784 jimport('joomla.filesystem.file');
11785
11786 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
11787
11788 if (empty($file['name']))
11789 {
11790 throw new RuntimeException('Missing file', 400);
11791 }
11792
11793 /**
11794 * Make sure the upload of the file didn't raise any errors.
11795 *
11796 * @link https://www.php.net/manual/en/features.file-upload.errors.php
11797 */
11798 if ((int) $file['error'])
11799 {
11800 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
11801 {
11802 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
11803 }
11804 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
11805 {
11806 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
11807 }
11808 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
11809 {
11810 $error = 'The uploaded file was only partially uploaded.';
11811 }
11812 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
11813 {
11814 $error = 'No file was uploaded.';
11815 }
11816 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
11817 {
11818 $error = 'Missing a temporary folder.';
11819 }
11820 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
11821 {
11822 $error = 'Failed to write file to disk.';
11823 }
11824 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
11825 {
11826 $error = 'A PHP extension stopped the file upload.';
11827 }
11828 else
11829 {
11830 $error = 'Unknown error.';
11831 }
11832
11833 throw new RuntimeException($error, 500);
11834 }
11835
11836 $src = $file['tmp_name'];
11837
11838 // extract file name and extension
11839 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
11840 {
11841 $filename = $match[1];
11842 $fileext = $match[2];
11843 }
11844 else
11845 {
11846 // probably no extension provided
11847 $filename = basename($file['name']);
11848 $fileext = '';
11849 }
11850
11851 $j = '';
11852
11853 if (file_exists($dest . $filename . $fileext))
11854 {
11855 $j = 2;
11856
11857 while (file_exists($dest . $filename . '-' . $j . $fileext))
11858 {
11859 $j++;
11860 }
11861
11862 $j = '-' . $j;
11863 }
11864
11865 $finaldest = $dest . $filename . $j . $fileext;
11866
11867 if ($filters !== '*')
11868 {
11869 $ext = $file['type'];
11870
11871 // check if we have a regex
11872 if (preg_match("/^[#\/]/", $filters) && preg_match("/[#\/][a-z]*$/", $filters))
11873 {
11874 if (!preg_match($filters, $ext))
11875 {
11876 // extension not supported
11877 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11878 }
11879 }
11880 else
11881 {
11882 // get all supported types
11883 $types = array_map('strtolower', array_filter(explode(',', $filters)));
11884
11885 if (!in_array($ext, $types))
11886 {
11887 // extension not supported
11888 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11889 }
11890 }
11891 }
11892
11893 // try to upload the file
11894 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
11895 {
11896 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
11897 }
11898
11899 $file = new stdClass;
11900 $file->name = $filename . $j;
11901 $file->ext = ltrim($fileext, '.');
11902 $file->filename = basename($finaldest);
11903 $file->path = $finaldest;
11904
11905 return $file;
11906 }
11907
11908 /**
11909 * Gets the instance of a specific report class.
11910 *
11911 * @param string $report the name of the report to load.
11912 *
11913 * @return mixed false or report object instance.
11914 *
11915 * @since 1.3.0
11916 */
11917 public static function getReportInstance($report)
11918 {
11919 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
11920
11921 return VikBookingReport::getInstanceOf($report);
11922 }
11923
11924 /**
11925 * Checks whether the guest reviews are enabled and VCM is installed.
11926 *
11927 * @return boolean true if enabled, false otherwise.
11928 *
11929 * @since 1.3.0
11930 */
11931 public static function allowGuestReviews()
11932 {
11933 $dbo = JFactory::getDbo();
11934 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
11935 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
11936 $dbo->setQuery($q);
11937 $dbo->execute();
11938 if ($dbo->getNumRows()) {
11939 $s = $dbo->loadResult();
11940 return ((int)$s === 1 && $vcm_installed);
11941 }
11942 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
11943 $dbo->setQuery($q);
11944 $dbo->execute();
11945 return $vcm_installed;
11946 }
11947
11948 /**
11949 * Gets the minimum chars for the guest review message.
11950 *
11951 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
11952 *
11953 * @since 1.3.0
11954 */
11955 public static function guestReviewMinChars()
11956 {
11957 $dbo = JFactory::getDbo();
11958 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
11959 $dbo->setQuery($q);
11960 $dbo->execute();
11961 if ($dbo->getNumRows() > 0) {
11962 return (int)$dbo->loadResult();
11963 }
11964 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
11965 $dbo->setQuery($q);
11966 $dbo->execute();
11967 return 15;
11968 }
11969
11970 /**
11971 * The approval type for new guest reviews.
11972 *
11973 * @return string auto or manual.
11974 *
11975 * @since 1.3.0
11976 */
11977 public static function guestReviewsApproval()
11978 {
11979 $dbo = JFactory::getDbo();
11980 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
11981 $dbo->setQuery($q);
11982 $dbo->execute();
11983 if ($dbo->getNumRows() > 0) {
11984 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
11985 }
11986 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
11987 $dbo->setQuery($q);
11988 $dbo->execute();
11989 return 'auto';
11990 }
11991
11992 /**
11993 * The type of reviews guests should leave.
11994 *
11995 * @return string global or service.
11996 *
11997 * @since 1.3.0
11998 */
11999 public static function guestReviewsType()
12000 {
12001 $dbo = JFactory::getDbo();
12002 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12003 $dbo->setQuery($q);
12004 $dbo->execute();
12005 if ($dbo->getNumRows() > 0) {
12006 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12007 }
12008 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12009 $dbo->setQuery($q);
12010 $dbo->execute();
12011 return 'service';
12012 }
12013
12014 /**
12015 * The services to be reviewed by the guests.
12016 *
12017 * @return array list, empty or not, of the services to be reviewed.
12018 *
12019 * @since 1.3.0
12020 */
12021 public static function guestReviewsServices()
12022 {
12023 $dbo = JFactory::getDbo();
12024 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12025 $dbo->setQuery($q);
12026 $dbo->execute();
12027 if ($dbo->getNumRows()) {
12028 return $dbo->loadAssocList();
12029 }
12030 // insert the default services
12031 $default_services = array(
12032 JText::translate('VBOGREVVALUE'),
12033 JText::translate('VBOGREVLOCATION'),
12034 JText::translate('VBOGREVSTAFF'),
12035 JText::translate('VBOGREVCLEAN'),
12036 JText::translate('VBOGREVCOMFORT'),
12037 JText::translate('VBOGREVFACILITIES'),
12038 );
12039 foreach ($default_services as $def_service) {
12040 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12041 $dbo->setQuery($q);
12042 $dbo->execute();
12043 }
12044 return array();
12045 }
12046
12047 /**
12048 * Checks whether the guest reviews should be downloaded (max 1 per day).
12049 * VCM must be installed in order to download the new reviews.
12050 *
12051 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12052 *
12053 * @since 1.3.0
12054 */
12055 public static function shouldDownloadReviews()
12056 {
12057 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12058 return -1;
12059 }
12060
12061 $today = date('Y-m-d');
12062
12063 $dbo = JFactory::getDbo();
12064 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12065 $dbo->setQuery($q);
12066 $dbo->execute();
12067 if ($dbo->getNumRows()) {
12068 $last_download = $dbo->loadResult();
12069 // update last download day
12070 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12071 $dbo->setQuery($q);
12072 $dbo->execute();
12073 //
12074 return ($last_download != $today ? 1 : 0);
12075 }
12076 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12077 $dbo->setQuery($q);
12078 $dbo->execute();
12079 return 1;
12080 }
12081
12082 /**
12083 * Tells whether a booking can be reviewed.
12084 *
12085 * @param array $booking booking array details.
12086 *
12087 * @return boolean true if a review can be left, false otherwise.
12088 *
12089 * @since 1.3.0
12090 */
12091 public static function canBookingBeReviewed($booking)
12092 {
12093 // reviews must be enabled and supported
12094 if (!self::allowGuestReviews()) {
12095 return false;
12096 }
12097
12098 // booking status must be confirmed
12099 if ($booking['status'] != 'confirmed') {
12100 return false;
12101 }
12102
12103 // review can be left starting from the check-out day
12104 $checkout_info = getdate($booking['checkout']);
12105 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12106 if (time() < $checkout_midn) {
12107 return false;
12108 }
12109
12110 // make sure a review for this booking does not exist
12111 $noreview = true;
12112 $dbo = JFactory::getDbo();
12113 try {
12114 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12115 $dbo->setQuery($q);
12116 $dbo->execute();
12117 $noreview = ($dbo->getNumRows() < 1);
12118 } catch (Exception $e) {
12119 // if the query fails, we do not allow the review to be left
12120 $noreview = false;
12121 }
12122
12123 return $noreview;
12124 }
12125
12126 /**
12127 * Gets the review for a booking, no matter if it's published or not.
12128 * Review will be returned only if reviews are enabled and supported.
12129 * Only reviews left through the website, no OTA reviews as this method
12130 * should be used in the front-end to display the review to the guest.
12131 *
12132 * @param array $booking booking array details.
12133 *
12134 * @return mixed associative array or false.
12135 *
12136 * @since 1.3.0
12137 */
12138 public static function getBookingReview($booking)
12139 {
12140 // reviews must be enabled and supported
12141 if (!self::allowGuestReviews()) {
12142 return false;
12143 }
12144
12145 if (empty($booking['id'])) {
12146 return false;
12147 }
12148
12149 // make sure a review for this booking exists
12150 $dbo = JFactory::getDbo();
12151 $review = array();
12152 try {
12153 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12154 $dbo->setQuery($q);
12155 $dbo->execute();
12156 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12157 } catch (Exception $e) {
12158 // query has failed
12159 $review = array();
12160 }
12161
12162 if (count($review)) {
12163 // make sure the review was left from the website
12164 $review['uniquekey'] = (int)$review['uniquekey'];
12165 }
12166
12167 if (count($review) && !empty($review['uniquekey'])) {
12168 // this is an OTA review
12169 return false;
12170 }
12171
12172 if (count($review) && !empty($review['content'])) {
12173 // decode content
12174 $review['content'] = json_decode($review['content'], true);
12175 }
12176
12177 return count($review) ? $review : false;
12178 }
12179
12180 /**
12181 * Attempts to find a translation for a raw customer data label.
12182 * Bookings downloaded from the OTAs will save a raw string of information
12183 * composed of pairs of label-value separated by new line feeds.
12184 * We try to translate the given label into the current language.
12185 *
12186 * @param string $label the raw string label to translate.
12187 *
12188 * @return string either the original or the translated label.
12189 *
12190 * @since 1.3.5
12191 * @since 1.4.0 back-end support added.
12192 */
12193 public static function tnCustomerRawDataLabel($label)
12194 {
12195 // this is a map of the known labels
12196 $known_lbls = array(
12197 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12198 'LASTNAME' => 'ORDER_LNAME',
12199 'COUNTRY' => 'ORDER_STATE',
12200 'EMAIL' => 'VBMAIL',
12201 'TELEPHONE' => 'VBPHONE',
12202 'PHONE' => 'VBPHONE',
12203 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12204 'MEAL_PLAN' => 'VBOMEALPLAN',
12205 'CITY' => 'VBCITY',
12206 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12207 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12208 );
12209
12210 if (self::isAdmin()) {
12211 // override the map of known translation strings
12212 $known_lbls = array(
12213 'NAME' => 'ORDER_NAME',
12214 'LASTNAME' => 'ORDER_LNAME',
12215 'COUNTRY' => 'ORDER_STATE',
12216 'ADDRESS' => 'ORDER_ADDRESS',
12217 'CITY' => 'ORDER_CITY',
12218 'LOCATION' => 'ORDER_CITY',
12219 'EMAIL' => 'ORDER_EMAIL',
12220 'TELEPHONE' => 'ORDER_PHONE',
12221 'PHONE' => 'ORDER_PHONE',
12222 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12223 );
12224 }
12225
12226 // we get rid of any empty space by keeping the underscores
12227 $converted = str_replace(' ', '', strtoupper($label));
12228
12229 if (isset($known_lbls[$converted])) {
12230 // this language definition has been mapped
12231 return JText::translate($known_lbls[$converted]);
12232 }
12233
12234 // we try to guess the translation string by prepending VBO
12235 $guessed = JText::translate('VBO' . $converted);
12236 if ($guessed != 'VBO' . $converted) {
12237 // the label was translated correctly
12238 return $guessed;
12239 }
12240
12241 // this label could not be translated, so we return the plain string
12242 return $label;
12243 }
12244
12245 /**
12246 * We check whether some bookings are available for import from third party plugins.
12247 *
12248 * @return mixed array with list of plugins supported, false otherwise.
12249 *
12250 * @wponly this method is only useful for WordPress.
12251 *
12252 * @since 1.3.5
12253 */
12254 public static function canImportBookingsFromThirdPartyPlugins()
12255 {
12256 $dbo = JFactory::getDbo();
12257
12258 $plugins = array();
12259
12260 /**
12261 * As requested from hundreds of our clients, for the moment we check only the custom
12262 * post types of type "mphb_booking" to see if some bookings are available for import.
12263 */
12264 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
12265 $dbo->setQuery($q);
12266 $dbo->execute();
12267
12268 if (!$dbo->getNumRows()) {
12269 return false;
12270 }
12271
12272 // push this third party plugin
12273 $plugins['mphb'] = 'MotoPress Hotel Booking';
12274
12275 return count($plugins) ? $plugins : false;
12276 }
12277
12278 /**
12279 * This method returns a list of the known languages sorted by the
12280 * administrator custom preferences. Useful for the phone input fields.
12281 *
12282 * @param boolean $code_assoc whether to get an associative array with the lang name.
12283 *
12284 * @return array the sorted list of preferred countries.
12285 *
12286 * @since 1.3.11
12287 */
12288 public static function preferredCountriesOrdering($code_assoc = false)
12289 {
12290 $preferred_countries = array();
12291
12292 // try to get the preferred countries from db
12293 $dbo = JFactory::getDbo();
12294 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
12295 $dbo->setQuery($q);
12296 $setting = $dbo->loadResult();
12297 if (!$setting) {
12298 // create empty configuration record
12299 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
12300 $dbo->setQuery($q);
12301 $dbo->execute();
12302 } else {
12303 $preferred_countries = json_decode($setting);
12304 }
12305
12306 // get the default known languages
12307 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
12308
12309 if (!is_array($preferred_countries) || !count($preferred_countries)) {
12310 // sort the default known languages by country code alphabetically
12311 ksort($sorted_known_langs);
12312 foreach ($sorted_known_langs as $k => $v) {
12313 $langsep = strpos($k, '_') !== false ? '_' : '-';
12314 $langparts = explode($langsep, $k);
12315 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
12316 }
12317 // update the database record
12318 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
12319 $dbo->setQuery($q);
12320 $dbo->execute();
12321 }
12322
12323 if ($code_assoc) {
12324 // this is useful for displaying the preferred countries codes together with the language name
12325 $map = array();
12326 foreach ($preferred_countries as $ccode) {
12327 // look for the current country code in the keys of the known language tags
12328 $match_found = false;
12329 foreach ($sorted_known_langs as $langtag => $langinfo) {
12330 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
12331 $langparts = explode($langsep, $langtag);
12332 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
12333 // match found
12334 $match_found = true;
12335 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12336 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
12337 // match found
12338 $match_found = true;
12339 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12340 }
12341 }
12342 if (!$match_found) {
12343 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
12344 $map[$ccode] = strtoupper($ccode);
12345 }
12346 }
12347 if (count($map)) {
12348 // set the associatve array to be returned
12349 $preferred_countries = $map;
12350 }
12351 }
12352
12353 return $preferred_countries;
12354 }
12355
12356 /**
12357 * Gets the instance of the admin widgets helper class.
12358 *
12359 * @return VikBookingHelperAdminWidgets
12360 *
12361 * @since 1.4.0
12362 */
12363 public static function getAdminWidgetsInstance()
12364 {
12365 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
12366
12367 return VikBookingHelperAdminWidgets::getInstance();
12368 }
12369
12370 /**
12371 * Gets the instance of the conditional rules helper class.
12372 *
12373 * @param bool $require_only whether to return the object.
12374 *
12375 * @return mixed VikBookingHelperConditionalRules or true.
12376 *
12377 * @since 1.4.0
12378 */
12379 public static function getConditionalRulesInstance($require_only = false)
12380 {
12381 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
12382
12383 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
12384 }
12385
12386 /**
12387 * Gets the instance of the geocoding helper class.
12388 *
12389 * @param bool $require_only whether to return the object.
12390 *
12391 * @return mixed VikBookingHelperGeocoding or true.
12392 *
12393 * @since 1.4.0
12394 */
12395 public static function getGeocodingInstance($require_only = false)
12396 {
12397 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
12398
12399 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
12400 }
12401
12402 /**
12403 * Helper method to cope with the removal of the same method
12404 * in the JApplication class introduced with Joomla 4. Using
12405 * isClient() would break the compatibility with J < 3.7 so
12406 * we can rely on this helper method to avoid Fatal Errors.
12407 *
12408 * @return boolean
12409 *
12410 * @since October 2020
12411 */
12412 public static function isAdmin()
12413 {
12414 $app = JFactory::getApplication();
12415 if (method_exists($app, 'isClient')) {
12416 return $app->isClient('administrator');
12417 }
12418
12419 return $app->isAdmin();
12420 }
12421
12422 /**
12423 * Helper method to cope with the removal of the same method
12424 * in the JApplication class introduced with Joomla 4. Using
12425 * isClient() would break the compatibility with J < 3.7 so
12426 * we can rely on this helper method to avoid Fatal Errors.
12427 *
12428 * @return boolean
12429 *
12430 * @since October 2020
12431 */
12432 public static function isSite()
12433 {
12434 $app = JFactory::getApplication();
12435 if (method_exists($app, 'isClient')) {
12436 return $app->isClient('site');
12437 }
12438
12439 return $app->isSite();
12440 }
12441
12442 /**
12443 * Gets the Google Maps API Key.
12444 *
12445 * @return string
12446 *
12447 * @since 1.14 (J) - 1.4.0 (WP)
12448 */
12449 public static function getGoogleMapsKey()
12450 {
12451 $dbo = JFactory::getDbo();
12452 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gmapskey';";
12453 $dbo->setQuery($q);
12454 $dbo->execute();
12455 if ($dbo->getNumRows()) {
12456 return $dbo->loadResult();
12457 }
12458 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gmapskey', '');";
12459 $dbo->setQuery($q);
12460 $dbo->execute();
12461 return '';
12462 }
12463
12464 /**
12465 * Checks whether the interactive map booking is enabled.
12466 *
12467 * @return bool
12468 *
12469 * @since 1.14 (J) - 1.4.0 (WP)
12470 */
12471 public static function interactiveMapEnabled()
12472 {
12473 $dbo = JFactory::getDbo();
12474 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='interactive_map';";
12475 $dbo->setQuery($q);
12476 $dbo->execute();
12477 if ($dbo->getNumRows()) {
12478 return ((int)$dbo->loadResult() > 0);
12479 }
12480
12481 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('interactive_map', '0');";
12482 $dbo->setQuery($q);
12483 $dbo->execute();
12484
12485 return false;
12486 }
12487
12488 /**
12489 * Gets the preferred colors saved in the configuration, if any.
12490 *
12491 * @return array
12492 *
12493 * @since 1.14 (J) - 1.4.0 (WP)
12494 */
12495 public static function getPreferredColors()
12496 {
12497 $dbo = JFactory::getDbo();
12498 $pref_colors = array(
12499 'textcolor' => '',
12500 'bgcolor' => '',
12501 'fontcolor' => '',
12502 'bgcolorhov' => '',
12503 'fontcolorhov' => '',
12504 );
12505
12506 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='pref_colors';";
12507 $dbo->setQuery($q);
12508 $dbo->execute();
12509 if ($dbo->getNumRows()) {
12510 $colors = json_decode($dbo->loadResult(), true);
12511 if (!is_array($colors) || !isset($colors['textcolor'])) {
12512 return $pref_colors;
12513 }
12514 return $colors;
12515 }
12516 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('pref_colors', '{}');";
12517 $dbo->setQuery($q);
12518 $dbo->execute();
12519 return $pref_colors;
12520 }
12521
12522 /**
12523 * Adds to the document inline styles for the preferred colors, if any.
12524 *
12525 * @return void
12526 *
12527 * @since 1.14 (J) - 1.4.0 (WP)
12528 */
12529 public static function loadPreferredColorStyles()
12530 {
12531 $view = VikRequest::getString('view', '', 'request');
12532 $pref_colors = self::getPreferredColors();
12533
12534 $css_classes = array();
12535
12536 if (!empty($pref_colors['textcolor'])) {
12537 // titles and headings
12538 array_push($css_classes, '.vbo-pref-color-text { color: ' . $pref_colors['textcolor'] . ' !important; }');
12539 // stepbar, oconfirm
12540 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete, ol.vbo-stepbar li.vbo-step-current, ol.vbo-stepbar li.vbo-step-current:before, .vbo-coupon-outer, .vbo-enterpin-block { border-color: ' . $pref_colors['textcolor'] . ' !important; }');
12541 // buttons secondary color
12542 array_push($css_classes, '.vbo-pref-color-btn-secondary { border: 2px solid ' . $pref_colors['textcolor'] . ' !important; color: ' . $pref_colors['textcolor'] . ' !important; background: transparent !important; }');
12543 if (!empty($pref_colors['fontcolor'])) {
12544 array_push($css_classes, '.vbo-pref-color-btn-secondary:hover { color: ' . $pref_colors['fontcolor'] . ' !important; background: ' . $pref_colors['textcolor'] . ' !important; }');
12545 }
12546 // datepicker
12547 array_push($css_classes, '.ui-datepicker .ui-datepicker-today {
12548 color: ' . $pref_colors['textcolor'] . ' !important;
12549 border-color: ' . $pref_colors['textcolor'] . ' !important;
12550 }');
12551 array_push($css_classes, '.ui-datepicker .ui-datepicker-today a {
12552 color: ' . $pref_colors['textcolor'] . ' !important;
12553 }');
12554 // operators tableaux
12555 if ($view == 'tableaux') {
12556 array_push($css_classes, '.vbo-roomdaynote-empty .vbo-roomdaynote-trigger i { color: ' . $pref_colors['textcolor'] . ' !important; }');
12557 }
12558 }
12559
12560 if (!empty($pref_colors['bgcolor']) && !empty($pref_colors['fontcolor'])) {
12561 // elements with backgrounds
12562 array_push($css_classes, '.vbo-pref-color-element { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12563 array_push($css_classes, '.vbo-pref-bordercolor { border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12564 array_push($css_classes, '.vbo-pref-bordertext { color: ' . $pref_colors['bgcolor'] . ' !important; border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12565 // buttons with backgrounds
12566 array_push($css_classes, '.vbo-pref-color-btn { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12567 // stepbar
12568 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete:before { background-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12569 // datepicker
12570 array_push($css_classes, '.ui-datepicker table td:hover {
12571 border-color: ' . $pref_colors['bgcolor'] . ' !important;
12572 }');
12573 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day {
12574 background: ' . $pref_colors['bgcolor'] . ' !important;
12575 color: ' . $pref_colors['fontcolor'] . ' !important;
12576 }');
12577 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day a {
12578 color: ' . $pref_colors['fontcolor'] . ' !important;
12579 }');
12580 // operators tableaux
12581 if ($view == 'tableaux') {
12582 array_push($css_classes, '.vbo-tableaux-roombooks > div:not(.vbo-tableaux-booking-empty), .vbo-tableaux-togglefullscreen { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12583 }
12584 }
12585
12586 if (!empty($pref_colors['bgcolorhov']) && !empty($pref_colors['fontcolorhov'])) {
12587 // buttons with backgrounds during hover state
12588 array_push($css_classes, '.vbo-pref-color-btn:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12589 // operators tableaux
12590 if ($view == 'tableaux') {
12591 array_push($css_classes, '.vbo-tableaux-togglefullscreen:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12592 }
12593 }
12594
12595 if (!count($css_classes)) {
12596 return;
12597 }
12598
12599 // add in-line style declaration
12600 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
12601 }
12602
12603 /**
12604 * Given the full endpoint URL for the AJAX request, it
12605 * returns an appropriate URI for the current platform.
12606 *
12607 * @param mixed $query The query string or a routed URL.
12608 * @param boolean $xhtml Replace & by &amp; for XML compliance.
12609 *
12610 * @return string The AJAX end-point URI.
12611 *
12612 * @since 1.14 (J) - 1.4.0 (WP)
12613 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
12614 */
12615 public static function ajaxUrl($query = '', $xhtml = false)
12616 {
12617 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
12618 }
12619
12620 /**
12621 * Tells whether no tax rates have been defined so far.
12622 *
12623 * @return bool True if no tax rates defined, false otherwise.
12624 *
12625 * @since 1.14 (J) - 1.4.0 (WP)
12626 */
12627 public static function noTaxRates()
12628 {
12629 $dbo = JFactory::getDbo();
12630
12631 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
12632 $dbo->setQuery($q);
12633 $dbo->execute();
12634
12635 return ($dbo->getNumRows() < 1);
12636 }
12637
12638 /**
12639 * Booking Type feature is strictly connected to VCM and OTAs. Updated
12640 * versions of VBO will always support it as long as VCM is installed.
12641 * Needed by VCM to understand whether certain SQL queries can be performed.
12642 *
12643 * @return bool true if VCM exists, false otherwise.
12644 *
12645 * @since 1.14 (J) - 1.4.0 (WP)
12646 */
12647 public static function isBookingTypeSupported()
12648 {
12649 return is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12650 }
12651
12652 /**
12653 * Returns an instance of the Rates Flow helper class of VCM, if available.
12654 *
12655 * @param bool $anew True to request a new object instance.
12656 *
12657 * @return VCMRatesFlow|null
12658 *
12659 * @since 1.15.0 (J) - 1.5.0 (WP)
12660 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
12661 */
12662 public static function getRatesFlowInstance($anew = false)
12663 {
12664 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
12665 if (!is_file($vcm_fpath)) {
12666 return null;
12667 }
12668
12669 // load dependencies
12670 require_once $vcm_fpath;
12671
12672 // return the instance of the class
12673 return VCMRatesFlow::getInstance($anew);
12674 }
12675
12676 /**
12677 * Returns the current appearance preference.
12678 *
12679 * @return string light, auto or dark.
12680 *
12681 * @since 1.15.0 (J) - 1.5.0 (WP)
12682 */
12683 public static function getAppearancePref()
12684 {
12685 static $preference = null;
12686
12687 if ($preference !== null) {
12688 return $preference;
12689 }
12690
12691 // auto is the default appearance preference
12692 $default_pref = 'auto';
12693
12694 // accepted preferences
12695 $valid_pref = [
12696 'auto',
12697 'light',
12698 'dark',
12699 ];
12700
12701 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
12702 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
12703
12704 return $preference;
12705 }
12706
12707 /**
12708 * According to the appearance preferences, the apposite
12709 * CSS assets are loaded within the document.
12710 *
12711 * @param bool $get_info true to not load any assets.
12712 *
12713 * @return mixed string light, auto, dark, info array or false.
12714 *
12715 * @since 1.15.0 (J) - 1.5.0 (WP)
12716 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
12717 */
12718 public static function loadAppearancePreferenceAssets($get_info = false)
12719 {
12720 // get document object
12721 $document = JFactory::getDocument();
12722
12723 // load current color scheme preference
12724 $current_pref = self::getAppearancePref();
12725
12726 // define caching values
12727 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
12728
12729 // define file attributes
12730 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
12731
12732 // apposite file path and URI for back-end
12733 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
12734 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
12735
12736 // check if this is for the front-end ("auto" file only)
12737 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
12738 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
12739 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
12740 // check if front-end appearance is disabled
12741 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
12742 // do nothing for the front-end by default
12743 return false;
12744 }
12745 }
12746
12747 if (VBOPlatformDetection::isJoomla()) {
12748 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
12749 $css_uri = str_replace('resources/', '', $css_uri);
12750 }
12751
12752 if (!is_file($css_path)) {
12753 // this preference does not require a specific stylesheet
12754 return false;
12755 }
12756
12757 if ($get_info) {
12758 // do not actually load any assets
12759 return [
12760 'id' => $file_attr['id'],
12761 'href' => $css_uri,
12762 ];
12763 }
12764
12765 // load the apposite CSS file
12766 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
12767
12768 return $current_pref;
12769 }
12770
12771 /**
12772 * VBO trigger for VCM subscription status and updates. If called within the admin section
12773 * returns the expiration reminder array to display a modal window, and sets a warning
12774 * message. If called within the front-end, may trigger an email alert.
12775 *
12776 * @return mixed null, array or boolean depending on client position.
12777 *
12778 * @since 1.15.0 (J) - 1.5.0 (WP)
12779 */
12780 public static function getVCMSubscriptionStatus()
12781 {
12782 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
12783 if (!is_file($vcm_lib_path)) {
12784 // do nothing
12785 return null;
12786 }
12787
12788 // determine client position
12789 $is_admin = self::isAdmin();
12790
12791 try {
12792 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
12793 // VCM is outdated
12794 return null;
12795 }
12796
12797 // always attempt to load the admin lang handler of VCM
12798 $lang = JFactory::getLanguage();
12799
12800 // load the VCM admin language file
12801 $vcm_admin_lang_path = '';
12802 if (VBOPlatformDetection::isJoomla()) {
12803 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
12804 /**
12805 * @joomlaonly if not defined VCM Software Version constant, include the defines files.
12806 */
12807 if (!defined('VIKCHANNELMANAGER_SOFTWARE_VERSION')) {
12808 include_once VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'adapter' . DIRECTORY_SEPARATOR . 'defines.php';
12809 }
12810 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
12811 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
12812 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
12813 /**
12814 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
12815 */
12816 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
12817 }
12818 // load translation file
12819 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
12820 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
12821 /**
12822 * @wponly load language admin handler as well for WP.
12823 * We do this only because of WordPress, but in a way also compatible with Joomla as
12824 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
12825 */
12826 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
12827 }
12828
12829 if ($is_admin) {
12830 // back-end section of the site: check status
12831 if (VikChannelManager::loadExpirationDetails() === false) {
12832 // download and update the expiration details
12833 VikChannelManager::downloadExpirationDetails();
12834 }
12835 $expiration_reminder = null;
12836 if (VikChannelManager::isSubscriptionExpired()) {
12837 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
12838 } else {
12839 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
12840 // we also check if an update is available
12841 if (VikChannelManager::checkUpdates()) {
12842 // an update is more important
12843 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
12844 exit;
12845 }
12846 }
12847 // return reminder status
12848 return $expiration_reminder;
12849 }
12850
12851 // front-end section of the site: trigger alert, if any (bool)
12852 return VikChannelManager::checkSubscriptionReminder();
12853
12854 } catch (Exception $e) {
12855 // do nothing
12856 }
12857
12858 return null;
12859 }
12860
12861 /**
12862 * Given a list of website rate plans, returns the sorted list.
12863 * Naming score is the highest sorting method, pricing scoring is also supported.
12864 *
12865 * @param array $rate_plans the list of rate plan arrays to sort.
12866 * @param bool $assoc whether to keep the original array keys.
12867 *
12868 * @return array the sorted list of rate plan arrays.
12869 *
12870 * @since 1.15.0 (J) - 1.5.0 (WP)
12871 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
12872 */
12873 public static function sortRatePlans($rate_plans, $assoc = false)
12874 {
12875 if (!is_array($rate_plans) || count($rate_plans) < 2) {
12876 return $rate_plans;
12877 }
12878
12879 foreach ($rate_plans as $rp_ind => $rate_plan) {
12880 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
12881 // do not proceed in case of a weird array structure
12882 return $rate_plans;
12883 }
12884
12885 // count the sorting-score of this rate plan
12886 $sort_score = 0;
12887 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
12888 // "standard rate" rate plans should go first
12889 $sort_score += 3;
12890 } else {
12891 $sort_score--;
12892 }
12893
12894 // check for derived rate plan
12895 if (!empty($rate_plan['derived_id']) && !empty($rate_plan['derived_data'])) {
12896 // derived rates should go last
12897 $sort_score -= 2;
12898 } else {
12899 // parent rates should go first
12900 $sort_score += 2;
12901 }
12902
12903 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
12904 // this doesn't seem to be a "non refundable rate" so it should go first
12905 $sort_score += 2;
12906 }
12907
12908 // set sorting score for this rate plan ID
12909 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
12910 }
12911
12912 // apply custom sorting and keep index association
12913 uasort($rate_plans, function($a, $b) {
12914 // sort by name-scoring DESC
12915 $sort_diff = $b['sort_score'] - $a['sort_score'];
12916 if (isset($b['cost'])) {
12917 // apply also sorting by price ASC
12918 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
12919 }
12920 return $sort_diff;
12921 });
12922
12923 return !$assoc ? array_values($rate_plans) : $rate_plans;
12924 }
12925
12926 /**
12927 * Returns an instance of the Availability helper class.
12928 *
12929 * @param bool $anew True for forcing a new instance.
12930 *
12931 * @return VikBookingAvailability
12932 *
12933 * @since 1.15.0 (J) - 1.5.0 (WP)
12934 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
12935 */
12936 public static function getAvailabilityInstance($anew = false)
12937 {
12938 // require library
12939 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
12940
12941 // return the instance of the class
12942 return VikBookingAvailability::getInstance($anew);
12943 }
12944
12945 /**
12946 * Helper method to quickly guess the internal dependency to import.
12947 *
12948 * @param string $path_id either a full path to a file or its identifier.
12949 *
12950 * @return bool true if the requested library was imported or false.
12951 *
12952 * @since 1.15.0 (J) - 1.5.0 (WP)
12953 */
12954 public static function import($path_id)
12955 {
12956 if (empty($path_id)) {
12957 return false;
12958 }
12959
12960 // always append .php extension
12961 $path_id = basename($path_id, '.php') . '.php';
12962
12963 if (is_file($path_id)) {
12964 // full path given
12965 require_once $path_id;
12966
12967 return true;
12968 }
12969
12970 // try guessing where the file id is located
12971 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12972 // library was found
12973 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12974
12975 return true;
12976 }
12977
12978 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12979 // library was found
12980 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12981
12982 return true;
12983 }
12984
12985 return false;
12986 }
12987 }
12988
12989 if (!class_exists('VikResizer'))
12990 {
12991 class VikResizer
12992 {
12993 public function __construct()
12994 {
12995 // objects of this class can also be instantiated without calling the methods statically.
12996 }
12997
12998 /**
12999 * Resizes an image proportionally. For PNG files it can optionally
13000 * trim the image to exclude the transparency, and add some padding to it.
13001 * All PNG files keep the alpha background in the resized version.
13002 *
13003 * @param string $fileimg path to original image file
13004 * @param string $dest path to destination image file
13005 * @param int $towidth
13006 * @param int $toheight
13007 * @param bool $trim_png remove empty background from image
13008 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13009 *
13010 * @return boolean
13011 */
13012 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13013 {
13014 if (!is_file($fileimg)) {
13015 return false;
13016 }
13017 if (empty($towidth) && empty($toheight)) {
13018 return copy($fileimg, $dest);
13019 }
13020
13021 $result = true;
13022
13023 list ($owid, $ohei, $type) = getimagesize($fileimg);
13024
13025 if ($owid > $towidth || $ohei > $toheight) {
13026 $xscale = $owid / $towidth;
13027 $yscale = $ohei / $toheight;
13028 if ($yscale > $xscale) {
13029 $new_width = round($owid * (1 / $yscale));
13030 $new_height = round($ohei * (1 / $yscale));
13031 } else {
13032 $new_width = round($owid * (1 / $xscale));
13033 $new_height = round($ohei * (1 / $xscale));
13034 }
13035
13036 $imageresized = imagecreatetruecolor($new_width, $new_height);
13037
13038 switch ($type) {
13039 case '1' :
13040 $imagetmp = imagecreatefromgif ($fileimg);
13041 break;
13042 case '2' :
13043 $imagetmp = imagecreatefromjpeg($fileimg);
13044 break;
13045 case '18' :
13046 $imagetmp = imagecreatefromwebp($fileimg);
13047 break;
13048 default :
13049 //keep alpha for PNG files
13050 $background = imagecolorallocate($imageresized, 0, 0, 0);
13051 imagecolortransparent($imageresized, $background);
13052 imagealphablending($imageresized, false);
13053 imagesavealpha($imageresized, true);
13054 //
13055 $imagetmp = imagecreatefrompng($fileimg);
13056 break;
13057 }
13058
13059 if (!$imagetmp) {
13060 return false;
13061 }
13062
13063 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13064
13065 switch ($type) {
13066 case '1' :
13067 imagegif($imageresized, $dest);
13068 break;
13069 case '2' :
13070 imagejpeg($imageresized, $dest);
13071 break;
13072 case '18' :
13073 imagewebp($imageresized, $dest);
13074 break;
13075 default :
13076 if ($trim_png) {
13077 self::imageTrim($imageresized, $background, $trim_pad);
13078 }
13079 imagepng($imageresized, $dest);
13080 break;
13081 }
13082
13083 imagedestroy($imageresized);
13084 } else {
13085 $result = copy($fileimg, $dest);
13086 }
13087
13088 if (VBOPlatformDetection::isWordPress()) {
13089 /**
13090 * @wponly in order to not lose resized files after installing an update,
13091 * we need to move any uploaded file onto a recovery folder.
13092 */
13093 VikBookingLoader::import('update.manager');
13094 VikBookingUpdateManager::triggerUploadBackup($dest);
13095 }
13096
13097 return $result;
13098 }
13099
13100 /**
13101 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13102 * trim the image to exclude the transparency, and add some padding to it.
13103 * All PNG files keep the alpha background in the resized version.
13104 *
13105 * @param resource $im Image link resource (reference)
13106 * @param int $bg imagecolorallocate color identifier
13107 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13108 *
13109 * @return void
13110 */
13111 public static function imagetrim(&$im, $bg, $pad = null)
13112 {
13113 // Calculate padding for each side.
13114 if (isset($pad)) {
13115 $pp = explode(' ', $pad);
13116 if (isset($pp[3])) {
13117 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13118 } elseif (isset($pp[2])) {
13119 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13120 } elseif (isset($pp[1])) {
13121 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13122 } else {
13123 $p = array_fill(0, 4, (int) $pp[0]);
13124 }
13125 } else {
13126 $p = array_fill(0, 4, 0);
13127 }
13128
13129 // Get the image width and height.
13130 $imw = imagesx($im);
13131 $imh = imagesy($im);
13132
13133 // Set the X variables.
13134 $xmin = $imw;
13135 $xmax = 0;
13136
13137 // Start scanning for the edges.
13138 for ($iy=0; $iy<$imh; $iy++) {
13139 $first = true;
13140 for ($ix=0; $ix<$imw; $ix++) {
13141 $ndx = imagecolorat($im, $ix, $iy);
13142 if ($ndx != $bg) {
13143 if ($xmin > $ix) {
13144 $xmin = $ix;
13145 }
13146 if ($xmax < $ix) {
13147 $xmax = $ix;
13148 }
13149 if (!isset($ymin)) {
13150 $ymin = $iy;
13151 }
13152 $ymax = $iy;
13153 if ($first) {
13154 $ix = $xmax;
13155 $first = false;
13156 }
13157 }
13158 }
13159 }
13160
13161 // The new width and height of the image. (not including padding)
13162 $imw = 1+$xmax-$xmin; // Image width in pixels
13163 $imh = 1+$ymax-$ymin; // Image height in pixels
13164
13165 // Make another image to place the trimmed version in.
13166 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13167
13168 // Make the background of the new image the same as the background of the old one.
13169 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13170 imagefill($im2, 0, 0, $bg2);
13171
13172 // Copy it over to the new image.
13173 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13174
13175 // To finish up, we replace the old image which is referenced.
13176 $im = $im2;
13177 }
13178
13179 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13180 {
13181 if (!file_exists($fileimg)) {
13182 return false;
13183 }
13184 if (empty($towidth) && empty($toheight)) {
13185 copy($fileimg, $dest);
13186 return true;
13187 }
13188
13189 $exp = explode(",", $rgb);
13190 if (count($exp) == 3) {
13191 $r = trim($exp[0]);
13192 $g = trim($exp[1]);
13193 $b = trim($exp[2]);
13194 } else {
13195 $r = 0;
13196 $g = 0;
13197 $b = 0;
13198 }
13199
13200 list ($owid, $ohei, $type) = getimagesize($fileimg);
13201
13202 if ($owid > $towidth || $ohei > $toheight) {
13203 $xscale = $owid / $towidth;
13204 $yscale = $ohei / $toheight;
13205 if ($yscale > $xscale) {
13206 $new_width = round($owid * (1 / $yscale));
13207 $new_height = round($ohei * (1 / $yscale));
13208 $ydest = 0;
13209 $diff = $towidth - $new_width;
13210 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13211 } else {
13212 $new_width = round($owid * (1 / $xscale));
13213 $new_height = round($ohei * (1 / $xscale));
13214 $xdest = 0;
13215 $diff = $toheight - $new_height;
13216 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13217 }
13218
13219 $imageresized = imagecreatetruecolor($towidth, $toheight);
13220
13221 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13222 imagefill($imageresized, 0, 0, $bgColor);
13223
13224 switch ($type) {
13225 case '1' :
13226 $imagetmp = imagecreatefromgif ($fileimg);
13227 break;
13228 case '2' :
13229 $imagetmp = imagecreatefromjpeg($fileimg);
13230 break;
13231 default :
13232 $imagetmp = imagecreatefrompng($fileimg);
13233 break;
13234 }
13235
13236 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13237
13238 switch ($type) {
13239 case '1' :
13240 imagegif ($imageresized, $dest);
13241 break;
13242 case '2' :
13243 imagejpeg($imageresized, $dest);
13244 break;
13245 default :
13246 imagepng($imageresized, $dest);
13247 break;
13248 }
13249
13250 imagedestroy($imageresized);
13251
13252 return true;
13253 } else {
13254 copy($fileimg, $dest);
13255 }
13256 return true;
13257 }
13258
13259 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13260 {
13261 if (!file_exists($fileimg)) {
13262 return false;
13263 }
13264 if (empty($towidth) && empty($toheight)) {
13265 copy($fileimg, $dest);
13266 return true;
13267 }
13268
13269 list ($owid, $ohei, $type) = getimagesize($fileimg);
13270
13271 if ($owid <= $ohei) {
13272 $new_width = $towidth;
13273 $new_height = ($towidth / $owid) * $ohei;
13274 } else {
13275 $new_height = $toheight;
13276 $new_width = ($new_height / $ohei) * $owid;
13277 }
13278
13279 switch ($type) {
13280 case '1' :
13281 $img_src = imagecreatefromgif ($fileimg);
13282 $img_dest = imagecreate($new_width, $new_height);
13283 break;
13284 case '2' :
13285 $img_src = imagecreatefromjpeg($fileimg);
13286 $img_dest = imagecreatetruecolor($new_width, $new_height);
13287 break;
13288 default :
13289 $img_src = imagecreatefrompng($fileimg);
13290 $img_dest = imagecreatetruecolor($new_width, $new_height);
13291 break;
13292 }
13293
13294 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13295
13296 switch ($type) {
13297 case '1' :
13298 $cropped = imagecreate($towidth, $toheight);
13299 break;
13300 case '2' :
13301 $cropped = imagecreatetruecolor($towidth, $toheight);
13302 break;
13303 default :
13304 $cropped = imagecreatetruecolor($towidth, $toheight);
13305 break;
13306 }
13307
13308 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
13309
13310 switch ($type) {
13311 case '1' :
13312 imagegif ($cropped, $dest);
13313 break;
13314 case '2' :
13315 imagejpeg($cropped, $dest);
13316 break;
13317 default :
13318 imagepng($cropped, $dest);
13319 break;
13320 }
13321
13322 imagedestroy($img_dest);
13323 imagedestroy($cropped);
13324
13325 return true;
13326 }
13327 }
13328 }
13329