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

14,033 lines 467.6 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 * Generates a random UUID v4.
368 *
369 * A UUID is a 16-octet (128-bit) number. In its canonical form, a UUID is represented by 32
370 * hexadecimal digits, displayed in five groups separated by hyphens, in the form 8-4-4-4-12
371 * for a total of 36 characters (32 alphanumeric characters and four hyphens).
372 *
373 * @return string
374 *
375 * @since 1.18.8 (J) - 1.8.8 (WP)
376 */
377 public static function uuid()
378 {
379 /*
380 return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
381 // 32 bits for "time_low"
382 mt_rand(0, 0xffff), mt_rand(0, 0xffff),
383
384 // 16 bits for "time_mid"
385 mt_rand(0, 0xffff),
386
387 // 16 bits for "time_hi_and_version",
388 // four most significant bits holds version number 4
389 mt_rand(0, 0x0fff) | 0x4000,
390
391 // 16 bits, 8 bits for "clk_seq_hi_res",
392 // 8 bits for "clk_seq_low",
393 // two most significant bits holds zero and one for variant DCE1.1
394 mt_rand(0, 0x3fff) | 0x8000,
395
396 // 48 bits for "node"
397 mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
398 );
399 */
400
401 // generate 16 random bytes through CSPRNG
402 $data = random_bytes(16);
403
404 // four most significant bits holds version number 4
405 $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
406 // two most significant bits holds zero and one for variant DCE1.1
407 $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
408
409 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
410 }
411
412 /**
413 * Load the restrictions applying the given filters to the passed rooms.
414 * The ordering of the query SHOULD remain unchanged, because it's required
415 * to have it by ascending order of the ID. So the older records first.
416 * Even when no filters, we always exclude expired restrictions.
417 *
418 * @param boolean $filters whether to apply filters
419 * @param array $rooms the list of rooms to filter
420 *
421 * @return array the list of restrictions found or an empty array.
422 */
423 public static function loadRestrictions($filters = true, $rooms = [])
424 {
425 $dbo = JFactory::getDbo();
426
427 $restrictions = [];
428 $limts = strtotime(date('Y-m-d'));
429
430 if (!$filters) {
431 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
432 } else {
433 if (!$rooms) {
434 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
435 } else {
436 $clause = [];
437 foreach ($rooms as $idr) {
438 if (empty($idr)) {
439 continue;
440 }
441 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
442 }
443 if ($clause) {
444 $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;";
445 } else {
446 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
447 }
448 }
449 }
450
451 $dbo->setQuery($q);
452 $allrestrictions = $dbo->loadAssocList();
453
454 foreach ($allrestrictions as $k => $res) {
455 if (!empty($res['month'])) {
456 $restrictions[$res['month']] = $res;
457 } else {
458 if (!isset($restrictions['range'])) {
459 $restrictions['range'] = array();
460 }
461 $restrictions['range'][$k] = $res;
462 }
463 }
464
465 return $restrictions;
466 }
467
468 /**
469 * Given a list of restriction records split into range or months, filters
470 * the list by including the "global" restrictions applied to all rooms.
471 *
472 * @param array $restrictions List of restriction records loaded.
473 *
474 * @return array Associative list of filtered restriction records.
475 */
476 public static function globalRestrictions(array $restrictions)
477 {
478 $list = [];
479
480 // iterate all records
481 foreach ($restrictions as $kr => $rr) {
482 if ($kr == 'range') {
483 // scan all date-range restriction records
484 foreach ($rr as $kd => $dr) {
485 if ($dr['allrooms'] == 1) {
486 $list['range'][$kd] = $restrictions[$kr][$kd];
487 }
488 }
489 // go next
490 continue;
491 }
492
493 // month restriction record
494 if ($rr['allrooms'] == 1) {
495 $list[$kr] = $restrictions[$kr];
496 }
497 }
498
499 return $list;
500 }
501
502 /**
503 * Given a list of restriction records split into range or months, filters the list
504 * by including the "listing-level" or "global" restrictions affecting to given rooms.
505 *
506 * @param array $restrictions List of restriction records loaded.
507 * @param array $room_ids List of involved and eligible room IDs.
508 *
509 * @return array Associative list of filtered restriction records.
510 *
511 * @since 1.18.2 (J) - 1.8.2 (WP)
512 */
513 public static function listingRestrictions(array $restrictions, array $room_ids)
514 {
515 $list = [];
516
517 // iterate all records
518 foreach ($restrictions as $kr => $rr) {
519 if ($kr == 'range') {
520 // scan all date-range restriction records
521 foreach ($rr as $kd => $dr) {
522 if ($dr['allrooms'] == 1) {
523 // push global restriction
524 $list['range'][$kd] = $restrictions[$kr][$kd];
525 } elseif (!empty($dr['idrooms'])) {
526 // scan the list of room IDs to match against
527 foreach ($room_ids as $room_id) {
528 if (strpos($dr['idrooms'], '-' . $room_id . '-') !== false) {
529 // push listing-level restriction
530 $list['range'][$kd] = $restrictions[$kr][$kd];
531 break;
532 }
533 }
534 }
535 }
536 // go next
537 continue;
538 }
539
540 // month restriction record
541 if ($rr['allrooms'] == 1) {
542 // push global restriction
543 $list[$kr] = $restrictions[$kr];
544 } elseif (!empty($rr['idrooms'])) {
545 // scan the list of room IDs to match against
546 foreach ($room_ids as $room_id) {
547 if (strpos($rr['idrooms'], '-' . $room_id . '-') !== false) {
548 // push listing-level restriction
549 $list[$kr] = $restrictions[$kr];
550 break;
551 }
552 }
553 }
554 }
555
556 return $list;
557 }
558
559 /**
560 * From the given restrictions, check-in, check-out and nights, looks for
561 * a restriction to be returned and applied over these stays.
562 * In order to give priority to newer Restriction IDs, the order of the
563 * $restrictions array should be ascending, so that the newer restrictions
564 * will overwrite the array with the last ID (more recent). Only for date ranges.
565 * Returns an array with the record of the restriction found. The loop is not
566 * broken when the first valid restriction is found to give priority to
567 * newer restriction IDs to overwrite older records.
568 *
569 * @param int $first the unix timestamp for the check-in date
570 * @param int $second the unix timestamp for the check-out date
571 * @param int $daysdiff the number of nights of stay
572 * @param array $restrictions the list of restrictions loaded
573 *
574 * @return array the restriction found, or an empty array.
575 */
576 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
577 $season_restrictions = array();
578 $restrcheckin = getdate($first);
579 $restrcheckout = getdate($second);
580 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
581 //restriction found for this month, checking:
582 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
583 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
584 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
585 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
586 //Week Day Arrival Restriction
587 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
588 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
589 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
590 }
591 $season_restrictions['wdays'] = $rvalidwdays;
592 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
593 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
594 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
595 }
596 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
597 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
598 }
599 }
600 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
601 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
602 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
603 $season_restrictions['allowed'] = false;
604 }
605 }
606 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
607 $season_restrictions['allowed'] = false;
608 }
609 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
610 } elseif (array_key_exists('range', $restrictions)) {
611 foreach ($restrictions['range'] as $restr) {
612 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
613 //restriction found for this date range, checking:
614 $season_restrictions['id'] = $restr['id'];
615 $season_restrictions['name'] = $restr['name'];
616 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
617 if (strlen((string)$restr['wday']) > 0) {
618 //Week Day Arrival Restriction
619 $rvalidwdays = array($restr['wday']);
620 if (strlen((string)$restr['wdaytwo']) > 0) {
621 $rvalidwdays[] = $restr['wdaytwo'];
622 }
623 $season_restrictions['wdays'] = $rvalidwdays;
624 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
625 if (!empty($restr['ctad'])) {
626 $season_restrictions['cta'] = explode(',', $restr['ctad']);
627 }
628 if (!empty($restr['ctdd'])) {
629 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
630 }
631 }
632 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
633 $season_restrictions['maxlos'] = $restr['maxlos'];
634 if ($daysdiff > $restr['maxlos']) {
635 $season_restrictions['allowed'] = false;
636 }
637 }
638 if ($daysdiff < $restr['minlos']) {
639 $season_restrictions['allowed'] = false;
640 }
641 $season_restrictions['minlos'] = $restr['minlos'];
642 }
643 }
644 }
645
646 return $season_restrictions;
647 }
648
649 public static function compareSeasonRestrictionsNights($restrictions)
650 {
651 $base_compare = array();
652 $base_nights = 0;
653 foreach ($restrictions as $nights => $restr) {
654 $base_compare = $restr;
655 $base_nights = $nights;
656 break;
657 }
658
659 /**
660 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
661 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
662 *
663 * @since 1.15.0 (J) - 1.5.0 (WP)
664 */
665 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
666
667 foreach ($restrictions as $nights => $restr) {
668 if ($nights == $base_nights) {
669 continue;
670 }
671
672 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
673 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
674
675 // get associative array of differences
676 $diff = array_diff($use_base_compare, $use_restr);
677
678 if (count($diff) > 0 && array_key_exists('id', $diff)) {
679 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
680 // only one Restriction per time should be applied to certain Season Dates but check just in case.
681 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
682 }
683 }
684
685 return array();
686 }
687
688 /**
689 * Methods using array_diff() may need one-dimension arrays, because this
690 * native functions applies a string comparison, and so casting an array
691 * value to a string would generate a Notice message. This method simply
692 * converts any sub-array into a restorable string into a non-scalar var.
693 * This only works when comparing 2 array variables.
694 *
695 * @param array $arr the hypothetical multi dimension array.
696 *
697 * @return array the one dimension array to be passed to array_diff()
698 * and the list of keys converted to strings for comparison.
699 *
700 * @since 1.15.0 (J) - 1.5.0 (WP)
701 */
702 public static function prepareMultiDimArrayDiff($arr)
703 {
704 $casted_keys = array();
705
706 if (!is_array($arr) || !count($arr)) {
707 return array($casted_keys, $arr);
708 }
709
710 foreach ($arr as $key => $val) {
711 if (!is_scalar($val)) {
712 // memorize original value
713 $casted_keys[$key] = $val;
714 // cast to string must be applied
715 $arr[$key] = json_encode($val);
716 }
717 }
718
719 return array($casted_keys, $arr);
720 }
721
722 /**
723 * Methods using array_diff() may need one-dimension arrays, because this
724 * native functions applies a string comparison, and so casting an array
725 * value to a string would generate a Notice message. This method restores
726 * the original values of the "prepared" arrays by getting the list of the
727 * modified keys from non-scalar to scalar values.
728 * This only works when comparing 2 array variables.
729 *
730 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
731 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
732 * @param array $arr the array result of array_diff().
733 *
734 * @return array the array result of array_diff() with its original values.
735 *
736 * @since 1.15.0 (J) - 1.5.0 (WP)
737 */
738 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
739 {
740 if (!is_array($arr) || !count($arr)) {
741 return $arr;
742 }
743
744 foreach ($arr as $key => $val) {
745 // keys returned by array_diff can be present in both $keys_one and $keys_two
746 if (isset($keys_one[$key])) {
747 // always give higher priority to the 1st argument keys for array_diff()
748 $arr[$key] = json_decode($keys_one[$key]);
749 } elseif (isset($keys_two[$key])) {
750 // fallback to 2nd argument keys
751 $arr[$key] = json_decode($keys_two[$key]);
752 }
753 }
754
755 return $arr;
756 }
757
758 public static function roomRestrictions($roomid, $restrictions) {
759 $ret = array();
760 if (!empty($roomid) && count($restrictions) > 0) {
761 foreach ($restrictions as $kr => $rr) {
762 if ($kr == 'range') {
763 foreach ($rr as $kd => $dr) {
764 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
765 $allrooms = explode(';', $dr['idrooms']);
766 if (in_array('-'.$roomid.'-', $allrooms)) {
767 $ret['range'][$kd] = $restrictions[$kr][$kd];
768 }
769 }
770 }
771 } else {
772 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
773 $allrooms = explode(';', $rr['idrooms']);
774 if (in_array('-'.$roomid.'-', $allrooms)) {
775 $ret[$kr] = $restrictions[$kr];
776 }
777 }
778 }
779 }
780 }
781 return $ret;
782 }
783
784 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
785 {
786 // default states
787 $restrictionerrmsg = '';
788 $restrictions_affcount = 0;
789 $minlos_errors_pool = [];
790
791 // check for month-level or range-level restrictions
792 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
793 //restriction found for this month, checking:
794 $restrictions_affcount++;
795 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
796 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
797 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
798 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
799 }
800 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
801 $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']) : ''));
802 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
803 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
804 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
805 }
806 }
807 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
808 if (count($comborestr) > 0) {
809 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
810 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
811 $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']));
812 }
813 }
814 }
815 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
816 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
817 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
818 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
819 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
820 }
821 }
822 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
823 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
824 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
825 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
826 }
827 }
828 }
829 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
830 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
831 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
832 }
833 }
834 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
835 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
836 }
837 } elseif (array_key_exists('range', $roomrestr)) {
838 // start valid flag
839 $restrictionsvalid = true;
840
841 /**
842 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
843 * The most recent restrictions will have a higher priority over the oldest ones.
844 *
845 * @since 1.12.1
846 */
847 $minlos_priority = [
848 'ok' => [],
849 'nok' => [],
850 ];
851
852 /**
853 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
854 *
855 * @since 1.16.10 (J) - 1.6.10 (WP)
856 */
857 $ctad_priority = [
858 'ok' => [],
859 'nok' => [],
860 ];
861
862 foreach ($roomrestr['range'] as $restr) {
863 /**
864 * We should not always add 82799 seconds to the end date of the restriction
865 * because if they only last for one day (like a Saturday), then $restr['dto']
866 * will be already set to the time 23:59:59.
867 *
868 * @since 1.13 (J) - 1.2.18 (WP)
869 * @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
870 * is already set to 23:59:59 (VCM Connector may be responsible), so no seconds should be added or the restriction end date
871 * will be the day after, and if after the check-in time, it may be included.
872 */
873 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) && substr((string) $restr['dto'], -1, 1) != 9 ? 82799 : 0;
874
875 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
876 // restriction found for this date range based on arrival date, check if compliant
877 $restrictions_affcount++;
878
879 // set flag for CTA/CTD compliance
880 $cta_ctd_passed = true;
881
882 if (strlen((string)$restr['wday']) > 0) {
883 $rvalidwdays = array($restr['wday']);
884 if (strlen((string)$restr['wdaytwo']) > 0) {
885 $rvalidwdays[] = $restr['wdaytwo'];
886 }
887 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
888 $restrictionsvalid = false;
889 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
890 } elseif ($restr['multiplyminlos'] == 1) {
891 if (($daysdiff % $restr['minlos']) != 0) {
892 $restrictionsvalid = false;
893 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
894 }
895 }
896 $comborestr = self::parseJsDrangeWdayCombo($restr);
897 if ($comborestr) {
898 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
899 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
900 $restrictionsvalid = false;
901 $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']));
902 }
903 }
904 }
905 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
906 if (!empty($restr['ctad'])) {
907 $ctarestrictions = explode(',', $restr['ctad']);
908 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
909 $restrictionsvalid = false;
910 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
911 $cta_ctd_passed = false;
912 }
913 }
914 if (!empty($restr['ctdd'])) {
915 $ctdrestrictions = explode(',', $restr['ctdd']);
916 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
917 $restrictionsvalid = false;
918 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
919 $cta_ctd_passed = false;
920 }
921 }
922 }
923
924 // check CTA/CTD compliance
925 if (!$cta_ctd_passed) {
926 array_push($ctad_priority['nok'], (int)$restr['id']);
927 } else {
928 array_push($ctad_priority['ok'], (int)$restr['id']);
929 }
930
931 // max LOS validation
932 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
933 if ($daysdiff > $restr['maxlos']) {
934 $restrictionsvalid = false;
935 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
936 }
937 }
938
939 // min LOS validation
940 if ($daysdiff < $restr['minlos']) {
941 $restrictionsvalid = false;
942 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
943 // push error value
944 array_push($minlos_priority['nok'], (int) $restr['id']);
945 // set error message with related minimum stay
946 $minlos_errors_pool[$restr['minlos']] = $restrictionerrmsg;
947 } else {
948 array_push($minlos_priority['ok'], (int) $restr['id']);
949 }
950 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
951 /**
952 * We validate the CTD restrictions depending on the check-out date.
953 *
954 * @since 1.16.3 (J) - 1.6.3 (WP)
955 */
956 $ctdrestrictions = explode(',', $restr['ctdd']);
957 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
958 $restrictions_affcount++;
959 $restrictionsvalid = false;
960 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
961 }
962 }
963 }
964
965 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
966 // a more recent restriction is allowing this MinLOS
967 // ensure there are no recent CTA/CTD rules not compliant
968 $cta_ctd_priority_ok = true;
969
970 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
971 // a more recent restriction is not compliant with the CTA/CTD rules
972 $cta_ctd_priority_ok = false;
973 }
974
975 if ($cta_ctd_priority_ok) {
976 // we unset the error message because more recent restriction(s) are allowing this stay dates
977 $restrictionerrmsg = '';
978 }
979 }
980 }
981
982 // check global restriction of Min LOS for TAC functions in VBO and VCM
983 if (empty($restrictionerrmsg) && $roomrestr && $restrictions_affcount <= 0) {
984 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
985 $globminlos = self::getDefaultNightsCalendar();
986 if ($globminlos > 1 && $daysdiff < $globminlos) {
987 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
988 }
989 }
990
991 /**
992 * When working with room-level restrictions and different minLOS across multiple apartments,
993 * we need the display the actually lowest minimum stay among all listings from the errors found.
994 *
995 * @since 1.17.3 (J) - 1.7.3 (WP)
996 */
997 if ($restrictionerrmsg && in_array($restrictionerrmsg, $minlos_errors_pool) && count($minlos_errors_pool) > 1) {
998 // make sure to return the error message for the lowest minimum stay
999 $lowest_minlos = min(array_map('intval', array_keys($minlos_errors_pool)));
1000 $restrictionerrmsg = $minlos_errors_pool[$lowest_minlos] ?? $restrictionerrmsg;
1001 }
1002
1003 // return the restriction error message string, if anything wrong was found
1004 return $restrictionerrmsg;
1005 }
1006
1007 public static function parseJsDrangeWdayCombo($drestr) {
1008 $combo = array();
1009 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
1010 $cparts = explode(':', $drestr['wdaycombo']);
1011 foreach ($cparts as $kc => $cw) {
1012 if (!empty($cw)) {
1013 $nowcombo = explode('-', $cw);
1014 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
1015 }
1016 }
1017 }
1018 return $combo;
1019 }
1020
1021 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
1022 $dbo = JFactory::getDbo();
1023 $pkg = array();
1024 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1025 $dbo->setQuery($q);
1026 $dbo->execute();
1027 if ($dbo->getNumRows() == 1) {
1028 $pkg = $dbo->loadAssoc();
1029 $vbo_tn = self::getTranslator();
1030 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
1031 } else {
1032 return JText::translate('VBOPKGERRNOTFOUND');
1033 }
1034 $rooms_req = array();
1035 foreach ($rooms as $num => $room) {
1036 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
1037 $rooms_req[] = $room['id'];
1038 }
1039 }
1040 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
1041 $dbo->setQuery($q);
1042 $dbo->execute();
1043 if ($dbo->getNumRows() != count($rooms_req)) {
1044 //error, not all the rooms requested are available for this package
1045 return JText::translate('VBOPKGERRNOTROOM');
1046 }
1047 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
1048 return JText::translate('VBOPKGERRNUMNIGHTS');
1049 }
1050 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
1051 return JText::translate('VBOPKGERRCHECKIND');
1052 }
1053 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
1054 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
1055 return JText::translate('VBOPKGERRCHECKOUTD');
1056 }
1057 if (!empty($pkg['excldates'])) {
1058 //this would check if any stay date is excluded
1059 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
1060 //check just the arrival and departure dates
1061 $bookdates_ts = array($checkints, $checkoutts);
1062 $bookdates = array();
1063 foreach ($bookdates_ts as $bookdate_ts) {
1064 $info_d = getdate($bookdate_ts);
1065 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
1066 }
1067 $edates = explode(';', $pkg['excldates']);
1068 foreach ($edates as $edate) {
1069 if (!empty($edate) && in_array($edate, $bookdates)) {
1070 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
1071 }
1072 }
1073 }
1074 return $pkg;
1075 }
1076
1077 public static function getPackage($pkg_id) {
1078 $dbo = JFactory::getDbo();
1079 $pkg = array();
1080 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1081 $dbo->setQuery($q);
1082 $dbo->execute();
1083 if ($dbo->getNumRows() == 1) {
1084 $pkg = $dbo->loadAssoc();
1085 }
1086 return $pkg;
1087 }
1088
1089 /**
1090 * Returns the requested room parameter or default value.
1091 *
1092 * @param string $paramname The parameter name.
1093 * @param mixed $params The room params string, or json-decoded array/object.
1094 * @param mixed $def The default value to return as fallback.
1095 *
1096 * @return mixed
1097 *
1098 * @since 1.17.3 (J) - 1.7.3 (WP) added 3rd argument $def.
1099 */
1100 public static function getRoomParam($paramname, $params, $def = '')
1101 {
1102 if (empty($params)) {
1103 return $def;
1104 }
1105
1106 if (is_string($params)) {
1107 $params = (array) json_decode($params, true);
1108 } elseif (is_object($params)) {
1109 $params = (array) $params;
1110 }
1111
1112 if (!is_array($params)) {
1113 return $def;
1114 }
1115
1116 return $params[$paramname] ?? $def;
1117 }
1118
1119 public static function filterNightsSeasonsCal($arr_nights) {
1120 $nights = array();
1121 foreach ($arr_nights as $night) {
1122 if (intval(trim($night)) > 0) {
1123 $nights[] = intval(trim($night));
1124 }
1125 }
1126 sort($nights);
1127 return array_unique($nights);
1128 }
1129
1130 public static function getSeasonRangeTs($from, $to, $year) {
1131 $sfrom = 0;
1132 $sto = 0;
1133 $tsbase = mktime(0, 0, 0, 1, 1, $year);
1134 $curyear = $year;
1135 $tsbasetwo = $tsbase;
1136 $curyeartwo = $year;
1137 if ($from > $to) {
1138 //between two years
1139 $curyeartwo += 1;
1140 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
1141 }
1142 $sfrom = ($tsbase + $from);
1143 $sto = ($tsbasetwo + $to);
1144 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
1145 //leap years
1146 $infoseason = getdate($sfrom);
1147 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1148 if ($infoseason[0] > $leapts) {
1149 /**
1150 * Timestamp must be greater than the leap-day of Feb 29th.
1151 * It used to be checked for >= $leapts.
1152 *
1153 * @since July 3rd 2019
1154 */
1155 $sfrom += 86400;
1156 if ($curyear == $curyeartwo) {
1157 $sto += 86400;
1158 }
1159 }
1160 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
1161 //leap years
1162 $infoseason = getdate($sto);
1163 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1164 if ($infoseason[0] > $leapts) {
1165 /**
1166 * Timestamp must be greater than the leap-day of Feb 29th.
1167 * It used to be checked for >= $leapts.
1168 *
1169 * @since July 3rd 2019
1170 */
1171 $sto += 86400;
1172 }
1173 }
1174 return array($sfrom, $sto);
1175 }
1176
1177 public static function sortSeasonsRangeTs ($all_seasons) {
1178 $sorted = array();
1179 $map = array();
1180 foreach ($all_seasons as $key => $season) {
1181 $map[$key] = $season['from_ts'];
1182 }
1183 asort($map);
1184 foreach ($map as $key => $s) {
1185 $sorted[] = $all_seasons[$key];
1186 }
1187 return $sorted;
1188 }
1189
1190 public static function formatSeasonDates ($from_ts, $to_ts) {
1191 $one = getdate($from_ts);
1192 $two = getdate($to_ts);
1193 $months_map = array(
1194 1 => JText::translate('VBSHORTMONTHONE'),
1195 2 => JText::translate('VBSHORTMONTHTWO'),
1196 3 => JText::translate('VBSHORTMONTHTHREE'),
1197 4 => JText::translate('VBSHORTMONTHFOUR'),
1198 5 => JText::translate('VBSHORTMONTHFIVE'),
1199 6 => JText::translate('VBSHORTMONTHSIX'),
1200 7 => JText::translate('VBSHORTMONTHSEVEN'),
1201 8 => JText::translate('VBSHORTMONTHEIGHT'),
1202 9 => JText::translate('VBSHORTMONTHNINE'),
1203 10 => JText::translate('VBSHORTMONTHTEN'),
1204 11 => JText::translate('VBSHORTMONTHELEVEN'),
1205 12 => JText::translate('VBSHORTMONTHTWELVE')
1206 );
1207 $mday_map = array(
1208 1 => JText::translate('VBMDAYFRIST'),
1209 2 => JText::translate('VBMDAYSECOND'),
1210 3 => JText::translate('VBMDAYTHIRD'),
1211 'generic' => JText::translate('VBMDAYNUMGEN')
1212 );
1213 if ($one['year'] == $two['year']) {
1214 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>';
1215 }
1216 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'];
1217 }
1218
1219 public static function getFirstCustDataField($custdata) {
1220 $first_field = '';
1221 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1222 //Room is closed with this booking
1223 return '----';
1224 }
1225 $parts = explode("\n", $custdata);
1226 foreach ($parts as $part) {
1227 if (!empty($part)) {
1228 $field = explode(':', trim($part));
1229 if (!empty($field[1])) {
1230 return trim($field[1]);
1231 }
1232 }
1233 }
1234 return $first_field;
1235 }
1236
1237 /**
1238 * This method composes a string to be logged for the admin
1239 * to keep track of what was inside the booking before the
1240 * modification. Returns a string and it uses language definitions
1241 * that should be available on the front-end and back-end INI files.
1242 *
1243 * @param array $old_booking the array of the booking prior to the modification
1244 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1245 *
1246 * @return string text describing the situation with the booking before the changes.
1247 *
1248 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1249 */
1250 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1251 {
1252 $vbo_df = self::getDateFormat();
1253 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1254 $wdays_map = array(
1255 JText::translate('VBWEEKDAYZERO'),
1256 JText::translate('VBWEEKDAYONE'),
1257 JText::translate('VBWEEKDAYTWO'),
1258 JText::translate('VBWEEKDAYTHREE'),
1259 JText::translate('VBWEEKDAYFOUR'),
1260 JText::translate('VBWEEKDAYFIVE'),
1261 JText::translate('VBWEEKDAYSIX'),
1262 );
1263 $now_info = getdate();
1264 $checkin_info = getdate($old_booking['checkin']);
1265 $checkout_info = getdate($old_booking['checkout']);
1266
1267 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1268 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1269 $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]);
1270
1271 $orooms_map = [];
1272 $orooms_arr = [];
1273
1274 // scan the previously booked room records
1275 foreach ((array) ($old_booking['rooms_info'] ?? []) as $oroom) {
1276 // determine room name
1277 $roomName = $oroom['name'] ?? $oroom['roomnamevb'] ?? $oroom['idroom'] ?? '---';
1278 $orooms_arr[] = sprintf(
1279 '%s, %s: %d, %s: %d',
1280 $roomName,
1281 JText::translate('VBMAILADULTS'),
1282 (int) ($oroom['adults'] ?? 0),
1283 JText::translate('VBMAILCHILDREN'),
1284 (int) ($oroom['children'] ?? 0),
1285 );
1286 if (!empty($oroom['idroom'])) {
1287 $orooms_map[$oroom['idroom']] = $roomName;
1288 }
1289 }
1290
1291 // build previous rooms booked string
1292 $prev_rooms = $orooms_arr ? implode("\n", $orooms_arr) : '';
1293
1294 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && $orooms_map) {
1295 $split_stay_prev_infos = [];
1296 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1297 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1298 continue;
1299 }
1300 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1301 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1302 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1303
1304 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1305 if (!empty($room_nights)) {
1306 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1307 }
1308 $split_stay_descr .= ', ';
1309 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1310
1311 // push split stay description string
1312 $split_stay_prev_infos[] = $split_stay_descr;
1313 }
1314 if (count($split_stay_prev_infos)) {
1315 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1316 }
1317 }
1318
1319 $currencyname = self::getCurrencyName();
1320 $prev_total = self::formatCurrencyNumber(self::numberFormat($old_booking['total']), $currencyname);
1321
1322 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1323 }
1324
1325 /**
1326 * This method invokes the class
1327 * VikChannelManagerLogos (new in VCM 1.6.4) to
1328 * map the name of a channel to its corresponding logo.
1329 * The method can also be used to get an istance of the class.
1330 *
1331 * @param mixed $provenience either a string or an array with main and sub channels.
1332 * @param boolean $get_istance
1333 *
1334 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1335 */
1336 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1337 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1338 return false;
1339 }
1340 if (!class_exists('VikChannelManagerLogos')) {
1341 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1342 }
1343
1344 /**
1345 * Due to the new iCal channel we now support main and sub channels.
1346 *
1347 * @since 1.13 - VCM 1.7.0
1348 */
1349 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1350 $provenience = explode('_', $provenience);
1351 }
1352 $main_channel = '';
1353 $full_channel = '';
1354 if (is_array($provenience)) {
1355 if (count($provenience) > 1) {
1356 $main_channel = $provenience[1];
1357 } else {
1358 $main_channel = $provenience[0];
1359 }
1360 if (stripos($provenience[0], 'ical') !== false) {
1361 $full_channel = implode('_', $provenience);
1362 }
1363 } else {
1364 $main_channel = $provenience;
1365 }
1366
1367 // get object instance by passing the main provenience
1368 $obj = new VikChannelManagerLogos($main_channel);
1369
1370 // update provenience with main and full channel
1371 if (!empty($full_channel)) {
1372 $obj->setProvenience($main_channel, $full_channel);
1373 }
1374
1375 // return either the instance or the logo URL for this channel source
1376 return $get_istance ? $obj : $obj->getLogoURL();
1377 }
1378
1379 public static function vcmAutoUpdate()
1380 {
1381 static $autoUpdate = null;
1382
1383 if ($autoUpdate !== null) {
1384 return $autoUpdate;
1385 }
1386
1387 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1388 $autoUpdate = -1;
1389 return $autoUpdate;
1390 }
1391
1392 $autoUpdate = (int) VBOFactory::getConfig()->getBool('vcmautoupd', false);
1393
1394 return $autoUpdate;
1395 }
1396
1397 public static function getVcmInvoker()
1398 {
1399 if (!class_exists('VboVcmInvoker')) {
1400 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1401 }
1402
1403 return new VboVcmInvoker();
1404 }
1405
1406 /**
1407 * Returns an instance of VikBooking History object.
1408 *
1409 * @param int $bid optional booking ID to bind.
1410 *
1411 * @return VboBookingHistory
1412 *
1413 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1414 */
1415 public static function getBookingHistoryInstance($bid = 0)
1416 {
1417 if (!class_exists('VboBookingHistory')) {
1418 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1419 }
1420
1421 return new VboBookingHistory($bid);
1422 }
1423
1424 /**
1425 * Returns an instance of the VCMChatHandler class to handle
1426 * the messaging/chat for the given reservation ID.
1427 *
1428 * @param int $oid the ID of the booking in VBO
1429 * @param string $channel the name of the source channel
1430 *
1431 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1432 *
1433 * @since 1.11.2 (J) - 1.1.2 (WP)
1434 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1435 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1436 * @since 1.17.6 (J) - 1.7.6 (WP) empty signature arguments will only require the dependencies.
1437 */
1438 public static function getVcmChatInstance($oid, $channel = null)
1439 {
1440 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1441 if (!is_file($vcm_messaging_helper)) {
1442 // VCM is not available
1443 return null;
1444 }
1445
1446 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1447 // VCM must be inactive, prevent the loading of the instance
1448 return null;
1449 }
1450
1451 // make sure the channel name is correct as it may contain the sub-network
1452 if (!empty($channel)) {
1453 $segments = explode('_', $channel);
1454 if (count($segments) > 1) {
1455 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1456 $channel = $segments[0];
1457 }
1458 }
1459
1460 // always require main file of the abstract class even if arguments are empty/invalid
1461 require_once $vcm_messaging_helper;
1462
1463 if (empty($oid) && empty($channel)) {
1464 // do not proceed in order to save resources
1465 return null;
1466 }
1467
1468 // return the instance of the class for this channel handler
1469 return VCMChatHandler::getInstance($oid, $channel);
1470 }
1471
1472 /**
1473 * Returns an instance of the VCMOpportunityHandler class to handle
1474 * the various opportunities through VCM.
1475 *
1476 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1477 *
1478 * @since 1.2.0
1479 */
1480 public static function getVcmOpportunityInstance() {
1481 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1482 if (!is_file($vcm_opp_helper)) {
1483 // VCM is not available
1484 return null;
1485 }
1486 // require main file of the class
1487 require_once $vcm_opp_helper;
1488 // return the instance of the class
1489 return VCMOpportunityHandler::getInstance();
1490 }
1491
1492 /**
1493 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1494 *
1495 * @param array $data optional data to bind to the object.
1496 *
1497 * @return bool
1498 *
1499 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1500 *
1501 * @requires VCM >= 1.8.24
1502 */
1503 public static function vcmBcomReportingSupported(array $data = [])
1504 {
1505 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1506 }
1507
1508 /**
1509 * Gets a list of all channels supporting the promotions.
1510 *
1511 * @param string $key the key of the handler.
1512 *
1513 * @return mixed false if VCM is not installed, empty array otherwise.
1514 *
1515 * @requires Vik Channel Manager 1.7.1
1516 *
1517 * @since 1.3.0
1518 */
1519 public static function getPromotionHandlers($key = null)
1520 {
1521 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1522 // VCM not installed
1523 return false;
1524 }
1525
1526 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1527 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1528 return [];
1529 }
1530
1531 return VikChannelManager::getPromotionHandlers($key);
1532 }
1533
1534 /**
1535 * Gets the factors for suggesting the application of the promotions.
1536 *
1537 * @param mixed $data some optional instructions to be passed as argument.
1538 *
1539 * @return mixed false if VCM is not installed, associative array otherwise.
1540 *
1541 * @requires Vik Channel Manager 1.7.1
1542 *
1543 * @since 1.3.0
1544 */
1545 public static function getPromotionFactors($data = null)
1546 {
1547 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1548 // VCM not installed
1549 return false;
1550 }
1551
1552 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1553 // VCM is outdated
1554 return false;
1555 }
1556
1557 return VikChannelManager::getPromotionFactors($data);
1558 }
1559
1560 public static function getTheme()
1561 {
1562 return VBOFactory::getConfig()->get('theme', '');
1563 }
1564
1565 public static function getFooterOrdMail($vbo_tn = null)
1566 {
1567 $dbo = JFactory::getDbo();
1568
1569 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1570 $dbo->setQuery($q);
1571 $ft = $dbo->loadAssocList();
1572
1573 if (!is_object($vbo_tn)) {
1574 $vbo_tn = self::getTranslator();
1575 }
1576
1577 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1578
1579 return $ft ? $ft[0]['setting'] : '';
1580 }
1581
1582 public static function requireLogin()
1583 {
1584 return VBOFactory::getConfig()->getBool('requirelogin', false);
1585 }
1586
1587 public static function autoRoomUnit()
1588 {
1589 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1590 }
1591
1592 public static function todayBookings()
1593 {
1594 static $todayBookings = null;
1595
1596 if ($todayBookings === null) {
1597 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1598 }
1599
1600 return $todayBookings;
1601 }
1602
1603 public static function couponsEnabled()
1604 {
1605 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1606 }
1607
1608 public static function customersPinEnabled()
1609 {
1610 return VBOFactory::getConfig()->getBool('enablepin', false);
1611 }
1612
1613 /**
1614 * Detects the type of visitor from the user agent.
1615 * Known types are: computer, smartphone, tablet.
1616 *
1617 * @param boolean $returnua whether the type of visitor should be returned. If false
1618 * boolean is returned in case of mobile device detected.
1619 * @param boolean $loadassets whether the system should load an apposite CSS file.
1620 *
1621 * @return mixed string for the type of visitor or boolean if mobile detected.
1622 *
1623 * @since 1.0.13 - Revision September 2018
1624 */
1625 public static function detectUserAgent($returnua = false, $loadassets = true) {
1626 $session = JFactory::getSession();
1627 $sval = $session->get('vbuseragent', '');
1628 $mobiles = array('tablet', 'smartphone');
1629 if (!empty($sval)) {
1630 if ($loadassets) {
1631 self::userAgentStyleSheet($sval);
1632 }
1633 return $returnua ? $sval : in_array($sval, $mobiles);
1634 }
1635
1636 // detect visitor (device) type
1637 $visitoris = 'computer';
1638 try {
1639 if (!class_exists('MobileDetector')) {
1640 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1641 }
1642 $detector = new MobileDetector;
1643 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1644 } catch (Exception $e) {
1645 // do nothing
1646 }
1647
1648 $session->set('vbuseragent', $visitoris);
1649 if ($loadassets) {
1650 self::userAgentStyleSheet($visitoris);
1651 }
1652
1653 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1654 }
1655
1656 public static function userAgentStyleSheet($ua)
1657 {
1658 /**
1659 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1660 */
1661 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1662 return;
1663 }
1664 //
1665
1666 $document = JFactory::getDocument();
1667 /**
1668 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1669 */
1670 if ($ua == 'smartphone') {
1671 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1672 } elseif ($ua == 'tablet') {
1673 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1674 }
1675 return true;
1676 }
1677
1678 public static function loadJquery()
1679 {
1680 return VBOFactory::getConfig()->getBool('loadjquery', true);
1681 }
1682
1683 public static function loadBootstrap()
1684 {
1685 return VBOFactory::getConfig()->getBool('bootstrap', true);
1686 }
1687
1688 public static function allowMultiLanguage()
1689 {
1690 return VBOFactory::getConfig()->getBool('multilang', true);
1691 }
1692
1693 public static function getTranslator()
1694 {
1695 if (!class_exists('VikBookingTranslator')) {
1696 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1697 }
1698
1699 return new VikBookingTranslator();
1700 }
1701
1702 /**
1703 * New method's name spelled correctly. The object will be cached by default.
1704 *
1705 * @return VikBookingCustomersPin
1706 *
1707 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1708 */
1709 public static function getCPinInstance()
1710 {
1711 static $cpin_instance = null;
1712
1713 if (!$cpin_instance) {
1714 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1715
1716 $cpin_instance = new VikBookingCustomersPin();
1717 }
1718
1719 return $cpin_instance;
1720 }
1721
1722 /**
1723 * Old method's name spelled wrongly for BC.
1724 *
1725 * @return VikBookingCustomersPin
1726 */
1727 public static function getCPinIstance()
1728 {
1729 return self::getCPinInstance();
1730 }
1731
1732 /**
1733 * Returns an instance of the VikBookingTracker Class.
1734 * It is also possible to call this method to just require the library.
1735 * This is useful for the back-end to access some static methods
1736 * without tracking any data.
1737 *
1738 * @param boolean $require_only whether to return the object.
1739 *
1740 * @return VikBookingTracker
1741 *
1742 * @since 1.11
1743 */
1744 public static function getTracker($require_only = false) {
1745 if (!class_exists('VikBookingTracker')) {
1746 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1747 }
1748 return $require_only ? true : VikBookingTracker::getInstance();
1749 }
1750
1751 /**
1752 * Returns an instance of the VikBookingOperator Class.
1753 *
1754 * @return VikBookingOperator
1755 *
1756 * @since 1.11
1757 */
1758 public static function getOperatorInstance() {
1759 if (!class_exists('VikBookingOperator')) {
1760 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1761 }
1762 return VikBookingOperator::getInstance();
1763 }
1764
1765 /**
1766 * Returns an instance of the VikBookingFestivities Class.
1767 *
1768 * @return VikBookingFestivities
1769 *
1770 * @since 1.12
1771 */
1772 public static function getFestivitiesInstance() {
1773 if (!class_exists('VikBookingFestivities')) {
1774 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1775 }
1776 return VikBookingFestivities::getInstance();
1777 }
1778
1779 /**
1780 * Returns an instance of the VikBookingCriticalDates Class.
1781 *
1782 * @return VikBookingCriticalDates
1783 *
1784 * @since 1.13.5
1785 */
1786 public static function getCriticalDatesInstance() {
1787 if (!class_exists('VikBookingCriticalDates')) {
1788 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1789 }
1790 return VikBookingCriticalDates::getInstance();
1791 }
1792
1793 /**
1794 * Checks whether the chat is enabled.
1795 *
1796 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1797 *
1798 * @since 1.12
1799 */
1800 public static function chatEnabled() {
1801 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1802 return -1;
1803 }
1804 $dbo = JFactory::getDbo();
1805 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1806 $dbo->setQuery($q);
1807 $dbo->execute();
1808 if (!$dbo->getNumRows()) {
1809 // enable the chat by default if VCM is installed
1810 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1811 $dbo->setQuery($q);
1812 $dbo->execute();
1813 return 1;
1814 }
1815 $s = $dbo->loadResult();
1816 return intval($s);
1817 }
1818
1819 /**
1820 * Loads the chat parameters from the configuration.
1821 *
1822 * @return object stdClass object from decoded JSON string
1823 *
1824 * @since 1.12
1825 */
1826 public static function getChatParams() {
1827 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1828 return new stdClass;
1829 }
1830 $dbo = JFactory::getDbo();
1831 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1832 $dbo->setQuery($q);
1833 $dbo->execute();
1834 if (!$dbo->getNumRows()) {
1835 // compose default and basic chat params for the first time
1836 $basic_params = new stdClass;
1837 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1838 $basic_params->av_type = 'checkin';
1839 $basic_params->av_days = 0;
1840
1841 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1842 $dbo->setQuery($q);
1843 $dbo->execute();
1844
1845 return $basic_params;
1846 }
1847 return json_decode($dbo->loadResult());
1848 }
1849
1850 /**
1851 * Checks whether the pre-checkin is enabled.
1852 *
1853 * @return int 0 if disabled, 1 otherwise
1854 *
1855 * @since 1.12
1856 */
1857 public static function precheckinEnabled()
1858 {
1859 return (int) VBOFactory::getConfig()->getBool('precheckinenabled', true);
1860 }
1861
1862 /**
1863 * Returns the minimum number of days in advance to
1864 * enable the pre-checkin via front-end.
1865 *
1866 * @return int the min number of days in advance for pre-checkin.
1867 *
1868 * @since 1.12
1869 */
1870 public static function precheckinMinOffset()
1871 {
1872 return VBOFactory::getConfig()->getInt('precheckinminoffset', 0);
1873 }
1874
1875 /**
1876 * Whether upselling extra services is enabled.
1877 *
1878 * @return boolean true if enabled, false otherwise.
1879 *
1880 * @since 1.13
1881 */
1882 public static function upsellingEnabled()
1883 {
1884 return VBOFactory::getConfig()->getBool('upselling', true);
1885 }
1886
1887 /**
1888 * Collects a list of options/extra that can be upsold for the
1889 * the rooms booked.
1890 *
1891 * @param array $upsell_data list of stdClass objects for each room booked.
1892 * @param array $info some booking details (id, checkin, checkout).
1893 * @param object $vbo_tn the translation object.
1894 *
1895 * @return array list of upsellable options for each room booked.
1896 *
1897 * @since 1.13 (J) - 1.3.0 (WP)
1898 * @since 1.17.7 (J) - 1.7.7 (WP) added support for damage deposit to confirmed bookings.
1899 */
1900 public static function loadUpsellingData($upsell_data, $info, $vbo_tn)
1901 {
1902 $dbo = JFactory::getDbo();
1903
1904 // get all rooms booked
1905 $all_room_ids = [];
1906 foreach ($upsell_data as $v) {
1907 if (!in_array($v->id, $all_room_ids)) {
1908 array_push($all_room_ids, (int) $v->id);
1909 }
1910 }
1911
1912 $dbo->setQuery(
1913 $dbo->getQuery(true)
1914 ->select([
1915 $dbo->qn('id'),
1916 $dbo->qn('idopt'),
1917 ])
1918 ->from($dbo->qn('#__vikbooking_rooms'))
1919 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_room_ids) . ')')
1920 );
1921 $records = $dbo->loadAssocList();
1922 if (!$records) {
1923 // no rooms found
1924 return [];
1925 }
1926
1927 // get all suitable options
1928 $all_options = [];
1929 $rooms_options = [];
1930 foreach ($records as $v) {
1931 $allopts = explode(';', $v['idopt']);
1932 $room_opt = [];
1933 foreach ($allopts as $o) {
1934 if (empty($o)) {
1935 continue;
1936 }
1937 if (!in_array($o, $all_options)) {
1938 array_push($all_options, (int) $o);
1939 }
1940 array_push($room_opt, (int) $o);
1941 }
1942 $rooms_options[$v['id']] = $room_opt;
1943 }
1944 if (!$all_options) {
1945 // no options found
1946 return [];
1947 }
1948
1949 // load all options that could be used by the booked rooms no matter what was already booked
1950 $dbo->setQuery(
1951 $dbo->getQuery(true)
1952 ->select('*')
1953 ->from($dbo->qn('#__vikbooking_optionals'))
1954 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1955 ->where($dbo->qn('forcesel') . ' = 0')
1956 ->where($dbo->qn('ifchildren') . ' = 0')
1957 ->where($dbo->qn('is_citytax') . ' = 0')
1958 ->where($dbo->qn('is_fee') . ' = 0')
1959 ->order($dbo->qn('ordering') . ' ASC')
1960 );
1961 $records = $dbo->loadAssocList();
1962
1963 if (!strcasecmp(($info['status'] ?? ''), 'confirmed')) {
1964 // merge mandatory damage deposit
1965 $dbo->setQuery(
1966 $dbo->getQuery(true)
1967 ->select('*')
1968 ->from($dbo->qn('#__vikbooking_optionals'))
1969 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1970 ->where($dbo->qn('forcesel') . ' = 1')
1971 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
1972 );
1973 $dd_records = $dbo->loadAssocList();
1974
1975 // merge with regular options
1976 $records = array_merge($records, $dd_records);
1977 }
1978
1979 if (!$records) {
1980 // no upsell-able options found
1981 return [];
1982 }
1983
1984 // filter options by available date and translate records
1985 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1986 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1987 $records = !is_array($records) ? [] : $records;
1988
1989 $tot_upsellable = 0;
1990 foreach ($upsell_data as $k => $rdata) {
1991 if (!isset($upsell_data[$k]->upsellable)) {
1992 $upsell_data[$k]->upsellable = [];
1993 }
1994 foreach ($records as $opt) {
1995 if (!empty($opt['ageintervals'])) {
1996 // upsellable options should not contain age intervals for children
1997 continue;
1998 }
1999 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
2000 // this option is not assigned to this room
2001 continue;
2002 }
2003 // check if the option is suited for this room party
2004 $clone_opt = [$opt];
2005 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
2006 if (!is_array($clone_opt) || !$clone_opt) {
2007 // this option is not suited for this room party
2008 continue;
2009 }
2010
2011 if (in_array($opt['id'], $rdata->options)) {
2012 // this option has already been booked
2013 continue;
2014 }
2015
2016 // push this option and increase counter
2017 array_push($upsell_data[$k]->upsellable, $opt);
2018 $tot_upsellable++;
2019 }
2020 }
2021
2022 // if no upsellable options were found, we return an empty array
2023 return $tot_upsellable > 0 ? $upsell_data : [];
2024 }
2025
2026 /**
2027 * Returns the minimum days in advance for booking, by considering by default
2028 * also the property closing dates. If the property is currently closed, then
2029 * the minimum number of days in advance will be increased.
2030 *
2031 * @param bool $no_closing_dates whether to skip checking closing dates.
2032 *
2033 * @return int the number of days in advance for booking.
2034 *
2035 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
2036 * getMinDaysAdvance($skipsession = false) to:
2037 * getMinDaysAdvance($no_closing_dates = false)
2038 * This was made to return the proper number of days in advance
2039 * in case the property is currently closed.
2040 */
2041 public static function getMinDaysAdvance($no_closing_dates = false)
2042 {
2043 // cache value in static var
2044 static $getMinDaysAdvance = null;
2045
2046 if ($getMinDaysAdvance) {
2047 return $getMinDaysAdvance;
2048 }
2049
2050 $mind = VBOFactory::getConfig()->getInt('mindaysadvance', 0);
2051
2052 // update cached var
2053 $getMinDaysAdvance = $mind;
2054
2055 if ($no_closing_dates) {
2056 // do not check the closing dates
2057 return $mind;
2058 }
2059
2060 // check if the property is currently closed
2061 $cur_closed_dates = self::getClosingDates();
2062 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
2063 $today_midnight = mktime(0, 0, 0);
2064 $closed_until = null;
2065 foreach ($cur_closed_dates as $kcd => $vcd) {
2066 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
2067 // closing period found
2068 $closed_until = $vcd['to'];
2069 break;
2070 }
2071 }
2072 if ($closed_until !== null) {
2073 // count the number of days until property is closed
2074 $mind = 0;
2075 $today_info = getdate($today_midnight);
2076 while ($today_info[0] <= $closed_until) {
2077 $mind++;
2078 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
2079 }
2080 }
2081 }
2082
2083 // update cached var
2084 $getMinDaysAdvance = $mind;
2085
2086 return $mind;
2087 }
2088
2089 public static function getDefaultNightsCalendar()
2090 {
2091 return VBOFactory::getConfig()->getInt('autodefcalnights', 1);
2092 }
2093
2094 public static function getSearchNumRooms($skipsession = true)
2095 {
2096 $dbo = JFactory::getDbo();
2097 if ($skipsession) {
2098 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2099 $dbo->setQuery($q);
2100 $dbo->execute();
2101 $s = $dbo->loadAssocList();
2102 return (int)$s[0]['setting'];
2103 }
2104 $session = JFactory::getSession();
2105 $sval = $session->get('vbsearchNumRooms', '');
2106 if (!empty($sval)) {
2107 return (int)$sval;
2108 }
2109 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2110 $dbo->setQuery($q);
2111 $dbo->execute();
2112 $s = $dbo->loadAssocList();
2113 $session->set('vbsearchNumRooms', $s[0]['setting']);
2114 return (int)$s[0]['setting'];
2115 }
2116
2117 public static function getSearchNumAdults($skipsession = true)
2118 {
2119 $dbo = JFactory::getDbo();
2120 if ($skipsession) {
2121 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2122 $dbo->setQuery($q);
2123 $dbo->execute();
2124 $s = $dbo->loadAssocList();
2125 return $s[0]['setting'];
2126 }
2127 $session = JFactory::getSession();
2128 $sval = $session->get('vbsearchNumAdults', '');
2129 if (!empty($sval)) {
2130 return $sval;
2131 }
2132 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2133 $dbo->setQuery($q);
2134 $dbo->execute();
2135 $s = $dbo->loadAssocList();
2136 $session->set('vbsearchNumAdults', $s[0]['setting']);
2137 return $s[0]['setting'];
2138 }
2139
2140 public static function getSearchNumChildren($skipsession = true)
2141 {
2142 $dbo = JFactory::getDbo();
2143 if ($skipsession) {
2144 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2145 $dbo->setQuery($q);
2146 $dbo->execute();
2147 $s = $dbo->loadAssocList();
2148 return $s[0]['setting'];
2149 }
2150 $session = JFactory::getSession();
2151 $sval = $session->get('vbsearchNumChildren', '');
2152 if (!empty($sval)) {
2153 return $sval;
2154 }
2155 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2156 $dbo->setQuery($q);
2157 $dbo->execute();
2158 $s = $dbo->loadAssocList();
2159 $session->set('vbsearchNumChildren', $s[0]['setting']);
2160 return $s[0]['setting'];
2161 }
2162
2163 public static function getSmartSearchType($skipsession = true)
2164 {
2165 $dbo = JFactory::getDbo();
2166 if ($skipsession) {
2167 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2168 $dbo->setQuery($q);
2169 $dbo->execute();
2170 $s = $dbo->loadAssocList();
2171 return $s[0]['setting'];
2172 }
2173 $session = JFactory::getSession();
2174 $sval = $session->get('vbsmartSearchType', '');
2175 if (!empty($sval)) {
2176 return $sval;
2177 }
2178 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2179 $dbo->setQuery($q);
2180 $dbo->execute();
2181 $s = $dbo->loadAssocList();
2182 $session->set('vbsmartSearchType', $s[0]['setting']);
2183 return $s[0]['setting'];
2184 }
2185
2186 /**
2187 * Retrieves the listing-level minimum advance booking offset.
2188 *
2189 * @param int $idroom The listing ID to evaluate.
2190 * @param bool $period Whether to format the value as a period string.
2191 *
2192 * @return int|string|null Number of hours or period string ("2H"), if set.
2193 *
2194 * @since 1.18.3 (J) - 1.8.3 (WP)
2195 */
2196 public static function getMinDateFutureListing($idroom, $period = false)
2197 {
2198 $config = VBOFactory::getConfig();
2199
2200 $room_level_min_notice = $config->get("room_{$idroom}_min_adv_notice", null);
2201
2202 if (is_null($room_level_min_notice) || $room_level_min_notice === '') {
2203 return null;
2204 }
2205
2206 if ($period) {
2207 return sprintf('%dH', (int) $room_level_min_notice);
2208 }
2209
2210 return (int) $room_level_min_notice;
2211 }
2212
2213 /**
2214 * Returns the maximum advance booking notice (offset) as a period.
2215 * It either returns the global configuration setting, or the value
2216 * defined at room-level, if any and if requested.
2217 *
2218 * @param int $idroom optional room ID for room-level setting.
2219 *
2220 * @return string the maximum booking notice period (i.e. +1Y).
2221 *
2222 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
2223 * $idroom to support room-level settings.
2224 */
2225 public static function getMaxDateFuture($idroom = 0)
2226 {
2227 static $max_adv_book_pool = [];
2228
2229 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
2230
2231 if (isset($max_adv_book_pool[$pool_id])) {
2232 return $max_adv_book_pool[$pool_id];
2233 }
2234
2235 $config = VBOFactory::getConfig();
2236
2237 if ($pool_id === 'global') {
2238 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
2239 } else {
2240 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
2241 if (!empty($room_level_max_notice)) {
2242 // setting is defined at room-level
2243 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2244 } else {
2245 // recurse for global setting
2246 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2247 }
2248 }
2249
2250 return $max_adv_book_pool[$pool_id];
2251 }
2252
2253 /**
2254 * Validates the maximum advance booking notice against the requested check-in
2255 * timestamp. The method supports the validation at both room and property levels.
2256 *
2257 * @param int $checkints the check-in date timestamp.
2258 * @param int $idroom the optional room ID for room-level validation.
2259 *
2260 * @return string error date string in case of validation failure.
2261 *
2262 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2263 */
2264 public static function validateMaxDateBookings($checkints, $idroom = 0)
2265 {
2266 $datelim = self::getMaxDateFuture($idroom);
2267 $datelim = empty($datelim) ? '+2y' : $datelim;
2268 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2269 $quantlim = substr($datelim, -1, 1);
2270
2271 $now = getdate();
2272 if ($quantlim == 'w') {
2273 $until_ts = strtotime("+$numlim weeks") + 86399;
2274 } else {
2275 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2276 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2277 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2278 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2279 }
2280
2281 if ($until_ts > $now[0] && $checkints > $until_ts) {
2282 $vbo_df = self::getDateFormat();
2283 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2284
2285 // error, return the maximum date in the future allowed
2286 return date($df, $until_ts);
2287 }
2288
2289 // validation passed
2290 return '';
2291 }
2292
2293 /**
2294 * Validates the minimum advance booking notice against the requested check-in
2295 * timestamp. The method supports the validation at both room and property levels.
2296 *
2297 * @param int $checkints The check-in date timestamp.
2298 * @param int $idroom Optional room ID for room-level validation.
2299 *
2300 * @return int|float Minimum days (int) or hours (float) in advance required.
2301 *
2302 * @since 1.18.3 (J) - 1.8.3 (WP) introduced $idroom argument for room-level.
2303 */
2304 public static function validateMinDaysAdvance($checkints, $idroom = 0)
2305 {
2306 if ($idroom) {
2307 // listing-level minimum advance booking notice (hours, could be null or 0)
2308 $minhadv = self::getMinDateFutureListing($idroom);
2309
2310 // perform listing-level validation only when value is set
2311 if (!$minhadv) {
2312 // validation passed
2313 return '';
2314 }
2315
2316 // access listing params
2317 $listing_data = self::getRoomInfo($idroom, ['params'], $no_cache = true);
2318 if ($listing_data['params'] ?? null) {
2319 // access listing-level check-in time
2320 $listing_checkin = self::getRoomParam('checkin', $listing_data['params']);
2321 if ($listing_checkin) {
2322 // overwrite check-in time
2323 $listing_checkin_parts = explode(':', $listing_checkin);
2324 $checkin_hours = (int) $listing_checkin_parts[0];
2325 $checkin_minutes = (int) $listing_checkin_parts[1];
2326 // read current check-in date-time
2327 $tsinfo = getdate($checkints);
2328 // update check-in time with proper hours, minutes and seconds
2329 $checkints = mktime($checkin_hours, $checkin_minutes, 0, $tsinfo['mon'], $tsinfo['mday'], $tsinfo['year']);
2330 }
2331 }
2332
2333 if ($checkints < strtotime('+' . $minhadv . ($minhadv > 1 ? 'hours' : 'hour'))) {
2334 return $minhadv >= 24 ? floor($minhadv / 24) : ($minhadv / 24);
2335 }
2336
2337 // validation passed without checking the global settings when listing ID given
2338 return '';
2339 }
2340
2341 // global validation
2342 $mindadv = self::getMinDaysAdvance(true);
2343 if ($mindadv > 0) {
2344 $tsinfo = getdate($checkints);
2345 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2346 if ($checkints < $limit_ts) {
2347 return $mindadv;
2348 }
2349 }
2350
2351 // validation passed
2352 return '';
2353 }
2354
2355 /**
2356 * The only supported calendar type has been changed to jQuery UI.
2357 *
2358 * @since 1.13
2359 */
2360 public static function calendarType($skipsession = false)
2361 {
2362 return 'jqueryui';
2363 }
2364
2365 public static function getSiteLogo()
2366 {
2367 static $siteLogo = null;
2368
2369 if ($siteLogo !== null) {
2370 return $siteLogo;
2371 }
2372
2373 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2374
2375 return $siteLogo;
2376 }
2377
2378 public static function getBackendLogo()
2379 {
2380 static $backLogo = null;
2381
2382 if ($backLogo !== null) {
2383 return $backLogo;
2384 }
2385
2386 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2387
2388 return $backLogo;
2389 }
2390
2391 public static function numCalendars()
2392 {
2393 static $numCalendars = null;
2394
2395 if ($numCalendars !== null) {
2396 return $numCalendars;
2397 }
2398
2399 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2400
2401 return $numCalendars;
2402 }
2403
2404 public static function getFirstWeekDay()
2405 {
2406 static $firstWeekDay = null;
2407
2408 if ($firstWeekDay !== null) {
2409 return $firstWeekDay;
2410 }
2411
2412 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2413
2414 return $firstWeekDay;
2415 }
2416
2417 public static function showPartlyReserved()
2418 {
2419 static $partlyReserved = null;
2420
2421 if ($partlyReserved !== null) {
2422 return $partlyReserved;
2423 }
2424
2425 $partlyReserved = VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2426
2427 return $partlyReserved;
2428 }
2429
2430 public static function showStatusCheckinoutOnly()
2431 {
2432 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2433 }
2434
2435 public static function getDisclaimer($vbo_tn = null)
2436 {
2437 $dbo = JFactory::getDbo();
2438
2439 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2440 $dbo->setQuery($q);
2441 $ft = $dbo->loadAssocList();
2442 if (!$ft) {
2443 return '';
2444 }
2445
2446 if (!is_object($vbo_tn)) {
2447 $vbo_tn = self::getTranslator();
2448 }
2449 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2450
2451 return $ft[0]['setting'];
2452 }
2453
2454 public static function showFooter()
2455 {
2456 return VBOFactory::getConfig()->getBool('showfooter', false);
2457 }
2458
2459 public static function getPriceName($idp, $vbo_tn = null)
2460 {
2461 $dbo = JFactory::getDbo();
2462
2463 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2464 $dbo->setQuery($q, 0, 1);
2465 $n = $dbo->loadAssocList();
2466 if ($n) {
2467 if (is_object($vbo_tn)) {
2468 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2469 }
2470 return $n[0]['name'];
2471 }
2472
2473 return "";
2474 }
2475
2476 public static function getPriceAttr($idp, $vbo_tn = null)
2477 {
2478 $dbo = JFactory::getDbo();
2479
2480 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2481 $dbo->setQuery($q, 0, 1);
2482 $n = $dbo->loadAssocList();
2483 if ($n) {
2484 if (is_object($vbo_tn)) {
2485 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2486 }
2487 return $n[0]['attr'];
2488 }
2489
2490 return "";
2491 }
2492
2493 /**
2494 * Fetches the requested rate plan information.
2495 *
2496 * @param int $idp the rate plan ID.
2497 * @param object $vbo_tn the Vik Booking translator object.
2498 *
2499 * @return array empty array or associative record.
2500 */
2501 public static function getPriceInfo($idp, $vbo_tn = null)
2502 {
2503 // cache values
2504 static $priceInfos = [];
2505
2506 $idp = (int)$idp;
2507
2508 if ($priceInfos && isset($priceInfos[$idp])) {
2509 return $priceInfos[$idp];
2510 }
2511
2512 $dbo = JFactory::getDbo();
2513
2514 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2515 $dbo->setQuery($q, 0, 1);
2516 $rplan = $dbo->loadAssocList();
2517 if (!$rplan) {
2518 return [];
2519 }
2520
2521 if (is_object($vbo_tn)) {
2522 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2523 }
2524
2525 // set cached value
2526 $priceInfos[$idp] = $rplan[0];
2527
2528 return $rplan[0];
2529 }
2530
2531 public static function getAliq($idal)
2532 {
2533 $dbo = JFactory::getDbo();
2534
2535 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2536 $dbo->setQuery($q);
2537 $aliq = $dbo->loadResult();
2538 if ($aliq) {
2539 return $aliq;
2540 }
2541
2542 return 0;
2543 }
2544
2545 public static function getTimeOpenStore($skipsession = false)
2546 {
2547 // cache value in static var
2548 static $getTimeOpenStore = null;
2549
2550 if ($getTimeOpenStore) {
2551 return $getTimeOpenStore;
2552 }
2553 //
2554
2555 $dbo = JFactory::getDbo();
2556 $session = JFactory::getSession();
2557
2558 if ($skipsession) {
2559 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2560 if (empty($setting) && $setting != "0") {
2561 return false;
2562 } else {
2563 $x = explode("-", $setting);
2564 if (!empty($x[1]) && $x[1] != "0") {
2565 $getTimeOpenStore = $x;
2566 return $x;
2567 }
2568 }
2569 } else {
2570 $sval = $session->get('vbgetTimeOpenStore', '');
2571 if (!empty($sval)) {
2572 return $sval;
2573 } else {
2574 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2575 if (empty($setting) && $setting != "0") {
2576 return false;
2577 } else {
2578 $x = explode("-", $setting);
2579 if (!empty($x[1]) && $x[1] != "0") {
2580 $session->set('vbgetTimeOpenStore', $x);
2581 $getTimeOpenStore = $x;
2582 return $x;
2583 }
2584 }
2585 }
2586 }
2587 return false;
2588 }
2589
2590 public static function getHoursMinutes($secs)
2591 {
2592 if ($secs >= 3600) {
2593 $op = $secs / 3600;
2594 $hours = floor($op);
2595 $less = $hours * 3600;
2596 $newsec = $secs - $less;
2597 $optwo = $newsec / 60;
2598 $minutes = floor($optwo);
2599 } else {
2600 $hours = "0";
2601 $optwo = $secs / 60;
2602 $minutes = floor($optwo);
2603 }
2604
2605 return [$hours, $minutes];
2606 }
2607
2608 public static function getClosingDates()
2609 {
2610 // cache value in static var
2611 static $getClosingDates = null;
2612
2613 if (is_array($getClosingDates)) {
2614 return $getClosingDates;
2615 }
2616
2617 $dbo = JFactory::getDbo();
2618
2619 $allcd = [];
2620
2621 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2622 $dbo->setQuery($q);
2623 $s = $dbo->loadAssocList();
2624 if ($s && !empty($s[0]['setting'])) {
2625 $allcd = json_decode($s[0]['setting'], true);
2626 $allcd = is_array($allcd) ? $allcd : array();
2627 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2628 foreach ($allcd as $k => $v) {
2629 if ($v['to'] < $base_ts) {
2630 unset($allcd[$k]);
2631 }
2632 }
2633 $allcd = array_values($allcd);
2634 }
2635
2636 $getClosingDates = $allcd;
2637
2638 return $allcd;
2639 }
2640
2641 public static function parseJsClosingDates()
2642 {
2643 $cd = self::getClosingDates();
2644 if (!$cd) {
2645 return [];
2646 }
2647
2648 $cdjs = [];
2649 foreach ($cd as $k => $v) {
2650 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2651 }
2652
2653 return $cdjs;
2654 }
2655
2656 /**
2657 * Given stay date timestamps, tells if the booking is disallowed due to global closing dates.
2658 *
2659 * @param int $checkints The check-in date timestamp.
2660 * @param int $checkoutts The check-in date timestamp.
2661 * @param ?string $df Optional date format to return the closed stay dates.
2662 * @param bool $closingcmp True to obtain the stay components affected by a global closure.
2663 *
2664 * @return string|array Empty string if no closures, closure dates string or affected components list.
2665 *
2666 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $closingcmp to improve the search suggestion details.
2667 */
2668 public static function validateClosingDates($checkints, $checkoutts, $df = null, $closingcmp = false)
2669 {
2670 $cd = self::getClosingDates();
2671
2672 if (!$cd) {
2673 return '';
2674 }
2675
2676 $df = empty($df) ? 'Y-m-d' : $df;
2677 $margin_seconds = 22 * 60 * 60;
2678
2679 foreach ($cd as $k => $v) {
2680 // determine if the stay dates are closed
2681 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2682 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2683 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2684
2685 if ($inner_closed || $outer_closed || $middle_closed) {
2686 // stay dates are closed
2687 if ($closingcmp) {
2688 // return the components affected by the globally closed dates to allow analysis
2689 return [
2690 'checkin' => $inner_closed,
2691 'checkout' => $outer_closed,
2692 'stay' => $middle_closed,
2693 ];
2694 }
2695
2696 // return the string with the range of dates closed globally
2697 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2698 }
2699 }
2700
2701 return '';
2702 }
2703
2704 /**
2705 * Whether the categories dropdown filter menu should be displayed.
2706 *
2707 * @param bool $skipsession [optional] re-read the configuration setting.
2708 *
2709 * @return bool
2710 *
2711 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2712 */
2713 public static function showCategoriesFront($skipsession = true)
2714 {
2715 return VBOFactory::getConfig()->getBool('showcategories', false);
2716 }
2717
2718 /**
2719 * Whether the number of children dropdown menu should be displayed.
2720 * Defaults to skip the session values and to re-read the configuration setting.
2721 *
2722 * @param bool $skipsession [optional] re-read the configuration setting.
2723 *
2724 * @return bool
2725 *
2726 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2727 */
2728 public static function showChildrenFront($skipsession = true)
2729 {
2730 return VBOFactory::getConfig()->getBool('showchildren', true);
2731 }
2732
2733 public static function allowBooking()
2734 {
2735 // cache value in static var
2736 static $allowBooking = null;
2737
2738 if (is_bool($allowBooking)) {
2739 return $allowBooking;
2740 }
2741
2742 $allowBooking = (bool) VBOFactory::getConfig()->get('allowbooking');
2743
2744 return $allowBooking;
2745 }
2746
2747 public static function getDisabledBookingMsg($vbo_tn = null) {
2748 $dbo = JFactory::getDbo();
2749 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2750 $dbo->setQuery($q);
2751 $dbo->execute();
2752 $s = $dbo->loadAssocList();
2753 if (!is_object($vbo_tn)) {
2754 $vbo_tn = self::getTranslator();
2755 }
2756 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2757 return $s[0]['setting'];
2758 }
2759
2760 /**
2761 * Returns the text for the guests allowed policy.
2762 *
2763 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2764 *
2765 * @return string either the raw or the translated policy text.
2766 *
2767 * @since 1.16.5 (WP) - 1.6.5 (WP)
2768 */
2769 public static function getGuestsAllowedPolicy($vbo_tn = null)
2770 {
2771 $dbo = JFactory::getDbo();
2772
2773 $q = $dbo->getQuery(true)
2774 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2775 ->from($dbo->qn('#__vikbooking_texts'))
2776 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2777
2778 $dbo->setQuery($q);
2779 $text_record = $dbo->loadAssocList();
2780
2781 if (!$text_record) {
2782 // missing record, insert it with empty values
2783 if (self::isAdmin()) {
2784 $record = new stdClass;
2785 $record->param = 'guests_allowed_policy';
2786 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2787 $record->setting = '';
2788 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2789 }
2790
2791 return '';
2792 }
2793
2794 if (is_object($vbo_tn)) {
2795 // translate the record only if requested
2796 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2797 }
2798
2799 return $text_record[0]['setting'];
2800 }
2801
2802 public static function getDateFormat($skipsession = true)
2803 {
2804 // cache value in static var
2805 static $getDateFormat = null;
2806
2807 if ($getDateFormat) {
2808 return $getDateFormat;
2809 }
2810
2811 $dbo = JFactory::getDbo();
2812
2813 if ($skipsession) {
2814 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2815 $dbo->setQuery($q, 0, 1);
2816 $getDateFormat = $dbo->loadResult();
2817
2818 return $getDateFormat;
2819 }
2820
2821 $session = JFactory::getSession();
2822 $sval = $session->get('vbgetDateFormat', '');
2823 if (!empty($sval)) {
2824 return $sval;
2825 }
2826
2827 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2828 $dbo->setQuery($q, 0, 1);
2829 $getDateFormat = $dbo->loadResult();
2830
2831 $session->set('vbgetDateFormat', $getDateFormat);
2832
2833 return $getDateFormat;
2834 }
2835
2836 public static function getDateSeparator($skipsession = true)
2837 {
2838 // cache value in static var
2839 static $getDateSeparator = null;
2840
2841 if ($getDateSeparator) {
2842 return $getDateSeparator;
2843 }
2844
2845 $dbo = JFactory::getDbo();
2846
2847 if ($skipsession) {
2848 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2849 $dbo->setQuery($q, 0, 1);
2850 $val = $dbo->loadResult();
2851
2852 $getDateSeparator = empty($val) ? "/" : $val;
2853
2854 return $getDateSeparator;
2855 }
2856
2857 $session = JFactory::getSession();
2858 $sval = $session->get('vbgetDateSep', '');
2859 if (!empty($sval)) {
2860 return $sval;
2861 }
2862
2863 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2864 $dbo->setQuery($q, 0, 1);
2865 $val = $dbo->loadResult();
2866
2867 $getDateSeparator = empty($val) ? "/" : $val;
2868
2869 $session->set('vbgetDateSep', $getDateSeparator);
2870
2871 return $getDateSeparator;
2872 }
2873
2874 public static function getHoursMoreRb($skipsession = false)
2875 {
2876 // cache value in static var
2877 static $getHoursMoreRb = null;
2878
2879 if ($getHoursMoreRb !== null) {
2880 return $getHoursMoreRb;
2881 }
2882
2883 $dbo = JFactory::getDbo();
2884
2885 if ($skipsession) {
2886 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2887 $dbo->setQuery($q, 0, 1);
2888 $getHoursMoreRb = (int)$dbo->loadResult();
2889
2890 return $getHoursMoreRb;
2891 }
2892
2893 $session = JFactory::getSession();
2894 $sval = $session->get('getHoursMoreRb', '');
2895 if (strlen($sval)) {
2896 return $sval;
2897 }
2898
2899 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2900 $dbo->setQuery($q, 0, 1);
2901 $getHoursMoreRb = (int)$dbo->loadResult();
2902
2903 $session->set('getHoursMoreRb', $getHoursMoreRb);
2904
2905 return $getHoursMoreRb;
2906 }
2907
2908 public static function getHoursRoomAvail()
2909 {
2910 $dbo = JFactory::getDbo();
2911
2912 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2913 $dbo->setQuery($q, 0, 1);
2914
2915 return (int)$dbo->loadResult();
2916 }
2917
2918 public static function getFrontTitle($vbo_tn = null) {
2919 // cache value in static var
2920 static $getFrontTitle = null;
2921
2922 if ($getFrontTitle) {
2923 return $getFrontTitle;
2924 }
2925 //
2926
2927 $dbo = JFactory::getDbo();
2928 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2929 $dbo->setQuery($q);
2930 $dbo->execute();
2931 $ft = $dbo->loadAssocList();
2932 if (!is_object($vbo_tn)) {
2933 $vbo_tn = self::getTranslator();
2934 }
2935 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2936
2937 $getFrontTitle = $ft[0]['setting'];
2938
2939 return $ft[0]['setting'];
2940 }
2941
2942 public static function getFrontTitleTag()
2943 {
2944 return VBOFactory::getConfig()->get('fronttitletag', '');
2945 }
2946
2947 public static function getFrontTitleTagClass()
2948 {
2949 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2950 }
2951
2952 /**
2953 * Returns the configured currency name. Often used to apply currency
2954 * conversions, and so it should match the currency ISO 4217 (Alpha-3) code.
2955 *
2956 * @param bool $iso True to ensure we fetch a 3-char currency code.
2957 *
2958 * @return string
2959 *
2960 * @since 1.18.0 (J) - 1.8.0 (WP) added argument $iso.
2961 */
2962 public static function getCurrencyName($iso = false)
2963 {
2964 static $currencyName = null;
2965
2966 if ($currencyName !== null) {
2967 return $currencyName;
2968 }
2969
2970 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2971
2972 if ($iso && strlen((string) $currencyName) != 3) {
2973 // attempt to fetch the currency code configuration setting
2974 $currencyName = self::getCurrencyCodePp();
2975 }
2976
2977 return $currencyName;
2978 }
2979
2980 public static function getCurrencySymb()
2981 {
2982 // cache value in static var
2983 static $getCurrencySymb = null;
2984
2985 if ($getCurrencySymb) {
2986 return $getCurrencySymb;
2987 }
2988
2989 $getCurrencySymb = VBOFactory::getConfig()->get('currencysymb', '');
2990
2991 return $getCurrencySymb;
2992 }
2993
2994 /**
2995 * Returns the configured currency position: either "before" or "after".
2996 *
2997 * @return string
2998 *
2999 * @since 1.18.3 (J) - 1.8.3 (WP)
3000 */
3001 public static function getCurrencyPosition()
3002 {
3003 // cache value in static var
3004 static $getCurrencyPos = null;
3005
3006 if ($getCurrencyPos) {
3007 return $getCurrencyPos;
3008 }
3009
3010 $getCurrencyPos = VBOFactory::getConfig()->get('currencypos', 'before');
3011
3012 return $getCurrencyPos;
3013 }
3014
3015 /**
3016 * Given a number, formats it according to currency preferences.
3017 *
3018 * @param string $number The number to format.
3019 * @param ?string $currency Optional currency to use.
3020 * @param ?array $config Optional format configuration (i.e. currency/number "HTML template").
3021 *
3022 * @return string
3023 *
3024 * @since 1.18.3 (J) - 1.8.3 (WP)
3025 */
3026 public static function formatCurrencyNumber(string $number, ?string $currency = null, ?array $config = null)
3027 {
3028 // get the currency symbol/value to use
3029 $currency = $currency ?: self::getCurrencySymb();
3030
3031 // obtain currency template (wildcard expected)
3032 if (is_string($config[0] ?? null) || is_string($config['currency'] ?? null)) {
3033 // apply templating to string
3034 $currency = sprintf($config[0] ?? $config['currency'], $currency);
3035 }
3036
3037 // obtain number template
3038 if (is_string($config[1] ?? null) || is_string($config['number'] ?? null)) {
3039 // apply templating to string
3040 $number = sprintf($config[1] ?? $config['number'], $number);
3041 }
3042
3043 // default value spacing (empty white space)
3044 $spacing = (string) ($config['spacing'] ?? ' ');
3045
3046 if (self::getCurrencyPosition() === 'after') {
3047 return sprintf('%s' . $spacing . '%s', $number, $currency);
3048 }
3049
3050 return sprintf('%s' . $spacing . '%s', $currency, $number);
3051 }
3052
3053 public static function getCurrencyCodePp()
3054 {
3055 static $currencyCode = null;
3056
3057 if ($currencyCode !== null) {
3058 return $currencyCode;
3059 }
3060
3061 $currencyCode = VBOFactory::getConfig()->get('currencycodepp', '');
3062
3063 return $currencyCode;
3064 }
3065
3066 public static function getNumberFormatData()
3067 {
3068 // cache value in static var
3069 static $getNumberFormatData = null;
3070
3071 if ($getNumberFormatData) {
3072 return $getNumberFormatData;
3073 }
3074
3075 $getNumberFormatData = VBOFactory::getConfig()->get('numberformat', '');
3076
3077 return $getNumberFormatData;
3078 }
3079
3080 /**
3081 * It is possible to hide the decimals if they are like "N.00".
3082 *
3083 * @return int 0 if disabled, 1 if enabled (default).
3084 *
3085 * @since 1.14 (J) - 1.4.0 (WP)
3086 */
3087 public static function hideEmptyDecimals()
3088 {
3089 // cache value in static var
3090 static $hideEmptyDecimals = null;
3091
3092 if ($hideEmptyDecimals !== null) {
3093 return $hideEmptyDecimals;
3094 }
3095
3096 $hideEmptyDecimals = (int) VBOFactory::getConfig()->get('noemptydecimals', '1');
3097
3098 return $hideEmptyDecimals;
3099 }
3100
3101 public static function numberFormat($num)
3102 {
3103 $formatvals = self::getNumberFormatData();
3104 $formatparts = explode(':', $formatvals);
3105
3106 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
3107 // number has got no decimals
3108 $formatparts[0] = 0;
3109 }
3110
3111 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
3112 }
3113
3114 /**
3115 * Given a date timestamp, returns the formatted date.
3116 *
3117 * @param int|string $ts the date representation in Unix timestamp.
3118 * @param bool $wtime if true, a formatted date-time will be returned.
3119 *
3120 * @return string the formatted date according to settings.
3121 *
3122 * @since 1.16.0 (J) - 1.6.0 (WP)
3123 */
3124 public static function formatDateTs($ts, $wtime = false)
3125 {
3126 $nowdf = self::getDateFormat();
3127 if ($nowdf == "%d/%m/%Y") {
3128 $df = 'd/m/Y';
3129 } elseif ($nowdf == "%m/%d/%Y") {
3130 $df = 'm/d/Y';
3131 } else {
3132 $df = 'Y/m/d';
3133 }
3134 $datesep = self::getDateSeparator();
3135
3136 $time_f = $wtime ? ' H:i' : '';
3137
3138 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
3139 }
3140
3141 /**
3142 * Given a country name, 3-char or 2-char code, this method attempts
3143 * to guess the best language to assign to the booking according to
3144 * what languages are installed on the website. This way, cron jobs
3145 * and any other email notification will be correctly and automatically
3146 * sent to the guest in the proper language without any manual action.
3147 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
3148 *
3149 * @param string $country the country name, 3-char or 2-char code.
3150 * @param string $locale optional locale lang tag of the guest.
3151 *
3152 * @return mixed the best lang-tag string to use or null.
3153 *
3154 * @since 1.16.0 (J) - 1.6.0 (WP)
3155 */
3156 public static function guessBookingLangFromCountry($country, $locale = null)
3157 {
3158 if (empty($country)) {
3159 return null;
3160 }
3161
3162 // get all the available languages
3163 $known_langs = self::getVboApplication()->getKnownLanguages();
3164 if (!is_array($known_langs) || !count($known_langs)) {
3165 return null;
3166 }
3167
3168 // check if the booking included a supported "locale" for the guest
3169 if (!empty($locale)) {
3170 // make the locale compatible with the language tags format
3171 $locale = str_replace('_', '-', $locale);
3172 foreach ($known_langs as $ltag => $ldet) {
3173 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
3174 // we support this language tag, so we just use it
3175 return $ltag;
3176 }
3177 }
3178 }
3179
3180 // build similarities with country-languages
3181 $similarities = array(
3182 'AU' => 'en',
3183 'GB' => 'en',
3184 'IE' => 'en',
3185 'NZ' => 'en',
3186 'US' => 'en',
3187 'CA' => array(
3188 'en',
3189 'fr',
3190 ),
3191 'CL' => 'es',
3192 'AR' => 'es',
3193 'PE' => 'es',
3194 'MX' => 'es',
3195 'CR' => 'es',
3196 'CO' => 'es',
3197 'EC' => 'es',
3198 'BO' => 'es',
3199 'CU' => 'es',
3200 'VE' => 'es',
3201 'BE' => 'fr',
3202 'LU' => 'fr',
3203 'CH' => array(
3204 'de',
3205 'it',
3206 'fr',
3207 ),
3208 'AT' => 'de',
3209 'GR' => 'el',
3210 'GL' => 'dk',
3211 );
3212
3213 // fetch values from db
3214 $dbo = JFactory::getDbo();
3215 $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);
3216 $dbo->setQuery($q, 0, 1);
3217 $dbo->execute();
3218 if (!$dbo->getNumRows()) {
3219 return null;
3220 }
3221 $country_record = $dbo->loadAssoc();
3222
3223 // assign country name/code versions
3224 $country_name = $country_record['country_name'];
3225 $country_3char = strtoupper($country_record['country_3_code']);
3226 $country_2char = strtoupper($country_record['country_2_code']);
3227
3228 // build an associative array of language tags and related match-score
3229 $langtags_score = array();
3230 foreach ($known_langs as $ltag => $ldet) {
3231 // default language tag score is 0 for no matches
3232 $langtags_score[$ltag] = 0;
3233 // get language and country codes
3234 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
3235
3236 // check matches with the installed language details
3237 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
3238 // increase language tag score
3239 $langtags_score[$ltag]++;
3240 }
3241 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
3242 // increase language tag score
3243 $langtags_score[$ltag]++;
3244 }
3245 if (!empty($ldet['locale'])) {
3246 // sanitize locale for matching the 2-char code safely
3247 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
3248 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
3249 // increase language tag score
3250 $langtags_score[$ltag]++;
3251 }
3252 }
3253 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
3254 // increase language tag score
3255 $langtags_score[$ltag]++;
3256 }
3257 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
3258 // increase language tag score
3259 $langtags_score[$ltag]++;
3260 }
3261
3262 // check language similarities between countries
3263 if (isset($similarities[$country_2char])) {
3264 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
3265 // check if language tag(s) is available for this spoken language
3266 foreach ($spoken_tags as $spoken_tag) {
3267 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
3268 // increase language tag score
3269 $langtags_score[$ltag]++;
3270 }
3271 }
3272 }
3273 }
3274
3275 // make sure at least one language tag has got some points
3276 if (max($langtags_score) === 0) {
3277 // no languages installed to honor this country
3278 return null;
3279 }
3280
3281 // sort language tag scores
3282 arsort($langtags_score);
3283
3284 // reset array pointer to the first (highest) element
3285 reset($langtags_score);
3286
3287 // return the language tag with the highest score
3288 return key($langtags_score);
3289 }
3290
3291 public static function getIntroMain($vbo_tn = null) {
3292 $dbo = JFactory::getDbo();
3293 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
3294 $dbo->setQuery($q);
3295 $dbo->execute();
3296 $ft = $dbo->loadAssocList();
3297 if (!is_object($vbo_tn)) {
3298 $vbo_tn = self::getTranslator();
3299 }
3300 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3301 return $ft[0]['setting'];
3302 }
3303
3304 public static function getClosingMain($vbo_tn = null) {
3305 $dbo = JFactory::getDbo();
3306 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3307 $dbo->setQuery($q);
3308 $dbo->execute();
3309 $ft = $dbo->loadAssocList();
3310 if (!is_object($vbo_tn)) {
3311 $vbo_tn = self::getTranslator();
3312 }
3313 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3314 return $ft[0]['setting'];
3315 }
3316
3317 public static function getFullFrontTitle($vbo_tn = null) {
3318 $dbo = JFactory::getDbo();
3319 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3320 $dbo->setQuery($q);
3321 $dbo->execute();
3322 $ft = $dbo->loadAssocList();
3323 if (!is_object($vbo_tn)) {
3324 $vbo_tn = self::getTranslator();
3325 }
3326 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3327 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3328 $dbo->setQuery($q);
3329 $dbo->execute();
3330 $fttag = $dbo->loadAssocList();
3331 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3332 $dbo->setQuery($q);
3333 $dbo->execute();
3334 $fttagclass = $dbo->loadAssocList();
3335 if (empty($ft[0]['setting'])) {
3336 return "";
3337 } else {
3338 if (empty($fttag[0]['setting'])) {
3339 return $ft[0]['setting'] . "<br/>\n";
3340 } else {
3341 $tag = str_replace("<", "", $fttag[0]['setting']);
3342 $tag = str_replace(">", "", $tag);
3343 $tag = str_replace("/", "", $tag);
3344 $tag = trim($tag);
3345 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3346 }
3347 }
3348 }
3349
3350 /**
3351 * Tells whether a date string is valid.
3352 *
3353 * @param ?string $date The date to evaluate.
3354 *
3355 * @return bool
3356 *
3357 * @since 1.18.8 (J) - 1.8.8 (WP) Military format Y-m-d is always accepted.
3358 */
3359 public static function dateIsValid(?string $date)
3360 {
3361 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", (string) $date)) {
3362 // date is in Y-m-d format (with no time)
3363 return true;
3364 }
3365
3366 $df = self::getDateFormat();
3367 $datesep = self::getDateSeparator();
3368 if (strlen((string) $date) != 10) {
3369 return false;
3370 }
3371 $cur_dsep = "/";
3372 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3373 $cur_dsep = $datesep;
3374 }
3375 $x = explode($cur_dsep, $date);
3376 if ($df == "%d/%m/%Y") {
3377 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3378 return false;
3379 }
3380 } elseif ($df == "%m/%d/%Y") {
3381 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3382 return false;
3383 }
3384 } else {
3385 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3386 return false;
3387 }
3388 }
3389 return true;
3390 }
3391
3392 public static function sayDateFormat() {
3393 $dbo = JFactory::getDbo();
3394 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3395 $dbo->setQuery($q);
3396 $dbo->execute();
3397 $s = $dbo->loadAssocList();
3398 if ($s[0]['setting'] == "%d/%m/%Y") {
3399 return JText::translate('VBCONFIGONETWELVE');
3400 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3401 return JText::translate('VBCONFIGONEMDY');
3402 } else {
3403 return JText::translate('VBCONFIGONETENTHREE');
3404 }
3405 }
3406
3407 /**
3408 * Calculates the Unix timestamp from the given date and
3409 * time. Avoids DST issues thanks to mktime. With prior releases,
3410 * DST issues may occur due to the sum of seconds.
3411 *
3412 * @param string $date the date string formatted with the current settings
3413 * @param int $h hours from 0 to 23 for check-in/check-out
3414 * @param int $m minutes from 0 to 59 for check-in/check-out
3415 * @param int $s seconds from 0 to 59 for check-in/check-out
3416 *
3417 * @return int the Unix timestamp of the date
3418 *
3419 * @since 1.0.14
3420 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3421 * signature modified for $h and $m that now take 0 as default value.
3422 */
3423 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3424 $df = self::getDateFormat();
3425 $datesep = self::getDateSeparator();
3426 $cur_dsep = "/";
3427 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3428 $cur_dsep = $datesep;
3429 }
3430 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3431 // date is in Y-m-d format (with no time)
3432 $cur_dsep = '-';
3433 $df = "%Y/%m/%d";
3434 }
3435 $x = explode($cur_dsep, $date);
3436 if (!(count($x) > 2)) {
3437 return 0;
3438 }
3439 if ($df == "%d/%m/%Y") {
3440 $month = (int)$x[1];
3441 $mday = (int)$x[0];
3442 $year = (int)$x[2];
3443 } elseif ($df == "%m/%d/%Y") {
3444 $month = (int)$x[0];
3445 $mday = (int)$x[1];
3446 $year = (int)$x[2];
3447 } else {
3448 $month = (int)$x[1];
3449 $mday = (int)$x[2];
3450 $year = (int)$x[0];
3451 }
3452 $h = empty($h) ? 0 : (int)$h;
3453 $m = empty($m) ? 0 : (int)$m;
3454 $s = $s > 0 && $s <= 59 ? $s : 0;
3455
3456 return mktime($h, $m, $s, $month, $mday, $year);
3457 }
3458
3459 public static function ivaInclusa($skipsession = false) {
3460 // cache value in static var
3461 static $getTaxIncluded = null;
3462
3463 if ($getTaxIncluded !== null) {
3464 return (bool)$getTaxIncluded;
3465 }
3466
3467 $dbo = JFactory::getDbo();
3468
3469 if ($skipsession) {
3470 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3471 $dbo->setQuery($q, 0, 1);
3472 $dbo->execute();
3473 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3474
3475 // cache value and return it
3476 $getTaxIncluded = $vat_incl_data;
3477
3478 return (bool)$vat_incl_data;
3479 }
3480
3481 $session = JFactory::getSession();
3482 $sval = $session->get('getTaxIncluded', '');
3483 if (strlen($sval)) {
3484 return (bool)$sval;
3485 }
3486
3487 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3488 $dbo->setQuery($q, 0, 1);
3489 $dbo->execute();
3490 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3491
3492 // update session value, cache it and return it
3493 $session->set('getTaxIncluded', $vat_incl_data);
3494 $getTaxIncluded = $vat_incl_data;
3495
3496 return (bool)$vat_incl_data;
3497 }
3498
3499 public static function showTaxOnSummaryOnly($skipsession = false) {
3500 if ($skipsession) {
3501 $dbo = JFactory::getDbo();
3502 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3503 $dbo->setQuery($q);
3504 $dbo->execute();
3505 $s = $dbo->loadAssocList();
3506 return (intval($s[0]['setting']) == 1 ? true : false);
3507 } else {
3508 $session = JFactory::getSession();
3509 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3510 if (strlen($sval) > 0) {
3511 return (intval($sval) == 1 ? true : false);
3512 } else {
3513 $dbo = JFactory::getDbo();
3514 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3515 $dbo->setQuery($q);
3516 $dbo->execute();
3517 $s = $dbo->loadAssocList();
3518 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3519 return (intval($s[0]['setting']) == 1 ? true : false);
3520 }
3521 }
3522 }
3523
3524 public static function tokenForm() {
3525 $dbo = JFactory::getDbo();
3526 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3527 $dbo->setQuery($q);
3528 $dbo->execute();
3529 $s = $dbo->loadAssocList();
3530 return (intval($s[0]['setting']) == 1 ? true : false);
3531 }
3532
3533 public static function getPaypalAcc() {
3534 $dbo = JFactory::getDbo();
3535 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3536 $dbo->setQuery($q);
3537 $dbo->execute();
3538 $s = $dbo->loadAssocList();
3539 return $s[0]['setting'];
3540 }
3541
3542 public static function getAccPerCent() {
3543 $dbo = JFactory::getDbo();
3544 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3545 $dbo->setQuery($q);
3546 $dbo->execute();
3547 $s = $dbo->loadAssocList();
3548 return $s[0]['setting'];
3549 }
3550
3551 public static function getTypeDeposit($skipsession = false) {
3552 if ($skipsession) {
3553 $dbo = JFactory::getDbo();
3554 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3555 $dbo->setQuery($q);
3556 $dbo->execute();
3557 $s = $dbo->loadAssocList();
3558 return $s[0]['setting'];
3559 } else {
3560 $session = JFactory::getSession();
3561 $sval = $session->get('getTypeDeposit', '');
3562 if (strlen($sval) > 0) {
3563 return $sval;
3564 } else {
3565 $dbo = JFactory::getDbo();
3566 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3567 $dbo->setQuery($q);
3568 $dbo->execute();
3569 $s = $dbo->loadAssocList();
3570 $session->set('getTypeDeposit', $s[0]['setting']);
3571 return $s[0]['setting'];
3572 }
3573 }
3574 }
3575
3576 public static function multiplePayments()
3577 {
3578 return VBOFactory::getConfig()->getBool('multipay', false);
3579 }
3580
3581 public static function getAdminMail()
3582 {
3583 return VBOFactory::getConfig()->getString('adminemail', '');
3584 }
3585
3586 public static function getSenderMail()
3587 {
3588 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3589
3590 return empty($sender) ? self::getAdminMail() : $sender;
3591 }
3592
3593 public static function getPaymentName($vbo_tn = null) {
3594 $dbo = JFactory::getDbo();
3595 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3596 $dbo->setQuery($q);
3597 $dbo->execute();
3598 $s = $dbo->loadAssocList();
3599 if (!is_object($vbo_tn)) {
3600 $vbo_tn = self::getTranslator();
3601 }
3602 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3603 return $s[0]['setting'];
3604 }
3605
3606 public static function getTermsConditions($vbo_tn = null) {
3607 //VBO 1.10
3608 $dbo = JFactory::getDbo();
3609 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3610 $dbo->setQuery($q);
3611 $dbo->execute();
3612 if ($dbo->getNumRows() > 0) {
3613 $s = $dbo->loadAssocList();
3614 if (!is_object($vbo_tn)) {
3615 $vbo_tn = self::getTranslator();
3616 }
3617 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3618 } else {
3619 //the record has never been saved. Compose it with the default lang definition
3620 $timeopst = self::getTimeOpenStore(true);
3621 if (is_array($timeopst)) {
3622 $openat = self::getHoursMinutes($timeopst[0]);
3623 $closeat = self::getHoursMinutes($timeopst[1]);
3624 } else {
3625 $openat = array(12, 0);
3626 $closeat = array(10, 0);
3627 }
3628 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3629 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3630 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3631 }
3632
3633 return $s[0]['setting'];
3634 }
3635
3636 public static function getMinutesLock($conv = false)
3637 {
3638 static $minutesLock = null;
3639
3640 if ($minutesLock === null) {
3641 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3642 }
3643
3644 if (!$minutesLock) {
3645 return 0;
3646 }
3647
3648 if ($conv) {
3649 return (time() + ($minutesLock * 60));
3650 }
3651
3652 return $minutesLock;
3653 }
3654
3655 /**
3656 * Checks if the room units are temporarily locked on the given dates.
3657 *
3658 * @param int $idroom the room ID.
3659 * @param int $units the total number of room units.
3660 * @param int $first the check-in timestamp.
3661 * @param int $second the check-out timestamp.
3662 * @param bool $occupied if true, the system will also consider the booked units.
3663 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3664 *
3665 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3666 *
3667 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3668 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3669 */
3670 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3671 {
3672 $dbo = JFactory::getDbo();
3673
3674 if ($units < 1) {
3675 return false;
3676 }
3677
3678 $actnow = time();
3679
3680 // clean up expired records
3681 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3682 $dbo->setQuery($q);
3683 $dbo->execute();
3684
3685 $secdiff = $second - $first;
3686 $daysdiff = $secdiff / 86400;
3687 if (is_int($daysdiff)) {
3688 if ($daysdiff < 1) {
3689 $daysdiff = 1;
3690 }
3691 } else {
3692 if ($daysdiff < 1) {
3693 $daysdiff = 1;
3694 } else {
3695 $sum = floor($daysdiff) * 86400;
3696 $newdiff = $secdiff - $sum;
3697 $maxhmore = self::getHoursMoreRb() * 3600;
3698 if ($maxhmore >= $newdiff) {
3699 $daysdiff = floor($daysdiff);
3700 } else {
3701 $daysdiff = ceil($daysdiff);
3702 }
3703 }
3704 }
3705 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3706 $tmplock_map = [];
3707
3708 // check temporarily locked records
3709 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3710 $dbo->setQuery($check);
3711 $busy = $dbo->loadAssocList();
3712 if ($busy) {
3713 foreach ($groupdays as $kg => $gday) {
3714 $bfound = 0;
3715 foreach ($busy as $bu) {
3716 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3717 $bfound++;
3718 }
3719 }
3720 $tmplock_map[$kg] = $bfound;
3721 if ($bfound >= $units) {
3722 return false;
3723 }
3724 }
3725 }
3726
3727 if ($occupied) {
3728 // check also the busy records and some the occupied units to the temporarily locked ones
3729 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3730 $dbo->setQuery($check);
3731 $busy = $dbo->loadAssocList();
3732 if (!$busy) {
3733 return true;
3734 }
3735
3736 foreach ($groupdays as $kg => $gday) {
3737 $bfound = 0;
3738 foreach ($busy as $bu) {
3739 if (in_array($bu['id'], $skip_busy_ids)) {
3740 // Booking modification
3741 continue;
3742 }
3743 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3744 $bfound++;
3745 }
3746 }
3747 // sum units occupied to the units temporarily locked
3748 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3749 if ($bfound >= $units) {
3750 return false;
3751 }
3752 }
3753 }
3754
3755 return true;
3756 }
3757
3758 public static function getGroupDays($first, $second, $daysdiff) {
3759 $ret = array();
3760 $ret[] = $first;
3761 if ($daysdiff > 1) {
3762 $start = getdate($first);
3763 $end = getdate($second);
3764 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3765 for($i = 1; $i < $daysdiff; $i++) {
3766 $checkday = $start['mday'] + $i;
3767 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3768 if ($dayts != $endcheck) {
3769 $ret[] = $dayts;
3770 }
3771 }
3772 }
3773 $ret[] = $second;
3774 return $ret;
3775 }
3776
3777 /**
3778 * Counts the hours of difference between the current
3779 * server time and the selected check-in date and time.
3780 *
3781 * @param int $checkin_ts
3782 * @param [int] $now_ts
3783 *
3784 * @return int
3785 */
3786 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3787 $hoursdiff = 0;
3788
3789 if (empty($now_ts)) {
3790 $now_ts = time();
3791 }
3792
3793 if ($now_ts >= $checkin_ts) {
3794 return $hoursdiff;
3795 }
3796
3797 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3798
3799 return $hoursdiff;
3800 }
3801
3802 /**
3803 * Loads all the occupied records for the given rooms and offset.
3804 *
3805 * @param array $roomids the optional list of room IDs to filter.
3806 * @param int $from_ts the optional base date to use.
3807 * @param int $max_ts the optional max date to use.
3808 * @param bool $closures whether to ignore, include or exclude closures.
3809 *
3810 * @return array
3811 *
3812 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3813 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3814 */
3815 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3816 {
3817 $base_ts = empty($from_ts) ? time() : $from_ts;
3818 $busy = [];
3819 $clauses = [];
3820
3821 if (is_array($roomids) && count($roomids)) {
3822 // filter busy records only by the requested room ids
3823 $roomids = array_map(function($rid) {
3824 return (int)$rid;
3825 }, $roomids);
3826 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3827 }
3828
3829 // exclude past reservations
3830 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3831
3832 if (!empty($max_ts)) {
3833 // exclude future reservations (recommended for large datasets)
3834 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3835 }
3836
3837 if (is_bool($closures)) {
3838 // include or exclude closures
3839 if ($closures) {
3840 $clauses[] = "`o`.`closure` = 1";
3841 } else {
3842 $clauses[] = "`o`.`closure` != 1";
3843 }
3844 }
3845
3846 $dbo = JFactory::getDbo();
3847
3848 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3849 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3850 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3851 WHERE " . implode(' AND ', $clauses) . ";";
3852 $dbo->setQuery($q);
3853 $allbusy = $dbo->loadAssocList();
3854 if ($allbusy) {
3855 foreach ($allbusy as $br) {
3856 if (!isset($busy[$br['idroom']])) {
3857 $busy[$br['idroom']] = [];
3858 }
3859 $busy[$br['idroom']][] = $br;
3860 }
3861 }
3862
3863 return $busy;
3864 }
3865
3866 /**
3867 * Loads all the busy records by excluding the closures.
3868 * Built for the page dashboard to calculate the actual
3869 * rooms occupancy by excluding the rooms closed.
3870 *
3871 * @param array $roomids the optional list of room IDs to filter.
3872 * @param int $from_ts the optional base date to use.
3873 * @param int $max_ts the optional max date to use.
3874 *
3875 * @return array the list of busy records.
3876 *
3877 * @since 1.12
3878 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3879 */
3880 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3881 {
3882 if (!is_array($roomids) || !count($roomids)) {
3883 return [];
3884 }
3885
3886 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3887 }
3888
3889 /**
3890 * Loads the occupied record IDs for a given booking ID.
3891 *
3892 * @param int $idorder The booking record ID.
3893 *
3894 * @return array List of busy IDs, if any.
3895 */
3896 public static function loadBookingBusyIds($idorder)
3897 {
3898 if (empty($idorder)) {
3899 return [];
3900 }
3901
3902 $dbo = JFactory::getDbo();
3903
3904 $dbo->setQuery(
3905 $dbo->getQuery(true)
3906 ->select($dbo->qn('idbusy'))
3907 ->from($dbo->qn('#__vikbooking_ordersbusy'))
3908 ->where($dbo->qn('idorder') . ' = ' . (int) $idorder)
3909 );
3910
3911 return array_map('intval', array_column($dbo->loadAssocList(), 'idbusy'));
3912 }
3913
3914 public static function loadLockedRecords($roomids, $ts = 0) {
3915 $dbo = JFactory::getDbo();
3916 $actnow = empty($ts) ? time() : $ts;
3917 $locked = array();
3918 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3919 $dbo->setQuery($q);
3920 $dbo->execute();
3921 if (!is_array($roomids) || !(count($roomids) > 0)) {
3922 return $locked;
3923 }
3924 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3925 $dbo->setQuery($check);
3926 $all_locked = $dbo->loadAssocList();
3927 if ($all_locked) {
3928 foreach ($all_locked as $kb => $br) {
3929 $locked[$br['idroom']][$kb] = $br;
3930 }
3931 }
3932 return $locked;
3933 }
3934
3935 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3936 $bookings = [];
3937 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3938 return $bookings;
3939 }
3940 $dbo = JFactory::getDbo();
3941 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3942 $dbo->setQuery($q);
3943 $all_booking_ids = $dbo->loadAssocList();
3944 if ($all_booking_ids) {
3945 $oids = array();
3946 foreach ($all_booking_ids as $bid) {
3947 $oids[] = $bid['idorder'];
3948 }
3949 $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` ".
3950 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3951 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3952 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3953 "ORDER BY `o`.`checkout` ASC;";
3954 $dbo->setQuery($q);
3955 $bookings = $dbo->loadAssocList();
3956 if (!$bookings) {
3957 $bookings = [];
3958 }
3959 }
3960 return $bookings;
3961 }
3962
3963 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3964 {
3965 $dbo = JFactory::getDbo();
3966
3967 if ($units < 1) {
3968 return false;
3969 }
3970
3971 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3972 if (!$room_info) {
3973 return false;
3974 }
3975
3976 $secdiff = $second - $first;
3977 $daysdiff = $secdiff / 86400;
3978 if (is_int($daysdiff)) {
3979 if ($daysdiff < 1) {
3980 $daysdiff = 1;
3981 }
3982 } else {
3983 if ($daysdiff < 1) {
3984 $daysdiff = 1;
3985 } else {
3986 $sum = floor($daysdiff) * 86400;
3987 $newdiff = $secdiff - $sum;
3988 $maxhmore = self::getHoursMoreRb() * 3600;
3989 if ($maxhmore >= $newdiff) {
3990 $daysdiff = floor($daysdiff);
3991 } else {
3992 $daysdiff = ceil($daysdiff);
3993 }
3994 }
3995 }
3996
3997 $q = $dbo->getQuery(true)
3998 ->select($dbo->qn([
3999 'id',
4000 'checkin',
4001 'realback',
4002 ]))
4003 ->from($dbo->qn('#__vikbooking_busy'))
4004 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
4005 ->where($dbo->qn('realback') . ' >= ' . $first);
4006
4007 $dbo->setQuery($q);
4008 $busy = $dbo->loadAssocList();
4009 if (!$busy) {
4010 return ($units <= ($room_info['units'] ?? 1));
4011 }
4012
4013 $groupdays = self::getGroupDays($first, $second, $daysdiff);
4014 foreach ($groupdays as $gday) {
4015 $bfound = 0;
4016 foreach ($busy as $bu) {
4017 if (in_array($bu['id'], $skip_busy_ids)) {
4018 // booking modification
4019 continue;
4020 }
4021 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
4022 $bfound++;
4023 }
4024 }
4025 if ($bfound >= $units) {
4026 return false;
4027 }
4028 }
4029
4030 return ($units <= ($room_info['units'] ?? 1));
4031 }
4032
4033 public static function payTotal()
4034 {
4035 return VBOFactory::getConfig()->getBool('paytotal', true);
4036 }
4037
4038 public static function getDepositIfDays()
4039 {
4040 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
4041 }
4042
4043 public static function depositAllowedDaysAdv($checkints) {
4044 $days_adv = self::getDepositIfDays();
4045 if (!($days_adv > 0) || !($checkints > 0)) {
4046 return true;
4047 }
4048 $now_info = getdate();
4049 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
4050 return $maxts > $checkints ? false : true;
4051 }
4052
4053 public static function depositCustomerChoice() {
4054 $dbo = JFactory::getDbo();
4055 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
4056 $dbo->setQuery($q);
4057 $dbo->execute();
4058 $s = $dbo->loadAssocList();
4059 return (intval($s[0]['setting']) == 1 ? true : false);
4060 }
4061
4062 public static function getDepositOverrides($getjson = false) {
4063 $dbo = JFactory::getDbo();
4064 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
4065 $dbo->setQuery($q);
4066 $dbo->execute();
4067 if ($dbo->getNumRows() > 0) {
4068 $s = $dbo->loadResult();
4069 return $getjson ? $s : json_decode($s, true);
4070 }
4071 //count of this array will be at least 1 to store the "more" property
4072 $def_arr = array('more' => '');
4073 $def_val = json_encode($def_arr);
4074 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
4075 $dbo->setQuery($q);
4076 $dbo->execute();
4077 return $getjson ? $def_val : $def_arr;
4078 }
4079
4080 public static function calcDepositOverride($amount_deposit, $nights) {
4081 $overrides = self::getDepositOverrides();
4082 $nights = intval($nights);
4083 $andmore = intval($overrides['more']);
4084 if (!(count($overrides) > 1)) {
4085 //no overrides
4086 return $amount_deposit;
4087 }
4088 foreach ($overrides as $k => $v) {
4089 if ((int)$k == $nights && strlen($v) > 0) {
4090 //exact override found
4091 return (float)$v;
4092 }
4093 }
4094 if ($andmore > 0 && $andmore <= $nights) {
4095 foreach ($overrides as $k => $v) {
4096 if ((int)$k == $andmore && strlen($v) > 0) {
4097 //"and more" nights found
4098 return (float)$v;
4099 }
4100 }
4101 }
4102 //nothing was found
4103 return $amount_deposit;
4104 }
4105
4106 public static function noDepositForNonRefund() {
4107 $dbo = JFactory::getDbo();
4108 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
4109 $dbo->setQuery($q);
4110 $dbo->execute();
4111 if ($dbo->getNumRows() > 0) {
4112 $s = $dbo->loadResult();
4113 return ((int)$s == 1);
4114 }
4115 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
4116 $dbo->setQuery($q);
4117 $dbo->execute();
4118 //default to false
4119 return false;
4120 }
4121
4122 /**
4123 * This method returns the room-rate array with the lowest price
4124 * that matches the preferred rate plan parameter (if available).
4125 * The array $website_rates could not be an array, or it could be
4126 * an array with the error string (response from the TACVBO Class).
4127 * The method has been introduced in VBO 1.10 and it's mainly used
4128 * by the module mod_vikbooking_channelrates and its ajax requests.
4129 *
4130 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
4131 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
4132 *
4133 * @return array
4134 */
4135 public static function getBestRoomRate($website_rates, $def_rplan) {
4136 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
4137 return array();
4138 }
4139 $best_room_rate = array();
4140 foreach ($website_rates as $rid => $tars) {
4141 foreach ($tars as $tar) {
4142 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
4143 //the array $website_rates is already sorted by price ASC, so we take the first useful array
4144 $best_room_rate = $tar;
4145 break 2;
4146 }
4147 }
4148 }
4149 if (!(count($best_room_rate) > 0)) {
4150 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
4151 foreach ($website_rates as $rid => $tars) {
4152 foreach ($tars as $tar) {
4153 $best_room_rate = $tar;
4154 break 2;
4155 }
4156 }
4157 }
4158
4159 return $best_room_rate;
4160 }
4161
4162 /**
4163 * This method returns an array with the details
4164 * of all channels in VCM that supports AV requests,
4165 * and that have at least one room type mapped.
4166 * The method invokes the Logos Class to return details
4167 * about the name and logo URL of the channel.
4168 * The method has been introduced in VBO 1.10 and it's mainly used
4169 * by the module mod_vikbooking_channelrates and its ajax requests.
4170 *
4171 * @param array $channels an array of channel IDs to be mapped on the VCM relations
4172 *
4173 * @return array
4174 */
4175 public static function getChannelsMap($channels) {
4176 if (!is_array($channels) || !(count($channels))) {
4177 return array();
4178 }
4179 $vcm_logos = self::getVcmChannelsLogo('', true);
4180 if (!is_object($vcm_logos)) {
4181 return array();
4182 }
4183 $channels_ids = array();
4184 foreach ($channels as $chid) {
4185 $ichid = (int)$chid;
4186 if ($ichid < 1) {
4187 continue;
4188 }
4189 array_push($channels_ids, $ichid);
4190 }
4191 if (!(count($channels_ids))) {
4192 return array();
4193 }
4194 $dbo = JFactory::getDbo();
4195 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
4196 $dbo->setQuery($q);
4197 $dbo->execute();
4198 if ($dbo->getNumRows() < 1) {
4199 return array();
4200 }
4201 $channels_names = $dbo->loadAssocList();
4202 $channels_map = array();
4203 foreach ($channels_names as $ch) {
4204 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
4205 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
4206 $chdata = array(
4207 'id' => $ch['idchannel'],
4208 'name' => ucwords($ch['channel']),
4209 'logo' => $ota_logo_url
4210 );
4211 array_push($channels_map, $chdata);
4212 }
4213 return $channels_map;
4214 }
4215
4216 /**
4217 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
4218 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
4219 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
4220 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
4221 * module/widget and its ajax requests.
4222 *
4223 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
4224 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
4225 *
4226 * @return mixed string (empty or indicating the alteration), or associative array per channel.
4227 *
4228 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
4229 */
4230 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
4231 {
4232 $otas_rates_val = '';
4233 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
4234 return $otas_rates_val;
4235 }
4236 if (!class_exists('VikChannelManager')) {
4237 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
4238 }
4239 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
4240 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
4241 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
4242 // the Bulk Rates Cache contains data for this room type and rate plan
4243 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
4244 // build rates modification string
4245 if ((int)$data_cont['rmod'] > 0) {
4246 // rates were modified for the OTAs, check how
4247 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
4248 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
4249 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
4250 // check alterations per channel, if requested
4251 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
4252 // we compose an associative array with the alteration for each channel (0 = generic rule)
4253 $channel_alterations = array($otas_rates_val);
4254 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
4255 if ((int)$ch_mods['rmod'] > 0) {
4256 // custom rate alteration for this channel
4257 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
4258 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
4259 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
4260 } else {
4261 // no rates alteration for this channel identifier
4262 $ch_alter_string = '+0%';
4263 }
4264 $channel_alterations[$ch_id] = $ch_alter_string;
4265 }
4266 // overwrite return value with associative array
4267 $otas_rates_val = $channel_alterations;
4268 }
4269 }
4270 }
4271 }
4272
4273 return $otas_rates_val;
4274 }
4275
4276 /**
4277 * This method checks if some non-refundable rates were selected
4278 * (`free_cancellation`=0), the only argument is an array of tariffs.
4279 * The property 'idprice' must be defined on each sub-array.
4280 *
4281 * @param $tars array
4282 *
4283 * @return boolean
4284 **/
4285 public static function findNonRefundableRates($tars = [])
4286 {
4287 $id_prices = [];
4288
4289 foreach ($tars as $tar) {
4290 if (isset($tar['idprice'])) {
4291 if (!in_array($tar['idprice'], $id_prices)) {
4292 array_push($id_prices, (int)$tar['idprice']);
4293 }
4294 continue;
4295 }
4296 foreach ($tar as $t) {
4297 if (isset($t['idprice'])) {
4298 if (!in_array($t['idprice'], $id_prices)) {
4299 array_push($id_prices, (int)$t['idprice']);
4300 }
4301 }
4302 }
4303 }
4304
4305 if (!count($id_prices)) {
4306 // no price IDs found (probably a package)
4307 return false;
4308 }
4309
4310 $dbo = JFactory::getDbo();
4311
4312 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
4313 $dbo->setQuery($q);
4314 $dbo->execute();
4315
4316 return (bool)($dbo->getNumRows() > 0);
4317 }
4318
4319 /**
4320 * This method checks if the deposit is allowed depending on
4321 * the selected rate plans (idprice) for the rooms reserved.
4322 * If the configuration setting is enabled, and if some
4323 * non-refundable rates were selected (`free_cancellation`=0),
4324 * the method will return false, because the deposit is not allowed.
4325 * The only argument is an array of tariffs. The property 'idprice'
4326 * must be defined on each sub-array (multi-dimension supported)
4327 * throgh the method findNonRefundableRates();
4328 *
4329 * @param $tars array
4330 *
4331 * @return boolean
4332 **/
4333 public static function allowDepositFromRates($tars) {
4334 if (!self::noDepositForNonRefund()) {
4335 //deposit can be paid also if non-refundable rates
4336 return true;
4337 }
4338 return !self::findNonRefundableRates($tars);
4339 }
4340
4341 public static function showSearchSuggestions() {
4342 $dbo = JFactory::getDbo();
4343 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4344 $dbo->setQuery($q);
4345 $dbo->execute();
4346 if ($dbo->getNumRows() > 0) {
4347 return (int)$dbo->loadResult();
4348 }
4349 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4350 $dbo->setQuery($q);
4351 $dbo->execute();
4352 return 1;
4353 }
4354
4355 /**
4356 * Given a code, returns the coupon information array.
4357 *
4358 * @param string $code the coupon code.
4359 *
4360 * @return array empty array or coupon record.
4361 *
4362 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4363 */
4364 public static function getCouponInfo($code)
4365 {
4366 $dbo = JFactory::getDbo();
4367
4368 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4369 $dbo->setQuery($q, 0, 1);
4370 $coupon = $dbo->loadAssoc();
4371 if (!$coupon) {
4372 return [];
4373 }
4374
4375 // check for customers assigned
4376 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4377 $dbo->setQuery($q);
4378 $coupon_customers = $dbo->loadAssocList();
4379 if ($coupon_customers) {
4380 // set customers and automatic properties
4381 $coupon['customers'] = [];
4382 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4383 foreach ($coupon_customers as $coupon_customer) {
4384 // push customer ID
4385 $coupon['customers'][] = $coupon_customer['idcustomer'];
4386 }
4387 }
4388
4389 return $coupon;
4390 }
4391
4392 /**
4393 * Reads the details of a given room record ID.
4394 *
4395 * @param int $idroom The room ID to fetch.
4396 * @param array $columns The optional room columns to fetch.
4397 * @param bool $no_cache Whether to ignore a previously cached record.
4398 *
4399 * @return array
4400 *
4401 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4402 */
4403 public static function getRoomInfo($idroom, $columns = [], $no_cache = false)
4404 {
4405 static $room_infos = [];
4406
4407 if (isset($room_infos[$idroom]) && !$no_cache) {
4408 return $room_infos[$idroom];
4409 }
4410
4411 $dbo = JFactory::getDbo();
4412
4413 if ($columns) {
4414 $columns = array_map([$dbo, 'qn'], $columns);
4415 }
4416
4417 $q = $dbo->getQuery(true)
4418 ->select($columns ? $columns : '*')
4419 ->from($dbo->qn('#__vikbooking_rooms'))
4420 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4421
4422 $dbo->setQuery($q);
4423 $room = $dbo->loadAssoc();
4424
4425 if ($no_cache) {
4426 return $room ?: [];
4427 }
4428
4429 $room_infos[$idroom] = $room ?: [];
4430
4431 return $room_infos[$idroom];
4432 }
4433
4434 public static function loadOrdersRoomsData($idorder)
4435 {
4436 $dbo = JFactory::getDbo();
4437
4438 $q = $dbo->getQuery(true)
4439 ->select($dbo->qn('or') . '.*')
4440 ->select($dbo->qn('r.name', 'room_name'))
4441 ->select($dbo->qn('r.units', 'tot_units'))
4442 ->select($dbo->qn('r.params'))
4443 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4444 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4445 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4446 ->order($dbo->qn('or.id') . ' ASC');
4447
4448 $dbo->setQuery($q);
4449
4450 return $dbo->loadAssocList();
4451 }
4452
4453 public static function sayCategory($ids, $vbo_tn = null)
4454 {
4455 $dbo = JFactory::getDbo();
4456 $split = explode(";", $ids);
4457 $say = "";
4458 foreach ($split as $k => $s) {
4459 if (!strlen($s)) {
4460 continue;
4461 }
4462 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4463 $dbo->setQuery($q);
4464 $nam = $dbo->loadAssocList();
4465 if (!$nam) {
4466 continue;
4467 }
4468 if (is_object($vbo_tn)) {
4469 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4470 }
4471 $say .= $nam[0]['name'];
4472 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4473 }
4474 return $say;
4475 }
4476
4477 /**
4478 * Returns a list of the given room amenities string.
4479 *
4480 * @param string $amenities Room amenities string.
4481 * @param ?object $vbo_tn Optional VBOTranslator.
4482 * @param bool $all True for loading all records when no amenities given.
4483 *
4484 * @return array
4485 *
4486 * @since 1.17.3 (J) - 1.7.3 (WP)
4487 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $all.
4488 */
4489 public static function loadRoomAmenities($amenities, $vbo_tn = null, $all = false)
4490 {
4491 $dbo = JFactory::getDbo();
4492
4493 $split = explode(";", (string) $amenities);
4494 $where = [];
4495 foreach ($split as $s) {
4496 if (!empty($s)) {
4497 $where[] = (int) $s;
4498 }
4499 }
4500
4501 $list = [];
4502 $where = array_filter($where);
4503
4504 $query = $dbo->getQuery(true)
4505 ->select('*')
4506 ->from($dbo->qn('#__vikbooking_characteristics'))
4507 ->order($dbo->qn('ordering') . ' ASC');
4508
4509 if ($where) {
4510 $query->where($dbo->qn('id') . ' IN (' . implode(', ', $where) . ')');
4511 }
4512
4513 if ($where || $all) {
4514 $dbo->setQuery($query);
4515 $list = $dbo->loadAssocList();
4516 if ($list && is_object($vbo_tn)) {
4517 $vbo_tn->translateContents($list, '#__vikbooking_characteristics');
4518 }
4519 }
4520
4521 return $list;
4522 }
4523
4524 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4525 {
4526 $carat = '';
4527 $arr = self::loadRoomAmenities($idc, $vbo_tn);
4528
4529 if ($arr) {
4530 $carat .= "<div class=\"vbo-room-carats\">\n";
4531 foreach ($arr as $a) {
4532 if (!empty($a['textimg'])) {
4533 //tooltip icon text is not empty
4534 if (!empty($a['icon'])) {
4535 //an icon has been uploaded: display the image
4536 $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";
4537 } else {
4538 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4539 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4540 $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";
4541 } else {
4542 //display just the text
4543 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4544 }
4545 }
4546 } else {
4547 $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");
4548 }
4549 }
4550 $carat .= "</div>\n";
4551 }
4552
4553 return $carat;
4554 }
4555
4556 public static function getRoomOptionals($idopts, $vbo_tn = null)
4557 {
4558 $dbo = JFactory::getDbo();
4559 $split = explode(";", $idopts);
4560 $fetch = array();
4561 foreach ($split as $s) {
4562 if (!empty($s)) {
4563 $fetch[] = $s;
4564 }
4565 }
4566 if ($fetch) {
4567 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', array_map('intval', $fetch)) . ") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4568 $dbo->setQuery($q);
4569 $arr = $dbo->loadAssocList();
4570 if ($arr) {
4571 if (is_object($vbo_tn)) {
4572 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4573 }
4574 return $arr;
4575 }
4576 }
4577 return [];
4578 }
4579
4580 public static function getSingleOption($idopt, $vbo_tn = null)
4581 {
4582 $dbo = JFactory::getDbo();
4583 $opt = array();
4584 if (!empty($idopt)) {
4585 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4586 $dbo->setQuery($q);
4587 $opt = $dbo->loadAssoc();
4588 if ($opt && is_object($vbo_tn)) {
4589 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4590 }
4591 }
4592 return $opt;
4593 }
4594
4595 /**
4596 * Unsets the options that are not available in the requested dates.
4597 *
4598 * @param array $optionals the array of options passed by reference
4599 * @param int $checkin the timestamp of the check-in date and time
4600 * @param int $checkout the timestamp of the check-out date and time
4601 *
4602 * @return void
4603 *
4604 * @since 1.11
4605 */
4606 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4607 {
4608 if (!$optionals || empty($checkin) || empty($checkout)) {
4609 return;
4610 }
4611
4612 foreach ($optionals as $k => $v) {
4613 if (!empty($v['alwaysav'])) {
4614 $dates = explode(';', $v['alwaysav']);
4615 if (empty($dates[0]) || empty($dates[1])) {
4616 continue;
4617 }
4618 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4619 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4620 unset($optionals[$k]);
4621 }
4622 }
4623 }
4624 }
4625
4626 /**
4627 * Unsets the options that are not suited for the requested room party.
4628 *
4629 * @param array $optionals the array of options passed by reference.
4630 * @param int $adults the number of adults in the room party.
4631 * @param int $children the number of children in the room party.
4632 *
4633 * @return void
4634 *
4635 * @since 1.13.5
4636 */
4637 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4638 {
4639 if (!$optionals || (empty($adults) && empty($children))) {
4640 return;
4641 }
4642
4643 foreach ($optionals as $k => $v) {
4644 if (empty($v['oparams'])) {
4645 continue;
4646 }
4647 $v['oparams'] = json_decode($v['oparams'], true);
4648 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4649 continue;
4650 }
4651 if (!empty($v['oparams']['minguestsnum'])) {
4652 // filter by minimum adults/guests
4653 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4654 // minimum number of adults not sufficient, unset option
4655 unset($optionals[$k]);
4656 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4657 // minimum number of total guests not sufficient, unset option
4658 unset($optionals[$k]);
4659 }
4660 }
4661 if (!empty($v['oparams']['maxguestsnum'])) {
4662 // filter by maximum adults/guests
4663 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4664 // maximum number of adults exceeded, unset option
4665 unset($optionals[$k]);
4666 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4667 // maximum number of total guests exceeded, unset option
4668 unset($optionals[$k]);
4669 }
4670 }
4671 }
4672 }
4673
4674 /**
4675 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4676 *
4677 * @param array $id_rooms list of room IDs.
4678 * @param int $num_adults number of adults.
4679 * @param int $num_nights number of nights of stay.
4680 * @param int $checkin optional check-in timestamp.
4681 * @param int $checkout optional check-out timestamp.
4682 *
4683 * @return array associative information of city taxes, fees and options.
4684 *
4685 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4686 */
4687 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4688 {
4689 $dbo = JFactory::getDbo();
4690
4691 $taxes = 0;
4692 $fees = 0;
4693
4694 $options_data = [];
4695 $id_options = [];
4696
4697 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4698 $dbo->setQuery($q);
4699 $assocs = $dbo->loadAssocList();
4700 if ($assocs) {
4701 foreach ($assocs as $opts) {
4702 if (!empty($opts['idopt'])) {
4703 $r_ido = explode(';', rtrim($opts['idopt']));
4704 foreach ($r_ido as $ido) {
4705 if (!empty($ido) && !in_array($ido, $id_options)) {
4706 $id_options[] = (int)$ido;
4707 }
4708 }
4709 }
4710 }
4711 }
4712
4713 if ($id_options) {
4714 $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);";
4715 $dbo->setQuery($q);
4716 $alltaxesfees = $dbo->loadAssocList();
4717
4718 if ($checkin && $checkout) {
4719 // filter the records by stay dates to support, for example, seasonal city taxes
4720 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4721 }
4722
4723 foreach ($alltaxesfees as $tf) {
4724 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4725 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4726 $realcost = $tf['maxprice'];
4727 }
4728 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4729
4730 /**
4731 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
4732 *
4733 * @since 1.17.7 (J) - 1.7.7 (WP)
4734 */
4735 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$tf, ['days' => $num_nights], ['adults' => $num_adults]]);
4736 if ($custom_calculation) {
4737 $realcost = (float) $custom_calculation[0];
4738 }
4739
4740 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4741 if ($tf['is_citytax'] == 1) {
4742 $taxes += $realcost;
4743 } elseif ($tf['is_fee'] == 1) {
4744 $fees += $realcost;
4745 }
4746 $optsett = explode('-', $tf['forceval']);
4747 $options_data[] = $tf['id'].':'.$optsett[0];
4748 }
4749 }
4750
4751 return [
4752 'city_taxes' => $taxes,
4753 'fees' => $fees,
4754 'options' => $options_data
4755 ];
4756 }
4757
4758 /**
4759 * From a list of option/extras records, it returns an array with the options/extras
4760 * that do not support age intervals, and options for the children age intervals.
4761 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4762 *
4763 * @param array $optionals the full list of options/extras records.
4764 * @param int $adults the number of adults in the room requested.
4765 * @param int $children the number of children in the room requested.
4766 *
4767 * @return array the filtered regular and children age intervals options.
4768 */
4769 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4770 {
4771 // container for age intervals
4772 $ageintervals = [];
4773
4774 // regular options should start as an array
4775 if (!is_array($optionals)) {
4776 $optionals = [];
4777 }
4778
4779 // check for the first valid age intervals option
4780 foreach ($optionals as $kopt => $opt) {
4781 if (!empty($opt['ageintervals'])) {
4782 $intervals = explode(';;', $opt['ageintervals']);
4783 foreach ($intervals as $intv) {
4784 if (empty($intv)) {
4785 continue;
4786 }
4787 $parts = explode('_', $intv);
4788 if (count($parts) >= 3) {
4789 // age intervals option found, get the first and only one
4790 $ageintervals = $optionals[$kopt];
4791 break 2;
4792 }
4793 }
4794 }
4795 }
4796
4797 if ($ageintervals) {
4798 /**
4799 * We allow price adjustments for a minimum total guests by overriding the costs
4800 * for each age interval for specific child numbers when the party covers some children.
4801 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4802 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4803 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4804 *
4805 * @since 1.13.5
4806 */
4807 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4808 $oparams = !is_array($oparams) ? array() : $oparams;
4809 $adults = (int)$adults;
4810 $children = (int)$children;
4811 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4812 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4813 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4814 if ($children_to_pay < $children) {
4815 // compose string for free age intervals
4816 $intervals = explode(';;', $ageintervals['ageintervals']);
4817 foreach ($intervals as $k => $v) {
4818 $parts = explode('_', $v);
4819 // set cost to 0 (key = 2)
4820 $parts[2] = '0';
4821 $intervals[$k] = implode('_', $parts);
4822 }
4823 $free_ageintervals = implode(';;', $intervals);
4824 for ($i = 1; $i <= $children; $i++) {
4825 if (($children - $i + 1) <= $children_to_pay) {
4826 // costs for this Nth last child should be regular as this child should pay
4827 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4828 } else {
4829 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4830 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4831 }
4832 }
4833 }
4834 }
4835
4836 // remove age intervals from regular options
4837 foreach ($optionals as $kopt => $opt) {
4838 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4839 // unset the option of type age intervals from the regular options
4840 unset($optionals[$kopt]);
4841 }
4842 }
4843 }
4844
4845 // return the filtered list of regular options and age intervals options
4846 return [$optionals, $ageintervals];
4847 }
4848
4849 /**
4850 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4851 * This is because some children in the party may not need to pay anything due to the min guests.
4852 *
4853 * @param array $optional the option full record to parse.
4854 * @param int $adults the number of adults in the room requested.
4855 * @param int $children the number of children in the room requested.
4856 *
4857 * @return array associative array with default age intervals and override strings.
4858 *
4859 * @since 1.13.5
4860 */
4861 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4862 {
4863 if (!is_array($optional)) {
4864 $optional = array();
4865 }
4866
4867 $overrides = array(
4868 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4869 );
4870
4871 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4872 $oparams = !is_array($oparams) ? array() : $oparams;
4873 $adults = (int)$adults;
4874 $children = (int)$children;
4875
4876 if (!count($oparams)) {
4877 return $overrides;
4878 }
4879
4880 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4881 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4882 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4883 if ($children_to_pay < $children) {
4884 // compose string for free age intervals
4885 $intervals = explode(';;', $optional['ageintervals']);
4886 foreach ($intervals as $k => $v) {
4887 $parts = explode('_', $v);
4888 // set cost to 0 (key = 2)
4889 $parts[2] = '0';
4890 $intervals[$k] = implode('_', $parts);
4891 }
4892 $free_ageintervals = implode(';;', $intervals);
4893 for ($i = 1; $i <= $children; $i++) {
4894 if (($children - $i + 1) <= $children_to_pay) {
4895 // costs for this Nth last child should be regular as this child should pay
4896 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4897 } else {
4898 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4899 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4900 }
4901 }
4902 }
4903 }
4904
4905 return $overrides;
4906 }
4907
4908 /**
4909 * Returns the number/index of the children being parsed given the full list of room options string,
4910 * the current key in the array of the split room options string, the ID of the ageintervals option,
4911 * and the total number of children. This is useful for later applying the cost overrides for the
4912 * children ages through the method getOptionIntervalChildOverrides().
4913 *
4914 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4915 * @param int $optid the ID of the option of type age intervals to check.
4916 * @param int $roptkey the current position in the loop of the room options string.
4917 * @param int $children the number of children in the room requested.
4918 *
4919 * @return int the position/number of the children being parsed, -1 if not found.
4920 *
4921 * @since 1.13.5
4922 */
4923 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4924 {
4925 // default to -1 for not found
4926 $child_num = -1;
4927
4928 $valid_opt_counter = 0;
4929
4930 $roptions = explode(";", $roptstr);
4931 foreach ($roptions as $k => $opt) {
4932 if (empty($opt)) {
4933 continue;
4934 }
4935 $optvals = explode(":", $opt);
4936 /**
4937 * In some cases, like the "saveorder" task, the room option string may contain
4938 * the room number beside the option ID, separated with an underscore.
4939 * If an underscore is present, we need to split it to find the option ID.
4940 */
4941 if (strpos($optvals[0], '_') !== false) {
4942 // underscore found in room number portion, extract the option ID
4943 $rn_parts = explode('_', $optvals[0]);
4944 // 0th element is the room number in the party, 1st elem is the option ID
4945 $optvals[0] = $rn_parts[1];
4946 }
4947 //
4948 if ((int)$optvals[0] != (int)$optid) {
4949 // we are not interested into this option ID
4950 continue;
4951 }
4952 // increase counter for this option ID
4953 $valid_opt_counter++;
4954
4955 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4956 // children position found, we need to return it as a 0th base
4957 $child_num = ($valid_opt_counter - 1);
4958 break;
4959 }
4960 }
4961
4962 return $child_num;
4963 }
4964
4965 public static function getOptionIntervalsCosts($intvstr) {
4966 $optcosts = array();
4967 $intervals = explode(';;', $intvstr);
4968 foreach ($intervals as $kintv => $intv) {
4969 if (empty($intv)) continue;
4970 $parts = explode('_', $intv);
4971 if (count($parts) >= 3) {
4972 $optcosts[$kintv] = (float)$parts[2];
4973 }
4974 }
4975 return $optcosts;
4976 }
4977
4978 public static function getOptionIntervalsAges($intvstr) {
4979 $optages = array();
4980 $intervals = explode(';;', $intvstr);
4981 foreach ($intervals as $kintv => $intv) {
4982 if (empty($intv)) continue;
4983 $parts = explode('_', $intv);
4984 if (count($parts) >= 3) {
4985 $optages[$kintv] = $parts[0].' - '.$parts[1];
4986 }
4987 }
4988 return $optages;
4989 }
4990
4991 public static function getOptionIntervalsPercentage($intvstr) {
4992 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4993 $optcostspcent = array();
4994 $intervals = explode(';;', $intvstr);
4995 foreach ($intervals as $kintv => $intv) {
4996 if (empty($intv)) continue;
4997 $parts = explode('_', $intv);
4998 if (count($parts) >= 3) {
4999 //fixed amount
5000 $setval = 0;
5001 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
5002 //percentage value of the room base cost (VBO 1.10)
5003 $setval = 2;
5004 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
5005 //percentage value of the adults tariff
5006 $setval = 1;
5007 }
5008 $optcostspcent[$kintv] = $setval;
5009 }
5010 }
5011 return $optcostspcent;
5012 }
5013
5014 public static function dayValidTs($days, $first, $second)
5015 {
5016 $secdiff = $second - $first;
5017 $daysdiff = $secdiff / 86400;
5018 if (is_int($daysdiff)) {
5019 if ($daysdiff < 1) {
5020 $daysdiff = 1;
5021 }
5022 } else {
5023 if ($daysdiff < 1) {
5024 $daysdiff = 1;
5025 } else {
5026 $sum = floor($daysdiff) * 86400;
5027 $newdiff = $secdiff - $sum;
5028 $maxhmore = self::getHoursMoreRb() * 3600;
5029 if ($maxhmore >= $newdiff) {
5030 $daysdiff = floor($daysdiff);
5031 } else {
5032 $daysdiff = ceil($daysdiff);
5033 }
5034 }
5035 }
5036
5037 return ($daysdiff == $days);
5038 }
5039
5040 /**
5041 * Calculates the stay fee inclusive of taxes.
5042 *
5043 * @param float $cost the cost to evaluate.
5044 * @param int $idprice the rate plan ID associated.
5045 *
5046 * @return float the given cost after tax, if taxes had to be applied.
5047 */
5048 public static function sayCostPlusIva($cost, $idprice)
5049 {
5050 $ivainclusa = self::ivaInclusa();
5051 if ($ivainclusa) {
5052 return $cost;
5053 }
5054
5055 static $tax_rplans_map = [];
5056
5057 if (array_key_exists($idprice, $tax_rplans_map)) {
5058 $tax_rate = $tax_rplans_map[$idprice];
5059 } else {
5060 $dbo = JFactory::getDbo();
5061 $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 . ";";
5062 $dbo->setQuery($q);
5063 $tax_rate = $dbo->loadAssoc();
5064
5065 // cache value
5066 $tax_rplans_map[$idprice] = $tax_rate;
5067 }
5068
5069 if (!$tax_rate || empty($tax_rate['aliq'])) {
5070 return $cost;
5071 }
5072
5073 $subt = 100 + $tax_rate['aliq'];
5074 $op = ($cost * $subt / 100);
5075
5076 /**
5077 * Tax Cap implementation for prices tax excluded (most common).
5078 *
5079 * @since 1.12
5080 */
5081 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5082 $op = ($cost + $tax_rate['taxcap']);
5083 }
5084
5085 // apply rounding to avoid issues with multiple tax rates when tax excluded
5086 $formatvals = self::getNumberFormatData();
5087 $formatparts = explode(':', $formatvals);
5088 $rounded_op = round($op, (int)$formatparts[0]);
5089 if ($rounded_op > $op) {
5090 return $rounded_op;
5091 }
5092
5093 /**
5094 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5095 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5096 *
5097 * @since 1.11.1
5098 */
5099 return round($op, (int)$formatparts[0]);
5100 }
5101
5102 /**
5103 * Calculates the stay fee exclusive of taxes.
5104 *
5105 * @param float $cost the cost to evaluate.
5106 * @param int $idprice the rate plan ID associated.
5107 *
5108 * @return float the given cost before tax.
5109 */
5110 public static function sayCostMinusIva($cost, $idprice)
5111 {
5112 $ivainclusa = self::ivaInclusa();
5113 if (!$ivainclusa) {
5114 return $cost;
5115 }
5116
5117 static $tax_rplans_map = [];
5118
5119 if (array_key_exists($idprice, $tax_rplans_map)) {
5120 $tax_rate = $tax_rplans_map[$idprice];
5121 } else {
5122 $dbo = JFactory::getDbo();
5123 $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 . ";";
5124 $dbo->setQuery($q);
5125 $tax_rate = $dbo->loadAssoc();
5126
5127 // cache value
5128 $tax_rplans_map[$idprice] = $tax_rate;
5129 }
5130
5131 if (!$tax_rate || empty($tax_rate['aliq'])) {
5132 return $cost;
5133 }
5134
5135 $subt = 100 + $tax_rate['aliq'];
5136 $op = ($cost * 100 / $subt);
5137
5138 /**
5139 * Tax Cap implementation also when prices tax included.
5140 *
5141 * @since 1.12
5142 */
5143 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5144 $op = ($cost - $tax_rate['taxcap']);
5145 }
5146
5147 // apply rounding to avoid issues with multiple tax rates when tax included
5148 $formatvals = self::getNumberFormatData();
5149 $formatparts = explode(':', $formatvals);
5150 $rounded_op = round($op, (int)$formatparts[0]);
5151 if ($rounded_op < $op) {
5152 return $rounded_op;
5153 }
5154
5155 /**
5156 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5157 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5158 *
5159 * @since 1.11.1
5160 */
5161 return round($op, (int)$formatparts[0]);
5162 }
5163
5164 /**
5165 * Given an option/extra cost, determines whether taxes should be applied over it.
5166 * Can also be used to calculate taxes on the extra costs per room in the bookings
5167 *
5168 * @param float $cost the cost to evaluate, option or extra service fee.
5169 * @param int $idiva the ID of the tax rate to use to apply taxes.
5170 * @param bool $force if true, taxes will always be applied.
5171 *
5172 * @return float the given option/extra cost plus tax, if taxes had to be applied.
5173 *
5174 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5175 */
5176 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
5177 {
5178 $ivainclusa = self::ivaInclusa();
5179 if ($ivainclusa && $force !== true) {
5180 return $cost;
5181 }
5182
5183 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5184 if (!$tax_rate) {
5185 return $cost;
5186 }
5187
5188 $subt = 100 + $tax_rate['aliq'];
5189 $op = ($cost * $subt / 100);
5190
5191 /**
5192 * Tax Cap implementation for prices tax excluded (most common).
5193 *
5194 * @since 1.12
5195 */
5196 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5197 $op = ($cost + $tax_rate['taxcap']);
5198 }
5199
5200 // apply rounding to avoid issues with multiple tax rates when tax excluded
5201 $formatvals = self::getNumberFormatData();
5202 $formatparts = explode(':', $formatvals);
5203 $rounded_op = round($op, (int)$formatparts[0]);
5204 if ($rounded_op > $op) {
5205 return $rounded_op;
5206 }
5207
5208 return $op;
5209 }
5210
5211 /**
5212 * Calculates the net amount for an option/extra service.
5213 *
5214 * @param float $cost the cost to evaluate, option or extra service fee.
5215 * @param int $idiva the ID of the tax rate to use to apply taxes.
5216 *
5217 * @return float the given option/extra cost before tax, if taxes were inclusive.
5218 */
5219 public static function sayOptionalsMinusIva($cost, $idiva)
5220 {
5221 if (empty($idiva)) {
5222 return $cost;
5223 }
5224
5225 $ivainclusa = self::ivaInclusa();
5226 if (!$ivainclusa) {
5227 return $cost;
5228 }
5229
5230 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5231 if (!$tax_rate) {
5232 return $cost;
5233 }
5234
5235 $subt = 100 + $tax_rate['aliq'];
5236 $op = ($cost * 100 / $subt);
5237
5238 /**
5239 * Tax Cap implementation also when prices tax included.
5240 *
5241 * @since 1.12
5242 */
5243 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5244 $op = ($cost - $tax_rate['taxcap']);
5245 }
5246
5247 // apply rounding to avoid issues with multiple tax rates when tax included
5248 $formatvals = self::getNumberFormatData();
5249 $formatparts = explode(':', $formatvals);
5250 $rounded_op = round($op, (int)$formatparts[0]);
5251 if ($rounded_op < $op) {
5252 return $rounded_op;
5253 }
5254
5255 return $op;
5256 }
5257
5258 /**
5259 * Given a cost, determines whether taxes should be applied over it.
5260 *
5261 * @param float $cost the cost to evaluate, could be a room custom price.
5262 * @param int $idiva the ID of the tax rate to use to apply taxes.
5263 * @param bool $force if true, taxes will always be applied.
5264 *
5265 * @return float the given cost plus tax, if taxes had to be applied.
5266 *
5267 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5268 */
5269 public static function sayPackagePlusIva($cost, $idiva, $force = false)
5270 {
5271 $ivainclusa = self::ivaInclusa();
5272 if ($ivainclusa && $force !== true) {
5273 return $cost;
5274 }
5275
5276 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5277 if (!$tax_rate) {
5278 return $cost;
5279 }
5280
5281 $subt = 100 + $tax_rate['aliq'];
5282 $op = ($cost * $subt / 100);
5283
5284 /**
5285 * Tax Cap implementation for prices tax excluded (most common).
5286 *
5287 * @since 1.12
5288 */
5289 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5290 $op = ($cost + $tax_rate['taxcap']);
5291 }
5292
5293 // apply rounding to avoid issues with multiple tax rates when tax excluded
5294 $formatvals = self::getNumberFormatData();
5295 $formatparts = explode(':', $formatvals);
5296 $rounded_op = round($op, (int)$formatparts[0]);
5297 if ($rounded_op > $op) {
5298 return $rounded_op;
5299 }
5300
5301 return $op;
5302 }
5303
5304 /**
5305 * Calculates the net amount for a package.
5306 *
5307 * @param float $cost the cost to evaluate, option or extra service fee.
5308 * @param int $idiva the ID of the tax rate to use to apply taxes.
5309 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
5310 *
5311 * @return float the given cost before tax.
5312 */
5313 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
5314 {
5315 $ivainclusa = self::ivaInclusa();
5316 if (!$ivainclusa && $force_invoice_excltax !== true) {
5317 return $cost;
5318 }
5319
5320 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5321 if (!$tax_rate) {
5322 return $cost;
5323 }
5324
5325 $subt = 100 + $tax_rate['aliq'];
5326 $op = ($cost * 100 / $subt);
5327
5328 /**
5329 * Tax Cap implementation also when prices tax included.
5330 *
5331 * @since 1.12
5332 */
5333 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5334 $op = ($cost - $tax_rate['taxcap']);
5335 }
5336
5337 // apply rounding to avoid issues with multiple tax rates when tax included
5338 $formatvals = self::getNumberFormatData();
5339 $formatparts = explode(':', $formatvals);
5340 $rounded_op = round($op, (int)$formatparts[0]);
5341 if ($rounded_op < $op) {
5342 return $rounded_op;
5343 }
5344
5345 return $op;
5346 }
5347
5348 public static function getSecretLink()
5349 {
5350 $dbo = JFactory::getDbo();
5351
5352 $sid = mt_rand();
5353
5354 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
5355 $dbo->setQuery($q);
5356 $all = $dbo->loadAssocList();
5357 if ($all) {
5358 $list = [];
5359 foreach ($all as $s) {
5360 $list[] = $s['sid'];
5361 }
5362 if (in_array($sid, $list)) {
5363 while(in_array($sid, $list)) {
5364 $sid++;
5365 }
5366 }
5367 }
5368
5369 return $sid;
5370 }
5371
5372 /**
5373 * Generates a confirmation number for the given booking.
5374 * Modified the way random string-numbers are obtained to
5375 * avoid any possible brute-force attempt.
5376 *
5377 * @since 1.15.1 (J) - 1.5.4 (WP)
5378 */
5379 public static function generateConfirmNumber($oid, $update = true)
5380 {
5381 $dbo = JFactory::getDbo();
5382
5383 $confirmnumb = date('y') . $oid;
5384 for ($i = 0; $i < 8; $i++) {
5385 $confirmnumb .= rand(0, 9);
5386 }
5387
5388 if ($update) {
5389 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5390 $dbo->setQuery($q);
5391 $dbo->execute();
5392 }
5393
5394 return $confirmnumb;
5395 }
5396
5397 public static function buildCustData($arr, $sep) {
5398 $cdata = "";
5399 foreach ($arr as $k => $e) {
5400 if (strlen($e)) {
5401 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5402 }
5403 }
5404 return $cdata;
5405 }
5406
5407 /**
5408 * This method parses the Joomla menu object to see if a menu item of a
5409 * specific type is available, to get its ID. Useful when links should be
5410 * displayed in pages where there is no Itemid set (booking details pages).
5411 *
5412 * @param array $viewtypes list of accepted menu items
5413 * @param string $lang the optional language to use.
5414 *
5415 * @return int
5416 *
5417 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5418 */
5419 public static function findProperItemIdType($viewtypes, $lang = null)
5420 {
5421 if (VBOPlatformDetection::isWordPress()) {
5422 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5423
5424 $itemid = $model->best($viewtypes, $lang);
5425
5426 if ($itemid) {
5427 return $itemid;
5428 }
5429
5430 return 0;
5431 }
5432
5433 $bestitemid = 0;
5434
5435 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5436
5437 $app = JFactory::getApplication();
5438
5439 $menu = $app->getMenu('site');
5440
5441 if (!$menu) {
5442 return 0;
5443 }
5444
5445 $menu_items = $menu->getMenu();
5446
5447 if (!$menu_items) {
5448 return 0;
5449 }
5450
5451 foreach ($menu_items as $itemid => $item) {
5452 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5453 // proper menu item type found
5454 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5455
5456 if (isset($item->language) && $item->language == $current_lang) {
5457 // we found the exact menu item type for the given language
5458 return $itemid;
5459 }
5460 }
5461 }
5462
5463 return $bestitemid;
5464 }
5465
5466 /**
5467 * Rewrites an internal URI that needs to be used outside of the website.
5468 * This means that the routed URI MUST start with the base path of the site.
5469 *
5470 * @param mixed $query The query string or an associative array of data.
5471 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5472 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5473 *
5474 * @return string The complete routed URI.
5475 *
5476 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5477 * routing from back-end (if available on the CMS version).
5478 */
5479 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5480 {
5481 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5482 }
5483
5484 /**
5485 * Generates an iCal file to be attached to the email message for the
5486 * customer or the administrator with some basic booking details.
5487 *
5488 * @param string $recip either admin or customer.
5489 * @param array $booking the booking array or some keys.
5490 *
5491 * @return mixed string in case of success, false otherwise.
5492 *
5493 * @since 1.12.0
5494 */
5495 public static function getEmailIcal($recip, $booking) {
5496 // load configuration setting
5497 $attachical = self::attachIcal();
5498
5499 if ($attachical === 0) {
5500 // do not attach any iCal file
5501 return false;
5502 }
5503
5504 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5505 // skip the iCal for the admin
5506 return false;
5507 }
5508
5509 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5510 // skip the iCal for the customer
5511 return false;
5512 }
5513
5514 if (strpos($recip, 'admin') !== false) {
5515 // prepare event description and summary for the admin
5516 $description = $booking['custdata'];
5517 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5518 $fname = $booking['ts'] . '.ics';
5519 } else {
5520 // event description and summary for the customer
5521 $description = '';
5522 $summary = self::getFrontTitle();
5523 $fname = 'reservation_reminder.ics';
5524 }
5525
5526 // prepare iCal head
5527 $company_name = self::getFrontTitle();
5528 $ics_str = "BEGIN:VCALENDAR\r\n" .
5529 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5530 "CALSCALE:GREGORIAN\r\n" .
5531 "VERSION:2.0\r\n";
5532 // compose iCal body
5533 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5534 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5535 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5536 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5537 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5538 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5539 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5540 $ics_str .= 'END:VEVENT'."\r\n";
5541 // close iCal file content
5542 $ics_str .= "END:VCALENDAR";
5543
5544 /**
5545 * Trigger event to allow third party plugins to overwrite the iCal file.
5546 *
5547 * @since 1.16.0 (J) - 1.6.0 (WP)
5548 */
5549 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5550
5551 if (empty($ics_str)) {
5552 return false;
5553 }
5554
5555 // store the event onto a .ics file. We use the resources folder in back-end.
5556 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5557 $fp = fopen($fpath, 'w+');
5558 $bytes = fwrite($fp, $ics_str);
5559 fclose($fp);
5560
5561 return $bytes ? $fpath : false;
5562 }
5563
5564 public static function loadEmailTemplate($booking_info = array()) {
5565 define('_VIKBOOKINGEXEC', '1');
5566 ob_start();
5567 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5568 $content = ob_get_contents();
5569 ob_end_clean();
5570 return $content;
5571 }
5572
5573 /**
5574 * Parses the raw HTML content of the booking email template.
5575 *
5576 * @param string $tmpl the raw content of the template.
5577 * @param mixed $bid int for the booking ID or booking array.
5578 * @param array $rooms list of rooms booked and translated.
5579 * @param array $rates list of translated rates for the booked rooms.
5580 * @param array $options list of translated options booked.
5581 * @param float [$total] the booking total amount (in case it has changed).
5582 * @param string [$link] the booking link can be passed for the no-deposit.
5583 *
5584 * @return string the HTML content of the parsed email template.
5585 *
5586 * @since 1.13 with different arguments.
5587 */
5588 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5589 {
5590 $app = JFactory::getApplication();
5591 $dbo = JFactory::getDbo();
5592 $vbo_tn = self::getTranslator();
5593
5594 // availability helper
5595 $av_helper = self::getAvailabilityInstance();
5596
5597 // get necessary values
5598 if (is_array($bid)) {
5599 // we got the full booking record
5600 $order_info = $bid;
5601 $bid = $order_info['id'];
5602 } else {
5603 $order_info = array();
5604 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5605 $dbo->setQuery($q);
5606 $order_info = $dbo->loadAssoc();
5607 if (!$order_info) {
5608 throw new Exception('Booking not found', 404);
5609 }
5610 }
5611
5612 $tars_info = array();
5613 $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'] . ";";
5614 $dbo->setQuery($q);
5615 $tars_info = $dbo->loadAssocList();
5616 if (!$tars_info) {
5617 throw new Exception('No rooms found', 404);
5618 }
5619
5620 /**
5621 * Split stay reservation.
5622 *
5623 * @since 1.16.0 (J) - 1.6.0 (WP)
5624 */
5625 $room_stay_dates = [];
5626 if ($order_info['split_stay']) {
5627 if ($order_info['status'] == 'confirmed') {
5628 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5629 } else {
5630 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5631 }
5632 // immediately count the number of nights of stay for each split room
5633 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5634 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5635 // overwrite values for compatibility with non-confirmed bookings
5636 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5637 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5638 }
5639 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5640 // overwrite the whole array
5641 $room_stay_dates[$sps_r_k] = $sps_r_v;
5642 }
5643 }
5644
5645 // check if the language in use is the same as the one used during the checkout
5646 $lang = JFactory::getLanguage();
5647 if (!empty($order_info['lang'])) {
5648 if ($lang->getTag() != $order_info['lang']) {
5649 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5650 if (VBOPlatformDetection::isJoomla()) {
5651 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5652 }
5653 }
5654 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5655 // force the translation to start because contents should be translated
5656 $vbo_tn::$force_tolang = $order_info['lang'];
5657 }
5658 }
5659
5660 // values for replacements
5661 $company_name = self::getFrontTitle();
5662 $currencyname = self::getCurrencyName();
5663 $sitelogo = self::getSiteLogo();
5664 $footermess = self::getFooterOrdMail($vbo_tn);
5665 $dateformat = self::getDateFormat();
5666 $datesep = self::getDateSeparator();
5667 if ($dateformat == "%d/%m/%Y") {
5668 $df = 'd/m/Y';
5669 } elseif ($dateformat == "%m/%d/%Y") {
5670 $df = 'm/d/Y';
5671 } else {
5672 $df = 'Y/m/d';
5673 }
5674 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5675 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5676 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5677 $customer_info = nl2br($order_info['custdata']);
5678 $company_logo = '';
5679 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5680 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5681 }
5682 if ($order_info['status'] == 'cancelled') {
5683 $confirmnumber = '';
5684 $status_str = JText::translate('VBCANCELLED');
5685 } elseif ($order_info['status'] == 'standby') {
5686 $confirmnumber = '';
5687 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5688 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5689 // this is an inquiry reservation
5690 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5691 }
5692 } else {
5693 $confirmnumber = $order_info['confirmnumber'];
5694 $status_str = JText::translate('VBCOMPLETED');
5695 }
5696 // booking total amount
5697 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5698 // booking link
5699 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5700 if (is_null($link)) {
5701 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5702 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5703 }
5704
5705 // raw HTML content
5706 $parsed = $tmpl;
5707
5708 /**
5709 * Trigger event to allow third-party plugins to manipulate the template string.
5710 *
5711 * @since 1.16.10 (J) - 1.6.10 (WP)
5712 */
5713 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5714
5715 /**
5716 * Parse all conditional text rules.
5717 *
5718 * @since 1.14 (J) - 1.4.0 (WP)
5719 */
5720 self::getConditionalRulesInstance()
5721 ->set(['booking', 'rooms'], [$order_info, $rooms])
5722 ->parseTokens($parsed);
5723
5724 /**
5725 * Parse all Door Access Control tags.
5726 *
5727 * @since 1.18.4 (J) - 1.8.4 (WP)
5728 */
5729 VBOFactory::getDoorAccessControl()
5730 ->parseTokens((new VBOBookingRegistry($order_info, $rooms)), $parsed);
5731
5732 // special tokens (tags) replacement
5733 $parsed = str_replace("{logo}", $company_logo, $parsed);
5734 $parsed = str_replace("{company_name}", $company_name, $parsed);
5735 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5736 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5737 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5738 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5739 $parsed = str_replace("{order_status}", $status_str, $parsed);
5740 $parsed = str_replace("{order_date}", $create_date, $parsed);
5741
5742 // customer record
5743 $cpin = self::getCPinInstance();
5744 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5745 $customer_name = '';
5746 $customer_pin = '';
5747 if ($customer_record) {
5748 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5749 $customer_pin = $customer_record['pin'];
5750 }
5751 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5752 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5753
5754 // PIN Code
5755 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5756 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5757 if (!empty($customer_pin)) {
5758 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5759 }
5760 }
5761
5762 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5763
5764 // Confirmation Number
5765 if ($confirmnumber) {
5766 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5767 } else {
5768 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5769 }
5770 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5771 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5772
5773 $roomsnum = count($rooms);
5774 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5775 $roomstr = "";
5776 $tot_adults = 0;
5777 $tot_children = 0;
5778 $tot_guests = 0;
5779
5780 // Rooms Distinctive Features
5781 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5782
5783 foreach ($rooms as $num => $r) {
5784 // guests
5785 $tot_adults += (int) $r['adults'];
5786 $tot_children += (int) $r['children'];
5787 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5788
5789 // room info
5790 $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/>";
5791 // Rooms Distinctive Features
5792 if (is_array($matches[1] ?? []) && $matches[1]) {
5793 $distinctive_features = array();
5794 $rparams = (array)json_decode($r['params'], true);
5795 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'])) {
5796 $distinctive_features = $rparams['features'][$r['roomindex']];
5797 }
5798 $docheck = (count($distinctive_features) > 0);
5799 foreach ($matches[1] as $reqf) {
5800 $feature_found = false;
5801 if ($docheck) {
5802 foreach ($distinctive_features as $dfk => $dfv) {
5803 if (stripos($dfk, $reqf) !== false) {
5804 $feature_found = $dfk;
5805 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5806 break;
5807 }
5808 }
5809 }
5810 }
5811 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5812 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5813 }
5814 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5815 }
5816 }
5817 }
5818
5819 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5820 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5821 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5822
5823 // custom fields replace
5824 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5825 if (is_array($cmatches[1]) && count($cmatches[1])) {
5826 $cfids = array();
5827 foreach ($cmatches[1] as $cfid ) {
5828 $cfids[] = $cfid;
5829 }
5830 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5831 $dbo->setQuery($q);
5832 $cfields = $dbo->loadAssocList();
5833 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5834 $cfmap = array();
5835 foreach ($cfields as $cf) {
5836 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5837 }
5838 $cfmapreplace = array();
5839 $partsreceived = explode("\n", $order_info['custdata']);
5840 if (count($partsreceived) > 0) {
5841 foreach ($partsreceived as $pst) {
5842 if (!empty($pst)) {
5843 $tmpdata = explode(":", $pst);
5844 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5845 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5846 }
5847 }
5848 }
5849 }
5850 foreach ($cmatches[1] as $cfid ) {
5851 if (array_key_exists($cfid, $cfmapreplace)) {
5852 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5853 } else {
5854 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5855 }
5856 }
5857 }
5858
5859 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5860 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5861 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5862 $parsed = str_replace("{num_nights}", ($order_info['days'] ?? 1), $parsed);
5863
5864 // order details
5865 $orderdetails = "";
5866 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5867 foreach ($rooms as $num => $r) {
5868 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5869 $use_room_ind = $num - $room_loop_ind;
5870 $split_stay_str = '';
5871 $split_stay_info = [];
5872 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) {
5873 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5874 $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']);
5875 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5876 }
5877
5878 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5879 $faredets = explode(":", $expdet[0]);
5880 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5881 if (!empty($expdet[1])) {
5882 $attrfaredets = explode(":", $expdet[1]);
5883 if (strlen($attrfaredets[1]) > 0) {
5884 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5885 }
5886 }
5887 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5888 $orderdetails .= '<div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($fareprice), $currencyname) . '</span></div></div>';
5889 // options
5890 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5891 foreach ($options[$num] as $oo) {
5892 $expopts = explode("\n", $oo);
5893 foreach ($expopts as $optinfo) {
5894 if (!empty($optinfo)) {
5895 $splitopt = explode(":", $optinfo);
5896 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5897 $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>';
5898 }
5899 }
5900 }
5901 }
5902 //
5903 if ($roomsnum > 1 && $num < $roomsnum) {
5904 $orderdetails .= '<br/>';
5905 }
5906 }
5907
5908 // coupon
5909 if (!empty($order_info['coupon'])) {
5910 $expcoupon = explode(";", $order_info['coupon']);
5911 $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>';
5912 }
5913
5914 // discount and payment method
5915 $payment_info = [];
5916 $payment_name = '';
5917 if (!empty($order_info['idpayment'])) {
5918 $exppay = explode('=', $order_info['idpayment']);
5919 $payment = self::getPayment($exppay[0], $vbo_tn);
5920 if ((array) $payment) {
5921 $payment_name = $payment['name'];
5922 }
5923 }
5924
5925 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5926
5927 if ($order_info['status'] != 'cancelled') {
5928 if ($payment_info) {
5929 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5930 // Discount (not charge)
5931 if ($payment_info['val_pcent'] == 1) {
5932 // fixed value
5933 $total -= $payment_info['charge'];
5934 $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>';
5935 } else {
5936 // percent value
5937 $percent_disc = $total * $payment_info['charge'] / 100;
5938 $total -= $percent_disc;
5939 $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>';
5940 }
5941 }
5942 }
5943 }
5944
5945 // booking details string
5946 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5947
5948 // additional information
5949 $parsed = str_replace("{order_total}", self::formatCurrencyNumber(self::numberFormat($total), $currencyname), $parsed);
5950 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5951 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5952 $parsed = str_replace("{booking_link}", $link, $parsed);
5953
5954 // deposit
5955 $deposit_str = '';
5956 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5957 $percentdeposit = self::getAccPerCent();
5958 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5959 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5960 if (self::getTypeDeposit() == "fixed") {
5961 $deposit_amount = $percentdeposit;
5962 } else {
5963 $baseTotal = $total;
5964 if ($order_info['tot_damage_dep'] ?? 0) {
5965 // check if the damage deposit will have to be paid later/separately
5966 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($order_info, $rooms);
5967 if (($damage_deposit_payment['damagedep_gross'] ?? 0) && ($damage_deposit_payment['payment_window'] ?? []) && ($damage_deposit_payment['payment_window']['payable'] ?? null) !== true) {
5968 // damage deposit is not payable right now
5969 $baseTotal -= (float) $order_info['tot_damage_dep'];
5970 }
5971 }
5972 $deposit_amount = $baseTotal * $percentdeposit / 100;
5973 }
5974 if ($deposit_amount > 0) {
5975 $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>';
5976 }
5977 }
5978 }
5979 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5980 //
5981 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5982 $totpaid_str = '';
5983 if ($order_info['refund'] > 0) {
5984 $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>';
5985 }
5986 if ($order_info['status'] != 'cancelled') {
5987 $tot_paid = $order_info['totpaid'];
5988 $diff_topay = (float)$total - (float)$tot_paid;
5989 if ((float)$tot_paid > 0) {
5990 $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>';
5991 // only in case the remaining balance is greater than 1 to avoid commissions issues
5992 if ($diff_topay > 1) {
5993 $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>';
5994 }
5995 }
5996 }
5997 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5998 $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>';
5999 }
6000 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
6001
6002 /**
6003 * Language direction (LTR or RTL).
6004 *
6005 * @since 1.16.4 (J) - 1.6.4 (WP)
6006 */
6007 $lang_direction = 'ltr';
6008 if (!empty($order_info['lang'])) {
6009 // attempt to define the language direction and replace the special tag
6010 try {
6011 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
6012 } catch (Throwable $e) {
6013 // do nothing, default to ltr
6014 $lang_direction = 'ltr';
6015 }
6016 // static map for Arabic and Hebrew
6017 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
6018 }
6019 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
6020 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
6021 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
6022
6023 /**
6024 * Replace static floating styles depending on language direction (LTR or RTL).
6025 * For BC with older email template sources, we need to keep the inline style
6026 * for the floating of certain elements, which looks good only on LTR.
6027 *
6028 * @since 1.16.4 (J) - 1.6.4 (WP)
6029 */
6030 if ($lang_direction === 'rtl') {
6031 // get rid of inline static style declaration because the template uses CSS classes
6032 $parsed = str_replace("float: right;", '', $parsed);
6033 }
6034
6035 return $parsed;
6036 }
6037
6038 /**
6039 * New method for sending booking email messages
6040 * to the guest or to the administrator(s).
6041 *
6042 * @param int $bid the booking ID.
6043 * @param array $for guest, admin or a custom email address.
6044 * @param bool $send whether to send or return the HTML message.
6045 * @param bool $no_config if true, no configuration settings will be used for the status.
6046 * @param string $type optional type to indicate a booking modification.
6047 *
6048 * @return mixed True or False depending on the result or HTML string for the preview.
6049 *
6050 * @since 1.13 (J) - 1.3 (WP)
6051 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
6052 * the reservation status is pending and against the configuration.
6053 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
6054 */
6055 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
6056 {
6057 $dbo = JFactory::getDbo();
6058 $app = JFactory::getApplication();
6059
6060 $vbo_tn = self::getTranslator();
6061 $av_helper = self::getAvailabilityInstance();
6062
6063 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
6064 $dbo->setQuery($q);
6065 $booking = $dbo->loadAssoc();
6066 if (!$booking) {
6067 return false;
6068 }
6069
6070 $result = false;
6071
6072 // check if the language in use is the same as the one used during the checkout
6073 $lang = JFactory::getLanguage();
6074 if (!empty($booking['lang'])) {
6075 if ($lang->getTag() != $booking['lang']) {
6076 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
6077 if (VBOPlatformDetection::isJoomla()) {
6078 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
6079 }
6080 }
6081 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
6082 // force the translation to start because contents should be translated
6083 $vbo_tn::$force_tolang = $booking['lang'];
6084 }
6085 }
6086
6087 // load rooms booked
6088 $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;";
6089 $dbo->setQuery($q);
6090 $ordersrooms = $dbo->loadAssocList();
6091 if (!$ordersrooms) {
6092 return false;
6093 }
6094 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
6095
6096 /**
6097 * Split stay reservation.
6098 *
6099 * @since 1.16.0 (J) - 1.6.0 (WP)
6100 */
6101 $room_stay_dates = [];
6102 if ($booking['split_stay']) {
6103 if ($booking['status'] == 'confirmed') {
6104 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
6105 } else {
6106 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
6107 }
6108 // immediately count the number of nights of stay for each split room
6109 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6110 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6111 // overwrite values for compatibility with non-confirmed bookings
6112 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6113 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6114 }
6115 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6116 // overwrite the whole array
6117 $room_stay_dates[$sps_r_k] = $sps_r_v;
6118 }
6119 }
6120
6121 $rooms = array();
6122 $tars = array();
6123 $is_package = !empty($booking['pkg']) ? true : false;
6124 $ftitle = self::getFrontTitle();
6125 $currencyname = self::getCurrencyName();
6126 foreach ($ordersrooms as $kor => $or) {
6127 $num = $kor + 1;
6128 $rooms[$num] = $or;
6129 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6130 // package or custom cost set from the back-end
6131 continue;
6132 }
6133
6134 // determine the days to consider for the count of the availability
6135 $room_nights = $booking['days'];
6136 $room_checkin = $booking['checkin'];
6137 $room_checkout = $booking['checkout'];
6138 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6139 $room_nights = $room_stay_dates[$kor]['nights'];
6140 $room_checkin = $room_stay_dates[$kor]['checkin'];
6141 $room_checkout = $room_stay_dates[$kor]['checkout'];
6142 }
6143
6144 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
6145 $dbo->setQuery($q);
6146 $tar = $dbo->loadAssocList();
6147 if (!$tar) {
6148 // tariff not found
6149 if (self::isAdmin()) {
6150 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
6151 }
6152 continue;
6153 }
6154
6155 // apply seasonal rates
6156 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
6157
6158 // different usage
6159 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
6160 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
6161 // Occupancy Override
6162 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
6163 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
6164 //
6165 if (is_array($diffusageprice)) {
6166 // set a charge or discount to the price(s) for the different usage of the room
6167 foreach ($tar as $kpr => $vpr) {
6168 $tar[$kpr]['diffusage'] = $or['adults'];
6169 if ($diffusageprice['chdisc'] == 1) {
6170 // charge
6171 if ($diffusageprice['valpcent'] == 1) {
6172 // fixed value
6173 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6174 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6175 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
6176 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6177 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
6178 } else {
6179 // percentage value
6180 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6181 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
6182 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
6183 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6184 $tar[$kpr]['cost'] = $aduseval;
6185 }
6186 } else {
6187 // discount
6188 if ($diffusageprice['valpcent'] == 1) {
6189 // fixed value
6190 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6191 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6192 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
6193 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6194 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
6195 } else {
6196 // percentage value
6197 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6198 $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);
6199 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
6200 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6201 $tar[$kpr]['cost'] = $aduseval;
6202 }
6203 }
6204 }
6205 }
6206 }
6207 //
6208 $tars[$num] = $tar[0];
6209 }
6210
6211 $secdiff = $booking['checkout'] - $booking['checkin'];
6212 $daysdiff = $secdiff / 86400;
6213 if (is_int($daysdiff)) {
6214 if ($daysdiff < 1) {
6215 $daysdiff = 1;
6216 }
6217 } else {
6218 if ($daysdiff < 1) {
6219 $daysdiff = 1;
6220 } else {
6221 $sum = floor($daysdiff) * 86400;
6222 $newdiff = $secdiff - $sum;
6223 $maxhmore = self::getHoursMoreRb() * 3600;
6224 if ($maxhmore >= $newdiff) {
6225 $daysdiff = floor($daysdiff);
6226 } else {
6227 $daysdiff = ceil($daysdiff);
6228 }
6229 }
6230 }
6231
6232 $isdue = 0;
6233 $pricestr = array();
6234 $optstr = array();
6235 foreach ($ordersrooms as $kor => $or) {
6236 $num = $kor + 1;
6237
6238 // determine the days to consider for the count of the availability
6239 $room_nights = $booking['days'];
6240 $room_checkin = $booking['checkin'];
6241 $room_checkout = $booking['checkout'];
6242 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6243 $room_nights = $room_stay_dates[$kor]['nights'];
6244 $room_checkin = $room_stay_dates[$kor]['checkin'];
6245 $room_checkout = $room_stay_dates[$kor]['checkout'];
6246 }
6247
6248 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6249 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
6250 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
6251 $isdue += $calctar;
6252 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
6253 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
6254 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6255 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
6256 $tars[$num]['calctar'] = $calctar;
6257 $isdue += $calctar;
6258 $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'] : "");
6259 }
6260
6261 if (!empty($or['optionals'])) {
6262 $stepo = explode(";", $or['optionals']);
6263 foreach ($stepo as $roptkey => $oo) {
6264 if (empty($oo)) {
6265 continue;
6266 }
6267 $stept = explode(":", $oo);
6268 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
6269 $dbo->setQuery($q);
6270 $actopt = $dbo->loadAssocList();
6271 if ($actopt) {
6272 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
6273 $chvar = '';
6274 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
6275 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
6276 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
6277 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
6278 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
6279 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
6280 $agestept = explode('-', $stept[1]);
6281 $stept[1] = $agestept[0];
6282 $chvar = $agestept[1];
6283 $realcost = 0;
6284 if (!empty($chvar)) {
6285 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
6286 // percentage value of the adults tariff
6287 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6288 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6289 } else {
6290 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6291 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6292 }
6293 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
6294 // VBO 1.10 - percentage value of room base cost
6295 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6296 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6297 } else {
6298 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
6299 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6300 }
6301 }
6302 $actopt[0]['chageintv'] = $chvar;
6303 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
6304 $actopt[0]['quan'] = $stept[1];
6305 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
6306 }
6307 } else {
6308 $actopt[0]['quan'] = $stept[1];
6309 // VBO 1.11 - options percentage cost of the room total fee
6310 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6311 $deftar_basecosts = $or['cust_cost'];
6312 } else {
6313 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6314 }
6315 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
6316 //
6317 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
6318 }
6319 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
6320 $realcost = $actopt[0]['maxprice'];
6321 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
6322 $realcost = $actopt[0]['maxprice'] * $stept[1];
6323 }
6324 }
6325 if ($actopt[0]['perperson'] == 1) {
6326 $realcost = $realcost * $or['adults'];
6327 }
6328
6329 /**
6330 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
6331 *
6332 * @since 1.17.7 (J) - 1.7.7 (WP)
6333 */
6334 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
6335 if ($custom_calculation) {
6336 $realcost = (float) $custom_calculation[0];
6337 }
6338
6339 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
6340 $isdue += $tmpopr;
6341 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
6342 }
6343 }
6344 }
6345
6346 // custom extra costs
6347 if (!empty($or['extracosts'])) {
6348 $cur_extra_costs = json_decode($or['extracosts'], true);
6349 foreach ($cur_extra_costs as $eck => $ecv) {
6350 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
6351 $isdue += $ecplustax;
6352 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
6353 }
6354 }
6355 }
6356
6357 // force the original total amount if rates have changed
6358 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
6359 $isdue = $booking['total'];
6360 }
6361
6362 // mail subject
6363 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
6364 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
6365
6366 // inject the recipient of the message for the template
6367 $booking['for'] = $for;
6368
6369 // load template file that will get $booking as variable
6370 $tmpl = self::loadEmailTemplate($booking);
6371
6372 // parse email template
6373 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
6374 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
6375
6376 if ($send !== true) {
6377 // return the content of the email message parsed
6378 return $hmess;
6379 }
6380
6381 // when the message can be sent
6382 $sendwhen = self::getSendEmailWhen();
6383
6384 if ($no_config) {
6385 // force the configuration setting to send the message no matter what's the booking status
6386 $sendwhen = 1;
6387 }
6388
6389 // send the message
6390 foreach ($for as $who) {
6391 $use_subject = $subject;
6392 $recipients = array();
6393 $attachments = self::addEmailAttachment(null);
6394 $attach_ical = false;
6395 $force_replyto = null;
6396 if (strpos($who, '@') !== false) {
6397 // send email to custom email address
6398 array_push($recipients, trim($who));
6399 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6400 // send email to the customer
6401 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6402 continue;
6403 }
6404 array_push($recipients, $booking['custmail']);
6405 /**
6406 * Check whether an iCal should be attached for the customer.
6407 *
6408 * @since 1.12.0
6409 */
6410 $attach_ical = self::getEmailIcal('customer', $booking);
6411 } elseif (stripos($who, 'admin') !== false) {
6412 // send email to the administrator(s)
6413 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6414 continue;
6415 }
6416 $use_subject = $subject . ' #' . $booking['id'];
6417 if (!strcasecmp($type, 'modified')) {
6418 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6419 }
6420 $adminemail = self::getAdminMail();
6421 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6422 if (empty($adminemail) && empty($extra_admin_recipients)) {
6423 // Prevent Joomla Exceptions that would stop the script execution
6424 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6425 continue;
6426 }
6427 if (strpos($adminemail, ',') !== false) {
6428 // multiple addresses
6429 $adminemails = explode(',', $adminemail);
6430 foreach ($adminemails as $am) {
6431 if (strpos($am, '@') !== false) {
6432 array_push($recipients, trim($am));
6433 }
6434 }
6435 } else {
6436 // single address
6437 array_push($recipients, trim($adminemail));
6438 }
6439
6440 // merge extra recipients
6441 $recipients = array_merge($recipients, $extra_admin_recipients);
6442
6443 // admin should reply to the customer
6444 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6445
6446 /**
6447 * Check whether an iCal should be attached for the admin.
6448 *
6449 * @since 1.2.0
6450 */
6451 $attach_ical = self::getEmailIcal('admin', array(
6452 'ts' => $booking['ts'],
6453 'custdata' => $booking['custdata'],
6454 'checkin' => $booking['checkin'],
6455 'checkout' => $booking['checkout'],
6456 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6457 ));
6458 }
6459
6460 // send the message, recipients should always be an array to support multiple admin addresses
6461
6462 // get sender e-mail
6463 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6464
6465 // init mail data
6466 $mail = new VBOMailWrapper([
6467 'sender' => [$adsendermail, $ftitle],
6468 'recipient' => $recipients,
6469 'bcc' => self::addAdminEmailRecipient(null, true),
6470 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6471 'subject' => $use_subject,
6472 'content' => $hmess,
6473 'attachments' => $attachments,
6474 ]);
6475
6476 if ($attach_ical !== false && $booking['status'] != 'standby') {
6477 // attach iCal file
6478 $mail->addAttachment($attach_ical);
6479 }
6480
6481 /**
6482 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6483 *
6484 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6485 *
6486 * @since 1.15.4 (J) - 1.5.10 (WP)
6487 */
6488 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6489
6490 // send e-mail
6491 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6492
6493 // unlink iCal file
6494 if ($attach_ical !== false) {
6495 @unlink($attach_ical);
6496 }
6497 }
6498
6499 return $result;
6500 }
6501
6502 /**
6503 * This method allows to add one or more recipient email
6504 * addresses for the next queue of email sending for the admin.
6505 * This method can be used in the template file for the customer
6506 * email to register an additional email address, maybe when a
6507 * specific room-type is booked.
6508 * The methods sending the email messages are supposed to call this
6509 * method by passing no arguments to obtain the extra addresses set.
6510 *
6511 * @param mixed $email null, string or array of email address(es).
6512 * @param bool $bcc if true, addresses will be used as bcc.
6513 * @param bool $reset if true, the queues will be emptied.
6514 *
6515 * @return array the current extra recipients or bcc addresses set.
6516 *
6517 * @since 1.13 (J) - 1.3.0 (WP)
6518 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6519 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6520 */
6521 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6522 {
6523 static $extra_recipients = [];
6524 static $extra_bcc = [];
6525
6526 if ($reset) {
6527 $extra_recipients = [];
6528 $extra_bcc = [];
6529 }
6530
6531 if (!empty($email)) {
6532 if (is_scalar($email)) {
6533 if ($bcc) {
6534 array_push($extra_bcc, $email);
6535 } else {
6536 array_push($extra_recipients, $email);
6537 }
6538 } else {
6539 if ($bcc) {
6540 $extra_bcc = array_merge($extra_bcc, $email);
6541 } else {
6542 $extra_recipients = array_merge($extra_recipients, $email);
6543 }
6544 }
6545 }
6546
6547 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6548 }
6549
6550 /**
6551 * This method serves to add one or more attachments to the
6552 * next queue of email sending for the admin.
6553 * The methods sending the email messages are supposed to call this
6554 * method by passing a null argument to obtain the attachments set.
6555 *
6556 * @param mixed $file null or string with path to file to attach.
6557 * @param bool $reset if true, the queue will be emptied.
6558 *
6559 * @return array the current attachments set.
6560 *
6561 * @since 1.14
6562 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6563 */
6564 public static function addEmailAttachment($file, $reset = false)
6565 {
6566 static $extra_attachments = [];
6567
6568 if ($reset) {
6569 $extra_attachments = [];
6570 }
6571
6572 if (!empty($file)) {
6573 if (is_scalar($file)) {
6574 array_push($extra_attachments, $file);
6575 } else {
6576 $extra_attachments = array_merge($extra_attachments, $file);
6577 }
6578 }
6579
6580 return array_unique($extra_attachments);
6581 }
6582
6583 /**
6584 * This method is called whenever some rooms get booked.
6585 * It checks whether the rooms involved have shared calendars.
6586 * If some are found, then also such rooms will be occupied.
6587 *
6588 * @param int $bid the booking ID.
6589 * @param array $roomids the list of rooms booked.
6590 * @param int $checkin checkin timestamp.
6591 * @param int $checkout checkout timestamp.
6592 *
6593 * @return boolean true if some other cals were occupied, false otherwise.
6594 *
6595 * @since 1.13 (J) - 1.3.0 (WP)
6596 *
6597 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6598 */
6599 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6600 {
6601 $dbo = JFactory::getDbo();
6602 $bid = (int)$bid;
6603
6604 // availability helper
6605 $av_helper = self::getAvailabilityInstance();
6606
6607 // check for split stay rooms reservation
6608 $known_split_stay = false;
6609 $is_split_stay = 0;
6610
6611 if (empty($checkin) || empty($checkout)) {
6612 // get checkin and checkout timestamps from booking
6613 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6614 $dbo->setQuery($q);
6615 $dbo->execute();
6616 if (!$dbo->getNumRows()) {
6617 // booking not found
6618 return false;
6619 }
6620 $bdata = $dbo->loadAssoc();
6621 $checkin = $bdata['checkin'];
6622 $checkout = $bdata['checkout'];
6623 $is_split_stay = $bdata['split_stay'];
6624 // turn flag on
6625 $known_split_stay = true;
6626 }
6627
6628 if (!$known_split_stay) {
6629 // we need to query the db to access this information
6630 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6631 $dbo->setQuery($q, 0, 1);
6632 $dbo->execute();
6633 if ($dbo->getNumRows()) {
6634 $is_split_stay = (int)$dbo->loadResult();
6635 }
6636 // turn flag on
6637 $known_split_stay = true;
6638 }
6639
6640 // check split stay room records
6641 $room_stay_dates = [];
6642 if ($is_split_stay) {
6643 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6644 // immediately count the number of nights of stay for each split room
6645 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6646 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6647 // overwrite values for compatibility with non-confirmed bookings
6648 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6649 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6650 }
6651 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6652 // overwrite the whole array
6653 $room_stay_dates[$sps_r_k] = $sps_r_v;
6654 }
6655 }
6656
6657 // get the rooms booked
6658 if (!count($roomids)) {
6659 // get the IDs of all rooms booked
6660 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6661 $dbo->setQuery($q);
6662 $orr = $dbo->loadAssocList();
6663 if ($orr) {
6664 foreach ($orr as $or) {
6665 array_push($roomids, $or['idroom']);
6666 }
6667 }
6668 }
6669 if (!count($roomids) || empty($bid)) {
6670 // unable to proceed
6671 return false;
6672 }
6673 $roomids = array_unique($roomids);
6674
6675 // build room stay dates map in case of split stay
6676 $room_split_stay_map = [];
6677 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6678 // we have unique room IDs in a split stay reservation for more than one room
6679 foreach ($room_stay_dates as $room_stay_date) {
6680 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6681 }
6682 }
6683
6684 // get rooms involved
6685 $involved = [];
6686 $involved_stay_map = [];
6687 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6688 $dbo->setQuery($q);
6689 $rooms_found = $dbo->loadAssocList();
6690 if (!$rooms_found) {
6691 // no rooms involved that need their calendars updated
6692 return false;
6693 }
6694 foreach ($rooms_found as $rf) {
6695 if (!in_array($rf['mainroom'], $roomids)) {
6696 // push room ID
6697 array_push($involved, $rf['mainroom']);
6698 // check for specific stay dates in connected room
6699 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6700 // split stay information available
6701 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6702 }
6703 }
6704 if (!in_array($rf['childroom'], $roomids)) {
6705 // push room ID
6706 array_push($involved, $rf['childroom']);
6707 // check for specific stay dates in connected room
6708 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6709 // split stay information available
6710 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6711 }
6712 }
6713 }
6714 $involved = array_unique($involved);
6715 if (!count($involved)) {
6716 // no rooms involved
6717 return false;
6718 }
6719
6720 // turnover seconds
6721 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6722
6723 // occupy the calendars for the involved rooms found
6724 $bids_generated = [];
6725 foreach ($involved as $rid) {
6726 // determine the values to use
6727 $room_checkin = (int)$checkin;
6728 $room_checkout = (int)$checkout;
6729 $room_realback = ($checkout + $turnover_secs);
6730 if (isset($involved_stay_map[$rid])) {
6731 // split stay dates available
6732 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6733 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6734 $room_realback = ($room_checkout + $turnover_secs);
6735 }
6736
6737 // occupy record in room with shared calendar
6738 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6739 $dbo->setQuery($q);
6740 $dbo->execute();
6741 array_push($bids_generated, $dbo->insertid());
6742 }
6743
6744 // store busy relations created
6745 foreach ($bids_generated as $busyid) {
6746 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6747 $dbo->setQuery($q);
6748 $dbo->execute();
6749 }
6750
6751 return true;
6752 }
6753
6754 /**
6755 * This method is needed whenever a booking gets modified by
6756 * adding or removing rooms. This way we reset (remove) all
6757 * busy records that were previously stored due to a shared
6758 * calendar. The correct relations should then be re-created
6759 * by calling updateSharedCalendars().
6760 *
6761 * @param int $bid the booking ID.
6762 *
6763 * @return boolean true if some records were cleaned.
6764 *
6765 * @see updateSharedCalendars() should be called after.
6766 *
6767 * @since 1.3.0
6768 */
6769 public static function cleanSharedCalendarsBusy($bid) {
6770 $dbo = JFactory::getDbo();
6771 $bid = (int)$bid;
6772 // get all the occupied records due to shared calendars for this booking
6773 $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;";
6774 $dbo->setQuery($q);
6775 $allbusy = $dbo->loadAssocList();
6776 if (!$allbusy) {
6777 return false;
6778 }
6779
6780 $busy_ids = array();
6781 foreach ($allbusy as $b) {
6782 // push busy ID to be removed later
6783 array_push($busy_ids, $b['id']);
6784 // delete the current busy ID-booking ID relation for this shared calendar
6785 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6786 $dbo->setQuery($q);
6787 $dbo->execute();
6788 }
6789 if (count($busy_ids)) {
6790 // delete all busy records due to shared calendars
6791 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6792 $dbo->setQuery($q);
6793 $dbo->execute();
6794 }
6795
6796 return true;
6797 }
6798
6799 public static function sendJutility() {
6800 //deprecated in VBO 1.10
6801 $dbo = JFactory::getDbo();
6802 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6803 $dbo->setQuery($q);
6804 $dbo->execute();
6805 $s = $dbo->loadAssocList();
6806 return (intval($s[0]['setting']) == 1 ? true : false);
6807 }
6808
6809 public static function getCategoryName($idcat, $vbo_tn = null) {
6810 $dbo = JFactory::getDbo();
6811 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6812 $dbo->setQuery($q);
6813 $dbo->execute();
6814 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6815 if (is_object($vbo_tn) && count($p) > 0) {
6816 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6817 }
6818 return count($p) > 0 ? $p[0]['name'] : '';
6819 }
6820
6821 public static function loadAdultsDiff($idroom, $adults)
6822 {
6823 static $obp_map = [];
6824
6825 $map_sign = "{$idroom}:{$adults}";
6826
6827 if (array_key_exists($map_sign, $obp_map)) {
6828 return $obp_map[$map_sign];
6829 }
6830
6831 $dbo = JFactory::getDbo();
6832
6833 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6834 $dbo->setQuery($q, 0, 1);
6835 $obp_diff = $dbo->loadAssoc();
6836 if ($obp_diff) {
6837 // cache value and return it
6838 $obp_map[$map_sign] = $obp_diff;
6839
6840 return $obp_diff;
6841 }
6842
6843 // cache value and return it
6844 $obp_map[$map_sign] = null;
6845
6846 return null;
6847 }
6848
6849 public static function loadRoomAdultsDiff($idroom)
6850 {
6851 $dbo = JFactory::getDbo();
6852
6853 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6854 $dbo->setQuery($q);
6855 $diff = $dbo->loadAssocList();
6856 if ($diff) {
6857 $roomdiff = [];
6858 foreach ($diff as $v) {
6859 $roomdiff[$v['adults']] = $v;
6860 }
6861
6862 return $roomdiff;
6863 }
6864
6865 return [];
6866 }
6867
6868 public static function occupancyOverrideExists($tar, $adults) {
6869 foreach ($tar as $k => $v) {
6870 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6871 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6872 return $v['occupancy_ovr'][$adults];
6873 }
6874 }
6875 }
6876 return false;
6877 }
6878
6879 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6880 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6881 $charges = array();
6882 if (!($children > 0) || !(count($ages) > 0)) {
6883 return $charges;
6884 }
6885 $dbo = JFactory::getDbo();
6886 $id_options = array();
6887 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6888 $dbo->setQuery($q);
6889 $dbo->execute();
6890 if ($dbo->getNumRows() > 0) {
6891 $assocs = $dbo->loadAssocList();
6892 foreach ($assocs as $opts) {
6893 if (!empty($opts['idopt'])) {
6894 $r_ido = explode(';', rtrim($opts['idopt']));
6895 foreach ($r_ido as $ido) {
6896 if (!empty($ido) && !in_array($ido, $id_options)) {
6897 $id_options[] = $ido;
6898 }
6899 }
6900 }
6901 }
6902 }
6903 if (count($id_options) > 0) {
6904 $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;";
6905 $dbo->setQuery($q);
6906 $dbo->execute();
6907 if ($dbo->getNumRows() > 0) {
6908 $ageintervals = $dbo->loadAssocList();
6909 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6910 $age_range = array();
6911 foreach ($split_ages as $kg => $spage) {
6912 if (empty($spage)) {
6913 continue;
6914 }
6915 $parts = explode('_', $spage);
6916 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6917 $ind = count($age_range);
6918 $age_range[$ind]['from'] = intval($parts[0]);
6919 $age_range[$ind]['to'] = intval($parts[1]);
6920 //taxes are calculated later in VCM
6921 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6922 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6923 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6924 }
6925 }
6926 if (count($age_range) > 0) {
6927 $tot_charge = 0;
6928 $affected = array();
6929 $option_str = '';
6930 foreach ($ages as $age) {
6931 if (strlen($age) == 0) {
6932 continue;
6933 }
6934 foreach ($age_range as $range) {
6935 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6936 $tot_charge += $range['cost'];
6937 $affected[] = $age;
6938 $option_str .= $range['option_str'].';';
6939 break;
6940 }
6941 }
6942 }
6943 if ($tot_charge > 0) {
6944 $charges['total'] = $tot_charge;
6945 $charges['affected'] = $affected;
6946 $charges['options'] = $option_str;
6947 }
6948 }
6949 }
6950 }
6951
6952 return $charges;
6953 }
6954
6955 public static function sortRoomPrices($arr) {
6956 $newarr = array();
6957 foreach ($arr as $k => $v) {
6958 $newarr[$k] = $v['cost'];
6959 }
6960 asort($newarr);
6961 $sorted = array();
6962 foreach ($newarr as $k => $v) {
6963 $sorted[$k] = $arr[$k];
6964 }
6965 return $sorted;
6966 }
6967
6968 public static function sortResults($arr) {
6969 $newarr = array();
6970 foreach ($arr as $k => $v) {
6971 $newarr[$k] = $v[0]['cost'];
6972 }
6973 asort($newarr);
6974 $sorted = array();
6975 foreach ($newarr as $k => $v) {
6976 $sorted[$k] = $arr[$k];
6977 }
6978 return $sorted;
6979 }
6980
6981 public static function sortMultipleResults($arr) {
6982 foreach ($arr as $k => $v) {
6983 $newarr = array();
6984 foreach ($v as $subk => $subv) {
6985 $newarr[$subk] = $subv[0]['cost'];
6986 }
6987 asort($newarr);
6988 $sorted = array();
6989 foreach ($newarr as $nk => $v) {
6990 $sorted[$nk] = $arr[$k][$nk];
6991 }
6992 $arr[$k] = $sorted;
6993 }
6994 return $arr;
6995 }
6996
6997 /**
6998 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6999 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
7000 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
7001 *
7002 * @param array $promos optional list of promotion IDs to register in the execution flow.
7003 *
7004 * @return array list of cached promotion IDs (if any).
7005 *
7006 * @since 1.16.4 (J) - 1.6.4 (WP)
7007 */
7008 public static function registerPromotionIds(array $promos = [])
7009 {
7010 static $flagged_promos = [];
7011
7012 if ($promos) {
7013 // set cached value
7014 $flagged_promos = $promos;
7015 }
7016
7017 return $flagged_promos;
7018 }
7019
7020 /**
7021 * Getter and setter for seasons cache allowed.
7022 *
7023 * @param bool $enabled true or false for setter, null for getter.
7024 *
7025 * @return bool|null
7026 *
7027 * @since 1.16.5 (J) - 1.6.5 (WP)
7028 */
7029 public static function setSeasonsCache($enabled = null)
7030 {
7031 static $seasons_cache = null;
7032
7033 if ($enabled !== null) {
7034 // set value
7035 $seasons_cache = (bool)$enabled;
7036 }
7037
7038 return $seasons_cache;
7039 }
7040
7041 /**
7042 * Allows to preload and cache seasonal records for a list of rooms and dates.
7043 * Any subsequent preloading of season records for the same room(s) will return
7044 * the cached list. Should be used to preload a large window of data so that
7045 * subsequent calls will always return cached value.
7046 *
7047 * @param array $rooms List of involved room IDs.
7048 * @param int|bool $from Unix timestamp for start date or false to unset cache.
7049 * @param int $to Unix timestamp for end date.
7050 *
7051 * @return ?array
7052 *
7053 * @since 1.17.2 (J) - 1.7.2 (WP)
7054 */
7055 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
7056 {
7057 static $preloaded_records = [];
7058
7059 if (!$rooms) {
7060 // no cached signature
7061 return [];
7062 }
7063
7064 $signature = md5(implode(',', $rooms));
7065
7066 if ($from === false) {
7067 // unset cached records
7068 unset($preloaded_records[$signature]);
7069
7070 return;
7071 }
7072
7073 if ($from !== null && $to !== null) {
7074 // setter args signature for preloading records
7075 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
7076
7077 return;
7078 }
7079
7080 // getter args signature for getting the preloaded and cached records
7081 return $preloaded_records[$signature] ?? [];
7082 }
7083
7084 /**
7085 * Fetches all season records affecting a range of date timestamps.
7086 * Useful to pre-cache season records in case of hundreds of thousands
7087 * of records, but it can use up several MBs of server's memory.
7088 *
7089 * @param int $from unix timestamp for start date.
7090 * @param int $to unix timestamp for end date.
7091 * @param array $rooms optional list of involved room IDs.
7092 *
7093 * @return array
7094 *
7095 * @since 1.16.10 (J) - 1.6.10 (WP)
7096 */
7097 public static function getDateSeasonRecords($from, $to, array $rooms = [])
7098 {
7099 /**
7100 * We allow external systems to preload season records and cache them.
7101 *
7102 * @since 1.17.2 (J) - 1.7.2 (WP)
7103 */
7104 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
7105 return $preloaded_records;
7106 }
7107
7108 /**
7109 * For a more accurate records caching, ensure we have a range of two dates at least.
7110 *
7111 * @since 1.17.2 (J) - 1.7.2 (WP)
7112 */
7113 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
7114 // add one day to the end timestamp
7115 $to = strtotime('+1 day', $to);
7116 }
7117
7118 $dbo = JFactory::getDbo();
7119
7120 $one = getdate($from);
7121 $two = getdate($to);
7122
7123 // leap years
7124 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7125 $isleap = true;
7126 } else {
7127 $isleap = false;
7128 }
7129
7130 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7131 $tomidnightone = intval($one['hours']) * 3600;
7132 $tomidnightone += intval($one['minutes']) * 60;
7133 $sfrom = $from - $baseone - $tomidnightone;
7134 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7135 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7136 $tomidnighttwo = intval($two['hours']) * 3600;
7137 $tomidnighttwo += intval($two['minutes']) * 60;
7138 $sto = $to - $basetwo - $tomidnighttwo;
7139
7140 // leap years, check what dates to manipulate
7141 if ($isleap) {
7142 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7143 if ($one[0] > $leapts) {
7144 /**
7145 * Timestamp must be greater than the leap-day of Feb 29th.
7146 * It used to be checked for >= $leapts.
7147 *
7148 * @since July 3rd 2019
7149 */
7150 $sfrom -= 86400;
7151 }
7152 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7153 // lower checkin date when in leap year but not for checkout
7154 $sto -= 86400;
7155 }
7156 }
7157
7158 // check for DST changes to adjust the query values to fetch
7159 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7160 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7161 // we are parsing the day when the DST changed (probably a Sunday)
7162 if (!date('I', $from)) {
7163 // DST was just turned off
7164 $sfrom -= 3600;
7165 }
7166 }
7167 }
7168
7169 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7170 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7171 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7172 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7173 ($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`)") .
7174 ($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`)") .
7175 ($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`)") .
7176 ($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`)") .
7177 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7178 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7179
7180 /**
7181 * Avoid issues when querying data for a whole year by fetching all records.
7182 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
7183 *
7184 * @since 1.17.3 (J) - 1.7.3 (WP)
7185 */
7186 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
7187 // fetch all records when targeting a whole year of data
7188 $q = $dbo->getQuery(true)
7189 ->select('*')
7190 ->from($dbo->qn('#__vikbooking_seasons'))
7191 ->order($dbo->qn('promo') . ' ASC');
7192 }
7193
7194 $dbo->setQuery($q);
7195 $seasons = $dbo->loadAssocList();
7196
7197 if ($rooms) {
7198 // filter records by affected room IDs
7199 $seasons = array_filter($seasons, function($s) use ($rooms) {
7200 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
7201 foreach ($rooms as $idroom) {
7202 if (in_array("-" . $idroom . "-", $allrooms)) {
7203 return true;
7204 }
7205 }
7206 return false;
7207 });
7208
7209 // reset array keys
7210 $seasons = array_values($seasons);
7211 }
7212
7213 return $seasons;
7214 }
7215
7216 /**
7217 * Fetches all season records affecting week days with no dates.
7218 * Useful to avoid duplicate queries and cache/preload the records.
7219 *
7220 * @return array
7221 *
7222 * @since 1.18.5 (J) - 1.8.5 (WP)
7223 */
7224 public static function getWdaySeasonRecords()
7225 {
7226 static $cached_wdayseasons = null;
7227
7228 if (!is_null($cached_wdayseasons)) {
7229 return $cached_wdayseasons;
7230 }
7231
7232 $dbo = JFactory::getDbo();
7233
7234 $dbo->setQuery("SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));");
7235 $cached_wdayseasons = $dbo->loadAssocList();
7236
7237 return $cached_wdayseasons;
7238 }
7239
7240 /**
7241 * Applies the seasonal rates over a list of room rate records.
7242 *
7243 * @param array $arr list of room rate records.
7244 * @param int $from unix timestamp for start date.
7245 * @param int $to unix timestamp for end date.
7246 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7247 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7248 *
7249 * @return array list of manipulated room rate records.
7250 *
7251 * @since 1.18.0 (J) - 1.8.0 (WP) added support to records caching and preloading with args $seasons_dates and $seasons_wdays.
7252 * @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.
7253 */
7254 public static function applySeasonalPrices(array $arr, $from, $to, array $seasons_dates = [], ?array $seasons_wdays = null)
7255 {
7256 static $cached_seasons = [];
7257
7258 $cache_signature_wdays = '';
7259 if (self::setSeasonsCache() !== false && !$seasons_wdays) {
7260 // enable week-day season records caching
7261 $cache_signature_wdays = "wdays";
7262 }
7263
7264 $dbo = JFactory::getDbo();
7265 $vbo_tn = self::getTranslator();
7266
7267 $roomschange = [];
7268 $one = getdate($from);
7269
7270 // leap years
7271 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7272 $isleap = true;
7273 } else {
7274 $isleap = false;
7275 }
7276
7277 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7278 $tomidnightone = intval($one['hours']) * 3600;
7279 $tomidnightone += intval($one['minutes']) * 60;
7280 $sfrom = $from - $baseone - $tomidnightone;
7281 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7282 $two = getdate($to);
7283 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7284 $tomidnighttwo = intval($two['hours']) * 3600;
7285 $tomidnighttwo += intval($two['minutes']) * 60;
7286 $sto = $to - $basetwo - $tomidnighttwo;
7287
7288 // leap years, check what dates to manipulate
7289 if ($isleap) {
7290 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7291 if ($one[0] > $leapts) {
7292 /**
7293 * Timestamp must be greater than the leap-day of Feb 29th.
7294 * It used to be checked for >= $leapts.
7295 *
7296 * @since July 3rd 2019
7297 */
7298 $sfrom -= 86400;
7299 }
7300 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7301 // lower checkin date when in leap year but not for checkout
7302 $sto -= 86400;
7303 }
7304 }
7305
7306 // check for DST changes to adjust the query values to fetch
7307 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7308 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7309 // we are parsing the day when the DST changed (probably a Sunday)
7310 if (!date('I', $from)) {
7311 // DST was just turned off
7312 $sfrom -= 3600;
7313 }
7314 }
7315 }
7316
7317 // count nights requested
7318 $booking_nights = 1;
7319 foreach ($arr as $k => $a) {
7320 if (!isset($a[0])) {
7321 continue;
7322 }
7323 if (isset($a[0]['booking_nights'])) {
7324 // this value may be set when displaying pricing calendars
7325 $booking_nights = $a[0]['booking_nights'];
7326 break;
7327 }
7328 if (isset($a[0]['days'])) {
7329 $booking_nights = $a[0]['days'];
7330 break;
7331 }
7332 }
7333
7334 /**
7335 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7336 *
7337 * @since 1.16.4 (J) - 1.6.4 (WP)
7338 */
7339 $skip_promo_ids = self::registerPromotionIds();
7340
7341 $totseasons = 0;
7342 if (!$seasons_dates) {
7343 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7344 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7345 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7346 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7347 ($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`)") .
7348 ($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`)") .
7349 ($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`)") .
7350 ($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`)") .
7351 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7352 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7353
7354 // get the season records by running the query
7355 $dbo->setQuery($q);
7356 $seasons = $dbo->loadAssocList();
7357
7358 // count total seasons
7359 $totseasons = $seasons ? count($seasons) : 0;
7360 }
7361
7362 if ($totseasons > 0 || $seasons_dates) {
7363 if ($totseasons > 0) {
7364 $seasons = $seasons;
7365 } else {
7366 $seasons = $seasons_dates;
7367 }
7368 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7369 $applyseasons = false;
7370 $mem = [];
7371 foreach ($arr as $k => $a) {
7372 $mem[$k]['daysused'] = 0;
7373 $mem[$k]['sum'] = [];
7374 $mem[$k]['spids'] = [];
7375 /**
7376 * The keys below are all needed to apply the promotions on the room's final cost.
7377 *
7378 * @since 1.13.5
7379 */
7380 $mem[$k]['diffs'] = [];
7381 $mem[$k]['trans_keys'] = [];
7382 $mem[$k]['trans_factors'] = [];
7383 $mem[$k]['trans_affdays'] = [];
7384 }
7385 foreach ($seasons as $s) {
7386 // check if this is a promotion registered for skipping
7387 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7388 continue;
7389 }
7390
7391 // Special Price tied to the year
7392 if (!empty($s['year']) && $s['year'] > 0) {
7393 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7394 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7395 //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
7396 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7397 continue;
7398 }
7399 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7400 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7401 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7402 if ($calc_ends < ($from - $tomidnightone)) {
7403 continue;
7404 }
7405 }
7406 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7407 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7408 if ($s['from'] > $s['to']) {
7409 continue;
7410 }
7411 if (($basetwo + $s['from'] + 86399) > $to) {
7412 /**
7413 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7414 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7415 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7416 *
7417 * @since 1.14.3 (J) - 1.4.3 (WP)
7418 */
7419 continue;
7420 }
7421 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7422 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7423 /**
7424 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7425 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7426 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7427 *
7428 * @since 1.14.3 (J) - 1.4.3 (WP)
7429 */
7430 continue;
7431 }
7432 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7433 // 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)
7434 if (($baseone + $s['from']) > $to) {
7435 continue;
7436 }
7437 }
7438 }
7439 //
7440 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7441 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7442 $inits = $baseone + $s['from'];
7443 if ($s['from'] < $s['to']) {
7444 $ends = $basetwo + $s['to'];
7445 // check if the inits must be set to the year after
7446 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7447 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7448 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7449 $inits = $tmpbase + $s['from'];
7450 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7451 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7452 $ends = $baseone + $s['to'];
7453 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7454 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7455 $ends = $baseone + $s['to'];
7456 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7457 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7458 $inits = $basetwo + $s['from'];
7459 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7460 // booking dates across two years for a season tied to a specific year
7461 if ($one['year'] == $s['year']) {
7462 $inits = $baseone + $s['from'];
7463 } else {
7464 $inits = $basetwo + $s['from'];
7465 }
7466 if ($two['year'] == $s['year']) {
7467 $ends = $basetwo + $s['to'];
7468 } else {
7469 $ends = $baseone + $s['to'];
7470 }
7471 }
7472 } else {
7473 // between 2 years
7474 if ($baseone < $basetwo) {
7475 // ex. 29/12/2012 - 14/01/2013
7476 $ends = $basetwo + $s['to'];
7477 } else {
7478 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7479 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7480 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7481 $ends = $tmpbase + $s['to'];
7482 } else {
7483 // ex. 03/01 - 09/01
7484 $ends = $basetwo + $s['to'];
7485 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7486 $inits = $tmpbase + $s['from'];
7487 }
7488 }
7489 }
7490 // leap years
7491 if ($isleap == true) {
7492 $infoseason = getdate($inits);
7493 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7494 // leap years, check what dates to manipulate
7495 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7496 /**
7497 * Timestamp must be greater than the leap-day of Feb 29th.
7498 * It used to be checked for >= $leapts.
7499 *
7500 * @since July 3rd 2019
7501 */
7502 $inits += 86400;
7503 if ($s['from'] < $s['to']) {
7504 // increase season end date only if still on the same leap year
7505 $ends += 86400;
7506 }
7507 }
7508 }
7509
7510 // promotions
7511 $promotion = [];
7512 if (($s['promo'] ?? 0) == 1) {
7513 $daysadv = (($inits - time()) / 86400);
7514 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7515 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7516 continue;
7517 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7518 $secstocheckin = ($from - time());
7519 if ($s['promolastmin'] < $secstocheckin) {
7520 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7521 continue;
7522 }
7523 }
7524 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7525 /**
7526 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7527 *
7528 * @since 1.13.5
7529 */
7530 continue;
7531 }
7532 $promotion['todaydaysadv'] = $daysadv;
7533 $promotion['promodaysadv'] = $s['promodaysadv'];
7534 $promotion['promotxt'] = $s['promotxt'];
7535 }
7536
7537 // occupancy override
7538 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7539
7540 // week days
7541 $filterwdays = !empty($s['wdays']) ? true : false;
7542 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7543 if (is_array($wdays) && $wdays) {
7544 foreach ($wdays as $kw => $wd) {
7545 if (strlen($wd) == 0) {
7546 unset($wdays[$kw]);
7547 }
7548 }
7549 }
7550
7551 // checkin must be after the beginning of the season
7552 $checkininclok = true;
7553 if ($s['checkinincl'] == 1) {
7554 $checkininclok = false;
7555 if ($s['from'] < $s['to']) {
7556 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7557 $checkininclok = true;
7558 }
7559 } else {
7560 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7561 $checkininclok = true;
7562 }
7563 }
7564 }
7565 if ($checkininclok !== true) {
7566 continue;
7567 }
7568
7569 foreach ($arr as $k => $a) {
7570 // applied only to some types of price
7571 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7572 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7573 continue;
7574 }
7575 }
7576 // applied only to some room types
7577 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7578 continue;
7579 }
7580
7581 // count affected nights of stay
7582 $affdays = 0;
7583 $season_fromdayts = $fromdayts;
7584 $is_dst = date('I', $season_fromdayts);
7585 for ($i = 0; $i < $a[0]['days']; $i++) {
7586 $todayts = $season_fromdayts + ($i * 86400);
7587 $is_now_dst = date('I', $todayts);
7588 if ($is_dst != $is_now_dst) {
7589 // Daylight Saving Time has changed, check how
7590 if ((bool)$is_dst === true) {
7591 $todayts += 3600;
7592 $season_fromdayts += 3600;
7593 } else {
7594 $todayts -= 3600;
7595 $season_fromdayts -= 3600;
7596 }
7597 $is_dst = $is_now_dst;
7598 }
7599 if ($todayts >= $inits && $todayts <= $ends) {
7600 // week days
7601 if ($filterwdays == true) {
7602 $checkwday = getdate($todayts);
7603 if (in_array($checkwday['wday'], $wdays)) {
7604 $affdays++;
7605 }
7606 } else {
7607 $affdays++;
7608 }
7609 //
7610 }
7611 }
7612
7613 if (!($affdays > 0)) {
7614 // no nights affected
7615 continue;
7616 }
7617
7618 // apply the rule
7619 $applyseasons = true;
7620 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7621
7622 // modification factor object
7623 $factor = new stdClass;
7624
7625 // calculate new price progressively
7626 if (intval($s['val_pcent']) == 2) {
7627 // percentage value
7628 $factor->pcent = 1;
7629 $pctval = $s['diffcost'];
7630 if (strlen((string)$s['losoverride'])) {
7631 // values overrides
7632 $arrvaloverrides = [];
7633 $valovrparts = explode('_', $s['losoverride']);
7634 foreach ($valovrparts as $valovr) {
7635 if (!empty($valovr)) {
7636 $ovrinfo = explode(':', $valovr);
7637 if (strstr($ovrinfo[0], '-i') != false) {
7638 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7639 if ((int)$ovrinfo[0] < $a[0]['days']) {
7640 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7641 }
7642 }
7643 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7644 }
7645 }
7646 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7647 $pctval = $arrvaloverrides[$a[0]['days']];
7648 }
7649 }
7650 if (intval($s['type']) == 1) {
7651 // charge
7652 $factor->type = '+';
7653 $cpercent = 100 + $pctval;
7654 } else {
7655 // discount
7656 $factor->type = '-';
7657 $cpercent = 100 - $pctval;
7658 }
7659 $factor->amount = $pctval;
7660 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7661 } else {
7662 // absolute value
7663 $factor->pcent = 0;
7664 $absval = $s['diffcost'];
7665 if (strlen((string)$s['losoverride'])) {
7666 // values overrides
7667 $arrvaloverrides = [];
7668 $valovrparts = explode('_', $s['losoverride']);
7669 foreach ($valovrparts as $valovr) {
7670 if (!empty($valovr)) {
7671 $ovrinfo = explode(':', $valovr);
7672 if (strstr($ovrinfo[0], '-i') != false) {
7673 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7674 if ((int)$ovrinfo[0] < $a[0]['days']) {
7675 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7676 }
7677 }
7678 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7679 }
7680 }
7681 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7682 $absval = $arrvaloverrides[$a[0]['days']];
7683 }
7684 }
7685 if (intval($s['type']) == 1) {
7686 // charge
7687 $factor->type = '+';
7688 $newprice = ($dailyprice + $absval) * $affdays;
7689 } else {
7690 // discount
7691 $factor->type = '-';
7692 $newprice = ($dailyprice - $absval) * $affdays;
7693 }
7694 $factor->amount = $absval;
7695 }
7696
7697 // apply rounding
7698 $factor->roundmode = $s['roundmode'] ?? null;
7699 if (!empty($s['roundmode'])) {
7700 $newprice = round($newprice, 0, constant($s['roundmode']));
7701 } else {
7702 $newprice = round($newprice, 2);
7703 }
7704
7705 // define the promotion (only if no value overrides set the amount to 0)
7706 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7707 /**
7708 * Include the discount information (if any). The cost re-calculated may not be
7709 * precise if multiple special prices were applied over the same dates.
7710 *
7711 * @since 1.13
7712 */
7713 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7714 $promotion['discount'] = [
7715 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7716 'pcent' => (int)($s['val_pcent'] == 2),
7717 ];
7718 }
7719 //
7720 $mem[$k]['promotion'] = $promotion;
7721 }
7722
7723 // define the occupancy override
7724 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7725 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7726 }
7727
7728 // push special price ID
7729 if (!in_array($s['id'], $mem[$k]['spids'])) {
7730 array_push($mem[$k]['spids'], $s['id']);
7731 }
7732
7733 // push difference generated only if to be applied progressively
7734 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7735 /**
7736 * Push the difference generated by this special price for later transliteration of final price,
7737 * only if the special price is calculated progressively and not on the final price.
7738 *
7739 * @since 1.13.5
7740 */
7741 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7742 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7743 /**
7744 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7745 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7746 * all differences applied by the previous special pricing rules.
7747 *
7748 * @since 1.13.5
7749 */
7750 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7751 array_push($mem[$k]['trans_factors'], $factor);
7752 array_push($mem[$k]['trans_affdays'], $affdays);
7753 }
7754
7755 // push values in memory array
7756 array_push($mem[$k]['sum'], $newprice);
7757 $mem[$k]['daysused'] += $affdays;
7758 array_push($roomschange, $a[0]['idroom']);
7759 }
7760 }
7761 if ($applyseasons) {
7762 foreach ($mem as $k => $v) {
7763 if ($v['daysused'] > 0 && $v['sum']) {
7764 $newprice = 0;
7765 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7766 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7767 $addrest = $restdays * $dailyprice;
7768 $newprice += $addrest;
7769
7770 // calculate new final cost
7771 $redo_rounding = null;
7772 foreach ($v['sum'] as $sum_index => $add) {
7773 /**
7774 * The application of the various special pricing rules is made in a progressive and cumulative way
7775 * by always starting from the room base cost or its average daily cost. However, promotions may need
7776 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7777 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7778 * onto the differences generated by all the regular and progressively applied special pricing rules.
7779 *
7780 * @since 1.13.5
7781 */
7782 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7783 /**
7784 * This progressive price difference must be applied on the room final cost, so we need to
7785 * apply the transliteration over the other differences applied by other special prices.
7786 */
7787 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7788 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7789 // this is the % promotion we are looking for applying it on the final cost
7790 $factor = $v['trans_factors'][$transliterate_key];
7791 if (is_object($factor) && $factor->pcent) {
7792 $final_factor = 0;
7793 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7794 $final_factor += $prog_diff * $factor->amount / 100;
7795 }
7796 // update rounding
7797 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7798
7799 /**
7800 * Leverage the final factor depending on how many nights this affected.
7801 *
7802 * @since 1.15.0 (J) - 1.5.0 (WP)
7803 */
7804 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7805 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7806 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7807 }
7808
7809 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7810 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7811 }
7812 }
7813 }
7814
7815 // apply new price progressively
7816 $newprice += $add;
7817 }
7818
7819 // apply rounding from factor
7820 if (!empty($redo_rounding)) {
7821 $newprice = round($newprice, 0, constant($redo_rounding));
7822 }
7823
7824 // set promotion (if any)
7825 if (isset($v['promotion'])) {
7826 $arr[$k][0]['promotion'] = $v['promotion'];
7827 }
7828
7829 // set occupancy overrides (if any)
7830 if (isset($v['occupancy_ovr'])) {
7831 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7832 }
7833
7834 // set new final cost and update nights affected
7835 $arr[$k][0]['cost'] = $newprice;
7836 $arr[$k][0]['affdays'] = $v['daysused'];
7837 if (array_key_exists('spids', $v) && $v['spids']) {
7838 $arr[$k][0]['spids'] = $v['spids'];
7839 }
7840 }
7841 }
7842 }
7843 }
7844
7845 // week days with no season
7846 $roomschange = array_unique($roomschange);
7847 $totspecials = 0;
7848 if (!is_array($seasons_wdays)) {
7849 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7850 // avoid making a query
7851 $specials = $cached_seasons[$cache_signature_wdays];
7852 } else {
7853 // get the week-days-with-no-season records by running the query
7854 $specials = self::getWdaySeasonRecords();
7855 }
7856
7857 // count records
7858 $totspecials = $specials ? count($specials) : 0;
7859
7860 if ($cache_signature_wdays) {
7861 // cache week-days-with-no-season records
7862 $cached_seasons[$cache_signature_wdays] = $specials;
7863 }
7864 }
7865 if ($totspecials > 0 || $seasons_wdays) {
7866 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7867 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7868 $applyseasons = false;
7869 /**
7870 * We no longer unset the previous memory of the seasons with dates filters
7871 * because we need the responses to be merged. We do it only if not set.
7872 * We only keep the property 'spids' but the others should be unset.
7873 *
7874 * @since 1.11
7875 */
7876 if (!isset($mem)) {
7877 $mem = [];
7878 foreach ($arr as $k => $a) {
7879 $mem[$k]['daysused'] = 0;
7880 $mem[$k]['sum'] = [];
7881 $mem[$k]['spids'] = [];
7882 }
7883 } else {
7884 foreach ($arr as $k => $a) {
7885 $mem[$k]['daysused'] = 0;
7886 $mem[$k]['sum'] = [];
7887 }
7888 }
7889 //
7890 foreach ($specials as $s) {
7891 // check if this is a promotion registered for skipping
7892 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7893 continue;
7894 }
7895
7896 // Special Price tied to the year
7897 if (!empty($s['year']) && $s['year'] > 0) {
7898 if ($one['year'] != $s['year']) {
7899 continue;
7900 }
7901 }
7902
7903 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7904 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7905 // week days
7906 $filterwdays = !empty($s['wdays']) ? true : false;
7907 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7908 if (is_array($wdays) && $wdays) {
7909 foreach ($wdays as $kw => $wd) {
7910 if (strlen($wd) == 0) {
7911 unset($wdays[$kw]);
7912 }
7913 }
7914 }
7915
7916 foreach ($arr as $k => $a) {
7917 // only rooms with no price modifications from seasons
7918
7919 // applied only to some types of price
7920 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7921 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7922 continue;
7923 }
7924 }
7925
7926 /**
7927 * We should not exclude the rooms that already had a modification of the price through a season
7928 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7929 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7930 *
7931 * @since 1.11
7932 */
7933 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7934 continue;
7935 }
7936
7937 $affdays = 0;
7938 $season_fromdayts = $fromdayts;
7939 $is_dst = date('I', $season_fromdayts);
7940 for ($i = 0; $i < $a[0]['days']; $i++) {
7941 $todayts = $season_fromdayts + ($i * 86400);
7942 $is_now_dst = date('I', $todayts);
7943 if ($is_dst != $is_now_dst) {
7944 // Daylight Saving Time has changed, check how
7945 if ((bool)$is_dst === true) {
7946 $todayts += 3600;
7947 $season_fromdayts += 3600;
7948 } else {
7949 $todayts -= 3600;
7950 $season_fromdayts -= 3600;
7951 }
7952 $is_dst = $is_now_dst;
7953 }
7954 // week days
7955 if ($filterwdays == true) {
7956 $checkwday = getdate($todayts);
7957 if (in_array($checkwday['wday'], $wdays)) {
7958 $affdays++;
7959 }
7960 }
7961 }
7962 if (!($affdays > 0)) {
7963 // no nights affected
7964 continue;
7965 }
7966
7967 // apply the rule
7968 $applyseasons = true;
7969 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7970
7971 if (intval($s['val_pcent']) == 2) {
7972 // percentage value
7973 $pctval = $s['diffcost'];
7974 if (strlen((string)$s['losoverride'])) {
7975 // values overrides
7976 $arrvaloverrides = [];
7977 $valovrparts = explode('_', $s['losoverride']);
7978 foreach ($valovrparts as $valovr) {
7979 if (!empty($valovr)) {
7980 $ovrinfo = explode(':', $valovr);
7981 if (strstr($ovrinfo[0], '-i') != false) {
7982 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7983 if ((int)$ovrinfo[0] < $a[0]['days']) {
7984 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7985 }
7986 }
7987 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7988 }
7989 }
7990 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7991 $pctval = $arrvaloverrides[$a[0]['days']];
7992 }
7993 }
7994 if (intval($s['type']) == 1) {
7995 // charge
7996 $cpercent = 100 + $pctval;
7997 } else {
7998 // discount
7999 $cpercent = 100 - $pctval;
8000 }
8001 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
8002 } else {
8003 // absolute value
8004 $absval = $s['diffcost'];
8005 if (strlen((string)$s['losoverride'])) {
8006 // values overrides
8007 $arrvaloverrides = [];
8008 $valovrparts = explode('_', $s['losoverride']);
8009 foreach ($valovrparts as $valovr) {
8010 if (!empty($valovr)) {
8011 $ovrinfo = explode(':', $valovr);
8012 if (strstr($ovrinfo[0], '-i') != false) {
8013 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8014 if ((int)$ovrinfo[0] < $a[0]['days']) {
8015 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
8016 }
8017 }
8018 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8019 }
8020 }
8021 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8022 $absval = $arrvaloverrides[$a[0]['days']];
8023 }
8024 }
8025 if (intval($s['type']) == 1) {
8026 // charge
8027 $newprice = ($dailyprice + $absval) * $affdays;
8028 } else {
8029 // discount
8030 $newprice = ($dailyprice - $absval) * $affdays;
8031 }
8032 }
8033
8034 // apply rounding
8035 if (!empty($s['roundmode'])) {
8036 $newprice = round($newprice, 0, constant($s['roundmode']));
8037 } else {
8038 $newprice = round($newprice, 2);
8039 }
8040
8041 // push values in memory array
8042 if (!in_array($s['id'], $mem[$k]['spids'])) {
8043 $mem[$k]['spids'][] = $s['id'];
8044 }
8045 $mem[$k]['sum'][] = $newprice;
8046 $mem[$k]['daysused'] += $affdays;
8047 }
8048 }
8049 if ($applyseasons) {
8050 foreach ($mem as $k => $v) {
8051 if ($v['daysused'] > 0 && $v['sum']) {
8052 $newprice = 0;
8053 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
8054 $restdays = $arr[$k][0]['days'] - $v['daysused'];
8055 $addrest = $restdays * $dailyprice;
8056 $newprice += $addrest;
8057 foreach ($v['sum'] as $add) {
8058 $newprice += $add;
8059 }
8060 $arr[$k][0]['cost'] = $newprice;
8061 $arr[$k][0]['affdays'] = $v['daysused'];
8062 if (array_key_exists('spids', $v) && $v['spids']) {
8063 $arr[$k][0]['spids'] = $v['spids'];
8064 }
8065 }
8066 }
8067 }
8068 }
8069 // end week days with no season
8070
8071 return $arr;
8072 }
8073
8074 /**
8075 * Applies the seasonal rates over room rate records.
8076 *
8077 * @param array $arr array of tariffs taken from the DB
8078 * @param int $from start timestamp
8079 * @param int $to end timestamp
8080 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
8081 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
8082 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
8083 *
8084 * @return array
8085 *
8086 * @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.
8087 */
8088 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], ?array $seasons_wdays = null)
8089 {
8090 static $cached_seasons = [];
8091
8092 $cache_signature = '';
8093 $cache_signature_wdays = '';
8094 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
8095 // enable season records caching
8096 $cache_signature = "{$from}_{$to}";
8097 }
8098 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
8099 // enable week-day season records caching
8100 $cache_signature_wdays = "wdays";
8101 }
8102
8103 $dbo = JFactory::getDbo();
8104 $vbo_tn = self::getTranslator();
8105
8106 $roomschange = [];
8107 $one = getdate($from);
8108
8109 // leap years
8110 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
8111 $isleap = true;
8112 } else {
8113 $isleap = false;
8114 }
8115
8116 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
8117 $tomidnightone = intval($one['hours']) * 3600;
8118 $tomidnightone += intval($one['minutes']) * 60;
8119 $sfrom = $from - $baseone - $tomidnightone;
8120 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
8121 $two = getdate($to);
8122 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
8123 $tomidnighttwo = intval($two['hours']) * 3600;
8124 $tomidnighttwo += intval($two['minutes']) * 60;
8125 $sto = $to - $basetwo - $tomidnighttwo;
8126
8127 // leap years, check what dates to manipulate
8128 if ($isleap) {
8129 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
8130 if ($one[0] > $leapts) {
8131 /**
8132 * Timestamp must be greater than the leap-day of Feb 29th.
8133 * It used to be checked for >= $leapts.
8134 *
8135 * @since July 3rd 2019
8136 */
8137 $sfrom -= 86400;
8138 }
8139 if ($two[0] > $leapts && $one['year'] == $two['year']) {
8140 // lower checkin date when in leap year but not for checkout
8141 $sto -= 86400;
8142 }
8143 }
8144
8145 // check for DST changes to adjust the query values to fetch
8146 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
8147 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
8148 // we are parsing the day when the DST changed (probably a Sunday)
8149 if (!date('I', $from)) {
8150 // DST was just turned off
8151 $sfrom -= 3600;
8152 }
8153 }
8154 }
8155
8156 // count nights requested
8157 $booking_nights = 1;
8158 foreach ($arr as $k => $a) {
8159 if (isset($a['booking_nights'])) {
8160 // this value may be set when displaying pricing calendars
8161 $booking_nights = $a['booking_nights'];
8162 break;
8163 }
8164 if (isset($a['days'])) {
8165 $booking_nights = $a['days'];
8166 break;
8167 }
8168 }
8169
8170 /**
8171 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
8172 *
8173 * @since 1.16.4 (J) - 1.6.4 (WP)
8174 */
8175 $skip_promo_ids = self::registerPromotionIds();
8176
8177 $totseasons = 0;
8178 if (!$parsed_season && !$seasons_dates) {
8179 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
8180 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
8181 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
8182 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
8183 ($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`)") .
8184 ($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`)") .
8185 ($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`)") .
8186 ($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`)") .
8187 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
8188 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
8189
8190 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
8191 // avoid making a query
8192 $seasons = $cached_seasons[$cache_signature];
8193 } else {
8194 // get the season records by running the query
8195 $dbo->setQuery($q);
8196 $seasons = $dbo->loadAssocList();
8197 }
8198
8199 // count total seasons
8200 $totseasons = $seasons ? count($seasons) : 0;
8201
8202 if ($cache_signature) {
8203 // cache season records
8204 $cached_seasons[$cache_signature] = $seasons;
8205 }
8206 }
8207
8208 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
8209 if ($totseasons > 0) {
8210 $seasons = $seasons;
8211 } elseif ($parsed_season) {
8212 $seasons = [$parsed_season];
8213 } else {
8214 $seasons = $seasons_dates;
8215 }
8216 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
8217 $applyseasons = false;
8218 $mem = [];
8219 foreach ($arr as $k => $a) {
8220 $mem[$k]['daysused'] = 0;
8221 $mem[$k]['sum'] = [];
8222 $mem[$k]['spids'] = [];
8223 /**
8224 * The keys below are all needed to apply the promotions on the room's final cost.
8225 *
8226 * @since 1.13.5
8227 */
8228 $mem[$k]['diffs'] = [];
8229 $mem[$k]['trans_keys'] = [];
8230 $mem[$k]['trans_factors'] = [];
8231 $mem[$k]['trans_affdays'] = [];
8232 }
8233 $affdayslistless = [];
8234 foreach ($seasons as $s) {
8235 // 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
8236 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
8237 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
8238 continue;
8239 }
8240
8241 /**
8242 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
8243 * We need to skip such fake seasons as they do not need any parsing.
8244 *
8245 * @since 1.13
8246 */
8247 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
8248 continue;
8249 }
8250
8251 // check if this is a promotion registered for skipping
8252 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8253 continue;
8254 }
8255
8256 // Special Price tied to the year
8257 if (!empty($s['year']) && $s['year'] > 0) {
8258 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
8259 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
8260 //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
8261 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
8262 continue;
8263 }
8264 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
8265 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
8266 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
8267 if ($calc_ends < ($from - $tomidnightone)) {
8268 continue;
8269 }
8270 }
8271 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
8272 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
8273 if ($s['from'] > $s['to']) {
8274 continue;
8275 }
8276 if (($basetwo + $s['from'] + 86399) > $to) {
8277 /**
8278 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
8279 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
8280 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
8281 *
8282 * @since 1.14.3 (J) - 1.4.3 (WP)
8283 */
8284 continue;
8285 }
8286 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
8287 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
8288 /**
8289 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
8290 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
8291 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
8292 *
8293 * @since 1.14.3 (J) - 1.4.3 (WP)
8294 */
8295 continue;
8296 }
8297 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
8298 // 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)
8299 if (($baseone + $s['from']) > $to) {
8300 continue;
8301 }
8302 }
8303 }
8304 //
8305 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8306 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8307 $inits = $baseone + $s['from'];
8308 if ($s['from'] < $s['to']) {
8309 $ends = $basetwo + $s['to'];
8310 // check if the inits must be set to the year after
8311 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
8312 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
8313 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8314 $inits = $tmpbase + $s['from'];
8315 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8316 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
8317 $ends = $baseone + $s['to'];
8318 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8319 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
8320 $ends = $baseone + $s['to'];
8321 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
8322 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
8323 $inits = $basetwo + $s['from'];
8324 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
8325 // booking dates across two years for a season tied to a specific year
8326 if ($one['year'] == $s['year']) {
8327 $inits = $baseone + $s['from'];
8328 } else {
8329 $inits = $basetwo + $s['from'];
8330 }
8331 if ($two['year'] == $s['year']) {
8332 $ends = $basetwo + $s['to'];
8333 } else {
8334 $ends = $baseone + $s['to'];
8335 }
8336 }
8337 } else {
8338 // between 2 years
8339 if ($baseone < $basetwo) {
8340 // ex. 29/12/2012 - 14/01/2013
8341 $ends = $basetwo + $s['to'];
8342 } else {
8343 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
8344 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
8345 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8346 $ends = $tmpbase + $s['to'];
8347 } else {
8348 // ex. 03/01 - 09/01
8349 $ends = $basetwo + $s['to'];
8350 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
8351 $inits = $tmpbase + $s['from'];
8352 }
8353 }
8354 }
8355 // leap years
8356 if ($isleap == true) {
8357 $infoseason = getdate($inits);
8358 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
8359 // leap years, check what dates to manipulate
8360 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
8361 /**
8362 * Timestamp must be greater than the leap-day of Feb 29th.
8363 * It used to be checked for >= $leapts.
8364 *
8365 * @since July 3rd 2019
8366 */
8367 $inits += 86400;
8368 if ($s['from'] < $s['to']) {
8369 // increase season end date only if still on the same leap year
8370 $ends += 86400;
8371 }
8372 }
8373 }
8374
8375 // promotions
8376 $promotion = [];
8377 if (($s['promo'] ?? 0) == 1) {
8378 $daysadv = (($inits - time()) / 86400);
8379 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
8380 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
8381 continue;
8382 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
8383 $secstocheckin = ($from - time());
8384 if ($s['promolastmin'] < $secstocheckin) {
8385 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
8386 continue;
8387 }
8388 }
8389 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
8390 /**
8391 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
8392 *
8393 * @since 1.13.5
8394 */
8395 continue;
8396 }
8397 $promotion['todaydaysadv'] = $daysadv;
8398 $promotion['promodaysadv'] = $s['promodaysadv'];
8399 $promotion['promotxt'] = $s['promotxt'];
8400 }
8401
8402 // occupancy override
8403 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
8404
8405 // week days
8406 $filterwdays = !empty($s['wdays']) ? true : false;
8407 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8408 if (is_array($wdays) && $wdays) {
8409 foreach ($wdays as $kw => $wd) {
8410 if (strlen($wd) == 0) {
8411 unset($wdays[$kw]);
8412 }
8413 }
8414 }
8415
8416 // checkin must be after the beginning of the season
8417 $checkininclok = true;
8418 if ($s['checkinincl'] == 1) {
8419 $checkininclok = false;
8420 if ($s['from'] < $s['to']) {
8421 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
8422 $checkininclok = true;
8423 }
8424 } else {
8425 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
8426 $checkininclok = true;
8427 }
8428 }
8429 }
8430 if ($checkininclok !== true) {
8431 continue;
8432 }
8433
8434 foreach ($arr as $k => $a) {
8435 // applied only to some types of price
8436 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8437 // VikBooking 1.6: Price Calendar sets the idprice to -1
8438 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8439 continue;
8440 }
8441 }
8442 // applied only to some room types
8443 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8444 continue;
8445 }
8446
8447 if (isset($a['days']) && is_float($a['days'])) {
8448 // prevent any possible warning with array_key_exists()
8449 $a['days'] = (int)$a['days'];
8450 }
8451
8452 $affdays = 0;
8453 $season_fromdayts = $fromdayts;
8454 $is_dst = date('I', $season_fromdayts);
8455 for ($i = 0; $i < $a['days']; $i++) {
8456 $todayts = $season_fromdayts + ($i * 86400);
8457 $is_now_dst = date('I', $todayts);
8458 if ($is_dst != $is_now_dst) {
8459 // Daylight Saving Time has changed, check how
8460 if ((bool)$is_dst === true) {
8461 $todayts += 3600;
8462 $season_fromdayts += 3600;
8463 } else {
8464 $todayts -= 3600;
8465 $season_fromdayts -= 3600;
8466 }
8467 $is_dst = $is_now_dst;
8468 }
8469 if ($todayts >= $inits && $todayts <= $ends) {
8470 $checkwday = getdate($todayts);
8471 // week days
8472 if ($filterwdays == true) {
8473 if (in_array($checkwday['wday'], $wdays)) {
8474 if (!isset($arr[$k]['affdayslist'])) {
8475 $arr[$k]['affdayslist'] = [];
8476 }
8477 $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;
8478 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8479 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8480 $affdays++;
8481 }
8482 } else {
8483 if (!isset($arr[$k]['affdayslist'])) {
8484 $arr[$k]['affdayslist'] = [];
8485 }
8486 $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;
8487 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8488 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8489 $affdays++;
8490 }
8491 //
8492 }
8493 }
8494
8495 if (!($affdays > 0)) {
8496 // no nights affected
8497 continue;
8498 }
8499
8500 // apply the rule
8501 $applyseasons = true;
8502 $dailyprice = $a['cost'] / $a['days'];
8503
8504 // modification factor object
8505 $factor = new stdClass;
8506
8507 // calculate new price progressively
8508 if (intval($s['val_pcent']) == 2) {
8509 // percentage value
8510 $factor->pcent = 1;
8511 $pctval = $s['diffcost'];
8512 if (strlen((string)$s['losoverride'])) {
8513 // values overrides
8514 $arrvaloverrides = [];
8515 $valovrparts = explode('_', $s['losoverride']);
8516 foreach ($valovrparts as $valovr) {
8517 if (!empty($valovr)) {
8518 $ovrinfo = explode(':', $valovr);
8519 if (strstr($ovrinfo[0], '-i') != false) {
8520 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8521 if ((int)$ovrinfo[0] < $a['days']) {
8522 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8523 }
8524 }
8525 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8526 }
8527 }
8528 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8529 $pctval = $arrvaloverrides[$a['days']];
8530 }
8531 }
8532 if (intval($s['type']) == 1) {
8533 // charge
8534 $factor->type = '+';
8535 $cpercent = 100 + $pctval;
8536 } else {
8537 // discount
8538 $factor->type = '-';
8539 $cpercent = 100 - $pctval;
8540 }
8541 $factor->amount = $pctval;
8542 $dailysum = ($dailyprice * $cpercent / 100);
8543 $newprice = $dailysum * $affdays;
8544 } else {
8545 // absolute value
8546 $factor->pcent = 0;
8547 $absval = $s['diffcost'];
8548 if (strlen((string)$s['losoverride'])) {
8549 // values overrides
8550 $arrvaloverrides = [];
8551 $valovrparts = explode('_', $s['losoverride']);
8552 foreach ($valovrparts as $valovr) {
8553 if (!empty($valovr)) {
8554 $ovrinfo = explode(':', $valovr);
8555 if (strstr($ovrinfo[0], '-i') != false) {
8556 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8557 if ((int)$ovrinfo[0] < $a['days']) {
8558 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8559 }
8560 }
8561 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8562 }
8563 }
8564 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8565 $absval = $arrvaloverrides[$a['days']];
8566 }
8567 }
8568 if (intval($s['type']) == 1) {
8569 // charge
8570 $factor->type = '+';
8571 $dailysum = ($dailyprice + $absval);
8572 $newprice = $dailysum * $affdays;
8573 } else {
8574 // discount
8575 $factor->type = '-';
8576 $dailysum = ($dailyprice - $absval);
8577 $newprice = $dailysum * $affdays;
8578 }
8579 $factor->amount = $absval;
8580 }
8581
8582 // apply rounding
8583 $factor->roundmode = $s['roundmode'] ?? null;
8584 if (!empty($s['roundmode'])) {
8585 $newprice = round($newprice, 0, constant($s['roundmode']));
8586 } else {
8587 $newprice = round($newprice, 2);
8588 }
8589
8590 // define the promotion (only if no value overrides set the amount to 0)
8591 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8592 /**
8593 * Include the discount information (if any). The cost re-calculated may not be
8594 * precise if multiple special prices were applied over the same dates.
8595 *
8596 * @since 1.13
8597 */
8598 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8599 $promotion['discount'] = [
8600 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8601 'pcent' => (int)($s['val_pcent'] == 2),
8602 ];
8603 }
8604 //
8605 $mem[$k]['promotion'] = $promotion;
8606 }
8607
8608 // define the occupancy override
8609 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8610 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8611 }
8612
8613 // affected days list
8614 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8615 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8616 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8617 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8618 }
8619 }
8620 }
8621
8622 // push special price ID
8623 if (!in_array($s['id'], $mem[$k]['spids'])) {
8624 array_push($mem[$k]['spids'], $s['id']);
8625 }
8626
8627 // push difference generated only if to be applied progressively
8628 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8629 /**
8630 * Push the difference generated by this special price for later transliteration of final price,
8631 * only if the special price is calculated progressively and not on the final price.
8632 *
8633 * @since 1.13.5
8634 */
8635 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8636 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8637 /**
8638 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8639 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8640 * all differences applied by the previous special pricing rules.
8641 *
8642 * @since 1.13.5
8643 */
8644 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8645 array_push($mem[$k]['trans_factors'], $factor);
8646 array_push($mem[$k]['trans_affdays'], $affdays);
8647 }
8648
8649 // push values in memory array
8650 array_push($mem[$k]['sum'], $newprice);
8651 $mem[$k]['daysused'] += $affdays;
8652 array_push($roomschange, $a['idroom']);
8653 }
8654 }
8655 if ($applyseasons) {
8656 foreach ($mem as $k => $v) {
8657 if ($v['daysused'] > 0 && $v['sum']) {
8658 $newprice = 0;
8659 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8660 $restdays = $arr[$k]['days'] - $v['daysused'];
8661 $addrest = $restdays * $dailyprice;
8662 $newprice += $addrest;
8663
8664 // calculate new final cost
8665 $redo_rounding = null;
8666 foreach ($v['sum'] as $sum_index => $add) {
8667 /**
8668 * The application of the various special pricing rules is made in a progressive and cumulative way
8669 * by always starting from the room base cost or its average daily cost. However, promotions may need
8670 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8671 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8672 * onto the differences generated by all the regular and progressively applied special pricing rules.
8673 *
8674 * @since 1.13.5
8675 */
8676 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8677 /**
8678 * This progressive price difference must be applied on the room final cost, so we need to
8679 * apply the transliteration over the other differences applied by other special prices.
8680 */
8681 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8682 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8683 // this is the % promotion we are looking for applying it on the final cost
8684 $factor = $v['trans_factors'][$transliterate_key];
8685 if (is_object($factor) && $factor->pcent) {
8686 $final_factor = 0;
8687 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8688 $final_factor += $prog_diff * $factor->amount / 100;
8689 }
8690 // update rounding
8691 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8692
8693 /**
8694 * Leverage the final factor depending on how many nights this affected.
8695 *
8696 * @since 1.15.0 (J) - 1.5.0 (WP)
8697 */
8698 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8699 $avg_final_factor = $final_factor / $arr[$k]['days'];
8700 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8701 }
8702
8703 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8704 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8705 }
8706 }
8707 }
8708
8709 // apply new price progressively
8710 $newprice += $add;
8711 }
8712
8713 // apply rounding from factor
8714 if (!empty($redo_rounding)) {
8715 $newprice = round($newprice, 0, constant($redo_rounding));
8716 }
8717
8718 // set promotion (if any)
8719 if (isset($v['promotion'])) {
8720 $arr[$k]['promotion'] = $v['promotion'];
8721 }
8722
8723 // set occupancy overrides (if any)
8724 if (isset($v['occupancy_ovr'])) {
8725 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8726 }
8727
8728 // set new final cost and update nights affected
8729 $arr[$k]['cost'] = $newprice;
8730 $arr[$k]['affdays'] = $v['daysused'];
8731 if (array_key_exists('spids', $v) && $v['spids']) {
8732 $arr[$k]['spids'] = $v['spids'];
8733 }
8734 }
8735 }
8736 }
8737 }
8738
8739 // week days with no season
8740 $roomschange = array_unique($roomschange);
8741 $totspecials = 0;
8742 if (!is_array($seasons_wdays)) {
8743 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8744 // avoid making a query
8745 $specials = $cached_seasons[$cache_signature_wdays];
8746 } else {
8747 // get the week-days-with-no-season records by running the query
8748 $specials = self::getWdaySeasonRecords();
8749 }
8750
8751 // count records
8752 $totspecials = $specials ? count($specials) : 0;
8753
8754 if ($cache_signature_wdays) {
8755 // cache week-days-with-no-season records
8756 $cached_seasons[$cache_signature_wdays] = $specials;
8757 }
8758 }
8759 if ($totspecials > 0 || $seasons_wdays) {
8760 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8761 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8762 $applyseasons = false;
8763 /**
8764 * We no longer unset the previous memory of the seasons with dates filters
8765 * because we need the responses to be merged. We do it only if not set.
8766 * We only keep the property 'spids' but the others should be unset.
8767 *
8768 * @since 1.11
8769 */
8770 if (!isset($mem)) {
8771 $mem = [];
8772 foreach ($arr as $k => $a) {
8773 $mem[$k]['daysused'] = 0;
8774 $mem[$k]['sum'] = [];
8775 $mem[$k]['spids'] = [];
8776 }
8777 } else {
8778 foreach ($arr as $k => $a) {
8779 $mem[$k]['daysused'] = 0;
8780 $mem[$k]['sum'] = [];
8781 }
8782 }
8783 //
8784 foreach ($specials as $s) {
8785 // 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
8786 if (!empty($s['from']) || !empty($s['to'])) {
8787 continue;
8788 }
8789
8790 // check if this is a promotion registered for skipping
8791 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8792 continue;
8793 }
8794
8795 // Special Price tied to the year
8796 if (!empty($s['year']) && $s['year'] > 0) {
8797 if ($one['year'] != $s['year']) {
8798 continue;
8799 }
8800 }
8801
8802 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8803 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8804 // week days
8805 $filterwdays = !empty($s['wdays']) ? true : false;
8806 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8807 if (is_array($wdays) && $wdays) {
8808 foreach ($wdays as $kw => $wd) {
8809 if (strlen($wd) == 0) {
8810 unset($wdays[$kw]);
8811 }
8812 }
8813 }
8814
8815 foreach ($arr as $k => $a) {
8816 // only rooms with no price modifications from seasons
8817
8818 // applied only to some types of price
8819 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8820 // VikBooking 1.6: Price Calendar sets the idprice to -1
8821 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8822 continue;
8823 }
8824 }
8825
8826 /**
8827 * We should not exclude the rooms that already had a modification of the price through a season
8828 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8829 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8830 *
8831 * @since 1.11
8832 */
8833 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8834 continue;
8835 }
8836
8837 if (isset($a['days']) && is_float($a['days'])) {
8838 // prevent any possible warning with array_key_exists()
8839 $a['days'] = (int)$a['days'];
8840 }
8841
8842 $affdays = 0;
8843 $season_fromdayts = $fromdayts;
8844 $is_dst = date('I', $season_fromdayts);
8845 for ($i = 0; $i < $a['days']; $i++) {
8846 $todayts = $season_fromdayts + ($i * 86400);
8847 $is_now_dst = date('I', $todayts);
8848 if ($is_dst != $is_now_dst) {
8849 // Daylight Saving Time has changed, check how
8850 if ((bool)$is_dst === true) {
8851 $todayts += 3600;
8852 $season_fromdayts += 3600;
8853 } else {
8854 $todayts -= 3600;
8855 $season_fromdayts -= 3600;
8856 }
8857 $is_dst = $is_now_dst;
8858 }
8859 // week days
8860 if ($filterwdays == true) {
8861 $checkwday = getdate($todayts);
8862 if (in_array($checkwday['wday'], $wdays)) {
8863 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8864 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8865 $affdays++;
8866 }
8867 }
8868 //
8869 }
8870
8871 if (!($affdays > 0)) {
8872 // no nights affected
8873 continue;
8874 }
8875
8876 // apply the rule
8877 $applyseasons = true;
8878 $dailyprice = $a['cost'] / $a['days'];
8879
8880 if (intval($s['val_pcent']) == 2) {
8881 // percentage value
8882 $pctval = $s['diffcost'];
8883 if (strlen((string)$s['losoverride'])) {
8884 // values overrides
8885 $arrvaloverrides = [];
8886 $valovrparts = explode('_', $s['losoverride']);
8887 foreach ($valovrparts as $valovr) {
8888 if (!empty($valovr)) {
8889 $ovrinfo = explode(':', $valovr);
8890 if (strstr($ovrinfo[0], '-i') != false) {
8891 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8892 if ((int)$ovrinfo[0] < $a['days']) {
8893 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8894 }
8895 }
8896 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8897 }
8898 }
8899 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8900 $pctval = $arrvaloverrides[$a['days']];
8901 }
8902 }
8903 if (intval($s['type']) == 1) {
8904 // charge
8905 $cpercent = 100 + $pctval;
8906 } else {
8907 // discount
8908 $cpercent = 100 - $pctval;
8909 }
8910 $dailysum = ($dailyprice * $cpercent / 100);
8911 $newprice = $dailysum * $affdays;
8912 } else {
8913 // absolute value
8914 $absval = $s['diffcost'];
8915 if (strlen((string)$s['losoverride'])) {
8916 // values overrides
8917 $arrvaloverrides = [];
8918 $valovrparts = explode('_', $s['losoverride']);
8919 foreach ($valovrparts as $valovr) {
8920 if (!empty($valovr)) {
8921 $ovrinfo = explode(':', $valovr);
8922 if (strstr($ovrinfo[0], '-i') != false) {
8923 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8924 if ((int)$ovrinfo[0] < $a['days']) {
8925 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8926 }
8927 }
8928 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8929 }
8930 }
8931 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8932 $absval = $arrvaloverrides[$a['days']];
8933 }
8934 }
8935 if (intval($s['type']) == 1) {
8936 // charge
8937 $dailysum = ($dailyprice + $absval);
8938 $newprice = $dailysum * $affdays;
8939 } else {
8940 // discount
8941 $dailysum = ($dailyprice - $absval);
8942 $newprice = $dailysum * $affdays;
8943 }
8944 }
8945
8946 // apply rounding
8947 if (!empty($s['roundmode'])) {
8948 $newprice = round($newprice, 0, constant($s['roundmode']));
8949 } else {
8950 $newprice = round($newprice, 2);
8951 }
8952
8953 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8954 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8955 }
8956 if (!in_array($s['id'], $mem[$k]['spids'])) {
8957 $mem[$k]['spids'][] = $s['id'];
8958 }
8959 $mem[$k]['sum'][] = $newprice;
8960 $mem[$k]['daysused'] += $affdays;
8961 }
8962 }
8963 if ($applyseasons) {
8964 foreach ($mem as $k => $v) {
8965 if ($v['daysused'] > 0 && $v['sum']) {
8966 $newprice = 0;
8967 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8968 $restdays = $arr[$k]['days'] - $v['daysused'];
8969 $addrest = $restdays * $dailyprice;
8970 $newprice += $addrest;
8971 foreach ($v['sum'] as $add) {
8972 $newprice += $add;
8973 }
8974 $arr[$k]['cost'] = $newprice;
8975 $arr[$k]['affdays'] = $v['daysused'];
8976 if (array_key_exists('spids', $v) && $v['spids']) {
8977 $arr[$k]['spids'] = $v['spids'];
8978 }
8979 }
8980 }
8981 }
8982 }
8983 // end week days with no season
8984
8985 return $arr;
8986 }
8987
8988 public static function getRoomRplansClosingDates($idroom)
8989 {
8990 $dbo = JFactory::getDbo();
8991
8992 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8993 $dbo->setQuery($q);
8994 $price_records = $dbo->loadAssocList();
8995 if (!$price_records) {
8996 return [];
8997 }
8998
8999 $closingd = [];
9000 foreach ($price_records as $prec) {
9001 if (empty($prec['closingd'])) {
9002 continue;
9003 }
9004 $price_closing = json_decode($prec['closingd'], true);
9005 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9006 continue;
9007 }
9008 // check expired dates and clean up
9009 $today_midnight = mktime(0, 0, 0);
9010 $cleaned = false;
9011 foreach ($price_closing[$idroom] as $k => $v) {
9012 if (strtotime($v) < $today_midnight) {
9013 $cleaned = true;
9014 unset($price_closing[$idroom][$k]);
9015 }
9016 }
9017 if (!$price_closing[$idroom]) {
9018 unset($price_closing[$idroom]);
9019 } elseif ($cleaned === true) {
9020 // reset array keys for smaller JSON size
9021 $price_closing[$idroom] = array_values($price_closing[$idroom]);
9022 }
9023 if ($cleaned === true) {
9024 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
9025 $dbo->setQuery($q);
9026 $dbo->execute();
9027 }
9028 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9029 continue;
9030 }
9031 $closingd[$prec['id']] = $price_closing[$idroom];
9032 }
9033
9034 return $closingd;
9035 }
9036
9037 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
9038 {
9039 $dbo = JFactory::getDbo();
9040
9041 $closingd = [];
9042
9043 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9044 $dbo->setQuery($q);
9045 $price_records = $dbo->loadAssocList();
9046 if ($price_records && $roomids) {
9047 $info_start = getdate($checkints);
9048 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
9049 $all_nights = [];
9050 for ($i = 0; $i < (int)$numnights; $i++) {
9051 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
9052 $all_nights[] = date('Y-m-d', $next_midnight);
9053 }
9054 foreach ($price_records as $prec) {
9055 if (empty($prec['closingd'])) {
9056 continue;
9057 }
9058 $price_closing = json_decode($prec['closingd'], true);
9059 if (!$price_closing) {
9060 continue;
9061 }
9062 foreach ($price_closing as $idroom => $rclosedd) {
9063 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
9064 continue;
9065 }
9066 if (!array_key_exists($idroom, $closingd)) {
9067 $closingd[$idroom] = [];
9068 }
9069 foreach ($all_nights as $night) {
9070 if (in_array($night, $rclosedd)) {
9071 if (array_key_exists($prec['id'], $closingd[$idroom])) {
9072 $closingd[$idroom][$prec['id']][] = $night;
9073 } else {
9074 $closingd[$idroom][$prec['id']] = [$night];
9075 }
9076 }
9077 }
9078 }
9079 }
9080 }
9081
9082 return $closingd;
9083 }
9084
9085 /**
9086 * Tells whether at least one payment method must be selected to check-out.
9087 *
9088 * @param array $rooms_involved optional list of room IDs being booked.
9089 *
9090 * @return bool true if at least one payment method is applicable.
9091 *
9092 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
9093 */
9094 public static function areTherePayments($rooms_involved = [])
9095 {
9096 $dbo = JFactory::getDbo();
9097
9098 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
9099 $dbo->setQuery($q);
9100 $dbo->execute();
9101
9102 if (!$dbo->getNumRows()) {
9103 // no published payment options found
9104 return false;
9105 }
9106
9107 $pay_options = $dbo->loadAssocList();
9108
9109 if (!count($rooms_involved)) {
9110 // if no filters given, return true
9111 return true;
9112 }
9113
9114 // validate room associations
9115 foreach ($pay_options as $pay_k => $pay_v) {
9116 if (empty($pay_v['idrooms'])) {
9117 continue;
9118 }
9119 $rooms_supported = json_decode($pay_v['idrooms']);
9120 if (!is_array($rooms_supported) || !count($rooms_supported)) {
9121 continue;
9122 }
9123 // make sure all rooms being booked are supported
9124 foreach ($rooms_involved as $room_id_booked) {
9125 if (!in_array($room_id_booked, $rooms_supported)) {
9126 // we need to unset this payment option as soon as we find one non-matching room ID
9127 unset($pay_options[$pay_k]);
9128 break;
9129 }
9130 }
9131 }
9132
9133 return (bool)count($pay_options);
9134 }
9135
9136 public static function getPayment($idp, $vbo_tn = null)
9137 {
9138 if (empty($idp)) {
9139 return false;
9140 }
9141
9142 if (strstr((string)$idp, '=') !== false) {
9143 $parts = explode('=', $idp);
9144 $idp = $parts[0];
9145 }
9146
9147 $dbo = JFactory::getDbo();
9148
9149 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
9150 $dbo->setQuery($q);
9151 $payment = $dbo->loadAssocList();
9152 if (!$payment) {
9153 return false;
9154 }
9155
9156 if (is_object($vbo_tn)) {
9157 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
9158 }
9159
9160 return $payment[0];
9161 }
9162
9163 public static function getCronKey()
9164 {
9165 return VBOFactory::getConfig()->get('cronkey', '');
9166 }
9167
9168 /**
9169 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
9170 *
9171 * @return int
9172 *
9173 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
9174 */
9175 public static function getNextInvoiceNumber()
9176 {
9177 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
9178 }
9179
9180 public static function getInvoiceNumberSuffix()
9181 {
9182 return VBOFactory::getConfig()->get('invoicesuffix', '');
9183 }
9184
9185 public static function getInvoiceCompanyInfo()
9186 {
9187 return VBOFactory::getConfig()->get('invcompanyinfo', '');
9188 }
9189
9190 /**
9191 * Gets the number for the next booking receipt generation,
9192 * updates the last receipt number used for the later iterations,
9193 * stores a new receipt record to keep track of the receipts issued.
9194 *
9195 * @param int $bid the Booking ID for which we are/want to generating the receipt.
9196 * @param [int] $newnum the last number used for generating the receipt.
9197 *
9198 * @return int
9199 */
9200 public static function getNextReceiptNumber ($bid, $newnum = false) {
9201 $dbo = JFactory::getDbo();
9202 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
9203 $dbo->setQuery($q);
9204 $dbo->execute();
9205 if ($dbo->getNumRows() > 0) {
9206 $s = $dbo->loadResult();
9207 //check if this booking has already a receipt, and return that number
9208 $prev_receipt = array();
9209 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
9210 $dbo->setQuery($q);
9211 $dbo->execute();
9212 if ($dbo->getNumRows() > 0) {
9213 $prev_receipt = $dbo->loadAssoc();
9214 }
9215 //update value (receipt generated)
9216 if ($newnum !== false && $newnum > 0) {
9217 $s = (int)$newnum;
9218 if (!(count($prev_receipt) > 0)) {
9219 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
9220 $dbo->setQuery($q);
9221 $dbo->execute();
9222 //insert the new receipt record
9223 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
9224 $dbo->setQuery($q);
9225 $dbo->execute();
9226 } else {
9227 //update receipt record
9228 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
9229 $dbo->setQuery($q);
9230 $dbo->execute();
9231 }
9232 }
9233 //
9234 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
9235 }
9236 //first execution of the method should create the configuration record
9237 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
9238 $dbo->setQuery($q);
9239 $dbo->execute();
9240 return 1;
9241 }
9242
9243 public static function getReceiptNotes ($newnotes = false) {
9244 $dbo = JFactory::getDbo();
9245 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
9246 $dbo->setQuery($q);
9247 $dbo->execute();
9248 if ($dbo->getNumRows() > 0) {
9249 $s = $dbo->loadResult();
9250 //update value
9251 if ($newnotes !== false) {
9252 $s = $newnotes;
9253 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
9254 $dbo->setQuery($q);
9255 $dbo->execute();
9256 }
9257 //
9258 return $s;
9259 }
9260 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
9261 $dbo->setQuery($q);
9262 $dbo->execute();
9263 return "";
9264 }
9265
9266 public static function loadColorTagsRules()
9267 {
9268 $color_tag_rules = [
9269 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
9270 'pend1' => 'VBWAITINGFORPAYMENT',
9271 'conf1' => 'VBDBTEXTROOMCLOSED',
9272 'ovbook' => 'VBO_BTYPE_OVERBOOKING',
9273 'conf2' => 'VBOCOLORTAGRULECONFTWO',
9274 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
9275 'inv1' => 'VBOCOLORTAGRULEINVONE',
9276 'rcp1' => 'VBOCOLORTAGRULERCPONE',
9277 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
9278 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
9279 'inv2' => 'VBOCOLORTAGRULEINVTWO',
9280 ];
9281
9282 /**
9283 * Trigger event to let third-party plugins override the default booking color tag rules.
9284 *
9285 * @since 1.16.4 (J) - 1.6.4 (WP)
9286 */
9287 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
9288
9289 return $color_tag_rules;
9290 }
9291
9292 public static function loadDefaultColorTags()
9293 {
9294 $default_color_tags = [
9295 [
9296 'color' => '#9b9b9b',
9297 'name' => 'VBWAITINGFORPAYMENT',
9298 'rule' => 'pend1',
9299 ],
9300 [
9301 'color' => '#333333',
9302 'name' => 'VBDBTEXTROOMCLOSED',
9303 'rule' => 'conf1',
9304 ],
9305 [
9306 'color' => '#f04848',
9307 'name' => 'VBO_BTYPE_OVERBOOKING',
9308 'rule' => 'ovbook',
9309 ],
9310 [
9311 'color' => '#ff8606',
9312 'name' => 'VBOCOLORTAGRULECONFTWO',
9313 'rule' => 'conf2',
9314 ],
9315 [
9316 'color' => '#0418c9',
9317 'name' => 'VBOCOLORTAGRULECONFTHREE',
9318 'rule' => 'conf3',
9319 ],
9320 [
9321 'color' => '#bed953',
9322 'name' => 'VBOCOLORTAGRULEINVONE',
9323 'rule' => 'inv1',
9324 ],
9325 [
9326 'color' => '#67f5b5',
9327 'name' => 'VBOCOLORTAGRULERCPONE',
9328 'rule' => 'rcp1',
9329 ],
9330 [
9331 'color' => '#04d2c2',
9332 'name' => 'VBOCOLORTAGRULECONFFOUR',
9333 'rule' => 'conf4',
9334 ],
9335 [
9336 'color' => '#00b316',
9337 'name' => 'VBOCOLORTAGRULECONFFIVE',
9338 'rule' => 'conf5',
9339 ],
9340 [
9341 'color' => '#00f323',
9342 'name' => 'VBOCOLORTAGRULEINVTWO',
9343 'rule' => 'inv2',
9344 ],
9345 ];
9346
9347 /**
9348 * Trigger event to let third-party plugins override the default color tags.
9349 *
9350 * @since 1.16.4 (J) - 1.6.4 (WP)
9351 */
9352 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
9353
9354 return $default_color_tags;
9355 }
9356
9357 public static function loadBookingsColorTags()
9358 {
9359 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
9360
9361 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
9362 }
9363
9364 public static function getBestColorContrast($hexcolor)
9365 {
9366 $hexcolor = str_replace('#', '', $hexcolor);
9367
9368 if (empty($hexcolor) || strlen($hexcolor) != 6) {
9369 return '#000000';
9370 }
9371
9372 $r = hexdec(substr($hexcolor, 0, 2));
9373 $g = hexdec(substr($hexcolor, 2, 2));
9374 $b = hexdec(substr($hexcolor, 4, 2));
9375
9376 // Counting the perceptive luminance - human eye favors green color
9377 // < 0.5 bright colors
9378 // > 0.5 dark colors
9379
9380 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
9381 }
9382
9383 public static function applyBookingColorTag($booking, $tags = [])
9384 {
9385 if (!is_array($tags) || !$tags) {
9386 $tags = self::loadBookingsColorTags();
9387 }
9388
9389 if (!empty($booking['colortag'])) {
9390 $color_tag_arr = json_decode($booking['colortag'], true);
9391 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
9392 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
9393 return $color_tag_arr;
9394 }
9395 }
9396
9397 $dbo = JFactory::getDbo();
9398
9399 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
9400 $docs_data = [];
9401 $invoice_numb = false;
9402 $receipt_numb = false;
9403
9404 if ($booking['status'] == 'confirmed' && ($booking['checkin'] ?? 0) > time()) {
9405 $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`
9406 FROM `#__vikbooking_orders` AS `b`
9407 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
9408 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
9409 WHERE `b`.`id`=" . (int)$bid . ";";
9410 $dbo->setQuery($q);
9411 $docs_data = $dbo->loadAssoc();
9412 if ($docs_data) {
9413 $invoice_numb = (!empty($docs_data['inv_id']));
9414 $receipt_numb = (!empty($docs_data['rcp_id']));
9415
9416 /**
9417 * We inject the invoice number or receipt number into the color tag rules.
9418 * This is useful for the Tableaux page to show the invoice/receipt number.
9419 *
9420 * @since 1.12 (J) - 1.1.7 (WP)
9421 */
9422 if ($invoice_numb || $receipt_numb) {
9423 foreach ($tags as &$tval) {
9424 if ($invoice_numb) {
9425 $tval['invoice_number'] = $docs_data['inv_number'];
9426 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
9427 }
9428 if ($receipt_numb) {
9429 $tval['receipt_number'] = $docs_data['rcp_number'];
9430 }
9431 }
9432 // unset last reference
9433 unset($tval);
9434 }
9435 }
9436 }
9437
9438 /**
9439 * Some OTA bookings using an OTA-Collect business model may look as not entirely
9440 * paid when they actually are, due to commissions and pass through taxes. We also
9441 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
9442 *
9443 * @since 1.16.3 (J) - 1.6.3 (WP)
9444 */
9445 $ota_collected_booking = false;
9446 $hotel_collect_vcc_unpaid = false;
9447 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
9448 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
9449 $ota_collected_booking = true;
9450 }
9451 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
9452 // VCC detected, check the history for a payment event
9453 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
9454 // list of history types to fetch
9455 $h_payment_types = [];
9456 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
9457 if ($h_payment_group && $h_payment_group['types']) {
9458 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
9459 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
9460 }
9461 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
9462 if (!$payment_ev_datas) {
9463 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
9464 $hotel_collect_vcc_unpaid = true;
9465 }
9466 }
9467 }
9468
9469 foreach ($tags as $tkey => $tval) {
9470 if (empty($tval['rule'])) {
9471 continue;
9472 }
9473
9474 /**
9475 * Trigger event to let third-party plugins validate the custom rule.
9476 *
9477 * @since 1.16.4 (J) - 1.6.4 (WP)
9478 */
9479 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
9480 if (!empty($custom_rule)) {
9481 return (array) $custom_rule[0];
9482 }
9483
9484 switch ($tval['rule']) {
9485 case 'pend1':
9486 // Room is waiting for the payment (locked record)
9487 if ($booking['status'] == 'standby') {
9488 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
9489 $dbo->setQuery($q);
9490 $tmp_lock = $dbo->loadAssoc();
9491 if ($tmp_lock) {
9492 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9493 return $tval;
9494 }
9495 }
9496 break;
9497 case 'conf1':
9498 // Confirmed (Room Closed)
9499 if ($booking['status'] == 'confirmed' && (!empty($booking['closure']) || $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED'))) {
9500 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9501 return $tval;
9502 }
9503 break;
9504 case 'ovbook':
9505 // Confirmed (Overbooking)
9506 if ($booking['status'] == 'confirmed' && !empty($booking['type']) && $booking['type'] == 'overbooking') {
9507 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9508 return $tval;
9509 }
9510 break;
9511 case 'conf2':
9512 // Confirmed (No Rate 0.00/NULL Total)
9513 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9514 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9515 return $tval;
9516 }
9517 break;
9518 case 'conf3':
9519 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9520 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) {
9521 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9522 return $tval;
9523 }
9524 break;
9525 case 'inv1':
9526 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9527 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9528 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9529 return $tval;
9530 }
9531 break;
9532 case 'rcp1':
9533 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9534 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9535 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9536 return $tval;
9537 }
9538 break;
9539 case 'conf4':
9540 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9541 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9542 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9543 return $tval;
9544 }
9545 break;
9546 case 'conf5':
9547 // Confirmed (Total > 0 && Total Paid >= Total)
9548 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
9549 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9550 return $tval;
9551 }
9552 break;
9553 case 'inv2':
9554 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9555 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9556 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9557 return $tval;
9558 }
9559 break;
9560 default:
9561 // unsupported rule
9562 break;
9563 }
9564 }
9565
9566 return [];
9567 }
9568
9569 public static function getBookingInfoFromID($bid)
9570 {
9571 $dbo = JFactory::getDbo();
9572
9573 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9574 $dbo->setQuery($q);
9575 $booking_info = $dbo->loadAssoc();
9576 if ($booking_info) {
9577 return $booking_info;
9578 }
9579
9580 return [];
9581 }
9582
9583 /**
9584 * Load booking details for a given list of IDs.
9585 *
9586 * @param int $roomid The room integer number.
9587 * @param array $room_bids_pool List of booking IDs involved.
9588 * @param bool $unassigned Whether to return also the unassigned bookings.
9589 *
9590 * @return array Associative list of sub-unit bookings, or list containing also the unassigned bookings.
9591 *
9592 * @since 1.18.2 (J) - 1.8.2 (WP) added argument $unassigned.
9593 */
9594 public static function loadRoomIndexesBookings(int $roomid, array $room_bids_pool, $unassigned = false)
9595 {
9596 $dbo = JFactory::getDbo();
9597
9598 $pool_values = [];
9599 if ($unassigned) {
9600 // prepare the default return value
9601 $pool_values = [
9602 [],
9603 [],
9604 ];
9605 }
9606
9607 if (empty($roomid) || !$room_bids_pool) {
9608 return $pool_values;
9609 }
9610
9611 $room_features_bookings = [];
9612 $unassigned_bookings = [];
9613
9614 $all_bids = [];
9615 foreach ($room_bids_pool as $day => $bids) {
9616 $all_bids = array_merge($all_bids, $bids);
9617 }
9618 $all_bids = array_map('intval', array_unique($all_bids));
9619 $all_bids_str = implode(', ', $all_bids);
9620
9621 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9622 $dbo->setQuery($q);
9623 $rbookings = $dbo->loadAssocList();
9624 if (!$rbookings) {
9625 return $pool_values;
9626 }
9627
9628 foreach ($rbookings as $k => $v) {
9629 if (empty($v['roomindex'])) {
9630 if ($unassigned) {
9631 // push this booking to the list of unassigned reservations
9632 $unassigned_bookings[] = $v['idorder'];
9633 }
9634 // do not proceed further
9635 continue;
9636 }
9637 if (!isset($room_features_bookings[$v['roomindex']])) {
9638 $room_features_bookings[$v['roomindex']] = [];
9639 }
9640 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9641 }
9642
9643 if ($unassigned) {
9644 // return a list of information
9645 return [
9646 $room_features_bookings,
9647 $unassigned_bookings,
9648 ];
9649 }
9650
9651 // return just the list of room sub-unit bookings
9652 return $room_features_bookings;
9653 }
9654
9655 public static function getSendEmailWhen()
9656 {
9657 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9658 }
9659
9660 public static function getMinutesAutoRemove()
9661 {
9662 static $minAutoRemove = null;
9663
9664 if ($minAutoRemove !== null) {
9665 return $minAutoRemove;
9666 }
9667
9668 $minAutoRemove = VBOFactory::getConfig()->getInt('minautoremove', 0);
9669
9670 return $minAutoRemove;
9671 }
9672
9673 /**
9674 * Returns the currently active SMS driver handle.
9675 *
9676 * @return string
9677 *
9678 * @since 1.18.8 (J) - 1.8.8 (WP) removed support for WhatsApp free-text messages due to Meta
9679 * business template requirements - use the apposite E4jConnect
9680 * WhatsApp Business Messaging service with Vik Channel Manager.
9681 */
9682 public static function getSMSAPIClass()
9683 {
9684 $smsApiValue = VBOFactory::getConfig()->getString('smsapi', '');
9685
9686 if (preg_match('/^whatsapp/i', $smsApiValue)) {
9687 // no longer supported
9688 $smsApiValue = '';
9689 }
9690
9691 return $smsApiValue;
9692 }
9693
9694 public static function autoSendSMSEnabled()
9695 {
9696 return VBOFactory::getConfig()->getBool('smsautosend', false);
9697 }
9698
9699 public static function getSendSMSTo()
9700 {
9701 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9702 }
9703
9704 public static function getSendSMSWhen()
9705 {
9706 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9707 }
9708
9709 public static function getSMSAdminPhone()
9710 {
9711 return VBOFactory::getConfig()->getString('smsadminphone', '');
9712 }
9713
9714 public static function getSMSParams($as_array = true)
9715 {
9716 if (!$as_array) {
9717 return VBOFactory::getConfig()->getString('smsparams', '');
9718 }
9719
9720 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9721 }
9722
9723 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9724 $dbo = JFactory::getDbo();
9725 switch (strtolower($booking_status)) {
9726 case 'standby':
9727 $status = 'pend';
9728 break;
9729 case 'cancelled':
9730 $status = 'canc';
9731 break;
9732 default:
9733 $status = '';
9734 break;
9735 }
9736 $paramtype = 'sms'.$type.'tpl'.$status;
9737 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9738 $dbo->setQuery($q);
9739 $dbo->execute();
9740 if ($dbo->getNumRows() < 1) {
9741 if ($status == 'canc') {
9742 //Type cancelled is used by VCM since v1.6.6
9743 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9744 $dbo->setQuery($q);
9745 $dbo->execute();
9746 }
9747 return '';
9748 }
9749 $ft = $dbo->loadAssocList();
9750 if (is_object($vbo_tn)) {
9751 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9752 }
9753 return $ft[0]['setting'];
9754 }
9755
9756 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9757 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9758 }
9759
9760 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9761 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9762 }
9763
9764 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9765 $dbo = JFactory::getDbo();
9766 $phone = str_replace(" ", '', trim($phone));
9767 $cprefix = '';
9768 if (!empty($country_threecode)) {
9769 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9770 $dbo->setQuery($q);
9771 $dbo->execute();
9772 if ($dbo->getNumRows() > 0) {
9773 $cprefix = $dbo->loadResult();
9774 $cprefix = str_replace(" ", '', trim($cprefix));
9775 }
9776 }
9777 if (!empty($cprefix)) {
9778 if (substr($phone, 0, 1) != '+') {
9779 if (substr($phone, 0, 2) == '00') {
9780 $phone = '+'.substr($phone, 2);
9781 } else {
9782 $phone = $cprefix.$phone;
9783 }
9784 }
9785 }
9786 return $phone;
9787 }
9788
9789 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9790 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9791
9792 /**
9793 * Trigger event to allow third-party plugins to manipulate the template string.
9794 *
9795 * @since 1.16.10 (J) - 1.6.10 (WP)
9796 */
9797 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9798
9799 /**
9800 * Parse all conditional text rules.
9801 *
9802 * @since 1.14 (J) - 1.4.0 (WP)
9803 */
9804 self::getConditionalRulesInstance()
9805 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9806 ->parseTokens($tpl);
9807
9808 /**
9809 * Parse all Door Access Control tags.
9810 *
9811 * @since 1.18.4 (J) - 1.8.4 (WP)
9812 */
9813 VBOFactory::getDoorAccessControl()
9814 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9815
9816 $vbo_df = self::getDateFormat();
9817 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9818 $datesep = self::getDateSeparator();
9819
9820 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9821 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9822 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9823 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9824 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9825 if (!empty($booking['confirmnumber'])) {
9826 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9827 } else {
9828 $tpl = str_replace("{confirmnumb}", '', $tpl);
9829 }
9830 if (!empty($booking['idorderota'])) {
9831 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9832 } else {
9833 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9834 }
9835
9836 $rooms_booked = array();
9837 $rooms_names = array();
9838 $tot_adults = 0;
9839 $tot_children = 0;
9840 $tot_guests = 0;
9841 foreach ($booking_rooms as $broom) {
9842 $rooms_names[] = $broom['room_name'];
9843 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9844 $rooms_booked[$broom['room_name']] += 1;
9845 } else {
9846 $rooms_booked[$broom['room_name']] = 1;
9847 }
9848 $tot_adults += (int)$broom['adults'];
9849 $tot_children += (int)$broom['children'];
9850 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9851 }
9852 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9853 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9854 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9855 $rooms_booked_quant = array();
9856 foreach ($rooms_booked as $rname => $quant) {
9857 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9858 }
9859 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9860 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9861 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9862 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9863 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9864 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9865 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9866 $remaining_bal = $booking['total'] - $booking['totpaid'];
9867 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9868
9869 return $tpl;
9870 }
9871
9872 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9873 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9874
9875 /**
9876 * Trigger event to allow third-party plugins to manipulate the template string.
9877 *
9878 * @since 1.16.10 (J) - 1.6.10 (WP)
9879 */
9880 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9881
9882 /**
9883 * Parse all conditional text rules.
9884 *
9885 * @since 1.14 (J) - 1.4.0 (WP)
9886 */
9887 self::getConditionalRulesInstance()
9888 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9889 ->parseTokens($tpl);
9890
9891 /**
9892 * Parse all Door Access Control tags.
9893 *
9894 * @since 1.18.4 (J) - 1.8.4 (WP)
9895 */
9896 VBOFactory::getDoorAccessControl()
9897 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9898
9899 $vbo_df = self::getDateFormat();
9900 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9901 $datesep = self::getDateSeparator();
9902
9903 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9904 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9905 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9906 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9907 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9908 if (!empty($booking['confirmnumber'])) {
9909 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9910 } else {
9911 $tpl = str_replace("{confirmnumb}", '', $tpl);
9912 }
9913 if (!empty($booking['idorderota'])) {
9914 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9915 } else {
9916 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9917 }
9918
9919 $rooms_booked = array();
9920 $rooms_names = array();
9921 $tot_adults = 0;
9922 $tot_children = 0;
9923 $tot_guests = 0;
9924 foreach ($booking_rooms as $broom) {
9925 $rooms_names[] = $broom['room_name'];
9926 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9927 $rooms_booked[$broom['room_name']] += 1;
9928 } else {
9929 $rooms_booked[$broom['room_name']] = 1;
9930 }
9931 $tot_adults += (int)$broom['adults'];
9932 $tot_children += (int)$broom['children'];
9933 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9934 }
9935 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9936 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9937 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9938 $rooms_booked_quant = array();
9939 foreach ($rooms_booked as $rname => $quant) {
9940 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9941 }
9942 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9943 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9944 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9945 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9946 $remaining_bal = $booking['total'] - $booking['totpaid'];
9947 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9948 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9949
9950 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9951 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9952
9953 if (VBOPlatformDetection::isWordPress()) {
9954 /**
9955 * @wponly Rewrite URI for front-end
9956 */
9957 $book_link = str_replace(JUri::root(), '', $book_link);
9958 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9959 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9960 if ($itemid) {
9961 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9962 }
9963 } else {
9964 /**
9965 * @joomlaonly Rewrite URI for front-end
9966 */
9967 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9968 $uri_parts = [
9969 'option' => 'com_vikbooking',
9970 'view' => 'booking',
9971 'sid' => $use_sid,
9972 'ts' => $booking['ts'],
9973 ];
9974 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9975 // inject lang in query string for proper link routing
9976 $uri_parts['lang'] = $booking['lang'];
9977 }
9978 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9979 }
9980
9981 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9982 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9983 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9984
9985 return $tpl;
9986 }
9987
9988 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9989 $dbo = JFactory::getDbo();
9990 if (!class_exists('VboApplication')) {
9991 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9992 }
9993 $vbo_app = new VboApplication;
9994 if (empty($oid)) {
9995 return false;
9996 }
9997 $sms_api = self::getSMSAPIClass();
9998 if (empty($sms_api)) {
9999 return false;
10000 }
10001 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
10002 return false;
10003 }
10004 $sms_api_params = self::getSMSParams();
10005 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
10006 return false;
10007 }
10008 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
10009 return false;
10010 }
10011 $send_sms_to = self::getSendSMSTo();
10012 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
10013 return false;
10014 }
10015 $booking = array();
10016 $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.";";
10017 $dbo->setQuery($q);
10018 $dbo->execute();
10019 if ($dbo->getNumRows() > 0) {
10020 $booking = $dbo->loadAssoc();
10021 }
10022 if (!(count($booking) > 0)) {
10023 return false;
10024 }
10025 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
10026 return false;
10027 }
10028 $booking_rooms = array();
10029 $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'].";";
10030 $dbo->setQuery($q);
10031 $dbo->execute();
10032 if ($dbo->getNumRows() > 0) {
10033 $booking_rooms = $dbo->loadAssocList();
10034 }
10035 $admin_phone = self::getSMSAdminPhone();
10036 $admin_sendermail = self::getSenderMail();
10037 $admin_email = self::getAdminMail();
10038 $f_result = false;
10039 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
10040 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
10041 //SMS for the administrator
10042 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
10043 if (!empty($sms_text)) {
10044 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10045 //administrator phone can contain multiple numbers separated by comma or semicolon
10046 $admin_phones = array();
10047 if (strpos($admin_phone, ',') !== false) {
10048 $all_phones = explode(',', $admin_phone);
10049 foreach ($all_phones as $aph) {
10050 if (!empty($aph)) {
10051 $admin_phones[] = trim($aph);
10052 }
10053 }
10054 } elseif (strpos($admin_phone, ';') !== false) {
10055 $all_phones = explode(';', $admin_phone);
10056 foreach ($all_phones as $aph) {
10057 if (!empty($aph)) {
10058 $admin_phones[] = trim($aph);
10059 }
10060 }
10061 } else {
10062 $admin_phones[] = $admin_phone;
10063 }
10064 foreach ($admin_phones as $admphone) {
10065 $response_obj = $sms_obj->sendMessage($admphone, strip_tags($sms_text));
10066 if ( !$sms_obj->validateResponse($response_obj) ) {
10067 //notify the administrator via email with the error of the SMS sending
10068 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10069 } else {
10070 $f_result = true;
10071 }
10072 }
10073 }
10074 }
10075 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
10076 //SMS for the Customer
10077 $vbo_tn = self::getTranslator();
10078 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
10079 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
10080 if (!empty($sms_text)) {
10081 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10082 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
10083 if ( !$sms_obj->validateResponse($response_obj) ) {
10084 //notify the administrator via email with the error of the SMS sending
10085 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10086 } else {
10087 $f_result = true;
10088 }
10089 }
10090 }
10091 return $f_result;
10092 }
10093
10094 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
10095 {
10096 if (!defined('_VIKBOOKINGEXEC')) {
10097 define('_VIKBOOKINGEXEC', '1');
10098 }
10099
10100 ob_start();
10101 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
10102 $content = ob_get_contents();
10103 ob_end_clean();
10104
10105 $default_params = array(
10106 'show_header' => 0,
10107 'header_data' => array(),
10108 'show_footer' => 0,
10109 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10110 'pdf_unit' => 'PDF_UNIT',
10111 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10112 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10113 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10114 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10115 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10116 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10117 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10118 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10119 'header_font_size' => '10',
10120 'body_font_size' => '10',
10121 'footer_font_size' => '8',
10122 'show_lines_taxrate_col' => 0,
10123 );
10124
10125 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
10126 $default_params = array_merge($default_params, $invoice_params);
10127 }
10128
10129 return array($content, $default_params);
10130 }
10131
10132 /**
10133 * Includes within a buffer the template file for the custom (manual) invoice.
10134 *
10135 * @param array $invoice the record of the custom invoice
10136 * @param array $customer the customer record
10137 *
10138 * @return string the HTML content of the template file
10139 *
10140 * @since 1.11.1
10141 */
10142 public static function loadCustomInvoiceTmpl($invoice, $customer) {
10143 if (!defined('_VIKBOOKINGEXEC')) {
10144 define('_VIKBOOKINGEXEC', '1');
10145 }
10146 ob_start();
10147 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
10148 $content = ob_get_contents();
10149 ob_end_clean();
10150 $default_params = array(
10151 'show_header' => 0,
10152 'header_data' => array(),
10153 'show_footer' => 0,
10154 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10155 'pdf_unit' => 'PDF_UNIT',
10156 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10157 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10158 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10159 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10160 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10161 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10162 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10163 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10164 'header_font_size' => '10',
10165 'body_font_size' => '10',
10166 'footer_font_size' => '8'
10167 );
10168 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
10169 $default_params = array_merge($default_params, $invoice_params);
10170 }
10171 return array($content, $default_params);
10172 }
10173
10174 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
10175 {
10176 // clone the original template file source
10177 $parsed = $invoicetpl;
10178
10179 /**
10180 * Parse all conditional text rules.
10181 *
10182 * @since 1.14 (J) - 1.4.0 (WP)
10183 */
10184 self::getConditionalRulesInstance()
10185 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
10186 ->parseTokens($parsed);
10187
10188 $dbo = JFactory::getDbo();
10189 if (is_null($vbo_tn)) {
10190 $vbo_tn = self::getTranslator();
10191 }
10192 // availability helper
10193 $av_helper = self::getAvailabilityInstance();
10194
10195 $nowdf = self::getDateFormat();
10196 if ($nowdf == "%d/%m/%Y") {
10197 $df = 'd/m/Y';
10198 } elseif ($nowdf == "%m/%d/%Y") {
10199 $df = 'm/d/Y';
10200 } else {
10201 $df = 'Y/m/d';
10202 }
10203 $datesep = self::getDateSeparator();
10204
10205 /**
10206 * Split stay reservation.
10207 *
10208 * @since 1.16.0 (J) - 1.6.0 (WP)
10209 */
10210 $room_stay_dates = [];
10211 if ($booking['split_stay']) {
10212 if ($booking['status'] == 'confirmed') {
10213 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
10214 } else {
10215 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
10216 }
10217 // immediately count the number of nights of stay for each split room
10218 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
10219 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
10220 // overwrite values for compatibility with non-confirmed bookings
10221 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
10222 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
10223 }
10224 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
10225 // overwrite the whole array
10226 $room_stay_dates[$sps_r_k] = $sps_r_v;
10227 }
10228 }
10229
10230 $companylogo = self::getSiteLogo();
10231 $uselogo = '';
10232 if (!empty($companylogo)) {
10233 /**
10234 * Let's try to prevent TCPDF errors.
10235 *
10236 * @since August 2nd 2019
10237 */
10238 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10239 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10240 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10241 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10242 }
10243 }
10244 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10245 $parsed = str_replace("{company_info}", $company_info, $parsed);
10246 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
10247 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
10248 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
10249 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10250
10251 // custom fields replacement
10252 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10253 if (is_array($cmatches[1]) && $cmatches[1]) {
10254 $cfids = array();
10255 foreach ($cmatches[1] as $cfid) {
10256 $cfids[] = $cfid;
10257 }
10258 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10259 $dbo->setQuery($q);
10260 $cfields = $dbo->loadAssocList();
10261 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10262 $cfmap = array();
10263 foreach ($cfields as $cf) {
10264 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10265 }
10266 $cfmapreplace = array();
10267 $partsreceived = explode("\n", $booking['custdata']);
10268 if (count($partsreceived) > 0) {
10269 foreach ($partsreceived as $pst) {
10270 if (!empty($pst)) {
10271 $tmpdata = explode(":", $pst);
10272 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10273 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10274 }
10275 }
10276 }
10277 }
10278 foreach ($cmatches[1] as $cfid) {
10279 if (array_key_exists($cfid, $cfmapreplace)) {
10280 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10281 } else {
10282 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10283 }
10284 }
10285 }
10286
10287 // invoice price description - Start
10288 $rooms = array();
10289 $tars = array();
10290 $arrpeople = array();
10291 $is_package = !empty($booking['pkg']) ? true : false;
10292 $tot_adults = 0;
10293 $tot_children = 0;
10294 $tot_guests = 0;
10295 foreach ($booking_rooms as $kor => $or) {
10296 $num = $kor + 1;
10297 $rooms[$num] = $or;
10298
10299 // determine the days to consider for the count of the availability
10300 $room_nights = $booking['days'];
10301 $room_checkin = $booking['checkin'];
10302 $room_checkout = $booking['checkout'];
10303 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10304 $room_nights = $room_stay_dates[$kor]['nights'];
10305 $room_checkin = $room_stay_dates[$kor]['checkin'];
10306 $room_checkout = $room_stay_dates[$kor]['checkout'];
10307 }
10308
10309 $arrpeople[$num]['adults'] = $or['adults'];
10310 $arrpeople[$num]['children'] = $or['children'];
10311 $tot_adults += $or['adults'];
10312 $tot_children += $or['children'];
10313 $tot_guests += ($or['adults'] + $or['children']);
10314 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10315 //package or custom cost set from the back-end
10316 continue;
10317 }
10318 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
10319 $dbo->setQuery($q);
10320 $tar = $dbo->loadAssocList();
10321 if ($tar) {
10322 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
10323 //different usage
10324 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
10325 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
10326 //Occupancy Override
10327 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
10328 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
10329 //
10330 if (is_array($diffusageprice)) {
10331 //set a charge or discount to the price(s) for the different usage of the room
10332 foreach ($tar as $kpr => $vpr) {
10333 $tar[$kpr]['diffusage'] = $or['adults'];
10334 if ($diffusageprice['chdisc'] == 1) {
10335 //charge
10336 if ($diffusageprice['valpcent'] == 1) {
10337 //fixed value
10338 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10339 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10340 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
10341 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10342 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
10343 } else {
10344 //percentage value
10345 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10346 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
10347 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
10348 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10349 $tar[$kpr]['cost'] = $aduseval;
10350 }
10351 } else {
10352 //discount
10353 if ($diffusageprice['valpcent'] == 1) {
10354 //fixed value
10355 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10356 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10357 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
10358 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10359 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
10360 } else {
10361 //percentage value
10362 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10363 $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);
10364 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
10365 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10366 $tar[$kpr]['cost'] = $aduseval;
10367 }
10368 }
10369 }
10370 }
10371 }
10372 //
10373 $tars[$num] = $tar[0];
10374 }
10375 }
10376 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10377 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10378 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10379 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10380 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10381 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10382
10383 $isdue = 0;
10384 $tot_taxes = 0;
10385 $tot_city_taxes = 0;
10386 $tot_fees = 0;
10387 $pricestr = array();
10388 $optstr = array();
10389 // start building the tax summary
10390 VBOTaxonomySummary::start();
10391 foreach ($booking_rooms as $kor => $or) {
10392 $num = $kor + 1;
10393
10394 // determine the days to consider for the count of the availability
10395 $room_nights = $booking['days'];
10396 $room_checkin = $booking['checkin'];
10397 $room_checkout = $booking['checkout'];
10398 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10399 $room_nights = $room_stay_dates[$kor]['nights'];
10400 $room_checkin = $room_stay_dates[$kor]['checkin'];
10401 $room_checkout = $room_stay_dates[$kor]['checkout'];
10402 }
10403
10404 $pricestr[$num] = array();
10405 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10406 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
10407 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
10408 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
10409 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
10410 $pricestr[$num]['tot'] = $calctar;
10411 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
10412 $tot_taxes += ($calctar - $cost_minus_tax);
10413 $isdue += $calctar;
10414 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
10415 $pricestr[$num]['tax_rate'] = $tax_rate;
10416 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
10417 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10418 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
10419 $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'] : "");
10420 $pricestr[$num]['tot'] = $calctar;
10421 $tars[$num]['calctar'] = $calctar;
10422 $isdue += $calctar;
10423 if ($calctar == $display_rate) {
10424 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
10425 $tot_taxes += ($display_rate - $cost_minus_tax);
10426 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
10427 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
10428 $pricestr[$num]['tax_rate'] = $tax_rate;
10429 } else {
10430 $tot_taxes += ($calctar - $display_rate);
10431 $pricestr[$num]['tax'] = ($calctar - $display_rate);
10432 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
10433 $pricestr[$num]['tax_rate'] = $tax_rate;
10434 }
10435 }
10436 $optstr[$num] = array();
10437 $opt_ind = 0;
10438 if (!empty($or['optionals'])) {
10439 $stepo = explode(";", $or['optionals']);
10440 foreach ($stepo as $roptkey => $oo) {
10441 if (!empty($oo)) {
10442 $stept = explode(":", $oo);
10443 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
10444 $dbo->setQuery($q);
10445 $actopt = $dbo->loadAssocList();
10446 if ($actopt) {
10447 if (is_object($vbo_tn)) {
10448 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
10449 }
10450 $optstr[$num][$opt_ind] = array();
10451 $chvar = '';
10452 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
10453 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
10454 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
10455 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
10456 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
10457 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
10458 $agestept = explode('-', $stept[1]);
10459 $stept[1] = $agestept[0];
10460 $chvar = $agestept[1];
10461 $realcost = 0;
10462 if (!empty($chvar)) {
10463 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
10464 //percentage value of the adults tariff
10465 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10466 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10467 } else {
10468 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10469 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10470 }
10471 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
10472 //VBO 1.10 - percentage value of room base cost
10473 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10474 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10475 } else {
10476 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
10477 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10478 }
10479 }
10480 $actopt[0]['chageintv'] = $chvar;
10481 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
10482 $actopt[0]['quan'] = $stept[1];
10483 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
10484 }
10485 } else {
10486 $actopt[0]['quan'] = $stept[1];
10487 // VBO 1.11 - options percentage cost of the room total fee
10488 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10489 $deftar_basecosts = $or['cust_cost'];
10490 } else {
10491 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10492 }
10493 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
10494 //
10495 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
10496 }
10497 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
10498 $realcost = $actopt[0]['maxprice'];
10499 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
10500 $realcost = $actopt[0]['maxprice'] * $stept[1];
10501 }
10502 }
10503 if ($actopt[0]['perperson'] == 1) {
10504 $realcost = $realcost * $or['adults'];
10505 }
10506
10507 /**
10508 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
10509 *
10510 * @since 1.17.7 (J) - 1.7.7 (WP)
10511 */
10512 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
10513 if ($custom_calculation) {
10514 $realcost = (float) $custom_calculation[0];
10515 }
10516
10517 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
10518 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
10519 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
10520 $optstr[$num][$opt_ind]['tax'] = 0;
10521 if ($actopt[0]['is_citytax'] == 1) {
10522 $tot_city_taxes += $tmpopr;
10523 } elseif ($actopt[0]['is_fee'] == 1) {
10524 $tot_fees += $tmpopr;
10525 }
10526 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
10527 if ($tmpopr == $realcost) {
10528 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
10529 $tot_taxes += ($realcost - $opt_minus_tax);
10530 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
10531 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
10532 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10533 } else {
10534 $tot_taxes += ($tmpopr - $realcost);
10535 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
10536 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
10537 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10538 }
10539 //
10540 $opt_ind++;
10541 $isdue += $tmpopr;
10542 }
10543 }
10544 }
10545 }
10546
10547 // custom extra costs
10548 if (!empty($or['extracosts'])) {
10549 $cur_extra_costs = json_decode($or['extracosts'], true);
10550 foreach ($cur_extra_costs as $eck => $ecv) {
10551 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10552 $isdue += $ecplustax;
10553 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
10554 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
10555 $optstr[$num][$opt_ind]['tax'] = 0;
10556 if ($ecplustax == $ecv['cost']) {
10557 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10558 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
10559 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
10560 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
10561 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10562 } else {
10563 $tot_taxes += ($ecplustax - $ecv['cost']);
10564 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
10565 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
10566 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10567 }
10568 $opt_ind++;
10569 }
10570 }
10571 }
10572
10573 $usedcoupon = false;
10574 if (strlen($booking['coupon']) > 0) {
10575 $orig_isdue = $isdue;
10576 $expcoupon = explode(";", $booking['coupon']);
10577 $usedcoupon = $expcoupon;
10578 $isdue = $isdue - (float)$expcoupon[1];
10579 if ($isdue != $orig_isdue) {
10580 //lower taxes proportionally
10581 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10582 }
10583 }
10584 if ($booking['refund'] > 0) {
10585 $orig_isdue = $isdue;
10586 $isdue -= $booking['refund'];
10587 if ($isdue != $orig_isdue) {
10588 //lower taxes proportionally
10589 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10590 }
10591 }
10592 $rows_written = 0;
10593 $inv_rows = '';
10594 foreach ($pricestr as $num => $price_descr) {
10595 // add room split stay information, if any
10596 $split_stay_str = '';
10597 $kor = $num - 1;
10598 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10599 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10600 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10601 }
10602
10603 $inv_rows .= '<tr>'."\n";
10604 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10605 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($price_descr['tot'] - $price_descr['tax'])), $booking['currencyname']) . '</td>'."\n";
10606 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tax']), $booking['currencyname']) . '</td>'."\n";
10607 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tot']), $booking['currencyname']) . '</td>'."\n";
10608 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10609 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10610 }
10611 $inv_rows .= '</tr>'."\n";
10612 $rows_written++;
10613 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10614 foreach ($optstr[$num] as $optk => $optv) {
10615 $inv_rows .= '<tr>'."\n";
10616 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10617 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($optv['tot'] - $optv['tax'])), $booking['currencyname']) . '</td>'."\n";
10618 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tax']), $booking['currencyname']) . '</td>'."\n";
10619 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tot']), $booking['currencyname']) . '</td>'."\n";
10620 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10621 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10622 }
10623 $inv_rows .= '</tr>'."\n";
10624 $rows_written++;
10625 }
10626 }
10627 }
10628
10629 // if discount print row
10630 if ($usedcoupon !== false) {
10631 $inv_rows .= '<tr>'."\n";
10632 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10633 $inv_rows .= '</tr>'."\n";
10634 $inv_rows .= '<tr>'."\n";
10635 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10636 $inv_rows .= '<td></td>'."\n";
10637 $inv_rows .= '<td></td>'."\n";
10638 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($usedcoupon[1]), $booking['currencyname']) . '</td>'."\n";
10639 $inv_rows .= '</tr>'."\n";
10640 $rows_written += 2;
10641 }
10642 // if refunded amount, print row
10643 if ($booking['refund'] > 0) {
10644 $inv_rows .= '<tr>'."\n";
10645 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10646 $inv_rows .= '</tr>'."\n";
10647 $inv_rows .= '<tr>'."\n";
10648 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10649 $inv_rows .= '<td></td>'."\n";
10650 $inv_rows .= '<td></td>'."\n";
10651 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($booking['refund']), $booking['currencyname']) . '</td>'."\n";
10652 $inv_rows .= '</tr>'."\n";
10653 $rows_written += 2;
10654 }
10655 //
10656 $min_records = 8;
10657 if ($rows_written < $min_records) {
10658 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10659 $inv_rows .= '<tr>'."\n";
10660 $inv_rows .= '<td></td>'."\n";
10661 $inv_rows .= '<td></td>'."\n";
10662 $inv_rows .= '<td></td>'."\n";
10663 $inv_rows .= '</tr>'."\n";
10664 }
10665 }
10666 //invoice price description - End
10667 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10668 $parsed = str_replace("{invoice_totalnet}", self::formatCurrencyNumber(self::numberFormat(($isdue - $tot_taxes)), $booking['currencyname']), $parsed);
10669 $parsed = str_replace("{invoice_totaltax}", self::formatCurrencyNumber(self::numberFormat($tot_taxes), $booking['currencyname']), $parsed);
10670 $parsed = str_replace("{invoice_grandtotal}", self::formatCurrencyNumber(self::numberFormat($isdue), $booking['currencyname']), $parsed);
10671 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10672
10673 // invoice tax summary
10674 $tax_summary = VBOTaxonomySummary::get();
10675 $tax_names = VBOTaxonomySummary::getNames();
10676 $taxsum_rows = "";
10677 foreach ($tax_summary as $tax_rate => $tax_amount) {
10678 if (!$tax_rate && !$tax_amount) {
10679 continue;
10680 }
10681 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10682 $taxsum_rows .= '<tr>' . "\n";
10683 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10684 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10685 $taxsum_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($tax_amount), $booking['currencyname']) . '</td>' . "\n";
10686 $taxsum_rows .= '</tr>' . "\n";
10687 }
10688 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10689
10690 return $parsed;
10691 }
10692
10693 /**
10694 * Specific method for parsing the template file for the custom (manual) invoices.
10695 *
10696 * @param string $invoicetpl the plain custom invoice template file before parsing
10697 * @param array $invoice the invoice record
10698 * @param array $customer the customer record
10699 *
10700 * @return string the HTML content of the parsed custom invoice template
10701 *
10702 * @since 1.11.1
10703 */
10704 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10705 $nowdf = self::getDateFormat(true);
10706 if ($nowdf == "%d/%m/%Y") {
10707 $df = 'd/m/Y';
10708 } elseif ($nowdf == "%m/%d/%Y") {
10709 $df = 'm/d/Y';
10710 } else {
10711 $df = 'Y/m/d';
10712 }
10713 $datesep = self::getDateSeparator(true);
10714 $companylogo = self::getSiteLogo();
10715 $uselogo = '';
10716 if (!empty($companylogo)) {
10717 /**
10718 * Let's try to prevent TCPDF errors by checking if the file exists.
10719 *
10720 * @since August 2nd 2019
10721 */
10722 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10723 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10724 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10725 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10726 }
10727 }
10728 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10729 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10730 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10731 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10732 // customer information
10733 $custinfo = '';
10734 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10735 $custinfo .= $customer['email'] . "\n";
10736 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10737 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10738 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10739 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10740 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10741 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10742 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10743 // invoice notes
10744 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10745
10746 return $invoicetpl;
10747 }
10748
10749 /**
10750 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10751 *
10752 * @param array $booking the booking information.
10753 * @param int $invoice_num the invoice number.
10754 * @param string $invoice_suff the invoice suffix.
10755 * @param string $invoice_date the invoice date.
10756 * @param string $company_info optional company information.
10757 * @param bool $translate whether the invoice contents should be translated in the booking language.
10758 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10759 *
10760 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10761 *
10762 * @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.
10763 */
10764 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10765 {
10766 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10767 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10768 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10769
10770 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10771 return false;
10772 }
10773
10774 /**
10775 * Trigger event to allow third party plugins to override some values for the invoice generation.
10776 *
10777 * @since 1.16.3 (J) - 1.6.3 (WP)
10778 */
10779 $event_args = [
10780 'invoice_num' => $invoice_num,
10781 'invoice_suff' => $invoice_suff,
10782 'invoice_date' => $invoice_date,
10783 'company_info' => $company_info,
10784 'translate' => $translate,
10785 ];
10786 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10787 if (is_array($event_args) && $event_args) {
10788 // extract the new values to be used
10789 extract($event_args);
10790 }
10791
10792 $dbo = JFactory::getDbo();
10793 $vbo_tn = self::getTranslator();
10794
10795 // inject the currency name value
10796 $currencyname = self::getCurrencyName();
10797 $booking['currencyname'] = $currencyname;
10798
10799 // make sure we've got a customer ID assigned to the reservation record
10800 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10801 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10802 if ($booking_customer) {
10803 $booking['idcustomer'] = $booking_customer['id'];
10804 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10805 $booking['customer_pin'] = $booking_customer['pin'];
10806 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10807 }
10808 }
10809
10810 $nowdf = self::getDateFormat(true);
10811 if ($nowdf == "%d/%m/%Y") {
10812 $df = 'd/m/Y';
10813 } elseif ($nowdf == "%m/%d/%Y") {
10814 $df = 'm/d/Y';
10815 } else {
10816 $df = 'Y/m/d';
10817 }
10818 $datesep = self::getDateSeparator(true);
10819
10820 if (empty($invoice_date)) {
10821 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10822 $used_date = $booking['ts'];
10823 } else {
10824 /**
10825 * We could be re-generating an invoice for a booking that already had an invoice.
10826 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10827 * It is now possible to keep the same invoice date as the previous one, so check
10828 * what value contains $invoice_date to see if it's different from today's date.
10829 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10830 * to also check the length of the string $invoice_date before using that date.
10831 *
10832 * @since 1.10 - August 2018
10833 */
10834 $base_ts = time();
10835 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10836 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10837 }
10838 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10839 $used_date = $base_ts;
10840 }
10841
10842 // load room reservation records
10843 $q = $dbo->getQuery(true)
10844 ->select($dbo->qn('or') . '.*')
10845 ->select($dbo->qn('r.name', 'room_name'))
10846 ->select($dbo->qn('r.fromadult'))
10847 ->select($dbo->qn('r.toadult'))
10848 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10849 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10850 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10851 ->order($dbo->qn('or.id') . ' ASC');
10852
10853 $dbo->setQuery($q);
10854 $booking_rooms = $dbo->loadAssocList();
10855
10856 if (!$booking_rooms) {
10857 return false;
10858 }
10859
10860 // check if an invoice for this reservation exists
10861 $invoice_id = 0;
10862 $dbo->setQuery(
10863 $dbo->getQuery(true)
10864 ->select('*')
10865 ->from($dbo->qn('#__vikbooking_invoices'))
10866 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10867 ->order($dbo->qn('id') . ' DESC')
10868 );
10869 $invoice_data = $dbo->loadAssoc();
10870 if ($invoice_data) {
10871 $invoice_id = $invoice_data['id'];
10872 }
10873
10874 // if requested, prepare data to refresh the PDF invoice
10875 if ($refresh_pdf === true) {
10876 if (!$invoice_id || !$invoice_data) {
10877 // cannot refresh the PDF if an invoice was never created before
10878 return false;
10879 }
10880
10881 // load the same exact previous information to refresh the invoice
10882 $invoice_num = $invoice_data['number'];
10883 $invoice_suff = $invoice_suff;
10884 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10885 $used_date = $invoice_data['for_date'];
10886
10887 // parse the previous (current) invoice number
10888 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10889 if ($matches) {
10890 // overwrite values
10891 $invoice_num = $matches[1] ?: $invoice_num;
10892 $invoice_suff = $matches[2] ?: $invoice_suff;
10893 }
10894 }
10895
10896 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10897 if ($translate === true) {
10898 if (!empty($booking['lang'])) {
10899 $lang = JFactory::getLanguage();
10900 if ($lang->getTag() != $booking['lang']) {
10901 if (VBOPlatformDetection::isWordPress()) {
10902 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10903 } else {
10904 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10905 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10906 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10907 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10908 }
10909 }
10910 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10911 // force the translation to start because contents should be translated
10912 $vbo_tn::$force_tolang = $booking['lang'];
10913 }
10914 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10915 }
10916 }
10917
10918 // the PDF file name
10919 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10920
10921 /**
10922 * If an analogic invoice is not available, make sure to create the record
10923 * before letting the e-invoicing drivers run to avoid a loop.
10924 *
10925 * @since 1.16.7 (J) - 1.6.7 (WP)
10926 */
10927 $retval = null;
10928 if (!$refresh_pdf && !$invoice_id) {
10929 // prepare record object
10930 $invoice_record = new stdClass;
10931 $invoice_record->number = $invoice_num . $invoice_suff;
10932 $invoice_record->file_name = $pdffname;
10933 $invoice_record->idorder = (int)$booking['id'];
10934 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10935 $invoice_record->created_on = time();
10936 $invoice_record->for_date = (int)$used_date;
10937
10938 // insert record
10939 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10940 return false;
10941 }
10942
10943 $retval = $invoice_record->id ?? false;
10944
10945 if (!$retval) {
10946 return false;
10947 }
10948 }
10949
10950 /**
10951 * The generation of the analogic invoices can trigger the drivers for the
10952 * generation of the e-Invoices if they are set to automatically run.
10953 * However, the e-Invoicing classes may be calling this method, so we need
10954 * to make sure the eInvoicing class is not running before proceeding.
10955 * This method could be called within a loop, and so the second iterations
10956 * may already have loaded the eInvocing class. For this we use a static variable.
10957 *
10958 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10959 * though additional information may be available only after the transmission. On top
10960 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10961 */
10962 static $einvocing_can_run = false;
10963
10964 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10965 // allow second iterations calling this method to run
10966 $einvocing_can_run = true;
10967 }
10968
10969 // always load all drivers, db and plugin based ones
10970 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10971
10972 foreach ($drivers as $driver_obj) {
10973 // set the flag of the external call
10974 $driver_obj->externalCall = __METHOD__;
10975
10976 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10977 $driver_obj->externalData['einvnum'] = $invoice_num;
10978
10979 // let the driver eventually elaborate the booking information before invoicing
10980 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10981
10982 if ($einvocing_can_run === true && !$refresh_pdf) {
10983 // generate the e-Invoice
10984 $driver_err = '';
10985 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10986 $driver_err = $driver_obj->getError();
10987 VikError::raiseWarning('', $driver_err);
10988 }
10989
10990 // Booking History - store log for the e-Invoice result
10991 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10992 }
10993 }
10994
10995 /**
10996 * Start the generation of the analogic (courtesy) PDF invoice.
10997 */
10998
10999 // load dependencies
11000 if (!class_exists('TCPDF')) {
11001 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11002 }
11003 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11004
11005 /**
11006 * Trigger event to allow third party plugins to return a specific font name.
11007 *
11008 * @since 1.16.0 (J) - 1.6.0 (WP)
11009 */
11010 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11011 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11012 $usepdffont = $custom_pdf_font[0];
11013 }
11014
11015 // set array variable to the template file
11016 $booking_info = self::getBookingInfoFromID($booking['id']);
11017
11018 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
11019
11020 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
11021
11022 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11023
11024 if (is_file($pathpdf)) {
11025 @unlink($pathpdf);
11026 }
11027
11028 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11029
11030 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11031 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
11032
11033 // header for each page of the pdf
11034 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
11035 $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]);
11036 }
11037
11038 // change some currencies to their unicode (decimal) value
11039 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11040 if (array_key_exists($booking['currencyname'], $unichr_map)) {
11041 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
11042 }
11043
11044 // header and footer fonts
11045 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11046 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11047
11048 // margins
11049 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11050 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11051 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11052
11053 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11054 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11055 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11056
11057 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11058 $pdf->SetPrintHeader(false);
11059 }
11060 if ($pdfparams['show_footer'] == 0) {
11061 $pdf->SetPrintFooter(false);
11062 }
11063
11064 $pdf->AddPage();
11065 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11066 $pdf->lastPage();
11067 $pdf->Output($pathpdf, 'F');
11068
11069 if (!is_file($pathpdf)) {
11070 return false;
11071 }
11072
11073 if (VBOPlatformDetection::isWordPress()) {
11074 /**
11075 * @wponly - trigger files mirroring
11076 */
11077 VikBookingLoader::import('update.manager');
11078 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11079 }
11080
11081 // Booking History
11082 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
11083
11084 if ($retval) {
11085 // a new invoice ID was inserted above
11086 return $retval;
11087 }
11088
11089 // prepare record object to be updated (if insert needed, it's done at first)
11090 $invoice_record = new stdClass;
11091 $invoice_record->id = (int)$invoice_id;
11092 $invoice_record->number = $invoice_num . $invoice_suff;
11093 $invoice_record->file_name = $pdffname;
11094 $invoice_record->idorder = (int)$booking['id'];
11095 $invoice_record->idcustomer = (int)$booking['idcustomer'];
11096 $invoice_record->created_on = time();
11097 $invoice_record->for_date = (int)$used_date;
11098
11099 // update record
11100 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
11101
11102 // return the PDF invoice ID
11103 return $invoice_id;
11104 }
11105
11106 /**
11107 * Generates an analogic invoice in PDF format for a custom list of services.
11108 * No bookings are assigned to this custom invoice. The method parses the same
11109 * invoice template as for the regular process with real booking IDs.
11110 * The invoice number must be stored before calling this method.
11111 *
11112 * @param int $invoice_id the ID of the custom invoice record
11113 *
11114 * @return bool
11115 *
11116 * @since 1.11.1 (J) - 1.1.1 (WP)
11117 */
11118 public static function generateCustomInvoice($invoice_id)
11119 {
11120 $dbo = JFactory::getDbo();
11121
11122 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
11123 $dbo->setQuery($q);
11124 $invoice = $dbo->loadAssoc();
11125 if (!$invoice) {
11126 return false;
11127 }
11128
11129 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
11130 $rawcont = is_array($rawcont) ? $rawcont : array();
11131 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
11132 $invoice['rawcont'] = $rawcont;
11133 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
11134 if (!$rawcont || !$rows || !$customer) {
11135 // at least one invoice raw is mandatory as well as the customer
11136 return false;
11137 }
11138
11139 /**
11140 * Start the generation of the electronic invoice, if any driver was enabled.
11141 *
11142 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11143 * though additional information would be available only after the transmission.
11144 */
11145 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11146
11147 if ($drivers) {
11148 // make sure the invoice number is just a number
11149 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
11150
11151 // invoke all drivers that should run automatically
11152 foreach ($drivers as $driver_obj) {
11153 // set the flag of the external call
11154 $driver_obj->externalCall = __METHOD__;
11155
11156 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
11157 $driver_obj->externalData['einvnum'] = $invoice['number'];
11158 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
11159 $driver_obj->externalData['einvcustom'] = $invoice;
11160
11161 // prepare data array for the generation of the e-Invoice
11162 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
11163
11164 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
11165 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
11166 if ($preveinv !== false) {
11167 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
11168 }
11169
11170 // generate the e-Invoice
11171 $driver_err = '';
11172 if (!$driver_obj->generateEInvoice($einvdata)) {
11173 $driver_err = $driver_obj->getError();
11174 VikError::raiseWarning('', $driver_err);
11175 }
11176 }
11177 }
11178
11179 /**
11180 * Start the generation of the analogic (courtesy) PDF invoice.
11181 */
11182
11183 // load dependencies
11184 if (!class_exists('TCPDF')) {
11185 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11186 }
11187 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11188
11189 /**
11190 * Trigger event to allow third party plugins to return a specific font name.
11191 *
11192 * @since 1.16.0 (J) - 1.6.0 (WP)
11193 */
11194 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11195 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11196 $usepdffont = $custom_pdf_font[0];
11197 }
11198
11199 // load invoice template file
11200 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
11201
11202 // parse invoice template file
11203 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
11204
11205 $pdffname = $invoice['file_name'];
11206 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11207
11208 if (is_file($pathpdf)) {
11209 @unlink($pathpdf);
11210 }
11211
11212 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11213
11214 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11215 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
11216
11217 // header for each page of the pdf
11218 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
11219 $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]);
11220 }
11221
11222 // change some currencies to their unicode (decimal) value
11223 $currencyname = self::getCurrencyName();
11224 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11225 if (array_key_exists($currencyname, $unichr_map)) {
11226 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
11227 }
11228
11229 // header and footer fonts
11230 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11231 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11232
11233 // margins
11234 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11235 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11236 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11237
11238 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11239 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11240 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11241
11242 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11243 $pdf->SetPrintHeader(false);
11244 }
11245 if ($pdfparams['show_footer'] == 0) {
11246 $pdf->SetPrintFooter(false);
11247 }
11248
11249 $pdf->AddPage();
11250 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11251 $pdf->lastPage();
11252 $pdf->Output($pathpdf, 'F');
11253
11254 if (!is_file($pathpdf)) {
11255 return false;
11256 }
11257
11258 if (VBOPlatformDetection::isWordPress()) {
11259 /**
11260 * @wponly - trigger files mirroring
11261 */
11262 VikBookingLoader::import('update.manager');
11263 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11264 }
11265
11266 // return the result of the generation of the PDF invoice
11267 return true;
11268 }
11269
11270 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
11271 {
11272 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
11273 return false;
11274 }
11275
11276 $dbo = JFactory::getDbo();
11277
11278 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
11279 $dbo->setQuery($q);
11280 $invoice_data = $dbo->loadAssoc();
11281
11282 if (!$invoice_data) {
11283 return false;
11284 }
11285
11286 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
11287 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
11288 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
11289 if (!file_exists($invoice_file_path)) {
11290 return false;
11291 }
11292
11293 $vbo_app = self::getVboApplication();
11294 $admin_sendermail = self::getSenderMail();
11295
11296 $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);
11297
11298 // update record
11299 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
11300 $dbo->setQuery($q);
11301 $dbo->execute();
11302
11303 return true;
11304 }
11305
11306 public static function loadCheckinDocTmpl(array $booking_info = [], array $booking_rooms = [], array $customer = [])
11307 {
11308 if (!defined('_VIKBOOKINGEXEC')) {
11309 define('_VIKBOOKINGEXEC', '1');
11310 }
11311
11312 ob_start();
11313 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php';
11314 $content = ob_get_contents();
11315 ob_end_clean();
11316
11317 $default_params = [
11318 'show_header' => 0,
11319 'header_data' => [],
11320 'show_footer' => 0,
11321 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
11322 'pdf_unit' => 'PDF_UNIT',
11323 'pdf_page_format' => 'PDF_PAGE_FORMAT',
11324 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
11325 'pdf_margin_top' => 'PDF_MARGIN_TOP',
11326 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
11327 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
11328 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
11329 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
11330 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
11331 'header_font_size' => '10',
11332 'body_font_size' => '10',
11333 'footer_font_size' => '8'
11334 ];
11335
11336 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
11337 $default_params = array_merge($default_params, $checkin_params);
11338 }
11339
11340 return [$content, $default_params];
11341 }
11342
11343 public static function parseCheckinDocTemplate(string $checkintpl, array $booking, array $booking_rooms, array $customer = [])
11344 {
11345 $dbo = JFactory::getDbo();
11346 $app = JFactory::getApplication();
11347
11348 // clone string
11349 $parsed = $checkintpl;
11350
11351 $datesep = self::getDateSeparator();
11352 $nowdf = self::getDateFormat();
11353 if ($nowdf == "%d/%m/%Y") {
11354 $df = 'd/m/Y';
11355 } elseif ($nowdf == "%m/%d/%Y") {
11356 $df = 'm/d/Y';
11357 } else {
11358 $df = 'Y/m/d';
11359 }
11360
11361 $company_name = self::getFrontTitle();
11362 $company_info = self::getInvoiceCompanyInfo();
11363 $companylogo = self::getSiteLogo();
11364 $uselogo = '';
11365 if (!empty($companylogo)) {
11366 /**
11367 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
11368 *
11369 * @since March 16th 2021
11370 */
11371 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11372 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
11373 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
11374 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11375 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
11376 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
11377 }
11378 }
11379
11380 $parsed = str_replace("{company_name}", $company_name, $parsed);
11381 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
11382 $parsed = str_replace("{company_info}", $company_info, $parsed);
11383 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
11384 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
11385 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
11386 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
11387
11388 $tot_guests = 0;
11389 $tot_adults = 0;
11390 $tot_children = 0;
11391 foreach ($booking_rooms as $kor => $or) {
11392 $tot_guests += ($or['adults'] + $or['children']);
11393 $tot_adults += $or['adults'];
11394 $tot_children += $or['children'];
11395 }
11396 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
11397 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
11398 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
11399 if ($customer && isset($customer['comments'])) {
11400 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
11401 }
11402
11403 $termsconds = self::getTermsConditions();
11404 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
11405
11406 // custom fields replacemenet
11407 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
11408 if (is_array($cmatches[1]) && $cmatches[1]) {
11409 $cfids = array();
11410 foreach ($cmatches[1] as $cfid) {
11411 $cfids[] = $cfid;
11412 }
11413 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
11414 $dbo->setQuery($q);
11415 $cfields = $dbo->loadAssocList();
11416 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
11417 $cfmap = array();
11418 foreach ($cfields as $cf) {
11419 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
11420 }
11421 $cfmapreplace = array();
11422 $partsreceived = explode("\n", $booking['custdata']);
11423 if (count($partsreceived) > 0) {
11424 foreach ($partsreceived as $pst) {
11425 if (!empty($pst)) {
11426 $tmpdata = explode(":", $pst);
11427 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
11428 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
11429 }
11430 }
11431 }
11432 }
11433 foreach ($cmatches[1] as $cfid) {
11434 if (array_key_exists($cfid, $cfmapreplace)) {
11435 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
11436 } else {
11437 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
11438 }
11439 }
11440 }
11441
11442 /**
11443 * Parse all conditional text rules.
11444 */
11445 self::getConditionalRulesInstance()
11446 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
11447 ->parseTokens($parsed);
11448
11449 /**
11450 * Parse all Door Access Control tags.
11451 *
11452 * @since 1.18.4 (J) - 1.8.4 (WP)
11453 */
11454 VBOFactory::getDoorAccessControl()
11455 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $parsed);
11456
11457 return $parsed;
11458 }
11459
11460 /**
11461 * Returns an array of key-value pairs to be used for building the Guest Details
11462 * in the Check-in process. The keys will be compared to the fields of the table
11463 * #__customers to see if some values already exist. The values can use lang defs
11464 * of both front-end or back-end. To be called as list(fields, attributes).
11465 *
11466 * @param boolean $precheckin true if requested for front-end pre check-in.
11467 * @param string $type optional type of custom pax fields to force.
11468 *
11469 * @return array key-value pairs for showing and collecting guest details.
11470 *
11471 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
11472 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
11473 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
11474 */
11475 public static function getPaxFields($precheckin = false, $type = null)
11476 {
11477 // check the type of pax fields collection data
11478 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
11479
11480 if (!$precheckin) {
11481 // back-end check-in ("registration") key-value pairs
11482
11483 // return the requested pax fields collection list
11484 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
11485
11486 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11487 // requested driver returned a list of fields
11488 return $custom_pax_fields;
11489 }
11490
11491 // in case the driver is unsupported, fallback to "basic" (default) driver
11492 return VBOCheckinPax::getFields('basic');
11493 }
11494
11495 // default front-end key-value pairs for pre check-in
11496 $precheckin_pax_fields = [
11497 [
11498 'first_name' => JText::translate('VBCCFIRSTNAME'),
11499 'last_name' => JText::translate('VBCCLASTNAME'),
11500 'date_birth' => JText::translate('ORDER_DBIRTH'),
11501 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
11502 'country' => JText::translate('ORDER_STATE'),
11503 'city' => JText::translate('ORDER_CITY'),
11504 'zip' => JText::translate('ORDER_ZIP'),
11505 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
11506 'gender' => JText::translate('VBOCUSTGENDER'),
11507 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
11508 'docnum' => JText::translate('VBOCUSTDOCNUM'),
11509 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
11510 ],
11511 [
11512 'first_name' => 'text',
11513 'last_name' => 'text',
11514 'date_birth' => 'calendar',
11515 'place_birth' => 'text',
11516 'country' => 'country',
11517 'city' => 'text',
11518 'zip' => 'text',
11519 'nationality' => 'country',
11520 'gender' => [
11521 'M' => JText::translate('VBOCUSTGENDERM'),
11522 'F' => JText::translate('VBOCUSTGENDERF'),
11523 ],
11524 'doctype' => 'text',
11525 'docnum' => 'text',
11526 'documents' => 'file',
11527 ],
11528 ];
11529
11530 // access the pre-checkin pax fields collection list from the current collector
11531 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
11532
11533 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11534 // set the pax fields for pre-checkin from the given data collection driver
11535 $precheckin_pax_fields = $custom_pax_fields;
11536 }
11537
11538 // make a safe copy of the default precheckin fields
11539 $use_precheckin_pax_fields = $precheckin_pax_fields;
11540
11541 /**
11542 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
11543 *
11544 * @since 1.16.3 (J) - 1.6.3 (WP)
11545 */
11546 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
11547
11548 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
11549 // use the filtered pax fields
11550 $use_precheckin_pax_fields = $precheckin_pax_fields;
11551 }
11552
11553 // return the front-end key-value pairs for pre check-in
11554 return $use_precheckin_pax_fields;
11555 }
11556
11557 /**
11558 * Returns the associative list of countries from the DB.
11559 *
11560 * @param bool $tn whether to translate the country name.
11561 * @param bool $no_id whether to unset the ID of the country.
11562 *
11563 * @return array associative or empty array.
11564 *
11565 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
11566 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
11567 */
11568 public static function getCountriesArray($tn = true, $no_id = true)
11569 {
11570 $dbo = JFactory::getDbo();
11571 $all_countries = [];
11572
11573 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
11574 $dbo->setQuery($q);
11575 $countries = $dbo->loadAssocList();
11576 if (!$countries) {
11577 return [];
11578 }
11579
11580 if ($tn === true) {
11581 $vbo_tn = self::getTranslator();
11582 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
11583 // re-apply sorting by country name
11584 $sorting = [];
11585 foreach ($countries as $country) {
11586 $sorting[$country['country_name']] = $country;
11587 }
11588 ksort($sorting);
11589 $sorted = [];
11590 foreach ($sorting as $country) {
11591 $sorted[] = $country;
11592 }
11593 $countries = $sorted;
11594 unset($sorting, $sorted);
11595 }
11596
11597 foreach ($countries as $v) {
11598 if ($no_id) {
11599 // keep the original structure by unsetting the ID only needed for translation
11600 unset($v['id']);
11601 }
11602 $all_countries[$v['country_3_code']] = $v;
11603 }
11604
11605 return $all_countries;
11606 }
11607
11608 public static function getCountriesSelect(string $name, array $all_countries = [], ?string $current_value = '', ?string $empty_value = ' ', ?string $idAttr = null)
11609 {
11610 if (!$all_countries) {
11611 $all_countries = self::getCountriesArray();
11612 }
11613
11614 $countries = '<select ' . ($idAttr ? 'id="' . $idAttr . '" ' : '') . 'name="' . $name . '">' . "\n";
11615 if (strlen($empty_value)) {
11616 $countries .= '<option value="">' . $empty_value . '</option>' . "\n";
11617 }
11618 foreach ($all_countries as $v) {
11619 $countries .= '<option value="' . $v['country_3_code'] . '"' . ($v['country_3_code'] == $current_value ? ' selected="selected"' : '') . '>' . $v['country_name'] . '</option>' . "\n";
11620 }
11621 $countries .= '</select>';
11622
11623 return $countries;
11624 }
11625
11626 public static function getThumbSize($skipsession = false)
11627 {
11628 static $thumb_size = null;
11629
11630 if ($thumb_size) {
11631 return $thumb_size;
11632 }
11633
11634 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11635
11636 return $thumb_size;
11637 }
11638
11639 /**
11640 * Checks whether an iCal file for the reservation should be
11641 * attached to the confirmation email for customer and/or admin.
11642 *
11643 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11644 *
11645 * @since 1.12.0 (J) - 1.2.0 (WP)
11646 */
11647 public static function attachIcal()
11648 {
11649 return VBOFactory::getConfig()->getInt('attachical', 1);
11650 }
11651
11652 /**
11653 * How the calculation of the orphan dates should take place.
11654 *
11655 * @return string "next" for only checking the bookings ahead, "prevnext" if
11656 * also the previous bookings should be checked.
11657 *
11658 * @since 1.13 (J) - 1.3.0 (WP)
11659 */
11660 public static function orphansCalculation()
11661 {
11662 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11663 }
11664
11665 /**
11666 * Returns the name of the template file to use for the front-end View "search".
11667 *
11668 * @return string the name of the template file. New and upgraded user will both use the classic file.
11669 *
11670 * @since 1.13 (J) - 1.3.0 (WP)
11671 */
11672 public static function searchResultsTmpl()
11673 {
11674 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11675 }
11676
11677 /**
11678 * Returns a string without any new-line characters
11679 * to be used for JavaScript values without facing
11680 * errors like 'unterminated string literal'.
11681 * By passing nl2br($str) as argument, we can keep
11682 * the wanted new-line HTML tags for PRE tags.
11683 * We use implode() with just one argument to
11684 * not use an empty string as glue for the string.
11685 *
11686 * @param $str string
11687 *
11688 * @return string
11689 */
11690 public static function strTrimLiteral($str) {
11691 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11692 $lines = explode("\n", $str);
11693 $new_lines = array();
11694 foreach ($lines as $i => $line) {
11695 if (strlen($line)) {
11696 $new_lines[] = trim($line);
11697 }
11698 }
11699 return implode($new_lines);
11700 }
11701
11702 /**
11703 * Returns an instance of the VboApplication class.
11704 *
11705 * @return VboApplication
11706 */
11707 public static function getVboApplication()
11708 {
11709 // require library
11710 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11711
11712 // return a new instance
11713 return new VboApplication();
11714 }
11715
11716 /**
11717 * Returns the translation of the given 0-indexed week day.
11718 *
11719 * @param int $wd the 0-indexed day of the week.
11720 * @param bool $short whether to get the short version of the weekday.
11721 *
11722 * @return string the name of the week day.
11723 *
11724 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11725 */
11726 public static function sayWeekDay($wd, $short = false)
11727 {
11728 switch ($wd) {
11729 case '6' :
11730 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11731 break;
11732 case '5' :
11733 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11734 break;
11735 case '4' :
11736 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11737 break;
11738 case '3' :
11739 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11740 break;
11741 case '2' :
11742 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11743 break;
11744 case '1' :
11745 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11746 break;
11747 default :
11748 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11749 break;
11750 }
11751 return $ret;
11752 }
11753
11754 /**
11755 * Returns the translation of the given 1-indexed month of the year.
11756 *
11757 * @param int $idm the 1-indexed month of the year.
11758 * @param bool $short whether to get the short version of the month.
11759 *
11760 * @return string the name of the month.
11761 *
11762 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11763 */
11764 public static function sayMonth($idm, $short = false)
11765 {
11766 switch ($idm) {
11767 case '12' :
11768 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11769 break;
11770 case '11' :
11771 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11772 break;
11773 case '10' :
11774 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11775 break;
11776 case '9' :
11777 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11778 break;
11779 case '8' :
11780 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11781 break;
11782 case '7' :
11783 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11784 break;
11785 case '6' :
11786 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11787 break;
11788 case '5' :
11789 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11790 break;
11791 case '4' :
11792 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11793 break;
11794 case '3' :
11795 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11796 break;
11797 case '2' :
11798 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11799 break;
11800 default :
11801 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11802 break;
11803 }
11804 return $ret;
11805 }
11806
11807 public static function sayDayMonth($d) {
11808 switch ($d) {
11809 case '31' :
11810 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11811 break;
11812 case '30' :
11813 $ret = JText::translate('VBDAYMONTHTHIRTY');
11814 break;
11815 case '29' :
11816 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11817 break;
11818 case '28' :
11819 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11820 break;
11821 case '27' :
11822 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11823 break;
11824 case '26' :
11825 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11826 break;
11827 case '25' :
11828 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11829 break;
11830 case '24' :
11831 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11832 break;
11833 case '23' :
11834 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11835 break;
11836 case '22' :
11837 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11838 break;
11839 case '21' :
11840 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11841 break;
11842 case '20' :
11843 $ret = JText::translate('VBDAYMONTHTWENTY');
11844 break;
11845 case '19' :
11846 $ret = JText::translate('VBDAYMONTHNINETEEN');
11847 break;
11848 case '18' :
11849 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11850 break;
11851 case '17' :
11852 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11853 break;
11854 case '16' :
11855 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11856 break;
11857 case '15' :
11858 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11859 break;
11860 case '14' :
11861 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11862 break;
11863 case '13' :
11864 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11865 break;
11866 case '12' :
11867 $ret = JText::translate('VBDAYMONTHTWELVE');
11868 break;
11869 case '11' :
11870 $ret = JText::translate('VBDAYMONTHELEVEN');
11871 break;
11872 case '10' :
11873 $ret = JText::translate('VBDAYMONTHTEN');
11874 break;
11875 case '9' :
11876 $ret = JText::translate('VBDAYMONTHNINE');
11877 break;
11878 case '8' :
11879 $ret = JText::translate('VBDAYMONTHEIGHT');
11880 break;
11881 case '7' :
11882 $ret = JText::translate('VBDAYMONTHSEVEN');
11883 break;
11884 case '6' :
11885 $ret = JText::translate('VBDAYMONTHSIX');
11886 break;
11887 case '5' :
11888 $ret = JText::translate('VBDAYMONTHFIVE');
11889 break;
11890 case '4' :
11891 $ret = JText::translate('VBDAYMONTHFOUR');
11892 break;
11893 case '3' :
11894 $ret = JText::translate('VBDAYMONTHTHREE');
11895 break;
11896 case '2' :
11897 $ret = JText::translate('VBDAYMONTHTWO');
11898 break;
11899 default :
11900 $ret = JText::translate('VBDAYMONTHONE');
11901 break;
11902 }
11903 return $ret;
11904 }
11905
11906 public static function totElements($arr) {
11907 $n = 0;
11908 if (is_array($arr)) {
11909 foreach ($arr as $a) {
11910 if (!empty($a)) {
11911 $n++;
11912 }
11913 }
11914 return $n;
11915 }
11916 return false;
11917 }
11918
11919 /**
11920 * Returns a list of documents that were uploaded
11921 * for the specified customer.
11922 *
11923 * @param integer $id The customer ID.
11924 *
11925 * @return array A list of documents.
11926 *
11927 * @since 1.3.0
11928 */
11929 public static function getCustomerDocuments($id)
11930 {
11931 $dbo = JFactory::getDbo();
11932
11933 $q = $dbo->getQuery(true)
11934 ->select($dbo->qn('docsfolder'))
11935 ->from($dbo->qn('#__vikbooking_customers'))
11936 ->where($dbo->qn('id') . ' = ' . (int) $id);
11937
11938 $dbo->setQuery($q, 0, 1);
11939 $dbo->execute();
11940
11941 if (!$dbo->getNumRows())
11942 {
11943 // customer not found
11944 return array();
11945 }
11946
11947 // retrieve customer documents directory name
11948 $dirname = $dbo->loadResult();
11949
11950 if (empty($dirname))
11951 {
11952 // no available directory
11953 return array();
11954 }
11955
11956 // build documents folder path
11957 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11958
11959 if (!is_dir($dirname))
11960 {
11961 // the customer directory doesn't exist
11962 return array();
11963 }
11964
11965 // read all files from customer directory
11966 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11967
11968 $files = array();
11969
11970 foreach ($glob as $path)
11971 {
11972 // skip "index.html"
11973 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11974 {
11975 // extract name and extension from file path
11976 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11977 {
11978 $name = $match[1];
11979 $ext = $match[2];
11980 }
11981 else
11982 {
11983 $name = basename($path);
11984 $ext = '';
11985 }
11986
11987 $file = new stdClass;
11988 $file->path = $path;
11989 $file->name = $name;
11990 $file->ext = $ext;
11991 $file->basename = $file->name . '.' . $file->ext;
11992 $file->size = filesize($path);
11993 $file->date = filemtime($path);
11994 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11995
11996 $files[] = $file;
11997 }
11998 }
11999
12000 // sort files by creation date
12001 usort($files, function($a, $b)
12002 {
12003 return $b->date - $a->date;
12004 });
12005
12006 return $files;
12007 }
12008
12009 public static function displayPaymentParameters($pfile, $pparams = '') {
12010 $html = '<p>---------</p>';
12011
12012 /**
12013 * @wponly The payment gateway is now loaded
12014 * using the apposite dispatcher.
12015 *
12016 * @since 1.0.5
12017 */
12018 JLoader::import('adapter.payment.dispatcher');
12019
12020 try
12021 {
12022 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
12023 }
12024 catch (Exception $e)
12025 {
12026 // payment not found
12027 $html = $e->getMessage();
12028
12029 if ($code = $e->getCode())
12030 {
12031 $html = '<b>' . $code . '</b> : ' . $html;
12032 }
12033
12034 return $html;
12035 }
12036 //
12037
12038 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
12039 $arrparams = !is_array($arrparams) ? array() : $arrparams;
12040
12041 // get admin parameters
12042 $pconfig = $payment->getAdminParameters();
12043
12044 if (count($pconfig) > 0) {
12045 $html = '';
12046 foreach ($pconfig as $value => $cont) {
12047 if (empty($value)) {
12048 continue;
12049 }
12050 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12051 $label = $labelparts[0];
12052 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12053 if (!empty($cont['help'])) {
12054 $labelhelp = $cont['help'];
12055 }
12056 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12057 $html .= '<div class="vbo-param-container">';
12058 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12059 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12060 }
12061 $html .= '<div class="vbo-param-setting">';
12062 switch ($cont['type']) {
12063 case 'custom':
12064 $html .= $cont['html'];
12065 break;
12066 case 'select':
12067 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12068 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12069 if (isset($cont['multiple']) && $cont['multiple']) {
12070 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
12071 } else {
12072 $html .= '<select name="vikpaymentparams['.$value.']">';
12073 }
12074 foreach ($options as $optkey => $poption) {
12075 $checkval = $is_assoc ? $optkey : $poption;
12076 $selected = false;
12077 if (isset($arrparams[$value])) {
12078 if (is_array($arrparams[$value])) {
12079 $selected = in_array($checkval, $arrparams[$value]);
12080 } else {
12081 $selected = ($checkval == $arrparams[$value]);
12082 }
12083 } elseif (isset($default_paramv)) {
12084 if (is_array($default_paramv)) {
12085 $selected = in_array($checkval, $default_paramv);
12086 } else {
12087 $selected = ($checkval == $default_paramv);
12088 }
12089 }
12090 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12091 }
12092 $html .= '</select>';
12093 break;
12094 case 'password':
12095 $html .= '<div class="btn-wrapper input-append">';
12096 $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"/>';
12097 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12098 $html .= '</div>';
12099 break;
12100 case 'number':
12101 $number_attr = array();
12102 if (isset($cont['min'])) {
12103 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12104 }
12105 if (isset($cont['max'])) {
12106 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12107 }
12108 if (isset($cont['step'])) {
12109 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12110 }
12111 $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) . '/>';
12112 break;
12113 case 'textarea':
12114 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12115 break;
12116 case 'hidden':
12117 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
12118 break;
12119 case 'checkbox':
12120 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12121 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
12122 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
12123 break;
12124 default:
12125 $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"/>';
12126 break;
12127 }
12128 if (strlen($labelhelp) > 0) {
12129 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12130 }
12131 $html .= '</div>';
12132 $html .= '</div>';
12133 }
12134 // JS helper function to toggle the password fields
12135 $html .= "\n" . '<script>' . "\n";
12136 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
12137 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12138 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12139 $html .= ' var inp_type = inp.attr("type");' . "\n";
12140 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12141 $html .= '}' . "\n";
12142 $html .= "\n" . '</script>' . "\n";
12143 }
12144
12145 return $html;
12146 }
12147
12148 public static function displaySMSParameters($pfile, $params = null)
12149 {
12150 $html = '---------';
12151
12152 $params = is_string($params) ? json_decode($params, true) : $params;
12153 $params = is_array($params) ? $params : [];
12154
12155 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
12156 return $html;
12157 }
12158
12159 // attempt to load the class file
12160 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
12161
12162 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
12163 return $html;
12164 }
12165
12166 // load the gateway parameters
12167 $config = VikSmsApi::getAdminParameters();
12168
12169 if (!is_array($config) || !count($config)) {
12170 return $html;
12171 }
12172
12173 // flags for JS helpers
12174 $js_helpers = array();
12175
12176 $html = '';
12177 foreach ($config as $value => $cont) {
12178 if (empty($value)) {
12179 continue;
12180 }
12181 $inp_attr = '';
12182 if (isset($cont['attributes'])) {
12183 foreach ($cont['attributes'] as $inpk => $inpv) {
12184 $inp_attr .= $inpk.'="'.$inpv.'" ';
12185 }
12186 $inp_attr = ' ' . rtrim($inp_attr);
12187 }
12188 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12189 $label = $labelparts[0];
12190 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12191 if (!empty($cont['help'])) {
12192 $labelhelp = $cont['help'];
12193 }
12194 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12195 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
12196 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12197 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12198 }
12199 $html .= '<div class="vbo-param-setting">';
12200 switch ($cont['type']) {
12201 case 'custom':
12202 $html .= $cont['html'];
12203 break;
12204 case 'select':
12205 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12206 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12207 if (isset($cont['multiple']) && $cont['multiple']) {
12208 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
12209 } else {
12210 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
12211 }
12212 foreach ($options as $optkey => $poption) {
12213 $checkval = $is_assoc ? $optkey : $poption;
12214 $selected = false;
12215 if (isset($params[$value])) {
12216 if (is_array($params[$value])) {
12217 $selected = in_array($checkval, $params[$value]);
12218 } else {
12219 $selected = ($checkval == $params[$value]);
12220 }
12221 } elseif (isset($default_paramv)) {
12222 if (is_array($default_paramv)) {
12223 $selected = in_array($checkval, $default_paramv);
12224 } else {
12225 $selected = ($checkval == $default_paramv);
12226 }
12227 }
12228 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12229 }
12230 $html .= '</select>';
12231 break;
12232 case 'password':
12233 $html .= '<div class="btn-wrapper input-append">';
12234 $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 . '/>';
12235 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12236 $html .= '</div>';
12237 // set flag for JS helper
12238 $js_helpers[] = $cont['type'];
12239 break;
12240 case 'number':
12241 $number_attr = array();
12242 if (isset($cont['min'])) {
12243 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12244 }
12245 if (isset($cont['max'])) {
12246 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12247 }
12248 if (isset($cont['step'])) {
12249 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12250 }
12251 $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 . '/>';
12252 break;
12253 case 'textarea':
12254 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12255 break;
12256 case 'visual_html':
12257 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
12258 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
12259 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
12260 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
12261 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
12262 break;
12263 case 'hidden':
12264 $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 . '/>';
12265 break;
12266 case 'checkbox':
12267 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12268 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
12269 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
12270 break;
12271 default:
12272 $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 . '/>';
12273 break;
12274 }
12275 if (strlen($labelhelp) > 0) {
12276 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12277 }
12278 $html .= '</div>';
12279 $html .= '</div>';
12280 }
12281
12282 // JS helper functions
12283 if (in_array('password', $js_helpers)) {
12284 // toggle the password fields
12285 $html .= "\n" . '<script>' . "\n";
12286 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
12287 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12288 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12289 $html .= ' var inp_type = inp.attr("type");' . "\n";
12290 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12291 $html .= '}' . "\n";
12292 $html .= "\n" . '</script>' . "\n";
12293 }
12294
12295 return $html;
12296 }
12297
12298 /**
12299 * Renders the params of the requested cron job file-class.
12300 *
12301 * @param string $file the name of the cron job driver file.
12302 * @param array $params the parameters array.
12303 *
12304 * @return string the necessary HTML content to render.
12305 *
12306 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
12307 */
12308 public static function displayCronParameters($file, $params = [])
12309 {
12310 try
12311 {
12312 // attempt to create a new instance
12313 $job = VBOFactory::getCronFactory()->createInstance($file);
12314 }
12315 catch (Exception $e)
12316 {
12317 // something went wrong, display error message
12318 return '<p>' . $e->getMessage() . '<p>';
12319 }
12320
12321 // get admin parameters
12322 $config = $job->getForm();
12323
12324 if (!is_array($config) || !$config) {
12325 return '<p>---------</p>';
12326 }
12327
12328 /**
12329 * Render the cron parameters through the global params rendering class.
12330 *
12331 * @since 1.16.9 (J) - 1.6.9 (WP)
12332 */
12333 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
12334 }
12335
12336 public static function invokeChannelManager($skiporder = true, $order = array()) {
12337 $task = VikRequest::getString('task', '', 'request');
12338 $view = VikRequest::getString('view', '', 'request');
12339 $tmpl = VikRequest::getString('tmpl', '', 'request');
12340 $noimpression = array('vieworder', 'booking');
12341 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')) {
12342 //VCM Channel Impression
12343 if (!class_exists('VikChannelManagerConfig')) {
12344 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12345 }
12346 if (!class_exists('VikChannelManager')) {
12347 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12348 }
12349 VikChannelManager::invokeChannelImpression();
12350 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
12351 //VCM Channel Conversion-Impression
12352 if (!class_exists('VikChannelManagerConfig')) {
12353 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12354 }
12355 if (!class_exists('VikChannelManager')) {
12356 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12357 }
12358 VikChannelManager::invokeChannelConversionImpression($order);
12359 }
12360 }
12361
12362 public static function validEmail($email) {
12363 $isValid = true;
12364 $atIndex = strrpos($email, "@");
12365 if (is_bool($atIndex) && !$atIndex) {
12366 $isValid = false;
12367 } else {
12368 $domain = substr($email, $atIndex +1);
12369 $local = substr($email, 0, $atIndex);
12370 $localLen = strlen($local);
12371 $domainLen = strlen($domain);
12372 if ($localLen < 1 || $localLen > 64) {
12373 // local part length exceeded
12374 $isValid = false;
12375 } else
12376 if ($domainLen < 1 || $domainLen > 255) {
12377 // domain part length exceeded
12378 $isValid = false;
12379 } else
12380 if ($local[0] == '.' || $local[$localLen -1] == '.') {
12381 // local part starts or ends with '.'
12382 $isValid = false;
12383 } else
12384 if (preg_match('/\\.\\./', $local)) {
12385 // local part has two consecutive dots
12386 $isValid = false;
12387 } else
12388 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
12389 // character not valid in domain part
12390 $isValid = false;
12391 } else
12392 if (preg_match('/\\.\\./', $domain)) {
12393 // domain part has two consecutive dots
12394 $isValid = false;
12395 } else
12396 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
12397 // character not valid in local part unless
12398 // local part is quoted
12399 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
12400 $isValid = false;
12401 }
12402 }
12403 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
12404 // domain not found in DNS
12405 $isValid = false;
12406 }
12407 }
12408 return $isValid;
12409 }
12410
12411 public static function caniWrite($path) {
12412 if ($path[strlen($path) - 1] == '/') {
12413 // ricorsivo return a temporary file path
12414 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
12415 }
12416 if (is_dir($path)) {
12417 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
12418 }
12419 // check tmp file for read/write capabilities
12420 $rm = file_exists($path);
12421 $f = @fopen($path, 'a');
12422 if ($f === false) {
12423 return false;
12424 }
12425 fclose($f);
12426 if (!$rm) {
12427 unlink($path);
12428 }
12429 return true;
12430 }
12431
12432 /**
12433 * Alias method of JFile::upload to unify any
12434 * upload function into one.
12435 *
12436 * @param string $src The name of the php (temporary) uploaded file.
12437 * @param string $dest The path (including filename) to move the uploaded file to.
12438 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
12439 *
12440 * @return boolean True on success.
12441 *
12442 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
12443 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
12444 */
12445 public static function uploadFile($src, $dest, $copy_only = false) {
12446 // always attempt to include the File class
12447 jimport('joomla.filesystem.file');
12448
12449 // upload the file
12450 if (!$copy_only) {
12451 $result = JFile::upload($src, $dest);
12452 } else {
12453 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
12454 $result = JFile::copy($src, $dest);
12455 }
12456
12457 /**
12458 * @wponly in order to not lose uploaded files after installing an update,
12459 * we need to move any uploaded file onto a recovery folder.
12460 */
12461 if ($result) {
12462 VikBookingLoader::import('update.manager');
12463 VikBookingUpdateManager::triggerUploadBackup($dest);
12464 }
12465 //
12466
12467 // return upload result
12468 return $result;
12469 }
12470
12471 /**
12472 * Helper method used to upload the given file (retrieved from $_FILES)
12473 * into the specified destination.
12474 *
12475 * @param array $file An associative array with the file details.
12476 * @param string $dest The destination path.
12477 * @param string $filters A string (or a regex) containing the allowed extensions.
12478 *
12479 * @return array The uploading result.
12480 *
12481 * @throws RuntimeException
12482 *
12483 * @since 1.3.0
12484 */
12485 public static function uploadFileFromRequest($file, $dest, $filters = '*')
12486 {
12487 jimport('joomla.filesystem.file');
12488
12489 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
12490
12491 if (empty($file['name']))
12492 {
12493 throw new RuntimeException('Missing file', 400);
12494 }
12495
12496 /**
12497 * Make sure the upload of the file didn't raise any errors.
12498 *
12499 * @link https://www.php.net/manual/en/features.file-upload.errors.php
12500 */
12501 if ((int) $file['error'])
12502 {
12503 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
12504 {
12505 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
12506 }
12507 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
12508 {
12509 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
12510 }
12511 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
12512 {
12513 $error = 'The uploaded file was only partially uploaded.';
12514 }
12515 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
12516 {
12517 $error = 'No file was uploaded.';
12518 }
12519 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
12520 {
12521 $error = 'Missing a temporary folder.';
12522 }
12523 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
12524 {
12525 $error = 'Failed to write file to disk.';
12526 }
12527 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
12528 {
12529 $error = 'A PHP extension stopped the file upload.';
12530 }
12531 else
12532 {
12533 $error = 'Unknown error.';
12534 }
12535
12536 throw new RuntimeException($error, 500);
12537 }
12538
12539 $src = $file['tmp_name'];
12540
12541 // extract file name and extension
12542 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
12543 {
12544 $filename = $match[1];
12545 $fileext = $match[2];
12546 }
12547 else
12548 {
12549 // probably no extension provided
12550 $filename = basename($file['name']);
12551 $fileext = '';
12552 }
12553
12554 $j = '';
12555
12556 if (file_exists($dest . $filename . $fileext))
12557 {
12558 $j = 2;
12559
12560 while (file_exists($dest . $filename . '-' . $j . $fileext))
12561 {
12562 $j++;
12563 }
12564
12565 $j = '-' . $j;
12566 }
12567
12568 $finaldest = $dest . $filename . $j . $fileext;
12569
12570 // make sure the file extension is supported
12571 if (!self::isFileTypeCompatible(basename($finaldest), $filters))
12572 {
12573 // extension not supported
12574 throw new RuntimeException(sprintf('Extension [%s] is not supported', $fileext), 400);
12575 }
12576
12577 // try to upload the file
12578 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
12579 {
12580 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
12581 }
12582
12583 $file = new stdClass;
12584 $file->name = $filename . $j;
12585 $file->ext = ltrim($fileext, '.');
12586 $file->filename = basename($finaldest);
12587 $file->path = $finaldest;
12588
12589 return $file;
12590 }
12591
12592 /**
12593 * Helper method used to check whether the given file name
12594 * supports one of the given filters.
12595 *
12596 * @param mixed $file Either the file name or the uploaded file.
12597 * @param string $filters Either a regex or a comma-separated list of supported extensions.
12598 * The regex must be inclusive of delimiters.
12599 *
12600 * @return bool True if supported, false otherwise.
12601 *
12602 * @since 1.7.3 (WP) - 1.17.3 (J)
12603 */
12604 public static function isFileTypeCompatible($file, $filters)
12605 {
12606 // make sure the filters query is not empty
12607 if (strlen($filters) == 0)
12608 {
12609 // cannot assert whether the file could be accepted or not
12610 return false;
12611 }
12612
12613 // check whether all the files are accepted
12614 if ($filters == '*')
12615 {
12616 return true;
12617 }
12618
12619 // use the file MIME TYPE in case of array
12620 if (is_array($file))
12621 {
12622 $file = $file['type'];
12623 }
12624
12625 // check if we are dealing with a regex
12626 if (static::isRegex($filters))
12627 {
12628 return (bool) preg_match($filters, $file);
12629 }
12630
12631 // fallback to comma-separated list
12632 $types = array_filter(preg_split("/\s*,\s*/", $filters));
12633
12634 foreach ($types as $t)
12635 {
12636 // remove initial dot if specified
12637 $t = ltrim($t, '.');
12638 // escape slashes to avoid breaking the regex
12639 $t = preg_replace("/\//", '\/', $t);
12640
12641 // check if the file ends with the given extension
12642 if (preg_match("/{$t}$/i", $file))
12643 {
12644 return true;
12645 }
12646 }
12647
12648 return false;
12649 }
12650
12651 /**
12652 * Checks whether the given string is a structured PCRE regex.
12653 * It simply makes sure that the string owns valid delimiters.
12654 * A delimiter can be any non-alphanumeric, non-backslash,
12655 * non-whitespace character.
12656 *
12657 * @param string $str The string to check.
12658 *
12659 * @return boolean True if a regex, false otherwise.
12660 *
12661 * @since 1.7.3 (WP) - 1.17.3 (J)
12662 */
12663 public static function isRegex($str)
12664 {
12665 // first of all make sure the first character is a supported delimiter
12666 if (!preg_match("/^([!#$%&'*+,.\/:;=?@^_`|~\-(\[{<\"])/", $str, $match))
12667 {
12668 // no valid delimiter
12669 return false;
12670 }
12671
12672 // get delimiter
12673 $d = $match[1];
12674
12675 // lookup used to check if we should take a different ending delimiter
12676 $lookup = array(
12677 '{' => '}',
12678 '[' => ']',
12679 '(' => ')',
12680 '<' => '>',
12681 );
12682
12683 if (isset($lookup[$d]))
12684 {
12685 $d = $lookup[$d];
12686 }
12687
12688 // make sure the regex ends with the delimiter found
12689 return (bool) preg_match("/\\{$d}[gimsxU]*$/", $str);
12690 }
12691
12692 /**
12693 * Gets the instance of a specific report class.
12694 *
12695 * @param string $report the name of the report to load.
12696 *
12697 * @return mixed false or report object instance.
12698 *
12699 * @since 1.3.0
12700 */
12701 public static function getReportInstance($report)
12702 {
12703 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12704
12705 return VikBookingReport::getInstanceOf($report);
12706 }
12707
12708 /**
12709 * Checks whether the guest reviews are enabled and VCM is installed.
12710 *
12711 * @return boolean true if enabled, false otherwise.
12712 *
12713 * @since 1.3.0
12714 */
12715 public static function allowGuestReviews()
12716 {
12717 $dbo = JFactory::getDbo();
12718 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12719 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12720 $dbo->setQuery($q);
12721 $dbo->execute();
12722 if ($dbo->getNumRows()) {
12723 $s = $dbo->loadResult();
12724 return ((int)$s === 1 && $vcm_installed);
12725 }
12726 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12727 $dbo->setQuery($q);
12728 $dbo->execute();
12729 return $vcm_installed;
12730 }
12731
12732 /**
12733 * Gets the minimum chars for the guest review message.
12734 *
12735 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12736 *
12737 * @since 1.3.0
12738 */
12739 public static function guestReviewMinChars()
12740 {
12741 $dbo = JFactory::getDbo();
12742 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12743 $dbo->setQuery($q);
12744 $dbo->execute();
12745 if ($dbo->getNumRows() > 0) {
12746 return (int)$dbo->loadResult();
12747 }
12748 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12749 $dbo->setQuery($q);
12750 $dbo->execute();
12751 return 15;
12752 }
12753
12754 /**
12755 * The approval type for new guest reviews.
12756 *
12757 * @return string auto or manual.
12758 *
12759 * @since 1.3.0
12760 */
12761 public static function guestReviewsApproval()
12762 {
12763 $dbo = JFactory::getDbo();
12764 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12765 $dbo->setQuery($q);
12766 $dbo->execute();
12767 if ($dbo->getNumRows() > 0) {
12768 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12769 }
12770 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12771 $dbo->setQuery($q);
12772 $dbo->execute();
12773 return 'auto';
12774 }
12775
12776 /**
12777 * The type of reviews guests should leave.
12778 *
12779 * @return string global or service.
12780 *
12781 * @since 1.3.0
12782 */
12783 public static function guestReviewsType()
12784 {
12785 $dbo = JFactory::getDbo();
12786 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12787 $dbo->setQuery($q);
12788 $dbo->execute();
12789 if ($dbo->getNumRows() > 0) {
12790 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12791 }
12792 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12793 $dbo->setQuery($q);
12794 $dbo->execute();
12795 return 'service';
12796 }
12797
12798 /**
12799 * The services to be reviewed by the guests.
12800 *
12801 * @return array list, empty or not, of the services to be reviewed.
12802 *
12803 * @since 1.3.0
12804 */
12805 public static function guestReviewsServices()
12806 {
12807 $dbo = JFactory::getDbo();
12808 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12809 $dbo->setQuery($q);
12810 $dbo->execute();
12811 if ($dbo->getNumRows()) {
12812 return $dbo->loadAssocList();
12813 }
12814 // insert the default services
12815 $default_services = array(
12816 JText::translate('VBOGREVVALUE'),
12817 JText::translate('VBOGREVLOCATION'),
12818 JText::translate('VBOGREVSTAFF'),
12819 JText::translate('VBOGREVCLEAN'),
12820 JText::translate('VBOGREVCOMFORT'),
12821 JText::translate('VBOGREVFACILITIES'),
12822 );
12823 foreach ($default_services as $def_service) {
12824 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12825 $dbo->setQuery($q);
12826 $dbo->execute();
12827 }
12828 return array();
12829 }
12830
12831 /**
12832 * Checks whether the guest reviews should be downloaded (max 1 per day).
12833 * VCM must be installed in order to download the new reviews.
12834 *
12835 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12836 *
12837 * @since 1.3.0
12838 */
12839 public static function shouldDownloadReviews()
12840 {
12841 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12842 return -1;
12843 }
12844
12845 $today = date('Y-m-d');
12846
12847 $dbo = JFactory::getDbo();
12848 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12849 $dbo->setQuery($q);
12850 $dbo->execute();
12851 if ($dbo->getNumRows()) {
12852 $last_download = $dbo->loadResult();
12853 // update last download day
12854 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12855 $dbo->setQuery($q);
12856 $dbo->execute();
12857 //
12858 return ($last_download != $today ? 1 : 0);
12859 }
12860 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12861 $dbo->setQuery($q);
12862 $dbo->execute();
12863 return 1;
12864 }
12865
12866 /**
12867 * Tells whether a booking can be reviewed.
12868 *
12869 * @param array $booking booking array details.
12870 *
12871 * @return boolean true if a review can be left, false otherwise.
12872 *
12873 * @since 1.3.0
12874 */
12875 public static function canBookingBeReviewed($booking)
12876 {
12877 // reviews must be enabled and supported
12878 if (!self::allowGuestReviews()) {
12879 return false;
12880 }
12881
12882 // booking status must be confirmed
12883 if ($booking['status'] != 'confirmed') {
12884 return false;
12885 }
12886
12887 // review can be left starting from the check-out day
12888 $checkout_info = getdate($booking['checkout']);
12889 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12890 if (time() < $checkout_midn) {
12891 return false;
12892 }
12893
12894 // make sure a review for this booking does not exist
12895 $noreview = true;
12896 $dbo = JFactory::getDbo();
12897 try {
12898 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12899 $dbo->setQuery($q);
12900 $dbo->execute();
12901 $noreview = ($dbo->getNumRows() < 1);
12902 } catch (Exception $e) {
12903 // if the query fails, we do not allow the review to be left
12904 $noreview = false;
12905 }
12906
12907 return $noreview;
12908 }
12909
12910 /**
12911 * Gets the review for a booking, no matter if it's published or not.
12912 * Review will be returned only if reviews are enabled and supported.
12913 * Only reviews left through the website, no OTA reviews as this method
12914 * should be used in the front-end to display the review to the guest.
12915 *
12916 * @param array $booking booking array details.
12917 *
12918 * @return mixed associative array or false.
12919 *
12920 * @since 1.3.0
12921 */
12922 public static function getBookingReview($booking)
12923 {
12924 // reviews must be enabled and supported
12925 if (!self::allowGuestReviews()) {
12926 return false;
12927 }
12928
12929 if (empty($booking['id'])) {
12930 return false;
12931 }
12932
12933 // make sure a review for this booking exists
12934 $dbo = JFactory::getDbo();
12935 $review = array();
12936 try {
12937 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12938 $dbo->setQuery($q);
12939 $dbo->execute();
12940 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12941 } catch (Exception $e) {
12942 // query has failed
12943 $review = array();
12944 }
12945
12946 if (count($review)) {
12947 // make sure the review was left from the website
12948 $review['uniquekey'] = (int)$review['uniquekey'];
12949 }
12950
12951 if (count($review) && !empty($review['uniquekey'])) {
12952 // this is an OTA review
12953 return false;
12954 }
12955
12956 if (count($review) && !empty($review['content'])) {
12957 // decode content
12958 $review['content'] = json_decode($review['content'], true);
12959 }
12960
12961 return count($review) ? $review : false;
12962 }
12963
12964 /**
12965 * Attempts to find a translation for a raw customer data label.
12966 * Bookings downloaded from the OTAs will save a raw string of information
12967 * composed of pairs of label-value separated by new line feeds.
12968 * We try to translate the given label into the current language.
12969 *
12970 * @param string $label the raw string label to translate.
12971 *
12972 * @return string either the original or the translated label.
12973 *
12974 * @since 1.3.5
12975 * @since 1.4.0 back-end support added.
12976 */
12977 public static function tnCustomerRawDataLabel($label)
12978 {
12979 // this is a map of the known labels
12980 $known_lbls = array(
12981 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12982 'LASTNAME' => 'ORDER_LNAME',
12983 'COUNTRY' => 'ORDER_STATE',
12984 'EMAIL' => 'VBMAIL',
12985 'TELEPHONE' => 'VBPHONE',
12986 'PHONE' => 'VBPHONE',
12987 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12988 'MEAL_PLAN' => 'VBOMEALPLAN',
12989 'CITY' => 'VBCITY',
12990 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12991 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12992 );
12993
12994 if (self::isAdmin()) {
12995 // override the map of known translation strings
12996 $known_lbls = array(
12997 'NAME' => 'ORDER_NAME',
12998 'LASTNAME' => 'ORDER_LNAME',
12999 'COUNTRY' => 'ORDER_STATE',
13000 'ADDRESS' => 'ORDER_ADDRESS',
13001 'CITY' => 'ORDER_CITY',
13002 'LOCATION' => 'ORDER_CITY',
13003 'EMAIL' => 'ORDER_EMAIL',
13004 'TELEPHONE' => 'ORDER_PHONE',
13005 'PHONE' => 'ORDER_PHONE',
13006 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13007 );
13008 }
13009
13010 // we get rid of any empty space by keeping the underscores
13011 $converted = str_replace(' ', '', strtoupper($label));
13012
13013 if (isset($known_lbls[$converted])) {
13014 // this language definition has been mapped
13015 return JText::translate($known_lbls[$converted]);
13016 }
13017
13018 // we try to guess the translation string by prepending VBO
13019 $guessed = JText::translate('VBO' . $converted);
13020 if ($guessed != 'VBO' . $converted) {
13021 // the label was translated correctly
13022 return $guessed;
13023 }
13024
13025 // this label could not be translated, so we return the plain string
13026 return $label;
13027 }
13028
13029 /**
13030 * We check whether some bookings are available for import from third party plugins.
13031 *
13032 * @return mixed array with list of plugins supported, false otherwise.
13033 *
13034 * @wponly this method is only useful for WordPress.
13035 *
13036 * @since 1.3.5
13037 */
13038 public static function canImportBookingsFromThirdPartyPlugins()
13039 {
13040 $dbo = JFactory::getDbo();
13041
13042 $plugins = array();
13043
13044 /**
13045 * As requested from hundreds of our clients, for the moment we check only the custom
13046 * post types of type "mphb_booking" to see if some bookings are available for import.
13047 */
13048 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
13049 $dbo->setQuery($q);
13050 $dbo->execute();
13051
13052 if (!$dbo->getNumRows()) {
13053 return false;
13054 }
13055
13056 // push this third party plugin
13057 $plugins['mphb'] = 'MotoPress Hotel Booking';
13058
13059 return count($plugins) ? $plugins : false;
13060 }
13061
13062 /**
13063 * This method returns a list of the known languages sorted by the
13064 * administrator custom preferences. Useful for the phone input fields.
13065 *
13066 * @param boolean $code_assoc whether to get an associative array with the lang name.
13067 *
13068 * @return array the sorted list of preferred countries.
13069 *
13070 * @since 1.3.11
13071 */
13072 public static function preferredCountriesOrdering($code_assoc = false)
13073 {
13074 $preferred_countries = array();
13075
13076 // try to get the preferred countries from db
13077 $dbo = JFactory::getDbo();
13078 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
13079 $dbo->setQuery($q);
13080 $setting = $dbo->loadResult();
13081 if (!$setting) {
13082 // create empty configuration record
13083 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
13084 $dbo->setQuery($q);
13085 $dbo->execute();
13086 } else {
13087 $preferred_countries = json_decode($setting);
13088 }
13089
13090 // get the default known languages
13091 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
13092
13093 if (!is_array($preferred_countries) || !count($preferred_countries)) {
13094 // sort the default known languages by country code alphabetically
13095 ksort($sorted_known_langs);
13096 foreach ($sorted_known_langs as $k => $v) {
13097 $langsep = strpos($k, '_') !== false ? '_' : '-';
13098 $langparts = explode($langsep, $k);
13099 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
13100 }
13101 // update the database record
13102 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
13103 $dbo->setQuery($q);
13104 $dbo->execute();
13105 }
13106
13107 if ($code_assoc) {
13108 // this is useful for displaying the preferred countries codes together with the language name
13109 $map = array();
13110 foreach ($preferred_countries as $ccode) {
13111 // look for the current country code in the keys of the known language tags
13112 $match_found = false;
13113 foreach ($sorted_known_langs as $langtag => $langinfo) {
13114 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
13115 $langparts = explode($langsep, $langtag);
13116 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
13117 // match found
13118 $match_found = true;
13119 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13120 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
13121 // match found
13122 $match_found = true;
13123 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13124 }
13125 }
13126 if (!$match_found) {
13127 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
13128 $map[$ccode] = strtoupper($ccode);
13129 }
13130 }
13131 if (count($map)) {
13132 // set the associatve array to be returned
13133 $preferred_countries = $map;
13134 }
13135 }
13136
13137 return $preferred_countries;
13138 }
13139
13140 /**
13141 * Gets the instance of the admin widgets helper class.
13142 *
13143 * @return VikBookingHelperAdminWidgets
13144 *
13145 * @since 1.4.0
13146 */
13147 public static function getAdminWidgetsInstance()
13148 {
13149 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
13150
13151 return VikBookingHelperAdminWidgets::getInstance();
13152 }
13153
13154 /**
13155 * Gets the instance of the conditional rules helper class.
13156 *
13157 * @param bool $require_only whether to return the object.
13158 *
13159 * @return mixed VikBookingHelperConditionalRules or true.
13160 *
13161 * @since 1.4.0
13162 */
13163 public static function getConditionalRulesInstance($require_only = false)
13164 {
13165 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
13166
13167 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
13168 }
13169
13170 /**
13171 * Gets the instance of the geocoding helper class.
13172 *
13173 * @param bool $require_only whether to return the object.
13174 *
13175 * @return mixed VikBookingHelperGeocoding or true.
13176 *
13177 * @since 1.4.0
13178 */
13179 public static function getGeocodingInstance($require_only = false)
13180 {
13181 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
13182
13183 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
13184 }
13185
13186 /**
13187 * Helper method to cope with the removal of the same method
13188 * in the JApplication class introduced with Joomla 4. Using
13189 * isClient() would break the compatibility with J < 3.7 so
13190 * we can rely on this helper method to avoid Fatal Errors.
13191 *
13192 * @return boolean
13193 *
13194 * @since October 2020
13195 */
13196 public static function isAdmin()
13197 {
13198 $app = JFactory::getApplication();
13199 if (method_exists($app, 'isClient')) {
13200 return $app->isClient('administrator');
13201 }
13202
13203 return $app->isAdmin();
13204 }
13205
13206 /**
13207 * Helper method to cope with the removal of the same method
13208 * in the JApplication class introduced with Joomla 4. Using
13209 * isClient() would break the compatibility with J < 3.7 so
13210 * we can rely on this helper method to avoid Fatal Errors.
13211 *
13212 * @return boolean
13213 *
13214 * @since October 2020
13215 */
13216 public static function isSite()
13217 {
13218 $app = JFactory::getApplication();
13219 if (method_exists($app, 'isClient')) {
13220 return $app->isClient('site');
13221 }
13222
13223 return $app->isSite();
13224 }
13225
13226 /**
13227 * Gets the Google Maps API Key.
13228 *
13229 * @return string
13230 *
13231 * @since 1.14 (J) - 1.4.0 (WP)
13232 */
13233 public static function getGoogleMapsKey()
13234 {
13235 return VBOFactory::getConfig()->getString('gmapskey', '');
13236 }
13237
13238 /**
13239 * Checks whether the interactive map booking is enabled.
13240 *
13241 * @return bool
13242 *
13243 * @since 1.14 (J) - 1.4.0 (WP)
13244 */
13245 public static function interactiveMapEnabled()
13246 {
13247 return VBOFactory::getConfig()->getBool('interactive_map', false);
13248 }
13249
13250 /**
13251 * Gets the preferred colors saved in the configuration, if any.
13252 *
13253 * @return array
13254 *
13255 * @since 1.14 (J) - 1.4.0 (WP)
13256 */
13257 public static function getPreferredColors()
13258 {
13259 $config = VBOFactory::getConfig();
13260
13261 // default colors
13262 $pref_colors = [
13263 'textcolor' => '#2282bd',
13264 'bgcolor' => '#2282bd',
13265 'fontcolor' => '#fff',
13266 'bgcolorhov' => '#1890ce',
13267 'fontcolorhov' => '#fff',
13268 ];
13269
13270 // saved colors
13271 $colors = $config->getArray('pref_colors', []);
13272
13273 return $colors ?: $pref_colors;
13274 }
13275
13276 /**
13277 * Adds to the document inline styles for the preferred colors, if any.
13278 *
13279 * @return void
13280 *
13281 * @since 1.14 (J) - 1.4.0 (WP)
13282 */
13283 public static function loadPreferredColorStyles()
13284 {
13285 $pref_colors = self::getPreferredColors();
13286 $css_classes = [];
13287
13288 /**
13289 * Register all preferred colors as CSS variables.
13290 *
13291 * @since 1.18.8 (J) - 1.8.8 (WP)
13292 */
13293 $css_vars = array_filter(array_combine(array_keys($pref_colors), array_values($pref_colors)));
13294 if ($css_vars) {
13295 $css_vars_decl = array_map(function($k, $v) {
13296 return sprintf('--vbo-pref-%s: %s;', preg_replace('/[^a-z0-9]/', '', strtolower($k)), $v);
13297 }, array_keys($css_vars), array_values($css_vars));
13298 $css_classes[] = ':root {' . "\n" . implode("\n", $css_vars_decl) . "\n" . '}';
13299 }
13300
13301 // add in-line style declaration
13302 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
13303 }
13304
13305 /**
13306 * Given the full endpoint URL for the AJAX request, it
13307 * returns an appropriate URI for the current platform.
13308 *
13309 * @param mixed $query The query string or a routed URL.
13310 * @param boolean $xhtml Replace & by &amp; for XML compliance.
13311 *
13312 * @return string The AJAX end-point URI.
13313 *
13314 * @since 1.14 (J) - 1.4.0 (WP)
13315 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
13316 */
13317 public static function ajaxUrl($query = '', $xhtml = false)
13318 {
13319 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
13320 }
13321
13322 /**
13323 * Tells whether no tax rates have been defined so far.
13324 *
13325 * @return bool True if no tax rates defined, false otherwise.
13326 *
13327 * @since 1.14 (J) - 1.4.0 (WP)
13328 */
13329 public static function noTaxRates()
13330 {
13331 $dbo = JFactory::getDbo();
13332
13333 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
13334 $dbo->setQuery($q);
13335 $dbo->execute();
13336
13337 return ($dbo->getNumRows() < 1);
13338 }
13339
13340 /**
13341 * Booking Type feature is strictly connected to VCM and OTAs. Updated
13342 * versions of VBO will always support it as long as VCM is installed.
13343 * Needed by VCM to understand whether certain SQL queries can be performed.
13344 *
13345 * @return bool true if VCM exists, false otherwise.
13346 *
13347 * @since 1.14 (J) - 1.4.0 (WP)
13348 */
13349 public static function isBookingTypeSupported()
13350 {
13351 static $isBookingTypeSupported = null;
13352
13353 if (is_bool($isBookingTypeSupported)) {
13354 return $isBookingTypeSupported;
13355 }
13356
13357 $isBookingTypeSupported = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
13358
13359 return $isBookingTypeSupported;
13360 }
13361
13362 /**
13363 * Returns an instance of the Rates Flow helper class of VCM, if available.
13364 *
13365 * @param bool $anew True to request a new object instance.
13366 *
13367 * @return VCMRatesFlow|null
13368 *
13369 * @since 1.15.0 (J) - 1.5.0 (WP)
13370 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
13371 */
13372 public static function getRatesFlowInstance($anew = false)
13373 {
13374 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
13375 if (!is_file($vcm_fpath)) {
13376 return null;
13377 }
13378
13379 // load dependencies
13380 require_once $vcm_fpath;
13381
13382 // return the instance of the class
13383 return VCMRatesFlow::getInstance($anew);
13384 }
13385
13386 /**
13387 * Returns the current appearance preference.
13388 *
13389 * @return string light, auto or dark.
13390 *
13391 * @since 1.15.0 (J) - 1.5.0 (WP)
13392 */
13393 public static function getAppearancePref()
13394 {
13395 static $preference = null;
13396
13397 if ($preference !== null) {
13398 return $preference;
13399 }
13400
13401 // auto is the default appearance preference
13402 $default_pref = 'auto';
13403
13404 // accepted preferences
13405 $valid_pref = [
13406 'auto',
13407 'light',
13408 'dark',
13409 ];
13410
13411 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
13412 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
13413
13414 return $preference;
13415 }
13416
13417 /**
13418 * According to the appearance preferences, the apposite
13419 * CSS assets are loaded within the document.
13420 *
13421 * @param bool $get_info true to not load any assets.
13422 *
13423 * @return mixed string light, auto, dark, info array or false.
13424 *
13425 * @since 1.15.0 (J) - 1.5.0 (WP)
13426 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
13427 */
13428 public static function loadAppearancePreferenceAssets($get_info = false)
13429 {
13430 // get document object
13431 $document = JFactory::getDocument();
13432
13433 // load current color scheme preference
13434 $current_pref = self::getAppearancePref();
13435
13436 // define caching values
13437 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
13438
13439 // define file attributes
13440 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
13441
13442 // apposite file path and URI for back-end
13443 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
13444 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
13445
13446 // check if this is for the front-end ("auto" file only)
13447 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
13448 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
13449 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
13450 // check if front-end appearance is disabled
13451 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
13452 // do nothing for the front-end by default
13453 return false;
13454 }
13455 }
13456
13457 if (VBOPlatformDetection::isJoomla()) {
13458 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
13459 $css_uri = str_replace('resources/', '', $css_uri);
13460 }
13461
13462 if (!is_file($css_path)) {
13463 // this preference does not require a specific stylesheet
13464 return false;
13465 }
13466
13467 if ($get_info) {
13468 // do not actually load any assets
13469 return [
13470 'id' => $file_attr['id'],
13471 'href' => $css_uri,
13472 ];
13473 }
13474
13475 // load the apposite CSS file
13476 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
13477
13478 return $current_pref;
13479 }
13480
13481 /**
13482 * VBO trigger for VCM subscription status and updates. If called within the admin section
13483 * returns the expiration reminder array to display a modal window, and sets a warning
13484 * message. If called within the front-end, may trigger an email alert.
13485 *
13486 * @return mixed null, array or boolean depending on client position.
13487 *
13488 * @since 1.15.0 (J) - 1.5.0 (WP)
13489 */
13490 public static function getVCMSubscriptionStatus()
13491 {
13492 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
13493 if (!is_file($vcm_lib_path)) {
13494 // do nothing
13495 return null;
13496 }
13497
13498 // determine client position
13499 $is_admin = self::isAdmin();
13500
13501 try {
13502 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
13503 // VCM is outdated
13504 return null;
13505 }
13506
13507 // always attempt to load the admin lang handler of VCM
13508 $lang = JFactory::getLanguage();
13509
13510 // load the VCM admin language file
13511 $vcm_admin_lang_path = '';
13512 if (VBOPlatformDetection::isJoomla()) {
13513 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
13514 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
13515 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
13516 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
13517 /**
13518 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
13519 */
13520 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
13521 }
13522 // load translation file
13523 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
13524 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
13525 /**
13526 * @wponly load language admin handler as well for WP.
13527 * We do this only because of WordPress, but in a way also compatible with Joomla as
13528 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
13529 */
13530 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
13531 }
13532
13533 if ($is_admin) {
13534 // back-end section of the site: check status
13535 if (VikChannelManager::loadExpirationDetails() === false) {
13536 // download and update the expiration details
13537 VikChannelManager::downloadExpirationDetails();
13538 }
13539 $expiration_reminder = null;
13540 if (VikChannelManager::isSubscriptionExpired()) {
13541 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
13542 } else {
13543 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
13544 // we also check if an update is available
13545 if (VikChannelManager::checkUpdates()) {
13546 // an update is more important
13547 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
13548 exit;
13549 }
13550 }
13551 // return reminder status
13552 return $expiration_reminder;
13553 }
13554
13555 // front-end section of the site: trigger alert, if any (bool)
13556 return VikChannelManager::checkSubscriptionReminder();
13557
13558 } catch (Exception $e) {
13559 // do nothing
13560 }
13561
13562 return null;
13563 }
13564
13565 /**
13566 * Given a list of website rate plans, returns the sorted list.
13567 * Naming score is the highest sorting method, pricing scoring is also supported.
13568 *
13569 * @param array $rate_plans the list of rate plan arrays to sort.
13570 * @param bool $assoc whether to keep the original array keys.
13571 *
13572 * @return array the sorted list of rate plan arrays.
13573 *
13574 * @since 1.15.0 (J) - 1.5.0 (WP)
13575 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
13576 */
13577 public static function sortRatePlans($rate_plans, $assoc = false)
13578 {
13579 if (!is_array($rate_plans) || count($rate_plans) < 2) {
13580 return $rate_plans;
13581 }
13582
13583 foreach ($rate_plans as $rp_ind => $rate_plan) {
13584 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
13585 // do not proceed in case of a weird array structure
13586 return $rate_plans;
13587 }
13588
13589 // count the sorting-score of this rate plan
13590 $sort_score = 0;
13591 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
13592 // "standard rate" rate plans should go first
13593 $sort_score += 3;
13594 } else {
13595 $sort_score--;
13596 }
13597
13598 // check for derived rate plan
13599 if (!empty($rate_plan['derived_id'])) {
13600 // derived rates should go last
13601 $sort_score -= 2;
13602 } else {
13603 // parent rates should go first
13604 $sort_score += 2;
13605 }
13606
13607 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
13608 // this doesn't seem to be a "non refundable rate" so it should go first
13609 $sort_score += 2;
13610 }
13611
13612 // set sorting score for this rate plan ID
13613 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13614 }
13615
13616 // apply custom sorting and keep index association
13617 uasort($rate_plans, function($a, $b) {
13618 // sort by name-scoring DESC
13619 $sort_diff = $b['sort_score'] - $a['sort_score'];
13620 if (isset($b['cost'])) {
13621 // apply also sorting by price ASC
13622 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13623 }
13624 return $sort_diff;
13625 });
13626
13627 return !$assoc ? array_values($rate_plans) : $rate_plans;
13628 }
13629
13630 /**
13631 * Returns an instance of the Availability helper class.
13632 *
13633 * @param bool $anew True for forcing a new instance.
13634 *
13635 * @return VikBookingAvailability
13636 *
13637 * @since 1.15.0 (J) - 1.5.0 (WP)
13638 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13639 */
13640 public static function getAvailabilityInstance($anew = false)
13641 {
13642 // require library
13643 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13644
13645 // return the instance of the class
13646 return VikBookingAvailability::getInstance($anew);
13647 }
13648
13649 /**
13650 * Helper method to quickly guess the internal dependency to import.
13651 *
13652 * @param string $path_id either a full path to a file or its identifier.
13653 *
13654 * @return bool true if the requested library was imported or false.
13655 *
13656 * @since 1.15.0 (J) - 1.5.0 (WP)
13657 */
13658 public static function import($path_id)
13659 {
13660 if (empty($path_id)) {
13661 return false;
13662 }
13663
13664 // always append .php extension
13665 $path_id = basename($path_id, '.php') . '.php';
13666
13667 if (is_file($path_id)) {
13668 // full path given
13669 require_once $path_id;
13670
13671 return true;
13672 }
13673
13674 // try guessing where the file id is located
13675 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13676 // library was found
13677 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13678
13679 return true;
13680 }
13681
13682 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13683 // library was found
13684 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13685
13686 return true;
13687 }
13688
13689 return false;
13690 }
13691 }
13692
13693 if (!class_exists('VikResizer'))
13694 {
13695 class VikResizer
13696 {
13697 public function __construct()
13698 {
13699 // objects of this class can also be instantiated without calling the methods statically.
13700 }
13701
13702 /**
13703 * Resizes an image proportionally. For PNG files it can optionally
13704 * trim the image to exclude the transparency, and add some padding to it.
13705 * All PNG files keep the alpha background in the resized version.
13706 *
13707 * @param string $fileimg path to original image file
13708 * @param string $dest path to destination image file
13709 * @param int $towidth
13710 * @param int $toheight
13711 * @param bool $trim_png remove empty background from image
13712 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13713 *
13714 * @return boolean
13715 */
13716 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13717 {
13718 if (!is_file($fileimg)) {
13719 return false;
13720 }
13721 if (empty($towidth) && empty($toheight)) {
13722 return copy($fileimg, $dest);
13723 }
13724
13725 $result = true;
13726
13727 list ($owid, $ohei, $type) = getimagesize($fileimg);
13728
13729 if ($owid > $towidth || $ohei > $toheight) {
13730 $xscale = $owid / $towidth;
13731 $yscale = $ohei / $toheight;
13732 if ($yscale > $xscale) {
13733 $new_width = round($owid * (1 / $yscale));
13734 $new_height = round($ohei * (1 / $yscale));
13735 } else {
13736 $new_width = round($owid * (1 / $xscale));
13737 $new_height = round($ohei * (1 / $xscale));
13738 }
13739
13740 $imageresized = imagecreatetruecolor($new_width, $new_height);
13741
13742 switch ($type) {
13743 case '1' :
13744 $imagetmp = imagecreatefromgif ($fileimg);
13745 break;
13746 case '2' :
13747 $imagetmp = imagecreatefromjpeg($fileimg);
13748 break;
13749 case '18' :
13750 $imagetmp = imagecreatefromwebp($fileimg);
13751 break;
13752 default :
13753 //keep alpha for PNG files
13754 $background = imagecolorallocate($imageresized, 0, 0, 0);
13755 imagecolortransparent($imageresized, $background);
13756 imagealphablending($imageresized, false);
13757 imagesavealpha($imageresized, true);
13758 //
13759 $imagetmp = imagecreatefrompng($fileimg);
13760 break;
13761 }
13762
13763 if (!$imagetmp) {
13764 return false;
13765 }
13766
13767 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13768
13769 switch ($type) {
13770 case '1' :
13771 imagegif($imageresized, $dest);
13772 break;
13773 case '2' :
13774 imagejpeg($imageresized, $dest);
13775 break;
13776 case '18' :
13777 imagewebp($imageresized, $dest);
13778 break;
13779 default :
13780 if ($trim_png) {
13781 self::imageTrim($imageresized, $background, $trim_pad);
13782 }
13783 imagepng($imageresized, $dest);
13784 break;
13785 }
13786
13787 imagedestroy($imageresized);
13788 } else {
13789 $result = copy($fileimg, $dest);
13790 }
13791
13792 if (VBOPlatformDetection::isWordPress()) {
13793 /**
13794 * @wponly in order to not lose resized files after installing an update,
13795 * we need to move any uploaded file onto a recovery folder.
13796 */
13797 VikBookingLoader::import('update.manager');
13798 VikBookingUpdateManager::triggerUploadBackup($dest);
13799 }
13800
13801 return $result;
13802 }
13803
13804 /**
13805 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13806 * trim the image to exclude the transparency, and add some padding to it.
13807 * All PNG files keep the alpha background in the resized version.
13808 *
13809 * @param resource $im Image link resource (reference)
13810 * @param int $bg imagecolorallocate color identifier
13811 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13812 *
13813 * @return void
13814 */
13815 public static function imagetrim(&$im, $bg, $pad = null)
13816 {
13817 // Calculate padding for each side.
13818 if (isset($pad)) {
13819 $pp = explode(' ', $pad);
13820 if (isset($pp[3])) {
13821 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13822 } elseif (isset($pp[2])) {
13823 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13824 } elseif (isset($pp[1])) {
13825 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13826 } else {
13827 $p = array_fill(0, 4, (int) $pp[0]);
13828 }
13829 } else {
13830 $p = array_fill(0, 4, 0);
13831 }
13832
13833 // Get the image width and height.
13834 $imw = imagesx($im);
13835 $imh = imagesy($im);
13836
13837 // Set the X variables.
13838 $xmin = $imw;
13839 $xmax = 0;
13840
13841 // Start scanning for the edges.
13842 for ($iy=0; $iy<$imh; $iy++) {
13843 $first = true;
13844 for ($ix=0; $ix<$imw; $ix++) {
13845 $ndx = imagecolorat($im, $ix, $iy);
13846 if ($ndx != $bg) {
13847 if ($xmin > $ix) {
13848 $xmin = $ix;
13849 }
13850 if ($xmax < $ix) {
13851 $xmax = $ix;
13852 }
13853 if (!isset($ymin)) {
13854 $ymin = $iy;
13855 }
13856 $ymax = $iy;
13857 if ($first) {
13858 $ix = $xmax;
13859 $first = false;
13860 }
13861 }
13862 }
13863 }
13864
13865 // The new width and height of the image. (not including padding)
13866 $imw = 1+$xmax-$xmin; // Image width in pixels
13867 $imh = 1+$ymax-$ymin; // Image height in pixels
13868
13869 // Make another image to place the trimmed version in.
13870 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13871
13872 // Make the background of the new image the same as the background of the old one.
13873 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13874 imagefill($im2, 0, 0, $bg2);
13875
13876 // Copy it over to the new image.
13877 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13878
13879 // To finish up, we replace the old image which is referenced.
13880 $im = $im2;
13881 }
13882
13883 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13884 {
13885 if (!file_exists($fileimg)) {
13886 return false;
13887 }
13888 if (empty($towidth) && empty($toheight)) {
13889 copy($fileimg, $dest);
13890 return true;
13891 }
13892
13893 $exp = explode(",", $rgb);
13894 if (count($exp) == 3) {
13895 $r = trim($exp[0]);
13896 $g = trim($exp[1]);
13897 $b = trim($exp[2]);
13898 } else {
13899 $r = 0;
13900 $g = 0;
13901 $b = 0;
13902 }
13903
13904 list ($owid, $ohei, $type) = getimagesize($fileimg);
13905
13906 if ($owid > $towidth || $ohei > $toheight) {
13907 $xscale = $owid / $towidth;
13908 $yscale = $ohei / $toheight;
13909 if ($yscale > $xscale) {
13910 $new_width = round($owid * (1 / $yscale));
13911 $new_height = round($ohei * (1 / $yscale));
13912 $ydest = 0;
13913 $diff = $towidth - $new_width;
13914 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13915 } else {
13916 $new_width = round($owid * (1 / $xscale));
13917 $new_height = round($ohei * (1 / $xscale));
13918 $xdest = 0;
13919 $diff = $toheight - $new_height;
13920 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13921 }
13922
13923 $imageresized = imagecreatetruecolor($towidth, $toheight);
13924
13925 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13926 imagefill($imageresized, 0, 0, $bgColor);
13927
13928 switch ($type) {
13929 case '1' :
13930 $imagetmp = imagecreatefromgif ($fileimg);
13931 break;
13932 case '2' :
13933 $imagetmp = imagecreatefromjpeg($fileimg);
13934 break;
13935 default :
13936 $imagetmp = imagecreatefrompng($fileimg);
13937 break;
13938 }
13939
13940 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13941
13942 switch ($type) {
13943 case '1' :
13944 imagegif ($imageresized, $dest);
13945 break;
13946 case '2' :
13947 imagejpeg($imageresized, $dest);
13948 break;
13949 default :
13950 imagepng($imageresized, $dest);
13951 break;
13952 }
13953
13954 imagedestroy($imageresized);
13955
13956 return true;
13957 } else {
13958 copy($fileimg, $dest);
13959 }
13960 return true;
13961 }
13962
13963 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13964 {
13965 if (!file_exists($fileimg)) {
13966 return false;
13967 }
13968 if (empty($towidth) && empty($toheight)) {
13969 copy($fileimg, $dest);
13970 return true;
13971 }
13972
13973 list ($owid, $ohei, $type) = getimagesize($fileimg);
13974
13975 if ($owid <= $ohei) {
13976 $new_width = $towidth;
13977 $new_height = ($towidth / $owid) * $ohei;
13978 } else {
13979 $new_height = $toheight;
13980 $new_width = ($new_height / $ohei) * $owid;
13981 }
13982
13983 switch ($type) {
13984 case '1' :
13985 $img_src = imagecreatefromgif ($fileimg);
13986 $img_dest = imagecreate($new_width, $new_height);
13987 break;
13988 case '2' :
13989 $img_src = imagecreatefromjpeg($fileimg);
13990 $img_dest = imagecreatetruecolor($new_width, $new_height);
13991 break;
13992 default :
13993 $img_src = imagecreatefrompng($fileimg);
13994 $img_dest = imagecreatetruecolor($new_width, $new_height);
13995 break;
13996 }
13997
13998 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13999
14000 switch ($type) {
14001 case '1' :
14002 $cropped = imagecreate($towidth, $toheight);
14003 break;
14004 case '2' :
14005 $cropped = imagecreatetruecolor($towidth, $toheight);
14006 break;
14007 default :
14008 $cropped = imagecreatetruecolor($towidth, $toheight);
14009 break;
14010 }
14011
14012 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
14013
14014 switch ($type) {
14015 case '1' :
14016 imagegif ($cropped, $dest);
14017 break;
14018 case '2' :
14019 imagejpeg($cropped, $dest);
14020 break;
14021 default :
14022 imagepng($cropped, $dest);
14023 break;
14024 }
14025
14026 imagedestroy($img_dest);
14027 imagedestroy($cropped);
14028
14029 return true;
14030 }
14031 }
14032 }
14033