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

14,055 lines 468.5 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 mixed $oid VBO booking ID or a linear array containing ID and secret key (timestamp).
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 * @since 1.18.13 (J) - 1.8.13 (WP) added support to $oid argument as either int or array.
1438 */
1439 public static function getVcmChatInstance($oid, $channel = null)
1440 {
1441 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1442 if (!is_file($vcm_messaging_helper)) {
1443 // VCM is not available
1444 return null;
1445 }
1446
1447 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1448 // VCM must be inactive, prevent the loading of the instance
1449 return null;
1450 }
1451
1452 // make sure the channel name is correct as it may contain the sub-network
1453 if (!empty($channel)) {
1454 $segments = explode('_', $channel);
1455 if (count($segments) > 1) {
1456 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1457 $channel = $segments[0];
1458 }
1459 }
1460
1461 // always require main file of the abstract class even if arguments are empty/invalid
1462 require_once $vcm_messaging_helper;
1463
1464 if (empty($oid) && empty($channel)) {
1465 // do not proceed in order to save resources
1466 return null;
1467 }
1468
1469 // return the instance of the class for this channel handler
1470 return VCMChatHandler::getInstance($oid, $channel);
1471 }
1472
1473 /**
1474 * Returns an instance of the VCMOpportunityHandler class to handle
1475 * the various opportunities through VCM.
1476 *
1477 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1478 *
1479 * @since 1.2.0
1480 */
1481 public static function getVcmOpportunityInstance() {
1482 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1483 if (!is_file($vcm_opp_helper)) {
1484 // VCM is not available
1485 return null;
1486 }
1487 // require main file of the class
1488 require_once $vcm_opp_helper;
1489 // return the instance of the class
1490 return VCMOpportunityHandler::getInstance();
1491 }
1492
1493 /**
1494 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1495 *
1496 * @param array $data optional data to bind to the object.
1497 *
1498 * @return bool
1499 *
1500 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1501 *
1502 * @requires VCM >= 1.8.24
1503 */
1504 public static function vcmBcomReportingSupported(array $data = [])
1505 {
1506 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1507 }
1508
1509 /**
1510 * Gets a list of all channels supporting the promotions.
1511 *
1512 * @param string $key the key of the handler.
1513 *
1514 * @return mixed false if VCM is not installed, empty array otherwise.
1515 *
1516 * @requires Vik Channel Manager 1.7.1
1517 *
1518 * @since 1.3.0
1519 */
1520 public static function getPromotionHandlers($key = null)
1521 {
1522 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1523 // VCM not installed
1524 return false;
1525 }
1526
1527 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1528 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1529 return [];
1530 }
1531
1532 return VikChannelManager::getPromotionHandlers($key);
1533 }
1534
1535 /**
1536 * Gets the factors for suggesting the application of the promotions.
1537 *
1538 * @param mixed $data some optional instructions to be passed as argument.
1539 *
1540 * @return mixed false if VCM is not installed, associative array otherwise.
1541 *
1542 * @requires Vik Channel Manager 1.7.1
1543 *
1544 * @since 1.3.0
1545 */
1546 public static function getPromotionFactors($data = null)
1547 {
1548 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1549 // VCM not installed
1550 return false;
1551 }
1552
1553 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1554 // VCM is outdated
1555 return false;
1556 }
1557
1558 return VikChannelManager::getPromotionFactors($data);
1559 }
1560
1561 public static function getTheme()
1562 {
1563 return VBOFactory::getConfig()->get('theme', '');
1564 }
1565
1566 public static function getFooterOrdMail($vbo_tn = null)
1567 {
1568 $dbo = JFactory::getDbo();
1569
1570 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1571 $dbo->setQuery($q);
1572 $ft = $dbo->loadAssocList();
1573
1574 if (!is_object($vbo_tn)) {
1575 $vbo_tn = self::getTranslator();
1576 }
1577
1578 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1579
1580 return $ft ? $ft[0]['setting'] : '';
1581 }
1582
1583 public static function requireLogin()
1584 {
1585 return VBOFactory::getConfig()->getBool('requirelogin', false);
1586 }
1587
1588 public static function autoRoomUnit()
1589 {
1590 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1591 }
1592
1593 public static function todayBookings()
1594 {
1595 static $todayBookings = null;
1596
1597 if ($todayBookings === null) {
1598 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1599 }
1600
1601 return $todayBookings;
1602 }
1603
1604 public static function couponsEnabled()
1605 {
1606 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1607 }
1608
1609 public static function customersPinEnabled()
1610 {
1611 return VBOFactory::getConfig()->getBool('enablepin', false);
1612 }
1613
1614 /**
1615 * Detects the type of visitor from the user agent.
1616 * Known types are: computer, smartphone, tablet.
1617 *
1618 * @param boolean $returnua whether the type of visitor should be returned. If false
1619 * boolean is returned in case of mobile device detected.
1620 * @param boolean $loadassets whether the system should load an apposite CSS file.
1621 *
1622 * @return mixed string for the type of visitor or boolean if mobile detected.
1623 *
1624 * @since 1.0.13 - Revision September 2018
1625 */
1626 public static function detectUserAgent($returnua = false, $loadassets = true) {
1627 $session = JFactory::getSession();
1628 $sval = $session->get('vbuseragent', '');
1629 $mobiles = array('tablet', 'smartphone');
1630 if (!empty($sval)) {
1631 if ($loadassets) {
1632 self::userAgentStyleSheet($sval);
1633 }
1634 return $returnua ? $sval : in_array($sval, $mobiles);
1635 }
1636
1637 // detect visitor (device) type
1638 $visitoris = 'computer';
1639 try {
1640 if (!class_exists('MobileDetector')) {
1641 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1642 }
1643 $detector = new MobileDetector;
1644 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1645 } catch (Exception $e) {
1646 // do nothing
1647 }
1648
1649 $session->set('vbuseragent', $visitoris);
1650 if ($loadassets) {
1651 self::userAgentStyleSheet($visitoris);
1652 }
1653
1654 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1655 }
1656
1657 public static function userAgentStyleSheet($ua)
1658 {
1659 /**
1660 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1661 */
1662 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1663 return;
1664 }
1665 //
1666
1667 $document = JFactory::getDocument();
1668 /**
1669 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1670 */
1671 if ($ua == 'smartphone') {
1672 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1673 } elseif ($ua == 'tablet') {
1674 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1675 }
1676 return true;
1677 }
1678
1679 public static function loadJquery()
1680 {
1681 return VBOFactory::getConfig()->getBool('loadjquery', true);
1682 }
1683
1684 public static function loadBootstrap()
1685 {
1686 return VBOFactory::getConfig()->getBool('bootstrap', true);
1687 }
1688
1689 public static function allowMultiLanguage()
1690 {
1691 return VBOFactory::getConfig()->getBool('multilang', true);
1692 }
1693
1694 public static function getTranslator()
1695 {
1696 if (!class_exists('VikBookingTranslator')) {
1697 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1698 }
1699
1700 return new VikBookingTranslator();
1701 }
1702
1703 /**
1704 * New method's name spelled correctly. The object will be cached by default.
1705 *
1706 * @return VikBookingCustomersPin
1707 *
1708 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1709 */
1710 public static function getCPinInstance()
1711 {
1712 static $cpin_instance = null;
1713
1714 if (!$cpin_instance) {
1715 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1716
1717 $cpin_instance = new VikBookingCustomersPin();
1718 }
1719
1720 return $cpin_instance;
1721 }
1722
1723 /**
1724 * Old method's name spelled wrongly for BC.
1725 *
1726 * @return VikBookingCustomersPin
1727 */
1728 public static function getCPinIstance()
1729 {
1730 return self::getCPinInstance();
1731 }
1732
1733 /**
1734 * Returns an instance of the VikBookingTracker Class.
1735 * It is also possible to call this method to just require the library.
1736 * This is useful for the back-end to access some static methods
1737 * without tracking any data.
1738 *
1739 * @param boolean $require_only whether to return the object.
1740 *
1741 * @return VikBookingTracker
1742 *
1743 * @since 1.11
1744 */
1745 public static function getTracker($require_only = false) {
1746 if (!class_exists('VikBookingTracker')) {
1747 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1748 }
1749 return $require_only ? true : VikBookingTracker::getInstance();
1750 }
1751
1752 /**
1753 * Returns an instance of the VikBookingOperator Class.
1754 *
1755 * @return VikBookingOperator
1756 *
1757 * @since 1.11
1758 */
1759 public static function getOperatorInstance() {
1760 if (!class_exists('VikBookingOperator')) {
1761 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1762 }
1763 return VikBookingOperator::getInstance();
1764 }
1765
1766 /**
1767 * Returns an instance of the VikBookingFestivities Class.
1768 *
1769 * @return VikBookingFestivities
1770 *
1771 * @since 1.12
1772 */
1773 public static function getFestivitiesInstance() {
1774 if (!class_exists('VikBookingFestivities')) {
1775 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1776 }
1777 return VikBookingFestivities::getInstance();
1778 }
1779
1780 /**
1781 * Returns an instance of the VikBookingCriticalDates Class.
1782 *
1783 * @return VikBookingCriticalDates
1784 *
1785 * @since 1.13.5
1786 */
1787 public static function getCriticalDatesInstance() {
1788 if (!class_exists('VikBookingCriticalDates')) {
1789 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1790 }
1791 return VikBookingCriticalDates::getInstance();
1792 }
1793
1794 /**
1795 * Checks whether the chat is enabled.
1796 *
1797 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1798 *
1799 * @since 1.12
1800 */
1801 public static function chatEnabled() {
1802 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1803 return -1;
1804 }
1805 $dbo = JFactory::getDbo();
1806 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1807 $dbo->setQuery($q);
1808 $dbo->execute();
1809 if (!$dbo->getNumRows()) {
1810 // enable the chat by default if VCM is installed
1811 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1812 $dbo->setQuery($q);
1813 $dbo->execute();
1814 return 1;
1815 }
1816 $s = $dbo->loadResult();
1817 return intval($s);
1818 }
1819
1820 /**
1821 * Loads the chat parameters from the configuration.
1822 *
1823 * @return object stdClass object from decoded JSON string
1824 *
1825 * @since 1.12
1826 */
1827 public static function getChatParams() {
1828 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1829 return new stdClass;
1830 }
1831 $dbo = JFactory::getDbo();
1832 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1833 $dbo->setQuery($q);
1834 $dbo->execute();
1835 if (!$dbo->getNumRows()) {
1836 // compose default and basic chat params for the first time
1837 $basic_params = new stdClass;
1838 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1839 $basic_params->av_type = 'checkin';
1840 $basic_params->av_days = 0;
1841
1842 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1843 $dbo->setQuery($q);
1844 $dbo->execute();
1845
1846 return $basic_params;
1847 }
1848 return json_decode($dbo->loadResult());
1849 }
1850
1851 /**
1852 * Checks whether the pre-checkin is enabled.
1853 *
1854 * @return int 0 if disabled, 1 otherwise
1855 *
1856 * @since 1.12
1857 */
1858 public static function precheckinEnabled()
1859 {
1860 return (int) VBOFactory::getConfig()->getBool('precheckinenabled', true);
1861 }
1862
1863 /**
1864 * Returns the minimum number of days in advance to
1865 * enable the pre-checkin via front-end.
1866 *
1867 * @return int the min number of days in advance for pre-checkin.
1868 *
1869 * @since 1.12
1870 */
1871 public static function precheckinMinOffset()
1872 {
1873 return VBOFactory::getConfig()->getInt('precheckinminoffset', 0);
1874 }
1875
1876 /**
1877 * Whether upselling extra services is enabled.
1878 *
1879 * @return boolean true if enabled, false otherwise.
1880 *
1881 * @since 1.13
1882 */
1883 public static function upsellingEnabled()
1884 {
1885 return VBOFactory::getConfig()->getBool('upselling', true);
1886 }
1887
1888 /**
1889 * Collects a list of options/extra that can be upsold for the
1890 * the rooms booked.
1891 *
1892 * @param array $upsell_data list of stdClass objects for each room booked.
1893 * @param array $info some booking details (id, checkin, checkout).
1894 * @param object $vbo_tn the translation object.
1895 *
1896 * @return array list of upsellable options for each room booked.
1897 *
1898 * @since 1.13 (J) - 1.3.0 (WP)
1899 * @since 1.17.7 (J) - 1.7.7 (WP) added support for damage deposit to confirmed bookings.
1900 */
1901 public static function loadUpsellingData($upsell_data, $info, $vbo_tn)
1902 {
1903 $dbo = JFactory::getDbo();
1904
1905 // get all rooms booked
1906 $all_room_ids = [];
1907 foreach ($upsell_data as $v) {
1908 if (!in_array($v->id, $all_room_ids)) {
1909 array_push($all_room_ids, (int) $v->id);
1910 }
1911 }
1912
1913 $dbo->setQuery(
1914 $dbo->getQuery(true)
1915 ->select([
1916 $dbo->qn('id'),
1917 $dbo->qn('idopt'),
1918 ])
1919 ->from($dbo->qn('#__vikbooking_rooms'))
1920 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_room_ids) . ')')
1921 );
1922 $records = $dbo->loadAssocList();
1923 if (!$records) {
1924 // no rooms found
1925 return [];
1926 }
1927
1928 // get all suitable options
1929 $all_options = [];
1930 $rooms_options = [];
1931 foreach ($records as $v) {
1932 $allopts = explode(';', $v['idopt']);
1933 $room_opt = [];
1934 foreach ($allopts as $o) {
1935 if (empty($o)) {
1936 continue;
1937 }
1938 if (!in_array($o, $all_options)) {
1939 array_push($all_options, (int) $o);
1940 }
1941 array_push($room_opt, (int) $o);
1942 }
1943 $rooms_options[$v['id']] = $room_opt;
1944 }
1945 if (!$all_options) {
1946 // no options found
1947 return [];
1948 }
1949
1950 // load all options that could be used by the booked rooms no matter what was already booked
1951 $dbo->setQuery(
1952 $dbo->getQuery(true)
1953 ->select('*')
1954 ->from($dbo->qn('#__vikbooking_optionals'))
1955 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1956 ->where($dbo->qn('forcesel') . ' = 0')
1957 ->where($dbo->qn('ifchildren') . ' = 0')
1958 ->where($dbo->qn('is_citytax') . ' = 0')
1959 ->where($dbo->qn('is_fee') . ' = 0')
1960 ->order($dbo->qn('ordering') . ' ASC')
1961 );
1962 $records = $dbo->loadAssocList();
1963
1964 if (!strcasecmp(($info['status'] ?? ''), 'confirmed')) {
1965 // merge mandatory damage deposit
1966 $dbo->setQuery(
1967 $dbo->getQuery(true)
1968 ->select('*')
1969 ->from($dbo->qn('#__vikbooking_optionals'))
1970 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1971 ->where($dbo->qn('forcesel') . ' = 1')
1972 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
1973 );
1974 $dd_records = $dbo->loadAssocList();
1975
1976 // merge with regular options
1977 $records = array_merge($records, $dd_records);
1978 }
1979
1980 if (!$records) {
1981 // no upsell-able options found
1982 return [];
1983 }
1984
1985 // filter options by available date and translate records
1986 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1987 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1988 $records = !is_array($records) ? [] : $records;
1989
1990 $tot_upsellable = 0;
1991 foreach ($upsell_data as $k => $rdata) {
1992 if (!isset($upsell_data[$k]->upsellable)) {
1993 $upsell_data[$k]->upsellable = [];
1994 }
1995 foreach ($records as $opt) {
1996 if (!empty($opt['ageintervals'])) {
1997 // upsellable options should not contain age intervals for children
1998 continue;
1999 }
2000 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
2001 // this option is not assigned to this room
2002 continue;
2003 }
2004 // check if the option is suited for this room party
2005 $clone_opt = [$opt];
2006 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
2007 if (!is_array($clone_opt) || !$clone_opt) {
2008 // this option is not suited for this room party
2009 continue;
2010 }
2011
2012 if (in_array($opt['id'], $rdata->options)) {
2013 // this option has already been booked
2014 continue;
2015 }
2016
2017 // push this option and increase counter
2018 array_push($upsell_data[$k]->upsellable, $opt);
2019 $tot_upsellable++;
2020 }
2021 }
2022
2023 // if no upsellable options were found, we return an empty array
2024 return $tot_upsellable > 0 ? $upsell_data : [];
2025 }
2026
2027 /**
2028 * Returns the minimum days in advance for booking, by considering by default
2029 * also the property closing dates. If the property is currently closed, then
2030 * the minimum number of days in advance will be increased.
2031 *
2032 * @param bool $no_closing_dates whether to skip checking closing dates.
2033 *
2034 * @return int the number of days in advance for booking.
2035 *
2036 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
2037 * getMinDaysAdvance($skipsession = false) to:
2038 * getMinDaysAdvance($no_closing_dates = false)
2039 * This was made to return the proper number of days in advance
2040 * in case the property is currently closed.
2041 */
2042 public static function getMinDaysAdvance($no_closing_dates = false)
2043 {
2044 // cache value in static var
2045 static $getMinDaysAdvance = null;
2046
2047 if ($getMinDaysAdvance) {
2048 return $getMinDaysAdvance;
2049 }
2050
2051 $mind = VBOFactory::getConfig()->getInt('mindaysadvance', 0);
2052
2053 // update cached var
2054 $getMinDaysAdvance = $mind;
2055
2056 if ($no_closing_dates) {
2057 // do not check the closing dates
2058 return $mind;
2059 }
2060
2061 // check if the property is currently closed
2062 $cur_closed_dates = self::getClosingDates();
2063 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
2064 $today_midnight = mktime(0, 0, 0);
2065 $closed_until = null;
2066 foreach ($cur_closed_dates as $kcd => $vcd) {
2067 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
2068 // closing period found
2069 $closed_until = $vcd['to'];
2070 break;
2071 }
2072 }
2073 if ($closed_until !== null) {
2074 // count the number of days until property is closed
2075 $mind = 0;
2076 $today_info = getdate($today_midnight);
2077 while ($today_info[0] <= $closed_until) {
2078 $mind++;
2079 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
2080 }
2081 }
2082 }
2083
2084 // update cached var
2085 $getMinDaysAdvance = $mind;
2086
2087 return $mind;
2088 }
2089
2090 public static function getDefaultNightsCalendar()
2091 {
2092 return VBOFactory::getConfig()->getInt('autodefcalnights', 1);
2093 }
2094
2095 public static function getSearchNumRooms($skipsession = true)
2096 {
2097 $dbo = JFactory::getDbo();
2098 if ($skipsession) {
2099 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2100 $dbo->setQuery($q);
2101 $dbo->execute();
2102 $s = $dbo->loadAssocList();
2103 return (int)$s[0]['setting'];
2104 }
2105 $session = JFactory::getSession();
2106 $sval = $session->get('vbsearchNumRooms', '');
2107 if (!empty($sval)) {
2108 return (int)$sval;
2109 }
2110 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2111 $dbo->setQuery($q);
2112 $dbo->execute();
2113 $s = $dbo->loadAssocList();
2114 $session->set('vbsearchNumRooms', $s[0]['setting']);
2115 return (int)$s[0]['setting'];
2116 }
2117
2118 public static function getSearchNumAdults($skipsession = true)
2119 {
2120 $dbo = JFactory::getDbo();
2121 if ($skipsession) {
2122 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2123 $dbo->setQuery($q);
2124 $dbo->execute();
2125 $s = $dbo->loadAssocList();
2126 return $s[0]['setting'];
2127 }
2128 $session = JFactory::getSession();
2129 $sval = $session->get('vbsearchNumAdults', '');
2130 if (!empty($sval)) {
2131 return $sval;
2132 }
2133 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2134 $dbo->setQuery($q);
2135 $dbo->execute();
2136 $s = $dbo->loadAssocList();
2137 $session->set('vbsearchNumAdults', $s[0]['setting']);
2138 return $s[0]['setting'];
2139 }
2140
2141 public static function getSearchNumChildren($skipsession = true)
2142 {
2143 $dbo = JFactory::getDbo();
2144 if ($skipsession) {
2145 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2146 $dbo->setQuery($q);
2147 $dbo->execute();
2148 $s = $dbo->loadAssocList();
2149 return $s[0]['setting'];
2150 }
2151 $session = JFactory::getSession();
2152 $sval = $session->get('vbsearchNumChildren', '');
2153 if (!empty($sval)) {
2154 return $sval;
2155 }
2156 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2157 $dbo->setQuery($q);
2158 $dbo->execute();
2159 $s = $dbo->loadAssocList();
2160 $session->set('vbsearchNumChildren', $s[0]['setting']);
2161 return $s[0]['setting'];
2162 }
2163
2164 public static function getSmartSearchType($skipsession = true)
2165 {
2166 $dbo = JFactory::getDbo();
2167 if ($skipsession) {
2168 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2169 $dbo->setQuery($q);
2170 $dbo->execute();
2171 $s = $dbo->loadAssocList();
2172 return $s[0]['setting'];
2173 }
2174 $session = JFactory::getSession();
2175 $sval = $session->get('vbsmartSearchType', '');
2176 if (!empty($sval)) {
2177 return $sval;
2178 }
2179 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2180 $dbo->setQuery($q);
2181 $dbo->execute();
2182 $s = $dbo->loadAssocList();
2183 $session->set('vbsmartSearchType', $s[0]['setting']);
2184 return $s[0]['setting'];
2185 }
2186
2187 /**
2188 * Retrieves the listing-level minimum advance booking offset.
2189 *
2190 * @param int $idroom The listing ID to evaluate.
2191 * @param bool $period Whether to format the value as a period string.
2192 *
2193 * @return int|string|null Number of hours or period string ("2H"), if set.
2194 *
2195 * @since 1.18.3 (J) - 1.8.3 (WP)
2196 */
2197 public static function getMinDateFutureListing($idroom, $period = false)
2198 {
2199 $config = VBOFactory::getConfig();
2200
2201 $room_level_min_notice = $config->get("room_{$idroom}_min_adv_notice", null);
2202
2203 if (is_null($room_level_min_notice) || $room_level_min_notice === '') {
2204 return null;
2205 }
2206
2207 if ($period) {
2208 return sprintf('%dH', (int) $room_level_min_notice);
2209 }
2210
2211 return (int) $room_level_min_notice;
2212 }
2213
2214 /**
2215 * Returns the maximum advance booking notice (offset) as a period.
2216 * It either returns the global configuration setting, or the value
2217 * defined at room-level, if any and if requested.
2218 *
2219 * @param int $idroom optional room ID for room-level setting.
2220 *
2221 * @return string the maximum booking notice period (i.e. +1Y).
2222 *
2223 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
2224 * $idroom to support room-level settings.
2225 */
2226 public static function getMaxDateFuture($idroom = 0)
2227 {
2228 static $max_adv_book_pool = [];
2229
2230 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
2231
2232 if (isset($max_adv_book_pool[$pool_id])) {
2233 return $max_adv_book_pool[$pool_id];
2234 }
2235
2236 $config = VBOFactory::getConfig();
2237
2238 if ($pool_id === 'global') {
2239 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
2240 } else {
2241 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
2242 if (!empty($room_level_max_notice)) {
2243 // setting is defined at room-level
2244 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2245 } else {
2246 // recurse for global setting
2247 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2248 }
2249 }
2250
2251 return $max_adv_book_pool[$pool_id];
2252 }
2253
2254 /**
2255 * Validates the maximum advance booking notice against the requested check-in
2256 * timestamp. The method supports the validation at both room and property levels.
2257 *
2258 * @param int $checkints the check-in date timestamp.
2259 * @param int $idroom the optional room ID for room-level validation.
2260 *
2261 * @return string error date string in case of validation failure.
2262 *
2263 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2264 */
2265 public static function validateMaxDateBookings($checkints, $idroom = 0)
2266 {
2267 $datelim = self::getMaxDateFuture($idroom);
2268 $datelim = empty($datelim) ? '+2y' : $datelim;
2269 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2270 $quantlim = substr($datelim, -1, 1);
2271
2272 $now = getdate();
2273 if ($quantlim == 'w') {
2274 $until_ts = strtotime("+$numlim weeks") + 86399;
2275 } else {
2276 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2277 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2278 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2279 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2280 }
2281
2282 if ($until_ts > $now[0] && $checkints > $until_ts) {
2283 $vbo_df = self::getDateFormat();
2284 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2285
2286 // error, return the maximum date in the future allowed
2287 return date($df, $until_ts);
2288 }
2289
2290 // validation passed
2291 return '';
2292 }
2293
2294 /**
2295 * Validates the minimum advance booking notice against the requested check-in
2296 * timestamp. The method supports the validation at both room and property levels.
2297 *
2298 * @param int $checkints The check-in date timestamp.
2299 * @param int $idroom Optional room ID for room-level validation.
2300 *
2301 * @return int|float Minimum days (int) or hours (float) in advance required.
2302 *
2303 * @since 1.18.3 (J) - 1.8.3 (WP) introduced $idroom argument for room-level.
2304 */
2305 public static function validateMinDaysAdvance($checkints, $idroom = 0)
2306 {
2307 if ($idroom) {
2308 // listing-level minimum advance booking notice (hours, could be null or 0)
2309 $minhadv = self::getMinDateFutureListing($idroom);
2310
2311 // perform listing-level validation only when value is set
2312 if (!$minhadv) {
2313 // validation passed
2314 return '';
2315 }
2316
2317 // access listing params
2318 $listing_data = self::getRoomInfo($idroom, ['params'], $no_cache = true);
2319 if ($listing_data['params'] ?? null) {
2320 // access listing-level check-in time
2321 $listing_checkin = self::getRoomParam('checkin', $listing_data['params']);
2322 if ($listing_checkin) {
2323 // overwrite check-in time
2324 $listing_checkin_parts = explode(':', $listing_checkin);
2325 $checkin_hours = (int) $listing_checkin_parts[0];
2326 $checkin_minutes = (int) $listing_checkin_parts[1];
2327 // read current check-in date-time
2328 $tsinfo = getdate($checkints);
2329 // update check-in time with proper hours, minutes and seconds
2330 $checkints = mktime($checkin_hours, $checkin_minutes, 0, $tsinfo['mon'], $tsinfo['mday'], $tsinfo['year']);
2331 }
2332 }
2333
2334 if ($checkints < strtotime('+' . $minhadv . ($minhadv > 1 ? 'hours' : 'hour'))) {
2335 return $minhadv >= 24 ? floor($minhadv / 24) : ($minhadv / 24);
2336 }
2337
2338 // validation passed without checking the global settings when listing ID given
2339 return '';
2340 }
2341
2342 // global validation
2343 $mindadv = self::getMinDaysAdvance(true);
2344 if ($mindadv > 0) {
2345 $tsinfo = getdate($checkints);
2346 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2347 if ($checkints < $limit_ts) {
2348 return $mindadv;
2349 }
2350 }
2351
2352 // validation passed
2353 return '';
2354 }
2355
2356 /**
2357 * The only supported calendar type has been changed to jQuery UI.
2358 *
2359 * @since 1.13
2360 */
2361 public static function calendarType($skipsession = false)
2362 {
2363 return 'jqueryui';
2364 }
2365
2366 public static function getSiteLogo()
2367 {
2368 static $siteLogo = null;
2369
2370 if ($siteLogo !== null) {
2371 return $siteLogo;
2372 }
2373
2374 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2375
2376 return $siteLogo;
2377 }
2378
2379 public static function getBackendLogo()
2380 {
2381 static $backLogo = null;
2382
2383 if ($backLogo !== null) {
2384 return $backLogo;
2385 }
2386
2387 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2388
2389 return $backLogo;
2390 }
2391
2392 public static function numCalendars()
2393 {
2394 static $numCalendars = null;
2395
2396 if ($numCalendars !== null) {
2397 return $numCalendars;
2398 }
2399
2400 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2401
2402 return $numCalendars;
2403 }
2404
2405 public static function getFirstWeekDay()
2406 {
2407 static $firstWeekDay = null;
2408
2409 if ($firstWeekDay !== null) {
2410 return $firstWeekDay;
2411 }
2412
2413 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2414
2415 return $firstWeekDay;
2416 }
2417
2418 public static function showPartlyReserved()
2419 {
2420 static $partlyReserved = null;
2421
2422 if ($partlyReserved !== null) {
2423 return $partlyReserved;
2424 }
2425
2426 $partlyReserved = VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2427
2428 return $partlyReserved;
2429 }
2430
2431 public static function showStatusCheckinoutOnly()
2432 {
2433 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2434 }
2435
2436 public static function getDisclaimer($vbo_tn = null)
2437 {
2438 $dbo = JFactory::getDbo();
2439
2440 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2441 $dbo->setQuery($q);
2442 $ft = $dbo->loadAssocList();
2443 if (!$ft) {
2444 return '';
2445 }
2446
2447 if (!is_object($vbo_tn)) {
2448 $vbo_tn = self::getTranslator();
2449 }
2450 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2451
2452 return $ft[0]['setting'];
2453 }
2454
2455 public static function showFooter()
2456 {
2457 return VBOFactory::getConfig()->getBool('showfooter', false);
2458 }
2459
2460 public static function getPriceName($idp, $vbo_tn = null)
2461 {
2462 $dbo = JFactory::getDbo();
2463
2464 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2465 $dbo->setQuery($q, 0, 1);
2466 $n = $dbo->loadAssocList();
2467 if ($n) {
2468 if (is_object($vbo_tn)) {
2469 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2470 }
2471 return $n[0]['name'];
2472 }
2473
2474 return "";
2475 }
2476
2477 public static function getPriceAttr($idp, $vbo_tn = null)
2478 {
2479 $dbo = JFactory::getDbo();
2480
2481 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2482 $dbo->setQuery($q, 0, 1);
2483 $n = $dbo->loadAssocList();
2484 if ($n) {
2485 if (is_object($vbo_tn)) {
2486 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2487 }
2488 return $n[0]['attr'];
2489 }
2490
2491 return "";
2492 }
2493
2494 /**
2495 * Fetches the requested rate plan information.
2496 *
2497 * @param int $idp the rate plan ID.
2498 * @param object $vbo_tn the Vik Booking translator object.
2499 *
2500 * @return array empty array or associative record.
2501 */
2502 public static function getPriceInfo($idp, $vbo_tn = null)
2503 {
2504 // cache values
2505 static $priceInfos = [];
2506
2507 $idp = (int)$idp;
2508
2509 if ($priceInfos && isset($priceInfos[$idp])) {
2510 return $priceInfos[$idp];
2511 }
2512
2513 $dbo = JFactory::getDbo();
2514
2515 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2516 $dbo->setQuery($q, 0, 1);
2517 $rplan = $dbo->loadAssocList();
2518 if (!$rplan) {
2519 return [];
2520 }
2521
2522 if (is_object($vbo_tn)) {
2523 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2524 }
2525
2526 // set cached value
2527 $priceInfos[$idp] = $rplan[0];
2528
2529 return $rplan[0];
2530 }
2531
2532 public static function getAliq($idal)
2533 {
2534 $dbo = JFactory::getDbo();
2535
2536 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2537 $dbo->setQuery($q);
2538 $aliq = $dbo->loadResult();
2539 if ($aliq) {
2540 return $aliq;
2541 }
2542
2543 return 0;
2544 }
2545
2546 public static function getTimeOpenStore($skipsession = false)
2547 {
2548 // cache value in static var
2549 static $getTimeOpenStore = null;
2550
2551 if ($getTimeOpenStore) {
2552 return $getTimeOpenStore;
2553 }
2554 //
2555
2556 $dbo = JFactory::getDbo();
2557 $session = JFactory::getSession();
2558
2559 if ($skipsession) {
2560 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2561 if (empty($setting) && $setting != "0") {
2562 return false;
2563 } else {
2564 $x = explode("-", $setting);
2565 if (!empty($x[1]) && $x[1] != "0") {
2566 $getTimeOpenStore = $x;
2567 return $x;
2568 }
2569 }
2570 } else {
2571 $sval = $session->get('vbgetTimeOpenStore', '');
2572 if (!empty($sval)) {
2573 return $sval;
2574 } else {
2575 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2576 if (empty($setting) && $setting != "0") {
2577 return false;
2578 } else {
2579 $x = explode("-", $setting);
2580 if (!empty($x[1]) && $x[1] != "0") {
2581 $session->set('vbgetTimeOpenStore', $x);
2582 $getTimeOpenStore = $x;
2583 return $x;
2584 }
2585 }
2586 }
2587 }
2588 return false;
2589 }
2590
2591 public static function getHoursMinutes($secs)
2592 {
2593 if ($secs >= 3600) {
2594 $op = $secs / 3600;
2595 $hours = floor($op);
2596 $less = $hours * 3600;
2597 $newsec = $secs - $less;
2598 $optwo = $newsec / 60;
2599 $minutes = floor($optwo);
2600 } else {
2601 $hours = "0";
2602 $optwo = $secs / 60;
2603 $minutes = floor($optwo);
2604 }
2605
2606 return [$hours, $minutes];
2607 }
2608
2609 public static function getClosingDates()
2610 {
2611 // cache value in static var
2612 static $getClosingDates = null;
2613
2614 if (is_array($getClosingDates)) {
2615 return $getClosingDates;
2616 }
2617
2618 $dbo = JFactory::getDbo();
2619
2620 $allcd = [];
2621
2622 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2623 $dbo->setQuery($q);
2624 $s = $dbo->loadAssocList();
2625 if ($s && !empty($s[0]['setting'])) {
2626 $allcd = json_decode($s[0]['setting'], true);
2627 $allcd = is_array($allcd) ? $allcd : array();
2628 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2629 foreach ($allcd as $k => $v) {
2630 if ($v['to'] < $base_ts) {
2631 unset($allcd[$k]);
2632 }
2633 }
2634 $allcd = array_values($allcd);
2635 }
2636
2637 $getClosingDates = $allcd;
2638
2639 return $allcd;
2640 }
2641
2642 public static function parseJsClosingDates()
2643 {
2644 $cd = self::getClosingDates();
2645 if (!$cd) {
2646 return [];
2647 }
2648
2649 $cdjs = [];
2650 foreach ($cd as $k => $v) {
2651 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2652 }
2653
2654 return $cdjs;
2655 }
2656
2657 /**
2658 * Given stay date timestamps, tells if the booking is disallowed due to global closing dates.
2659 *
2660 * @param int $checkints The check-in date timestamp.
2661 * @param int $checkoutts The check-in date timestamp.
2662 * @param ?string $df Optional date format to return the closed stay dates.
2663 * @param bool $closingcmp True to obtain the stay components affected by a global closure.
2664 *
2665 * @return string|array Empty string if no closures, closure dates string or affected components list.
2666 *
2667 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $closingcmp to improve the search suggestion details.
2668 */
2669 public static function validateClosingDates($checkints, $checkoutts, $df = null, $closingcmp = false)
2670 {
2671 $cd = self::getClosingDates();
2672
2673 if (!$cd) {
2674 return '';
2675 }
2676
2677 $df = empty($df) ? 'Y-m-d' : $df;
2678 $margin_seconds = 22 * 60 * 60;
2679
2680 foreach ($cd as $k => $v) {
2681 // determine if the stay dates are closed
2682 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2683 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2684 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2685
2686 if ($inner_closed || $outer_closed || $middle_closed) {
2687 // stay dates are closed
2688 if ($closingcmp) {
2689 // return the components affected by the globally closed dates to allow analysis
2690 return [
2691 'checkin' => $inner_closed,
2692 'checkout' => $outer_closed,
2693 'stay' => $middle_closed,
2694 ];
2695 }
2696
2697 // return the string with the range of dates closed globally
2698 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2699 }
2700 }
2701
2702 return '';
2703 }
2704
2705 /**
2706 * Whether the categories dropdown filter menu should be displayed.
2707 *
2708 * @param bool $skipsession [optional] re-read the configuration setting.
2709 *
2710 * @return bool
2711 *
2712 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2713 */
2714 public static function showCategoriesFront($skipsession = true)
2715 {
2716 return VBOFactory::getConfig()->getBool('showcategories', false);
2717 }
2718
2719 /**
2720 * Whether the number of children dropdown menu should be displayed.
2721 * Defaults to skip the session values and to re-read the configuration setting.
2722 *
2723 * @param bool $skipsession [optional] re-read the configuration setting.
2724 *
2725 * @return bool
2726 *
2727 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2728 */
2729 public static function showChildrenFront($skipsession = true)
2730 {
2731 return VBOFactory::getConfig()->getBool('showchildren', true);
2732 }
2733
2734 public static function allowBooking()
2735 {
2736 // cache value in static var
2737 static $allowBooking = null;
2738
2739 if (is_bool($allowBooking)) {
2740 return $allowBooking;
2741 }
2742
2743 $allowBooking = (bool) VBOFactory::getConfig()->get('allowbooking');
2744
2745 return $allowBooking;
2746 }
2747
2748 public static function getDisabledBookingMsg($vbo_tn = null) {
2749 $dbo = JFactory::getDbo();
2750 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2751 $dbo->setQuery($q);
2752 $dbo->execute();
2753 $s = $dbo->loadAssocList();
2754 if (!is_object($vbo_tn)) {
2755 $vbo_tn = self::getTranslator();
2756 }
2757 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2758 return $s[0]['setting'];
2759 }
2760
2761 /**
2762 * Returns the text for the guests allowed policy.
2763 *
2764 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2765 *
2766 * @return string either the raw or the translated policy text.
2767 *
2768 * @since 1.16.5 (WP) - 1.6.5 (WP)
2769 */
2770 public static function getGuestsAllowedPolicy($vbo_tn = null)
2771 {
2772 $dbo = JFactory::getDbo();
2773
2774 $q = $dbo->getQuery(true)
2775 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2776 ->from($dbo->qn('#__vikbooking_texts'))
2777 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2778
2779 $dbo->setQuery($q);
2780 $text_record = $dbo->loadAssocList();
2781
2782 if (!$text_record) {
2783 // missing record, insert it with empty values
2784 if (self::isAdmin()) {
2785 $record = new stdClass;
2786 $record->param = 'guests_allowed_policy';
2787 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2788 $record->setting = '';
2789 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2790 }
2791
2792 return '';
2793 }
2794
2795 if (is_object($vbo_tn)) {
2796 // translate the record only if requested
2797 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2798 }
2799
2800 return $text_record[0]['setting'];
2801 }
2802
2803 public static function getDateFormat($skipsession = true)
2804 {
2805 // cache value in static var
2806 static $getDateFormat = null;
2807
2808 if ($getDateFormat) {
2809 return $getDateFormat;
2810 }
2811
2812 $dbo = JFactory::getDbo();
2813
2814 if ($skipsession) {
2815 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2816 $dbo->setQuery($q, 0, 1);
2817 $getDateFormat = $dbo->loadResult();
2818
2819 return $getDateFormat;
2820 }
2821
2822 $session = JFactory::getSession();
2823 $sval = $session->get('vbgetDateFormat', '');
2824 if (!empty($sval)) {
2825 return $sval;
2826 }
2827
2828 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2829 $dbo->setQuery($q, 0, 1);
2830 $getDateFormat = $dbo->loadResult();
2831
2832 $session->set('vbgetDateFormat', $getDateFormat);
2833
2834 return $getDateFormat;
2835 }
2836
2837 public static function getDateSeparator($skipsession = true)
2838 {
2839 // cache value in static var
2840 static $getDateSeparator = null;
2841
2842 if ($getDateSeparator) {
2843 return $getDateSeparator;
2844 }
2845
2846 $dbo = JFactory::getDbo();
2847
2848 if ($skipsession) {
2849 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2850 $dbo->setQuery($q, 0, 1);
2851 $val = $dbo->loadResult();
2852
2853 $getDateSeparator = empty($val) ? "/" : $val;
2854
2855 return $getDateSeparator;
2856 }
2857
2858 $session = JFactory::getSession();
2859 $sval = $session->get('vbgetDateSep', '');
2860 if (!empty($sval)) {
2861 return $sval;
2862 }
2863
2864 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2865 $dbo->setQuery($q, 0, 1);
2866 $val = $dbo->loadResult();
2867
2868 $getDateSeparator = empty($val) ? "/" : $val;
2869
2870 $session->set('vbgetDateSep', $getDateSeparator);
2871
2872 return $getDateSeparator;
2873 }
2874
2875 public static function getHoursMoreRb($skipsession = false)
2876 {
2877 // cache value in static var
2878 static $getHoursMoreRb = null;
2879
2880 if ($getHoursMoreRb !== null) {
2881 return $getHoursMoreRb;
2882 }
2883
2884 $dbo = JFactory::getDbo();
2885
2886 if ($skipsession) {
2887 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2888 $dbo->setQuery($q, 0, 1);
2889 $getHoursMoreRb = (int)$dbo->loadResult();
2890
2891 return $getHoursMoreRb;
2892 }
2893
2894 $session = JFactory::getSession();
2895 $sval = $session->get('getHoursMoreRb', '');
2896 if (strlen($sval)) {
2897 return $sval;
2898 }
2899
2900 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2901 $dbo->setQuery($q, 0, 1);
2902 $getHoursMoreRb = (int)$dbo->loadResult();
2903
2904 $session->set('getHoursMoreRb', $getHoursMoreRb);
2905
2906 return $getHoursMoreRb;
2907 }
2908
2909 public static function getHoursRoomAvail()
2910 {
2911 $dbo = JFactory::getDbo();
2912
2913 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2914 $dbo->setQuery($q, 0, 1);
2915
2916 return (int)$dbo->loadResult();
2917 }
2918
2919 public static function getFrontTitle($vbo_tn = null) {
2920 // cache value in static var
2921 static $getFrontTitle = null;
2922
2923 if ($getFrontTitle) {
2924 return $getFrontTitle;
2925 }
2926 //
2927
2928 $dbo = JFactory::getDbo();
2929 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2930 $dbo->setQuery($q);
2931 $dbo->execute();
2932 $ft = $dbo->loadAssocList();
2933 if (!is_object($vbo_tn)) {
2934 $vbo_tn = self::getTranslator();
2935 }
2936 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2937
2938 $getFrontTitle = $ft[0]['setting'];
2939
2940 return $ft[0]['setting'];
2941 }
2942
2943 public static function getFrontTitleTag()
2944 {
2945 return VBOFactory::getConfig()->get('fronttitletag', '');
2946 }
2947
2948 public static function getFrontTitleTagClass()
2949 {
2950 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2951 }
2952
2953 /**
2954 * Returns the configured currency name. Often used to apply currency
2955 * conversions, and so it should match the currency ISO 4217 (Alpha-3) code.
2956 *
2957 * @param bool $iso True to ensure we fetch a 3-char currency code.
2958 *
2959 * @return string
2960 *
2961 * @since 1.18.0 (J) - 1.8.0 (WP) added argument $iso.
2962 */
2963 public static function getCurrencyName($iso = false)
2964 {
2965 static $currencyName = null;
2966
2967 if ($currencyName !== null) {
2968 return $currencyName;
2969 }
2970
2971 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2972
2973 if ($iso && strlen((string) $currencyName) != 3) {
2974 // attempt to fetch the currency code configuration setting
2975 $currencyName = self::getCurrencyCodePp();
2976 }
2977
2978 return $currencyName;
2979 }
2980
2981 public static function getCurrencySymb()
2982 {
2983 // cache value in static var
2984 static $getCurrencySymb = null;
2985
2986 if ($getCurrencySymb) {
2987 return $getCurrencySymb;
2988 }
2989
2990 $getCurrencySymb = VBOFactory::getConfig()->get('currencysymb', '');
2991
2992 return $getCurrencySymb;
2993 }
2994
2995 /**
2996 * Returns the configured currency position: either "before" or "after".
2997 *
2998 * @return string
2999 *
3000 * @since 1.18.3 (J) - 1.8.3 (WP)
3001 */
3002 public static function getCurrencyPosition()
3003 {
3004 // cache value in static var
3005 static $getCurrencyPos = null;
3006
3007 if ($getCurrencyPos) {
3008 return $getCurrencyPos;
3009 }
3010
3011 $getCurrencyPos = VBOFactory::getConfig()->get('currencypos', 'before');
3012
3013 return $getCurrencyPos;
3014 }
3015
3016 /**
3017 * Given a number, formats it according to currency preferences.
3018 *
3019 * @param string $number The number to format.
3020 * @param ?string $currency Optional currency to use.
3021 * @param ?array $config Optional format configuration (i.e. currency/number "HTML template").
3022 *
3023 * @return string
3024 *
3025 * @since 1.18.3 (J) - 1.8.3 (WP)
3026 */
3027 public static function formatCurrencyNumber(string $number, ?string $currency = null, ?array $config = null)
3028 {
3029 // get the currency symbol/value to use
3030 $currency = $currency ?: self::getCurrencySymb();
3031
3032 // obtain currency template (wildcard expected)
3033 if (is_string($config[0] ?? null) || is_string($config['currency'] ?? null)) {
3034 // apply templating to string
3035 $currency = sprintf($config[0] ?? $config['currency'], $currency);
3036 }
3037
3038 // obtain number template
3039 if (is_string($config[1] ?? null) || is_string($config['number'] ?? null)) {
3040 // apply templating to string
3041 $number = sprintf($config[1] ?? $config['number'], $number);
3042 }
3043
3044 // default value spacing (empty white space)
3045 $spacing = (string) ($config['spacing'] ?? ' ');
3046
3047 if (self::getCurrencyPosition() === 'after') {
3048 return sprintf('%s' . $spacing . '%s', $number, $currency);
3049 }
3050
3051 return sprintf('%s' . $spacing . '%s', $currency, $number);
3052 }
3053
3054 public static function getCurrencyCodePp()
3055 {
3056 static $currencyCode = null;
3057
3058 if ($currencyCode !== null) {
3059 return $currencyCode;
3060 }
3061
3062 $currencyCode = VBOFactory::getConfig()->get('currencycodepp', '');
3063
3064 return $currencyCode;
3065 }
3066
3067 public static function getNumberFormatData()
3068 {
3069 // cache value in static var
3070 static $getNumberFormatData = null;
3071
3072 if ($getNumberFormatData) {
3073 return $getNumberFormatData;
3074 }
3075
3076 $getNumberFormatData = VBOFactory::getConfig()->get('numberformat', '');
3077
3078 return $getNumberFormatData;
3079 }
3080
3081 /**
3082 * It is possible to hide the decimals if they are like "N.00".
3083 *
3084 * @return int 0 if disabled, 1 if enabled (default).
3085 *
3086 * @since 1.14 (J) - 1.4.0 (WP)
3087 */
3088 public static function hideEmptyDecimals()
3089 {
3090 // cache value in static var
3091 static $hideEmptyDecimals = null;
3092
3093 if ($hideEmptyDecimals !== null) {
3094 return $hideEmptyDecimals;
3095 }
3096
3097 $hideEmptyDecimals = (int) VBOFactory::getConfig()->get('noemptydecimals', '1');
3098
3099 return $hideEmptyDecimals;
3100 }
3101
3102 public static function numberFormat($num)
3103 {
3104 $formatvals = self::getNumberFormatData();
3105 $formatparts = explode(':', $formatvals);
3106
3107 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
3108 // number has got no decimals
3109 $formatparts[0] = 0;
3110 }
3111
3112 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
3113 }
3114
3115 /**
3116 * Given a date timestamp, returns the formatted date.
3117 *
3118 * @param int|string $ts the date representation in Unix timestamp.
3119 * @param bool $wtime if true, a formatted date-time will be returned.
3120 *
3121 * @return string the formatted date according to settings.
3122 *
3123 * @since 1.16.0 (J) - 1.6.0 (WP)
3124 */
3125 public static function formatDateTs($ts, $wtime = false)
3126 {
3127 $nowdf = self::getDateFormat();
3128 if ($nowdf == "%d/%m/%Y") {
3129 $df = 'd/m/Y';
3130 } elseif ($nowdf == "%m/%d/%Y") {
3131 $df = 'm/d/Y';
3132 } else {
3133 $df = 'Y/m/d';
3134 }
3135 $datesep = self::getDateSeparator();
3136
3137 $time_f = $wtime ? ' H:i' : '';
3138
3139 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
3140 }
3141
3142 /**
3143 * Given a country name, 3-char or 2-char code, this method attempts
3144 * to guess the best language to assign to the booking according to
3145 * what languages are installed on the website. This way, cron jobs
3146 * and any other email notification will be correctly and automatically
3147 * sent to the guest in the proper language without any manual action.
3148 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
3149 *
3150 * @param string $country the country name, 3-char or 2-char code.
3151 * @param string $locale optional locale lang tag of the guest.
3152 *
3153 * @return mixed the best lang-tag string to use or null.
3154 *
3155 * @since 1.16.0 (J) - 1.6.0 (WP)
3156 */
3157 public static function guessBookingLangFromCountry($country, $locale = null)
3158 {
3159 if (empty($country)) {
3160 return null;
3161 }
3162
3163 // get all the available languages
3164 $known_langs = self::getVboApplication()->getKnownLanguages();
3165 if (!is_array($known_langs) || !count($known_langs)) {
3166 return null;
3167 }
3168
3169 // check if the booking included a supported "locale" for the guest
3170 if (!empty($locale)) {
3171 // make the locale compatible with the language tags format
3172 $locale = str_replace('_', '-', $locale);
3173 foreach ($known_langs as $ltag => $ldet) {
3174 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
3175 // we support this language tag, so we just use it
3176 return $ltag;
3177 }
3178 }
3179 }
3180
3181 // build similarities with country-languages
3182 $similarities = array(
3183 'AU' => 'en',
3184 'GB' => 'en',
3185 'IE' => 'en',
3186 'NZ' => 'en',
3187 'US' => 'en',
3188 'CA' => array(
3189 'en',
3190 'fr',
3191 ),
3192 'CL' => 'es',
3193 'AR' => 'es',
3194 'PE' => 'es',
3195 'MX' => 'es',
3196 'CR' => 'es',
3197 'CO' => 'es',
3198 'EC' => 'es',
3199 'BO' => 'es',
3200 'CU' => 'es',
3201 'VE' => 'es',
3202 'BE' => 'fr',
3203 'LU' => 'fr',
3204 'CH' => array(
3205 'de',
3206 'it',
3207 'fr',
3208 ),
3209 'AT' => 'de',
3210 'GR' => 'el',
3211 'GL' => 'dk',
3212 );
3213
3214 // fetch values from db
3215 $dbo = JFactory::getDbo();
3216 $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);
3217 $dbo->setQuery($q, 0, 1);
3218 $dbo->execute();
3219 if (!$dbo->getNumRows()) {
3220 return null;
3221 }
3222 $country_record = $dbo->loadAssoc();
3223
3224 // assign country name/code versions
3225 $country_name = $country_record['country_name'];
3226 $country_3char = strtoupper($country_record['country_3_code']);
3227 $country_2char = strtoupper($country_record['country_2_code']);
3228
3229 // build an associative array of language tags and related match-score
3230 $langtags_score = array();
3231 foreach ($known_langs as $ltag => $ldet) {
3232 // default language tag score is 0 for no matches
3233 $langtags_score[$ltag] = 0;
3234 // get language and country codes
3235 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
3236
3237 // check matches with the installed language details
3238 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
3239 // increase language tag score
3240 $langtags_score[$ltag]++;
3241 }
3242 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
3243 // increase language tag score
3244 $langtags_score[$ltag]++;
3245 }
3246 if (!empty($ldet['locale'])) {
3247 // sanitize locale for matching the 2-char code safely
3248 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
3249 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
3250 // increase language tag score
3251 $langtags_score[$ltag]++;
3252 }
3253 }
3254 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
3255 // increase language tag score
3256 $langtags_score[$ltag]++;
3257 }
3258 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
3259 // increase language tag score
3260 $langtags_score[$ltag]++;
3261 }
3262
3263 // check language similarities between countries
3264 if (isset($similarities[$country_2char])) {
3265 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
3266 // check if language tag(s) is available for this spoken language
3267 foreach ($spoken_tags as $spoken_tag) {
3268 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
3269 // increase language tag score
3270 $langtags_score[$ltag]++;
3271 }
3272 }
3273 }
3274 }
3275
3276 // make sure at least one language tag has got some points
3277 if (max($langtags_score) === 0) {
3278 // no languages installed to honor this country
3279 return null;
3280 }
3281
3282 // sort language tag scores
3283 arsort($langtags_score);
3284
3285 // reset array pointer to the first (highest) element
3286 reset($langtags_score);
3287
3288 // return the language tag with the highest score
3289 return key($langtags_score);
3290 }
3291
3292 public static function getIntroMain($vbo_tn = null) {
3293 $dbo = JFactory::getDbo();
3294 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
3295 $dbo->setQuery($q);
3296 $dbo->execute();
3297 $ft = $dbo->loadAssocList();
3298 if (!is_object($vbo_tn)) {
3299 $vbo_tn = self::getTranslator();
3300 }
3301 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3302 return $ft[0]['setting'];
3303 }
3304
3305 public static function getClosingMain($vbo_tn = null) {
3306 $dbo = JFactory::getDbo();
3307 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3308 $dbo->setQuery($q);
3309 $dbo->execute();
3310 $ft = $dbo->loadAssocList();
3311 if (!is_object($vbo_tn)) {
3312 $vbo_tn = self::getTranslator();
3313 }
3314 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3315 return $ft[0]['setting'];
3316 }
3317
3318 public static function getFullFrontTitle($vbo_tn = null) {
3319 $dbo = JFactory::getDbo();
3320 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3321 $dbo->setQuery($q);
3322 $dbo->execute();
3323 $ft = $dbo->loadAssocList();
3324 if (!is_object($vbo_tn)) {
3325 $vbo_tn = self::getTranslator();
3326 }
3327 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3328 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3329 $dbo->setQuery($q);
3330 $dbo->execute();
3331 $fttag = $dbo->loadAssocList();
3332 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3333 $dbo->setQuery($q);
3334 $dbo->execute();
3335 $fttagclass = $dbo->loadAssocList();
3336 if (empty($ft[0]['setting'])) {
3337 return "";
3338 } else {
3339 if (empty($fttag[0]['setting'])) {
3340 return $ft[0]['setting'] . "<br/>\n";
3341 } else {
3342 $tag = str_replace("<", "", $fttag[0]['setting']);
3343 $tag = str_replace(">", "", $tag);
3344 $tag = str_replace("/", "", $tag);
3345 $tag = trim($tag);
3346 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3347 }
3348 }
3349 }
3350
3351 /**
3352 * Tells whether a date string is valid.
3353 *
3354 * @param ?string $date The date to evaluate.
3355 *
3356 * @return bool
3357 *
3358 * @since 1.18.8 (J) - 1.8.8 (WP) Military format Y-m-d is always accepted.
3359 */
3360 public static function dateIsValid(?string $date)
3361 {
3362 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", (string) $date)) {
3363 // date is in Y-m-d format (with no time)
3364 return true;
3365 }
3366
3367 $df = self::getDateFormat();
3368 $datesep = self::getDateSeparator();
3369 if (strlen((string) $date) != 10) {
3370 return false;
3371 }
3372 $cur_dsep = "/";
3373 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3374 $cur_dsep = $datesep;
3375 }
3376 $x = explode($cur_dsep, $date);
3377 if ($df == "%d/%m/%Y") {
3378 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3379 return false;
3380 }
3381 } elseif ($df == "%m/%d/%Y") {
3382 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3383 return false;
3384 }
3385 } else {
3386 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3387 return false;
3388 }
3389 }
3390 return true;
3391 }
3392
3393 public static function sayDateFormat() {
3394 $dbo = JFactory::getDbo();
3395 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3396 $dbo->setQuery($q);
3397 $dbo->execute();
3398 $s = $dbo->loadAssocList();
3399 if ($s[0]['setting'] == "%d/%m/%Y") {
3400 return JText::translate('VBCONFIGONETWELVE');
3401 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3402 return JText::translate('VBCONFIGONEMDY');
3403 } else {
3404 return JText::translate('VBCONFIGONETENTHREE');
3405 }
3406 }
3407
3408 /**
3409 * Calculates the Unix timestamp from the given date and
3410 * time. Avoids DST issues thanks to mktime. With prior releases,
3411 * DST issues may occur due to the sum of seconds.
3412 *
3413 * @param string $date the date string formatted with the current settings
3414 * @param int $h hours from 0 to 23 for check-in/check-out
3415 * @param int $m minutes from 0 to 59 for check-in/check-out
3416 * @param int $s seconds from 0 to 59 for check-in/check-out
3417 *
3418 * @return int the Unix timestamp of the date
3419 *
3420 * @since 1.0.14
3421 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3422 * signature modified for $h and $m that now take 0 as default value.
3423 */
3424 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3425 $df = self::getDateFormat();
3426 $datesep = self::getDateSeparator();
3427 $cur_dsep = "/";
3428 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3429 $cur_dsep = $datesep;
3430 }
3431 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3432 // date is in Y-m-d format (with no time)
3433 $cur_dsep = '-';
3434 $df = "%Y/%m/%d";
3435 }
3436 $x = explode($cur_dsep, $date);
3437 if (!(count($x) > 2)) {
3438 return 0;
3439 }
3440 if ($df == "%d/%m/%Y") {
3441 $month = (int)$x[1];
3442 $mday = (int)$x[0];
3443 $year = (int)$x[2];
3444 } elseif ($df == "%m/%d/%Y") {
3445 $month = (int)$x[0];
3446 $mday = (int)$x[1];
3447 $year = (int)$x[2];
3448 } else {
3449 $month = (int)$x[1];
3450 $mday = (int)$x[2];
3451 $year = (int)$x[0];
3452 }
3453 $h = empty($h) ? 0 : (int)$h;
3454 $m = empty($m) ? 0 : (int)$m;
3455 $s = $s > 0 && $s <= 59 ? $s : 0;
3456
3457 return mktime($h, $m, $s, $month, $mday, $year);
3458 }
3459
3460 public static function ivaInclusa($skipsession = false) {
3461 // cache value in static var
3462 static $getTaxIncluded = null;
3463
3464 if ($getTaxIncluded !== null) {
3465 return (bool)$getTaxIncluded;
3466 }
3467
3468 $dbo = JFactory::getDbo();
3469
3470 if ($skipsession) {
3471 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3472 $dbo->setQuery($q, 0, 1);
3473 $dbo->execute();
3474 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3475
3476 // cache value and return it
3477 $getTaxIncluded = $vat_incl_data;
3478
3479 return (bool)$vat_incl_data;
3480 }
3481
3482 $session = JFactory::getSession();
3483 $sval = $session->get('getTaxIncluded', '');
3484 if (strlen($sval)) {
3485 return (bool)$sval;
3486 }
3487
3488 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3489 $dbo->setQuery($q, 0, 1);
3490 $dbo->execute();
3491 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3492
3493 // update session value, cache it and return it
3494 $session->set('getTaxIncluded', $vat_incl_data);
3495 $getTaxIncluded = $vat_incl_data;
3496
3497 return (bool)$vat_incl_data;
3498 }
3499
3500 public static function showTaxOnSummaryOnly($skipsession = false) {
3501 if ($skipsession) {
3502 $dbo = JFactory::getDbo();
3503 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3504 $dbo->setQuery($q);
3505 $dbo->execute();
3506 $s = $dbo->loadAssocList();
3507 return (intval($s[0]['setting']) == 1 ? true : false);
3508 } else {
3509 $session = JFactory::getSession();
3510 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3511 if (strlen($sval) > 0) {
3512 return (intval($sval) == 1 ? true : false);
3513 } else {
3514 $dbo = JFactory::getDbo();
3515 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3516 $dbo->setQuery($q);
3517 $dbo->execute();
3518 $s = $dbo->loadAssocList();
3519 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3520 return (intval($s[0]['setting']) == 1 ? true : false);
3521 }
3522 }
3523 }
3524
3525 public static function tokenForm() {
3526 $dbo = JFactory::getDbo();
3527 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3528 $dbo->setQuery($q);
3529 $dbo->execute();
3530 $s = $dbo->loadAssocList();
3531 return (intval($s[0]['setting']) == 1 ? true : false);
3532 }
3533
3534 public static function getPaypalAcc() {
3535 $dbo = JFactory::getDbo();
3536 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3537 $dbo->setQuery($q);
3538 $dbo->execute();
3539 $s = $dbo->loadAssocList();
3540 return $s[0]['setting'];
3541 }
3542
3543 public static function getAccPerCent() {
3544 $dbo = JFactory::getDbo();
3545 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3546 $dbo->setQuery($q);
3547 $dbo->execute();
3548 $s = $dbo->loadAssocList();
3549 return $s[0]['setting'];
3550 }
3551
3552 public static function getTypeDeposit($skipsession = false) {
3553 if ($skipsession) {
3554 $dbo = JFactory::getDbo();
3555 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3556 $dbo->setQuery($q);
3557 $dbo->execute();
3558 $s = $dbo->loadAssocList();
3559 return $s[0]['setting'];
3560 } else {
3561 $session = JFactory::getSession();
3562 $sval = $session->get('getTypeDeposit', '');
3563 if (strlen($sval) > 0) {
3564 return $sval;
3565 } else {
3566 $dbo = JFactory::getDbo();
3567 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3568 $dbo->setQuery($q);
3569 $dbo->execute();
3570 $s = $dbo->loadAssocList();
3571 $session->set('getTypeDeposit', $s[0]['setting']);
3572 return $s[0]['setting'];
3573 }
3574 }
3575 }
3576
3577 public static function multiplePayments()
3578 {
3579 return VBOFactory::getConfig()->getBool('multipay', false);
3580 }
3581
3582 public static function getAdminMail()
3583 {
3584 return VBOFactory::getConfig()->getString('adminemail', '');
3585 }
3586
3587 public static function getSenderMail()
3588 {
3589 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3590
3591 return empty($sender) ? self::getAdminMail() : $sender;
3592 }
3593
3594 public static function getPaymentName($vbo_tn = null) {
3595 $dbo = JFactory::getDbo();
3596 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3597 $dbo->setQuery($q);
3598 $dbo->execute();
3599 $s = $dbo->loadAssocList();
3600 if (!is_object($vbo_tn)) {
3601 $vbo_tn = self::getTranslator();
3602 }
3603 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3604 return $s[0]['setting'];
3605 }
3606
3607 public static function getTermsConditions($vbo_tn = null) {
3608 //VBO 1.10
3609 $dbo = JFactory::getDbo();
3610 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3611 $dbo->setQuery($q);
3612 $dbo->execute();
3613 if ($dbo->getNumRows() > 0) {
3614 $s = $dbo->loadAssocList();
3615 if (!is_object($vbo_tn)) {
3616 $vbo_tn = self::getTranslator();
3617 }
3618 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3619 } else {
3620 //the record has never been saved. Compose it with the default lang definition
3621 $timeopst = self::getTimeOpenStore(true);
3622 if (is_array($timeopst)) {
3623 $openat = self::getHoursMinutes($timeopst[0]);
3624 $closeat = self::getHoursMinutes($timeopst[1]);
3625 } else {
3626 $openat = array(12, 0);
3627 $closeat = array(10, 0);
3628 }
3629 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3630 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3631 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3632 }
3633
3634 return $s[0]['setting'];
3635 }
3636
3637 public static function getMinutesLock($conv = false)
3638 {
3639 static $minutesLock = null;
3640
3641 if ($minutesLock === null) {
3642 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3643 }
3644
3645 if (!$minutesLock) {
3646 return 0;
3647 }
3648
3649 if ($conv) {
3650 return (time() + ($minutesLock * 60));
3651 }
3652
3653 return $minutesLock;
3654 }
3655
3656 /**
3657 * Checks if the room units are temporarily locked on the given dates.
3658 *
3659 * @param int $idroom the room ID.
3660 * @param int $units the total number of room units.
3661 * @param int $first the check-in timestamp.
3662 * @param int $second the check-out timestamp.
3663 * @param bool $occupied if true, the system will also consider the booked units.
3664 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3665 *
3666 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3667 *
3668 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3669 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3670 */
3671 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3672 {
3673 $dbo = JFactory::getDbo();
3674
3675 if ($units < 1) {
3676 return false;
3677 }
3678
3679 $actnow = time();
3680
3681 // clean up expired records
3682 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3683 $dbo->setQuery($q);
3684 $dbo->execute();
3685
3686 $secdiff = $second - $first;
3687 $daysdiff = $secdiff / 86400;
3688 if (is_int($daysdiff)) {
3689 if ($daysdiff < 1) {
3690 $daysdiff = 1;
3691 }
3692 } else {
3693 if ($daysdiff < 1) {
3694 $daysdiff = 1;
3695 } else {
3696 $sum = floor($daysdiff) * 86400;
3697 $newdiff = $secdiff - $sum;
3698 $maxhmore = self::getHoursMoreRb() * 3600;
3699 if ($maxhmore >= $newdiff) {
3700 $daysdiff = floor($daysdiff);
3701 } else {
3702 $daysdiff = ceil($daysdiff);
3703 }
3704 }
3705 }
3706 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3707 $tmplock_map = [];
3708
3709 // check temporarily locked records
3710 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3711 $dbo->setQuery($check);
3712 $busy = $dbo->loadAssocList();
3713 if ($busy) {
3714 foreach ($groupdays as $kg => $gday) {
3715 $bfound = 0;
3716 foreach ($busy as $bu) {
3717 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3718 $bfound++;
3719 }
3720 }
3721 $tmplock_map[$kg] = $bfound;
3722 if ($bfound >= $units) {
3723 return false;
3724 }
3725 }
3726 }
3727
3728 if ($occupied) {
3729 // check also the busy records and some the occupied units to the temporarily locked ones
3730 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3731 $dbo->setQuery($check);
3732 $busy = $dbo->loadAssocList();
3733 if (!$busy) {
3734 return true;
3735 }
3736
3737 foreach ($groupdays as $kg => $gday) {
3738 $bfound = 0;
3739 foreach ($busy as $bu) {
3740 if (in_array($bu['id'], $skip_busy_ids)) {
3741 // Booking modification
3742 continue;
3743 }
3744 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3745 $bfound++;
3746 }
3747 }
3748 // sum units occupied to the units temporarily locked
3749 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3750 if ($bfound >= $units) {
3751 return false;
3752 }
3753 }
3754 }
3755
3756 return true;
3757 }
3758
3759 public static function getGroupDays($first, $second, $daysdiff) {
3760 $ret = array();
3761 $ret[] = $first;
3762 if ($daysdiff > 1) {
3763 $start = getdate($first);
3764 $end = getdate($second);
3765 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3766 for($i = 1; $i < $daysdiff; $i++) {
3767 $checkday = $start['mday'] + $i;
3768 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3769 if ($dayts != $endcheck) {
3770 $ret[] = $dayts;
3771 }
3772 }
3773 }
3774 $ret[] = $second;
3775 return $ret;
3776 }
3777
3778 /**
3779 * Counts the hours of difference between the current
3780 * server time and the selected check-in date and time.
3781 *
3782 * @param int $checkin_ts
3783 * @param [int] $now_ts
3784 *
3785 * @return int
3786 */
3787 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3788 $hoursdiff = 0;
3789
3790 if (empty($now_ts)) {
3791 $now_ts = time();
3792 }
3793
3794 if ($now_ts >= $checkin_ts) {
3795 return $hoursdiff;
3796 }
3797
3798 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3799
3800 return $hoursdiff;
3801 }
3802
3803 /**
3804 * Loads all the occupied records for the given rooms and offset.
3805 *
3806 * @param array $roomids the optional list of room IDs to filter.
3807 * @param int $from_ts the optional base date to use.
3808 * @param int $max_ts the optional max date to use.
3809 * @param bool $closures whether to ignore, include or exclude closures.
3810 *
3811 * @return array
3812 *
3813 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3814 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3815 */
3816 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3817 {
3818 $base_ts = empty($from_ts) ? time() : $from_ts;
3819 $busy = [];
3820 $clauses = [];
3821
3822 if (is_array($roomids) && count($roomids)) {
3823 // filter busy records only by the requested room ids
3824 $roomids = array_map(function($rid) {
3825 return (int)$rid;
3826 }, $roomids);
3827 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3828 }
3829
3830 // exclude past reservations
3831 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3832
3833 if (!empty($max_ts)) {
3834 // exclude future reservations (recommended for large datasets)
3835 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3836 }
3837
3838 if (is_bool($closures)) {
3839 // include or exclude closures
3840 if ($closures) {
3841 $clauses[] = "`o`.`closure` = 1";
3842 } else {
3843 $clauses[] = "`o`.`closure` != 1";
3844 }
3845 }
3846
3847 $dbo = JFactory::getDbo();
3848
3849 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3850 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3851 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3852 WHERE " . implode(' AND ', $clauses) . ";";
3853 $dbo->setQuery($q);
3854 $allbusy = $dbo->loadAssocList();
3855 if ($allbusy) {
3856 foreach ($allbusy as $br) {
3857 if (!isset($busy[$br['idroom']])) {
3858 $busy[$br['idroom']] = [];
3859 }
3860 $busy[$br['idroom']][] = $br;
3861 }
3862 }
3863
3864 return $busy;
3865 }
3866
3867 /**
3868 * Loads all the busy records by excluding the closures.
3869 * Built for the page dashboard to calculate the actual
3870 * rooms occupancy by excluding the rooms closed.
3871 *
3872 * @param array $roomids the optional list of room IDs to filter.
3873 * @param int $from_ts the optional base date to use.
3874 * @param int $max_ts the optional max date to use.
3875 *
3876 * @return array the list of busy records.
3877 *
3878 * @since 1.12
3879 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3880 */
3881 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3882 {
3883 if (!is_array($roomids) || !count($roomids)) {
3884 return [];
3885 }
3886
3887 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3888 }
3889
3890 /**
3891 * Loads the occupied record IDs for a given booking ID.
3892 *
3893 * @param int $idorder The booking record ID.
3894 *
3895 * @return array List of busy IDs, if any.
3896 */
3897 public static function loadBookingBusyIds($idorder)
3898 {
3899 if (empty($idorder)) {
3900 return [];
3901 }
3902
3903 $dbo = JFactory::getDbo();
3904
3905 $dbo->setQuery(
3906 $dbo->getQuery(true)
3907 ->select($dbo->qn('idbusy'))
3908 ->from($dbo->qn('#__vikbooking_ordersbusy'))
3909 ->where($dbo->qn('idorder') . ' = ' . (int) $idorder)
3910 );
3911
3912 return array_map('intval', array_column($dbo->loadAssocList(), 'idbusy'));
3913 }
3914
3915 public static function loadLockedRecords($roomids, $ts = 0) {
3916 $dbo = JFactory::getDbo();
3917 $actnow = empty($ts) ? time() : $ts;
3918 $locked = array();
3919 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3920 $dbo->setQuery($q);
3921 $dbo->execute();
3922 if (!is_array($roomids) || !(count($roomids) > 0)) {
3923 return $locked;
3924 }
3925 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3926 $dbo->setQuery($check);
3927 $all_locked = $dbo->loadAssocList();
3928 if ($all_locked) {
3929 foreach ($all_locked as $kb => $br) {
3930 $locked[$br['idroom']][$kb] = $br;
3931 }
3932 }
3933 return $locked;
3934 }
3935
3936 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3937 $bookings = [];
3938 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3939 return $bookings;
3940 }
3941 $dbo = JFactory::getDbo();
3942 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3943 $dbo->setQuery($q);
3944 $all_booking_ids = $dbo->loadAssocList();
3945 if ($all_booking_ids) {
3946 $oids = array();
3947 foreach ($all_booking_ids as $bid) {
3948 $oids[] = $bid['idorder'];
3949 }
3950 $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` ".
3951 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3952 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3953 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3954 "ORDER BY `o`.`checkout` ASC;";
3955 $dbo->setQuery($q);
3956 $bookings = $dbo->loadAssocList();
3957 if (!$bookings) {
3958 $bookings = [];
3959 }
3960 }
3961 return $bookings;
3962 }
3963
3964 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3965 {
3966 $dbo = JFactory::getDbo();
3967
3968 if ($units < 1) {
3969 return false;
3970 }
3971
3972 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3973 if (!$room_info) {
3974 return false;
3975 }
3976
3977 $secdiff = $second - $first;
3978 $daysdiff = $secdiff / 86400;
3979 if (is_int($daysdiff)) {
3980 if ($daysdiff < 1) {
3981 $daysdiff = 1;
3982 }
3983 } else {
3984 if ($daysdiff < 1) {
3985 $daysdiff = 1;
3986 } else {
3987 $sum = floor($daysdiff) * 86400;
3988 $newdiff = $secdiff - $sum;
3989 $maxhmore = self::getHoursMoreRb() * 3600;
3990 if ($maxhmore >= $newdiff) {
3991 $daysdiff = floor($daysdiff);
3992 } else {
3993 $daysdiff = ceil($daysdiff);
3994 }
3995 }
3996 }
3997
3998 $q = $dbo->getQuery(true)
3999 ->select($dbo->qn([
4000 'id',
4001 'checkin',
4002 'realback',
4003 ]))
4004 ->from($dbo->qn('#__vikbooking_busy'))
4005 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
4006 ->where($dbo->qn('realback') . ' >= ' . $first);
4007
4008 $dbo->setQuery($q);
4009 $busy = $dbo->loadAssocList();
4010 if (!$busy) {
4011 return ($units <= ($room_info['units'] ?? 1));
4012 }
4013
4014 $groupdays = self::getGroupDays($first, $second, $daysdiff);
4015 foreach ($groupdays as $gday) {
4016 $bfound = 0;
4017 foreach ($busy as $bu) {
4018 if (in_array($bu['id'], $skip_busy_ids)) {
4019 // booking modification
4020 continue;
4021 }
4022 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
4023 $bfound++;
4024 }
4025 }
4026 if ($bfound >= $units) {
4027 return false;
4028 }
4029 }
4030
4031 return ($units <= ($room_info['units'] ?? 1));
4032 }
4033
4034 public static function payTotal()
4035 {
4036 return VBOFactory::getConfig()->getBool('paytotal', true);
4037 }
4038
4039 public static function getDepositIfDays()
4040 {
4041 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
4042 }
4043
4044 public static function depositAllowedDaysAdv($checkints) {
4045 $days_adv = self::getDepositIfDays();
4046 if (!($days_adv > 0) || !($checkints > 0)) {
4047 return true;
4048 }
4049 $now_info = getdate();
4050 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
4051 return $maxts > $checkints ? false : true;
4052 }
4053
4054 public static function depositCustomerChoice() {
4055 $dbo = JFactory::getDbo();
4056 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
4057 $dbo->setQuery($q);
4058 $dbo->execute();
4059 $s = $dbo->loadAssocList();
4060 return (intval($s[0]['setting']) == 1 ? true : false);
4061 }
4062
4063 public static function getDepositOverrides($getjson = false) {
4064 $dbo = JFactory::getDbo();
4065 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
4066 $dbo->setQuery($q);
4067 $dbo->execute();
4068 if ($dbo->getNumRows() > 0) {
4069 $s = $dbo->loadResult();
4070 return $getjson ? $s : json_decode($s, true);
4071 }
4072 //count of this array will be at least 1 to store the "more" property
4073 $def_arr = array('more' => '');
4074 $def_val = json_encode($def_arr);
4075 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
4076 $dbo->setQuery($q);
4077 $dbo->execute();
4078 return $getjson ? $def_val : $def_arr;
4079 }
4080
4081 public static function calcDepositOverride($amount_deposit, $nights) {
4082 $overrides = self::getDepositOverrides();
4083 $nights = intval($nights);
4084 $andmore = intval($overrides['more']);
4085 if (!(count($overrides) > 1)) {
4086 //no overrides
4087 return $amount_deposit;
4088 }
4089 foreach ($overrides as $k => $v) {
4090 if ((int)$k == $nights && strlen($v) > 0) {
4091 //exact override found
4092 return (float)$v;
4093 }
4094 }
4095 if ($andmore > 0 && $andmore <= $nights) {
4096 foreach ($overrides as $k => $v) {
4097 if ((int)$k == $andmore && strlen($v) > 0) {
4098 //"and more" nights found
4099 return (float)$v;
4100 }
4101 }
4102 }
4103 //nothing was found
4104 return $amount_deposit;
4105 }
4106
4107 public static function noDepositForNonRefund() {
4108 $dbo = JFactory::getDbo();
4109 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
4110 $dbo->setQuery($q);
4111 $dbo->execute();
4112 if ($dbo->getNumRows() > 0) {
4113 $s = $dbo->loadResult();
4114 return ((int)$s == 1);
4115 }
4116 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
4117 $dbo->setQuery($q);
4118 $dbo->execute();
4119 //default to false
4120 return false;
4121 }
4122
4123 /**
4124 * This method returns the room-rate array with the lowest price
4125 * that matches the preferred rate plan parameter (if available).
4126 * The array $website_rates could not be an array, or it could be
4127 * an array with the error string (response from the TACVBO Class).
4128 * The method has been introduced in VBO 1.10 and it's mainly used
4129 * by the module mod_vikbooking_channelrates and its ajax requests.
4130 *
4131 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
4132 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
4133 *
4134 * @return array
4135 */
4136 public static function getBestRoomRate($website_rates, $def_rplan) {
4137 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
4138 return array();
4139 }
4140 $best_room_rate = array();
4141 foreach ($website_rates as $rid => $tars) {
4142 foreach ($tars as $tar) {
4143 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
4144 //the array $website_rates is already sorted by price ASC, so we take the first useful array
4145 $best_room_rate = $tar;
4146 break 2;
4147 }
4148 }
4149 }
4150 if (!(count($best_room_rate) > 0)) {
4151 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
4152 foreach ($website_rates as $rid => $tars) {
4153 foreach ($tars as $tar) {
4154 $best_room_rate = $tar;
4155 break 2;
4156 }
4157 }
4158 }
4159
4160 return $best_room_rate;
4161 }
4162
4163 /**
4164 * This method returns an array with the details
4165 * of all channels in VCM that supports AV requests,
4166 * and that have at least one room type mapped.
4167 * The method invokes the Logos Class to return details
4168 * about the name and logo URL of the channel.
4169 * The method has been introduced in VBO 1.10 and it's mainly used
4170 * by the module mod_vikbooking_channelrates and its ajax requests.
4171 *
4172 * @param array $channels an array of channel IDs to be mapped on the VCM relations
4173 *
4174 * @return array
4175 */
4176 public static function getChannelsMap($channels) {
4177 if (!is_array($channels) || !(count($channels))) {
4178 return array();
4179 }
4180 $vcm_logos = self::getVcmChannelsLogo('', true);
4181 if (!is_object($vcm_logos)) {
4182 return array();
4183 }
4184 $channels_ids = array();
4185 foreach ($channels as $chid) {
4186 $ichid = (int)$chid;
4187 if ($ichid < 1) {
4188 continue;
4189 }
4190 array_push($channels_ids, $ichid);
4191 }
4192 if (!(count($channels_ids))) {
4193 return array();
4194 }
4195 $dbo = JFactory::getDbo();
4196 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
4197 $dbo->setQuery($q);
4198 $dbo->execute();
4199 if ($dbo->getNumRows() < 1) {
4200 return array();
4201 }
4202 $channels_names = $dbo->loadAssocList();
4203 $channels_map = array();
4204 foreach ($channels_names as $ch) {
4205 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
4206 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
4207 $chdata = array(
4208 'id' => $ch['idchannel'],
4209 'name' => ucwords($ch['channel']),
4210 'logo' => $ota_logo_url
4211 );
4212 array_push($channels_map, $chdata);
4213 }
4214 return $channels_map;
4215 }
4216
4217 /**
4218 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
4219 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
4220 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
4221 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
4222 * module/widget and its ajax requests.
4223 *
4224 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
4225 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
4226 *
4227 * @return mixed string (empty or indicating the alteration), or associative array per channel.
4228 *
4229 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
4230 */
4231 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
4232 {
4233 $otas_rates_val = '';
4234 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
4235 return $otas_rates_val;
4236 }
4237 if (!class_exists('VikChannelManager')) {
4238 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
4239 }
4240 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
4241 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
4242 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
4243 // the Bulk Rates Cache contains data for this room type and rate plan
4244 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
4245 // build rates modification string
4246 if ((int)$data_cont['rmod'] > 0) {
4247 // rates were modified for the OTAs, check how
4248 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
4249 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
4250 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
4251 // check alterations per channel, if requested
4252 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
4253 // we compose an associative array with the alteration for each channel (0 = generic rule)
4254 $channel_alterations = array($otas_rates_val);
4255 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
4256 if ((int)$ch_mods['rmod'] > 0) {
4257 // custom rate alteration for this channel
4258 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
4259 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
4260 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
4261 } else {
4262 // no rates alteration for this channel identifier
4263 $ch_alter_string = '+0%';
4264 }
4265 $channel_alterations[$ch_id] = $ch_alter_string;
4266 }
4267 // overwrite return value with associative array
4268 $otas_rates_val = $channel_alterations;
4269 }
4270 }
4271 }
4272 }
4273
4274 return $otas_rates_val;
4275 }
4276
4277 /**
4278 * This method checks if some non-refundable rates were selected
4279 * (`free_cancellation`=0), the only argument is an array of tariffs.
4280 * The property 'idprice' must be defined on each sub-array.
4281 *
4282 * @param $tars array
4283 *
4284 * @return boolean
4285 **/
4286 public static function findNonRefundableRates($tars = [])
4287 {
4288 $id_prices = [];
4289
4290 foreach ($tars as $tar) {
4291 if (isset($tar['idprice'])) {
4292 if (!in_array($tar['idprice'], $id_prices)) {
4293 array_push($id_prices, (int)$tar['idprice']);
4294 }
4295 continue;
4296 }
4297 foreach ($tar as $t) {
4298 if (isset($t['idprice'])) {
4299 if (!in_array($t['idprice'], $id_prices)) {
4300 array_push($id_prices, (int)$t['idprice']);
4301 }
4302 }
4303 }
4304 }
4305
4306 if (!count($id_prices)) {
4307 // no price IDs found (probably a package)
4308 return false;
4309 }
4310
4311 $dbo = JFactory::getDbo();
4312
4313 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
4314 $dbo->setQuery($q);
4315 $dbo->execute();
4316
4317 return (bool)($dbo->getNumRows() > 0);
4318 }
4319
4320 /**
4321 * This method checks if the deposit is allowed depending on
4322 * the selected rate plans (idprice) for the rooms reserved.
4323 * If the configuration setting is enabled, and if some
4324 * non-refundable rates were selected (`free_cancellation`=0),
4325 * the method will return false, because the deposit is not allowed.
4326 * The only argument is an array of tariffs. The property 'idprice'
4327 * must be defined on each sub-array (multi-dimension supported)
4328 * throgh the method findNonRefundableRates();
4329 *
4330 * @param $tars array
4331 *
4332 * @return boolean
4333 **/
4334 public static function allowDepositFromRates($tars) {
4335 if (!self::noDepositForNonRefund()) {
4336 //deposit can be paid also if non-refundable rates
4337 return true;
4338 }
4339 return !self::findNonRefundableRates($tars);
4340 }
4341
4342 public static function showSearchSuggestions() {
4343 $dbo = JFactory::getDbo();
4344 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4345 $dbo->setQuery($q);
4346 $dbo->execute();
4347 if ($dbo->getNumRows() > 0) {
4348 return (int)$dbo->loadResult();
4349 }
4350 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4351 $dbo->setQuery($q);
4352 $dbo->execute();
4353 return 1;
4354 }
4355
4356 /**
4357 * Given a code, returns the coupon information array.
4358 *
4359 * @param string $code the coupon code.
4360 *
4361 * @return array empty array or coupon record.
4362 *
4363 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4364 */
4365 public static function getCouponInfo($code)
4366 {
4367 $dbo = JFactory::getDbo();
4368
4369 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4370 $dbo->setQuery($q, 0, 1);
4371 $coupon = $dbo->loadAssoc();
4372 if (!$coupon) {
4373 return [];
4374 }
4375
4376 // check for customers assigned
4377 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4378 $dbo->setQuery($q);
4379 $coupon_customers = $dbo->loadAssocList();
4380 if ($coupon_customers) {
4381 // set customers and automatic properties
4382 $coupon['customers'] = [];
4383 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4384 foreach ($coupon_customers as $coupon_customer) {
4385 // push customer ID
4386 $coupon['customers'][] = $coupon_customer['idcustomer'];
4387 }
4388 }
4389
4390 return $coupon;
4391 }
4392
4393 /**
4394 * Reads the details of a given room record ID.
4395 *
4396 * @param int $idroom The room ID to fetch.
4397 * @param array $columns The optional room columns to fetch.
4398 * @param bool $no_cache Whether to ignore a previously cached record.
4399 * @param bool $translate Whether to translate the record.
4400 *
4401 * @return array
4402 *
4403 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4404 * @since 1.18.12 (J) - 1.8.12 (WP) added argument $translate.
4405 */
4406 public static function getRoomInfo($idroom, $columns = [], $no_cache = false, $translate = false)
4407 {
4408 static $room_infos = [];
4409
4410 if (isset($room_infos[$idroom]) && !$no_cache) {
4411 return $room_infos[$idroom];
4412 }
4413
4414 $dbo = JFactory::getDbo();
4415
4416 if ($columns) {
4417 $columns = array_map([$dbo, 'qn'], $columns);
4418 }
4419
4420 $q = $dbo->getQuery(true)
4421 ->select($columns ? $columns : '*')
4422 ->from($dbo->qn('#__vikbooking_rooms'))
4423 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4424
4425 $dbo->setQuery($q);
4426 $room = $dbo->loadAssoc();
4427
4428 if ($translate && $room) {
4429 // apply translation
4430 if ($columns && !in_array('id', $columns)) {
4431 // force the record identifier
4432 $room['id'] = $idroom;
4433 }
4434 $records = [$room];
4435 VikBooking::getTranslator()->translateContents($records, '#__vikbooking_rooms');
4436 $room = $records[0];
4437 }
4438
4439 if ($no_cache) {
4440 return $room ?: [];
4441 }
4442
4443 $room_infos[$idroom] = $room ?: [];
4444
4445 return $room_infos[$idroom];
4446 }
4447
4448 public static function loadOrdersRoomsData($idorder)
4449 {
4450 $dbo = JFactory::getDbo();
4451
4452 $q = $dbo->getQuery(true)
4453 ->select($dbo->qn('or') . '.*')
4454 ->select($dbo->qn('r.name', 'room_name'))
4455 ->select($dbo->qn('r.units', 'tot_units'))
4456 ->select($dbo->qn('r.params'))
4457 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4458 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4459 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4460 ->order($dbo->qn('or.id') . ' ASC');
4461
4462 $dbo->setQuery($q);
4463
4464 return $dbo->loadAssocList();
4465 }
4466
4467 public static function sayCategory($ids, $vbo_tn = null)
4468 {
4469 $dbo = JFactory::getDbo();
4470 $split = explode(";", $ids);
4471 $say = "";
4472 foreach ($split as $k => $s) {
4473 if (!strlen($s)) {
4474 continue;
4475 }
4476 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4477 $dbo->setQuery($q);
4478 $nam = $dbo->loadAssocList();
4479 if (!$nam) {
4480 continue;
4481 }
4482 if (is_object($vbo_tn)) {
4483 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4484 }
4485 $say .= $nam[0]['name'];
4486 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4487 }
4488 return $say;
4489 }
4490
4491 /**
4492 * Returns a list of the given room amenities string.
4493 *
4494 * @param string $amenities Room amenities string.
4495 * @param ?object $vbo_tn Optional VBOTranslator.
4496 * @param bool $all True for loading all records when no amenities given.
4497 *
4498 * @return array
4499 *
4500 * @since 1.17.3 (J) - 1.7.3 (WP)
4501 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $all.
4502 */
4503 public static function loadRoomAmenities($amenities, $vbo_tn = null, $all = false)
4504 {
4505 $dbo = JFactory::getDbo();
4506
4507 $split = explode(";", (string) $amenities);
4508 $where = [];
4509 foreach ($split as $s) {
4510 if (!empty($s)) {
4511 $where[] = (int) $s;
4512 }
4513 }
4514
4515 $list = [];
4516 $where = array_filter($where);
4517
4518 $query = $dbo->getQuery(true)
4519 ->select('*')
4520 ->from($dbo->qn('#__vikbooking_characteristics'))
4521 ->order($dbo->qn('ordering') . ' ASC');
4522
4523 if ($where) {
4524 $query->where($dbo->qn('id') . ' IN (' . implode(', ', $where) . ')');
4525 }
4526
4527 if ($where || $all) {
4528 $dbo->setQuery($query);
4529 $list = $dbo->loadAssocList();
4530 if ($list && is_object($vbo_tn)) {
4531 $vbo_tn->translateContents($list, '#__vikbooking_characteristics');
4532 }
4533 }
4534
4535 return $list;
4536 }
4537
4538 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4539 {
4540 $carat = '';
4541 $arr = self::loadRoomAmenities($idc, $vbo_tn);
4542
4543 if ($arr) {
4544 $carat .= "<div class=\"vbo-room-carats\">\n";
4545 foreach ($arr as $a) {
4546 if (!empty($a['textimg'])) {
4547 //tooltip icon text is not empty
4548 if (!empty($a['icon'])) {
4549 //an icon has been uploaded: display the image
4550 $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";
4551 } else {
4552 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4553 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4554 $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";
4555 } else {
4556 //display just the text
4557 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4558 }
4559 }
4560 } else {
4561 $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");
4562 }
4563 }
4564 $carat .= "</div>\n";
4565 }
4566
4567 return $carat;
4568 }
4569
4570 public static function getRoomOptionals($idopts, $vbo_tn = null)
4571 {
4572 $dbo = JFactory::getDbo();
4573 $split = explode(";", $idopts);
4574 $fetch = array();
4575 foreach ($split as $s) {
4576 if (!empty($s)) {
4577 $fetch[] = $s;
4578 }
4579 }
4580 if ($fetch) {
4581 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', array_map('intval', $fetch)) . ") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4582 $dbo->setQuery($q);
4583 $arr = $dbo->loadAssocList();
4584 if ($arr) {
4585 if (is_object($vbo_tn)) {
4586 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4587 }
4588 return $arr;
4589 }
4590 }
4591 return [];
4592 }
4593
4594 public static function getSingleOption($idopt, $vbo_tn = null)
4595 {
4596 $dbo = JFactory::getDbo();
4597 $opt = array();
4598 if (!empty($idopt)) {
4599 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4600 $dbo->setQuery($q);
4601 $opt = $dbo->loadAssoc();
4602 if ($opt && is_object($vbo_tn)) {
4603 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4604 }
4605 }
4606 return $opt;
4607 }
4608
4609 /**
4610 * Unsets the options that are not available in the requested dates.
4611 *
4612 * @param array $optionals the array of options passed by reference
4613 * @param int $checkin the timestamp of the check-in date and time
4614 * @param int $checkout the timestamp of the check-out date and time
4615 *
4616 * @return void
4617 *
4618 * @since 1.11
4619 */
4620 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4621 {
4622 if (!$optionals || empty($checkin) || empty($checkout)) {
4623 return;
4624 }
4625
4626 foreach ($optionals as $k => $v) {
4627 if (!empty($v['alwaysav'])) {
4628 $dates = explode(';', $v['alwaysav']);
4629 if (empty($dates[0]) || empty($dates[1])) {
4630 continue;
4631 }
4632 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4633 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4634 unset($optionals[$k]);
4635 }
4636 }
4637 }
4638 }
4639
4640 /**
4641 * Unsets the options that are not suited for the requested room party.
4642 *
4643 * @param array $optionals the array of options passed by reference.
4644 * @param int $adults the number of adults in the room party.
4645 * @param int $children the number of children in the room party.
4646 *
4647 * @return void
4648 *
4649 * @since 1.13.5
4650 */
4651 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4652 {
4653 if (!$optionals || (empty($adults) && empty($children))) {
4654 return;
4655 }
4656
4657 foreach ($optionals as $k => $v) {
4658 if (empty($v['oparams'])) {
4659 continue;
4660 }
4661 $v['oparams'] = json_decode($v['oparams'], true);
4662 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4663 continue;
4664 }
4665 if (!empty($v['oparams']['minguestsnum'])) {
4666 // filter by minimum adults/guests
4667 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4668 // minimum number of adults not sufficient, unset option
4669 unset($optionals[$k]);
4670 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4671 // minimum number of total guests not sufficient, unset option
4672 unset($optionals[$k]);
4673 }
4674 }
4675 if (!empty($v['oparams']['maxguestsnum'])) {
4676 // filter by maximum adults/guests
4677 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4678 // maximum number of adults exceeded, unset option
4679 unset($optionals[$k]);
4680 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4681 // maximum number of total guests exceeded, unset option
4682 unset($optionals[$k]);
4683 }
4684 }
4685 }
4686 }
4687
4688 /**
4689 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4690 *
4691 * @param array $id_rooms list of room IDs.
4692 * @param int $num_adults number of adults.
4693 * @param int $num_nights number of nights of stay.
4694 * @param int $checkin optional check-in timestamp.
4695 * @param int $checkout optional check-out timestamp.
4696 *
4697 * @return array associative information of city taxes, fees and options.
4698 *
4699 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4700 */
4701 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4702 {
4703 $dbo = JFactory::getDbo();
4704
4705 $taxes = 0;
4706 $fees = 0;
4707
4708 $options_data = [];
4709 $id_options = [];
4710
4711 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4712 $dbo->setQuery($q);
4713 $assocs = $dbo->loadAssocList();
4714 if ($assocs) {
4715 foreach ($assocs as $opts) {
4716 if (!empty($opts['idopt'])) {
4717 $r_ido = explode(';', rtrim($opts['idopt']));
4718 foreach ($r_ido as $ido) {
4719 if (!empty($ido) && !in_array($ido, $id_options)) {
4720 $id_options[] = (int)$ido;
4721 }
4722 }
4723 }
4724 }
4725 }
4726
4727 if ($id_options) {
4728 $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);";
4729 $dbo->setQuery($q);
4730 $alltaxesfees = $dbo->loadAssocList();
4731
4732 if ($checkin && $checkout) {
4733 // filter the records by stay dates to support, for example, seasonal city taxes
4734 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4735 }
4736
4737 foreach ($alltaxesfees as $tf) {
4738 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4739 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4740 $realcost = $tf['maxprice'];
4741 }
4742 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4743
4744 /**
4745 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
4746 *
4747 * @since 1.17.7 (J) - 1.7.7 (WP)
4748 */
4749 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$tf, ['days' => $num_nights], ['adults' => $num_adults]]);
4750 if ($custom_calculation) {
4751 $realcost = (float) $custom_calculation[0];
4752 }
4753
4754 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4755 if ($tf['is_citytax'] == 1) {
4756 $taxes += $realcost;
4757 } elseif ($tf['is_fee'] == 1) {
4758 $fees += $realcost;
4759 }
4760 $optsett = explode('-', $tf['forceval']);
4761 $options_data[] = $tf['id'].':'.$optsett[0];
4762 }
4763 }
4764
4765 return [
4766 'city_taxes' => $taxes,
4767 'fees' => $fees,
4768 'options' => $options_data
4769 ];
4770 }
4771
4772 /**
4773 * From a list of option/extras records, it returns an array with the options/extras
4774 * that do not support age intervals, and options for the children age intervals.
4775 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4776 *
4777 * @param array $optionals the full list of options/extras records.
4778 * @param int $adults the number of adults in the room requested.
4779 * @param int $children the number of children in the room requested.
4780 *
4781 * @return array the filtered regular and children age intervals options.
4782 */
4783 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4784 {
4785 // container for age intervals
4786 $ageintervals = [];
4787
4788 // regular options should start as an array
4789 if (!is_array($optionals)) {
4790 $optionals = [];
4791 }
4792
4793 // check for the first valid age intervals option
4794 foreach ($optionals as $kopt => $opt) {
4795 if (!empty($opt['ageintervals'])) {
4796 $intervals = explode(';;', $opt['ageintervals']);
4797 foreach ($intervals as $intv) {
4798 if (empty($intv)) {
4799 continue;
4800 }
4801 $parts = explode('_', $intv);
4802 if (count($parts) >= 3) {
4803 // age intervals option found, get the first and only one
4804 $ageintervals = $optionals[$kopt];
4805 break 2;
4806 }
4807 }
4808 }
4809 }
4810
4811 if ($ageintervals) {
4812 /**
4813 * We allow price adjustments for a minimum total guests by overriding the costs
4814 * for each age interval for specific child numbers when the party covers some children.
4815 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4816 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4817 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4818 *
4819 * @since 1.13.5
4820 */
4821 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4822 $oparams = !is_array($oparams) ? array() : $oparams;
4823 $adults = (int)$adults;
4824 $children = (int)$children;
4825 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4826 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4827 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4828 if ($children_to_pay < $children) {
4829 // compose string for free age intervals
4830 $intervals = explode(';;', $ageintervals['ageintervals']);
4831 foreach ($intervals as $k => $v) {
4832 $parts = explode('_', $v);
4833 // set cost to 0 (key = 2)
4834 $parts[2] = '0';
4835 $intervals[$k] = implode('_', $parts);
4836 }
4837 $free_ageintervals = implode(';;', $intervals);
4838 for ($i = 1; $i <= $children; $i++) {
4839 if (($children - $i + 1) <= $children_to_pay) {
4840 // costs for this Nth last child should be regular as this child should pay
4841 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4842 } else {
4843 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4844 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4845 }
4846 }
4847 }
4848 }
4849
4850 // remove age intervals from regular options
4851 foreach ($optionals as $kopt => $opt) {
4852 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4853 // unset the option of type age intervals from the regular options
4854 unset($optionals[$kopt]);
4855 }
4856 }
4857 }
4858
4859 // return the filtered list of regular options and age intervals options
4860 return [$optionals, $ageintervals];
4861 }
4862
4863 /**
4864 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4865 * This is because some children in the party may not need to pay anything due to the min guests.
4866 *
4867 * @param array $optional the option full record to parse.
4868 * @param int $adults the number of adults in the room requested.
4869 * @param int $children the number of children in the room requested.
4870 *
4871 * @return array associative array with default age intervals and override strings.
4872 *
4873 * @since 1.13.5
4874 */
4875 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4876 {
4877 if (!is_array($optional)) {
4878 $optional = array();
4879 }
4880
4881 $overrides = array(
4882 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4883 );
4884
4885 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4886 $oparams = !is_array($oparams) ? array() : $oparams;
4887 $adults = (int)$adults;
4888 $children = (int)$children;
4889
4890 if (!count($oparams)) {
4891 return $overrides;
4892 }
4893
4894 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4895 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4896 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4897 if ($children_to_pay < $children) {
4898 // compose string for free age intervals
4899 $intervals = explode(';;', $optional['ageintervals']);
4900 foreach ($intervals as $k => $v) {
4901 $parts = explode('_', $v);
4902 // set cost to 0 (key = 2)
4903 $parts[2] = '0';
4904 $intervals[$k] = implode('_', $parts);
4905 }
4906 $free_ageintervals = implode(';;', $intervals);
4907 for ($i = 1; $i <= $children; $i++) {
4908 if (($children - $i + 1) <= $children_to_pay) {
4909 // costs for this Nth last child should be regular as this child should pay
4910 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4911 } else {
4912 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4913 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4914 }
4915 }
4916 }
4917 }
4918
4919 return $overrides;
4920 }
4921
4922 /**
4923 * Returns the number/index of the children being parsed given the full list of room options string,
4924 * the current key in the array of the split room options string, the ID of the ageintervals option,
4925 * and the total number of children. This is useful for later applying the cost overrides for the
4926 * children ages through the method getOptionIntervalChildOverrides().
4927 *
4928 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4929 * @param int $optid the ID of the option of type age intervals to check.
4930 * @param int $roptkey the current position in the loop of the room options string.
4931 * @param int $children the number of children in the room requested.
4932 *
4933 * @return int the position/number of the children being parsed, -1 if not found.
4934 *
4935 * @since 1.13.5
4936 */
4937 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4938 {
4939 // default to -1 for not found
4940 $child_num = -1;
4941
4942 $valid_opt_counter = 0;
4943
4944 $roptions = explode(";", $roptstr);
4945 foreach ($roptions as $k => $opt) {
4946 if (empty($opt)) {
4947 continue;
4948 }
4949 $optvals = explode(":", $opt);
4950 /**
4951 * In some cases, like the "saveorder" task, the room option string may contain
4952 * the room number beside the option ID, separated with an underscore.
4953 * If an underscore is present, we need to split it to find the option ID.
4954 */
4955 if (strpos($optvals[0], '_') !== false) {
4956 // underscore found in room number portion, extract the option ID
4957 $rn_parts = explode('_', $optvals[0]);
4958 // 0th element is the room number in the party, 1st elem is the option ID
4959 $optvals[0] = $rn_parts[1];
4960 }
4961 //
4962 if ((int)$optvals[0] != (int)$optid) {
4963 // we are not interested into this option ID
4964 continue;
4965 }
4966 // increase counter for this option ID
4967 $valid_opt_counter++;
4968
4969 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4970 // children position found, we need to return it as a 0th base
4971 $child_num = ($valid_opt_counter - 1);
4972 break;
4973 }
4974 }
4975
4976 return $child_num;
4977 }
4978
4979 public static function getOptionIntervalsCosts($intvstr) {
4980 $optcosts = array();
4981 $intervals = explode(';;', $intvstr);
4982 foreach ($intervals as $kintv => $intv) {
4983 if (empty($intv)) continue;
4984 $parts = explode('_', $intv);
4985 if (count($parts) >= 3) {
4986 $optcosts[$kintv] = (float)$parts[2];
4987 }
4988 }
4989 return $optcosts;
4990 }
4991
4992 public static function getOptionIntervalsAges($intvstr) {
4993 $optages = 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 $optages[$kintv] = $parts[0].' - '.$parts[1];
5000 }
5001 }
5002 return $optages;
5003 }
5004
5005 public static function getOptionIntervalsPercentage($intvstr) {
5006 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
5007 $optcostspcent = array();
5008 $intervals = explode(';;', $intvstr);
5009 foreach ($intervals as $kintv => $intv) {
5010 if (empty($intv)) continue;
5011 $parts = explode('_', $intv);
5012 if (count($parts) >= 3) {
5013 //fixed amount
5014 $setval = 0;
5015 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
5016 //percentage value of the room base cost (VBO 1.10)
5017 $setval = 2;
5018 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
5019 //percentage value of the adults tariff
5020 $setval = 1;
5021 }
5022 $optcostspcent[$kintv] = $setval;
5023 }
5024 }
5025 return $optcostspcent;
5026 }
5027
5028 public static function dayValidTs($days, $first, $second)
5029 {
5030 $secdiff = $second - $first;
5031 $daysdiff = $secdiff / 86400;
5032 if (is_int($daysdiff)) {
5033 if ($daysdiff < 1) {
5034 $daysdiff = 1;
5035 }
5036 } else {
5037 if ($daysdiff < 1) {
5038 $daysdiff = 1;
5039 } else {
5040 $sum = floor($daysdiff) * 86400;
5041 $newdiff = $secdiff - $sum;
5042 $maxhmore = self::getHoursMoreRb() * 3600;
5043 if ($maxhmore >= $newdiff) {
5044 $daysdiff = floor($daysdiff);
5045 } else {
5046 $daysdiff = ceil($daysdiff);
5047 }
5048 }
5049 }
5050
5051 return ($daysdiff == $days);
5052 }
5053
5054 /**
5055 * Calculates the stay fee inclusive of taxes.
5056 *
5057 * @param float $cost the cost to evaluate.
5058 * @param int $idprice the rate plan ID associated.
5059 *
5060 * @return float the given cost after tax, if taxes had to be applied.
5061 */
5062 public static function sayCostPlusIva($cost, $idprice)
5063 {
5064 $ivainclusa = self::ivaInclusa();
5065 if ($ivainclusa) {
5066 return $cost;
5067 }
5068
5069 static $tax_rplans_map = [];
5070
5071 if (array_key_exists($idprice, $tax_rplans_map)) {
5072 $tax_rate = $tax_rplans_map[$idprice];
5073 } else {
5074 $dbo = JFactory::getDbo();
5075 $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 . ";";
5076 $dbo->setQuery($q);
5077 $tax_rate = $dbo->loadAssoc();
5078
5079 // cache value
5080 $tax_rplans_map[$idprice] = $tax_rate;
5081 }
5082
5083 if (!$tax_rate || empty($tax_rate['aliq'])) {
5084 return $cost;
5085 }
5086
5087 $subt = 100 + $tax_rate['aliq'];
5088 $op = ($cost * $subt / 100);
5089
5090 /**
5091 * Tax Cap implementation for prices tax excluded (most common).
5092 *
5093 * @since 1.12
5094 */
5095 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5096 $op = ($cost + $tax_rate['taxcap']);
5097 }
5098
5099 // apply rounding to avoid issues with multiple tax rates when tax excluded
5100 $formatvals = self::getNumberFormatData();
5101 $formatparts = explode(':', $formatvals);
5102 $rounded_op = round($op, (int)$formatparts[0]);
5103 if ($rounded_op > $op) {
5104 return $rounded_op;
5105 }
5106
5107 /**
5108 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5109 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5110 *
5111 * @since 1.11.1
5112 */
5113 return round($op, (int)$formatparts[0]);
5114 }
5115
5116 /**
5117 * Calculates the stay fee exclusive of taxes.
5118 *
5119 * @param float $cost the cost to evaluate.
5120 * @param int $idprice the rate plan ID associated.
5121 *
5122 * @return float the given cost before tax.
5123 */
5124 public static function sayCostMinusIva($cost, $idprice)
5125 {
5126 $ivainclusa = self::ivaInclusa();
5127 if (!$ivainclusa) {
5128 return $cost;
5129 }
5130
5131 static $tax_rplans_map = [];
5132
5133 if (array_key_exists($idprice, $tax_rplans_map)) {
5134 $tax_rate = $tax_rplans_map[$idprice];
5135 } else {
5136 $dbo = JFactory::getDbo();
5137 $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 . ";";
5138 $dbo->setQuery($q);
5139 $tax_rate = $dbo->loadAssoc();
5140
5141 // cache value
5142 $tax_rplans_map[$idprice] = $tax_rate;
5143 }
5144
5145 if (!$tax_rate || empty($tax_rate['aliq'])) {
5146 return $cost;
5147 }
5148
5149 $subt = 100 + $tax_rate['aliq'];
5150 $op = ($cost * 100 / $subt);
5151
5152 /**
5153 * Tax Cap implementation also when prices tax included.
5154 *
5155 * @since 1.12
5156 */
5157 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5158 $op = ($cost - $tax_rate['taxcap']);
5159 }
5160
5161 // apply rounding to avoid issues with multiple tax rates when tax included
5162 $formatvals = self::getNumberFormatData();
5163 $formatparts = explode(':', $formatvals);
5164 $rounded_op = round($op, (int)$formatparts[0]);
5165 if ($rounded_op < $op) {
5166 return $rounded_op;
5167 }
5168
5169 /**
5170 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5171 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5172 *
5173 * @since 1.11.1
5174 */
5175 return round($op, (int)$formatparts[0]);
5176 }
5177
5178 /**
5179 * Given an option/extra cost, determines whether taxes should be applied over it.
5180 * Can also be used to calculate taxes on the extra costs per room in the bookings
5181 *
5182 * @param float $cost the cost to evaluate, option or extra service fee.
5183 * @param int $idiva the ID of the tax rate to use to apply taxes.
5184 * @param bool $force if true, taxes will always be applied.
5185 *
5186 * @return float the given option/extra cost plus tax, if taxes had to be applied.
5187 *
5188 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5189 */
5190 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
5191 {
5192 $ivainclusa = self::ivaInclusa();
5193 if ($ivainclusa && $force !== true) {
5194 return $cost;
5195 }
5196
5197 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5198 if (!$tax_rate) {
5199 return $cost;
5200 }
5201
5202 $subt = 100 + $tax_rate['aliq'];
5203 $op = ($cost * $subt / 100);
5204
5205 /**
5206 * Tax Cap implementation for prices tax excluded (most common).
5207 *
5208 * @since 1.12
5209 */
5210 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5211 $op = ($cost + $tax_rate['taxcap']);
5212 }
5213
5214 // apply rounding to avoid issues with multiple tax rates when tax excluded
5215 $formatvals = self::getNumberFormatData();
5216 $formatparts = explode(':', $formatvals);
5217 $rounded_op = round($op, (int)$formatparts[0]);
5218 if ($rounded_op > $op) {
5219 return $rounded_op;
5220 }
5221
5222 return $op;
5223 }
5224
5225 /**
5226 * Calculates the net amount for an option/extra service.
5227 *
5228 * @param float $cost the cost to evaluate, option or extra service fee.
5229 * @param int $idiva the ID of the tax rate to use to apply taxes.
5230 *
5231 * @return float the given option/extra cost before tax, if taxes were inclusive.
5232 */
5233 public static function sayOptionalsMinusIva($cost, $idiva)
5234 {
5235 if (empty($idiva)) {
5236 return $cost;
5237 }
5238
5239 $ivainclusa = self::ivaInclusa();
5240 if (!$ivainclusa) {
5241 return $cost;
5242 }
5243
5244 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5245 if (!$tax_rate) {
5246 return $cost;
5247 }
5248
5249 $subt = 100 + $tax_rate['aliq'];
5250 $op = ($cost * 100 / $subt);
5251
5252 /**
5253 * Tax Cap implementation also when prices tax included.
5254 *
5255 * @since 1.12
5256 */
5257 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5258 $op = ($cost - $tax_rate['taxcap']);
5259 }
5260
5261 // apply rounding to avoid issues with multiple tax rates when tax included
5262 $formatvals = self::getNumberFormatData();
5263 $formatparts = explode(':', $formatvals);
5264 $rounded_op = round($op, (int)$formatparts[0]);
5265 if ($rounded_op < $op) {
5266 return $rounded_op;
5267 }
5268
5269 return $op;
5270 }
5271
5272 /**
5273 * Given a cost, determines whether taxes should be applied over it.
5274 *
5275 * @param float $cost the cost to evaluate, could be a room custom price.
5276 * @param int $idiva the ID of the tax rate to use to apply taxes.
5277 * @param bool $force if true, taxes will always be applied.
5278 *
5279 * @return float the given cost plus tax, if taxes had to be applied.
5280 *
5281 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5282 */
5283 public static function sayPackagePlusIva($cost, $idiva, $force = false)
5284 {
5285 $ivainclusa = self::ivaInclusa();
5286 if ($ivainclusa && $force !== true) {
5287 return $cost;
5288 }
5289
5290 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5291 if (!$tax_rate) {
5292 return $cost;
5293 }
5294
5295 $subt = 100 + $tax_rate['aliq'];
5296 $op = ($cost * $subt / 100);
5297
5298 /**
5299 * Tax Cap implementation for prices tax excluded (most common).
5300 *
5301 * @since 1.12
5302 */
5303 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5304 $op = ($cost + $tax_rate['taxcap']);
5305 }
5306
5307 // apply rounding to avoid issues with multiple tax rates when tax excluded
5308 $formatvals = self::getNumberFormatData();
5309 $formatparts = explode(':', $formatvals);
5310 $rounded_op = round($op, (int)$formatparts[0]);
5311 if ($rounded_op > $op) {
5312 return $rounded_op;
5313 }
5314
5315 return $op;
5316 }
5317
5318 /**
5319 * Calculates the net amount for a package.
5320 *
5321 * @param float $cost the cost to evaluate, option or extra service fee.
5322 * @param int $idiva the ID of the tax rate to use to apply taxes.
5323 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
5324 *
5325 * @return float the given cost before tax.
5326 */
5327 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
5328 {
5329 $ivainclusa = self::ivaInclusa();
5330 if (!$ivainclusa && $force_invoice_excltax !== true) {
5331 return $cost;
5332 }
5333
5334 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5335 if (!$tax_rate) {
5336 return $cost;
5337 }
5338
5339 $subt = 100 + $tax_rate['aliq'];
5340 $op = ($cost * 100 / $subt);
5341
5342 /**
5343 * Tax Cap implementation also when prices tax included.
5344 *
5345 * @since 1.12
5346 */
5347 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5348 $op = ($cost - $tax_rate['taxcap']);
5349 }
5350
5351 // apply rounding to avoid issues with multiple tax rates when tax included
5352 $formatvals = self::getNumberFormatData();
5353 $formatparts = explode(':', $formatvals);
5354 $rounded_op = round($op, (int)$formatparts[0]);
5355 if ($rounded_op < $op) {
5356 return $rounded_op;
5357 }
5358
5359 return $op;
5360 }
5361
5362 public static function getSecretLink()
5363 {
5364 $dbo = JFactory::getDbo();
5365
5366 $sid = mt_rand();
5367
5368 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
5369 $dbo->setQuery($q);
5370 $all = $dbo->loadAssocList();
5371 if ($all) {
5372 $list = [];
5373 foreach ($all as $s) {
5374 $list[] = $s['sid'];
5375 }
5376 if (in_array($sid, $list)) {
5377 while(in_array($sid, $list)) {
5378 $sid++;
5379 }
5380 }
5381 }
5382
5383 return $sid;
5384 }
5385
5386 /**
5387 * Generates a confirmation number for the given booking.
5388 * Modified the way random string-numbers are obtained to
5389 * avoid any possible brute-force attempt.
5390 *
5391 * @since 1.15.1 (J) - 1.5.4 (WP)
5392 */
5393 public static function generateConfirmNumber($oid, $update = true)
5394 {
5395 $dbo = JFactory::getDbo();
5396
5397 $confirmnumb = date('y') . $oid;
5398 for ($i = 0; $i < 8; $i++) {
5399 $confirmnumb .= rand(0, 9);
5400 }
5401
5402 if ($update) {
5403 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5404 $dbo->setQuery($q);
5405 $dbo->execute();
5406 }
5407
5408 return $confirmnumb;
5409 }
5410
5411 public static function buildCustData($arr, $sep) {
5412 $cdata = "";
5413 foreach ($arr as $k => $e) {
5414 if (strlen($e)) {
5415 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5416 }
5417 }
5418 return $cdata;
5419 }
5420
5421 /**
5422 * This method parses the Joomla menu object to see if a menu item of a
5423 * specific type is available, to get its ID. Useful when links should be
5424 * displayed in pages where there is no Itemid set (booking details pages).
5425 *
5426 * @param array $viewtypes list of accepted menu items
5427 * @param string $lang the optional language to use.
5428 *
5429 * @return int
5430 *
5431 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5432 */
5433 public static function findProperItemIdType($viewtypes, $lang = null)
5434 {
5435 if (VBOPlatformDetection::isWordPress()) {
5436 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5437
5438 $itemid = $model->best($viewtypes, $lang);
5439
5440 if ($itemid) {
5441 return $itemid;
5442 }
5443
5444 return 0;
5445 }
5446
5447 $bestitemid = 0;
5448
5449 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5450
5451 $app = JFactory::getApplication();
5452
5453 $menu = $app->getMenu('site');
5454
5455 if (!$menu) {
5456 return 0;
5457 }
5458
5459 $menu_items = $menu->getMenu();
5460
5461 if (!$menu_items) {
5462 return 0;
5463 }
5464
5465 foreach ($menu_items as $itemid => $item) {
5466 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5467 // proper menu item type found
5468 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5469
5470 if (isset($item->language) && $item->language == $current_lang) {
5471 // we found the exact menu item type for the given language
5472 return $itemid;
5473 }
5474 }
5475 }
5476
5477 return $bestitemid;
5478 }
5479
5480 /**
5481 * Rewrites an internal URI that needs to be used outside of the website.
5482 * This means that the routed URI MUST start with the base path of the site.
5483 *
5484 * @param mixed $query The query string or an associative array of data.
5485 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5486 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5487 *
5488 * @return string The complete routed URI.
5489 *
5490 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5491 * routing from back-end (if available on the CMS version).
5492 */
5493 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5494 {
5495 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5496 }
5497
5498 /**
5499 * Generates an iCal file to be attached to the email message for the
5500 * customer or the administrator with some basic booking details.
5501 *
5502 * @param string $recip either admin or customer.
5503 * @param array $booking the booking array or some keys.
5504 *
5505 * @return mixed string in case of success, false otherwise.
5506 *
5507 * @since 1.12.0
5508 */
5509 public static function getEmailIcal($recip, $booking) {
5510 // load configuration setting
5511 $attachical = self::attachIcal();
5512
5513 if ($attachical === 0) {
5514 // do not attach any iCal file
5515 return false;
5516 }
5517
5518 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5519 // skip the iCal for the admin
5520 return false;
5521 }
5522
5523 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5524 // skip the iCal for the customer
5525 return false;
5526 }
5527
5528 if (strpos($recip, 'admin') !== false) {
5529 // prepare event description and summary for the admin
5530 $description = $booking['custdata'];
5531 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5532 $fname = $booking['ts'] . '.ics';
5533 } else {
5534 // event description and summary for the customer
5535 $description = '';
5536 $summary = self::getFrontTitle();
5537 $fname = 'reservation_reminder.ics';
5538 }
5539
5540 // prepare iCal head
5541 $company_name = self::getFrontTitle();
5542 $ics_str = "BEGIN:VCALENDAR\r\n" .
5543 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5544 "CALSCALE:GREGORIAN\r\n" .
5545 "VERSION:2.0\r\n";
5546 // compose iCal body
5547 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5548 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5549 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5550 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5551 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5552 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5553 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5554 $ics_str .= 'END:VEVENT'."\r\n";
5555 // close iCal file content
5556 $ics_str .= "END:VCALENDAR";
5557
5558 /**
5559 * Trigger event to allow third party plugins to overwrite the iCal file.
5560 *
5561 * @since 1.16.0 (J) - 1.6.0 (WP)
5562 */
5563 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5564
5565 if (empty($ics_str)) {
5566 return false;
5567 }
5568
5569 // store the event onto a .ics file. We use the resources folder in back-end.
5570 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5571 $fp = fopen($fpath, 'w+');
5572 $bytes = fwrite($fp, $ics_str);
5573 fclose($fp);
5574
5575 return $bytes ? $fpath : false;
5576 }
5577
5578 public static function loadEmailTemplate($booking_info = array()) {
5579 define('_VIKBOOKINGEXEC', '1');
5580 ob_start();
5581 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5582 $content = ob_get_contents();
5583 ob_end_clean();
5584 return $content;
5585 }
5586
5587 /**
5588 * Parses the raw HTML content of the booking email template.
5589 *
5590 * @param string $tmpl the raw content of the template.
5591 * @param mixed $bid int for the booking ID or booking array.
5592 * @param array $rooms list of rooms booked and translated.
5593 * @param array $rates list of translated rates for the booked rooms.
5594 * @param array $options list of translated options booked.
5595 * @param float [$total] the booking total amount (in case it has changed).
5596 * @param string [$link] the booking link can be passed for the no-deposit.
5597 *
5598 * @return string the HTML content of the parsed email template.
5599 *
5600 * @since 1.13 with different arguments.
5601 */
5602 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5603 {
5604 $app = JFactory::getApplication();
5605 $dbo = JFactory::getDbo();
5606 $vbo_tn = self::getTranslator();
5607
5608 // availability helper
5609 $av_helper = self::getAvailabilityInstance();
5610
5611 // get necessary values
5612 if (is_array($bid)) {
5613 // we got the full booking record
5614 $order_info = $bid;
5615 $bid = $order_info['id'];
5616 } else {
5617 $order_info = array();
5618 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5619 $dbo->setQuery($q);
5620 $order_info = $dbo->loadAssoc();
5621 if (!$order_info) {
5622 throw new Exception('Booking not found', 404);
5623 }
5624 }
5625
5626 $tars_info = array();
5627 $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'] . ";";
5628 $dbo->setQuery($q);
5629 $tars_info = $dbo->loadAssocList();
5630 if (!$tars_info) {
5631 throw new Exception('No rooms found', 404);
5632 }
5633
5634 /**
5635 * Split stay reservation.
5636 *
5637 * @since 1.16.0 (J) - 1.6.0 (WP)
5638 */
5639 $room_stay_dates = [];
5640 if ($order_info['split_stay']) {
5641 if ($order_info['status'] == 'confirmed') {
5642 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5643 } else {
5644 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5645 }
5646 // immediately count the number of nights of stay for each split room
5647 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5648 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5649 // overwrite values for compatibility with non-confirmed bookings
5650 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5651 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5652 }
5653 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5654 // overwrite the whole array
5655 $room_stay_dates[$sps_r_k] = $sps_r_v;
5656 }
5657 }
5658
5659 // check if the language in use is the same as the one used during the checkout
5660 $lang = JFactory::getLanguage();
5661 if (!empty($order_info['lang'])) {
5662 if ($lang->getTag() != $order_info['lang']) {
5663 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5664 if (VBOPlatformDetection::isJoomla()) {
5665 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5666 }
5667 }
5668 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5669 // force the translation to start because contents should be translated
5670 $vbo_tn::$force_tolang = $order_info['lang'];
5671 }
5672 }
5673
5674 // values for replacements
5675 $company_name = self::getFrontTitle();
5676 $currencyname = self::getCurrencyName();
5677 $sitelogo = self::getSiteLogo();
5678 $footermess = self::getFooterOrdMail($vbo_tn);
5679 $dateformat = self::getDateFormat();
5680 $datesep = self::getDateSeparator();
5681 if ($dateformat == "%d/%m/%Y") {
5682 $df = 'd/m/Y';
5683 } elseif ($dateformat == "%m/%d/%Y") {
5684 $df = 'm/d/Y';
5685 } else {
5686 $df = 'Y/m/d';
5687 }
5688 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5689 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5690 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5691 $customer_info = nl2br($order_info['custdata']);
5692 $company_logo = '';
5693 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5694 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5695 }
5696 if ($order_info['status'] == 'cancelled') {
5697 $confirmnumber = '';
5698 $status_str = JText::translate('VBCANCELLED');
5699 } elseif ($order_info['status'] == 'standby') {
5700 $confirmnumber = '';
5701 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5702 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5703 // this is an inquiry reservation
5704 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5705 }
5706 } else {
5707 $confirmnumber = $order_info['confirmnumber'];
5708 $status_str = JText::translate('VBCOMPLETED');
5709 }
5710 // booking total amount
5711 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5712 // booking link
5713 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5714 if (is_null($link)) {
5715 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5716 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5717 }
5718
5719 // raw HTML content
5720 $parsed = $tmpl;
5721
5722 /**
5723 * Trigger event to allow third-party plugins to manipulate the template string.
5724 *
5725 * @since 1.16.10 (J) - 1.6.10 (WP)
5726 */
5727 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5728
5729 /**
5730 * Parse all conditional text rules.
5731 *
5732 * @since 1.14 (J) - 1.4.0 (WP)
5733 */
5734 self::getConditionalRulesInstance()
5735 ->set(['booking', 'rooms'], [$order_info, $rooms])
5736 ->parseTokens($parsed);
5737
5738 /**
5739 * Parse all Door Access Control tags.
5740 *
5741 * @since 1.18.4 (J) - 1.8.4 (WP)
5742 */
5743 VBOFactory::getDoorAccessControl()
5744 ->parseTokens((new VBOBookingRegistry($order_info, $rooms)), $parsed);
5745
5746 // special tokens (tags) replacement
5747 $parsed = str_replace("{logo}", $company_logo, $parsed);
5748 $parsed = str_replace("{company_name}", $company_name, $parsed);
5749 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5750 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5751 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5752 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5753 $parsed = str_replace("{order_status}", $status_str, $parsed);
5754 $parsed = str_replace("{order_date}", $create_date, $parsed);
5755
5756 // customer record
5757 $cpin = self::getCPinInstance();
5758 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5759 $customer_name = '';
5760 $customer_pin = '';
5761 if ($customer_record) {
5762 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5763 $customer_pin = $customer_record['pin'];
5764 }
5765 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5766 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5767
5768 // PIN Code
5769 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5770 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5771 if (!empty($customer_pin)) {
5772 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5773 }
5774 }
5775
5776 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5777
5778 // Confirmation Number
5779 if ($confirmnumber) {
5780 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5781 } else {
5782 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5783 }
5784 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5785 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5786
5787 $roomsnum = count($rooms);
5788 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5789 $roomstr = "";
5790 $tot_adults = 0;
5791 $tot_children = 0;
5792 $tot_guests = 0;
5793
5794 // Rooms Distinctive Features
5795 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5796
5797 foreach ($rooms as $num => $r) {
5798 // guests
5799 $tot_adults += (int) $r['adults'];
5800 $tot_children += (int) $r['children'];
5801 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5802
5803 // room info
5804 $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/>";
5805 // Rooms Distinctive Features
5806 if (is_array($matches[1] ?? []) && $matches[1]) {
5807 $distinctive_features = array();
5808 $rparams = (array)json_decode($r['params'], true);
5809 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'])) {
5810 $distinctive_features = $rparams['features'][$r['roomindex']];
5811 }
5812 $docheck = (count($distinctive_features) > 0);
5813 foreach ($matches[1] as $reqf) {
5814 $feature_found = false;
5815 if ($docheck) {
5816 foreach ($distinctive_features as $dfk => $dfv) {
5817 if (stripos($dfk, $reqf) !== false) {
5818 $feature_found = $dfk;
5819 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5820 break;
5821 }
5822 }
5823 }
5824 }
5825 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5826 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5827 }
5828 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5829 }
5830 }
5831 }
5832
5833 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5834 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5835 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5836
5837 // custom fields replace
5838 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5839 if (is_array($cmatches[1]) && count($cmatches[1])) {
5840 $cfids = array();
5841 foreach ($cmatches[1] as $cfid ) {
5842 $cfids[] = $cfid;
5843 }
5844 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5845 $dbo->setQuery($q);
5846 $cfields = $dbo->loadAssocList();
5847 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5848 $cfmap = array();
5849 foreach ($cfields as $cf) {
5850 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5851 }
5852 $cfmapreplace = array();
5853 $partsreceived = explode("\n", $order_info['custdata']);
5854 if (count($partsreceived) > 0) {
5855 foreach ($partsreceived as $pst) {
5856 if (!empty($pst)) {
5857 $tmpdata = explode(":", $pst);
5858 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5859 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5860 }
5861 }
5862 }
5863 }
5864 foreach ($cmatches[1] as $cfid ) {
5865 if (array_key_exists($cfid, $cfmapreplace)) {
5866 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5867 } else {
5868 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5869 }
5870 }
5871 }
5872
5873 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5874 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5875 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5876 $parsed = str_replace("{num_nights}", ($order_info['days'] ?? 1), $parsed);
5877
5878 // order details
5879 $orderdetails = "";
5880 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5881 foreach ($rooms as $num => $r) {
5882 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5883 $use_room_ind = $num - $room_loop_ind;
5884 $split_stay_str = '';
5885 $split_stay_info = [];
5886 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) {
5887 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5888 $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']);
5889 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5890 }
5891
5892 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5893 $faredets = explode(":", $expdet[0]);
5894 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5895 if (!empty($expdet[1])) {
5896 $attrfaredets = explode(":", $expdet[1]);
5897 if (strlen($attrfaredets[1]) > 0) {
5898 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5899 }
5900 }
5901 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5902 $orderdetails .= '<div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($fareprice), $currencyname) . '</span></div></div>';
5903 // options
5904 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5905 foreach ($options[$num] as $oo) {
5906 $expopts = explode("\n", $oo);
5907 foreach ($expopts as $optinfo) {
5908 if (!empty($optinfo)) {
5909 $splitopt = explode(":", $optinfo);
5910 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5911 $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>';
5912 }
5913 }
5914 }
5915 }
5916 //
5917 if ($roomsnum > 1 && $num < $roomsnum) {
5918 $orderdetails .= '<br/>';
5919 }
5920 }
5921
5922 // coupon
5923 if (!empty($order_info['coupon'])) {
5924 $expcoupon = explode(";", $order_info['coupon']);
5925 $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>';
5926 }
5927
5928 // discount and payment method
5929 $payment_info = [];
5930 $payment_name = '';
5931 if (!empty($order_info['idpayment'])) {
5932 $exppay = explode('=', $order_info['idpayment']);
5933 $payment = self::getPayment($exppay[0], $vbo_tn);
5934 if ((array) $payment) {
5935 $payment_name = $payment['name'];
5936 }
5937 }
5938
5939 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5940
5941 if ($order_info['status'] != 'cancelled') {
5942 if ($payment_info) {
5943 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5944 // Discount (not charge)
5945 if ($payment_info['val_pcent'] == 1) {
5946 // fixed value
5947 $total -= $payment_info['charge'];
5948 $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>';
5949 } else {
5950 // percent value
5951 $percent_disc = $total * $payment_info['charge'] / 100;
5952 $total -= $percent_disc;
5953 $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>';
5954 }
5955 }
5956 }
5957 }
5958
5959 // booking details string
5960 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5961
5962 // additional information
5963 $parsed = str_replace("{order_total}", self::formatCurrencyNumber(self::numberFormat($total), $currencyname), $parsed);
5964 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5965 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5966 $parsed = str_replace("{booking_link}", $link, $parsed);
5967
5968 // deposit
5969 $deposit_str = '';
5970 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5971 $percentdeposit = self::getAccPerCent();
5972 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5973 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5974 if (self::getTypeDeposit() == "fixed") {
5975 $deposit_amount = $percentdeposit;
5976 } else {
5977 $baseTotal = $total;
5978 if ($order_info['tot_damage_dep'] ?? 0) {
5979 // check if the damage deposit will have to be paid later/separately
5980 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($order_info, $rooms);
5981 if (($damage_deposit_payment['damagedep_gross'] ?? 0) && ($damage_deposit_payment['payment_window'] ?? []) && ($damage_deposit_payment['payment_window']['payable'] ?? null) !== true) {
5982 // damage deposit is not payable right now
5983 $baseTotal -= (float) $order_info['tot_damage_dep'];
5984 }
5985 }
5986 $deposit_amount = $baseTotal * $percentdeposit / 100;
5987 }
5988 if ($deposit_amount > 0) {
5989 $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>';
5990 }
5991 }
5992 }
5993 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5994 //
5995 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5996 $totpaid_str = '';
5997 if ($order_info['refund'] > 0) {
5998 $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>';
5999 }
6000 if ($order_info['status'] != 'cancelled') {
6001 $tot_paid = $order_info['totpaid'];
6002 $diff_topay = (float)$total - (float)$tot_paid;
6003 if ((float)$tot_paid > 0) {
6004 $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>';
6005 // only in case the remaining balance is greater than 1 to avoid commissions issues
6006 if ($diff_topay > 1) {
6007 $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>';
6008 }
6009 }
6010 }
6011 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
6012 $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>';
6013 }
6014 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
6015
6016 /**
6017 * Language direction (LTR or RTL).
6018 *
6019 * @since 1.16.4 (J) - 1.6.4 (WP)
6020 */
6021 $lang_direction = 'ltr';
6022 if (!empty($order_info['lang'])) {
6023 // attempt to define the language direction and replace the special tag
6024 try {
6025 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
6026 } catch (Throwable $e) {
6027 // do nothing, default to ltr
6028 $lang_direction = 'ltr';
6029 }
6030 // static map for Arabic and Hebrew
6031 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
6032 }
6033 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
6034 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
6035 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
6036
6037 /**
6038 * Replace static floating styles depending on language direction (LTR or RTL).
6039 * For BC with older email template sources, we need to keep the inline style
6040 * for the floating of certain elements, which looks good only on LTR.
6041 *
6042 * @since 1.16.4 (J) - 1.6.4 (WP)
6043 */
6044 if ($lang_direction === 'rtl') {
6045 // get rid of inline static style declaration because the template uses CSS classes
6046 $parsed = str_replace("float: right;", '', $parsed);
6047 }
6048
6049 return $parsed;
6050 }
6051
6052 /**
6053 * New method for sending booking email messages
6054 * to the guest or to the administrator(s).
6055 *
6056 * @param int $bid the booking ID.
6057 * @param array $for guest, admin or a custom email address.
6058 * @param bool $send whether to send or return the HTML message.
6059 * @param bool $no_config if true, no configuration settings will be used for the status.
6060 * @param string $type optional type to indicate a booking modification.
6061 *
6062 * @return mixed True or False depending on the result or HTML string for the preview.
6063 *
6064 * @since 1.13 (J) - 1.3 (WP)
6065 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
6066 * the reservation status is pending and against the configuration.
6067 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
6068 */
6069 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
6070 {
6071 $dbo = JFactory::getDbo();
6072 $app = JFactory::getApplication();
6073
6074 $vbo_tn = self::getTranslator();
6075 $av_helper = self::getAvailabilityInstance();
6076
6077 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
6078 $dbo->setQuery($q);
6079 $booking = $dbo->loadAssoc();
6080 if (!$booking) {
6081 return false;
6082 }
6083
6084 $result = false;
6085
6086 // check if the language in use is the same as the one used during the checkout
6087 $lang = JFactory::getLanguage();
6088 if (!empty($booking['lang'])) {
6089 if ($lang->getTag() != $booking['lang']) {
6090 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
6091 if (VBOPlatformDetection::isJoomla()) {
6092 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
6093 }
6094 }
6095 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
6096 // force the translation to start because contents should be translated
6097 $vbo_tn::$force_tolang = $booking['lang'];
6098 }
6099 }
6100
6101 // load rooms booked
6102 $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;";
6103 $dbo->setQuery($q);
6104 $ordersrooms = $dbo->loadAssocList();
6105 if (!$ordersrooms) {
6106 return false;
6107 }
6108 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
6109
6110 /**
6111 * Split stay reservation.
6112 *
6113 * @since 1.16.0 (J) - 1.6.0 (WP)
6114 */
6115 $room_stay_dates = [];
6116 if ($booking['split_stay']) {
6117 if ($booking['status'] == 'confirmed') {
6118 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
6119 } else {
6120 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
6121 }
6122 // immediately count the number of nights of stay for each split room
6123 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6124 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6125 // overwrite values for compatibility with non-confirmed bookings
6126 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6127 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6128 }
6129 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6130 // overwrite the whole array
6131 $room_stay_dates[$sps_r_k] = $sps_r_v;
6132 }
6133 }
6134
6135 $rooms = array();
6136 $tars = array();
6137 $is_package = !empty($booking['pkg']) ? true : false;
6138 $ftitle = self::getFrontTitle();
6139 $currencyname = self::getCurrencyName();
6140 foreach ($ordersrooms as $kor => $or) {
6141 $num = $kor + 1;
6142 $rooms[$num] = $or;
6143 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6144 // package or custom cost set from the back-end
6145 continue;
6146 }
6147
6148 // determine the days to consider for the count of the availability
6149 $room_nights = $booking['days'];
6150 $room_checkin = $booking['checkin'];
6151 $room_checkout = $booking['checkout'];
6152 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6153 $room_nights = $room_stay_dates[$kor]['nights'];
6154 $room_checkin = $room_stay_dates[$kor]['checkin'];
6155 $room_checkout = $room_stay_dates[$kor]['checkout'];
6156 }
6157
6158 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
6159 $dbo->setQuery($q);
6160 $tar = $dbo->loadAssocList();
6161 if (!$tar) {
6162 // tariff not found
6163 if (self::isAdmin()) {
6164 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
6165 }
6166 continue;
6167 }
6168
6169 // apply seasonal rates
6170 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
6171
6172 // different usage
6173 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
6174 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
6175 // Occupancy Override
6176 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
6177 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
6178 //
6179 if (is_array($diffusageprice)) {
6180 // set a charge or discount to the price(s) for the different usage of the room
6181 foreach ($tar as $kpr => $vpr) {
6182 $tar[$kpr]['diffusage'] = $or['adults'];
6183 if ($diffusageprice['chdisc'] == 1) {
6184 // charge
6185 if ($diffusageprice['valpcent'] == 1) {
6186 // fixed value
6187 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6188 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6189 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
6190 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6191 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
6192 } else {
6193 // percentage value
6194 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6195 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
6196 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
6197 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6198 $tar[$kpr]['cost'] = $aduseval;
6199 }
6200 } else {
6201 // discount
6202 if ($diffusageprice['valpcent'] == 1) {
6203 // fixed value
6204 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6205 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6206 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
6207 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6208 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
6209 } else {
6210 // percentage value
6211 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6212 $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);
6213 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
6214 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6215 $tar[$kpr]['cost'] = $aduseval;
6216 }
6217 }
6218 }
6219 }
6220 }
6221 //
6222 $tars[$num] = $tar[0];
6223 }
6224
6225 $secdiff = $booking['checkout'] - $booking['checkin'];
6226 $daysdiff = $secdiff / 86400;
6227 if (is_int($daysdiff)) {
6228 if ($daysdiff < 1) {
6229 $daysdiff = 1;
6230 }
6231 } else {
6232 if ($daysdiff < 1) {
6233 $daysdiff = 1;
6234 } else {
6235 $sum = floor($daysdiff) * 86400;
6236 $newdiff = $secdiff - $sum;
6237 $maxhmore = self::getHoursMoreRb() * 3600;
6238 if ($maxhmore >= $newdiff) {
6239 $daysdiff = floor($daysdiff);
6240 } else {
6241 $daysdiff = ceil($daysdiff);
6242 }
6243 }
6244 }
6245
6246 $isdue = 0;
6247 $pricestr = array();
6248 $optstr = array();
6249 foreach ($ordersrooms as $kor => $or) {
6250 $num = $kor + 1;
6251
6252 // determine the days to consider for the count of the availability
6253 $room_nights = $booking['days'];
6254 $room_checkin = $booking['checkin'];
6255 $room_checkout = $booking['checkout'];
6256 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6257 $room_nights = $room_stay_dates[$kor]['nights'];
6258 $room_checkin = $room_stay_dates[$kor]['checkin'];
6259 $room_checkout = $room_stay_dates[$kor]['checkout'];
6260 }
6261
6262 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6263 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
6264 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
6265 $isdue += $calctar;
6266 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
6267 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
6268 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6269 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
6270 $tars[$num]['calctar'] = $calctar;
6271 $isdue += $calctar;
6272 $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'] : "");
6273 }
6274
6275 if (!empty($or['optionals'])) {
6276 $stepo = explode(";", $or['optionals']);
6277 foreach ($stepo as $roptkey => $oo) {
6278 if (empty($oo)) {
6279 continue;
6280 }
6281 $stept = explode(":", $oo);
6282 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
6283 $dbo->setQuery($q);
6284 $actopt = $dbo->loadAssocList();
6285 if ($actopt) {
6286 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
6287 $chvar = '';
6288 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
6289 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
6290 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
6291 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
6292 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
6293 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
6294 $agestept = explode('-', $stept[1]);
6295 $stept[1] = $agestept[0];
6296 $chvar = $agestept[1];
6297 $realcost = 0;
6298 if (!empty($chvar)) {
6299 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
6300 // percentage value of the adults tariff
6301 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6302 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6303 } else {
6304 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6305 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6306 }
6307 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
6308 // VBO 1.10 - percentage value of room base cost
6309 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6310 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6311 } else {
6312 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
6313 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6314 }
6315 }
6316 $actopt[0]['chageintv'] = $chvar;
6317 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
6318 $actopt[0]['quan'] = $stept[1];
6319 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
6320 }
6321 } else {
6322 $actopt[0]['quan'] = $stept[1];
6323 // VBO 1.11 - options percentage cost of the room total fee
6324 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6325 $deftar_basecosts = $or['cust_cost'];
6326 } else {
6327 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6328 }
6329 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
6330 //
6331 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
6332 }
6333 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
6334 $realcost = $actopt[0]['maxprice'];
6335 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
6336 $realcost = $actopt[0]['maxprice'] * $stept[1];
6337 }
6338 }
6339 if ($actopt[0]['perperson'] == 1) {
6340 $realcost = $realcost * $or['adults'];
6341 }
6342
6343 /**
6344 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
6345 *
6346 * @since 1.17.7 (J) - 1.7.7 (WP)
6347 */
6348 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
6349 if ($custom_calculation) {
6350 $realcost = (float) $custom_calculation[0];
6351 }
6352
6353 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
6354 $isdue += $tmpopr;
6355 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
6356 }
6357 }
6358 }
6359
6360 // custom extra costs
6361 if (!empty($or['extracosts'])) {
6362 $cur_extra_costs = json_decode($or['extracosts'], true);
6363 foreach ($cur_extra_costs as $eck => $ecv) {
6364 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
6365 $isdue += $ecplustax;
6366 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
6367 }
6368 }
6369 }
6370
6371 // force the original total amount if rates have changed
6372 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
6373 $isdue = $booking['total'];
6374 }
6375
6376 // mail subject
6377 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
6378 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
6379
6380 // inject the recipient of the message for the template
6381 $booking['for'] = $for;
6382
6383 // load template file that will get $booking as variable
6384 $tmpl = self::loadEmailTemplate($booking);
6385
6386 // parse email template
6387 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
6388 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
6389
6390 if ($send !== true) {
6391 // return the content of the email message parsed
6392 return $hmess;
6393 }
6394
6395 // when the message can be sent
6396 $sendwhen = self::getSendEmailWhen();
6397
6398 if ($no_config) {
6399 // force the configuration setting to send the message no matter what's the booking status
6400 $sendwhen = 1;
6401 }
6402
6403 // send the message
6404 foreach ($for as $who) {
6405 $use_subject = $subject;
6406 $recipients = array();
6407 $attachments = self::addEmailAttachment(null);
6408 $attach_ical = false;
6409 $force_replyto = null;
6410 if (strpos($who, '@') !== false) {
6411 // send email to custom email address
6412 array_push($recipients, trim($who));
6413 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6414 // send email to the customer
6415 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6416 continue;
6417 }
6418 array_push($recipients, $booking['custmail']);
6419 /**
6420 * Check whether an iCal should be attached for the customer.
6421 *
6422 * @since 1.12.0
6423 */
6424 $attach_ical = self::getEmailIcal('customer', $booking);
6425 } elseif (stripos($who, 'admin') !== false) {
6426 // send email to the administrator(s)
6427 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6428 continue;
6429 }
6430 $use_subject = $subject . ' #' . $booking['id'];
6431 if (!strcasecmp($type, 'modified')) {
6432 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6433 }
6434 $adminemail = self::getAdminMail();
6435 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6436 if (empty($adminemail) && empty($extra_admin_recipients)) {
6437 // Prevent Joomla Exceptions that would stop the script execution
6438 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6439 continue;
6440 }
6441 if (strpos($adminemail, ',') !== false) {
6442 // multiple addresses
6443 $adminemails = explode(',', $adminemail);
6444 foreach ($adminemails as $am) {
6445 if (strpos($am, '@') !== false) {
6446 array_push($recipients, trim($am));
6447 }
6448 }
6449 } else {
6450 // single address
6451 array_push($recipients, trim($adminemail));
6452 }
6453
6454 // merge extra recipients
6455 $recipients = array_merge($recipients, $extra_admin_recipients);
6456
6457 // admin should reply to the customer
6458 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6459
6460 /**
6461 * Check whether an iCal should be attached for the admin.
6462 *
6463 * @since 1.2.0
6464 */
6465 $attach_ical = self::getEmailIcal('admin', array(
6466 'ts' => $booking['ts'],
6467 'custdata' => $booking['custdata'],
6468 'checkin' => $booking['checkin'],
6469 'checkout' => $booking['checkout'],
6470 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6471 ));
6472 }
6473
6474 // send the message, recipients should always be an array to support multiple admin addresses
6475
6476 // get sender e-mail
6477 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6478
6479 // init mail data
6480 $mail = new VBOMailWrapper([
6481 'sender' => [$adsendermail, $ftitle],
6482 'recipient' => $recipients,
6483 'bcc' => self::addAdminEmailRecipient(null, true),
6484 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6485 'subject' => $use_subject,
6486 'content' => $hmess,
6487 'attachments' => $attachments,
6488 ]);
6489
6490 if ($attach_ical !== false && $booking['status'] != 'standby') {
6491 // attach iCal file
6492 $mail->addAttachment($attach_ical);
6493 }
6494
6495 /**
6496 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6497 *
6498 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6499 *
6500 * @since 1.15.4 (J) - 1.5.10 (WP)
6501 */
6502 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6503
6504 // send e-mail
6505 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6506
6507 // unlink iCal file
6508 if ($attach_ical !== false) {
6509 @unlink($attach_ical);
6510 }
6511 }
6512
6513 return $result;
6514 }
6515
6516 /**
6517 * This method allows to add one or more recipient email
6518 * addresses for the next queue of email sending for the admin.
6519 * This method can be used in the template file for the customer
6520 * email to register an additional email address, maybe when a
6521 * specific room-type is booked.
6522 * The methods sending the email messages are supposed to call this
6523 * method by passing no arguments to obtain the extra addresses set.
6524 *
6525 * @param mixed $email null, string or array of email address(es).
6526 * @param bool $bcc if true, addresses will be used as bcc.
6527 * @param bool $reset if true, the queues will be emptied.
6528 *
6529 * @return array the current extra recipients or bcc addresses set.
6530 *
6531 * @since 1.13 (J) - 1.3.0 (WP)
6532 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6533 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6534 */
6535 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6536 {
6537 static $extra_recipients = [];
6538 static $extra_bcc = [];
6539
6540 if ($reset) {
6541 $extra_recipients = [];
6542 $extra_bcc = [];
6543 }
6544
6545 if (!empty($email)) {
6546 if (is_scalar($email)) {
6547 if ($bcc) {
6548 array_push($extra_bcc, $email);
6549 } else {
6550 array_push($extra_recipients, $email);
6551 }
6552 } else {
6553 if ($bcc) {
6554 $extra_bcc = array_merge($extra_bcc, $email);
6555 } else {
6556 $extra_recipients = array_merge($extra_recipients, $email);
6557 }
6558 }
6559 }
6560
6561 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6562 }
6563
6564 /**
6565 * This method serves to add one or more attachments to the
6566 * next queue of email sending for the admin.
6567 * The methods sending the email messages are supposed to call this
6568 * method by passing a null argument to obtain the attachments set.
6569 *
6570 * @param mixed $file null or string with path to file to attach.
6571 * @param bool $reset if true, the queue will be emptied.
6572 *
6573 * @return array the current attachments set.
6574 *
6575 * @since 1.14
6576 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6577 */
6578 public static function addEmailAttachment($file, $reset = false)
6579 {
6580 static $extra_attachments = [];
6581
6582 if ($reset) {
6583 $extra_attachments = [];
6584 }
6585
6586 if (!empty($file)) {
6587 if (is_scalar($file)) {
6588 array_push($extra_attachments, $file);
6589 } else {
6590 $extra_attachments = array_merge($extra_attachments, $file);
6591 }
6592 }
6593
6594 return array_unique($extra_attachments);
6595 }
6596
6597 /**
6598 * This method is called whenever some rooms get booked.
6599 * It checks whether the rooms involved have shared calendars.
6600 * If some are found, then also such rooms will be occupied.
6601 *
6602 * @param int $bid the booking ID.
6603 * @param array $roomids the list of rooms booked.
6604 * @param int $checkin checkin timestamp.
6605 * @param int $checkout checkout timestamp.
6606 *
6607 * @return boolean true if some other cals were occupied, false otherwise.
6608 *
6609 * @since 1.13 (J) - 1.3.0 (WP)
6610 *
6611 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6612 */
6613 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6614 {
6615 $dbo = JFactory::getDbo();
6616 $bid = (int)$bid;
6617
6618 // availability helper
6619 $av_helper = self::getAvailabilityInstance();
6620
6621 // check for split stay rooms reservation
6622 $known_split_stay = false;
6623 $is_split_stay = 0;
6624
6625 if (empty($checkin) || empty($checkout)) {
6626 // get checkin and checkout timestamps from booking
6627 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6628 $dbo->setQuery($q);
6629 $dbo->execute();
6630 if (!$dbo->getNumRows()) {
6631 // booking not found
6632 return false;
6633 }
6634 $bdata = $dbo->loadAssoc();
6635 $checkin = $bdata['checkin'];
6636 $checkout = $bdata['checkout'];
6637 $is_split_stay = $bdata['split_stay'];
6638 // turn flag on
6639 $known_split_stay = true;
6640 }
6641
6642 if (!$known_split_stay) {
6643 // we need to query the db to access this information
6644 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6645 $dbo->setQuery($q, 0, 1);
6646 $dbo->execute();
6647 if ($dbo->getNumRows()) {
6648 $is_split_stay = (int)$dbo->loadResult();
6649 }
6650 // turn flag on
6651 $known_split_stay = true;
6652 }
6653
6654 // check split stay room records
6655 $room_stay_dates = [];
6656 if ($is_split_stay) {
6657 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6658 // immediately count the number of nights of stay for each split room
6659 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6660 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6661 // overwrite values for compatibility with non-confirmed bookings
6662 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6663 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6664 }
6665 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6666 // overwrite the whole array
6667 $room_stay_dates[$sps_r_k] = $sps_r_v;
6668 }
6669 }
6670
6671 // get the rooms booked
6672 if (!count($roomids)) {
6673 // get the IDs of all rooms booked
6674 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6675 $dbo->setQuery($q);
6676 $orr = $dbo->loadAssocList();
6677 if ($orr) {
6678 foreach ($orr as $or) {
6679 array_push($roomids, $or['idroom']);
6680 }
6681 }
6682 }
6683 if (!count($roomids) || empty($bid)) {
6684 // unable to proceed
6685 return false;
6686 }
6687 $roomids = array_unique($roomids);
6688
6689 // build room stay dates map in case of split stay
6690 $room_split_stay_map = [];
6691 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6692 // we have unique room IDs in a split stay reservation for more than one room
6693 foreach ($room_stay_dates as $room_stay_date) {
6694 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6695 }
6696 }
6697
6698 // get rooms involved
6699 $involved = [];
6700 $involved_stay_map = [];
6701 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6702 $dbo->setQuery($q);
6703 $rooms_found = $dbo->loadAssocList();
6704 if (!$rooms_found) {
6705 // no rooms involved that need their calendars updated
6706 return false;
6707 }
6708 foreach ($rooms_found as $rf) {
6709 if (!in_array($rf['mainroom'], $roomids)) {
6710 // push room ID
6711 array_push($involved, $rf['mainroom']);
6712 // check for specific stay dates in connected room
6713 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6714 // split stay information available
6715 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6716 }
6717 }
6718 if (!in_array($rf['childroom'], $roomids)) {
6719 // push room ID
6720 array_push($involved, $rf['childroom']);
6721 // check for specific stay dates in connected room
6722 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6723 // split stay information available
6724 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6725 }
6726 }
6727 }
6728 $involved = array_unique($involved);
6729 if (!count($involved)) {
6730 // no rooms involved
6731 return false;
6732 }
6733
6734 // turnover seconds
6735 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6736
6737 // occupy the calendars for the involved rooms found
6738 $bids_generated = [];
6739 foreach ($involved as $rid) {
6740 // determine the values to use
6741 $room_checkin = (int)$checkin;
6742 $room_checkout = (int)$checkout;
6743 $room_realback = ($checkout + $turnover_secs);
6744 if (isset($involved_stay_map[$rid])) {
6745 // split stay dates available
6746 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6747 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6748 $room_realback = ($room_checkout + $turnover_secs);
6749 }
6750
6751 // occupy record in room with shared calendar
6752 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6753 $dbo->setQuery($q);
6754 $dbo->execute();
6755 array_push($bids_generated, $dbo->insertid());
6756 }
6757
6758 // store busy relations created
6759 foreach ($bids_generated as $busyid) {
6760 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6761 $dbo->setQuery($q);
6762 $dbo->execute();
6763 }
6764
6765 return true;
6766 }
6767
6768 /**
6769 * This method is needed whenever a booking gets modified by
6770 * adding or removing rooms. This way we reset (remove) all
6771 * busy records that were previously stored due to a shared
6772 * calendar. The correct relations should then be re-created
6773 * by calling updateSharedCalendars().
6774 *
6775 * @param int $bid the booking ID.
6776 *
6777 * @return boolean true if some records were cleaned.
6778 *
6779 * @see updateSharedCalendars() should be called after.
6780 *
6781 * @since 1.3.0
6782 */
6783 public static function cleanSharedCalendarsBusy($bid) {
6784 $dbo = JFactory::getDbo();
6785 $bid = (int)$bid;
6786 // get all the occupied records due to shared calendars for this booking
6787 $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;";
6788 $dbo->setQuery($q);
6789 $allbusy = $dbo->loadAssocList();
6790 if (!$allbusy) {
6791 return false;
6792 }
6793
6794 $busy_ids = array();
6795 foreach ($allbusy as $b) {
6796 // push busy ID to be removed later
6797 array_push($busy_ids, $b['id']);
6798 // delete the current busy ID-booking ID relation for this shared calendar
6799 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6800 $dbo->setQuery($q);
6801 $dbo->execute();
6802 }
6803 if (count($busy_ids)) {
6804 // delete all busy records due to shared calendars
6805 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6806 $dbo->setQuery($q);
6807 $dbo->execute();
6808 }
6809
6810 return true;
6811 }
6812
6813 public static function sendJutility() {
6814 //deprecated in VBO 1.10
6815 $dbo = JFactory::getDbo();
6816 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6817 $dbo->setQuery($q);
6818 $dbo->execute();
6819 $s = $dbo->loadAssocList();
6820 return (intval($s[0]['setting']) == 1 ? true : false);
6821 }
6822
6823 public static function getCategoryName($idcat, $vbo_tn = null) {
6824 $dbo = JFactory::getDbo();
6825 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6826 $dbo->setQuery($q);
6827 $dbo->execute();
6828 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6829 if (is_object($vbo_tn) && count($p) > 0) {
6830 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6831 }
6832 return count($p) > 0 ? $p[0]['name'] : '';
6833 }
6834
6835 public static function loadAdultsDiff($idroom, $adults)
6836 {
6837 static $obp_map = [];
6838
6839 $map_sign = "{$idroom}:{$adults}";
6840
6841 if (array_key_exists($map_sign, $obp_map)) {
6842 return $obp_map[$map_sign];
6843 }
6844
6845 $dbo = JFactory::getDbo();
6846
6847 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6848 $dbo->setQuery($q, 0, 1);
6849 $obp_diff = $dbo->loadAssoc();
6850 if ($obp_diff) {
6851 // cache value and return it
6852 $obp_map[$map_sign] = $obp_diff;
6853
6854 return $obp_diff;
6855 }
6856
6857 // cache value and return it
6858 $obp_map[$map_sign] = null;
6859
6860 return null;
6861 }
6862
6863 public static function loadRoomAdultsDiff($idroom)
6864 {
6865 $dbo = JFactory::getDbo();
6866
6867 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6868 $dbo->setQuery($q);
6869 $diff = $dbo->loadAssocList();
6870 if ($diff) {
6871 $roomdiff = [];
6872 foreach ($diff as $v) {
6873 $roomdiff[$v['adults']] = $v;
6874 }
6875
6876 return $roomdiff;
6877 }
6878
6879 return [];
6880 }
6881
6882 public static function occupancyOverrideExists($tar, $adults) {
6883 foreach ($tar as $k => $v) {
6884 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6885 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6886 return $v['occupancy_ovr'][$adults];
6887 }
6888 }
6889 }
6890 return false;
6891 }
6892
6893 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6894 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6895 $charges = array();
6896 if (!($children > 0) || !(count($ages) > 0)) {
6897 return $charges;
6898 }
6899 $dbo = JFactory::getDbo();
6900 $id_options = array();
6901 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6902 $dbo->setQuery($q);
6903 $dbo->execute();
6904 if ($dbo->getNumRows() > 0) {
6905 $assocs = $dbo->loadAssocList();
6906 foreach ($assocs as $opts) {
6907 if (!empty($opts['idopt'])) {
6908 $r_ido = explode(';', rtrim($opts['idopt']));
6909 foreach ($r_ido as $ido) {
6910 if (!empty($ido) && !in_array($ido, $id_options)) {
6911 $id_options[] = $ido;
6912 }
6913 }
6914 }
6915 }
6916 }
6917 if (count($id_options) > 0) {
6918 $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;";
6919 $dbo->setQuery($q);
6920 $dbo->execute();
6921 if ($dbo->getNumRows() > 0) {
6922 $ageintervals = $dbo->loadAssocList();
6923 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6924 $age_range = array();
6925 foreach ($split_ages as $kg => $spage) {
6926 if (empty($spage)) {
6927 continue;
6928 }
6929 $parts = explode('_', $spage);
6930 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6931 $ind = count($age_range);
6932 $age_range[$ind]['from'] = intval($parts[0]);
6933 $age_range[$ind]['to'] = intval($parts[1]);
6934 //taxes are calculated later in VCM
6935 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6936 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6937 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6938 }
6939 }
6940 if (count($age_range) > 0) {
6941 $tot_charge = 0;
6942 $affected = array();
6943 $option_str = '';
6944 foreach ($ages as $age) {
6945 if (strlen($age) == 0) {
6946 continue;
6947 }
6948 foreach ($age_range as $range) {
6949 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6950 $tot_charge += $range['cost'];
6951 $affected[] = $age;
6952 $option_str .= $range['option_str'].';';
6953 break;
6954 }
6955 }
6956 }
6957 if ($tot_charge > 0) {
6958 $charges['total'] = $tot_charge;
6959 $charges['affected'] = $affected;
6960 $charges['options'] = $option_str;
6961 }
6962 }
6963 }
6964 }
6965
6966 return $charges;
6967 }
6968
6969 public static function sortRoomPrices($arr) {
6970 $newarr = array();
6971 foreach ($arr as $k => $v) {
6972 $newarr[$k] = $v['cost'];
6973 }
6974 asort($newarr);
6975 $sorted = array();
6976 foreach ($newarr as $k => $v) {
6977 $sorted[$k] = $arr[$k];
6978 }
6979 return $sorted;
6980 }
6981
6982 public static function sortResults($arr) {
6983 $newarr = array();
6984 foreach ($arr as $k => $v) {
6985 $newarr[$k] = $v[0]['cost'];
6986 }
6987 asort($newarr);
6988 $sorted = array();
6989 foreach ($newarr as $k => $v) {
6990 $sorted[$k] = $arr[$k];
6991 }
6992 return $sorted;
6993 }
6994
6995 public static function sortMultipleResults($arr) {
6996 foreach ($arr as $k => $v) {
6997 $newarr = array();
6998 foreach ($v as $subk => $subv) {
6999 $newarr[$subk] = $subv[0]['cost'];
7000 }
7001 asort($newarr);
7002 $sorted = array();
7003 foreach ($newarr as $nk => $v) {
7004 $sorted[$nk] = $arr[$k][$nk];
7005 }
7006 $arr[$k] = $sorted;
7007 }
7008 return $arr;
7009 }
7010
7011 /**
7012 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
7013 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
7014 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
7015 *
7016 * @param array $promos optional list of promotion IDs to register in the execution flow.
7017 *
7018 * @return array list of cached promotion IDs (if any).
7019 *
7020 * @since 1.16.4 (J) - 1.6.4 (WP)
7021 */
7022 public static function registerPromotionIds(array $promos = [])
7023 {
7024 static $flagged_promos = [];
7025
7026 if ($promos) {
7027 // set cached value
7028 $flagged_promos = $promos;
7029 }
7030
7031 return $flagged_promos;
7032 }
7033
7034 /**
7035 * Getter and setter for seasons cache allowed.
7036 *
7037 * @param bool $enabled true or false for setter, null for getter.
7038 *
7039 * @return bool|null
7040 *
7041 * @since 1.16.5 (J) - 1.6.5 (WP)
7042 */
7043 public static function setSeasonsCache($enabled = null)
7044 {
7045 static $seasons_cache = null;
7046
7047 if ($enabled !== null) {
7048 // set value
7049 $seasons_cache = (bool)$enabled;
7050 }
7051
7052 return $seasons_cache;
7053 }
7054
7055 /**
7056 * Allows to preload and cache seasonal records for a list of rooms and dates.
7057 * Any subsequent preloading of season records for the same room(s) will return
7058 * the cached list. Should be used to preload a large window of data so that
7059 * subsequent calls will always return cached value.
7060 *
7061 * @param array $rooms List of involved room IDs.
7062 * @param int|bool $from Unix timestamp for start date or false to unset cache.
7063 * @param int $to Unix timestamp for end date.
7064 *
7065 * @return ?array
7066 *
7067 * @since 1.17.2 (J) - 1.7.2 (WP)
7068 */
7069 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
7070 {
7071 static $preloaded_records = [];
7072
7073 if (!$rooms) {
7074 // no cached signature
7075 return [];
7076 }
7077
7078 $signature = md5(implode(',', $rooms));
7079
7080 if ($from === false) {
7081 // unset cached records
7082 unset($preloaded_records[$signature]);
7083
7084 return;
7085 }
7086
7087 if ($from !== null && $to !== null) {
7088 // setter args signature for preloading records
7089 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
7090
7091 return;
7092 }
7093
7094 // getter args signature for getting the preloaded and cached records
7095 return $preloaded_records[$signature] ?? [];
7096 }
7097
7098 /**
7099 * Fetches all season records affecting a range of date timestamps.
7100 * Useful to pre-cache season records in case of hundreds of thousands
7101 * of records, but it can use up several MBs of server's memory.
7102 *
7103 * @param int $from unix timestamp for start date.
7104 * @param int $to unix timestamp for end date.
7105 * @param array $rooms optional list of involved room IDs.
7106 *
7107 * @return array
7108 *
7109 * @since 1.16.10 (J) - 1.6.10 (WP)
7110 */
7111 public static function getDateSeasonRecords($from, $to, array $rooms = [])
7112 {
7113 /**
7114 * We allow external systems to preload season records and cache them.
7115 *
7116 * @since 1.17.2 (J) - 1.7.2 (WP)
7117 */
7118 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
7119 return $preloaded_records;
7120 }
7121
7122 /**
7123 * For a more accurate records caching, ensure we have a range of two dates at least.
7124 *
7125 * @since 1.17.2 (J) - 1.7.2 (WP)
7126 */
7127 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
7128 // add one day to the end timestamp
7129 $to = strtotime('+1 day', $to);
7130 }
7131
7132 $dbo = JFactory::getDbo();
7133
7134 $one = getdate($from);
7135 $two = getdate($to);
7136
7137 // leap years
7138 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7139 $isleap = true;
7140 } else {
7141 $isleap = false;
7142 }
7143
7144 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7145 $tomidnightone = intval($one['hours']) * 3600;
7146 $tomidnightone += intval($one['minutes']) * 60;
7147 $sfrom = $from - $baseone - $tomidnightone;
7148 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7149 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7150 $tomidnighttwo = intval($two['hours']) * 3600;
7151 $tomidnighttwo += intval($two['minutes']) * 60;
7152 $sto = $to - $basetwo - $tomidnighttwo;
7153
7154 // leap years, check what dates to manipulate
7155 if ($isleap) {
7156 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7157 if ($one[0] > $leapts) {
7158 /**
7159 * Timestamp must be greater than the leap-day of Feb 29th.
7160 * It used to be checked for >= $leapts.
7161 *
7162 * @since July 3rd 2019
7163 */
7164 $sfrom -= 86400;
7165 }
7166 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7167 // lower checkin date when in leap year but not for checkout
7168 $sto -= 86400;
7169 }
7170 }
7171
7172 // check for DST changes to adjust the query values to fetch
7173 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7174 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7175 // we are parsing the day when the DST changed (probably a Sunday)
7176 if (!date('I', $from)) {
7177 // DST was just turned off
7178 $sfrom -= 3600;
7179 }
7180 }
7181 }
7182
7183 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7184 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7185 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7186 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7187 ($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`)") .
7188 ($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`)") .
7189 ($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`)") .
7190 ($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`)") .
7191 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7192 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7193
7194 /**
7195 * Avoid issues when querying data for a whole year by fetching all records.
7196 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
7197 *
7198 * @since 1.17.3 (J) - 1.7.3 (WP)
7199 */
7200 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
7201 // fetch all records when targeting a whole year of data
7202 $q = $dbo->getQuery(true)
7203 ->select('*')
7204 ->from($dbo->qn('#__vikbooking_seasons'))
7205 ->order($dbo->qn('promo') . ' ASC');
7206 }
7207
7208 $dbo->setQuery($q);
7209 $seasons = $dbo->loadAssocList();
7210
7211 if ($rooms) {
7212 // filter records by affected room IDs
7213 $seasons = array_filter($seasons, function($s) use ($rooms) {
7214 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
7215 foreach ($rooms as $idroom) {
7216 if (in_array("-" . $idroom . "-", $allrooms)) {
7217 return true;
7218 }
7219 }
7220 return false;
7221 });
7222
7223 // reset array keys
7224 $seasons = array_values($seasons);
7225 }
7226
7227 return $seasons;
7228 }
7229
7230 /**
7231 * Fetches all season records affecting week days with no dates.
7232 * Useful to avoid duplicate queries and cache/preload the records.
7233 *
7234 * @return array
7235 *
7236 * @since 1.18.5 (J) - 1.8.5 (WP)
7237 */
7238 public static function getWdaySeasonRecords()
7239 {
7240 static $cached_wdayseasons = null;
7241
7242 if (!is_null($cached_wdayseasons)) {
7243 return $cached_wdayseasons;
7244 }
7245
7246 $dbo = JFactory::getDbo();
7247
7248 $dbo->setQuery("SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));");
7249 $cached_wdayseasons = $dbo->loadAssocList();
7250
7251 return $cached_wdayseasons;
7252 }
7253
7254 /**
7255 * Applies the seasonal rates over a list of room rate records.
7256 *
7257 * @param array $arr list of room rate records.
7258 * @param int $from unix timestamp for start date.
7259 * @param int $to unix timestamp for end date.
7260 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7261 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7262 *
7263 * @return array list of manipulated room rate records.
7264 *
7265 * @since 1.18.0 (J) - 1.8.0 (WP) added support to records caching and preloading with args $seasons_dates and $seasons_wdays.
7266 * @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.
7267 */
7268 public static function applySeasonalPrices(array $arr, $from, $to, array $seasons_dates = [], ?array $seasons_wdays = null)
7269 {
7270 static $cached_seasons = [];
7271
7272 $cache_signature_wdays = '';
7273 if (self::setSeasonsCache() !== false && !$seasons_wdays) {
7274 // enable week-day season records caching
7275 $cache_signature_wdays = "wdays";
7276 }
7277
7278 $dbo = JFactory::getDbo();
7279 $vbo_tn = self::getTranslator();
7280
7281 $roomschange = [];
7282 $one = getdate($from);
7283
7284 // leap years
7285 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7286 $isleap = true;
7287 } else {
7288 $isleap = false;
7289 }
7290
7291 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7292 $tomidnightone = intval($one['hours']) * 3600;
7293 $tomidnightone += intval($one['minutes']) * 60;
7294 $sfrom = $from - $baseone - $tomidnightone;
7295 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7296 $two = getdate($to);
7297 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7298 $tomidnighttwo = intval($two['hours']) * 3600;
7299 $tomidnighttwo += intval($two['minutes']) * 60;
7300 $sto = $to - $basetwo - $tomidnighttwo;
7301
7302 // leap years, check what dates to manipulate
7303 if ($isleap) {
7304 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7305 if ($one[0] > $leapts) {
7306 /**
7307 * Timestamp must be greater than the leap-day of Feb 29th.
7308 * It used to be checked for >= $leapts.
7309 *
7310 * @since July 3rd 2019
7311 */
7312 $sfrom -= 86400;
7313 }
7314 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7315 // lower checkin date when in leap year but not for checkout
7316 $sto -= 86400;
7317 }
7318 }
7319
7320 // check for DST changes to adjust the query values to fetch
7321 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7322 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7323 // we are parsing the day when the DST changed (probably a Sunday)
7324 if (!date('I', $from)) {
7325 // DST was just turned off
7326 $sfrom -= 3600;
7327 }
7328 }
7329 }
7330
7331 // count nights requested
7332 $booking_nights = 1;
7333 foreach ($arr as $k => $a) {
7334 if (!isset($a[0])) {
7335 continue;
7336 }
7337 if (isset($a[0]['booking_nights'])) {
7338 // this value may be set when displaying pricing calendars
7339 $booking_nights = $a[0]['booking_nights'];
7340 break;
7341 }
7342 if (isset($a[0]['days'])) {
7343 $booking_nights = $a[0]['days'];
7344 break;
7345 }
7346 }
7347
7348 /**
7349 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7350 *
7351 * @since 1.16.4 (J) - 1.6.4 (WP)
7352 */
7353 $skip_promo_ids = self::registerPromotionIds();
7354
7355 $totseasons = 0;
7356 if (!$seasons_dates) {
7357 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7358 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7359 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7360 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7361 ($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`)") .
7362 ($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`)") .
7363 ($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`)") .
7364 ($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`)") .
7365 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7366 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7367
7368 // get the season records by running the query
7369 $dbo->setQuery($q);
7370 $seasons = $dbo->loadAssocList();
7371
7372 // count total seasons
7373 $totseasons = $seasons ? count($seasons) : 0;
7374 }
7375
7376 if ($totseasons > 0 || $seasons_dates) {
7377 if ($totseasons > 0) {
7378 $seasons = $seasons;
7379 } else {
7380 $seasons = $seasons_dates;
7381 }
7382 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7383 $applyseasons = false;
7384 $mem = [];
7385 foreach ($arr as $k => $a) {
7386 $mem[$k]['daysused'] = 0;
7387 $mem[$k]['sum'] = [];
7388 $mem[$k]['spids'] = [];
7389 /**
7390 * The keys below are all needed to apply the promotions on the room's final cost.
7391 *
7392 * @since 1.13.5
7393 */
7394 $mem[$k]['diffs'] = [];
7395 $mem[$k]['trans_keys'] = [];
7396 $mem[$k]['trans_factors'] = [];
7397 $mem[$k]['trans_affdays'] = [];
7398 }
7399 foreach ($seasons as $s) {
7400 // check if this is a promotion registered for skipping
7401 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7402 continue;
7403 }
7404
7405 // Special Price tied to the year
7406 if (!empty($s['year']) && $s['year'] > 0) {
7407 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7408 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7409 //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
7410 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7411 continue;
7412 }
7413 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7414 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7415 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7416 if ($calc_ends < ($from - $tomidnightone)) {
7417 continue;
7418 }
7419 }
7420 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7421 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7422 if ($s['from'] > $s['to']) {
7423 continue;
7424 }
7425 if (($basetwo + $s['from'] + 86399) > $to) {
7426 /**
7427 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7428 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7429 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7430 *
7431 * @since 1.14.3 (J) - 1.4.3 (WP)
7432 */
7433 continue;
7434 }
7435 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7436 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7437 /**
7438 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7439 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7440 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7441 *
7442 * @since 1.14.3 (J) - 1.4.3 (WP)
7443 */
7444 continue;
7445 }
7446 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7447 // 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)
7448 if (($baseone + $s['from']) > $to) {
7449 continue;
7450 }
7451 }
7452 }
7453 //
7454 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7455 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7456 $inits = $baseone + $s['from'];
7457 if ($s['from'] < $s['to']) {
7458 $ends = $basetwo + $s['to'];
7459 // check if the inits must be set to the year after
7460 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7461 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7462 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7463 $inits = $tmpbase + $s['from'];
7464 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7465 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7466 $ends = $baseone + $s['to'];
7467 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7468 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7469 $ends = $baseone + $s['to'];
7470 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7471 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7472 $inits = $basetwo + $s['from'];
7473 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7474 // booking dates across two years for a season tied to a specific year
7475 if ($one['year'] == $s['year']) {
7476 $inits = $baseone + $s['from'];
7477 } else {
7478 $inits = $basetwo + $s['from'];
7479 }
7480 if ($two['year'] == $s['year']) {
7481 $ends = $basetwo + $s['to'];
7482 } else {
7483 $ends = $baseone + $s['to'];
7484 }
7485 }
7486 } else {
7487 // between 2 years
7488 if ($baseone < $basetwo) {
7489 // ex. 29/12/2012 - 14/01/2013
7490 $ends = $basetwo + $s['to'];
7491 } else {
7492 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7493 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7494 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7495 $ends = $tmpbase + $s['to'];
7496 } else {
7497 // ex. 03/01 - 09/01
7498 $ends = $basetwo + $s['to'];
7499 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7500 $inits = $tmpbase + $s['from'];
7501 }
7502 }
7503 }
7504 // leap years
7505 if ($isleap == true) {
7506 $infoseason = getdate($inits);
7507 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7508 // leap years, check what dates to manipulate
7509 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7510 /**
7511 * Timestamp must be greater than the leap-day of Feb 29th.
7512 * It used to be checked for >= $leapts.
7513 *
7514 * @since July 3rd 2019
7515 */
7516 $inits += 86400;
7517 if ($s['from'] < $s['to']) {
7518 // increase season end date only if still on the same leap year
7519 $ends += 86400;
7520 }
7521 }
7522 }
7523
7524 // promotions
7525 $promotion = [];
7526 if (($s['promo'] ?? 0) == 1) {
7527 $daysadv = (($inits - time()) / 86400);
7528 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7529 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7530 continue;
7531 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7532 $secstocheckin = ($from - time());
7533 if ($s['promolastmin'] < $secstocheckin) {
7534 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7535 continue;
7536 }
7537 }
7538 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7539 /**
7540 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7541 *
7542 * @since 1.13.5
7543 */
7544 continue;
7545 }
7546 $promotion['todaydaysadv'] = $daysadv;
7547 $promotion['promodaysadv'] = $s['promodaysadv'];
7548 $promotion['promotxt'] = $s['promotxt'];
7549 }
7550
7551 // occupancy override
7552 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7553
7554 // week days
7555 $filterwdays = !empty($s['wdays']) ? true : false;
7556 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7557 if (is_array($wdays) && $wdays) {
7558 foreach ($wdays as $kw => $wd) {
7559 if (strlen($wd) == 0) {
7560 unset($wdays[$kw]);
7561 }
7562 }
7563 }
7564
7565 // checkin must be after the beginning of the season
7566 $checkininclok = true;
7567 if ($s['checkinincl'] == 1) {
7568 $checkininclok = false;
7569 if ($s['from'] < $s['to']) {
7570 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7571 $checkininclok = true;
7572 }
7573 } else {
7574 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7575 $checkininclok = true;
7576 }
7577 }
7578 }
7579 if ($checkininclok !== true) {
7580 continue;
7581 }
7582
7583 foreach ($arr as $k => $a) {
7584 // applied only to some types of price
7585 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7586 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7587 continue;
7588 }
7589 }
7590 // applied only to some room types
7591 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7592 continue;
7593 }
7594
7595 // count affected nights of stay
7596 $affdays = 0;
7597 $season_fromdayts = $fromdayts;
7598 $is_dst = date('I', $season_fromdayts);
7599 for ($i = 0; $i < $a[0]['days']; $i++) {
7600 $todayts = $season_fromdayts + ($i * 86400);
7601 $is_now_dst = date('I', $todayts);
7602 if ($is_dst != $is_now_dst) {
7603 // Daylight Saving Time has changed, check how
7604 if ((bool)$is_dst === true) {
7605 $todayts += 3600;
7606 $season_fromdayts += 3600;
7607 } else {
7608 $todayts -= 3600;
7609 $season_fromdayts -= 3600;
7610 }
7611 $is_dst = $is_now_dst;
7612 }
7613 if ($todayts >= $inits && $todayts <= $ends) {
7614 // week days
7615 if ($filterwdays == true) {
7616 $checkwday = getdate($todayts);
7617 if (in_array($checkwday['wday'], $wdays)) {
7618 $affdays++;
7619 }
7620 } else {
7621 $affdays++;
7622 }
7623 //
7624 }
7625 }
7626
7627 if (!($affdays > 0)) {
7628 // no nights affected
7629 continue;
7630 }
7631
7632 // apply the rule
7633 $applyseasons = true;
7634 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7635
7636 // modification factor object
7637 $factor = new stdClass;
7638
7639 // calculate new price progressively
7640 if (intval($s['val_pcent']) == 2) {
7641 // percentage value
7642 $factor->pcent = 1;
7643 $pctval = $s['diffcost'];
7644 if (strlen((string)$s['losoverride'])) {
7645 // values overrides
7646 $arrvaloverrides = [];
7647 $valovrparts = explode('_', $s['losoverride']);
7648 foreach ($valovrparts as $valovr) {
7649 if (!empty($valovr)) {
7650 $ovrinfo = explode(':', $valovr);
7651 if (strstr($ovrinfo[0], '-i') != false) {
7652 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7653 if ((int)$ovrinfo[0] < $a[0]['days']) {
7654 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7655 }
7656 }
7657 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7658 }
7659 }
7660 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7661 $pctval = $arrvaloverrides[$a[0]['days']];
7662 }
7663 }
7664 if (intval($s['type']) == 1) {
7665 // charge
7666 $factor->type = '+';
7667 $cpercent = 100 + $pctval;
7668 } else {
7669 // discount
7670 $factor->type = '-';
7671 $cpercent = 100 - $pctval;
7672 }
7673 $factor->amount = $pctval;
7674 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7675 } else {
7676 // absolute value
7677 $factor->pcent = 0;
7678 $absval = $s['diffcost'];
7679 if (strlen((string)$s['losoverride'])) {
7680 // values overrides
7681 $arrvaloverrides = [];
7682 $valovrparts = explode('_', $s['losoverride']);
7683 foreach ($valovrparts as $valovr) {
7684 if (!empty($valovr)) {
7685 $ovrinfo = explode(':', $valovr);
7686 if (strstr($ovrinfo[0], '-i') != false) {
7687 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7688 if ((int)$ovrinfo[0] < $a[0]['days']) {
7689 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7690 }
7691 }
7692 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7693 }
7694 }
7695 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7696 $absval = $arrvaloverrides[$a[0]['days']];
7697 }
7698 }
7699 if (intval($s['type']) == 1) {
7700 // charge
7701 $factor->type = '+';
7702 $newprice = ($dailyprice + $absval) * $affdays;
7703 } else {
7704 // discount
7705 $factor->type = '-';
7706 $newprice = ($dailyprice - $absval) * $affdays;
7707 }
7708 $factor->amount = $absval;
7709 }
7710
7711 // apply rounding
7712 $factor->roundmode = $s['roundmode'] ?? null;
7713 if (!empty($s['roundmode'])) {
7714 $newprice = round($newprice, 0, constant($s['roundmode']));
7715 } else {
7716 $newprice = round($newprice, 2);
7717 }
7718
7719 // define the promotion (only if no value overrides set the amount to 0)
7720 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7721 /**
7722 * Include the discount information (if any). The cost re-calculated may not be
7723 * precise if multiple special prices were applied over the same dates.
7724 *
7725 * @since 1.13
7726 */
7727 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7728 $promotion['discount'] = [
7729 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7730 'pcent' => (int)($s['val_pcent'] == 2),
7731 ];
7732 }
7733 //
7734 $mem[$k]['promotion'] = $promotion;
7735 }
7736
7737 // define the occupancy override
7738 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7739 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7740 }
7741
7742 // push special price ID
7743 if (!in_array($s['id'], $mem[$k]['spids'])) {
7744 array_push($mem[$k]['spids'], $s['id']);
7745 }
7746
7747 // push difference generated only if to be applied progressively
7748 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7749 /**
7750 * Push the difference generated by this special price for later transliteration of final price,
7751 * only if the special price is calculated progressively and not on the final price.
7752 *
7753 * @since 1.13.5
7754 */
7755 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7756 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7757 /**
7758 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7759 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7760 * all differences applied by the previous special pricing rules.
7761 *
7762 * @since 1.13.5
7763 */
7764 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7765 array_push($mem[$k]['trans_factors'], $factor);
7766 array_push($mem[$k]['trans_affdays'], $affdays);
7767 }
7768
7769 // push values in memory array
7770 array_push($mem[$k]['sum'], $newprice);
7771 $mem[$k]['daysused'] += $affdays;
7772 array_push($roomschange, $a[0]['idroom']);
7773 }
7774 }
7775 if ($applyseasons) {
7776 foreach ($mem as $k => $v) {
7777 if ($v['daysused'] > 0 && $v['sum']) {
7778 $newprice = 0;
7779 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7780 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7781 $addrest = $restdays * $dailyprice;
7782 $newprice += $addrest;
7783
7784 // calculate new final cost
7785 $redo_rounding = null;
7786 foreach ($v['sum'] as $sum_index => $add) {
7787 /**
7788 * The application of the various special pricing rules is made in a progressive and cumulative way
7789 * by always starting from the room base cost or its average daily cost. However, promotions may need
7790 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7791 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7792 * onto the differences generated by all the regular and progressively applied special pricing rules.
7793 *
7794 * @since 1.13.5
7795 */
7796 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7797 /**
7798 * This progressive price difference must be applied on the room final cost, so we need to
7799 * apply the transliteration over the other differences applied by other special prices.
7800 */
7801 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7802 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7803 // this is the % promotion we are looking for applying it on the final cost
7804 $factor = $v['trans_factors'][$transliterate_key];
7805 if (is_object($factor) && $factor->pcent) {
7806 $final_factor = 0;
7807 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7808 $final_factor += $prog_diff * $factor->amount / 100;
7809 }
7810 // update rounding
7811 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7812
7813 /**
7814 * Leverage the final factor depending on how many nights this affected.
7815 *
7816 * @since 1.15.0 (J) - 1.5.0 (WP)
7817 */
7818 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7819 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7820 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7821 }
7822
7823 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7824 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7825 }
7826 }
7827 }
7828
7829 // apply new price progressively
7830 $newprice += $add;
7831 }
7832
7833 // apply rounding from factor
7834 if (!empty($redo_rounding)) {
7835 $newprice = round($newprice, 0, constant($redo_rounding));
7836 }
7837
7838 // set promotion (if any)
7839 if (isset($v['promotion'])) {
7840 $arr[$k][0]['promotion'] = $v['promotion'];
7841 }
7842
7843 // set occupancy overrides (if any)
7844 if (isset($v['occupancy_ovr'])) {
7845 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7846 }
7847
7848 // set new final cost and update nights affected
7849 $arr[$k][0]['cost'] = $newprice;
7850 $arr[$k][0]['affdays'] = $v['daysused'];
7851 if (array_key_exists('spids', $v) && $v['spids']) {
7852 $arr[$k][0]['spids'] = $v['spids'];
7853 }
7854 }
7855 }
7856 }
7857 }
7858
7859 // week days with no season
7860 $roomschange = array_unique($roomschange);
7861 $totspecials = 0;
7862 if (!is_array($seasons_wdays)) {
7863 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7864 // avoid making a query
7865 $specials = $cached_seasons[$cache_signature_wdays];
7866 } else {
7867 // get the week-days-with-no-season records by running the query
7868 $specials = self::getWdaySeasonRecords();
7869 }
7870
7871 // count records
7872 $totspecials = $specials ? count($specials) : 0;
7873
7874 if ($cache_signature_wdays) {
7875 // cache week-days-with-no-season records
7876 $cached_seasons[$cache_signature_wdays] = $specials;
7877 }
7878 }
7879 if ($totspecials > 0 || $seasons_wdays) {
7880 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7881 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7882 $applyseasons = false;
7883 /**
7884 * We no longer unset the previous memory of the seasons with dates filters
7885 * because we need the responses to be merged. We do it only if not set.
7886 * We only keep the property 'spids' but the others should be unset.
7887 *
7888 * @since 1.11
7889 */
7890 if (!isset($mem)) {
7891 $mem = [];
7892 foreach ($arr as $k => $a) {
7893 $mem[$k]['daysused'] = 0;
7894 $mem[$k]['sum'] = [];
7895 $mem[$k]['spids'] = [];
7896 }
7897 } else {
7898 foreach ($arr as $k => $a) {
7899 $mem[$k]['daysused'] = 0;
7900 $mem[$k]['sum'] = [];
7901 }
7902 }
7903 //
7904 foreach ($specials as $s) {
7905 // check if this is a promotion registered for skipping
7906 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7907 continue;
7908 }
7909
7910 // Special Price tied to the year
7911 if (!empty($s['year']) && $s['year'] > 0) {
7912 if ($one['year'] != $s['year']) {
7913 continue;
7914 }
7915 }
7916
7917 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7918 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7919 // week days
7920 $filterwdays = !empty($s['wdays']) ? true : false;
7921 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7922 if (is_array($wdays) && $wdays) {
7923 foreach ($wdays as $kw => $wd) {
7924 if (strlen($wd) == 0) {
7925 unset($wdays[$kw]);
7926 }
7927 }
7928 }
7929
7930 foreach ($arr as $k => $a) {
7931 // only rooms with no price modifications from seasons
7932
7933 // applied only to some types of price
7934 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7935 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7936 continue;
7937 }
7938 }
7939
7940 /**
7941 * We should not exclude the rooms that already had a modification of the price through a season
7942 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7943 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7944 *
7945 * @since 1.11
7946 */
7947 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7948 continue;
7949 }
7950
7951 $affdays = 0;
7952 $season_fromdayts = $fromdayts;
7953 $is_dst = date('I', $season_fromdayts);
7954 for ($i = 0; $i < $a[0]['days']; $i++) {
7955 $todayts = $season_fromdayts + ($i * 86400);
7956 $is_now_dst = date('I', $todayts);
7957 if ($is_dst != $is_now_dst) {
7958 // Daylight Saving Time has changed, check how
7959 if ((bool)$is_dst === true) {
7960 $todayts += 3600;
7961 $season_fromdayts += 3600;
7962 } else {
7963 $todayts -= 3600;
7964 $season_fromdayts -= 3600;
7965 }
7966 $is_dst = $is_now_dst;
7967 }
7968 // week days
7969 if ($filterwdays == true) {
7970 $checkwday = getdate($todayts);
7971 if (in_array($checkwday['wday'], $wdays)) {
7972 $affdays++;
7973 }
7974 }
7975 }
7976 if (!($affdays > 0)) {
7977 // no nights affected
7978 continue;
7979 }
7980
7981 // apply the rule
7982 $applyseasons = true;
7983 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7984
7985 if (intval($s['val_pcent']) == 2) {
7986 // percentage value
7987 $pctval = $s['diffcost'];
7988 if (strlen((string)$s['losoverride'])) {
7989 // values overrides
7990 $arrvaloverrides = [];
7991 $valovrparts = explode('_', $s['losoverride']);
7992 foreach ($valovrparts as $valovr) {
7993 if (!empty($valovr)) {
7994 $ovrinfo = explode(':', $valovr);
7995 if (strstr($ovrinfo[0], '-i') != false) {
7996 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7997 if ((int)$ovrinfo[0] < $a[0]['days']) {
7998 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7999 }
8000 }
8001 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8002 }
8003 }
8004 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8005 $pctval = $arrvaloverrides[$a[0]['days']];
8006 }
8007 }
8008 if (intval($s['type']) == 1) {
8009 // charge
8010 $cpercent = 100 + $pctval;
8011 } else {
8012 // discount
8013 $cpercent = 100 - $pctval;
8014 }
8015 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
8016 } else {
8017 // absolute value
8018 $absval = $s['diffcost'];
8019 if (strlen((string)$s['losoverride'])) {
8020 // values overrides
8021 $arrvaloverrides = [];
8022 $valovrparts = explode('_', $s['losoverride']);
8023 foreach ($valovrparts as $valovr) {
8024 if (!empty($valovr)) {
8025 $ovrinfo = explode(':', $valovr);
8026 if (strstr($ovrinfo[0], '-i') != false) {
8027 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8028 if ((int)$ovrinfo[0] < $a[0]['days']) {
8029 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
8030 }
8031 }
8032 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8033 }
8034 }
8035 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8036 $absval = $arrvaloverrides[$a[0]['days']];
8037 }
8038 }
8039 if (intval($s['type']) == 1) {
8040 // charge
8041 $newprice = ($dailyprice + $absval) * $affdays;
8042 } else {
8043 // discount
8044 $newprice = ($dailyprice - $absval) * $affdays;
8045 }
8046 }
8047
8048 // apply rounding
8049 if (!empty($s['roundmode'])) {
8050 $newprice = round($newprice, 0, constant($s['roundmode']));
8051 } else {
8052 $newprice = round($newprice, 2);
8053 }
8054
8055 // push values in memory array
8056 if (!in_array($s['id'], $mem[$k]['spids'])) {
8057 $mem[$k]['spids'][] = $s['id'];
8058 }
8059 $mem[$k]['sum'][] = $newprice;
8060 $mem[$k]['daysused'] += $affdays;
8061 }
8062 }
8063 if ($applyseasons) {
8064 foreach ($mem as $k => $v) {
8065 if ($v['daysused'] > 0 && $v['sum']) {
8066 $newprice = 0;
8067 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
8068 $restdays = $arr[$k][0]['days'] - $v['daysused'];
8069 $addrest = $restdays * $dailyprice;
8070 $newprice += $addrest;
8071 foreach ($v['sum'] as $add) {
8072 $newprice += $add;
8073 }
8074 $arr[$k][0]['cost'] = $newprice;
8075 $arr[$k][0]['affdays'] = $v['daysused'];
8076 if (array_key_exists('spids', $v) && $v['spids']) {
8077 $arr[$k][0]['spids'] = $v['spids'];
8078 }
8079 }
8080 }
8081 }
8082 }
8083 // end week days with no season
8084
8085 return $arr;
8086 }
8087
8088 /**
8089 * Applies the seasonal rates over room rate records.
8090 *
8091 * @param array $arr array of tariffs taken from the DB
8092 * @param int $from start timestamp
8093 * @param int $to end timestamp
8094 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
8095 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
8096 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
8097 *
8098 * @return array
8099 *
8100 * @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.
8101 */
8102 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], ?array $seasons_wdays = null)
8103 {
8104 static $cached_seasons = [];
8105
8106 $cache_signature = '';
8107 $cache_signature_wdays = '';
8108 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
8109 // enable season records caching
8110 $cache_signature = "{$from}_{$to}";
8111 }
8112 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
8113 // enable week-day season records caching
8114 $cache_signature_wdays = "wdays";
8115 }
8116
8117 $dbo = JFactory::getDbo();
8118 $vbo_tn = self::getTranslator();
8119
8120 $roomschange = [];
8121 $one = getdate($from);
8122
8123 // leap years
8124 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
8125 $isleap = true;
8126 } else {
8127 $isleap = false;
8128 }
8129
8130 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
8131 $tomidnightone = intval($one['hours']) * 3600;
8132 $tomidnightone += intval($one['minutes']) * 60;
8133 $sfrom = $from - $baseone - $tomidnightone;
8134 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
8135 $two = getdate($to);
8136 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
8137 $tomidnighttwo = intval($two['hours']) * 3600;
8138 $tomidnighttwo += intval($two['minutes']) * 60;
8139 $sto = $to - $basetwo - $tomidnighttwo;
8140
8141 // leap years, check what dates to manipulate
8142 if ($isleap) {
8143 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
8144 if ($one[0] > $leapts) {
8145 /**
8146 * Timestamp must be greater than the leap-day of Feb 29th.
8147 * It used to be checked for >= $leapts.
8148 *
8149 * @since July 3rd 2019
8150 */
8151 $sfrom -= 86400;
8152 }
8153 if ($two[0] > $leapts && $one['year'] == $two['year']) {
8154 // lower checkin date when in leap year but not for checkout
8155 $sto -= 86400;
8156 }
8157 }
8158
8159 // check for DST changes to adjust the query values to fetch
8160 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
8161 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
8162 // we are parsing the day when the DST changed (probably a Sunday)
8163 if (!date('I', $from)) {
8164 // DST was just turned off
8165 $sfrom -= 3600;
8166 }
8167 }
8168 }
8169
8170 // count nights requested
8171 $booking_nights = 1;
8172 foreach ($arr as $k => $a) {
8173 if (isset($a['booking_nights'])) {
8174 // this value may be set when displaying pricing calendars
8175 $booking_nights = $a['booking_nights'];
8176 break;
8177 }
8178 if (isset($a['days'])) {
8179 $booking_nights = $a['days'];
8180 break;
8181 }
8182 }
8183
8184 /**
8185 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
8186 *
8187 * @since 1.16.4 (J) - 1.6.4 (WP)
8188 */
8189 $skip_promo_ids = self::registerPromotionIds();
8190
8191 $totseasons = 0;
8192 if (!$parsed_season && !$seasons_dates) {
8193 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
8194 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
8195 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
8196 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
8197 ($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`)") .
8198 ($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`)") .
8199 ($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`)") .
8200 ($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`)") .
8201 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
8202 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
8203
8204 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
8205 // avoid making a query
8206 $seasons = $cached_seasons[$cache_signature];
8207 } else {
8208 // get the season records by running the query
8209 $dbo->setQuery($q);
8210 $seasons = $dbo->loadAssocList();
8211 }
8212
8213 // count total seasons
8214 $totseasons = $seasons ? count($seasons) : 0;
8215
8216 if ($cache_signature) {
8217 // cache season records
8218 $cached_seasons[$cache_signature] = $seasons;
8219 }
8220 }
8221
8222 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
8223 if ($totseasons > 0) {
8224 $seasons = $seasons;
8225 } elseif ($parsed_season) {
8226 $seasons = [$parsed_season];
8227 } else {
8228 $seasons = $seasons_dates;
8229 }
8230 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
8231 $applyseasons = false;
8232 $mem = [];
8233 foreach ($arr as $k => $a) {
8234 $mem[$k]['daysused'] = 0;
8235 $mem[$k]['sum'] = [];
8236 $mem[$k]['spids'] = [];
8237 /**
8238 * The keys below are all needed to apply the promotions on the room's final cost.
8239 *
8240 * @since 1.13.5
8241 */
8242 $mem[$k]['diffs'] = [];
8243 $mem[$k]['trans_keys'] = [];
8244 $mem[$k]['trans_factors'] = [];
8245 $mem[$k]['trans_affdays'] = [];
8246 }
8247 $affdayslistless = [];
8248 foreach ($seasons as $s) {
8249 // 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
8250 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
8251 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
8252 continue;
8253 }
8254
8255 /**
8256 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
8257 * We need to skip such fake seasons as they do not need any parsing.
8258 *
8259 * @since 1.13
8260 */
8261 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
8262 continue;
8263 }
8264
8265 // check if this is a promotion registered for skipping
8266 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8267 continue;
8268 }
8269
8270 // Special Price tied to the year
8271 if (!empty($s['year']) && $s['year'] > 0) {
8272 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
8273 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
8274 //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
8275 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
8276 continue;
8277 }
8278 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
8279 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
8280 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
8281 if ($calc_ends < ($from - $tomidnightone)) {
8282 continue;
8283 }
8284 }
8285 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
8286 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
8287 if ($s['from'] > $s['to']) {
8288 continue;
8289 }
8290 if (($basetwo + $s['from'] + 86399) > $to) {
8291 /**
8292 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
8293 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
8294 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
8295 *
8296 * @since 1.14.3 (J) - 1.4.3 (WP)
8297 */
8298 continue;
8299 }
8300 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
8301 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
8302 /**
8303 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
8304 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
8305 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
8306 *
8307 * @since 1.14.3 (J) - 1.4.3 (WP)
8308 */
8309 continue;
8310 }
8311 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
8312 // 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)
8313 if (($baseone + $s['from']) > $to) {
8314 continue;
8315 }
8316 }
8317 }
8318 //
8319 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8320 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8321 $inits = $baseone + $s['from'];
8322 if ($s['from'] < $s['to']) {
8323 $ends = $basetwo + $s['to'];
8324 // check if the inits must be set to the year after
8325 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
8326 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
8327 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8328 $inits = $tmpbase + $s['from'];
8329 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8330 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
8331 $ends = $baseone + $s['to'];
8332 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8333 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
8334 $ends = $baseone + $s['to'];
8335 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
8336 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
8337 $inits = $basetwo + $s['from'];
8338 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
8339 // booking dates across two years for a season tied to a specific year
8340 if ($one['year'] == $s['year']) {
8341 $inits = $baseone + $s['from'];
8342 } else {
8343 $inits = $basetwo + $s['from'];
8344 }
8345 if ($two['year'] == $s['year']) {
8346 $ends = $basetwo + $s['to'];
8347 } else {
8348 $ends = $baseone + $s['to'];
8349 }
8350 }
8351 } else {
8352 // between 2 years
8353 if ($baseone < $basetwo) {
8354 // ex. 29/12/2012 - 14/01/2013
8355 $ends = $basetwo + $s['to'];
8356 } else {
8357 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
8358 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
8359 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8360 $ends = $tmpbase + $s['to'];
8361 } else {
8362 // ex. 03/01 - 09/01
8363 $ends = $basetwo + $s['to'];
8364 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
8365 $inits = $tmpbase + $s['from'];
8366 }
8367 }
8368 }
8369 // leap years
8370 if ($isleap == true) {
8371 $infoseason = getdate($inits);
8372 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
8373 // leap years, check what dates to manipulate
8374 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
8375 /**
8376 * Timestamp must be greater than the leap-day of Feb 29th.
8377 * It used to be checked for >= $leapts.
8378 *
8379 * @since July 3rd 2019
8380 */
8381 $inits += 86400;
8382 if ($s['from'] < $s['to']) {
8383 // increase season end date only if still on the same leap year
8384 $ends += 86400;
8385 }
8386 }
8387 }
8388
8389 // promotions
8390 $promotion = [];
8391 if (($s['promo'] ?? 0) == 1) {
8392 $daysadv = (($inits - time()) / 86400);
8393 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
8394 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
8395 continue;
8396 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
8397 $secstocheckin = ($from - time());
8398 if ($s['promolastmin'] < $secstocheckin) {
8399 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
8400 continue;
8401 }
8402 }
8403 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
8404 /**
8405 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
8406 *
8407 * @since 1.13.5
8408 */
8409 continue;
8410 }
8411 $promotion['todaydaysadv'] = $daysadv;
8412 $promotion['promodaysadv'] = $s['promodaysadv'];
8413 $promotion['promotxt'] = $s['promotxt'];
8414 }
8415
8416 // occupancy override
8417 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
8418
8419 // week days
8420 $filterwdays = !empty($s['wdays']) ? true : false;
8421 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8422 if (is_array($wdays) && $wdays) {
8423 foreach ($wdays as $kw => $wd) {
8424 if (strlen($wd) == 0) {
8425 unset($wdays[$kw]);
8426 }
8427 }
8428 }
8429
8430 // checkin must be after the beginning of the season
8431 $checkininclok = true;
8432 if ($s['checkinincl'] == 1) {
8433 $checkininclok = false;
8434 if ($s['from'] < $s['to']) {
8435 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
8436 $checkininclok = true;
8437 }
8438 } else {
8439 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
8440 $checkininclok = true;
8441 }
8442 }
8443 }
8444 if ($checkininclok !== true) {
8445 continue;
8446 }
8447
8448 foreach ($arr as $k => $a) {
8449 // applied only to some types of price
8450 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8451 // VikBooking 1.6: Price Calendar sets the idprice to -1
8452 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8453 continue;
8454 }
8455 }
8456 // applied only to some room types
8457 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8458 continue;
8459 }
8460
8461 if (isset($a['days']) && is_float($a['days'])) {
8462 // prevent any possible warning with array_key_exists()
8463 $a['days'] = (int)$a['days'];
8464 }
8465
8466 $affdays = 0;
8467 $season_fromdayts = $fromdayts;
8468 $is_dst = date('I', $season_fromdayts);
8469 for ($i = 0; $i < $a['days']; $i++) {
8470 $todayts = $season_fromdayts + ($i * 86400);
8471 $is_now_dst = date('I', $todayts);
8472 if ($is_dst != $is_now_dst) {
8473 // Daylight Saving Time has changed, check how
8474 if ((bool)$is_dst === true) {
8475 $todayts += 3600;
8476 $season_fromdayts += 3600;
8477 } else {
8478 $todayts -= 3600;
8479 $season_fromdayts -= 3600;
8480 }
8481 $is_dst = $is_now_dst;
8482 }
8483 if ($todayts >= $inits && $todayts <= $ends) {
8484 $checkwday = getdate($todayts);
8485 // week days
8486 if ($filterwdays == true) {
8487 if (in_array($checkwday['wday'], $wdays)) {
8488 if (!isset($arr[$k]['affdayslist'])) {
8489 $arr[$k]['affdayslist'] = [];
8490 }
8491 $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;
8492 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8493 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8494 $affdays++;
8495 }
8496 } else {
8497 if (!isset($arr[$k]['affdayslist'])) {
8498 $arr[$k]['affdayslist'] = [];
8499 }
8500 $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;
8501 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8502 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8503 $affdays++;
8504 }
8505 //
8506 }
8507 }
8508
8509 if (!($affdays > 0)) {
8510 // no nights affected
8511 continue;
8512 }
8513
8514 // apply the rule
8515 $applyseasons = true;
8516 $dailyprice = $a['cost'] / $a['days'];
8517
8518 // modification factor object
8519 $factor = new stdClass;
8520
8521 // calculate new price progressively
8522 if (intval($s['val_pcent']) == 2) {
8523 // percentage value
8524 $factor->pcent = 1;
8525 $pctval = $s['diffcost'];
8526 if (strlen((string)$s['losoverride'])) {
8527 // values overrides
8528 $arrvaloverrides = [];
8529 $valovrparts = explode('_', $s['losoverride']);
8530 foreach ($valovrparts as $valovr) {
8531 if (!empty($valovr)) {
8532 $ovrinfo = explode(':', $valovr);
8533 if (strstr($ovrinfo[0], '-i') != false) {
8534 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8535 if ((int)$ovrinfo[0] < $a['days']) {
8536 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8537 }
8538 }
8539 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8540 }
8541 }
8542 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8543 $pctval = $arrvaloverrides[$a['days']];
8544 }
8545 }
8546 if (intval($s['type']) == 1) {
8547 // charge
8548 $factor->type = '+';
8549 $cpercent = 100 + $pctval;
8550 } else {
8551 // discount
8552 $factor->type = '-';
8553 $cpercent = 100 - $pctval;
8554 }
8555 $factor->amount = $pctval;
8556 $dailysum = ($dailyprice * $cpercent / 100);
8557 $newprice = $dailysum * $affdays;
8558 } else {
8559 // absolute value
8560 $factor->pcent = 0;
8561 $absval = $s['diffcost'];
8562 if (strlen((string)$s['losoverride'])) {
8563 // values overrides
8564 $arrvaloverrides = [];
8565 $valovrparts = explode('_', $s['losoverride']);
8566 foreach ($valovrparts as $valovr) {
8567 if (!empty($valovr)) {
8568 $ovrinfo = explode(':', $valovr);
8569 if (strstr($ovrinfo[0], '-i') != false) {
8570 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8571 if ((int)$ovrinfo[0] < $a['days']) {
8572 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8573 }
8574 }
8575 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8576 }
8577 }
8578 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8579 $absval = $arrvaloverrides[$a['days']];
8580 }
8581 }
8582 if (intval($s['type']) == 1) {
8583 // charge
8584 $factor->type = '+';
8585 $dailysum = ($dailyprice + $absval);
8586 $newprice = $dailysum * $affdays;
8587 } else {
8588 // discount
8589 $factor->type = '-';
8590 $dailysum = ($dailyprice - $absval);
8591 $newprice = $dailysum * $affdays;
8592 }
8593 $factor->amount = $absval;
8594 }
8595
8596 // apply rounding
8597 $factor->roundmode = $s['roundmode'] ?? null;
8598 if (!empty($s['roundmode'])) {
8599 $newprice = round($newprice, 0, constant($s['roundmode']));
8600 } else {
8601 $newprice = round($newprice, 2);
8602 }
8603
8604 // define the promotion (only if no value overrides set the amount to 0)
8605 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8606 /**
8607 * Include the discount information (if any). The cost re-calculated may not be
8608 * precise if multiple special prices were applied over the same dates.
8609 *
8610 * @since 1.13
8611 */
8612 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8613 $promotion['discount'] = [
8614 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8615 'pcent' => (int)($s['val_pcent'] == 2),
8616 ];
8617 }
8618 //
8619 $mem[$k]['promotion'] = $promotion;
8620 }
8621
8622 // define the occupancy override
8623 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8624 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8625 }
8626
8627 // affected days list
8628 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8629 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8630 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8631 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8632 }
8633 }
8634 }
8635
8636 // push special price ID
8637 if (!in_array($s['id'], $mem[$k]['spids'])) {
8638 array_push($mem[$k]['spids'], $s['id']);
8639 }
8640
8641 // push difference generated only if to be applied progressively
8642 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8643 /**
8644 * Push the difference generated by this special price for later transliteration of final price,
8645 * only if the special price is calculated progressively and not on the final price.
8646 *
8647 * @since 1.13.5
8648 */
8649 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8650 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8651 /**
8652 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8653 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8654 * all differences applied by the previous special pricing rules.
8655 *
8656 * @since 1.13.5
8657 */
8658 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8659 array_push($mem[$k]['trans_factors'], $factor);
8660 array_push($mem[$k]['trans_affdays'], $affdays);
8661 }
8662
8663 // push values in memory array
8664 array_push($mem[$k]['sum'], $newprice);
8665 $mem[$k]['daysused'] += $affdays;
8666 array_push($roomschange, $a['idroom']);
8667 }
8668 }
8669 if ($applyseasons) {
8670 foreach ($mem as $k => $v) {
8671 if ($v['daysused'] > 0 && $v['sum']) {
8672 $newprice = 0;
8673 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8674 $restdays = $arr[$k]['days'] - $v['daysused'];
8675 $addrest = $restdays * $dailyprice;
8676 $newprice += $addrest;
8677
8678 // calculate new final cost
8679 $redo_rounding = null;
8680 foreach ($v['sum'] as $sum_index => $add) {
8681 /**
8682 * The application of the various special pricing rules is made in a progressive and cumulative way
8683 * by always starting from the room base cost or its average daily cost. However, promotions may need
8684 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8685 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8686 * onto the differences generated by all the regular and progressively applied special pricing rules.
8687 *
8688 * @since 1.13.5
8689 */
8690 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8691 /**
8692 * This progressive price difference must be applied on the room final cost, so we need to
8693 * apply the transliteration over the other differences applied by other special prices.
8694 */
8695 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8696 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8697 // this is the % promotion we are looking for applying it on the final cost
8698 $factor = $v['trans_factors'][$transliterate_key];
8699 if (is_object($factor) && $factor->pcent) {
8700 $final_factor = 0;
8701 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8702 $final_factor += $prog_diff * $factor->amount / 100;
8703 }
8704 // update rounding
8705 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8706
8707 /**
8708 * Leverage the final factor depending on how many nights this affected.
8709 *
8710 * @since 1.15.0 (J) - 1.5.0 (WP)
8711 */
8712 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8713 $avg_final_factor = $final_factor / $arr[$k]['days'];
8714 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8715 }
8716
8717 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8718 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8719 }
8720 }
8721 }
8722
8723 // apply new price progressively
8724 $newprice += $add;
8725 }
8726
8727 // apply rounding from factor
8728 if (!empty($redo_rounding)) {
8729 $newprice = round($newprice, 0, constant($redo_rounding));
8730 }
8731
8732 // set promotion (if any)
8733 if (isset($v['promotion'])) {
8734 $arr[$k]['promotion'] = $v['promotion'];
8735 }
8736
8737 // set occupancy overrides (if any)
8738 if (isset($v['occupancy_ovr'])) {
8739 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8740 }
8741
8742 // set new final cost and update nights affected
8743 $arr[$k]['cost'] = $newprice;
8744 $arr[$k]['affdays'] = $v['daysused'];
8745 if (array_key_exists('spids', $v) && $v['spids']) {
8746 $arr[$k]['spids'] = $v['spids'];
8747 }
8748 }
8749 }
8750 }
8751 }
8752
8753 // week days with no season
8754 $roomschange = array_unique($roomschange);
8755 $totspecials = 0;
8756 if (!is_array($seasons_wdays)) {
8757 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8758 // avoid making a query
8759 $specials = $cached_seasons[$cache_signature_wdays];
8760 } else {
8761 // get the week-days-with-no-season records by running the query
8762 $specials = self::getWdaySeasonRecords();
8763 }
8764
8765 // count records
8766 $totspecials = $specials ? count($specials) : 0;
8767
8768 if ($cache_signature_wdays) {
8769 // cache week-days-with-no-season records
8770 $cached_seasons[$cache_signature_wdays] = $specials;
8771 }
8772 }
8773 if ($totspecials > 0 || $seasons_wdays) {
8774 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8775 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8776 $applyseasons = false;
8777 /**
8778 * We no longer unset the previous memory of the seasons with dates filters
8779 * because we need the responses to be merged. We do it only if not set.
8780 * We only keep the property 'spids' but the others should be unset.
8781 *
8782 * @since 1.11
8783 */
8784 if (!isset($mem)) {
8785 $mem = [];
8786 foreach ($arr as $k => $a) {
8787 $mem[$k]['daysused'] = 0;
8788 $mem[$k]['sum'] = [];
8789 $mem[$k]['spids'] = [];
8790 }
8791 } else {
8792 foreach ($arr as $k => $a) {
8793 $mem[$k]['daysused'] = 0;
8794 $mem[$k]['sum'] = [];
8795 }
8796 }
8797 //
8798 foreach ($specials as $s) {
8799 // 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
8800 if (!empty($s['from']) || !empty($s['to'])) {
8801 continue;
8802 }
8803
8804 // check if this is a promotion registered for skipping
8805 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8806 continue;
8807 }
8808
8809 // Special Price tied to the year
8810 if (!empty($s['year']) && $s['year'] > 0) {
8811 if ($one['year'] != $s['year']) {
8812 continue;
8813 }
8814 }
8815
8816 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8817 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8818 // week days
8819 $filterwdays = !empty($s['wdays']) ? true : false;
8820 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8821 if (is_array($wdays) && $wdays) {
8822 foreach ($wdays as $kw => $wd) {
8823 if (strlen($wd) == 0) {
8824 unset($wdays[$kw]);
8825 }
8826 }
8827 }
8828
8829 foreach ($arr as $k => $a) {
8830 // only rooms with no price modifications from seasons
8831
8832 // applied only to some types of price
8833 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8834 // VikBooking 1.6: Price Calendar sets the idprice to -1
8835 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8836 continue;
8837 }
8838 }
8839
8840 /**
8841 * We should not exclude the rooms that already had a modification of the price through a season
8842 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8843 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8844 *
8845 * @since 1.11
8846 */
8847 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8848 continue;
8849 }
8850
8851 if (isset($a['days']) && is_float($a['days'])) {
8852 // prevent any possible warning with array_key_exists()
8853 $a['days'] = (int)$a['days'];
8854 }
8855
8856 $affdays = 0;
8857 $season_fromdayts = $fromdayts;
8858 $is_dst = date('I', $season_fromdayts);
8859 for ($i = 0; $i < $a['days']; $i++) {
8860 $todayts = $season_fromdayts + ($i * 86400);
8861 $is_now_dst = date('I', $todayts);
8862 if ($is_dst != $is_now_dst) {
8863 // Daylight Saving Time has changed, check how
8864 if ((bool)$is_dst === true) {
8865 $todayts += 3600;
8866 $season_fromdayts += 3600;
8867 } else {
8868 $todayts -= 3600;
8869 $season_fromdayts -= 3600;
8870 }
8871 $is_dst = $is_now_dst;
8872 }
8873 // week days
8874 if ($filterwdays == true) {
8875 $checkwday = getdate($todayts);
8876 if (in_array($checkwday['wday'], $wdays)) {
8877 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8878 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8879 $affdays++;
8880 }
8881 }
8882 //
8883 }
8884
8885 if (!($affdays > 0)) {
8886 // no nights affected
8887 continue;
8888 }
8889
8890 // apply the rule
8891 $applyseasons = true;
8892 $dailyprice = $a['cost'] / $a['days'];
8893
8894 if (intval($s['val_pcent']) == 2) {
8895 // percentage value
8896 $pctval = $s['diffcost'];
8897 if (strlen((string)$s['losoverride'])) {
8898 // values overrides
8899 $arrvaloverrides = [];
8900 $valovrparts = explode('_', $s['losoverride']);
8901 foreach ($valovrparts as $valovr) {
8902 if (!empty($valovr)) {
8903 $ovrinfo = explode(':', $valovr);
8904 if (strstr($ovrinfo[0], '-i') != false) {
8905 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8906 if ((int)$ovrinfo[0] < $a['days']) {
8907 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8908 }
8909 }
8910 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8911 }
8912 }
8913 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8914 $pctval = $arrvaloverrides[$a['days']];
8915 }
8916 }
8917 if (intval($s['type']) == 1) {
8918 // charge
8919 $cpercent = 100 + $pctval;
8920 } else {
8921 // discount
8922 $cpercent = 100 - $pctval;
8923 }
8924 $dailysum = ($dailyprice * $cpercent / 100);
8925 $newprice = $dailysum * $affdays;
8926 } else {
8927 // absolute value
8928 $absval = $s['diffcost'];
8929 if (strlen((string)$s['losoverride'])) {
8930 // values overrides
8931 $arrvaloverrides = [];
8932 $valovrparts = explode('_', $s['losoverride']);
8933 foreach ($valovrparts as $valovr) {
8934 if (!empty($valovr)) {
8935 $ovrinfo = explode(':', $valovr);
8936 if (strstr($ovrinfo[0], '-i') != false) {
8937 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8938 if ((int)$ovrinfo[0] < $a['days']) {
8939 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8940 }
8941 }
8942 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8943 }
8944 }
8945 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8946 $absval = $arrvaloverrides[$a['days']];
8947 }
8948 }
8949 if (intval($s['type']) == 1) {
8950 // charge
8951 $dailysum = ($dailyprice + $absval);
8952 $newprice = $dailysum * $affdays;
8953 } else {
8954 // discount
8955 $dailysum = ($dailyprice - $absval);
8956 $newprice = $dailysum * $affdays;
8957 }
8958 }
8959
8960 // apply rounding
8961 if (!empty($s['roundmode'])) {
8962 $newprice = round($newprice, 0, constant($s['roundmode']));
8963 } else {
8964 $newprice = round($newprice, 2);
8965 }
8966
8967 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8968 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8969 }
8970 if (!in_array($s['id'], $mem[$k]['spids'])) {
8971 $mem[$k]['spids'][] = $s['id'];
8972 }
8973 $mem[$k]['sum'][] = $newprice;
8974 $mem[$k]['daysused'] += $affdays;
8975 }
8976 }
8977 if ($applyseasons) {
8978 foreach ($mem as $k => $v) {
8979 if ($v['daysused'] > 0 && $v['sum']) {
8980 $newprice = 0;
8981 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8982 $restdays = $arr[$k]['days'] - $v['daysused'];
8983 $addrest = $restdays * $dailyprice;
8984 $newprice += $addrest;
8985 foreach ($v['sum'] as $add) {
8986 $newprice += $add;
8987 }
8988 $arr[$k]['cost'] = $newprice;
8989 $arr[$k]['affdays'] = $v['daysused'];
8990 if (array_key_exists('spids', $v) && $v['spids']) {
8991 $arr[$k]['spids'] = $v['spids'];
8992 }
8993 }
8994 }
8995 }
8996 }
8997 // end week days with no season
8998
8999 return $arr;
9000 }
9001
9002 public static function getRoomRplansClosingDates($idroom)
9003 {
9004 $dbo = JFactory::getDbo();
9005
9006 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9007 $dbo->setQuery($q);
9008 $price_records = $dbo->loadAssocList();
9009 if (!$price_records) {
9010 return [];
9011 }
9012
9013 $closingd = [];
9014 foreach ($price_records as $prec) {
9015 if (empty($prec['closingd'])) {
9016 continue;
9017 }
9018 $price_closing = json_decode($prec['closingd'], true);
9019 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9020 continue;
9021 }
9022 // check expired dates and clean up
9023 $today_midnight = mktime(0, 0, 0);
9024 $cleaned = false;
9025 foreach ($price_closing[$idroom] as $k => $v) {
9026 if (strtotime($v) < $today_midnight) {
9027 $cleaned = true;
9028 unset($price_closing[$idroom][$k]);
9029 }
9030 }
9031 if (!$price_closing[$idroom]) {
9032 unset($price_closing[$idroom]);
9033 } elseif ($cleaned === true) {
9034 // reset array keys for smaller JSON size
9035 $price_closing[$idroom] = array_values($price_closing[$idroom]);
9036 }
9037 if ($cleaned === true) {
9038 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
9039 $dbo->setQuery($q);
9040 $dbo->execute();
9041 }
9042 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9043 continue;
9044 }
9045 $closingd[$prec['id']] = $price_closing[$idroom];
9046 }
9047
9048 return $closingd;
9049 }
9050
9051 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
9052 {
9053 $dbo = JFactory::getDbo();
9054
9055 $closingd = [];
9056
9057 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9058 $dbo->setQuery($q);
9059 $price_records = $dbo->loadAssocList();
9060 if ($price_records && $roomids) {
9061 $info_start = getdate($checkints);
9062 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
9063 $all_nights = [];
9064 for ($i = 0; $i < (int)$numnights; $i++) {
9065 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
9066 $all_nights[] = date('Y-m-d', $next_midnight);
9067 }
9068 foreach ($price_records as $prec) {
9069 if (empty($prec['closingd'])) {
9070 continue;
9071 }
9072 $price_closing = json_decode($prec['closingd'], true);
9073 if (!$price_closing) {
9074 continue;
9075 }
9076 foreach ($price_closing as $idroom => $rclosedd) {
9077 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
9078 continue;
9079 }
9080 if (!array_key_exists($idroom, $closingd)) {
9081 $closingd[$idroom] = [];
9082 }
9083 foreach ($all_nights as $night) {
9084 if (in_array($night, $rclosedd)) {
9085 if (array_key_exists($prec['id'], $closingd[$idroom])) {
9086 $closingd[$idroom][$prec['id']][] = $night;
9087 } else {
9088 $closingd[$idroom][$prec['id']] = [$night];
9089 }
9090 }
9091 }
9092 }
9093 }
9094 }
9095
9096 return $closingd;
9097 }
9098
9099 /**
9100 * Tells whether at least one payment method must be selected to check-out.
9101 *
9102 * @param array $rooms_involved optional list of room IDs being booked.
9103 *
9104 * @return bool true if at least one payment method is applicable.
9105 *
9106 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
9107 */
9108 public static function areTherePayments($rooms_involved = [])
9109 {
9110 $dbo = JFactory::getDbo();
9111
9112 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
9113 $dbo->setQuery($q);
9114 $dbo->execute();
9115
9116 if (!$dbo->getNumRows()) {
9117 // no published payment options found
9118 return false;
9119 }
9120
9121 $pay_options = $dbo->loadAssocList();
9122
9123 if (!count($rooms_involved)) {
9124 // if no filters given, return true
9125 return true;
9126 }
9127
9128 // validate room associations
9129 foreach ($pay_options as $pay_k => $pay_v) {
9130 if (empty($pay_v['idrooms'])) {
9131 continue;
9132 }
9133 $rooms_supported = json_decode($pay_v['idrooms']);
9134 if (!is_array($rooms_supported) || !count($rooms_supported)) {
9135 continue;
9136 }
9137 // make sure all rooms being booked are supported
9138 foreach ($rooms_involved as $room_id_booked) {
9139 if (!in_array($room_id_booked, $rooms_supported)) {
9140 // we need to unset this payment option as soon as we find one non-matching room ID
9141 unset($pay_options[$pay_k]);
9142 break;
9143 }
9144 }
9145 }
9146
9147 return (bool)count($pay_options);
9148 }
9149
9150 public static function getPayment($idp, $vbo_tn = null)
9151 {
9152 if (empty($idp)) {
9153 return false;
9154 }
9155
9156 if (strstr((string)$idp, '=') !== false) {
9157 $parts = explode('=', $idp);
9158 $idp = $parts[0];
9159 }
9160
9161 $dbo = JFactory::getDbo();
9162
9163 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
9164 $dbo->setQuery($q);
9165 $payment = $dbo->loadAssocList();
9166 if (!$payment) {
9167 return false;
9168 }
9169
9170 if (is_object($vbo_tn)) {
9171 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
9172 }
9173
9174 return $payment[0];
9175 }
9176
9177 public static function getCronKey()
9178 {
9179 return VBOFactory::getConfig()->get('cronkey', '');
9180 }
9181
9182 /**
9183 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
9184 *
9185 * @return int
9186 *
9187 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
9188 */
9189 public static function getNextInvoiceNumber()
9190 {
9191 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
9192 }
9193
9194 public static function getInvoiceNumberSuffix()
9195 {
9196 return VBOFactory::getConfig()->get('invoicesuffix', '');
9197 }
9198
9199 public static function getInvoiceCompanyInfo()
9200 {
9201 return VBOFactory::getConfig()->get('invcompanyinfo', '');
9202 }
9203
9204 /**
9205 * Gets the number for the next booking receipt generation,
9206 * updates the last receipt number used for the later iterations,
9207 * stores a new receipt record to keep track of the receipts issued.
9208 *
9209 * @param int $bid the Booking ID for which we are/want to generating the receipt.
9210 * @param [int] $newnum the last number used for generating the receipt.
9211 *
9212 * @return int
9213 */
9214 public static function getNextReceiptNumber ($bid, $newnum = false) {
9215 $dbo = JFactory::getDbo();
9216 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
9217 $dbo->setQuery($q);
9218 $dbo->execute();
9219 if ($dbo->getNumRows() > 0) {
9220 $s = $dbo->loadResult();
9221 //check if this booking has already a receipt, and return that number
9222 $prev_receipt = array();
9223 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
9224 $dbo->setQuery($q);
9225 $dbo->execute();
9226 if ($dbo->getNumRows() > 0) {
9227 $prev_receipt = $dbo->loadAssoc();
9228 }
9229 //update value (receipt generated)
9230 if ($newnum !== false && $newnum > 0) {
9231 $s = (int)$newnum;
9232 if (!(count($prev_receipt) > 0)) {
9233 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
9234 $dbo->setQuery($q);
9235 $dbo->execute();
9236 //insert the new receipt record
9237 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
9238 $dbo->setQuery($q);
9239 $dbo->execute();
9240 } else {
9241 //update receipt record
9242 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
9243 $dbo->setQuery($q);
9244 $dbo->execute();
9245 }
9246 }
9247 //
9248 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
9249 }
9250 //first execution of the method should create the configuration record
9251 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
9252 $dbo->setQuery($q);
9253 $dbo->execute();
9254 return 1;
9255 }
9256
9257 public static function getReceiptNotes ($newnotes = false) {
9258 $dbo = JFactory::getDbo();
9259 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
9260 $dbo->setQuery($q);
9261 $dbo->execute();
9262 if ($dbo->getNumRows() > 0) {
9263 $s = $dbo->loadResult();
9264 //update value
9265 if ($newnotes !== false) {
9266 $s = $newnotes;
9267 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
9268 $dbo->setQuery($q);
9269 $dbo->execute();
9270 }
9271 //
9272 return $s;
9273 }
9274 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
9275 $dbo->setQuery($q);
9276 $dbo->execute();
9277 return "";
9278 }
9279
9280 public static function loadColorTagsRules()
9281 {
9282 $color_tag_rules = [
9283 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
9284 'pend1' => 'VBWAITINGFORPAYMENT',
9285 'conf1' => 'VBDBTEXTROOMCLOSED',
9286 'ovbook' => 'VBO_BTYPE_OVERBOOKING',
9287 'conf2' => 'VBOCOLORTAGRULECONFTWO',
9288 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
9289 'inv1' => 'VBOCOLORTAGRULEINVONE',
9290 'rcp1' => 'VBOCOLORTAGRULERCPONE',
9291 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
9292 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
9293 'inv2' => 'VBOCOLORTAGRULEINVTWO',
9294 ];
9295
9296 /**
9297 * Trigger event to let third-party plugins override the default booking color tag rules.
9298 *
9299 * @since 1.16.4 (J) - 1.6.4 (WP)
9300 */
9301 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
9302
9303 return $color_tag_rules;
9304 }
9305
9306 public static function loadDefaultColorTags()
9307 {
9308 $default_color_tags = [
9309 [
9310 'color' => '#9b9b9b',
9311 'name' => 'VBWAITINGFORPAYMENT',
9312 'rule' => 'pend1',
9313 ],
9314 [
9315 'color' => '#333333',
9316 'name' => 'VBDBTEXTROOMCLOSED',
9317 'rule' => 'conf1',
9318 ],
9319 [
9320 'color' => '#f04848',
9321 'name' => 'VBO_BTYPE_OVERBOOKING',
9322 'rule' => 'ovbook',
9323 ],
9324 [
9325 'color' => '#ff8606',
9326 'name' => 'VBOCOLORTAGRULECONFTWO',
9327 'rule' => 'conf2',
9328 ],
9329 [
9330 'color' => '#0418c9',
9331 'name' => 'VBOCOLORTAGRULECONFTHREE',
9332 'rule' => 'conf3',
9333 ],
9334 [
9335 'color' => '#bed953',
9336 'name' => 'VBOCOLORTAGRULEINVONE',
9337 'rule' => 'inv1',
9338 ],
9339 [
9340 'color' => '#67f5b5',
9341 'name' => 'VBOCOLORTAGRULERCPONE',
9342 'rule' => 'rcp1',
9343 ],
9344 [
9345 'color' => '#04d2c2',
9346 'name' => 'VBOCOLORTAGRULECONFFOUR',
9347 'rule' => 'conf4',
9348 ],
9349 [
9350 'color' => '#00b316',
9351 'name' => 'VBOCOLORTAGRULECONFFIVE',
9352 'rule' => 'conf5',
9353 ],
9354 [
9355 'color' => '#00f323',
9356 'name' => 'VBOCOLORTAGRULEINVTWO',
9357 'rule' => 'inv2',
9358 ],
9359 ];
9360
9361 /**
9362 * Trigger event to let third-party plugins override the default color tags.
9363 *
9364 * @since 1.16.4 (J) - 1.6.4 (WP)
9365 */
9366 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
9367
9368 return $default_color_tags;
9369 }
9370
9371 public static function loadBookingsColorTags()
9372 {
9373 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
9374
9375 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
9376 }
9377
9378 public static function getBestColorContrast($hexcolor)
9379 {
9380 $hexcolor = str_replace('#', '', $hexcolor);
9381
9382 if (empty($hexcolor) || strlen($hexcolor) != 6) {
9383 return '#000000';
9384 }
9385
9386 $r = hexdec(substr($hexcolor, 0, 2));
9387 $g = hexdec(substr($hexcolor, 2, 2));
9388 $b = hexdec(substr($hexcolor, 4, 2));
9389
9390 // Counting the perceptive luminance - human eye favors green color
9391 // < 0.5 bright colors
9392 // > 0.5 dark colors
9393
9394 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
9395 }
9396
9397 public static function applyBookingColorTag($booking, $tags = [])
9398 {
9399 if (!is_array($tags) || !$tags) {
9400 $tags = self::loadBookingsColorTags();
9401 }
9402
9403 if (!empty($booking['colortag'])) {
9404 $color_tag_arr = json_decode($booking['colortag'], true);
9405 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
9406 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
9407 return $color_tag_arr;
9408 }
9409 }
9410
9411 $dbo = JFactory::getDbo();
9412
9413 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
9414 $docs_data = [];
9415 $invoice_numb = false;
9416 $receipt_numb = false;
9417
9418 if ($booking['status'] == 'confirmed' && ($booking['checkin'] ?? 0) > time()) {
9419 $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`
9420 FROM `#__vikbooking_orders` AS `b`
9421 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
9422 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
9423 WHERE `b`.`id`=" . (int)$bid . ";";
9424 $dbo->setQuery($q);
9425 $docs_data = $dbo->loadAssoc();
9426 if ($docs_data) {
9427 $invoice_numb = (!empty($docs_data['inv_id']));
9428 $receipt_numb = (!empty($docs_data['rcp_id']));
9429
9430 /**
9431 * We inject the invoice number or receipt number into the color tag rules.
9432 * This is useful for the Tableaux page to show the invoice/receipt number.
9433 *
9434 * @since 1.12 (J) - 1.1.7 (WP)
9435 */
9436 if ($invoice_numb || $receipt_numb) {
9437 foreach ($tags as &$tval) {
9438 if ($invoice_numb) {
9439 $tval['invoice_number'] = $docs_data['inv_number'];
9440 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
9441 }
9442 if ($receipt_numb) {
9443 $tval['receipt_number'] = $docs_data['rcp_number'];
9444 }
9445 }
9446 // unset last reference
9447 unset($tval);
9448 }
9449 }
9450 }
9451
9452 /**
9453 * Some OTA bookings using an OTA-Collect business model may look as not entirely
9454 * paid when they actually are, due to commissions and pass through taxes. We also
9455 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
9456 *
9457 * @since 1.16.3 (J) - 1.6.3 (WP)
9458 */
9459 $ota_collected_booking = false;
9460 $hotel_collect_vcc_unpaid = false;
9461 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
9462 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
9463 $ota_collected_booking = true;
9464 }
9465 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
9466 // VCC detected, check the history for a payment event
9467 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
9468 // list of history types to fetch
9469 $h_payment_types = [];
9470 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
9471 if ($h_payment_group && $h_payment_group['types']) {
9472 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
9473 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
9474 }
9475 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
9476 if (!$payment_ev_datas) {
9477 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
9478 $hotel_collect_vcc_unpaid = true;
9479 }
9480 }
9481 }
9482
9483 foreach ($tags as $tkey => $tval) {
9484 if (empty($tval['rule'])) {
9485 continue;
9486 }
9487
9488 /**
9489 * Trigger event to let third-party plugins validate the custom rule.
9490 *
9491 * @since 1.16.4 (J) - 1.6.4 (WP)
9492 */
9493 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
9494 if (!empty($custom_rule)) {
9495 return (array) $custom_rule[0];
9496 }
9497
9498 switch ($tval['rule']) {
9499 case 'pend1':
9500 // Room is waiting for the payment (locked record)
9501 if ($booking['status'] == 'standby') {
9502 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
9503 $dbo->setQuery($q);
9504 $tmp_lock = $dbo->loadAssoc();
9505 if ($tmp_lock) {
9506 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9507 return $tval;
9508 }
9509 }
9510 break;
9511 case 'conf1':
9512 // Confirmed (Room Closed)
9513 if ($booking['status'] == 'confirmed' && (!empty($booking['closure']) || $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED'))) {
9514 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9515 return $tval;
9516 }
9517 break;
9518 case 'ovbook':
9519 // Confirmed (Overbooking)
9520 if ($booking['status'] == 'confirmed' && !empty($booking['type']) && $booking['type'] == 'overbooking') {
9521 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9522 return $tval;
9523 }
9524 break;
9525 case 'conf2':
9526 // Confirmed (No Rate 0.00/NULL Total)
9527 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9528 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9529 return $tval;
9530 }
9531 break;
9532 case 'conf3':
9533 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9534 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) {
9535 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9536 return $tval;
9537 }
9538 break;
9539 case 'inv1':
9540 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9541 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9542 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9543 return $tval;
9544 }
9545 break;
9546 case 'rcp1':
9547 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9548 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9549 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9550 return $tval;
9551 }
9552 break;
9553 case 'conf4':
9554 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9555 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9556 // make sure the commissions are not making the difference
9557 if ($booking['totpaid'] + ($booking['cmms'] ?? 0) < $booking['total']) {
9558 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9559 return $tval;
9560 }
9561 }
9562 break;
9563 case 'conf5':
9564 // Confirmed (Total > 0 && Total Paid >= Total)
9565 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || ($booking['totpaid'] + ($booking['cmms'] ?? 0) >= $booking['total']) || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
9566 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9567 return $tval;
9568 }
9569 break;
9570 case 'inv2':
9571 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9572 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9573 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9574 return $tval;
9575 }
9576 break;
9577 default:
9578 // unsupported rule
9579 break;
9580 }
9581 }
9582
9583 return [];
9584 }
9585
9586 public static function getBookingInfoFromID($bid)
9587 {
9588 $dbo = JFactory::getDbo();
9589
9590 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9591 $dbo->setQuery($q);
9592 $booking_info = $dbo->loadAssoc();
9593 if ($booking_info) {
9594 return $booking_info;
9595 }
9596
9597 return [];
9598 }
9599
9600 /**
9601 * Load booking details for a given list of IDs.
9602 *
9603 * @param int $roomid The room integer number.
9604 * @param array $room_bids_pool List of booking IDs involved.
9605 * @param bool $unassigned Whether to return also the unassigned bookings.
9606 *
9607 * @return array Associative list of sub-unit bookings, or list containing also the unassigned bookings.
9608 *
9609 * @since 1.18.2 (J) - 1.8.2 (WP) added argument $unassigned.
9610 */
9611 public static function loadRoomIndexesBookings(int $roomid, array $room_bids_pool, $unassigned = false)
9612 {
9613 $dbo = JFactory::getDbo();
9614
9615 $pool_values = [];
9616 if ($unassigned) {
9617 // prepare the default return value
9618 $pool_values = [
9619 [],
9620 [],
9621 ];
9622 }
9623
9624 if (empty($roomid) || !$room_bids_pool) {
9625 return $pool_values;
9626 }
9627
9628 $room_features_bookings = [];
9629 $unassigned_bookings = [];
9630
9631 $all_bids = [];
9632 foreach ($room_bids_pool as $day => $bids) {
9633 $all_bids = array_merge($all_bids, $bids);
9634 }
9635 $all_bids = array_map('intval', array_unique($all_bids));
9636 $all_bids_str = implode(', ', $all_bids);
9637
9638 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9639 $dbo->setQuery($q);
9640 $rbookings = $dbo->loadAssocList();
9641 if (!$rbookings) {
9642 return $pool_values;
9643 }
9644
9645 foreach ($rbookings as $k => $v) {
9646 if (empty($v['roomindex'])) {
9647 if ($unassigned) {
9648 // push this booking to the list of unassigned reservations
9649 $unassigned_bookings[] = $v['idorder'];
9650 }
9651 // do not proceed further
9652 continue;
9653 }
9654 if (!isset($room_features_bookings[$v['roomindex']])) {
9655 $room_features_bookings[$v['roomindex']] = [];
9656 }
9657 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9658 }
9659
9660 if ($unassigned) {
9661 // return a list of information
9662 return [
9663 $room_features_bookings,
9664 $unassigned_bookings,
9665 ];
9666 }
9667
9668 // return just the list of room sub-unit bookings
9669 return $room_features_bookings;
9670 }
9671
9672 public static function getSendEmailWhen()
9673 {
9674 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9675 }
9676
9677 public static function getMinutesAutoRemove()
9678 {
9679 static $minAutoRemove = null;
9680
9681 if ($minAutoRemove !== null) {
9682 return $minAutoRemove;
9683 }
9684
9685 $minAutoRemove = VBOFactory::getConfig()->getInt('minautoremove', 0);
9686
9687 return $minAutoRemove;
9688 }
9689
9690 /**
9691 * Returns the currently active SMS driver handle.
9692 *
9693 * @return string
9694 *
9695 * @since 1.18.8 (J) - 1.8.8 (WP) removed support for WhatsApp free-text messages due to Meta
9696 * business template requirements - use the apposite E4jConnect
9697 * WhatsApp Business Messaging service with Vik Channel Manager.
9698 */
9699 public static function getSMSAPIClass()
9700 {
9701 $smsApiValue = VBOFactory::getConfig()->getString('smsapi', '');
9702
9703 if (preg_match('/^whatsapp/i', $smsApiValue)) {
9704 // no longer supported
9705 $smsApiValue = '';
9706 }
9707
9708 return $smsApiValue;
9709 }
9710
9711 public static function autoSendSMSEnabled()
9712 {
9713 return VBOFactory::getConfig()->getBool('smsautosend', false);
9714 }
9715
9716 public static function getSendSMSTo()
9717 {
9718 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9719 }
9720
9721 public static function getSendSMSWhen()
9722 {
9723 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9724 }
9725
9726 public static function getSMSAdminPhone()
9727 {
9728 return VBOFactory::getConfig()->getString('smsadminphone', '');
9729 }
9730
9731 public static function getSMSParams($as_array = true)
9732 {
9733 if (!$as_array) {
9734 return VBOFactory::getConfig()->getString('smsparams', '');
9735 }
9736
9737 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9738 }
9739
9740 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9741 $dbo = JFactory::getDbo();
9742 switch (strtolower($booking_status)) {
9743 case 'standby':
9744 $status = 'pend';
9745 break;
9746 case 'cancelled':
9747 $status = 'canc';
9748 break;
9749 default:
9750 $status = '';
9751 break;
9752 }
9753 $paramtype = 'sms'.$type.'tpl'.$status;
9754 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9755 $dbo->setQuery($q);
9756 $dbo->execute();
9757 if ($dbo->getNumRows() < 1) {
9758 if ($status == 'canc') {
9759 //Type cancelled is used by VCM since v1.6.6
9760 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9761 $dbo->setQuery($q);
9762 $dbo->execute();
9763 }
9764 return '';
9765 }
9766 $ft = $dbo->loadAssocList();
9767 if (is_object($vbo_tn)) {
9768 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9769 }
9770 return $ft[0]['setting'];
9771 }
9772
9773 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9774 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9775 }
9776
9777 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9778 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9779 }
9780
9781 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9782 $dbo = JFactory::getDbo();
9783 $phone = str_replace(" ", '', trim($phone));
9784 $cprefix = '';
9785 if (!empty($country_threecode)) {
9786 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9787 $dbo->setQuery($q);
9788 $dbo->execute();
9789 if ($dbo->getNumRows() > 0) {
9790 $cprefix = $dbo->loadResult();
9791 $cprefix = str_replace(" ", '', trim($cprefix));
9792 }
9793 }
9794 if (!empty($cprefix)) {
9795 if (substr($phone, 0, 1) != '+') {
9796 if (substr($phone, 0, 2) == '00') {
9797 $phone = '+'.substr($phone, 2);
9798 } else {
9799 $phone = $cprefix.$phone;
9800 }
9801 }
9802 }
9803 return $phone;
9804 }
9805
9806 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9807 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9808
9809 /**
9810 * Trigger event to allow third-party plugins to manipulate the template string.
9811 *
9812 * @since 1.16.10 (J) - 1.6.10 (WP)
9813 */
9814 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9815
9816 /**
9817 * Parse all conditional text rules.
9818 *
9819 * @since 1.14 (J) - 1.4.0 (WP)
9820 */
9821 self::getConditionalRulesInstance()
9822 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9823 ->parseTokens($tpl);
9824
9825 /**
9826 * Parse all Door Access Control tags.
9827 *
9828 * @since 1.18.4 (J) - 1.8.4 (WP)
9829 */
9830 VBOFactory::getDoorAccessControl()
9831 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9832
9833 $vbo_df = self::getDateFormat();
9834 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9835 $datesep = self::getDateSeparator();
9836
9837 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9838 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9839 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9840 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9841 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9842 if (!empty($booking['confirmnumber'])) {
9843 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9844 } else {
9845 $tpl = str_replace("{confirmnumb}", '', $tpl);
9846 }
9847 if (!empty($booking['idorderota'])) {
9848 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9849 } else {
9850 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9851 }
9852
9853 $rooms_booked = array();
9854 $rooms_names = array();
9855 $tot_adults = 0;
9856 $tot_children = 0;
9857 $tot_guests = 0;
9858 foreach ($booking_rooms as $broom) {
9859 $rooms_names[] = $broom['room_name'];
9860 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9861 $rooms_booked[$broom['room_name']] += 1;
9862 } else {
9863 $rooms_booked[$broom['room_name']] = 1;
9864 }
9865 $tot_adults += (int)$broom['adults'];
9866 $tot_children += (int)$broom['children'];
9867 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9868 }
9869 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9870 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9871 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9872 $rooms_booked_quant = array();
9873 foreach ($rooms_booked as $rname => $quant) {
9874 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9875 }
9876 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9877 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9878 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9879 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9880 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9881 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9882 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9883 $remaining_bal = $booking['total'] - $booking['totpaid'];
9884 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9885
9886 return $tpl;
9887 }
9888
9889 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9890 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9891
9892 /**
9893 * Trigger event to allow third-party plugins to manipulate the template string.
9894 *
9895 * @since 1.16.10 (J) - 1.6.10 (WP)
9896 */
9897 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9898
9899 /**
9900 * Parse all conditional text rules.
9901 *
9902 * @since 1.14 (J) - 1.4.0 (WP)
9903 */
9904 self::getConditionalRulesInstance()
9905 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9906 ->parseTokens($tpl);
9907
9908 /**
9909 * Parse all Door Access Control tags.
9910 *
9911 * @since 1.18.4 (J) - 1.8.4 (WP)
9912 */
9913 VBOFactory::getDoorAccessControl()
9914 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9915
9916 $vbo_df = self::getDateFormat();
9917 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9918 $datesep = self::getDateSeparator();
9919
9920 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9921 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9922 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9923 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9924 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9925 if (!empty($booking['confirmnumber'])) {
9926 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9927 } else {
9928 $tpl = str_replace("{confirmnumb}", '', $tpl);
9929 }
9930 if (!empty($booking['idorderota'])) {
9931 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9932 } else {
9933 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9934 }
9935
9936 $rooms_booked = array();
9937 $rooms_names = array();
9938 $tot_adults = 0;
9939 $tot_children = 0;
9940 $tot_guests = 0;
9941 foreach ($booking_rooms as $broom) {
9942 $rooms_names[] = $broom['room_name'];
9943 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9944 $rooms_booked[$broom['room_name']] += 1;
9945 } else {
9946 $rooms_booked[$broom['room_name']] = 1;
9947 }
9948 $tot_adults += (int)$broom['adults'];
9949 $tot_children += (int)$broom['children'];
9950 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9951 }
9952 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9953 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9954 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9955 $rooms_booked_quant = array();
9956 foreach ($rooms_booked as $rname => $quant) {
9957 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9958 }
9959 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9960 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9961 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9962 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9963 $remaining_bal = $booking['total'] - $booking['totpaid'];
9964 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9965 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9966
9967 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9968 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9969
9970 if (VBOPlatformDetection::isWordPress()) {
9971 /**
9972 * @wponly Rewrite URI for front-end
9973 */
9974 $book_link = str_replace(JUri::root(), '', $book_link);
9975 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9976 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9977 if ($itemid) {
9978 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9979 }
9980 } else {
9981 /**
9982 * @joomlaonly Rewrite URI for front-end
9983 */
9984 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9985 $uri_parts = [
9986 'option' => 'com_vikbooking',
9987 'view' => 'booking',
9988 'sid' => $use_sid,
9989 'ts' => $booking['ts'],
9990 ];
9991 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9992 // inject lang in query string for proper link routing
9993 $uri_parts['lang'] = $booking['lang'];
9994 }
9995 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9996 }
9997
9998 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9999 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
10000 $tpl = str_replace('{booking_url}', $book_link, $tpl);
10001
10002 return $tpl;
10003 }
10004
10005 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
10006 $dbo = JFactory::getDbo();
10007 if (!class_exists('VboApplication')) {
10008 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
10009 }
10010 $vbo_app = new VboApplication;
10011 if (empty($oid)) {
10012 return false;
10013 }
10014 $sms_api = self::getSMSAPIClass();
10015 if (empty($sms_api)) {
10016 return false;
10017 }
10018 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
10019 return false;
10020 }
10021 $sms_api_params = self::getSMSParams();
10022 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
10023 return false;
10024 }
10025 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
10026 return false;
10027 }
10028 $send_sms_to = self::getSendSMSTo();
10029 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
10030 return false;
10031 }
10032 $booking = array();
10033 $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.";";
10034 $dbo->setQuery($q);
10035 $dbo->execute();
10036 if ($dbo->getNumRows() > 0) {
10037 $booking = $dbo->loadAssoc();
10038 }
10039 if (!(count($booking) > 0)) {
10040 return false;
10041 }
10042 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
10043 return false;
10044 }
10045 $booking_rooms = array();
10046 $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'].";";
10047 $dbo->setQuery($q);
10048 $dbo->execute();
10049 if ($dbo->getNumRows() > 0) {
10050 $booking_rooms = $dbo->loadAssocList();
10051 }
10052 $admin_phone = self::getSMSAdminPhone();
10053 $admin_sendermail = self::getSenderMail();
10054 $admin_email = self::getAdminMail();
10055 $f_result = false;
10056 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
10057 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
10058 //SMS for the administrator
10059 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
10060 if (!empty($sms_text)) {
10061 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10062 //administrator phone can contain multiple numbers separated by comma or semicolon
10063 $admin_phones = array();
10064 if (strpos($admin_phone, ',') !== false) {
10065 $all_phones = explode(',', $admin_phone);
10066 foreach ($all_phones as $aph) {
10067 if (!empty($aph)) {
10068 $admin_phones[] = trim($aph);
10069 }
10070 }
10071 } elseif (strpos($admin_phone, ';') !== false) {
10072 $all_phones = explode(';', $admin_phone);
10073 foreach ($all_phones as $aph) {
10074 if (!empty($aph)) {
10075 $admin_phones[] = trim($aph);
10076 }
10077 }
10078 } else {
10079 $admin_phones[] = $admin_phone;
10080 }
10081 foreach ($admin_phones as $admphone) {
10082 $response_obj = $sms_obj->sendMessage($admphone, 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('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10086 } else {
10087 $f_result = true;
10088 }
10089 }
10090 }
10091 }
10092 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
10093 //SMS for the Customer
10094 $vbo_tn = self::getTranslator();
10095 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
10096 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
10097 if (!empty($sms_text)) {
10098 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10099 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
10100 if ( !$sms_obj->validateResponse($response_obj) ) {
10101 //notify the administrator via email with the error of the SMS sending
10102 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10103 } else {
10104 $f_result = true;
10105 }
10106 }
10107 }
10108 return $f_result;
10109 }
10110
10111 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
10112 {
10113 if (!defined('_VIKBOOKINGEXEC')) {
10114 define('_VIKBOOKINGEXEC', '1');
10115 }
10116
10117 ob_start();
10118 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
10119 $content = ob_get_contents();
10120 ob_end_clean();
10121
10122 $default_params = array(
10123 'show_header' => 0,
10124 'header_data' => array(),
10125 'show_footer' => 0,
10126 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10127 'pdf_unit' => 'PDF_UNIT',
10128 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10129 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10130 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10131 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10132 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10133 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10134 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10135 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10136 'header_font_size' => '10',
10137 'body_font_size' => '10',
10138 'footer_font_size' => '8',
10139 'show_lines_taxrate_col' => 0,
10140 );
10141
10142 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
10143 $default_params = array_merge($default_params, $invoice_params);
10144 }
10145
10146 return array($content, $default_params);
10147 }
10148
10149 /**
10150 * Includes within a buffer the template file for the custom (manual) invoice.
10151 *
10152 * @param array $invoice the record of the custom invoice
10153 * @param array $customer the customer record
10154 *
10155 * @return string the HTML content of the template file
10156 *
10157 * @since 1.11.1
10158 */
10159 public static function loadCustomInvoiceTmpl($invoice, $customer) {
10160 if (!defined('_VIKBOOKINGEXEC')) {
10161 define('_VIKBOOKINGEXEC', '1');
10162 }
10163 ob_start();
10164 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
10165 $content = ob_get_contents();
10166 ob_end_clean();
10167 $default_params = array(
10168 'show_header' => 0,
10169 'header_data' => array(),
10170 'show_footer' => 0,
10171 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10172 'pdf_unit' => 'PDF_UNIT',
10173 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10174 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10175 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10176 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10177 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10178 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10179 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10180 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10181 'header_font_size' => '10',
10182 'body_font_size' => '10',
10183 'footer_font_size' => '8'
10184 );
10185 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
10186 $default_params = array_merge($default_params, $invoice_params);
10187 }
10188 return array($content, $default_params);
10189 }
10190
10191 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
10192 {
10193 // clone the original template file source
10194 $parsed = $invoicetpl;
10195
10196 /**
10197 * Parse all conditional text rules.
10198 *
10199 * @since 1.14 (J) - 1.4.0 (WP)
10200 */
10201 self::getConditionalRulesInstance()
10202 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
10203 ->parseTokens($parsed);
10204
10205 $dbo = JFactory::getDbo();
10206 if (is_null($vbo_tn)) {
10207 $vbo_tn = self::getTranslator();
10208 }
10209 // availability helper
10210 $av_helper = self::getAvailabilityInstance();
10211
10212 $nowdf = self::getDateFormat();
10213 if ($nowdf == "%d/%m/%Y") {
10214 $df = 'd/m/Y';
10215 } elseif ($nowdf == "%m/%d/%Y") {
10216 $df = 'm/d/Y';
10217 } else {
10218 $df = 'Y/m/d';
10219 }
10220 $datesep = self::getDateSeparator();
10221
10222 /**
10223 * Split stay reservation.
10224 *
10225 * @since 1.16.0 (J) - 1.6.0 (WP)
10226 */
10227 $room_stay_dates = [];
10228 if ($booking['split_stay']) {
10229 if ($booking['status'] == 'confirmed') {
10230 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
10231 } else {
10232 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
10233 }
10234 // immediately count the number of nights of stay for each split room
10235 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
10236 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
10237 // overwrite values for compatibility with non-confirmed bookings
10238 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
10239 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
10240 }
10241 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
10242 // overwrite the whole array
10243 $room_stay_dates[$sps_r_k] = $sps_r_v;
10244 }
10245 }
10246
10247 $companylogo = self::getSiteLogo();
10248 $uselogo = '';
10249 if (!empty($companylogo)) {
10250 /**
10251 * Let's try to prevent TCPDF errors.
10252 *
10253 * @since August 2nd 2019
10254 */
10255 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10256 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10257 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10258 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10259 }
10260 }
10261 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10262 $parsed = str_replace("{company_info}", $company_info, $parsed);
10263 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
10264 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
10265 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
10266 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10267
10268 // custom fields replacement
10269 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10270 if (is_array($cmatches[1]) && $cmatches[1]) {
10271 $cfids = array();
10272 foreach ($cmatches[1] as $cfid) {
10273 $cfids[] = $cfid;
10274 }
10275 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10276 $dbo->setQuery($q);
10277 $cfields = $dbo->loadAssocList();
10278 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10279 $cfmap = array();
10280 foreach ($cfields as $cf) {
10281 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10282 }
10283 $cfmapreplace = array();
10284 $partsreceived = explode("\n", $booking['custdata']);
10285 if (count($partsreceived) > 0) {
10286 foreach ($partsreceived as $pst) {
10287 if (!empty($pst)) {
10288 $tmpdata = explode(":", $pst);
10289 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10290 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10291 }
10292 }
10293 }
10294 }
10295 foreach ($cmatches[1] as $cfid) {
10296 if (array_key_exists($cfid, $cfmapreplace)) {
10297 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10298 } else {
10299 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10300 }
10301 }
10302 }
10303
10304 // invoice price description - Start
10305 $rooms = array();
10306 $tars = array();
10307 $arrpeople = array();
10308 $is_package = !empty($booking['pkg']) ? true : false;
10309 $tot_adults = 0;
10310 $tot_children = 0;
10311 $tot_guests = 0;
10312 foreach ($booking_rooms as $kor => $or) {
10313 $num = $kor + 1;
10314 $rooms[$num] = $or;
10315
10316 // determine the days to consider for the count of the availability
10317 $room_nights = $booking['days'];
10318 $room_checkin = $booking['checkin'];
10319 $room_checkout = $booking['checkout'];
10320 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10321 $room_nights = $room_stay_dates[$kor]['nights'];
10322 $room_checkin = $room_stay_dates[$kor]['checkin'];
10323 $room_checkout = $room_stay_dates[$kor]['checkout'];
10324 }
10325
10326 $arrpeople[$num]['adults'] = $or['adults'];
10327 $arrpeople[$num]['children'] = $or['children'];
10328 $tot_adults += $or['adults'];
10329 $tot_children += $or['children'];
10330 $tot_guests += ($or['adults'] + $or['children']);
10331 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10332 //package or custom cost set from the back-end
10333 continue;
10334 }
10335 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
10336 $dbo->setQuery($q);
10337 $tar = $dbo->loadAssocList();
10338 if ($tar) {
10339 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
10340 //different usage
10341 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
10342 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
10343 //Occupancy Override
10344 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
10345 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
10346 //
10347 if (is_array($diffusageprice)) {
10348 //set a charge or discount to the price(s) for the different usage of the room
10349 foreach ($tar as $kpr => $vpr) {
10350 $tar[$kpr]['diffusage'] = $or['adults'];
10351 if ($diffusageprice['chdisc'] == 1) {
10352 //charge
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'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 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 } else {
10369 //discount
10370 if ($diffusageprice['valpcent'] == 1) {
10371 //fixed value
10372 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10373 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10374 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
10375 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10376 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
10377 } else {
10378 //percentage value
10379 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10380 $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);
10381 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
10382 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10383 $tar[$kpr]['cost'] = $aduseval;
10384 }
10385 }
10386 }
10387 }
10388 }
10389 //
10390 $tars[$num] = $tar[0];
10391 }
10392 }
10393 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10394 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10395 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10396 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10397 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10398 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10399
10400 $isdue = 0;
10401 $tot_taxes = 0;
10402 $tot_city_taxes = 0;
10403 $tot_fees = 0;
10404 $pricestr = array();
10405 $optstr = array();
10406 // start building the tax summary
10407 VBOTaxonomySummary::start();
10408 foreach ($booking_rooms as $kor => $or) {
10409 $num = $kor + 1;
10410
10411 // determine the days to consider for the count of the availability
10412 $room_nights = $booking['days'];
10413 $room_checkin = $booking['checkin'];
10414 $room_checkout = $booking['checkout'];
10415 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10416 $room_nights = $room_stay_dates[$kor]['nights'];
10417 $room_checkin = $room_stay_dates[$kor]['checkin'];
10418 $room_checkout = $room_stay_dates[$kor]['checkout'];
10419 }
10420
10421 $pricestr[$num] = array();
10422 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10423 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
10424 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
10425 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
10426 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
10427 $pricestr[$num]['tot'] = $calctar;
10428 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
10429 $tot_taxes += ($calctar - $cost_minus_tax);
10430 $isdue += $calctar;
10431 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
10432 $pricestr[$num]['tax_rate'] = $tax_rate;
10433 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
10434 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10435 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
10436 $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'] : "");
10437 $pricestr[$num]['tot'] = $calctar;
10438 $tars[$num]['calctar'] = $calctar;
10439 $isdue += $calctar;
10440 if ($calctar == $display_rate) {
10441 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
10442 $tot_taxes += ($display_rate - $cost_minus_tax);
10443 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
10444 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
10445 $pricestr[$num]['tax_rate'] = $tax_rate;
10446 } else {
10447 $tot_taxes += ($calctar - $display_rate);
10448 $pricestr[$num]['tax'] = ($calctar - $display_rate);
10449 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
10450 $pricestr[$num]['tax_rate'] = $tax_rate;
10451 }
10452 }
10453 $optstr[$num] = array();
10454 $opt_ind = 0;
10455 if (!empty($or['optionals'])) {
10456 $stepo = explode(";", $or['optionals']);
10457 foreach ($stepo as $roptkey => $oo) {
10458 if (!empty($oo)) {
10459 $stept = explode(":", $oo);
10460 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
10461 $dbo->setQuery($q);
10462 $actopt = $dbo->loadAssocList();
10463 if ($actopt) {
10464 if (is_object($vbo_tn)) {
10465 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
10466 }
10467 $optstr[$num][$opt_ind] = array();
10468 $chvar = '';
10469 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
10470 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
10471 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
10472 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
10473 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
10474 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
10475 $agestept = explode('-', $stept[1]);
10476 $stept[1] = $agestept[0];
10477 $chvar = $agestept[1];
10478 $realcost = 0;
10479 if (!empty($chvar)) {
10480 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
10481 //percentage value of the adults tariff
10482 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10483 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10484 } else {
10485 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10486 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10487 }
10488 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
10489 //VBO 1.10 - percentage value of room base cost
10490 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10491 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10492 } else {
10493 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
10494 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10495 }
10496 }
10497 $actopt[0]['chageintv'] = $chvar;
10498 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
10499 $actopt[0]['quan'] = $stept[1];
10500 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
10501 }
10502 } else {
10503 $actopt[0]['quan'] = $stept[1];
10504 // VBO 1.11 - options percentage cost of the room total fee
10505 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10506 $deftar_basecosts = $or['cust_cost'];
10507 } else {
10508 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10509 }
10510 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
10511 //
10512 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
10513 }
10514 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
10515 $realcost = $actopt[0]['maxprice'];
10516 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
10517 $realcost = $actopt[0]['maxprice'] * $stept[1];
10518 }
10519 }
10520 if ($actopt[0]['perperson'] == 1) {
10521 $realcost = $realcost * $or['adults'];
10522 }
10523
10524 /**
10525 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
10526 *
10527 * @since 1.17.7 (J) - 1.7.7 (WP)
10528 */
10529 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
10530 if ($custom_calculation) {
10531 $realcost = (float) $custom_calculation[0];
10532 }
10533
10534 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
10535 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
10536 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
10537 $optstr[$num][$opt_ind]['tax'] = 0;
10538 if ($actopt[0]['is_citytax'] == 1) {
10539 $tot_city_taxes += $tmpopr;
10540 } elseif ($actopt[0]['is_fee'] == 1) {
10541 $tot_fees += $tmpopr;
10542 }
10543 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
10544 if ($tmpopr == $realcost) {
10545 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
10546 $tot_taxes += ($realcost - $opt_minus_tax);
10547 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
10548 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
10549 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10550 } else {
10551 $tot_taxes += ($tmpopr - $realcost);
10552 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
10553 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
10554 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10555 }
10556 //
10557 $opt_ind++;
10558 $isdue += $tmpopr;
10559 }
10560 }
10561 }
10562 }
10563
10564 // custom extra costs
10565 if (!empty($or['extracosts'])) {
10566 $cur_extra_costs = json_decode($or['extracosts'], true);
10567 foreach ($cur_extra_costs as $eck => $ecv) {
10568 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10569 $isdue += $ecplustax;
10570 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
10571 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
10572 $optstr[$num][$opt_ind]['tax'] = 0;
10573 if ($ecplustax == $ecv['cost']) {
10574 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10575 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
10576 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
10577 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
10578 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10579 } else {
10580 $tot_taxes += ($ecplustax - $ecv['cost']);
10581 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
10582 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
10583 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10584 }
10585 $opt_ind++;
10586 }
10587 }
10588 }
10589
10590 $usedcoupon = false;
10591 if (strlen($booking['coupon']) > 0) {
10592 $orig_isdue = $isdue;
10593 $expcoupon = explode(";", $booking['coupon']);
10594 $usedcoupon = $expcoupon;
10595 $isdue = $isdue - (float)$expcoupon[1];
10596 if ($isdue != $orig_isdue) {
10597 //lower taxes proportionally
10598 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10599 }
10600 }
10601 if ($booking['refund'] > 0) {
10602 $orig_isdue = $isdue;
10603 $isdue -= $booking['refund'];
10604 if ($isdue != $orig_isdue) {
10605 //lower taxes proportionally
10606 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10607 }
10608 }
10609 $rows_written = 0;
10610 $inv_rows = '';
10611 foreach ($pricestr as $num => $price_descr) {
10612 // add room split stay information, if any
10613 $split_stay_str = '';
10614 $kor = $num - 1;
10615 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10616 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10617 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10618 }
10619
10620 $inv_rows .= '<tr>'."\n";
10621 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10622 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($price_descr['tot'] - $price_descr['tax'])), $booking['currencyname']) . '</td>'."\n";
10623 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tax']), $booking['currencyname']) . '</td>'."\n";
10624 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tot']), $booking['currencyname']) . '</td>'."\n";
10625 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10626 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10627 }
10628 $inv_rows .= '</tr>'."\n";
10629 $rows_written++;
10630 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10631 foreach ($optstr[$num] as $optk => $optv) {
10632 $inv_rows .= '<tr>'."\n";
10633 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10634 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($optv['tot'] - $optv['tax'])), $booking['currencyname']) . '</td>'."\n";
10635 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tax']), $booking['currencyname']) . '</td>'."\n";
10636 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tot']), $booking['currencyname']) . '</td>'."\n";
10637 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10638 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10639 }
10640 $inv_rows .= '</tr>'."\n";
10641 $rows_written++;
10642 }
10643 }
10644 }
10645
10646 // if discount print row
10647 if ($usedcoupon !== false) {
10648 $inv_rows .= '<tr>'."\n";
10649 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10650 $inv_rows .= '</tr>'."\n";
10651 $inv_rows .= '<tr>'."\n";
10652 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10653 $inv_rows .= '<td></td>'."\n";
10654 $inv_rows .= '<td></td>'."\n";
10655 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($usedcoupon[1]), $booking['currencyname']) . '</td>'."\n";
10656 $inv_rows .= '</tr>'."\n";
10657 $rows_written += 2;
10658 }
10659 // if refunded amount, print row
10660 if ($booking['refund'] > 0) {
10661 $inv_rows .= '<tr>'."\n";
10662 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10663 $inv_rows .= '</tr>'."\n";
10664 $inv_rows .= '<tr>'."\n";
10665 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10666 $inv_rows .= '<td></td>'."\n";
10667 $inv_rows .= '<td></td>'."\n";
10668 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($booking['refund']), $booking['currencyname']) . '</td>'."\n";
10669 $inv_rows .= '</tr>'."\n";
10670 $rows_written += 2;
10671 }
10672 //
10673 $min_records = 8;
10674 if ($rows_written < $min_records) {
10675 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10676 $inv_rows .= '<tr>'."\n";
10677 $inv_rows .= '<td></td>'."\n";
10678 $inv_rows .= '<td></td>'."\n";
10679 $inv_rows .= '<td></td>'."\n";
10680 $inv_rows .= '</tr>'."\n";
10681 }
10682 }
10683 //invoice price description - End
10684 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10685 $parsed = str_replace("{invoice_totalnet}", self::formatCurrencyNumber(self::numberFormat(($isdue - $tot_taxes)), $booking['currencyname']), $parsed);
10686 $parsed = str_replace("{invoice_totaltax}", self::formatCurrencyNumber(self::numberFormat($tot_taxes), $booking['currencyname']), $parsed);
10687 $parsed = str_replace("{invoice_grandtotal}", self::formatCurrencyNumber(self::numberFormat($isdue), $booking['currencyname']), $parsed);
10688 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10689
10690 // invoice tax summary
10691 $tax_summary = VBOTaxonomySummary::get();
10692 $tax_names = VBOTaxonomySummary::getNames();
10693 $taxsum_rows = "";
10694 foreach ($tax_summary as $tax_rate => $tax_amount) {
10695 if (!$tax_rate && !$tax_amount) {
10696 continue;
10697 }
10698 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10699 $taxsum_rows .= '<tr>' . "\n";
10700 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10701 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10702 $taxsum_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($tax_amount), $booking['currencyname']) . '</td>' . "\n";
10703 $taxsum_rows .= '</tr>' . "\n";
10704 }
10705 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10706
10707 return $parsed;
10708 }
10709
10710 /**
10711 * Specific method for parsing the template file for the custom (manual) invoices.
10712 *
10713 * @param string $invoicetpl the plain custom invoice template file before parsing
10714 * @param array $invoice the invoice record
10715 * @param array $customer the customer record
10716 *
10717 * @return string the HTML content of the parsed custom invoice template
10718 *
10719 * @since 1.11.1
10720 */
10721 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10722 $nowdf = self::getDateFormat(true);
10723 if ($nowdf == "%d/%m/%Y") {
10724 $df = 'd/m/Y';
10725 } elseif ($nowdf == "%m/%d/%Y") {
10726 $df = 'm/d/Y';
10727 } else {
10728 $df = 'Y/m/d';
10729 }
10730 $datesep = self::getDateSeparator(true);
10731 $companylogo = self::getSiteLogo();
10732 $uselogo = '';
10733 if (!empty($companylogo)) {
10734 /**
10735 * Let's try to prevent TCPDF errors by checking if the file exists.
10736 *
10737 * @since August 2nd 2019
10738 */
10739 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10740 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10741 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10742 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10743 }
10744 }
10745 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10746 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10747 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10748 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10749 // customer information
10750 $custinfo = '';
10751 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10752 $custinfo .= $customer['email'] . "\n";
10753 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10754 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10755 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10756 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10757 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10758 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10759 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10760 // invoice notes
10761 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10762
10763 return $invoicetpl;
10764 }
10765
10766 /**
10767 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10768 *
10769 * @param array $booking the booking information.
10770 * @param int $invoice_num the invoice number.
10771 * @param string $invoice_suff the invoice suffix.
10772 * @param string $invoice_date the invoice date.
10773 * @param string $company_info optional company information.
10774 * @param bool $translate whether the invoice contents should be translated in the booking language.
10775 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10776 *
10777 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10778 *
10779 * @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.
10780 */
10781 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10782 {
10783 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10784 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10785 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10786
10787 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10788 return false;
10789 }
10790
10791 /**
10792 * Trigger event to allow third party plugins to override some values for the invoice generation.
10793 *
10794 * @since 1.16.3 (J) - 1.6.3 (WP)
10795 */
10796 $event_args = [
10797 'invoice_num' => $invoice_num,
10798 'invoice_suff' => $invoice_suff,
10799 'invoice_date' => $invoice_date,
10800 'company_info' => $company_info,
10801 'translate' => $translate,
10802 ];
10803 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10804 if (is_array($event_args) && $event_args) {
10805 // extract the new values to be used
10806 extract($event_args);
10807 }
10808
10809 $dbo = JFactory::getDbo();
10810 $vbo_tn = self::getTranslator();
10811
10812 // inject the currency name value
10813 $currencyname = self::getCurrencyName();
10814 $booking['currencyname'] = $currencyname;
10815
10816 // make sure we've got a customer ID assigned to the reservation record
10817 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10818 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10819 if ($booking_customer) {
10820 $booking['idcustomer'] = $booking_customer['id'];
10821 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10822 $booking['customer_pin'] = $booking_customer['pin'];
10823 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10824 }
10825 }
10826
10827 $nowdf = self::getDateFormat(true);
10828 if ($nowdf == "%d/%m/%Y") {
10829 $df = 'd/m/Y';
10830 } elseif ($nowdf == "%m/%d/%Y") {
10831 $df = 'm/d/Y';
10832 } else {
10833 $df = 'Y/m/d';
10834 }
10835 $datesep = self::getDateSeparator(true);
10836
10837 if (empty($invoice_date)) {
10838 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10839 $used_date = $booking['ts'];
10840 } else {
10841 /**
10842 * We could be re-generating an invoice for a booking that already had an invoice.
10843 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10844 * It is now possible to keep the same invoice date as the previous one, so check
10845 * what value contains $invoice_date to see if it's different from today's date.
10846 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10847 * to also check the length of the string $invoice_date before using that date.
10848 *
10849 * @since 1.10 - August 2018
10850 */
10851 $base_ts = time();
10852 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10853 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10854 }
10855 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10856 $used_date = $base_ts;
10857 }
10858
10859 // load room reservation records
10860 $q = $dbo->getQuery(true)
10861 ->select($dbo->qn('or') . '.*')
10862 ->select($dbo->qn('r.name', 'room_name'))
10863 ->select($dbo->qn('r.fromadult'))
10864 ->select($dbo->qn('r.toadult'))
10865 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10866 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10867 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10868 ->order($dbo->qn('or.id') . ' ASC');
10869
10870 $dbo->setQuery($q);
10871 $booking_rooms = $dbo->loadAssocList();
10872
10873 if (!$booking_rooms) {
10874 return false;
10875 }
10876
10877 // check if an invoice for this reservation exists
10878 $invoice_id = 0;
10879 $dbo->setQuery(
10880 $dbo->getQuery(true)
10881 ->select('*')
10882 ->from($dbo->qn('#__vikbooking_invoices'))
10883 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10884 ->order($dbo->qn('id') . ' DESC')
10885 );
10886 $invoice_data = $dbo->loadAssoc();
10887 if ($invoice_data) {
10888 $invoice_id = $invoice_data['id'];
10889 }
10890
10891 // if requested, prepare data to refresh the PDF invoice
10892 if ($refresh_pdf === true) {
10893 if (!$invoice_id || !$invoice_data) {
10894 // cannot refresh the PDF if an invoice was never created before
10895 return false;
10896 }
10897
10898 // load the same exact previous information to refresh the invoice
10899 $invoice_num = $invoice_data['number'];
10900 $invoice_suff = $invoice_suff;
10901 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10902 $used_date = $invoice_data['for_date'];
10903
10904 // parse the previous (current) invoice number
10905 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10906 if ($matches) {
10907 // overwrite values
10908 $invoice_num = $matches[1] ?: $invoice_num;
10909 $invoice_suff = $matches[2] ?: $invoice_suff;
10910 }
10911 }
10912
10913 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10914 if ($translate === true) {
10915 if (!empty($booking['lang'])) {
10916 $lang = JFactory::getLanguage();
10917 if ($lang->getTag() != $booking['lang']) {
10918 if (VBOPlatformDetection::isWordPress()) {
10919 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10920 } else {
10921 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10922 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10923 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10924 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10925 }
10926 }
10927 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10928 // force the translation to start because contents should be translated
10929 $vbo_tn::$force_tolang = $booking['lang'];
10930 }
10931 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10932 }
10933 }
10934
10935 // the PDF file name
10936 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10937
10938 /**
10939 * If an analogic invoice is not available, make sure to create the record
10940 * before letting the e-invoicing drivers run to avoid a loop.
10941 *
10942 * @since 1.16.7 (J) - 1.6.7 (WP)
10943 */
10944 $retval = null;
10945 if (!$refresh_pdf && !$invoice_id) {
10946 // prepare record object
10947 $invoice_record = new stdClass;
10948 $invoice_record->number = $invoice_num . $invoice_suff;
10949 $invoice_record->file_name = $pdffname;
10950 $invoice_record->idorder = (int)$booking['id'];
10951 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10952 $invoice_record->created_on = time();
10953 $invoice_record->for_date = (int)$used_date;
10954
10955 // insert record
10956 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10957 return false;
10958 }
10959
10960 $retval = $invoice_record->id ?? false;
10961
10962 if (!$retval) {
10963 return false;
10964 }
10965 }
10966
10967 /**
10968 * The generation of the analogic invoices can trigger the drivers for the
10969 * generation of the e-Invoices if they are set to automatically run.
10970 * However, the e-Invoicing classes may be calling this method, so we need
10971 * to make sure the eInvoicing class is not running before proceeding.
10972 * This method could be called within a loop, and so the second iterations
10973 * may already have loaded the eInvocing class. For this we use a static variable.
10974 *
10975 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10976 * though additional information may be available only after the transmission. On top
10977 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10978 */
10979 static $einvocing_can_run = false;
10980
10981 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10982 // allow second iterations calling this method to run
10983 $einvocing_can_run = true;
10984 }
10985
10986 // always load all drivers, db and plugin based ones
10987 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10988
10989 foreach ($drivers as $driver_obj) {
10990 // set the flag of the external call
10991 $driver_obj->externalCall = __METHOD__;
10992
10993 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10994 $driver_obj->externalData['einvnum'] = $invoice_num;
10995
10996 // let the driver eventually elaborate the booking information before invoicing
10997 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10998
10999 if ($einvocing_can_run === true && !$refresh_pdf) {
11000 // generate the e-Invoice
11001 $driver_err = '';
11002 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
11003 $driver_err = $driver_obj->getError();
11004 VikError::raiseWarning('', $driver_err);
11005 }
11006
11007 // Booking History - store log for the e-Invoice result
11008 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
11009 }
11010 }
11011
11012 /**
11013 * Start the generation of the analogic (courtesy) PDF invoice.
11014 */
11015
11016 // load dependencies
11017 if (!class_exists('TCPDF')) {
11018 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11019 }
11020 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11021
11022 /**
11023 * Trigger event to allow third party plugins to return a specific font name.
11024 *
11025 * @since 1.16.0 (J) - 1.6.0 (WP)
11026 */
11027 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11028 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11029 $usepdffont = $custom_pdf_font[0];
11030 }
11031
11032 // set array variable to the template file
11033 $booking_info = self::getBookingInfoFromID($booking['id']);
11034
11035 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
11036
11037 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
11038
11039 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11040
11041 if (is_file($pathpdf)) {
11042 @unlink($pathpdf);
11043 }
11044
11045 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11046
11047 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11048 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
11049
11050 // header for each page of the pdf
11051 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
11052 $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]);
11053 }
11054
11055 // change some currencies to their unicode (decimal) value
11056 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11057 if (array_key_exists($booking['currencyname'], $unichr_map)) {
11058 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
11059 }
11060
11061 // header and footer fonts
11062 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11063 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11064
11065 // margins
11066 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11067 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11068 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11069
11070 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11071 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11072 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11073
11074 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11075 $pdf->SetPrintHeader(false);
11076 }
11077 if ($pdfparams['show_footer'] == 0) {
11078 $pdf->SetPrintFooter(false);
11079 }
11080
11081 $pdf->AddPage();
11082 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11083 $pdf->lastPage();
11084 $pdf->Output($pathpdf, 'F');
11085
11086 if (!is_file($pathpdf)) {
11087 return false;
11088 }
11089
11090 if (VBOPlatformDetection::isWordPress()) {
11091 /**
11092 * @wponly - trigger files mirroring
11093 */
11094 VikBookingLoader::import('update.manager');
11095 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11096 }
11097
11098 // Booking History
11099 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
11100
11101 if ($retval) {
11102 // a new invoice ID was inserted above
11103 return $retval;
11104 }
11105
11106 // prepare record object to be updated (if insert needed, it's done at first)
11107 $invoice_record = new stdClass;
11108 $invoice_record->id = (int)$invoice_id;
11109 $invoice_record->number = $invoice_num . $invoice_suff;
11110 $invoice_record->file_name = $pdffname;
11111 $invoice_record->idorder = (int)$booking['id'];
11112 $invoice_record->idcustomer = (int)$booking['idcustomer'];
11113 $invoice_record->created_on = time();
11114 $invoice_record->for_date = (int)$used_date;
11115
11116 // update record
11117 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
11118
11119 // return the PDF invoice ID
11120 return $invoice_id;
11121 }
11122
11123 /**
11124 * Generates an analogic invoice in PDF format for a custom list of services.
11125 * No bookings are assigned to this custom invoice. The method parses the same
11126 * invoice template as for the regular process with real booking IDs.
11127 * The invoice number must be stored before calling this method.
11128 *
11129 * @param int $invoice_id the ID of the custom invoice record
11130 *
11131 * @return bool
11132 *
11133 * @since 1.11.1 (J) - 1.1.1 (WP)
11134 */
11135 public static function generateCustomInvoice($invoice_id)
11136 {
11137 $dbo = JFactory::getDbo();
11138
11139 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
11140 $dbo->setQuery($q);
11141 $invoice = $dbo->loadAssoc();
11142 if (!$invoice) {
11143 return false;
11144 }
11145
11146 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
11147 $rawcont = is_array($rawcont) ? $rawcont : array();
11148 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
11149 $invoice['rawcont'] = $rawcont;
11150 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
11151 if (!$rawcont || !$rows || !$customer) {
11152 // at least one invoice raw is mandatory as well as the customer
11153 return false;
11154 }
11155
11156 /**
11157 * Start the generation of the electronic invoice, if any driver was enabled.
11158 *
11159 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11160 * though additional information would be available only after the transmission.
11161 */
11162 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11163
11164 if ($drivers) {
11165 // make sure the invoice number is just a number
11166 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
11167
11168 // invoke all drivers that should run automatically
11169 foreach ($drivers as $driver_obj) {
11170 // set the flag of the external call
11171 $driver_obj->externalCall = __METHOD__;
11172
11173 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
11174 $driver_obj->externalData['einvnum'] = $invoice['number'];
11175 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
11176 $driver_obj->externalData['einvcustom'] = $invoice;
11177
11178 // prepare data array for the generation of the e-Invoice
11179 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
11180
11181 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
11182 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
11183 if ($preveinv !== false) {
11184 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
11185 }
11186
11187 // generate the e-Invoice
11188 $driver_err = '';
11189 if (!$driver_obj->generateEInvoice($einvdata)) {
11190 $driver_err = $driver_obj->getError();
11191 VikError::raiseWarning('', $driver_err);
11192 }
11193 }
11194 }
11195
11196 /**
11197 * Start the generation of the analogic (courtesy) PDF invoice.
11198 */
11199
11200 // load dependencies
11201 if (!class_exists('TCPDF')) {
11202 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11203 }
11204 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11205
11206 /**
11207 * Trigger event to allow third party plugins to return a specific font name.
11208 *
11209 * @since 1.16.0 (J) - 1.6.0 (WP)
11210 */
11211 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11212 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11213 $usepdffont = $custom_pdf_font[0];
11214 }
11215
11216 // load invoice template file
11217 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
11218
11219 // parse invoice template file
11220 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
11221
11222 $pdffname = $invoice['file_name'];
11223 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11224
11225 if (is_file($pathpdf)) {
11226 @unlink($pathpdf);
11227 }
11228
11229 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11230
11231 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11232 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
11233
11234 // header for each page of the pdf
11235 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
11236 $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]);
11237 }
11238
11239 // change some currencies to their unicode (decimal) value
11240 $currencyname = self::getCurrencyName();
11241 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11242 if (array_key_exists($currencyname, $unichr_map)) {
11243 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
11244 }
11245
11246 // header and footer fonts
11247 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11248 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11249
11250 // margins
11251 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11252 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11253 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11254
11255 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11256 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11257 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11258
11259 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11260 $pdf->SetPrintHeader(false);
11261 }
11262 if ($pdfparams['show_footer'] == 0) {
11263 $pdf->SetPrintFooter(false);
11264 }
11265
11266 $pdf->AddPage();
11267 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11268 $pdf->lastPage();
11269 $pdf->Output($pathpdf, 'F');
11270
11271 if (!is_file($pathpdf)) {
11272 return false;
11273 }
11274
11275 if (VBOPlatformDetection::isWordPress()) {
11276 /**
11277 * @wponly - trigger files mirroring
11278 */
11279 VikBookingLoader::import('update.manager');
11280 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11281 }
11282
11283 // return the result of the generation of the PDF invoice
11284 return true;
11285 }
11286
11287 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
11288 {
11289 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
11290 return false;
11291 }
11292
11293 $dbo = JFactory::getDbo();
11294
11295 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
11296 $dbo->setQuery($q);
11297 $invoice_data = $dbo->loadAssoc();
11298
11299 if (!$invoice_data) {
11300 return false;
11301 }
11302
11303 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
11304 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
11305 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
11306 if (!file_exists($invoice_file_path)) {
11307 return false;
11308 }
11309
11310 $vbo_app = self::getVboApplication();
11311 $admin_sendermail = self::getSenderMail();
11312
11313 $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);
11314
11315 // update record
11316 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
11317 $dbo->setQuery($q);
11318 $dbo->execute();
11319
11320 return true;
11321 }
11322
11323 public static function loadCheckinDocTmpl(array $booking_info = [], array $booking_rooms = [], array $customer = [])
11324 {
11325 if (!defined('_VIKBOOKINGEXEC')) {
11326 define('_VIKBOOKINGEXEC', '1');
11327 }
11328
11329 ob_start();
11330 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php';
11331 $content = ob_get_contents();
11332 ob_end_clean();
11333
11334 $default_params = [
11335 'show_header' => 0,
11336 'header_data' => [],
11337 'show_footer' => 0,
11338 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
11339 'pdf_unit' => 'PDF_UNIT',
11340 'pdf_page_format' => 'PDF_PAGE_FORMAT',
11341 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
11342 'pdf_margin_top' => 'PDF_MARGIN_TOP',
11343 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
11344 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
11345 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
11346 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
11347 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
11348 'header_font_size' => '10',
11349 'body_font_size' => '10',
11350 'footer_font_size' => '8'
11351 ];
11352
11353 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
11354 $default_params = array_merge($default_params, $checkin_params);
11355 }
11356
11357 return [$content, $default_params];
11358 }
11359
11360 public static function parseCheckinDocTemplate(string $checkintpl, array $booking, array $booking_rooms, array $customer = [])
11361 {
11362 $dbo = JFactory::getDbo();
11363 $app = JFactory::getApplication();
11364
11365 // clone string
11366 $parsed = $checkintpl;
11367
11368 $datesep = self::getDateSeparator();
11369 $nowdf = self::getDateFormat();
11370 if ($nowdf == "%d/%m/%Y") {
11371 $df = 'd/m/Y';
11372 } elseif ($nowdf == "%m/%d/%Y") {
11373 $df = 'm/d/Y';
11374 } else {
11375 $df = 'Y/m/d';
11376 }
11377
11378 $company_name = self::getFrontTitle();
11379 $company_info = self::getInvoiceCompanyInfo();
11380 $companylogo = self::getSiteLogo();
11381 $uselogo = '';
11382 if (!empty($companylogo)) {
11383 /**
11384 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
11385 *
11386 * @since March 16th 2021
11387 */
11388 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11389 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
11390 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
11391 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11392 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
11393 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
11394 }
11395 }
11396
11397 $parsed = str_replace("{company_name}", $company_name, $parsed);
11398 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
11399 $parsed = str_replace("{company_info}", $company_info, $parsed);
11400 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
11401 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
11402 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
11403 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
11404
11405 $tot_guests = 0;
11406 $tot_adults = 0;
11407 $tot_children = 0;
11408 foreach ($booking_rooms as $kor => $or) {
11409 $tot_guests += ($or['adults'] + $or['children']);
11410 $tot_adults += $or['adults'];
11411 $tot_children += $or['children'];
11412 }
11413 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
11414 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
11415 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
11416 if ($customer && isset($customer['comments'])) {
11417 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
11418 }
11419
11420 $termsconds = self::getTermsConditions();
11421 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
11422
11423 // custom fields replacemenet
11424 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
11425 if (is_array($cmatches[1]) && $cmatches[1]) {
11426 $cfids = array();
11427 foreach ($cmatches[1] as $cfid) {
11428 $cfids[] = $cfid;
11429 }
11430 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
11431 $dbo->setQuery($q);
11432 $cfields = $dbo->loadAssocList();
11433 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
11434 $cfmap = array();
11435 foreach ($cfields as $cf) {
11436 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
11437 }
11438 $cfmapreplace = array();
11439 $partsreceived = explode("\n", $booking['custdata']);
11440 if (count($partsreceived) > 0) {
11441 foreach ($partsreceived as $pst) {
11442 if (!empty($pst)) {
11443 $tmpdata = explode(":", $pst);
11444 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
11445 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
11446 }
11447 }
11448 }
11449 }
11450 foreach ($cmatches[1] as $cfid) {
11451 if (array_key_exists($cfid, $cfmapreplace)) {
11452 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
11453 } else {
11454 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
11455 }
11456 }
11457 }
11458
11459 /**
11460 * Parse all conditional text rules.
11461 */
11462 self::getConditionalRulesInstance()
11463 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
11464 ->parseTokens($parsed);
11465
11466 /**
11467 * Parse all Door Access Control tags.
11468 *
11469 * @since 1.18.4 (J) - 1.8.4 (WP)
11470 */
11471 VBOFactory::getDoorAccessControl()
11472 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $parsed);
11473
11474 return $parsed;
11475 }
11476
11477 /**
11478 * Returns an array of key-value pairs to be used for building the Guest Details
11479 * in the Check-in process. The keys will be compared to the fields of the table
11480 * #__customers to see if some values already exist. The values can use lang defs
11481 * of both front-end or back-end. To be called as list(fields, attributes).
11482 *
11483 * @param boolean $precheckin true if requested for front-end pre check-in.
11484 * @param string $type optional type of custom pax fields to force.
11485 *
11486 * @return array key-value pairs for showing and collecting guest details.
11487 *
11488 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
11489 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
11490 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
11491 */
11492 public static function getPaxFields($precheckin = false, $type = null)
11493 {
11494 // check the type of pax fields collection data
11495 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
11496
11497 if (!$precheckin) {
11498 // back-end check-in ("registration") key-value pairs
11499
11500 // return the requested pax fields collection list
11501 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
11502
11503 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11504 // requested driver returned a list of fields
11505 return $custom_pax_fields;
11506 }
11507
11508 // in case the driver is unsupported, fallback to "basic" (default) driver
11509 return VBOCheckinPax::getFields('basic');
11510 }
11511
11512 // default front-end key-value pairs for pre check-in
11513 $precheckin_pax_fields = [
11514 [
11515 'first_name' => JText::translate('VBCCFIRSTNAME'),
11516 'last_name' => JText::translate('VBCCLASTNAME'),
11517 'date_birth' => JText::translate('ORDER_DBIRTH'),
11518 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
11519 'country' => JText::translate('ORDER_STATE'),
11520 'city' => JText::translate('ORDER_CITY'),
11521 'zip' => JText::translate('ORDER_ZIP'),
11522 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
11523 'gender' => JText::translate('VBOCUSTGENDER'),
11524 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
11525 'docnum' => JText::translate('VBOCUSTDOCNUM'),
11526 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
11527 ],
11528 [
11529 'first_name' => 'text',
11530 'last_name' => 'text',
11531 'date_birth' => 'calendar',
11532 'place_birth' => 'text',
11533 'country' => 'country',
11534 'city' => 'text',
11535 'zip' => 'text',
11536 'nationality' => 'country',
11537 'gender' => [
11538 'M' => JText::translate('VBOCUSTGENDERM'),
11539 'F' => JText::translate('VBOCUSTGENDERF'),
11540 ],
11541 'doctype' => 'text',
11542 'docnum' => 'text',
11543 'documents' => 'file',
11544 ],
11545 ];
11546
11547 // access the pre-checkin pax fields collection list from the current collector
11548 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
11549
11550 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11551 // set the pax fields for pre-checkin from the given data collection driver
11552 $precheckin_pax_fields = $custom_pax_fields;
11553 }
11554
11555 // make a safe copy of the default precheckin fields
11556 $use_precheckin_pax_fields = $precheckin_pax_fields;
11557
11558 /**
11559 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
11560 *
11561 * @since 1.16.3 (J) - 1.6.3 (WP)
11562 */
11563 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
11564
11565 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
11566 // use the filtered pax fields
11567 $use_precheckin_pax_fields = $precheckin_pax_fields;
11568 }
11569
11570 // return the front-end key-value pairs for pre check-in
11571 return $use_precheckin_pax_fields;
11572 }
11573
11574 /**
11575 * Returns the associative list of countries from the DB.
11576 *
11577 * @param bool $tn whether to translate the country name.
11578 * @param bool $no_id whether to unset the ID of the country.
11579 *
11580 * @return array associative or empty array.
11581 *
11582 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
11583 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
11584 */
11585 public static function getCountriesArray($tn = true, $no_id = true)
11586 {
11587 $dbo = JFactory::getDbo();
11588 $all_countries = [];
11589
11590 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
11591 $dbo->setQuery($q);
11592 $countries = $dbo->loadAssocList();
11593 if (!$countries) {
11594 return [];
11595 }
11596
11597 if ($tn === true) {
11598 $vbo_tn = self::getTranslator();
11599 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
11600 // re-apply sorting by country name
11601 $sorting = [];
11602 foreach ($countries as $country) {
11603 $sorting[$country['country_name']] = $country;
11604 }
11605 ksort($sorting);
11606 $sorted = [];
11607 foreach ($sorting as $country) {
11608 $sorted[] = $country;
11609 }
11610 $countries = $sorted;
11611 unset($sorting, $sorted);
11612 }
11613
11614 foreach ($countries as $v) {
11615 if ($no_id) {
11616 // keep the original structure by unsetting the ID only needed for translation
11617 unset($v['id']);
11618 }
11619 $all_countries[$v['country_3_code']] = $v;
11620 }
11621
11622 return $all_countries;
11623 }
11624
11625 public static function getCountriesSelect(string $name, array $all_countries = [], ?string $current_value = '', ?string $empty_value = ' ', ?string $idAttr = null)
11626 {
11627 if (!$all_countries) {
11628 $all_countries = self::getCountriesArray();
11629 }
11630
11631 $countries = '<select ' . ($idAttr ? 'id="' . $idAttr . '" ' : '') . 'name="' . $name . '">' . "\n";
11632 if (strlen($empty_value)) {
11633 $countries .= '<option value="">' . $empty_value . '</option>' . "\n";
11634 }
11635 foreach ($all_countries as $v) {
11636 $countries .= '<option value="' . $v['country_3_code'] . '"' . ($v['country_3_code'] == $current_value ? ' selected="selected"' : '') . '>' . $v['country_name'] . '</option>' . "\n";
11637 }
11638 $countries .= '</select>';
11639
11640 return $countries;
11641 }
11642
11643 public static function getThumbSize($skipsession = false)
11644 {
11645 static $thumb_size = null;
11646
11647 if ($thumb_size) {
11648 return $thumb_size;
11649 }
11650
11651 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11652
11653 return $thumb_size;
11654 }
11655
11656 /**
11657 * Checks whether an iCal file for the reservation should be
11658 * attached to the confirmation email for customer and/or admin.
11659 *
11660 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11661 *
11662 * @since 1.12.0 (J) - 1.2.0 (WP)
11663 */
11664 public static function attachIcal()
11665 {
11666 return VBOFactory::getConfig()->getInt('attachical', 1);
11667 }
11668
11669 /**
11670 * How the calculation of the orphan dates should take place.
11671 *
11672 * @return string "next" for only checking the bookings ahead, "prevnext" if
11673 * also the previous bookings should be checked.
11674 *
11675 * @since 1.13 (J) - 1.3.0 (WP)
11676 */
11677 public static function orphansCalculation()
11678 {
11679 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11680 }
11681
11682 /**
11683 * Returns the name of the template file to use for the front-end View "search".
11684 *
11685 * @return string the name of the template file. New and upgraded user will both use the classic file.
11686 *
11687 * @since 1.13 (J) - 1.3.0 (WP)
11688 */
11689 public static function searchResultsTmpl()
11690 {
11691 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11692 }
11693
11694 /**
11695 * Returns a string without any new-line characters
11696 * to be used for JavaScript values without facing
11697 * errors like 'unterminated string literal'.
11698 * By passing nl2br($str) as argument, we can keep
11699 * the wanted new-line HTML tags for PRE tags.
11700 * We use implode() with just one argument to
11701 * not use an empty string as glue for the string.
11702 *
11703 * @param $str string
11704 *
11705 * @return string
11706 */
11707 public static function strTrimLiteral($str) {
11708 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11709 $lines = explode("\n", $str);
11710 $new_lines = array();
11711 foreach ($lines as $i => $line) {
11712 if (strlen($line)) {
11713 $new_lines[] = trim($line);
11714 }
11715 }
11716 return implode($new_lines);
11717 }
11718
11719 /**
11720 * Returns an instance of the VboApplication class.
11721 *
11722 * @return VboApplication
11723 */
11724 public static function getVboApplication()
11725 {
11726 // require library
11727 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11728
11729 // return a new instance
11730 return new VboApplication();
11731 }
11732
11733 /**
11734 * Returns the translation of the given 0-indexed week day.
11735 *
11736 * @param int $wd the 0-indexed day of the week.
11737 * @param bool $short whether to get the short version of the weekday.
11738 *
11739 * @return string the name of the week day.
11740 *
11741 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11742 */
11743 public static function sayWeekDay($wd, $short = false)
11744 {
11745 switch ($wd) {
11746 case '6' :
11747 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11748 break;
11749 case '5' :
11750 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11751 break;
11752 case '4' :
11753 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11754 break;
11755 case '3' :
11756 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11757 break;
11758 case '2' :
11759 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11760 break;
11761 case '1' :
11762 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11763 break;
11764 default :
11765 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11766 break;
11767 }
11768 return $ret;
11769 }
11770
11771 /**
11772 * Returns the translation of the given 1-indexed month of the year.
11773 *
11774 * @param int $idm the 1-indexed month of the year.
11775 * @param bool $short whether to get the short version of the month.
11776 *
11777 * @return string the name of the month.
11778 *
11779 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11780 */
11781 public static function sayMonth($idm, $short = false)
11782 {
11783 switch ($idm) {
11784 case '12' :
11785 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11786 break;
11787 case '11' :
11788 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11789 break;
11790 case '10' :
11791 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11792 break;
11793 case '9' :
11794 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11795 break;
11796 case '8' :
11797 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11798 break;
11799 case '7' :
11800 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11801 break;
11802 case '6' :
11803 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11804 break;
11805 case '5' :
11806 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11807 break;
11808 case '4' :
11809 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11810 break;
11811 case '3' :
11812 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11813 break;
11814 case '2' :
11815 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11816 break;
11817 default :
11818 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11819 break;
11820 }
11821 return $ret;
11822 }
11823
11824 public static function sayDayMonth($d) {
11825 switch ($d) {
11826 case '31' :
11827 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11828 break;
11829 case '30' :
11830 $ret = JText::translate('VBDAYMONTHTHIRTY');
11831 break;
11832 case '29' :
11833 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11834 break;
11835 case '28' :
11836 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11837 break;
11838 case '27' :
11839 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11840 break;
11841 case '26' :
11842 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11843 break;
11844 case '25' :
11845 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11846 break;
11847 case '24' :
11848 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11849 break;
11850 case '23' :
11851 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11852 break;
11853 case '22' :
11854 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11855 break;
11856 case '21' :
11857 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11858 break;
11859 case '20' :
11860 $ret = JText::translate('VBDAYMONTHTWENTY');
11861 break;
11862 case '19' :
11863 $ret = JText::translate('VBDAYMONTHNINETEEN');
11864 break;
11865 case '18' :
11866 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11867 break;
11868 case '17' :
11869 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11870 break;
11871 case '16' :
11872 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11873 break;
11874 case '15' :
11875 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11876 break;
11877 case '14' :
11878 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11879 break;
11880 case '13' :
11881 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11882 break;
11883 case '12' :
11884 $ret = JText::translate('VBDAYMONTHTWELVE');
11885 break;
11886 case '11' :
11887 $ret = JText::translate('VBDAYMONTHELEVEN');
11888 break;
11889 case '10' :
11890 $ret = JText::translate('VBDAYMONTHTEN');
11891 break;
11892 case '9' :
11893 $ret = JText::translate('VBDAYMONTHNINE');
11894 break;
11895 case '8' :
11896 $ret = JText::translate('VBDAYMONTHEIGHT');
11897 break;
11898 case '7' :
11899 $ret = JText::translate('VBDAYMONTHSEVEN');
11900 break;
11901 case '6' :
11902 $ret = JText::translate('VBDAYMONTHSIX');
11903 break;
11904 case '5' :
11905 $ret = JText::translate('VBDAYMONTHFIVE');
11906 break;
11907 case '4' :
11908 $ret = JText::translate('VBDAYMONTHFOUR');
11909 break;
11910 case '3' :
11911 $ret = JText::translate('VBDAYMONTHTHREE');
11912 break;
11913 case '2' :
11914 $ret = JText::translate('VBDAYMONTHTWO');
11915 break;
11916 default :
11917 $ret = JText::translate('VBDAYMONTHONE');
11918 break;
11919 }
11920 return $ret;
11921 }
11922
11923 public static function totElements($arr) {
11924 $n = 0;
11925 if (is_array($arr)) {
11926 foreach ($arr as $a) {
11927 if (!empty($a)) {
11928 $n++;
11929 }
11930 }
11931 return $n;
11932 }
11933 return false;
11934 }
11935
11936 /**
11937 * Returns a list of documents that were uploaded
11938 * for the specified customer.
11939 *
11940 * @param integer $id The customer ID.
11941 *
11942 * @return array A list of documents.
11943 *
11944 * @since 1.3.0
11945 */
11946 public static function getCustomerDocuments($id)
11947 {
11948 $dbo = JFactory::getDbo();
11949
11950 $q = $dbo->getQuery(true)
11951 ->select($dbo->qn('docsfolder'))
11952 ->from($dbo->qn('#__vikbooking_customers'))
11953 ->where($dbo->qn('id') . ' = ' . (int) $id);
11954
11955 $dbo->setQuery($q, 0, 1);
11956 $dbo->execute();
11957
11958 if (!$dbo->getNumRows())
11959 {
11960 // customer not found
11961 return array();
11962 }
11963
11964 // retrieve customer documents directory name
11965 $dirname = $dbo->loadResult();
11966
11967 if (empty($dirname))
11968 {
11969 // no available directory
11970 return array();
11971 }
11972
11973 // build documents folder path
11974 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11975
11976 if (!is_dir($dirname))
11977 {
11978 // the customer directory doesn't exist
11979 return array();
11980 }
11981
11982 // read all files from customer directory
11983 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11984
11985 $files = array();
11986
11987 foreach ($glob as $path)
11988 {
11989 // skip "index.html"
11990 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11991 {
11992 // extract name and extension from file path
11993 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11994 {
11995 $name = $match[1];
11996 $ext = $match[2];
11997 }
11998 else
11999 {
12000 $name = basename($path);
12001 $ext = '';
12002 }
12003
12004 $file = new stdClass;
12005 $file->path = $path;
12006 $file->name = $name;
12007 $file->ext = $ext;
12008 $file->basename = $file->name . '.' . $file->ext;
12009 $file->size = filesize($path);
12010 $file->date = filemtime($path);
12011 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
12012
12013 $files[] = $file;
12014 }
12015 }
12016
12017 // sort files by creation date
12018 usort($files, function($a, $b)
12019 {
12020 return $b->date - $a->date;
12021 });
12022
12023 return $files;
12024 }
12025
12026 public static function displayPaymentParameters($pfile, $pparams = '') {
12027 $html = '<p>---------</p>';
12028
12029 /**
12030 * @wponly The payment gateway is now loaded
12031 * using the apposite dispatcher.
12032 *
12033 * @since 1.0.5
12034 */
12035 JLoader::import('adapter.payment.dispatcher');
12036
12037 try
12038 {
12039 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
12040 }
12041 catch (Exception $e)
12042 {
12043 // payment not found
12044 $html = $e->getMessage();
12045
12046 if ($code = $e->getCode())
12047 {
12048 $html = '<b>' . $code . '</b> : ' . $html;
12049 }
12050
12051 return $html;
12052 }
12053 //
12054
12055 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
12056 $arrparams = !is_array($arrparams) ? array() : $arrparams;
12057
12058 // get admin parameters
12059 $pconfig = $payment->getAdminParameters();
12060
12061 if (count($pconfig) > 0) {
12062 $html = '';
12063 foreach ($pconfig as $value => $cont) {
12064 if (empty($value)) {
12065 continue;
12066 }
12067 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12068 $label = $labelparts[0];
12069 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12070 if (!empty($cont['help'])) {
12071 $labelhelp = $cont['help'];
12072 }
12073 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12074 $html .= '<div class="vbo-param-container">';
12075 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12076 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12077 }
12078 $html .= '<div class="vbo-param-setting">';
12079 switch ($cont['type']) {
12080 case 'custom':
12081 $html .= $cont['html'];
12082 break;
12083 case 'select':
12084 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12085 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12086 if (isset($cont['multiple']) && $cont['multiple']) {
12087 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
12088 } else {
12089 $html .= '<select name="vikpaymentparams['.$value.']">';
12090 }
12091 foreach ($options as $optkey => $poption) {
12092 $checkval = $is_assoc ? $optkey : $poption;
12093 $selected = false;
12094 if (isset($arrparams[$value])) {
12095 if (is_array($arrparams[$value])) {
12096 $selected = in_array($checkval, $arrparams[$value]);
12097 } else {
12098 $selected = ($checkval == $arrparams[$value]);
12099 }
12100 } elseif (isset($default_paramv)) {
12101 if (is_array($default_paramv)) {
12102 $selected = in_array($checkval, $default_paramv);
12103 } else {
12104 $selected = ($checkval == $default_paramv);
12105 }
12106 }
12107 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12108 }
12109 $html .= '</select>';
12110 break;
12111 case 'password':
12112 $html .= '<div class="btn-wrapper input-append">';
12113 $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"/>';
12114 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12115 $html .= '</div>';
12116 break;
12117 case 'number':
12118 $number_attr = array();
12119 if (isset($cont['min'])) {
12120 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12121 }
12122 if (isset($cont['max'])) {
12123 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12124 }
12125 if (isset($cont['step'])) {
12126 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12127 }
12128 $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) . '/>';
12129 break;
12130 case 'textarea':
12131 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12132 break;
12133 case 'hidden':
12134 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
12135 break;
12136 case 'checkbox':
12137 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12138 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
12139 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
12140 break;
12141 default:
12142 $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"/>';
12143 break;
12144 }
12145 if (strlen($labelhelp) > 0) {
12146 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12147 }
12148 $html .= '</div>';
12149 $html .= '</div>';
12150 }
12151 // JS helper function to toggle the password fields
12152 $html .= "\n" . '<script>' . "\n";
12153 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
12154 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12155 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12156 $html .= ' var inp_type = inp.attr("type");' . "\n";
12157 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12158 $html .= '}' . "\n";
12159 $html .= "\n" . '</script>' . "\n";
12160 }
12161
12162 return $html;
12163 }
12164
12165 public static function displaySMSParameters($pfile, $params = null)
12166 {
12167 $html = '---------';
12168
12169 $params = is_string($params) ? json_decode($params, true) : $params;
12170 $params = is_array($params) ? $params : [];
12171
12172 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
12173 return $html;
12174 }
12175
12176 // attempt to load the class file
12177 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
12178
12179 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
12180 return $html;
12181 }
12182
12183 // load the gateway parameters
12184 $config = VikSmsApi::getAdminParameters();
12185
12186 if (!is_array($config) || !count($config)) {
12187 return $html;
12188 }
12189
12190 // flags for JS helpers
12191 $js_helpers = array();
12192
12193 $html = '';
12194 foreach ($config as $value => $cont) {
12195 if (empty($value)) {
12196 continue;
12197 }
12198 $inp_attr = '';
12199 if (isset($cont['attributes'])) {
12200 foreach ($cont['attributes'] as $inpk => $inpv) {
12201 $inp_attr .= $inpk.'="'.$inpv.'" ';
12202 }
12203 $inp_attr = ' ' . rtrim($inp_attr);
12204 }
12205 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12206 $label = $labelparts[0];
12207 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12208 if (!empty($cont['help'])) {
12209 $labelhelp = $cont['help'];
12210 }
12211 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12212 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
12213 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12214 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12215 }
12216 $html .= '<div class="vbo-param-setting">';
12217 switch ($cont['type']) {
12218 case 'custom':
12219 $html .= $cont['html'];
12220 break;
12221 case 'select':
12222 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12223 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12224 if (isset($cont['multiple']) && $cont['multiple']) {
12225 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
12226 } else {
12227 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
12228 }
12229 foreach ($options as $optkey => $poption) {
12230 $checkval = $is_assoc ? $optkey : $poption;
12231 $selected = false;
12232 if (isset($params[$value])) {
12233 if (is_array($params[$value])) {
12234 $selected = in_array($checkval, $params[$value]);
12235 } else {
12236 $selected = ($checkval == $params[$value]);
12237 }
12238 } elseif (isset($default_paramv)) {
12239 if (is_array($default_paramv)) {
12240 $selected = in_array($checkval, $default_paramv);
12241 } else {
12242 $selected = ($checkval == $default_paramv);
12243 }
12244 }
12245 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12246 }
12247 $html .= '</select>';
12248 break;
12249 case 'password':
12250 $html .= '<div class="btn-wrapper input-append">';
12251 $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 . '/>';
12252 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12253 $html .= '</div>';
12254 // set flag for JS helper
12255 $js_helpers[] = $cont['type'];
12256 break;
12257 case 'number':
12258 $number_attr = array();
12259 if (isset($cont['min'])) {
12260 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12261 }
12262 if (isset($cont['max'])) {
12263 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12264 }
12265 if (isset($cont['step'])) {
12266 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12267 }
12268 $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 . '/>';
12269 break;
12270 case 'textarea':
12271 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12272 break;
12273 case 'visual_html':
12274 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
12275 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
12276 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
12277 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
12278 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
12279 break;
12280 case 'hidden':
12281 $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 . '/>';
12282 break;
12283 case 'checkbox':
12284 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12285 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
12286 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
12287 break;
12288 default:
12289 $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 . '/>';
12290 break;
12291 }
12292 if (strlen($labelhelp) > 0) {
12293 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12294 }
12295 $html .= '</div>';
12296 $html .= '</div>';
12297 }
12298
12299 // JS helper functions
12300 if (in_array('password', $js_helpers)) {
12301 // toggle the password fields
12302 $html .= "\n" . '<script>' . "\n";
12303 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
12304 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12305 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12306 $html .= ' var inp_type = inp.attr("type");' . "\n";
12307 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12308 $html .= '}' . "\n";
12309 $html .= "\n" . '</script>' . "\n";
12310 }
12311
12312 return $html;
12313 }
12314
12315 /**
12316 * Renders the params of the requested cron job file-class.
12317 *
12318 * @param string $file the name of the cron job driver file.
12319 * @param array $params the parameters array.
12320 *
12321 * @return string the necessary HTML content to render.
12322 *
12323 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
12324 */
12325 public static function displayCronParameters($file, $params = [])
12326 {
12327 try
12328 {
12329 // attempt to create a new instance
12330 $job = VBOFactory::getCronFactory()->createInstance($file);
12331 }
12332 catch (Exception $e)
12333 {
12334 // something went wrong, display error message
12335 return '<p>' . $e->getMessage() . '<p>';
12336 }
12337
12338 // get admin parameters
12339 $config = $job->getForm();
12340
12341 if (!is_array($config) || !$config) {
12342 return '<p>---------</p>';
12343 }
12344
12345 /**
12346 * Render the cron parameters through the global params rendering class.
12347 *
12348 * @since 1.16.9 (J) - 1.6.9 (WP)
12349 */
12350 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
12351 }
12352
12353 public static function invokeChannelManager($skiporder = true, $order = array()) {
12354 $task = VikRequest::getString('task', '', 'request');
12355 $view = VikRequest::getString('view', '', 'request');
12356 $tmpl = VikRequest::getString('tmpl', '', 'request');
12357 $noimpression = array('vieworder', 'booking');
12358 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')) {
12359 //VCM Channel Impression
12360 if (!class_exists('VikChannelManagerConfig')) {
12361 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12362 }
12363 if (!class_exists('VikChannelManager')) {
12364 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12365 }
12366 VikChannelManager::invokeChannelImpression();
12367 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
12368 //VCM Channel Conversion-Impression
12369 if (!class_exists('VikChannelManagerConfig')) {
12370 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12371 }
12372 if (!class_exists('VikChannelManager')) {
12373 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12374 }
12375 VikChannelManager::invokeChannelConversionImpression($order);
12376 }
12377 }
12378
12379 public static function validEmail($email) {
12380 $isValid = true;
12381 $atIndex = strrpos($email, "@");
12382 if (is_bool($atIndex) && !$atIndex) {
12383 $isValid = false;
12384 } else {
12385 $domain = substr($email, $atIndex +1);
12386 $local = substr($email, 0, $atIndex);
12387 $localLen = strlen($local);
12388 $domainLen = strlen($domain);
12389 if ($localLen < 1 || $localLen > 64) {
12390 // local part length exceeded
12391 $isValid = false;
12392 } else
12393 if ($domainLen < 1 || $domainLen > 255) {
12394 // domain part length exceeded
12395 $isValid = false;
12396 } else
12397 if ($local[0] == '.' || $local[$localLen -1] == '.') {
12398 // local part starts or ends with '.'
12399 $isValid = false;
12400 } else
12401 if (preg_match('/\\.\\./', $local)) {
12402 // local part has two consecutive dots
12403 $isValid = false;
12404 } else
12405 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
12406 // character not valid in domain part
12407 $isValid = false;
12408 } else
12409 if (preg_match('/\\.\\./', $domain)) {
12410 // domain part has two consecutive dots
12411 $isValid = false;
12412 } else
12413 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
12414 // character not valid in local part unless
12415 // local part is quoted
12416 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
12417 $isValid = false;
12418 }
12419 }
12420 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
12421 // domain not found in DNS
12422 $isValid = false;
12423 }
12424 }
12425 return $isValid;
12426 }
12427
12428 public static function caniWrite($path) {
12429 if ($path[strlen($path) - 1] == '/') {
12430 // ricorsivo return a temporary file path
12431 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
12432 }
12433 if (is_dir($path)) {
12434 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
12435 }
12436 // check tmp file for read/write capabilities
12437 $rm = file_exists($path);
12438 $f = @fopen($path, 'a');
12439 if ($f === false) {
12440 return false;
12441 }
12442 fclose($f);
12443 if (!$rm) {
12444 unlink($path);
12445 }
12446 return true;
12447 }
12448
12449 /**
12450 * Alias method of JFile::upload to unify any
12451 * upload function into one.
12452 *
12453 * @param string $src The name of the php (temporary) uploaded file.
12454 * @param string $dest The path (including filename) to move the uploaded file to.
12455 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
12456 *
12457 * @return boolean True on success.
12458 *
12459 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
12460 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
12461 */
12462 public static function uploadFile($src, $dest, $copy_only = false) {
12463 // always attempt to include the File class
12464 jimport('joomla.filesystem.file');
12465
12466 // upload the file
12467 if (!$copy_only) {
12468 $result = JFile::upload($src, $dest);
12469 } else {
12470 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
12471 $result = JFile::copy($src, $dest);
12472 }
12473
12474 /**
12475 * @wponly in order to not lose uploaded files after installing an update,
12476 * we need to move any uploaded file onto a recovery folder.
12477 */
12478 if ($result) {
12479 VikBookingLoader::import('update.manager');
12480 VikBookingUpdateManager::triggerUploadBackup($dest);
12481 }
12482 //
12483
12484 // return upload result
12485 return $result;
12486 }
12487
12488 /**
12489 * Helper method used to upload the given file (retrieved from $_FILES)
12490 * into the specified destination.
12491 *
12492 * @param array $file An associative array with the file details.
12493 * @param string $dest The destination path.
12494 * @param string $filters A string (or a regex) containing the allowed extensions.
12495 *
12496 * @return array The uploading result.
12497 *
12498 * @throws RuntimeException
12499 *
12500 * @since 1.3.0
12501 */
12502 public static function uploadFileFromRequest($file, $dest, $filters = '*')
12503 {
12504 jimport('joomla.filesystem.file');
12505
12506 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
12507
12508 if (empty($file['name']))
12509 {
12510 throw new RuntimeException('Missing file', 400);
12511 }
12512
12513 /**
12514 * Make sure the upload of the file didn't raise any errors.
12515 *
12516 * @link https://www.php.net/manual/en/features.file-upload.errors.php
12517 */
12518 if ((int) $file['error'])
12519 {
12520 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
12521 {
12522 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
12523 }
12524 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
12525 {
12526 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
12527 }
12528 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
12529 {
12530 $error = 'The uploaded file was only partially uploaded.';
12531 }
12532 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
12533 {
12534 $error = 'No file was uploaded.';
12535 }
12536 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
12537 {
12538 $error = 'Missing a temporary folder.';
12539 }
12540 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
12541 {
12542 $error = 'Failed to write file to disk.';
12543 }
12544 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
12545 {
12546 $error = 'A PHP extension stopped the file upload.';
12547 }
12548 else
12549 {
12550 $error = 'Unknown error.';
12551 }
12552
12553 throw new RuntimeException($error, 500);
12554 }
12555
12556 $src = $file['tmp_name'];
12557
12558 // extract file name and extension
12559 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
12560 {
12561 $filename = $match[1];
12562 $fileext = $match[2];
12563 }
12564 else
12565 {
12566 // probably no extension provided
12567 $filename = basename($file['name']);
12568 $fileext = '';
12569 }
12570
12571 $j = '';
12572
12573 if (file_exists($dest . $filename . $fileext))
12574 {
12575 $j = 2;
12576
12577 while (file_exists($dest . $filename . '-' . $j . $fileext))
12578 {
12579 $j++;
12580 }
12581
12582 $j = '-' . $j;
12583 }
12584
12585 $finaldest = $dest . $filename . $j . $fileext;
12586
12587 // make sure the file extension is supported
12588 if (!self::isFileTypeCompatible(basename($finaldest), $filters))
12589 {
12590 // extension not supported
12591 throw new RuntimeException(sprintf('Extension [%s] is not supported', $fileext), 400);
12592 }
12593
12594 // try to upload the file
12595 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
12596 {
12597 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
12598 }
12599
12600 $file = new stdClass;
12601 $file->name = $filename . $j;
12602 $file->ext = ltrim($fileext, '.');
12603 $file->filename = basename($finaldest);
12604 $file->path = $finaldest;
12605
12606 return $file;
12607 }
12608
12609 /**
12610 * Helper method used to check whether the given file name
12611 * supports one of the given filters.
12612 *
12613 * @param mixed $file Either the file name or the uploaded file.
12614 * @param string $filters Either a regex or a comma-separated list of supported extensions.
12615 * The regex must be inclusive of delimiters.
12616 *
12617 * @return bool True if supported, false otherwise.
12618 *
12619 * @since 1.7.3 (WP) - 1.17.3 (J)
12620 */
12621 public static function isFileTypeCompatible($file, $filters)
12622 {
12623 // make sure the filters query is not empty
12624 if (strlen($filters) == 0)
12625 {
12626 // cannot assert whether the file could be accepted or not
12627 return false;
12628 }
12629
12630 // check whether all the files are accepted
12631 if ($filters == '*')
12632 {
12633 return true;
12634 }
12635
12636 // use the file MIME TYPE in case of array
12637 if (is_array($file))
12638 {
12639 $file = $file['type'];
12640 }
12641
12642 // check if we are dealing with a regex
12643 if (static::isRegex($filters))
12644 {
12645 return (bool) preg_match($filters, $file);
12646 }
12647
12648 // fallback to comma-separated list
12649 $types = array_filter(preg_split("/\s*,\s*/", $filters));
12650
12651 foreach ($types as $t)
12652 {
12653 // remove initial dot if specified
12654 $t = ltrim($t, '.');
12655 // escape slashes to avoid breaking the regex
12656 $t = preg_replace("/\//", '\/', $t);
12657
12658 // check if the file ends with the given extension
12659 if (preg_match("/{$t}$/i", $file))
12660 {
12661 return true;
12662 }
12663 }
12664
12665 return false;
12666 }
12667
12668 /**
12669 * Checks whether the given string is a structured PCRE regex.
12670 * It simply makes sure that the string owns valid delimiters.
12671 * A delimiter can be any non-alphanumeric, non-backslash,
12672 * non-whitespace character.
12673 *
12674 * @param string $str The string to check.
12675 *
12676 * @return boolean True if a regex, false otherwise.
12677 *
12678 * @since 1.7.3 (WP) - 1.17.3 (J)
12679 */
12680 public static function isRegex($str)
12681 {
12682 // first of all make sure the first character is a supported delimiter
12683 if (!preg_match("/^([!#$%&'*+,.\/:;=?@^_`|~\-(\[{<\"])/", $str, $match))
12684 {
12685 // no valid delimiter
12686 return false;
12687 }
12688
12689 // get delimiter
12690 $d = $match[1];
12691
12692 // lookup used to check if we should take a different ending delimiter
12693 $lookup = array(
12694 '{' => '}',
12695 '[' => ']',
12696 '(' => ')',
12697 '<' => '>',
12698 );
12699
12700 if (isset($lookup[$d]))
12701 {
12702 $d = $lookup[$d];
12703 }
12704
12705 // make sure the regex ends with the delimiter found
12706 return (bool) preg_match("/\\{$d}[gimsxU]*$/", $str);
12707 }
12708
12709 /**
12710 * Gets the instance of a specific report class.
12711 *
12712 * @param string $report the name of the report to load.
12713 *
12714 * @return mixed false or report object instance.
12715 *
12716 * @since 1.3.0
12717 */
12718 public static function getReportInstance($report)
12719 {
12720 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12721
12722 return VikBookingReport::getInstanceOf($report);
12723 }
12724
12725 /**
12726 * Checks whether the guest reviews are enabled and VCM is installed.
12727 *
12728 * @return boolean true if enabled, false otherwise.
12729 *
12730 * @since 1.3.0
12731 */
12732 public static function allowGuestReviews()
12733 {
12734 $dbo = JFactory::getDbo();
12735 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12736 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12737 $dbo->setQuery($q);
12738 $dbo->execute();
12739 if ($dbo->getNumRows()) {
12740 $s = $dbo->loadResult();
12741 return ((int)$s === 1 && $vcm_installed);
12742 }
12743 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12744 $dbo->setQuery($q);
12745 $dbo->execute();
12746 return $vcm_installed;
12747 }
12748
12749 /**
12750 * Gets the minimum chars for the guest review message.
12751 *
12752 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12753 *
12754 * @since 1.3.0
12755 */
12756 public static function guestReviewMinChars()
12757 {
12758 $dbo = JFactory::getDbo();
12759 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12760 $dbo->setQuery($q);
12761 $dbo->execute();
12762 if ($dbo->getNumRows() > 0) {
12763 return (int)$dbo->loadResult();
12764 }
12765 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12766 $dbo->setQuery($q);
12767 $dbo->execute();
12768 return 15;
12769 }
12770
12771 /**
12772 * The approval type for new guest reviews.
12773 *
12774 * @return string auto or manual.
12775 *
12776 * @since 1.3.0
12777 */
12778 public static function guestReviewsApproval()
12779 {
12780 $dbo = JFactory::getDbo();
12781 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12782 $dbo->setQuery($q);
12783 $dbo->execute();
12784 if ($dbo->getNumRows() > 0) {
12785 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12786 }
12787 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12788 $dbo->setQuery($q);
12789 $dbo->execute();
12790 return 'auto';
12791 }
12792
12793 /**
12794 * The type of reviews guests should leave.
12795 *
12796 * @return string global or service.
12797 *
12798 * @since 1.3.0
12799 */
12800 public static function guestReviewsType()
12801 {
12802 $dbo = JFactory::getDbo();
12803 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12804 $dbo->setQuery($q);
12805 $dbo->execute();
12806 if ($dbo->getNumRows() > 0) {
12807 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12808 }
12809 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12810 $dbo->setQuery($q);
12811 $dbo->execute();
12812 return 'service';
12813 }
12814
12815 /**
12816 * The services to be reviewed by the guests.
12817 *
12818 * @return array list, empty or not, of the services to be reviewed.
12819 *
12820 * @since 1.3.0
12821 */
12822 public static function guestReviewsServices()
12823 {
12824 $dbo = JFactory::getDbo();
12825 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12826 $dbo->setQuery($q);
12827 $dbo->execute();
12828 if ($dbo->getNumRows()) {
12829 return $dbo->loadAssocList();
12830 }
12831 // insert the default services
12832 $default_services = array(
12833 JText::translate('VBOGREVVALUE'),
12834 JText::translate('VBOGREVLOCATION'),
12835 JText::translate('VBOGREVSTAFF'),
12836 JText::translate('VBOGREVCLEAN'),
12837 JText::translate('VBOGREVCOMFORT'),
12838 JText::translate('VBOGREVFACILITIES'),
12839 );
12840 foreach ($default_services as $def_service) {
12841 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12842 $dbo->setQuery($q);
12843 $dbo->execute();
12844 }
12845 return array();
12846 }
12847
12848 /**
12849 * Checks whether the guest reviews should be downloaded (max 1 per day).
12850 * VCM must be installed in order to download the new reviews.
12851 *
12852 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12853 *
12854 * @since 1.3.0
12855 */
12856 public static function shouldDownloadReviews()
12857 {
12858 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12859 return -1;
12860 }
12861
12862 $today = date('Y-m-d');
12863
12864 $dbo = JFactory::getDbo();
12865 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12866 $dbo->setQuery($q);
12867 $dbo->execute();
12868 if ($dbo->getNumRows()) {
12869 $last_download = $dbo->loadResult();
12870 // update last download day
12871 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12872 $dbo->setQuery($q);
12873 $dbo->execute();
12874 //
12875 return ($last_download != $today ? 1 : 0);
12876 }
12877 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12878 $dbo->setQuery($q);
12879 $dbo->execute();
12880 return 1;
12881 }
12882
12883 /**
12884 * Tells whether a booking can be reviewed.
12885 *
12886 * @param array $booking booking array details.
12887 *
12888 * @return boolean true if a review can be left, false otherwise.
12889 *
12890 * @since 1.3.0
12891 */
12892 public static function canBookingBeReviewed($booking)
12893 {
12894 // reviews must be enabled and supported
12895 if (!self::allowGuestReviews()) {
12896 return false;
12897 }
12898
12899 // booking status must be confirmed
12900 if ($booking['status'] != 'confirmed') {
12901 return false;
12902 }
12903
12904 // review can be left starting from the check-out day
12905 $checkout_info = getdate($booking['checkout']);
12906 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12907 if (time() < $checkout_midn) {
12908 return false;
12909 }
12910
12911 // make sure a review for this booking does not exist
12912 $noreview = true;
12913 $dbo = JFactory::getDbo();
12914 try {
12915 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12916 $dbo->setQuery($q);
12917 $dbo->execute();
12918 $noreview = ($dbo->getNumRows() < 1);
12919 } catch (Exception $e) {
12920 // if the query fails, we do not allow the review to be left
12921 $noreview = false;
12922 }
12923
12924 return $noreview;
12925 }
12926
12927 /**
12928 * Gets the review for a booking, no matter if it's published or not.
12929 * Review will be returned only if reviews are enabled and supported.
12930 * Only reviews left through the website, no OTA reviews as this method
12931 * should be used in the front-end to display the review to the guest.
12932 *
12933 * @param array $booking booking array details.
12934 *
12935 * @return mixed associative array or false.
12936 *
12937 * @since 1.3.0
12938 */
12939 public static function getBookingReview($booking)
12940 {
12941 // reviews must be enabled and supported
12942 if (!self::allowGuestReviews()) {
12943 return false;
12944 }
12945
12946 if (empty($booking['id'])) {
12947 return false;
12948 }
12949
12950 // make sure a review for this booking exists
12951 $dbo = JFactory::getDbo();
12952 $review = array();
12953 try {
12954 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12955 $dbo->setQuery($q);
12956 $dbo->execute();
12957 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12958 } catch (Exception $e) {
12959 // query has failed
12960 $review = array();
12961 }
12962
12963 if (count($review)) {
12964 // make sure the review was left from the website
12965 $review['uniquekey'] = (int)$review['uniquekey'];
12966 }
12967
12968 if (count($review) && !empty($review['uniquekey'])) {
12969 // this is an OTA review
12970 return false;
12971 }
12972
12973 if (count($review) && !empty($review['content'])) {
12974 // decode content
12975 $review['content'] = json_decode($review['content'], true);
12976 }
12977
12978 return count($review) ? $review : false;
12979 }
12980
12981 /**
12982 * Attempts to find a translation for a raw customer data label.
12983 * Bookings downloaded from the OTAs will save a raw string of information
12984 * composed of pairs of label-value separated by new line feeds.
12985 * We try to translate the given label into the current language.
12986 *
12987 * @param string $label the raw string label to translate.
12988 *
12989 * @return string either the original or the translated label.
12990 *
12991 * @since 1.3.5
12992 * @since 1.4.0 back-end support added.
12993 */
12994 public static function tnCustomerRawDataLabel($label)
12995 {
12996 // this is a map of the known labels
12997 $known_lbls = array(
12998 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12999 'LASTNAME' => 'ORDER_LNAME',
13000 'COUNTRY' => 'ORDER_STATE',
13001 'EMAIL' => 'VBMAIL',
13002 'TELEPHONE' => 'VBPHONE',
13003 'PHONE' => 'VBPHONE',
13004 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13005 'MEAL_PLAN' => 'VBOMEALPLAN',
13006 'CITY' => 'VBCITY',
13007 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
13008 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
13009 );
13010
13011 if (self::isAdmin()) {
13012 // override the map of known translation strings
13013 $known_lbls = array(
13014 'NAME' => 'ORDER_NAME',
13015 'LASTNAME' => 'ORDER_LNAME',
13016 'COUNTRY' => 'ORDER_STATE',
13017 'ADDRESS' => 'ORDER_ADDRESS',
13018 'CITY' => 'ORDER_CITY',
13019 'LOCATION' => 'ORDER_CITY',
13020 'EMAIL' => 'ORDER_EMAIL',
13021 'TELEPHONE' => 'ORDER_PHONE',
13022 'PHONE' => 'ORDER_PHONE',
13023 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13024 );
13025 }
13026
13027 // we get rid of any empty space by keeping the underscores
13028 $converted = str_replace(' ', '', strtoupper($label));
13029
13030 if (isset($known_lbls[$converted])) {
13031 // this language definition has been mapped
13032 return JText::translate($known_lbls[$converted]);
13033 }
13034
13035 // we try to guess the translation string by prepending VBO
13036 $guessed = JText::translate('VBO' . $converted);
13037 if ($guessed != 'VBO' . $converted) {
13038 // the label was translated correctly
13039 return $guessed;
13040 }
13041
13042 // this label could not be translated, so we return the plain string
13043 return $label;
13044 }
13045
13046 /**
13047 * We check whether some bookings are available for import from third party plugins.
13048 *
13049 * @return mixed array with list of plugins supported, false otherwise.
13050 *
13051 * @wponly this method is only useful for WordPress.
13052 *
13053 * @since 1.3.5
13054 */
13055 public static function canImportBookingsFromThirdPartyPlugins()
13056 {
13057 $dbo = JFactory::getDbo();
13058
13059 $plugins = array();
13060
13061 /**
13062 * As requested from hundreds of our clients, for the moment we check only the custom
13063 * post types of type "mphb_booking" to see if some bookings are available for import.
13064 */
13065 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
13066 $dbo->setQuery($q);
13067 $dbo->execute();
13068
13069 if (!$dbo->getNumRows()) {
13070 return false;
13071 }
13072
13073 // push this third party plugin
13074 $plugins['mphb'] = 'MotoPress Hotel Booking';
13075
13076 return count($plugins) ? $plugins : false;
13077 }
13078
13079 /**
13080 * This method returns a list of the known languages sorted by the
13081 * administrator custom preferences. Useful for the phone input fields.
13082 *
13083 * @param boolean $code_assoc whether to get an associative array with the lang name.
13084 *
13085 * @return array the sorted list of preferred countries.
13086 *
13087 * @since 1.3.11
13088 */
13089 public static function preferredCountriesOrdering($code_assoc = false)
13090 {
13091 $preferred_countries = array();
13092
13093 // try to get the preferred countries from db
13094 $dbo = JFactory::getDbo();
13095 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
13096 $dbo->setQuery($q);
13097 $setting = $dbo->loadResult();
13098 if (!$setting) {
13099 // create empty configuration record
13100 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
13101 $dbo->setQuery($q);
13102 $dbo->execute();
13103 } else {
13104 $preferred_countries = json_decode($setting);
13105 }
13106
13107 // get the default known languages
13108 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
13109
13110 if (!is_array($preferred_countries) || !count($preferred_countries)) {
13111 // sort the default known languages by country code alphabetically
13112 ksort($sorted_known_langs);
13113 foreach ($sorted_known_langs as $k => $v) {
13114 $langsep = strpos($k, '_') !== false ? '_' : '-';
13115 $langparts = explode($langsep, $k);
13116 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
13117 }
13118 // update the database record
13119 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
13120 $dbo->setQuery($q);
13121 $dbo->execute();
13122 }
13123
13124 if ($code_assoc) {
13125 // this is useful for displaying the preferred countries codes together with the language name
13126 $map = array();
13127 foreach ($preferred_countries as $ccode) {
13128 // look for the current country code in the keys of the known language tags
13129 $match_found = false;
13130 foreach ($sorted_known_langs as $langtag => $langinfo) {
13131 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
13132 $langparts = explode($langsep, $langtag);
13133 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
13134 // match found
13135 $match_found = true;
13136 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13137 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
13138 // match found
13139 $match_found = true;
13140 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13141 }
13142 }
13143 if (!$match_found) {
13144 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
13145 $map[$ccode] = strtoupper($ccode);
13146 }
13147 }
13148 if (count($map)) {
13149 // set the associatve array to be returned
13150 $preferred_countries = $map;
13151 }
13152 }
13153
13154 return $preferred_countries;
13155 }
13156
13157 /**
13158 * Gets the instance of the admin widgets helper class.
13159 *
13160 * @return VikBookingHelperAdminWidgets
13161 *
13162 * @since 1.4.0
13163 */
13164 public static function getAdminWidgetsInstance()
13165 {
13166 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
13167
13168 return VikBookingHelperAdminWidgets::getInstance();
13169 }
13170
13171 /**
13172 * Gets the instance of the conditional rules helper class.
13173 *
13174 * @param bool $require_only whether to return the object.
13175 *
13176 * @return mixed VikBookingHelperConditionalRules or true.
13177 *
13178 * @since 1.4.0
13179 */
13180 public static function getConditionalRulesInstance($require_only = false)
13181 {
13182 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
13183
13184 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
13185 }
13186
13187 /**
13188 * Gets the instance of the geocoding helper class.
13189 *
13190 * @param bool $require_only whether to return the object.
13191 *
13192 * @return mixed VikBookingHelperGeocoding or true.
13193 *
13194 * @since 1.4.0
13195 */
13196 public static function getGeocodingInstance($require_only = false)
13197 {
13198 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
13199
13200 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
13201 }
13202
13203 /**
13204 * Helper method to cope with the removal of the same method
13205 * in the JApplication class introduced with Joomla 4. Using
13206 * isClient() would break the compatibility with J < 3.7 so
13207 * we can rely on this helper method to avoid Fatal Errors.
13208 *
13209 * @return boolean
13210 *
13211 * @since October 2020
13212 */
13213 public static function isAdmin()
13214 {
13215 $app = JFactory::getApplication();
13216 if (method_exists($app, 'isClient')) {
13217 return $app->isClient('administrator');
13218 }
13219
13220 return $app->isAdmin();
13221 }
13222
13223 /**
13224 * Helper method to cope with the removal of the same method
13225 * in the JApplication class introduced with Joomla 4. Using
13226 * isClient() would break the compatibility with J < 3.7 so
13227 * we can rely on this helper method to avoid Fatal Errors.
13228 *
13229 * @return boolean
13230 *
13231 * @since October 2020
13232 */
13233 public static function isSite()
13234 {
13235 $app = JFactory::getApplication();
13236 if (method_exists($app, 'isClient')) {
13237 return $app->isClient('site');
13238 }
13239
13240 return $app->isSite();
13241 }
13242
13243 /**
13244 * Gets the Google Maps API Key.
13245 *
13246 * @return string
13247 *
13248 * @since 1.14 (J) - 1.4.0 (WP)
13249 */
13250 public static function getGoogleMapsKey()
13251 {
13252 return VBOFactory::getConfig()->getString('gmapskey', '');
13253 }
13254
13255 /**
13256 * Checks whether the interactive map booking is enabled.
13257 *
13258 * @return bool
13259 *
13260 * @since 1.14 (J) - 1.4.0 (WP)
13261 */
13262 public static function interactiveMapEnabled()
13263 {
13264 return VBOFactory::getConfig()->getBool('interactive_map', false);
13265 }
13266
13267 /**
13268 * Gets the preferred colors saved in the configuration, if any.
13269 *
13270 * @return array
13271 *
13272 * @since 1.14 (J) - 1.4.0 (WP)
13273 */
13274 public static function getPreferredColors()
13275 {
13276 $config = VBOFactory::getConfig();
13277
13278 // default colors
13279 $pref_colors = [
13280 'textcolor' => '#2282bd',
13281 'bgcolor' => '#2282bd',
13282 'fontcolor' => '#fff',
13283 'bgcolorhov' => '#1890ce',
13284 'fontcolorhov' => '#fff',
13285 ];
13286
13287 // saved colors
13288 $colors = $config->getArray('pref_colors', []);
13289
13290 if (!array_filter($colors)) {
13291 // unset the list to fallback onto the default ones
13292 $colors = [];
13293 }
13294
13295 return $colors ?: $pref_colors;
13296 }
13297
13298 /**
13299 * Adds to the document inline styles for the preferred colors, if any.
13300 *
13301 * @return void
13302 *
13303 * @since 1.14 (J) - 1.4.0 (WP)
13304 */
13305 public static function loadPreferredColorStyles()
13306 {
13307 $pref_colors = self::getPreferredColors();
13308 $css_classes = [];
13309
13310 /**
13311 * Register all preferred colors as CSS variables.
13312 *
13313 * @since 1.18.8 (J) - 1.8.8 (WP)
13314 */
13315 $css_vars = array_filter(array_combine(array_keys($pref_colors), array_values($pref_colors)));
13316 if ($css_vars) {
13317 $css_vars_decl = array_map(function($k, $v) {
13318 return sprintf('--vbo-pref-%s: %s;', preg_replace('/[^a-z0-9]/', '', strtolower($k)), $v);
13319 }, array_keys($css_vars), array_values($css_vars));
13320 $css_classes[] = ':root {' . "\n" . implode("\n", $css_vars_decl) . "\n" . '}';
13321 }
13322
13323 // add in-line style declaration
13324 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
13325 }
13326
13327 /**
13328 * Given the full endpoint URL for the AJAX request, it
13329 * returns an appropriate URI for the current platform.
13330 *
13331 * @param mixed $query The query string or a routed URL.
13332 * @param boolean $xhtml Replace & by &amp; for XML compliance.
13333 *
13334 * @return string The AJAX end-point URI.
13335 *
13336 * @since 1.14 (J) - 1.4.0 (WP)
13337 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
13338 */
13339 public static function ajaxUrl($query = '', $xhtml = false)
13340 {
13341 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
13342 }
13343
13344 /**
13345 * Tells whether no tax rates have been defined so far.
13346 *
13347 * @return bool True if no tax rates defined, false otherwise.
13348 *
13349 * @since 1.14 (J) - 1.4.0 (WP)
13350 */
13351 public static function noTaxRates()
13352 {
13353 $dbo = JFactory::getDbo();
13354
13355 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
13356 $dbo->setQuery($q);
13357 $dbo->execute();
13358
13359 return ($dbo->getNumRows() < 1);
13360 }
13361
13362 /**
13363 * Booking Type feature is strictly connected to VCM and OTAs. Updated
13364 * versions of VBO will always support it as long as VCM is installed.
13365 * Needed by VCM to understand whether certain SQL queries can be performed.
13366 *
13367 * @return bool true if VCM exists, false otherwise.
13368 *
13369 * @since 1.14 (J) - 1.4.0 (WP)
13370 */
13371 public static function isBookingTypeSupported()
13372 {
13373 static $isBookingTypeSupported = null;
13374
13375 if (is_bool($isBookingTypeSupported)) {
13376 return $isBookingTypeSupported;
13377 }
13378
13379 $isBookingTypeSupported = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
13380
13381 return $isBookingTypeSupported;
13382 }
13383
13384 /**
13385 * Returns an instance of the Rates Flow helper class of VCM, if available.
13386 *
13387 * @param bool $anew True to request a new object instance.
13388 *
13389 * @return VCMRatesFlow|null
13390 *
13391 * @since 1.15.0 (J) - 1.5.0 (WP)
13392 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
13393 */
13394 public static function getRatesFlowInstance($anew = false)
13395 {
13396 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
13397 if (!is_file($vcm_fpath)) {
13398 return null;
13399 }
13400
13401 // load dependencies
13402 require_once $vcm_fpath;
13403
13404 // return the instance of the class
13405 return VCMRatesFlow::getInstance($anew);
13406 }
13407
13408 /**
13409 * Returns the current appearance preference.
13410 *
13411 * @return string light, auto or dark.
13412 *
13413 * @since 1.15.0 (J) - 1.5.0 (WP)
13414 */
13415 public static function getAppearancePref()
13416 {
13417 static $preference = null;
13418
13419 if ($preference !== null) {
13420 return $preference;
13421 }
13422
13423 // auto is the default appearance preference
13424 $default_pref = 'auto';
13425
13426 // accepted preferences
13427 $valid_pref = [
13428 'auto',
13429 'light',
13430 'dark',
13431 ];
13432
13433 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
13434 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
13435
13436 return $preference;
13437 }
13438
13439 /**
13440 * According to the appearance preferences, the apposite
13441 * CSS assets are loaded within the document.
13442 *
13443 * @param bool $get_info true to not load any assets.
13444 *
13445 * @return mixed string light, auto, dark, info array or false.
13446 *
13447 * @since 1.15.0 (J) - 1.5.0 (WP)
13448 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
13449 */
13450 public static function loadAppearancePreferenceAssets($get_info = false)
13451 {
13452 // get document object
13453 $document = JFactory::getDocument();
13454
13455 // load current color scheme preference
13456 $current_pref = self::getAppearancePref();
13457
13458 // define caching values
13459 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
13460
13461 // define file attributes
13462 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
13463
13464 // apposite file path and URI for back-end
13465 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
13466 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
13467
13468 // check if this is for the front-end ("auto" file only)
13469 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
13470 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
13471 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
13472 // check if front-end appearance is disabled
13473 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
13474 // do nothing for the front-end by default
13475 return false;
13476 }
13477 }
13478
13479 if (VBOPlatformDetection::isJoomla()) {
13480 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
13481 $css_uri = str_replace('resources/', '', $css_uri);
13482 }
13483
13484 if (!is_file($css_path)) {
13485 // this preference does not require a specific stylesheet
13486 return false;
13487 }
13488
13489 if ($get_info) {
13490 // do not actually load any assets
13491 return [
13492 'id' => $file_attr['id'],
13493 'href' => $css_uri,
13494 ];
13495 }
13496
13497 // load the apposite CSS file
13498 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
13499
13500 return $current_pref;
13501 }
13502
13503 /**
13504 * VBO trigger for VCM subscription status and updates. If called within the admin section
13505 * returns the expiration reminder array to display a modal window, and sets a warning
13506 * message. If called within the front-end, may trigger an email alert.
13507 *
13508 * @return mixed null, array or boolean depending on client position.
13509 *
13510 * @since 1.15.0 (J) - 1.5.0 (WP)
13511 */
13512 public static function getVCMSubscriptionStatus()
13513 {
13514 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
13515 if (!is_file($vcm_lib_path)) {
13516 // do nothing
13517 return null;
13518 }
13519
13520 // determine client position
13521 $is_admin = self::isAdmin();
13522
13523 try {
13524 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
13525 // VCM is outdated
13526 return null;
13527 }
13528
13529 // always attempt to load the admin lang handler of VCM
13530 $lang = JFactory::getLanguage();
13531
13532 // load the VCM admin language file
13533 $vcm_admin_lang_path = '';
13534 if (VBOPlatformDetection::isJoomla()) {
13535 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
13536 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
13537 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
13538 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
13539 /**
13540 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
13541 */
13542 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
13543 }
13544 // load translation file
13545 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
13546 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
13547 /**
13548 * @wponly load language admin handler as well for WP.
13549 * We do this only because of WordPress, but in a way also compatible with Joomla as
13550 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
13551 */
13552 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
13553 }
13554
13555 if ($is_admin) {
13556 // back-end section of the site: check status
13557 if (VikChannelManager::loadExpirationDetails() === false) {
13558 // download and update the expiration details
13559 VikChannelManager::downloadExpirationDetails();
13560 }
13561 $expiration_reminder = null;
13562 if (VikChannelManager::isSubscriptionExpired()) {
13563 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
13564 } else {
13565 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
13566 // we also check if an update is available
13567 if (VikChannelManager::checkUpdates()) {
13568 // an update is more important
13569 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
13570 exit;
13571 }
13572 }
13573 // return reminder status
13574 return $expiration_reminder;
13575 }
13576
13577 // front-end section of the site: trigger alert, if any (bool)
13578 return VikChannelManager::checkSubscriptionReminder();
13579
13580 } catch (Exception $e) {
13581 // do nothing
13582 }
13583
13584 return null;
13585 }
13586
13587 /**
13588 * Given a list of website rate plans, returns the sorted list.
13589 * Naming score is the highest sorting method, pricing scoring is also supported.
13590 *
13591 * @param array $rate_plans the list of rate plan arrays to sort.
13592 * @param bool $assoc whether to keep the original array keys.
13593 *
13594 * @return array the sorted list of rate plan arrays.
13595 *
13596 * @since 1.15.0 (J) - 1.5.0 (WP)
13597 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
13598 */
13599 public static function sortRatePlans($rate_plans, $assoc = false)
13600 {
13601 if (!is_array($rate_plans) || count($rate_plans) < 2) {
13602 return $rate_plans;
13603 }
13604
13605 foreach ($rate_plans as $rp_ind => $rate_plan) {
13606 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
13607 // do not proceed in case of a weird array structure
13608 return $rate_plans;
13609 }
13610
13611 // count the sorting-score of this rate plan
13612 $sort_score = 0;
13613 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
13614 // "standard rate" rate plans should go first
13615 $sort_score += 3;
13616 } else {
13617 $sort_score--;
13618 }
13619
13620 // check for derived rate plan
13621 if (!empty($rate_plan['derived_id'])) {
13622 // derived rates should go last
13623 $sort_score -= 2;
13624 } else {
13625 // parent rates should go first
13626 $sort_score += 2;
13627 }
13628
13629 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
13630 // this doesn't seem to be a "non refundable rate" so it should go first
13631 $sort_score += 2;
13632 }
13633
13634 // set sorting score for this rate plan ID
13635 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13636 }
13637
13638 // apply custom sorting and keep index association
13639 uasort($rate_plans, function($a, $b) {
13640 // sort by name-scoring DESC
13641 $sort_diff = $b['sort_score'] - $a['sort_score'];
13642 if (isset($b['cost'])) {
13643 // apply also sorting by price ASC
13644 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13645 }
13646 return $sort_diff;
13647 });
13648
13649 return !$assoc ? array_values($rate_plans) : $rate_plans;
13650 }
13651
13652 /**
13653 * Returns an instance of the Availability helper class.
13654 *
13655 * @param bool $anew True for forcing a new instance.
13656 *
13657 * @return VikBookingAvailability
13658 *
13659 * @since 1.15.0 (J) - 1.5.0 (WP)
13660 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13661 */
13662 public static function getAvailabilityInstance($anew = false)
13663 {
13664 // require library
13665 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13666
13667 // return the instance of the class
13668 return VikBookingAvailability::getInstance($anew);
13669 }
13670
13671 /**
13672 * Helper method to quickly guess the internal dependency to import.
13673 *
13674 * @param string $path_id either a full path to a file or its identifier.
13675 *
13676 * @return bool true if the requested library was imported or false.
13677 *
13678 * @since 1.15.0 (J) - 1.5.0 (WP)
13679 */
13680 public static function import($path_id)
13681 {
13682 if (empty($path_id)) {
13683 return false;
13684 }
13685
13686 // always append .php extension
13687 $path_id = basename($path_id, '.php') . '.php';
13688
13689 if (is_file($path_id)) {
13690 // full path given
13691 require_once $path_id;
13692
13693 return true;
13694 }
13695
13696 // try guessing where the file id is located
13697 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13698 // library was found
13699 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13700
13701 return true;
13702 }
13703
13704 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13705 // library was found
13706 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13707
13708 return true;
13709 }
13710
13711 return false;
13712 }
13713 }
13714
13715 if (!class_exists('VikResizer'))
13716 {
13717 class VikResizer
13718 {
13719 public function __construct()
13720 {
13721 // objects of this class can also be instantiated without calling the methods statically.
13722 }
13723
13724 /**
13725 * Resizes an image proportionally. For PNG files it can optionally
13726 * trim the image to exclude the transparency, and add some padding to it.
13727 * All PNG files keep the alpha background in the resized version.
13728 *
13729 * @param string $fileimg path to original image file
13730 * @param string $dest path to destination image file
13731 * @param int $towidth
13732 * @param int $toheight
13733 * @param bool $trim_png remove empty background from image
13734 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13735 *
13736 * @return boolean
13737 */
13738 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13739 {
13740 if (!is_file($fileimg)) {
13741 return false;
13742 }
13743 if (empty($towidth) && empty($toheight)) {
13744 return copy($fileimg, $dest);
13745 }
13746
13747 $result = true;
13748
13749 list ($owid, $ohei, $type) = getimagesize($fileimg);
13750
13751 if ($owid > $towidth || $ohei > $toheight) {
13752 $xscale = $owid / $towidth;
13753 $yscale = $ohei / $toheight;
13754 if ($yscale > $xscale) {
13755 $new_width = round($owid * (1 / $yscale));
13756 $new_height = round($ohei * (1 / $yscale));
13757 } else {
13758 $new_width = round($owid * (1 / $xscale));
13759 $new_height = round($ohei * (1 / $xscale));
13760 }
13761
13762 $imageresized = imagecreatetruecolor($new_width, $new_height);
13763
13764 switch ($type) {
13765 case '1' :
13766 $imagetmp = imagecreatefromgif ($fileimg);
13767 break;
13768 case '2' :
13769 $imagetmp = imagecreatefromjpeg($fileimg);
13770 break;
13771 case '18' :
13772 $imagetmp = imagecreatefromwebp($fileimg);
13773 break;
13774 default :
13775 //keep alpha for PNG files
13776 $background = imagecolorallocate($imageresized, 0, 0, 0);
13777 imagecolortransparent($imageresized, $background);
13778 imagealphablending($imageresized, false);
13779 imagesavealpha($imageresized, true);
13780 //
13781 $imagetmp = imagecreatefrompng($fileimg);
13782 break;
13783 }
13784
13785 if (!$imagetmp) {
13786 return false;
13787 }
13788
13789 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13790
13791 switch ($type) {
13792 case '1' :
13793 imagegif($imageresized, $dest);
13794 break;
13795 case '2' :
13796 imagejpeg($imageresized, $dest);
13797 break;
13798 case '18' :
13799 imagewebp($imageresized, $dest);
13800 break;
13801 default :
13802 if ($trim_png) {
13803 self::imageTrim($imageresized, $background, $trim_pad);
13804 }
13805 imagepng($imageresized, $dest);
13806 break;
13807 }
13808
13809 imagedestroy($imageresized);
13810 } else {
13811 $result = copy($fileimg, $dest);
13812 }
13813
13814 if (VBOPlatformDetection::isWordPress()) {
13815 /**
13816 * @wponly in order to not lose resized files after installing an update,
13817 * we need to move any uploaded file onto a recovery folder.
13818 */
13819 VikBookingLoader::import('update.manager');
13820 VikBookingUpdateManager::triggerUploadBackup($dest);
13821 }
13822
13823 return $result;
13824 }
13825
13826 /**
13827 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13828 * trim the image to exclude the transparency, and add some padding to it.
13829 * All PNG files keep the alpha background in the resized version.
13830 *
13831 * @param resource $im Image link resource (reference)
13832 * @param int $bg imagecolorallocate color identifier
13833 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13834 *
13835 * @return void
13836 */
13837 public static function imagetrim(&$im, $bg, $pad = null)
13838 {
13839 // Calculate padding for each side.
13840 if (isset($pad)) {
13841 $pp = explode(' ', $pad);
13842 if (isset($pp[3])) {
13843 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13844 } elseif (isset($pp[2])) {
13845 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13846 } elseif (isset($pp[1])) {
13847 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13848 } else {
13849 $p = array_fill(0, 4, (int) $pp[0]);
13850 }
13851 } else {
13852 $p = array_fill(0, 4, 0);
13853 }
13854
13855 // Get the image width and height.
13856 $imw = imagesx($im);
13857 $imh = imagesy($im);
13858
13859 // Set the X variables.
13860 $xmin = $imw;
13861 $xmax = 0;
13862
13863 // Start scanning for the edges.
13864 for ($iy=0; $iy<$imh; $iy++) {
13865 $first = true;
13866 for ($ix=0; $ix<$imw; $ix++) {
13867 $ndx = imagecolorat($im, $ix, $iy);
13868 if ($ndx != $bg) {
13869 if ($xmin > $ix) {
13870 $xmin = $ix;
13871 }
13872 if ($xmax < $ix) {
13873 $xmax = $ix;
13874 }
13875 if (!isset($ymin)) {
13876 $ymin = $iy;
13877 }
13878 $ymax = $iy;
13879 if ($first) {
13880 $ix = $xmax;
13881 $first = false;
13882 }
13883 }
13884 }
13885 }
13886
13887 // The new width and height of the image. (not including padding)
13888 $imw = 1+$xmax-$xmin; // Image width in pixels
13889 $imh = 1+$ymax-$ymin; // Image height in pixels
13890
13891 // Make another image to place the trimmed version in.
13892 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13893
13894 // Make the background of the new image the same as the background of the old one.
13895 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13896 imagefill($im2, 0, 0, $bg2);
13897
13898 // Copy it over to the new image.
13899 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13900
13901 // To finish up, we replace the old image which is referenced.
13902 $im = $im2;
13903 }
13904
13905 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13906 {
13907 if (!file_exists($fileimg)) {
13908 return false;
13909 }
13910 if (empty($towidth) && empty($toheight)) {
13911 copy($fileimg, $dest);
13912 return true;
13913 }
13914
13915 $exp = explode(",", $rgb);
13916 if (count($exp) == 3) {
13917 $r = trim($exp[0]);
13918 $g = trim($exp[1]);
13919 $b = trim($exp[2]);
13920 } else {
13921 $r = 0;
13922 $g = 0;
13923 $b = 0;
13924 }
13925
13926 list ($owid, $ohei, $type) = getimagesize($fileimg);
13927
13928 if ($owid > $towidth || $ohei > $toheight) {
13929 $xscale = $owid / $towidth;
13930 $yscale = $ohei / $toheight;
13931 if ($yscale > $xscale) {
13932 $new_width = round($owid * (1 / $yscale));
13933 $new_height = round($ohei * (1 / $yscale));
13934 $ydest = 0;
13935 $diff = $towidth - $new_width;
13936 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13937 } else {
13938 $new_width = round($owid * (1 / $xscale));
13939 $new_height = round($ohei * (1 / $xscale));
13940 $xdest = 0;
13941 $diff = $toheight - $new_height;
13942 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13943 }
13944
13945 $imageresized = imagecreatetruecolor($towidth, $toheight);
13946
13947 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13948 imagefill($imageresized, 0, 0, $bgColor);
13949
13950 switch ($type) {
13951 case '1' :
13952 $imagetmp = imagecreatefromgif ($fileimg);
13953 break;
13954 case '2' :
13955 $imagetmp = imagecreatefromjpeg($fileimg);
13956 break;
13957 default :
13958 $imagetmp = imagecreatefrompng($fileimg);
13959 break;
13960 }
13961
13962 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13963
13964 switch ($type) {
13965 case '1' :
13966 imagegif ($imageresized, $dest);
13967 break;
13968 case '2' :
13969 imagejpeg($imageresized, $dest);
13970 break;
13971 default :
13972 imagepng($imageresized, $dest);
13973 break;
13974 }
13975
13976 imagedestroy($imageresized);
13977
13978 return true;
13979 } else {
13980 copy($fileimg, $dest);
13981 }
13982 return true;
13983 }
13984
13985 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13986 {
13987 if (!file_exists($fileimg)) {
13988 return false;
13989 }
13990 if (empty($towidth) && empty($toheight)) {
13991 copy($fileimg, $dest);
13992 return true;
13993 }
13994
13995 list ($owid, $ohei, $type) = getimagesize($fileimg);
13996
13997 if ($owid <= $ohei) {
13998 $new_width = $towidth;
13999 $new_height = ($towidth / $owid) * $ohei;
14000 } else {
14001 $new_height = $toheight;
14002 $new_width = ($new_height / $ohei) * $owid;
14003 }
14004
14005 switch ($type) {
14006 case '1' :
14007 $img_src = imagecreatefromgif ($fileimg);
14008 $img_dest = imagecreate($new_width, $new_height);
14009 break;
14010 case '2' :
14011 $img_src = imagecreatefromjpeg($fileimg);
14012 $img_dest = imagecreatetruecolor($new_width, $new_height);
14013 break;
14014 default :
14015 $img_src = imagecreatefrompng($fileimg);
14016 $img_dest = imagecreatetruecolor($new_width, $new_height);
14017 break;
14018 }
14019
14020 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
14021
14022 switch ($type) {
14023 case '1' :
14024 $cropped = imagecreate($towidth, $toheight);
14025 break;
14026 case '2' :
14027 $cropped = imagecreatetruecolor($towidth, $toheight);
14028 break;
14029 default :
14030 $cropped = imagecreatetruecolor($towidth, $toheight);
14031 break;
14032 }
14033
14034 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
14035
14036 switch ($type) {
14037 case '1' :
14038 imagegif ($cropped, $dest);
14039 break;
14040 case '2' :
14041 imagejpeg($cropped, $dest);
14042 break;
14043 default :
14044 imagepng($cropped, $dest);
14045 break;
14046 }
14047
14048 imagedestroy($img_dest);
14049 imagedestroy($cropped);
14050
14051 return true;
14052 }
14053 }
14054 }
14055