PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
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.8.7, at site/helpers/lib.vikbooking.php

14,037 lines 470.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 /**
184 * Returns an associative list of booking room records and occupied sub-unit indexes.
185 *
186 * @param array $order The current booking record for which the list is needed.
187 * @param int $idroom The booked room ID involved for the associative list.
188 *
189 * @return array
190 *
191 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
192 */
193 public static function getRoomUnitNumsUnavailable(array $order, $idroom)
194 {
195 $dbo = JFactory::getDbo();
196
197 // list of data to build
198 $unavailable_indexes = [];
199
200 $first = $order['checkin'];
201 $second = $order['checkout'];
202 $secdiff = $second - $first;
203 $daysdiff = $secdiff / 86400;
204 if (is_int($daysdiff)) {
205 if ($daysdiff < 1) {
206 $daysdiff = 1;
207 }
208 } else {
209 if ($daysdiff < 1) {
210 $daysdiff = 1;
211 } else {
212 $sum = floor($daysdiff) * 86400;
213 $newdiff = $secdiff - $sum;
214 $maxhmore = self::getHoursMoreRb() * 3600;
215 if ($maxhmore >= $newdiff) {
216 $daysdiff = floor($daysdiff);
217 } else {
218 $daysdiff = ceil($daysdiff);
219 }
220 }
221 }
222
223 // the busy records information pool
224 $busyMap = [];
225
226 // query just the busy records
227 $dbo->setQuery(
228 $dbo->getQuery(true)
229 ->select($dbo->qn([
230 'b.idroom',
231 'b.checkin',
232 'b.checkout',
233 'b.realback',
234 'ob.idorder',
235 ]))
236 ->from($dbo->qn('#__vikbooking_busy', 'b'))
237 ->innerJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('ob.idbusy') . ' = ' . $dbo->qn('b.id'))
238 ->where($dbo->qn('b.idroom') . ' = ' . (int) $idroom)
239 ->where($dbo->qn('b.checkin') . ' < ' . $second)
240 ->where($dbo->qn('b.checkout') . ' > ' . $first)
241 ->where($dbo->qn('ob.idorder') . ' != ' . (int) $order['id'])
242 ->order($dbo->qn('b.id') . ' ASC')
243 );
244 $busyRecords = $dbo->loadAssocList();
245
246 if (!$busyRecords) {
247 // useless to proceed
248 return [];
249 }
250
251 // query bookings and rooms based on the busy records obtained
252 $dbo->setQuery(
253 $dbo->getQuery(true)
254 ->select([
255 $dbo->qn('or.id', 'or_id'),
256 $dbo->qn('or.idorder'),
257 $dbo->qn('or.idroom'),
258 $dbo->qn('or.roomindex'),
259 ])
260 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
261 ->where($dbo->qn('or.idroom') . ' = ' . (int) $idroom)
262 ->where($dbo->qn('or.idorder') . ' IN (' . implode(', ', array_map('intval', array_column($busyRecords, 'idorder'))) . ')')
263 ->order($dbo->qn('or.id') . ' ASC')
264 );
265 $busyRooms = $dbo->loadAssocList();
266
267 // build the final map by merging values
268 foreach ($busyRecords as $busyRecord) {
269 // busy signature
270 $signature = sprintf('%d-%d', $busyRecord['idorder'], $busyRecord['idroom']);
271 // push initial record values
272 $busyMap[$signature][] = $busyRecord;
273 }
274
275 // list of booking-id/booking-room-id signatures
276 $roomSignaturesCounter = [];
277
278 foreach ($busyRooms as $busyRoom) {
279 // busy signature
280 $signature = sprintf('%d-%d', $busyRoom['idorder'], $busyRoom['idroom']);
281
282 // determine booking room signature index
283 $roomSignaturesCounter[$signature] = ($roomSignaturesCounter[$signature] ?? -1) + 1;
284
285 if (!isset($busyMap[$signature][$roomSignaturesCounter[$signature]])) {
286 // unmatched signature
287 continue;
288 }
289
290 // merge booking room values with existing busy record values
291 $busyMap[$signature][$roomSignaturesCounter[$signature]] = array_merge($busyMap[$signature][$roomSignaturesCounter[$signature]], $busyRoom);
292 }
293
294 // simulate a list of associative busy and booking room records
295 $busy = [];
296 foreach ($busyMap as $mapList) {
297 foreach ($mapList as $record) {
298 // push record
299 $busy[] = $record;
300 }
301 }
302
303 if (!$busy) {
304 // useless to proceed
305 return [];
306 }
307
308 // get a list of affected timestamps
309 $groupdays = self::getGroupDays($first, $second, $daysdiff);
310
311 // scan the affected stay timestamps to build the sub-unit matches
312 foreach ($groupdays as $gday) {
313 foreach ($busy as $bu) {
314 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
315 continue;
316 }
317 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
318 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
319 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
320 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
321 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
322 }
323 }
324 }
325 }
326
327 return $unavailable_indexes;
328 }
329
330 /**
331 * Returns an associative list of booking room records and available sub-unit indexes.
332 *
333 * @param array $order The current booking record for which the list is needed.
334 * @param int $idroom The booked room ID involved for the associative list.
335 *
336 * @return array
337 *
338 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
339 */
340 public static function getRoomUnitNumsAvailable(array $order, $idroom)
341 {
342 // build available indexes
343 $available_indexes = [];
344
345 // get the unavailable room indexes
346 $unavailable_indexes = self::getRoomUnitNumsUnavailable($order, $idroom);
347
348 // load room params
349 $room_data = self::getRoomInfo($idroom, ['params'], $no_cache = false);
350 $room_params_arr = (array) json_decode($room_data['params'] ?? '[]', true);
351
352 if (is_array($room_params_arr['features'] ?? null) && $room_params_arr['features']) {
353 foreach ($room_params_arr['features'] as $rind => $rfeatures) {
354 if (in_array($rind, $unavailable_indexes)) {
355 continue;
356 }
357 // push available room index
358 $available_indexes[] = $rind;
359 }
360 }
361
362 // return the available indexes
363 return $available_indexes;
364 }
365
366 /**
367 * Load the restrictions applying the given filters to the passed rooms.
368 * The ordering of the query SHOULD remain unchanged, because it's required
369 * to have it by ascending order of the ID. So the older records first.
370 * Even when no filters, we always exclude expired restrictions.
371 *
372 * @param boolean $filters whether to apply filters
373 * @param array $rooms the list of rooms to filter
374 *
375 * @return array the list of restrictions found or an empty array.
376 */
377 public static function loadRestrictions($filters = true, $rooms = [])
378 {
379 $dbo = JFactory::getDbo();
380
381 $restrictions = [];
382 $limts = strtotime(date('Y-m-d'));
383
384 if (!$filters) {
385 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
386 } else {
387 if (!$rooms) {
388 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
389 } else {
390 $clause = [];
391 foreach ($rooms as $idr) {
392 if (empty($idr)) {
393 continue;
394 }
395 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
396 }
397 if ($clause) {
398 $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;";
399 } else {
400 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
401 }
402 }
403 }
404
405 $dbo->setQuery($q);
406 $allrestrictions = $dbo->loadAssocList();
407
408 foreach ($allrestrictions as $k => $res) {
409 if (!empty($res['month'])) {
410 $restrictions[$res['month']] = $res;
411 } else {
412 if (!isset($restrictions['range'])) {
413 $restrictions['range'] = array();
414 }
415 $restrictions['range'][$k] = $res;
416 }
417 }
418
419 return $restrictions;
420 }
421
422 /**
423 * Given a list of restriction records split into range or months, filters
424 * the list by including the "global" restrictions applied to all rooms.
425 *
426 * @param array $restrictions List of restriction records loaded.
427 *
428 * @return array Associative list of filtered restriction records.
429 */
430 public static function globalRestrictions(array $restrictions)
431 {
432 $list = [];
433
434 // iterate all records
435 foreach ($restrictions as $kr => $rr) {
436 if ($kr == 'range') {
437 // scan all date-range restriction records
438 foreach ($rr as $kd => $dr) {
439 if ($dr['allrooms'] == 1) {
440 $list['range'][$kd] = $restrictions[$kr][$kd];
441 }
442 }
443 // go next
444 continue;
445 }
446
447 // month restriction record
448 if ($rr['allrooms'] == 1) {
449 $list[$kr] = $restrictions[$kr];
450 }
451 }
452
453 return $list;
454 }
455
456 /**
457 * Given a list of restriction records split into range or months, filters the list
458 * by including the "listing-level" or "global" restrictions affecting to given rooms.
459 *
460 * @param array $restrictions List of restriction records loaded.
461 * @param array $room_ids List of involved and eligible room IDs.
462 *
463 * @return array Associative list of filtered restriction records.
464 *
465 * @since 1.18.2 (J) - 1.8.2 (WP)
466 */
467 public static function listingRestrictions(array $restrictions, array $room_ids)
468 {
469 $list = [];
470
471 // iterate all records
472 foreach ($restrictions as $kr => $rr) {
473 if ($kr == 'range') {
474 // scan all date-range restriction records
475 foreach ($rr as $kd => $dr) {
476 if ($dr['allrooms'] == 1) {
477 // push global restriction
478 $list['range'][$kd] = $restrictions[$kr][$kd];
479 } elseif (!empty($dr['idrooms'])) {
480 // scan the list of room IDs to match against
481 foreach ($room_ids as $room_id) {
482 if (strpos($dr['idrooms'], '-' . $room_id . '-') !== false) {
483 // push listing-level restriction
484 $list['range'][$kd] = $restrictions[$kr][$kd];
485 break;
486 }
487 }
488 }
489 }
490 // go next
491 continue;
492 }
493
494 // month restriction record
495 if ($rr['allrooms'] == 1) {
496 // push global restriction
497 $list[$kr] = $restrictions[$kr];
498 } elseif (!empty($rr['idrooms'])) {
499 // scan the list of room IDs to match against
500 foreach ($room_ids as $room_id) {
501 if (strpos($rr['idrooms'], '-' . $room_id . '-') !== false) {
502 // push listing-level restriction
503 $list[$kr] = $restrictions[$kr];
504 break;
505 }
506 }
507 }
508 }
509
510 return $list;
511 }
512
513 /**
514 * From the given restrictions, check-in, check-out and nights, looks for
515 * a restriction to be returned and applied over these stays.
516 * In order to give priority to newer Restriction IDs, the order of the
517 * $restrictions array should be ascending, so that the newer restrictions
518 * will overwrite the array with the last ID (more recent). Only for date ranges.
519 * Returns an array with the record of the restriction found. The loop is not
520 * broken when the first valid restriction is found to give priority to
521 * newer restriction IDs to overwrite older records.
522 *
523 * @param int $first the unix timestamp for the check-in date
524 * @param int $second the unix timestamp for the check-out date
525 * @param int $daysdiff the number of nights of stay
526 * @param array $restrictions the list of restrictions loaded
527 *
528 * @return array the restriction found, or an empty array.
529 */
530 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
531 $season_restrictions = array();
532 $restrcheckin = getdate($first);
533 $restrcheckout = getdate($second);
534 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
535 //restriction found for this month, checking:
536 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
537 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
538 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
539 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
540 //Week Day Arrival Restriction
541 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
542 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
543 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
544 }
545 $season_restrictions['wdays'] = $rvalidwdays;
546 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
547 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
548 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
549 }
550 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
551 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
552 }
553 }
554 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
555 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
556 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
557 $season_restrictions['allowed'] = false;
558 }
559 }
560 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
561 $season_restrictions['allowed'] = false;
562 }
563 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
564 } elseif (array_key_exists('range', $restrictions)) {
565 foreach ($restrictions['range'] as $restr) {
566 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
567 //restriction found for this date range, checking:
568 $season_restrictions['id'] = $restr['id'];
569 $season_restrictions['name'] = $restr['name'];
570 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
571 if (strlen((string)$restr['wday']) > 0) {
572 //Week Day Arrival Restriction
573 $rvalidwdays = array($restr['wday']);
574 if (strlen((string)$restr['wdaytwo']) > 0) {
575 $rvalidwdays[] = $restr['wdaytwo'];
576 }
577 $season_restrictions['wdays'] = $rvalidwdays;
578 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
579 if (!empty($restr['ctad'])) {
580 $season_restrictions['cta'] = explode(',', $restr['ctad']);
581 }
582 if (!empty($restr['ctdd'])) {
583 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
584 }
585 }
586 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
587 $season_restrictions['maxlos'] = $restr['maxlos'];
588 if ($daysdiff > $restr['maxlos']) {
589 $season_restrictions['allowed'] = false;
590 }
591 }
592 if ($daysdiff < $restr['minlos']) {
593 $season_restrictions['allowed'] = false;
594 }
595 $season_restrictions['minlos'] = $restr['minlos'];
596 }
597 }
598 }
599
600 return $season_restrictions;
601 }
602
603 public static function compareSeasonRestrictionsNights($restrictions)
604 {
605 $base_compare = array();
606 $base_nights = 0;
607 foreach ($restrictions as $nights => $restr) {
608 $base_compare = $restr;
609 $base_nights = $nights;
610 break;
611 }
612
613 /**
614 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
615 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
616 *
617 * @since 1.15.0 (J) - 1.5.0 (WP)
618 */
619 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
620
621 foreach ($restrictions as $nights => $restr) {
622 if ($nights == $base_nights) {
623 continue;
624 }
625
626 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
627 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
628
629 // get associative array of differences
630 $diff = array_diff($use_base_compare, $use_restr);
631
632 if (count($diff) > 0 && array_key_exists('id', $diff)) {
633 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
634 // only one Restriction per time should be applied to certain Season Dates but check just in case.
635 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
636 }
637 }
638
639 return array();
640 }
641
642 /**
643 * Methods using array_diff() may need one-dimension arrays, because this
644 * native functions applies a string comparison, and so casting an array
645 * value to a string would generate a Notice message. This method simply
646 * converts any sub-array into a restorable string into a non-scalar var.
647 * This only works when comparing 2 array variables.
648 *
649 * @param array $arr the hypothetical multi dimension array.
650 *
651 * @return array the one dimension array to be passed to array_diff()
652 * and the list of keys converted to strings for comparison.
653 *
654 * @since 1.15.0 (J) - 1.5.0 (WP)
655 */
656 public static function prepareMultiDimArrayDiff($arr)
657 {
658 $casted_keys = array();
659
660 if (!is_array($arr) || !count($arr)) {
661 return array($casted_keys, $arr);
662 }
663
664 foreach ($arr as $key => $val) {
665 if (!is_scalar($val)) {
666 // memorize original value
667 $casted_keys[$key] = $val;
668 // cast to string must be applied
669 $arr[$key] = json_encode($val);
670 }
671 }
672
673 return array($casted_keys, $arr);
674 }
675
676 /**
677 * Methods using array_diff() may need one-dimension arrays, because this
678 * native functions applies a string comparison, and so casting an array
679 * value to a string would generate a Notice message. This method restores
680 * the original values of the "prepared" arrays by getting the list of the
681 * modified keys from non-scalar to scalar values.
682 * This only works when comparing 2 array variables.
683 *
684 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
685 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
686 * @param array $arr the array result of array_diff().
687 *
688 * @return array the array result of array_diff() with its original values.
689 *
690 * @since 1.15.0 (J) - 1.5.0 (WP)
691 */
692 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
693 {
694 if (!is_array($arr) || !count($arr)) {
695 return $arr;
696 }
697
698 foreach ($arr as $key => $val) {
699 // keys returned by array_diff can be present in both $keys_one and $keys_two
700 if (isset($keys_one[$key])) {
701 // always give higher priority to the 1st argument keys for array_diff()
702 $arr[$key] = json_decode($keys_one[$key]);
703 } elseif (isset($keys_two[$key])) {
704 // fallback to 2nd argument keys
705 $arr[$key] = json_decode($keys_two[$key]);
706 }
707 }
708
709 return $arr;
710 }
711
712 public static function roomRestrictions($roomid, $restrictions) {
713 $ret = array();
714 if (!empty($roomid) && count($restrictions) > 0) {
715 foreach ($restrictions as $kr => $rr) {
716 if ($kr == 'range') {
717 foreach ($rr as $kd => $dr) {
718 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
719 $allrooms = explode(';', $dr['idrooms']);
720 if (in_array('-'.$roomid.'-', $allrooms)) {
721 $ret['range'][$kd] = $restrictions[$kr][$kd];
722 }
723 }
724 }
725 } else {
726 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
727 $allrooms = explode(';', $rr['idrooms']);
728 if (in_array('-'.$roomid.'-', $allrooms)) {
729 $ret[$kr] = $restrictions[$kr];
730 }
731 }
732 }
733 }
734 }
735 return $ret;
736 }
737
738 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
739 {
740 // default states
741 $restrictionerrmsg = '';
742 $restrictions_affcount = 0;
743 $minlos_errors_pool = [];
744
745 // check for month-level or range-level restrictions
746 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
747 //restriction found for this month, checking:
748 $restrictions_affcount++;
749 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
750 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
751 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
752 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
753 }
754 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
755 $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']) : ''));
756 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
757 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
758 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
759 }
760 }
761 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
762 if (count($comborestr) > 0) {
763 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
764 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
765 $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']));
766 }
767 }
768 }
769 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
770 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
771 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
772 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
773 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
774 }
775 }
776 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
777 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
778 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
779 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
780 }
781 }
782 }
783 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
784 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
785 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
786 }
787 }
788 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
789 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
790 }
791 } elseif (array_key_exists('range', $roomrestr)) {
792 // start valid flag
793 $restrictionsvalid = true;
794
795 /**
796 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
797 * The most recent restrictions will have a higher priority over the oldest ones.
798 *
799 * @since 1.12.1
800 */
801 $minlos_priority = [
802 'ok' => [],
803 'nok' => [],
804 ];
805
806 /**
807 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
808 *
809 * @since 1.16.10 (J) - 1.6.10 (WP)
810 */
811 $ctad_priority = [
812 'ok' => [],
813 'nok' => [],
814 ];
815
816 foreach ($roomrestr['range'] as $restr) {
817 /**
818 * We should not always add 82799 seconds to the end date of the restriction
819 * because if they only last for one day (like a Saturday), then $restr['dto']
820 * will be already set to the time 23:59:59.
821 *
822 * @since 1.13 (J) - 1.2.18 (WP)
823 * @since 1.18.3 (J) - 1.8.3 (WP) timestamps ending with 9 for a range greater than 1 day indicate that the restriction end time
824 * is already set to 23:59:59 (VCM Connector may be responsible), so no seconds should be added or the restriction end date
825 * will be the day after, and if after the check-in time, it may be included.
826 */
827 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) && substr((string) $restr['dto'], -1, 1) != 9 ? 82799 : 0;
828
829 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
830 // restriction found for this date range based on arrival date, check if compliant
831 $restrictions_affcount++;
832
833 // set flag for CTA/CTD compliance
834 $cta_ctd_passed = true;
835
836 if (strlen((string)$restr['wday']) > 0) {
837 $rvalidwdays = array($restr['wday']);
838 if (strlen((string)$restr['wdaytwo']) > 0) {
839 $rvalidwdays[] = $restr['wdaytwo'];
840 }
841 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
842 $restrictionsvalid = false;
843 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
844 } elseif ($restr['multiplyminlos'] == 1) {
845 if (($daysdiff % $restr['minlos']) != 0) {
846 $restrictionsvalid = false;
847 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
848 }
849 }
850 $comborestr = self::parseJsDrangeWdayCombo($restr);
851 if ($comborestr) {
852 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
853 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
854 $restrictionsvalid = false;
855 $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']));
856 }
857 }
858 }
859 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
860 if (!empty($restr['ctad'])) {
861 $ctarestrictions = explode(',', $restr['ctad']);
862 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
863 $restrictionsvalid = false;
864 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
865 $cta_ctd_passed = false;
866 }
867 }
868 if (!empty($restr['ctdd'])) {
869 $ctdrestrictions = explode(',', $restr['ctdd']);
870 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
871 $restrictionsvalid = false;
872 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
873 $cta_ctd_passed = false;
874 }
875 }
876 }
877
878 // check CTA/CTD compliance
879 if (!$cta_ctd_passed) {
880 array_push($ctad_priority['nok'], (int)$restr['id']);
881 } else {
882 array_push($ctad_priority['ok'], (int)$restr['id']);
883 }
884
885 // max LOS validation
886 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
887 if ($daysdiff > $restr['maxlos']) {
888 $restrictionsvalid = false;
889 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
890 }
891 }
892
893 // min LOS validation
894 if ($daysdiff < $restr['minlos']) {
895 $restrictionsvalid = false;
896 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
897 // push error value
898 array_push($minlos_priority['nok'], (int) $restr['id']);
899 // set error message with related minimum stay
900 $minlos_errors_pool[$restr['minlos']] = $restrictionerrmsg;
901 } else {
902 array_push($minlos_priority['ok'], (int) $restr['id']);
903 }
904 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
905 /**
906 * We validate the CTD restrictions depending on the check-out date.
907 *
908 * @since 1.16.3 (J) - 1.6.3 (WP)
909 */
910 $ctdrestrictions = explode(',', $restr['ctdd']);
911 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
912 $restrictions_affcount++;
913 $restrictionsvalid = false;
914 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
915 }
916 }
917 }
918
919 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
920 // a more recent restriction is allowing this MinLOS
921 // ensure there are no recent CTA/CTD rules not compliant
922 $cta_ctd_priority_ok = true;
923
924 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
925 // a more recent restriction is not compliant with the CTA/CTD rules
926 $cta_ctd_priority_ok = false;
927 }
928
929 if ($cta_ctd_priority_ok) {
930 // we unset the error message because more recent restriction(s) are allowing this stay dates
931 $restrictionerrmsg = '';
932 }
933 }
934 }
935
936 // check global restriction of Min LOS for TAC functions in VBO and VCM
937 if (empty($restrictionerrmsg) && $roomrestr && $restrictions_affcount <= 0) {
938 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
939 $globminlos = self::getDefaultNightsCalendar();
940 if ($globminlos > 1 && $daysdiff < $globminlos) {
941 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
942 }
943 }
944
945 /**
946 * When working with room-level restrictions and different minLOS across multiple apartments,
947 * we need the display the actually lowest minimum stay among all listings from the errors found.
948 *
949 * @since 1.17.3 (J) - 1.7.3 (WP)
950 */
951 if ($restrictionerrmsg && in_array($restrictionerrmsg, $minlos_errors_pool) && count($minlos_errors_pool) > 1) {
952 // make sure to return the error message for the lowest minimum stay
953 $lowest_minlos = min(array_map('intval', array_keys($minlos_errors_pool)));
954 $restrictionerrmsg = $minlos_errors_pool[$lowest_minlos] ?? $restrictionerrmsg;
955 }
956
957 // return the restriction error message string, if anything wrong was found
958 return $restrictionerrmsg;
959 }
960
961 public static function parseJsDrangeWdayCombo($drestr) {
962 $combo = array();
963 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
964 $cparts = explode(':', $drestr['wdaycombo']);
965 foreach ($cparts as $kc => $cw) {
966 if (!empty($cw)) {
967 $nowcombo = explode('-', $cw);
968 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
969 }
970 }
971 }
972 return $combo;
973 }
974
975 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
976 $dbo = JFactory::getDbo();
977 $pkg = array();
978 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
979 $dbo->setQuery($q);
980 $dbo->execute();
981 if ($dbo->getNumRows() == 1) {
982 $pkg = $dbo->loadAssoc();
983 $vbo_tn = self::getTranslator();
984 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
985 } else {
986 return JText::translate('VBOPKGERRNOTFOUND');
987 }
988 $rooms_req = array();
989 foreach ($rooms as $num => $room) {
990 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
991 $rooms_req[] = $room['id'];
992 }
993 }
994 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
995 $dbo->setQuery($q);
996 $dbo->execute();
997 if ($dbo->getNumRows() != count($rooms_req)) {
998 //error, not all the rooms requested are available for this package
999 return JText::translate('VBOPKGERRNOTROOM');
1000 }
1001 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
1002 return JText::translate('VBOPKGERRNUMNIGHTS');
1003 }
1004 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
1005 return JText::translate('VBOPKGERRCHECKIND');
1006 }
1007 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
1008 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
1009 return JText::translate('VBOPKGERRCHECKOUTD');
1010 }
1011 if (!empty($pkg['excldates'])) {
1012 //this would check if any stay date is excluded
1013 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
1014 //check just the arrival and departure dates
1015 $bookdates_ts = array($checkints, $checkoutts);
1016 $bookdates = array();
1017 foreach ($bookdates_ts as $bookdate_ts) {
1018 $info_d = getdate($bookdate_ts);
1019 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
1020 }
1021 $edates = explode(';', $pkg['excldates']);
1022 foreach ($edates as $edate) {
1023 if (!empty($edate) && in_array($edate, $bookdates)) {
1024 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
1025 }
1026 }
1027 }
1028 return $pkg;
1029 }
1030
1031 public static function getPackage($pkg_id) {
1032 $dbo = JFactory::getDbo();
1033 $pkg = array();
1034 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1035 $dbo->setQuery($q);
1036 $dbo->execute();
1037 if ($dbo->getNumRows() == 1) {
1038 $pkg = $dbo->loadAssoc();
1039 }
1040 return $pkg;
1041 }
1042
1043 /**
1044 * Returns the requested room parameter or default value.
1045 *
1046 * @param string $paramname The parameter name.
1047 * @param mixed $params The room params string, or json-decoded array/object.
1048 * @param mixed $def The default value to return as fallback.
1049 *
1050 * @return mixed
1051 *
1052 * @since 1.17.3 (J) - 1.7.3 (WP) added 3rd argument $def.
1053 */
1054 public static function getRoomParam($paramname, $params, $def = '')
1055 {
1056 if (empty($params)) {
1057 return $def;
1058 }
1059
1060 if (is_string($params)) {
1061 $params = (array) json_decode($params, true);
1062 } elseif (is_object($params)) {
1063 $params = (array) $params;
1064 }
1065
1066 if (!is_array($params)) {
1067 return $def;
1068 }
1069
1070 return $params[$paramname] ?? $def;
1071 }
1072
1073 public static function filterNightsSeasonsCal($arr_nights) {
1074 $nights = array();
1075 foreach ($arr_nights as $night) {
1076 if (intval(trim($night)) > 0) {
1077 $nights[] = intval(trim($night));
1078 }
1079 }
1080 sort($nights);
1081 return array_unique($nights);
1082 }
1083
1084 public static function getSeasonRangeTs($from, $to, $year) {
1085 $sfrom = 0;
1086 $sto = 0;
1087 $tsbase = mktime(0, 0, 0, 1, 1, $year);
1088 $curyear = $year;
1089 $tsbasetwo = $tsbase;
1090 $curyeartwo = $year;
1091 if ($from > $to) {
1092 //between two years
1093 $curyeartwo += 1;
1094 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
1095 }
1096 $sfrom = ($tsbase + $from);
1097 $sto = ($tsbasetwo + $to);
1098 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
1099 //leap years
1100 $infoseason = getdate($sfrom);
1101 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1102 if ($infoseason[0] > $leapts) {
1103 /**
1104 * Timestamp must be greater than the leap-day of Feb 29th.
1105 * It used to be checked for >= $leapts.
1106 *
1107 * @since July 3rd 2019
1108 */
1109 $sfrom += 86400;
1110 if ($curyear == $curyeartwo) {
1111 $sto += 86400;
1112 }
1113 }
1114 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
1115 //leap years
1116 $infoseason = getdate($sto);
1117 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1118 if ($infoseason[0] > $leapts) {
1119 /**
1120 * Timestamp must be greater than the leap-day of Feb 29th.
1121 * It used to be checked for >= $leapts.
1122 *
1123 * @since July 3rd 2019
1124 */
1125 $sto += 86400;
1126 }
1127 }
1128 return array($sfrom, $sto);
1129 }
1130
1131 public static function sortSeasonsRangeTs ($all_seasons) {
1132 $sorted = array();
1133 $map = array();
1134 foreach ($all_seasons as $key => $season) {
1135 $map[$key] = $season['from_ts'];
1136 }
1137 asort($map);
1138 foreach ($map as $key => $s) {
1139 $sorted[] = $all_seasons[$key];
1140 }
1141 return $sorted;
1142 }
1143
1144 public static function formatSeasonDates ($from_ts, $to_ts) {
1145 $one = getdate($from_ts);
1146 $two = getdate($to_ts);
1147 $months_map = array(
1148 1 => JText::translate('VBSHORTMONTHONE'),
1149 2 => JText::translate('VBSHORTMONTHTWO'),
1150 3 => JText::translate('VBSHORTMONTHTHREE'),
1151 4 => JText::translate('VBSHORTMONTHFOUR'),
1152 5 => JText::translate('VBSHORTMONTHFIVE'),
1153 6 => JText::translate('VBSHORTMONTHSIX'),
1154 7 => JText::translate('VBSHORTMONTHSEVEN'),
1155 8 => JText::translate('VBSHORTMONTHEIGHT'),
1156 9 => JText::translate('VBSHORTMONTHNINE'),
1157 10 => JText::translate('VBSHORTMONTHTEN'),
1158 11 => JText::translate('VBSHORTMONTHELEVEN'),
1159 12 => JText::translate('VBSHORTMONTHTWELVE')
1160 );
1161 $mday_map = array(
1162 1 => JText::translate('VBMDAYFRIST'),
1163 2 => JText::translate('VBMDAYSECOND'),
1164 3 => JText::translate('VBMDAYTHIRD'),
1165 'generic' => JText::translate('VBMDAYNUMGEN')
1166 );
1167 if ($one['year'] == $two['year']) {
1168 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>';
1169 }
1170 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'];
1171 }
1172
1173 public static function getFirstCustDataField($custdata) {
1174 $first_field = '';
1175 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1176 //Room is closed with this booking
1177 return '----';
1178 }
1179 $parts = explode("\n", $custdata);
1180 foreach ($parts as $part) {
1181 if (!empty($part)) {
1182 $field = explode(':', trim($part));
1183 if (!empty($field[1])) {
1184 return trim($field[1]);
1185 }
1186 }
1187 }
1188 return $first_field;
1189 }
1190
1191 /**
1192 * This method composes a string to be logged for the admin
1193 * to keep track of what was inside the booking before the
1194 * modification. Returns a string and it uses language definitions
1195 * that should be available on the front-end and back-end INI files.
1196 *
1197 * @param array $old_booking the array of the booking prior to the modification
1198 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1199 *
1200 * @return string text describing the situation with the booking before the changes.
1201 *
1202 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1203 */
1204 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1205 {
1206 $vbo_df = self::getDateFormat();
1207 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1208 $wdays_map = array(
1209 JText::translate('VBWEEKDAYZERO'),
1210 JText::translate('VBWEEKDAYONE'),
1211 JText::translate('VBWEEKDAYTWO'),
1212 JText::translate('VBWEEKDAYTHREE'),
1213 JText::translate('VBWEEKDAYFOUR'),
1214 JText::translate('VBWEEKDAYFIVE'),
1215 JText::translate('VBWEEKDAYSIX'),
1216 );
1217 $now_info = getdate();
1218 $checkin_info = getdate($old_booking['checkin']);
1219 $checkout_info = getdate($old_booking['checkout']);
1220
1221 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1222 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1223 $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]);
1224 $prev_rooms = '';
1225
1226 $orooms_map = [];
1227 $orooms_arr = [];
1228
1229 if (isset($old_booking['rooms_info'])) {
1230 foreach ($old_booking['rooms_info'] as $oroom) {
1231 $orooms_arr[] = $oroom['name'].', '.JText::translate('VBMAILADULTS').': '.$oroom['adults'].', '.JText::translate('VBMAILCHILDREN').': '.$oroom['children'];
1232 if (!empty($oroom['idroom'])) {
1233 $orooms_map[$oroom['idroom']] = $oroom['name'];
1234 }
1235 }
1236 $prev_rooms = implode("\n", $orooms_arr);
1237 }
1238
1239 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && count($orooms_map)) {
1240 $split_stay_prev_infos = [];
1241 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1242 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1243 continue;
1244 }
1245 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1246 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1247 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1248
1249 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1250 if (!empty($room_nights)) {
1251 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1252 }
1253 $split_stay_descr .= ', ';
1254 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1255
1256 // push split stay description string
1257 $split_stay_prev_infos[] = $split_stay_descr;
1258 }
1259 if (count($split_stay_prev_infos)) {
1260 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1261 }
1262 }
1263
1264 $currencyname = self::getCurrencyName();
1265 $prev_total = self::formatCurrencyNumber(self::numberFormat($old_booking['total']), $currencyname);
1266
1267 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1268 }
1269
1270 /**
1271 * This method invokes the class
1272 * VikChannelManagerLogos (new in VCM 1.6.4) to
1273 * map the name of a channel to its corresponding logo.
1274 * The method can also be used to get an istance of the class.
1275 *
1276 * @param mixed $provenience either a string or an array with main and sub channels.
1277 * @param boolean $get_istance
1278 *
1279 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1280 */
1281 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1282 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1283 return false;
1284 }
1285 if (!class_exists('VikChannelManagerLogos')) {
1286 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1287 }
1288
1289 /**
1290 * Due to the new iCal channel we now support main and sub channels.
1291 *
1292 * @since 1.13 - VCM 1.7.0
1293 */
1294 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1295 $provenience = explode('_', $provenience);
1296 }
1297 $main_channel = '';
1298 $full_channel = '';
1299 if (is_array($provenience)) {
1300 if (count($provenience) > 1) {
1301 $main_channel = $provenience[1];
1302 } else {
1303 $main_channel = $provenience[0];
1304 }
1305 if (stripos($provenience[0], 'ical') !== false) {
1306 $full_channel = implode('_', $provenience);
1307 }
1308 } else {
1309 $main_channel = $provenience;
1310 }
1311
1312 // get object instance by passing the main provenience
1313 $obj = new VikChannelManagerLogos($main_channel);
1314
1315 // update provenience with main and full channel
1316 if (!empty($full_channel)) {
1317 $obj->setProvenience($main_channel, $full_channel);
1318 }
1319
1320 // return either the instance or the logo URL for this channel source
1321 return $get_istance ? $obj : $obj->getLogoURL();
1322 }
1323
1324 public static function vcmAutoUpdate()
1325 {
1326 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1327 return -1;
1328 }
1329
1330 $vcm_auto_upd = (int)VBOFactory::getConfig()->get('vcmautoupd', 0);
1331
1332 return $vcm_auto_upd ? 1 : 0;
1333 }
1334
1335 public static function getVcmInvoker()
1336 {
1337 if (!class_exists('VboVcmInvoker')) {
1338 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1339 }
1340
1341 return new VboVcmInvoker();
1342 }
1343
1344 /**
1345 * Returns an instance of VikBooking History object.
1346 *
1347 * @param int $bid optional booking ID to bind.
1348 *
1349 * @return VboBookingHistory
1350 *
1351 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1352 */
1353 public static function getBookingHistoryInstance($bid = 0)
1354 {
1355 if (!class_exists('VboBookingHistory')) {
1356 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1357 }
1358
1359 return new VboBookingHistory($bid);
1360 }
1361
1362 /**
1363 * Returns an instance of the VCMChatHandler class to handle
1364 * the messaging/chat for the given reservation ID.
1365 *
1366 * @param int $oid the ID of the booking in VBO
1367 * @param string $channel the name of the source channel
1368 *
1369 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1370 *
1371 * @since 1.11.2 (J) - 1.1.2 (WP)
1372 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1373 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1374 * @since 1.17.6 (J) - 1.7.6 (WP) empty signature arguments will only require the dependencies.
1375 */
1376 public static function getVcmChatInstance($oid, $channel = null)
1377 {
1378 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1379 if (!is_file($vcm_messaging_helper)) {
1380 // VCM is not available
1381 return null;
1382 }
1383
1384 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1385 // VCM must be inactive, prevent the loading of the instance
1386 return null;
1387 }
1388
1389 // make sure the channel name is correct as it may contain the sub-network
1390 if (!empty($channel)) {
1391 $segments = explode('_', $channel);
1392 if (count($segments) > 1) {
1393 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1394 $channel = $segments[0];
1395 }
1396 }
1397
1398 // always require main file of the abstract class even if arguments are empty/invalid
1399 require_once $vcm_messaging_helper;
1400
1401 if (empty($oid) && empty($channel)) {
1402 // do not proceed in order to save resources
1403 return null;
1404 }
1405
1406 // return the instance of the class for this channel handler
1407 return VCMChatHandler::getInstance($oid, $channel);
1408 }
1409
1410 /**
1411 * Returns an instance of the VCMOpportunityHandler class to handle
1412 * the various opportunities through VCM.
1413 *
1414 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1415 *
1416 * @since 1.2.0
1417 */
1418 public static function getVcmOpportunityInstance() {
1419 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1420 if (!is_file($vcm_opp_helper)) {
1421 // VCM is not available
1422 return null;
1423 }
1424 // require main file of the class
1425 require_once $vcm_opp_helper;
1426 // return the instance of the class
1427 return VCMOpportunityHandler::getInstance();
1428 }
1429
1430 /**
1431 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1432 *
1433 * @param array $data optional data to bind to the object.
1434 *
1435 * @return bool
1436 *
1437 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1438 *
1439 * @requires VCM >= 1.8.24
1440 */
1441 public static function vcmBcomReportingSupported(array $data = [])
1442 {
1443 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1444 }
1445
1446 /**
1447 * Gets a list of all channels supporting the promotions.
1448 *
1449 * @param string $key the key of the handler.
1450 *
1451 * @return mixed false if VCM is not installed, empty array otherwise.
1452 *
1453 * @requires Vik Channel Manager 1.7.1
1454 *
1455 * @since 1.3.0
1456 */
1457 public static function getPromotionHandlers($key = null)
1458 {
1459 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1460 // VCM not installed
1461 return false;
1462 }
1463
1464 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1465 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1466 return [];
1467 }
1468
1469 return VikChannelManager::getPromotionHandlers($key);
1470 }
1471
1472 /**
1473 * Gets the factors for suggesting the application of the promotions.
1474 *
1475 * @param mixed $data some optional instructions to be passed as argument.
1476 *
1477 * @return mixed false if VCM is not installed, associative array otherwise.
1478 *
1479 * @requires Vik Channel Manager 1.7.1
1480 *
1481 * @since 1.3.0
1482 */
1483 public static function getPromotionFactors($data = null)
1484 {
1485 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1486 // VCM not installed
1487 return false;
1488 }
1489
1490 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1491 // VCM is outdated
1492 return false;
1493 }
1494
1495 return VikChannelManager::getPromotionFactors($data);
1496 }
1497
1498 public static function getTheme()
1499 {
1500 return VBOFactory::getConfig()->get('theme', '');
1501 }
1502
1503 public static function getFooterOrdMail($vbo_tn = null)
1504 {
1505 $dbo = JFactory::getDbo();
1506
1507 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1508 $dbo->setQuery($q);
1509 $ft = $dbo->loadAssocList();
1510
1511 if (!is_object($vbo_tn)) {
1512 $vbo_tn = self::getTranslator();
1513 }
1514
1515 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1516
1517 return $ft ? $ft[0]['setting'] : '';
1518 }
1519
1520 public static function requireLogin()
1521 {
1522 return VBOFactory::getConfig()->getBool('requirelogin', false);
1523 }
1524
1525 public static function autoRoomUnit()
1526 {
1527 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1528 }
1529
1530 public static function todayBookings()
1531 {
1532 static $todayBookings = null;
1533
1534 if ($todayBookings === null) {
1535 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1536 }
1537
1538 return $todayBookings;
1539 }
1540
1541 public static function couponsEnabled()
1542 {
1543 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1544 }
1545
1546 public static function customersPinEnabled()
1547 {
1548 return VBOFactory::getConfig()->getBool('enablepin', false);
1549 }
1550
1551 /**
1552 * Detects the type of visitor from the user agent.
1553 * Known types are: computer, smartphone, tablet.
1554 *
1555 * @param boolean $returnua whether the type of visitor should be returned. If false
1556 * boolean is returned in case of mobile device detected.
1557 * @param boolean $loadassets whether the system should load an apposite CSS file.
1558 *
1559 * @return mixed string for the type of visitor or boolean if mobile detected.
1560 *
1561 * @since 1.0.13 - Revision September 2018
1562 */
1563 public static function detectUserAgent($returnua = false, $loadassets = true) {
1564 $session = JFactory::getSession();
1565 $sval = $session->get('vbuseragent', '');
1566 $mobiles = array('tablet', 'smartphone');
1567 if (!empty($sval)) {
1568 if ($loadassets) {
1569 self::userAgentStyleSheet($sval);
1570 }
1571 return $returnua ? $sval : in_array($sval, $mobiles);
1572 }
1573
1574 // detect visitor (device) type
1575 $visitoris = 'computer';
1576 try {
1577 if (!class_exists('MobileDetector')) {
1578 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1579 }
1580 $detector = new MobileDetector;
1581 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1582 } catch (Exception $e) {
1583 // do nothing
1584 }
1585
1586 $session->set('vbuseragent', $visitoris);
1587 if ($loadassets) {
1588 self::userAgentStyleSheet($visitoris);
1589 }
1590
1591 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1592 }
1593
1594 public static function userAgentStyleSheet($ua)
1595 {
1596 /**
1597 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1598 */
1599 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1600 return;
1601 }
1602 //
1603
1604 $document = JFactory::getDocument();
1605 /**
1606 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1607 */
1608 if ($ua == 'smartphone') {
1609 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1610 } elseif ($ua == 'tablet') {
1611 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1612 }
1613 return true;
1614 }
1615
1616 public static function loadJquery()
1617 {
1618 return VBOFactory::getConfig()->getBool('loadjquery', true);
1619 }
1620
1621 public static function loadBootstrap()
1622 {
1623 return VBOFactory::getConfig()->getBool('bootstrap', true);
1624 }
1625
1626 public static function allowMultiLanguage()
1627 {
1628 return VBOFactory::getConfig()->getBool('multilang', true);
1629 }
1630
1631 public static function getTranslator()
1632 {
1633 if (!class_exists('VikBookingTranslator')) {
1634 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1635 }
1636
1637 return new VikBookingTranslator();
1638 }
1639
1640 /**
1641 * New method's name spelled correctly. The object will be cached by default.
1642 *
1643 * @return VikBookingCustomersPin
1644 *
1645 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1646 */
1647 public static function getCPinInstance()
1648 {
1649 static $cpin_instance = null;
1650
1651 if (!$cpin_instance) {
1652 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1653
1654 $cpin_instance = new VikBookingCustomersPin();
1655 }
1656
1657 return $cpin_instance;
1658 }
1659
1660 /**
1661 * Old method's name spelled wrongly for BC.
1662 *
1663 * @return VikBookingCustomersPin
1664 */
1665 public static function getCPinIstance()
1666 {
1667 return self::getCPinInstance();
1668 }
1669
1670 /**
1671 * Returns an instance of the VikBookingTracker Class.
1672 * It is also possible to call this method to just require the library.
1673 * This is useful for the back-end to access some static methods
1674 * without tracking any data.
1675 *
1676 * @param boolean $require_only whether to return the object.
1677 *
1678 * @return VikBookingTracker
1679 *
1680 * @since 1.11
1681 */
1682 public static function getTracker($require_only = false) {
1683 if (!class_exists('VikBookingTracker')) {
1684 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1685 }
1686 return $require_only ? true : VikBookingTracker::getInstance();
1687 }
1688
1689 /**
1690 * Returns an instance of the VikBookingOperator Class.
1691 *
1692 * @return VikBookingOperator
1693 *
1694 * @since 1.11
1695 */
1696 public static function getOperatorInstance() {
1697 if (!class_exists('VikBookingOperator')) {
1698 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1699 }
1700 return VikBookingOperator::getInstance();
1701 }
1702
1703 /**
1704 * Returns an instance of the VikBookingFestivities Class.
1705 *
1706 * @return VikBookingFestivities
1707 *
1708 * @since 1.12
1709 */
1710 public static function getFestivitiesInstance() {
1711 if (!class_exists('VikBookingFestivities')) {
1712 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1713 }
1714 return VikBookingFestivities::getInstance();
1715 }
1716
1717 /**
1718 * Returns an instance of the VikBookingCriticalDates Class.
1719 *
1720 * @return VikBookingCriticalDates
1721 *
1722 * @since 1.13.5
1723 */
1724 public static function getCriticalDatesInstance() {
1725 if (!class_exists('VikBookingCriticalDates')) {
1726 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1727 }
1728 return VikBookingCriticalDates::getInstance();
1729 }
1730
1731 /**
1732 * Checks whether the chat is enabled.
1733 *
1734 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1735 *
1736 * @since 1.12
1737 */
1738 public static function chatEnabled() {
1739 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1740 return -1;
1741 }
1742 $dbo = JFactory::getDbo();
1743 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1744 $dbo->setQuery($q);
1745 $dbo->execute();
1746 if (!$dbo->getNumRows()) {
1747 // enable the chat by default if VCM is installed
1748 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1749 $dbo->setQuery($q);
1750 $dbo->execute();
1751 return 1;
1752 }
1753 $s = $dbo->loadResult();
1754 return intval($s);
1755 }
1756
1757 /**
1758 * Loads the chat parameters from the configuration.
1759 *
1760 * @return object stdClass object from decoded JSON string
1761 *
1762 * @since 1.12
1763 */
1764 public static function getChatParams() {
1765 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1766 return new stdClass;
1767 }
1768 $dbo = JFactory::getDbo();
1769 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1770 $dbo->setQuery($q);
1771 $dbo->execute();
1772 if (!$dbo->getNumRows()) {
1773 // compose default and basic chat params for the first time
1774 $basic_params = new stdClass;
1775 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1776 $basic_params->av_type = 'checkin';
1777 $basic_params->av_days = 0;
1778
1779 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1780 $dbo->setQuery($q);
1781 $dbo->execute();
1782
1783 return $basic_params;
1784 }
1785 return json_decode($dbo->loadResult());
1786 }
1787
1788 /**
1789 * Checks whether the pre-checkin is enabled.
1790 *
1791 * @return int 0 if disabled, 1 otherwise
1792 *
1793 * @since 1.12
1794 */
1795 public static function precheckinEnabled()
1796 {
1797 return (int) VBOFactory::getConfig()->getBool('precheckinenabled', true);
1798 }
1799
1800 /**
1801 * Returns the minimum number of days in advance to
1802 * enable the pre-checkin via front-end.
1803 *
1804 * @return int the min number of days in advance for pre-checkin.
1805 *
1806 * @since 1.12
1807 */
1808 public static function precheckinMinOffset()
1809 {
1810 return VBOFactory::getConfig()->getInt('precheckinminoffset', 0);
1811 }
1812
1813 /**
1814 * Whether upselling extra services is enabled.
1815 *
1816 * @return boolean true if enabled, false otherwise.
1817 *
1818 * @since 1.13
1819 */
1820 public static function upsellingEnabled()
1821 {
1822 return VBOFactory::getConfig()->getBool('upselling', true);
1823 }
1824
1825 /**
1826 * Collects a list of options/extra that can be upsold for the
1827 * the rooms booked.
1828 *
1829 * @param array $upsell_data list of stdClass objects for each room booked.
1830 * @param array $info some booking details (id, checkin, checkout).
1831 * @param object $vbo_tn the translation object.
1832 *
1833 * @return array list of upsellable options for each room booked.
1834 *
1835 * @since 1.13 (J) - 1.3.0 (WP)
1836 * @since 1.17.7 (J) - 1.7.7 (WP) added support for damage deposit to confirmed bookings.
1837 */
1838 public static function loadUpsellingData($upsell_data, $info, $vbo_tn)
1839 {
1840 $dbo = JFactory::getDbo();
1841
1842 // get all rooms booked
1843 $all_room_ids = [];
1844 foreach ($upsell_data as $v) {
1845 if (!in_array($v->id, $all_room_ids)) {
1846 array_push($all_room_ids, (int) $v->id);
1847 }
1848 }
1849
1850 $dbo->setQuery(
1851 $dbo->getQuery(true)
1852 ->select([
1853 $dbo->qn('id'),
1854 $dbo->qn('idopt'),
1855 ])
1856 ->from($dbo->qn('#__vikbooking_rooms'))
1857 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_room_ids) . ')')
1858 );
1859 $records = $dbo->loadAssocList();
1860 if (!$records) {
1861 // no rooms found
1862 return [];
1863 }
1864
1865 // get all suitable options
1866 $all_options = [];
1867 $rooms_options = [];
1868 foreach ($records as $v) {
1869 $allopts = explode(';', $v['idopt']);
1870 $room_opt = [];
1871 foreach ($allopts as $o) {
1872 if (empty($o)) {
1873 continue;
1874 }
1875 if (!in_array($o, $all_options)) {
1876 array_push($all_options, (int) $o);
1877 }
1878 array_push($room_opt, (int) $o);
1879 }
1880 $rooms_options[$v['id']] = $room_opt;
1881 }
1882 if (!$all_options) {
1883 // no options found
1884 return [];
1885 }
1886
1887 // load all options that could be used by the booked rooms no matter what was already booked
1888 $dbo->setQuery(
1889 $dbo->getQuery(true)
1890 ->select('*')
1891 ->from($dbo->qn('#__vikbooking_optionals'))
1892 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1893 ->where($dbo->qn('forcesel') . ' = 0')
1894 ->where($dbo->qn('ifchildren') . ' = 0')
1895 ->where($dbo->qn('is_citytax') . ' = 0')
1896 ->where($dbo->qn('is_fee') . ' = 0')
1897 );
1898 $records = $dbo->loadAssocList();
1899
1900 if (!strcasecmp(($info['status'] ?? ''), 'confirmed')) {
1901 // merge mandatory damage deposit
1902 $dbo->setQuery(
1903 $dbo->getQuery(true)
1904 ->select('*')
1905 ->from($dbo->qn('#__vikbooking_optionals'))
1906 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1907 ->where($dbo->qn('forcesel') . ' = 1')
1908 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
1909 );
1910 $dd_records = $dbo->loadAssocList();
1911
1912 // merge with regular options
1913 $records = array_merge($records, $dd_records);
1914 }
1915
1916 if (!$records) {
1917 // no upsell-able options found
1918 return [];
1919 }
1920
1921 // filter options by available date and translate records
1922 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1923 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1924 $records = !is_array($records) ? [] : $records;
1925
1926 $tot_upsellable = 0;
1927 foreach ($upsell_data as $k => $rdata) {
1928 if (!isset($upsell_data[$k]->upsellable)) {
1929 $upsell_data[$k]->upsellable = [];
1930 }
1931 foreach ($records as $opt) {
1932 if (!empty($opt['ageintervals'])) {
1933 // upsellable options should not contain age intervals for children
1934 continue;
1935 }
1936 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
1937 // this option is not assigned to this room
1938 continue;
1939 }
1940 // check if the option is suited for this room party
1941 $clone_opt = [$opt];
1942 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
1943 if (!is_array($clone_opt) || !$clone_opt) {
1944 // this option is not suited for this room party
1945 continue;
1946 }
1947
1948 if (in_array($opt['id'], $rdata->options)) {
1949 // this option has already been booked
1950 continue;
1951 }
1952
1953 // push this option and increase counter
1954 array_push($upsell_data[$k]->upsellable, $opt);
1955 $tot_upsellable++;
1956 }
1957 }
1958
1959 // if no upsellable options were found, we return an empty array
1960 return $tot_upsellable > 0 ? $upsell_data : [];
1961 }
1962
1963 /**
1964 * Returns the minimum days in advance for booking, by considering by default
1965 * also the property closing dates. If the property is currently closed, then
1966 * the minimum number of days in advance will be increased.
1967 *
1968 * @param bool $no_closing_dates whether to skip checking closing dates.
1969 *
1970 * @return int the number of days in advance for booking.
1971 *
1972 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
1973 * getMinDaysAdvance($skipsession = false) to:
1974 * getMinDaysAdvance($no_closing_dates = false)
1975 * This was made to return the proper number of days in advance
1976 * in case the property is currently closed.
1977 */
1978 public static function getMinDaysAdvance($no_closing_dates = false) {
1979 // cache value in static var
1980 static $getMinDaysAdvance = null;
1981
1982 if ($getMinDaysAdvance) {
1983 return $getMinDaysAdvance;
1984 }
1985 //
1986
1987 $dbo = JFactory::getDbo();
1988
1989 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='mindaysadvance';";
1990 $dbo->setQuery($q);
1991 $dbo->execute();
1992 $mind = $dbo->getNumRows() ? (int)$dbo->loadResult() : 0;
1993
1994 // update cached var
1995 $getMinDaysAdvance = $mind;
1996
1997 if ($no_closing_dates) {
1998 // do not check the closing dates
1999 return $mind;
2000 }
2001
2002 // check if the property is currently closed
2003 $cur_closed_dates = self::getClosingDates();
2004 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
2005 $today_midnight = mktime(0, 0, 0);
2006 $closed_until = null;
2007 foreach ($cur_closed_dates as $kcd => $vcd) {
2008 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
2009 // closing period found
2010 $closed_until = $vcd['to'];
2011 break;
2012 }
2013 }
2014 if ($closed_until !== null) {
2015 // count the number of days until property is closed
2016 $mind = 0;
2017 $today_info = getdate($today_midnight);
2018 while ($today_info[0] <= $closed_until) {
2019 $mind++;
2020 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
2021 }
2022 }
2023 }
2024
2025 // update cached var
2026 $getMinDaysAdvance = $mind;
2027
2028 return $mind;
2029 }
2030
2031 public static function getDefaultNightsCalendar()
2032 {
2033 return VBOFactory::getConfig()->getInt('autodefcalnights', 1);
2034 }
2035
2036 public static function getSearchNumRooms($skipsession = true)
2037 {
2038 $dbo = JFactory::getDbo();
2039 if ($skipsession) {
2040 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2041 $dbo->setQuery($q);
2042 $dbo->execute();
2043 $s = $dbo->loadAssocList();
2044 return (int)$s[0]['setting'];
2045 }
2046 $session = JFactory::getSession();
2047 $sval = $session->get('vbsearchNumRooms', '');
2048 if (!empty($sval)) {
2049 return (int)$sval;
2050 }
2051 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2052 $dbo->setQuery($q);
2053 $dbo->execute();
2054 $s = $dbo->loadAssocList();
2055 $session->set('vbsearchNumRooms', $s[0]['setting']);
2056 return (int)$s[0]['setting'];
2057 }
2058
2059 public static function getSearchNumAdults($skipsession = true)
2060 {
2061 $dbo = JFactory::getDbo();
2062 if ($skipsession) {
2063 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2064 $dbo->setQuery($q);
2065 $dbo->execute();
2066 $s = $dbo->loadAssocList();
2067 return $s[0]['setting'];
2068 }
2069 $session = JFactory::getSession();
2070 $sval = $session->get('vbsearchNumAdults', '');
2071 if (!empty($sval)) {
2072 return $sval;
2073 }
2074 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2075 $dbo->setQuery($q);
2076 $dbo->execute();
2077 $s = $dbo->loadAssocList();
2078 $session->set('vbsearchNumAdults', $s[0]['setting']);
2079 return $s[0]['setting'];
2080 }
2081
2082 public static function getSearchNumChildren($skipsession = true)
2083 {
2084 $dbo = JFactory::getDbo();
2085 if ($skipsession) {
2086 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2087 $dbo->setQuery($q);
2088 $dbo->execute();
2089 $s = $dbo->loadAssocList();
2090 return $s[0]['setting'];
2091 }
2092 $session = JFactory::getSession();
2093 $sval = $session->get('vbsearchNumChildren', '');
2094 if (!empty($sval)) {
2095 return $sval;
2096 }
2097 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2098 $dbo->setQuery($q);
2099 $dbo->execute();
2100 $s = $dbo->loadAssocList();
2101 $session->set('vbsearchNumChildren', $s[0]['setting']);
2102 return $s[0]['setting'];
2103 }
2104
2105 public static function getSmartSearchType($skipsession = true)
2106 {
2107 $dbo = JFactory::getDbo();
2108 if ($skipsession) {
2109 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2110 $dbo->setQuery($q);
2111 $dbo->execute();
2112 $s = $dbo->loadAssocList();
2113 return $s[0]['setting'];
2114 }
2115 $session = JFactory::getSession();
2116 $sval = $session->get('vbsmartSearchType', '');
2117 if (!empty($sval)) {
2118 return $sval;
2119 }
2120 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2121 $dbo->setQuery($q);
2122 $dbo->execute();
2123 $s = $dbo->loadAssocList();
2124 $session->set('vbsmartSearchType', $s[0]['setting']);
2125 return $s[0]['setting'];
2126 }
2127
2128 /**
2129 * Retrieves the listing-level minimum advance booking offset.
2130 *
2131 * @param int $idroom The listing ID to evaluate.
2132 * @param bool $period Whether to format the value as a period string.
2133 *
2134 * @return int|string|null Number of hours or period string ("2H"), if set.
2135 *
2136 * @since 1.18.3 (J) - 1.8.3 (WP)
2137 */
2138 public static function getMinDateFutureListing($idroom, $period = false)
2139 {
2140 $config = VBOFactory::getConfig();
2141
2142 $room_level_min_notice = $config->get("room_{$idroom}_min_adv_notice", null);
2143
2144 if (is_null($room_level_min_notice) || $room_level_min_notice === '') {
2145 return null;
2146 }
2147
2148 if ($period) {
2149 return sprintf('%dH', (int) $room_level_min_notice);
2150 }
2151
2152 return (int) $room_level_min_notice;
2153 }
2154
2155 /**
2156 * Returns the maximum advance booking notice (offset) as a period.
2157 * It either returns the global configuration setting, or the value
2158 * defined at room-level, if any and if requested.
2159 *
2160 * @param int $idroom optional room ID for room-level setting.
2161 *
2162 * @return string the maximum booking notice period (i.e. +1Y).
2163 *
2164 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
2165 * $idroom to support room-level settings.
2166 */
2167 public static function getMaxDateFuture($idroom = 0)
2168 {
2169 static $max_adv_book_pool = [];
2170
2171 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
2172
2173 if (isset($max_adv_book_pool[$pool_id])) {
2174 return $max_adv_book_pool[$pool_id];
2175 }
2176
2177 $config = VBOFactory::getConfig();
2178
2179 if ($pool_id === 'global') {
2180 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
2181 } else {
2182 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
2183 if (!empty($room_level_max_notice)) {
2184 // setting is defined at room-level
2185 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2186 } else {
2187 // recurse for global setting
2188 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2189 }
2190 }
2191
2192 return $max_adv_book_pool[$pool_id];
2193 }
2194
2195 /**
2196 * Validates the maximum advance booking notice against the requested check-in
2197 * timestamp. The method supports the validation at both room and property levels.
2198 *
2199 * @param int $checkints the check-in date timestamp.
2200 * @param int $idroom the optional room ID for room-level validation.
2201 *
2202 * @return string error date string in case of validation failure.
2203 *
2204 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2205 */
2206 public static function validateMaxDateBookings($checkints, $idroom = 0)
2207 {
2208 $datelim = self::getMaxDateFuture($idroom);
2209 $datelim = empty($datelim) ? '+2y' : $datelim;
2210 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2211 $quantlim = substr($datelim, -1, 1);
2212
2213 $now = getdate();
2214 if ($quantlim == 'w') {
2215 $until_ts = strtotime("+$numlim weeks") + 86399;
2216 } else {
2217 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2218 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2219 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2220 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2221 }
2222
2223 if ($until_ts > $now[0] && $checkints > $until_ts) {
2224 $vbo_df = self::getDateFormat();
2225 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2226
2227 // error, return the maximum date in the future allowed
2228 return date($df, $until_ts);
2229 }
2230
2231 // validation passed
2232 return '';
2233 }
2234
2235 /**
2236 * Validates the minimum advance booking notice against the requested check-in
2237 * timestamp. The method supports the validation at both room and property levels.
2238 *
2239 * @param int $checkints The check-in date timestamp.
2240 * @param int $idroom Optional room ID for room-level validation.
2241 *
2242 * @return int|float Minimum days (int) or hours (float) in advance required.
2243 *
2244 * @since 1.18.3 (J) - 1.8.3 (WP) introduced $idroom argument for room-level.
2245 */
2246 public static function validateMinDaysAdvance($checkints, $idroom = 0)
2247 {
2248 if ($idroom) {
2249 // listing-level minimum advance booking notice (hours, could be null or 0)
2250 $minhadv = self::getMinDateFutureListing($idroom);
2251
2252 // perform listing-level validation only when value is set
2253 if (!$minhadv) {
2254 // validation passed
2255 return '';
2256 }
2257
2258 // access listing params
2259 $listing_data = self::getRoomInfo($idroom, ['params'], $no_cache = true);
2260 if ($listing_data['params'] ?? null) {
2261 // access listing-level check-in time
2262 $listing_checkin = self::getRoomParam('checkin', $listing_data['params']);
2263 if ($listing_checkin) {
2264 // overwrite check-in time
2265 $listing_checkin_parts = explode(':', $listing_checkin);
2266 $checkin_hours = (int) $listing_checkin_parts[0];
2267 $checkin_minutes = (int) $listing_checkin_parts[1];
2268 // read current check-in date-time
2269 $tsinfo = getdate($checkints);
2270 // update check-in time with proper hours, minutes and seconds
2271 $checkints = mktime($checkin_hours, $checkin_minutes, 0, $tsinfo['mon'], $tsinfo['mday'], $tsinfo['year']);
2272 }
2273 }
2274
2275 if ($checkints < strtotime('+' . $minhadv . ($minhadv > 1 ? 'hours' : 'hour'))) {
2276 return $minhadv >= 24 ? floor($minhadv / 24) : ($minhadv / 24);
2277 }
2278
2279 // validation passed without checking the global settings when listing ID given
2280 return '';
2281 }
2282
2283 // global validation
2284 $mindadv = self::getMinDaysAdvance(true);
2285 if ($mindadv > 0) {
2286 $tsinfo = getdate($checkints);
2287 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2288 if ($checkints < $limit_ts) {
2289 return $mindadv;
2290 }
2291 }
2292
2293 // validation passed
2294 return '';
2295 }
2296
2297 /**
2298 * The only supported calendar type has been changed to jQuery UI.
2299 *
2300 * @since 1.13
2301 */
2302 public static function calendarType($skipsession = false)
2303 {
2304 return 'jqueryui';
2305 }
2306
2307 public static function getSiteLogo()
2308 {
2309 static $siteLogo = null;
2310
2311 if ($siteLogo !== null) {
2312 return $siteLogo;
2313 }
2314
2315 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2316
2317 return $siteLogo;
2318 }
2319
2320 public static function getBackendLogo()
2321 {
2322 static $backLogo = null;
2323
2324 if ($backLogo !== null) {
2325 return $backLogo;
2326 }
2327
2328 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2329
2330 return $backLogo;
2331 }
2332
2333 public static function numCalendars()
2334 {
2335 static $numCalendars = null;
2336
2337 if ($numCalendars !== null) {
2338 return $numCalendars;
2339 }
2340
2341 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2342
2343 return $numCalendars;
2344 }
2345
2346 public static function getFirstWeekDay()
2347 {
2348 static $firstWeekDay = null;
2349
2350 if ($firstWeekDay !== null) {
2351 return $firstWeekDay;
2352 }
2353
2354 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2355
2356 return $firstWeekDay;
2357 }
2358
2359 public static function showPartlyReserved()
2360 {
2361 static $partlyReserved = null;
2362
2363 if ($partlyReserved !== null) {
2364 return $partlyReserved;
2365 }
2366
2367 $partlyReserved = VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2368
2369 return $partlyReserved;
2370 }
2371
2372 public static function showStatusCheckinoutOnly()
2373 {
2374 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2375 }
2376
2377 public static function getDisclaimer($vbo_tn = null)
2378 {
2379 $dbo = JFactory::getDbo();
2380
2381 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2382 $dbo->setQuery($q);
2383 $ft = $dbo->loadAssocList();
2384 if (!$ft) {
2385 return '';
2386 }
2387
2388 if (!is_object($vbo_tn)) {
2389 $vbo_tn = self::getTranslator();
2390 }
2391 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2392
2393 return $ft[0]['setting'];
2394 }
2395
2396 public static function showFooter()
2397 {
2398 return VBOFactory::getConfig()->getBool('showfooter', false);
2399 }
2400
2401 public static function getPriceName($idp, $vbo_tn = null)
2402 {
2403 $dbo = JFactory::getDbo();
2404
2405 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2406 $dbo->setQuery($q, 0, 1);
2407 $n = $dbo->loadAssocList();
2408 if ($n) {
2409 if (is_object($vbo_tn)) {
2410 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2411 }
2412 return $n[0]['name'];
2413 }
2414
2415 return "";
2416 }
2417
2418 public static function getPriceAttr($idp, $vbo_tn = null)
2419 {
2420 $dbo = JFactory::getDbo();
2421
2422 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2423 $dbo->setQuery($q, 0, 1);
2424 $n = $dbo->loadAssocList();
2425 if ($n) {
2426 if (is_object($vbo_tn)) {
2427 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2428 }
2429 return $n[0]['attr'];
2430 }
2431
2432 return "";
2433 }
2434
2435 /**
2436 * Fetches the requested rate plan information.
2437 *
2438 * @param int $idp the rate plan ID.
2439 * @param object $vbo_tn the Vik Booking translator object.
2440 *
2441 * @return array empty array or associative record.
2442 */
2443 public static function getPriceInfo($idp, $vbo_tn = null)
2444 {
2445 // cache values
2446 static $priceInfos = [];
2447
2448 $idp = (int)$idp;
2449
2450 if ($priceInfos && isset($priceInfos[$idp])) {
2451 return $priceInfos[$idp];
2452 }
2453
2454 $dbo = JFactory::getDbo();
2455
2456 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2457 $dbo->setQuery($q, 0, 1);
2458 $rplan = $dbo->loadAssocList();
2459 if (!$rplan) {
2460 return [];
2461 }
2462
2463 if (is_object($vbo_tn)) {
2464 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2465 }
2466
2467 // set cached value
2468 $priceInfos[$idp] = $rplan[0];
2469
2470 return $rplan[0];
2471 }
2472
2473 public static function getAliq($idal)
2474 {
2475 $dbo = JFactory::getDbo();
2476
2477 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2478 $dbo->setQuery($q);
2479 $aliq = $dbo->loadResult();
2480 if ($aliq) {
2481 return $aliq;
2482 }
2483
2484 return 0;
2485 }
2486
2487 public static function getTimeOpenStore($skipsession = false)
2488 {
2489 // cache value in static var
2490 static $getTimeOpenStore = null;
2491
2492 if ($getTimeOpenStore) {
2493 return $getTimeOpenStore;
2494 }
2495 //
2496
2497 $dbo = JFactory::getDbo();
2498 $session = JFactory::getSession();
2499
2500 if ($skipsession) {
2501 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2502 if (empty($setting) && $setting != "0") {
2503 return false;
2504 } else {
2505 $x = explode("-", $setting);
2506 if (!empty($x[1]) && $x[1] != "0") {
2507 $getTimeOpenStore = $x;
2508 return $x;
2509 }
2510 }
2511 } else {
2512 $sval = $session->get('vbgetTimeOpenStore', '');
2513 if (!empty($sval)) {
2514 return $sval;
2515 } else {
2516 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2517 if (empty($setting) && $setting != "0") {
2518 return false;
2519 } else {
2520 $x = explode("-", $setting);
2521 if (!empty($x[1]) && $x[1] != "0") {
2522 $session->set('vbgetTimeOpenStore', $x);
2523 $getTimeOpenStore = $x;
2524 return $x;
2525 }
2526 }
2527 }
2528 }
2529 return false;
2530 }
2531
2532 public static function getHoursMinutes($secs)
2533 {
2534 if ($secs >= 3600) {
2535 $op = $secs / 3600;
2536 $hours = floor($op);
2537 $less = $hours * 3600;
2538 $newsec = $secs - $less;
2539 $optwo = $newsec / 60;
2540 $minutes = floor($optwo);
2541 } else {
2542 $hours = "0";
2543 $optwo = $secs / 60;
2544 $minutes = floor($optwo);
2545 }
2546
2547 return [$hours, $minutes];
2548 }
2549
2550 public static function getClosingDates()
2551 {
2552 // cache value in static var
2553 static $getClosingDates = null;
2554
2555 if (is_array($getClosingDates)) {
2556 return $getClosingDates;
2557 }
2558
2559 $dbo = JFactory::getDbo();
2560
2561 $allcd = [];
2562
2563 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2564 $dbo->setQuery($q);
2565 $s = $dbo->loadAssocList();
2566 if ($s && !empty($s[0]['setting'])) {
2567 $allcd = json_decode($s[0]['setting'], true);
2568 $allcd = is_array($allcd) ? $allcd : array();
2569 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2570 foreach ($allcd as $k => $v) {
2571 if ($v['to'] < $base_ts) {
2572 unset($allcd[$k]);
2573 }
2574 }
2575 $allcd = array_values($allcd);
2576 }
2577
2578 $getClosingDates = $allcd;
2579
2580 return $allcd;
2581 }
2582
2583 public static function parseJsClosingDates()
2584 {
2585 $cd = self::getClosingDates();
2586 if (!$cd) {
2587 return [];
2588 }
2589
2590 $cdjs = [];
2591 foreach ($cd as $k => $v) {
2592 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2593 }
2594
2595 return $cdjs;
2596 }
2597
2598 /**
2599 * Given stay date timestamps, tells if the booking is disallowed due to global closing dates.
2600 *
2601 * @param int $checkints The check-in date timestamp.
2602 * @param int $checkoutts The check-in date timestamp.
2603 * @param ?string $df Optional date format to return the closed stay dates.
2604 * @param bool $closingcmp True to obtain the stay components affected by a global closure.
2605 *
2606 * @return string|array Empty string if no closures, closure dates string or affected components list.
2607 *
2608 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $closingcmp to improve the search suggestion details.
2609 */
2610 public static function validateClosingDates($checkints, $checkoutts, $df = null, $closingcmp = false)
2611 {
2612 $cd = self::getClosingDates();
2613
2614 if (!$cd) {
2615 return '';
2616 }
2617
2618 $df = empty($df) ? 'Y-m-d' : $df;
2619 $margin_seconds = 22 * 60 * 60;
2620
2621 foreach ($cd as $k => $v) {
2622 // determine if the stay dates are closed
2623 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2624 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2625 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2626
2627 if ($inner_closed || $outer_closed || $middle_closed) {
2628 // stay dates are closed
2629 if ($closingcmp) {
2630 // return the components affected by the globally closed dates to allow analysis
2631 return [
2632 'checkin' => $inner_closed,
2633 'checkout' => $outer_closed,
2634 'stay' => $middle_closed,
2635 ];
2636 }
2637
2638 // return the string with the range of dates closed globally
2639 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2640 }
2641 }
2642
2643 return '';
2644 }
2645
2646 /**
2647 * Whether the categories dropdown filter menu should be displayed.
2648 *
2649 * @param bool $skipsession [optional] re-read the configuration setting.
2650 *
2651 * @return bool
2652 *
2653 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2654 */
2655 public static function showCategoriesFront($skipsession = true)
2656 {
2657 return VBOFactory::getConfig()->getBool('showcategories', false);
2658 }
2659
2660 /**
2661 * Whether the number of children dropdown menu should be displayed.
2662 * Defaults to skip the session values and to re-read the configuration setting.
2663 *
2664 * @param bool $skipsession [optional] re-read the configuration setting.
2665 *
2666 * @return bool
2667 *
2668 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2669 */
2670 public static function showChildrenFront($skipsession = true)
2671 {
2672 return VBOFactory::getConfig()->getBool('showchildren', true);
2673 }
2674
2675 public static function allowBooking()
2676 {
2677 // cache value in static var
2678 static $allowBooking = null;
2679
2680 if (is_bool($allowBooking)) {
2681 return $allowBooking;
2682 }
2683
2684 $allowBooking = (bool) VBOFactory::getConfig()->get('allowbooking');
2685
2686 return $allowBooking;
2687 }
2688
2689 public static function getDisabledBookingMsg($vbo_tn = null) {
2690 $dbo = JFactory::getDbo();
2691 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2692 $dbo->setQuery($q);
2693 $dbo->execute();
2694 $s = $dbo->loadAssocList();
2695 if (!is_object($vbo_tn)) {
2696 $vbo_tn = self::getTranslator();
2697 }
2698 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2699 return $s[0]['setting'];
2700 }
2701
2702 /**
2703 * Returns the text for the guests allowed policy.
2704 *
2705 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2706 *
2707 * @return string either the raw or the translated policy text.
2708 *
2709 * @since 1.16.5 (WP) - 1.6.5 (WP)
2710 */
2711 public static function getGuestsAllowedPolicy($vbo_tn = null)
2712 {
2713 $dbo = JFactory::getDbo();
2714
2715 $q = $dbo->getQuery(true)
2716 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2717 ->from($dbo->qn('#__vikbooking_texts'))
2718 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2719
2720 $dbo->setQuery($q);
2721 $text_record = $dbo->loadAssocList();
2722
2723 if (!$text_record) {
2724 // missing record, insert it with empty values
2725 if (self::isAdmin()) {
2726 $record = new stdClass;
2727 $record->param = 'guests_allowed_policy';
2728 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2729 $record->setting = '';
2730 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2731 }
2732
2733 return '';
2734 }
2735
2736 if (is_object($vbo_tn)) {
2737 // translate the record only if requested
2738 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2739 }
2740
2741 return $text_record[0]['setting'];
2742 }
2743
2744 public static function getDateFormat($skipsession = true)
2745 {
2746 // cache value in static var
2747 static $getDateFormat = null;
2748
2749 if ($getDateFormat) {
2750 return $getDateFormat;
2751 }
2752
2753 $dbo = JFactory::getDbo();
2754
2755 if ($skipsession) {
2756 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2757 $dbo->setQuery($q, 0, 1);
2758 $getDateFormat = $dbo->loadResult();
2759
2760 return $getDateFormat;
2761 }
2762
2763 $session = JFactory::getSession();
2764 $sval = $session->get('vbgetDateFormat', '');
2765 if (!empty($sval)) {
2766 return $sval;
2767 }
2768
2769 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2770 $dbo->setQuery($q, 0, 1);
2771 $getDateFormat = $dbo->loadResult();
2772
2773 $session->set('vbgetDateFormat', $getDateFormat);
2774
2775 return $getDateFormat;
2776 }
2777
2778 public static function getDateSeparator($skipsession = true)
2779 {
2780 // cache value in static var
2781 static $getDateSeparator = null;
2782
2783 if ($getDateSeparator) {
2784 return $getDateSeparator;
2785 }
2786
2787 $dbo = JFactory::getDbo();
2788
2789 if ($skipsession) {
2790 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2791 $dbo->setQuery($q, 0, 1);
2792 $val = $dbo->loadResult();
2793
2794 $getDateSeparator = empty($val) ? "/" : $val;
2795
2796 return $getDateSeparator;
2797 }
2798
2799 $session = JFactory::getSession();
2800 $sval = $session->get('vbgetDateSep', '');
2801 if (!empty($sval)) {
2802 return $sval;
2803 }
2804
2805 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2806 $dbo->setQuery($q, 0, 1);
2807 $val = $dbo->loadResult();
2808
2809 $getDateSeparator = empty($val) ? "/" : $val;
2810
2811 $session->set('vbgetDateSep', $getDateSeparator);
2812
2813 return $getDateSeparator;
2814 }
2815
2816 public static function getHoursMoreRb($skipsession = false)
2817 {
2818 // cache value in static var
2819 static $getHoursMoreRb = null;
2820
2821 if ($getHoursMoreRb !== null) {
2822 return $getHoursMoreRb;
2823 }
2824
2825 $dbo = JFactory::getDbo();
2826
2827 if ($skipsession) {
2828 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2829 $dbo->setQuery($q, 0, 1);
2830 $getHoursMoreRb = (int)$dbo->loadResult();
2831
2832 return $getHoursMoreRb;
2833 }
2834
2835 $session = JFactory::getSession();
2836 $sval = $session->get('getHoursMoreRb', '');
2837 if (strlen($sval)) {
2838 return $sval;
2839 }
2840
2841 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2842 $dbo->setQuery($q, 0, 1);
2843 $getHoursMoreRb = (int)$dbo->loadResult();
2844
2845 $session->set('getHoursMoreRb', $getHoursMoreRb);
2846
2847 return $getHoursMoreRb;
2848 }
2849
2850 public static function getHoursRoomAvail()
2851 {
2852 $dbo = JFactory::getDbo();
2853
2854 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2855 $dbo->setQuery($q, 0, 1);
2856
2857 return (int)$dbo->loadResult();
2858 }
2859
2860 public static function getFrontTitle($vbo_tn = null) {
2861 // cache value in static var
2862 static $getFrontTitle = null;
2863
2864 if ($getFrontTitle) {
2865 return $getFrontTitle;
2866 }
2867 //
2868
2869 $dbo = JFactory::getDbo();
2870 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2871 $dbo->setQuery($q);
2872 $dbo->execute();
2873 $ft = $dbo->loadAssocList();
2874 if (!is_object($vbo_tn)) {
2875 $vbo_tn = self::getTranslator();
2876 }
2877 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2878
2879 $getFrontTitle = $ft[0]['setting'];
2880
2881 return $ft[0]['setting'];
2882 }
2883
2884 public static function getFrontTitleTag()
2885 {
2886 return VBOFactory::getConfig()->get('fronttitletag', '');
2887 }
2888
2889 public static function getFrontTitleTagClass()
2890 {
2891 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2892 }
2893
2894 /**
2895 * Returns the configured currency name. Often used to apply currency
2896 * conversions, and so it should match the currency ISO 4217 (Alpha-3) code.
2897 *
2898 * @param bool $iso True to ensure we fetch a 3-char currency code.
2899 *
2900 * @return string
2901 *
2902 * @since 1.18.0 (J) - 1.8.0 (WP) added argument $iso.
2903 */
2904 public static function getCurrencyName($iso = false)
2905 {
2906 static $currencyName = null;
2907
2908 if ($currencyName !== null) {
2909 return $currencyName;
2910 }
2911
2912 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2913
2914 if ($iso && strlen((string) $currencyName) != 3) {
2915 // attempt to fetch the currency code configuration setting
2916 $currencyName = self::getCurrencyCodePp();
2917 }
2918
2919 return $currencyName;
2920 }
2921
2922 public static function getCurrencySymb()
2923 {
2924 // cache value in static var
2925 static $getCurrencySymb = null;
2926
2927 if ($getCurrencySymb) {
2928 return $getCurrencySymb;
2929 }
2930
2931 $getCurrencySymb = VBOFactory::getConfig()->get('currencysymb', '');
2932
2933 return $getCurrencySymb;
2934 }
2935
2936 /**
2937 * Returns the configured currency position: either "before" or "after".
2938 *
2939 * @return string
2940 *
2941 * @since 1.18.3 (J) - 1.8.3 (WP)
2942 */
2943 public static function getCurrencyPosition()
2944 {
2945 // cache value in static var
2946 static $getCurrencyPos = null;
2947
2948 if ($getCurrencyPos) {
2949 return $getCurrencyPos;
2950 }
2951
2952 $getCurrencyPos = VBOFactory::getConfig()->get('currencypos', 'before');
2953
2954 return $getCurrencyPos;
2955 }
2956
2957 /**
2958 * Given a number, formats it according to currency preferences.
2959 *
2960 * @param string $number The number to format.
2961 * @param ?string $currency Optional currency to use.
2962 * @param ?array $config Optional format configuration (i.e. currency/number "HTML template").
2963 *
2964 * @return string
2965 *
2966 * @since 1.18.3 (J) - 1.8.3 (WP)
2967 */
2968 public static function formatCurrencyNumber(string $number, ?string $currency = null, ?array $config = null)
2969 {
2970 // get the currency symbol/value to use
2971 $currency = $currency ?: self::getCurrencySymb();
2972
2973 // obtain currency template (wildcard expected)
2974 if (is_string($config[0] ?? null) || is_string($config['currency'] ?? null)) {
2975 // apply templating to string
2976 $currency = sprintf($config[0] ?? $config['currency'], $currency);
2977 }
2978
2979 // obtain number template
2980 if (is_string($config[1] ?? null) || is_string($config['number'] ?? null)) {
2981 // apply templating to string
2982 $number = sprintf($config[1] ?? $config['number'], $number);
2983 }
2984
2985 // default value spacing (empty white space)
2986 $spacing = (string) ($config['spacing'] ?? ' ');
2987
2988 if (self::getCurrencyPosition() === 'after') {
2989 return sprintf('%s' . $spacing . '%s', $number, $currency);
2990 }
2991
2992 return sprintf('%s' . $spacing . '%s', $currency, $number);
2993 }
2994
2995 public static function getCurrencyCodePp()
2996 {
2997 static $currencyCode = null;
2998
2999 if ($currencyCode !== null) {
3000 return $currencyCode;
3001 }
3002
3003 $currencyCode = VBOFactory::getConfig()->get('currencycodepp', '');
3004
3005 return $currencyCode;
3006 }
3007
3008 public static function getNumberFormatData()
3009 {
3010 // cache value in static var
3011 static $getNumberFormatData = null;
3012
3013 if ($getNumberFormatData) {
3014 return $getNumberFormatData;
3015 }
3016
3017 $getNumberFormatData = VBOFactory::getConfig()->get('numberformat', '');
3018
3019 return $getNumberFormatData;
3020 }
3021
3022 /**
3023 * It is possible to hide the decimals if they are like "N.00".
3024 *
3025 * @return int 0 if disabled, 1 if enabled (default).
3026 *
3027 * @since 1.14 (J) - 1.4.0 (WP)
3028 */
3029 public static function hideEmptyDecimals()
3030 {
3031 // cache value in static var
3032 static $hideEmptyDecimals = null;
3033
3034 if ($hideEmptyDecimals !== null) {
3035 return $hideEmptyDecimals;
3036 }
3037
3038 $hideEmptyDecimals = (int) VBOFactory::getConfig()->get('noemptydecimals', '1');
3039
3040 return $hideEmptyDecimals;
3041 }
3042
3043 public static function numberFormat($num)
3044 {
3045 $formatvals = self::getNumberFormatData();
3046 $formatparts = explode(':', $formatvals);
3047
3048 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
3049 // number has got no decimals
3050 $formatparts[0] = 0;
3051 }
3052
3053 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
3054 }
3055
3056 /**
3057 * Given a date timestamp, returns the formatted date.
3058 *
3059 * @param int|string $ts the date representation in Unix timestamp.
3060 * @param bool $wtime if true, a formatted date-time will be returned.
3061 *
3062 * @return string the formatted date according to settings.
3063 *
3064 * @since 1.16.0 (J) - 1.6.0 (WP)
3065 */
3066 public static function formatDateTs($ts, $wtime = false)
3067 {
3068 $nowdf = self::getDateFormat();
3069 if ($nowdf == "%d/%m/%Y") {
3070 $df = 'd/m/Y';
3071 } elseif ($nowdf == "%m/%d/%Y") {
3072 $df = 'm/d/Y';
3073 } else {
3074 $df = 'Y/m/d';
3075 }
3076 $datesep = self::getDateSeparator();
3077
3078 $time_f = $wtime ? ' H:i' : '';
3079
3080 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
3081 }
3082
3083 /**
3084 * Given a country name, 3-char or 2-char code, this method attempts
3085 * to guess the best language to assign to the booking according to
3086 * what languages are installed on the website. This way, cron jobs
3087 * and any other email notification will be correctly and automatically
3088 * sent to the guest in the proper language without any manual action.
3089 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
3090 *
3091 * @param string $country the country name, 3-char or 2-char code.
3092 * @param string $locale optional locale lang tag of the guest.
3093 *
3094 * @return mixed the best lang-tag string to use or null.
3095 *
3096 * @since 1.16.0 (J) - 1.6.0 (WP)
3097 */
3098 public static function guessBookingLangFromCountry($country, $locale = null)
3099 {
3100 if (empty($country)) {
3101 return null;
3102 }
3103
3104 // get all the available languages
3105 $known_langs = self::getVboApplication()->getKnownLanguages();
3106 if (!is_array($known_langs) || !count($known_langs)) {
3107 return null;
3108 }
3109
3110 // check if the booking included a supported "locale" for the guest
3111 if (!empty($locale)) {
3112 // make the locale compatible with the language tags format
3113 $locale = str_replace('_', '-', $locale);
3114 foreach ($known_langs as $ltag => $ldet) {
3115 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
3116 // we support this language tag, so we just use it
3117 return $ltag;
3118 }
3119 }
3120 }
3121
3122 // build similarities with country-languages
3123 $similarities = array(
3124 'AU' => 'en',
3125 'GB' => 'en',
3126 'IE' => 'en',
3127 'NZ' => 'en',
3128 'US' => 'en',
3129 'CA' => array(
3130 'en',
3131 'fr',
3132 ),
3133 'CL' => 'es',
3134 'AR' => 'es',
3135 'PE' => 'es',
3136 'MX' => 'es',
3137 'CR' => 'es',
3138 'CO' => 'es',
3139 'EC' => 'es',
3140 'BO' => 'es',
3141 'CU' => 'es',
3142 'VE' => 'es',
3143 'BE' => 'fr',
3144 'LU' => 'fr',
3145 'CH' => array(
3146 'de',
3147 'it',
3148 'fr',
3149 ),
3150 'AT' => 'de',
3151 'GR' => 'el',
3152 'GL' => 'dk',
3153 );
3154
3155 // fetch values from db
3156 $dbo = JFactory::getDbo();
3157 $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);
3158 $dbo->setQuery($q, 0, 1);
3159 $dbo->execute();
3160 if (!$dbo->getNumRows()) {
3161 return null;
3162 }
3163 $country_record = $dbo->loadAssoc();
3164
3165 // assign country name/code versions
3166 $country_name = $country_record['country_name'];
3167 $country_3char = strtoupper($country_record['country_3_code']);
3168 $country_2char = strtoupper($country_record['country_2_code']);
3169
3170 // build an associative array of language tags and related match-score
3171 $langtags_score = array();
3172 foreach ($known_langs as $ltag => $ldet) {
3173 // default language tag score is 0 for no matches
3174 $langtags_score[$ltag] = 0;
3175 // get language and country codes
3176 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
3177
3178 // check matches with the installed language details
3179 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
3180 // increase language tag score
3181 $langtags_score[$ltag]++;
3182 }
3183 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
3184 // increase language tag score
3185 $langtags_score[$ltag]++;
3186 }
3187 if (!empty($ldet['locale'])) {
3188 // sanitize locale for matching the 2-char code safely
3189 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
3190 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
3191 // increase language tag score
3192 $langtags_score[$ltag]++;
3193 }
3194 }
3195 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
3196 // increase language tag score
3197 $langtags_score[$ltag]++;
3198 }
3199 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
3200 // increase language tag score
3201 $langtags_score[$ltag]++;
3202 }
3203
3204 // check language similarities between countries
3205 if (isset($similarities[$country_2char])) {
3206 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
3207 // check if language tag(s) is available for this spoken language
3208 foreach ($spoken_tags as $spoken_tag) {
3209 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
3210 // increase language tag score
3211 $langtags_score[$ltag]++;
3212 }
3213 }
3214 }
3215 }
3216
3217 // make sure at least one language tag has got some points
3218 if (max($langtags_score) === 0) {
3219 // no languages installed to honor this country
3220 return null;
3221 }
3222
3223 // sort language tag scores
3224 arsort($langtags_score);
3225
3226 // reset array pointer to the first (highest) element
3227 reset($langtags_score);
3228
3229 // return the language tag with the highest score
3230 return key($langtags_score);
3231 }
3232
3233 public static function getIntroMain($vbo_tn = null) {
3234 $dbo = JFactory::getDbo();
3235 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
3236 $dbo->setQuery($q);
3237 $dbo->execute();
3238 $ft = $dbo->loadAssocList();
3239 if (!is_object($vbo_tn)) {
3240 $vbo_tn = self::getTranslator();
3241 }
3242 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3243 return $ft[0]['setting'];
3244 }
3245
3246 public static function getClosingMain($vbo_tn = null) {
3247 $dbo = JFactory::getDbo();
3248 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3249 $dbo->setQuery($q);
3250 $dbo->execute();
3251 $ft = $dbo->loadAssocList();
3252 if (!is_object($vbo_tn)) {
3253 $vbo_tn = self::getTranslator();
3254 }
3255 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3256 return $ft[0]['setting'];
3257 }
3258
3259 public static function getFullFrontTitle($vbo_tn = null) {
3260 $dbo = JFactory::getDbo();
3261 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3262 $dbo->setQuery($q);
3263 $dbo->execute();
3264 $ft = $dbo->loadAssocList();
3265 if (!is_object($vbo_tn)) {
3266 $vbo_tn = self::getTranslator();
3267 }
3268 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3269 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3270 $dbo->setQuery($q);
3271 $dbo->execute();
3272 $fttag = $dbo->loadAssocList();
3273 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3274 $dbo->setQuery($q);
3275 $dbo->execute();
3276 $fttagclass = $dbo->loadAssocList();
3277 if (empty($ft[0]['setting'])) {
3278 return "";
3279 } else {
3280 if (empty($fttag[0]['setting'])) {
3281 return $ft[0]['setting'] . "<br/>\n";
3282 } else {
3283 $tag = str_replace("<", "", $fttag[0]['setting']);
3284 $tag = str_replace(">", "", $tag);
3285 $tag = str_replace("/", "", $tag);
3286 $tag = trim($tag);
3287 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3288 }
3289 }
3290 }
3291
3292 public static function dateIsValid($date) {
3293 $df = self::getDateFormat();
3294 $datesep = self::getDateSeparator();
3295 if (strlen($date) != 10) {
3296 return false;
3297 }
3298 $cur_dsep = "/";
3299 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3300 $cur_dsep = $datesep;
3301 }
3302 $x = explode($cur_dsep, $date);
3303 if ($df == "%d/%m/%Y") {
3304 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3305 return false;
3306 }
3307 } elseif ($df == "%m/%d/%Y") {
3308 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3309 return false;
3310 }
3311 } else {
3312 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3313 return false;
3314 }
3315 }
3316 return true;
3317 }
3318
3319 public static function sayDateFormat() {
3320 $dbo = JFactory::getDbo();
3321 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3322 $dbo->setQuery($q);
3323 $dbo->execute();
3324 $s = $dbo->loadAssocList();
3325 if ($s[0]['setting'] == "%d/%m/%Y") {
3326 return JText::translate('VBCONFIGONETWELVE');
3327 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3328 return JText::translate('VBCONFIGONEMDY');
3329 } else {
3330 return JText::translate('VBCONFIGONETENTHREE');
3331 }
3332 }
3333
3334 /**
3335 * Calculates the Unix timestamp from the given date and
3336 * time. Avoids DST issues thanks to mktime. With prior releases,
3337 * DST issues may occur due to the sum of seconds.
3338 *
3339 * @param string $date the date string formatted with the current settings
3340 * @param int $h hours from 0 to 23 for check-in/check-out
3341 * @param int $m minutes from 0 to 59 for check-in/check-out
3342 * @param int $s seconds from 0 to 59 for check-in/check-out
3343 *
3344 * @return int the Unix timestamp of the date
3345 *
3346 * @since 1.0.14
3347 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3348 * signature modified for $h and $m that now take 0 as default value.
3349 */
3350 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3351 $df = self::getDateFormat();
3352 $datesep = self::getDateSeparator();
3353 $cur_dsep = "/";
3354 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3355 $cur_dsep = $datesep;
3356 }
3357 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3358 // date is in Y-m-d format (with no time)
3359 $cur_dsep = '-';
3360 $df = "%Y/%m/%d";
3361 }
3362 $x = explode($cur_dsep, $date);
3363 if (!(count($x) > 2)) {
3364 return 0;
3365 }
3366 if ($df == "%d/%m/%Y") {
3367 $month = (int)$x[1];
3368 $mday = (int)$x[0];
3369 $year = (int)$x[2];
3370 } elseif ($df == "%m/%d/%Y") {
3371 $month = (int)$x[0];
3372 $mday = (int)$x[1];
3373 $year = (int)$x[2];
3374 } else {
3375 $month = (int)$x[1];
3376 $mday = (int)$x[2];
3377 $year = (int)$x[0];
3378 }
3379 $h = empty($h) ? 0 : (int)$h;
3380 $m = empty($m) ? 0 : (int)$m;
3381 $s = $s > 0 && $s <= 59 ? $s : 0;
3382
3383 return mktime($h, $m, $s, $month, $mday, $year);
3384 }
3385
3386 public static function ivaInclusa($skipsession = false) {
3387 // cache value in static var
3388 static $getTaxIncluded = null;
3389
3390 if ($getTaxIncluded !== null) {
3391 return (bool)$getTaxIncluded;
3392 }
3393
3394 $dbo = JFactory::getDbo();
3395
3396 if ($skipsession) {
3397 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3398 $dbo->setQuery($q, 0, 1);
3399 $dbo->execute();
3400 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3401
3402 // cache value and return it
3403 $getTaxIncluded = $vat_incl_data;
3404
3405 return (bool)$vat_incl_data;
3406 }
3407
3408 $session = JFactory::getSession();
3409 $sval = $session->get('getTaxIncluded', '');
3410 if (strlen($sval)) {
3411 return (bool)$sval;
3412 }
3413
3414 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3415 $dbo->setQuery($q, 0, 1);
3416 $dbo->execute();
3417 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3418
3419 // update session value, cache it and return it
3420 $session->set('getTaxIncluded', $vat_incl_data);
3421 $getTaxIncluded = $vat_incl_data;
3422
3423 return (bool)$vat_incl_data;
3424 }
3425
3426 public static function showTaxOnSummaryOnly($skipsession = false) {
3427 if ($skipsession) {
3428 $dbo = JFactory::getDbo();
3429 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3430 $dbo->setQuery($q);
3431 $dbo->execute();
3432 $s = $dbo->loadAssocList();
3433 return (intval($s[0]['setting']) == 1 ? true : false);
3434 } else {
3435 $session = JFactory::getSession();
3436 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3437 if (strlen($sval) > 0) {
3438 return (intval($sval) == 1 ? true : false);
3439 } else {
3440 $dbo = JFactory::getDbo();
3441 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3442 $dbo->setQuery($q);
3443 $dbo->execute();
3444 $s = $dbo->loadAssocList();
3445 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3446 return (intval($s[0]['setting']) == 1 ? true : false);
3447 }
3448 }
3449 }
3450
3451 public static function tokenForm() {
3452 $dbo = JFactory::getDbo();
3453 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3454 $dbo->setQuery($q);
3455 $dbo->execute();
3456 $s = $dbo->loadAssocList();
3457 return (intval($s[0]['setting']) == 1 ? true : false);
3458 }
3459
3460 public static function getPaypalAcc() {
3461 $dbo = JFactory::getDbo();
3462 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3463 $dbo->setQuery($q);
3464 $dbo->execute();
3465 $s = $dbo->loadAssocList();
3466 return $s[0]['setting'];
3467 }
3468
3469 public static function getAccPerCent() {
3470 $dbo = JFactory::getDbo();
3471 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3472 $dbo->setQuery($q);
3473 $dbo->execute();
3474 $s = $dbo->loadAssocList();
3475 return $s[0]['setting'];
3476 }
3477
3478 public static function getTypeDeposit($skipsession = false) {
3479 if ($skipsession) {
3480 $dbo = JFactory::getDbo();
3481 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3482 $dbo->setQuery($q);
3483 $dbo->execute();
3484 $s = $dbo->loadAssocList();
3485 return $s[0]['setting'];
3486 } else {
3487 $session = JFactory::getSession();
3488 $sval = $session->get('getTypeDeposit', '');
3489 if (strlen($sval) > 0) {
3490 return $sval;
3491 } else {
3492 $dbo = JFactory::getDbo();
3493 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3494 $dbo->setQuery($q);
3495 $dbo->execute();
3496 $s = $dbo->loadAssocList();
3497 $session->set('getTypeDeposit', $s[0]['setting']);
3498 return $s[0]['setting'];
3499 }
3500 }
3501 }
3502
3503 public static function multiplePayments()
3504 {
3505 return VBOFactory::getConfig()->getBool('multipay', false);
3506 }
3507
3508 public static function getAdminMail()
3509 {
3510 return VBOFactory::getConfig()->getString('adminemail', '');
3511 }
3512
3513 public static function getSenderMail()
3514 {
3515 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3516
3517 return empty($sender) ? self::getAdminMail() : $sender;
3518 }
3519
3520 public static function getPaymentName($vbo_tn = null) {
3521 $dbo = JFactory::getDbo();
3522 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3523 $dbo->setQuery($q);
3524 $dbo->execute();
3525 $s = $dbo->loadAssocList();
3526 if (!is_object($vbo_tn)) {
3527 $vbo_tn = self::getTranslator();
3528 }
3529 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3530 return $s[0]['setting'];
3531 }
3532
3533 public static function getTermsConditions($vbo_tn = null) {
3534 //VBO 1.10
3535 $dbo = JFactory::getDbo();
3536 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3537 $dbo->setQuery($q);
3538 $dbo->execute();
3539 if ($dbo->getNumRows() > 0) {
3540 $s = $dbo->loadAssocList();
3541 if (!is_object($vbo_tn)) {
3542 $vbo_tn = self::getTranslator();
3543 }
3544 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3545 } else {
3546 //the record has never been saved. Compose it with the default lang definition
3547 $timeopst = self::getTimeOpenStore(true);
3548 if (is_array($timeopst)) {
3549 $openat = self::getHoursMinutes($timeopst[0]);
3550 $closeat = self::getHoursMinutes($timeopst[1]);
3551 } else {
3552 $openat = array(12, 0);
3553 $closeat = array(10, 0);
3554 }
3555 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3556 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3557 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3558 }
3559
3560 return $s[0]['setting'];
3561 }
3562
3563 public static function getMinutesLock($conv = false)
3564 {
3565 static $minutesLock = null;
3566
3567 if ($minutesLock === null) {
3568 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3569 }
3570
3571 if (!$minutesLock) {
3572 return 0;
3573 }
3574
3575 if ($conv) {
3576 return (time() + ($minutesLock * 60));
3577 }
3578
3579 return $minutesLock;
3580 }
3581
3582 /**
3583 * Checks if the room units are temporarily locked on the given dates.
3584 *
3585 * @param int $idroom the room ID.
3586 * @param int $units the total number of room units.
3587 * @param int $first the check-in timestamp.
3588 * @param int $second the check-out timestamp.
3589 * @param bool $occupied if true, the system will also consider the booked units.
3590 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3591 *
3592 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3593 *
3594 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3595 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3596 */
3597 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3598 {
3599 $dbo = JFactory::getDbo();
3600
3601 if ($units < 1) {
3602 return false;
3603 }
3604
3605 $actnow = time();
3606
3607 // clean up expired records
3608 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3609 $dbo->setQuery($q);
3610 $dbo->execute();
3611
3612 $secdiff = $second - $first;
3613 $daysdiff = $secdiff / 86400;
3614 if (is_int($daysdiff)) {
3615 if ($daysdiff < 1) {
3616 $daysdiff = 1;
3617 }
3618 } else {
3619 if ($daysdiff < 1) {
3620 $daysdiff = 1;
3621 } else {
3622 $sum = floor($daysdiff) * 86400;
3623 $newdiff = $secdiff - $sum;
3624 $maxhmore = self::getHoursMoreRb() * 3600;
3625 if ($maxhmore >= $newdiff) {
3626 $daysdiff = floor($daysdiff);
3627 } else {
3628 $daysdiff = ceil($daysdiff);
3629 }
3630 }
3631 }
3632 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3633 $tmplock_map = [];
3634
3635 // check temporarily locked records
3636 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3637 $dbo->setQuery($check);
3638 $busy = $dbo->loadAssocList();
3639 if ($busy) {
3640 foreach ($groupdays as $kg => $gday) {
3641 $bfound = 0;
3642 foreach ($busy as $bu) {
3643 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3644 $bfound++;
3645 }
3646 }
3647 $tmplock_map[$kg] = $bfound;
3648 if ($bfound >= $units) {
3649 return false;
3650 }
3651 }
3652 }
3653
3654 if ($occupied) {
3655 // check also the busy records and some the occupied units to the temporarily locked ones
3656 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3657 $dbo->setQuery($check);
3658 $busy = $dbo->loadAssocList();
3659 if (!$busy) {
3660 return true;
3661 }
3662
3663 foreach ($groupdays as $kg => $gday) {
3664 $bfound = 0;
3665 foreach ($busy as $bu) {
3666 if (in_array($bu['id'], $skip_busy_ids)) {
3667 // Booking modification
3668 continue;
3669 }
3670 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3671 $bfound++;
3672 }
3673 }
3674 // sum units occupied to the units temporarily locked
3675 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3676 if ($bfound >= $units) {
3677 return false;
3678 }
3679 }
3680 }
3681
3682 return true;
3683 }
3684
3685 public static function getGroupDays($first, $second, $daysdiff) {
3686 $ret = array();
3687 $ret[] = $first;
3688 if ($daysdiff > 1) {
3689 $start = getdate($first);
3690 $end = getdate($second);
3691 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3692 for($i = 1; $i < $daysdiff; $i++) {
3693 $checkday = $start['mday'] + $i;
3694 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3695 if ($dayts != $endcheck) {
3696 $ret[] = $dayts;
3697 }
3698 }
3699 }
3700 $ret[] = $second;
3701 return $ret;
3702 }
3703
3704 /**
3705 * Counts the hours of difference between the current
3706 * server time and the selected check-in date and time.
3707 *
3708 * @param int $checkin_ts
3709 * @param [int] $now_ts
3710 *
3711 * @return int
3712 */
3713 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3714 $hoursdiff = 0;
3715
3716 if (empty($now_ts)) {
3717 $now_ts = time();
3718 }
3719
3720 if ($now_ts >= $checkin_ts) {
3721 return $hoursdiff;
3722 }
3723
3724 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3725
3726 return $hoursdiff;
3727 }
3728
3729 /**
3730 * Loads all the occupied records for the given rooms and offset.
3731 *
3732 * @param array $roomids the optional list of room IDs to filter.
3733 * @param int $from_ts the optional base date to use.
3734 * @param int $max_ts the optional max date to use.
3735 * @param bool $closures whether to ignore, include or exclude closures.
3736 *
3737 * @return array
3738 *
3739 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3740 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3741 */
3742 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3743 {
3744 $base_ts = empty($from_ts) ? time() : $from_ts;
3745 $busy = [];
3746 $clauses = [];
3747
3748 if (is_array($roomids) && count($roomids)) {
3749 // filter busy records only by the requested room ids
3750 $roomids = array_map(function($rid) {
3751 return (int)$rid;
3752 }, $roomids);
3753 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3754 }
3755
3756 // exclude past reservations
3757 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3758
3759 if (!empty($max_ts)) {
3760 // exclude future reservations (recommended for large datasets)
3761 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3762 }
3763
3764 if (is_bool($closures)) {
3765 // include or exclude closures
3766 if ($closures) {
3767 $clauses[] = "`o`.`closure` = 1";
3768 } else {
3769 $clauses[] = "`o`.`closure` != 1";
3770 }
3771 }
3772
3773 $dbo = JFactory::getDbo();
3774
3775 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3776 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3777 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3778 WHERE " . implode(' AND ', $clauses) . ";";
3779 $dbo->setQuery($q);
3780 $allbusy = $dbo->loadAssocList();
3781 if ($allbusy) {
3782 foreach ($allbusy as $br) {
3783 if (!isset($busy[$br['idroom']])) {
3784 $busy[$br['idroom']] = [];
3785 }
3786 $busy[$br['idroom']][] = $br;
3787 }
3788 }
3789
3790 return $busy;
3791 }
3792
3793 /**
3794 * Loads all the busy records by excluding the closures.
3795 * Built for the page dashboard to calculate the actual
3796 * rooms occupancy by excluding the rooms closed.
3797 *
3798 * @param array $roomids the optional list of room IDs to filter.
3799 * @param int $from_ts the optional base date to use.
3800 * @param int $max_ts the optional max date to use.
3801 *
3802 * @return array the list of busy records.
3803 *
3804 * @since 1.12
3805 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3806 */
3807 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3808 {
3809 if (!is_array($roomids) || !count($roomids)) {
3810 return [];
3811 }
3812
3813 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3814 }
3815
3816 /**
3817 * Loads the occupied record IDs for a given booking ID.
3818 *
3819 * @param int $idorder The booking record ID.
3820 *
3821 * @return array List of busy IDs, if any.
3822 */
3823 public static function loadBookingBusyIds($idorder)
3824 {
3825 if (empty($idorder)) {
3826 return [];
3827 }
3828
3829 $dbo = JFactory::getDbo();
3830
3831 $dbo->setQuery(
3832 $dbo->getQuery(true)
3833 ->select($dbo->qn('idbusy'))
3834 ->from($dbo->qn('#__vikbooking_ordersbusy'))
3835 ->where($dbo->qn('idorder') . ' = ' . (int) $idorder)
3836 );
3837
3838 return array_map('intval', array_column($dbo->loadAssocList(), 'idbusy'));
3839 }
3840
3841 public static function loadLockedRecords($roomids, $ts = 0) {
3842 $dbo = JFactory::getDbo();
3843 $actnow = empty($ts) ? time() : $ts;
3844 $locked = array();
3845 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3846 $dbo->setQuery($q);
3847 $dbo->execute();
3848 if (!is_array($roomids) || !(count($roomids) > 0)) {
3849 return $locked;
3850 }
3851 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3852 $dbo->setQuery($check);
3853 $all_locked = $dbo->loadAssocList();
3854 if ($all_locked) {
3855 foreach ($all_locked as $kb => $br) {
3856 $locked[$br['idroom']][$kb] = $br;
3857 }
3858 }
3859 return $locked;
3860 }
3861
3862 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3863 $bookings = [];
3864 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3865 return $bookings;
3866 }
3867 $dbo = JFactory::getDbo();
3868 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3869 $dbo->setQuery($q);
3870 $all_booking_ids = $dbo->loadAssocList();
3871 if ($all_booking_ids) {
3872 $oids = array();
3873 foreach ($all_booking_ids as $bid) {
3874 $oids[] = $bid['idorder'];
3875 }
3876 $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` ".
3877 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3878 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3879 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3880 "ORDER BY `o`.`checkout` ASC;";
3881 $dbo->setQuery($q);
3882 $bookings = $dbo->loadAssocList();
3883 if (!$bookings) {
3884 $bookings = [];
3885 }
3886 }
3887 return $bookings;
3888 }
3889
3890 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3891 {
3892 $dbo = JFactory::getDbo();
3893
3894 if ($units < 1) {
3895 return false;
3896 }
3897
3898 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3899 if (!$room_info) {
3900 return false;
3901 }
3902
3903 $secdiff = $second - $first;
3904 $daysdiff = $secdiff / 86400;
3905 if (is_int($daysdiff)) {
3906 if ($daysdiff < 1) {
3907 $daysdiff = 1;
3908 }
3909 } else {
3910 if ($daysdiff < 1) {
3911 $daysdiff = 1;
3912 } else {
3913 $sum = floor($daysdiff) * 86400;
3914 $newdiff = $secdiff - $sum;
3915 $maxhmore = self::getHoursMoreRb() * 3600;
3916 if ($maxhmore >= $newdiff) {
3917 $daysdiff = floor($daysdiff);
3918 } else {
3919 $daysdiff = ceil($daysdiff);
3920 }
3921 }
3922 }
3923
3924 $q = $dbo->getQuery(true)
3925 ->select($dbo->qn([
3926 'id',
3927 'checkin',
3928 'realback',
3929 ]))
3930 ->from($dbo->qn('#__vikbooking_busy'))
3931 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
3932 ->where($dbo->qn('realback') . ' >= ' . $first);
3933
3934 $dbo->setQuery($q);
3935 $busy = $dbo->loadAssocList();
3936 if (!$busy) {
3937 return ($units <= ($room_info['units'] ?? 1));
3938 }
3939
3940 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3941 foreach ($groupdays as $gday) {
3942 $bfound = 0;
3943 foreach ($busy as $bu) {
3944 if (in_array($bu['id'], $skip_busy_ids)) {
3945 // booking modification
3946 continue;
3947 }
3948 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3949 $bfound++;
3950 }
3951 }
3952 if ($bfound >= $units) {
3953 return false;
3954 }
3955 }
3956
3957 return ($units <= ($room_info['units'] ?? 1));
3958 }
3959
3960 public static function payTotal()
3961 {
3962 return VBOFactory::getConfig()->getBool('paytotal', true);
3963 }
3964
3965 public static function getDepositIfDays()
3966 {
3967 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
3968 }
3969
3970 public static function depositAllowedDaysAdv($checkints) {
3971 $days_adv = self::getDepositIfDays();
3972 if (!($days_adv > 0) || !($checkints > 0)) {
3973 return true;
3974 }
3975 $now_info = getdate();
3976 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
3977 return $maxts > $checkints ? false : true;
3978 }
3979
3980 public static function depositCustomerChoice() {
3981 $dbo = JFactory::getDbo();
3982 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
3983 $dbo->setQuery($q);
3984 $dbo->execute();
3985 $s = $dbo->loadAssocList();
3986 return (intval($s[0]['setting']) == 1 ? true : false);
3987 }
3988
3989 public static function getDepositOverrides($getjson = false) {
3990 $dbo = JFactory::getDbo();
3991 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
3992 $dbo->setQuery($q);
3993 $dbo->execute();
3994 if ($dbo->getNumRows() > 0) {
3995 $s = $dbo->loadResult();
3996 return $getjson ? $s : json_decode($s, true);
3997 }
3998 //count of this array will be at least 1 to store the "more" property
3999 $def_arr = array('more' => '');
4000 $def_val = json_encode($def_arr);
4001 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
4002 $dbo->setQuery($q);
4003 $dbo->execute();
4004 return $getjson ? $def_val : $def_arr;
4005 }
4006
4007 public static function calcDepositOverride($amount_deposit, $nights) {
4008 $overrides = self::getDepositOverrides();
4009 $nights = intval($nights);
4010 $andmore = intval($overrides['more']);
4011 if (!(count($overrides) > 1)) {
4012 //no overrides
4013 return $amount_deposit;
4014 }
4015 foreach ($overrides as $k => $v) {
4016 if ((int)$k == $nights && strlen($v) > 0) {
4017 //exact override found
4018 return (float)$v;
4019 }
4020 }
4021 if ($andmore > 0 && $andmore <= $nights) {
4022 foreach ($overrides as $k => $v) {
4023 if ((int)$k == $andmore && strlen($v) > 0) {
4024 //"and more" nights found
4025 return (float)$v;
4026 }
4027 }
4028 }
4029 //nothing was found
4030 return $amount_deposit;
4031 }
4032
4033 public static function noDepositForNonRefund() {
4034 $dbo = JFactory::getDbo();
4035 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
4036 $dbo->setQuery($q);
4037 $dbo->execute();
4038 if ($dbo->getNumRows() > 0) {
4039 $s = $dbo->loadResult();
4040 return ((int)$s == 1);
4041 }
4042 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
4043 $dbo->setQuery($q);
4044 $dbo->execute();
4045 //default to false
4046 return false;
4047 }
4048
4049 /**
4050 * This method returns the room-rate array with the lowest price
4051 * that matches the preferred rate plan parameter (if available).
4052 * The array $website_rates could not be an array, or it could be
4053 * an array with the error string (response from the TACVBO Class).
4054 * The method has been introduced in VBO 1.10 and it's mainly used
4055 * by the module mod_vikbooking_channelrates and its ajax requests.
4056 *
4057 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
4058 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
4059 *
4060 * @return array
4061 */
4062 public static function getBestRoomRate($website_rates, $def_rplan) {
4063 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
4064 return array();
4065 }
4066 $best_room_rate = array();
4067 foreach ($website_rates as $rid => $tars) {
4068 foreach ($tars as $tar) {
4069 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
4070 //the array $website_rates is already sorted by price ASC, so we take the first useful array
4071 $best_room_rate = $tar;
4072 break 2;
4073 }
4074 }
4075 }
4076 if (!(count($best_room_rate) > 0)) {
4077 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
4078 foreach ($website_rates as $rid => $tars) {
4079 foreach ($tars as $tar) {
4080 $best_room_rate = $tar;
4081 break 2;
4082 }
4083 }
4084 }
4085
4086 return $best_room_rate;
4087 }
4088
4089 /**
4090 * This method returns an array with the details
4091 * of all channels in VCM that supports AV requests,
4092 * and that have at least one room type mapped.
4093 * The method invokes the Logos Class to return details
4094 * about the name and logo URL of the channel.
4095 * The method has been introduced in VBO 1.10 and it's mainly used
4096 * by the module mod_vikbooking_channelrates and its ajax requests.
4097 *
4098 * @param array $channels an array of channel IDs to be mapped on the VCM relations
4099 *
4100 * @return array
4101 */
4102 public static function getChannelsMap($channels) {
4103 if (!is_array($channels) || !(count($channels))) {
4104 return array();
4105 }
4106 $vcm_logos = self::getVcmChannelsLogo('', true);
4107 if (!is_object($vcm_logos)) {
4108 return array();
4109 }
4110 $channels_ids = array();
4111 foreach ($channels as $chid) {
4112 $ichid = (int)$chid;
4113 if ($ichid < 1) {
4114 continue;
4115 }
4116 array_push($channels_ids, $ichid);
4117 }
4118 if (!(count($channels_ids))) {
4119 return array();
4120 }
4121 $dbo = JFactory::getDbo();
4122 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
4123 $dbo->setQuery($q);
4124 $dbo->execute();
4125 if ($dbo->getNumRows() < 1) {
4126 return array();
4127 }
4128 $channels_names = $dbo->loadAssocList();
4129 $channels_map = array();
4130 foreach ($channels_names as $ch) {
4131 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
4132 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
4133 $chdata = array(
4134 'id' => $ch['idchannel'],
4135 'name' => ucwords($ch['channel']),
4136 'logo' => $ota_logo_url
4137 );
4138 array_push($channels_map, $chdata);
4139 }
4140 return $channels_map;
4141 }
4142
4143 /**
4144 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
4145 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
4146 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
4147 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
4148 * module/widget and its ajax requests.
4149 *
4150 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
4151 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
4152 *
4153 * @return mixed string (empty or indicating the alteration), or associative array per channel.
4154 *
4155 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
4156 */
4157 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
4158 {
4159 $otas_rates_val = '';
4160 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
4161 return $otas_rates_val;
4162 }
4163 if (!class_exists('VikChannelManager')) {
4164 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
4165 }
4166 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
4167 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
4168 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
4169 // the Bulk Rates Cache contains data for this room type and rate plan
4170 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
4171 // build rates modification string
4172 if ((int)$data_cont['rmod'] > 0) {
4173 // rates were modified for the OTAs, check how
4174 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
4175 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
4176 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
4177 // check alterations per channel, if requested
4178 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
4179 // we compose an associative array with the alteration for each channel (0 = generic rule)
4180 $channel_alterations = array($otas_rates_val);
4181 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
4182 if ((int)$ch_mods['rmod'] > 0) {
4183 // custom rate alteration for this channel
4184 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
4185 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
4186 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
4187 } else {
4188 // no rates alteration for this channel identifier
4189 $ch_alter_string = '+0%';
4190 }
4191 $channel_alterations[$ch_id] = $ch_alter_string;
4192 }
4193 // overwrite return value with associative array
4194 $otas_rates_val = $channel_alterations;
4195 }
4196 }
4197 }
4198 }
4199
4200 return $otas_rates_val;
4201 }
4202
4203 /**
4204 * This method checks if some non-refundable rates were selected
4205 * (`free_cancellation`=0), the only argument is an array of tariffs.
4206 * The property 'idprice' must be defined on each sub-array.
4207 *
4208 * @param $tars array
4209 *
4210 * @return boolean
4211 **/
4212 public static function findNonRefundableRates($tars = [])
4213 {
4214 $id_prices = [];
4215
4216 foreach ($tars as $tar) {
4217 if (isset($tar['idprice'])) {
4218 if (!in_array($tar['idprice'], $id_prices)) {
4219 array_push($id_prices, (int)$tar['idprice']);
4220 }
4221 continue;
4222 }
4223 foreach ($tar as $t) {
4224 if (isset($t['idprice'])) {
4225 if (!in_array($t['idprice'], $id_prices)) {
4226 array_push($id_prices, (int)$t['idprice']);
4227 }
4228 }
4229 }
4230 }
4231
4232 if (!count($id_prices)) {
4233 // no price IDs found (probably a package)
4234 return false;
4235 }
4236
4237 $dbo = JFactory::getDbo();
4238
4239 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
4240 $dbo->setQuery($q);
4241 $dbo->execute();
4242
4243 return (bool)($dbo->getNumRows() > 0);
4244 }
4245
4246 /**
4247 * This method checks if the deposit is allowed depending on
4248 * the selected rate plans (idprice) for the rooms reserved.
4249 * If the configuration setting is enabled, and if some
4250 * non-refundable rates were selected (`free_cancellation`=0),
4251 * the method will return false, because the deposit is not allowed.
4252 * The only argument is an array of tariffs. The property 'idprice'
4253 * must be defined on each sub-array (multi-dimension supported)
4254 * throgh the method findNonRefundableRates();
4255 *
4256 * @param $tars array
4257 *
4258 * @return boolean
4259 **/
4260 public static function allowDepositFromRates($tars) {
4261 if (!self::noDepositForNonRefund()) {
4262 //deposit can be paid also if non-refundable rates
4263 return true;
4264 }
4265 return !self::findNonRefundableRates($tars);
4266 }
4267
4268 public static function showSearchSuggestions() {
4269 $dbo = JFactory::getDbo();
4270 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4271 $dbo->setQuery($q);
4272 $dbo->execute();
4273 if ($dbo->getNumRows() > 0) {
4274 return (int)$dbo->loadResult();
4275 }
4276 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4277 $dbo->setQuery($q);
4278 $dbo->execute();
4279 return 1;
4280 }
4281
4282 /**
4283 * Given a code, returns the coupon information array.
4284 *
4285 * @param string $code the coupon code.
4286 *
4287 * @return array empty array or coupon record.
4288 *
4289 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4290 */
4291 public static function getCouponInfo($code)
4292 {
4293 $dbo = JFactory::getDbo();
4294
4295 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4296 $dbo->setQuery($q, 0, 1);
4297 $coupon = $dbo->loadAssoc();
4298 if (!$coupon) {
4299 return [];
4300 }
4301
4302 // check for customers assigned
4303 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4304 $dbo->setQuery($q);
4305 $coupon_customers = $dbo->loadAssocList();
4306 if ($coupon_customers) {
4307 // set customers and automatic properties
4308 $coupon['customers'] = [];
4309 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4310 foreach ($coupon_customers as $coupon_customer) {
4311 // push customer ID
4312 $coupon['customers'][] = $coupon_customer['idcustomer'];
4313 }
4314 }
4315
4316 return $coupon;
4317 }
4318
4319 /**
4320 * Reads the details of a given room record ID.
4321 *
4322 * @param int $idroom The room ID to fetch.
4323 * @param array $columns The optional room columns to fetch.
4324 * @param bool $no_cache Whether to ignore a previously cached record.
4325 *
4326 * @return array
4327 *
4328 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4329 */
4330 public static function getRoomInfo($idroom, $columns = [], $no_cache = false)
4331 {
4332 static $room_infos = [];
4333
4334 if (isset($room_infos[$idroom]) && !$no_cache) {
4335 return $room_infos[$idroom];
4336 }
4337
4338 $dbo = JFactory::getDbo();
4339
4340 if ($columns) {
4341 $columns = array_map([$dbo, 'qn'], $columns);
4342 }
4343
4344 $q = $dbo->getQuery(true)
4345 ->select($columns ? $columns : '*')
4346 ->from($dbo->qn('#__vikbooking_rooms'))
4347 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4348
4349 $dbo->setQuery($q);
4350 $room = $dbo->loadAssoc();
4351
4352 if ($no_cache) {
4353 return $room ?: [];
4354 }
4355
4356 $room_infos[$idroom] = $room ?: [];
4357
4358 return $room_infos[$idroom];
4359 }
4360
4361 public static function loadOrdersRoomsData($idorder)
4362 {
4363 $dbo = JFactory::getDbo();
4364
4365 $q = $dbo->getQuery(true)
4366 ->select($dbo->qn('or') . '.*')
4367 ->select($dbo->qn('r.name', 'room_name'))
4368 ->select($dbo->qn('r.units', 'tot_units'))
4369 ->select($dbo->qn('r.params'))
4370 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4371 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4372 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4373 ->order($dbo->qn('or.id') . ' ASC');
4374
4375 $dbo->setQuery($q);
4376
4377 return $dbo->loadAssocList();
4378 }
4379
4380 public static function sayCategory($ids, $vbo_tn = null)
4381 {
4382 $dbo = JFactory::getDbo();
4383 $split = explode(";", $ids);
4384 $say = "";
4385 foreach ($split as $k => $s) {
4386 if (!strlen($s)) {
4387 continue;
4388 }
4389 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4390 $dbo->setQuery($q);
4391 $nam = $dbo->loadAssocList();
4392 if (!$nam) {
4393 continue;
4394 }
4395 if (is_object($vbo_tn)) {
4396 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4397 }
4398 $say .= $nam[0]['name'];
4399 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4400 }
4401 return $say;
4402 }
4403
4404 /**
4405 * Returns a list of the given room amenities string.
4406 *
4407 * @param string $amenities Room amenities string.
4408 * @param ?object $vbo_tn Optional VBOTranslator.
4409 * @param bool $all True for loading all records when no amenities given.
4410 *
4411 * @return array
4412 *
4413 * @since 1.17.3 (J) - 1.7.3 (WP)
4414 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $all.
4415 */
4416 public static function loadRoomAmenities($amenities, $vbo_tn = null, $all = false)
4417 {
4418 $dbo = JFactory::getDbo();
4419
4420 $split = explode(";", (string) $amenities);
4421 $where = [];
4422 foreach ($split as $s) {
4423 if (!empty($s)) {
4424 $where[] = (int) $s;
4425 }
4426 }
4427
4428 $list = [];
4429 $where = array_filter($where);
4430
4431 $query = $dbo->getQuery(true)
4432 ->select('*')
4433 ->from($dbo->qn('#__vikbooking_characteristics'))
4434 ->order($dbo->qn('ordering') . ' ASC');
4435
4436 if ($where) {
4437 $query->where($dbo->qn('id') . ' IN (' . implode(', ', $where) . ')');
4438 }
4439
4440 if ($where || $all) {
4441 $dbo->setQuery($query);
4442 $list = $dbo->loadAssocList();
4443 if ($list && is_object($vbo_tn)) {
4444 $vbo_tn->translateContents($list, '#__vikbooking_characteristics');
4445 }
4446 }
4447
4448 return $list;
4449 }
4450
4451 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4452 {
4453 $carat = '';
4454 $arr = self::loadRoomAmenities($idc, $vbo_tn);
4455
4456 if ($arr) {
4457 $carat .= "<div class=\"vbo-room-carats\">\n";
4458 foreach ($arr as $a) {
4459 if (!empty($a['textimg'])) {
4460 //tooltip icon text is not empty
4461 if (!empty($a['icon'])) {
4462 //an icon has been uploaded: display the image
4463 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"" . htmlspecialchars((string) $a['textimg'], ENT_QUOTES, 'UTF-8') . "\"><img src=\"".VBO_SITE_URI."resources/uploads/" . $a['icon'] . "\" alt=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\" /></span></span>\n";
4464 } else {
4465 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4466 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4467 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\">" . $a['textimg'] . "</span></span>\n";
4468 } else {
4469 //display just the text
4470 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4471 }
4472 }
4473 } else {
4474 $carat .= (!empty($a['icon']) ? "<span class=\"vbo-room-carat\"><img src=\"".VBO_SITE_URI."resources/uploads/" . $a['icon'] . "\" alt=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\" title=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\"/></span>\n" : "<span class=\"vbo-room-carat\">" . $a['name'] . "</span>\n");
4475 }
4476 }
4477 $carat .= "</div>\n";
4478 }
4479
4480 return $carat;
4481 }
4482
4483 public static function getRoomOptionals($idopts, $vbo_tn = null)
4484 {
4485 $dbo = JFactory::getDbo();
4486 $split = explode(";", $idopts);
4487 $fetch = array();
4488 foreach ($split as $s) {
4489 if (!empty($s)) {
4490 $fetch[] = $s;
4491 }
4492 }
4493 if ($fetch) {
4494 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $fetch).") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4495 $dbo->setQuery($q);
4496 $arr = $dbo->loadAssocList();
4497 if ($arr) {
4498 if (is_object($vbo_tn)) {
4499 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4500 }
4501 return $arr;
4502 }
4503 }
4504 return [];
4505 }
4506
4507 public static function getSingleOption($idopt, $vbo_tn = null)
4508 {
4509 $dbo = JFactory::getDbo();
4510 $opt = array();
4511 if (!empty($idopt)) {
4512 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4513 $dbo->setQuery($q);
4514 $opt = $dbo->loadAssoc();
4515 if ($opt && is_object($vbo_tn)) {
4516 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4517 }
4518 }
4519 return $opt;
4520 }
4521
4522 /**
4523 * Unsets the options that are not available in the requested dates.
4524 *
4525 * @param array $optionals the array of options passed by reference
4526 * @param int $checkin the timestamp of the check-in date and time
4527 * @param int $checkout the timestamp of the check-out date and time
4528 *
4529 * @return void
4530 *
4531 * @since 1.11
4532 */
4533 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4534 {
4535 if (!$optionals || empty($checkin) || empty($checkout)) {
4536 return;
4537 }
4538
4539 foreach ($optionals as $k => $v) {
4540 if (!empty($v['alwaysav'])) {
4541 $dates = explode(';', $v['alwaysav']);
4542 if (empty($dates[0]) || empty($dates[1])) {
4543 continue;
4544 }
4545 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4546 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4547 unset($optionals[$k]);
4548 }
4549 }
4550 }
4551 }
4552
4553 /**
4554 * Unsets the options that are not suited for the requested room party.
4555 *
4556 * @param array $optionals the array of options passed by reference.
4557 * @param int $adults the number of adults in the room party.
4558 * @param int $children the number of children in the room party.
4559 *
4560 * @return void
4561 *
4562 * @since 1.13.5
4563 */
4564 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4565 {
4566 if (!$optionals || (empty($adults) && empty($children))) {
4567 return;
4568 }
4569
4570 foreach ($optionals as $k => $v) {
4571 if (empty($v['oparams'])) {
4572 continue;
4573 }
4574 $v['oparams'] = json_decode($v['oparams'], true);
4575 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4576 continue;
4577 }
4578 if (!empty($v['oparams']['minguestsnum'])) {
4579 // filter by minimum adults/guests
4580 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4581 // minimum number of adults not sufficient, unset option
4582 unset($optionals[$k]);
4583 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4584 // minimum number of total guests not sufficient, unset option
4585 unset($optionals[$k]);
4586 }
4587 }
4588 if (!empty($v['oparams']['maxguestsnum'])) {
4589 // filter by maximum adults/guests
4590 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4591 // maximum number of adults exceeded, unset option
4592 unset($optionals[$k]);
4593 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4594 // maximum number of total guests exceeded, unset option
4595 unset($optionals[$k]);
4596 }
4597 }
4598 }
4599 }
4600
4601 /**
4602 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4603 *
4604 * @param array $id_rooms list of room IDs.
4605 * @param int $num_adults number of adults.
4606 * @param int $num_nights number of nights of stay.
4607 * @param int $checkin optional check-in timestamp.
4608 * @param int $checkout optional check-out timestamp.
4609 *
4610 * @return array associative information of city taxes, fees and options.
4611 *
4612 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4613 */
4614 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4615 {
4616 $dbo = JFactory::getDbo();
4617
4618 $taxes = 0;
4619 $fees = 0;
4620
4621 $options_data = [];
4622 $id_options = [];
4623
4624 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4625 $dbo->setQuery($q);
4626 $assocs = $dbo->loadAssocList();
4627 if ($assocs) {
4628 foreach ($assocs as $opts) {
4629 if (!empty($opts['idopt'])) {
4630 $r_ido = explode(';', rtrim($opts['idopt']));
4631 foreach ($r_ido as $ido) {
4632 if (!empty($ido) && !in_array($ido, $id_options)) {
4633 $id_options[] = (int)$ido;
4634 }
4635 }
4636 }
4637 }
4638 }
4639
4640 if ($id_options) {
4641 $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);";
4642 $dbo->setQuery($q);
4643 $alltaxesfees = $dbo->loadAssocList();
4644
4645 if ($checkin && $checkout) {
4646 // filter the records by stay dates to support, for example, seasonal city taxes
4647 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4648 }
4649
4650 foreach ($alltaxesfees as $tf) {
4651 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4652 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4653 $realcost = $tf['maxprice'];
4654 }
4655 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4656
4657 /**
4658 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
4659 *
4660 * @since 1.17.7 (J) - 1.7.7 (WP)
4661 */
4662 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$tf, ['days' => $num_nights], ['adults' => $num_adults]]);
4663 if ($custom_calculation) {
4664 $realcost = (float) $custom_calculation[0];
4665 }
4666
4667 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4668 if ($tf['is_citytax'] == 1) {
4669 $taxes += $realcost;
4670 } elseif ($tf['is_fee'] == 1) {
4671 $fees += $realcost;
4672 }
4673 $optsett = explode('-', $tf['forceval']);
4674 $options_data[] = $tf['id'].':'.$optsett[0];
4675 }
4676 }
4677
4678 return [
4679 'city_taxes' => $taxes,
4680 'fees' => $fees,
4681 'options' => $options_data
4682 ];
4683 }
4684
4685 /**
4686 * From a list of option/extras records, it returns an array with the options/extras
4687 * that do not support age intervals, and options for the children age intervals.
4688 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4689 *
4690 * @param array $optionals the full list of options/extras records.
4691 * @param int $adults the number of adults in the room requested.
4692 * @param int $children the number of children in the room requested.
4693 *
4694 * @return array the filtered regular and children age intervals options.
4695 */
4696 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4697 {
4698 // container for age intervals
4699 $ageintervals = [];
4700
4701 // regular options should start as an array
4702 if (!is_array($optionals)) {
4703 $optionals = [];
4704 }
4705
4706 // check for the first valid age intervals option
4707 foreach ($optionals as $kopt => $opt) {
4708 if (!empty($opt['ageintervals'])) {
4709 $intervals = explode(';;', $opt['ageintervals']);
4710 foreach ($intervals as $intv) {
4711 if (empty($intv)) {
4712 continue;
4713 }
4714 $parts = explode('_', $intv);
4715 if (count($parts) >= 3) {
4716 // age intervals option found, get the first and only one
4717 $ageintervals = $optionals[$kopt];
4718 break 2;
4719 }
4720 }
4721 }
4722 }
4723
4724 if ($ageintervals) {
4725 /**
4726 * We allow price adjustments for a minimum total guests by overriding the costs
4727 * for each age interval for specific child numbers when the party covers some children.
4728 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4729 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4730 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4731 *
4732 * @since 1.13.5
4733 */
4734 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4735 $oparams = !is_array($oparams) ? array() : $oparams;
4736 $adults = (int)$adults;
4737 $children = (int)$children;
4738 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4739 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4740 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4741 if ($children_to_pay < $children) {
4742 // compose string for free age intervals
4743 $intervals = explode(';;', $ageintervals['ageintervals']);
4744 foreach ($intervals as $k => $v) {
4745 $parts = explode('_', $v);
4746 // set cost to 0 (key = 2)
4747 $parts[2] = '0';
4748 $intervals[$k] = implode('_', $parts);
4749 }
4750 $free_ageintervals = implode(';;', $intervals);
4751 for ($i = 1; $i <= $children; $i++) {
4752 if (($children - $i + 1) <= $children_to_pay) {
4753 // costs for this Nth last child should be regular as this child should pay
4754 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4755 } else {
4756 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4757 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4758 }
4759 }
4760 }
4761 }
4762
4763 // remove age intervals from regular options
4764 foreach ($optionals as $kopt => $opt) {
4765 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4766 // unset the option of type age intervals from the regular options
4767 unset($optionals[$kopt]);
4768 }
4769 }
4770 }
4771
4772 // return the filtered list of regular options and age intervals options
4773 return [$optionals, $ageintervals];
4774 }
4775
4776 /**
4777 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4778 * This is because some children in the party may not need to pay anything due to the min guests.
4779 *
4780 * @param array $optional the option full record to parse.
4781 * @param int $adults the number of adults in the room requested.
4782 * @param int $children the number of children in the room requested.
4783 *
4784 * @return array associative array with default age intervals and override strings.
4785 *
4786 * @since 1.13.5
4787 */
4788 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4789 {
4790 if (!is_array($optional)) {
4791 $optional = array();
4792 }
4793
4794 $overrides = array(
4795 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4796 );
4797
4798 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4799 $oparams = !is_array($oparams) ? array() : $oparams;
4800 $adults = (int)$adults;
4801 $children = (int)$children;
4802
4803 if (!count($oparams)) {
4804 return $overrides;
4805 }
4806
4807 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4808 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4809 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4810 if ($children_to_pay < $children) {
4811 // compose string for free age intervals
4812 $intervals = explode(';;', $optional['ageintervals']);
4813 foreach ($intervals as $k => $v) {
4814 $parts = explode('_', $v);
4815 // set cost to 0 (key = 2)
4816 $parts[2] = '0';
4817 $intervals[$k] = implode('_', $parts);
4818 }
4819 $free_ageintervals = implode(';;', $intervals);
4820 for ($i = 1; $i <= $children; $i++) {
4821 if (($children - $i + 1) <= $children_to_pay) {
4822 // costs for this Nth last child should be regular as this child should pay
4823 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4824 } else {
4825 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4826 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4827 }
4828 }
4829 }
4830 }
4831
4832 return $overrides;
4833 }
4834
4835 /**
4836 * Returns the number/index of the children being parsed given the full list of room options string,
4837 * the current key in the array of the split room options string, the ID of the ageintervals option,
4838 * and the total number of children. This is useful for later applying the cost overrides for the
4839 * children ages through the method getOptionIntervalChildOverrides().
4840 *
4841 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4842 * @param int $optid the ID of the option of type age intervals to check.
4843 * @param int $roptkey the current position in the loop of the room options string.
4844 * @param int $children the number of children in the room requested.
4845 *
4846 * @return int the position/number of the children being parsed, -1 if not found.
4847 *
4848 * @since 1.13.5
4849 */
4850 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4851 {
4852 // default to -1 for not found
4853 $child_num = -1;
4854
4855 $valid_opt_counter = 0;
4856
4857 $roptions = explode(";", $roptstr);
4858 foreach ($roptions as $k => $opt) {
4859 if (empty($opt)) {
4860 continue;
4861 }
4862 $optvals = explode(":", $opt);
4863 /**
4864 * In some cases, like the "saveorder" task, the room option string may contain
4865 * the room number beside the option ID, separated with an underscore.
4866 * If an underscore is present, we need to split it to find the option ID.
4867 */
4868 if (strpos($optvals[0], '_') !== false) {
4869 // underscore found in room number portion, extract the option ID
4870 $rn_parts = explode('_', $optvals[0]);
4871 // 0th element is the room number in the party, 1st elem is the option ID
4872 $optvals[0] = $rn_parts[1];
4873 }
4874 //
4875 if ((int)$optvals[0] != (int)$optid) {
4876 // we are not interested into this option ID
4877 continue;
4878 }
4879 // increase counter for this option ID
4880 $valid_opt_counter++;
4881
4882 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4883 // children position found, we need to return it as a 0th base
4884 $child_num = ($valid_opt_counter - 1);
4885 break;
4886 }
4887 }
4888
4889 return $child_num;
4890 }
4891
4892 public static function getOptionIntervalsCosts($intvstr) {
4893 $optcosts = array();
4894 $intervals = explode(';;', $intvstr);
4895 foreach ($intervals as $kintv => $intv) {
4896 if (empty($intv)) continue;
4897 $parts = explode('_', $intv);
4898 if (count($parts) >= 3) {
4899 $optcosts[$kintv] = (float)$parts[2];
4900 }
4901 }
4902 return $optcosts;
4903 }
4904
4905 public static function getOptionIntervalsAges($intvstr) {
4906 $optages = array();
4907 $intervals = explode(';;', $intvstr);
4908 foreach ($intervals as $kintv => $intv) {
4909 if (empty($intv)) continue;
4910 $parts = explode('_', $intv);
4911 if (count($parts) >= 3) {
4912 $optages[$kintv] = $parts[0].' - '.$parts[1];
4913 }
4914 }
4915 return $optages;
4916 }
4917
4918 public static function getOptionIntervalsPercentage($intvstr) {
4919 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4920 $optcostspcent = array();
4921 $intervals = explode(';;', $intvstr);
4922 foreach ($intervals as $kintv => $intv) {
4923 if (empty($intv)) continue;
4924 $parts = explode('_', $intv);
4925 if (count($parts) >= 3) {
4926 //fixed amount
4927 $setval = 0;
4928 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
4929 //percentage value of the room base cost (VBO 1.10)
4930 $setval = 2;
4931 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
4932 //percentage value of the adults tariff
4933 $setval = 1;
4934 }
4935 $optcostspcent[$kintv] = $setval;
4936 }
4937 }
4938 return $optcostspcent;
4939 }
4940
4941 public static function dayValidTs($days, $first, $second)
4942 {
4943 $secdiff = $second - $first;
4944 $daysdiff = $secdiff / 86400;
4945 if (is_int($daysdiff)) {
4946 if ($daysdiff < 1) {
4947 $daysdiff = 1;
4948 }
4949 } else {
4950 if ($daysdiff < 1) {
4951 $daysdiff = 1;
4952 } else {
4953 $sum = floor($daysdiff) * 86400;
4954 $newdiff = $secdiff - $sum;
4955 $maxhmore = self::getHoursMoreRb() * 3600;
4956 if ($maxhmore >= $newdiff) {
4957 $daysdiff = floor($daysdiff);
4958 } else {
4959 $daysdiff = ceil($daysdiff);
4960 }
4961 }
4962 }
4963
4964 return ($daysdiff == $days);
4965 }
4966
4967 /**
4968 * Calculates the stay fee inclusive of taxes.
4969 *
4970 * @param float $cost the cost to evaluate.
4971 * @param int $idprice the rate plan ID associated.
4972 *
4973 * @return float the given cost after tax, if taxes had to be applied.
4974 */
4975 public static function sayCostPlusIva($cost, $idprice)
4976 {
4977 $ivainclusa = self::ivaInclusa();
4978 if ($ivainclusa) {
4979 return $cost;
4980 }
4981
4982 static $tax_rplans_map = [];
4983
4984 if (array_key_exists($idprice, $tax_rplans_map)) {
4985 $tax_rate = $tax_rplans_map[$idprice];
4986 } else {
4987 $dbo = JFactory::getDbo();
4988 $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 . ";";
4989 $dbo->setQuery($q);
4990 $tax_rate = $dbo->loadAssoc();
4991
4992 // cache value
4993 $tax_rplans_map[$idprice] = $tax_rate;
4994 }
4995
4996 if (!$tax_rate || empty($tax_rate['aliq'])) {
4997 return $cost;
4998 }
4999
5000 $subt = 100 + $tax_rate['aliq'];
5001 $op = ($cost * $subt / 100);
5002
5003 /**
5004 * Tax Cap implementation for prices tax excluded (most common).
5005 *
5006 * @since 1.12
5007 */
5008 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5009 $op = ($cost + $tax_rate['taxcap']);
5010 }
5011
5012 // apply rounding to avoid issues with multiple tax rates when tax excluded
5013 $formatvals = self::getNumberFormatData();
5014 $formatparts = explode(':', $formatvals);
5015 $rounded_op = round($op, (int)$formatparts[0]);
5016 if ($rounded_op > $op) {
5017 return $rounded_op;
5018 }
5019
5020 /**
5021 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5022 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5023 *
5024 * @since 1.11.1
5025 */
5026 return round($op, (int)$formatparts[0]);
5027 }
5028
5029 /**
5030 * Calculates the stay fee exclusive of taxes.
5031 *
5032 * @param float $cost the cost to evaluate.
5033 * @param int $idprice the rate plan ID associated.
5034 *
5035 * @return float the given cost before tax.
5036 */
5037 public static function sayCostMinusIva($cost, $idprice)
5038 {
5039 $ivainclusa = self::ivaInclusa();
5040 if (!$ivainclusa) {
5041 return $cost;
5042 }
5043
5044 static $tax_rplans_map = [];
5045
5046 if (array_key_exists($idprice, $tax_rplans_map)) {
5047 $tax_rate = $tax_rplans_map[$idprice];
5048 } else {
5049 $dbo = JFactory::getDbo();
5050 $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 . ";";
5051 $dbo->setQuery($q);
5052 $tax_rate = $dbo->loadAssoc();
5053
5054 // cache value
5055 $tax_rplans_map[$idprice] = $tax_rate;
5056 }
5057
5058 if (!$tax_rate || empty($tax_rate['aliq'])) {
5059 return $cost;
5060 }
5061
5062 $subt = 100 + $tax_rate['aliq'];
5063 $op = ($cost * 100 / $subt);
5064
5065 /**
5066 * Tax Cap implementation also when prices tax included.
5067 *
5068 * @since 1.12
5069 */
5070 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5071 $op = ($cost - $tax_rate['taxcap']);
5072 }
5073
5074 // apply rounding to avoid issues with multiple tax rates when tax included
5075 $formatvals = self::getNumberFormatData();
5076 $formatparts = explode(':', $formatvals);
5077 $rounded_op = round($op, (int)$formatparts[0]);
5078 if ($rounded_op < $op) {
5079 return $rounded_op;
5080 }
5081
5082 /**
5083 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5084 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5085 *
5086 * @since 1.11.1
5087 */
5088 return round($op, (int)$formatparts[0]);
5089 }
5090
5091 /**
5092 * Given an option/extra cost, determines whether taxes should be applied over it.
5093 * Can also be used to calculate taxes on the extra costs per room in the bookings
5094 *
5095 * @param float $cost the cost to evaluate, option or extra service fee.
5096 * @param int $idiva the ID of the tax rate to use to apply taxes.
5097 * @param bool $force if true, taxes will always be applied.
5098 *
5099 * @return float the given option/extra cost plus tax, if taxes had to be applied.
5100 *
5101 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5102 */
5103 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
5104 {
5105 $ivainclusa = self::ivaInclusa();
5106 if ($ivainclusa && $force !== true) {
5107 return $cost;
5108 }
5109
5110 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5111 if (!$tax_rate) {
5112 return $cost;
5113 }
5114
5115 $subt = 100 + $tax_rate['aliq'];
5116 $op = ($cost * $subt / 100);
5117
5118 /**
5119 * Tax Cap implementation for prices tax excluded (most common).
5120 *
5121 * @since 1.12
5122 */
5123 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5124 $op = ($cost + $tax_rate['taxcap']);
5125 }
5126
5127 // apply rounding to avoid issues with multiple tax rates when tax excluded
5128 $formatvals = self::getNumberFormatData();
5129 $formatparts = explode(':', $formatvals);
5130 $rounded_op = round($op, (int)$formatparts[0]);
5131 if ($rounded_op > $op) {
5132 return $rounded_op;
5133 }
5134
5135 return $op;
5136 }
5137
5138 /**
5139 * Calculates the net amount for an option/extra service.
5140 *
5141 * @param float $cost the cost to evaluate, option or extra service fee.
5142 * @param int $idiva the ID of the tax rate to use to apply taxes.
5143 *
5144 * @return float the given option/extra cost before tax, if taxes were inclusive.
5145 */
5146 public static function sayOptionalsMinusIva($cost, $idiva)
5147 {
5148 if (empty($idiva)) {
5149 return $cost;
5150 }
5151
5152 $ivainclusa = self::ivaInclusa();
5153 if (!$ivainclusa) {
5154 return $cost;
5155 }
5156
5157 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5158 if (!$tax_rate) {
5159 return $cost;
5160 }
5161
5162 $subt = 100 + $tax_rate['aliq'];
5163 $op = ($cost * 100 / $subt);
5164
5165 /**
5166 * Tax Cap implementation also when prices tax included.
5167 *
5168 * @since 1.12
5169 */
5170 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5171 $op = ($cost - $tax_rate['taxcap']);
5172 }
5173
5174 // apply rounding to avoid issues with multiple tax rates when tax included
5175 $formatvals = self::getNumberFormatData();
5176 $formatparts = explode(':', $formatvals);
5177 $rounded_op = round($op, (int)$formatparts[0]);
5178 if ($rounded_op < $op) {
5179 return $rounded_op;
5180 }
5181
5182 return $op;
5183 }
5184
5185 /**
5186 * Given a cost, determines whether taxes should be applied over it.
5187 *
5188 * @param float $cost the cost to evaluate, could be a room custom price.
5189 * @param int $idiva the ID of the tax rate to use to apply taxes.
5190 * @param bool $force if true, taxes will always be applied.
5191 *
5192 * @return float the given cost plus tax, if taxes had to be applied.
5193 *
5194 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5195 */
5196 public static function sayPackagePlusIva($cost, $idiva, $force = false)
5197 {
5198 $ivainclusa = self::ivaInclusa();
5199 if ($ivainclusa && $force !== true) {
5200 return $cost;
5201 }
5202
5203 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5204 if (!$tax_rate) {
5205 return $cost;
5206 }
5207
5208 $subt = 100 + $tax_rate['aliq'];
5209 $op = ($cost * $subt / 100);
5210
5211 /**
5212 * Tax Cap implementation for prices tax excluded (most common).
5213 *
5214 * @since 1.12
5215 */
5216 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5217 $op = ($cost + $tax_rate['taxcap']);
5218 }
5219
5220 // apply rounding to avoid issues with multiple tax rates when tax excluded
5221 $formatvals = self::getNumberFormatData();
5222 $formatparts = explode(':', $formatvals);
5223 $rounded_op = round($op, (int)$formatparts[0]);
5224 if ($rounded_op > $op) {
5225 return $rounded_op;
5226 }
5227
5228 return $op;
5229 }
5230
5231 /**
5232 * Calculates the net amount for a package.
5233 *
5234 * @param float $cost the cost to evaluate, option or extra service fee.
5235 * @param int $idiva the ID of the tax rate to use to apply taxes.
5236 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
5237 *
5238 * @return float the given cost before tax.
5239 */
5240 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
5241 {
5242 $ivainclusa = self::ivaInclusa();
5243 if (!$ivainclusa && $force_invoice_excltax !== true) {
5244 return $cost;
5245 }
5246
5247 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5248 if (!$tax_rate) {
5249 return $cost;
5250 }
5251
5252 $subt = 100 + $tax_rate['aliq'];
5253 $op = ($cost * 100 / $subt);
5254
5255 /**
5256 * Tax Cap implementation also when prices tax included.
5257 *
5258 * @since 1.12
5259 */
5260 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5261 $op = ($cost - $tax_rate['taxcap']);
5262 }
5263
5264 // apply rounding to avoid issues with multiple tax rates when tax included
5265 $formatvals = self::getNumberFormatData();
5266 $formatparts = explode(':', $formatvals);
5267 $rounded_op = round($op, (int)$formatparts[0]);
5268 if ($rounded_op < $op) {
5269 return $rounded_op;
5270 }
5271
5272 return $op;
5273 }
5274
5275 public static function getSecretLink()
5276 {
5277 $dbo = JFactory::getDbo();
5278
5279 $sid = mt_rand();
5280
5281 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
5282 $dbo->setQuery($q);
5283 $all = $dbo->loadAssocList();
5284 if ($all) {
5285 $list = [];
5286 foreach ($all as $s) {
5287 $list[] = $s['sid'];
5288 }
5289 if (in_array($sid, $list)) {
5290 while(in_array($sid, $list)) {
5291 $sid++;
5292 }
5293 }
5294 }
5295
5296 return $sid;
5297 }
5298
5299 /**
5300 * Generates a confirmation number for the given booking.
5301 * Modified the way random string-numbers are obtained to
5302 * avoid any possible brute-force attempt.
5303 *
5304 * @since 1.15.1 (J) - 1.5.4 (WP)
5305 */
5306 public static function generateConfirmNumber($oid, $update = true)
5307 {
5308 $dbo = JFactory::getDbo();
5309
5310 $confirmnumb = date('y') . $oid;
5311 for ($i = 0; $i < 8; $i++) {
5312 $confirmnumb .= rand(0, 9);
5313 }
5314
5315 if ($update) {
5316 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5317 $dbo->setQuery($q);
5318 $dbo->execute();
5319 }
5320
5321 return $confirmnumb;
5322 }
5323
5324 public static function buildCustData($arr, $sep) {
5325 $cdata = "";
5326 foreach ($arr as $k => $e) {
5327 if (strlen($e)) {
5328 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5329 }
5330 }
5331 return $cdata;
5332 }
5333
5334 /**
5335 * This method parses the Joomla menu object to see if a menu item of a
5336 * specific type is available, to get its ID. Useful when links should be
5337 * displayed in pages where there is no Itemid set (booking details pages).
5338 *
5339 * @param array $viewtypes list of accepted menu items
5340 * @param string $lang the optional language to use.
5341 *
5342 * @return int
5343 *
5344 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5345 */
5346 public static function findProperItemIdType($viewtypes, $lang = null)
5347 {
5348 if (VBOPlatformDetection::isWordPress()) {
5349 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5350
5351 $itemid = $model->best($viewtypes, $lang);
5352
5353 if ($itemid) {
5354 return $itemid;
5355 }
5356
5357 return 0;
5358 }
5359
5360 $bestitemid = 0;
5361
5362 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5363
5364 $app = JFactory::getApplication();
5365
5366 $menu = $app->getMenu('site');
5367
5368 if (!$menu) {
5369 return 0;
5370 }
5371
5372 $menu_items = $menu->getMenu();
5373
5374 if (!$menu_items) {
5375 return 0;
5376 }
5377
5378 foreach ($menu_items as $itemid => $item) {
5379 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5380 // proper menu item type found
5381 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5382
5383 if (isset($item->language) && $item->language == $current_lang) {
5384 // we found the exact menu item type for the given language
5385 return $itemid;
5386 }
5387 }
5388 }
5389
5390 return $bestitemid;
5391 }
5392
5393 /**
5394 * Rewrites an internal URI that needs to be used outside of the website.
5395 * This means that the routed URI MUST start with the base path of the site.
5396 *
5397 * @param mixed $query The query string or an associative array of data.
5398 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5399 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5400 *
5401 * @return string The complete routed URI.
5402 *
5403 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5404 * routing from back-end (if available on the CMS version).
5405 */
5406 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5407 {
5408 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5409 }
5410
5411 /**
5412 * Generates an iCal file to be attached to the email message for the
5413 * customer or the administrator with some basic booking details.
5414 *
5415 * @param string $recip either admin or customer.
5416 * @param array $booking the booking array or some keys.
5417 *
5418 * @return mixed string in case of success, false otherwise.
5419 *
5420 * @since 1.12.0
5421 */
5422 public static function getEmailIcal($recip, $booking) {
5423 // load configuration setting
5424 $attachical = self::attachIcal();
5425
5426 if ($attachical === 0) {
5427 // do not attach any iCal file
5428 return false;
5429 }
5430
5431 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5432 // skip the iCal for the admin
5433 return false;
5434 }
5435
5436 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5437 // skip the iCal for the customer
5438 return false;
5439 }
5440
5441 if (strpos($recip, 'admin') !== false) {
5442 // prepare event description and summary for the admin
5443 $description = $booking['custdata'];
5444 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5445 $fname = $booking['ts'] . '.ics';
5446 } else {
5447 // event description and summary for the customer
5448 $description = '';
5449 $summary = self::getFrontTitle();
5450 $fname = 'reservation_reminder.ics';
5451 }
5452
5453 // prepare iCal head
5454 $company_name = self::getFrontTitle();
5455 $ics_str = "BEGIN:VCALENDAR\r\n" .
5456 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5457 "CALSCALE:GREGORIAN\r\n" .
5458 "VERSION:2.0\r\n";
5459 // compose iCal body
5460 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5461 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5462 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5463 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5464 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5465 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5466 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5467 $ics_str .= 'END:VEVENT'."\r\n";
5468 // close iCal file content
5469 $ics_str .= "END:VCALENDAR";
5470
5471 /**
5472 * Trigger event to allow third party plugins to overwrite the iCal file.
5473 *
5474 * @since 1.16.0 (J) - 1.6.0 (WP)
5475 */
5476 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5477
5478 if (empty($ics_str)) {
5479 return false;
5480 }
5481
5482 // store the event onto a .ics file. We use the resources folder in back-end.
5483 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5484 $fp = fopen($fpath, 'w+');
5485 $bytes = fwrite($fp, $ics_str);
5486 fclose($fp);
5487
5488 return $bytes ? $fpath : false;
5489 }
5490
5491 public static function loadEmailTemplate($booking_info = array()) {
5492 define('_VIKBOOKINGEXEC', '1');
5493 ob_start();
5494 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5495 $content = ob_get_contents();
5496 ob_end_clean();
5497 return $content;
5498 }
5499
5500 /**
5501 * Parses the raw HTML content of the booking email template.
5502 *
5503 * @param string $tmpl the raw content of the template.
5504 * @param mixed $bid int for the booking ID or booking array.
5505 * @param array $rooms list of rooms booked and translated.
5506 * @param array $rates list of translated rates for the booked rooms.
5507 * @param array $options list of translated options booked.
5508 * @param float [$total] the booking total amount (in case it has changed).
5509 * @param string [$link] the booking link can be passed for the no-deposit.
5510 *
5511 * @return string the HTML content of the parsed email template.
5512 *
5513 * @since 1.13 with different arguments.
5514 */
5515 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5516 {
5517 $app = JFactory::getApplication();
5518 $dbo = JFactory::getDbo();
5519 $vbo_tn = self::getTranslator();
5520
5521 // availability helper
5522 $av_helper = self::getAvailabilityInstance();
5523
5524 // get necessary values
5525 if (is_array($bid)) {
5526 // we got the full booking record
5527 $order_info = $bid;
5528 $bid = $order_info['id'];
5529 } else {
5530 $order_info = array();
5531 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5532 $dbo->setQuery($q);
5533 $order_info = $dbo->loadAssoc();
5534 if (!$order_info) {
5535 throw new Exception('Booking not found', 404);
5536 }
5537 }
5538
5539 $tars_info = array();
5540 $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'] . ";";
5541 $dbo->setQuery($q);
5542 $tars_info = $dbo->loadAssocList();
5543 if (!$tars_info) {
5544 throw new Exception('No rooms found', 404);
5545 }
5546
5547 /**
5548 * Split stay reservation.
5549 *
5550 * @since 1.16.0 (J) - 1.6.0 (WP)
5551 */
5552 $room_stay_dates = [];
5553 if ($order_info['split_stay']) {
5554 if ($order_info['status'] == 'confirmed') {
5555 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5556 } else {
5557 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5558 }
5559 // immediately count the number of nights of stay for each split room
5560 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5561 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5562 // overwrite values for compatibility with non-confirmed bookings
5563 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5564 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5565 }
5566 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5567 // overwrite the whole array
5568 $room_stay_dates[$sps_r_k] = $sps_r_v;
5569 }
5570 }
5571
5572 // check if the language in use is the same as the one used during the checkout
5573 $lang = JFactory::getLanguage();
5574 if (!empty($order_info['lang'])) {
5575 if ($lang->getTag() != $order_info['lang']) {
5576 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5577 if (VBOPlatformDetection::isJoomla()) {
5578 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5579 }
5580 }
5581 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5582 // force the translation to start because contents should be translated
5583 $vbo_tn::$force_tolang = $order_info['lang'];
5584 }
5585 }
5586
5587 // values for replacements
5588 $company_name = self::getFrontTitle();
5589 $currencyname = self::getCurrencyName();
5590 $sitelogo = self::getSiteLogo();
5591 $footermess = self::getFooterOrdMail($vbo_tn);
5592 $dateformat = self::getDateFormat();
5593 $datesep = self::getDateSeparator();
5594 if ($dateformat == "%d/%m/%Y") {
5595 $df = 'd/m/Y';
5596 } elseif ($dateformat == "%m/%d/%Y") {
5597 $df = 'm/d/Y';
5598 } else {
5599 $df = 'Y/m/d';
5600 }
5601 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5602 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5603 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5604 $customer_info = nl2br($order_info['custdata']);
5605 $company_logo = '';
5606 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5607 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5608 }
5609 if ($order_info['status'] == 'cancelled') {
5610 $confirmnumber = '';
5611 $status_str = JText::translate('VBCANCELLED');
5612 } elseif ($order_info['status'] == 'standby') {
5613 $confirmnumber = '';
5614 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5615 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5616 // this is an inquiry reservation
5617 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5618 }
5619 } else {
5620 $confirmnumber = $order_info['confirmnumber'];
5621 $status_str = JText::translate('VBCOMPLETED');
5622 }
5623 // booking total amount
5624 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5625 // booking link
5626 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5627 if (is_null($link)) {
5628 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5629 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5630 }
5631
5632 // raw HTML content
5633 $parsed = $tmpl;
5634
5635 /**
5636 * Trigger event to allow third-party plugins to manipulate the template string.
5637 *
5638 * @since 1.16.10 (J) - 1.6.10 (WP)
5639 */
5640 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5641
5642 /**
5643 * Parse all conditional text rules.
5644 *
5645 * @since 1.14 (J) - 1.4.0 (WP)
5646 */
5647 self::getConditionalRulesInstance()
5648 ->set(['booking', 'rooms'], [$order_info, $rooms])
5649 ->parseTokens($parsed);
5650
5651 /**
5652 * Parse all Door Access Control tags.
5653 *
5654 * @since 1.18.4 (J) - 1.8.4 (WP)
5655 */
5656 VBOFactory::getDoorAccessControl()
5657 ->parseTokens((new VBOBookingRegistry($order_info, $rooms)), $parsed);
5658
5659 // special tokens (tags) replacement
5660 $parsed = str_replace("{logo}", $company_logo, $parsed);
5661 $parsed = str_replace("{company_name}", $company_name, $parsed);
5662 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5663 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5664 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5665 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5666 $parsed = str_replace("{order_status}", $status_str, $parsed);
5667 $parsed = str_replace("{order_date}", $create_date, $parsed);
5668
5669 // customer record
5670 $cpin = self::getCPinInstance();
5671 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5672 $customer_name = '';
5673 $customer_pin = '';
5674 if ($customer_record) {
5675 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5676 $customer_pin = $customer_record['pin'];
5677 }
5678 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5679 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5680
5681 // PIN Code
5682 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5683 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5684 if (!empty($customer_pin)) {
5685 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5686 }
5687 }
5688
5689 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5690
5691 // Confirmation Number
5692 if ($confirmnumber) {
5693 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5694 } else {
5695 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5696 }
5697 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5698 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5699
5700 $roomsnum = count($rooms);
5701 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5702 $roomstr = "";
5703 $tot_adults = 0;
5704 $tot_children = 0;
5705 $tot_guests = 0;
5706
5707 // Rooms Distinctive Features
5708 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5709
5710 foreach ($rooms as $num => $r) {
5711 // guests
5712 $tot_adults += (int) $r['adults'];
5713 $tot_children += (int) $r['children'];
5714 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5715
5716 // room info
5717 $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/>";
5718 // Rooms Distinctive Features
5719 if (is_array($matches[1] ?? []) && $matches[1]) {
5720 $distinctive_features = array();
5721 $rparams = (array)json_decode($r['params'], true);
5722 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'])) {
5723 $distinctive_features = $rparams['features'][$r['roomindex']];
5724 }
5725 $docheck = (count($distinctive_features) > 0);
5726 foreach ($matches[1] as $reqf) {
5727 $feature_found = false;
5728 if ($docheck) {
5729 foreach ($distinctive_features as $dfk => $dfv) {
5730 if (stripos($dfk, $reqf) !== false) {
5731 $feature_found = $dfk;
5732 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5733 break;
5734 }
5735 }
5736 }
5737 }
5738 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5739 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5740 }
5741 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5742 }
5743 }
5744 }
5745
5746 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5747 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5748 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5749
5750 // custom fields replace
5751 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5752 if (is_array($cmatches[1]) && count($cmatches[1])) {
5753 $cfids = array();
5754 foreach ($cmatches[1] as $cfid ) {
5755 $cfids[] = $cfid;
5756 }
5757 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5758 $dbo->setQuery($q);
5759 $cfields = $dbo->loadAssocList();
5760 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5761 $cfmap = array();
5762 foreach ($cfields as $cf) {
5763 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5764 }
5765 $cfmapreplace = array();
5766 $partsreceived = explode("\n", $order_info['custdata']);
5767 if (count($partsreceived) > 0) {
5768 foreach ($partsreceived as $pst) {
5769 if (!empty($pst)) {
5770 $tmpdata = explode(":", $pst);
5771 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5772 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5773 }
5774 }
5775 }
5776 }
5777 foreach ($cmatches[1] as $cfid ) {
5778 if (array_key_exists($cfid, $cfmapreplace)) {
5779 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5780 } else {
5781 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5782 }
5783 }
5784 }
5785
5786 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5787 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5788 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5789 $parsed = str_replace("{num_nights}", ($order_info['days'] ?? 1), $parsed);
5790
5791 // order details
5792 $orderdetails = "";
5793 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5794 foreach ($rooms as $num => $r) {
5795 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5796 $use_room_ind = $num - $room_loop_ind;
5797 $split_stay_str = '';
5798 $split_stay_info = [];
5799 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) {
5800 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5801 $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']);
5802 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5803 }
5804
5805 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5806 $faredets = explode(":", $expdet[0]);
5807 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5808 if (!empty($expdet[1])) {
5809 $attrfaredets = explode(":", $expdet[1]);
5810 if (strlen($attrfaredets[1]) > 0) {
5811 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5812 }
5813 }
5814 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5815 $orderdetails .= '<div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($fareprice), $currencyname) . '</span></div></div>';
5816 // options
5817 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5818 foreach ($options[$num] as $oo) {
5819 $expopts = explode("\n", $oo);
5820 foreach ($expopts as $optinfo) {
5821 if (!empty($optinfo)) {
5822 $splitopt = explode(":", $optinfo);
5823 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5824 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($optprice), $currencyname) . '</span></div></div>';
5825 }
5826 }
5827 }
5828 }
5829 //
5830 if ($roomsnum > 1 && $num < $roomsnum) {
5831 $orderdetails .= '<br/>';
5832 }
5833 }
5834
5835 // coupon
5836 if (!empty($order_info['coupon'])) {
5837 $expcoupon = explode(";", $order_info['coupon']);
5838 $orderdetails .= '<br/><div class="discount"><span>'.JText::translate('VBCOUPON').' '.$expcoupon[2].'</span><div class="service-amount" style="float: right;"><span>- ' . self::formatCurrencyNumber(self::numberFormat($expcoupon[1]), $currencyname) . '</span></div></div>';
5839 }
5840
5841 // discount and payment method
5842 $payment_info = [];
5843 $payment_name = '';
5844 if (!empty($order_info['idpayment'])) {
5845 $exppay = explode('=', $order_info['idpayment']);
5846 $payment = self::getPayment($exppay[0], $vbo_tn);
5847 if ((array) $payment) {
5848 $payment_name = $payment['name'];
5849 }
5850 }
5851
5852 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5853
5854 if ($order_info['status'] != 'cancelled') {
5855 if ($payment_info) {
5856 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5857 // Discount (not charge)
5858 if ($payment_info['val_pcent'] == 1) {
5859 // fixed value
5860 $total -= $payment_info['charge'];
5861 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- ' . self::formatCurrencyNumber(self::numberFormat($payment_info['charge']), $currencyname) . '</span></div></div>';
5862 } else {
5863 // percent value
5864 $percent_disc = $total * $payment_info['charge'] / 100;
5865 $total -= $percent_disc;
5866 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- ' . self::formatCurrencyNumber(self::numberFormat($percent_disc), $currencyname) . '</span></div></div>';
5867 }
5868 }
5869 }
5870 }
5871
5872 // booking details string
5873 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5874
5875 // additional information
5876 $parsed = str_replace("{order_total}", self::formatCurrencyNumber(self::numberFormat($total), $currencyname), $parsed);
5877 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5878 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5879 $parsed = str_replace("{booking_link}", $link, $parsed);
5880
5881 // deposit
5882 $deposit_str = '';
5883 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5884 $percentdeposit = self::getAccPerCent();
5885 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5886 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5887 if (self::getTypeDeposit() == "fixed") {
5888 $deposit_amount = $percentdeposit;
5889 } else {
5890 $baseTotal = $total;
5891 if ($order_info['tot_damage_dep'] ?? 0) {
5892 // check if the damage deposit will have to be paid later/separately
5893 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($order_info, $rooms);
5894 if (($damage_deposit_payment['damagedep_gross'] ?? 0) && ($damage_deposit_payment['payment_window'] ?? []) && ($damage_deposit_payment['payment_window']['payable'] ?? null) !== true) {
5895 // damage deposit is not payable right now
5896 $baseTotal -= (float) $order_info['tot_damage_dep'];
5897 }
5898 }
5899 $deposit_amount = $baseTotal * $percentdeposit / 100;
5900 }
5901 if ($deposit_amount > 0) {
5902 $deposit_str = '<div class="deposit"><span>'.JText::translate('VBLEAVEDEPOSIT').'</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($deposit_amount), $currencyname) . '</strong></div></div>';
5903 }
5904 }
5905 }
5906 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5907 //
5908 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5909 $totpaid_str = '';
5910 if ($order_info['refund'] > 0) {
5911 $totpaid_str .= '<div class="amountpaid amountrefunded"><span>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($order_info['refund']), $currencyname) . '</strong></div></div>';
5912 }
5913 if ($order_info['status'] != 'cancelled') {
5914 $tot_paid = $order_info['totpaid'];
5915 $diff_topay = (float)$total - (float)$tot_paid;
5916 if ((float)$tot_paid > 0) {
5917 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBAMOUNTPAID').'</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($tot_paid), $currencyname) . '</strong></div></div>';
5918 // only in case the remaining balance is greater than 1 to avoid commissions issues
5919 if ($diff_topay > 1) {
5920 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBTOTALREMAINING').'</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($diff_topay), $currencyname) . '</strong></div></div>';
5921 }
5922 }
5923 }
5924 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5925 $totpaid_str .= '<div class="amountpaid amountcancfee"><span>' . JText::translate('VBO_CANC_FEE') . '</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($order_info['canc_fee']), $currencyname) . '</strong></div></div>';
5926 }
5927 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
5928
5929 /**
5930 * Language direction (LTR or RTL).
5931 *
5932 * @since 1.16.4 (J) - 1.6.4 (WP)
5933 */
5934 $lang_direction = 'ltr';
5935 if (!empty($order_info['lang'])) {
5936 // attempt to define the language direction and replace the special tag
5937 try {
5938 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
5939 } catch (Throwable $e) {
5940 // do nothing, default to ltr
5941 $lang_direction = 'ltr';
5942 }
5943 // static map for Arabic and Hebrew
5944 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
5945 }
5946 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
5947 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
5948 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
5949
5950 /**
5951 * Replace static floating styles depending on language direction (LTR or RTL).
5952 * For BC with older email template sources, we need to keep the inline style
5953 * for the floating of certain elements, which looks good only on LTR.
5954 *
5955 * @since 1.16.4 (J) - 1.6.4 (WP)
5956 */
5957 if ($lang_direction === 'rtl') {
5958 // get rid of inline static style declaration because the template uses CSS classes
5959 $parsed = str_replace("float: right;", '', $parsed);
5960 }
5961
5962 return $parsed;
5963 }
5964
5965 /**
5966 * New method for sending booking email messages
5967 * to the guest or to the administrator(s).
5968 *
5969 * @param int $bid the booking ID.
5970 * @param array $for guest, admin or a custom email address.
5971 * @param bool $send whether to send or return the HTML message.
5972 * @param bool $no_config if true, no configuration settings will be used for the status.
5973 * @param string $type optional type to indicate a booking modification.
5974 *
5975 * @return mixed True or False depending on the result or HTML string for the preview.
5976 *
5977 * @since 1.13 (J) - 1.3 (WP)
5978 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
5979 * the reservation status is pending and against the configuration.
5980 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
5981 */
5982 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
5983 {
5984 $dbo = JFactory::getDbo();
5985 $app = JFactory::getApplication();
5986
5987 $vbo_tn = self::getTranslator();
5988 $av_helper = self::getAvailabilityInstance();
5989
5990 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5991 $dbo->setQuery($q);
5992 $booking = $dbo->loadAssoc();
5993 if (!$booking) {
5994 return false;
5995 }
5996
5997 $result = false;
5998
5999 // check if the language in use is the same as the one used during the checkout
6000 $lang = JFactory::getLanguage();
6001 if (!empty($booking['lang'])) {
6002 if ($lang->getTag() != $booking['lang']) {
6003 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
6004 if (VBOPlatformDetection::isJoomla()) {
6005 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
6006 }
6007 }
6008 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
6009 // force the translation to start because contents should be translated
6010 $vbo_tn::$force_tolang = $booking['lang'];
6011 }
6012 }
6013
6014 // load rooms booked
6015 $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;";
6016 $dbo->setQuery($q);
6017 $ordersrooms = $dbo->loadAssocList();
6018 if (!$ordersrooms) {
6019 return false;
6020 }
6021 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
6022
6023 /**
6024 * Split stay reservation.
6025 *
6026 * @since 1.16.0 (J) - 1.6.0 (WP)
6027 */
6028 $room_stay_dates = [];
6029 if ($booking['split_stay']) {
6030 if ($booking['status'] == 'confirmed') {
6031 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
6032 } else {
6033 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
6034 }
6035 // immediately count the number of nights of stay for each split room
6036 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6037 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6038 // overwrite values for compatibility with non-confirmed bookings
6039 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6040 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6041 }
6042 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6043 // overwrite the whole array
6044 $room_stay_dates[$sps_r_k] = $sps_r_v;
6045 }
6046 }
6047
6048 $rooms = array();
6049 $tars = array();
6050 $is_package = !empty($booking['pkg']) ? true : false;
6051 $ftitle = self::getFrontTitle();
6052 $currencyname = self::getCurrencyName();
6053 foreach ($ordersrooms as $kor => $or) {
6054 $num = $kor + 1;
6055 $rooms[$num] = $or;
6056 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6057 // package or custom cost set from the back-end
6058 continue;
6059 }
6060
6061 // determine the days to consider for the count of the availability
6062 $room_nights = $booking['days'];
6063 $room_checkin = $booking['checkin'];
6064 $room_checkout = $booking['checkout'];
6065 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6066 $room_nights = $room_stay_dates[$kor]['nights'];
6067 $room_checkin = $room_stay_dates[$kor]['checkin'];
6068 $room_checkout = $room_stay_dates[$kor]['checkout'];
6069 }
6070
6071 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
6072 $dbo->setQuery($q);
6073 $tar = $dbo->loadAssocList();
6074 if (!$tar) {
6075 // tariff not found
6076 if (self::isAdmin()) {
6077 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
6078 }
6079 continue;
6080 }
6081
6082 // apply seasonal rates
6083 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
6084
6085 // different usage
6086 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
6087 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
6088 // Occupancy Override
6089 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
6090 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
6091 //
6092 if (is_array($diffusageprice)) {
6093 // set a charge or discount to the price(s) for the different usage of the room
6094 foreach ($tar as $kpr => $vpr) {
6095 $tar[$kpr]['diffusage'] = $or['adults'];
6096 if ($diffusageprice['chdisc'] == 1) {
6097 // charge
6098 if ($diffusageprice['valpcent'] == 1) {
6099 // fixed value
6100 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6101 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6102 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
6103 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6104 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
6105 } else {
6106 // percentage value
6107 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6108 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
6109 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
6110 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6111 $tar[$kpr]['cost'] = $aduseval;
6112 }
6113 } else {
6114 // discount
6115 if ($diffusageprice['valpcent'] == 1) {
6116 // fixed value
6117 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6118 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6119 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
6120 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6121 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
6122 } else {
6123 // percentage value
6124 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6125 $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);
6126 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
6127 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6128 $tar[$kpr]['cost'] = $aduseval;
6129 }
6130 }
6131 }
6132 }
6133 }
6134 //
6135 $tars[$num] = $tar[0];
6136 }
6137
6138 $secdiff = $booking['checkout'] - $booking['checkin'];
6139 $daysdiff = $secdiff / 86400;
6140 if (is_int($daysdiff)) {
6141 if ($daysdiff < 1) {
6142 $daysdiff = 1;
6143 }
6144 } else {
6145 if ($daysdiff < 1) {
6146 $daysdiff = 1;
6147 } else {
6148 $sum = floor($daysdiff) * 86400;
6149 $newdiff = $secdiff - $sum;
6150 $maxhmore = self::getHoursMoreRb() * 3600;
6151 if ($maxhmore >= $newdiff) {
6152 $daysdiff = floor($daysdiff);
6153 } else {
6154 $daysdiff = ceil($daysdiff);
6155 }
6156 }
6157 }
6158
6159 $isdue = 0;
6160 $pricestr = array();
6161 $optstr = array();
6162 foreach ($ordersrooms as $kor => $or) {
6163 $num = $kor + 1;
6164
6165 // determine the days to consider for the count of the availability
6166 $room_nights = $booking['days'];
6167 $room_checkin = $booking['checkin'];
6168 $room_checkout = $booking['checkout'];
6169 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6170 $room_nights = $room_stay_dates[$kor]['nights'];
6171 $room_checkin = $room_stay_dates[$kor]['checkin'];
6172 $room_checkout = $room_stay_dates[$kor]['checkout'];
6173 }
6174
6175 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6176 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
6177 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
6178 $isdue += $calctar;
6179 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
6180 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
6181 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6182 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
6183 $tars[$num]['calctar'] = $calctar;
6184 $isdue += $calctar;
6185 $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'] : "");
6186 }
6187
6188 if (!empty($or['optionals'])) {
6189 $stepo = explode(";", $or['optionals']);
6190 foreach ($stepo as $roptkey => $oo) {
6191 if (empty($oo)) {
6192 continue;
6193 }
6194 $stept = explode(":", $oo);
6195 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
6196 $dbo->setQuery($q);
6197 $actopt = $dbo->loadAssocList();
6198 if ($actopt) {
6199 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
6200 $chvar = '';
6201 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
6202 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
6203 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
6204 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
6205 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
6206 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
6207 $agestept = explode('-', $stept[1]);
6208 $stept[1] = $agestept[0];
6209 $chvar = $agestept[1];
6210 $realcost = 0;
6211 if (!empty($chvar)) {
6212 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
6213 // percentage value of the adults tariff
6214 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6215 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6216 } else {
6217 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6218 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6219 }
6220 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
6221 // VBO 1.10 - percentage value of room base cost
6222 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6223 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6224 } else {
6225 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
6226 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6227 }
6228 }
6229 $actopt[0]['chageintv'] = $chvar;
6230 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
6231 $actopt[0]['quan'] = $stept[1];
6232 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
6233 }
6234 } else {
6235 $actopt[0]['quan'] = $stept[1];
6236 // VBO 1.11 - options percentage cost of the room total fee
6237 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6238 $deftar_basecosts = $or['cust_cost'];
6239 } else {
6240 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6241 }
6242 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
6243 //
6244 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
6245 }
6246 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
6247 $realcost = $actopt[0]['maxprice'];
6248 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
6249 $realcost = $actopt[0]['maxprice'] * $stept[1];
6250 }
6251 }
6252 if ($actopt[0]['perperson'] == 1) {
6253 $realcost = $realcost * $or['adults'];
6254 }
6255
6256 /**
6257 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
6258 *
6259 * @since 1.17.7 (J) - 1.7.7 (WP)
6260 */
6261 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
6262 if ($custom_calculation) {
6263 $realcost = (float) $custom_calculation[0];
6264 }
6265
6266 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
6267 $isdue += $tmpopr;
6268 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
6269 }
6270 }
6271 }
6272
6273 // custom extra costs
6274 if (!empty($or['extracosts'])) {
6275 $cur_extra_costs = json_decode($or['extracosts'], true);
6276 foreach ($cur_extra_costs as $eck => $ecv) {
6277 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
6278 $isdue += $ecplustax;
6279 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
6280 }
6281 }
6282 }
6283
6284 // force the original total amount if rates have changed
6285 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
6286 $isdue = $booking['total'];
6287 }
6288
6289 // mail subject
6290 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
6291 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
6292
6293 // inject the recipient of the message for the template
6294 $booking['for'] = $for;
6295
6296 // load template file that will get $booking as variable
6297 $tmpl = self::loadEmailTemplate($booking);
6298
6299 // parse email template
6300 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
6301 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
6302
6303 if ($send !== true) {
6304 // return the content of the email message parsed
6305 return $hmess;
6306 }
6307
6308 // when the message can be sent
6309 $sendwhen = self::getSendEmailWhen();
6310
6311 if ($no_config) {
6312 // force the configuration setting to send the message no matter what's the booking status
6313 $sendwhen = 1;
6314 }
6315
6316 // send the message
6317 foreach ($for as $who) {
6318 $use_subject = $subject;
6319 $recipients = array();
6320 $attachments = self::addEmailAttachment(null);
6321 $attach_ical = false;
6322 $force_replyto = null;
6323 if (strpos($who, '@') !== false) {
6324 // send email to custom email address
6325 array_push($recipients, trim($who));
6326 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6327 // send email to the customer
6328 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6329 continue;
6330 }
6331 array_push($recipients, $booking['custmail']);
6332 /**
6333 * Check whether an iCal should be attached for the customer.
6334 *
6335 * @since 1.12.0
6336 */
6337 $attach_ical = self::getEmailIcal('customer', $booking);
6338 } elseif (stripos($who, 'admin') !== false) {
6339 // send email to the administrator(s)
6340 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6341 continue;
6342 }
6343 $use_subject = $subject . ' #' . $booking['id'];
6344 if (!strcasecmp($type, 'modified')) {
6345 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6346 }
6347 $adminemail = self::getAdminMail();
6348 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6349 if (empty($adminemail) && empty($extra_admin_recipients)) {
6350 // Prevent Joomla Exceptions that would stop the script execution
6351 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6352 continue;
6353 }
6354 if (strpos($adminemail, ',') !== false) {
6355 // multiple addresses
6356 $adminemails = explode(',', $adminemail);
6357 foreach ($adminemails as $am) {
6358 if (strpos($am, '@') !== false) {
6359 array_push($recipients, trim($am));
6360 }
6361 }
6362 } else {
6363 // single address
6364 array_push($recipients, trim($adminemail));
6365 }
6366
6367 // merge extra recipients
6368 $recipients = array_merge($recipients, $extra_admin_recipients);
6369
6370 // admin should reply to the customer
6371 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6372
6373 /**
6374 * Check whether an iCal should be attached for the admin.
6375 *
6376 * @since 1.2.0
6377 */
6378 $attach_ical = self::getEmailIcal('admin', array(
6379 'ts' => $booking['ts'],
6380 'custdata' => $booking['custdata'],
6381 'checkin' => $booking['checkin'],
6382 'checkout' => $booking['checkout'],
6383 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6384 ));
6385 }
6386
6387 // send the message, recipients should always be an array to support multiple admin addresses
6388
6389 // get sender e-mail
6390 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6391
6392 // init mail data
6393 $mail = new VBOMailWrapper([
6394 'sender' => [$adsendermail, $ftitle],
6395 'recipient' => $recipients,
6396 'bcc' => self::addAdminEmailRecipient(null, true),
6397 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6398 'subject' => $use_subject,
6399 'content' => $hmess,
6400 'attachments' => $attachments,
6401 ]);
6402
6403 if ($attach_ical !== false && $booking['status'] != 'standby') {
6404 // attach iCal file
6405 $mail->addAttachment($attach_ical);
6406 }
6407
6408 /**
6409 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6410 *
6411 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6412 *
6413 * @since 1.15.4 (J) - 1.5.10 (WP)
6414 */
6415 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6416
6417 // send e-mail
6418 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6419
6420 // unlink iCal file
6421 if ($attach_ical !== false) {
6422 @unlink($attach_ical);
6423 }
6424 }
6425
6426 return $result;
6427 }
6428
6429 /**
6430 * This method allows to add one or more recipient email
6431 * addresses for the next queue of email sending for the admin.
6432 * This method can be used in the template file for the customer
6433 * email to register an additional email address, maybe when a
6434 * specific room-type is booked.
6435 * The methods sending the email messages are supposed to call this
6436 * method by passing no arguments to obtain the extra addresses set.
6437 *
6438 * @param mixed $email null, string or array of email address(es).
6439 * @param bool $bcc if true, addresses will be used as bcc.
6440 * @param bool $reset if true, the queues will be emptied.
6441 *
6442 * @return array the current extra recipients or bcc addresses set.
6443 *
6444 * @since 1.13 (J) - 1.3.0 (WP)
6445 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6446 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6447 */
6448 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6449 {
6450 static $extra_recipients = [];
6451 static $extra_bcc = [];
6452
6453 if ($reset) {
6454 $extra_recipients = [];
6455 $extra_bcc = [];
6456 }
6457
6458 if (!empty($email)) {
6459 if (is_scalar($email)) {
6460 if ($bcc) {
6461 array_push($extra_bcc, $email);
6462 } else {
6463 array_push($extra_recipients, $email);
6464 }
6465 } else {
6466 if ($bcc) {
6467 $extra_bcc = array_merge($extra_bcc, $email);
6468 } else {
6469 $extra_recipients = array_merge($extra_recipients, $email);
6470 }
6471 }
6472 }
6473
6474 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6475 }
6476
6477 /**
6478 * This method serves to add one or more attachments to the
6479 * next queue of email sending for the admin.
6480 * The methods sending the email messages are supposed to call this
6481 * method by passing a null argument to obtain the attachments set.
6482 *
6483 * @param mixed $file null or string with path to file to attach.
6484 * @param bool $reset if true, the queue will be emptied.
6485 *
6486 * @return array the current attachments set.
6487 *
6488 * @since 1.14
6489 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6490 */
6491 public static function addEmailAttachment($file, $reset = false)
6492 {
6493 static $extra_attachments = [];
6494
6495 if ($reset) {
6496 $extra_attachments = [];
6497 }
6498
6499 if (!empty($file)) {
6500 if (is_scalar($file)) {
6501 array_push($extra_attachments, $file);
6502 } else {
6503 $extra_attachments = array_merge($extra_attachments, $file);
6504 }
6505 }
6506
6507 return array_unique($extra_attachments);
6508 }
6509
6510 /**
6511 * This method is called whenever some rooms get booked.
6512 * It checks whether the rooms involved have shared calendars.
6513 * If some are found, then also such rooms will be occupied.
6514 *
6515 * @param int $bid the booking ID.
6516 * @param array $roomids the list of rooms booked.
6517 * @param int $checkin checkin timestamp.
6518 * @param int $checkout checkout timestamp.
6519 *
6520 * @return boolean true if some other cals were occupied, false otherwise.
6521 *
6522 * @since 1.13 (J) - 1.3.0 (WP)
6523 *
6524 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6525 */
6526 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6527 {
6528 $dbo = JFactory::getDbo();
6529 $bid = (int)$bid;
6530
6531 // availability helper
6532 $av_helper = self::getAvailabilityInstance();
6533
6534 // check for split stay rooms reservation
6535 $known_split_stay = false;
6536 $is_split_stay = 0;
6537
6538 if (empty($checkin) || empty($checkout)) {
6539 // get checkin and checkout timestamps from booking
6540 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6541 $dbo->setQuery($q);
6542 $dbo->execute();
6543 if (!$dbo->getNumRows()) {
6544 // booking not found
6545 return false;
6546 }
6547 $bdata = $dbo->loadAssoc();
6548 $checkin = $bdata['checkin'];
6549 $checkout = $bdata['checkout'];
6550 $is_split_stay = $bdata['split_stay'];
6551 // turn flag on
6552 $known_split_stay = true;
6553 }
6554
6555 if (!$known_split_stay) {
6556 // we need to query the db to access this information
6557 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6558 $dbo->setQuery($q, 0, 1);
6559 $dbo->execute();
6560 if ($dbo->getNumRows()) {
6561 $is_split_stay = (int)$dbo->loadResult();
6562 }
6563 // turn flag on
6564 $known_split_stay = true;
6565 }
6566
6567 // check split stay room records
6568 $room_stay_dates = [];
6569 if ($is_split_stay) {
6570 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6571 // immediately count the number of nights of stay for each split room
6572 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6573 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6574 // overwrite values for compatibility with non-confirmed bookings
6575 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6576 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6577 }
6578 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6579 // overwrite the whole array
6580 $room_stay_dates[$sps_r_k] = $sps_r_v;
6581 }
6582 }
6583
6584 // get the rooms booked
6585 if (!count($roomids)) {
6586 // get the IDs of all rooms booked
6587 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6588 $dbo->setQuery($q);
6589 $orr = $dbo->loadAssocList();
6590 if ($orr) {
6591 foreach ($orr as $or) {
6592 array_push($roomids, $or['idroom']);
6593 }
6594 }
6595 }
6596 if (!count($roomids) || empty($bid)) {
6597 // unable to proceed
6598 return false;
6599 }
6600 $roomids = array_unique($roomids);
6601
6602 // build room stay dates map in case of split stay
6603 $room_split_stay_map = [];
6604 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6605 // we have unique room IDs in a split stay reservation for more than one room
6606 foreach ($room_stay_dates as $room_stay_date) {
6607 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6608 }
6609 }
6610
6611 // get rooms involved
6612 $involved = [];
6613 $involved_stay_map = [];
6614 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6615 $dbo->setQuery($q);
6616 $rooms_found = $dbo->loadAssocList();
6617 if (!$rooms_found) {
6618 // no rooms involved that need their calendars updated
6619 return false;
6620 }
6621 foreach ($rooms_found as $rf) {
6622 if (!in_array($rf['mainroom'], $roomids)) {
6623 // push room ID
6624 array_push($involved, $rf['mainroom']);
6625 // check for specific stay dates in connected room
6626 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6627 // split stay information available
6628 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6629 }
6630 }
6631 if (!in_array($rf['childroom'], $roomids)) {
6632 // push room ID
6633 array_push($involved, $rf['childroom']);
6634 // check for specific stay dates in connected room
6635 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6636 // split stay information available
6637 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6638 }
6639 }
6640 }
6641 $involved = array_unique($involved);
6642 if (!count($involved)) {
6643 // no rooms involved
6644 return false;
6645 }
6646
6647 // turnover seconds
6648 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6649
6650 // occupy the calendars for the involved rooms found
6651 $bids_generated = [];
6652 foreach ($involved as $rid) {
6653 // determine the values to use
6654 $room_checkin = (int)$checkin;
6655 $room_checkout = (int)$checkout;
6656 $room_realback = ($checkout + $turnover_secs);
6657 if (isset($involved_stay_map[$rid])) {
6658 // split stay dates available
6659 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6660 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6661 $room_realback = ($room_checkout + $turnover_secs);
6662 }
6663
6664 // occupy record in room with shared calendar
6665 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6666 $dbo->setQuery($q);
6667 $dbo->execute();
6668 array_push($bids_generated, $dbo->insertid());
6669 }
6670
6671 // store busy relations created
6672 foreach ($bids_generated as $busyid) {
6673 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6674 $dbo->setQuery($q);
6675 $dbo->execute();
6676 }
6677
6678 return true;
6679 }
6680
6681 /**
6682 * This method is needed whenever a booking gets modified by
6683 * adding or removing rooms. This way we reset (remove) all
6684 * busy records that were previously stored due to a shared
6685 * calendar. The correct relations should then be re-created
6686 * by calling updateSharedCalendars().
6687 *
6688 * @param int $bid the booking ID.
6689 *
6690 * @return boolean true if some records were cleaned.
6691 *
6692 * @see updateSharedCalendars() should be called after.
6693 *
6694 * @since 1.3.0
6695 */
6696 public static function cleanSharedCalendarsBusy($bid) {
6697 $dbo = JFactory::getDbo();
6698 $bid = (int)$bid;
6699 // get all the occupied records due to shared calendars for this booking
6700 $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;";
6701 $dbo->setQuery($q);
6702 $allbusy = $dbo->loadAssocList();
6703 if (!$allbusy) {
6704 return false;
6705 }
6706
6707 $busy_ids = array();
6708 foreach ($allbusy as $b) {
6709 // push busy ID to be removed later
6710 array_push($busy_ids, $b['id']);
6711 // delete the current busy ID-booking ID relation for this shared calendar
6712 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6713 $dbo->setQuery($q);
6714 $dbo->execute();
6715 }
6716 if (count($busy_ids)) {
6717 // delete all busy records due to shared calendars
6718 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6719 $dbo->setQuery($q);
6720 $dbo->execute();
6721 }
6722
6723 return true;
6724 }
6725
6726 public static function sendJutility() {
6727 //deprecated in VBO 1.10
6728 $dbo = JFactory::getDbo();
6729 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6730 $dbo->setQuery($q);
6731 $dbo->execute();
6732 $s = $dbo->loadAssocList();
6733 return (intval($s[0]['setting']) == 1 ? true : false);
6734 }
6735
6736 public static function getCategoryName($idcat, $vbo_tn = null) {
6737 $dbo = JFactory::getDbo();
6738 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6739 $dbo->setQuery($q);
6740 $dbo->execute();
6741 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6742 if (is_object($vbo_tn) && count($p) > 0) {
6743 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6744 }
6745 return count($p) > 0 ? $p[0]['name'] : '';
6746 }
6747
6748 public static function loadAdultsDiff($idroom, $adults)
6749 {
6750 static $obp_map = [];
6751
6752 $map_sign = "{$idroom}:{$adults}";
6753
6754 if (array_key_exists($map_sign, $obp_map)) {
6755 return $obp_map[$map_sign];
6756 }
6757
6758 $dbo = JFactory::getDbo();
6759
6760 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6761 $dbo->setQuery($q, 0, 1);
6762 $obp_diff = $dbo->loadAssoc();
6763 if ($obp_diff) {
6764 // cache value and return it
6765 $obp_map[$map_sign] = $obp_diff;
6766
6767 return $obp_diff;
6768 }
6769
6770 // cache value and return it
6771 $obp_map[$map_sign] = null;
6772
6773 return null;
6774 }
6775
6776 public static function loadRoomAdultsDiff($idroom)
6777 {
6778 $dbo = JFactory::getDbo();
6779
6780 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6781 $dbo->setQuery($q);
6782 $diff = $dbo->loadAssocList();
6783 if ($diff) {
6784 $roomdiff = [];
6785 foreach ($diff as $v) {
6786 $roomdiff[$v['adults']] = $v;
6787 }
6788
6789 return $roomdiff;
6790 }
6791
6792 return [];
6793 }
6794
6795 public static function occupancyOverrideExists($tar, $adults) {
6796 foreach ($tar as $k => $v) {
6797 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6798 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6799 return $v['occupancy_ovr'][$adults];
6800 }
6801 }
6802 }
6803 return false;
6804 }
6805
6806 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6807 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6808 $charges = array();
6809 if (!($children > 0) || !(count($ages) > 0)) {
6810 return $charges;
6811 }
6812 $dbo = JFactory::getDbo();
6813 $id_options = array();
6814 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6815 $dbo->setQuery($q);
6816 $dbo->execute();
6817 if ($dbo->getNumRows() > 0) {
6818 $assocs = $dbo->loadAssocList();
6819 foreach ($assocs as $opts) {
6820 if (!empty($opts['idopt'])) {
6821 $r_ido = explode(';', rtrim($opts['idopt']));
6822 foreach ($r_ido as $ido) {
6823 if (!empty($ido) && !in_array($ido, $id_options)) {
6824 $id_options[] = $ido;
6825 }
6826 }
6827 }
6828 }
6829 }
6830 if (count($id_options) > 0) {
6831 $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;";
6832 $dbo->setQuery($q);
6833 $dbo->execute();
6834 if ($dbo->getNumRows() > 0) {
6835 $ageintervals = $dbo->loadAssocList();
6836 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6837 $age_range = array();
6838 foreach ($split_ages as $kg => $spage) {
6839 if (empty($spage)) {
6840 continue;
6841 }
6842 $parts = explode('_', $spage);
6843 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6844 $ind = count($age_range);
6845 $age_range[$ind]['from'] = intval($parts[0]);
6846 $age_range[$ind]['to'] = intval($parts[1]);
6847 //taxes are calculated later in VCM
6848 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6849 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6850 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6851 }
6852 }
6853 if (count($age_range) > 0) {
6854 $tot_charge = 0;
6855 $affected = array();
6856 $option_str = '';
6857 foreach ($ages as $age) {
6858 if (strlen($age) == 0) {
6859 continue;
6860 }
6861 foreach ($age_range as $range) {
6862 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6863 $tot_charge += $range['cost'];
6864 $affected[] = $age;
6865 $option_str .= $range['option_str'].';';
6866 break;
6867 }
6868 }
6869 }
6870 if ($tot_charge > 0) {
6871 $charges['total'] = $tot_charge;
6872 $charges['affected'] = $affected;
6873 $charges['options'] = $option_str;
6874 }
6875 }
6876 }
6877 }
6878
6879 return $charges;
6880 }
6881
6882 public static function sortRoomPrices($arr) {
6883 $newarr = array();
6884 foreach ($arr as $k => $v) {
6885 $newarr[$k] = $v['cost'];
6886 }
6887 asort($newarr);
6888 $sorted = array();
6889 foreach ($newarr as $k => $v) {
6890 $sorted[$k] = $arr[$k];
6891 }
6892 return $sorted;
6893 }
6894
6895 public static function sortResults($arr) {
6896 $newarr = array();
6897 foreach ($arr as $k => $v) {
6898 $newarr[$k] = $v[0]['cost'];
6899 }
6900 asort($newarr);
6901 $sorted = array();
6902 foreach ($newarr as $k => $v) {
6903 $sorted[$k] = $arr[$k];
6904 }
6905 return $sorted;
6906 }
6907
6908 public static function sortMultipleResults($arr) {
6909 foreach ($arr as $k => $v) {
6910 $newarr = array();
6911 foreach ($v as $subk => $subv) {
6912 $newarr[$subk] = $subv[0]['cost'];
6913 }
6914 asort($newarr);
6915 $sorted = array();
6916 foreach ($newarr as $nk => $v) {
6917 $sorted[$nk] = $arr[$k][$nk];
6918 }
6919 $arr[$k] = $sorted;
6920 }
6921 return $arr;
6922 }
6923
6924 /**
6925 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6926 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
6927 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
6928 *
6929 * @param array $promos optional list of promotion IDs to register in the execution flow.
6930 *
6931 * @return array list of cached promotion IDs (if any).
6932 *
6933 * @since 1.16.4 (J) - 1.6.4 (WP)
6934 */
6935 public static function registerPromotionIds(array $promos = [])
6936 {
6937 static $flagged_promos = [];
6938
6939 if ($promos) {
6940 // set cached value
6941 $flagged_promos = $promos;
6942 }
6943
6944 return $flagged_promos;
6945 }
6946
6947 /**
6948 * Getter and setter for seasons cache allowed.
6949 *
6950 * @param bool $enabled true or false for setter, null for getter.
6951 *
6952 * @return bool|null
6953 *
6954 * @since 1.16.5 (J) - 1.6.5 (WP)
6955 */
6956 public static function setSeasonsCache($enabled = null)
6957 {
6958 static $seasons_cache = null;
6959
6960 if ($enabled !== null) {
6961 // set value
6962 $seasons_cache = (bool)$enabled;
6963 }
6964
6965 return $seasons_cache;
6966 }
6967
6968 /**
6969 * Allows to preload and cache seasonal records for a list of rooms and dates.
6970 * Any subsequent preloading of season records for the same room(s) will return
6971 * the cached list. Should be used to preload a large window of data so that
6972 * subsequent calls will always return cached value.
6973 *
6974 * @param array $rooms List of involved room IDs.
6975 * @param int|bool $from Unix timestamp for start date or false to unset cache.
6976 * @param int $to Unix timestamp for end date.
6977 *
6978 * @return ?array
6979 *
6980 * @since 1.17.2 (J) - 1.7.2 (WP)
6981 */
6982 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
6983 {
6984 static $preloaded_records = [];
6985
6986 if (!$rooms) {
6987 // no cached signature
6988 return [];
6989 }
6990
6991 $signature = md5(implode(',', $rooms));
6992
6993 if ($from === false) {
6994 // unset cached records
6995 unset($preloaded_records[$signature]);
6996
6997 return;
6998 }
6999
7000 if ($from !== null && $to !== null) {
7001 // setter args signature for preloading records
7002 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
7003
7004 return;
7005 }
7006
7007 // getter args signature for getting the preloaded and cached records
7008 return $preloaded_records[$signature] ?? [];
7009 }
7010
7011 /**
7012 * Fetches all season records affecting a range of date timestamps.
7013 * Useful to pre-cache season records in case of hundreds of thousands
7014 * of records, but it can use up several MBs of server's memory.
7015 *
7016 * @param int $from unix timestamp for start date.
7017 * @param int $to unix timestamp for end date.
7018 * @param array $rooms optional list of involved room IDs.
7019 *
7020 * @return array
7021 *
7022 * @since 1.16.10 (J) - 1.6.10 (WP)
7023 */
7024 public static function getDateSeasonRecords($from, $to, array $rooms = [])
7025 {
7026 /**
7027 * We allow external systems to preload season records and cache them.
7028 *
7029 * @since 1.17.2 (J) - 1.7.2 (WP)
7030 */
7031 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
7032 return $preloaded_records;
7033 }
7034
7035 /**
7036 * For a more accurate records caching, ensure we have a range of two dates at least.
7037 *
7038 * @since 1.17.2 (J) - 1.7.2 (WP)
7039 */
7040 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
7041 // add one day to the end timestamp
7042 $to = strtotime('+1 day', $to);
7043 }
7044
7045 $dbo = JFactory::getDbo();
7046
7047 $one = getdate($from);
7048 $two = getdate($to);
7049
7050 // leap years
7051 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7052 $isleap = true;
7053 } else {
7054 $isleap = false;
7055 }
7056
7057 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7058 $tomidnightone = intval($one['hours']) * 3600;
7059 $tomidnightone += intval($one['minutes']) * 60;
7060 $sfrom = $from - $baseone - $tomidnightone;
7061 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7062 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7063 $tomidnighttwo = intval($two['hours']) * 3600;
7064 $tomidnighttwo += intval($two['minutes']) * 60;
7065 $sto = $to - $basetwo - $tomidnighttwo;
7066
7067 // leap years, check what dates to manipulate
7068 if ($isleap) {
7069 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7070 if ($one[0] > $leapts) {
7071 /**
7072 * Timestamp must be greater than the leap-day of Feb 29th.
7073 * It used to be checked for >= $leapts.
7074 *
7075 * @since July 3rd 2019
7076 */
7077 $sfrom -= 86400;
7078 }
7079 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7080 // lower checkin date when in leap year but not for checkout
7081 $sto -= 86400;
7082 }
7083 }
7084
7085 // check for DST changes to adjust the query values to fetch
7086 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7087 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7088 // we are parsing the day when the DST changed (probably a Sunday)
7089 if (!date('I', $from)) {
7090 // DST was just turned off
7091 $sfrom -= 3600;
7092 }
7093 }
7094 }
7095
7096 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7097 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7098 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7099 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7100 ($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`)") .
7101 ($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`)") .
7102 ($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`)") .
7103 ($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`)") .
7104 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7105 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7106
7107 /**
7108 * Avoid issues when querying data for a whole year by fetching all records.
7109 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
7110 *
7111 * @since 1.17.3 (J) - 1.7.3 (WP)
7112 */
7113 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
7114 // fetch all records when targeting a whole year of data
7115 $q = $dbo->getQuery(true)
7116 ->select('*')
7117 ->from($dbo->qn('#__vikbooking_seasons'))
7118 ->order($dbo->qn('promo') . ' ASC');
7119 }
7120
7121 $dbo->setQuery($q);
7122 $seasons = $dbo->loadAssocList();
7123
7124 if ($rooms) {
7125 // filter records by affected room IDs
7126 $seasons = array_filter($seasons, function($s) use ($rooms) {
7127 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
7128 foreach ($rooms as $idroom) {
7129 if (in_array("-" . $idroom . "-", $allrooms)) {
7130 return true;
7131 }
7132 }
7133 return false;
7134 });
7135
7136 // reset array keys
7137 $seasons = array_values($seasons);
7138 }
7139
7140 return $seasons;
7141 }
7142
7143 /**
7144 * Fetches all season records affecting week days with no dates.
7145 * Useful to avoid duplicate queries and cache/preload the records.
7146 *
7147 * @return array
7148 *
7149 * @since 1.18.5 (J) - 1.8.5 (WP)
7150 */
7151 public static function getWdaySeasonRecords()
7152 {
7153 static $cached_wdayseasons = null;
7154
7155 if (!is_null($cached_wdayseasons)) {
7156 return $cached_wdayseasons;
7157 }
7158
7159 $dbo = JFactory::getDbo();
7160
7161 $dbo->setQuery("SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));");
7162 $cached_wdayseasons = $dbo->loadAssocList();
7163
7164 return $cached_wdayseasons;
7165 }
7166
7167 /**
7168 * Applies the seasonal rates over a list of room rate records.
7169 *
7170 * @param array $arr list of room rate records.
7171 * @param int $from unix timestamp for start date.
7172 * @param int $to unix timestamp for end date.
7173 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7174 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7175 *
7176 * @return array list of manipulated room rate records.
7177 *
7178 * @since 1.18.0 (J) - 1.8.0 (WP) added support to records caching and preloading with args $seasons_dates and $seasons_wdays.
7179 * @since 1.18.5 (J) - 1.8.5 (WP) argument $seasons_wdays is nullable to allow preloading an empty list of records and save queries.
7180 */
7181 public static function applySeasonalPrices(array $arr, $from, $to, array $seasons_dates = [], ?array $seasons_wdays = null)
7182 {
7183 static $cached_seasons = [];
7184
7185 $cache_signature_wdays = '';
7186 if (self::setSeasonsCache() !== false && !$seasons_wdays) {
7187 // enable week-day season records caching
7188 $cache_signature_wdays = "wdays";
7189 }
7190
7191 $dbo = JFactory::getDbo();
7192 $vbo_tn = self::getTranslator();
7193
7194 $roomschange = [];
7195 $one = getdate($from);
7196
7197 // leap years
7198 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7199 $isleap = true;
7200 } else {
7201 $isleap = false;
7202 }
7203
7204 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7205 $tomidnightone = intval($one['hours']) * 3600;
7206 $tomidnightone += intval($one['minutes']) * 60;
7207 $sfrom = $from - $baseone - $tomidnightone;
7208 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7209 $two = getdate($to);
7210 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7211 $tomidnighttwo = intval($two['hours']) * 3600;
7212 $tomidnighttwo += intval($two['minutes']) * 60;
7213 $sto = $to - $basetwo - $tomidnighttwo;
7214
7215 // leap years, check what dates to manipulate
7216 if ($isleap) {
7217 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7218 if ($one[0] > $leapts) {
7219 /**
7220 * Timestamp must be greater than the leap-day of Feb 29th.
7221 * It used to be checked for >= $leapts.
7222 *
7223 * @since July 3rd 2019
7224 */
7225 $sfrom -= 86400;
7226 }
7227 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7228 // lower checkin date when in leap year but not for checkout
7229 $sto -= 86400;
7230 }
7231 }
7232
7233 // check for DST changes to adjust the query values to fetch
7234 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7235 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7236 // we are parsing the day when the DST changed (probably a Sunday)
7237 if (!date('I', $from)) {
7238 // DST was just turned off
7239 $sfrom -= 3600;
7240 }
7241 }
7242 }
7243
7244 // count nights requested
7245 $booking_nights = 1;
7246 foreach ($arr as $k => $a) {
7247 if (!isset($a[0])) {
7248 continue;
7249 }
7250 if (isset($a[0]['booking_nights'])) {
7251 // this value may be set when displaying pricing calendars
7252 $booking_nights = $a[0]['booking_nights'];
7253 break;
7254 }
7255 if (isset($a[0]['days'])) {
7256 $booking_nights = $a[0]['days'];
7257 break;
7258 }
7259 }
7260
7261 /**
7262 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7263 *
7264 * @since 1.16.4 (J) - 1.6.4 (WP)
7265 */
7266 $skip_promo_ids = self::registerPromotionIds();
7267
7268 $totseasons = 0;
7269 if (!$seasons_dates) {
7270 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7271 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7272 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7273 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7274 ($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`)") .
7275 ($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`)") .
7276 ($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`)") .
7277 ($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`)") .
7278 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7279 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7280
7281 // get the season records by running the query
7282 $dbo->setQuery($q);
7283 $seasons = $dbo->loadAssocList();
7284
7285 // count total seasons
7286 $totseasons = $seasons ? count($seasons) : 0;
7287 }
7288
7289 if ($totseasons > 0 || $seasons_dates) {
7290 if ($totseasons > 0) {
7291 $seasons = $seasons;
7292 } else {
7293 $seasons = $seasons_dates;
7294 }
7295 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7296 $applyseasons = false;
7297 $mem = [];
7298 foreach ($arr as $k => $a) {
7299 $mem[$k]['daysused'] = 0;
7300 $mem[$k]['sum'] = [];
7301 $mem[$k]['spids'] = [];
7302 /**
7303 * The keys below are all needed to apply the promotions on the room's final cost.
7304 *
7305 * @since 1.13.5
7306 */
7307 $mem[$k]['diffs'] = [];
7308 $mem[$k]['trans_keys'] = [];
7309 $mem[$k]['trans_factors'] = [];
7310 $mem[$k]['trans_affdays'] = [];
7311 }
7312 foreach ($seasons as $s) {
7313 // check if this is a promotion registered for skipping
7314 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7315 continue;
7316 }
7317
7318 // Special Price tied to the year
7319 if (!empty($s['year']) && $s['year'] > 0) {
7320 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7321 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7322 //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
7323 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7324 continue;
7325 }
7326 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7327 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7328 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7329 if ($calc_ends < ($from - $tomidnightone)) {
7330 continue;
7331 }
7332 }
7333 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7334 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7335 if ($s['from'] > $s['to']) {
7336 continue;
7337 }
7338 if (($basetwo + $s['from'] + 86399) > $to) {
7339 /**
7340 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7341 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7342 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7343 *
7344 * @since 1.14.3 (J) - 1.4.3 (WP)
7345 */
7346 continue;
7347 }
7348 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7349 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7350 /**
7351 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7352 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7353 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7354 *
7355 * @since 1.14.3 (J) - 1.4.3 (WP)
7356 */
7357 continue;
7358 }
7359 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7360 // 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)
7361 if (($baseone + $s['from']) > $to) {
7362 continue;
7363 }
7364 }
7365 }
7366 //
7367 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7368 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7369 $inits = $baseone + $s['from'];
7370 if ($s['from'] < $s['to']) {
7371 $ends = $basetwo + $s['to'];
7372 // check if the inits must be set to the year after
7373 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7374 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7375 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7376 $inits = $tmpbase + $s['from'];
7377 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7378 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7379 $ends = $baseone + $s['to'];
7380 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7381 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7382 $ends = $baseone + $s['to'];
7383 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7384 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7385 $inits = $basetwo + $s['from'];
7386 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7387 // booking dates across two years for a season tied to a specific year
7388 if ($one['year'] == $s['year']) {
7389 $inits = $baseone + $s['from'];
7390 } else {
7391 $inits = $basetwo + $s['from'];
7392 }
7393 if ($two['year'] == $s['year']) {
7394 $ends = $basetwo + $s['to'];
7395 } else {
7396 $ends = $baseone + $s['to'];
7397 }
7398 }
7399 } else {
7400 // between 2 years
7401 if ($baseone < $basetwo) {
7402 // ex. 29/12/2012 - 14/01/2013
7403 $ends = $basetwo + $s['to'];
7404 } else {
7405 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7406 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7407 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7408 $ends = $tmpbase + $s['to'];
7409 } else {
7410 // ex. 03/01 - 09/01
7411 $ends = $basetwo + $s['to'];
7412 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7413 $inits = $tmpbase + $s['from'];
7414 }
7415 }
7416 }
7417 // leap years
7418 if ($isleap == true) {
7419 $infoseason = getdate($inits);
7420 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7421 // leap years, check what dates to manipulate
7422 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7423 /**
7424 * Timestamp must be greater than the leap-day of Feb 29th.
7425 * It used to be checked for >= $leapts.
7426 *
7427 * @since July 3rd 2019
7428 */
7429 $inits += 86400;
7430 if ($s['from'] < $s['to']) {
7431 // increase season end date only if still on the same leap year
7432 $ends += 86400;
7433 }
7434 }
7435 }
7436
7437 // promotions
7438 $promotion = [];
7439 if (($s['promo'] ?? 0) == 1) {
7440 $daysadv = (($inits - time()) / 86400);
7441 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7442 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7443 continue;
7444 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7445 $secstocheckin = ($from - time());
7446 if ($s['promolastmin'] < $secstocheckin) {
7447 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7448 continue;
7449 }
7450 }
7451 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7452 /**
7453 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7454 *
7455 * @since 1.13.5
7456 */
7457 continue;
7458 }
7459 $promotion['todaydaysadv'] = $daysadv;
7460 $promotion['promodaysadv'] = $s['promodaysadv'];
7461 $promotion['promotxt'] = $s['promotxt'];
7462 }
7463
7464 // occupancy override
7465 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7466
7467 // week days
7468 $filterwdays = !empty($s['wdays']) ? true : false;
7469 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7470 if (is_array($wdays) && $wdays) {
7471 foreach ($wdays as $kw => $wd) {
7472 if (strlen($wd) == 0) {
7473 unset($wdays[$kw]);
7474 }
7475 }
7476 }
7477
7478 // checkin must be after the beginning of the season
7479 $checkininclok = true;
7480 if ($s['checkinincl'] == 1) {
7481 $checkininclok = false;
7482 if ($s['from'] < $s['to']) {
7483 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7484 $checkininclok = true;
7485 }
7486 } else {
7487 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7488 $checkininclok = true;
7489 }
7490 }
7491 }
7492 if ($checkininclok !== true) {
7493 continue;
7494 }
7495
7496 foreach ($arr as $k => $a) {
7497 // applied only to some types of price
7498 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7499 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7500 continue;
7501 }
7502 }
7503 // applied only to some room types
7504 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7505 continue;
7506 }
7507
7508 // count affected nights of stay
7509 $affdays = 0;
7510 $season_fromdayts = $fromdayts;
7511 $is_dst = date('I', $season_fromdayts);
7512 for ($i = 0; $i < $a[0]['days']; $i++) {
7513 $todayts = $season_fromdayts + ($i * 86400);
7514 $is_now_dst = date('I', $todayts);
7515 if ($is_dst != $is_now_dst) {
7516 // Daylight Saving Time has changed, check how
7517 if ((bool)$is_dst === true) {
7518 $todayts += 3600;
7519 $season_fromdayts += 3600;
7520 } else {
7521 $todayts -= 3600;
7522 $season_fromdayts -= 3600;
7523 }
7524 $is_dst = $is_now_dst;
7525 }
7526 if ($todayts >= $inits && $todayts <= $ends) {
7527 // week days
7528 if ($filterwdays == true) {
7529 $checkwday = getdate($todayts);
7530 if (in_array($checkwday['wday'], $wdays)) {
7531 $affdays++;
7532 }
7533 } else {
7534 $affdays++;
7535 }
7536 //
7537 }
7538 }
7539
7540 if (!($affdays > 0)) {
7541 // no nights affected
7542 continue;
7543 }
7544
7545 // apply the rule
7546 $applyseasons = true;
7547 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7548
7549 // modification factor object
7550 $factor = new stdClass;
7551
7552 // calculate new price progressively
7553 if (intval($s['val_pcent']) == 2) {
7554 // percentage value
7555 $factor->pcent = 1;
7556 $pctval = $s['diffcost'];
7557 if (strlen((string)$s['losoverride'])) {
7558 // values overrides
7559 $arrvaloverrides = [];
7560 $valovrparts = explode('_', $s['losoverride']);
7561 foreach ($valovrparts as $valovr) {
7562 if (!empty($valovr)) {
7563 $ovrinfo = explode(':', $valovr);
7564 if (strstr($ovrinfo[0], '-i') != false) {
7565 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7566 if ((int)$ovrinfo[0] < $a[0]['days']) {
7567 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7568 }
7569 }
7570 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7571 }
7572 }
7573 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7574 $pctval = $arrvaloverrides[$a[0]['days']];
7575 }
7576 }
7577 if (intval($s['type']) == 1) {
7578 // charge
7579 $factor->type = '+';
7580 $cpercent = 100 + $pctval;
7581 } else {
7582 // discount
7583 $factor->type = '-';
7584 $cpercent = 100 - $pctval;
7585 }
7586 $factor->amount = $pctval;
7587 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7588 } else {
7589 // absolute value
7590 $factor->pcent = 0;
7591 $absval = $s['diffcost'];
7592 if (strlen((string)$s['losoverride'])) {
7593 // values overrides
7594 $arrvaloverrides = [];
7595 $valovrparts = explode('_', $s['losoverride']);
7596 foreach ($valovrparts as $valovr) {
7597 if (!empty($valovr)) {
7598 $ovrinfo = explode(':', $valovr);
7599 if (strstr($ovrinfo[0], '-i') != false) {
7600 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7601 if ((int)$ovrinfo[0] < $a[0]['days']) {
7602 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7603 }
7604 }
7605 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7606 }
7607 }
7608 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7609 $absval = $arrvaloverrides[$a[0]['days']];
7610 }
7611 }
7612 if (intval($s['type']) == 1) {
7613 // charge
7614 $factor->type = '+';
7615 $newprice = ($dailyprice + $absval) * $affdays;
7616 } else {
7617 // discount
7618 $factor->type = '-';
7619 $newprice = ($dailyprice - $absval) * $affdays;
7620 }
7621 $factor->amount = $absval;
7622 }
7623
7624 // apply rounding
7625 $factor->roundmode = $s['roundmode'] ?? null;
7626 if (!empty($s['roundmode'])) {
7627 $newprice = round($newprice, 0, constant($s['roundmode']));
7628 } else {
7629 $newprice = round($newprice, 2);
7630 }
7631
7632 // define the promotion (only if no value overrides set the amount to 0)
7633 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7634 /**
7635 * Include the discount information (if any). The cost re-calculated may not be
7636 * precise if multiple special prices were applied over the same dates.
7637 *
7638 * @since 1.13
7639 */
7640 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7641 $promotion['discount'] = [
7642 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7643 'pcent' => (int)($s['val_pcent'] == 2),
7644 ];
7645 }
7646 //
7647 $mem[$k]['promotion'] = $promotion;
7648 }
7649
7650 // define the occupancy override
7651 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7652 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7653 }
7654
7655 // push special price ID
7656 if (!in_array($s['id'], $mem[$k]['spids'])) {
7657 array_push($mem[$k]['spids'], $s['id']);
7658 }
7659
7660 // push difference generated only if to be applied progressively
7661 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7662 /**
7663 * Push the difference generated by this special price for later transliteration of final price,
7664 * only if the special price is calculated progressively and not on the final price.
7665 *
7666 * @since 1.13.5
7667 */
7668 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7669 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7670 /**
7671 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7672 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7673 * all differences applied by the previous special pricing rules.
7674 *
7675 * @since 1.13.5
7676 */
7677 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7678 array_push($mem[$k]['trans_factors'], $factor);
7679 array_push($mem[$k]['trans_affdays'], $affdays);
7680 }
7681
7682 // push values in memory array
7683 array_push($mem[$k]['sum'], $newprice);
7684 $mem[$k]['daysused'] += $affdays;
7685 array_push($roomschange, $a[0]['idroom']);
7686 }
7687 }
7688 if ($applyseasons) {
7689 foreach ($mem as $k => $v) {
7690 if ($v['daysused'] > 0 && $v['sum']) {
7691 $newprice = 0;
7692 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7693 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7694 $addrest = $restdays * $dailyprice;
7695 $newprice += $addrest;
7696
7697 // calculate new final cost
7698 $redo_rounding = null;
7699 foreach ($v['sum'] as $sum_index => $add) {
7700 /**
7701 * The application of the various special pricing rules is made in a progressive and cumulative way
7702 * by always starting from the room base cost or its average daily cost. However, promotions may need
7703 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7704 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7705 * onto the differences generated by all the regular and progressively applied special pricing rules.
7706 *
7707 * @since 1.13.5
7708 */
7709 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7710 /**
7711 * This progressive price difference must be applied on the room final cost, so we need to
7712 * apply the transliteration over the other differences applied by other special prices.
7713 */
7714 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7715 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7716 // this is the % promotion we are looking for applying it on the final cost
7717 $factor = $v['trans_factors'][$transliterate_key];
7718 if (is_object($factor) && $factor->pcent) {
7719 $final_factor = 0;
7720 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7721 $final_factor += $prog_diff * $factor->amount / 100;
7722 }
7723 // update rounding
7724 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7725
7726 /**
7727 * Leverage the final factor depending on how many nights this affected.
7728 *
7729 * @since 1.15.0 (J) - 1.5.0 (WP)
7730 */
7731 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7732 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7733 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7734 }
7735
7736 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7737 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7738 }
7739 }
7740 }
7741
7742 // apply new price progressively
7743 $newprice += $add;
7744 }
7745
7746 // apply rounding from factor
7747 if (!empty($redo_rounding)) {
7748 $newprice = round($newprice, 0, constant($redo_rounding));
7749 }
7750
7751 // set promotion (if any)
7752 if (isset($v['promotion'])) {
7753 $arr[$k][0]['promotion'] = $v['promotion'];
7754 }
7755
7756 // set occupancy overrides (if any)
7757 if (isset($v['occupancy_ovr'])) {
7758 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7759 }
7760
7761 // set new final cost and update nights affected
7762 $arr[$k][0]['cost'] = $newprice;
7763 $arr[$k][0]['affdays'] = $v['daysused'];
7764 if (array_key_exists('spids', $v) && $v['spids']) {
7765 $arr[$k][0]['spids'] = $v['spids'];
7766 }
7767 }
7768 }
7769 }
7770 }
7771
7772 // week days with no season
7773 $roomschange = array_unique($roomschange);
7774 $totspecials = 0;
7775 if (!is_array($seasons_wdays)) {
7776 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7777 // avoid making a query
7778 $specials = $cached_seasons[$cache_signature_wdays];
7779 } else {
7780 // get the week-days-with-no-season records by running the query
7781 $specials = self::getWdaySeasonRecords();
7782 }
7783
7784 // count records
7785 $totspecials = $specials ? count($specials) : 0;
7786
7787 if ($cache_signature_wdays) {
7788 // cache week-days-with-no-season records
7789 $cached_seasons[$cache_signature_wdays] = $specials;
7790 }
7791 }
7792 if ($totspecials > 0 || $seasons_wdays) {
7793 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7794 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7795 $applyseasons = false;
7796 /**
7797 * We no longer unset the previous memory of the seasons with dates filters
7798 * because we need the responses to be merged. We do it only if not set.
7799 * We only keep the property 'spids' but the others should be unset.
7800 *
7801 * @since 1.11
7802 */
7803 if (!isset($mem)) {
7804 $mem = [];
7805 foreach ($arr as $k => $a) {
7806 $mem[$k]['daysused'] = 0;
7807 $mem[$k]['sum'] = [];
7808 $mem[$k]['spids'] = [];
7809 }
7810 } else {
7811 foreach ($arr as $k => $a) {
7812 $mem[$k]['daysused'] = 0;
7813 $mem[$k]['sum'] = [];
7814 }
7815 }
7816 //
7817 foreach ($specials as $s) {
7818 // check if this is a promotion registered for skipping
7819 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7820 continue;
7821 }
7822
7823 // Special Price tied to the year
7824 if (!empty($s['year']) && $s['year'] > 0) {
7825 if ($one['year'] != $s['year']) {
7826 continue;
7827 }
7828 }
7829
7830 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7831 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7832 // week days
7833 $filterwdays = !empty($s['wdays']) ? true : false;
7834 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7835 if (is_array($wdays) && $wdays) {
7836 foreach ($wdays as $kw => $wd) {
7837 if (strlen($wd) == 0) {
7838 unset($wdays[$kw]);
7839 }
7840 }
7841 }
7842
7843 foreach ($arr as $k => $a) {
7844 // only rooms with no price modifications from seasons
7845
7846 // applied only to some types of price
7847 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7848 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7849 continue;
7850 }
7851 }
7852
7853 /**
7854 * We should not exclude the rooms that already had a modification of the price through a season
7855 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7856 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7857 *
7858 * @since 1.11
7859 */
7860 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7861 continue;
7862 }
7863
7864 $affdays = 0;
7865 $season_fromdayts = $fromdayts;
7866 $is_dst = date('I', $season_fromdayts);
7867 for ($i = 0; $i < $a[0]['days']; $i++) {
7868 $todayts = $season_fromdayts + ($i * 86400);
7869 $is_now_dst = date('I', $todayts);
7870 if ($is_dst != $is_now_dst) {
7871 // Daylight Saving Time has changed, check how
7872 if ((bool)$is_dst === true) {
7873 $todayts += 3600;
7874 $season_fromdayts += 3600;
7875 } else {
7876 $todayts -= 3600;
7877 $season_fromdayts -= 3600;
7878 }
7879 $is_dst = $is_now_dst;
7880 }
7881 // week days
7882 if ($filterwdays == true) {
7883 $checkwday = getdate($todayts);
7884 if (in_array($checkwday['wday'], $wdays)) {
7885 $affdays++;
7886 }
7887 }
7888 }
7889 if (!($affdays > 0)) {
7890 // no nights affected
7891 continue;
7892 }
7893
7894 // apply the rule
7895 $applyseasons = true;
7896 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7897
7898 if (intval($s['val_pcent']) == 2) {
7899 // percentage value
7900 $pctval = $s['diffcost'];
7901 if (strlen((string)$s['losoverride'])) {
7902 // values overrides
7903 $arrvaloverrides = [];
7904 $valovrparts = explode('_', $s['losoverride']);
7905 foreach ($valovrparts as $valovr) {
7906 if (!empty($valovr)) {
7907 $ovrinfo = explode(':', $valovr);
7908 if (strstr($ovrinfo[0], '-i') != false) {
7909 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7910 if ((int)$ovrinfo[0] < $a[0]['days']) {
7911 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7912 }
7913 }
7914 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7915 }
7916 }
7917 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7918 $pctval = $arrvaloverrides[$a[0]['days']];
7919 }
7920 }
7921 if (intval($s['type']) == 1) {
7922 // charge
7923 $cpercent = 100 + $pctval;
7924 } else {
7925 // discount
7926 $cpercent = 100 - $pctval;
7927 }
7928 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7929 } else {
7930 // absolute value
7931 $absval = $s['diffcost'];
7932 if (strlen((string)$s['losoverride'])) {
7933 // values overrides
7934 $arrvaloverrides = [];
7935 $valovrparts = explode('_', $s['losoverride']);
7936 foreach ($valovrparts as $valovr) {
7937 if (!empty($valovr)) {
7938 $ovrinfo = explode(':', $valovr);
7939 if (strstr($ovrinfo[0], '-i') != false) {
7940 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7941 if ((int)$ovrinfo[0] < $a[0]['days']) {
7942 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7943 }
7944 }
7945 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7946 }
7947 }
7948 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7949 $absval = $arrvaloverrides[$a[0]['days']];
7950 }
7951 }
7952 if (intval($s['type']) == 1) {
7953 // charge
7954 $newprice = ($dailyprice + $absval) * $affdays;
7955 } else {
7956 // discount
7957 $newprice = ($dailyprice - $absval) * $affdays;
7958 }
7959 }
7960
7961 // apply rounding
7962 if (!empty($s['roundmode'])) {
7963 $newprice = round($newprice, 0, constant($s['roundmode']));
7964 } else {
7965 $newprice = round($newprice, 2);
7966 }
7967
7968 // push values in memory array
7969 if (!in_array($s['id'], $mem[$k]['spids'])) {
7970 $mem[$k]['spids'][] = $s['id'];
7971 }
7972 $mem[$k]['sum'][] = $newprice;
7973 $mem[$k]['daysused'] += $affdays;
7974 }
7975 }
7976 if ($applyseasons) {
7977 foreach ($mem as $k => $v) {
7978 if ($v['daysused'] > 0 && $v['sum']) {
7979 $newprice = 0;
7980 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7981 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7982 $addrest = $restdays * $dailyprice;
7983 $newprice += $addrest;
7984 foreach ($v['sum'] as $add) {
7985 $newprice += $add;
7986 }
7987 $arr[$k][0]['cost'] = $newprice;
7988 $arr[$k][0]['affdays'] = $v['daysused'];
7989 if (array_key_exists('spids', $v) && $v['spids']) {
7990 $arr[$k][0]['spids'] = $v['spids'];
7991 }
7992 }
7993 }
7994 }
7995 }
7996 // end week days with no season
7997
7998 return $arr;
7999 }
8000
8001 /**
8002 * Applies the seasonal rates over room rate records.
8003 *
8004 * @param array $arr array of tariffs taken from the DB
8005 * @param int $from start timestamp
8006 * @param int $to end timestamp
8007 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
8008 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
8009 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
8010 *
8011 * @return array
8012 *
8013 * @since 1.18.5 (J) - 1.8.5 (WP) argument $seasons_wdays is nullable to allow preloading an empty list of records and save queries.
8014 */
8015 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], ?array $seasons_wdays = null)
8016 {
8017 static $cached_seasons = [];
8018
8019 $cache_signature = '';
8020 $cache_signature_wdays = '';
8021 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
8022 // enable season records caching
8023 $cache_signature = "{$from}_{$to}";
8024 }
8025 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
8026 // enable week-day season records caching
8027 $cache_signature_wdays = "wdays";
8028 }
8029
8030 $dbo = JFactory::getDbo();
8031 $vbo_tn = self::getTranslator();
8032
8033 $roomschange = [];
8034 $one = getdate($from);
8035
8036 // leap years
8037 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
8038 $isleap = true;
8039 } else {
8040 $isleap = false;
8041 }
8042
8043 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
8044 $tomidnightone = intval($one['hours']) * 3600;
8045 $tomidnightone += intval($one['minutes']) * 60;
8046 $sfrom = $from - $baseone - $tomidnightone;
8047 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
8048 $two = getdate($to);
8049 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
8050 $tomidnighttwo = intval($two['hours']) * 3600;
8051 $tomidnighttwo += intval($two['minutes']) * 60;
8052 $sto = $to - $basetwo - $tomidnighttwo;
8053
8054 // leap years, check what dates to manipulate
8055 if ($isleap) {
8056 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
8057 if ($one[0] > $leapts) {
8058 /**
8059 * Timestamp must be greater than the leap-day of Feb 29th.
8060 * It used to be checked for >= $leapts.
8061 *
8062 * @since July 3rd 2019
8063 */
8064 $sfrom -= 86400;
8065 }
8066 if ($two[0] > $leapts && $one['year'] == $two['year']) {
8067 // lower checkin date when in leap year but not for checkout
8068 $sto -= 86400;
8069 }
8070 }
8071
8072 // check for DST changes to adjust the query values to fetch
8073 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
8074 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
8075 // we are parsing the day when the DST changed (probably a Sunday)
8076 if (!date('I', $from)) {
8077 // DST was just turned off
8078 $sfrom -= 3600;
8079 }
8080 }
8081 }
8082
8083 // count nights requested
8084 $booking_nights = 1;
8085 foreach ($arr as $k => $a) {
8086 if (isset($a['booking_nights'])) {
8087 // this value may be set when displaying pricing calendars
8088 $booking_nights = $a['booking_nights'];
8089 break;
8090 }
8091 if (isset($a['days'])) {
8092 $booking_nights = $a['days'];
8093 break;
8094 }
8095 }
8096
8097 /**
8098 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
8099 *
8100 * @since 1.16.4 (J) - 1.6.4 (WP)
8101 */
8102 $skip_promo_ids = self::registerPromotionIds();
8103
8104 $totseasons = 0;
8105 if (!$parsed_season && !$seasons_dates) {
8106 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
8107 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
8108 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
8109 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
8110 ($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`)") .
8111 ($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`)") .
8112 ($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`)") .
8113 ($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`)") .
8114 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
8115 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
8116
8117 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
8118 // avoid making a query
8119 $seasons = $cached_seasons[$cache_signature];
8120 } else {
8121 // get the season records by running the query
8122 $dbo->setQuery($q);
8123 $seasons = $dbo->loadAssocList();
8124 }
8125
8126 // count total seasons
8127 $totseasons = $seasons ? count($seasons) : 0;
8128
8129 if ($cache_signature) {
8130 // cache season records
8131 $cached_seasons[$cache_signature] = $seasons;
8132 }
8133 }
8134
8135 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
8136 if ($totseasons > 0) {
8137 $seasons = $seasons;
8138 } elseif ($parsed_season) {
8139 $seasons = [$parsed_season];
8140 } else {
8141 $seasons = $seasons_dates;
8142 }
8143 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
8144 $applyseasons = false;
8145 $mem = [];
8146 foreach ($arr as $k => $a) {
8147 $mem[$k]['daysused'] = 0;
8148 $mem[$k]['sum'] = [];
8149 $mem[$k]['spids'] = [];
8150 /**
8151 * The keys below are all needed to apply the promotions on the room's final cost.
8152 *
8153 * @since 1.13.5
8154 */
8155 $mem[$k]['diffs'] = [];
8156 $mem[$k]['trans_keys'] = [];
8157 $mem[$k]['trans_factors'] = [];
8158 $mem[$k]['trans_affdays'] = [];
8159 }
8160 $affdayslistless = [];
8161 foreach ($seasons as $s) {
8162 // 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
8163 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
8164 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
8165 continue;
8166 }
8167
8168 /**
8169 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
8170 * We need to skip such fake seasons as they do not need any parsing.
8171 *
8172 * @since 1.13
8173 */
8174 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
8175 continue;
8176 }
8177
8178 // check if this is a promotion registered for skipping
8179 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8180 continue;
8181 }
8182
8183 // Special Price tied to the year
8184 if (!empty($s['year']) && $s['year'] > 0) {
8185 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
8186 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
8187 //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
8188 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
8189 continue;
8190 }
8191 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
8192 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
8193 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
8194 if ($calc_ends < ($from - $tomidnightone)) {
8195 continue;
8196 }
8197 }
8198 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
8199 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
8200 if ($s['from'] > $s['to']) {
8201 continue;
8202 }
8203 if (($basetwo + $s['from'] + 86399) > $to) {
8204 /**
8205 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
8206 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
8207 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
8208 *
8209 * @since 1.14.3 (J) - 1.4.3 (WP)
8210 */
8211 continue;
8212 }
8213 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
8214 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
8215 /**
8216 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
8217 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
8218 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
8219 *
8220 * @since 1.14.3 (J) - 1.4.3 (WP)
8221 */
8222 continue;
8223 }
8224 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
8225 // 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)
8226 if (($baseone + $s['from']) > $to) {
8227 continue;
8228 }
8229 }
8230 }
8231 //
8232 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8233 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8234 $inits = $baseone + $s['from'];
8235 if ($s['from'] < $s['to']) {
8236 $ends = $basetwo + $s['to'];
8237 // check if the inits must be set to the year after
8238 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
8239 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
8240 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8241 $inits = $tmpbase + $s['from'];
8242 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8243 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
8244 $ends = $baseone + $s['to'];
8245 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8246 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
8247 $ends = $baseone + $s['to'];
8248 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
8249 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
8250 $inits = $basetwo + $s['from'];
8251 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
8252 // booking dates across two years for a season tied to a specific year
8253 if ($one['year'] == $s['year']) {
8254 $inits = $baseone + $s['from'];
8255 } else {
8256 $inits = $basetwo + $s['from'];
8257 }
8258 if ($two['year'] == $s['year']) {
8259 $ends = $basetwo + $s['to'];
8260 } else {
8261 $ends = $baseone + $s['to'];
8262 }
8263 }
8264 } else {
8265 // between 2 years
8266 if ($baseone < $basetwo) {
8267 // ex. 29/12/2012 - 14/01/2013
8268 $ends = $basetwo + $s['to'];
8269 } else {
8270 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
8271 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
8272 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8273 $ends = $tmpbase + $s['to'];
8274 } else {
8275 // ex. 03/01 - 09/01
8276 $ends = $basetwo + $s['to'];
8277 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
8278 $inits = $tmpbase + $s['from'];
8279 }
8280 }
8281 }
8282 // leap years
8283 if ($isleap == true) {
8284 $infoseason = getdate($inits);
8285 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
8286 // leap years, check what dates to manipulate
8287 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
8288 /**
8289 * Timestamp must be greater than the leap-day of Feb 29th.
8290 * It used to be checked for >= $leapts.
8291 *
8292 * @since July 3rd 2019
8293 */
8294 $inits += 86400;
8295 if ($s['from'] < $s['to']) {
8296 // increase season end date only if still on the same leap year
8297 $ends += 86400;
8298 }
8299 }
8300 }
8301
8302 // promotions
8303 $promotion = [];
8304 if (($s['promo'] ?? 0) == 1) {
8305 $daysadv = (($inits - time()) / 86400);
8306 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
8307 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
8308 continue;
8309 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
8310 $secstocheckin = ($from - time());
8311 if ($s['promolastmin'] < $secstocheckin) {
8312 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
8313 continue;
8314 }
8315 }
8316 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
8317 /**
8318 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
8319 *
8320 * @since 1.13.5
8321 */
8322 continue;
8323 }
8324 $promotion['todaydaysadv'] = $daysadv;
8325 $promotion['promodaysadv'] = $s['promodaysadv'];
8326 $promotion['promotxt'] = $s['promotxt'];
8327 }
8328
8329 // occupancy override
8330 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
8331
8332 // week days
8333 $filterwdays = !empty($s['wdays']) ? true : false;
8334 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8335 if (is_array($wdays) && $wdays) {
8336 foreach ($wdays as $kw => $wd) {
8337 if (strlen($wd) == 0) {
8338 unset($wdays[$kw]);
8339 }
8340 }
8341 }
8342
8343 // checkin must be after the beginning of the season
8344 $checkininclok = true;
8345 if ($s['checkinincl'] == 1) {
8346 $checkininclok = false;
8347 if ($s['from'] < $s['to']) {
8348 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
8349 $checkininclok = true;
8350 }
8351 } else {
8352 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
8353 $checkininclok = true;
8354 }
8355 }
8356 }
8357 if ($checkininclok !== true) {
8358 continue;
8359 }
8360
8361 foreach ($arr as $k => $a) {
8362 // applied only to some types of price
8363 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8364 // VikBooking 1.6: Price Calendar sets the idprice to -1
8365 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8366 continue;
8367 }
8368 }
8369 // applied only to some room types
8370 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8371 continue;
8372 }
8373
8374 if (isset($a['days']) && is_float($a['days'])) {
8375 // prevent any possible warning with array_key_exists()
8376 $a['days'] = (int)$a['days'];
8377 }
8378
8379 $affdays = 0;
8380 $season_fromdayts = $fromdayts;
8381 $is_dst = date('I', $season_fromdayts);
8382 for ($i = 0; $i < $a['days']; $i++) {
8383 $todayts = $season_fromdayts + ($i * 86400);
8384 $is_now_dst = date('I', $todayts);
8385 if ($is_dst != $is_now_dst) {
8386 // Daylight Saving Time has changed, check how
8387 if ((bool)$is_dst === true) {
8388 $todayts += 3600;
8389 $season_fromdayts += 3600;
8390 } else {
8391 $todayts -= 3600;
8392 $season_fromdayts -= 3600;
8393 }
8394 $is_dst = $is_now_dst;
8395 }
8396 if ($todayts >= $inits && $todayts <= $ends) {
8397 $checkwday = getdate($todayts);
8398 // week days
8399 if ($filterwdays == true) {
8400 if (in_array($checkwday['wday'], $wdays)) {
8401 if (!isset($arr[$k]['affdayslist'])) {
8402 $arr[$k]['affdayslist'] = [];
8403 }
8404 $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;
8405 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8406 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8407 $affdays++;
8408 }
8409 } else {
8410 if (!isset($arr[$k]['affdayslist'])) {
8411 $arr[$k]['affdayslist'] = [];
8412 }
8413 $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;
8414 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8415 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8416 $affdays++;
8417 }
8418 //
8419 }
8420 }
8421
8422 if (!($affdays > 0)) {
8423 // no nights affected
8424 continue;
8425 }
8426
8427 // apply the rule
8428 $applyseasons = true;
8429 $dailyprice = $a['cost'] / $a['days'];
8430
8431 // modification factor object
8432 $factor = new stdClass;
8433
8434 // calculate new price progressively
8435 if (intval($s['val_pcent']) == 2) {
8436 // percentage value
8437 $factor->pcent = 1;
8438 $pctval = $s['diffcost'];
8439 if (strlen((string)$s['losoverride'])) {
8440 // values overrides
8441 $arrvaloverrides = [];
8442 $valovrparts = explode('_', $s['losoverride']);
8443 foreach ($valovrparts as $valovr) {
8444 if (!empty($valovr)) {
8445 $ovrinfo = explode(':', $valovr);
8446 if (strstr($ovrinfo[0], '-i') != false) {
8447 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8448 if ((int)$ovrinfo[0] < $a['days']) {
8449 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8450 }
8451 }
8452 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8453 }
8454 }
8455 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8456 $pctval = $arrvaloverrides[$a['days']];
8457 }
8458 }
8459 if (intval($s['type']) == 1) {
8460 // charge
8461 $factor->type = '+';
8462 $cpercent = 100 + $pctval;
8463 } else {
8464 // discount
8465 $factor->type = '-';
8466 $cpercent = 100 - $pctval;
8467 }
8468 $factor->amount = $pctval;
8469 $dailysum = ($dailyprice * $cpercent / 100);
8470 $newprice = $dailysum * $affdays;
8471 } else {
8472 // absolute value
8473 $factor->pcent = 0;
8474 $absval = $s['diffcost'];
8475 if (strlen((string)$s['losoverride'])) {
8476 // values overrides
8477 $arrvaloverrides = [];
8478 $valovrparts = explode('_', $s['losoverride']);
8479 foreach ($valovrparts as $valovr) {
8480 if (!empty($valovr)) {
8481 $ovrinfo = explode(':', $valovr);
8482 if (strstr($ovrinfo[0], '-i') != false) {
8483 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8484 if ((int)$ovrinfo[0] < $a['days']) {
8485 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8486 }
8487 }
8488 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8489 }
8490 }
8491 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8492 $absval = $arrvaloverrides[$a['days']];
8493 }
8494 }
8495 if (intval($s['type']) == 1) {
8496 // charge
8497 $factor->type = '+';
8498 $dailysum = ($dailyprice + $absval);
8499 $newprice = $dailysum * $affdays;
8500 } else {
8501 // discount
8502 $factor->type = '-';
8503 $dailysum = ($dailyprice - $absval);
8504 $newprice = $dailysum * $affdays;
8505 }
8506 $factor->amount = $absval;
8507 }
8508
8509 // apply rounding
8510 $factor->roundmode = $s['roundmode'] ?? null;
8511 if (!empty($s['roundmode'])) {
8512 $newprice = round($newprice, 0, constant($s['roundmode']));
8513 } else {
8514 $newprice = round($newprice, 2);
8515 }
8516
8517 // define the promotion (only if no value overrides set the amount to 0)
8518 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8519 /**
8520 * Include the discount information (if any). The cost re-calculated may not be
8521 * precise if multiple special prices were applied over the same dates.
8522 *
8523 * @since 1.13
8524 */
8525 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8526 $promotion['discount'] = [
8527 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8528 'pcent' => (int)($s['val_pcent'] == 2),
8529 ];
8530 }
8531 //
8532 $mem[$k]['promotion'] = $promotion;
8533 }
8534
8535 // define the occupancy override
8536 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8537 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8538 }
8539
8540 // affected days list
8541 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8542 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8543 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8544 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8545 }
8546 }
8547 }
8548
8549 // push special price ID
8550 if (!in_array($s['id'], $mem[$k]['spids'])) {
8551 array_push($mem[$k]['spids'], $s['id']);
8552 }
8553
8554 // push difference generated only if to be applied progressively
8555 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8556 /**
8557 * Push the difference generated by this special price for later transliteration of final price,
8558 * only if the special price is calculated progressively and not on the final price.
8559 *
8560 * @since 1.13.5
8561 */
8562 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8563 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8564 /**
8565 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8566 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8567 * all differences applied by the previous special pricing rules.
8568 *
8569 * @since 1.13.5
8570 */
8571 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8572 array_push($mem[$k]['trans_factors'], $factor);
8573 array_push($mem[$k]['trans_affdays'], $affdays);
8574 }
8575
8576 // push values in memory array
8577 array_push($mem[$k]['sum'], $newprice);
8578 $mem[$k]['daysused'] += $affdays;
8579 array_push($roomschange, $a['idroom']);
8580 }
8581 }
8582 if ($applyseasons) {
8583 foreach ($mem as $k => $v) {
8584 if ($v['daysused'] > 0 && $v['sum']) {
8585 $newprice = 0;
8586 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8587 $restdays = $arr[$k]['days'] - $v['daysused'];
8588 $addrest = $restdays * $dailyprice;
8589 $newprice += $addrest;
8590
8591 // calculate new final cost
8592 $redo_rounding = null;
8593 foreach ($v['sum'] as $sum_index => $add) {
8594 /**
8595 * The application of the various special pricing rules is made in a progressive and cumulative way
8596 * by always starting from the room base cost or its average daily cost. However, promotions may need
8597 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8598 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8599 * onto the differences generated by all the regular and progressively applied special pricing rules.
8600 *
8601 * @since 1.13.5
8602 */
8603 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8604 /**
8605 * This progressive price difference must be applied on the room final cost, so we need to
8606 * apply the transliteration over the other differences applied by other special prices.
8607 */
8608 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8609 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8610 // this is the % promotion we are looking for applying it on the final cost
8611 $factor = $v['trans_factors'][$transliterate_key];
8612 if (is_object($factor) && $factor->pcent) {
8613 $final_factor = 0;
8614 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8615 $final_factor += $prog_diff * $factor->amount / 100;
8616 }
8617 // update rounding
8618 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8619
8620 /**
8621 * Leverage the final factor depending on how many nights this affected.
8622 *
8623 * @since 1.15.0 (J) - 1.5.0 (WP)
8624 */
8625 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8626 $avg_final_factor = $final_factor / $arr[$k]['days'];
8627 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8628 }
8629
8630 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8631 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8632 }
8633 }
8634 }
8635
8636 // apply new price progressively
8637 $newprice += $add;
8638 }
8639
8640 // apply rounding from factor
8641 if (!empty($redo_rounding)) {
8642 $newprice = round($newprice, 0, constant($redo_rounding));
8643 }
8644
8645 // set promotion (if any)
8646 if (isset($v['promotion'])) {
8647 $arr[$k]['promotion'] = $v['promotion'];
8648 }
8649
8650 // set occupancy overrides (if any)
8651 if (isset($v['occupancy_ovr'])) {
8652 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8653 }
8654
8655 // set new final cost and update nights affected
8656 $arr[$k]['cost'] = $newprice;
8657 $arr[$k]['affdays'] = $v['daysused'];
8658 if (array_key_exists('spids', $v) && $v['spids']) {
8659 $arr[$k]['spids'] = $v['spids'];
8660 }
8661 }
8662 }
8663 }
8664 }
8665
8666 // week days with no season
8667 $roomschange = array_unique($roomschange);
8668 $totspecials = 0;
8669 if (!is_array($seasons_wdays)) {
8670 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8671 // avoid making a query
8672 $specials = $cached_seasons[$cache_signature_wdays];
8673 } else {
8674 // get the week-days-with-no-season records by running the query
8675 $specials = self::getWdaySeasonRecords();
8676 }
8677
8678 // count records
8679 $totspecials = $specials ? count($specials) : 0;
8680
8681 if ($cache_signature_wdays) {
8682 // cache week-days-with-no-season records
8683 $cached_seasons[$cache_signature_wdays] = $specials;
8684 }
8685 }
8686 if ($totspecials > 0 || $seasons_wdays) {
8687 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8688 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8689 $applyseasons = false;
8690 /**
8691 * We no longer unset the previous memory of the seasons with dates filters
8692 * because we need the responses to be merged. We do it only if not set.
8693 * We only keep the property 'spids' but the others should be unset.
8694 *
8695 * @since 1.11
8696 */
8697 if (!isset($mem)) {
8698 $mem = [];
8699 foreach ($arr as $k => $a) {
8700 $mem[$k]['daysused'] = 0;
8701 $mem[$k]['sum'] = [];
8702 $mem[$k]['spids'] = [];
8703 }
8704 } else {
8705 foreach ($arr as $k => $a) {
8706 $mem[$k]['daysused'] = 0;
8707 $mem[$k]['sum'] = [];
8708 }
8709 }
8710 //
8711 foreach ($specials as $s) {
8712 // 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
8713 if (!empty($s['from']) || !empty($s['to'])) {
8714 continue;
8715 }
8716
8717 // check if this is a promotion registered for skipping
8718 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8719 continue;
8720 }
8721
8722 // Special Price tied to the year
8723 if (!empty($s['year']) && $s['year'] > 0) {
8724 if ($one['year'] != $s['year']) {
8725 continue;
8726 }
8727 }
8728
8729 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8730 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8731 // week days
8732 $filterwdays = !empty($s['wdays']) ? true : false;
8733 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8734 if (is_array($wdays) && $wdays) {
8735 foreach ($wdays as $kw => $wd) {
8736 if (strlen($wd) == 0) {
8737 unset($wdays[$kw]);
8738 }
8739 }
8740 }
8741
8742 foreach ($arr as $k => $a) {
8743 // only rooms with no price modifications from seasons
8744
8745 // applied only to some types of price
8746 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8747 // VikBooking 1.6: Price Calendar sets the idprice to -1
8748 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8749 continue;
8750 }
8751 }
8752
8753 /**
8754 * We should not exclude the rooms that already had a modification of the price through a season
8755 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8756 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8757 *
8758 * @since 1.11
8759 */
8760 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8761 continue;
8762 }
8763
8764 if (isset($a['days']) && is_float($a['days'])) {
8765 // prevent any possible warning with array_key_exists()
8766 $a['days'] = (int)$a['days'];
8767 }
8768
8769 $affdays = 0;
8770 $season_fromdayts = $fromdayts;
8771 $is_dst = date('I', $season_fromdayts);
8772 for ($i = 0; $i < $a['days']; $i++) {
8773 $todayts = $season_fromdayts + ($i * 86400);
8774 $is_now_dst = date('I', $todayts);
8775 if ($is_dst != $is_now_dst) {
8776 // Daylight Saving Time has changed, check how
8777 if ((bool)$is_dst === true) {
8778 $todayts += 3600;
8779 $season_fromdayts += 3600;
8780 } else {
8781 $todayts -= 3600;
8782 $season_fromdayts -= 3600;
8783 }
8784 $is_dst = $is_now_dst;
8785 }
8786 // week days
8787 if ($filterwdays == true) {
8788 $checkwday = getdate($todayts);
8789 if (in_array($checkwday['wday'], $wdays)) {
8790 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8791 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8792 $affdays++;
8793 }
8794 }
8795 //
8796 }
8797
8798 if (!($affdays > 0)) {
8799 // no nights affected
8800 continue;
8801 }
8802
8803 // apply the rule
8804 $applyseasons = true;
8805 $dailyprice = $a['cost'] / $a['days'];
8806
8807 if (intval($s['val_pcent']) == 2) {
8808 // percentage value
8809 $pctval = $s['diffcost'];
8810 if (strlen((string)$s['losoverride'])) {
8811 // values overrides
8812 $arrvaloverrides = [];
8813 $valovrparts = explode('_', $s['losoverride']);
8814 foreach ($valovrparts as $valovr) {
8815 if (!empty($valovr)) {
8816 $ovrinfo = explode(':', $valovr);
8817 if (strstr($ovrinfo[0], '-i') != false) {
8818 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8819 if ((int)$ovrinfo[0] < $a['days']) {
8820 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8821 }
8822 }
8823 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8824 }
8825 }
8826 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8827 $pctval = $arrvaloverrides[$a['days']];
8828 }
8829 }
8830 if (intval($s['type']) == 1) {
8831 // charge
8832 $cpercent = 100 + $pctval;
8833 } else {
8834 // discount
8835 $cpercent = 100 - $pctval;
8836 }
8837 $dailysum = ($dailyprice * $cpercent / 100);
8838 $newprice = $dailysum * $affdays;
8839 } else {
8840 // absolute value
8841 $absval = $s['diffcost'];
8842 if (strlen((string)$s['losoverride'])) {
8843 // values overrides
8844 $arrvaloverrides = [];
8845 $valovrparts = explode('_', $s['losoverride']);
8846 foreach ($valovrparts as $valovr) {
8847 if (!empty($valovr)) {
8848 $ovrinfo = explode(':', $valovr);
8849 if (strstr($ovrinfo[0], '-i') != false) {
8850 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8851 if ((int)$ovrinfo[0] < $a['days']) {
8852 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8853 }
8854 }
8855 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8856 }
8857 }
8858 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8859 $absval = $arrvaloverrides[$a['days']];
8860 }
8861 }
8862 if (intval($s['type']) == 1) {
8863 // charge
8864 $dailysum = ($dailyprice + $absval);
8865 $newprice = $dailysum * $affdays;
8866 } else {
8867 // discount
8868 $dailysum = ($dailyprice - $absval);
8869 $newprice = $dailysum * $affdays;
8870 }
8871 }
8872
8873 // apply rounding
8874 if (!empty($s['roundmode'])) {
8875 $newprice = round($newprice, 0, constant($s['roundmode']));
8876 } else {
8877 $newprice = round($newprice, 2);
8878 }
8879
8880 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8881 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8882 }
8883 if (!in_array($s['id'], $mem[$k]['spids'])) {
8884 $mem[$k]['spids'][] = $s['id'];
8885 }
8886 $mem[$k]['sum'][] = $newprice;
8887 $mem[$k]['daysused'] += $affdays;
8888 }
8889 }
8890 if ($applyseasons) {
8891 foreach ($mem as $k => $v) {
8892 if ($v['daysused'] > 0 && $v['sum']) {
8893 $newprice = 0;
8894 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8895 $restdays = $arr[$k]['days'] - $v['daysused'];
8896 $addrest = $restdays * $dailyprice;
8897 $newprice += $addrest;
8898 foreach ($v['sum'] as $add) {
8899 $newprice += $add;
8900 }
8901 $arr[$k]['cost'] = $newprice;
8902 $arr[$k]['affdays'] = $v['daysused'];
8903 if (array_key_exists('spids', $v) && $v['spids']) {
8904 $arr[$k]['spids'] = $v['spids'];
8905 }
8906 }
8907 }
8908 }
8909 }
8910 // end week days with no season
8911
8912 return $arr;
8913 }
8914
8915 public static function getRoomRplansClosingDates($idroom)
8916 {
8917 $dbo = JFactory::getDbo();
8918
8919 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8920 $dbo->setQuery($q);
8921 $price_records = $dbo->loadAssocList();
8922 if (!$price_records) {
8923 return [];
8924 }
8925
8926 $closingd = [];
8927 foreach ($price_records as $prec) {
8928 if (empty($prec['closingd'])) {
8929 continue;
8930 }
8931 $price_closing = json_decode($prec['closingd'], true);
8932 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8933 continue;
8934 }
8935 // check expired dates and clean up
8936 $today_midnight = mktime(0, 0, 0);
8937 $cleaned = false;
8938 foreach ($price_closing[$idroom] as $k => $v) {
8939 if (strtotime($v) < $today_midnight) {
8940 $cleaned = true;
8941 unset($price_closing[$idroom][$k]);
8942 }
8943 }
8944 if (!$price_closing[$idroom]) {
8945 unset($price_closing[$idroom]);
8946 } elseif ($cleaned === true) {
8947 // reset array keys for smaller JSON size
8948 $price_closing[$idroom] = array_values($price_closing[$idroom]);
8949 }
8950 if ($cleaned === true) {
8951 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
8952 $dbo->setQuery($q);
8953 $dbo->execute();
8954 }
8955 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8956 continue;
8957 }
8958 $closingd[$prec['id']] = $price_closing[$idroom];
8959 }
8960
8961 return $closingd;
8962 }
8963
8964 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
8965 {
8966 $dbo = JFactory::getDbo();
8967
8968 $closingd = [];
8969
8970 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8971 $dbo->setQuery($q);
8972 $price_records = $dbo->loadAssocList();
8973 if ($price_records && $roomids) {
8974 $info_start = getdate($checkints);
8975 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
8976 $all_nights = [];
8977 for ($i = 0; $i < (int)$numnights; $i++) {
8978 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
8979 $all_nights[] = date('Y-m-d', $next_midnight);
8980 }
8981 foreach ($price_records as $prec) {
8982 if (empty($prec['closingd'])) {
8983 continue;
8984 }
8985 $price_closing = json_decode($prec['closingd'], true);
8986 if (!$price_closing) {
8987 continue;
8988 }
8989 foreach ($price_closing as $idroom => $rclosedd) {
8990 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
8991 continue;
8992 }
8993 if (!array_key_exists($idroom, $closingd)) {
8994 $closingd[$idroom] = [];
8995 }
8996 foreach ($all_nights as $night) {
8997 if (in_array($night, $rclosedd)) {
8998 if (array_key_exists($prec['id'], $closingd[$idroom])) {
8999 $closingd[$idroom][$prec['id']][] = $night;
9000 } else {
9001 $closingd[$idroom][$prec['id']] = [$night];
9002 }
9003 }
9004 }
9005 }
9006 }
9007 }
9008
9009 return $closingd;
9010 }
9011
9012 /**
9013 * Tells whether at least one payment method must be selected to check-out.
9014 *
9015 * @param array $rooms_involved optional list of room IDs being booked.
9016 *
9017 * @return bool true if at least one payment method is applicable.
9018 *
9019 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
9020 */
9021 public static function areTherePayments($rooms_involved = [])
9022 {
9023 $dbo = JFactory::getDbo();
9024
9025 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
9026 $dbo->setQuery($q);
9027 $dbo->execute();
9028
9029 if (!$dbo->getNumRows()) {
9030 // no published payment options found
9031 return false;
9032 }
9033
9034 $pay_options = $dbo->loadAssocList();
9035
9036 if (!count($rooms_involved)) {
9037 // if no filters given, return true
9038 return true;
9039 }
9040
9041 // validate room associations
9042 foreach ($pay_options as $pay_k => $pay_v) {
9043 if (empty($pay_v['idrooms'])) {
9044 continue;
9045 }
9046 $rooms_supported = json_decode($pay_v['idrooms']);
9047 if (!is_array($rooms_supported) || !count($rooms_supported)) {
9048 continue;
9049 }
9050 // make sure all rooms being booked are supported
9051 foreach ($rooms_involved as $room_id_booked) {
9052 if (!in_array($room_id_booked, $rooms_supported)) {
9053 // we need to unset this payment option as soon as we find one non-matching room ID
9054 unset($pay_options[$pay_k]);
9055 break;
9056 }
9057 }
9058 }
9059
9060 return (bool)count($pay_options);
9061 }
9062
9063 public static function getPayment($idp, $vbo_tn = null)
9064 {
9065 if (empty($idp)) {
9066 return false;
9067 }
9068
9069 if (strstr((string)$idp, '=') !== false) {
9070 $parts = explode('=', $idp);
9071 $idp = $parts[0];
9072 }
9073
9074 $dbo = JFactory::getDbo();
9075
9076 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
9077 $dbo->setQuery($q);
9078 $payment = $dbo->loadAssocList();
9079 if (!$payment) {
9080 return false;
9081 }
9082
9083 if (is_object($vbo_tn)) {
9084 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
9085 }
9086
9087 return $payment[0];
9088 }
9089
9090 public static function getCronKey()
9091 {
9092 return VBOFactory::getConfig()->get('cronkey', '');
9093 }
9094
9095 /**
9096 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
9097 *
9098 * @return int
9099 *
9100 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
9101 */
9102 public static function getNextInvoiceNumber()
9103 {
9104 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
9105 }
9106
9107 public static function getInvoiceNumberSuffix()
9108 {
9109 return VBOFactory::getConfig()->get('invoicesuffix', '');
9110 }
9111
9112 public static function getInvoiceCompanyInfo()
9113 {
9114 return VBOFactory::getConfig()->get('invcompanyinfo', '');
9115 }
9116
9117 /**
9118 * Gets the number for the next booking receipt generation,
9119 * updates the last receipt number used for the later iterations,
9120 * stores a new receipt record to keep track of the receipts issued.
9121 *
9122 * @param int $bid the Booking ID for which we are/want to generating the receipt.
9123 * @param [int] $newnum the last number used for generating the receipt.
9124 *
9125 * @return int
9126 */
9127 public static function getNextReceiptNumber ($bid, $newnum = false) {
9128 $dbo = JFactory::getDbo();
9129 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
9130 $dbo->setQuery($q);
9131 $dbo->execute();
9132 if ($dbo->getNumRows() > 0) {
9133 $s = $dbo->loadResult();
9134 //check if this booking has already a receipt, and return that number
9135 $prev_receipt = array();
9136 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
9137 $dbo->setQuery($q);
9138 $dbo->execute();
9139 if ($dbo->getNumRows() > 0) {
9140 $prev_receipt = $dbo->loadAssoc();
9141 }
9142 //update value (receipt generated)
9143 if ($newnum !== false && $newnum > 0) {
9144 $s = (int)$newnum;
9145 if (!(count($prev_receipt) > 0)) {
9146 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
9147 $dbo->setQuery($q);
9148 $dbo->execute();
9149 //insert the new receipt record
9150 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
9151 $dbo->setQuery($q);
9152 $dbo->execute();
9153 } else {
9154 //update receipt record
9155 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
9156 $dbo->setQuery($q);
9157 $dbo->execute();
9158 }
9159 }
9160 //
9161 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
9162 }
9163 //first execution of the method should create the configuration record
9164 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
9165 $dbo->setQuery($q);
9166 $dbo->execute();
9167 return 1;
9168 }
9169
9170 public static function getReceiptNotes ($newnotes = false) {
9171 $dbo = JFactory::getDbo();
9172 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
9173 $dbo->setQuery($q);
9174 $dbo->execute();
9175 if ($dbo->getNumRows() > 0) {
9176 $s = $dbo->loadResult();
9177 //update value
9178 if ($newnotes !== false) {
9179 $s = $newnotes;
9180 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
9181 $dbo->setQuery($q);
9182 $dbo->execute();
9183 }
9184 //
9185 return $s;
9186 }
9187 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
9188 $dbo->setQuery($q);
9189 $dbo->execute();
9190 return "";
9191 }
9192
9193 public static function loadColorTagsRules()
9194 {
9195 $color_tag_rules = [
9196 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
9197 'pend1' => 'VBWAITINGFORPAYMENT',
9198 'conf1' => 'VBDBTEXTROOMCLOSED',
9199 'ovbook' => 'VBO_BTYPE_OVERBOOKING',
9200 'conf2' => 'VBOCOLORTAGRULECONFTWO',
9201 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
9202 'inv1' => 'VBOCOLORTAGRULEINVONE',
9203 'rcp1' => 'VBOCOLORTAGRULERCPONE',
9204 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
9205 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
9206 'inv2' => 'VBOCOLORTAGRULEINVTWO',
9207 ];
9208
9209 /**
9210 * Trigger event to let third-party plugins override the default booking color tag rules.
9211 *
9212 * @since 1.16.4 (J) - 1.6.4 (WP)
9213 */
9214 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
9215
9216 return $color_tag_rules;
9217 }
9218
9219 public static function loadDefaultColorTags()
9220 {
9221 $default_color_tags = [
9222 [
9223 'color' => '#9b9b9b',
9224 'name' => 'VBWAITINGFORPAYMENT',
9225 'rule' => 'pend1',
9226 ],
9227 [
9228 'color' => '#333333',
9229 'name' => 'VBDBTEXTROOMCLOSED',
9230 'rule' => 'conf1',
9231 ],
9232 [
9233 'color' => '#f04848',
9234 'name' => 'VBO_BTYPE_OVERBOOKING',
9235 'rule' => 'ovbook',
9236 ],
9237 [
9238 'color' => '#ff8606',
9239 'name' => 'VBOCOLORTAGRULECONFTWO',
9240 'rule' => 'conf2',
9241 ],
9242 [
9243 'color' => '#0418c9',
9244 'name' => 'VBOCOLORTAGRULECONFTHREE',
9245 'rule' => 'conf3',
9246 ],
9247 [
9248 'color' => '#bed953',
9249 'name' => 'VBOCOLORTAGRULEINVONE',
9250 'rule' => 'inv1',
9251 ],
9252 [
9253 'color' => '#67f5b5',
9254 'name' => 'VBOCOLORTAGRULERCPONE',
9255 'rule' => 'rcp1',
9256 ],
9257 [
9258 'color' => '#04d2c2',
9259 'name' => 'VBOCOLORTAGRULECONFFOUR',
9260 'rule' => 'conf4',
9261 ],
9262 [
9263 'color' => '#00b316',
9264 'name' => 'VBOCOLORTAGRULECONFFIVE',
9265 'rule' => 'conf5',
9266 ],
9267 [
9268 'color' => '#00f323',
9269 'name' => 'VBOCOLORTAGRULEINVTWO',
9270 'rule' => 'inv2',
9271 ],
9272 ];
9273
9274 /**
9275 * Trigger event to let third-party plugins override the default color tags.
9276 *
9277 * @since 1.16.4 (J) - 1.6.4 (WP)
9278 */
9279 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
9280
9281 return $default_color_tags;
9282 }
9283
9284 public static function loadBookingsColorTags()
9285 {
9286 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
9287
9288 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
9289 }
9290
9291 public static function getBestColorContrast($hexcolor)
9292 {
9293 $hexcolor = str_replace('#', '', $hexcolor);
9294
9295 if (empty($hexcolor) || strlen($hexcolor) != 6) {
9296 return '#000000';
9297 }
9298
9299 $r = hexdec(substr($hexcolor, 0, 2));
9300 $g = hexdec(substr($hexcolor, 2, 2));
9301 $b = hexdec(substr($hexcolor, 4, 2));
9302
9303 // Counting the perceptive luminance - human eye favors green color
9304 // < 0.5 bright colors
9305 // > 0.5 dark colors
9306
9307 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
9308 }
9309
9310 public static function applyBookingColorTag($booking, $tags = [])
9311 {
9312 if (!is_array($tags) || !$tags) {
9313 $tags = self::loadBookingsColorTags();
9314 }
9315
9316 if (!empty($booking['colortag'])) {
9317 $color_tag_arr = json_decode($booking['colortag'], true);
9318 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
9319 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
9320 return $color_tag_arr;
9321 }
9322 }
9323
9324 $dbo = JFactory::getDbo();
9325
9326 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
9327 $docs_data = [];
9328 $invoice_numb = false;
9329 $receipt_numb = false;
9330
9331 if ($booking['status'] == 'confirmed' && ($booking['checkin'] ?? 0) > time()) {
9332 $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`
9333 FROM `#__vikbooking_orders` AS `b`
9334 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
9335 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
9336 WHERE `b`.`id`=" . (int)$bid . ";";
9337 $dbo->setQuery($q);
9338 $docs_data = $dbo->loadAssoc();
9339 if ($docs_data) {
9340 $invoice_numb = (!empty($docs_data['inv_id']));
9341 $receipt_numb = (!empty($docs_data['rcp_id']));
9342
9343 /**
9344 * We inject the invoice number or receipt number into the color tag rules.
9345 * This is useful for the Tableaux page to show the invoice/receipt number.
9346 *
9347 * @since 1.12 (J) - 1.1.7 (WP)
9348 */
9349 if ($invoice_numb || $receipt_numb) {
9350 foreach ($tags as &$tval) {
9351 if ($invoice_numb) {
9352 $tval['invoice_number'] = $docs_data['inv_number'];
9353 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
9354 }
9355 if ($receipt_numb) {
9356 $tval['receipt_number'] = $docs_data['rcp_number'];
9357 }
9358 }
9359 // unset last reference
9360 unset($tval);
9361 }
9362 }
9363 }
9364
9365 /**
9366 * Some OTA bookings using an OTA-Collect business model may look as not entirely
9367 * paid when they actually are, due to commissions and pass through taxes. We also
9368 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
9369 *
9370 * @since 1.16.3 (J) - 1.6.3 (WP)
9371 */
9372 $ota_collected_booking = false;
9373 $hotel_collect_vcc_unpaid = false;
9374 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
9375 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
9376 $ota_collected_booking = true;
9377 }
9378 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
9379 // VCC detected, check the history for a payment event
9380 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
9381 // list of history types to fetch
9382 $h_payment_types = [];
9383 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
9384 if ($h_payment_group && $h_payment_group['types']) {
9385 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
9386 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
9387 }
9388 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
9389 if (!$payment_ev_datas) {
9390 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
9391 $hotel_collect_vcc_unpaid = true;
9392 }
9393 }
9394 }
9395
9396 foreach ($tags as $tkey => $tval) {
9397 if (empty($tval['rule'])) {
9398 continue;
9399 }
9400
9401 /**
9402 * Trigger event to let third-party plugins validate the custom rule.
9403 *
9404 * @since 1.16.4 (J) - 1.6.4 (WP)
9405 */
9406 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
9407 if (!empty($custom_rule)) {
9408 return (array) $custom_rule[0];
9409 }
9410
9411 switch ($tval['rule']) {
9412 case 'pend1':
9413 // Room is waiting for the payment (locked record)
9414 if ($booking['status'] == 'standby') {
9415 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
9416 $dbo->setQuery($q);
9417 $tmp_lock = $dbo->loadAssoc();
9418 if ($tmp_lock) {
9419 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9420 return $tval;
9421 }
9422 }
9423 break;
9424 case 'conf1':
9425 // Confirmed (Room Closed)
9426 if ($booking['status'] == 'confirmed' && (!empty($booking['closure']) || $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED'))) {
9427 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9428 return $tval;
9429 }
9430 break;
9431 case 'ovbook':
9432 // Confirmed (Overbooking)
9433 if ($booking['status'] == 'confirmed' && !empty($booking['type']) && $booking['type'] == 'overbooking') {
9434 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9435 return $tval;
9436 }
9437 break;
9438 case 'conf2':
9439 // Confirmed (No Rate 0.00/NULL Total)
9440 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9441 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9442 return $tval;
9443 }
9444 break;
9445 case 'conf3':
9446 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9447 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) {
9448 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9449 return $tval;
9450 }
9451 break;
9452 case 'inv1':
9453 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9454 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9455 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9456 return $tval;
9457 }
9458 break;
9459 case 'rcp1':
9460 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9461 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9462 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9463 return $tval;
9464 }
9465 break;
9466 case 'conf4':
9467 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9468 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9469 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9470 return $tval;
9471 }
9472 break;
9473 case 'conf5':
9474 // Confirmed (Total > 0 && Total Paid >= Total)
9475 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
9476 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9477 return $tval;
9478 }
9479 break;
9480 case 'inv2':
9481 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9482 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9483 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9484 return $tval;
9485 }
9486 break;
9487 default:
9488 // unsupported rule
9489 break;
9490 }
9491 }
9492
9493 return [];
9494 }
9495
9496 public static function getBookingInfoFromID($bid)
9497 {
9498 $dbo = JFactory::getDbo();
9499
9500 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9501 $dbo->setQuery($q);
9502 $booking_info = $dbo->loadAssoc();
9503 if ($booking_info) {
9504 return $booking_info;
9505 }
9506
9507 return [];
9508 }
9509
9510 /**
9511 * Load booking details for a given list of IDs.
9512 *
9513 * @param int $roomid The room integer number.
9514 * @param array $room_bids_pool List of booking IDs involved.
9515 * @param bool $unassigned Whether to return also the unassigned bookings.
9516 *
9517 * @return array Associative list of sub-unit bookings, or list containing also the unassigned bookings.
9518 *
9519 * @since 1.18.2 (J) - 1.8.2 (WP) added argument $unassigned.
9520 */
9521 public static function loadRoomIndexesBookings(int $roomid, array $room_bids_pool, $unassigned = false)
9522 {
9523 $dbo = JFactory::getDbo();
9524
9525 $pool_values = [];
9526 if ($unassigned) {
9527 // prepare the default return value
9528 $pool_values = [
9529 [],
9530 [],
9531 ];
9532 }
9533
9534 if (empty($roomid) || !$room_bids_pool) {
9535 return $pool_values;
9536 }
9537
9538 $room_features_bookings = [];
9539 $unassigned_bookings = [];
9540
9541 $all_bids = [];
9542 foreach ($room_bids_pool as $day => $bids) {
9543 $all_bids = array_merge($all_bids, $bids);
9544 }
9545 $all_bids = array_map('intval', array_unique($all_bids));
9546 $all_bids_str = implode(', ', $all_bids);
9547
9548 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9549 $dbo->setQuery($q);
9550 $rbookings = $dbo->loadAssocList();
9551 if (!$rbookings) {
9552 return $pool_values;
9553 }
9554
9555 foreach ($rbookings as $k => $v) {
9556 if (empty($v['roomindex'])) {
9557 if ($unassigned) {
9558 // push this booking to the list of unassigned reservations
9559 $unassigned_bookings[] = $v['idorder'];
9560 }
9561 // do not proceed further
9562 continue;
9563 }
9564 if (!isset($room_features_bookings[$v['roomindex']])) {
9565 $room_features_bookings[$v['roomindex']] = [];
9566 }
9567 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9568 }
9569
9570 if ($unassigned) {
9571 // return a list of information
9572 return [
9573 $room_features_bookings,
9574 $unassigned_bookings,
9575 ];
9576 }
9577
9578 // return just the list of room sub-unit bookings
9579 return $room_features_bookings;
9580 }
9581
9582 public static function getSendEmailWhen()
9583 {
9584 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9585 }
9586
9587 public static function getMinutesAutoRemove()
9588 {
9589 return VBOFactory::getConfig()->getInt('minautoremove', 0);
9590 }
9591
9592 public static function getSMSAPIClass()
9593 {
9594 return VBOFactory::getConfig()->getString('smsapi', '');
9595 }
9596
9597 public static function autoSendSMSEnabled()
9598 {
9599 return VBOFactory::getConfig()->getBool('smsautosend', false);
9600 }
9601
9602 public static function getSendSMSTo()
9603 {
9604 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9605 }
9606
9607 public static function getSendSMSWhen()
9608 {
9609 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9610 }
9611
9612 public static function getSMSAdminPhone()
9613 {
9614 return VBOFactory::getConfig()->getString('smsadminphone', '');
9615 }
9616
9617 public static function getSMSParams($as_array = true)
9618 {
9619 if (!$as_array) {
9620 return VBOFactory::getConfig()->getString('smsparams', '');
9621 }
9622
9623 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9624 }
9625
9626 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9627 $dbo = JFactory::getDbo();
9628 switch (strtolower($booking_status)) {
9629 case 'standby':
9630 $status = 'pend';
9631 break;
9632 case 'cancelled':
9633 $status = 'canc';
9634 break;
9635 default:
9636 $status = '';
9637 break;
9638 }
9639 $paramtype = 'sms'.$type.'tpl'.$status;
9640 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9641 $dbo->setQuery($q);
9642 $dbo->execute();
9643 if ($dbo->getNumRows() < 1) {
9644 if ($status == 'canc') {
9645 //Type cancelled is used by VCM since v1.6.6
9646 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9647 $dbo->setQuery($q);
9648 $dbo->execute();
9649 }
9650 return '';
9651 }
9652 $ft = $dbo->loadAssocList();
9653 if (is_object($vbo_tn)) {
9654 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9655 }
9656 return $ft[0]['setting'];
9657 }
9658
9659 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9660 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9661 }
9662
9663 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9664 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9665 }
9666
9667 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9668 $dbo = JFactory::getDbo();
9669 $phone = str_replace(" ", '', trim($phone));
9670 $cprefix = '';
9671 if (!empty($country_threecode)) {
9672 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9673 $dbo->setQuery($q);
9674 $dbo->execute();
9675 if ($dbo->getNumRows() > 0) {
9676 $cprefix = $dbo->loadResult();
9677 $cprefix = str_replace(" ", '', trim($cprefix));
9678 }
9679 }
9680 if (!empty($cprefix)) {
9681 if (substr($phone, 0, 1) != '+') {
9682 if (substr($phone, 0, 2) == '00') {
9683 $phone = '+'.substr($phone, 2);
9684 } else {
9685 $phone = $cprefix.$phone;
9686 }
9687 }
9688 }
9689 return $phone;
9690 }
9691
9692 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9693 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9694
9695 /**
9696 * Trigger event to allow third-party plugins to manipulate the template string.
9697 *
9698 * @since 1.16.10 (J) - 1.6.10 (WP)
9699 */
9700 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9701
9702 /**
9703 * Parse all conditional text rules.
9704 *
9705 * @since 1.14 (J) - 1.4.0 (WP)
9706 */
9707 self::getConditionalRulesInstance()
9708 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9709 ->parseTokens($tpl);
9710
9711 /**
9712 * Parse all Door Access Control tags.
9713 *
9714 * @since 1.18.4 (J) - 1.8.4 (WP)
9715 */
9716 VBOFactory::getDoorAccessControl()
9717 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9718
9719 $vbo_df = self::getDateFormat();
9720 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9721 $datesep = self::getDateSeparator();
9722
9723 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9724 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9725 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9726 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9727 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9728 if (!empty($booking['confirmnumber'])) {
9729 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9730 } else {
9731 $tpl = str_replace("{confirmnumb}", '', $tpl);
9732 }
9733 if (!empty($booking['idorderota'])) {
9734 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9735 } else {
9736 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9737 }
9738
9739 $rooms_booked = array();
9740 $rooms_names = array();
9741 $tot_adults = 0;
9742 $tot_children = 0;
9743 $tot_guests = 0;
9744 foreach ($booking_rooms as $broom) {
9745 $rooms_names[] = $broom['room_name'];
9746 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9747 $rooms_booked[$broom['room_name']] += 1;
9748 } else {
9749 $rooms_booked[$broom['room_name']] = 1;
9750 }
9751 $tot_adults += (int)$broom['adults'];
9752 $tot_children += (int)$broom['children'];
9753 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9754 }
9755 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9756 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9757 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9758 $rooms_booked_quant = array();
9759 foreach ($rooms_booked as $rname => $quant) {
9760 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9761 }
9762 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9763 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9764 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9765 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9766 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9767 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9768 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9769 $remaining_bal = $booking['total'] - $booking['totpaid'];
9770 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9771
9772 return $tpl;
9773 }
9774
9775 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9776 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9777
9778 /**
9779 * Trigger event to allow third-party plugins to manipulate the template string.
9780 *
9781 * @since 1.16.10 (J) - 1.6.10 (WP)
9782 */
9783 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9784
9785 /**
9786 * Parse all conditional text rules.
9787 *
9788 * @since 1.14 (J) - 1.4.0 (WP)
9789 */
9790 self::getConditionalRulesInstance()
9791 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9792 ->parseTokens($tpl);
9793
9794 /**
9795 * Parse all Door Access Control tags.
9796 *
9797 * @since 1.18.4 (J) - 1.8.4 (WP)
9798 */
9799 VBOFactory::getDoorAccessControl()
9800 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9801
9802 $vbo_df = self::getDateFormat();
9803 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9804 $datesep = self::getDateSeparator();
9805
9806 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9807 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9808 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9809 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9810 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9811 if (!empty($booking['confirmnumber'])) {
9812 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9813 } else {
9814 $tpl = str_replace("{confirmnumb}", '', $tpl);
9815 }
9816 if (!empty($booking['idorderota'])) {
9817 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9818 } else {
9819 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9820 }
9821
9822 $rooms_booked = array();
9823 $rooms_names = array();
9824 $tot_adults = 0;
9825 $tot_children = 0;
9826 $tot_guests = 0;
9827 foreach ($booking_rooms as $broom) {
9828 $rooms_names[] = $broom['room_name'];
9829 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9830 $rooms_booked[$broom['room_name']] += 1;
9831 } else {
9832 $rooms_booked[$broom['room_name']] = 1;
9833 }
9834 $tot_adults += (int)$broom['adults'];
9835 $tot_children += (int)$broom['children'];
9836 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9837 }
9838 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9839 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9840 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9841 $rooms_booked_quant = array();
9842 foreach ($rooms_booked as $rname => $quant) {
9843 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9844 }
9845 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9846 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9847 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9848 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9849 $remaining_bal = $booking['total'] - $booking['totpaid'];
9850 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9851 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9852
9853 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9854 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9855
9856 if (VBOPlatformDetection::isWordPress()) {
9857 /**
9858 * @wponly Rewrite URI for front-end
9859 */
9860 $book_link = str_replace(JUri::root(), '', $book_link);
9861 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9862 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9863 if ($itemid) {
9864 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9865 }
9866 } else {
9867 /**
9868 * @joomlaonly Rewrite URI for front-end
9869 */
9870 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9871 $uri_parts = [
9872 'option' => 'com_vikbooking',
9873 'view' => 'booking',
9874 'sid' => $use_sid,
9875 'ts' => $booking['ts'],
9876 ];
9877 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9878 // inject lang in query string for proper link routing
9879 $uri_parts['lang'] = $booking['lang'];
9880 }
9881 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9882 }
9883
9884 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9885 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9886 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9887
9888 return $tpl;
9889 }
9890
9891 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9892 $dbo = JFactory::getDbo();
9893 if (!class_exists('VboApplication')) {
9894 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9895 }
9896 $vbo_app = new VboApplication;
9897 if (empty($oid)) {
9898 return false;
9899 }
9900 $sms_api = self::getSMSAPIClass();
9901 if (empty($sms_api)) {
9902 return false;
9903 }
9904 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
9905 return false;
9906 }
9907 $sms_api_params = self::getSMSParams();
9908 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
9909 return false;
9910 }
9911 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
9912 return false;
9913 }
9914 $send_sms_to = self::getSendSMSTo();
9915 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
9916 return false;
9917 }
9918 $booking = array();
9919 $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.";";
9920 $dbo->setQuery($q);
9921 $dbo->execute();
9922 if ($dbo->getNumRows() > 0) {
9923 $booking = $dbo->loadAssoc();
9924 }
9925 if (!(count($booking) > 0)) {
9926 return false;
9927 }
9928 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
9929 return false;
9930 }
9931 $booking_rooms = array();
9932 $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'].";";
9933 $dbo->setQuery($q);
9934 $dbo->execute();
9935 if ($dbo->getNumRows() > 0) {
9936 $booking_rooms = $dbo->loadAssocList();
9937 }
9938 $admin_phone = self::getSMSAdminPhone();
9939 $admin_sendermail = self::getSenderMail();
9940 $admin_email = self::getAdminMail();
9941 $f_result = false;
9942 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
9943 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
9944 //SMS for the administrator
9945 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
9946 if (!empty($sms_text)) {
9947 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9948 //administrator phone can contain multiple numbers separated by comma or semicolon
9949 $admin_phones = array();
9950 if (strpos($admin_phone, ',') !== false) {
9951 $all_phones = explode(',', $admin_phone);
9952 foreach ($all_phones as $aph) {
9953 if (!empty($aph)) {
9954 $admin_phones[] = trim($aph);
9955 }
9956 }
9957 } elseif (strpos($admin_phone, ';') !== false) {
9958 $all_phones = explode(';', $admin_phone);
9959 foreach ($all_phones as $aph) {
9960 if (!empty($aph)) {
9961 $admin_phones[] = trim($aph);
9962 }
9963 }
9964 } else {
9965 $admin_phones[] = $admin_phone;
9966 }
9967 foreach ($admin_phones as $admphone) {
9968 $response_obj = $sms_obj->sendMessage($admphone, strip_tags($sms_text));
9969 if ( !$sms_obj->validateResponse($response_obj) ) {
9970 //notify the administrator via email with the error of the SMS sending
9971 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9972 } else {
9973 $f_result = true;
9974 }
9975 }
9976 }
9977 }
9978 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
9979 //SMS for the Customer
9980 $vbo_tn = self::getTranslator();
9981 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
9982 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
9983 if (!empty($sms_text)) {
9984 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9985 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
9986 if ( !$sms_obj->validateResponse($response_obj) ) {
9987 //notify the administrator via email with the error of the SMS sending
9988 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9989 } else {
9990 $f_result = true;
9991 }
9992 }
9993 }
9994 return $f_result;
9995 }
9996
9997 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
9998 {
9999 if (!defined('_VIKBOOKINGEXEC')) {
10000 define('_VIKBOOKINGEXEC', '1');
10001 }
10002
10003 ob_start();
10004 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
10005 $content = ob_get_contents();
10006 ob_end_clean();
10007
10008 $default_params = array(
10009 'show_header' => 0,
10010 'header_data' => array(),
10011 'show_footer' => 0,
10012 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10013 'pdf_unit' => 'PDF_UNIT',
10014 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10015 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10016 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10017 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10018 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10019 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10020 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10021 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10022 'header_font_size' => '10',
10023 'body_font_size' => '10',
10024 'footer_font_size' => '8',
10025 'show_lines_taxrate_col' => 0,
10026 );
10027
10028 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
10029 $default_params = array_merge($default_params, $invoice_params);
10030 }
10031
10032 return array($content, $default_params);
10033 }
10034
10035 /**
10036 * Includes within a buffer the template file for the custom (manual) invoice.
10037 *
10038 * @param array $invoice the record of the custom invoice
10039 * @param array $customer the customer record
10040 *
10041 * @return string the HTML content of the template file
10042 *
10043 * @since 1.11.1
10044 */
10045 public static function loadCustomInvoiceTmpl($invoice, $customer) {
10046 if (!defined('_VIKBOOKINGEXEC')) {
10047 define('_VIKBOOKINGEXEC', '1');
10048 }
10049 ob_start();
10050 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
10051 $content = ob_get_contents();
10052 ob_end_clean();
10053 $default_params = array(
10054 'show_header' => 0,
10055 'header_data' => array(),
10056 'show_footer' => 0,
10057 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10058 'pdf_unit' => 'PDF_UNIT',
10059 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10060 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10061 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10062 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10063 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10064 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10065 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10066 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10067 'header_font_size' => '10',
10068 'body_font_size' => '10',
10069 'footer_font_size' => '8'
10070 );
10071 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
10072 $default_params = array_merge($default_params, $invoice_params);
10073 }
10074 return array($content, $default_params);
10075 }
10076
10077 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
10078 {
10079 // clone the original template file source
10080 $parsed = $invoicetpl;
10081
10082 /**
10083 * Parse all conditional text rules.
10084 *
10085 * @since 1.14 (J) - 1.4.0 (WP)
10086 */
10087 self::getConditionalRulesInstance()
10088 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
10089 ->parseTokens($parsed);
10090
10091 $dbo = JFactory::getDbo();
10092 if (is_null($vbo_tn)) {
10093 $vbo_tn = self::getTranslator();
10094 }
10095 // availability helper
10096 $av_helper = self::getAvailabilityInstance();
10097
10098 $nowdf = self::getDateFormat();
10099 if ($nowdf == "%d/%m/%Y") {
10100 $df = 'd/m/Y';
10101 } elseif ($nowdf == "%m/%d/%Y") {
10102 $df = 'm/d/Y';
10103 } else {
10104 $df = 'Y/m/d';
10105 }
10106 $datesep = self::getDateSeparator();
10107
10108 /**
10109 * Split stay reservation.
10110 *
10111 * @since 1.16.0 (J) - 1.6.0 (WP)
10112 */
10113 $room_stay_dates = [];
10114 if ($booking['split_stay']) {
10115 if ($booking['status'] == 'confirmed') {
10116 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
10117 } else {
10118 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
10119 }
10120 // immediately count the number of nights of stay for each split room
10121 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
10122 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
10123 // overwrite values for compatibility with non-confirmed bookings
10124 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
10125 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
10126 }
10127 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
10128 // overwrite the whole array
10129 $room_stay_dates[$sps_r_k] = $sps_r_v;
10130 }
10131 }
10132
10133 $companylogo = self::getSiteLogo();
10134 $uselogo = '';
10135 if (!empty($companylogo)) {
10136 /**
10137 * Let's try to prevent TCPDF errors.
10138 *
10139 * @since August 2nd 2019
10140 */
10141 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10142 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10143 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10144 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10145 }
10146 }
10147 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10148 $parsed = str_replace("{company_info}", $company_info, $parsed);
10149 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
10150 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
10151 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
10152 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10153
10154 // custom fields replacement
10155 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10156 if (is_array($cmatches[1]) && $cmatches[1]) {
10157 $cfids = array();
10158 foreach ($cmatches[1] as $cfid) {
10159 $cfids[] = $cfid;
10160 }
10161 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10162 $dbo->setQuery($q);
10163 $cfields = $dbo->loadAssocList();
10164 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10165 $cfmap = array();
10166 foreach ($cfields as $cf) {
10167 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10168 }
10169 $cfmapreplace = array();
10170 $partsreceived = explode("\n", $booking['custdata']);
10171 if (count($partsreceived) > 0) {
10172 foreach ($partsreceived as $pst) {
10173 if (!empty($pst)) {
10174 $tmpdata = explode(":", $pst);
10175 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10176 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10177 }
10178 }
10179 }
10180 }
10181 foreach ($cmatches[1] as $cfid) {
10182 if (array_key_exists($cfid, $cfmapreplace)) {
10183 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10184 } else {
10185 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10186 }
10187 }
10188 }
10189
10190 // invoice price description - Start
10191 $rooms = array();
10192 $tars = array();
10193 $arrpeople = array();
10194 $is_package = !empty($booking['pkg']) ? true : false;
10195 $tot_adults = 0;
10196 $tot_children = 0;
10197 $tot_guests = 0;
10198 foreach ($booking_rooms as $kor => $or) {
10199 $num = $kor + 1;
10200 $rooms[$num] = $or;
10201
10202 // determine the days to consider for the count of the availability
10203 $room_nights = $booking['days'];
10204 $room_checkin = $booking['checkin'];
10205 $room_checkout = $booking['checkout'];
10206 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10207 $room_nights = $room_stay_dates[$kor]['nights'];
10208 $room_checkin = $room_stay_dates[$kor]['checkin'];
10209 $room_checkout = $room_stay_dates[$kor]['checkout'];
10210 }
10211
10212 $arrpeople[$num]['adults'] = $or['adults'];
10213 $arrpeople[$num]['children'] = $or['children'];
10214 $tot_adults += $or['adults'];
10215 $tot_children += $or['children'];
10216 $tot_guests += ($or['adults'] + $or['children']);
10217 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10218 //package or custom cost set from the back-end
10219 continue;
10220 }
10221 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
10222 $dbo->setQuery($q);
10223 $tar = $dbo->loadAssocList();
10224 if ($tar) {
10225 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
10226 //different usage
10227 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
10228 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
10229 //Occupancy Override
10230 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
10231 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
10232 //
10233 if (is_array($diffusageprice)) {
10234 //set a charge or discount to the price(s) for the different usage of the room
10235 foreach ($tar as $kpr => $vpr) {
10236 $tar[$kpr]['diffusage'] = $or['adults'];
10237 if ($diffusageprice['chdisc'] == 1) {
10238 //charge
10239 if ($diffusageprice['valpcent'] == 1) {
10240 //fixed value
10241 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10242 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10243 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
10244 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10245 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
10246 } else {
10247 //percentage value
10248 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10249 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
10250 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
10251 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10252 $tar[$kpr]['cost'] = $aduseval;
10253 }
10254 } else {
10255 //discount
10256 if ($diffusageprice['valpcent'] == 1) {
10257 //fixed value
10258 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10259 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10260 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
10261 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10262 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
10263 } else {
10264 //percentage value
10265 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10266 $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);
10267 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
10268 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10269 $tar[$kpr]['cost'] = $aduseval;
10270 }
10271 }
10272 }
10273 }
10274 }
10275 //
10276 $tars[$num] = $tar[0];
10277 }
10278 }
10279 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10280 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10281 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10282 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10283 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10284 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10285
10286 $isdue = 0;
10287 $tot_taxes = 0;
10288 $tot_city_taxes = 0;
10289 $tot_fees = 0;
10290 $pricestr = array();
10291 $optstr = array();
10292 // start building the tax summary
10293 VBOTaxonomySummary::start();
10294 foreach ($booking_rooms as $kor => $or) {
10295 $num = $kor + 1;
10296
10297 // determine the days to consider for the count of the availability
10298 $room_nights = $booking['days'];
10299 $room_checkin = $booking['checkin'];
10300 $room_checkout = $booking['checkout'];
10301 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10302 $room_nights = $room_stay_dates[$kor]['nights'];
10303 $room_checkin = $room_stay_dates[$kor]['checkin'];
10304 $room_checkout = $room_stay_dates[$kor]['checkout'];
10305 }
10306
10307 $pricestr[$num] = array();
10308 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10309 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
10310 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
10311 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
10312 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
10313 $pricestr[$num]['tot'] = $calctar;
10314 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
10315 $tot_taxes += ($calctar - $cost_minus_tax);
10316 $isdue += $calctar;
10317 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
10318 $pricestr[$num]['tax_rate'] = $tax_rate;
10319 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
10320 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10321 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
10322 $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'] : "");
10323 $pricestr[$num]['tot'] = $calctar;
10324 $tars[$num]['calctar'] = $calctar;
10325 $isdue += $calctar;
10326 if ($calctar == $display_rate) {
10327 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
10328 $tot_taxes += ($display_rate - $cost_minus_tax);
10329 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
10330 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
10331 $pricestr[$num]['tax_rate'] = $tax_rate;
10332 } else {
10333 $tot_taxes += ($calctar - $display_rate);
10334 $pricestr[$num]['tax'] = ($calctar - $display_rate);
10335 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
10336 $pricestr[$num]['tax_rate'] = $tax_rate;
10337 }
10338 }
10339 $optstr[$num] = array();
10340 $opt_ind = 0;
10341 if (!empty($or['optionals'])) {
10342 $stepo = explode(";", $or['optionals']);
10343 foreach ($stepo as $roptkey => $oo) {
10344 if (!empty($oo)) {
10345 $stept = explode(":", $oo);
10346 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
10347 $dbo->setQuery($q);
10348 $actopt = $dbo->loadAssocList();
10349 if ($actopt) {
10350 if (is_object($vbo_tn)) {
10351 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
10352 }
10353 $optstr[$num][$opt_ind] = array();
10354 $chvar = '';
10355 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
10356 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
10357 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
10358 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
10359 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
10360 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
10361 $agestept = explode('-', $stept[1]);
10362 $stept[1] = $agestept[0];
10363 $chvar = $agestept[1];
10364 $realcost = 0;
10365 if (!empty($chvar)) {
10366 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
10367 //percentage value of the adults tariff
10368 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10369 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10370 } else {
10371 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10372 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10373 }
10374 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
10375 //VBO 1.10 - percentage value of room base cost
10376 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10377 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10378 } else {
10379 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
10380 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10381 }
10382 }
10383 $actopt[0]['chageintv'] = $chvar;
10384 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
10385 $actopt[0]['quan'] = $stept[1];
10386 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
10387 }
10388 } else {
10389 $actopt[0]['quan'] = $stept[1];
10390 // VBO 1.11 - options percentage cost of the room total fee
10391 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10392 $deftar_basecosts = $or['cust_cost'];
10393 } else {
10394 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10395 }
10396 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
10397 //
10398 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
10399 }
10400 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
10401 $realcost = $actopt[0]['maxprice'];
10402 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
10403 $realcost = $actopt[0]['maxprice'] * $stept[1];
10404 }
10405 }
10406 if ($actopt[0]['perperson'] == 1) {
10407 $realcost = $realcost * $or['adults'];
10408 }
10409
10410 /**
10411 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
10412 *
10413 * @since 1.17.7 (J) - 1.7.7 (WP)
10414 */
10415 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
10416 if ($custom_calculation) {
10417 $realcost = (float) $custom_calculation[0];
10418 }
10419
10420 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
10421 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
10422 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
10423 $optstr[$num][$opt_ind]['tax'] = 0;
10424 if ($actopt[0]['is_citytax'] == 1) {
10425 $tot_city_taxes += $tmpopr;
10426 } elseif ($actopt[0]['is_fee'] == 1) {
10427 $tot_fees += $tmpopr;
10428 }
10429 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
10430 if ($tmpopr == $realcost) {
10431 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
10432 $tot_taxes += ($realcost - $opt_minus_tax);
10433 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
10434 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
10435 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10436 } else {
10437 $tot_taxes += ($tmpopr - $realcost);
10438 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
10439 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
10440 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10441 }
10442 //
10443 $opt_ind++;
10444 $isdue += $tmpopr;
10445 }
10446 }
10447 }
10448 }
10449
10450 // custom extra costs
10451 if (!empty($or['extracosts'])) {
10452 $cur_extra_costs = json_decode($or['extracosts'], true);
10453 foreach ($cur_extra_costs as $eck => $ecv) {
10454 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10455 $isdue += $ecplustax;
10456 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
10457 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
10458 $optstr[$num][$opt_ind]['tax'] = 0;
10459 if ($ecplustax == $ecv['cost']) {
10460 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10461 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
10462 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
10463 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
10464 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10465 } else {
10466 $tot_taxes += ($ecplustax - $ecv['cost']);
10467 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
10468 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
10469 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10470 }
10471 $opt_ind++;
10472 }
10473 }
10474 }
10475
10476 $usedcoupon = false;
10477 if (strlen($booking['coupon']) > 0) {
10478 $orig_isdue = $isdue;
10479 $expcoupon = explode(";", $booking['coupon']);
10480 $usedcoupon = $expcoupon;
10481 $isdue = $isdue - (float)$expcoupon[1];
10482 if ($isdue != $orig_isdue) {
10483 //lower taxes proportionally
10484 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10485 }
10486 }
10487 if ($booking['refund'] > 0) {
10488 $orig_isdue = $isdue;
10489 $isdue -= $booking['refund'];
10490 if ($isdue != $orig_isdue) {
10491 //lower taxes proportionally
10492 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10493 }
10494 }
10495 $rows_written = 0;
10496 $inv_rows = '';
10497 foreach ($pricestr as $num => $price_descr) {
10498 // add room split stay information, if any
10499 $split_stay_str = '';
10500 $kor = $num - 1;
10501 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10502 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10503 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10504 }
10505
10506 $inv_rows .= '<tr>'."\n";
10507 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10508 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($price_descr['tot'] - $price_descr['tax'])), $booking['currencyname']) . '</td>'."\n";
10509 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tax']), $booking['currencyname']) . '</td>'."\n";
10510 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tot']), $booking['currencyname']) . '</td>'."\n";
10511 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10512 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10513 }
10514 $inv_rows .= '</tr>'."\n";
10515 $rows_written++;
10516 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10517 foreach ($optstr[$num] as $optk => $optv) {
10518 $inv_rows .= '<tr>'."\n";
10519 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10520 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($optv['tot'] - $optv['tax'])), $booking['currencyname']) . '</td>'."\n";
10521 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tax']), $booking['currencyname']) . '</td>'."\n";
10522 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tot']), $booking['currencyname']) . '</td>'."\n";
10523 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10524 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10525 }
10526 $inv_rows .= '</tr>'."\n";
10527 $rows_written++;
10528 }
10529 }
10530 }
10531
10532 // if discount print row
10533 if ($usedcoupon !== false) {
10534 $inv_rows .= '<tr>'."\n";
10535 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10536 $inv_rows .= '</tr>'."\n";
10537 $inv_rows .= '<tr>'."\n";
10538 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10539 $inv_rows .= '<td></td>'."\n";
10540 $inv_rows .= '<td></td>'."\n";
10541 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($usedcoupon[1]), $booking['currencyname']) . '</td>'."\n";
10542 $inv_rows .= '</tr>'."\n";
10543 $rows_written += 2;
10544 }
10545 // if refunded amount, print row
10546 if ($booking['refund'] > 0) {
10547 $inv_rows .= '<tr>'."\n";
10548 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10549 $inv_rows .= '</tr>'."\n";
10550 $inv_rows .= '<tr>'."\n";
10551 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10552 $inv_rows .= '<td></td>'."\n";
10553 $inv_rows .= '<td></td>'."\n";
10554 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($booking['refund']), $booking['currencyname']) . '</td>'."\n";
10555 $inv_rows .= '</tr>'."\n";
10556 $rows_written += 2;
10557 }
10558 //
10559 $min_records = 8;
10560 if ($rows_written < $min_records) {
10561 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10562 $inv_rows .= '<tr>'."\n";
10563 $inv_rows .= '<td></td>'."\n";
10564 $inv_rows .= '<td></td>'."\n";
10565 $inv_rows .= '<td></td>'."\n";
10566 $inv_rows .= '</tr>'."\n";
10567 }
10568 }
10569 //invoice price description - End
10570 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10571 $parsed = str_replace("{invoice_totalnet}", self::formatCurrencyNumber(self::numberFormat(($isdue - $tot_taxes)), $booking['currencyname']), $parsed);
10572 $parsed = str_replace("{invoice_totaltax}", self::formatCurrencyNumber(self::numberFormat($tot_taxes), $booking['currencyname']), $parsed);
10573 $parsed = str_replace("{invoice_grandtotal}", self::formatCurrencyNumber(self::numberFormat($isdue), $booking['currencyname']), $parsed);
10574 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10575
10576 // invoice tax summary
10577 $tax_summary = VBOTaxonomySummary::get();
10578 $tax_names = VBOTaxonomySummary::getNames();
10579 $taxsum_rows = "";
10580 foreach ($tax_summary as $tax_rate => $tax_amount) {
10581 if (!$tax_rate && !$tax_amount) {
10582 continue;
10583 }
10584 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10585 $taxsum_rows .= '<tr>' . "\n";
10586 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10587 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10588 $taxsum_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($tax_amount), $booking['currencyname']) . '</td>' . "\n";
10589 $taxsum_rows .= '</tr>' . "\n";
10590 }
10591 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10592
10593 return $parsed;
10594 }
10595
10596 /**
10597 * Specific method for parsing the template file for the custom (manual) invoices.
10598 *
10599 * @param string $invoicetpl the plain custom invoice template file before parsing
10600 * @param array $invoice the invoice record
10601 * @param array $customer the customer record
10602 *
10603 * @return string the HTML content of the parsed custom invoice template
10604 *
10605 * @since 1.11.1
10606 */
10607 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10608 $nowdf = self::getDateFormat(true);
10609 if ($nowdf == "%d/%m/%Y") {
10610 $df = 'd/m/Y';
10611 } elseif ($nowdf == "%m/%d/%Y") {
10612 $df = 'm/d/Y';
10613 } else {
10614 $df = 'Y/m/d';
10615 }
10616 $datesep = self::getDateSeparator(true);
10617 $companylogo = self::getSiteLogo();
10618 $uselogo = '';
10619 if (!empty($companylogo)) {
10620 /**
10621 * Let's try to prevent TCPDF errors by checking if the file exists.
10622 *
10623 * @since August 2nd 2019
10624 */
10625 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10626 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10627 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10628 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10629 }
10630 }
10631 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10632 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10633 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10634 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10635 // customer information
10636 $custinfo = '';
10637 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10638 $custinfo .= $customer['email'] . "\n";
10639 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10640 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10641 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10642 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10643 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10644 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10645 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10646 // invoice notes
10647 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10648
10649 return $invoicetpl;
10650 }
10651
10652 /**
10653 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10654 *
10655 * @param array $booking the booking information.
10656 * @param int $invoice_num the invoice number.
10657 * @param string $invoice_suff the invoice suffix.
10658 * @param string $invoice_date the invoice date.
10659 * @param string $company_info optional company information.
10660 * @param bool $translate whether the invoice contents should be translated in the booking language.
10661 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10662 *
10663 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10664 *
10665 * @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.
10666 */
10667 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10668 {
10669 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10670 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10671 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10672
10673 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10674 return false;
10675 }
10676
10677 /**
10678 * Trigger event to allow third party plugins to override some values for the invoice generation.
10679 *
10680 * @since 1.16.3 (J) - 1.6.3 (WP)
10681 */
10682 $event_args = [
10683 'invoice_num' => $invoice_num,
10684 'invoice_suff' => $invoice_suff,
10685 'invoice_date' => $invoice_date,
10686 'company_info' => $company_info,
10687 'translate' => $translate,
10688 ];
10689 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10690 if (is_array($event_args) && $event_args) {
10691 // extract the new values to be used
10692 extract($event_args);
10693 }
10694
10695 $dbo = JFactory::getDbo();
10696 $vbo_tn = self::getTranslator();
10697
10698 // inject the currency name value
10699 $currencyname = self::getCurrencyName();
10700 $booking['currencyname'] = $currencyname;
10701
10702 // make sure we've got a customer ID assigned to the reservation record
10703 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10704 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10705 if ($booking_customer) {
10706 $booking['idcustomer'] = $booking_customer['id'];
10707 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10708 $booking['customer_pin'] = $booking_customer['pin'];
10709 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10710 }
10711 }
10712
10713 $nowdf = self::getDateFormat(true);
10714 if ($nowdf == "%d/%m/%Y") {
10715 $df = 'd/m/Y';
10716 } elseif ($nowdf == "%m/%d/%Y") {
10717 $df = 'm/d/Y';
10718 } else {
10719 $df = 'Y/m/d';
10720 }
10721 $datesep = self::getDateSeparator(true);
10722
10723 if (empty($invoice_date)) {
10724 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10725 $used_date = $booking['ts'];
10726 } else {
10727 /**
10728 * We could be re-generating an invoice for a booking that already had an invoice.
10729 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10730 * It is now possible to keep the same invoice date as the previous one, so check
10731 * what value contains $invoice_date to see if it's different from today's date.
10732 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10733 * to also check the length of the string $invoice_date before using that date.
10734 *
10735 * @since 1.10 - August 2018
10736 */
10737 $base_ts = time();
10738 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10739 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10740 }
10741 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10742 $used_date = $base_ts;
10743 }
10744
10745 // load room reservation records
10746 $q = $dbo->getQuery(true)
10747 ->select($dbo->qn('or') . '.*')
10748 ->select($dbo->qn('r.name', 'room_name'))
10749 ->select($dbo->qn('r.fromadult'))
10750 ->select($dbo->qn('r.toadult'))
10751 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10752 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10753 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10754 ->order($dbo->qn('or.id') . ' ASC');
10755
10756 $dbo->setQuery($q);
10757 $booking_rooms = $dbo->loadAssocList();
10758
10759 if (!$booking_rooms) {
10760 return false;
10761 }
10762
10763 // check if an invoice for this reservation exists
10764 $invoice_id = 0;
10765 $dbo->setQuery(
10766 $dbo->getQuery(true)
10767 ->select('*')
10768 ->from($dbo->qn('#__vikbooking_invoices'))
10769 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10770 ->order($dbo->qn('id') . ' DESC')
10771 );
10772 $invoice_data = $dbo->loadAssoc();
10773 if ($invoice_data) {
10774 $invoice_id = $invoice_data['id'];
10775 }
10776
10777 // if requested, prepare data to refresh the PDF invoice
10778 if ($refresh_pdf === true) {
10779 if (!$invoice_id || !$invoice_data) {
10780 // cannot refresh the PDF if an invoice was never created before
10781 return false;
10782 }
10783
10784 // load the same exact previous information to refresh the invoice
10785 $invoice_num = $invoice_data['number'];
10786 $invoice_suff = $invoice_suff;
10787 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10788 $used_date = $invoice_data['for_date'];
10789
10790 // parse the previous (current) invoice number
10791 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10792 if ($matches) {
10793 // overwrite values
10794 $invoice_num = $matches[1] ?: $invoice_num;
10795 $invoice_suff = $matches[2] ?: $invoice_suff;
10796 }
10797 }
10798
10799 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10800 if ($translate === true) {
10801 if (!empty($booking['lang'])) {
10802 $lang = JFactory::getLanguage();
10803 if ($lang->getTag() != $booking['lang']) {
10804 if (VBOPlatformDetection::isWordPress()) {
10805 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10806 } else {
10807 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10808 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10809 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10810 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10811 }
10812 }
10813 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10814 // force the translation to start because contents should be translated
10815 $vbo_tn::$force_tolang = $booking['lang'];
10816 }
10817 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10818 }
10819 }
10820
10821 // the PDF file name
10822 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10823
10824 /**
10825 * If an analogic invoice is not available, make sure to create the record
10826 * before letting the e-invoicing drivers run to avoid a loop.
10827 *
10828 * @since 1.16.7 (J) - 1.6.7 (WP)
10829 */
10830 $retval = null;
10831 if (!$refresh_pdf && !$invoice_id) {
10832 // prepare record object
10833 $invoice_record = new stdClass;
10834 $invoice_record->number = $invoice_num . $invoice_suff;
10835 $invoice_record->file_name = $pdffname;
10836 $invoice_record->idorder = (int)$booking['id'];
10837 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10838 $invoice_record->created_on = time();
10839 $invoice_record->for_date = (int)$used_date;
10840
10841 // insert record
10842 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10843 return false;
10844 }
10845
10846 $retval = $invoice_record->id ?? false;
10847
10848 if (!$retval) {
10849 return false;
10850 }
10851 }
10852
10853 /**
10854 * The generation of the analogic invoices can trigger the drivers for the
10855 * generation of the e-Invoices if they are set to automatically run.
10856 * However, the e-Invoicing classes may be calling this method, so we need
10857 * to make sure the eInvoicing class is not running before proceeding.
10858 * This method could be called within a loop, and so the second iterations
10859 * may already have loaded the eInvocing class. For this we use a static variable.
10860 *
10861 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10862 * though additional information may be available only after the transmission. On top
10863 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10864 */
10865 static $einvocing_can_run = false;
10866
10867 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10868 // allow second iterations calling this method to run
10869 $einvocing_can_run = true;
10870 }
10871
10872 // always load all drivers, db and plugin based ones
10873 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10874
10875 foreach ($drivers as $driver_obj) {
10876 // set the flag of the external call
10877 $driver_obj->externalCall = __METHOD__;
10878
10879 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10880 $driver_obj->externalData['einvnum'] = $invoice_num;
10881
10882 // let the driver eventually elaborate the booking information before invoicing
10883 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10884
10885 if ($einvocing_can_run === true && !$refresh_pdf) {
10886 // generate the e-Invoice
10887 $driver_err = '';
10888 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10889 $driver_err = $driver_obj->getError();
10890 VikError::raiseWarning('', $driver_err);
10891 }
10892
10893 // Booking History - store log for the e-Invoice result
10894 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10895 }
10896 }
10897
10898 /**
10899 * Start the generation of the analogic (courtesy) PDF invoice.
10900 */
10901
10902 // load dependencies
10903 if (!class_exists('TCPDF')) {
10904 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10905 }
10906 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10907
10908 /**
10909 * Trigger event to allow third party plugins to return a specific font name.
10910 *
10911 * @since 1.16.0 (J) - 1.6.0 (WP)
10912 */
10913 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10914 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10915 $usepdffont = $custom_pdf_font[0];
10916 }
10917
10918 // set array variable to the template file
10919 $booking_info = self::getBookingInfoFromID($booking['id']);
10920
10921 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
10922
10923 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
10924
10925 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10926
10927 if (is_file($pathpdf)) {
10928 @unlink($pathpdf);
10929 }
10930
10931 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10932
10933 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10934 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
10935
10936 // header for each page of the pdf
10937 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
10938 $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]);
10939 }
10940
10941 // change some currencies to their unicode (decimal) value
10942 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10943 if (array_key_exists($booking['currencyname'], $unichr_map)) {
10944 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
10945 }
10946
10947 // header and footer fonts
10948 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10949 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10950
10951 // margins
10952 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10953 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10954 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10955
10956 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10957 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10958 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10959
10960 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10961 $pdf->SetPrintHeader(false);
10962 }
10963 if ($pdfparams['show_footer'] == 0) {
10964 $pdf->SetPrintFooter(false);
10965 }
10966
10967 $pdf->AddPage();
10968 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10969 $pdf->lastPage();
10970 $pdf->Output($pathpdf, 'F');
10971
10972 if (!is_file($pathpdf)) {
10973 return false;
10974 }
10975
10976 if (VBOPlatformDetection::isWordPress()) {
10977 /**
10978 * @wponly - trigger files mirroring
10979 */
10980 VikBookingLoader::import('update.manager');
10981 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10982 }
10983
10984 // Booking History
10985 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
10986
10987 if ($retval) {
10988 // a new invoice ID was inserted above
10989 return $retval;
10990 }
10991
10992 // prepare record object to be updated (if insert needed, it's done at first)
10993 $invoice_record = new stdClass;
10994 $invoice_record->id = (int)$invoice_id;
10995 $invoice_record->number = $invoice_num . $invoice_suff;
10996 $invoice_record->file_name = $pdffname;
10997 $invoice_record->idorder = (int)$booking['id'];
10998 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10999 $invoice_record->created_on = time();
11000 $invoice_record->for_date = (int)$used_date;
11001
11002 // update record
11003 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
11004
11005 // return the PDF invoice ID
11006 return $invoice_id;
11007 }
11008
11009 /**
11010 * Generates an analogic invoice in PDF format for a custom list of services.
11011 * No bookings are assigned to this custom invoice. The method parses the same
11012 * invoice template as for the regular process with real booking IDs.
11013 * The invoice number must be stored before calling this method.
11014 *
11015 * @param int $invoice_id the ID of the custom invoice record
11016 *
11017 * @return bool
11018 *
11019 * @since 1.11.1 (J) - 1.1.1 (WP)
11020 */
11021 public static function generateCustomInvoice($invoice_id)
11022 {
11023 $dbo = JFactory::getDbo();
11024
11025 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
11026 $dbo->setQuery($q);
11027 $invoice = $dbo->loadAssoc();
11028 if (!$invoice) {
11029 return false;
11030 }
11031
11032 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
11033 $rawcont = is_array($rawcont) ? $rawcont : array();
11034 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
11035 $invoice['rawcont'] = $rawcont;
11036 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
11037 if (!$rawcont || !$rows || !$customer) {
11038 // at least one invoice raw is mandatory as well as the customer
11039 return false;
11040 }
11041
11042 /**
11043 * Start the generation of the electronic invoice, if any driver was enabled.
11044 *
11045 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11046 * though additional information would be available only after the transmission.
11047 */
11048 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11049
11050 if ($drivers) {
11051 // make sure the invoice number is just a number
11052 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
11053
11054 // invoke all drivers that should run automatically
11055 foreach ($drivers as $driver_obj) {
11056 // set the flag of the external call
11057 $driver_obj->externalCall = __METHOD__;
11058
11059 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
11060 $driver_obj->externalData['einvnum'] = $invoice['number'];
11061 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
11062 $driver_obj->externalData['einvcustom'] = $invoice;
11063
11064 // prepare data array for the generation of the e-Invoice
11065 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
11066
11067 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
11068 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
11069 if ($preveinv !== false) {
11070 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
11071 }
11072
11073 // generate the e-Invoice
11074 $driver_err = '';
11075 if (!$driver_obj->generateEInvoice($einvdata)) {
11076 $driver_err = $driver_obj->getError();
11077 VikError::raiseWarning('', $driver_err);
11078 }
11079 }
11080 }
11081
11082 /**
11083 * Start the generation of the analogic (courtesy) PDF invoice.
11084 */
11085
11086 // load dependencies
11087 if (!class_exists('TCPDF')) {
11088 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11089 }
11090 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11091
11092 /**
11093 * Trigger event to allow third party plugins to return a specific font name.
11094 *
11095 * @since 1.16.0 (J) - 1.6.0 (WP)
11096 */
11097 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11098 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11099 $usepdffont = $custom_pdf_font[0];
11100 }
11101
11102 // load invoice template file
11103 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
11104
11105 // parse invoice template file
11106 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
11107
11108 $pdffname = $invoice['file_name'];
11109 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11110
11111 if (is_file($pathpdf)) {
11112 @unlink($pathpdf);
11113 }
11114
11115 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11116
11117 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11118 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
11119
11120 // header for each page of the pdf
11121 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
11122 $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]);
11123 }
11124
11125 // change some currencies to their unicode (decimal) value
11126 $currencyname = self::getCurrencyName();
11127 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11128 if (array_key_exists($currencyname, $unichr_map)) {
11129 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
11130 }
11131
11132 // header and footer fonts
11133 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11134 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11135
11136 // margins
11137 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11138 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11139 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11140
11141 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11142 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11143 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11144
11145 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11146 $pdf->SetPrintHeader(false);
11147 }
11148 if ($pdfparams['show_footer'] == 0) {
11149 $pdf->SetPrintFooter(false);
11150 }
11151
11152 $pdf->AddPage();
11153 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11154 $pdf->lastPage();
11155 $pdf->Output($pathpdf, 'F');
11156
11157 if (!is_file($pathpdf)) {
11158 return false;
11159 }
11160
11161 if (VBOPlatformDetection::isWordPress()) {
11162 /**
11163 * @wponly - trigger files mirroring
11164 */
11165 VikBookingLoader::import('update.manager');
11166 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11167 }
11168
11169 // return the result of the generation of the PDF invoice
11170 return true;
11171 }
11172
11173 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
11174 {
11175 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
11176 return false;
11177 }
11178
11179 $dbo = JFactory::getDbo();
11180
11181 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
11182 $dbo->setQuery($q);
11183 $invoice_data = $dbo->loadAssoc();
11184
11185 if (!$invoice_data) {
11186 return false;
11187 }
11188
11189 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
11190 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
11191 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
11192 if (!file_exists($invoice_file_path)) {
11193 return false;
11194 }
11195
11196 $vbo_app = self::getVboApplication();
11197 $admin_sendermail = self::getSenderMail();
11198
11199 $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);
11200
11201 // update record
11202 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
11203 $dbo->setQuery($q);
11204 $dbo->execute();
11205
11206 return true;
11207 }
11208
11209 public static function loadCheckinDocTmpl(array $booking_info = [], array $booking_rooms = [], array $customer = [])
11210 {
11211 if (!defined('_VIKBOOKINGEXEC')) {
11212 define('_VIKBOOKINGEXEC', '1');
11213 }
11214
11215 ob_start();
11216 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php';
11217 $content = ob_get_contents();
11218 ob_end_clean();
11219
11220 $default_params = [
11221 'show_header' => 0,
11222 'header_data' => [],
11223 'show_footer' => 0,
11224 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
11225 'pdf_unit' => 'PDF_UNIT',
11226 'pdf_page_format' => 'PDF_PAGE_FORMAT',
11227 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
11228 'pdf_margin_top' => 'PDF_MARGIN_TOP',
11229 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
11230 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
11231 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
11232 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
11233 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
11234 'header_font_size' => '10',
11235 'body_font_size' => '10',
11236 'footer_font_size' => '8'
11237 ];
11238
11239 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
11240 $default_params = array_merge($default_params, $checkin_params);
11241 }
11242
11243 return [$content, $default_params];
11244 }
11245
11246 public static function parseCheckinDocTemplate(string $checkintpl, array $booking, array $booking_rooms, array $customer = [])
11247 {
11248 $dbo = JFactory::getDbo();
11249 $app = JFactory::getApplication();
11250
11251 // clone string
11252 $parsed = $checkintpl;
11253
11254 $datesep = self::getDateSeparator();
11255 $nowdf = self::getDateFormat();
11256 if ($nowdf == "%d/%m/%Y") {
11257 $df = 'd/m/Y';
11258 } elseif ($nowdf == "%m/%d/%Y") {
11259 $df = 'm/d/Y';
11260 } else {
11261 $df = 'Y/m/d';
11262 }
11263
11264 $company_name = self::getFrontTitle();
11265 $company_info = self::getInvoiceCompanyInfo();
11266 $companylogo = self::getSiteLogo();
11267 $uselogo = '';
11268 if (!empty($companylogo)) {
11269 /**
11270 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
11271 *
11272 * @since March 16th 2021
11273 */
11274 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11275 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
11276 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
11277 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11278 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
11279 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
11280 }
11281 }
11282
11283 $parsed = str_replace("{company_name}", $company_name, $parsed);
11284 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
11285 $parsed = str_replace("{company_info}", $company_info, $parsed);
11286 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
11287 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
11288 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
11289 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
11290
11291 $tot_guests = 0;
11292 $tot_adults = 0;
11293 $tot_children = 0;
11294 foreach ($booking_rooms as $kor => $or) {
11295 $tot_guests += ($or['adults'] + $or['children']);
11296 $tot_adults += $or['adults'];
11297 $tot_children += $or['children'];
11298 }
11299 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
11300 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
11301 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
11302 if ($customer && isset($customer['comments'])) {
11303 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
11304 }
11305
11306 $termsconds = self::getTermsConditions();
11307 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
11308
11309 // custom fields replacemenet
11310 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
11311 if (is_array($cmatches[1]) && $cmatches[1]) {
11312 $cfids = array();
11313 foreach ($cmatches[1] as $cfid) {
11314 $cfids[] = $cfid;
11315 }
11316 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
11317 $dbo->setQuery($q);
11318 $cfields = $dbo->loadAssocList();
11319 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
11320 $cfmap = array();
11321 foreach ($cfields as $cf) {
11322 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
11323 }
11324 $cfmapreplace = array();
11325 $partsreceived = explode("\n", $booking['custdata']);
11326 if (count($partsreceived) > 0) {
11327 foreach ($partsreceived as $pst) {
11328 if (!empty($pst)) {
11329 $tmpdata = explode(":", $pst);
11330 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
11331 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
11332 }
11333 }
11334 }
11335 }
11336 foreach ($cmatches[1] as $cfid) {
11337 if (array_key_exists($cfid, $cfmapreplace)) {
11338 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
11339 } else {
11340 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
11341 }
11342 }
11343 }
11344
11345 /**
11346 * Parse all conditional text rules.
11347 */
11348 self::getConditionalRulesInstance()
11349 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
11350 ->parseTokens($parsed);
11351
11352 /**
11353 * Parse all Door Access Control tags.
11354 *
11355 * @since 1.18.4 (J) - 1.8.4 (WP)
11356 */
11357 VBOFactory::getDoorAccessControl()
11358 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $parsed);
11359
11360 return $parsed;
11361 }
11362
11363 /**
11364 * Returns an array of key-value pairs to be used for building the Guest Details
11365 * in the Check-in process. The keys will be compared to the fields of the table
11366 * #__customers to see if some values already exist. The values can use lang defs
11367 * of both front-end or back-end. To be called as list(fields, attributes).
11368 *
11369 * @param boolean $precheckin true if requested for front-end pre check-in.
11370 * @param string $type optional type of custom pax fields to force.
11371 *
11372 * @return array key-value pairs for showing and collecting guest details.
11373 *
11374 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
11375 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
11376 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
11377 */
11378 public static function getPaxFields($precheckin = false, $type = null)
11379 {
11380 // check the type of pax fields collection data
11381 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
11382
11383 if (!$precheckin) {
11384 // back-end check-in ("registration") key-value pairs
11385
11386 // return the requested pax fields collection list
11387 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
11388
11389 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11390 // requested driver returned a list of fields
11391 return $custom_pax_fields;
11392 }
11393
11394 // in case the driver is unsupported, fallback to "basic" (default) driver
11395 return VBOCheckinPax::getFields('basic');
11396 }
11397
11398 // default front-end key-value pairs for pre check-in
11399 $precheckin_pax_fields = [
11400 [
11401 'first_name' => JText::translate('VBCCFIRSTNAME'),
11402 'last_name' => JText::translate('VBCCLASTNAME'),
11403 'date_birth' => JText::translate('ORDER_DBIRTH'),
11404 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
11405 'country' => JText::translate('ORDER_STATE'),
11406 'city' => JText::translate('ORDER_CITY'),
11407 'zip' => JText::translate('ORDER_ZIP'),
11408 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
11409 'gender' => JText::translate('VBOCUSTGENDER'),
11410 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
11411 'docnum' => JText::translate('VBOCUSTDOCNUM'),
11412 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
11413 ],
11414 [
11415 'first_name' => 'text',
11416 'last_name' => 'text',
11417 'date_birth' => 'calendar',
11418 'place_birth' => 'text',
11419 'country' => 'country',
11420 'city' => 'text',
11421 'zip' => 'text',
11422 'nationality' => 'country',
11423 'gender' => [
11424 'M' => JText::translate('VBOCUSTGENDERM'),
11425 'F' => JText::translate('VBOCUSTGENDERF'),
11426 ],
11427 'doctype' => 'text',
11428 'docnum' => 'text',
11429 'documents' => 'file',
11430 ],
11431 ];
11432
11433 // access the pre-checkin pax fields collection list from the current collector
11434 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
11435
11436 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11437 // set the pax fields for pre-checkin from the given data collection driver
11438 $precheckin_pax_fields = $custom_pax_fields;
11439 }
11440
11441 // make a safe copy of the default precheckin fields
11442 $use_precheckin_pax_fields = $precheckin_pax_fields;
11443
11444 /**
11445 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
11446 *
11447 * @since 1.16.3 (J) - 1.6.3 (WP)
11448 */
11449 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
11450
11451 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
11452 // use the filtered pax fields
11453 $use_precheckin_pax_fields = $precheckin_pax_fields;
11454 }
11455
11456 // return the front-end key-value pairs for pre check-in
11457 return $use_precheckin_pax_fields;
11458 }
11459
11460 /**
11461 * Returns the associative list of countries from the DB.
11462 *
11463 * @param bool $tn whether to translate the country name.
11464 * @param bool $no_id whether to unset the ID of the country.
11465 *
11466 * @return array associative or empty array.
11467 *
11468 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
11469 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
11470 */
11471 public static function getCountriesArray($tn = true, $no_id = true)
11472 {
11473 $dbo = JFactory::getDbo();
11474 $all_countries = [];
11475
11476 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
11477 $dbo->setQuery($q);
11478 $countries = $dbo->loadAssocList();
11479 if (!$countries) {
11480 return [];
11481 }
11482
11483 if ($tn === true) {
11484 $vbo_tn = self::getTranslator();
11485 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
11486 // re-apply sorting by country name
11487 $sorting = [];
11488 foreach ($countries as $country) {
11489 $sorting[$country['country_name']] = $country;
11490 }
11491 ksort($sorting);
11492 $sorted = [];
11493 foreach ($sorting as $country) {
11494 $sorted[] = $country;
11495 }
11496 $countries = $sorted;
11497 unset($sorting, $sorted);
11498 }
11499
11500 foreach ($countries as $v) {
11501 if ($no_id) {
11502 // keep the original structure by unsetting the ID only needed for translation
11503 unset($v['id']);
11504 }
11505 $all_countries[$v['country_3_code']] = $v;
11506 }
11507
11508 return $all_countries;
11509 }
11510
11511 public static function getCountriesSelect(string $name, array $all_countries = [], ?string $current_value = '', ?string $empty_value = ' ', ?string $idAttr = null)
11512 {
11513 if (!$all_countries) {
11514 $all_countries = self::getCountriesArray();
11515 }
11516
11517 $countries = '<select ' . ($idAttr ? 'id="' . $idAttr . '" ' : '') . 'name="' . $name . '">' . "\n";
11518 if (strlen($empty_value)) {
11519 $countries .= '<option value="">' . $empty_value . '</option>' . "\n";
11520 }
11521 foreach ($all_countries as $v) {
11522 $countries .= '<option value="' . $v['country_3_code'] . '"' . ($v['country_3_code'] == $current_value ? ' selected="selected"' : '') . '>' . $v['country_name'] . '</option>' . "\n";
11523 }
11524 $countries .= '</select>';
11525
11526 return $countries;
11527 }
11528
11529 public static function getThumbSize($skipsession = false)
11530 {
11531 static $thumb_size = null;
11532
11533 if ($thumb_size) {
11534 return $thumb_size;
11535 }
11536
11537 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11538
11539 return $thumb_size;
11540 }
11541
11542 /**
11543 * Checks whether an iCal file for the reservation should be
11544 * attached to the confirmation email for customer and/or admin.
11545 *
11546 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11547 *
11548 * @since 1.12.0 (J) - 1.2.0 (WP)
11549 */
11550 public static function attachIcal()
11551 {
11552 return VBOFactory::getConfig()->getInt('attachical', 1);
11553 }
11554
11555 /**
11556 * How the calculation of the orphan dates should take place.
11557 *
11558 * @return string "next" for only checking the bookings ahead, "prevnext" if
11559 * also the previous bookings should be checked.
11560 *
11561 * @since 1.13 (J) - 1.3.0 (WP)
11562 */
11563 public static function orphansCalculation()
11564 {
11565 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11566 }
11567
11568 /**
11569 * Returns the name of the template file to use for the front-end View "search".
11570 *
11571 * @return string the name of the template file. New and upgraded user will both use the classic file.
11572 *
11573 * @since 1.13 (J) - 1.3.0 (WP)
11574 */
11575 public static function searchResultsTmpl()
11576 {
11577 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11578 }
11579
11580 /**
11581 * Returns a string without any new-line characters
11582 * to be used for JavaScript values without facing
11583 * errors like 'unterminated string literal'.
11584 * By passing nl2br($str) as argument, we can keep
11585 * the wanted new-line HTML tags for PRE tags.
11586 * We use implode() with just one argument to
11587 * not use an empty string as glue for the string.
11588 *
11589 * @param $str string
11590 *
11591 * @return string
11592 */
11593 public static function strTrimLiteral($str) {
11594 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11595 $lines = explode("\n", $str);
11596 $new_lines = array();
11597 foreach ($lines as $i => $line) {
11598 if (strlen($line)) {
11599 $new_lines[] = trim($line);
11600 }
11601 }
11602 return implode($new_lines);
11603 }
11604
11605 /**
11606 * Returns an instance of the VboApplication class.
11607 *
11608 * @return VboApplication
11609 */
11610 public static function getVboApplication()
11611 {
11612 // require library
11613 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11614
11615 // return a new instance
11616 return new VboApplication();
11617 }
11618
11619 /**
11620 * Returns the translation of the given 0-indexed week day.
11621 *
11622 * @param int $wd the 0-indexed day of the week.
11623 * @param bool $short whether to get the short version of the weekday.
11624 *
11625 * @return string the name of the week day.
11626 *
11627 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11628 */
11629 public static function sayWeekDay($wd, $short = false)
11630 {
11631 switch ($wd) {
11632 case '6' :
11633 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11634 break;
11635 case '5' :
11636 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11637 break;
11638 case '4' :
11639 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11640 break;
11641 case '3' :
11642 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11643 break;
11644 case '2' :
11645 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11646 break;
11647 case '1' :
11648 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11649 break;
11650 default :
11651 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11652 break;
11653 }
11654 return $ret;
11655 }
11656
11657 /**
11658 * Returns the translation of the given 1-indexed month of the year.
11659 *
11660 * @param int $idm the 1-indexed month of the year.
11661 * @param bool $short whether to get the short version of the month.
11662 *
11663 * @return string the name of the month.
11664 *
11665 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11666 */
11667 public static function sayMonth($idm, $short = false)
11668 {
11669 switch ($idm) {
11670 case '12' :
11671 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11672 break;
11673 case '11' :
11674 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11675 break;
11676 case '10' :
11677 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11678 break;
11679 case '9' :
11680 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11681 break;
11682 case '8' :
11683 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11684 break;
11685 case '7' :
11686 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11687 break;
11688 case '6' :
11689 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11690 break;
11691 case '5' :
11692 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11693 break;
11694 case '4' :
11695 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11696 break;
11697 case '3' :
11698 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11699 break;
11700 case '2' :
11701 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11702 break;
11703 default :
11704 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11705 break;
11706 }
11707 return $ret;
11708 }
11709
11710 public static function sayDayMonth($d) {
11711 switch ($d) {
11712 case '31' :
11713 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11714 break;
11715 case '30' :
11716 $ret = JText::translate('VBDAYMONTHTHIRTY');
11717 break;
11718 case '29' :
11719 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11720 break;
11721 case '28' :
11722 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11723 break;
11724 case '27' :
11725 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11726 break;
11727 case '26' :
11728 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11729 break;
11730 case '25' :
11731 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11732 break;
11733 case '24' :
11734 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11735 break;
11736 case '23' :
11737 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11738 break;
11739 case '22' :
11740 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11741 break;
11742 case '21' :
11743 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11744 break;
11745 case '20' :
11746 $ret = JText::translate('VBDAYMONTHTWENTY');
11747 break;
11748 case '19' :
11749 $ret = JText::translate('VBDAYMONTHNINETEEN');
11750 break;
11751 case '18' :
11752 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11753 break;
11754 case '17' :
11755 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11756 break;
11757 case '16' :
11758 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11759 break;
11760 case '15' :
11761 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11762 break;
11763 case '14' :
11764 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11765 break;
11766 case '13' :
11767 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11768 break;
11769 case '12' :
11770 $ret = JText::translate('VBDAYMONTHTWELVE');
11771 break;
11772 case '11' :
11773 $ret = JText::translate('VBDAYMONTHELEVEN');
11774 break;
11775 case '10' :
11776 $ret = JText::translate('VBDAYMONTHTEN');
11777 break;
11778 case '9' :
11779 $ret = JText::translate('VBDAYMONTHNINE');
11780 break;
11781 case '8' :
11782 $ret = JText::translate('VBDAYMONTHEIGHT');
11783 break;
11784 case '7' :
11785 $ret = JText::translate('VBDAYMONTHSEVEN');
11786 break;
11787 case '6' :
11788 $ret = JText::translate('VBDAYMONTHSIX');
11789 break;
11790 case '5' :
11791 $ret = JText::translate('VBDAYMONTHFIVE');
11792 break;
11793 case '4' :
11794 $ret = JText::translate('VBDAYMONTHFOUR');
11795 break;
11796 case '3' :
11797 $ret = JText::translate('VBDAYMONTHTHREE');
11798 break;
11799 case '2' :
11800 $ret = JText::translate('VBDAYMONTHTWO');
11801 break;
11802 default :
11803 $ret = JText::translate('VBDAYMONTHONE');
11804 break;
11805 }
11806 return $ret;
11807 }
11808
11809 public static function totElements($arr) {
11810 $n = 0;
11811 if (is_array($arr)) {
11812 foreach ($arr as $a) {
11813 if (!empty($a)) {
11814 $n++;
11815 }
11816 }
11817 return $n;
11818 }
11819 return false;
11820 }
11821
11822 /**
11823 * Returns a list of documents that were uploaded
11824 * for the specified customer.
11825 *
11826 * @param integer $id The customer ID.
11827 *
11828 * @return array A list of documents.
11829 *
11830 * @since 1.3.0
11831 */
11832 public static function getCustomerDocuments($id)
11833 {
11834 $dbo = JFactory::getDbo();
11835
11836 $q = $dbo->getQuery(true)
11837 ->select($dbo->qn('docsfolder'))
11838 ->from($dbo->qn('#__vikbooking_customers'))
11839 ->where($dbo->qn('id') . ' = ' . (int) $id);
11840
11841 $dbo->setQuery($q, 0, 1);
11842 $dbo->execute();
11843
11844 if (!$dbo->getNumRows())
11845 {
11846 // customer not found
11847 return array();
11848 }
11849
11850 // retrieve customer documents directory name
11851 $dirname = $dbo->loadResult();
11852
11853 if (empty($dirname))
11854 {
11855 // no available directory
11856 return array();
11857 }
11858
11859 // build documents folder path
11860 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11861
11862 if (!is_dir($dirname))
11863 {
11864 // the customer directory doesn't exist
11865 return array();
11866 }
11867
11868 // read all files from customer directory
11869 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11870
11871 $files = array();
11872
11873 foreach ($glob as $path)
11874 {
11875 // skip "index.html"
11876 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11877 {
11878 // extract name and extension from file path
11879 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11880 {
11881 $name = $match[1];
11882 $ext = $match[2];
11883 }
11884 else
11885 {
11886 $name = basename($path);
11887 $ext = '';
11888 }
11889
11890 $file = new stdClass;
11891 $file->path = $path;
11892 $file->name = $name;
11893 $file->ext = $ext;
11894 $file->basename = $file->name . '.' . $file->ext;
11895 $file->size = filesize($path);
11896 $file->date = filemtime($path);
11897 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11898
11899 $files[] = $file;
11900 }
11901 }
11902
11903 // sort files by creation date
11904 usort($files, function($a, $b)
11905 {
11906 return $b->date - $a->date;
11907 });
11908
11909 return $files;
11910 }
11911
11912 public static function displayPaymentParameters($pfile, $pparams = '') {
11913 $html = '<p>---------</p>';
11914
11915 /**
11916 * @wponly The payment gateway is now loaded
11917 * using the apposite dispatcher.
11918 *
11919 * @since 1.0.5
11920 */
11921 JLoader::import('adapter.payment.dispatcher');
11922
11923 try
11924 {
11925 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
11926 }
11927 catch (Exception $e)
11928 {
11929 // payment not found
11930 $html = $e->getMessage();
11931
11932 if ($code = $e->getCode())
11933 {
11934 $html = '<b>' . $code . '</b> : ' . $html;
11935 }
11936
11937 return $html;
11938 }
11939 //
11940
11941 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
11942 $arrparams = !is_array($arrparams) ? array() : $arrparams;
11943
11944 // get admin parameters
11945 $pconfig = $payment->getAdminParameters();
11946
11947 if (count($pconfig) > 0) {
11948 $html = '';
11949 foreach ($pconfig as $value => $cont) {
11950 if (empty($value)) {
11951 continue;
11952 }
11953 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11954 $label = $labelparts[0];
11955 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11956 if (!empty($cont['help'])) {
11957 $labelhelp = $cont['help'];
11958 }
11959 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11960 $html .= '<div class="vbo-param-container">';
11961 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11962 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11963 }
11964 $html .= '<div class="vbo-param-setting">';
11965 switch ($cont['type']) {
11966 case 'custom':
11967 $html .= $cont['html'];
11968 break;
11969 case 'select':
11970 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11971 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11972 if (isset($cont['multiple']) && $cont['multiple']) {
11973 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
11974 } else {
11975 $html .= '<select name="vikpaymentparams['.$value.']">';
11976 }
11977 foreach ($options as $optkey => $poption) {
11978 $checkval = $is_assoc ? $optkey : $poption;
11979 $selected = false;
11980 if (isset($arrparams[$value])) {
11981 if (is_array($arrparams[$value])) {
11982 $selected = in_array($checkval, $arrparams[$value]);
11983 } else {
11984 $selected = ($checkval == $arrparams[$value]);
11985 }
11986 } elseif (isset($default_paramv)) {
11987 if (is_array($default_paramv)) {
11988 $selected = in_array($checkval, $default_paramv);
11989 } else {
11990 $selected = ($checkval == $default_paramv);
11991 }
11992 }
11993 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11994 }
11995 $html .= '</select>';
11996 break;
11997 case 'password':
11998 $html .= '<div class="btn-wrapper input-append">';
11999 $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"/>';
12000 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12001 $html .= '</div>';
12002 break;
12003 case 'number':
12004 $number_attr = array();
12005 if (isset($cont['min'])) {
12006 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12007 }
12008 if (isset($cont['max'])) {
12009 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12010 }
12011 if (isset($cont['step'])) {
12012 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12013 }
12014 $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) . '/>';
12015 break;
12016 case 'textarea':
12017 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12018 break;
12019 case 'hidden':
12020 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
12021 break;
12022 case 'checkbox':
12023 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12024 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
12025 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
12026 break;
12027 default:
12028 $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"/>';
12029 break;
12030 }
12031 if (strlen($labelhelp) > 0) {
12032 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12033 }
12034 $html .= '</div>';
12035 $html .= '</div>';
12036 }
12037 // JS helper function to toggle the password fields
12038 $html .= "\n" . '<script>' . "\n";
12039 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
12040 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12041 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12042 $html .= ' var inp_type = inp.attr("type");' . "\n";
12043 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12044 $html .= '}' . "\n";
12045 $html .= "\n" . '</script>' . "\n";
12046 }
12047
12048 return $html;
12049 }
12050
12051 public static function displaySMSParameters($pfile, $params = null)
12052 {
12053 $html = '---------';
12054
12055 $params = is_string($params) ? json_decode($params, true) : $params;
12056 $params = is_array($params) ? $params : [];
12057
12058 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
12059 return $html;
12060 }
12061
12062 // attempt to load the class file
12063 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
12064
12065 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
12066 return $html;
12067 }
12068
12069 // load the gateway parameters
12070 $config = VikSmsApi::getAdminParameters();
12071
12072 if (!is_array($config) || !count($config)) {
12073 return $html;
12074 }
12075
12076 // flags for JS helpers
12077 $js_helpers = array();
12078
12079 $html = '';
12080 foreach ($config as $value => $cont) {
12081 if (empty($value)) {
12082 continue;
12083 }
12084 $inp_attr = '';
12085 if (isset($cont['attributes'])) {
12086 foreach ($cont['attributes'] as $inpk => $inpv) {
12087 $inp_attr .= $inpk.'="'.$inpv.'" ';
12088 }
12089 $inp_attr = ' ' . rtrim($inp_attr);
12090 }
12091 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12092 $label = $labelparts[0];
12093 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12094 if (!empty($cont['help'])) {
12095 $labelhelp = $cont['help'];
12096 }
12097 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12098 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
12099 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12100 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12101 }
12102 $html .= '<div class="vbo-param-setting">';
12103 switch ($cont['type']) {
12104 case 'custom':
12105 $html .= $cont['html'];
12106 break;
12107 case 'select':
12108 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12109 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12110 if (isset($cont['multiple']) && $cont['multiple']) {
12111 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
12112 } else {
12113 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
12114 }
12115 foreach ($options as $optkey => $poption) {
12116 $checkval = $is_assoc ? $optkey : $poption;
12117 $selected = false;
12118 if (isset($params[$value])) {
12119 if (is_array($params[$value])) {
12120 $selected = in_array($checkval, $params[$value]);
12121 } else {
12122 $selected = ($checkval == $params[$value]);
12123 }
12124 } elseif (isset($default_paramv)) {
12125 if (is_array($default_paramv)) {
12126 $selected = in_array($checkval, $default_paramv);
12127 } else {
12128 $selected = ($checkval == $default_paramv);
12129 }
12130 }
12131 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12132 }
12133 $html .= '</select>';
12134 break;
12135 case 'password':
12136 $html .= '<div class="btn-wrapper input-append">';
12137 $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 . '/>';
12138 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12139 $html .= '</div>';
12140 // set flag for JS helper
12141 $js_helpers[] = $cont['type'];
12142 break;
12143 case 'number':
12144 $number_attr = array();
12145 if (isset($cont['min'])) {
12146 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12147 }
12148 if (isset($cont['max'])) {
12149 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12150 }
12151 if (isset($cont['step'])) {
12152 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12153 }
12154 $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 . '/>';
12155 break;
12156 case 'textarea':
12157 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12158 break;
12159 case 'visual_html':
12160 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
12161 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
12162 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
12163 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
12164 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
12165 break;
12166 case 'hidden':
12167 $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 . '/>';
12168 break;
12169 case 'checkbox':
12170 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12171 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
12172 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
12173 break;
12174 default:
12175 $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 . '/>';
12176 break;
12177 }
12178 if (strlen($labelhelp) > 0) {
12179 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12180 }
12181 $html .= '</div>';
12182 $html .= '</div>';
12183 }
12184
12185 // JS helper functions
12186 if (in_array('password', $js_helpers)) {
12187 // toggle the password fields
12188 $html .= "\n" . '<script>' . "\n";
12189 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
12190 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12191 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12192 $html .= ' var inp_type = inp.attr("type");' . "\n";
12193 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12194 $html .= '}' . "\n";
12195 $html .= "\n" . '</script>' . "\n";
12196 }
12197
12198 return $html;
12199 }
12200
12201 /**
12202 * Renders the params of the requested cron job file-class.
12203 *
12204 * @param string $file the name of the cron job driver file.
12205 * @param array $params the parameters array.
12206 *
12207 * @return string the necessary HTML content to render.
12208 *
12209 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
12210 */
12211 public static function displayCronParameters($file, $params = [])
12212 {
12213 try
12214 {
12215 // attempt to create a new instance
12216 $job = VBOFactory::getCronFactory()->createInstance($file);
12217 }
12218 catch (Exception $e)
12219 {
12220 // something went wrong, display error message
12221 return '<p>' . $e->getMessage() . '<p>';
12222 }
12223
12224 // get admin parameters
12225 $config = $job->getForm();
12226
12227 if (!is_array($config) || !$config) {
12228 return '<p>---------</p>';
12229 }
12230
12231 /**
12232 * Render the cron parameters through the global params rendering class.
12233 *
12234 * @since 1.16.9 (J) - 1.6.9 (WP)
12235 */
12236 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
12237 }
12238
12239 public static function invokeChannelManager($skiporder = true, $order = array()) {
12240 $task = VikRequest::getString('task', '', 'request');
12241 $view = VikRequest::getString('view', '', 'request');
12242 $tmpl = VikRequest::getString('tmpl', '', 'request');
12243 $noimpression = array('vieworder', 'booking');
12244 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')) {
12245 //VCM Channel Impression
12246 if (!class_exists('VikChannelManagerConfig')) {
12247 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12248 }
12249 if (!class_exists('VikChannelManager')) {
12250 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12251 }
12252 VikChannelManager::invokeChannelImpression();
12253 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
12254 //VCM Channel Conversion-Impression
12255 if (!class_exists('VikChannelManagerConfig')) {
12256 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12257 }
12258 if (!class_exists('VikChannelManager')) {
12259 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12260 }
12261 VikChannelManager::invokeChannelConversionImpression($order);
12262 }
12263 }
12264
12265 public static function validEmail($email) {
12266 $isValid = true;
12267 $atIndex = strrpos($email, "@");
12268 if (is_bool($atIndex) && !$atIndex) {
12269 $isValid = false;
12270 } else {
12271 $domain = substr($email, $atIndex +1);
12272 $local = substr($email, 0, $atIndex);
12273 $localLen = strlen($local);
12274 $domainLen = strlen($domain);
12275 if ($localLen < 1 || $localLen > 64) {
12276 // local part length exceeded
12277 $isValid = false;
12278 } else
12279 if ($domainLen < 1 || $domainLen > 255) {
12280 // domain part length exceeded
12281 $isValid = false;
12282 } else
12283 if ($local[0] == '.' || $local[$localLen -1] == '.') {
12284 // local part starts or ends with '.'
12285 $isValid = false;
12286 } else
12287 if (preg_match('/\\.\\./', $local)) {
12288 // local part has two consecutive dots
12289 $isValid = false;
12290 } else
12291 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
12292 // character not valid in domain part
12293 $isValid = false;
12294 } else
12295 if (preg_match('/\\.\\./', $domain)) {
12296 // domain part has two consecutive dots
12297 $isValid = false;
12298 } else
12299 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
12300 // character not valid in local part unless
12301 // local part is quoted
12302 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
12303 $isValid = false;
12304 }
12305 }
12306 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
12307 // domain not found in DNS
12308 $isValid = false;
12309 }
12310 }
12311 return $isValid;
12312 }
12313
12314 public static function caniWrite($path) {
12315 if ($path[strlen($path) - 1] == '/') {
12316 // ricorsivo return a temporary file path
12317 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
12318 }
12319 if (is_dir($path)) {
12320 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
12321 }
12322 // check tmp file for read/write capabilities
12323 $rm = file_exists($path);
12324 $f = @fopen($path, 'a');
12325 if ($f === false) {
12326 return false;
12327 }
12328 fclose($f);
12329 if (!$rm) {
12330 unlink($path);
12331 }
12332 return true;
12333 }
12334
12335 /**
12336 * Alias method of JFile::upload to unify any
12337 * upload function into one.
12338 *
12339 * @param string $src The name of the php (temporary) uploaded file.
12340 * @param string $dest The path (including filename) to move the uploaded file to.
12341 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
12342 *
12343 * @return boolean True on success.
12344 *
12345 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
12346 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
12347 */
12348 public static function uploadFile($src, $dest, $copy_only = false) {
12349 // always attempt to include the File class
12350 jimport('joomla.filesystem.file');
12351
12352 // upload the file
12353 if (!$copy_only) {
12354 $result = JFile::upload($src, $dest);
12355 } else {
12356 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
12357 $result = JFile::copy($src, $dest);
12358 }
12359
12360 /**
12361 * @wponly in order to not lose uploaded files after installing an update,
12362 * we need to move any uploaded file onto a recovery folder.
12363 */
12364 if ($result) {
12365 VikBookingLoader::import('update.manager');
12366 VikBookingUpdateManager::triggerUploadBackup($dest);
12367 }
12368 //
12369
12370 // return upload result
12371 return $result;
12372 }
12373
12374 /**
12375 * Helper method used to upload the given file (retrieved from $_FILES)
12376 * into the specified destination.
12377 *
12378 * @param array $file An associative array with the file details.
12379 * @param string $dest The destination path.
12380 * @param string $filters A string (or a regex) containing the allowed extensions.
12381 *
12382 * @return array The uploading result.
12383 *
12384 * @throws RuntimeException
12385 *
12386 * @since 1.3.0
12387 */
12388 public static function uploadFileFromRequest($file, $dest, $filters = '*')
12389 {
12390 jimport('joomla.filesystem.file');
12391
12392 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
12393
12394 if (empty($file['name']))
12395 {
12396 throw new RuntimeException('Missing file', 400);
12397 }
12398
12399 /**
12400 * Make sure the upload of the file didn't raise any errors.
12401 *
12402 * @link https://www.php.net/manual/en/features.file-upload.errors.php
12403 */
12404 if ((int) $file['error'])
12405 {
12406 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
12407 {
12408 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
12409 }
12410 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
12411 {
12412 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
12413 }
12414 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
12415 {
12416 $error = 'The uploaded file was only partially uploaded.';
12417 }
12418 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
12419 {
12420 $error = 'No file was uploaded.';
12421 }
12422 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
12423 {
12424 $error = 'Missing a temporary folder.';
12425 }
12426 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
12427 {
12428 $error = 'Failed to write file to disk.';
12429 }
12430 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
12431 {
12432 $error = 'A PHP extension stopped the file upload.';
12433 }
12434 else
12435 {
12436 $error = 'Unknown error.';
12437 }
12438
12439 throw new RuntimeException($error, 500);
12440 }
12441
12442 $src = $file['tmp_name'];
12443
12444 // extract file name and extension
12445 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
12446 {
12447 $filename = $match[1];
12448 $fileext = $match[2];
12449 }
12450 else
12451 {
12452 // probably no extension provided
12453 $filename = basename($file['name']);
12454 $fileext = '';
12455 }
12456
12457 $j = '';
12458
12459 if (file_exists($dest . $filename . $fileext))
12460 {
12461 $j = 2;
12462
12463 while (file_exists($dest . $filename . '-' . $j . $fileext))
12464 {
12465 $j++;
12466 }
12467
12468 $j = '-' . $j;
12469 }
12470
12471 $finaldest = $dest . $filename . $j . $fileext;
12472
12473 // make sure the file extension is supported
12474 if (!self::isFileTypeCompatible(basename($finaldest), $filters))
12475 {
12476 // extension not supported
12477 throw new RuntimeException(sprintf('Extension [%s] is not supported', $fileext), 400);
12478 }
12479
12480 // try to upload the file
12481 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
12482 {
12483 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
12484 }
12485
12486 $file = new stdClass;
12487 $file->name = $filename . $j;
12488 $file->ext = ltrim($fileext, '.');
12489 $file->filename = basename($finaldest);
12490 $file->path = $finaldest;
12491
12492 return $file;
12493 }
12494
12495 /**
12496 * Helper method used to check whether the given file name
12497 * supports one of the given filters.
12498 *
12499 * @param mixed $file Either the file name or the uploaded file.
12500 * @param string $filters Either a regex or a comma-separated list of supported extensions.
12501 * The regex must be inclusive of delimiters.
12502 *
12503 * @return bool True if supported, false otherwise.
12504 *
12505 * @since 1.7.3 (WP) - 1.17.3 (J)
12506 */
12507 public static function isFileTypeCompatible($file, $filters)
12508 {
12509 // make sure the filters query is not empty
12510 if (strlen($filters) == 0)
12511 {
12512 // cannot assert whether the file could be accepted or not
12513 return false;
12514 }
12515
12516 // check whether all the files are accepted
12517 if ($filters == '*')
12518 {
12519 return true;
12520 }
12521
12522 // use the file MIME TYPE in case of array
12523 if (is_array($file))
12524 {
12525 $file = $file['type'];
12526 }
12527
12528 // check if we are dealing with a regex
12529 if (static::isRegex($filters))
12530 {
12531 return (bool) preg_match($filters, $file);
12532 }
12533
12534 // fallback to comma-separated list
12535 $types = array_filter(preg_split("/\s*,\s*/", $filters));
12536
12537 foreach ($types as $t)
12538 {
12539 // remove initial dot if specified
12540 $t = ltrim($t, '.');
12541 // escape slashes to avoid breaking the regex
12542 $t = preg_replace("/\//", '\/', $t);
12543
12544 // check if the file ends with the given extension
12545 if (preg_match("/{$t}$/i", $file))
12546 {
12547 return true;
12548 }
12549 }
12550
12551 return false;
12552 }
12553
12554 /**
12555 * Checks whether the given string is a structured PCRE regex.
12556 * It simply makes sure that the string owns valid delimiters.
12557 * A delimiter can be any non-alphanumeric, non-backslash,
12558 * non-whitespace character.
12559 *
12560 * @param string $str The string to check.
12561 *
12562 * @return boolean True if a regex, false otherwise.
12563 *
12564 * @since 1.7.3 (WP) - 1.17.3 (J)
12565 */
12566 public static function isRegex($str)
12567 {
12568 // first of all make sure the first character is a supported delimiter
12569 if (!preg_match("/^([!#$%&'*+,.\/:;=?@^_`|~\-(\[{<\"])/", $str, $match))
12570 {
12571 // no valid delimiter
12572 return false;
12573 }
12574
12575 // get delimiter
12576 $d = $match[1];
12577
12578 // lookup used to check if we should take a different ending delimiter
12579 $lookup = array(
12580 '{' => '}',
12581 '[' => ']',
12582 '(' => ')',
12583 '<' => '>',
12584 );
12585
12586 if (isset($lookup[$d]))
12587 {
12588 $d = $lookup[$d];
12589 }
12590
12591 // make sure the regex ends with the delimiter found
12592 return (bool) preg_match("/\\{$d}[gimsxU]*$/", $str);
12593 }
12594
12595 /**
12596 * Gets the instance of a specific report class.
12597 *
12598 * @param string $report the name of the report to load.
12599 *
12600 * @return mixed false or report object instance.
12601 *
12602 * @since 1.3.0
12603 */
12604 public static function getReportInstance($report)
12605 {
12606 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12607
12608 return VikBookingReport::getInstanceOf($report);
12609 }
12610
12611 /**
12612 * Checks whether the guest reviews are enabled and VCM is installed.
12613 *
12614 * @return boolean true if enabled, false otherwise.
12615 *
12616 * @since 1.3.0
12617 */
12618 public static function allowGuestReviews()
12619 {
12620 $dbo = JFactory::getDbo();
12621 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12622 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12623 $dbo->setQuery($q);
12624 $dbo->execute();
12625 if ($dbo->getNumRows()) {
12626 $s = $dbo->loadResult();
12627 return ((int)$s === 1 && $vcm_installed);
12628 }
12629 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12630 $dbo->setQuery($q);
12631 $dbo->execute();
12632 return $vcm_installed;
12633 }
12634
12635 /**
12636 * Gets the minimum chars for the guest review message.
12637 *
12638 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12639 *
12640 * @since 1.3.0
12641 */
12642 public static function guestReviewMinChars()
12643 {
12644 $dbo = JFactory::getDbo();
12645 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12646 $dbo->setQuery($q);
12647 $dbo->execute();
12648 if ($dbo->getNumRows() > 0) {
12649 return (int)$dbo->loadResult();
12650 }
12651 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12652 $dbo->setQuery($q);
12653 $dbo->execute();
12654 return 15;
12655 }
12656
12657 /**
12658 * The approval type for new guest reviews.
12659 *
12660 * @return string auto or manual.
12661 *
12662 * @since 1.3.0
12663 */
12664 public static function guestReviewsApproval()
12665 {
12666 $dbo = JFactory::getDbo();
12667 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12668 $dbo->setQuery($q);
12669 $dbo->execute();
12670 if ($dbo->getNumRows() > 0) {
12671 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12672 }
12673 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12674 $dbo->setQuery($q);
12675 $dbo->execute();
12676 return 'auto';
12677 }
12678
12679 /**
12680 * The type of reviews guests should leave.
12681 *
12682 * @return string global or service.
12683 *
12684 * @since 1.3.0
12685 */
12686 public static function guestReviewsType()
12687 {
12688 $dbo = JFactory::getDbo();
12689 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12690 $dbo->setQuery($q);
12691 $dbo->execute();
12692 if ($dbo->getNumRows() > 0) {
12693 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12694 }
12695 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12696 $dbo->setQuery($q);
12697 $dbo->execute();
12698 return 'service';
12699 }
12700
12701 /**
12702 * The services to be reviewed by the guests.
12703 *
12704 * @return array list, empty or not, of the services to be reviewed.
12705 *
12706 * @since 1.3.0
12707 */
12708 public static function guestReviewsServices()
12709 {
12710 $dbo = JFactory::getDbo();
12711 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12712 $dbo->setQuery($q);
12713 $dbo->execute();
12714 if ($dbo->getNumRows()) {
12715 return $dbo->loadAssocList();
12716 }
12717 // insert the default services
12718 $default_services = array(
12719 JText::translate('VBOGREVVALUE'),
12720 JText::translate('VBOGREVLOCATION'),
12721 JText::translate('VBOGREVSTAFF'),
12722 JText::translate('VBOGREVCLEAN'),
12723 JText::translate('VBOGREVCOMFORT'),
12724 JText::translate('VBOGREVFACILITIES'),
12725 );
12726 foreach ($default_services as $def_service) {
12727 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12728 $dbo->setQuery($q);
12729 $dbo->execute();
12730 }
12731 return array();
12732 }
12733
12734 /**
12735 * Checks whether the guest reviews should be downloaded (max 1 per day).
12736 * VCM must be installed in order to download the new reviews.
12737 *
12738 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12739 *
12740 * @since 1.3.0
12741 */
12742 public static function shouldDownloadReviews()
12743 {
12744 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12745 return -1;
12746 }
12747
12748 $today = date('Y-m-d');
12749
12750 $dbo = JFactory::getDbo();
12751 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12752 $dbo->setQuery($q);
12753 $dbo->execute();
12754 if ($dbo->getNumRows()) {
12755 $last_download = $dbo->loadResult();
12756 // update last download day
12757 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12758 $dbo->setQuery($q);
12759 $dbo->execute();
12760 //
12761 return ($last_download != $today ? 1 : 0);
12762 }
12763 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12764 $dbo->setQuery($q);
12765 $dbo->execute();
12766 return 1;
12767 }
12768
12769 /**
12770 * Tells whether a booking can be reviewed.
12771 *
12772 * @param array $booking booking array details.
12773 *
12774 * @return boolean true if a review can be left, false otherwise.
12775 *
12776 * @since 1.3.0
12777 */
12778 public static function canBookingBeReviewed($booking)
12779 {
12780 // reviews must be enabled and supported
12781 if (!self::allowGuestReviews()) {
12782 return false;
12783 }
12784
12785 // booking status must be confirmed
12786 if ($booking['status'] != 'confirmed') {
12787 return false;
12788 }
12789
12790 // review can be left starting from the check-out day
12791 $checkout_info = getdate($booking['checkout']);
12792 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12793 if (time() < $checkout_midn) {
12794 return false;
12795 }
12796
12797 // make sure a review for this booking does not exist
12798 $noreview = true;
12799 $dbo = JFactory::getDbo();
12800 try {
12801 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12802 $dbo->setQuery($q);
12803 $dbo->execute();
12804 $noreview = ($dbo->getNumRows() < 1);
12805 } catch (Exception $e) {
12806 // if the query fails, we do not allow the review to be left
12807 $noreview = false;
12808 }
12809
12810 return $noreview;
12811 }
12812
12813 /**
12814 * Gets the review for a booking, no matter if it's published or not.
12815 * Review will be returned only if reviews are enabled and supported.
12816 * Only reviews left through the website, no OTA reviews as this method
12817 * should be used in the front-end to display the review to the guest.
12818 *
12819 * @param array $booking booking array details.
12820 *
12821 * @return mixed associative array or false.
12822 *
12823 * @since 1.3.0
12824 */
12825 public static function getBookingReview($booking)
12826 {
12827 // reviews must be enabled and supported
12828 if (!self::allowGuestReviews()) {
12829 return false;
12830 }
12831
12832 if (empty($booking['id'])) {
12833 return false;
12834 }
12835
12836 // make sure a review for this booking exists
12837 $dbo = JFactory::getDbo();
12838 $review = array();
12839 try {
12840 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12841 $dbo->setQuery($q);
12842 $dbo->execute();
12843 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12844 } catch (Exception $e) {
12845 // query has failed
12846 $review = array();
12847 }
12848
12849 if (count($review)) {
12850 // make sure the review was left from the website
12851 $review['uniquekey'] = (int)$review['uniquekey'];
12852 }
12853
12854 if (count($review) && !empty($review['uniquekey'])) {
12855 // this is an OTA review
12856 return false;
12857 }
12858
12859 if (count($review) && !empty($review['content'])) {
12860 // decode content
12861 $review['content'] = json_decode($review['content'], true);
12862 }
12863
12864 return count($review) ? $review : false;
12865 }
12866
12867 /**
12868 * Attempts to find a translation for a raw customer data label.
12869 * Bookings downloaded from the OTAs will save a raw string of information
12870 * composed of pairs of label-value separated by new line feeds.
12871 * We try to translate the given label into the current language.
12872 *
12873 * @param string $label the raw string label to translate.
12874 *
12875 * @return string either the original or the translated label.
12876 *
12877 * @since 1.3.5
12878 * @since 1.4.0 back-end support added.
12879 */
12880 public static function tnCustomerRawDataLabel($label)
12881 {
12882 // this is a map of the known labels
12883 $known_lbls = array(
12884 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12885 'LASTNAME' => 'ORDER_LNAME',
12886 'COUNTRY' => 'ORDER_STATE',
12887 'EMAIL' => 'VBMAIL',
12888 'TELEPHONE' => 'VBPHONE',
12889 'PHONE' => 'VBPHONE',
12890 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12891 'MEAL_PLAN' => 'VBOMEALPLAN',
12892 'CITY' => 'VBCITY',
12893 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12894 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12895 );
12896
12897 if (self::isAdmin()) {
12898 // override the map of known translation strings
12899 $known_lbls = array(
12900 'NAME' => 'ORDER_NAME',
12901 'LASTNAME' => 'ORDER_LNAME',
12902 'COUNTRY' => 'ORDER_STATE',
12903 'ADDRESS' => 'ORDER_ADDRESS',
12904 'CITY' => 'ORDER_CITY',
12905 'LOCATION' => 'ORDER_CITY',
12906 'EMAIL' => 'ORDER_EMAIL',
12907 'TELEPHONE' => 'ORDER_PHONE',
12908 'PHONE' => 'ORDER_PHONE',
12909 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12910 );
12911 }
12912
12913 // we get rid of any empty space by keeping the underscores
12914 $converted = str_replace(' ', '', strtoupper($label));
12915
12916 if (isset($known_lbls[$converted])) {
12917 // this language definition has been mapped
12918 return JText::translate($known_lbls[$converted]);
12919 }
12920
12921 // we try to guess the translation string by prepending VBO
12922 $guessed = JText::translate('VBO' . $converted);
12923 if ($guessed != 'VBO' . $converted) {
12924 // the label was translated correctly
12925 return $guessed;
12926 }
12927
12928 // this label could not be translated, so we return the plain string
12929 return $label;
12930 }
12931
12932 /**
12933 * We check whether some bookings are available for import from third party plugins.
12934 *
12935 * @return mixed array with list of plugins supported, false otherwise.
12936 *
12937 * @wponly this method is only useful for WordPress.
12938 *
12939 * @since 1.3.5
12940 */
12941 public static function canImportBookingsFromThirdPartyPlugins()
12942 {
12943 $dbo = JFactory::getDbo();
12944
12945 $plugins = array();
12946
12947 /**
12948 * As requested from hundreds of our clients, for the moment we check only the custom
12949 * post types of type "mphb_booking" to see if some bookings are available for import.
12950 */
12951 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
12952 $dbo->setQuery($q);
12953 $dbo->execute();
12954
12955 if (!$dbo->getNumRows()) {
12956 return false;
12957 }
12958
12959 // push this third party plugin
12960 $plugins['mphb'] = 'MotoPress Hotel Booking';
12961
12962 return count($plugins) ? $plugins : false;
12963 }
12964
12965 /**
12966 * This method returns a list of the known languages sorted by the
12967 * administrator custom preferences. Useful for the phone input fields.
12968 *
12969 * @param boolean $code_assoc whether to get an associative array with the lang name.
12970 *
12971 * @return array the sorted list of preferred countries.
12972 *
12973 * @since 1.3.11
12974 */
12975 public static function preferredCountriesOrdering($code_assoc = false)
12976 {
12977 $preferred_countries = array();
12978
12979 // try to get the preferred countries from db
12980 $dbo = JFactory::getDbo();
12981 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
12982 $dbo->setQuery($q);
12983 $setting = $dbo->loadResult();
12984 if (!$setting) {
12985 // create empty configuration record
12986 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
12987 $dbo->setQuery($q);
12988 $dbo->execute();
12989 } else {
12990 $preferred_countries = json_decode($setting);
12991 }
12992
12993 // get the default known languages
12994 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
12995
12996 if (!is_array($preferred_countries) || !count($preferred_countries)) {
12997 // sort the default known languages by country code alphabetically
12998 ksort($sorted_known_langs);
12999 foreach ($sorted_known_langs as $k => $v) {
13000 $langsep = strpos($k, '_') !== false ? '_' : '-';
13001 $langparts = explode($langsep, $k);
13002 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
13003 }
13004 // update the database record
13005 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
13006 $dbo->setQuery($q);
13007 $dbo->execute();
13008 }
13009
13010 if ($code_assoc) {
13011 // this is useful for displaying the preferred countries codes together with the language name
13012 $map = array();
13013 foreach ($preferred_countries as $ccode) {
13014 // look for the current country code in the keys of the known language tags
13015 $match_found = false;
13016 foreach ($sorted_known_langs as $langtag => $langinfo) {
13017 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
13018 $langparts = explode($langsep, $langtag);
13019 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
13020 // match found
13021 $match_found = true;
13022 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13023 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
13024 // match found
13025 $match_found = true;
13026 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13027 }
13028 }
13029 if (!$match_found) {
13030 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
13031 $map[$ccode] = strtoupper($ccode);
13032 }
13033 }
13034 if (count($map)) {
13035 // set the associatve array to be returned
13036 $preferred_countries = $map;
13037 }
13038 }
13039
13040 return $preferred_countries;
13041 }
13042
13043 /**
13044 * Gets the instance of the admin widgets helper class.
13045 *
13046 * @return VikBookingHelperAdminWidgets
13047 *
13048 * @since 1.4.0
13049 */
13050 public static function getAdminWidgetsInstance()
13051 {
13052 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
13053
13054 return VikBookingHelperAdminWidgets::getInstance();
13055 }
13056
13057 /**
13058 * Gets the instance of the conditional rules helper class.
13059 *
13060 * @param bool $require_only whether to return the object.
13061 *
13062 * @return mixed VikBookingHelperConditionalRules or true.
13063 *
13064 * @since 1.4.0
13065 */
13066 public static function getConditionalRulesInstance($require_only = false)
13067 {
13068 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
13069
13070 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
13071 }
13072
13073 /**
13074 * Gets the instance of the geocoding helper class.
13075 *
13076 * @param bool $require_only whether to return the object.
13077 *
13078 * @return mixed VikBookingHelperGeocoding or true.
13079 *
13080 * @since 1.4.0
13081 */
13082 public static function getGeocodingInstance($require_only = false)
13083 {
13084 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
13085
13086 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
13087 }
13088
13089 /**
13090 * Helper method to cope with the removal of the same method
13091 * in the JApplication class introduced with Joomla 4. Using
13092 * isClient() would break the compatibility with J < 3.7 so
13093 * we can rely on this helper method to avoid Fatal Errors.
13094 *
13095 * @return boolean
13096 *
13097 * @since October 2020
13098 */
13099 public static function isAdmin()
13100 {
13101 $app = JFactory::getApplication();
13102 if (method_exists($app, 'isClient')) {
13103 return $app->isClient('administrator');
13104 }
13105
13106 return $app->isAdmin();
13107 }
13108
13109 /**
13110 * Helper method to cope with the removal of the same method
13111 * in the JApplication class introduced with Joomla 4. Using
13112 * isClient() would break the compatibility with J < 3.7 so
13113 * we can rely on this helper method to avoid Fatal Errors.
13114 *
13115 * @return boolean
13116 *
13117 * @since October 2020
13118 */
13119 public static function isSite()
13120 {
13121 $app = JFactory::getApplication();
13122 if (method_exists($app, 'isClient')) {
13123 return $app->isClient('site');
13124 }
13125
13126 return $app->isSite();
13127 }
13128
13129 /**
13130 * Gets the Google Maps API Key.
13131 *
13132 * @return string
13133 *
13134 * @since 1.14 (J) - 1.4.0 (WP)
13135 */
13136 public static function getGoogleMapsKey()
13137 {
13138 $dbo = JFactory::getDbo();
13139 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gmapskey';";
13140 $dbo->setQuery($q);
13141 $dbo->execute();
13142 if ($dbo->getNumRows()) {
13143 return $dbo->loadResult();
13144 }
13145 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gmapskey', '');";
13146 $dbo->setQuery($q);
13147 $dbo->execute();
13148 return '';
13149 }
13150
13151 /**
13152 * Checks whether the interactive map booking is enabled.
13153 *
13154 * @return bool
13155 *
13156 * @since 1.14 (J) - 1.4.0 (WP)
13157 */
13158 public static function interactiveMapEnabled()
13159 {
13160 $dbo = JFactory::getDbo();
13161 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='interactive_map';";
13162 $dbo->setQuery($q);
13163 $dbo->execute();
13164 if ($dbo->getNumRows()) {
13165 return ((int)$dbo->loadResult() > 0);
13166 }
13167
13168 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('interactive_map', '0');";
13169 $dbo->setQuery($q);
13170 $dbo->execute();
13171
13172 return false;
13173 }
13174
13175 /**
13176 * Gets the preferred colors saved in the configuration, if any.
13177 *
13178 * @return array
13179 *
13180 * @since 1.14 (J) - 1.4.0 (WP)
13181 */
13182 public static function getPreferredColors()
13183 {
13184 $dbo = JFactory::getDbo();
13185 $pref_colors = array(
13186 'textcolor' => '',
13187 'bgcolor' => '',
13188 'fontcolor' => '',
13189 'bgcolorhov' => '',
13190 'fontcolorhov' => '',
13191 );
13192
13193 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='pref_colors';";
13194 $dbo->setQuery($q);
13195 $dbo->execute();
13196 if ($dbo->getNumRows()) {
13197 $colors = json_decode($dbo->loadResult(), true);
13198 if (!is_array($colors) || !isset($colors['textcolor'])) {
13199 return $pref_colors;
13200 }
13201 return $colors;
13202 }
13203 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('pref_colors', '{}');";
13204 $dbo->setQuery($q);
13205 $dbo->execute();
13206 return $pref_colors;
13207 }
13208
13209 /**
13210 * Adds to the document inline styles for the preferred colors, if any.
13211 *
13212 * @return void
13213 *
13214 * @since 1.14 (J) - 1.4.0 (WP)
13215 */
13216 public static function loadPreferredColorStyles()
13217 {
13218 $view = VikRequest::getString('view', '', 'request');
13219 $pref_colors = self::getPreferredColors();
13220
13221 $css_classes = [];
13222
13223 if (!empty($pref_colors['textcolor'])) {
13224 // titles and headings
13225 array_push($css_classes, '.vbo-pref-color-text { color: ' . $pref_colors['textcolor'] . ' !important; }');
13226 // stepbar, oconfirm
13227 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; }');
13228 // buttons secondary color
13229 array_push($css_classes, '.vbo-pref-color-btn-secondary { border: 2px solid ' . $pref_colors['textcolor'] . ' !important; color: ' . $pref_colors['textcolor'] . ' !important; background: transparent !important; }');
13230 if (!empty($pref_colors['fontcolor'])) {
13231 array_push($css_classes, '.vbo-pref-color-btn-secondary:hover { color: ' . $pref_colors['fontcolor'] . ' !important; background: ' . $pref_colors['textcolor'] . ' !important; }');
13232 }
13233 // datepicker
13234 array_push($css_classes, '.ui-datepicker .ui-datepicker-today {
13235 border-color: ' . $pref_colors['textcolor'] . ' !important;
13236 color: ' . $pref_colors['textcolor'] . ' !important;
13237 }');
13238 // operators tableaux
13239 if ($view == 'tableaux') {
13240 array_push($css_classes, '.vbo-roomdaynote-empty .vbo-roomdaynote-trigger i { color: ' . $pref_colors['textcolor'] . ' !important; }');
13241 }
13242 }
13243
13244 if (!empty($pref_colors['bgcolor']) && !empty($pref_colors['fontcolor'])) {
13245 // elements with backgrounds
13246 array_push($css_classes, '.vbo-pref-color-element { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
13247 array_push($css_classes, '.vbo-pref-bordercolor { border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
13248 array_push($css_classes, '.vbo-pref-bordertext { color: ' . $pref_colors['bgcolor'] . ' !important; border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
13249 array_push($css_classes, '.vbo-pref-background { background: ' . $pref_colors['bgcolor'] . ' !important; }');
13250 // buttons with backgrounds
13251 array_push($css_classes, '.vbo-pref-color-btn { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
13252 // stepbar
13253 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete:before { background-color: ' . $pref_colors['bgcolor'] . ' !important; }');
13254 // datepicker
13255 array_push($css_classes, '.ui-datepicker-calendar td.checkin-date > *, .ui-datepicker-calendar td.checkout-date > *, .ui-datepicker-calendar td.ui-state-highlight > *, .ui-datepicker-calendar td.ui-datepicker-current-day > * {
13256 background: ' . $pref_colors['bgcolor'] . ' !important;
13257 border-color: ' . $pref_colors['bgcolor'] . ' !important;
13258 color: ' . $pref_colors['fontcolor'] . ' !important;
13259 }');
13260 array_push($css_classes, '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {
13261 border-color: ' . $pref_colors['bgcolor'] . ' !important;
13262 }');
13263 array_push($css_classes, '.ui-datepicker-header .ui-corner-all.ui-state-hover {
13264 border-color: ' . $pref_colors['bgcolor'] . ' !important;
13265 color: ' . $pref_colors['bgcolor'] . ' !important;
13266 }');
13267 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day a {
13268 color: ' . $pref_colors['fontcolor'] . ' !important;
13269 }');
13270 array_push($css_classes, '.ui-datepicker td:not(.ui-state-highlight):not(.ui-datepicker-unselectable):not(.date-will):not(.ui-datepicker-current-day) > *:hover {
13271 border-color: ' . $pref_colors['bgcolor'] . ' !important;
13272 color: ' . $pref_colors['bgcolor'] . ' !important;
13273 }');
13274 array_push($css_classes, '.ui-datepicker td.checkin-date a:hover, .ui-datepicker td.checkout-date a:hover, .ui-datepicker-calendar td.ui-datepicker-current-day > *:hover {
13275 color: ' . $pref_colors['fontcolor'] . ' !important;
13276 }');
13277 // dual slider
13278 array_push($css_classes, '.vbo-dual-slider-wrap input[type="range"]::-webkit-slider-thumb, .vbo-dual-slider-wrap input[type="range"]::-moz-range-thumb {
13279 background: ' . $pref_colors['bgcolor'] . ' !important;
13280 }');
13281 // operators tableaux
13282 if ($view == 'tableaux') {
13283 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; }');
13284 }
13285 // listing capacity room-details
13286 array_push($css_classes, '.vbo-rdetails-capacity-icn i {
13287 background: ' . $pref_colors['bgcolor'] . ' !important;
13288 color: ' . $pref_colors['fontcolor'] . ' !important;
13289 }');
13290 }
13291
13292 if (!empty($pref_colors['bgcolorhov']) && !empty($pref_colors['fontcolorhov'])) {
13293 // buttons with backgrounds during hover state
13294 array_push($css_classes, '.vbo-pref-color-btn:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
13295 // operators tableaux
13296 if ($view == 'tableaux') {
13297 array_push($css_classes, '.vbo-tableaux-togglefullscreen:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
13298 }
13299 }
13300
13301 if (!$css_classes) {
13302 return;
13303 }
13304
13305 // add in-line style declaration
13306 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
13307 }
13308
13309 /**
13310 * Given the full endpoint URL for the AJAX request, it
13311 * returns an appropriate URI for the current platform.
13312 *
13313 * @param mixed $query The query string or a routed URL.
13314 * @param boolean $xhtml Replace & by &amp; for XML compliance.
13315 *
13316 * @return string The AJAX end-point URI.
13317 *
13318 * @since 1.14 (J) - 1.4.0 (WP)
13319 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
13320 */
13321 public static function ajaxUrl($query = '', $xhtml = false)
13322 {
13323 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
13324 }
13325
13326 /**
13327 * Tells whether no tax rates have been defined so far.
13328 *
13329 * @return bool True if no tax rates defined, false otherwise.
13330 *
13331 * @since 1.14 (J) - 1.4.0 (WP)
13332 */
13333 public static function noTaxRates()
13334 {
13335 $dbo = JFactory::getDbo();
13336
13337 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
13338 $dbo->setQuery($q);
13339 $dbo->execute();
13340
13341 return ($dbo->getNumRows() < 1);
13342 }
13343
13344 /**
13345 * Booking Type feature is strictly connected to VCM and OTAs. Updated
13346 * versions of VBO will always support it as long as VCM is installed.
13347 * Needed by VCM to understand whether certain SQL queries can be performed.
13348 *
13349 * @return bool true if VCM exists, false otherwise.
13350 *
13351 * @since 1.14 (J) - 1.4.0 (WP)
13352 */
13353 public static function isBookingTypeSupported()
13354 {
13355 static $isBookingTypeSupported = null;
13356
13357 if (is_bool($isBookingTypeSupported)) {
13358 return $isBookingTypeSupported;
13359 }
13360
13361 $isBookingTypeSupported = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
13362
13363 return $isBookingTypeSupported;
13364 }
13365
13366 /**
13367 * Returns an instance of the Rates Flow helper class of VCM, if available.
13368 *
13369 * @param bool $anew True to request a new object instance.
13370 *
13371 * @return VCMRatesFlow|null
13372 *
13373 * @since 1.15.0 (J) - 1.5.0 (WP)
13374 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
13375 */
13376 public static function getRatesFlowInstance($anew = false)
13377 {
13378 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
13379 if (!is_file($vcm_fpath)) {
13380 return null;
13381 }
13382
13383 // load dependencies
13384 require_once $vcm_fpath;
13385
13386 // return the instance of the class
13387 return VCMRatesFlow::getInstance($anew);
13388 }
13389
13390 /**
13391 * Returns the current appearance preference.
13392 *
13393 * @return string light, auto or dark.
13394 *
13395 * @since 1.15.0 (J) - 1.5.0 (WP)
13396 */
13397 public static function getAppearancePref()
13398 {
13399 static $preference = null;
13400
13401 if ($preference !== null) {
13402 return $preference;
13403 }
13404
13405 // auto is the default appearance preference
13406 $default_pref = 'auto';
13407
13408 // accepted preferences
13409 $valid_pref = [
13410 'auto',
13411 'light',
13412 'dark',
13413 ];
13414
13415 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
13416 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
13417
13418 return $preference;
13419 }
13420
13421 /**
13422 * According to the appearance preferences, the apposite
13423 * CSS assets are loaded within the document.
13424 *
13425 * @param bool $get_info true to not load any assets.
13426 *
13427 * @return mixed string light, auto, dark, info array or false.
13428 *
13429 * @since 1.15.0 (J) - 1.5.0 (WP)
13430 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
13431 */
13432 public static function loadAppearancePreferenceAssets($get_info = false)
13433 {
13434 // get document object
13435 $document = JFactory::getDocument();
13436
13437 // load current color scheme preference
13438 $current_pref = self::getAppearancePref();
13439
13440 // define caching values
13441 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
13442
13443 // define file attributes
13444 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
13445
13446 // apposite file path and URI for back-end
13447 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
13448 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
13449
13450 // check if this is for the front-end ("auto" file only)
13451 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
13452 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
13453 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
13454 // check if front-end appearance is disabled
13455 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
13456 // do nothing for the front-end by default
13457 return false;
13458 }
13459 }
13460
13461 if (VBOPlatformDetection::isJoomla()) {
13462 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
13463 $css_uri = str_replace('resources/', '', $css_uri);
13464 }
13465
13466 if (!is_file($css_path)) {
13467 // this preference does not require a specific stylesheet
13468 return false;
13469 }
13470
13471 if ($get_info) {
13472 // do not actually load any assets
13473 return [
13474 'id' => $file_attr['id'],
13475 'href' => $css_uri,
13476 ];
13477 }
13478
13479 // load the apposite CSS file
13480 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
13481
13482 return $current_pref;
13483 }
13484
13485 /**
13486 * VBO trigger for VCM subscription status and updates. If called within the admin section
13487 * returns the expiration reminder array to display a modal window, and sets a warning
13488 * message. If called within the front-end, may trigger an email alert.
13489 *
13490 * @return mixed null, array or boolean depending on client position.
13491 *
13492 * @since 1.15.0 (J) - 1.5.0 (WP)
13493 */
13494 public static function getVCMSubscriptionStatus()
13495 {
13496 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
13497 if (!is_file($vcm_lib_path)) {
13498 // do nothing
13499 return null;
13500 }
13501
13502 // determine client position
13503 $is_admin = self::isAdmin();
13504
13505 try {
13506 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
13507 // VCM is outdated
13508 return null;
13509 }
13510
13511 // always attempt to load the admin lang handler of VCM
13512 $lang = JFactory::getLanguage();
13513
13514 // load the VCM admin language file
13515 $vcm_admin_lang_path = '';
13516 if (VBOPlatformDetection::isJoomla()) {
13517 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
13518 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
13519 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
13520 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
13521 /**
13522 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
13523 */
13524 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
13525 }
13526 // load translation file
13527 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
13528 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
13529 /**
13530 * @wponly load language admin handler as well for WP.
13531 * We do this only because of WordPress, but in a way also compatible with Joomla as
13532 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
13533 */
13534 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
13535 }
13536
13537 if ($is_admin) {
13538 // back-end section of the site: check status
13539 if (VikChannelManager::loadExpirationDetails() === false) {
13540 // download and update the expiration details
13541 VikChannelManager::downloadExpirationDetails();
13542 }
13543 $expiration_reminder = null;
13544 if (VikChannelManager::isSubscriptionExpired()) {
13545 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
13546 } else {
13547 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
13548 // we also check if an update is available
13549 if (VikChannelManager::checkUpdates()) {
13550 // an update is more important
13551 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
13552 exit;
13553 }
13554 }
13555 // return reminder status
13556 return $expiration_reminder;
13557 }
13558
13559 // front-end section of the site: trigger alert, if any (bool)
13560 return VikChannelManager::checkSubscriptionReminder();
13561
13562 } catch (Exception $e) {
13563 // do nothing
13564 }
13565
13566 return null;
13567 }
13568
13569 /**
13570 * Given a list of website rate plans, returns the sorted list.
13571 * Naming score is the highest sorting method, pricing scoring is also supported.
13572 *
13573 * @param array $rate_plans the list of rate plan arrays to sort.
13574 * @param bool $assoc whether to keep the original array keys.
13575 *
13576 * @return array the sorted list of rate plan arrays.
13577 *
13578 * @since 1.15.0 (J) - 1.5.0 (WP)
13579 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
13580 */
13581 public static function sortRatePlans($rate_plans, $assoc = false)
13582 {
13583 if (!is_array($rate_plans) || count($rate_plans) < 2) {
13584 return $rate_plans;
13585 }
13586
13587 foreach ($rate_plans as $rp_ind => $rate_plan) {
13588 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
13589 // do not proceed in case of a weird array structure
13590 return $rate_plans;
13591 }
13592
13593 // count the sorting-score of this rate plan
13594 $sort_score = 0;
13595 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
13596 // "standard rate" rate plans should go first
13597 $sort_score += 3;
13598 } else {
13599 $sort_score--;
13600 }
13601
13602 // check for derived rate plan
13603 if (!empty($rate_plan['derived_id'])) {
13604 // derived rates should go last
13605 $sort_score -= 2;
13606 } else {
13607 // parent rates should go first
13608 $sort_score += 2;
13609 }
13610
13611 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
13612 // this doesn't seem to be a "non refundable rate" so it should go first
13613 $sort_score += 2;
13614 }
13615
13616 // set sorting score for this rate plan ID
13617 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13618 }
13619
13620 // apply custom sorting and keep index association
13621 uasort($rate_plans, function($a, $b) {
13622 // sort by name-scoring DESC
13623 $sort_diff = $b['sort_score'] - $a['sort_score'];
13624 if (isset($b['cost'])) {
13625 // apply also sorting by price ASC
13626 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13627 }
13628 return $sort_diff;
13629 });
13630
13631 return !$assoc ? array_values($rate_plans) : $rate_plans;
13632 }
13633
13634 /**
13635 * Returns an instance of the Availability helper class.
13636 *
13637 * @param bool $anew True for forcing a new instance.
13638 *
13639 * @return VikBookingAvailability
13640 *
13641 * @since 1.15.0 (J) - 1.5.0 (WP)
13642 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13643 */
13644 public static function getAvailabilityInstance($anew = false)
13645 {
13646 // require library
13647 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13648
13649 // return the instance of the class
13650 return VikBookingAvailability::getInstance($anew);
13651 }
13652
13653 /**
13654 * Helper method to quickly guess the internal dependency to import.
13655 *
13656 * @param string $path_id either a full path to a file or its identifier.
13657 *
13658 * @return bool true if the requested library was imported or false.
13659 *
13660 * @since 1.15.0 (J) - 1.5.0 (WP)
13661 */
13662 public static function import($path_id)
13663 {
13664 if (empty($path_id)) {
13665 return false;
13666 }
13667
13668 // always append .php extension
13669 $path_id = basename($path_id, '.php') . '.php';
13670
13671 if (is_file($path_id)) {
13672 // full path given
13673 require_once $path_id;
13674
13675 return true;
13676 }
13677
13678 // try guessing where the file id is located
13679 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13680 // library was found
13681 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13682
13683 return true;
13684 }
13685
13686 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13687 // library was found
13688 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13689
13690 return true;
13691 }
13692
13693 return false;
13694 }
13695 }
13696
13697 if (!class_exists('VikResizer'))
13698 {
13699 class VikResizer
13700 {
13701 public function __construct()
13702 {
13703 // objects of this class can also be instantiated without calling the methods statically.
13704 }
13705
13706 /**
13707 * Resizes an image proportionally. For PNG files it can optionally
13708 * trim the image to exclude the transparency, and add some padding to it.
13709 * All PNG files keep the alpha background in the resized version.
13710 *
13711 * @param string $fileimg path to original image file
13712 * @param string $dest path to destination image file
13713 * @param int $towidth
13714 * @param int $toheight
13715 * @param bool $trim_png remove empty background from image
13716 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13717 *
13718 * @return boolean
13719 */
13720 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13721 {
13722 if (!is_file($fileimg)) {
13723 return false;
13724 }
13725 if (empty($towidth) && empty($toheight)) {
13726 return copy($fileimg, $dest);
13727 }
13728
13729 $result = true;
13730
13731 list ($owid, $ohei, $type) = getimagesize($fileimg);
13732
13733 if ($owid > $towidth || $ohei > $toheight) {
13734 $xscale = $owid / $towidth;
13735 $yscale = $ohei / $toheight;
13736 if ($yscale > $xscale) {
13737 $new_width = round($owid * (1 / $yscale));
13738 $new_height = round($ohei * (1 / $yscale));
13739 } else {
13740 $new_width = round($owid * (1 / $xscale));
13741 $new_height = round($ohei * (1 / $xscale));
13742 }
13743
13744 $imageresized = imagecreatetruecolor($new_width, $new_height);
13745
13746 switch ($type) {
13747 case '1' :
13748 $imagetmp = imagecreatefromgif ($fileimg);
13749 break;
13750 case '2' :
13751 $imagetmp = imagecreatefromjpeg($fileimg);
13752 break;
13753 case '18' :
13754 $imagetmp = imagecreatefromwebp($fileimg);
13755 break;
13756 default :
13757 //keep alpha for PNG files
13758 $background = imagecolorallocate($imageresized, 0, 0, 0);
13759 imagecolortransparent($imageresized, $background);
13760 imagealphablending($imageresized, false);
13761 imagesavealpha($imageresized, true);
13762 //
13763 $imagetmp = imagecreatefrompng($fileimg);
13764 break;
13765 }
13766
13767 if (!$imagetmp) {
13768 return false;
13769 }
13770
13771 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13772
13773 switch ($type) {
13774 case '1' :
13775 imagegif($imageresized, $dest);
13776 break;
13777 case '2' :
13778 imagejpeg($imageresized, $dest);
13779 break;
13780 case '18' :
13781 imagewebp($imageresized, $dest);
13782 break;
13783 default :
13784 if ($trim_png) {
13785 self::imageTrim($imageresized, $background, $trim_pad);
13786 }
13787 imagepng($imageresized, $dest);
13788 break;
13789 }
13790
13791 imagedestroy($imageresized);
13792 } else {
13793 $result = copy($fileimg, $dest);
13794 }
13795
13796 if (VBOPlatformDetection::isWordPress()) {
13797 /**
13798 * @wponly in order to not lose resized files after installing an update,
13799 * we need to move any uploaded file onto a recovery folder.
13800 */
13801 VikBookingLoader::import('update.manager');
13802 VikBookingUpdateManager::triggerUploadBackup($dest);
13803 }
13804
13805 return $result;
13806 }
13807
13808 /**
13809 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13810 * trim the image to exclude the transparency, and add some padding to it.
13811 * All PNG files keep the alpha background in the resized version.
13812 *
13813 * @param resource $im Image link resource (reference)
13814 * @param int $bg imagecolorallocate color identifier
13815 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13816 *
13817 * @return void
13818 */
13819 public static function imagetrim(&$im, $bg, $pad = null)
13820 {
13821 // Calculate padding for each side.
13822 if (isset($pad)) {
13823 $pp = explode(' ', $pad);
13824 if (isset($pp[3])) {
13825 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13826 } elseif (isset($pp[2])) {
13827 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13828 } elseif (isset($pp[1])) {
13829 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13830 } else {
13831 $p = array_fill(0, 4, (int) $pp[0]);
13832 }
13833 } else {
13834 $p = array_fill(0, 4, 0);
13835 }
13836
13837 // Get the image width and height.
13838 $imw = imagesx($im);
13839 $imh = imagesy($im);
13840
13841 // Set the X variables.
13842 $xmin = $imw;
13843 $xmax = 0;
13844
13845 // Start scanning for the edges.
13846 for ($iy=0; $iy<$imh; $iy++) {
13847 $first = true;
13848 for ($ix=0; $ix<$imw; $ix++) {
13849 $ndx = imagecolorat($im, $ix, $iy);
13850 if ($ndx != $bg) {
13851 if ($xmin > $ix) {
13852 $xmin = $ix;
13853 }
13854 if ($xmax < $ix) {
13855 $xmax = $ix;
13856 }
13857 if (!isset($ymin)) {
13858 $ymin = $iy;
13859 }
13860 $ymax = $iy;
13861 if ($first) {
13862 $ix = $xmax;
13863 $first = false;
13864 }
13865 }
13866 }
13867 }
13868
13869 // The new width and height of the image. (not including padding)
13870 $imw = 1+$xmax-$xmin; // Image width in pixels
13871 $imh = 1+$ymax-$ymin; // Image height in pixels
13872
13873 // Make another image to place the trimmed version in.
13874 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13875
13876 // Make the background of the new image the same as the background of the old one.
13877 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13878 imagefill($im2, 0, 0, $bg2);
13879
13880 // Copy it over to the new image.
13881 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13882
13883 // To finish up, we replace the old image which is referenced.
13884 $im = $im2;
13885 }
13886
13887 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13888 {
13889 if (!file_exists($fileimg)) {
13890 return false;
13891 }
13892 if (empty($towidth) && empty($toheight)) {
13893 copy($fileimg, $dest);
13894 return true;
13895 }
13896
13897 $exp = explode(",", $rgb);
13898 if (count($exp) == 3) {
13899 $r = trim($exp[0]);
13900 $g = trim($exp[1]);
13901 $b = trim($exp[2]);
13902 } else {
13903 $r = 0;
13904 $g = 0;
13905 $b = 0;
13906 }
13907
13908 list ($owid, $ohei, $type) = getimagesize($fileimg);
13909
13910 if ($owid > $towidth || $ohei > $toheight) {
13911 $xscale = $owid / $towidth;
13912 $yscale = $ohei / $toheight;
13913 if ($yscale > $xscale) {
13914 $new_width = round($owid * (1 / $yscale));
13915 $new_height = round($ohei * (1 / $yscale));
13916 $ydest = 0;
13917 $diff = $towidth - $new_width;
13918 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13919 } else {
13920 $new_width = round($owid * (1 / $xscale));
13921 $new_height = round($ohei * (1 / $xscale));
13922 $xdest = 0;
13923 $diff = $toheight - $new_height;
13924 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13925 }
13926
13927 $imageresized = imagecreatetruecolor($towidth, $toheight);
13928
13929 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13930 imagefill($imageresized, 0, 0, $bgColor);
13931
13932 switch ($type) {
13933 case '1' :
13934 $imagetmp = imagecreatefromgif ($fileimg);
13935 break;
13936 case '2' :
13937 $imagetmp = imagecreatefromjpeg($fileimg);
13938 break;
13939 default :
13940 $imagetmp = imagecreatefrompng($fileimg);
13941 break;
13942 }
13943
13944 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13945
13946 switch ($type) {
13947 case '1' :
13948 imagegif ($imageresized, $dest);
13949 break;
13950 case '2' :
13951 imagejpeg($imageresized, $dest);
13952 break;
13953 default :
13954 imagepng($imageresized, $dest);
13955 break;
13956 }
13957
13958 imagedestroy($imageresized);
13959
13960 return true;
13961 } else {
13962 copy($fileimg, $dest);
13963 }
13964 return true;
13965 }
13966
13967 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13968 {
13969 if (!file_exists($fileimg)) {
13970 return false;
13971 }
13972 if (empty($towidth) && empty($toheight)) {
13973 copy($fileimg, $dest);
13974 return true;
13975 }
13976
13977 list ($owid, $ohei, $type) = getimagesize($fileimg);
13978
13979 if ($owid <= $ohei) {
13980 $new_width = $towidth;
13981 $new_height = ($towidth / $owid) * $ohei;
13982 } else {
13983 $new_height = $toheight;
13984 $new_width = ($new_height / $ohei) * $owid;
13985 }
13986
13987 switch ($type) {
13988 case '1' :
13989 $img_src = imagecreatefromgif ($fileimg);
13990 $img_dest = imagecreate($new_width, $new_height);
13991 break;
13992 case '2' :
13993 $img_src = imagecreatefromjpeg($fileimg);
13994 $img_dest = imagecreatetruecolor($new_width, $new_height);
13995 break;
13996 default :
13997 $img_src = imagecreatefrompng($fileimg);
13998 $img_dest = imagecreatetruecolor($new_width, $new_height);
13999 break;
14000 }
14001
14002 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
14003
14004 switch ($type) {
14005 case '1' :
14006 $cropped = imagecreate($towidth, $toheight);
14007 break;
14008 case '2' :
14009 $cropped = imagecreatetruecolor($towidth, $toheight);
14010 break;
14011 default :
14012 $cropped = imagecreatetruecolor($towidth, $toheight);
14013 break;
14014 }
14015
14016 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
14017
14018 switch ($type) {
14019 case '1' :
14020 imagegif ($cropped, $dest);
14021 break;
14022 case '2' :
14023 imagejpeg($cropped, $dest);
14024 break;
14025 default :
14026 imagepng($cropped, $dest);
14027 break;
14028 }
14029
14030 imagedestroy($img_dest);
14031 imagedestroy($cropped);
14032
14033 return true;
14034 }
14035 }
14036 }
14037