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

14,041 lines 467.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 if (!class_exists('VikBookingIcons')) {
14 // require the Icons class
15 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "icons.php");
16 }
17
18 if (!function_exists('showSelectVb')) {
19 function showSelectVb($err, $err_code_info = array()) {
20 include(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'error_form.php');
21 }
22 }
23
24 class VikBooking
25 {
26 public static function addJoomlaUser($name, $username, $email, $password) {
27 //new method
28 jimport('joomla.application.component.helper');
29 $params = JComponentHelper::getParams('com_users');
30 $user = new JUser;
31 $data = array();
32 //Get the default new user group, Registered if not specified.
33 $system = $params->get('new_usertype', 2);
34 $data['groups'] = array();
35 $data['groups'][] = $system;
36 $data['name'] = $name;
37 $data['username'] = $username;
38 $data['email'] = self::getVboApplication()->emailToPunycode($email);
39 $data['password'] = $password;
40 $data['password2'] = $password;
41 $data['sendEmail'] = 0; //should the user receive system mails?
42 //$data['block'] = 0;
43 if (!$user->bind($data)) {
44 VikError::raiseWarning('', JText::translate($user->getError()));
45 return false;
46 }
47 if (!$user->save()) {
48 VikError::raiseWarning('', JText::translate($user->getError()));
49 return false;
50 }
51 return $user->id;
52 }
53
54 public static function userIsLogged() {
55 $user = JFactory::getUser();
56
57 return !$user->guest;
58 }
59
60 public static function prepareViewContent()
61 {
62 /**
63 * @wponly JApplication::getMenu() cannot be adapted to WP
64 */
65 }
66
67 public static function isFontAwesomeEnabled()
68 {
69 return VBOFactory::getConfig()->getBool('usefa', true);
70 }
71
72 public static function loadFontAwesome($force_load = false)
73 {
74 if (!self::isFontAwesomeEnabled() && !$force_load) {
75 return false;
76 }
77
78 /**
79 * We let the class VikBookingIcons load the proper FontAwesome libraries.
80 *
81 * @since 1.11
82 */
83 VikBookingIcons::loadAssets();
84
85 return true;
86 }
87
88 /**
89 * Checks if modifications or cancellations via front-end are allowed.
90 * 0 = everything is Disabled.
91 * 1 = Disabled, with request message (default).
92 * 2 = Modification Enabled, Cancellation Disabled.
93 * 3 = Cancellation Enabled, Modification Disabled.
94 * 4 = everything is Enabled.
95 *
96 * @return int
97 */
98 public static function getReservationModCanc()
99 {
100 return VBOFactory::getConfig()->getInt('resmodcanc', 1);
101 }
102
103 public static function getReservationModCancMin()
104 {
105 return VBOFactory::getConfig()->getInt('resmodcancmin', 1);
106 }
107
108 public static function getDefaultDistinctiveFeatures()
109 {
110 return [
111 'VBODEFAULTDISTFEATUREONE' => '',
112 // Below is the default feature for 'Room Code'. One default feature is sufficient
113 // 'VBODEFAULTDISTFEATURETWO' => '',
114 ];
115 }
116
117 /**
118 * Given the room's parameters and index, tries to take the first distinctive feature.
119 *
120 * @param mixed $rparams string to be decoded, or decoded array/object params
121 * @param int $rindex room index to look for, starting from 1.
122 *
123 * @return mixed false on failure, array otherwise [feature name, feature value]
124 *
125 * @since 1.14 (J) - 1.4.0 (WP)
126 */
127 public static function getRoomUnitDistinctiveFeature($rparams, $rindex) {
128 $rindex = (int)$rindex;
129 if ($rindex < 1 || empty($rparams)) {
130 return false;
131 }
132
133 if (is_string($rparams)) {
134 // decode params
135 $rparams = self::getRoomParam('features', $rparams);
136 }
137
138 if (is_object($rparams)) {
139 // typecast to array
140 $rparams = (array)$rparams;
141 }
142
143 if (!is_array($rparams) || !count($rparams)) {
144 return false;
145 }
146
147 if (isset($rparams['features'])) {
148 $rparams = $rparams['features'];
149 }
150
151 $feature = array();
152 foreach ($rparams as $param_index => $rfeatures) {
153 if ((int)$param_index != $rindex || !is_array($rfeatures) || !count($rfeatures)) {
154 continue;
155 }
156 foreach ($rfeatures as $featname => $featval) {
157 if (empty($featval)) {
158 continue;
159 }
160 // use the first distinctive feature
161 $tn_featname = JText::translate($featname);
162 if ($tn_featname == $featname) {
163 // no translation was applied
164 if (VBOPlatformDetection::isWordPress()) {
165 // try to apply a translation through Gettext even if we have to pass a variable
166 $tn_featname = __($featname);
167 } else {
168 // convert the string to a hypothetical INI constant
169 $ini_constant = str_replace(' ', '_', strtoupper($featname));
170 $tn_featname = JText::translate($ini_constant);
171 $tn_featname = $tn_featname == $ini_constant ? $featname : $tn_featname;
172 }
173 }
174 // store values and break loop
175 $feature = array($tn_featname, $featval);
176 break;
177 }
178 }
179
180 return count($feature) ? $feature : false;
181 }
182
183 /**
184 * Returns an associative list of booking room records and occupied sub-unit indexes.
185 *
186 * @param array $order The current booking record for which the list is needed.
187 * @param int $idroom The booked room ID involved for the associative list.
188 *
189 * @return array
190 *
191 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
192 */
193 public static function getRoomUnitNumsUnavailable(array $order, $idroom)
194 {
195 $dbo = JFactory::getDbo();
196
197 // list of data to build
198 $unavailable_indexes = [];
199
200 $first = $order['checkin'];
201 $second = $order['checkout'];
202 $secdiff = $second - $first;
203 $daysdiff = $secdiff / 86400;
204 if (is_int($daysdiff)) {
205 if ($daysdiff < 1) {
206 $daysdiff = 1;
207 }
208 } else {
209 if ($daysdiff < 1) {
210 $daysdiff = 1;
211 } else {
212 $sum = floor($daysdiff) * 86400;
213 $newdiff = $secdiff - $sum;
214 $maxhmore = self::getHoursMoreRb() * 3600;
215 if ($maxhmore >= $newdiff) {
216 $daysdiff = floor($daysdiff);
217 } else {
218 $daysdiff = ceil($daysdiff);
219 }
220 }
221 }
222
223 // the busy records information pool
224 $busyMap = [];
225
226 // query just the busy records
227 $dbo->setQuery(
228 $dbo->getQuery(true)
229 ->select($dbo->qn([
230 'b.idroom',
231 'b.checkin',
232 'b.checkout',
233 'b.realback',
234 'ob.idorder',
235 ]))
236 ->from($dbo->qn('#__vikbooking_busy', 'b'))
237 ->innerJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('ob.idbusy') . ' = ' . $dbo->qn('b.id'))
238 ->where($dbo->qn('b.idroom') . ' = ' . (int) $idroom)
239 ->where($dbo->qn('b.checkin') . ' < ' . $second)
240 ->where($dbo->qn('b.checkout') . ' > ' . $first)
241 ->where($dbo->qn('ob.idorder') . ' != ' . (int) $order['id'])
242 ->order($dbo->qn('b.id') . ' ASC')
243 );
244 $busyRecords = $dbo->loadAssocList();
245
246 if (!$busyRecords) {
247 // useless to proceed
248 return [];
249 }
250
251 // query bookings and rooms based on the busy records obtained
252 $dbo->setQuery(
253 $dbo->getQuery(true)
254 ->select([
255 $dbo->qn('or.id', 'or_id'),
256 $dbo->qn('or.idorder'),
257 $dbo->qn('or.idroom'),
258 $dbo->qn('or.roomindex'),
259 ])
260 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
261 ->where($dbo->qn('or.idroom') . ' = ' . (int) $idroom)
262 ->where($dbo->qn('or.idorder') . ' IN (' . implode(', ', array_map('intval', array_column($busyRecords, 'idorder'))) . ')')
263 ->order($dbo->qn('or.id') . ' ASC')
264 );
265 $busyRooms = $dbo->loadAssocList();
266
267 // build the final map by merging values
268 foreach ($busyRecords as $busyRecord) {
269 // busy signature
270 $signature = sprintf('%d-%d', $busyRecord['idorder'], $busyRecord['idroom']);
271 // push initial record values
272 $busyMap[$signature][] = $busyRecord;
273 }
274
275 // list of booking-id/booking-room-id signatures
276 $roomSignaturesCounter = [];
277
278 foreach ($busyRooms as $busyRoom) {
279 // busy signature
280 $signature = sprintf('%d-%d', $busyRoom['idorder'], $busyRoom['idroom']);
281
282 // determine booking room signature index
283 $roomSignaturesCounter[$signature] = ($roomSignaturesCounter[$signature] ?? -1) + 1;
284
285 if (!isset($busyMap[$signature][$roomSignaturesCounter[$signature]])) {
286 // unmatched signature
287 continue;
288 }
289
290 // merge booking room values with existing busy record values
291 $busyMap[$signature][$roomSignaturesCounter[$signature]] = array_merge($busyMap[$signature][$roomSignaturesCounter[$signature]], $busyRoom);
292 }
293
294 // simulate a list of associative busy and booking room records
295 $busy = [];
296 foreach ($busyMap as $mapList) {
297 foreach ($mapList as $record) {
298 // push record
299 $busy[] = $record;
300 }
301 }
302
303 if (!$busy) {
304 // useless to proceed
305 return [];
306 }
307
308 // get a list of affected timestamps
309 $groupdays = self::getGroupDays($first, $second, $daysdiff);
310
311 // scan the affected stay timestamps to build the sub-unit matches
312 foreach ($groupdays as $gday) {
313 foreach ($busy as $bu) {
314 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
315 continue;
316 }
317 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
318 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
319 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
320 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
321 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
322 }
323 }
324 }
325 }
326
327 return $unavailable_indexes;
328 }
329
330 /**
331 * Returns an associative list of booking room records and available sub-unit indexes.
332 *
333 * @param array $order The current booking record for which the list is needed.
334 * @param int $idroom The booked room ID involved for the associative list.
335 *
336 * @return array
337 *
338 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
339 */
340 public static function getRoomUnitNumsAvailable(array $order, $idroom)
341 {
342 // build available indexes
343 $available_indexes = [];
344
345 // get the unavailable room indexes
346 $unavailable_indexes = self::getRoomUnitNumsUnavailable($order, $idroom);
347
348 // load room params
349 $room_data = self::getRoomInfo($idroom, ['params'], $no_cache = false);
350 $room_params_arr = (array) json_decode($room_data['params'] ?? '[]', true);
351
352 if (is_array($room_params_arr['features'] ?? null) && $room_params_arr['features']) {
353 foreach ($room_params_arr['features'] as $rind => $rfeatures) {
354 if (in_array($rind, $unavailable_indexes)) {
355 continue;
356 }
357 // push available room index
358 $available_indexes[] = $rind;
359 }
360 }
361
362 // return the available indexes
363 return $available_indexes;
364 }
365
366 /**
367 * Generates a random UUID v4.
368 *
369 * A UUID is a 16-octet (128-bit) number. In its canonical form, a UUID is represented by 32
370 * hexadecimal digits, displayed in five groups separated by hyphens, in the form 8-4-4-4-12
371 * for a total of 36 characters (32 alphanumeric characters and four hyphens).
372 *
373 * @return string
374 *
375 * @since 1.18.8 (J) - 1.8.8 (WP)
376 */
377 public static function uuid()
378 {
379 /*
380 return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
381 // 32 bits for "time_low"
382 mt_rand(0, 0xffff), mt_rand(0, 0xffff),
383
384 // 16 bits for "time_mid"
385 mt_rand(0, 0xffff),
386
387 // 16 bits for "time_hi_and_version",
388 // four most significant bits holds version number 4
389 mt_rand(0, 0x0fff) | 0x4000,
390
391 // 16 bits, 8 bits for "clk_seq_hi_res",
392 // 8 bits for "clk_seq_low",
393 // two most significant bits holds zero and one for variant DCE1.1
394 mt_rand(0, 0x3fff) | 0x8000,
395
396 // 48 bits for "node"
397 mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
398 );
399 */
400
401 // generate 16 random bytes through CSPRNG
402 $data = random_bytes(16);
403
404 // four most significant bits holds version number 4
405 $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
406 // two most significant bits holds zero and one for variant DCE1.1
407 $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
408
409 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
410 }
411
412 /**
413 * Load the restrictions applying the given filters to the passed rooms.
414 * The ordering of the query SHOULD remain unchanged, because it's required
415 * to have it by ascending order of the ID. So the older records first.
416 * Even when no filters, we always exclude expired restrictions.
417 *
418 * @param boolean $filters whether to apply filters
419 * @param array $rooms the list of rooms to filter
420 *
421 * @return array the list of restrictions found or an empty array.
422 */
423 public static function loadRestrictions($filters = true, $rooms = [])
424 {
425 $dbo = JFactory::getDbo();
426
427 $restrictions = [];
428 $limts = strtotime(date('Y-m-d'));
429
430 if (!$filters) {
431 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
432 } else {
433 if (!$rooms) {
434 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
435 } else {
436 $clause = [];
437 foreach ($rooms as $idr) {
438 if (empty($idr)) {
439 continue;
440 }
441 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
442 }
443 if ($clause) {
444 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE (`dto` = 0 OR `dto` >= " . $limts . ") AND (`allrooms` = 1 OR (`allrooms` = 0 AND (" . implode(" OR ", $clause) . "))) ORDER BY `id` ASC;";
445 } else {
446 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
447 }
448 }
449 }
450
451 $dbo->setQuery($q);
452 $allrestrictions = $dbo->loadAssocList();
453
454 foreach ($allrestrictions as $k => $res) {
455 if (!empty($res['month'])) {
456 $restrictions[$res['month']] = $res;
457 } else {
458 if (!isset($restrictions['range'])) {
459 $restrictions['range'] = array();
460 }
461 $restrictions['range'][$k] = $res;
462 }
463 }
464
465 return $restrictions;
466 }
467
468 /**
469 * Given a list of restriction records split into range or months, filters
470 * the list by including the "global" restrictions applied to all rooms.
471 *
472 * @param array $restrictions List of restriction records loaded.
473 *
474 * @return array Associative list of filtered restriction records.
475 */
476 public static function globalRestrictions(array $restrictions)
477 {
478 $list = [];
479
480 // iterate all records
481 foreach ($restrictions as $kr => $rr) {
482 if ($kr == 'range') {
483 // scan all date-range restriction records
484 foreach ($rr as $kd => $dr) {
485 if ($dr['allrooms'] == 1) {
486 $list['range'][$kd] = $restrictions[$kr][$kd];
487 }
488 }
489 // go next
490 continue;
491 }
492
493 // month restriction record
494 if ($rr['allrooms'] == 1) {
495 $list[$kr] = $restrictions[$kr];
496 }
497 }
498
499 return $list;
500 }
501
502 /**
503 * Given a list of restriction records split into range or months, filters the list
504 * by including the "listing-level" or "global" restrictions affecting to given rooms.
505 *
506 * @param array $restrictions List of restriction records loaded.
507 * @param array $room_ids List of involved and eligible room IDs.
508 *
509 * @return array Associative list of filtered restriction records.
510 *
511 * @since 1.18.2 (J) - 1.8.2 (WP)
512 */
513 public static function listingRestrictions(array $restrictions, array $room_ids)
514 {
515 $list = [];
516
517 // iterate all records
518 foreach ($restrictions as $kr => $rr) {
519 if ($kr == 'range') {
520 // scan all date-range restriction records
521 foreach ($rr as $kd => $dr) {
522 if ($dr['allrooms'] == 1) {
523 // push global restriction
524 $list['range'][$kd] = $restrictions[$kr][$kd];
525 } elseif (!empty($dr['idrooms'])) {
526 // scan the list of room IDs to match against
527 foreach ($room_ids as $room_id) {
528 if (strpos($dr['idrooms'], '-' . $room_id . '-') !== false) {
529 // push listing-level restriction
530 $list['range'][$kd] = $restrictions[$kr][$kd];
531 break;
532 }
533 }
534 }
535 }
536 // go next
537 continue;
538 }
539
540 // month restriction record
541 if ($rr['allrooms'] == 1) {
542 // push global restriction
543 $list[$kr] = $restrictions[$kr];
544 } elseif (!empty($rr['idrooms'])) {
545 // scan the list of room IDs to match against
546 foreach ($room_ids as $room_id) {
547 if (strpos($rr['idrooms'], '-' . $room_id . '-') !== false) {
548 // push listing-level restriction
549 $list[$kr] = $restrictions[$kr];
550 break;
551 }
552 }
553 }
554 }
555
556 return $list;
557 }
558
559 /**
560 * From the given restrictions, check-in, check-out and nights, looks for
561 * a restriction to be returned and applied over these stays.
562 * In order to give priority to newer Restriction IDs, the order of the
563 * $restrictions array should be ascending, so that the newer restrictions
564 * will overwrite the array with the last ID (more recent). Only for date ranges.
565 * Returns an array with the record of the restriction found. The loop is not
566 * broken when the first valid restriction is found to give priority to
567 * newer restriction IDs to overwrite older records.
568 *
569 * @param int $first the unix timestamp for the check-in date
570 * @param int $second the unix timestamp for the check-out date
571 * @param int $daysdiff the number of nights of stay
572 * @param array $restrictions the list of restrictions loaded
573 *
574 * @return array the restriction found, or an empty array.
575 */
576 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
577 $season_restrictions = array();
578 $restrcheckin = getdate($first);
579 $restrcheckout = getdate($second);
580 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
581 //restriction found for this month, checking:
582 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
583 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
584 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
585 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
586 //Week Day Arrival Restriction
587 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
588 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
589 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
590 }
591 $season_restrictions['wdays'] = $rvalidwdays;
592 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
593 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
594 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
595 }
596 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
597 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
598 }
599 }
600 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
601 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
602 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
603 $season_restrictions['allowed'] = false;
604 }
605 }
606 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
607 $season_restrictions['allowed'] = false;
608 }
609 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
610 } elseif (array_key_exists('range', $restrictions)) {
611 foreach ($restrictions['range'] as $restr) {
612 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
613 //restriction found for this date range, checking:
614 $season_restrictions['id'] = $restr['id'];
615 $season_restrictions['name'] = $restr['name'];
616 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
617 if (strlen((string)$restr['wday']) > 0) {
618 //Week Day Arrival Restriction
619 $rvalidwdays = array($restr['wday']);
620 if (strlen((string)$restr['wdaytwo']) > 0) {
621 $rvalidwdays[] = $restr['wdaytwo'];
622 }
623 $season_restrictions['wdays'] = $rvalidwdays;
624 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
625 if (!empty($restr['ctad'])) {
626 $season_restrictions['cta'] = explode(',', $restr['ctad']);
627 }
628 if (!empty($restr['ctdd'])) {
629 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
630 }
631 }
632 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
633 $season_restrictions['maxlos'] = $restr['maxlos'];
634 if ($daysdiff > $restr['maxlos']) {
635 $season_restrictions['allowed'] = false;
636 }
637 }
638 if ($daysdiff < $restr['minlos']) {
639 $season_restrictions['allowed'] = false;
640 }
641 $season_restrictions['minlos'] = $restr['minlos'];
642 }
643 }
644 }
645
646 return $season_restrictions;
647 }
648
649 public static function compareSeasonRestrictionsNights($restrictions)
650 {
651 $base_compare = array();
652 $base_nights = 0;
653 foreach ($restrictions as $nights => $restr) {
654 $base_compare = $restr;
655 $base_nights = $nights;
656 break;
657 }
658
659 /**
660 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
661 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
662 *
663 * @since 1.15.0 (J) - 1.5.0 (WP)
664 */
665 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
666
667 foreach ($restrictions as $nights => $restr) {
668 if ($nights == $base_nights) {
669 continue;
670 }
671
672 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
673 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
674
675 // get associative array of differences
676 $diff = array_diff($use_base_compare, $use_restr);
677
678 if (count($diff) > 0 && array_key_exists('id', $diff)) {
679 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
680 // only one Restriction per time should be applied to certain Season Dates but check just in case.
681 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
682 }
683 }
684
685 return array();
686 }
687
688 /**
689 * Methods using array_diff() may need one-dimension arrays, because this
690 * native functions applies a string comparison, and so casting an array
691 * value to a string would generate a Notice message. This method simply
692 * converts any sub-array into a restorable string into a non-scalar var.
693 * This only works when comparing 2 array variables.
694 *
695 * @param array $arr the hypothetical multi dimension array.
696 *
697 * @return array the one dimension array to be passed to array_diff()
698 * and the list of keys converted to strings for comparison.
699 *
700 * @since 1.15.0 (J) - 1.5.0 (WP)
701 */
702 public static function prepareMultiDimArrayDiff($arr)
703 {
704 $casted_keys = array();
705
706 if (!is_array($arr) || !count($arr)) {
707 return array($casted_keys, $arr);
708 }
709
710 foreach ($arr as $key => $val) {
711 if (!is_scalar($val)) {
712 // memorize original value
713 $casted_keys[$key] = $val;
714 // cast to string must be applied
715 $arr[$key] = json_encode($val);
716 }
717 }
718
719 return array($casted_keys, $arr);
720 }
721
722 /**
723 * Methods using array_diff() may need one-dimension arrays, because this
724 * native functions applies a string comparison, and so casting an array
725 * value to a string would generate a Notice message. This method restores
726 * the original values of the "prepared" arrays by getting the list of the
727 * modified keys from non-scalar to scalar values.
728 * This only works when comparing 2 array variables.
729 *
730 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
731 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
732 * @param array $arr the array result of array_diff().
733 *
734 * @return array the array result of array_diff() with its original values.
735 *
736 * @since 1.15.0 (J) - 1.5.0 (WP)
737 */
738 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
739 {
740 if (!is_array($arr) || !count($arr)) {
741 return $arr;
742 }
743
744 foreach ($arr as $key => $val) {
745 // keys returned by array_diff can be present in both $keys_one and $keys_two
746 if (isset($keys_one[$key])) {
747 // always give higher priority to the 1st argument keys for array_diff()
748 $arr[$key] = json_decode($keys_one[$key]);
749 } elseif (isset($keys_two[$key])) {
750 // fallback to 2nd argument keys
751 $arr[$key] = json_decode($keys_two[$key]);
752 }
753 }
754
755 return $arr;
756 }
757
758 public static function roomRestrictions($roomid, $restrictions) {
759 $ret = array();
760 if (!empty($roomid) && count($restrictions) > 0) {
761 foreach ($restrictions as $kr => $rr) {
762 if ($kr == 'range') {
763 foreach ($rr as $kd => $dr) {
764 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
765 $allrooms = explode(';', $dr['idrooms']);
766 if (in_array('-'.$roomid.'-', $allrooms)) {
767 $ret['range'][$kd] = $restrictions[$kr][$kd];
768 }
769 }
770 }
771 } else {
772 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
773 $allrooms = explode(';', $rr['idrooms']);
774 if (in_array('-'.$roomid.'-', $allrooms)) {
775 $ret[$kr] = $restrictions[$kr];
776 }
777 }
778 }
779 }
780 }
781 return $ret;
782 }
783
784 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
785 {
786 // default states
787 $restrictionerrmsg = '';
788 $restrictions_affcount = 0;
789 $minlos_errors_pool = [];
790
791 // check for month-level or range-level restrictions
792 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
793 //restriction found for this month, checking:
794 $restrictions_affcount++;
795 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
796 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
797 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
798 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
799 }
800 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
801 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVAL', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wday']).(strlen($roomrestr[$restrcheckin['mon']]['wdaytwo']) > 0 ? '/'.self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wdaytwo']) : ''));
802 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
803 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
804 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
805 }
806 }
807 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
808 if (count($comborestr) > 0) {
809 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
810 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
811 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBO', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
812 }
813 }
814 }
815 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
816 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
817 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
818 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
819 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
820 }
821 }
822 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
823 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
824 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
825 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
826 }
827 }
828 }
829 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
830 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
831 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
832 }
833 }
834 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
835 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
836 }
837 } elseif (array_key_exists('range', $roomrestr)) {
838 // start valid flag
839 $restrictionsvalid = true;
840
841 /**
842 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
843 * The most recent restrictions will have a higher priority over the oldest ones.
844 *
845 * @since 1.12.1
846 */
847 $minlos_priority = [
848 'ok' => [],
849 'nok' => [],
850 ];
851
852 /**
853 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
854 *
855 * @since 1.16.10 (J) - 1.6.10 (WP)
856 */
857 $ctad_priority = [
858 'ok' => [],
859 'nok' => [],
860 ];
861
862 foreach ($roomrestr['range'] as $restr) {
863 /**
864 * We should not always add 82799 seconds to the end date of the restriction
865 * because if they only last for one day (like a Saturday), then $restr['dto']
866 * will be already set to the time 23:59:59.
867 *
868 * @since 1.13 (J) - 1.2.18 (WP)
869 * @since 1.18.3 (J) - 1.8.3 (WP) timestamps ending with 9 for a range greater than 1 day indicate that the restriction end time
870 * is already set to 23:59:59 (VCM Connector may be responsible), so no seconds should be added or the restriction end date
871 * will be the day after, and if after the check-in time, it may be included.
872 */
873 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) && substr((string) $restr['dto'], -1, 1) != 9 ? 82799 : 0;
874
875 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
876 // restriction found for this date range based on arrival date, check if compliant
877 $restrictions_affcount++;
878
879 // set flag for CTA/CTD compliance
880 $cta_ctd_passed = true;
881
882 if (strlen((string)$restr['wday']) > 0) {
883 $rvalidwdays = array($restr['wday']);
884 if (strlen((string)$restr['wdaytwo']) > 0) {
885 $rvalidwdays[] = $restr['wdaytwo'];
886 }
887 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
888 $restrictionsvalid = false;
889 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
890 } elseif ($restr['multiplyminlos'] == 1) {
891 if (($daysdiff % $restr['minlos']) != 0) {
892 $restrictionsvalid = false;
893 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
894 }
895 }
896 $comborestr = self::parseJsDrangeWdayCombo($restr);
897 if ($comborestr) {
898 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
899 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
900 $restrictionsvalid = false;
901 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBORANGE', self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
902 }
903 }
904 }
905 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
906 if (!empty($restr['ctad'])) {
907 $ctarestrictions = explode(',', $restr['ctad']);
908 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
909 $restrictionsvalid = false;
910 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
911 $cta_ctd_passed = false;
912 }
913 }
914 if (!empty($restr['ctdd'])) {
915 $ctdrestrictions = explode(',', $restr['ctdd']);
916 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
917 $restrictionsvalid = false;
918 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
919 $cta_ctd_passed = false;
920 }
921 }
922 }
923
924 // check CTA/CTD compliance
925 if (!$cta_ctd_passed) {
926 array_push($ctad_priority['nok'], (int)$restr['id']);
927 } else {
928 array_push($ctad_priority['ok'], (int)$restr['id']);
929 }
930
931 // max LOS validation
932 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
933 if ($daysdiff > $restr['maxlos']) {
934 $restrictionsvalid = false;
935 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
936 }
937 }
938
939 // min LOS validation
940 if ($daysdiff < $restr['minlos']) {
941 $restrictionsvalid = false;
942 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
943 // push error value
944 array_push($minlos_priority['nok'], (int) $restr['id']);
945 // set error message with related minimum stay
946 $minlos_errors_pool[$restr['minlos']] = $restrictionerrmsg;
947 } else {
948 array_push($minlos_priority['ok'], (int) $restr['id']);
949 }
950 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
951 /**
952 * We validate the CTD restrictions depending on the check-out date.
953 *
954 * @since 1.16.3 (J) - 1.6.3 (WP)
955 */
956 $ctdrestrictions = explode(',', $restr['ctdd']);
957 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
958 $restrictions_affcount++;
959 $restrictionsvalid = false;
960 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
961 }
962 }
963 }
964
965 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
966 // a more recent restriction is allowing this MinLOS
967 // ensure there are no recent CTA/CTD rules not compliant
968 $cta_ctd_priority_ok = true;
969
970 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
971 // a more recent restriction is not compliant with the CTA/CTD rules
972 $cta_ctd_priority_ok = false;
973 }
974
975 if ($cta_ctd_priority_ok) {
976 // we unset the error message because more recent restriction(s) are allowing this stay dates
977 $restrictionerrmsg = '';
978 }
979 }
980 }
981
982 // check global restriction of Min LOS for TAC functions in VBO and VCM
983 if (empty($restrictionerrmsg) && $roomrestr && $restrictions_affcount <= 0) {
984 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
985 $globminlos = self::getDefaultNightsCalendar();
986 if ($globminlos > 1 && $daysdiff < $globminlos) {
987 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
988 }
989 }
990
991 /**
992 * When working with room-level restrictions and different minLOS across multiple apartments,
993 * we need the display the actually lowest minimum stay among all listings from the errors found.
994 *
995 * @since 1.17.3 (J) - 1.7.3 (WP)
996 */
997 if ($restrictionerrmsg && in_array($restrictionerrmsg, $minlos_errors_pool) && count($minlos_errors_pool) > 1) {
998 // make sure to return the error message for the lowest minimum stay
999 $lowest_minlos = min(array_map('intval', array_keys($minlos_errors_pool)));
1000 $restrictionerrmsg = $minlos_errors_pool[$lowest_minlos] ?? $restrictionerrmsg;
1001 }
1002
1003 // return the restriction error message string, if anything wrong was found
1004 return $restrictionerrmsg;
1005 }
1006
1007 public static function parseJsDrangeWdayCombo($drestr) {
1008 $combo = array();
1009 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
1010 $cparts = explode(':', $drestr['wdaycombo']);
1011 foreach ($cparts as $kc => $cw) {
1012 if (!empty($cw)) {
1013 $nowcombo = explode('-', $cw);
1014 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
1015 }
1016 }
1017 }
1018 return $combo;
1019 }
1020
1021 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
1022 $dbo = JFactory::getDbo();
1023 $pkg = array();
1024 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1025 $dbo->setQuery($q);
1026 $dbo->execute();
1027 if ($dbo->getNumRows() == 1) {
1028 $pkg = $dbo->loadAssoc();
1029 $vbo_tn = self::getTranslator();
1030 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
1031 } else {
1032 return JText::translate('VBOPKGERRNOTFOUND');
1033 }
1034 $rooms_req = array();
1035 foreach ($rooms as $num => $room) {
1036 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
1037 $rooms_req[] = $room['id'];
1038 }
1039 }
1040 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
1041 $dbo->setQuery($q);
1042 $dbo->execute();
1043 if ($dbo->getNumRows() != count($rooms_req)) {
1044 //error, not all the rooms requested are available for this package
1045 return JText::translate('VBOPKGERRNOTROOM');
1046 }
1047 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
1048 return JText::translate('VBOPKGERRNUMNIGHTS');
1049 }
1050 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
1051 return JText::translate('VBOPKGERRCHECKIND');
1052 }
1053 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
1054 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
1055 return JText::translate('VBOPKGERRCHECKOUTD');
1056 }
1057 if (!empty($pkg['excldates'])) {
1058 //this would check if any stay date is excluded
1059 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
1060 //check just the arrival and departure dates
1061 $bookdates_ts = array($checkints, $checkoutts);
1062 $bookdates = array();
1063 foreach ($bookdates_ts as $bookdate_ts) {
1064 $info_d = getdate($bookdate_ts);
1065 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
1066 }
1067 $edates = explode(';', $pkg['excldates']);
1068 foreach ($edates as $edate) {
1069 if (!empty($edate) && in_array($edate, $bookdates)) {
1070 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
1071 }
1072 }
1073 }
1074 return $pkg;
1075 }
1076
1077 public static function getPackage($pkg_id) {
1078 $dbo = JFactory::getDbo();
1079 $pkg = array();
1080 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1081 $dbo->setQuery($q);
1082 $dbo->execute();
1083 if ($dbo->getNumRows() == 1) {
1084 $pkg = $dbo->loadAssoc();
1085 }
1086 return $pkg;
1087 }
1088
1089 /**
1090 * Returns the requested room parameter or default value.
1091 *
1092 * @param string $paramname The parameter name.
1093 * @param mixed $params The room params string, or json-decoded array/object.
1094 * @param mixed $def The default value to return as fallback.
1095 *
1096 * @return mixed
1097 *
1098 * @since 1.17.3 (J) - 1.7.3 (WP) added 3rd argument $def.
1099 */
1100 public static function getRoomParam($paramname, $params, $def = '')
1101 {
1102 if (empty($params)) {
1103 return $def;
1104 }
1105
1106 if (is_string($params)) {
1107 $params = (array) json_decode($params, true);
1108 } elseif (is_object($params)) {
1109 $params = (array) $params;
1110 }
1111
1112 if (!is_array($params)) {
1113 return $def;
1114 }
1115
1116 return $params[$paramname] ?? $def;
1117 }
1118
1119 public static function filterNightsSeasonsCal($arr_nights) {
1120 $nights = array();
1121 foreach ($arr_nights as $night) {
1122 if (intval(trim($night)) > 0) {
1123 $nights[] = intval(trim($night));
1124 }
1125 }
1126 sort($nights);
1127 return array_unique($nights);
1128 }
1129
1130 public static function getSeasonRangeTs($from, $to, $year) {
1131 $sfrom = 0;
1132 $sto = 0;
1133 $tsbase = mktime(0, 0, 0, 1, 1, $year);
1134 $curyear = $year;
1135 $tsbasetwo = $tsbase;
1136 $curyeartwo = $year;
1137 if ($from > $to) {
1138 //between two years
1139 $curyeartwo += 1;
1140 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
1141 }
1142 $sfrom = ($tsbase + $from);
1143 $sto = ($tsbasetwo + $to);
1144 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
1145 //leap years
1146 $infoseason = getdate($sfrom);
1147 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1148 if ($infoseason[0] > $leapts) {
1149 /**
1150 * Timestamp must be greater than the leap-day of Feb 29th.
1151 * It used to be checked for >= $leapts.
1152 *
1153 * @since July 3rd 2019
1154 */
1155 $sfrom += 86400;
1156 if ($curyear == $curyeartwo) {
1157 $sto += 86400;
1158 }
1159 }
1160 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
1161 //leap years
1162 $infoseason = getdate($sto);
1163 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1164 if ($infoseason[0] > $leapts) {
1165 /**
1166 * Timestamp must be greater than the leap-day of Feb 29th.
1167 * It used to be checked for >= $leapts.
1168 *
1169 * @since July 3rd 2019
1170 */
1171 $sto += 86400;
1172 }
1173 }
1174 return array($sfrom, $sto);
1175 }
1176
1177 public static function sortSeasonsRangeTs ($all_seasons) {
1178 $sorted = array();
1179 $map = array();
1180 foreach ($all_seasons as $key => $season) {
1181 $map[$key] = $season['from_ts'];
1182 }
1183 asort($map);
1184 foreach ($map as $key => $s) {
1185 $sorted[] = $all_seasons[$key];
1186 }
1187 return $sorted;
1188 }
1189
1190 public static function formatSeasonDates ($from_ts, $to_ts) {
1191 $one = getdate($from_ts);
1192 $two = getdate($to_ts);
1193 $months_map = array(
1194 1 => JText::translate('VBSHORTMONTHONE'),
1195 2 => JText::translate('VBSHORTMONTHTWO'),
1196 3 => JText::translate('VBSHORTMONTHTHREE'),
1197 4 => JText::translate('VBSHORTMONTHFOUR'),
1198 5 => JText::translate('VBSHORTMONTHFIVE'),
1199 6 => JText::translate('VBSHORTMONTHSIX'),
1200 7 => JText::translate('VBSHORTMONTHSEVEN'),
1201 8 => JText::translate('VBSHORTMONTHEIGHT'),
1202 9 => JText::translate('VBSHORTMONTHNINE'),
1203 10 => JText::translate('VBSHORTMONTHTEN'),
1204 11 => JText::translate('VBSHORTMONTHELEVEN'),
1205 12 => JText::translate('VBSHORTMONTHTWELVE')
1206 );
1207 $mday_map = array(
1208 1 => JText::translate('VBMDAYFRIST'),
1209 2 => JText::translate('VBMDAYSECOND'),
1210 3 => JText::translate('VBMDAYTHIRD'),
1211 'generic' => JText::translate('VBMDAYNUMGEN')
1212 );
1213 if ($one['year'] == $two['year']) {
1214 return $one['year'].' '.$months_map[(int)$one['mon']].' '.$one['mday'].'<sup>'.(array_key_exists((int)substr($one['mday'], -1), $mday_map) && ($one['mday'] < 10 || $one['mday'] > 20) ? $mday_map[(int)substr($one['mday'], -1)] : $mday_map['generic']).'</sup> - '.$months_map[(int)$two['mon']].' '.$two['mday'].'<sup>'.(array_key_exists((int)substr($two['mday'], -1), $mday_map) && ($two['mday'] < 10 || $two['mday'] > 20) ? $mday_map[(int)substr($two['mday'], -1)] : $mday_map['generic']).'</sup>';
1215 }
1216 return $months_map[(int)$one['mon']].' '.$one['mday'].'<sup>'.(array_key_exists((int)substr($one['mday'], -1), $mday_map) && ($one['mday'] < 10 || $one['mday'] > 20) ? $mday_map[(int)substr($one['mday'], -1)] : $mday_map['generic']).'</sup> '.$one['year'].' - '.$months_map[(int)$two['mon']].' '.$two['mday'].'<sup>'.(array_key_exists((int)substr($two['mday'], -1), $mday_map) && ($two['mday'] < 10 || $two['mday'] > 20) ? $mday_map[(int)substr($two['mday'], -1)] : $mday_map['generic']).'</sup> '.$two['year'];
1217 }
1218
1219 public static function getFirstCustDataField($custdata) {
1220 $first_field = '';
1221 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1222 //Room is closed with this booking
1223 return '----';
1224 }
1225 $parts = explode("\n", $custdata);
1226 foreach ($parts as $part) {
1227 if (!empty($part)) {
1228 $field = explode(':', trim($part));
1229 if (!empty($field[1])) {
1230 return trim($field[1]);
1231 }
1232 }
1233 }
1234 return $first_field;
1235 }
1236
1237 /**
1238 * This method composes a string to be logged for the admin
1239 * to keep track of what was inside the booking before the
1240 * modification. Returns a string and it uses language definitions
1241 * that should be available on the front-end and back-end INI files.
1242 *
1243 * @param array $old_booking the array of the booking prior to the modification
1244 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1245 *
1246 * @return string text describing the situation with the booking before the changes.
1247 *
1248 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1249 */
1250 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1251 {
1252 $vbo_df = self::getDateFormat();
1253 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1254 $wdays_map = array(
1255 JText::translate('VBWEEKDAYZERO'),
1256 JText::translate('VBWEEKDAYONE'),
1257 JText::translate('VBWEEKDAYTWO'),
1258 JText::translate('VBWEEKDAYTHREE'),
1259 JText::translate('VBWEEKDAYFOUR'),
1260 JText::translate('VBWEEKDAYFIVE'),
1261 JText::translate('VBWEEKDAYSIX'),
1262 );
1263 $now_info = getdate();
1264 $checkin_info = getdate($old_booking['checkin']);
1265 $checkout_info = getdate($old_booking['checkout']);
1266
1267 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1268 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1269 $prev_dates = $prev_nights.' - '.$wdays_map[$checkin_info['wday']].', '.date($df.' H:i', $checkin_info[0]).' - '.$wdays_map[$checkout_info['wday']].', '.date($df.' H:i', $checkout_info[0]);
1270
1271 $orooms_map = [];
1272 $orooms_arr = [];
1273
1274 // scan the previously booked room records
1275 foreach ((array) ($old_booking['rooms_info'] ?? []) as $oroom) {
1276 // determine room name
1277 $roomName = $oroom['name'] ?? $oroom['roomnamevb'] ?? $oroom['idroom'] ?? '---';
1278 $orooms_arr[] = sprintf(
1279 '%s, %s: %d, %s: %d',
1280 $roomName,
1281 JText::translate('VBMAILADULTS'),
1282 (int) ($oroom['adults'] ?? 0),
1283 JText::translate('VBMAILCHILDREN'),
1284 (int) ($oroom['children'] ?? 0),
1285 );
1286 if (!empty($oroom['idroom'])) {
1287 $orooms_map[$oroom['idroom']] = $roomName;
1288 }
1289 }
1290
1291 // build previous rooms booked string
1292 $prev_rooms = $orooms_arr ? implode("\n", $orooms_arr) : '';
1293
1294 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && $orooms_map) {
1295 $split_stay_prev_infos = [];
1296 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1297 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1298 continue;
1299 }
1300 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1301 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1302 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1303
1304 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1305 if (!empty($room_nights)) {
1306 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1307 }
1308 $split_stay_descr .= ', ';
1309 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1310
1311 // push split stay description string
1312 $split_stay_prev_infos[] = $split_stay_descr;
1313 }
1314 if (count($split_stay_prev_infos)) {
1315 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1316 }
1317 }
1318
1319 $currencyname = self::getCurrencyName();
1320 $prev_total = self::formatCurrencyNumber(self::numberFormat($old_booking['total']), $currencyname);
1321
1322 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1323 }
1324
1325 /**
1326 * This method invokes the class
1327 * VikChannelManagerLogos (new in VCM 1.6.4) to
1328 * map the name of a channel to its corresponding logo.
1329 * The method can also be used to get an istance of the class.
1330 *
1331 * @param mixed $provenience either a string or an array with main and sub channels.
1332 * @param boolean $get_istance
1333 *
1334 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1335 */
1336 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1337 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1338 return false;
1339 }
1340 if (!class_exists('VikChannelManagerLogos')) {
1341 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1342 }
1343
1344 /**
1345 * Due to the new iCal channel we now support main and sub channels.
1346 *
1347 * @since 1.13 - VCM 1.7.0
1348 */
1349 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1350 $provenience = explode('_', $provenience);
1351 }
1352 $main_channel = '';
1353 $full_channel = '';
1354 if (is_array($provenience)) {
1355 if (count($provenience) > 1) {
1356 $main_channel = $provenience[1];
1357 } else {
1358 $main_channel = $provenience[0];
1359 }
1360 if (stripos($provenience[0], 'ical') !== false) {
1361 $full_channel = implode('_', $provenience);
1362 }
1363 } else {
1364 $main_channel = $provenience;
1365 }
1366
1367 // get object instance by passing the main provenience
1368 $obj = new VikChannelManagerLogos($main_channel);
1369
1370 // update provenience with main and full channel
1371 if (!empty($full_channel)) {
1372 $obj->setProvenience($main_channel, $full_channel);
1373 }
1374
1375 // return either the instance or the logo URL for this channel source
1376 return $get_istance ? $obj : $obj->getLogoURL();
1377 }
1378
1379 public static function vcmAutoUpdate()
1380 {
1381 static $autoUpdate = null;
1382
1383 if ($autoUpdate !== null) {
1384 return $autoUpdate;
1385 }
1386
1387 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1388 $autoUpdate = -1;
1389 return $autoUpdate;
1390 }
1391
1392 $autoUpdate = (int) VBOFactory::getConfig()->getBool('vcmautoupd', false);
1393
1394 return $autoUpdate;
1395 }
1396
1397 public static function getVcmInvoker()
1398 {
1399 if (!class_exists('VboVcmInvoker')) {
1400 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1401 }
1402
1403 return new VboVcmInvoker();
1404 }
1405
1406 /**
1407 * Returns an instance of VikBooking History object.
1408 *
1409 * @param int $bid optional booking ID to bind.
1410 *
1411 * @return VboBookingHistory
1412 *
1413 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1414 */
1415 public static function getBookingHistoryInstance($bid = 0)
1416 {
1417 if (!class_exists('VboBookingHistory')) {
1418 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1419 }
1420
1421 return new VboBookingHistory($bid);
1422 }
1423
1424 /**
1425 * Returns an instance of the VCMChatHandler class to handle
1426 * the messaging/chat for the given reservation ID.
1427 *
1428 * @param int $oid the ID of the booking in VBO
1429 * @param string $channel the name of the source channel
1430 *
1431 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1432 *
1433 * @since 1.11.2 (J) - 1.1.2 (WP)
1434 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1435 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1436 * @since 1.17.6 (J) - 1.7.6 (WP) empty signature arguments will only require the dependencies.
1437 */
1438 public static function getVcmChatInstance($oid, $channel = null)
1439 {
1440 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1441 if (!is_file($vcm_messaging_helper)) {
1442 // VCM is not available
1443 return null;
1444 }
1445
1446 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1447 // VCM must be inactive, prevent the loading of the instance
1448 return null;
1449 }
1450
1451 // make sure the channel name is correct as it may contain the sub-network
1452 if (!empty($channel)) {
1453 $segments = explode('_', $channel);
1454 if (count($segments) > 1) {
1455 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1456 $channel = $segments[0];
1457 }
1458 }
1459
1460 // always require main file of the abstract class even if arguments are empty/invalid
1461 require_once $vcm_messaging_helper;
1462
1463 if (empty($oid) && empty($channel)) {
1464 // do not proceed in order to save resources
1465 return null;
1466 }
1467
1468 // return the instance of the class for this channel handler
1469 return VCMChatHandler::getInstance($oid, $channel);
1470 }
1471
1472 /**
1473 * Returns an instance of the VCMOpportunityHandler class to handle
1474 * the various opportunities through VCM.
1475 *
1476 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1477 *
1478 * @since 1.2.0
1479 */
1480 public static function getVcmOpportunityInstance() {
1481 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1482 if (!is_file($vcm_opp_helper)) {
1483 // VCM is not available
1484 return null;
1485 }
1486 // require main file of the class
1487 require_once $vcm_opp_helper;
1488 // return the instance of the class
1489 return VCMOpportunityHandler::getInstance();
1490 }
1491
1492 /**
1493 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1494 *
1495 * @param array $data optional data to bind to the object.
1496 *
1497 * @return bool
1498 *
1499 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1500 *
1501 * @requires VCM >= 1.8.24
1502 */
1503 public static function vcmBcomReportingSupported(array $data = [])
1504 {
1505 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1506 }
1507
1508 /**
1509 * Gets a list of all channels supporting the promotions.
1510 *
1511 * @param string $key the key of the handler.
1512 *
1513 * @return mixed false if VCM is not installed, empty array otherwise.
1514 *
1515 * @requires Vik Channel Manager 1.7.1
1516 *
1517 * @since 1.3.0
1518 */
1519 public static function getPromotionHandlers($key = null)
1520 {
1521 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1522 // VCM not installed
1523 return false;
1524 }
1525
1526 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1527 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1528 return [];
1529 }
1530
1531 return VikChannelManager::getPromotionHandlers($key);
1532 }
1533
1534 /**
1535 * Gets the factors for suggesting the application of the promotions.
1536 *
1537 * @param mixed $data some optional instructions to be passed as argument.
1538 *
1539 * @return mixed false if VCM is not installed, associative array otherwise.
1540 *
1541 * @requires Vik Channel Manager 1.7.1
1542 *
1543 * @since 1.3.0
1544 */
1545 public static function getPromotionFactors($data = null)
1546 {
1547 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1548 // VCM not installed
1549 return false;
1550 }
1551
1552 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1553 // VCM is outdated
1554 return false;
1555 }
1556
1557 return VikChannelManager::getPromotionFactors($data);
1558 }
1559
1560 public static function getTheme()
1561 {
1562 return VBOFactory::getConfig()->get('theme', '');
1563 }
1564
1565 public static function getFooterOrdMail($vbo_tn = null)
1566 {
1567 $dbo = JFactory::getDbo();
1568
1569 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1570 $dbo->setQuery($q);
1571 $ft = $dbo->loadAssocList();
1572
1573 if (!is_object($vbo_tn)) {
1574 $vbo_tn = self::getTranslator();
1575 }
1576
1577 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1578
1579 return $ft ? $ft[0]['setting'] : '';
1580 }
1581
1582 public static function requireLogin()
1583 {
1584 return VBOFactory::getConfig()->getBool('requirelogin', false);
1585 }
1586
1587 public static function autoRoomUnit()
1588 {
1589 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1590 }
1591
1592 public static function todayBookings()
1593 {
1594 static $todayBookings = null;
1595
1596 if ($todayBookings === null) {
1597 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1598 }
1599
1600 return $todayBookings;
1601 }
1602
1603 public static function couponsEnabled()
1604 {
1605 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1606 }
1607
1608 public static function customersPinEnabled()
1609 {
1610 return VBOFactory::getConfig()->getBool('enablepin', false);
1611 }
1612
1613 /**
1614 * Detects the type of visitor from the user agent.
1615 * Known types are: computer, smartphone, tablet.
1616 *
1617 * @param boolean $returnua whether the type of visitor should be returned. If false
1618 * boolean is returned in case of mobile device detected.
1619 * @param boolean $loadassets whether the system should load an apposite CSS file.
1620 *
1621 * @return mixed string for the type of visitor or boolean if mobile detected.
1622 *
1623 * @since 1.0.13 - Revision September 2018
1624 */
1625 public static function detectUserAgent($returnua = false, $loadassets = true) {
1626 $session = JFactory::getSession();
1627 $sval = $session->get('vbuseragent', '');
1628 $mobiles = array('tablet', 'smartphone');
1629 if (!empty($sval)) {
1630 if ($loadassets) {
1631 self::userAgentStyleSheet($sval);
1632 }
1633 return $returnua ? $sval : in_array($sval, $mobiles);
1634 }
1635
1636 // detect visitor (device) type
1637 $visitoris = 'computer';
1638 try {
1639 if (!class_exists('MobileDetector')) {
1640 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1641 }
1642 $detector = new MobileDetector;
1643 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1644 } catch (Exception $e) {
1645 // do nothing
1646 }
1647
1648 $session->set('vbuseragent', $visitoris);
1649 if ($loadassets) {
1650 self::userAgentStyleSheet($visitoris);
1651 }
1652
1653 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1654 }
1655
1656 public static function userAgentStyleSheet($ua)
1657 {
1658 /**
1659 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1660 */
1661 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1662 return;
1663 }
1664 //
1665
1666 $document = JFactory::getDocument();
1667 /**
1668 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1669 */
1670 if ($ua == 'smartphone') {
1671 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1672 } elseif ($ua == 'tablet') {
1673 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1674 }
1675 return true;
1676 }
1677
1678 public static function loadJquery()
1679 {
1680 return VBOFactory::getConfig()->getBool('loadjquery', true);
1681 }
1682
1683 public static function loadBootstrap()
1684 {
1685 return VBOFactory::getConfig()->getBool('bootstrap', true);
1686 }
1687
1688 public static function allowMultiLanguage()
1689 {
1690 return VBOFactory::getConfig()->getBool('multilang', true);
1691 }
1692
1693 public static function getTranslator()
1694 {
1695 if (!class_exists('VikBookingTranslator')) {
1696 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1697 }
1698
1699 return new VikBookingTranslator();
1700 }
1701
1702 /**
1703 * New method's name spelled correctly. The object will be cached by default.
1704 *
1705 * @return VikBookingCustomersPin
1706 *
1707 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1708 */
1709 public static function getCPinInstance()
1710 {
1711 static $cpin_instance = null;
1712
1713 if (!$cpin_instance) {
1714 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1715
1716 $cpin_instance = new VikBookingCustomersPin();
1717 }
1718
1719 return $cpin_instance;
1720 }
1721
1722 /**
1723 * Old method's name spelled wrongly for BC.
1724 *
1725 * @return VikBookingCustomersPin
1726 */
1727 public static function getCPinIstance()
1728 {
1729 return self::getCPinInstance();
1730 }
1731
1732 /**
1733 * Returns an instance of the VikBookingTracker Class.
1734 * It is also possible to call this method to just require the library.
1735 * This is useful for the back-end to access some static methods
1736 * without tracking any data.
1737 *
1738 * @param boolean $require_only whether to return the object.
1739 *
1740 * @return VikBookingTracker
1741 *
1742 * @since 1.11
1743 */
1744 public static function getTracker($require_only = false) {
1745 if (!class_exists('VikBookingTracker')) {
1746 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1747 }
1748 return $require_only ? true : VikBookingTracker::getInstance();
1749 }
1750
1751 /**
1752 * Returns an instance of the VikBookingOperator Class.
1753 *
1754 * @return VikBookingOperator
1755 *
1756 * @since 1.11
1757 */
1758 public static function getOperatorInstance() {
1759 if (!class_exists('VikBookingOperator')) {
1760 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1761 }
1762 return VikBookingOperator::getInstance();
1763 }
1764
1765 /**
1766 * Returns an instance of the VikBookingFestivities Class.
1767 *
1768 * @return VikBookingFestivities
1769 *
1770 * @since 1.12
1771 */
1772 public static function getFestivitiesInstance() {
1773 if (!class_exists('VikBookingFestivities')) {
1774 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1775 }
1776 return VikBookingFestivities::getInstance();
1777 }
1778
1779 /**
1780 * Returns an instance of the VikBookingCriticalDates Class.
1781 *
1782 * @return VikBookingCriticalDates
1783 *
1784 * @since 1.13.5
1785 */
1786 public static function getCriticalDatesInstance() {
1787 if (!class_exists('VikBookingCriticalDates')) {
1788 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1789 }
1790 return VikBookingCriticalDates::getInstance();
1791 }
1792
1793 /**
1794 * Checks whether the chat is enabled.
1795 *
1796 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1797 *
1798 * @since 1.12
1799 */
1800 public static function chatEnabled() {
1801 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1802 return -1;
1803 }
1804 $dbo = JFactory::getDbo();
1805 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1806 $dbo->setQuery($q);
1807 $dbo->execute();
1808 if (!$dbo->getNumRows()) {
1809 // enable the chat by default if VCM is installed
1810 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1811 $dbo->setQuery($q);
1812 $dbo->execute();
1813 return 1;
1814 }
1815 $s = $dbo->loadResult();
1816 return intval($s);
1817 }
1818
1819 /**
1820 * Loads the chat parameters from the configuration.
1821 *
1822 * @return object stdClass object from decoded JSON string
1823 *
1824 * @since 1.12
1825 */
1826 public static function getChatParams() {
1827 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1828 return new stdClass;
1829 }
1830 $dbo = JFactory::getDbo();
1831 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1832 $dbo->setQuery($q);
1833 $dbo->execute();
1834 if (!$dbo->getNumRows()) {
1835 // compose default and basic chat params for the first time
1836 $basic_params = new stdClass;
1837 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1838 $basic_params->av_type = 'checkin';
1839 $basic_params->av_days = 0;
1840
1841 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1842 $dbo->setQuery($q);
1843 $dbo->execute();
1844
1845 return $basic_params;
1846 }
1847 return json_decode($dbo->loadResult());
1848 }
1849
1850 /**
1851 * Checks whether the pre-checkin is enabled.
1852 *
1853 * @return int 0 if disabled, 1 otherwise
1854 *
1855 * @since 1.12
1856 */
1857 public static function precheckinEnabled()
1858 {
1859 return (int) VBOFactory::getConfig()->getBool('precheckinenabled', true);
1860 }
1861
1862 /**
1863 * Returns the minimum number of days in advance to
1864 * enable the pre-checkin via front-end.
1865 *
1866 * @return int the min number of days in advance for pre-checkin.
1867 *
1868 * @since 1.12
1869 */
1870 public static function precheckinMinOffset()
1871 {
1872 return VBOFactory::getConfig()->getInt('precheckinminoffset', 0);
1873 }
1874
1875 /**
1876 * Whether upselling extra services is enabled.
1877 *
1878 * @return boolean true if enabled, false otherwise.
1879 *
1880 * @since 1.13
1881 */
1882 public static function upsellingEnabled()
1883 {
1884 return VBOFactory::getConfig()->getBool('upselling', true);
1885 }
1886
1887 /**
1888 * Collects a list of options/extra that can be upsold for the
1889 * the rooms booked.
1890 *
1891 * @param array $upsell_data list of stdClass objects for each room booked.
1892 * @param array $info some booking details (id, checkin, checkout).
1893 * @param object $vbo_tn the translation object.
1894 *
1895 * @return array list of upsellable options for each room booked.
1896 *
1897 * @since 1.13 (J) - 1.3.0 (WP)
1898 * @since 1.17.7 (J) - 1.7.7 (WP) added support for damage deposit to confirmed bookings.
1899 */
1900 public static function loadUpsellingData($upsell_data, $info, $vbo_tn)
1901 {
1902 $dbo = JFactory::getDbo();
1903
1904 // get all rooms booked
1905 $all_room_ids = [];
1906 foreach ($upsell_data as $v) {
1907 if (!in_array($v->id, $all_room_ids)) {
1908 array_push($all_room_ids, (int) $v->id);
1909 }
1910 }
1911
1912 $dbo->setQuery(
1913 $dbo->getQuery(true)
1914 ->select([
1915 $dbo->qn('id'),
1916 $dbo->qn('idopt'),
1917 ])
1918 ->from($dbo->qn('#__vikbooking_rooms'))
1919 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_room_ids) . ')')
1920 );
1921 $records = $dbo->loadAssocList();
1922 if (!$records) {
1923 // no rooms found
1924 return [];
1925 }
1926
1927 // get all suitable options
1928 $all_options = [];
1929 $rooms_options = [];
1930 foreach ($records as $v) {
1931 $allopts = explode(';', $v['idopt']);
1932 $room_opt = [];
1933 foreach ($allopts as $o) {
1934 if (empty($o)) {
1935 continue;
1936 }
1937 if (!in_array($o, $all_options)) {
1938 array_push($all_options, (int) $o);
1939 }
1940 array_push($room_opt, (int) $o);
1941 }
1942 $rooms_options[$v['id']] = $room_opt;
1943 }
1944 if (!$all_options) {
1945 // no options found
1946 return [];
1947 }
1948
1949 // load all options that could be used by the booked rooms no matter what was already booked
1950 $dbo->setQuery(
1951 $dbo->getQuery(true)
1952 ->select('*')
1953 ->from($dbo->qn('#__vikbooking_optionals'))
1954 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1955 ->where($dbo->qn('forcesel') . ' = 0')
1956 ->where($dbo->qn('ifchildren') . ' = 0')
1957 ->where($dbo->qn('is_citytax') . ' = 0')
1958 ->where($dbo->qn('is_fee') . ' = 0')
1959 ->order($dbo->qn('ordering') . ' ASC')
1960 );
1961 $records = $dbo->loadAssocList();
1962
1963 if (!strcasecmp(($info['status'] ?? ''), 'confirmed')) {
1964 // merge mandatory damage deposit
1965 $dbo->setQuery(
1966 $dbo->getQuery(true)
1967 ->select('*')
1968 ->from($dbo->qn('#__vikbooking_optionals'))
1969 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1970 ->where($dbo->qn('forcesel') . ' = 1')
1971 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
1972 );
1973 $dd_records = $dbo->loadAssocList();
1974
1975 // merge with regular options
1976 $records = array_merge($records, $dd_records);
1977 }
1978
1979 if (!$records) {
1980 // no upsell-able options found
1981 return [];
1982 }
1983
1984 // filter options by available date and translate records
1985 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1986 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1987 $records = !is_array($records) ? [] : $records;
1988
1989 $tot_upsellable = 0;
1990 foreach ($upsell_data as $k => $rdata) {
1991 if (!isset($upsell_data[$k]->upsellable)) {
1992 $upsell_data[$k]->upsellable = [];
1993 }
1994 foreach ($records as $opt) {
1995 if (!empty($opt['ageintervals'])) {
1996 // upsellable options should not contain age intervals for children
1997 continue;
1998 }
1999 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
2000 // this option is not assigned to this room
2001 continue;
2002 }
2003 // check if the option is suited for this room party
2004 $clone_opt = [$opt];
2005 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
2006 if (!is_array($clone_opt) || !$clone_opt) {
2007 // this option is not suited for this room party
2008 continue;
2009 }
2010
2011 if (in_array($opt['id'], $rdata->options)) {
2012 // this option has already been booked
2013 continue;
2014 }
2015
2016 // push this option and increase counter
2017 array_push($upsell_data[$k]->upsellable, $opt);
2018 $tot_upsellable++;
2019 }
2020 }
2021
2022 // if no upsellable options were found, we return an empty array
2023 return $tot_upsellable > 0 ? $upsell_data : [];
2024 }
2025
2026 /**
2027 * Returns the minimum days in advance for booking, by considering by default
2028 * also the property closing dates. If the property is currently closed, then
2029 * the minimum number of days in advance will be increased.
2030 *
2031 * @param bool $no_closing_dates whether to skip checking closing dates.
2032 *
2033 * @return int the number of days in advance for booking.
2034 *
2035 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
2036 * getMinDaysAdvance($skipsession = false) to:
2037 * getMinDaysAdvance($no_closing_dates = false)
2038 * This was made to return the proper number of days in advance
2039 * in case the property is currently closed.
2040 */
2041 public static function getMinDaysAdvance($no_closing_dates = false)
2042 {
2043 // cache value in static var
2044 static $getMinDaysAdvance = null;
2045
2046 if ($getMinDaysAdvance) {
2047 return $getMinDaysAdvance;
2048 }
2049
2050 $mind = VBOFactory::getConfig()->getInt('mindaysadvance', 0);
2051
2052 // update cached var
2053 $getMinDaysAdvance = $mind;
2054
2055 if ($no_closing_dates) {
2056 // do not check the closing dates
2057 return $mind;
2058 }
2059
2060 // check if the property is currently closed
2061 $cur_closed_dates = self::getClosingDates();
2062 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
2063 $today_midnight = mktime(0, 0, 0);
2064 $closed_until = null;
2065 foreach ($cur_closed_dates as $kcd => $vcd) {
2066 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
2067 // closing period found
2068 $closed_until = $vcd['to'];
2069 break;
2070 }
2071 }
2072 if ($closed_until !== null) {
2073 // count the number of days until property is closed
2074 $mind = 0;
2075 $today_info = getdate($today_midnight);
2076 while ($today_info[0] <= $closed_until) {
2077 $mind++;
2078 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
2079 }
2080 }
2081 }
2082
2083 // update cached var
2084 $getMinDaysAdvance = $mind;
2085
2086 return $mind;
2087 }
2088
2089 public static function getDefaultNightsCalendar()
2090 {
2091 return VBOFactory::getConfig()->getInt('autodefcalnights', 1);
2092 }
2093
2094 public static function getSearchNumRooms($skipsession = true)
2095 {
2096 $dbo = JFactory::getDbo();
2097 if ($skipsession) {
2098 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2099 $dbo->setQuery($q);
2100 $dbo->execute();
2101 $s = $dbo->loadAssocList();
2102 return (int)$s[0]['setting'];
2103 }
2104 $session = JFactory::getSession();
2105 $sval = $session->get('vbsearchNumRooms', '');
2106 if (!empty($sval)) {
2107 return (int)$sval;
2108 }
2109 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2110 $dbo->setQuery($q);
2111 $dbo->execute();
2112 $s = $dbo->loadAssocList();
2113 $session->set('vbsearchNumRooms', $s[0]['setting']);
2114 return (int)$s[0]['setting'];
2115 }
2116
2117 public static function getSearchNumAdults($skipsession = true)
2118 {
2119 $dbo = JFactory::getDbo();
2120 if ($skipsession) {
2121 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2122 $dbo->setQuery($q);
2123 $dbo->execute();
2124 $s = $dbo->loadAssocList();
2125 return $s[0]['setting'];
2126 }
2127 $session = JFactory::getSession();
2128 $sval = $session->get('vbsearchNumAdults', '');
2129 if (!empty($sval)) {
2130 return $sval;
2131 }
2132 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2133 $dbo->setQuery($q);
2134 $dbo->execute();
2135 $s = $dbo->loadAssocList();
2136 $session->set('vbsearchNumAdults', $s[0]['setting']);
2137 return $s[0]['setting'];
2138 }
2139
2140 public static function getSearchNumChildren($skipsession = true)
2141 {
2142 $dbo = JFactory::getDbo();
2143 if ($skipsession) {
2144 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2145 $dbo->setQuery($q);
2146 $dbo->execute();
2147 $s = $dbo->loadAssocList();
2148 return $s[0]['setting'];
2149 }
2150 $session = JFactory::getSession();
2151 $sval = $session->get('vbsearchNumChildren', '');
2152 if (!empty($sval)) {
2153 return $sval;
2154 }
2155 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2156 $dbo->setQuery($q);
2157 $dbo->execute();
2158 $s = $dbo->loadAssocList();
2159 $session->set('vbsearchNumChildren', $s[0]['setting']);
2160 return $s[0]['setting'];
2161 }
2162
2163 public static function getSmartSearchType($skipsession = true)
2164 {
2165 $dbo = JFactory::getDbo();
2166 if ($skipsession) {
2167 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2168 $dbo->setQuery($q);
2169 $dbo->execute();
2170 $s = $dbo->loadAssocList();
2171 return $s[0]['setting'];
2172 }
2173 $session = JFactory::getSession();
2174 $sval = $session->get('vbsmartSearchType', '');
2175 if (!empty($sval)) {
2176 return $sval;
2177 }
2178 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2179 $dbo->setQuery($q);
2180 $dbo->execute();
2181 $s = $dbo->loadAssocList();
2182 $session->set('vbsmartSearchType', $s[0]['setting']);
2183 return $s[0]['setting'];
2184 }
2185
2186 /**
2187 * Retrieves the listing-level minimum advance booking offset.
2188 *
2189 * @param int $idroom The listing ID to evaluate.
2190 * @param bool $period Whether to format the value as a period string.
2191 *
2192 * @return int|string|null Number of hours or period string ("2H"), if set.
2193 *
2194 * @since 1.18.3 (J) - 1.8.3 (WP)
2195 */
2196 public static function getMinDateFutureListing($idroom, $period = false)
2197 {
2198 $config = VBOFactory::getConfig();
2199
2200 $room_level_min_notice = $config->get("room_{$idroom}_min_adv_notice", null);
2201
2202 if (is_null($room_level_min_notice) || $room_level_min_notice === '') {
2203 return null;
2204 }
2205
2206 if ($period) {
2207 return sprintf('%dH', (int) $room_level_min_notice);
2208 }
2209
2210 return (int) $room_level_min_notice;
2211 }
2212
2213 /**
2214 * Returns the maximum advance booking notice (offset) as a period.
2215 * It either returns the global configuration setting, or the value
2216 * defined at room-level, if any and if requested.
2217 *
2218 * @param int $idroom optional room ID for room-level setting.
2219 *
2220 * @return string the maximum booking notice period (i.e. +1Y).
2221 *
2222 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
2223 * $idroom to support room-level settings.
2224 */
2225 public static function getMaxDateFuture($idroom = 0)
2226 {
2227 static $max_adv_book_pool = [];
2228
2229 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
2230
2231 if (isset($max_adv_book_pool[$pool_id])) {
2232 return $max_adv_book_pool[$pool_id];
2233 }
2234
2235 $config = VBOFactory::getConfig();
2236
2237 if ($pool_id === 'global') {
2238 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
2239 } else {
2240 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
2241 if (!empty($room_level_max_notice)) {
2242 // setting is defined at room-level
2243 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2244 } else {
2245 // recurse for global setting
2246 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2247 }
2248 }
2249
2250 return $max_adv_book_pool[$pool_id];
2251 }
2252
2253 /**
2254 * Validates the maximum advance booking notice against the requested check-in
2255 * timestamp. The method supports the validation at both room and property levels.
2256 *
2257 * @param int $checkints the check-in date timestamp.
2258 * @param int $idroom the optional room ID for room-level validation.
2259 *
2260 * @return string error date string in case of validation failure.
2261 *
2262 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2263 */
2264 public static function validateMaxDateBookings($checkints, $idroom = 0)
2265 {
2266 $datelim = self::getMaxDateFuture($idroom);
2267 $datelim = empty($datelim) ? '+2y' : $datelim;
2268 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2269 $quantlim = substr($datelim, -1, 1);
2270
2271 $now = getdate();
2272 if ($quantlim == 'w') {
2273 $until_ts = strtotime("+$numlim weeks") + 86399;
2274 } else {
2275 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2276 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2277 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2278 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2279 }
2280
2281 if ($until_ts > $now[0] && $checkints > $until_ts) {
2282 $vbo_df = self::getDateFormat();
2283 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2284
2285 // error, return the maximum date in the future allowed
2286 return date($df, $until_ts);
2287 }
2288
2289 // validation passed
2290 return '';
2291 }
2292
2293 /**
2294 * Validates the minimum advance booking notice against the requested check-in
2295 * timestamp. The method supports the validation at both room and property levels.
2296 *
2297 * @param int $checkints The check-in date timestamp.
2298 * @param int $idroom Optional room ID for room-level validation.
2299 *
2300 * @return int|float Minimum days (int) or hours (float) in advance required.
2301 *
2302 * @since 1.18.3 (J) - 1.8.3 (WP) introduced $idroom argument for room-level.
2303 */
2304 public static function validateMinDaysAdvance($checkints, $idroom = 0)
2305 {
2306 if ($idroom) {
2307 // listing-level minimum advance booking notice (hours, could be null or 0)
2308 $minhadv = self::getMinDateFutureListing($idroom);
2309
2310 // perform listing-level validation only when value is set
2311 if (!$minhadv) {
2312 // validation passed
2313 return '';
2314 }
2315
2316 // access listing params
2317 $listing_data = self::getRoomInfo($idroom, ['params'], $no_cache = true);
2318 if ($listing_data['params'] ?? null) {
2319 // access listing-level check-in time
2320 $listing_checkin = self::getRoomParam('checkin', $listing_data['params']);
2321 if ($listing_checkin) {
2322 // overwrite check-in time
2323 $listing_checkin_parts = explode(':', $listing_checkin);
2324 $checkin_hours = (int) $listing_checkin_parts[0];
2325 $checkin_minutes = (int) $listing_checkin_parts[1];
2326 // read current check-in date-time
2327 $tsinfo = getdate($checkints);
2328 // update check-in time with proper hours, minutes and seconds
2329 $checkints = mktime($checkin_hours, $checkin_minutes, 0, $tsinfo['mon'], $tsinfo['mday'], $tsinfo['year']);
2330 }
2331 }
2332
2333 if ($checkints < strtotime('+' . $minhadv . ($minhadv > 1 ? 'hours' : 'hour'))) {
2334 return $minhadv >= 24 ? floor($minhadv / 24) : ($minhadv / 24);
2335 }
2336
2337 // validation passed without checking the global settings when listing ID given
2338 return '';
2339 }
2340
2341 // global validation
2342 $mindadv = self::getMinDaysAdvance(true);
2343 if ($mindadv > 0) {
2344 $tsinfo = getdate($checkints);
2345 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2346 if ($checkints < $limit_ts) {
2347 return $mindadv;
2348 }
2349 }
2350
2351 // validation passed
2352 return '';
2353 }
2354
2355 /**
2356 * The only supported calendar type has been changed to jQuery UI.
2357 *
2358 * @since 1.13
2359 */
2360 public static function calendarType($skipsession = false)
2361 {
2362 return 'jqueryui';
2363 }
2364
2365 public static function getSiteLogo()
2366 {
2367 static $siteLogo = null;
2368
2369 if ($siteLogo !== null) {
2370 return $siteLogo;
2371 }
2372
2373 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2374
2375 return $siteLogo;
2376 }
2377
2378 public static function getBackendLogo()
2379 {
2380 static $backLogo = null;
2381
2382 if ($backLogo !== null) {
2383 return $backLogo;
2384 }
2385
2386 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2387
2388 return $backLogo;
2389 }
2390
2391 public static function numCalendars()
2392 {
2393 static $numCalendars = null;
2394
2395 if ($numCalendars !== null) {
2396 return $numCalendars;
2397 }
2398
2399 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2400
2401 return $numCalendars;
2402 }
2403
2404 public static function getFirstWeekDay()
2405 {
2406 static $firstWeekDay = null;
2407
2408 if ($firstWeekDay !== null) {
2409 return $firstWeekDay;
2410 }
2411
2412 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2413
2414 return $firstWeekDay;
2415 }
2416
2417 public static function showPartlyReserved()
2418 {
2419 static $partlyReserved = null;
2420
2421 if ($partlyReserved !== null) {
2422 return $partlyReserved;
2423 }
2424
2425 $partlyReserved = VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2426
2427 return $partlyReserved;
2428 }
2429
2430 public static function showStatusCheckinoutOnly()
2431 {
2432 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2433 }
2434
2435 public static function getDisclaimer($vbo_tn = null)
2436 {
2437 $dbo = JFactory::getDbo();
2438
2439 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2440 $dbo->setQuery($q);
2441 $ft = $dbo->loadAssocList();
2442 if (!$ft) {
2443 return '';
2444 }
2445
2446 if (!is_object($vbo_tn)) {
2447 $vbo_tn = self::getTranslator();
2448 }
2449 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2450
2451 return $ft[0]['setting'];
2452 }
2453
2454 public static function showFooter()
2455 {
2456 return VBOFactory::getConfig()->getBool('showfooter', false);
2457 }
2458
2459 public static function getPriceName($idp, $vbo_tn = null)
2460 {
2461 $dbo = JFactory::getDbo();
2462
2463 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2464 $dbo->setQuery($q, 0, 1);
2465 $n = $dbo->loadAssocList();
2466 if ($n) {
2467 if (is_object($vbo_tn)) {
2468 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2469 }
2470 return $n[0]['name'];
2471 }
2472
2473 return "";
2474 }
2475
2476 public static function getPriceAttr($idp, $vbo_tn = null)
2477 {
2478 $dbo = JFactory::getDbo();
2479
2480 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2481 $dbo->setQuery($q, 0, 1);
2482 $n = $dbo->loadAssocList();
2483 if ($n) {
2484 if (is_object($vbo_tn)) {
2485 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2486 }
2487 return $n[0]['attr'];
2488 }
2489
2490 return "";
2491 }
2492
2493 /**
2494 * Fetches the requested rate plan information.
2495 *
2496 * @param int $idp the rate plan ID.
2497 * @param object $vbo_tn the Vik Booking translator object.
2498 *
2499 * @return array empty array or associative record.
2500 */
2501 public static function getPriceInfo($idp, $vbo_tn = null)
2502 {
2503 // cache values
2504 static $priceInfos = [];
2505
2506 $idp = (int)$idp;
2507
2508 if ($priceInfos && isset($priceInfos[$idp])) {
2509 return $priceInfos[$idp];
2510 }
2511
2512 $dbo = JFactory::getDbo();
2513
2514 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2515 $dbo->setQuery($q, 0, 1);
2516 $rplan = $dbo->loadAssocList();
2517 if (!$rplan) {
2518 return [];
2519 }
2520
2521 if (is_object($vbo_tn)) {
2522 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2523 }
2524
2525 // set cached value
2526 $priceInfos[$idp] = $rplan[0];
2527
2528 return $rplan[0];
2529 }
2530
2531 public static function getAliq($idal)
2532 {
2533 $dbo = JFactory::getDbo();
2534
2535 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2536 $dbo->setQuery($q);
2537 $aliq = $dbo->loadResult();
2538 if ($aliq) {
2539 return $aliq;
2540 }
2541
2542 return 0;
2543 }
2544
2545 public static function getTimeOpenStore($skipsession = false)
2546 {
2547 // cache value in static var
2548 static $getTimeOpenStore = null;
2549
2550 if ($getTimeOpenStore) {
2551 return $getTimeOpenStore;
2552 }
2553 //
2554
2555 $dbo = JFactory::getDbo();
2556 $session = JFactory::getSession();
2557
2558 if ($skipsession) {
2559 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2560 if (empty($setting) && $setting != "0") {
2561 return false;
2562 } else {
2563 $x = explode("-", $setting);
2564 if (!empty($x[1]) && $x[1] != "0") {
2565 $getTimeOpenStore = $x;
2566 return $x;
2567 }
2568 }
2569 } else {
2570 $sval = $session->get('vbgetTimeOpenStore', '');
2571 if (!empty($sval)) {
2572 return $sval;
2573 } else {
2574 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2575 if (empty($setting) && $setting != "0") {
2576 return false;
2577 } else {
2578 $x = explode("-", $setting);
2579 if (!empty($x[1]) && $x[1] != "0") {
2580 $session->set('vbgetTimeOpenStore', $x);
2581 $getTimeOpenStore = $x;
2582 return $x;
2583 }
2584 }
2585 }
2586 }
2587 return false;
2588 }
2589
2590 public static function getHoursMinutes($secs)
2591 {
2592 if ($secs >= 3600) {
2593 $op = $secs / 3600;
2594 $hours = floor($op);
2595 $less = $hours * 3600;
2596 $newsec = $secs - $less;
2597 $optwo = $newsec / 60;
2598 $minutes = floor($optwo);
2599 } else {
2600 $hours = "0";
2601 $optwo = $secs / 60;
2602 $minutes = floor($optwo);
2603 }
2604
2605 return [$hours, $minutes];
2606 }
2607
2608 public static function getClosingDates()
2609 {
2610 // cache value in static var
2611 static $getClosingDates = null;
2612
2613 if (is_array($getClosingDates)) {
2614 return $getClosingDates;
2615 }
2616
2617 $dbo = JFactory::getDbo();
2618
2619 $allcd = [];
2620
2621 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2622 $dbo->setQuery($q);
2623 $s = $dbo->loadAssocList();
2624 if ($s && !empty($s[0]['setting'])) {
2625 $allcd = json_decode($s[0]['setting'], true);
2626 $allcd = is_array($allcd) ? $allcd : array();
2627 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2628 foreach ($allcd as $k => $v) {
2629 if ($v['to'] < $base_ts) {
2630 unset($allcd[$k]);
2631 }
2632 }
2633 $allcd = array_values($allcd);
2634 }
2635
2636 $getClosingDates = $allcd;
2637
2638 return $allcd;
2639 }
2640
2641 public static function parseJsClosingDates()
2642 {
2643 $cd = self::getClosingDates();
2644 if (!$cd) {
2645 return [];
2646 }
2647
2648 $cdjs = [];
2649 foreach ($cd as $k => $v) {
2650 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2651 }
2652
2653 return $cdjs;
2654 }
2655
2656 /**
2657 * Given stay date timestamps, tells if the booking is disallowed due to global closing dates.
2658 *
2659 * @param int $checkints The check-in date timestamp.
2660 * @param int $checkoutts The check-in date timestamp.
2661 * @param ?string $df Optional date format to return the closed stay dates.
2662 * @param bool $closingcmp True to obtain the stay components affected by a global closure.
2663 *
2664 * @return string|array Empty string if no closures, closure dates string or affected components list.
2665 *
2666 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $closingcmp to improve the search suggestion details.
2667 */
2668 public static function validateClosingDates($checkints, $checkoutts, $df = null, $closingcmp = false)
2669 {
2670 $cd = self::getClosingDates();
2671
2672 if (!$cd) {
2673 return '';
2674 }
2675
2676 $df = empty($df) ? 'Y-m-d' : $df;
2677 $margin_seconds = 22 * 60 * 60;
2678
2679 foreach ($cd as $k => $v) {
2680 // determine if the stay dates are closed
2681 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2682 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2683 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2684
2685 if ($inner_closed || $outer_closed || $middle_closed) {
2686 // stay dates are closed
2687 if ($closingcmp) {
2688 // return the components affected by the globally closed dates to allow analysis
2689 return [
2690 'checkin' => $inner_closed,
2691 'checkout' => $outer_closed,
2692 'stay' => $middle_closed,
2693 ];
2694 }
2695
2696 // return the string with the range of dates closed globally
2697 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2698 }
2699 }
2700
2701 return '';
2702 }
2703
2704 /**
2705 * Whether the categories dropdown filter menu should be displayed.
2706 *
2707 * @param bool $skipsession [optional] re-read the configuration setting.
2708 *
2709 * @return bool
2710 *
2711 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2712 */
2713 public static function showCategoriesFront($skipsession = true)
2714 {
2715 return VBOFactory::getConfig()->getBool('showcategories', false);
2716 }
2717
2718 /**
2719 * Whether the number of children dropdown menu should be displayed.
2720 * Defaults to skip the session values and to re-read the configuration setting.
2721 *
2722 * @param bool $skipsession [optional] re-read the configuration setting.
2723 *
2724 * @return bool
2725 *
2726 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2727 */
2728 public static function showChildrenFront($skipsession = true)
2729 {
2730 return VBOFactory::getConfig()->getBool('showchildren', true);
2731 }
2732
2733 public static function allowBooking()
2734 {
2735 // cache value in static var
2736 static $allowBooking = null;
2737
2738 if (is_bool($allowBooking)) {
2739 return $allowBooking;
2740 }
2741
2742 $allowBooking = (bool) VBOFactory::getConfig()->get('allowbooking');
2743
2744 return $allowBooking;
2745 }
2746
2747 public static function getDisabledBookingMsg($vbo_tn = null) {
2748 $dbo = JFactory::getDbo();
2749 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2750 $dbo->setQuery($q);
2751 $dbo->execute();
2752 $s = $dbo->loadAssocList();
2753 if (!is_object($vbo_tn)) {
2754 $vbo_tn = self::getTranslator();
2755 }
2756 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2757 return $s[0]['setting'];
2758 }
2759
2760 /**
2761 * Returns the text for the guests allowed policy.
2762 *
2763 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2764 *
2765 * @return string either the raw or the translated policy text.
2766 *
2767 * @since 1.16.5 (WP) - 1.6.5 (WP)
2768 */
2769 public static function getGuestsAllowedPolicy($vbo_tn = null)
2770 {
2771 $dbo = JFactory::getDbo();
2772
2773 $q = $dbo->getQuery(true)
2774 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2775 ->from($dbo->qn('#__vikbooking_texts'))
2776 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2777
2778 $dbo->setQuery($q);
2779 $text_record = $dbo->loadAssocList();
2780
2781 if (!$text_record) {
2782 // missing record, insert it with empty values
2783 if (self::isAdmin()) {
2784 $record = new stdClass;
2785 $record->param = 'guests_allowed_policy';
2786 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2787 $record->setting = '';
2788 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2789 }
2790
2791 return '';
2792 }
2793
2794 if (is_object($vbo_tn)) {
2795 // translate the record only if requested
2796 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2797 }
2798
2799 return $text_record[0]['setting'];
2800 }
2801
2802 public static function getDateFormat($skipsession = true)
2803 {
2804 // cache value in static var
2805 static $getDateFormat = null;
2806
2807 if ($getDateFormat) {
2808 return $getDateFormat;
2809 }
2810
2811 $dbo = JFactory::getDbo();
2812
2813 if ($skipsession) {
2814 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2815 $dbo->setQuery($q, 0, 1);
2816 $getDateFormat = $dbo->loadResult();
2817
2818 return $getDateFormat;
2819 }
2820
2821 $session = JFactory::getSession();
2822 $sval = $session->get('vbgetDateFormat', '');
2823 if (!empty($sval)) {
2824 return $sval;
2825 }
2826
2827 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2828 $dbo->setQuery($q, 0, 1);
2829 $getDateFormat = $dbo->loadResult();
2830
2831 $session->set('vbgetDateFormat', $getDateFormat);
2832
2833 return $getDateFormat;
2834 }
2835
2836 public static function getDateSeparator($skipsession = true)
2837 {
2838 // cache value in static var
2839 static $getDateSeparator = null;
2840
2841 if ($getDateSeparator) {
2842 return $getDateSeparator;
2843 }
2844
2845 $dbo = JFactory::getDbo();
2846
2847 if ($skipsession) {
2848 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2849 $dbo->setQuery($q, 0, 1);
2850 $val = $dbo->loadResult();
2851
2852 $getDateSeparator = empty($val) ? "/" : $val;
2853
2854 return $getDateSeparator;
2855 }
2856
2857 $session = JFactory::getSession();
2858 $sval = $session->get('vbgetDateSep', '');
2859 if (!empty($sval)) {
2860 return $sval;
2861 }
2862
2863 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2864 $dbo->setQuery($q, 0, 1);
2865 $val = $dbo->loadResult();
2866
2867 $getDateSeparator = empty($val) ? "/" : $val;
2868
2869 $session->set('vbgetDateSep', $getDateSeparator);
2870
2871 return $getDateSeparator;
2872 }
2873
2874 public static function getHoursMoreRb($skipsession = false)
2875 {
2876 // cache value in static var
2877 static $getHoursMoreRb = null;
2878
2879 if ($getHoursMoreRb !== null) {
2880 return $getHoursMoreRb;
2881 }
2882
2883 $dbo = JFactory::getDbo();
2884
2885 if ($skipsession) {
2886 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2887 $dbo->setQuery($q, 0, 1);
2888 $getHoursMoreRb = (int)$dbo->loadResult();
2889
2890 return $getHoursMoreRb;
2891 }
2892
2893 $session = JFactory::getSession();
2894 $sval = $session->get('getHoursMoreRb', '');
2895 if (strlen($sval)) {
2896 return $sval;
2897 }
2898
2899 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2900 $dbo->setQuery($q, 0, 1);
2901 $getHoursMoreRb = (int)$dbo->loadResult();
2902
2903 $session->set('getHoursMoreRb', $getHoursMoreRb);
2904
2905 return $getHoursMoreRb;
2906 }
2907
2908 public static function getHoursRoomAvail()
2909 {
2910 $dbo = JFactory::getDbo();
2911
2912 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2913 $dbo->setQuery($q, 0, 1);
2914
2915 return (int)$dbo->loadResult();
2916 }
2917
2918 public static function getFrontTitle($vbo_tn = null) {
2919 // cache value in static var
2920 static $getFrontTitle = null;
2921
2922 if ($getFrontTitle) {
2923 return $getFrontTitle;
2924 }
2925 //
2926
2927 $dbo = JFactory::getDbo();
2928 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2929 $dbo->setQuery($q);
2930 $dbo->execute();
2931 $ft = $dbo->loadAssocList();
2932 if (!is_object($vbo_tn)) {
2933 $vbo_tn = self::getTranslator();
2934 }
2935 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2936
2937 $getFrontTitle = $ft[0]['setting'];
2938
2939 return $ft[0]['setting'];
2940 }
2941
2942 public static function getFrontTitleTag()
2943 {
2944 return VBOFactory::getConfig()->get('fronttitletag', '');
2945 }
2946
2947 public static function getFrontTitleTagClass()
2948 {
2949 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2950 }
2951
2952 /**
2953 * Returns the configured currency name. Often used to apply currency
2954 * conversions, and so it should match the currency ISO 4217 (Alpha-3) code.
2955 *
2956 * @param bool $iso True to ensure we fetch a 3-char currency code.
2957 *
2958 * @return string
2959 *
2960 * @since 1.18.0 (J) - 1.8.0 (WP) added argument $iso.
2961 */
2962 public static function getCurrencyName($iso = false)
2963 {
2964 static $currencyName = null;
2965
2966 if ($currencyName !== null) {
2967 return $currencyName;
2968 }
2969
2970 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2971
2972 if ($iso && strlen((string) $currencyName) != 3) {
2973 // attempt to fetch the currency code configuration setting
2974 $currencyName = self::getCurrencyCodePp();
2975 }
2976
2977 return $currencyName;
2978 }
2979
2980 public static function getCurrencySymb()
2981 {
2982 // cache value in static var
2983 static $getCurrencySymb = null;
2984
2985 if ($getCurrencySymb) {
2986 return $getCurrencySymb;
2987 }
2988
2989 $getCurrencySymb = VBOFactory::getConfig()->get('currencysymb', '');
2990
2991 return $getCurrencySymb;
2992 }
2993
2994 /**
2995 * Returns the configured currency position: either "before" or "after".
2996 *
2997 * @return string
2998 *
2999 * @since 1.18.3 (J) - 1.8.3 (WP)
3000 */
3001 public static function getCurrencyPosition()
3002 {
3003 // cache value in static var
3004 static $getCurrencyPos = null;
3005
3006 if ($getCurrencyPos) {
3007 return $getCurrencyPos;
3008 }
3009
3010 $getCurrencyPos = VBOFactory::getConfig()->get('currencypos', 'before');
3011
3012 return $getCurrencyPos;
3013 }
3014
3015 /**
3016 * Given a number, formats it according to currency preferences.
3017 *
3018 * @param string $number The number to format.
3019 * @param ?string $currency Optional currency to use.
3020 * @param ?array $config Optional format configuration (i.e. currency/number "HTML template").
3021 *
3022 * @return string
3023 *
3024 * @since 1.18.3 (J) - 1.8.3 (WP)
3025 */
3026 public static function formatCurrencyNumber(string $number, ?string $currency = null, ?array $config = null)
3027 {
3028 // get the currency symbol/value to use
3029 $currency = $currency ?: self::getCurrencySymb();
3030
3031 // obtain currency template (wildcard expected)
3032 if (is_string($config[0] ?? null) || is_string($config['currency'] ?? null)) {
3033 // apply templating to string
3034 $currency = sprintf($config[0] ?? $config['currency'], $currency);
3035 }
3036
3037 // obtain number template
3038 if (is_string($config[1] ?? null) || is_string($config['number'] ?? null)) {
3039 // apply templating to string
3040 $number = sprintf($config[1] ?? $config['number'], $number);
3041 }
3042
3043 // default value spacing (empty white space)
3044 $spacing = (string) ($config['spacing'] ?? ' ');
3045
3046 if (self::getCurrencyPosition() === 'after') {
3047 return sprintf('%s' . $spacing . '%s', $number, $currency);
3048 }
3049
3050 return sprintf('%s' . $spacing . '%s', $currency, $number);
3051 }
3052
3053 public static function getCurrencyCodePp()
3054 {
3055 static $currencyCode = null;
3056
3057 if ($currencyCode !== null) {
3058 return $currencyCode;
3059 }
3060
3061 $currencyCode = VBOFactory::getConfig()->get('currencycodepp', '');
3062
3063 return $currencyCode;
3064 }
3065
3066 public static function getNumberFormatData()
3067 {
3068 // cache value in static var
3069 static $getNumberFormatData = null;
3070
3071 if ($getNumberFormatData) {
3072 return $getNumberFormatData;
3073 }
3074
3075 $getNumberFormatData = VBOFactory::getConfig()->get('numberformat', '');
3076
3077 return $getNumberFormatData;
3078 }
3079
3080 /**
3081 * It is possible to hide the decimals if they are like "N.00".
3082 *
3083 * @return int 0 if disabled, 1 if enabled (default).
3084 *
3085 * @since 1.14 (J) - 1.4.0 (WP)
3086 */
3087 public static function hideEmptyDecimals()
3088 {
3089 // cache value in static var
3090 static $hideEmptyDecimals = null;
3091
3092 if ($hideEmptyDecimals !== null) {
3093 return $hideEmptyDecimals;
3094 }
3095
3096 $hideEmptyDecimals = (int) VBOFactory::getConfig()->get('noemptydecimals', '1');
3097
3098 return $hideEmptyDecimals;
3099 }
3100
3101 public static function numberFormat($num)
3102 {
3103 $formatvals = self::getNumberFormatData();
3104 $formatparts = explode(':', $formatvals);
3105
3106 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
3107 // number has got no decimals
3108 $formatparts[0] = 0;
3109 }
3110
3111 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
3112 }
3113
3114 /**
3115 * Given a date timestamp, returns the formatted date.
3116 *
3117 * @param int|string $ts the date representation in Unix timestamp.
3118 * @param bool $wtime if true, a formatted date-time will be returned.
3119 *
3120 * @return string the formatted date according to settings.
3121 *
3122 * @since 1.16.0 (J) - 1.6.0 (WP)
3123 */
3124 public static function formatDateTs($ts, $wtime = false)
3125 {
3126 $nowdf = self::getDateFormat();
3127 if ($nowdf == "%d/%m/%Y") {
3128 $df = 'd/m/Y';
3129 } elseif ($nowdf == "%m/%d/%Y") {
3130 $df = 'm/d/Y';
3131 } else {
3132 $df = 'Y/m/d';
3133 }
3134 $datesep = self::getDateSeparator();
3135
3136 $time_f = $wtime ? ' H:i' : '';
3137
3138 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
3139 }
3140
3141 /**
3142 * Given a country name, 3-char or 2-char code, this method attempts
3143 * to guess the best language to assign to the booking according to
3144 * what languages are installed on the website. This way, cron jobs
3145 * and any other email notification will be correctly and automatically
3146 * sent to the guest in the proper language without any manual action.
3147 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
3148 *
3149 * @param string $country the country name, 3-char or 2-char code.
3150 * @param string $locale optional locale lang tag of the guest.
3151 *
3152 * @return mixed the best lang-tag string to use or null.
3153 *
3154 * @since 1.16.0 (J) - 1.6.0 (WP)
3155 */
3156 public static function guessBookingLangFromCountry($country, $locale = null)
3157 {
3158 if (empty($country)) {
3159 return null;
3160 }
3161
3162 // get all the available languages
3163 $known_langs = self::getVboApplication()->getKnownLanguages();
3164 if (!is_array($known_langs) || !count($known_langs)) {
3165 return null;
3166 }
3167
3168 // check if the booking included a supported "locale" for the guest
3169 if (!empty($locale)) {
3170 // make the locale compatible with the language tags format
3171 $locale = str_replace('_', '-', $locale);
3172 foreach ($known_langs as $ltag => $ldet) {
3173 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
3174 // we support this language tag, so we just use it
3175 return $ltag;
3176 }
3177 }
3178 }
3179
3180 // build similarities with country-languages
3181 $similarities = array(
3182 'AU' => 'en',
3183 'GB' => 'en',
3184 'IE' => 'en',
3185 'NZ' => 'en',
3186 'US' => 'en',
3187 'CA' => array(
3188 'en',
3189 'fr',
3190 ),
3191 'CL' => 'es',
3192 'AR' => 'es',
3193 'PE' => 'es',
3194 'MX' => 'es',
3195 'CR' => 'es',
3196 'CO' => 'es',
3197 'EC' => 'es',
3198 'BO' => 'es',
3199 'CU' => 'es',
3200 'VE' => 'es',
3201 'BE' => 'fr',
3202 'LU' => 'fr',
3203 'CH' => array(
3204 'de',
3205 'it',
3206 'fr',
3207 ),
3208 'AT' => 'de',
3209 'GR' => 'el',
3210 'GL' => 'dk',
3211 );
3212
3213 // fetch values from db
3214 $dbo = JFactory::getDbo();
3215 $q = "SELECT `country_name`, `country_3_code`, `country_2_code` FROM `#__vikbooking_countries` WHERE `country_name`=" . $dbo->quote($country) . " OR `country_3_code`=" . $dbo->quote($country) . " OR `country_2_code`=" . $dbo->quote($country);
3216 $dbo->setQuery($q, 0, 1);
3217 $dbo->execute();
3218 if (!$dbo->getNumRows()) {
3219 return null;
3220 }
3221 $country_record = $dbo->loadAssoc();
3222
3223 // assign country name/code versions
3224 $country_name = $country_record['country_name'];
3225 $country_3char = strtoupper($country_record['country_3_code']);
3226 $country_2char = strtoupper($country_record['country_2_code']);
3227
3228 // build an associative array of language tags and related match-score
3229 $langtags_score = array();
3230 foreach ($known_langs as $ltag => $ldet) {
3231 // default language tag score is 0 for no matches
3232 $langtags_score[$ltag] = 0;
3233 // get language and country codes
3234 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
3235
3236 // check matches with the installed language details
3237 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
3238 // increase language tag score
3239 $langtags_score[$ltag]++;
3240 }
3241 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
3242 // increase language tag score
3243 $langtags_score[$ltag]++;
3244 }
3245 if (!empty($ldet['locale'])) {
3246 // sanitize locale for matching the 2-char code safely
3247 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
3248 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
3249 // increase language tag score
3250 $langtags_score[$ltag]++;
3251 }
3252 }
3253 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
3254 // increase language tag score
3255 $langtags_score[$ltag]++;
3256 }
3257 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
3258 // increase language tag score
3259 $langtags_score[$ltag]++;
3260 }
3261
3262 // check language similarities between countries
3263 if (isset($similarities[$country_2char])) {
3264 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
3265 // check if language tag(s) is available for this spoken language
3266 foreach ($spoken_tags as $spoken_tag) {
3267 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
3268 // increase language tag score
3269 $langtags_score[$ltag]++;
3270 }
3271 }
3272 }
3273 }
3274
3275 // make sure at least one language tag has got some points
3276 if (max($langtags_score) === 0) {
3277 // no languages installed to honor this country
3278 return null;
3279 }
3280
3281 // sort language tag scores
3282 arsort($langtags_score);
3283
3284 // reset array pointer to the first (highest) element
3285 reset($langtags_score);
3286
3287 // return the language tag with the highest score
3288 return key($langtags_score);
3289 }
3290
3291 public static function getIntroMain($vbo_tn = null) {
3292 $dbo = JFactory::getDbo();
3293 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
3294 $dbo->setQuery($q);
3295 $dbo->execute();
3296 $ft = $dbo->loadAssocList();
3297 if (!is_object($vbo_tn)) {
3298 $vbo_tn = self::getTranslator();
3299 }
3300 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3301 return $ft[0]['setting'];
3302 }
3303
3304 public static function getClosingMain($vbo_tn = null) {
3305 $dbo = JFactory::getDbo();
3306 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3307 $dbo->setQuery($q);
3308 $dbo->execute();
3309 $ft = $dbo->loadAssocList();
3310 if (!is_object($vbo_tn)) {
3311 $vbo_tn = self::getTranslator();
3312 }
3313 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3314 return $ft[0]['setting'];
3315 }
3316
3317 public static function getFullFrontTitle($vbo_tn = null) {
3318 $dbo = JFactory::getDbo();
3319 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3320 $dbo->setQuery($q);
3321 $dbo->execute();
3322 $ft = $dbo->loadAssocList();
3323 if (!is_object($vbo_tn)) {
3324 $vbo_tn = self::getTranslator();
3325 }
3326 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3327 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3328 $dbo->setQuery($q);
3329 $dbo->execute();
3330 $fttag = $dbo->loadAssocList();
3331 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3332 $dbo->setQuery($q);
3333 $dbo->execute();
3334 $fttagclass = $dbo->loadAssocList();
3335 if (empty($ft[0]['setting'])) {
3336 return "";
3337 } else {
3338 if (empty($fttag[0]['setting'])) {
3339 return $ft[0]['setting'] . "<br/>\n";
3340 } else {
3341 $tag = str_replace("<", "", $fttag[0]['setting']);
3342 $tag = str_replace(">", "", $tag);
3343 $tag = str_replace("/", "", $tag);
3344 $tag = trim($tag);
3345 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3346 }
3347 }
3348 }
3349
3350 /**
3351 * Tells whether a date string is valid.
3352 *
3353 * @param ?string $date The date to evaluate.
3354 *
3355 * @return bool
3356 *
3357 * @since 1.18.8 (J) - 1.8.8 (WP) Military format Y-m-d is always accepted.
3358 */
3359 public static function dateIsValid(?string $date)
3360 {
3361 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", (string) $date)) {
3362 // date is in Y-m-d format (with no time)
3363 return true;
3364 }
3365
3366 $df = self::getDateFormat();
3367 $datesep = self::getDateSeparator();
3368 if (strlen((string) $date) != 10) {
3369 return false;
3370 }
3371 $cur_dsep = "/";
3372 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3373 $cur_dsep = $datesep;
3374 }
3375 $x = explode($cur_dsep, $date);
3376 if ($df == "%d/%m/%Y") {
3377 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3378 return false;
3379 }
3380 } elseif ($df == "%m/%d/%Y") {
3381 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3382 return false;
3383 }
3384 } else {
3385 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3386 return false;
3387 }
3388 }
3389 return true;
3390 }
3391
3392 public static function sayDateFormat() {
3393 $dbo = JFactory::getDbo();
3394 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3395 $dbo->setQuery($q);
3396 $dbo->execute();
3397 $s = $dbo->loadAssocList();
3398 if ($s[0]['setting'] == "%d/%m/%Y") {
3399 return JText::translate('VBCONFIGONETWELVE');
3400 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3401 return JText::translate('VBCONFIGONEMDY');
3402 } else {
3403 return JText::translate('VBCONFIGONETENTHREE');
3404 }
3405 }
3406
3407 /**
3408 * Calculates the Unix timestamp from the given date and
3409 * time. Avoids DST issues thanks to mktime. With prior releases,
3410 * DST issues may occur due to the sum of seconds.
3411 *
3412 * @param string $date the date string formatted with the current settings
3413 * @param int $h hours from 0 to 23 for check-in/check-out
3414 * @param int $m minutes from 0 to 59 for check-in/check-out
3415 * @param int $s seconds from 0 to 59 for check-in/check-out
3416 *
3417 * @return int the Unix timestamp of the date
3418 *
3419 * @since 1.0.14
3420 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3421 * signature modified for $h and $m that now take 0 as default value.
3422 */
3423 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3424 $df = self::getDateFormat();
3425 $datesep = self::getDateSeparator();
3426 $cur_dsep = "/";
3427 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3428 $cur_dsep = $datesep;
3429 }
3430 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3431 // date is in Y-m-d format (with no time)
3432 $cur_dsep = '-';
3433 $df = "%Y/%m/%d";
3434 }
3435 $x = explode($cur_dsep, $date);
3436 if (!(count($x) > 2)) {
3437 return 0;
3438 }
3439 if ($df == "%d/%m/%Y") {
3440 $month = (int)$x[1];
3441 $mday = (int)$x[0];
3442 $year = (int)$x[2];
3443 } elseif ($df == "%m/%d/%Y") {
3444 $month = (int)$x[0];
3445 $mday = (int)$x[1];
3446 $year = (int)$x[2];
3447 } else {
3448 $month = (int)$x[1];
3449 $mday = (int)$x[2];
3450 $year = (int)$x[0];
3451 }
3452 $h = empty($h) ? 0 : (int)$h;
3453 $m = empty($m) ? 0 : (int)$m;
3454 $s = $s > 0 && $s <= 59 ? $s : 0;
3455
3456 return mktime($h, $m, $s, $month, $mday, $year);
3457 }
3458
3459 public static function ivaInclusa($skipsession = false) {
3460 // cache value in static var
3461 static $getTaxIncluded = null;
3462
3463 if ($getTaxIncluded !== null) {
3464 return (bool)$getTaxIncluded;
3465 }
3466
3467 $dbo = JFactory::getDbo();
3468
3469 if ($skipsession) {
3470 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3471 $dbo->setQuery($q, 0, 1);
3472 $dbo->execute();
3473 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3474
3475 // cache value and return it
3476 $getTaxIncluded = $vat_incl_data;
3477
3478 return (bool)$vat_incl_data;
3479 }
3480
3481 $session = JFactory::getSession();
3482 $sval = $session->get('getTaxIncluded', '');
3483 if (strlen($sval)) {
3484 return (bool)$sval;
3485 }
3486
3487 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3488 $dbo->setQuery($q, 0, 1);
3489 $dbo->execute();
3490 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3491
3492 // update session value, cache it and return it
3493 $session->set('getTaxIncluded', $vat_incl_data);
3494 $getTaxIncluded = $vat_incl_data;
3495
3496 return (bool)$vat_incl_data;
3497 }
3498
3499 public static function showTaxOnSummaryOnly($skipsession = false) {
3500 if ($skipsession) {
3501 $dbo = JFactory::getDbo();
3502 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3503 $dbo->setQuery($q);
3504 $dbo->execute();
3505 $s = $dbo->loadAssocList();
3506 return (intval($s[0]['setting']) == 1 ? true : false);
3507 } else {
3508 $session = JFactory::getSession();
3509 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3510 if (strlen($sval) > 0) {
3511 return (intval($sval) == 1 ? true : false);
3512 } else {
3513 $dbo = JFactory::getDbo();
3514 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3515 $dbo->setQuery($q);
3516 $dbo->execute();
3517 $s = $dbo->loadAssocList();
3518 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3519 return (intval($s[0]['setting']) == 1 ? true : false);
3520 }
3521 }
3522 }
3523
3524 public static function tokenForm() {
3525 $dbo = JFactory::getDbo();
3526 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3527 $dbo->setQuery($q);
3528 $dbo->execute();
3529 $s = $dbo->loadAssocList();
3530 return (intval($s[0]['setting']) == 1 ? true : false);
3531 }
3532
3533 public static function getPaypalAcc() {
3534 $dbo = JFactory::getDbo();
3535 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3536 $dbo->setQuery($q);
3537 $dbo->execute();
3538 $s = $dbo->loadAssocList();
3539 return $s[0]['setting'];
3540 }
3541
3542 public static function getAccPerCent() {
3543 $dbo = JFactory::getDbo();
3544 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3545 $dbo->setQuery($q);
3546 $dbo->execute();
3547 $s = $dbo->loadAssocList();
3548 return $s[0]['setting'];
3549 }
3550
3551 public static function getTypeDeposit($skipsession = false) {
3552 if ($skipsession) {
3553 $dbo = JFactory::getDbo();
3554 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3555 $dbo->setQuery($q);
3556 $dbo->execute();
3557 $s = $dbo->loadAssocList();
3558 return $s[0]['setting'];
3559 } else {
3560 $session = JFactory::getSession();
3561 $sval = $session->get('getTypeDeposit', '');
3562 if (strlen($sval) > 0) {
3563 return $sval;
3564 } else {
3565 $dbo = JFactory::getDbo();
3566 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3567 $dbo->setQuery($q);
3568 $dbo->execute();
3569 $s = $dbo->loadAssocList();
3570 $session->set('getTypeDeposit', $s[0]['setting']);
3571 return $s[0]['setting'];
3572 }
3573 }
3574 }
3575
3576 public static function multiplePayments()
3577 {
3578 return VBOFactory::getConfig()->getBool('multipay', false);
3579 }
3580
3581 public static function getAdminMail()
3582 {
3583 return VBOFactory::getConfig()->getString('adminemail', '');
3584 }
3585
3586 public static function getSenderMail()
3587 {
3588 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3589
3590 return empty($sender) ? self::getAdminMail() : $sender;
3591 }
3592
3593 public static function getPaymentName($vbo_tn = null) {
3594 $dbo = JFactory::getDbo();
3595 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3596 $dbo->setQuery($q);
3597 $dbo->execute();
3598 $s = $dbo->loadAssocList();
3599 if (!is_object($vbo_tn)) {
3600 $vbo_tn = self::getTranslator();
3601 }
3602 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3603 return $s[0]['setting'];
3604 }
3605
3606 public static function getTermsConditions($vbo_tn = null) {
3607 //VBO 1.10
3608 $dbo = JFactory::getDbo();
3609 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3610 $dbo->setQuery($q);
3611 $dbo->execute();
3612 if ($dbo->getNumRows() > 0) {
3613 $s = $dbo->loadAssocList();
3614 if (!is_object($vbo_tn)) {
3615 $vbo_tn = self::getTranslator();
3616 }
3617 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3618 } else {
3619 //the record has never been saved. Compose it with the default lang definition
3620 $timeopst = self::getTimeOpenStore(true);
3621 if (is_array($timeopst)) {
3622 $openat = self::getHoursMinutes($timeopst[0]);
3623 $closeat = self::getHoursMinutes($timeopst[1]);
3624 } else {
3625 $openat = array(12, 0);
3626 $closeat = array(10, 0);
3627 }
3628 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3629 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3630 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3631 }
3632
3633 return $s[0]['setting'];
3634 }
3635
3636 public static function getMinutesLock($conv = false)
3637 {
3638 static $minutesLock = null;
3639
3640 if ($minutesLock === null) {
3641 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3642 }
3643
3644 if (!$minutesLock) {
3645 return 0;
3646 }
3647
3648 if ($conv) {
3649 return (time() + ($minutesLock * 60));
3650 }
3651
3652 return $minutesLock;
3653 }
3654
3655 /**
3656 * Checks if the room units are temporarily locked on the given dates.
3657 *
3658 * @param int $idroom the room ID.
3659 * @param int $units the total number of room units.
3660 * @param int $first the check-in timestamp.
3661 * @param int $second the check-out timestamp.
3662 * @param bool $occupied if true, the system will also consider the booked units.
3663 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3664 *
3665 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3666 *
3667 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3668 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3669 */
3670 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3671 {
3672 $dbo = JFactory::getDbo();
3673
3674 if ($units < 1) {
3675 return false;
3676 }
3677
3678 $actnow = time();
3679
3680 // clean up expired records
3681 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3682 $dbo->setQuery($q);
3683 $dbo->execute();
3684
3685 $secdiff = $second - $first;
3686 $daysdiff = $secdiff / 86400;
3687 if (is_int($daysdiff)) {
3688 if ($daysdiff < 1) {
3689 $daysdiff = 1;
3690 }
3691 } else {
3692 if ($daysdiff < 1) {
3693 $daysdiff = 1;
3694 } else {
3695 $sum = floor($daysdiff) * 86400;
3696 $newdiff = $secdiff - $sum;
3697 $maxhmore = self::getHoursMoreRb() * 3600;
3698 if ($maxhmore >= $newdiff) {
3699 $daysdiff = floor($daysdiff);
3700 } else {
3701 $daysdiff = ceil($daysdiff);
3702 }
3703 }
3704 }
3705 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3706 $tmplock_map = [];
3707
3708 // check temporarily locked records
3709 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3710 $dbo->setQuery($check);
3711 $busy = $dbo->loadAssocList();
3712 if ($busy) {
3713 foreach ($groupdays as $kg => $gday) {
3714 $bfound = 0;
3715 foreach ($busy as $bu) {
3716 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3717 $bfound++;
3718 }
3719 }
3720 $tmplock_map[$kg] = $bfound;
3721 if ($bfound >= $units) {
3722 return false;
3723 }
3724 }
3725 }
3726
3727 if ($occupied) {
3728 // check also the busy records and some the occupied units to the temporarily locked ones
3729 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3730 $dbo->setQuery($check);
3731 $busy = $dbo->loadAssocList();
3732 if (!$busy) {
3733 return true;
3734 }
3735
3736 foreach ($groupdays as $kg => $gday) {
3737 $bfound = 0;
3738 foreach ($busy as $bu) {
3739 if (in_array($bu['id'], $skip_busy_ids)) {
3740 // Booking modification
3741 continue;
3742 }
3743 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3744 $bfound++;
3745 }
3746 }
3747 // sum units occupied to the units temporarily locked
3748 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3749 if ($bfound >= $units) {
3750 return false;
3751 }
3752 }
3753 }
3754
3755 return true;
3756 }
3757
3758 public static function getGroupDays($first, $second, $daysdiff) {
3759 $ret = array();
3760 $ret[] = $first;
3761 if ($daysdiff > 1) {
3762 $start = getdate($first);
3763 $end = getdate($second);
3764 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3765 for($i = 1; $i < $daysdiff; $i++) {
3766 $checkday = $start['mday'] + $i;
3767 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3768 if ($dayts != $endcheck) {
3769 $ret[] = $dayts;
3770 }
3771 }
3772 }
3773 $ret[] = $second;
3774 return $ret;
3775 }
3776
3777 /**
3778 * Counts the hours of difference between the current
3779 * server time and the selected check-in date and time.
3780 *
3781 * @param int $checkin_ts
3782 * @param [int] $now_ts
3783 *
3784 * @return int
3785 */
3786 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3787 $hoursdiff = 0;
3788
3789 if (empty($now_ts)) {
3790 $now_ts = time();
3791 }
3792
3793 if ($now_ts >= $checkin_ts) {
3794 return $hoursdiff;
3795 }
3796
3797 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3798
3799 return $hoursdiff;
3800 }
3801
3802 /**
3803 * Loads all the occupied records for the given rooms and offset.
3804 *
3805 * @param array $roomids the optional list of room IDs to filter.
3806 * @param int $from_ts the optional base date to use.
3807 * @param int $max_ts the optional max date to use.
3808 * @param bool $closures whether to ignore, include or exclude closures.
3809 *
3810 * @return array
3811 *
3812 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3813 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3814 */
3815 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3816 {
3817 $base_ts = empty($from_ts) ? time() : $from_ts;
3818 $busy = [];
3819 $clauses = [];
3820
3821 if (is_array($roomids) && count($roomids)) {
3822 // filter busy records only by the requested room ids
3823 $roomids = array_map(function($rid) {
3824 return (int)$rid;
3825 }, $roomids);
3826 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3827 }
3828
3829 // exclude past reservations
3830 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3831
3832 if (!empty($max_ts)) {
3833 // exclude future reservations (recommended for large datasets)
3834 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3835 }
3836
3837 if (is_bool($closures)) {
3838 // include or exclude closures
3839 if ($closures) {
3840 $clauses[] = "`o`.`closure` = 1";
3841 } else {
3842 $clauses[] = "`o`.`closure` != 1";
3843 }
3844 }
3845
3846 $dbo = JFactory::getDbo();
3847
3848 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3849 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3850 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3851 WHERE " . implode(' AND ', $clauses) . ";";
3852 $dbo->setQuery($q);
3853 $allbusy = $dbo->loadAssocList();
3854 if ($allbusy) {
3855 foreach ($allbusy as $br) {
3856 if (!isset($busy[$br['idroom']])) {
3857 $busy[$br['idroom']] = [];
3858 }
3859 $busy[$br['idroom']][] = $br;
3860 }
3861 }
3862
3863 return $busy;
3864 }
3865
3866 /**
3867 * Loads all the busy records by excluding the closures.
3868 * Built for the page dashboard to calculate the actual
3869 * rooms occupancy by excluding the rooms closed.
3870 *
3871 * @param array $roomids the optional list of room IDs to filter.
3872 * @param int $from_ts the optional base date to use.
3873 * @param int $max_ts the optional max date to use.
3874 *
3875 * @return array the list of busy records.
3876 *
3877 * @since 1.12
3878 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3879 */
3880 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3881 {
3882 if (!is_array($roomids) || !count($roomids)) {
3883 return [];
3884 }
3885
3886 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3887 }
3888
3889 /**
3890 * Loads the occupied record IDs for a given booking ID.
3891 *
3892 * @param int $idorder The booking record ID.
3893 *
3894 * @return array List of busy IDs, if any.
3895 */
3896 public static function loadBookingBusyIds($idorder)
3897 {
3898 if (empty($idorder)) {
3899 return [];
3900 }
3901
3902 $dbo = JFactory::getDbo();
3903
3904 $dbo->setQuery(
3905 $dbo->getQuery(true)
3906 ->select($dbo->qn('idbusy'))
3907 ->from($dbo->qn('#__vikbooking_ordersbusy'))
3908 ->where($dbo->qn('idorder') . ' = ' . (int) $idorder)
3909 );
3910
3911 return array_map('intval', array_column($dbo->loadAssocList(), 'idbusy'));
3912 }
3913
3914 public static function loadLockedRecords($roomids, $ts = 0) {
3915 $dbo = JFactory::getDbo();
3916 $actnow = empty($ts) ? time() : $ts;
3917 $locked = array();
3918 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3919 $dbo->setQuery($q);
3920 $dbo->execute();
3921 if (!is_array($roomids) || !(count($roomids) > 0)) {
3922 return $locked;
3923 }
3924 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3925 $dbo->setQuery($check);
3926 $all_locked = $dbo->loadAssocList();
3927 if ($all_locked) {
3928 foreach ($all_locked as $kb => $br) {
3929 $locked[$br['idroom']][$kb] = $br;
3930 }
3931 }
3932 return $locked;
3933 }
3934
3935 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3936 $bookings = [];
3937 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3938 return $bookings;
3939 }
3940 $dbo = JFactory::getDbo();
3941 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3942 $dbo->setQuery($q);
3943 $all_booking_ids = $dbo->loadAssocList();
3944 if ($all_booking_ids) {
3945 $oids = array();
3946 foreach ($all_booking_ids as $bid) {
3947 $oids[] = $bid['idorder'];
3948 }
3949 $q = "SELECT `or`.`idorder`,CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`) AS `nominative`,`or`.`roomindex`,`o`.`status`,`o`.`days`,`o`.`checkout`,`o`.`custdata`,`o`.`country`,`o`.`closure`,`o`.`checked` ".
3950 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3951 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3952 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3953 "ORDER BY `o`.`checkout` ASC;";
3954 $dbo->setQuery($q);
3955 $bookings = $dbo->loadAssocList();
3956 if (!$bookings) {
3957 $bookings = [];
3958 }
3959 }
3960 return $bookings;
3961 }
3962
3963 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3964 {
3965 $dbo = JFactory::getDbo();
3966
3967 if ($units < 1) {
3968 return false;
3969 }
3970
3971 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3972 if (!$room_info) {
3973 return false;
3974 }
3975
3976 $secdiff = $second - $first;
3977 $daysdiff = $secdiff / 86400;
3978 if (is_int($daysdiff)) {
3979 if ($daysdiff < 1) {
3980 $daysdiff = 1;
3981 }
3982 } else {
3983 if ($daysdiff < 1) {
3984 $daysdiff = 1;
3985 } else {
3986 $sum = floor($daysdiff) * 86400;
3987 $newdiff = $secdiff - $sum;
3988 $maxhmore = self::getHoursMoreRb() * 3600;
3989 if ($maxhmore >= $newdiff) {
3990 $daysdiff = floor($daysdiff);
3991 } else {
3992 $daysdiff = ceil($daysdiff);
3993 }
3994 }
3995 }
3996
3997 $q = $dbo->getQuery(true)
3998 ->select($dbo->qn([
3999 'id',
4000 'checkin',
4001 'realback',
4002 ]))
4003 ->from($dbo->qn('#__vikbooking_busy'))
4004 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
4005 ->where($dbo->qn('realback') . ' >= ' . $first);
4006
4007 $dbo->setQuery($q);
4008 $busy = $dbo->loadAssocList();
4009 if (!$busy) {
4010 return ($units <= ($room_info['units'] ?? 1));
4011 }
4012
4013 $groupdays = self::getGroupDays($first, $second, $daysdiff);
4014 foreach ($groupdays as $gday) {
4015 $bfound = 0;
4016 foreach ($busy as $bu) {
4017 if (in_array($bu['id'], $skip_busy_ids)) {
4018 // booking modification
4019 continue;
4020 }
4021 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
4022 $bfound++;
4023 }
4024 }
4025 if ($bfound >= $units) {
4026 return false;
4027 }
4028 }
4029
4030 return ($units <= ($room_info['units'] ?? 1));
4031 }
4032
4033 public static function payTotal()
4034 {
4035 return VBOFactory::getConfig()->getBool('paytotal', true);
4036 }
4037
4038 public static function getDepositIfDays()
4039 {
4040 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
4041 }
4042
4043 public static function depositAllowedDaysAdv($checkints) {
4044 $days_adv = self::getDepositIfDays();
4045 if (!($days_adv > 0) || !($checkints > 0)) {
4046 return true;
4047 }
4048 $now_info = getdate();
4049 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
4050 return $maxts > $checkints ? false : true;
4051 }
4052
4053 public static function depositCustomerChoice() {
4054 $dbo = JFactory::getDbo();
4055 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
4056 $dbo->setQuery($q);
4057 $dbo->execute();
4058 $s = $dbo->loadAssocList();
4059 return (intval($s[0]['setting']) == 1 ? true : false);
4060 }
4061
4062 public static function getDepositOverrides($getjson = false) {
4063 $dbo = JFactory::getDbo();
4064 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
4065 $dbo->setQuery($q);
4066 $dbo->execute();
4067 if ($dbo->getNumRows() > 0) {
4068 $s = $dbo->loadResult();
4069 return $getjson ? $s : json_decode($s, true);
4070 }
4071 //count of this array will be at least 1 to store the "more" property
4072 $def_arr = array('more' => '');
4073 $def_val = json_encode($def_arr);
4074 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
4075 $dbo->setQuery($q);
4076 $dbo->execute();
4077 return $getjson ? $def_val : $def_arr;
4078 }
4079
4080 public static function calcDepositOverride($amount_deposit, $nights) {
4081 $overrides = self::getDepositOverrides();
4082 $nights = intval($nights);
4083 $andmore = intval($overrides['more']);
4084 if (!(count($overrides) > 1)) {
4085 //no overrides
4086 return $amount_deposit;
4087 }
4088 foreach ($overrides as $k => $v) {
4089 if ((int)$k == $nights && strlen($v) > 0) {
4090 //exact override found
4091 return (float)$v;
4092 }
4093 }
4094 if ($andmore > 0 && $andmore <= $nights) {
4095 foreach ($overrides as $k => $v) {
4096 if ((int)$k == $andmore && strlen($v) > 0) {
4097 //"and more" nights found
4098 return (float)$v;
4099 }
4100 }
4101 }
4102 //nothing was found
4103 return $amount_deposit;
4104 }
4105
4106 public static function noDepositForNonRefund() {
4107 $dbo = JFactory::getDbo();
4108 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
4109 $dbo->setQuery($q);
4110 $dbo->execute();
4111 if ($dbo->getNumRows() > 0) {
4112 $s = $dbo->loadResult();
4113 return ((int)$s == 1);
4114 }
4115 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
4116 $dbo->setQuery($q);
4117 $dbo->execute();
4118 //default to false
4119 return false;
4120 }
4121
4122 /**
4123 * This method returns the room-rate array with the lowest price
4124 * that matches the preferred rate plan parameter (if available).
4125 * The array $website_rates could not be an array, or it could be
4126 * an array with the error string (response from the TACVBO Class).
4127 * The method has been introduced in VBO 1.10 and it's mainly used
4128 * by the module mod_vikbooking_channelrates and its ajax requests.
4129 *
4130 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
4131 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
4132 *
4133 * @return array
4134 */
4135 public static function getBestRoomRate($website_rates, $def_rplan) {
4136 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
4137 return array();
4138 }
4139 $best_room_rate = array();
4140 foreach ($website_rates as $rid => $tars) {
4141 foreach ($tars as $tar) {
4142 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
4143 //the array $website_rates is already sorted by price ASC, so we take the first useful array
4144 $best_room_rate = $tar;
4145 break 2;
4146 }
4147 }
4148 }
4149 if (!(count($best_room_rate) > 0)) {
4150 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
4151 foreach ($website_rates as $rid => $tars) {
4152 foreach ($tars as $tar) {
4153 $best_room_rate = $tar;
4154 break 2;
4155 }
4156 }
4157 }
4158
4159 return $best_room_rate;
4160 }
4161
4162 /**
4163 * This method returns an array with the details
4164 * of all channels in VCM that supports AV requests,
4165 * and that have at least one room type mapped.
4166 * The method invokes the Logos Class to return details
4167 * about the name and logo URL of the channel.
4168 * The method has been introduced in VBO 1.10 and it's mainly used
4169 * by the module mod_vikbooking_channelrates and its ajax requests.
4170 *
4171 * @param array $channels an array of channel IDs to be mapped on the VCM relations
4172 *
4173 * @return array
4174 */
4175 public static function getChannelsMap($channels) {
4176 if (!is_array($channels) || !(count($channels))) {
4177 return array();
4178 }
4179 $vcm_logos = self::getVcmChannelsLogo('', true);
4180 if (!is_object($vcm_logos)) {
4181 return array();
4182 }
4183 $channels_ids = array();
4184 foreach ($channels as $chid) {
4185 $ichid = (int)$chid;
4186 if ($ichid < 1) {
4187 continue;
4188 }
4189 array_push($channels_ids, $ichid);
4190 }
4191 if (!(count($channels_ids))) {
4192 return array();
4193 }
4194 $dbo = JFactory::getDbo();
4195 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
4196 $dbo->setQuery($q);
4197 $dbo->execute();
4198 if ($dbo->getNumRows() < 1) {
4199 return array();
4200 }
4201 $channels_names = $dbo->loadAssocList();
4202 $channels_map = array();
4203 foreach ($channels_names as $ch) {
4204 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
4205 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
4206 $chdata = array(
4207 'id' => $ch['idchannel'],
4208 'name' => ucwords($ch['channel']),
4209 'logo' => $ota_logo_url
4210 );
4211 array_push($channels_map, $chdata);
4212 }
4213 return $channels_map;
4214 }
4215
4216 /**
4217 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
4218 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
4219 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
4220 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
4221 * module/widget and its ajax requests.
4222 *
4223 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
4224 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
4225 *
4226 * @return mixed string (empty or indicating the alteration), or associative array per channel.
4227 *
4228 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
4229 */
4230 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
4231 {
4232 $otas_rates_val = '';
4233 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
4234 return $otas_rates_val;
4235 }
4236 if (!class_exists('VikChannelManager')) {
4237 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
4238 }
4239 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
4240 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
4241 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
4242 // the Bulk Rates Cache contains data for this room type and rate plan
4243 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
4244 // build rates modification string
4245 if ((int)$data_cont['rmod'] > 0) {
4246 // rates were modified for the OTAs, check how
4247 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
4248 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
4249 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
4250 // check alterations per channel, if requested
4251 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
4252 // we compose an associative array with the alteration for each channel (0 = generic rule)
4253 $channel_alterations = array($otas_rates_val);
4254 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
4255 if ((int)$ch_mods['rmod'] > 0) {
4256 // custom rate alteration for this channel
4257 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
4258 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
4259 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
4260 } else {
4261 // no rates alteration for this channel identifier
4262 $ch_alter_string = '+0%';
4263 }
4264 $channel_alterations[$ch_id] = $ch_alter_string;
4265 }
4266 // overwrite return value with associative array
4267 $otas_rates_val = $channel_alterations;
4268 }
4269 }
4270 }
4271 }
4272
4273 return $otas_rates_val;
4274 }
4275
4276 /**
4277 * This method checks if some non-refundable rates were selected
4278 * (`free_cancellation`=0), the only argument is an array of tariffs.
4279 * The property 'idprice' must be defined on each sub-array.
4280 *
4281 * @param $tars array
4282 *
4283 * @return boolean
4284 **/
4285 public static function findNonRefundableRates($tars = [])
4286 {
4287 $id_prices = [];
4288
4289 foreach ($tars as $tar) {
4290 if (isset($tar['idprice'])) {
4291 if (!in_array($tar['idprice'], $id_prices)) {
4292 array_push($id_prices, (int)$tar['idprice']);
4293 }
4294 continue;
4295 }
4296 foreach ($tar as $t) {
4297 if (isset($t['idprice'])) {
4298 if (!in_array($t['idprice'], $id_prices)) {
4299 array_push($id_prices, (int)$t['idprice']);
4300 }
4301 }
4302 }
4303 }
4304
4305 if (!count($id_prices)) {
4306 // no price IDs found (probably a package)
4307 return false;
4308 }
4309
4310 $dbo = JFactory::getDbo();
4311
4312 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
4313 $dbo->setQuery($q);
4314 $dbo->execute();
4315
4316 return (bool)($dbo->getNumRows() > 0);
4317 }
4318
4319 /**
4320 * This method checks if the deposit is allowed depending on
4321 * the selected rate plans (idprice) for the rooms reserved.
4322 * If the configuration setting is enabled, and if some
4323 * non-refundable rates were selected (`free_cancellation`=0),
4324 * the method will return false, because the deposit is not allowed.
4325 * The only argument is an array of tariffs. The property 'idprice'
4326 * must be defined on each sub-array (multi-dimension supported)
4327 * throgh the method findNonRefundableRates();
4328 *
4329 * @param $tars array
4330 *
4331 * @return boolean
4332 **/
4333 public static function allowDepositFromRates($tars) {
4334 if (!self::noDepositForNonRefund()) {
4335 //deposit can be paid also if non-refundable rates
4336 return true;
4337 }
4338 return !self::findNonRefundableRates($tars);
4339 }
4340
4341 public static function showSearchSuggestions() {
4342 $dbo = JFactory::getDbo();
4343 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4344 $dbo->setQuery($q);
4345 $dbo->execute();
4346 if ($dbo->getNumRows() > 0) {
4347 return (int)$dbo->loadResult();
4348 }
4349 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4350 $dbo->setQuery($q);
4351 $dbo->execute();
4352 return 1;
4353 }
4354
4355 /**
4356 * Given a code, returns the coupon information array.
4357 *
4358 * @param string $code the coupon code.
4359 *
4360 * @return array empty array or coupon record.
4361 *
4362 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4363 */
4364 public static function getCouponInfo($code)
4365 {
4366 $dbo = JFactory::getDbo();
4367
4368 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4369 $dbo->setQuery($q, 0, 1);
4370 $coupon = $dbo->loadAssoc();
4371 if (!$coupon) {
4372 return [];
4373 }
4374
4375 // check for customers assigned
4376 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4377 $dbo->setQuery($q);
4378 $coupon_customers = $dbo->loadAssocList();
4379 if ($coupon_customers) {
4380 // set customers and automatic properties
4381 $coupon['customers'] = [];
4382 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4383 foreach ($coupon_customers as $coupon_customer) {
4384 // push customer ID
4385 $coupon['customers'][] = $coupon_customer['idcustomer'];
4386 }
4387 }
4388
4389 return $coupon;
4390 }
4391
4392 /**
4393 * Reads the details of a given room record ID.
4394 *
4395 * @param int $idroom The room ID to fetch.
4396 * @param array $columns The optional room columns to fetch.
4397 * @param bool $no_cache Whether to ignore a previously cached record.
4398 *
4399 * @return array
4400 *
4401 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4402 */
4403 public static function getRoomInfo($idroom, $columns = [], $no_cache = false)
4404 {
4405 static $room_infos = [];
4406
4407 if (isset($room_infos[$idroom]) && !$no_cache) {
4408 return $room_infos[$idroom];
4409 }
4410
4411 $dbo = JFactory::getDbo();
4412
4413 if ($columns) {
4414 $columns = array_map([$dbo, 'qn'], $columns);
4415 }
4416
4417 $q = $dbo->getQuery(true)
4418 ->select($columns ? $columns : '*')
4419 ->from($dbo->qn('#__vikbooking_rooms'))
4420 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4421
4422 $dbo->setQuery($q);
4423 $room = $dbo->loadAssoc();
4424
4425 if ($no_cache) {
4426 return $room ?: [];
4427 }
4428
4429 $room_infos[$idroom] = $room ?: [];
4430
4431 return $room_infos[$idroom];
4432 }
4433
4434 public static function loadOrdersRoomsData($idorder)
4435 {
4436 $dbo = JFactory::getDbo();
4437
4438 $q = $dbo->getQuery(true)
4439 ->select($dbo->qn('or') . '.*')
4440 ->select($dbo->qn('r.name', 'room_name'))
4441 ->select($dbo->qn('r.units', 'tot_units'))
4442 ->select($dbo->qn('r.params'))
4443 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4444 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4445 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4446 ->order($dbo->qn('or.id') . ' ASC');
4447
4448 $dbo->setQuery($q);
4449
4450 return $dbo->loadAssocList();
4451 }
4452
4453 public static function sayCategory($ids, $vbo_tn = null)
4454 {
4455 $dbo = JFactory::getDbo();
4456 $split = explode(";", $ids);
4457 $say = "";
4458 foreach ($split as $k => $s) {
4459 if (!strlen($s)) {
4460 continue;
4461 }
4462 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4463 $dbo->setQuery($q);
4464 $nam = $dbo->loadAssocList();
4465 if (!$nam) {
4466 continue;
4467 }
4468 if (is_object($vbo_tn)) {
4469 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4470 }
4471 $say .= $nam[0]['name'];
4472 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4473 }
4474 return $say;
4475 }
4476
4477 /**
4478 * Returns a list of the given room amenities string.
4479 *
4480 * @param string $amenities Room amenities string.
4481 * @param ?object $vbo_tn Optional VBOTranslator.
4482 * @param bool $all True for loading all records when no amenities given.
4483 *
4484 * @return array
4485 *
4486 * @since 1.17.3 (J) - 1.7.3 (WP)
4487 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $all.
4488 */
4489 public static function loadRoomAmenities($amenities, $vbo_tn = null, $all = false)
4490 {
4491 $dbo = JFactory::getDbo();
4492
4493 $split = explode(";", (string) $amenities);
4494 $where = [];
4495 foreach ($split as $s) {
4496 if (!empty($s)) {
4497 $where[] = (int) $s;
4498 }
4499 }
4500
4501 $list = [];
4502 $where = array_filter($where);
4503
4504 $query = $dbo->getQuery(true)
4505 ->select('*')
4506 ->from($dbo->qn('#__vikbooking_characteristics'))
4507 ->order($dbo->qn('ordering') . ' ASC');
4508
4509 if ($where) {
4510 $query->where($dbo->qn('id') . ' IN (' . implode(', ', $where) . ')');
4511 }
4512
4513 if ($where || $all) {
4514 $dbo->setQuery($query);
4515 $list = $dbo->loadAssocList();
4516 if ($list && is_object($vbo_tn)) {
4517 $vbo_tn->translateContents($list, '#__vikbooking_characteristics');
4518 }
4519 }
4520
4521 return $list;
4522 }
4523
4524 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4525 {
4526 $carat = '';
4527 $arr = self::loadRoomAmenities($idc, $vbo_tn);
4528
4529 if ($arr) {
4530 $carat .= "<div class=\"vbo-room-carats\">\n";
4531 foreach ($arr as $a) {
4532 if (!empty($a['textimg'])) {
4533 //tooltip icon text is not empty
4534 if (!empty($a['icon'])) {
4535 //an icon has been uploaded: display the image
4536 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"" . htmlspecialchars((string) $a['textimg'], ENT_QUOTES, 'UTF-8') . "\"><img src=\"".VBO_SITE_URI."resources/uploads/" . $a['icon'] . "\" alt=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\" /></span></span>\n";
4537 } else {
4538 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4539 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4540 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\">" . $a['textimg'] . "</span></span>\n";
4541 } else {
4542 //display just the text
4543 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4544 }
4545 }
4546 } else {
4547 $carat .= (!empty($a['icon']) ? "<span class=\"vbo-room-carat\"><img src=\"".VBO_SITE_URI."resources/uploads/" . $a['icon'] . "\" alt=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\" title=\"" . htmlspecialchars((string) $a['name'], ENT_QUOTES, 'UTF-8') . "\"/></span>\n" : "<span class=\"vbo-room-carat\">" . $a['name'] . "</span>\n");
4548 }
4549 }
4550 $carat .= "</div>\n";
4551 }
4552
4553 return $carat;
4554 }
4555
4556 public static function getRoomOptionals($idopts, $vbo_tn = null)
4557 {
4558 $dbo = JFactory::getDbo();
4559 $split = explode(";", $idopts);
4560 $fetch = array();
4561 foreach ($split as $s) {
4562 if (!empty($s)) {
4563 $fetch[] = $s;
4564 }
4565 }
4566 if ($fetch) {
4567 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', array_map('intval', $fetch)) . ") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4568 $dbo->setQuery($q);
4569 $arr = $dbo->loadAssocList();
4570 if ($arr) {
4571 if (is_object($vbo_tn)) {
4572 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4573 }
4574 return $arr;
4575 }
4576 }
4577 return [];
4578 }
4579
4580 public static function getSingleOption($idopt, $vbo_tn = null)
4581 {
4582 $dbo = JFactory::getDbo();
4583 $opt = array();
4584 if (!empty($idopt)) {
4585 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4586 $dbo->setQuery($q);
4587 $opt = $dbo->loadAssoc();
4588 if ($opt && is_object($vbo_tn)) {
4589 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4590 }
4591 }
4592 return $opt;
4593 }
4594
4595 /**
4596 * Unsets the options that are not available in the requested dates.
4597 *
4598 * @param array $optionals the array of options passed by reference
4599 * @param int $checkin the timestamp of the check-in date and time
4600 * @param int $checkout the timestamp of the check-out date and time
4601 *
4602 * @return void
4603 *
4604 * @since 1.11
4605 */
4606 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4607 {
4608 if (!$optionals || empty($checkin) || empty($checkout)) {
4609 return;
4610 }
4611
4612 foreach ($optionals as $k => $v) {
4613 if (!empty($v['alwaysav'])) {
4614 $dates = explode(';', $v['alwaysav']);
4615 if (empty($dates[0]) || empty($dates[1])) {
4616 continue;
4617 }
4618 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4619 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4620 unset($optionals[$k]);
4621 }
4622 }
4623 }
4624 }
4625
4626 /**
4627 * Unsets the options that are not suited for the requested room party.
4628 *
4629 * @param array $optionals the array of options passed by reference.
4630 * @param int $adults the number of adults in the room party.
4631 * @param int $children the number of children in the room party.
4632 *
4633 * @return void
4634 *
4635 * @since 1.13.5
4636 */
4637 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4638 {
4639 if (!$optionals || (empty($adults) && empty($children))) {
4640 return;
4641 }
4642
4643 foreach ($optionals as $k => $v) {
4644 if (empty($v['oparams'])) {
4645 continue;
4646 }
4647 $v['oparams'] = json_decode($v['oparams'], true);
4648 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4649 continue;
4650 }
4651 if (!empty($v['oparams']['minguestsnum'])) {
4652 // filter by minimum adults/guests
4653 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4654 // minimum number of adults not sufficient, unset option
4655 unset($optionals[$k]);
4656 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4657 // minimum number of total guests not sufficient, unset option
4658 unset($optionals[$k]);
4659 }
4660 }
4661 if (!empty($v['oparams']['maxguestsnum'])) {
4662 // filter by maximum adults/guests
4663 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4664 // maximum number of adults exceeded, unset option
4665 unset($optionals[$k]);
4666 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4667 // maximum number of total guests exceeded, unset option
4668 unset($optionals[$k]);
4669 }
4670 }
4671 }
4672 }
4673
4674 /**
4675 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4676 *
4677 * @param array $id_rooms list of room IDs.
4678 * @param int $num_adults number of adults.
4679 * @param int $num_nights number of nights of stay.
4680 * @param int $checkin optional check-in timestamp.
4681 * @param int $checkout optional check-out timestamp.
4682 *
4683 * @return array associative information of city taxes, fees and options.
4684 *
4685 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4686 */
4687 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4688 {
4689 $dbo = JFactory::getDbo();
4690
4691 $taxes = 0;
4692 $fees = 0;
4693
4694 $options_data = [];
4695 $id_options = [];
4696
4697 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4698 $dbo->setQuery($q);
4699 $assocs = $dbo->loadAssocList();
4700 if ($assocs) {
4701 foreach ($assocs as $opts) {
4702 if (!empty($opts['idopt'])) {
4703 $r_ido = explode(';', rtrim($opts['idopt']));
4704 foreach ($r_ido as $ido) {
4705 if (!empty($ido) && !in_array($ido, $id_options)) {
4706 $id_options[] = (int)$ido;
4707 }
4708 }
4709 }
4710 }
4711 }
4712
4713 if ($id_options) {
4714 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(", ", $id_options) . ") AND `forcesel`=1 AND `ifchildren`=0 AND (`is_citytax`=1 OR `is_fee`=1);";
4715 $dbo->setQuery($q);
4716 $alltaxesfees = $dbo->loadAssocList();
4717
4718 if ($checkin && $checkout) {
4719 // filter the records by stay dates to support, for example, seasonal city taxes
4720 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4721 }
4722
4723 foreach ($alltaxesfees as $tf) {
4724 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4725 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4726 $realcost = $tf['maxprice'];
4727 }
4728 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4729
4730 /**
4731 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
4732 *
4733 * @since 1.17.7 (J) - 1.7.7 (WP)
4734 */
4735 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$tf, ['days' => $num_nights], ['adults' => $num_adults]]);
4736 if ($custom_calculation) {
4737 $realcost = (float) $custom_calculation[0];
4738 }
4739
4740 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4741 if ($tf['is_citytax'] == 1) {
4742 $taxes += $realcost;
4743 } elseif ($tf['is_fee'] == 1) {
4744 $fees += $realcost;
4745 }
4746 $optsett = explode('-', $tf['forceval']);
4747 $options_data[] = $tf['id'].':'.$optsett[0];
4748 }
4749 }
4750
4751 return [
4752 'city_taxes' => $taxes,
4753 'fees' => $fees,
4754 'options' => $options_data
4755 ];
4756 }
4757
4758 /**
4759 * From a list of option/extras records, it returns an array with the options/extras
4760 * that do not support age intervals, and options for the children age intervals.
4761 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4762 *
4763 * @param array $optionals the full list of options/extras records.
4764 * @param int $adults the number of adults in the room requested.
4765 * @param int $children the number of children in the room requested.
4766 *
4767 * @return array the filtered regular and children age intervals options.
4768 */
4769 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4770 {
4771 // container for age intervals
4772 $ageintervals = [];
4773
4774 // regular options should start as an array
4775 if (!is_array($optionals)) {
4776 $optionals = [];
4777 }
4778
4779 // check for the first valid age intervals option
4780 foreach ($optionals as $kopt => $opt) {
4781 if (!empty($opt['ageintervals'])) {
4782 $intervals = explode(';;', $opt['ageintervals']);
4783 foreach ($intervals as $intv) {
4784 if (empty($intv)) {
4785 continue;
4786 }
4787 $parts = explode('_', $intv);
4788 if (count($parts) >= 3) {
4789 // age intervals option found, get the first and only one
4790 $ageintervals = $optionals[$kopt];
4791 break 2;
4792 }
4793 }
4794 }
4795 }
4796
4797 if ($ageintervals) {
4798 /**
4799 * We allow price adjustments for a minimum total guests by overriding the costs
4800 * for each age interval for specific child numbers when the party covers some children.
4801 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4802 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4803 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4804 *
4805 * @since 1.13.5
4806 */
4807 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4808 $oparams = !is_array($oparams) ? array() : $oparams;
4809 $adults = (int)$adults;
4810 $children = (int)$children;
4811 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4812 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4813 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4814 if ($children_to_pay < $children) {
4815 // compose string for free age intervals
4816 $intervals = explode(';;', $ageintervals['ageintervals']);
4817 foreach ($intervals as $k => $v) {
4818 $parts = explode('_', $v);
4819 // set cost to 0 (key = 2)
4820 $parts[2] = '0';
4821 $intervals[$k] = implode('_', $parts);
4822 }
4823 $free_ageintervals = implode(';;', $intervals);
4824 for ($i = 1; $i <= $children; $i++) {
4825 if (($children - $i + 1) <= $children_to_pay) {
4826 // costs for this Nth last child should be regular as this child should pay
4827 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4828 } else {
4829 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4830 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4831 }
4832 }
4833 }
4834 }
4835
4836 // remove age intervals from regular options
4837 foreach ($optionals as $kopt => $opt) {
4838 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4839 // unset the option of type age intervals from the regular options
4840 unset($optionals[$kopt]);
4841 }
4842 }
4843 }
4844
4845 // return the filtered list of regular options and age intervals options
4846 return [$optionals, $ageintervals];
4847 }
4848
4849 /**
4850 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4851 * This is because some children in the party may not need to pay anything due to the min guests.
4852 *
4853 * @param array $optional the option full record to parse.
4854 * @param int $adults the number of adults in the room requested.
4855 * @param int $children the number of children in the room requested.
4856 *
4857 * @return array associative array with default age intervals and override strings.
4858 *
4859 * @since 1.13.5
4860 */
4861 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4862 {
4863 if (!is_array($optional)) {
4864 $optional = array();
4865 }
4866
4867 $overrides = array(
4868 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4869 );
4870
4871 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4872 $oparams = !is_array($oparams) ? array() : $oparams;
4873 $adults = (int)$adults;
4874 $children = (int)$children;
4875
4876 if (!count($oparams)) {
4877 return $overrides;
4878 }
4879
4880 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4881 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4882 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4883 if ($children_to_pay < $children) {
4884 // compose string for free age intervals
4885 $intervals = explode(';;', $optional['ageintervals']);
4886 foreach ($intervals as $k => $v) {
4887 $parts = explode('_', $v);
4888 // set cost to 0 (key = 2)
4889 $parts[2] = '0';
4890 $intervals[$k] = implode('_', $parts);
4891 }
4892 $free_ageintervals = implode(';;', $intervals);
4893 for ($i = 1; $i <= $children; $i++) {
4894 if (($children - $i + 1) <= $children_to_pay) {
4895 // costs for this Nth last child should be regular as this child should pay
4896 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4897 } else {
4898 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4899 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4900 }
4901 }
4902 }
4903 }
4904
4905 return $overrides;
4906 }
4907
4908 /**
4909 * Returns the number/index of the children being parsed given the full list of room options string,
4910 * the current key in the array of the split room options string, the ID of the ageintervals option,
4911 * and the total number of children. This is useful for later applying the cost overrides for the
4912 * children ages through the method getOptionIntervalChildOverrides().
4913 *
4914 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4915 * @param int $optid the ID of the option of type age intervals to check.
4916 * @param int $roptkey the current position in the loop of the room options string.
4917 * @param int $children the number of children in the room requested.
4918 *
4919 * @return int the position/number of the children being parsed, -1 if not found.
4920 *
4921 * @since 1.13.5
4922 */
4923 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4924 {
4925 // default to -1 for not found
4926 $child_num = -1;
4927
4928 $valid_opt_counter = 0;
4929
4930 $roptions = explode(";", $roptstr);
4931 foreach ($roptions as $k => $opt) {
4932 if (empty($opt)) {
4933 continue;
4934 }
4935 $optvals = explode(":", $opt);
4936 /**
4937 * In some cases, like the "saveorder" task, the room option string may contain
4938 * the room number beside the option ID, separated with an underscore.
4939 * If an underscore is present, we need to split it to find the option ID.
4940 */
4941 if (strpos($optvals[0], '_') !== false) {
4942 // underscore found in room number portion, extract the option ID
4943 $rn_parts = explode('_', $optvals[0]);
4944 // 0th element is the room number in the party, 1st elem is the option ID
4945 $optvals[0] = $rn_parts[1];
4946 }
4947 //
4948 if ((int)$optvals[0] != (int)$optid) {
4949 // we are not interested into this option ID
4950 continue;
4951 }
4952 // increase counter for this option ID
4953 $valid_opt_counter++;
4954
4955 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4956 // children position found, we need to return it as a 0th base
4957 $child_num = ($valid_opt_counter - 1);
4958 break;
4959 }
4960 }
4961
4962 return $child_num;
4963 }
4964
4965 public static function getOptionIntervalsCosts($intvstr) {
4966 $optcosts = array();
4967 $intervals = explode(';;', $intvstr);
4968 foreach ($intervals as $kintv => $intv) {
4969 if (empty($intv)) continue;
4970 $parts = explode('_', $intv);
4971 if (count($parts) >= 3) {
4972 $optcosts[$kintv] = (float)$parts[2];
4973 }
4974 }
4975 return $optcosts;
4976 }
4977
4978 public static function getOptionIntervalsAges($intvstr) {
4979 $optages = array();
4980 $intervals = explode(';;', $intvstr);
4981 foreach ($intervals as $kintv => $intv) {
4982 if (empty($intv)) continue;
4983 $parts = explode('_', $intv);
4984 if (count($parts) >= 3) {
4985 $optages[$kintv] = $parts[0].' - '.$parts[1];
4986 }
4987 }
4988 return $optages;
4989 }
4990
4991 public static function getOptionIntervalsPercentage($intvstr) {
4992 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4993 $optcostspcent = array();
4994 $intervals = explode(';;', $intvstr);
4995 foreach ($intervals as $kintv => $intv) {
4996 if (empty($intv)) continue;
4997 $parts = explode('_', $intv);
4998 if (count($parts) >= 3) {
4999 //fixed amount
5000 $setval = 0;
5001 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
5002 //percentage value of the room base cost (VBO 1.10)
5003 $setval = 2;
5004 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
5005 //percentage value of the adults tariff
5006 $setval = 1;
5007 }
5008 $optcostspcent[$kintv] = $setval;
5009 }
5010 }
5011 return $optcostspcent;
5012 }
5013
5014 public static function dayValidTs($days, $first, $second)
5015 {
5016 $secdiff = $second - $first;
5017 $daysdiff = $secdiff / 86400;
5018 if (is_int($daysdiff)) {
5019 if ($daysdiff < 1) {
5020 $daysdiff = 1;
5021 }
5022 } else {
5023 if ($daysdiff < 1) {
5024 $daysdiff = 1;
5025 } else {
5026 $sum = floor($daysdiff) * 86400;
5027 $newdiff = $secdiff - $sum;
5028 $maxhmore = self::getHoursMoreRb() * 3600;
5029 if ($maxhmore >= $newdiff) {
5030 $daysdiff = floor($daysdiff);
5031 } else {
5032 $daysdiff = ceil($daysdiff);
5033 }
5034 }
5035 }
5036
5037 return ($daysdiff == $days);
5038 }
5039
5040 /**
5041 * Calculates the stay fee inclusive of taxes.
5042 *
5043 * @param float $cost the cost to evaluate.
5044 * @param int $idprice the rate plan ID associated.
5045 *
5046 * @return float the given cost after tax, if taxes had to be applied.
5047 */
5048 public static function sayCostPlusIva($cost, $idprice)
5049 {
5050 $ivainclusa = self::ivaInclusa();
5051 if ($ivainclusa) {
5052 return $cost;
5053 }
5054
5055 static $tax_rplans_map = [];
5056
5057 if (array_key_exists($idprice, $tax_rplans_map)) {
5058 $tax_rate = $tax_rplans_map[$idprice];
5059 } else {
5060 $dbo = JFactory::getDbo();
5061 $q = "SELECT `p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` `i` ON `i`.`id`=`p`.`idiva` WHERE `p`.`id`=" . (int)$idprice . ";";
5062 $dbo->setQuery($q);
5063 $tax_rate = $dbo->loadAssoc();
5064
5065 // cache value
5066 $tax_rplans_map[$idprice] = $tax_rate;
5067 }
5068
5069 if (!$tax_rate || empty($tax_rate['aliq'])) {
5070 return $cost;
5071 }
5072
5073 $subt = 100 + $tax_rate['aliq'];
5074 $op = ($cost * $subt / 100);
5075
5076 /**
5077 * Tax Cap implementation for prices tax excluded (most common).
5078 *
5079 * @since 1.12
5080 */
5081 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5082 $op = ($cost + $tax_rate['taxcap']);
5083 }
5084
5085 // apply rounding to avoid issues with multiple tax rates when tax excluded
5086 $formatvals = self::getNumberFormatData();
5087 $formatparts = explode(':', $formatvals);
5088 $rounded_op = round($op, (int)$formatparts[0]);
5089 if ($rounded_op > $op) {
5090 return $rounded_op;
5091 }
5092
5093 /**
5094 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5095 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5096 *
5097 * @since 1.11.1
5098 */
5099 return round($op, (int)$formatparts[0]);
5100 }
5101
5102 /**
5103 * Calculates the stay fee exclusive of taxes.
5104 *
5105 * @param float $cost the cost to evaluate.
5106 * @param int $idprice the rate plan ID associated.
5107 *
5108 * @return float the given cost before tax.
5109 */
5110 public static function sayCostMinusIva($cost, $idprice)
5111 {
5112 $ivainclusa = self::ivaInclusa();
5113 if (!$ivainclusa) {
5114 return $cost;
5115 }
5116
5117 static $tax_rplans_map = [];
5118
5119 if (array_key_exists($idprice, $tax_rplans_map)) {
5120 $tax_rate = $tax_rplans_map[$idprice];
5121 } else {
5122 $dbo = JFactory::getDbo();
5123 $q = "SELECT `p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` `i` ON `i`.`id`=`p`.`idiva` WHERE `p`.`id`=" . (int)$idprice . ";";
5124 $dbo->setQuery($q);
5125 $tax_rate = $dbo->loadAssoc();
5126
5127 // cache value
5128 $tax_rplans_map[$idprice] = $tax_rate;
5129 }
5130
5131 if (!$tax_rate || empty($tax_rate['aliq'])) {
5132 return $cost;
5133 }
5134
5135 $subt = 100 + $tax_rate['aliq'];
5136 $op = ($cost * 100 / $subt);
5137
5138 /**
5139 * Tax Cap implementation also when prices tax included.
5140 *
5141 * @since 1.12
5142 */
5143 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5144 $op = ($cost - $tax_rate['taxcap']);
5145 }
5146
5147 // apply rounding to avoid issues with multiple tax rates when tax included
5148 $formatvals = self::getNumberFormatData();
5149 $formatparts = explode(':', $formatvals);
5150 $rounded_op = round($op, (int)$formatparts[0]);
5151 if ($rounded_op < $op) {
5152 return $rounded_op;
5153 }
5154
5155 /**
5156 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5157 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5158 *
5159 * @since 1.11.1
5160 */
5161 return round($op, (int)$formatparts[0]);
5162 }
5163
5164 /**
5165 * Given an option/extra cost, determines whether taxes should be applied over it.
5166 * Can also be used to calculate taxes on the extra costs per room in the bookings
5167 *
5168 * @param float $cost the cost to evaluate, option or extra service fee.
5169 * @param int $idiva the ID of the tax rate to use to apply taxes.
5170 * @param bool $force if true, taxes will always be applied.
5171 *
5172 * @return float the given option/extra cost plus tax, if taxes had to be applied.
5173 *
5174 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5175 */
5176 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
5177 {
5178 $ivainclusa = self::ivaInclusa();
5179 if ($ivainclusa && $force !== true) {
5180 return $cost;
5181 }
5182
5183 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5184 if (!$tax_rate) {
5185 return $cost;
5186 }
5187
5188 $subt = 100 + $tax_rate['aliq'];
5189 $op = ($cost * $subt / 100);
5190
5191 /**
5192 * Tax Cap implementation for prices tax excluded (most common).
5193 *
5194 * @since 1.12
5195 */
5196 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5197 $op = ($cost + $tax_rate['taxcap']);
5198 }
5199
5200 // apply rounding to avoid issues with multiple tax rates when tax excluded
5201 $formatvals = self::getNumberFormatData();
5202 $formatparts = explode(':', $formatvals);
5203 $rounded_op = round($op, (int)$formatparts[0]);
5204 if ($rounded_op > $op) {
5205 return $rounded_op;
5206 }
5207
5208 return $op;
5209 }
5210
5211 /**
5212 * Calculates the net amount for an option/extra service.
5213 *
5214 * @param float $cost the cost to evaluate, option or extra service fee.
5215 * @param int $idiva the ID of the tax rate to use to apply taxes.
5216 *
5217 * @return float the given option/extra cost before tax, if taxes were inclusive.
5218 */
5219 public static function sayOptionalsMinusIva($cost, $idiva)
5220 {
5221 if (empty($idiva)) {
5222 return $cost;
5223 }
5224
5225 $ivainclusa = self::ivaInclusa();
5226 if (!$ivainclusa) {
5227 return $cost;
5228 }
5229
5230 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5231 if (!$tax_rate) {
5232 return $cost;
5233 }
5234
5235 $subt = 100 + $tax_rate['aliq'];
5236 $op = ($cost * 100 / $subt);
5237
5238 /**
5239 * Tax Cap implementation also when prices tax included.
5240 *
5241 * @since 1.12
5242 */
5243 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5244 $op = ($cost - $tax_rate['taxcap']);
5245 }
5246
5247 // apply rounding to avoid issues with multiple tax rates when tax included
5248 $formatvals = self::getNumberFormatData();
5249 $formatparts = explode(':', $formatvals);
5250 $rounded_op = round($op, (int)$formatparts[0]);
5251 if ($rounded_op < $op) {
5252 return $rounded_op;
5253 }
5254
5255 return $op;
5256 }
5257
5258 /**
5259 * Given a cost, determines whether taxes should be applied over it.
5260 *
5261 * @param float $cost the cost to evaluate, could be a room custom price.
5262 * @param int $idiva the ID of the tax rate to use to apply taxes.
5263 * @param bool $force if true, taxes will always be applied.
5264 *
5265 * @return float the given cost plus tax, if taxes had to be applied.
5266 *
5267 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5268 */
5269 public static function sayPackagePlusIva($cost, $idiva, $force = false)
5270 {
5271 $ivainclusa = self::ivaInclusa();
5272 if ($ivainclusa && $force !== true) {
5273 return $cost;
5274 }
5275
5276 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5277 if (!$tax_rate) {
5278 return $cost;
5279 }
5280
5281 $subt = 100 + $tax_rate['aliq'];
5282 $op = ($cost * $subt / 100);
5283
5284 /**
5285 * Tax Cap implementation for prices tax excluded (most common).
5286 *
5287 * @since 1.12
5288 */
5289 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5290 $op = ($cost + $tax_rate['taxcap']);
5291 }
5292
5293 // apply rounding to avoid issues with multiple tax rates when tax excluded
5294 $formatvals = self::getNumberFormatData();
5295 $formatparts = explode(':', $formatvals);
5296 $rounded_op = round($op, (int)$formatparts[0]);
5297 if ($rounded_op > $op) {
5298 return $rounded_op;
5299 }
5300
5301 return $op;
5302 }
5303
5304 /**
5305 * Calculates the net amount for a package.
5306 *
5307 * @param float $cost the cost to evaluate, option or extra service fee.
5308 * @param int $idiva the ID of the tax rate to use to apply taxes.
5309 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
5310 *
5311 * @return float the given cost before tax.
5312 */
5313 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
5314 {
5315 $ivainclusa = self::ivaInclusa();
5316 if (!$ivainclusa && $force_invoice_excltax !== true) {
5317 return $cost;
5318 }
5319
5320 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5321 if (!$tax_rate) {
5322 return $cost;
5323 }
5324
5325 $subt = 100 + $tax_rate['aliq'];
5326 $op = ($cost * 100 / $subt);
5327
5328 /**
5329 * Tax Cap implementation also when prices tax included.
5330 *
5331 * @since 1.12
5332 */
5333 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5334 $op = ($cost - $tax_rate['taxcap']);
5335 }
5336
5337 // apply rounding to avoid issues with multiple tax rates when tax included
5338 $formatvals = self::getNumberFormatData();
5339 $formatparts = explode(':', $formatvals);
5340 $rounded_op = round($op, (int)$formatparts[0]);
5341 if ($rounded_op < $op) {
5342 return $rounded_op;
5343 }
5344
5345 return $op;
5346 }
5347
5348 public static function getSecretLink()
5349 {
5350 $dbo = JFactory::getDbo();
5351
5352 $sid = mt_rand();
5353
5354 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
5355 $dbo->setQuery($q);
5356 $all = $dbo->loadAssocList();
5357 if ($all) {
5358 $list = [];
5359 foreach ($all as $s) {
5360 $list[] = $s['sid'];
5361 }
5362 if (in_array($sid, $list)) {
5363 while(in_array($sid, $list)) {
5364 $sid++;
5365 }
5366 }
5367 }
5368
5369 return $sid;
5370 }
5371
5372 /**
5373 * Generates a confirmation number for the given booking.
5374 * Modified the way random string-numbers are obtained to
5375 * avoid any possible brute-force attempt.
5376 *
5377 * @since 1.15.1 (J) - 1.5.4 (WP)
5378 */
5379 public static function generateConfirmNumber($oid, $update = true)
5380 {
5381 $dbo = JFactory::getDbo();
5382
5383 $confirmnumb = date('y') . $oid;
5384 for ($i = 0; $i < 8; $i++) {
5385 $confirmnumb .= rand(0, 9);
5386 }
5387
5388 if ($update) {
5389 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5390 $dbo->setQuery($q);
5391 $dbo->execute();
5392 }
5393
5394 return $confirmnumb;
5395 }
5396
5397 public static function buildCustData($arr, $sep) {
5398 $cdata = "";
5399 foreach ($arr as $k => $e) {
5400 if (strlen($e)) {
5401 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5402 }
5403 }
5404 return $cdata;
5405 }
5406
5407 /**
5408 * This method parses the Joomla menu object to see if a menu item of a
5409 * specific type is available, to get its ID. Useful when links should be
5410 * displayed in pages where there is no Itemid set (booking details pages).
5411 *
5412 * @param array $viewtypes list of accepted menu items
5413 * @param string $lang the optional language to use.
5414 *
5415 * @return int
5416 *
5417 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5418 */
5419 public static function findProperItemIdType($viewtypes, $lang = null)
5420 {
5421 if (VBOPlatformDetection::isWordPress()) {
5422 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5423
5424 $itemid = $model->best($viewtypes, $lang);
5425
5426 if ($itemid) {
5427 return $itemid;
5428 }
5429
5430 return 0;
5431 }
5432
5433 $bestitemid = 0;
5434
5435 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5436
5437 $app = JFactory::getApplication();
5438
5439 $menu = $app->getMenu('site');
5440
5441 if (!$menu) {
5442 return 0;
5443 }
5444
5445 $menu_items = $menu->getMenu();
5446
5447 if (!$menu_items) {
5448 return 0;
5449 }
5450
5451 foreach ($menu_items as $itemid => $item) {
5452 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5453 // proper menu item type found
5454 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5455
5456 if (isset($item->language) && $item->language == $current_lang) {
5457 // we found the exact menu item type for the given language
5458 return $itemid;
5459 }
5460 }
5461 }
5462
5463 return $bestitemid;
5464 }
5465
5466 /**
5467 * Rewrites an internal URI that needs to be used outside of the website.
5468 * This means that the routed URI MUST start with the base path of the site.
5469 *
5470 * @param mixed $query The query string or an associative array of data.
5471 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5472 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5473 *
5474 * @return string The complete routed URI.
5475 *
5476 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5477 * routing from back-end (if available on the CMS version).
5478 */
5479 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5480 {
5481 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5482 }
5483
5484 /**
5485 * Generates an iCal file to be attached to the email message for the
5486 * customer or the administrator with some basic booking details.
5487 *
5488 * @param string $recip either admin or customer.
5489 * @param array $booking the booking array or some keys.
5490 *
5491 * @return mixed string in case of success, false otherwise.
5492 *
5493 * @since 1.12.0
5494 */
5495 public static function getEmailIcal($recip, $booking) {
5496 // load configuration setting
5497 $attachical = self::attachIcal();
5498
5499 if ($attachical === 0) {
5500 // do not attach any iCal file
5501 return false;
5502 }
5503
5504 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5505 // skip the iCal for the admin
5506 return false;
5507 }
5508
5509 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5510 // skip the iCal for the customer
5511 return false;
5512 }
5513
5514 if (strpos($recip, 'admin') !== false) {
5515 // prepare event description and summary for the admin
5516 $description = $booking['custdata'];
5517 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5518 $fname = $booking['ts'] . '.ics';
5519 } else {
5520 // event description and summary for the customer
5521 $description = '';
5522 $summary = self::getFrontTitle();
5523 $fname = 'reservation_reminder.ics';
5524 }
5525
5526 // prepare iCal head
5527 $company_name = self::getFrontTitle();
5528 $ics_str = "BEGIN:VCALENDAR\r\n" .
5529 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5530 "CALSCALE:GREGORIAN\r\n" .
5531 "VERSION:2.0\r\n";
5532 // compose iCal body
5533 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5534 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5535 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5536 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5537 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5538 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5539 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5540 $ics_str .= 'END:VEVENT'."\r\n";
5541 // close iCal file content
5542 $ics_str .= "END:VCALENDAR";
5543
5544 /**
5545 * Trigger event to allow third party plugins to overwrite the iCal file.
5546 *
5547 * @since 1.16.0 (J) - 1.6.0 (WP)
5548 */
5549 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5550
5551 if (empty($ics_str)) {
5552 return false;
5553 }
5554
5555 // store the event onto a .ics file. We use the resources folder in back-end.
5556 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5557 $fp = fopen($fpath, 'w+');
5558 $bytes = fwrite($fp, $ics_str);
5559 fclose($fp);
5560
5561 return $bytes ? $fpath : false;
5562 }
5563
5564 public static function loadEmailTemplate($booking_info = array()) {
5565 define('_VIKBOOKINGEXEC', '1');
5566 ob_start();
5567 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5568 $content = ob_get_contents();
5569 ob_end_clean();
5570 return $content;
5571 }
5572
5573 /**
5574 * Parses the raw HTML content of the booking email template.
5575 *
5576 * @param string $tmpl the raw content of the template.
5577 * @param mixed $bid int for the booking ID or booking array.
5578 * @param array $rooms list of rooms booked and translated.
5579 * @param array $rates list of translated rates for the booked rooms.
5580 * @param array $options list of translated options booked.
5581 * @param float [$total] the booking total amount (in case it has changed).
5582 * @param string [$link] the booking link can be passed for the no-deposit.
5583 *
5584 * @return string the HTML content of the parsed email template.
5585 *
5586 * @since 1.13 with different arguments.
5587 */
5588 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5589 {
5590 $app = JFactory::getApplication();
5591 $dbo = JFactory::getDbo();
5592 $vbo_tn = self::getTranslator();
5593
5594 // availability helper
5595 $av_helper = self::getAvailabilityInstance();
5596
5597 // get necessary values
5598 if (is_array($bid)) {
5599 // we got the full booking record
5600 $order_info = $bid;
5601 $bid = $order_info['id'];
5602 } else {
5603 $order_info = array();
5604 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5605 $dbo->setQuery($q);
5606 $order_info = $dbo->loadAssoc();
5607 if (!$order_info) {
5608 throw new Exception('Booking not found', 404);
5609 }
5610 }
5611
5612 $tars_info = array();
5613 $q = "SELECT `or`.`id`,`or`.`idroom`,`or`.`idtar`,`d`.`idprice` FROM `#__vikbooking_ordersrooms` AS `or` LEFT JOIN `#__vikbooking_dispcost` AS `d` ON `or`.`idtar`=`d`.`id` WHERE `or`.`idorder`=" . (int)$order_info['id'] . ";";
5614 $dbo->setQuery($q);
5615 $tars_info = $dbo->loadAssocList();
5616 if (!$tars_info) {
5617 throw new Exception('No rooms found', 404);
5618 }
5619
5620 /**
5621 * Split stay reservation.
5622 *
5623 * @since 1.16.0 (J) - 1.6.0 (WP)
5624 */
5625 $room_stay_dates = [];
5626 if ($order_info['split_stay']) {
5627 if ($order_info['status'] == 'confirmed') {
5628 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5629 } else {
5630 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5631 }
5632 // immediately count the number of nights of stay for each split room
5633 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5634 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5635 // overwrite values for compatibility with non-confirmed bookings
5636 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5637 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5638 }
5639 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5640 // overwrite the whole array
5641 $room_stay_dates[$sps_r_k] = $sps_r_v;
5642 }
5643 }
5644
5645 // check if the language in use is the same as the one used during the checkout
5646 $lang = JFactory::getLanguage();
5647 if (!empty($order_info['lang'])) {
5648 if ($lang->getTag() != $order_info['lang']) {
5649 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5650 if (VBOPlatformDetection::isJoomla()) {
5651 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5652 }
5653 }
5654 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5655 // force the translation to start because contents should be translated
5656 $vbo_tn::$force_tolang = $order_info['lang'];
5657 }
5658 }
5659
5660 // values for replacements
5661 $company_name = self::getFrontTitle();
5662 $currencyname = self::getCurrencyName();
5663 $sitelogo = self::getSiteLogo();
5664 $footermess = self::getFooterOrdMail($vbo_tn);
5665 $dateformat = self::getDateFormat();
5666 $datesep = self::getDateSeparator();
5667 if ($dateformat == "%d/%m/%Y") {
5668 $df = 'd/m/Y';
5669 } elseif ($dateformat == "%m/%d/%Y") {
5670 $df = 'm/d/Y';
5671 } else {
5672 $df = 'Y/m/d';
5673 }
5674 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5675 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5676 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5677 $customer_info = nl2br($order_info['custdata']);
5678 $company_logo = '';
5679 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5680 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5681 }
5682 if ($order_info['status'] == 'cancelled') {
5683 $confirmnumber = '';
5684 $status_str = JText::translate('VBCANCELLED');
5685 } elseif ($order_info['status'] == 'standby') {
5686 $confirmnumber = '';
5687 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5688 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5689 // this is an inquiry reservation
5690 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5691 }
5692 } else {
5693 $confirmnumber = $order_info['confirmnumber'];
5694 $status_str = JText::translate('VBCOMPLETED');
5695 }
5696 // booking total amount
5697 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5698 // booking link
5699 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5700 if (is_null($link)) {
5701 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5702 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5703 }
5704
5705 // raw HTML content
5706 $parsed = $tmpl;
5707
5708 /**
5709 * Trigger event to allow third-party plugins to manipulate the template string.
5710 *
5711 * @since 1.16.10 (J) - 1.6.10 (WP)
5712 */
5713 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5714
5715 /**
5716 * Parse all conditional text rules.
5717 *
5718 * @since 1.14 (J) - 1.4.0 (WP)
5719 */
5720 self::getConditionalRulesInstance()
5721 ->set(['booking', 'rooms'], [$order_info, $rooms])
5722 ->parseTokens($parsed);
5723
5724 /**
5725 * Parse all Door Access Control tags.
5726 *
5727 * @since 1.18.4 (J) - 1.8.4 (WP)
5728 */
5729 VBOFactory::getDoorAccessControl()
5730 ->parseTokens((new VBOBookingRegistry($order_info, $rooms)), $parsed);
5731
5732 // special tokens (tags) replacement
5733 $parsed = str_replace("{logo}", $company_logo, $parsed);
5734 $parsed = str_replace("{company_name}", $company_name, $parsed);
5735 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5736 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5737 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5738 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5739 $parsed = str_replace("{order_status}", $status_str, $parsed);
5740 $parsed = str_replace("{order_date}", $create_date, $parsed);
5741
5742 // customer record
5743 $cpin = self::getCPinInstance();
5744 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5745 $customer_name = '';
5746 $customer_pin = '';
5747 if ($customer_record) {
5748 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5749 $customer_pin = $customer_record['pin'];
5750 }
5751 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5752 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5753
5754 // PIN Code
5755 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5756 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5757 if (!empty($customer_pin)) {
5758 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5759 }
5760 }
5761
5762 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5763
5764 // Confirmation Number
5765 if ($confirmnumber) {
5766 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5767 } else {
5768 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5769 }
5770 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5771 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5772
5773 $roomsnum = count($rooms);
5774 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5775 $roomstr = "";
5776 $tot_adults = 0;
5777 $tot_children = 0;
5778 $tot_guests = 0;
5779
5780 // Rooms Distinctive Features
5781 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5782
5783 foreach ($rooms as $num => $r) {
5784 // guests
5785 $tot_adults += (int) $r['adults'];
5786 $tot_children += (int) $r['children'];
5787 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5788
5789 // room info
5790 $roomstr .= "<strong>".$r['name']."</strong> ".$r['adults']." ".($r['adults'] > 1 ? JText::translate('VBMAILADULTS') : JText::translate('VBMAILADULT')).($r['children'] > 0 ? ", ".$r['children']." ".($r['children'] > 1 ? JText::translate('VBMAILCHILDREN') : JText::translate('VBMAILCHILD')) : "")."<br/>";
5791 // Rooms Distinctive Features
5792 if (is_array($matches[1] ?? []) && $matches[1]) {
5793 $distinctive_features = array();
5794 $rparams = (array)json_decode($r['params'], true);
5795 if (array_key_exists('features', $rparams) && count($rparams['features']) > 0 && array_key_exists('roomindex', $r) && !empty($r['roomindex']) && array_key_exists($r['roomindex'], $rparams['features'])) {
5796 $distinctive_features = $rparams['features'][$r['roomindex']];
5797 }
5798 $docheck = (count($distinctive_features) > 0);
5799 foreach ($matches[1] as $reqf) {
5800 $feature_found = false;
5801 if ($docheck) {
5802 foreach ($distinctive_features as $dfk => $dfv) {
5803 if (stripos($dfk, $reqf) !== false) {
5804 $feature_found = $dfk;
5805 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5806 break;
5807 }
5808 }
5809 }
5810 }
5811 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5812 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5813 }
5814 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5815 }
5816 }
5817 }
5818
5819 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5820 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5821 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5822
5823 // custom fields replace
5824 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5825 if (is_array($cmatches[1]) && count($cmatches[1])) {
5826 $cfids = array();
5827 foreach ($cmatches[1] as $cfid ) {
5828 $cfids[] = $cfid;
5829 }
5830 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5831 $dbo->setQuery($q);
5832 $cfields = $dbo->loadAssocList();
5833 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5834 $cfmap = array();
5835 foreach ($cfields as $cf) {
5836 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5837 }
5838 $cfmapreplace = array();
5839 $partsreceived = explode("\n", $order_info['custdata']);
5840 if (count($partsreceived) > 0) {
5841 foreach ($partsreceived as $pst) {
5842 if (!empty($pst)) {
5843 $tmpdata = explode(":", $pst);
5844 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5845 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5846 }
5847 }
5848 }
5849 }
5850 foreach ($cmatches[1] as $cfid ) {
5851 if (array_key_exists($cfid, $cfmapreplace)) {
5852 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5853 } else {
5854 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5855 }
5856 }
5857 }
5858
5859 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5860 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5861 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5862 $parsed = str_replace("{num_nights}", ($order_info['days'] ?? 1), $parsed);
5863
5864 // order details
5865 $orderdetails = "";
5866 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5867 foreach ($rooms as $num => $r) {
5868 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5869 $use_room_ind = $num - $room_loop_ind;
5870 $split_stay_str = '';
5871 $split_stay_info = [];
5872 if ($order_info['split_stay'] && !empty($room_stay_dates) && isset($room_stay_dates[$use_room_ind]) && $room_stay_dates[$use_room_ind]['idroom'] == $room_id) {
5873 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5874 $split_stay_info[] = date(str_replace("/", $datesep, $df), $room_stay_dates[$use_room_ind]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$use_room_ind]['checkout']);
5875 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5876 }
5877
5878 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5879 $faredets = explode(":", $expdet[0]);
5880 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5881 if (!empty($expdet[1])) {
5882 $attrfaredets = explode(":", $expdet[1]);
5883 if (strlen($attrfaredets[1]) > 0) {
5884 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5885 }
5886 }
5887 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5888 $orderdetails .= '<div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($fareprice), $currencyname) . '</span></div></div>';
5889 // options
5890 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5891 foreach ($options[$num] as $oo) {
5892 $expopts = explode("\n", $oo);
5893 foreach ($expopts as $optinfo) {
5894 if (!empty($optinfo)) {
5895 $splitopt = explode(":", $optinfo);
5896 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5897 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($optprice), $currencyname) . '</span></div></div>';
5898 }
5899 }
5900 }
5901 }
5902 //
5903 if ($roomsnum > 1 && $num < $roomsnum) {
5904 $orderdetails .= '<br/>';
5905 }
5906 }
5907
5908 // coupon
5909 if (!empty($order_info['coupon'])) {
5910 $expcoupon = explode(";", $order_info['coupon']);
5911 $orderdetails .= '<br/><div class="discount"><span>'.JText::translate('VBCOUPON').' '.$expcoupon[2].'</span><div class="service-amount" style="float: right;"><span>- ' . self::formatCurrencyNumber(self::numberFormat($expcoupon[1]), $currencyname) . '</span></div></div>';
5912 }
5913
5914 // discount and payment method
5915 $payment_info = [];
5916 $payment_name = '';
5917 if (!empty($order_info['idpayment'])) {
5918 $exppay = explode('=', $order_info['idpayment']);
5919 $payment = self::getPayment($exppay[0], $vbo_tn);
5920 if ((array) $payment) {
5921 $payment_name = $payment['name'];
5922 }
5923 }
5924
5925 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5926
5927 if ($order_info['status'] != 'cancelled') {
5928 if ($payment_info) {
5929 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5930 // Discount (not charge)
5931 if ($payment_info['val_pcent'] == 1) {
5932 // fixed value
5933 $total -= $payment_info['charge'];
5934 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- ' . self::formatCurrencyNumber(self::numberFormat($payment_info['charge']), $currencyname) . '</span></div></div>';
5935 } else {
5936 // percent value
5937 $percent_disc = $total * $payment_info['charge'] / 100;
5938 $total -= $percent_disc;
5939 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- ' . self::formatCurrencyNumber(self::numberFormat($percent_disc), $currencyname) . '</span></div></div>';
5940 }
5941 }
5942 }
5943 }
5944
5945 // booking details string
5946 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5947
5948 // additional information
5949 $parsed = str_replace("{order_total}", self::formatCurrencyNumber(self::numberFormat($total), $currencyname), $parsed);
5950 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5951 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5952 $parsed = str_replace("{booking_link}", $link, $parsed);
5953
5954 // deposit
5955 $deposit_str = '';
5956 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5957 $percentdeposit = self::getAccPerCent();
5958 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5959 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5960 if (self::getTypeDeposit() == "fixed") {
5961 $deposit_amount = $percentdeposit;
5962 } else {
5963 $baseTotal = $total;
5964 if ($order_info['tot_damage_dep'] ?? 0) {
5965 // check if the damage deposit will have to be paid later/separately
5966 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($order_info, $rooms);
5967 if (($damage_deposit_payment['damagedep_gross'] ?? 0) && ($damage_deposit_payment['payment_window'] ?? []) && ($damage_deposit_payment['payment_window']['payable'] ?? null) !== true) {
5968 // damage deposit is not payable right now
5969 $baseTotal -= (float) $order_info['tot_damage_dep'];
5970 }
5971 }
5972 $deposit_amount = $baseTotal * $percentdeposit / 100;
5973 }
5974 if ($deposit_amount > 0) {
5975 $deposit_str = '<div class="deposit"><span>'.JText::translate('VBLEAVEDEPOSIT').'</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($deposit_amount), $currencyname) . '</strong></div></div>';
5976 }
5977 }
5978 }
5979 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5980 //
5981 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5982 $totpaid_str = '';
5983 if ($order_info['refund'] > 0) {
5984 $totpaid_str .= '<div class="amountpaid amountrefunded"><span>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($order_info['refund']), $currencyname) . '</strong></div></div>';
5985 }
5986 if ($order_info['status'] != 'cancelled') {
5987 $tot_paid = $order_info['totpaid'];
5988 $diff_topay = (float)$total - (float)$tot_paid;
5989 if ((float)$tot_paid > 0) {
5990 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBAMOUNTPAID').'</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($tot_paid), $currencyname) . '</strong></div></div>';
5991 // only in case the remaining balance is greater than 1 to avoid commissions issues
5992 if ($diff_topay > 1) {
5993 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBTOTALREMAINING').'</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($diff_topay), $currencyname) . '</strong></div></div>';
5994 }
5995 }
5996 }
5997 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5998 $totpaid_str .= '<div class="amountpaid amountcancfee"><span>' . JText::translate('VBO_CANC_FEE') . '</span><div class="service-amount" style="float: right;"><strong>' . self::formatCurrencyNumber(self::numberFormat($order_info['canc_fee']), $currencyname) . '</strong></div></div>';
5999 }
6000 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
6001
6002 /**
6003 * Language direction (LTR or RTL).
6004 *
6005 * @since 1.16.4 (J) - 1.6.4 (WP)
6006 */
6007 $lang_direction = 'ltr';
6008 if (!empty($order_info['lang'])) {
6009 // attempt to define the language direction and replace the special tag
6010 try {
6011 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
6012 } catch (Throwable $e) {
6013 // do nothing, default to ltr
6014 $lang_direction = 'ltr';
6015 }
6016 // static map for Arabic and Hebrew
6017 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
6018 }
6019 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
6020 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
6021 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
6022
6023 /**
6024 * Replace static floating styles depending on language direction (LTR or RTL).
6025 * For BC with older email template sources, we need to keep the inline style
6026 * for the floating of certain elements, which looks good only on LTR.
6027 *
6028 * @since 1.16.4 (J) - 1.6.4 (WP)
6029 */
6030 if ($lang_direction === 'rtl') {
6031 // get rid of inline static style declaration because the template uses CSS classes
6032 $parsed = str_replace("float: right;", '', $parsed);
6033 }
6034
6035 return $parsed;
6036 }
6037
6038 /**
6039 * New method for sending booking email messages
6040 * to the guest or to the administrator(s).
6041 *
6042 * @param int $bid the booking ID.
6043 * @param array $for guest, admin or a custom email address.
6044 * @param bool $send whether to send or return the HTML message.
6045 * @param bool $no_config if true, no configuration settings will be used for the status.
6046 * @param string $type optional type to indicate a booking modification.
6047 *
6048 * @return mixed True or False depending on the result or HTML string for the preview.
6049 *
6050 * @since 1.13 (J) - 1.3 (WP)
6051 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
6052 * the reservation status is pending and against the configuration.
6053 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
6054 */
6055 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
6056 {
6057 $dbo = JFactory::getDbo();
6058 $app = JFactory::getApplication();
6059
6060 $vbo_tn = self::getTranslator();
6061 $av_helper = self::getAvailabilityInstance();
6062
6063 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
6064 $dbo->setQuery($q);
6065 $booking = $dbo->loadAssoc();
6066 if (!$booking) {
6067 return false;
6068 }
6069
6070 $result = false;
6071
6072 // check if the language in use is the same as the one used during the checkout
6073 $lang = JFactory::getLanguage();
6074 if (!empty($booking['lang'])) {
6075 if ($lang->getTag() != $booking['lang']) {
6076 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
6077 if (VBOPlatformDetection::isJoomla()) {
6078 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
6079 }
6080 }
6081 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
6082 // force the translation to start because contents should be translated
6083 $vbo_tn::$force_tolang = $booking['lang'];
6084 }
6085 }
6086
6087 // load rooms booked
6088 $q = "SELECT `or`.*,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`units`,`r`.`fromadult`,`r`.`toadult`,`r`.`params` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=" . $booking['id'] . " AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
6089 $dbo->setQuery($q);
6090 $ordersrooms = $dbo->loadAssocList();
6091 if (!$ordersrooms) {
6092 return false;
6093 }
6094 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
6095
6096 /**
6097 * Split stay reservation.
6098 *
6099 * @since 1.16.0 (J) - 1.6.0 (WP)
6100 */
6101 $room_stay_dates = [];
6102 if ($booking['split_stay']) {
6103 if ($booking['status'] == 'confirmed') {
6104 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
6105 } else {
6106 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
6107 }
6108 // immediately count the number of nights of stay for each split room
6109 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6110 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6111 // overwrite values for compatibility with non-confirmed bookings
6112 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6113 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6114 }
6115 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6116 // overwrite the whole array
6117 $room_stay_dates[$sps_r_k] = $sps_r_v;
6118 }
6119 }
6120
6121 $rooms = array();
6122 $tars = array();
6123 $is_package = !empty($booking['pkg']) ? true : false;
6124 $ftitle = self::getFrontTitle();
6125 $currencyname = self::getCurrencyName();
6126 foreach ($ordersrooms as $kor => $or) {
6127 $num = $kor + 1;
6128 $rooms[$num] = $or;
6129 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6130 // package or custom cost set from the back-end
6131 continue;
6132 }
6133
6134 // determine the days to consider for the count of the availability
6135 $room_nights = $booking['days'];
6136 $room_checkin = $booking['checkin'];
6137 $room_checkout = $booking['checkout'];
6138 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6139 $room_nights = $room_stay_dates[$kor]['nights'];
6140 $room_checkin = $room_stay_dates[$kor]['checkin'];
6141 $room_checkout = $room_stay_dates[$kor]['checkout'];
6142 }
6143
6144 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
6145 $dbo->setQuery($q);
6146 $tar = $dbo->loadAssocList();
6147 if (!$tar) {
6148 // tariff not found
6149 if (self::isAdmin()) {
6150 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
6151 }
6152 continue;
6153 }
6154
6155 // apply seasonal rates
6156 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
6157
6158 // different usage
6159 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
6160 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
6161 // Occupancy Override
6162 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
6163 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
6164 //
6165 if (is_array($diffusageprice)) {
6166 // set a charge or discount to the price(s) for the different usage of the room
6167 foreach ($tar as $kpr => $vpr) {
6168 $tar[$kpr]['diffusage'] = $or['adults'];
6169 if ($diffusageprice['chdisc'] == 1) {
6170 // charge
6171 if ($diffusageprice['valpcent'] == 1) {
6172 // fixed value
6173 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6174 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6175 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
6176 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6177 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
6178 } else {
6179 // percentage value
6180 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6181 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
6182 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
6183 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6184 $tar[$kpr]['cost'] = $aduseval;
6185 }
6186 } else {
6187 // discount
6188 if ($diffusageprice['valpcent'] == 1) {
6189 // fixed value
6190 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6191 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6192 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
6193 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6194 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
6195 } else {
6196 // percentage value
6197 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6198 $aduseval = $diffusageprice['pernight'] == 1 ? round($vpr['cost'] - ((($vpr['cost'] / $tar[$kpr]['days']) * $diffusageprice['value'] / 100) * $tar[$kpr]['days']), 2) : round(($vpr['cost'] * (100 - $diffusageprice['value']) / 100), 2);
6199 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
6200 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6201 $tar[$kpr]['cost'] = $aduseval;
6202 }
6203 }
6204 }
6205 }
6206 }
6207 //
6208 $tars[$num] = $tar[0];
6209 }
6210
6211 $secdiff = $booking['checkout'] - $booking['checkin'];
6212 $daysdiff = $secdiff / 86400;
6213 if (is_int($daysdiff)) {
6214 if ($daysdiff < 1) {
6215 $daysdiff = 1;
6216 }
6217 } else {
6218 if ($daysdiff < 1) {
6219 $daysdiff = 1;
6220 } else {
6221 $sum = floor($daysdiff) * 86400;
6222 $newdiff = $secdiff - $sum;
6223 $maxhmore = self::getHoursMoreRb() * 3600;
6224 if ($maxhmore >= $newdiff) {
6225 $daysdiff = floor($daysdiff);
6226 } else {
6227 $daysdiff = ceil($daysdiff);
6228 }
6229 }
6230 }
6231
6232 $isdue = 0;
6233 $pricestr = array();
6234 $optstr = array();
6235 foreach ($ordersrooms as $kor => $or) {
6236 $num = $kor + 1;
6237
6238 // determine the days to consider for the count of the availability
6239 $room_nights = $booking['days'];
6240 $room_checkin = $booking['checkin'];
6241 $room_checkout = $booking['checkout'];
6242 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6243 $room_nights = $room_stay_dates[$kor]['nights'];
6244 $room_checkin = $room_stay_dates[$kor]['checkin'];
6245 $room_checkout = $room_stay_dates[$kor]['checkout'];
6246 }
6247
6248 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6249 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
6250 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
6251 $isdue += $calctar;
6252 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
6253 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
6254 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6255 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
6256 $tars[$num]['calctar'] = $calctar;
6257 $isdue += $calctar;
6258 $pricestr[$num] = self::getPriceName($tars[$num]['idprice'], $vbo_tn) . ": " . $calctar . " " . $currencyname . (!empty($tars[$num]['attrdata']) ? "\n" . self::getPriceAttr($tars[$num]['idprice'], $vbo_tn) . ": " . $tars[$num]['attrdata'] : "");
6259 }
6260
6261 if (!empty($or['optionals'])) {
6262 $stepo = explode(";", $or['optionals']);
6263 foreach ($stepo as $roptkey => $oo) {
6264 if (empty($oo)) {
6265 continue;
6266 }
6267 $stept = explode(":", $oo);
6268 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
6269 $dbo->setQuery($q);
6270 $actopt = $dbo->loadAssocList();
6271 if ($actopt) {
6272 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
6273 $chvar = '';
6274 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
6275 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
6276 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
6277 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
6278 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
6279 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
6280 $agestept = explode('-', $stept[1]);
6281 $stept[1] = $agestept[0];
6282 $chvar = $agestept[1];
6283 $realcost = 0;
6284 if (!empty($chvar)) {
6285 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
6286 // percentage value of the adults tariff
6287 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6288 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6289 } else {
6290 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6291 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6292 }
6293 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
6294 // VBO 1.10 - percentage value of room base cost
6295 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6296 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6297 } else {
6298 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
6299 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6300 }
6301 }
6302 $actopt[0]['chageintv'] = $chvar;
6303 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
6304 $actopt[0]['quan'] = $stept[1];
6305 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
6306 }
6307 } else {
6308 $actopt[0]['quan'] = $stept[1];
6309 // VBO 1.11 - options percentage cost of the room total fee
6310 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6311 $deftar_basecosts = $or['cust_cost'];
6312 } else {
6313 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6314 }
6315 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
6316 //
6317 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
6318 }
6319 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
6320 $realcost = $actopt[0]['maxprice'];
6321 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
6322 $realcost = $actopt[0]['maxprice'] * $stept[1];
6323 }
6324 }
6325 if ($actopt[0]['perperson'] == 1) {
6326 $realcost = $realcost * $or['adults'];
6327 }
6328
6329 /**
6330 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
6331 *
6332 * @since 1.17.7 (J) - 1.7.7 (WP)
6333 */
6334 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
6335 if ($custom_calculation) {
6336 $realcost = (float) $custom_calculation[0];
6337 }
6338
6339 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
6340 $isdue += $tmpopr;
6341 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
6342 }
6343 }
6344 }
6345
6346 // custom extra costs
6347 if (!empty($or['extracosts'])) {
6348 $cur_extra_costs = json_decode($or['extracosts'], true);
6349 foreach ($cur_extra_costs as $eck => $ecv) {
6350 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
6351 $isdue += $ecplustax;
6352 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
6353 }
6354 }
6355 }
6356
6357 // force the original total amount if rates have changed
6358 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
6359 $isdue = $booking['total'];
6360 }
6361
6362 // mail subject
6363 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
6364 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
6365
6366 // inject the recipient of the message for the template
6367 $booking['for'] = $for;
6368
6369 // load template file that will get $booking as variable
6370 $tmpl = self::loadEmailTemplate($booking);
6371
6372 // parse email template
6373 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
6374 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
6375
6376 if ($send !== true) {
6377 // return the content of the email message parsed
6378 return $hmess;
6379 }
6380
6381 // when the message can be sent
6382 $sendwhen = self::getSendEmailWhen();
6383
6384 if ($no_config) {
6385 // force the configuration setting to send the message no matter what's the booking status
6386 $sendwhen = 1;
6387 }
6388
6389 // send the message
6390 foreach ($for as $who) {
6391 $use_subject = $subject;
6392 $recipients = array();
6393 $attachments = self::addEmailAttachment(null);
6394 $attach_ical = false;
6395 $force_replyto = null;
6396 if (strpos($who, '@') !== false) {
6397 // send email to custom email address
6398 array_push($recipients, trim($who));
6399 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6400 // send email to the customer
6401 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6402 continue;
6403 }
6404 array_push($recipients, $booking['custmail']);
6405 /**
6406 * Check whether an iCal should be attached for the customer.
6407 *
6408 * @since 1.12.0
6409 */
6410 $attach_ical = self::getEmailIcal('customer', $booking);
6411 } elseif (stripos($who, 'admin') !== false) {
6412 // send email to the administrator(s)
6413 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6414 continue;
6415 }
6416 $use_subject = $subject . ' #' . $booking['id'];
6417 if (!strcasecmp($type, 'modified')) {
6418 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6419 }
6420 $adminemail = self::getAdminMail();
6421 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6422 if (empty($adminemail) && empty($extra_admin_recipients)) {
6423 // Prevent Joomla Exceptions that would stop the script execution
6424 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6425 continue;
6426 }
6427 if (strpos($adminemail, ',') !== false) {
6428 // multiple addresses
6429 $adminemails = explode(',', $adminemail);
6430 foreach ($adminemails as $am) {
6431 if (strpos($am, '@') !== false) {
6432 array_push($recipients, trim($am));
6433 }
6434 }
6435 } else {
6436 // single address
6437 array_push($recipients, trim($adminemail));
6438 }
6439
6440 // merge extra recipients
6441 $recipients = array_merge($recipients, $extra_admin_recipients);
6442
6443 // admin should reply to the customer
6444 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6445
6446 /**
6447 * Check whether an iCal should be attached for the admin.
6448 *
6449 * @since 1.2.0
6450 */
6451 $attach_ical = self::getEmailIcal('admin', array(
6452 'ts' => $booking['ts'],
6453 'custdata' => $booking['custdata'],
6454 'checkin' => $booking['checkin'],
6455 'checkout' => $booking['checkout'],
6456 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6457 ));
6458 }
6459
6460 // send the message, recipients should always be an array to support multiple admin addresses
6461
6462 // get sender e-mail
6463 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6464
6465 // init mail data
6466 $mail = new VBOMailWrapper([
6467 'sender' => [$adsendermail, $ftitle],
6468 'recipient' => $recipients,
6469 'bcc' => self::addAdminEmailRecipient(null, true),
6470 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6471 'subject' => $use_subject,
6472 'content' => $hmess,
6473 'attachments' => $attachments,
6474 ]);
6475
6476 if ($attach_ical !== false && $booking['status'] != 'standby') {
6477 // attach iCal file
6478 $mail->addAttachment($attach_ical);
6479 }
6480
6481 /**
6482 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6483 *
6484 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6485 *
6486 * @since 1.15.4 (J) - 1.5.10 (WP)
6487 */
6488 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6489
6490 // send e-mail
6491 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6492
6493 // unlink iCal file
6494 if ($attach_ical !== false) {
6495 @unlink($attach_ical);
6496 }
6497 }
6498
6499 return $result;
6500 }
6501
6502 /**
6503 * This method allows to add one or more recipient email
6504 * addresses for the next queue of email sending for the admin.
6505 * This method can be used in the template file for the customer
6506 * email to register an additional email address, maybe when a
6507 * specific room-type is booked.
6508 * The methods sending the email messages are supposed to call this
6509 * method by passing no arguments to obtain the extra addresses set.
6510 *
6511 * @param mixed $email null, string or array of email address(es).
6512 * @param bool $bcc if true, addresses will be used as bcc.
6513 * @param bool $reset if true, the queues will be emptied.
6514 *
6515 * @return array the current extra recipients or bcc addresses set.
6516 *
6517 * @since 1.13 (J) - 1.3.0 (WP)
6518 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6519 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6520 */
6521 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6522 {
6523 static $extra_recipients = [];
6524 static $extra_bcc = [];
6525
6526 if ($reset) {
6527 $extra_recipients = [];
6528 $extra_bcc = [];
6529 }
6530
6531 if (!empty($email)) {
6532 if (is_scalar($email)) {
6533 if ($bcc) {
6534 array_push($extra_bcc, $email);
6535 } else {
6536 array_push($extra_recipients, $email);
6537 }
6538 } else {
6539 if ($bcc) {
6540 $extra_bcc = array_merge($extra_bcc, $email);
6541 } else {
6542 $extra_recipients = array_merge($extra_recipients, $email);
6543 }
6544 }
6545 }
6546
6547 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6548 }
6549
6550 /**
6551 * This method serves to add one or more attachments to the
6552 * next queue of email sending for the admin.
6553 * The methods sending the email messages are supposed to call this
6554 * method by passing a null argument to obtain the attachments set.
6555 *
6556 * @param mixed $file null or string with path to file to attach.
6557 * @param bool $reset if true, the queue will be emptied.
6558 *
6559 * @return array the current attachments set.
6560 *
6561 * @since 1.14
6562 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6563 */
6564 public static function addEmailAttachment($file, $reset = false)
6565 {
6566 static $extra_attachments = [];
6567
6568 if ($reset) {
6569 $extra_attachments = [];
6570 }
6571
6572 if (!empty($file)) {
6573 if (is_scalar($file)) {
6574 array_push($extra_attachments, $file);
6575 } else {
6576 $extra_attachments = array_merge($extra_attachments, $file);
6577 }
6578 }
6579
6580 return array_unique($extra_attachments);
6581 }
6582
6583 /**
6584 * This method is called whenever some rooms get booked.
6585 * It checks whether the rooms involved have shared calendars.
6586 * If some are found, then also such rooms will be occupied.
6587 *
6588 * @param int $bid the booking ID.
6589 * @param array $roomids the list of rooms booked.
6590 * @param int $checkin checkin timestamp.
6591 * @param int $checkout checkout timestamp.
6592 *
6593 * @return boolean true if some other cals were occupied, false otherwise.
6594 *
6595 * @since 1.13 (J) - 1.3.0 (WP)
6596 *
6597 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6598 */
6599 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6600 {
6601 $dbo = JFactory::getDbo();
6602 $bid = (int)$bid;
6603
6604 // availability helper
6605 $av_helper = self::getAvailabilityInstance();
6606
6607 // check for split stay rooms reservation
6608 $known_split_stay = false;
6609 $is_split_stay = 0;
6610
6611 if (empty($checkin) || empty($checkout)) {
6612 // get checkin and checkout timestamps from booking
6613 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6614 $dbo->setQuery($q);
6615 $dbo->execute();
6616 if (!$dbo->getNumRows()) {
6617 // booking not found
6618 return false;
6619 }
6620 $bdata = $dbo->loadAssoc();
6621 $checkin = $bdata['checkin'];
6622 $checkout = $bdata['checkout'];
6623 $is_split_stay = $bdata['split_stay'];
6624 // turn flag on
6625 $known_split_stay = true;
6626 }
6627
6628 if (!$known_split_stay) {
6629 // we need to query the db to access this information
6630 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6631 $dbo->setQuery($q, 0, 1);
6632 $dbo->execute();
6633 if ($dbo->getNumRows()) {
6634 $is_split_stay = (int)$dbo->loadResult();
6635 }
6636 // turn flag on
6637 $known_split_stay = true;
6638 }
6639
6640 // check split stay room records
6641 $room_stay_dates = [];
6642 if ($is_split_stay) {
6643 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6644 // immediately count the number of nights of stay for each split room
6645 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6646 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6647 // overwrite values for compatibility with non-confirmed bookings
6648 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6649 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6650 }
6651 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6652 // overwrite the whole array
6653 $room_stay_dates[$sps_r_k] = $sps_r_v;
6654 }
6655 }
6656
6657 // get the rooms booked
6658 if (!count($roomids)) {
6659 // get the IDs of all rooms booked
6660 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6661 $dbo->setQuery($q);
6662 $orr = $dbo->loadAssocList();
6663 if ($orr) {
6664 foreach ($orr as $or) {
6665 array_push($roomids, $or['idroom']);
6666 }
6667 }
6668 }
6669 if (!count($roomids) || empty($bid)) {
6670 // unable to proceed
6671 return false;
6672 }
6673 $roomids = array_unique($roomids);
6674
6675 // build room stay dates map in case of split stay
6676 $room_split_stay_map = [];
6677 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6678 // we have unique room IDs in a split stay reservation for more than one room
6679 foreach ($room_stay_dates as $room_stay_date) {
6680 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6681 }
6682 }
6683
6684 // get rooms involved
6685 $involved = [];
6686 $involved_stay_map = [];
6687 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6688 $dbo->setQuery($q);
6689 $rooms_found = $dbo->loadAssocList();
6690 if (!$rooms_found) {
6691 // no rooms involved that need their calendars updated
6692 return false;
6693 }
6694 foreach ($rooms_found as $rf) {
6695 if (!in_array($rf['mainroom'], $roomids)) {
6696 // push room ID
6697 array_push($involved, $rf['mainroom']);
6698 // check for specific stay dates in connected room
6699 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6700 // split stay information available
6701 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6702 }
6703 }
6704 if (!in_array($rf['childroom'], $roomids)) {
6705 // push room ID
6706 array_push($involved, $rf['childroom']);
6707 // check for specific stay dates in connected room
6708 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6709 // split stay information available
6710 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6711 }
6712 }
6713 }
6714 $involved = array_unique($involved);
6715 if (!count($involved)) {
6716 // no rooms involved
6717 return false;
6718 }
6719
6720 // turnover seconds
6721 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6722
6723 // occupy the calendars for the involved rooms found
6724 $bids_generated = [];
6725 foreach ($involved as $rid) {
6726 // determine the values to use
6727 $room_checkin = (int)$checkin;
6728 $room_checkout = (int)$checkout;
6729 $room_realback = ($checkout + $turnover_secs);
6730 if (isset($involved_stay_map[$rid])) {
6731 // split stay dates available
6732 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6733 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6734 $room_realback = ($room_checkout + $turnover_secs);
6735 }
6736
6737 // occupy record in room with shared calendar
6738 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6739 $dbo->setQuery($q);
6740 $dbo->execute();
6741 array_push($bids_generated, $dbo->insertid());
6742 }
6743
6744 // store busy relations created
6745 foreach ($bids_generated as $busyid) {
6746 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6747 $dbo->setQuery($q);
6748 $dbo->execute();
6749 }
6750
6751 return true;
6752 }
6753
6754 /**
6755 * This method is needed whenever a booking gets modified by
6756 * adding or removing rooms. This way we reset (remove) all
6757 * busy records that were previously stored due to a shared
6758 * calendar. The correct relations should then be re-created
6759 * by calling updateSharedCalendars().
6760 *
6761 * @param int $bid the booking ID.
6762 *
6763 * @return boolean true if some records were cleaned.
6764 *
6765 * @see updateSharedCalendars() should be called after.
6766 *
6767 * @since 1.3.0
6768 */
6769 public static function cleanSharedCalendarsBusy($bid) {
6770 $dbo = JFactory::getDbo();
6771 $bid = (int)$bid;
6772 // get all the occupied records due to shared calendars for this booking
6773 $q = "SELECT `b`.`id`, `b`.`idroom`, `b`.`sharedcal`, `ob`.`idorder`, `ob`.`idbusy` FROM `#__vikbooking_busy` AS `b` LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id` WHERE `ob`.`idorder`={$bid} AND `b`.`sharedcal`=1;";
6774 $dbo->setQuery($q);
6775 $allbusy = $dbo->loadAssocList();
6776 if (!$allbusy) {
6777 return false;
6778 }
6779
6780 $busy_ids = array();
6781 foreach ($allbusy as $b) {
6782 // push busy ID to be removed later
6783 array_push($busy_ids, $b['id']);
6784 // delete the current busy ID-booking ID relation for this shared calendar
6785 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6786 $dbo->setQuery($q);
6787 $dbo->execute();
6788 }
6789 if (count($busy_ids)) {
6790 // delete all busy records due to shared calendars
6791 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6792 $dbo->setQuery($q);
6793 $dbo->execute();
6794 }
6795
6796 return true;
6797 }
6798
6799 public static function sendJutility() {
6800 //deprecated in VBO 1.10
6801 $dbo = JFactory::getDbo();
6802 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6803 $dbo->setQuery($q);
6804 $dbo->execute();
6805 $s = $dbo->loadAssocList();
6806 return (intval($s[0]['setting']) == 1 ? true : false);
6807 }
6808
6809 public static function getCategoryName($idcat, $vbo_tn = null) {
6810 $dbo = JFactory::getDbo();
6811 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6812 $dbo->setQuery($q);
6813 $dbo->execute();
6814 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6815 if (is_object($vbo_tn) && count($p) > 0) {
6816 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6817 }
6818 return count($p) > 0 ? $p[0]['name'] : '';
6819 }
6820
6821 public static function loadAdultsDiff($idroom, $adults)
6822 {
6823 static $obp_map = [];
6824
6825 $map_sign = "{$idroom}:{$adults}";
6826
6827 if (array_key_exists($map_sign, $obp_map)) {
6828 return $obp_map[$map_sign];
6829 }
6830
6831 $dbo = JFactory::getDbo();
6832
6833 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6834 $dbo->setQuery($q, 0, 1);
6835 $obp_diff = $dbo->loadAssoc();
6836 if ($obp_diff) {
6837 // cache value and return it
6838 $obp_map[$map_sign] = $obp_diff;
6839
6840 return $obp_diff;
6841 }
6842
6843 // cache value and return it
6844 $obp_map[$map_sign] = null;
6845
6846 return null;
6847 }
6848
6849 public static function loadRoomAdultsDiff($idroom)
6850 {
6851 $dbo = JFactory::getDbo();
6852
6853 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6854 $dbo->setQuery($q);
6855 $diff = $dbo->loadAssocList();
6856 if ($diff) {
6857 $roomdiff = [];
6858 foreach ($diff as $v) {
6859 $roomdiff[$v['adults']] = $v;
6860 }
6861
6862 return $roomdiff;
6863 }
6864
6865 return [];
6866 }
6867
6868 public static function occupancyOverrideExists($tar, $adults) {
6869 foreach ($tar as $k => $v) {
6870 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6871 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6872 return $v['occupancy_ovr'][$adults];
6873 }
6874 }
6875 }
6876 return false;
6877 }
6878
6879 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6880 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6881 $charges = array();
6882 if (!($children > 0) || !(count($ages) > 0)) {
6883 return $charges;
6884 }
6885 $dbo = JFactory::getDbo();
6886 $id_options = array();
6887 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6888 $dbo->setQuery($q);
6889 $dbo->execute();
6890 if ($dbo->getNumRows() > 0) {
6891 $assocs = $dbo->loadAssocList();
6892 foreach ($assocs as $opts) {
6893 if (!empty($opts['idopt'])) {
6894 $r_ido = explode(';', rtrim($opts['idopt']));
6895 foreach ($r_ido as $ido) {
6896 if (!empty($ido) && !in_array($ido, $id_options)) {
6897 $id_options[] = $ido;
6898 }
6899 }
6900 }
6901 }
6902 }
6903 if (count($id_options) > 0) {
6904 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $id_options).") AND `ifchildren`=1 AND (LENGTH(`ageintervals`) > 0 OR `ageintervals` IS NOT NULL) LIMIT 1;";
6905 $dbo->setQuery($q);
6906 $dbo->execute();
6907 if ($dbo->getNumRows() > 0) {
6908 $ageintervals = $dbo->loadAssocList();
6909 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6910 $age_range = array();
6911 foreach ($split_ages as $kg => $spage) {
6912 if (empty($spage)) {
6913 continue;
6914 }
6915 $parts = explode('_', $spage);
6916 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6917 $ind = count($age_range);
6918 $age_range[$ind]['from'] = intval($parts[0]);
6919 $age_range[$ind]['to'] = intval($parts[1]);
6920 //taxes are calculated later in VCM
6921 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6922 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6923 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6924 }
6925 }
6926 if (count($age_range) > 0) {
6927 $tot_charge = 0;
6928 $affected = array();
6929 $option_str = '';
6930 foreach ($ages as $age) {
6931 if (strlen($age) == 0) {
6932 continue;
6933 }
6934 foreach ($age_range as $range) {
6935 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6936 $tot_charge += $range['cost'];
6937 $affected[] = $age;
6938 $option_str .= $range['option_str'].';';
6939 break;
6940 }
6941 }
6942 }
6943 if ($tot_charge > 0) {
6944 $charges['total'] = $tot_charge;
6945 $charges['affected'] = $affected;
6946 $charges['options'] = $option_str;
6947 }
6948 }
6949 }
6950 }
6951
6952 return $charges;
6953 }
6954
6955 public static function sortRoomPrices($arr) {
6956 $newarr = array();
6957 foreach ($arr as $k => $v) {
6958 $newarr[$k] = $v['cost'];
6959 }
6960 asort($newarr);
6961 $sorted = array();
6962 foreach ($newarr as $k => $v) {
6963 $sorted[$k] = $arr[$k];
6964 }
6965 return $sorted;
6966 }
6967
6968 public static function sortResults($arr) {
6969 $newarr = array();
6970 foreach ($arr as $k => $v) {
6971 $newarr[$k] = $v[0]['cost'];
6972 }
6973 asort($newarr);
6974 $sorted = array();
6975 foreach ($newarr as $k => $v) {
6976 $sorted[$k] = $arr[$k];
6977 }
6978 return $sorted;
6979 }
6980
6981 public static function sortMultipleResults($arr) {
6982 foreach ($arr as $k => $v) {
6983 $newarr = array();
6984 foreach ($v as $subk => $subv) {
6985 $newarr[$subk] = $subv[0]['cost'];
6986 }
6987 asort($newarr);
6988 $sorted = array();
6989 foreach ($newarr as $nk => $v) {
6990 $sorted[$nk] = $arr[$k][$nk];
6991 }
6992 $arr[$k] = $sorted;
6993 }
6994 return $arr;
6995 }
6996
6997 /**
6998 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6999 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
7000 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
7001 *
7002 * @param array $promos optional list of promotion IDs to register in the execution flow.
7003 *
7004 * @return array list of cached promotion IDs (if any).
7005 *
7006 * @since 1.16.4 (J) - 1.6.4 (WP)
7007 */
7008 public static function registerPromotionIds(array $promos = [])
7009 {
7010 static $flagged_promos = [];
7011
7012 if ($promos) {
7013 // set cached value
7014 $flagged_promos = $promos;
7015 }
7016
7017 return $flagged_promos;
7018 }
7019
7020 /**
7021 * Getter and setter for seasons cache allowed.
7022 *
7023 * @param bool $enabled true or false for setter, null for getter.
7024 *
7025 * @return bool|null
7026 *
7027 * @since 1.16.5 (J) - 1.6.5 (WP)
7028 */
7029 public static function setSeasonsCache($enabled = null)
7030 {
7031 static $seasons_cache = null;
7032
7033 if ($enabled !== null) {
7034 // set value
7035 $seasons_cache = (bool)$enabled;
7036 }
7037
7038 return $seasons_cache;
7039 }
7040
7041 /**
7042 * Allows to preload and cache seasonal records for a list of rooms and dates.
7043 * Any subsequent preloading of season records for the same room(s) will return
7044 * the cached list. Should be used to preload a large window of data so that
7045 * subsequent calls will always return cached value.
7046 *
7047 * @param array $rooms List of involved room IDs.
7048 * @param int|bool $from Unix timestamp for start date or false to unset cache.
7049 * @param int $to Unix timestamp for end date.
7050 *
7051 * @return ?array
7052 *
7053 * @since 1.17.2 (J) - 1.7.2 (WP)
7054 */
7055 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
7056 {
7057 static $preloaded_records = [];
7058
7059 if (!$rooms) {
7060 // no cached signature
7061 return [];
7062 }
7063
7064 $signature = md5(implode(',', $rooms));
7065
7066 if ($from === false) {
7067 // unset cached records
7068 unset($preloaded_records[$signature]);
7069
7070 return;
7071 }
7072
7073 if ($from !== null && $to !== null) {
7074 // setter args signature for preloading records
7075 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
7076
7077 return;
7078 }
7079
7080 // getter args signature for getting the preloaded and cached records
7081 return $preloaded_records[$signature] ?? [];
7082 }
7083
7084 /**
7085 * Fetches all season records affecting a range of date timestamps.
7086 * Useful to pre-cache season records in case of hundreds of thousands
7087 * of records, but it can use up several MBs of server's memory.
7088 *
7089 * @param int $from unix timestamp for start date.
7090 * @param int $to unix timestamp for end date.
7091 * @param array $rooms optional list of involved room IDs.
7092 *
7093 * @return array
7094 *
7095 * @since 1.16.10 (J) - 1.6.10 (WP)
7096 */
7097 public static function getDateSeasonRecords($from, $to, array $rooms = [])
7098 {
7099 /**
7100 * We allow external systems to preload season records and cache them.
7101 *
7102 * @since 1.17.2 (J) - 1.7.2 (WP)
7103 */
7104 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
7105 return $preloaded_records;
7106 }
7107
7108 /**
7109 * For a more accurate records caching, ensure we have a range of two dates at least.
7110 *
7111 * @since 1.17.2 (J) - 1.7.2 (WP)
7112 */
7113 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
7114 // add one day to the end timestamp
7115 $to = strtotime('+1 day', $to);
7116 }
7117
7118 $dbo = JFactory::getDbo();
7119
7120 $one = getdate($from);
7121 $two = getdate($to);
7122
7123 // leap years
7124 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7125 $isleap = true;
7126 } else {
7127 $isleap = false;
7128 }
7129
7130 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7131 $tomidnightone = intval($one['hours']) * 3600;
7132 $tomidnightone += intval($one['minutes']) * 60;
7133 $sfrom = $from - $baseone - $tomidnightone;
7134 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7135 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7136 $tomidnighttwo = intval($two['hours']) * 3600;
7137 $tomidnighttwo += intval($two['minutes']) * 60;
7138 $sto = $to - $basetwo - $tomidnighttwo;
7139
7140 // leap years, check what dates to manipulate
7141 if ($isleap) {
7142 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7143 if ($one[0] > $leapts) {
7144 /**
7145 * Timestamp must be greater than the leap-day of Feb 29th.
7146 * It used to be checked for >= $leapts.
7147 *
7148 * @since July 3rd 2019
7149 */
7150 $sfrom -= 86400;
7151 }
7152 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7153 // lower checkin date when in leap year but not for checkout
7154 $sto -= 86400;
7155 }
7156 }
7157
7158 // check for DST changes to adjust the query values to fetch
7159 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7160 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7161 // we are parsing the day when the DST changed (probably a Sunday)
7162 if (!date('I', $from)) {
7163 // DST was just turned off
7164 $sfrom -= 3600;
7165 }
7166 }
7167 }
7168
7169 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7170 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7171 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7172 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7173 ($sto > $sfrom ? "OR (`from` >= " . $sfrom . " AND `from` <= " . $sto . " AND `to` >= " . $sfrom . " AND `to` <= " . $sto . ")" : "OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` > `to`)") .
7174 ($sto > $sfrom ? " OR (`from` <= " . $sfrom . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`) OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` >= " . $sto . " AND `from` > `to`)" : " OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . " AND `from` >= " . $sto . " AND `to` > " . $sto . " AND `from` < `to`)") .
7175 ($sto > $sfrom ? " OR (`from` >= " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` < " . $sfrom . " AND `to` >= " . $sto . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `from` < `to`)") .
7176 ($sto > $sfrom ? " OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` > " . $sfrom . " AND `to` > " . $sto . " AND `from` < `to`) OR (`from` < " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` < `to`)") .
7177 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7178 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7179
7180 /**
7181 * Avoid issues when querying data for a whole year by fetching all records.
7182 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
7183 *
7184 * @since 1.17.3 (J) - 1.7.3 (WP)
7185 */
7186 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
7187 // fetch all records when targeting a whole year of data
7188 $q = $dbo->getQuery(true)
7189 ->select('*')
7190 ->from($dbo->qn('#__vikbooking_seasons'))
7191 ->order($dbo->qn('promo') . ' ASC');
7192 }
7193
7194 $dbo->setQuery($q);
7195 $seasons = $dbo->loadAssocList();
7196
7197 if ($rooms) {
7198 // filter records by affected room IDs
7199 $seasons = array_filter($seasons, function($s) use ($rooms) {
7200 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
7201 foreach ($rooms as $idroom) {
7202 if (in_array("-" . $idroom . "-", $allrooms)) {
7203 return true;
7204 }
7205 }
7206 return false;
7207 });
7208
7209 // reset array keys
7210 $seasons = array_values($seasons);
7211 }
7212
7213 return $seasons;
7214 }
7215
7216 /**
7217 * Fetches all season records affecting week days with no dates.
7218 * Useful to avoid duplicate queries and cache/preload the records.
7219 *
7220 * @return array
7221 *
7222 * @since 1.18.5 (J) - 1.8.5 (WP)
7223 */
7224 public static function getWdaySeasonRecords()
7225 {
7226 static $cached_wdayseasons = null;
7227
7228 if (!is_null($cached_wdayseasons)) {
7229 return $cached_wdayseasons;
7230 }
7231
7232 $dbo = JFactory::getDbo();
7233
7234 $dbo->setQuery("SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));");
7235 $cached_wdayseasons = $dbo->loadAssocList();
7236
7237 return $cached_wdayseasons;
7238 }
7239
7240 /**
7241 * Applies the seasonal rates over a list of room rate records.
7242 *
7243 * @param array $arr list of room rate records.
7244 * @param int $from unix timestamp for start date.
7245 * @param int $to unix timestamp for end date.
7246 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7247 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7248 *
7249 * @return array list of manipulated room rate records.
7250 *
7251 * @since 1.18.0 (J) - 1.8.0 (WP) added support to records caching and preloading with args $seasons_dates and $seasons_wdays.
7252 * @since 1.18.5 (J) - 1.8.5 (WP) argument $seasons_wdays is nullable to allow preloading an empty list of records and save queries.
7253 */
7254 public static function applySeasonalPrices(array $arr, $from, $to, array $seasons_dates = [], ?array $seasons_wdays = null)
7255 {
7256 static $cached_seasons = [];
7257
7258 $cache_signature_wdays = '';
7259 if (self::setSeasonsCache() !== false && !$seasons_wdays) {
7260 // enable week-day season records caching
7261 $cache_signature_wdays = "wdays";
7262 }
7263
7264 $dbo = JFactory::getDbo();
7265 $vbo_tn = self::getTranslator();
7266
7267 $roomschange = [];
7268 $one = getdate($from);
7269
7270 // leap years
7271 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7272 $isleap = true;
7273 } else {
7274 $isleap = false;
7275 }
7276
7277 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7278 $tomidnightone = intval($one['hours']) * 3600;
7279 $tomidnightone += intval($one['minutes']) * 60;
7280 $sfrom = $from - $baseone - $tomidnightone;
7281 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7282 $two = getdate($to);
7283 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7284 $tomidnighttwo = intval($two['hours']) * 3600;
7285 $tomidnighttwo += intval($two['minutes']) * 60;
7286 $sto = $to - $basetwo - $tomidnighttwo;
7287
7288 // leap years, check what dates to manipulate
7289 if ($isleap) {
7290 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7291 if ($one[0] > $leapts) {
7292 /**
7293 * Timestamp must be greater than the leap-day of Feb 29th.
7294 * It used to be checked for >= $leapts.
7295 *
7296 * @since July 3rd 2019
7297 */
7298 $sfrom -= 86400;
7299 }
7300 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7301 // lower checkin date when in leap year but not for checkout
7302 $sto -= 86400;
7303 }
7304 }
7305
7306 // check for DST changes to adjust the query values to fetch
7307 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7308 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7309 // we are parsing the day when the DST changed (probably a Sunday)
7310 if (!date('I', $from)) {
7311 // DST was just turned off
7312 $sfrom -= 3600;
7313 }
7314 }
7315 }
7316
7317 // count nights requested
7318 $booking_nights = 1;
7319 foreach ($arr as $k => $a) {
7320 if (!isset($a[0])) {
7321 continue;
7322 }
7323 if (isset($a[0]['booking_nights'])) {
7324 // this value may be set when displaying pricing calendars
7325 $booking_nights = $a[0]['booking_nights'];
7326 break;
7327 }
7328 if (isset($a[0]['days'])) {
7329 $booking_nights = $a[0]['days'];
7330 break;
7331 }
7332 }
7333
7334 /**
7335 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7336 *
7337 * @since 1.16.4 (J) - 1.6.4 (WP)
7338 */
7339 $skip_promo_ids = self::registerPromotionIds();
7340
7341 $totseasons = 0;
7342 if (!$seasons_dates) {
7343 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7344 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7345 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7346 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7347 ($sto > $sfrom ? "OR (`from` >= " . $sfrom . " AND `from` <= " . $sto . " AND `to` >= " . $sfrom . " AND `to` <= " . $sto . ")" : "OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` > `to`)") .
7348 ($sto > $sfrom ? " OR (`from` <= " . $sfrom . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`) OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` >= " . $sto . " AND `from` > `to`)" : " OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . " AND `from` >= " . $sto . " AND `to` > " . $sto . " AND `from` < `to`)") .
7349 ($sto > $sfrom ? " OR (`from` >= " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` < " . $sfrom . " AND `to` >= " . $sto . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `from` < `to`)") .
7350 ($sto > $sfrom ? " OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` > " . $sfrom . " AND `to` > " . $sto . " AND `from` < `to`) OR (`from` < " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` < `to`)") .
7351 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7352 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7353
7354 // get the season records by running the query
7355 $dbo->setQuery($q);
7356 $seasons = $dbo->loadAssocList();
7357
7358 // count total seasons
7359 $totseasons = $seasons ? count($seasons) : 0;
7360 }
7361
7362 if ($totseasons > 0 || $seasons_dates) {
7363 if ($totseasons > 0) {
7364 $seasons = $seasons;
7365 } else {
7366 $seasons = $seasons_dates;
7367 }
7368 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7369 $applyseasons = false;
7370 $mem = [];
7371 foreach ($arr as $k => $a) {
7372 $mem[$k]['daysused'] = 0;
7373 $mem[$k]['sum'] = [];
7374 $mem[$k]['spids'] = [];
7375 /**
7376 * The keys below are all needed to apply the promotions on the room's final cost.
7377 *
7378 * @since 1.13.5
7379 */
7380 $mem[$k]['diffs'] = [];
7381 $mem[$k]['trans_keys'] = [];
7382 $mem[$k]['trans_factors'] = [];
7383 $mem[$k]['trans_affdays'] = [];
7384 }
7385 foreach ($seasons as $s) {
7386 // check if this is a promotion registered for skipping
7387 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7388 continue;
7389 }
7390
7391 // Special Price tied to the year
7392 if (!empty($s['year']) && $s['year'] > 0) {
7393 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7394 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7395 //VBO 1.9 - tied to the year can be set for prev year (Dec 27 to Jan 3) and booking can be Jan 1 to Jan 3 - do not skip in this case
7396 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7397 continue;
7398 }
7399 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7400 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7401 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7402 if ($calc_ends < ($from - $tomidnightone)) {
7403 continue;
7404 }
7405 }
7406 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7407 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7408 if ($s['from'] > $s['to']) {
7409 continue;
7410 }
7411 if (($basetwo + $s['from'] + 86399) > $to) {
7412 /**
7413 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7414 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7415 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7416 *
7417 * @since 1.14.3 (J) - 1.4.3 (WP)
7418 */
7419 continue;
7420 }
7421 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7422 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7423 /**
7424 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7425 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7426 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7427 *
7428 * @since 1.14.3 (J) - 1.4.3 (WP)
7429 */
7430 continue;
7431 }
7432 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7433 // season tied to the year 2017 accross 2018 and we are parsing dates at the beginning of 2017 due to beginning loop in 2016 (Rates Overview)
7434 if (($baseone + $s['from']) > $to) {
7435 continue;
7436 }
7437 }
7438 }
7439 //
7440 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7441 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7442 $inits = $baseone + $s['from'];
7443 if ($s['from'] < $s['to']) {
7444 $ends = $basetwo + $s['to'];
7445 // check if the inits must be set to the year after
7446 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7447 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7448 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7449 $inits = $tmpbase + $s['from'];
7450 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7451 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7452 $ends = $baseone + $s['to'];
7453 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7454 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7455 $ends = $baseone + $s['to'];
7456 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7457 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7458 $inits = $basetwo + $s['from'];
7459 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7460 // booking dates across two years for a season tied to a specific year
7461 if ($one['year'] == $s['year']) {
7462 $inits = $baseone + $s['from'];
7463 } else {
7464 $inits = $basetwo + $s['from'];
7465 }
7466 if ($two['year'] == $s['year']) {
7467 $ends = $basetwo + $s['to'];
7468 } else {
7469 $ends = $baseone + $s['to'];
7470 }
7471 }
7472 } else {
7473 // between 2 years
7474 if ($baseone < $basetwo) {
7475 // ex. 29/12/2012 - 14/01/2013
7476 $ends = $basetwo + $s['to'];
7477 } else {
7478 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7479 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7480 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7481 $ends = $tmpbase + $s['to'];
7482 } else {
7483 // ex. 03/01 - 09/01
7484 $ends = $basetwo + $s['to'];
7485 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7486 $inits = $tmpbase + $s['from'];
7487 }
7488 }
7489 }
7490 // leap years
7491 if ($isleap == true) {
7492 $infoseason = getdate($inits);
7493 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7494 // leap years, check what dates to manipulate
7495 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7496 /**
7497 * Timestamp must be greater than the leap-day of Feb 29th.
7498 * It used to be checked for >= $leapts.
7499 *
7500 * @since July 3rd 2019
7501 */
7502 $inits += 86400;
7503 if ($s['from'] < $s['to']) {
7504 // increase season end date only if still on the same leap year
7505 $ends += 86400;
7506 }
7507 }
7508 }
7509
7510 // promotions
7511 $promotion = [];
7512 if (($s['promo'] ?? 0) == 1) {
7513 $daysadv = (($inits - time()) / 86400);
7514 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7515 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7516 continue;
7517 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7518 $secstocheckin = ($from - time());
7519 if ($s['promolastmin'] < $secstocheckin) {
7520 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7521 continue;
7522 }
7523 }
7524 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7525 /**
7526 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7527 *
7528 * @since 1.13.5
7529 */
7530 continue;
7531 }
7532 $promotion['todaydaysadv'] = $daysadv;
7533 $promotion['promodaysadv'] = $s['promodaysadv'];
7534 $promotion['promotxt'] = $s['promotxt'];
7535 }
7536
7537 // occupancy override
7538 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7539
7540 // week days
7541 $filterwdays = !empty($s['wdays']) ? true : false;
7542 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7543 if (is_array($wdays) && $wdays) {
7544 foreach ($wdays as $kw => $wd) {
7545 if (strlen($wd) == 0) {
7546 unset($wdays[$kw]);
7547 }
7548 }
7549 }
7550
7551 // checkin must be after the beginning of the season
7552 $checkininclok = true;
7553 if ($s['checkinincl'] == 1) {
7554 $checkininclok = false;
7555 if ($s['from'] < $s['to']) {
7556 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7557 $checkininclok = true;
7558 }
7559 } else {
7560 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7561 $checkininclok = true;
7562 }
7563 }
7564 }
7565 if ($checkininclok !== true) {
7566 continue;
7567 }
7568
7569 foreach ($arr as $k => $a) {
7570 // applied only to some types of price
7571 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7572 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7573 continue;
7574 }
7575 }
7576 // applied only to some room types
7577 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7578 continue;
7579 }
7580
7581 // count affected nights of stay
7582 $affdays = 0;
7583 $season_fromdayts = $fromdayts;
7584 $is_dst = date('I', $season_fromdayts);
7585 for ($i = 0; $i < $a[0]['days']; $i++) {
7586 $todayts = $season_fromdayts + ($i * 86400);
7587 $is_now_dst = date('I', $todayts);
7588 if ($is_dst != $is_now_dst) {
7589 // Daylight Saving Time has changed, check how
7590 if ((bool)$is_dst === true) {
7591 $todayts += 3600;
7592 $season_fromdayts += 3600;
7593 } else {
7594 $todayts -= 3600;
7595 $season_fromdayts -= 3600;
7596 }
7597 $is_dst = $is_now_dst;
7598 }
7599 if ($todayts >= $inits && $todayts <= $ends) {
7600 // week days
7601 if ($filterwdays == true) {
7602 $checkwday = getdate($todayts);
7603 if (in_array($checkwday['wday'], $wdays)) {
7604 $affdays++;
7605 }
7606 } else {
7607 $affdays++;
7608 }
7609 //
7610 }
7611 }
7612
7613 if (!($affdays > 0)) {
7614 // no nights affected
7615 continue;
7616 }
7617
7618 // apply the rule
7619 $applyseasons = true;
7620 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7621
7622 // modification factor object
7623 $factor = new stdClass;
7624
7625 // calculate new price progressively
7626 if (intval($s['val_pcent']) == 2) {
7627 // percentage value
7628 $factor->pcent = 1;
7629 $pctval = $s['diffcost'];
7630 if (strlen((string)$s['losoverride'])) {
7631 // values overrides
7632 $arrvaloverrides = [];
7633 $valovrparts = explode('_', $s['losoverride']);
7634 foreach ($valovrparts as $valovr) {
7635 if (!empty($valovr)) {
7636 $ovrinfo = explode(':', $valovr);
7637 if (strstr($ovrinfo[0], '-i') != false) {
7638 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7639 if ((int)$ovrinfo[0] < $a[0]['days']) {
7640 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7641 }
7642 }
7643 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7644 }
7645 }
7646 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7647 $pctval = $arrvaloverrides[$a[0]['days']];
7648 }
7649 }
7650 if (intval($s['type']) == 1) {
7651 // charge
7652 $factor->type = '+';
7653 $cpercent = 100 + $pctval;
7654 } else {
7655 // discount
7656 $factor->type = '-';
7657 $cpercent = 100 - $pctval;
7658 }
7659 $factor->amount = $pctval;
7660 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7661 } else {
7662 // absolute value
7663 $factor->pcent = 0;
7664 $absval = $s['diffcost'];
7665 if (strlen((string)$s['losoverride'])) {
7666 // values overrides
7667 $arrvaloverrides = [];
7668 $valovrparts = explode('_', $s['losoverride']);
7669 foreach ($valovrparts as $valovr) {
7670 if (!empty($valovr)) {
7671 $ovrinfo = explode(':', $valovr);
7672 if (strstr($ovrinfo[0], '-i') != false) {
7673 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7674 if ((int)$ovrinfo[0] < $a[0]['days']) {
7675 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7676 }
7677 }
7678 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7679 }
7680 }
7681 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7682 $absval = $arrvaloverrides[$a[0]['days']];
7683 }
7684 }
7685 if (intval($s['type']) == 1) {
7686 // charge
7687 $factor->type = '+';
7688 $newprice = ($dailyprice + $absval) * $affdays;
7689 } else {
7690 // discount
7691 $factor->type = '-';
7692 $newprice = ($dailyprice - $absval) * $affdays;
7693 }
7694 $factor->amount = $absval;
7695 }
7696
7697 // apply rounding
7698 $factor->roundmode = $s['roundmode'] ?? null;
7699 if (!empty($s['roundmode'])) {
7700 $newprice = round($newprice, 0, constant($s['roundmode']));
7701 } else {
7702 $newprice = round($newprice, 2);
7703 }
7704
7705 // define the promotion (only if no value overrides set the amount to 0)
7706 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7707 /**
7708 * Include the discount information (if any). The cost re-calculated may not be
7709 * precise if multiple special prices were applied over the same dates.
7710 *
7711 * @since 1.13
7712 */
7713 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7714 $promotion['discount'] = [
7715 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7716 'pcent' => (int)($s['val_pcent'] == 2),
7717 ];
7718 }
7719 //
7720 $mem[$k]['promotion'] = $promotion;
7721 }
7722
7723 // define the occupancy override
7724 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7725 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7726 }
7727
7728 // push special price ID
7729 if (!in_array($s['id'], $mem[$k]['spids'])) {
7730 array_push($mem[$k]['spids'], $s['id']);
7731 }
7732
7733 // push difference generated only if to be applied progressively
7734 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7735 /**
7736 * Push the difference generated by this special price for later transliteration of final price,
7737 * only if the special price is calculated progressively and not on the final price.
7738 *
7739 * @since 1.13.5
7740 */
7741 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7742 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7743 /**
7744 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7745 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7746 * all differences applied by the previous special pricing rules.
7747 *
7748 * @since 1.13.5
7749 */
7750 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7751 array_push($mem[$k]['trans_factors'], $factor);
7752 array_push($mem[$k]['trans_affdays'], $affdays);
7753 }
7754
7755 // push values in memory array
7756 array_push($mem[$k]['sum'], $newprice);
7757 $mem[$k]['daysused'] += $affdays;
7758 array_push($roomschange, $a[0]['idroom']);
7759 }
7760 }
7761 if ($applyseasons) {
7762 foreach ($mem as $k => $v) {
7763 if ($v['daysused'] > 0 && $v['sum']) {
7764 $newprice = 0;
7765 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7766 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7767 $addrest = $restdays * $dailyprice;
7768 $newprice += $addrest;
7769
7770 // calculate new final cost
7771 $redo_rounding = null;
7772 foreach ($v['sum'] as $sum_index => $add) {
7773 /**
7774 * The application of the various special pricing rules is made in a progressive and cumulative way
7775 * by always starting from the room base cost or its average daily cost. However, promotions may need
7776 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7777 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7778 * onto the differences generated by all the regular and progressively applied special pricing rules.
7779 *
7780 * @since 1.13.5
7781 */
7782 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7783 /**
7784 * This progressive price difference must be applied on the room final cost, so we need to
7785 * apply the transliteration over the other differences applied by other special prices.
7786 */
7787 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7788 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7789 // this is the % promotion we are looking for applying it on the final cost
7790 $factor = $v['trans_factors'][$transliterate_key];
7791 if (is_object($factor) && $factor->pcent) {
7792 $final_factor = 0;
7793 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7794 $final_factor += $prog_diff * $factor->amount / 100;
7795 }
7796 // update rounding
7797 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7798
7799 /**
7800 * Leverage the final factor depending on how many nights this affected.
7801 *
7802 * @since 1.15.0 (J) - 1.5.0 (WP)
7803 */
7804 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7805 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7806 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7807 }
7808
7809 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7810 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7811 }
7812 }
7813 }
7814
7815 // apply new price progressively
7816 $newprice += $add;
7817 }
7818
7819 // apply rounding from factor
7820 if (!empty($redo_rounding)) {
7821 $newprice = round($newprice, 0, constant($redo_rounding));
7822 }
7823
7824 // set promotion (if any)
7825 if (isset($v['promotion'])) {
7826 $arr[$k][0]['promotion'] = $v['promotion'];
7827 }
7828
7829 // set occupancy overrides (if any)
7830 if (isset($v['occupancy_ovr'])) {
7831 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7832 }
7833
7834 // set new final cost and update nights affected
7835 $arr[$k][0]['cost'] = $newprice;
7836 $arr[$k][0]['affdays'] = $v['daysused'];
7837 if (array_key_exists('spids', $v) && $v['spids']) {
7838 $arr[$k][0]['spids'] = $v['spids'];
7839 }
7840 }
7841 }
7842 }
7843 }
7844
7845 // week days with no season
7846 $roomschange = array_unique($roomschange);
7847 $totspecials = 0;
7848 if (!is_array($seasons_wdays)) {
7849 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7850 // avoid making a query
7851 $specials = $cached_seasons[$cache_signature_wdays];
7852 } else {
7853 // get the week-days-with-no-season records by running the query
7854 $specials = self::getWdaySeasonRecords();
7855 }
7856
7857 // count records
7858 $totspecials = $specials ? count($specials) : 0;
7859
7860 if ($cache_signature_wdays) {
7861 // cache week-days-with-no-season records
7862 $cached_seasons[$cache_signature_wdays] = $specials;
7863 }
7864 }
7865 if ($totspecials > 0 || $seasons_wdays) {
7866 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7867 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7868 $applyseasons = false;
7869 /**
7870 * We no longer unset the previous memory of the seasons with dates filters
7871 * because we need the responses to be merged. We do it only if not set.
7872 * We only keep the property 'spids' but the others should be unset.
7873 *
7874 * @since 1.11
7875 */
7876 if (!isset($mem)) {
7877 $mem = [];
7878 foreach ($arr as $k => $a) {
7879 $mem[$k]['daysused'] = 0;
7880 $mem[$k]['sum'] = [];
7881 $mem[$k]['spids'] = [];
7882 }
7883 } else {
7884 foreach ($arr as $k => $a) {
7885 $mem[$k]['daysused'] = 0;
7886 $mem[$k]['sum'] = [];
7887 }
7888 }
7889 //
7890 foreach ($specials as $s) {
7891 // check if this is a promotion registered for skipping
7892 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7893 continue;
7894 }
7895
7896 // Special Price tied to the year
7897 if (!empty($s['year']) && $s['year'] > 0) {
7898 if ($one['year'] != $s['year']) {
7899 continue;
7900 }
7901 }
7902
7903 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7904 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7905 // week days
7906 $filterwdays = !empty($s['wdays']) ? true : false;
7907 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7908 if (is_array($wdays) && $wdays) {
7909 foreach ($wdays as $kw => $wd) {
7910 if (strlen($wd) == 0) {
7911 unset($wdays[$kw]);
7912 }
7913 }
7914 }
7915
7916 foreach ($arr as $k => $a) {
7917 // only rooms with no price modifications from seasons
7918
7919 // applied only to some types of price
7920 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7921 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7922 continue;
7923 }
7924 }
7925
7926 /**
7927 * We should not exclude the rooms that already had a modification of the price through a season
7928 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7929 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7930 *
7931 * @since 1.11
7932 */
7933 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7934 continue;
7935 }
7936
7937 $affdays = 0;
7938 $season_fromdayts = $fromdayts;
7939 $is_dst = date('I', $season_fromdayts);
7940 for ($i = 0; $i < $a[0]['days']; $i++) {
7941 $todayts = $season_fromdayts + ($i * 86400);
7942 $is_now_dst = date('I', $todayts);
7943 if ($is_dst != $is_now_dst) {
7944 // Daylight Saving Time has changed, check how
7945 if ((bool)$is_dst === true) {
7946 $todayts += 3600;
7947 $season_fromdayts += 3600;
7948 } else {
7949 $todayts -= 3600;
7950 $season_fromdayts -= 3600;
7951 }
7952 $is_dst = $is_now_dst;
7953 }
7954 // week days
7955 if ($filterwdays == true) {
7956 $checkwday = getdate($todayts);
7957 if (in_array($checkwday['wday'], $wdays)) {
7958 $affdays++;
7959 }
7960 }
7961 }
7962 if (!($affdays > 0)) {
7963 // no nights affected
7964 continue;
7965 }
7966
7967 // apply the rule
7968 $applyseasons = true;
7969 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7970
7971 if (intval($s['val_pcent']) == 2) {
7972 // percentage value
7973 $pctval = $s['diffcost'];
7974 if (strlen((string)$s['losoverride'])) {
7975 // values overrides
7976 $arrvaloverrides = [];
7977 $valovrparts = explode('_', $s['losoverride']);
7978 foreach ($valovrparts as $valovr) {
7979 if (!empty($valovr)) {
7980 $ovrinfo = explode(':', $valovr);
7981 if (strstr($ovrinfo[0], '-i') != false) {
7982 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7983 if ((int)$ovrinfo[0] < $a[0]['days']) {
7984 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7985 }
7986 }
7987 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7988 }
7989 }
7990 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7991 $pctval = $arrvaloverrides[$a[0]['days']];
7992 }
7993 }
7994 if (intval($s['type']) == 1) {
7995 // charge
7996 $cpercent = 100 + $pctval;
7997 } else {
7998 // discount
7999 $cpercent = 100 - $pctval;
8000 }
8001 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
8002 } else {
8003 // absolute value
8004 $absval = $s['diffcost'];
8005 if (strlen((string)$s['losoverride'])) {
8006 // values overrides
8007 $arrvaloverrides = [];
8008 $valovrparts = explode('_', $s['losoverride']);
8009 foreach ($valovrparts as $valovr) {
8010 if (!empty($valovr)) {
8011 $ovrinfo = explode(':', $valovr);
8012 if (strstr($ovrinfo[0], '-i') != false) {
8013 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8014 if ((int)$ovrinfo[0] < $a[0]['days']) {
8015 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
8016 }
8017 }
8018 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8019 }
8020 }
8021 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8022 $absval = $arrvaloverrides[$a[0]['days']];
8023 }
8024 }
8025 if (intval($s['type']) == 1) {
8026 // charge
8027 $newprice = ($dailyprice + $absval) * $affdays;
8028 } else {
8029 // discount
8030 $newprice = ($dailyprice - $absval) * $affdays;
8031 }
8032 }
8033
8034 // apply rounding
8035 if (!empty($s['roundmode'])) {
8036 $newprice = round($newprice, 0, constant($s['roundmode']));
8037 } else {
8038 $newprice = round($newprice, 2);
8039 }
8040
8041 // push values in memory array
8042 if (!in_array($s['id'], $mem[$k]['spids'])) {
8043 $mem[$k]['spids'][] = $s['id'];
8044 }
8045 $mem[$k]['sum'][] = $newprice;
8046 $mem[$k]['daysused'] += $affdays;
8047 }
8048 }
8049 if ($applyseasons) {
8050 foreach ($mem as $k => $v) {
8051 if ($v['daysused'] > 0 && $v['sum']) {
8052 $newprice = 0;
8053 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
8054 $restdays = $arr[$k][0]['days'] - $v['daysused'];
8055 $addrest = $restdays * $dailyprice;
8056 $newprice += $addrest;
8057 foreach ($v['sum'] as $add) {
8058 $newprice += $add;
8059 }
8060 $arr[$k][0]['cost'] = $newprice;
8061 $arr[$k][0]['affdays'] = $v['daysused'];
8062 if (array_key_exists('spids', $v) && $v['spids']) {
8063 $arr[$k][0]['spids'] = $v['spids'];
8064 }
8065 }
8066 }
8067 }
8068 }
8069 // end week days with no season
8070
8071 return $arr;
8072 }
8073
8074 /**
8075 * Applies the seasonal rates over room rate records.
8076 *
8077 * @param array $arr array of tariffs taken from the DB
8078 * @param int $from start timestamp
8079 * @param int $to end timestamp
8080 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
8081 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
8082 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
8083 *
8084 * @return array
8085 *
8086 * @since 1.18.5 (J) - 1.8.5 (WP) argument $seasons_wdays is nullable to allow preloading an empty list of records and save queries.
8087 */
8088 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], ?array $seasons_wdays = null)
8089 {
8090 static $cached_seasons = [];
8091
8092 $cache_signature = '';
8093 $cache_signature_wdays = '';
8094 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
8095 // enable season records caching
8096 $cache_signature = "{$from}_{$to}";
8097 }
8098 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
8099 // enable week-day season records caching
8100 $cache_signature_wdays = "wdays";
8101 }
8102
8103 $dbo = JFactory::getDbo();
8104 $vbo_tn = self::getTranslator();
8105
8106 $roomschange = [];
8107 $one = getdate($from);
8108
8109 // leap years
8110 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
8111 $isleap = true;
8112 } else {
8113 $isleap = false;
8114 }
8115
8116 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
8117 $tomidnightone = intval($one['hours']) * 3600;
8118 $tomidnightone += intval($one['minutes']) * 60;
8119 $sfrom = $from - $baseone - $tomidnightone;
8120 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
8121 $two = getdate($to);
8122 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
8123 $tomidnighttwo = intval($two['hours']) * 3600;
8124 $tomidnighttwo += intval($two['minutes']) * 60;
8125 $sto = $to - $basetwo - $tomidnighttwo;
8126
8127 // leap years, check what dates to manipulate
8128 if ($isleap) {
8129 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
8130 if ($one[0] > $leapts) {
8131 /**
8132 * Timestamp must be greater than the leap-day of Feb 29th.
8133 * It used to be checked for >= $leapts.
8134 *
8135 * @since July 3rd 2019
8136 */
8137 $sfrom -= 86400;
8138 }
8139 if ($two[0] > $leapts && $one['year'] == $two['year']) {
8140 // lower checkin date when in leap year but not for checkout
8141 $sto -= 86400;
8142 }
8143 }
8144
8145 // check for DST changes to adjust the query values to fetch
8146 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
8147 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
8148 // we are parsing the day when the DST changed (probably a Sunday)
8149 if (!date('I', $from)) {
8150 // DST was just turned off
8151 $sfrom -= 3600;
8152 }
8153 }
8154 }
8155
8156 // count nights requested
8157 $booking_nights = 1;
8158 foreach ($arr as $k => $a) {
8159 if (isset($a['booking_nights'])) {
8160 // this value may be set when displaying pricing calendars
8161 $booking_nights = $a['booking_nights'];
8162 break;
8163 }
8164 if (isset($a['days'])) {
8165 $booking_nights = $a['days'];
8166 break;
8167 }
8168 }
8169
8170 /**
8171 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
8172 *
8173 * @since 1.16.4 (J) - 1.6.4 (WP)
8174 */
8175 $skip_promo_ids = self::registerPromotionIds();
8176
8177 $totseasons = 0;
8178 if (!$parsed_season && !$seasons_dates) {
8179 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
8180 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
8181 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
8182 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
8183 ($sto > $sfrom ? "OR (`from` >= " . $sfrom . " AND `from` <= " . $sto . " AND `to` >= " . $sfrom . " AND `to` <= " . $sto . ")" : "OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` > `to`)") .
8184 ($sto > $sfrom ? " OR (`from` <= " . $sfrom . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`) OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` >= " . $sto . " AND `from` > `to`)" : " OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . " AND `from` >= " . $sto . " AND `to` > " . $sto . " AND `from` < `to`)") .
8185 ($sto > $sfrom ? " OR (`from` >= " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` < " . $sfrom . " AND `to` >= " . $sto . " AND `from` <= " . $sto . " AND `to` < " . $sfrom . " AND `from` < `to`)") .
8186 ($sto > $sfrom ? " OR (`from` > " . $sfrom . " AND `from` > " . $sto . " AND `to` >= " . $sfrom . " AND `to` < " . $sto . " AND `from` > `to`)" : " OR (`from` >= " . $sfrom . " AND `from` > " . $sto . " AND `to` > " . $sfrom . " AND `to` > " . $sto . " AND `from` < `to`) OR (`from` < " . $sfrom . " AND `from` < " . $sto . " AND `to` < " . $sfrom . " AND `to` <= " . $sto . " AND `from` < `to`)") .
8187 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
8188 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
8189
8190 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
8191 // avoid making a query
8192 $seasons = $cached_seasons[$cache_signature];
8193 } else {
8194 // get the season records by running the query
8195 $dbo->setQuery($q);
8196 $seasons = $dbo->loadAssocList();
8197 }
8198
8199 // count total seasons
8200 $totseasons = $seasons ? count($seasons) : 0;
8201
8202 if ($cache_signature) {
8203 // cache season records
8204 $cached_seasons[$cache_signature] = $seasons;
8205 }
8206 }
8207
8208 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
8209 if ($totseasons > 0) {
8210 $seasons = $seasons;
8211 } elseif ($parsed_season) {
8212 $seasons = [$parsed_season];
8213 } else {
8214 $seasons = $seasons_dates;
8215 }
8216 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
8217 $applyseasons = false;
8218 $mem = [];
8219 foreach ($arr as $k => $a) {
8220 $mem[$k]['daysused'] = 0;
8221 $mem[$k]['sum'] = [];
8222 $mem[$k]['spids'] = [];
8223 /**
8224 * The keys below are all needed to apply the promotions on the room's final cost.
8225 *
8226 * @since 1.13.5
8227 */
8228 $mem[$k]['diffs'] = [];
8229 $mem[$k]['trans_keys'] = [];
8230 $mem[$k]['trans_factors'] = [];
8231 $mem[$k]['trans_affdays'] = [];
8232 }
8233 $affdayslistless = [];
8234 foreach ($seasons as $s) {
8235 // double check that the 'from' and 'to' properties are not empty (dates filter), in case VCM passes an array of seasons already taken from the DB
8236 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
8237 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
8238 continue;
8239 }
8240
8241 /**
8242 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
8243 * We need to skip such fake seasons as they do not need any parsing.
8244 *
8245 * @since 1.13
8246 */
8247 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
8248 continue;
8249 }
8250
8251 // check if this is a promotion registered for skipping
8252 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8253 continue;
8254 }
8255
8256 // Special Price tied to the year
8257 if (!empty($s['year']) && $s['year'] > 0) {
8258 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
8259 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
8260 //VBO 1.9 - tied to the year can be set for prev year (Dec 27 to Jan 3) and booking can be Jan 1 to Jan 3 - do not skip in this case
8261 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
8262 continue;
8263 }
8264 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
8265 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
8266 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
8267 if ($calc_ends < ($from - $tomidnightone)) {
8268 continue;
8269 }
8270 }
8271 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
8272 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
8273 if ($s['from'] > $s['to']) {
8274 continue;
8275 }
8276 if (($basetwo + $s['from'] + 86399) > $to) {
8277 /**
8278 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
8279 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
8280 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
8281 *
8282 * @since 1.14.3 (J) - 1.4.3 (WP)
8283 */
8284 continue;
8285 }
8286 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
8287 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
8288 /**
8289 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
8290 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
8291 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
8292 *
8293 * @since 1.14.3 (J) - 1.4.3 (WP)
8294 */
8295 continue;
8296 }
8297 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
8298 // season tied to the year 2017 accross 2018 and we are parsing dates at the beginning of 2017 due to beginning loop in 2016 (Rates Overview)
8299 if (($baseone + $s['from']) > $to) {
8300 continue;
8301 }
8302 }
8303 }
8304 //
8305 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8306 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8307 $inits = $baseone + $s['from'];
8308 if ($s['from'] < $s['to']) {
8309 $ends = $basetwo + $s['to'];
8310 // check if the inits must be set to the year after
8311 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
8312 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
8313 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8314 $inits = $tmpbase + $s['from'];
8315 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8316 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
8317 $ends = $baseone + $s['to'];
8318 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8319 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
8320 $ends = $baseone + $s['to'];
8321 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
8322 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
8323 $inits = $basetwo + $s['from'];
8324 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
8325 // booking dates across two years for a season tied to a specific year
8326 if ($one['year'] == $s['year']) {
8327 $inits = $baseone + $s['from'];
8328 } else {
8329 $inits = $basetwo + $s['from'];
8330 }
8331 if ($two['year'] == $s['year']) {
8332 $ends = $basetwo + $s['to'];
8333 } else {
8334 $ends = $baseone + $s['to'];
8335 }
8336 }
8337 } else {
8338 // between 2 years
8339 if ($baseone < $basetwo) {
8340 // ex. 29/12/2012 - 14/01/2013
8341 $ends = $basetwo + $s['to'];
8342 } else {
8343 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
8344 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
8345 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8346 $ends = $tmpbase + $s['to'];
8347 } else {
8348 // ex. 03/01 - 09/01
8349 $ends = $basetwo + $s['to'];
8350 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
8351 $inits = $tmpbase + $s['from'];
8352 }
8353 }
8354 }
8355 // leap years
8356 if ($isleap == true) {
8357 $infoseason = getdate($inits);
8358 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
8359 // leap years, check what dates to manipulate
8360 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
8361 /**
8362 * Timestamp must be greater than the leap-day of Feb 29th.
8363 * It used to be checked for >= $leapts.
8364 *
8365 * @since July 3rd 2019
8366 */
8367 $inits += 86400;
8368 if ($s['from'] < $s['to']) {
8369 // increase season end date only if still on the same leap year
8370 $ends += 86400;
8371 }
8372 }
8373 }
8374
8375 // promotions
8376 $promotion = [];
8377 if (($s['promo'] ?? 0) == 1) {
8378 $daysadv = (($inits - time()) / 86400);
8379 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
8380 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
8381 continue;
8382 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
8383 $secstocheckin = ($from - time());
8384 if ($s['promolastmin'] < $secstocheckin) {
8385 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
8386 continue;
8387 }
8388 }
8389 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
8390 /**
8391 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
8392 *
8393 * @since 1.13.5
8394 */
8395 continue;
8396 }
8397 $promotion['todaydaysadv'] = $daysadv;
8398 $promotion['promodaysadv'] = $s['promodaysadv'];
8399 $promotion['promotxt'] = $s['promotxt'];
8400 }
8401
8402 // occupancy override
8403 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
8404
8405 // week days
8406 $filterwdays = !empty($s['wdays']) ? true : false;
8407 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8408 if (is_array($wdays) && $wdays) {
8409 foreach ($wdays as $kw => $wd) {
8410 if (strlen($wd) == 0) {
8411 unset($wdays[$kw]);
8412 }
8413 }
8414 }
8415
8416 // checkin must be after the beginning of the season
8417 $checkininclok = true;
8418 if ($s['checkinincl'] == 1) {
8419 $checkininclok = false;
8420 if ($s['from'] < $s['to']) {
8421 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
8422 $checkininclok = true;
8423 }
8424 } else {
8425 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
8426 $checkininclok = true;
8427 }
8428 }
8429 }
8430 if ($checkininclok !== true) {
8431 continue;
8432 }
8433
8434 foreach ($arr as $k => $a) {
8435 // applied only to some types of price
8436 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8437 // VikBooking 1.6: Price Calendar sets the idprice to -1
8438 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8439 continue;
8440 }
8441 }
8442 // applied only to some room types
8443 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8444 continue;
8445 }
8446
8447 if (isset($a['days']) && is_float($a['days'])) {
8448 // prevent any possible warning with array_key_exists()
8449 $a['days'] = (int)$a['days'];
8450 }
8451
8452 $affdays = 0;
8453 $season_fromdayts = $fromdayts;
8454 $is_dst = date('I', $season_fromdayts);
8455 for ($i = 0; $i < $a['days']; $i++) {
8456 $todayts = $season_fromdayts + ($i * 86400);
8457 $is_now_dst = date('I', $todayts);
8458 if ($is_dst != $is_now_dst) {
8459 // Daylight Saving Time has changed, check how
8460 if ((bool)$is_dst === true) {
8461 $todayts += 3600;
8462 $season_fromdayts += 3600;
8463 } else {
8464 $todayts -= 3600;
8465 $season_fromdayts -= 3600;
8466 }
8467 $is_dst = $is_now_dst;
8468 }
8469 if ($todayts >= $inits && $todayts <= $ends) {
8470 $checkwday = getdate($todayts);
8471 // week days
8472 if ($filterwdays == true) {
8473 if (in_array($checkwday['wday'], $wdays)) {
8474 if (!isset($arr[$k]['affdayslist'])) {
8475 $arr[$k]['affdayslist'] = [];
8476 }
8477 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = isset($arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']]) && $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] > 0 ? $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] : 0;
8478 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8479 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8480 $affdays++;
8481 }
8482 } else {
8483 if (!isset($arr[$k]['affdayslist'])) {
8484 $arr[$k]['affdayslist'] = [];
8485 }
8486 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = isset($arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']]) && $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] > 0 ? $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] : 0;
8487 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8488 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8489 $affdays++;
8490 }
8491 //
8492 }
8493 }
8494
8495 if (!($affdays > 0)) {
8496 // no nights affected
8497 continue;
8498 }
8499
8500 // apply the rule
8501 $applyseasons = true;
8502 $dailyprice = $a['cost'] / $a['days'];
8503
8504 // modification factor object
8505 $factor = new stdClass;
8506
8507 // calculate new price progressively
8508 if (intval($s['val_pcent']) == 2) {
8509 // percentage value
8510 $factor->pcent = 1;
8511 $pctval = $s['diffcost'];
8512 if (strlen((string)$s['losoverride'])) {
8513 // values overrides
8514 $arrvaloverrides = [];
8515 $valovrparts = explode('_', $s['losoverride']);
8516 foreach ($valovrparts as $valovr) {
8517 if (!empty($valovr)) {
8518 $ovrinfo = explode(':', $valovr);
8519 if (strstr($ovrinfo[0], '-i') != false) {
8520 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8521 if ((int)$ovrinfo[0] < $a['days']) {
8522 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8523 }
8524 }
8525 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8526 }
8527 }
8528 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8529 $pctval = $arrvaloverrides[$a['days']];
8530 }
8531 }
8532 if (intval($s['type']) == 1) {
8533 // charge
8534 $factor->type = '+';
8535 $cpercent = 100 + $pctval;
8536 } else {
8537 // discount
8538 $factor->type = '-';
8539 $cpercent = 100 - $pctval;
8540 }
8541 $factor->amount = $pctval;
8542 $dailysum = ($dailyprice * $cpercent / 100);
8543 $newprice = $dailysum * $affdays;
8544 } else {
8545 // absolute value
8546 $factor->pcent = 0;
8547 $absval = $s['diffcost'];
8548 if (strlen((string)$s['losoverride'])) {
8549 // values overrides
8550 $arrvaloverrides = [];
8551 $valovrparts = explode('_', $s['losoverride']);
8552 foreach ($valovrparts as $valovr) {
8553 if (!empty($valovr)) {
8554 $ovrinfo = explode(':', $valovr);
8555 if (strstr($ovrinfo[0], '-i') != false) {
8556 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8557 if ((int)$ovrinfo[0] < $a['days']) {
8558 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8559 }
8560 }
8561 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8562 }
8563 }
8564 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8565 $absval = $arrvaloverrides[$a['days']];
8566 }
8567 }
8568 if (intval($s['type']) == 1) {
8569 // charge
8570 $factor->type = '+';
8571 $dailysum = ($dailyprice + $absval);
8572 $newprice = $dailysum * $affdays;
8573 } else {
8574 // discount
8575 $factor->type = '-';
8576 $dailysum = ($dailyprice - $absval);
8577 $newprice = $dailysum * $affdays;
8578 }
8579 $factor->amount = $absval;
8580 }
8581
8582 // apply rounding
8583 $factor->roundmode = $s['roundmode'] ?? null;
8584 if (!empty($s['roundmode'])) {
8585 $newprice = round($newprice, 0, constant($s['roundmode']));
8586 } else {
8587 $newprice = round($newprice, 2);
8588 }
8589
8590 // define the promotion (only if no value overrides set the amount to 0)
8591 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8592 /**
8593 * Include the discount information (if any). The cost re-calculated may not be
8594 * precise if multiple special prices were applied over the same dates.
8595 *
8596 * @since 1.13
8597 */
8598 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8599 $promotion['discount'] = [
8600 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8601 'pcent' => (int)($s['val_pcent'] == 2),
8602 ];
8603 }
8604 //
8605 $mem[$k]['promotion'] = $promotion;
8606 }
8607
8608 // define the occupancy override
8609 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8610 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8611 }
8612
8613 // affected days list
8614 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8615 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8616 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8617 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8618 }
8619 }
8620 }
8621
8622 // push special price ID
8623 if (!in_array($s['id'], $mem[$k]['spids'])) {
8624 array_push($mem[$k]['spids'], $s['id']);
8625 }
8626
8627 // push difference generated only if to be applied progressively
8628 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8629 /**
8630 * Push the difference generated by this special price for later transliteration of final price,
8631 * only if the special price is calculated progressively and not on the final price.
8632 *
8633 * @since 1.13.5
8634 */
8635 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8636 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8637 /**
8638 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8639 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8640 * all differences applied by the previous special pricing rules.
8641 *
8642 * @since 1.13.5
8643 */
8644 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8645 array_push($mem[$k]['trans_factors'], $factor);
8646 array_push($mem[$k]['trans_affdays'], $affdays);
8647 }
8648
8649 // push values in memory array
8650 array_push($mem[$k]['sum'], $newprice);
8651 $mem[$k]['daysused'] += $affdays;
8652 array_push($roomschange, $a['idroom']);
8653 }
8654 }
8655 if ($applyseasons) {
8656 foreach ($mem as $k => $v) {
8657 if ($v['daysused'] > 0 && $v['sum']) {
8658 $newprice = 0;
8659 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8660 $restdays = $arr[$k]['days'] - $v['daysused'];
8661 $addrest = $restdays * $dailyprice;
8662 $newprice += $addrest;
8663
8664 // calculate new final cost
8665 $redo_rounding = null;
8666 foreach ($v['sum'] as $sum_index => $add) {
8667 /**
8668 * The application of the various special pricing rules is made in a progressive and cumulative way
8669 * by always starting from the room base cost or its average daily cost. However, promotions may need
8670 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8671 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8672 * onto the differences generated by all the regular and progressively applied special pricing rules.
8673 *
8674 * @since 1.13.5
8675 */
8676 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8677 /**
8678 * This progressive price difference must be applied on the room final cost, so we need to
8679 * apply the transliteration over the other differences applied by other special prices.
8680 */
8681 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8682 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8683 // this is the % promotion we are looking for applying it on the final cost
8684 $factor = $v['trans_factors'][$transliterate_key];
8685 if (is_object($factor) && $factor->pcent) {
8686 $final_factor = 0;
8687 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8688 $final_factor += $prog_diff * $factor->amount / 100;
8689 }
8690 // update rounding
8691 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8692
8693 /**
8694 * Leverage the final factor depending on how many nights this affected.
8695 *
8696 * @since 1.15.0 (J) - 1.5.0 (WP)
8697 */
8698 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8699 $avg_final_factor = $final_factor / $arr[$k]['days'];
8700 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8701 }
8702
8703 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8704 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8705 }
8706 }
8707 }
8708
8709 // apply new price progressively
8710 $newprice += $add;
8711 }
8712
8713 // apply rounding from factor
8714 if (!empty($redo_rounding)) {
8715 $newprice = round($newprice, 0, constant($redo_rounding));
8716 }
8717
8718 // set promotion (if any)
8719 if (isset($v['promotion'])) {
8720 $arr[$k]['promotion'] = $v['promotion'];
8721 }
8722
8723 // set occupancy overrides (if any)
8724 if (isset($v['occupancy_ovr'])) {
8725 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8726 }
8727
8728 // set new final cost and update nights affected
8729 $arr[$k]['cost'] = $newprice;
8730 $arr[$k]['affdays'] = $v['daysused'];
8731 if (array_key_exists('spids', $v) && $v['spids']) {
8732 $arr[$k]['spids'] = $v['spids'];
8733 }
8734 }
8735 }
8736 }
8737 }
8738
8739 // week days with no season
8740 $roomschange = array_unique($roomschange);
8741 $totspecials = 0;
8742 if (!is_array($seasons_wdays)) {
8743 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8744 // avoid making a query
8745 $specials = $cached_seasons[$cache_signature_wdays];
8746 } else {
8747 // get the week-days-with-no-season records by running the query
8748 $specials = self::getWdaySeasonRecords();
8749 }
8750
8751 // count records
8752 $totspecials = $specials ? count($specials) : 0;
8753
8754 if ($cache_signature_wdays) {
8755 // cache week-days-with-no-season records
8756 $cached_seasons[$cache_signature_wdays] = $specials;
8757 }
8758 }
8759 if ($totspecials > 0 || $seasons_wdays) {
8760 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8761 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8762 $applyseasons = false;
8763 /**
8764 * We no longer unset the previous memory of the seasons with dates filters
8765 * because we need the responses to be merged. We do it only if not set.
8766 * We only keep the property 'spids' but the others should be unset.
8767 *
8768 * @since 1.11
8769 */
8770 if (!isset($mem)) {
8771 $mem = [];
8772 foreach ($arr as $k => $a) {
8773 $mem[$k]['daysused'] = 0;
8774 $mem[$k]['sum'] = [];
8775 $mem[$k]['spids'] = [];
8776 }
8777 } else {
8778 foreach ($arr as $k => $a) {
8779 $mem[$k]['daysused'] = 0;
8780 $mem[$k]['sum'] = [];
8781 }
8782 }
8783 //
8784 foreach ($specials as $s) {
8785 // double check that the 'from' and 'to' properties are empty (only weekdays), in case VCM passes an array of seasons already taken from the DB
8786 if (!empty($s['from']) || !empty($s['to'])) {
8787 continue;
8788 }
8789
8790 // check if this is a promotion registered for skipping
8791 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8792 continue;
8793 }
8794
8795 // Special Price tied to the year
8796 if (!empty($s['year']) && $s['year'] > 0) {
8797 if ($one['year'] != $s['year']) {
8798 continue;
8799 }
8800 }
8801
8802 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8803 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8804 // week days
8805 $filterwdays = !empty($s['wdays']) ? true : false;
8806 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8807 if (is_array($wdays) && $wdays) {
8808 foreach ($wdays as $kw => $wd) {
8809 if (strlen($wd) == 0) {
8810 unset($wdays[$kw]);
8811 }
8812 }
8813 }
8814
8815 foreach ($arr as $k => $a) {
8816 // only rooms with no price modifications from seasons
8817
8818 // applied only to some types of price
8819 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8820 // VikBooking 1.6: Price Calendar sets the idprice to -1
8821 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8822 continue;
8823 }
8824 }
8825
8826 /**
8827 * We should not exclude the rooms that already had a modification of the price through a season
8828 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8829 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8830 *
8831 * @since 1.11
8832 */
8833 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8834 continue;
8835 }
8836
8837 if (isset($a['days']) && is_float($a['days'])) {
8838 // prevent any possible warning with array_key_exists()
8839 $a['days'] = (int)$a['days'];
8840 }
8841
8842 $affdays = 0;
8843 $season_fromdayts = $fromdayts;
8844 $is_dst = date('I', $season_fromdayts);
8845 for ($i = 0; $i < $a['days']; $i++) {
8846 $todayts = $season_fromdayts + ($i * 86400);
8847 $is_now_dst = date('I', $todayts);
8848 if ($is_dst != $is_now_dst) {
8849 // Daylight Saving Time has changed, check how
8850 if ((bool)$is_dst === true) {
8851 $todayts += 3600;
8852 $season_fromdayts += 3600;
8853 } else {
8854 $todayts -= 3600;
8855 $season_fromdayts -= 3600;
8856 }
8857 $is_dst = $is_now_dst;
8858 }
8859 // week days
8860 if ($filterwdays == true) {
8861 $checkwday = getdate($todayts);
8862 if (in_array($checkwday['wday'], $wdays)) {
8863 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8864 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8865 $affdays++;
8866 }
8867 }
8868 //
8869 }
8870
8871 if (!($affdays > 0)) {
8872 // no nights affected
8873 continue;
8874 }
8875
8876 // apply the rule
8877 $applyseasons = true;
8878 $dailyprice = $a['cost'] / $a['days'];
8879
8880 if (intval($s['val_pcent']) == 2) {
8881 // percentage value
8882 $pctval = $s['diffcost'];
8883 if (strlen((string)$s['losoverride'])) {
8884 // values overrides
8885 $arrvaloverrides = [];
8886 $valovrparts = explode('_', $s['losoverride']);
8887 foreach ($valovrparts as $valovr) {
8888 if (!empty($valovr)) {
8889 $ovrinfo = explode(':', $valovr);
8890 if (strstr($ovrinfo[0], '-i') != false) {
8891 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8892 if ((int)$ovrinfo[0] < $a['days']) {
8893 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8894 }
8895 }
8896 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8897 }
8898 }
8899 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8900 $pctval = $arrvaloverrides[$a['days']];
8901 }
8902 }
8903 if (intval($s['type']) == 1) {
8904 // charge
8905 $cpercent = 100 + $pctval;
8906 } else {
8907 // discount
8908 $cpercent = 100 - $pctval;
8909 }
8910 $dailysum = ($dailyprice * $cpercent / 100);
8911 $newprice = $dailysum * $affdays;
8912 } else {
8913 // absolute value
8914 $absval = $s['diffcost'];
8915 if (strlen((string)$s['losoverride'])) {
8916 // values overrides
8917 $arrvaloverrides = [];
8918 $valovrparts = explode('_', $s['losoverride']);
8919 foreach ($valovrparts as $valovr) {
8920 if (!empty($valovr)) {
8921 $ovrinfo = explode(':', $valovr);
8922 if (strstr($ovrinfo[0], '-i') != false) {
8923 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8924 if ((int)$ovrinfo[0] < $a['days']) {
8925 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8926 }
8927 }
8928 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8929 }
8930 }
8931 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8932 $absval = $arrvaloverrides[$a['days']];
8933 }
8934 }
8935 if (intval($s['type']) == 1) {
8936 // charge
8937 $dailysum = ($dailyprice + $absval);
8938 $newprice = $dailysum * $affdays;
8939 } else {
8940 // discount
8941 $dailysum = ($dailyprice - $absval);
8942 $newprice = $dailysum * $affdays;
8943 }
8944 }
8945
8946 // apply rounding
8947 if (!empty($s['roundmode'])) {
8948 $newprice = round($newprice, 0, constant($s['roundmode']));
8949 } else {
8950 $newprice = round($newprice, 2);
8951 }
8952
8953 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8954 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8955 }
8956 if (!in_array($s['id'], $mem[$k]['spids'])) {
8957 $mem[$k]['spids'][] = $s['id'];
8958 }
8959 $mem[$k]['sum'][] = $newprice;
8960 $mem[$k]['daysused'] += $affdays;
8961 }
8962 }
8963 if ($applyseasons) {
8964 foreach ($mem as $k => $v) {
8965 if ($v['daysused'] > 0 && $v['sum']) {
8966 $newprice = 0;
8967 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8968 $restdays = $arr[$k]['days'] - $v['daysused'];
8969 $addrest = $restdays * $dailyprice;
8970 $newprice += $addrest;
8971 foreach ($v['sum'] as $add) {
8972 $newprice += $add;
8973 }
8974 $arr[$k]['cost'] = $newprice;
8975 $arr[$k]['affdays'] = $v['daysused'];
8976 if (array_key_exists('spids', $v) && $v['spids']) {
8977 $arr[$k]['spids'] = $v['spids'];
8978 }
8979 }
8980 }
8981 }
8982 }
8983 // end week days with no season
8984
8985 return $arr;
8986 }
8987
8988 public static function getRoomRplansClosingDates($idroom)
8989 {
8990 $dbo = JFactory::getDbo();
8991
8992 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8993 $dbo->setQuery($q);
8994 $price_records = $dbo->loadAssocList();
8995 if (!$price_records) {
8996 return [];
8997 }
8998
8999 $closingd = [];
9000 foreach ($price_records as $prec) {
9001 if (empty($prec['closingd'])) {
9002 continue;
9003 }
9004 $price_closing = json_decode($prec['closingd'], true);
9005 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9006 continue;
9007 }
9008 // check expired dates and clean up
9009 $today_midnight = mktime(0, 0, 0);
9010 $cleaned = false;
9011 foreach ($price_closing[$idroom] as $k => $v) {
9012 if (strtotime($v) < $today_midnight) {
9013 $cleaned = true;
9014 unset($price_closing[$idroom][$k]);
9015 }
9016 }
9017 if (!$price_closing[$idroom]) {
9018 unset($price_closing[$idroom]);
9019 } elseif ($cleaned === true) {
9020 // reset array keys for smaller JSON size
9021 $price_closing[$idroom] = array_values($price_closing[$idroom]);
9022 }
9023 if ($cleaned === true) {
9024 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
9025 $dbo->setQuery($q);
9026 $dbo->execute();
9027 }
9028 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9029 continue;
9030 }
9031 $closingd[$prec['id']] = $price_closing[$idroom];
9032 }
9033
9034 return $closingd;
9035 }
9036
9037 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
9038 {
9039 $dbo = JFactory::getDbo();
9040
9041 $closingd = [];
9042
9043 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9044 $dbo->setQuery($q);
9045 $price_records = $dbo->loadAssocList();
9046 if ($price_records && $roomids) {
9047 $info_start = getdate($checkints);
9048 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
9049 $all_nights = [];
9050 for ($i = 0; $i < (int)$numnights; $i++) {
9051 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
9052 $all_nights[] = date('Y-m-d', $next_midnight);
9053 }
9054 foreach ($price_records as $prec) {
9055 if (empty($prec['closingd'])) {
9056 continue;
9057 }
9058 $price_closing = json_decode($prec['closingd'], true);
9059 if (!$price_closing) {
9060 continue;
9061 }
9062 foreach ($price_closing as $idroom => $rclosedd) {
9063 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
9064 continue;
9065 }
9066 if (!array_key_exists($idroom, $closingd)) {
9067 $closingd[$idroom] = [];
9068 }
9069 foreach ($all_nights as $night) {
9070 if (in_array($night, $rclosedd)) {
9071 if (array_key_exists($prec['id'], $closingd[$idroom])) {
9072 $closingd[$idroom][$prec['id']][] = $night;
9073 } else {
9074 $closingd[$idroom][$prec['id']] = [$night];
9075 }
9076 }
9077 }
9078 }
9079 }
9080 }
9081
9082 return $closingd;
9083 }
9084
9085 /**
9086 * Tells whether at least one payment method must be selected to check-out.
9087 *
9088 * @param array $rooms_involved optional list of room IDs being booked.
9089 *
9090 * @return bool true if at least one payment method is applicable.
9091 *
9092 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
9093 */
9094 public static function areTherePayments($rooms_involved = [])
9095 {
9096 $dbo = JFactory::getDbo();
9097
9098 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
9099 $dbo->setQuery($q);
9100 $dbo->execute();
9101
9102 if (!$dbo->getNumRows()) {
9103 // no published payment options found
9104 return false;
9105 }
9106
9107 $pay_options = $dbo->loadAssocList();
9108
9109 if (!count($rooms_involved)) {
9110 // if no filters given, return true
9111 return true;
9112 }
9113
9114 // validate room associations
9115 foreach ($pay_options as $pay_k => $pay_v) {
9116 if (empty($pay_v['idrooms'])) {
9117 continue;
9118 }
9119 $rooms_supported = json_decode($pay_v['idrooms']);
9120 if (!is_array($rooms_supported) || !count($rooms_supported)) {
9121 continue;
9122 }
9123 // make sure all rooms being booked are supported
9124 foreach ($rooms_involved as $room_id_booked) {
9125 if (!in_array($room_id_booked, $rooms_supported)) {
9126 // we need to unset this payment option as soon as we find one non-matching room ID
9127 unset($pay_options[$pay_k]);
9128 break;
9129 }
9130 }
9131 }
9132
9133 return (bool)count($pay_options);
9134 }
9135
9136 public static function getPayment($idp, $vbo_tn = null)
9137 {
9138 if (empty($idp)) {
9139 return false;
9140 }
9141
9142 if (strstr((string)$idp, '=') !== false) {
9143 $parts = explode('=', $idp);
9144 $idp = $parts[0];
9145 }
9146
9147 $dbo = JFactory::getDbo();
9148
9149 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
9150 $dbo->setQuery($q);
9151 $payment = $dbo->loadAssocList();
9152 if (!$payment) {
9153 return false;
9154 }
9155
9156 if (is_object($vbo_tn)) {
9157 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
9158 }
9159
9160 return $payment[0];
9161 }
9162
9163 public static function getCronKey()
9164 {
9165 return VBOFactory::getConfig()->get('cronkey', '');
9166 }
9167
9168 /**
9169 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
9170 *
9171 * @return int
9172 *
9173 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
9174 */
9175 public static function getNextInvoiceNumber()
9176 {
9177 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
9178 }
9179
9180 public static function getInvoiceNumberSuffix()
9181 {
9182 return VBOFactory::getConfig()->get('invoicesuffix', '');
9183 }
9184
9185 public static function getInvoiceCompanyInfo()
9186 {
9187 return VBOFactory::getConfig()->get('invcompanyinfo', '');
9188 }
9189
9190 /**
9191 * Gets the number for the next booking receipt generation,
9192 * updates the last receipt number used for the later iterations,
9193 * stores a new receipt record to keep track of the receipts issued.
9194 *
9195 * @param int $bid the Booking ID for which we are/want to generating the receipt.
9196 * @param [int] $newnum the last number used for generating the receipt.
9197 *
9198 * @return int
9199 */
9200 public static function getNextReceiptNumber ($bid, $newnum = false) {
9201 $dbo = JFactory::getDbo();
9202 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
9203 $dbo->setQuery($q);
9204 $dbo->execute();
9205 if ($dbo->getNumRows() > 0) {
9206 $s = $dbo->loadResult();
9207 //check if this booking has already a receipt, and return that number
9208 $prev_receipt = array();
9209 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
9210 $dbo->setQuery($q);
9211 $dbo->execute();
9212 if ($dbo->getNumRows() > 0) {
9213 $prev_receipt = $dbo->loadAssoc();
9214 }
9215 //update value (receipt generated)
9216 if ($newnum !== false && $newnum > 0) {
9217 $s = (int)$newnum;
9218 if (!(count($prev_receipt) > 0)) {
9219 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
9220 $dbo->setQuery($q);
9221 $dbo->execute();
9222 //insert the new receipt record
9223 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
9224 $dbo->setQuery($q);
9225 $dbo->execute();
9226 } else {
9227 //update receipt record
9228 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
9229 $dbo->setQuery($q);
9230 $dbo->execute();
9231 }
9232 }
9233 //
9234 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
9235 }
9236 //first execution of the method should create the configuration record
9237 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
9238 $dbo->setQuery($q);
9239 $dbo->execute();
9240 return 1;
9241 }
9242
9243 public static function getReceiptNotes ($newnotes = false) {
9244 $dbo = JFactory::getDbo();
9245 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
9246 $dbo->setQuery($q);
9247 $dbo->execute();
9248 if ($dbo->getNumRows() > 0) {
9249 $s = $dbo->loadResult();
9250 //update value
9251 if ($newnotes !== false) {
9252 $s = $newnotes;
9253 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
9254 $dbo->setQuery($q);
9255 $dbo->execute();
9256 }
9257 //
9258 return $s;
9259 }
9260 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
9261 $dbo->setQuery($q);
9262 $dbo->execute();
9263 return "";
9264 }
9265
9266 public static function loadColorTagsRules()
9267 {
9268 $color_tag_rules = [
9269 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
9270 'pend1' => 'VBWAITINGFORPAYMENT',
9271 'conf1' => 'VBDBTEXTROOMCLOSED',
9272 'ovbook' => 'VBO_BTYPE_OVERBOOKING',
9273 'conf2' => 'VBOCOLORTAGRULECONFTWO',
9274 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
9275 'inv1' => 'VBOCOLORTAGRULEINVONE',
9276 'rcp1' => 'VBOCOLORTAGRULERCPONE',
9277 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
9278 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
9279 'inv2' => 'VBOCOLORTAGRULEINVTWO',
9280 ];
9281
9282 /**
9283 * Trigger event to let third-party plugins override the default booking color tag rules.
9284 *
9285 * @since 1.16.4 (J) - 1.6.4 (WP)
9286 */
9287 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
9288
9289 return $color_tag_rules;
9290 }
9291
9292 public static function loadDefaultColorTags()
9293 {
9294 $default_color_tags = [
9295 [
9296 'color' => '#9b9b9b',
9297 'name' => 'VBWAITINGFORPAYMENT',
9298 'rule' => 'pend1',
9299 ],
9300 [
9301 'color' => '#333333',
9302 'name' => 'VBDBTEXTROOMCLOSED',
9303 'rule' => 'conf1',
9304 ],
9305 [
9306 'color' => '#f04848',
9307 'name' => 'VBO_BTYPE_OVERBOOKING',
9308 'rule' => 'ovbook',
9309 ],
9310 [
9311 'color' => '#ff8606',
9312 'name' => 'VBOCOLORTAGRULECONFTWO',
9313 'rule' => 'conf2',
9314 ],
9315 [
9316 'color' => '#0418c9',
9317 'name' => 'VBOCOLORTAGRULECONFTHREE',
9318 'rule' => 'conf3',
9319 ],
9320 [
9321 'color' => '#bed953',
9322 'name' => 'VBOCOLORTAGRULEINVONE',
9323 'rule' => 'inv1',
9324 ],
9325 [
9326 'color' => '#67f5b5',
9327 'name' => 'VBOCOLORTAGRULERCPONE',
9328 'rule' => 'rcp1',
9329 ],
9330 [
9331 'color' => '#04d2c2',
9332 'name' => 'VBOCOLORTAGRULECONFFOUR',
9333 'rule' => 'conf4',
9334 ],
9335 [
9336 'color' => '#00b316',
9337 'name' => 'VBOCOLORTAGRULECONFFIVE',
9338 'rule' => 'conf5',
9339 ],
9340 [
9341 'color' => '#00f323',
9342 'name' => 'VBOCOLORTAGRULEINVTWO',
9343 'rule' => 'inv2',
9344 ],
9345 ];
9346
9347 /**
9348 * Trigger event to let third-party plugins override the default color tags.
9349 *
9350 * @since 1.16.4 (J) - 1.6.4 (WP)
9351 */
9352 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
9353
9354 return $default_color_tags;
9355 }
9356
9357 public static function loadBookingsColorTags()
9358 {
9359 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
9360
9361 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
9362 }
9363
9364 public static function getBestColorContrast($hexcolor)
9365 {
9366 $hexcolor = str_replace('#', '', $hexcolor);
9367
9368 if (empty($hexcolor) || strlen($hexcolor) != 6) {
9369 return '#000000';
9370 }
9371
9372 $r = hexdec(substr($hexcolor, 0, 2));
9373 $g = hexdec(substr($hexcolor, 2, 2));
9374 $b = hexdec(substr($hexcolor, 4, 2));
9375
9376 // Counting the perceptive luminance - human eye favors green color
9377 // < 0.5 bright colors
9378 // > 0.5 dark colors
9379
9380 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
9381 }
9382
9383 public static function applyBookingColorTag($booking, $tags = [])
9384 {
9385 if (!is_array($tags) || !$tags) {
9386 $tags = self::loadBookingsColorTags();
9387 }
9388
9389 if (!empty($booking['colortag'])) {
9390 $color_tag_arr = json_decode($booking['colortag'], true);
9391 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
9392 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
9393 return $color_tag_arr;
9394 }
9395 }
9396
9397 $dbo = JFactory::getDbo();
9398
9399 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
9400 $docs_data = [];
9401 $invoice_numb = false;
9402 $receipt_numb = false;
9403
9404 if ($booking['status'] == 'confirmed' && ($booking['checkin'] ?? 0) > time()) {
9405 $q = "SELECT `b`.`id` AS `o_id`, `i`.`id` AS `inv_id`, `i`.`number` AS `inv_number`, `i`.`file_name` AS `inv_file_name`, `r`.`id` AS `rcp_id`, `r`.`number` AS `rcp_number`
9406 FROM `#__vikbooking_orders` AS `b`
9407 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
9408 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
9409 WHERE `b`.`id`=" . (int)$bid . ";";
9410 $dbo->setQuery($q);
9411 $docs_data = $dbo->loadAssoc();
9412 if ($docs_data) {
9413 $invoice_numb = (!empty($docs_data['inv_id']));
9414 $receipt_numb = (!empty($docs_data['rcp_id']));
9415
9416 /**
9417 * We inject the invoice number or receipt number into the color tag rules.
9418 * This is useful for the Tableaux page to show the invoice/receipt number.
9419 *
9420 * @since 1.12 (J) - 1.1.7 (WP)
9421 */
9422 if ($invoice_numb || $receipt_numb) {
9423 foreach ($tags as &$tval) {
9424 if ($invoice_numb) {
9425 $tval['invoice_number'] = $docs_data['inv_number'];
9426 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
9427 }
9428 if ($receipt_numb) {
9429 $tval['receipt_number'] = $docs_data['rcp_number'];
9430 }
9431 }
9432 // unset last reference
9433 unset($tval);
9434 }
9435 }
9436 }
9437
9438 /**
9439 * Some OTA bookings using an OTA-Collect business model may look as not entirely
9440 * paid when they actually are, due to commissions and pass through taxes. We also
9441 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
9442 *
9443 * @since 1.16.3 (J) - 1.6.3 (WP)
9444 */
9445 $ota_collected_booking = false;
9446 $hotel_collect_vcc_unpaid = false;
9447 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
9448 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
9449 $ota_collected_booking = true;
9450 }
9451 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
9452 // VCC detected, check the history for a payment event
9453 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
9454 // list of history types to fetch
9455 $h_payment_types = [];
9456 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
9457 if ($h_payment_group && $h_payment_group['types']) {
9458 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
9459 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
9460 }
9461 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
9462 if (!$payment_ev_datas) {
9463 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
9464 $hotel_collect_vcc_unpaid = true;
9465 }
9466 }
9467 }
9468
9469 foreach ($tags as $tkey => $tval) {
9470 if (empty($tval['rule'])) {
9471 continue;
9472 }
9473
9474 /**
9475 * Trigger event to let third-party plugins validate the custom rule.
9476 *
9477 * @since 1.16.4 (J) - 1.6.4 (WP)
9478 */
9479 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
9480 if (!empty($custom_rule)) {
9481 return (array) $custom_rule[0];
9482 }
9483
9484 switch ($tval['rule']) {
9485 case 'pend1':
9486 // Room is waiting for the payment (locked record)
9487 if ($booking['status'] == 'standby') {
9488 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
9489 $dbo->setQuery($q);
9490 $tmp_lock = $dbo->loadAssoc();
9491 if ($tmp_lock) {
9492 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9493 return $tval;
9494 }
9495 }
9496 break;
9497 case 'conf1':
9498 // Confirmed (Room Closed)
9499 if ($booking['status'] == 'confirmed' && (!empty($booking['closure']) || $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED'))) {
9500 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9501 return $tval;
9502 }
9503 break;
9504 case 'ovbook':
9505 // Confirmed (Overbooking)
9506 if ($booking['status'] == 'confirmed' && !empty($booking['type']) && $booking['type'] == 'overbooking') {
9507 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9508 return $tval;
9509 }
9510 break;
9511 case 'conf2':
9512 // Confirmed (No Rate 0.00/NULL Total)
9513 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9514 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9515 return $tval;
9516 }
9517 break;
9518 case 'conf3':
9519 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9520 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null || $hotel_collect_vcc_unpaid) && $invoice_numb === false && $receipt_numb === false) {
9521 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9522 return $tval;
9523 }
9524 break;
9525 case 'inv1':
9526 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9527 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9528 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9529 return $tval;
9530 }
9531 break;
9532 case 'rcp1':
9533 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9534 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9535 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9536 return $tval;
9537 }
9538 break;
9539 case 'conf4':
9540 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9541 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9542 // make sure the commissions are not making the difference
9543 if ($booking['totpaid'] + ($booking['cmms'] ?? 0) < $booking['total']) {
9544 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9545 return $tval;
9546 }
9547 }
9548 break;
9549 case 'conf5':
9550 // Confirmed (Total > 0 && Total Paid >= Total)
9551 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) {
9552 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9553 return $tval;
9554 }
9555 break;
9556 case 'inv2':
9557 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9558 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9559 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9560 return $tval;
9561 }
9562 break;
9563 default:
9564 // unsupported rule
9565 break;
9566 }
9567 }
9568
9569 return [];
9570 }
9571
9572 public static function getBookingInfoFromID($bid)
9573 {
9574 $dbo = JFactory::getDbo();
9575
9576 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9577 $dbo->setQuery($q);
9578 $booking_info = $dbo->loadAssoc();
9579 if ($booking_info) {
9580 return $booking_info;
9581 }
9582
9583 return [];
9584 }
9585
9586 /**
9587 * Load booking details for a given list of IDs.
9588 *
9589 * @param int $roomid The room integer number.
9590 * @param array $room_bids_pool List of booking IDs involved.
9591 * @param bool $unassigned Whether to return also the unassigned bookings.
9592 *
9593 * @return array Associative list of sub-unit bookings, or list containing also the unassigned bookings.
9594 *
9595 * @since 1.18.2 (J) - 1.8.2 (WP) added argument $unassigned.
9596 */
9597 public static function loadRoomIndexesBookings(int $roomid, array $room_bids_pool, $unassigned = false)
9598 {
9599 $dbo = JFactory::getDbo();
9600
9601 $pool_values = [];
9602 if ($unassigned) {
9603 // prepare the default return value
9604 $pool_values = [
9605 [],
9606 [],
9607 ];
9608 }
9609
9610 if (empty($roomid) || !$room_bids_pool) {
9611 return $pool_values;
9612 }
9613
9614 $room_features_bookings = [];
9615 $unassigned_bookings = [];
9616
9617 $all_bids = [];
9618 foreach ($room_bids_pool as $day => $bids) {
9619 $all_bids = array_merge($all_bids, $bids);
9620 }
9621 $all_bids = array_map('intval', array_unique($all_bids));
9622 $all_bids_str = implode(', ', $all_bids);
9623
9624 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9625 $dbo->setQuery($q);
9626 $rbookings = $dbo->loadAssocList();
9627 if (!$rbookings) {
9628 return $pool_values;
9629 }
9630
9631 foreach ($rbookings as $k => $v) {
9632 if (empty($v['roomindex'])) {
9633 if ($unassigned) {
9634 // push this booking to the list of unassigned reservations
9635 $unassigned_bookings[] = $v['idorder'];
9636 }
9637 // do not proceed further
9638 continue;
9639 }
9640 if (!isset($room_features_bookings[$v['roomindex']])) {
9641 $room_features_bookings[$v['roomindex']] = [];
9642 }
9643 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9644 }
9645
9646 if ($unassigned) {
9647 // return a list of information
9648 return [
9649 $room_features_bookings,
9650 $unassigned_bookings,
9651 ];
9652 }
9653
9654 // return just the list of room sub-unit bookings
9655 return $room_features_bookings;
9656 }
9657
9658 public static function getSendEmailWhen()
9659 {
9660 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9661 }
9662
9663 public static function getMinutesAutoRemove()
9664 {
9665 static $minAutoRemove = null;
9666
9667 if ($minAutoRemove !== null) {
9668 return $minAutoRemove;
9669 }
9670
9671 $minAutoRemove = VBOFactory::getConfig()->getInt('minautoremove', 0);
9672
9673 return $minAutoRemove;
9674 }
9675
9676 /**
9677 * Returns the currently active SMS driver handle.
9678 *
9679 * @return string
9680 *
9681 * @since 1.18.8 (J) - 1.8.8 (WP) removed support for WhatsApp free-text messages due to Meta
9682 * business template requirements - use the apposite E4jConnect
9683 * WhatsApp Business Messaging service with Vik Channel Manager.
9684 */
9685 public static function getSMSAPIClass()
9686 {
9687 $smsApiValue = VBOFactory::getConfig()->getString('smsapi', '');
9688
9689 if (preg_match('/^whatsapp/i', $smsApiValue)) {
9690 // no longer supported
9691 $smsApiValue = '';
9692 }
9693
9694 return $smsApiValue;
9695 }
9696
9697 public static function autoSendSMSEnabled()
9698 {
9699 return VBOFactory::getConfig()->getBool('smsautosend', false);
9700 }
9701
9702 public static function getSendSMSTo()
9703 {
9704 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9705 }
9706
9707 public static function getSendSMSWhen()
9708 {
9709 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9710 }
9711
9712 public static function getSMSAdminPhone()
9713 {
9714 return VBOFactory::getConfig()->getString('smsadminphone', '');
9715 }
9716
9717 public static function getSMSParams($as_array = true)
9718 {
9719 if (!$as_array) {
9720 return VBOFactory::getConfig()->getString('smsparams', '');
9721 }
9722
9723 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9724 }
9725
9726 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9727 $dbo = JFactory::getDbo();
9728 switch (strtolower($booking_status)) {
9729 case 'standby':
9730 $status = 'pend';
9731 break;
9732 case 'cancelled':
9733 $status = 'canc';
9734 break;
9735 default:
9736 $status = '';
9737 break;
9738 }
9739 $paramtype = 'sms'.$type.'tpl'.$status;
9740 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9741 $dbo->setQuery($q);
9742 $dbo->execute();
9743 if ($dbo->getNumRows() < 1) {
9744 if ($status == 'canc') {
9745 //Type cancelled is used by VCM since v1.6.6
9746 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9747 $dbo->setQuery($q);
9748 $dbo->execute();
9749 }
9750 return '';
9751 }
9752 $ft = $dbo->loadAssocList();
9753 if (is_object($vbo_tn)) {
9754 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9755 }
9756 return $ft[0]['setting'];
9757 }
9758
9759 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9760 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9761 }
9762
9763 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9764 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9765 }
9766
9767 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9768 $dbo = JFactory::getDbo();
9769 $phone = str_replace(" ", '', trim($phone));
9770 $cprefix = '';
9771 if (!empty($country_threecode)) {
9772 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9773 $dbo->setQuery($q);
9774 $dbo->execute();
9775 if ($dbo->getNumRows() > 0) {
9776 $cprefix = $dbo->loadResult();
9777 $cprefix = str_replace(" ", '', trim($cprefix));
9778 }
9779 }
9780 if (!empty($cprefix)) {
9781 if (substr($phone, 0, 1) != '+') {
9782 if (substr($phone, 0, 2) == '00') {
9783 $phone = '+'.substr($phone, 2);
9784 } else {
9785 $phone = $cprefix.$phone;
9786 }
9787 }
9788 }
9789 return $phone;
9790 }
9791
9792 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9793 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9794
9795 /**
9796 * Trigger event to allow third-party plugins to manipulate the template string.
9797 *
9798 * @since 1.16.10 (J) - 1.6.10 (WP)
9799 */
9800 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9801
9802 /**
9803 * Parse all conditional text rules.
9804 *
9805 * @since 1.14 (J) - 1.4.0 (WP)
9806 */
9807 self::getConditionalRulesInstance()
9808 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9809 ->parseTokens($tpl);
9810
9811 /**
9812 * Parse all Door Access Control tags.
9813 *
9814 * @since 1.18.4 (J) - 1.8.4 (WP)
9815 */
9816 VBOFactory::getDoorAccessControl()
9817 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9818
9819 $vbo_df = self::getDateFormat();
9820 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9821 $datesep = self::getDateSeparator();
9822
9823 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9824 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9825 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9826 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9827 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9828 if (!empty($booking['confirmnumber'])) {
9829 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9830 } else {
9831 $tpl = str_replace("{confirmnumb}", '', $tpl);
9832 }
9833 if (!empty($booking['idorderota'])) {
9834 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9835 } else {
9836 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9837 }
9838
9839 $rooms_booked = array();
9840 $rooms_names = array();
9841 $tot_adults = 0;
9842 $tot_children = 0;
9843 $tot_guests = 0;
9844 foreach ($booking_rooms as $broom) {
9845 $rooms_names[] = $broom['room_name'];
9846 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9847 $rooms_booked[$broom['room_name']] += 1;
9848 } else {
9849 $rooms_booked[$broom['room_name']] = 1;
9850 }
9851 $tot_adults += (int)$broom['adults'];
9852 $tot_children += (int)$broom['children'];
9853 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9854 }
9855 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9856 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9857 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9858 $rooms_booked_quant = array();
9859 foreach ($rooms_booked as $rname => $quant) {
9860 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9861 }
9862 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9863 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9864 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9865 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9866 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9867 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9868 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9869 $remaining_bal = $booking['total'] - $booking['totpaid'];
9870 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9871
9872 return $tpl;
9873 }
9874
9875 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9876 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9877
9878 /**
9879 * Trigger event to allow third-party plugins to manipulate the template string.
9880 *
9881 * @since 1.16.10 (J) - 1.6.10 (WP)
9882 */
9883 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9884
9885 /**
9886 * Parse all conditional text rules.
9887 *
9888 * @since 1.14 (J) - 1.4.0 (WP)
9889 */
9890 self::getConditionalRulesInstance()
9891 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9892 ->parseTokens($tpl);
9893
9894 /**
9895 * Parse all Door Access Control tags.
9896 *
9897 * @since 1.18.4 (J) - 1.8.4 (WP)
9898 */
9899 VBOFactory::getDoorAccessControl()
9900 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9901
9902 $vbo_df = self::getDateFormat();
9903 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9904 $datesep = self::getDateSeparator();
9905
9906 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9907 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9908 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9909 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9910 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9911 if (!empty($booking['confirmnumber'])) {
9912 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9913 } else {
9914 $tpl = str_replace("{confirmnumb}", '', $tpl);
9915 }
9916 if (!empty($booking['idorderota'])) {
9917 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9918 } else {
9919 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9920 }
9921
9922 $rooms_booked = array();
9923 $rooms_names = array();
9924 $tot_adults = 0;
9925 $tot_children = 0;
9926 $tot_guests = 0;
9927 foreach ($booking_rooms as $broom) {
9928 $rooms_names[] = $broom['room_name'];
9929 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9930 $rooms_booked[$broom['room_name']] += 1;
9931 } else {
9932 $rooms_booked[$broom['room_name']] = 1;
9933 }
9934 $tot_adults += (int)$broom['adults'];
9935 $tot_children += (int)$broom['children'];
9936 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9937 }
9938 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9939 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9940 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9941 $rooms_booked_quant = array();
9942 foreach ($rooms_booked as $rname => $quant) {
9943 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9944 }
9945 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9946 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9947 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9948 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9949 $remaining_bal = $booking['total'] - $booking['totpaid'];
9950 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9951 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9952
9953 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9954 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9955
9956 if (VBOPlatformDetection::isWordPress()) {
9957 /**
9958 * @wponly Rewrite URI for front-end
9959 */
9960 $book_link = str_replace(JUri::root(), '', $book_link);
9961 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9962 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9963 if ($itemid) {
9964 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9965 }
9966 } else {
9967 /**
9968 * @joomlaonly Rewrite URI for front-end
9969 */
9970 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9971 $uri_parts = [
9972 'option' => 'com_vikbooking',
9973 'view' => 'booking',
9974 'sid' => $use_sid,
9975 'ts' => $booking['ts'],
9976 ];
9977 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9978 // inject lang in query string for proper link routing
9979 $uri_parts['lang'] = $booking['lang'];
9980 }
9981 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9982 }
9983
9984 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9985 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9986 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9987
9988 return $tpl;
9989 }
9990
9991 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9992 $dbo = JFactory::getDbo();
9993 if (!class_exists('VboApplication')) {
9994 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9995 }
9996 $vbo_app = new VboApplication;
9997 if (empty($oid)) {
9998 return false;
9999 }
10000 $sms_api = self::getSMSAPIClass();
10001 if (empty($sms_api)) {
10002 return false;
10003 }
10004 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
10005 return false;
10006 }
10007 $sms_api_params = self::getSMSParams();
10008 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
10009 return false;
10010 }
10011 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
10012 return false;
10013 }
10014 $send_sms_to = self::getSendSMSTo();
10015 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
10016 return false;
10017 }
10018 $booking = array();
10019 $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.";";
10020 $dbo->setQuery($q);
10021 $dbo->execute();
10022 if ($dbo->getNumRows() > 0) {
10023 $booking = $dbo->loadAssoc();
10024 }
10025 if (!(count($booking) > 0)) {
10026 return false;
10027 }
10028 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
10029 return false;
10030 }
10031 $booking_rooms = array();
10032 $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'].";";
10033 $dbo->setQuery($q);
10034 $dbo->execute();
10035 if ($dbo->getNumRows() > 0) {
10036 $booking_rooms = $dbo->loadAssocList();
10037 }
10038 $admin_phone = self::getSMSAdminPhone();
10039 $admin_sendermail = self::getSenderMail();
10040 $admin_email = self::getAdminMail();
10041 $f_result = false;
10042 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
10043 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
10044 //SMS for the administrator
10045 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
10046 if (!empty($sms_text)) {
10047 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10048 //administrator phone can contain multiple numbers separated by comma or semicolon
10049 $admin_phones = array();
10050 if (strpos($admin_phone, ',') !== false) {
10051 $all_phones = explode(',', $admin_phone);
10052 foreach ($all_phones as $aph) {
10053 if (!empty($aph)) {
10054 $admin_phones[] = trim($aph);
10055 }
10056 }
10057 } elseif (strpos($admin_phone, ';') !== false) {
10058 $all_phones = explode(';', $admin_phone);
10059 foreach ($all_phones as $aph) {
10060 if (!empty($aph)) {
10061 $admin_phones[] = trim($aph);
10062 }
10063 }
10064 } else {
10065 $admin_phones[] = $admin_phone;
10066 }
10067 foreach ($admin_phones as $admphone) {
10068 $response_obj = $sms_obj->sendMessage($admphone, strip_tags($sms_text));
10069 if ( !$sms_obj->validateResponse($response_obj) ) {
10070 //notify the administrator via email with the error of the SMS sending
10071 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10072 } else {
10073 $f_result = true;
10074 }
10075 }
10076 }
10077 }
10078 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
10079 //SMS for the Customer
10080 $vbo_tn = self::getTranslator();
10081 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
10082 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
10083 if (!empty($sms_text)) {
10084 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10085 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
10086 if ( !$sms_obj->validateResponse($response_obj) ) {
10087 //notify the administrator via email with the error of the SMS sending
10088 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10089 } else {
10090 $f_result = true;
10091 }
10092 }
10093 }
10094 return $f_result;
10095 }
10096
10097 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
10098 {
10099 if (!defined('_VIKBOOKINGEXEC')) {
10100 define('_VIKBOOKINGEXEC', '1');
10101 }
10102
10103 ob_start();
10104 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
10105 $content = ob_get_contents();
10106 ob_end_clean();
10107
10108 $default_params = array(
10109 'show_header' => 0,
10110 'header_data' => array(),
10111 'show_footer' => 0,
10112 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10113 'pdf_unit' => 'PDF_UNIT',
10114 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10115 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10116 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10117 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10118 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10119 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10120 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10121 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10122 'header_font_size' => '10',
10123 'body_font_size' => '10',
10124 'footer_font_size' => '8',
10125 'show_lines_taxrate_col' => 0,
10126 );
10127
10128 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
10129 $default_params = array_merge($default_params, $invoice_params);
10130 }
10131
10132 return array($content, $default_params);
10133 }
10134
10135 /**
10136 * Includes within a buffer the template file for the custom (manual) invoice.
10137 *
10138 * @param array $invoice the record of the custom invoice
10139 * @param array $customer the customer record
10140 *
10141 * @return string the HTML content of the template file
10142 *
10143 * @since 1.11.1
10144 */
10145 public static function loadCustomInvoiceTmpl($invoice, $customer) {
10146 if (!defined('_VIKBOOKINGEXEC')) {
10147 define('_VIKBOOKINGEXEC', '1');
10148 }
10149 ob_start();
10150 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
10151 $content = ob_get_contents();
10152 ob_end_clean();
10153 $default_params = array(
10154 'show_header' => 0,
10155 'header_data' => array(),
10156 'show_footer' => 0,
10157 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10158 'pdf_unit' => 'PDF_UNIT',
10159 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10160 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10161 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10162 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10163 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10164 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10165 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10166 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10167 'header_font_size' => '10',
10168 'body_font_size' => '10',
10169 'footer_font_size' => '8'
10170 );
10171 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
10172 $default_params = array_merge($default_params, $invoice_params);
10173 }
10174 return array($content, $default_params);
10175 }
10176
10177 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
10178 {
10179 // clone the original template file source
10180 $parsed = $invoicetpl;
10181
10182 /**
10183 * Parse all conditional text rules.
10184 *
10185 * @since 1.14 (J) - 1.4.0 (WP)
10186 */
10187 self::getConditionalRulesInstance()
10188 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
10189 ->parseTokens($parsed);
10190
10191 $dbo = JFactory::getDbo();
10192 if (is_null($vbo_tn)) {
10193 $vbo_tn = self::getTranslator();
10194 }
10195 // availability helper
10196 $av_helper = self::getAvailabilityInstance();
10197
10198 $nowdf = self::getDateFormat();
10199 if ($nowdf == "%d/%m/%Y") {
10200 $df = 'd/m/Y';
10201 } elseif ($nowdf == "%m/%d/%Y") {
10202 $df = 'm/d/Y';
10203 } else {
10204 $df = 'Y/m/d';
10205 }
10206 $datesep = self::getDateSeparator();
10207
10208 /**
10209 * Split stay reservation.
10210 *
10211 * @since 1.16.0 (J) - 1.6.0 (WP)
10212 */
10213 $room_stay_dates = [];
10214 if ($booking['split_stay']) {
10215 if ($booking['status'] == 'confirmed') {
10216 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
10217 } else {
10218 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
10219 }
10220 // immediately count the number of nights of stay for each split room
10221 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
10222 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
10223 // overwrite values for compatibility with non-confirmed bookings
10224 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
10225 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
10226 }
10227 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
10228 // overwrite the whole array
10229 $room_stay_dates[$sps_r_k] = $sps_r_v;
10230 }
10231 }
10232
10233 $companylogo = self::getSiteLogo();
10234 $uselogo = '';
10235 if (!empty($companylogo)) {
10236 /**
10237 * Let's try to prevent TCPDF errors.
10238 *
10239 * @since August 2nd 2019
10240 */
10241 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10242 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10243 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10244 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10245 }
10246 }
10247 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10248 $parsed = str_replace("{company_info}", $company_info, $parsed);
10249 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
10250 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
10251 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
10252 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10253
10254 // custom fields replacement
10255 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10256 if (is_array($cmatches[1]) && $cmatches[1]) {
10257 $cfids = array();
10258 foreach ($cmatches[1] as $cfid) {
10259 $cfids[] = $cfid;
10260 }
10261 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10262 $dbo->setQuery($q);
10263 $cfields = $dbo->loadAssocList();
10264 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10265 $cfmap = array();
10266 foreach ($cfields as $cf) {
10267 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10268 }
10269 $cfmapreplace = array();
10270 $partsreceived = explode("\n", $booking['custdata']);
10271 if (count($partsreceived) > 0) {
10272 foreach ($partsreceived as $pst) {
10273 if (!empty($pst)) {
10274 $tmpdata = explode(":", $pst);
10275 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10276 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10277 }
10278 }
10279 }
10280 }
10281 foreach ($cmatches[1] as $cfid) {
10282 if (array_key_exists($cfid, $cfmapreplace)) {
10283 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10284 } else {
10285 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10286 }
10287 }
10288 }
10289
10290 // invoice price description - Start
10291 $rooms = array();
10292 $tars = array();
10293 $arrpeople = array();
10294 $is_package = !empty($booking['pkg']) ? true : false;
10295 $tot_adults = 0;
10296 $tot_children = 0;
10297 $tot_guests = 0;
10298 foreach ($booking_rooms as $kor => $or) {
10299 $num = $kor + 1;
10300 $rooms[$num] = $or;
10301
10302 // determine the days to consider for the count of the availability
10303 $room_nights = $booking['days'];
10304 $room_checkin = $booking['checkin'];
10305 $room_checkout = $booking['checkout'];
10306 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10307 $room_nights = $room_stay_dates[$kor]['nights'];
10308 $room_checkin = $room_stay_dates[$kor]['checkin'];
10309 $room_checkout = $room_stay_dates[$kor]['checkout'];
10310 }
10311
10312 $arrpeople[$num]['adults'] = $or['adults'];
10313 $arrpeople[$num]['children'] = $or['children'];
10314 $tot_adults += $or['adults'];
10315 $tot_children += $or['children'];
10316 $tot_guests += ($or['adults'] + $or['children']);
10317 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10318 //package or custom cost set from the back-end
10319 continue;
10320 }
10321 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
10322 $dbo->setQuery($q);
10323 $tar = $dbo->loadAssocList();
10324 if ($tar) {
10325 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
10326 //different usage
10327 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
10328 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
10329 //Occupancy Override
10330 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
10331 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
10332 //
10333 if (is_array($diffusageprice)) {
10334 //set a charge or discount to the price(s) for the different usage of the room
10335 foreach ($tar as $kpr => $vpr) {
10336 $tar[$kpr]['diffusage'] = $or['adults'];
10337 if ($diffusageprice['chdisc'] == 1) {
10338 //charge
10339 if ($diffusageprice['valpcent'] == 1) {
10340 //fixed value
10341 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10342 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10343 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
10344 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10345 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
10346 } else {
10347 //percentage value
10348 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10349 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
10350 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
10351 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10352 $tar[$kpr]['cost'] = $aduseval;
10353 }
10354 } else {
10355 //discount
10356 if ($diffusageprice['valpcent'] == 1) {
10357 //fixed value
10358 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10359 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10360 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
10361 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10362 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
10363 } else {
10364 //percentage value
10365 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10366 $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);
10367 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
10368 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10369 $tar[$kpr]['cost'] = $aduseval;
10370 }
10371 }
10372 }
10373 }
10374 }
10375 //
10376 $tars[$num] = $tar[0];
10377 }
10378 }
10379 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10380 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10381 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10382 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10383 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10384 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10385
10386 $isdue = 0;
10387 $tot_taxes = 0;
10388 $tot_city_taxes = 0;
10389 $tot_fees = 0;
10390 $pricestr = array();
10391 $optstr = array();
10392 // start building the tax summary
10393 VBOTaxonomySummary::start();
10394 foreach ($booking_rooms as $kor => $or) {
10395 $num = $kor + 1;
10396
10397 // determine the days to consider for the count of the availability
10398 $room_nights = $booking['days'];
10399 $room_checkin = $booking['checkin'];
10400 $room_checkout = $booking['checkout'];
10401 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10402 $room_nights = $room_stay_dates[$kor]['nights'];
10403 $room_checkin = $room_stay_dates[$kor]['checkin'];
10404 $room_checkout = $room_stay_dates[$kor]['checkout'];
10405 }
10406
10407 $pricestr[$num] = array();
10408 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10409 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
10410 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
10411 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
10412 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
10413 $pricestr[$num]['tot'] = $calctar;
10414 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
10415 $tot_taxes += ($calctar - $cost_minus_tax);
10416 $isdue += $calctar;
10417 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
10418 $pricestr[$num]['tax_rate'] = $tax_rate;
10419 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
10420 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10421 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
10422 $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'] : "");
10423 $pricestr[$num]['tot'] = $calctar;
10424 $tars[$num]['calctar'] = $calctar;
10425 $isdue += $calctar;
10426 if ($calctar == $display_rate) {
10427 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
10428 $tot_taxes += ($display_rate - $cost_minus_tax);
10429 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
10430 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
10431 $pricestr[$num]['tax_rate'] = $tax_rate;
10432 } else {
10433 $tot_taxes += ($calctar - $display_rate);
10434 $pricestr[$num]['tax'] = ($calctar - $display_rate);
10435 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
10436 $pricestr[$num]['tax_rate'] = $tax_rate;
10437 }
10438 }
10439 $optstr[$num] = array();
10440 $opt_ind = 0;
10441 if (!empty($or['optionals'])) {
10442 $stepo = explode(";", $or['optionals']);
10443 foreach ($stepo as $roptkey => $oo) {
10444 if (!empty($oo)) {
10445 $stept = explode(":", $oo);
10446 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
10447 $dbo->setQuery($q);
10448 $actopt = $dbo->loadAssocList();
10449 if ($actopt) {
10450 if (is_object($vbo_tn)) {
10451 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
10452 }
10453 $optstr[$num][$opt_ind] = array();
10454 $chvar = '';
10455 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
10456 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
10457 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
10458 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
10459 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
10460 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
10461 $agestept = explode('-', $stept[1]);
10462 $stept[1] = $agestept[0];
10463 $chvar = $agestept[1];
10464 $realcost = 0;
10465 if (!empty($chvar)) {
10466 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
10467 //percentage value of the adults tariff
10468 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10469 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10470 } else {
10471 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10472 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10473 }
10474 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
10475 //VBO 1.10 - percentage value of room base cost
10476 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10477 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10478 } else {
10479 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
10480 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10481 }
10482 }
10483 $actopt[0]['chageintv'] = $chvar;
10484 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
10485 $actopt[0]['quan'] = $stept[1];
10486 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
10487 }
10488 } else {
10489 $actopt[0]['quan'] = $stept[1];
10490 // VBO 1.11 - options percentage cost of the room total fee
10491 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10492 $deftar_basecosts = $or['cust_cost'];
10493 } else {
10494 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10495 }
10496 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
10497 //
10498 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
10499 }
10500 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
10501 $realcost = $actopt[0]['maxprice'];
10502 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
10503 $realcost = $actopt[0]['maxprice'] * $stept[1];
10504 }
10505 }
10506 if ($actopt[0]['perperson'] == 1) {
10507 $realcost = $realcost * $or['adults'];
10508 }
10509
10510 /**
10511 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
10512 *
10513 * @since 1.17.7 (J) - 1.7.7 (WP)
10514 */
10515 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
10516 if ($custom_calculation) {
10517 $realcost = (float) $custom_calculation[0];
10518 }
10519
10520 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
10521 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
10522 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
10523 $optstr[$num][$opt_ind]['tax'] = 0;
10524 if ($actopt[0]['is_citytax'] == 1) {
10525 $tot_city_taxes += $tmpopr;
10526 } elseif ($actopt[0]['is_fee'] == 1) {
10527 $tot_fees += $tmpopr;
10528 }
10529 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
10530 if ($tmpopr == $realcost) {
10531 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
10532 $tot_taxes += ($realcost - $opt_minus_tax);
10533 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
10534 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
10535 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10536 } else {
10537 $tot_taxes += ($tmpopr - $realcost);
10538 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
10539 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
10540 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10541 }
10542 //
10543 $opt_ind++;
10544 $isdue += $tmpopr;
10545 }
10546 }
10547 }
10548 }
10549
10550 // custom extra costs
10551 if (!empty($or['extracosts'])) {
10552 $cur_extra_costs = json_decode($or['extracosts'], true);
10553 foreach ($cur_extra_costs as $eck => $ecv) {
10554 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10555 $isdue += $ecplustax;
10556 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
10557 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
10558 $optstr[$num][$opt_ind]['tax'] = 0;
10559 if ($ecplustax == $ecv['cost']) {
10560 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10561 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
10562 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
10563 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
10564 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10565 } else {
10566 $tot_taxes += ($ecplustax - $ecv['cost']);
10567 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
10568 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
10569 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10570 }
10571 $opt_ind++;
10572 }
10573 }
10574 }
10575
10576 $usedcoupon = false;
10577 if (strlen($booking['coupon']) > 0) {
10578 $orig_isdue = $isdue;
10579 $expcoupon = explode(";", $booking['coupon']);
10580 $usedcoupon = $expcoupon;
10581 $isdue = $isdue - (float)$expcoupon[1];
10582 if ($isdue != $orig_isdue) {
10583 //lower taxes proportionally
10584 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10585 }
10586 }
10587 if ($booking['refund'] > 0) {
10588 $orig_isdue = $isdue;
10589 $isdue -= $booking['refund'];
10590 if ($isdue != $orig_isdue) {
10591 //lower taxes proportionally
10592 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10593 }
10594 }
10595 $rows_written = 0;
10596 $inv_rows = '';
10597 foreach ($pricestr as $num => $price_descr) {
10598 // add room split stay information, if any
10599 $split_stay_str = '';
10600 $kor = $num - 1;
10601 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10602 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10603 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10604 }
10605
10606 $inv_rows .= '<tr>'."\n";
10607 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10608 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($price_descr['tot'] - $price_descr['tax'])), $booking['currencyname']) . '</td>'."\n";
10609 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tax']), $booking['currencyname']) . '</td>'."\n";
10610 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tot']), $booking['currencyname']) . '</td>'."\n";
10611 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10612 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10613 }
10614 $inv_rows .= '</tr>'."\n";
10615 $rows_written++;
10616 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10617 foreach ($optstr[$num] as $optk => $optv) {
10618 $inv_rows .= '<tr>'."\n";
10619 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10620 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($optv['tot'] - $optv['tax'])), $booking['currencyname']) . '</td>'."\n";
10621 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tax']), $booking['currencyname']) . '</td>'."\n";
10622 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tot']), $booking['currencyname']) . '</td>'."\n";
10623 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10624 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10625 }
10626 $inv_rows .= '</tr>'."\n";
10627 $rows_written++;
10628 }
10629 }
10630 }
10631
10632 // if discount print row
10633 if ($usedcoupon !== false) {
10634 $inv_rows .= '<tr>'."\n";
10635 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10636 $inv_rows .= '</tr>'."\n";
10637 $inv_rows .= '<tr>'."\n";
10638 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10639 $inv_rows .= '<td></td>'."\n";
10640 $inv_rows .= '<td></td>'."\n";
10641 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($usedcoupon[1]), $booking['currencyname']) . '</td>'."\n";
10642 $inv_rows .= '</tr>'."\n";
10643 $rows_written += 2;
10644 }
10645 // if refunded amount, print row
10646 if ($booking['refund'] > 0) {
10647 $inv_rows .= '<tr>'."\n";
10648 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10649 $inv_rows .= '</tr>'."\n";
10650 $inv_rows .= '<tr>'."\n";
10651 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10652 $inv_rows .= '<td></td>'."\n";
10653 $inv_rows .= '<td></td>'."\n";
10654 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($booking['refund']), $booking['currencyname']) . '</td>'."\n";
10655 $inv_rows .= '</tr>'."\n";
10656 $rows_written += 2;
10657 }
10658 //
10659 $min_records = 8;
10660 if ($rows_written < $min_records) {
10661 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10662 $inv_rows .= '<tr>'."\n";
10663 $inv_rows .= '<td></td>'."\n";
10664 $inv_rows .= '<td></td>'."\n";
10665 $inv_rows .= '<td></td>'."\n";
10666 $inv_rows .= '</tr>'."\n";
10667 }
10668 }
10669 //invoice price description - End
10670 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10671 $parsed = str_replace("{invoice_totalnet}", self::formatCurrencyNumber(self::numberFormat(($isdue - $tot_taxes)), $booking['currencyname']), $parsed);
10672 $parsed = str_replace("{invoice_totaltax}", self::formatCurrencyNumber(self::numberFormat($tot_taxes), $booking['currencyname']), $parsed);
10673 $parsed = str_replace("{invoice_grandtotal}", self::formatCurrencyNumber(self::numberFormat($isdue), $booking['currencyname']), $parsed);
10674 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10675
10676 // invoice tax summary
10677 $tax_summary = VBOTaxonomySummary::get();
10678 $tax_names = VBOTaxonomySummary::getNames();
10679 $taxsum_rows = "";
10680 foreach ($tax_summary as $tax_rate => $tax_amount) {
10681 if (!$tax_rate && !$tax_amount) {
10682 continue;
10683 }
10684 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10685 $taxsum_rows .= '<tr>' . "\n";
10686 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10687 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10688 $taxsum_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($tax_amount), $booking['currencyname']) . '</td>' . "\n";
10689 $taxsum_rows .= '</tr>' . "\n";
10690 }
10691 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10692
10693 return $parsed;
10694 }
10695
10696 /**
10697 * Specific method for parsing the template file for the custom (manual) invoices.
10698 *
10699 * @param string $invoicetpl the plain custom invoice template file before parsing
10700 * @param array $invoice the invoice record
10701 * @param array $customer the customer record
10702 *
10703 * @return string the HTML content of the parsed custom invoice template
10704 *
10705 * @since 1.11.1
10706 */
10707 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10708 $nowdf = self::getDateFormat(true);
10709 if ($nowdf == "%d/%m/%Y") {
10710 $df = 'd/m/Y';
10711 } elseif ($nowdf == "%m/%d/%Y") {
10712 $df = 'm/d/Y';
10713 } else {
10714 $df = 'Y/m/d';
10715 }
10716 $datesep = self::getDateSeparator(true);
10717 $companylogo = self::getSiteLogo();
10718 $uselogo = '';
10719 if (!empty($companylogo)) {
10720 /**
10721 * Let's try to prevent TCPDF errors by checking if the file exists.
10722 *
10723 * @since August 2nd 2019
10724 */
10725 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10726 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10727 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10728 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10729 }
10730 }
10731 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10732 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10733 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10734 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10735 // customer information
10736 $custinfo = '';
10737 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10738 $custinfo .= $customer['email'] . "\n";
10739 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10740 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10741 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10742 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10743 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10744 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10745 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10746 // invoice notes
10747 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10748
10749 return $invoicetpl;
10750 }
10751
10752 /**
10753 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10754 *
10755 * @param array $booking the booking information.
10756 * @param int $invoice_num the invoice number.
10757 * @param string $invoice_suff the invoice suffix.
10758 * @param string $invoice_date the invoice date.
10759 * @param string $company_info optional company information.
10760 * @param bool $translate whether the invoice contents should be translated in the booking language.
10761 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10762 *
10763 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10764 *
10765 * @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.
10766 */
10767 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10768 {
10769 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10770 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10771 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10772
10773 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10774 return false;
10775 }
10776
10777 /**
10778 * Trigger event to allow third party plugins to override some values for the invoice generation.
10779 *
10780 * @since 1.16.3 (J) - 1.6.3 (WP)
10781 */
10782 $event_args = [
10783 'invoice_num' => $invoice_num,
10784 'invoice_suff' => $invoice_suff,
10785 'invoice_date' => $invoice_date,
10786 'company_info' => $company_info,
10787 'translate' => $translate,
10788 ];
10789 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10790 if (is_array($event_args) && $event_args) {
10791 // extract the new values to be used
10792 extract($event_args);
10793 }
10794
10795 $dbo = JFactory::getDbo();
10796 $vbo_tn = self::getTranslator();
10797
10798 // inject the currency name value
10799 $currencyname = self::getCurrencyName();
10800 $booking['currencyname'] = $currencyname;
10801
10802 // make sure we've got a customer ID assigned to the reservation record
10803 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10804 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10805 if ($booking_customer) {
10806 $booking['idcustomer'] = $booking_customer['id'];
10807 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10808 $booking['customer_pin'] = $booking_customer['pin'];
10809 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10810 }
10811 }
10812
10813 $nowdf = self::getDateFormat(true);
10814 if ($nowdf == "%d/%m/%Y") {
10815 $df = 'd/m/Y';
10816 } elseif ($nowdf == "%m/%d/%Y") {
10817 $df = 'm/d/Y';
10818 } else {
10819 $df = 'Y/m/d';
10820 }
10821 $datesep = self::getDateSeparator(true);
10822
10823 if (empty($invoice_date)) {
10824 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10825 $used_date = $booking['ts'];
10826 } else {
10827 /**
10828 * We could be re-generating an invoice for a booking that already had an invoice.
10829 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10830 * It is now possible to keep the same invoice date as the previous one, so check
10831 * what value contains $invoice_date to see if it's different from today's date.
10832 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10833 * to also check the length of the string $invoice_date before using that date.
10834 *
10835 * @since 1.10 - August 2018
10836 */
10837 $base_ts = time();
10838 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10839 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10840 }
10841 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10842 $used_date = $base_ts;
10843 }
10844
10845 // load room reservation records
10846 $q = $dbo->getQuery(true)
10847 ->select($dbo->qn('or') . '.*')
10848 ->select($dbo->qn('r.name', 'room_name'))
10849 ->select($dbo->qn('r.fromadult'))
10850 ->select($dbo->qn('r.toadult'))
10851 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10852 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10853 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10854 ->order($dbo->qn('or.id') . ' ASC');
10855
10856 $dbo->setQuery($q);
10857 $booking_rooms = $dbo->loadAssocList();
10858
10859 if (!$booking_rooms) {
10860 return false;
10861 }
10862
10863 // check if an invoice for this reservation exists
10864 $invoice_id = 0;
10865 $dbo->setQuery(
10866 $dbo->getQuery(true)
10867 ->select('*')
10868 ->from($dbo->qn('#__vikbooking_invoices'))
10869 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10870 ->order($dbo->qn('id') . ' DESC')
10871 );
10872 $invoice_data = $dbo->loadAssoc();
10873 if ($invoice_data) {
10874 $invoice_id = $invoice_data['id'];
10875 }
10876
10877 // if requested, prepare data to refresh the PDF invoice
10878 if ($refresh_pdf === true) {
10879 if (!$invoice_id || !$invoice_data) {
10880 // cannot refresh the PDF if an invoice was never created before
10881 return false;
10882 }
10883
10884 // load the same exact previous information to refresh the invoice
10885 $invoice_num = $invoice_data['number'];
10886 $invoice_suff = $invoice_suff;
10887 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10888 $used_date = $invoice_data['for_date'];
10889
10890 // parse the previous (current) invoice number
10891 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10892 if ($matches) {
10893 // overwrite values
10894 $invoice_num = $matches[1] ?: $invoice_num;
10895 $invoice_suff = $matches[2] ?: $invoice_suff;
10896 }
10897 }
10898
10899 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10900 if ($translate === true) {
10901 if (!empty($booking['lang'])) {
10902 $lang = JFactory::getLanguage();
10903 if ($lang->getTag() != $booking['lang']) {
10904 if (VBOPlatformDetection::isWordPress()) {
10905 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10906 } else {
10907 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10908 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10909 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10910 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10911 }
10912 }
10913 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10914 // force the translation to start because contents should be translated
10915 $vbo_tn::$force_tolang = $booking['lang'];
10916 }
10917 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10918 }
10919 }
10920
10921 // the PDF file name
10922 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10923
10924 /**
10925 * If an analogic invoice is not available, make sure to create the record
10926 * before letting the e-invoicing drivers run to avoid a loop.
10927 *
10928 * @since 1.16.7 (J) - 1.6.7 (WP)
10929 */
10930 $retval = null;
10931 if (!$refresh_pdf && !$invoice_id) {
10932 // prepare record object
10933 $invoice_record = new stdClass;
10934 $invoice_record->number = $invoice_num . $invoice_suff;
10935 $invoice_record->file_name = $pdffname;
10936 $invoice_record->idorder = (int)$booking['id'];
10937 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10938 $invoice_record->created_on = time();
10939 $invoice_record->for_date = (int)$used_date;
10940
10941 // insert record
10942 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10943 return false;
10944 }
10945
10946 $retval = $invoice_record->id ?? false;
10947
10948 if (!$retval) {
10949 return false;
10950 }
10951 }
10952
10953 /**
10954 * The generation of the analogic invoices can trigger the drivers for the
10955 * generation of the e-Invoices if they are set to automatically run.
10956 * However, the e-Invoicing classes may be calling this method, so we need
10957 * to make sure the eInvoicing class is not running before proceeding.
10958 * This method could be called within a loop, and so the second iterations
10959 * may already have loaded the eInvocing class. For this we use a static variable.
10960 *
10961 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10962 * though additional information may be available only after the transmission. On top
10963 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10964 */
10965 static $einvocing_can_run = false;
10966
10967 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10968 // allow second iterations calling this method to run
10969 $einvocing_can_run = true;
10970 }
10971
10972 // always load all drivers, db and plugin based ones
10973 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10974
10975 foreach ($drivers as $driver_obj) {
10976 // set the flag of the external call
10977 $driver_obj->externalCall = __METHOD__;
10978
10979 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10980 $driver_obj->externalData['einvnum'] = $invoice_num;
10981
10982 // let the driver eventually elaborate the booking information before invoicing
10983 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10984
10985 if ($einvocing_can_run === true && !$refresh_pdf) {
10986 // generate the e-Invoice
10987 $driver_err = '';
10988 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10989 $driver_err = $driver_obj->getError();
10990 VikError::raiseWarning('', $driver_err);
10991 }
10992
10993 // Booking History - store log for the e-Invoice result
10994 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10995 }
10996 }
10997
10998 /**
10999 * Start the generation of the analogic (courtesy) PDF invoice.
11000 */
11001
11002 // load dependencies
11003 if (!class_exists('TCPDF')) {
11004 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11005 }
11006 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11007
11008 /**
11009 * Trigger event to allow third party plugins to return a specific font name.
11010 *
11011 * @since 1.16.0 (J) - 1.6.0 (WP)
11012 */
11013 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11014 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11015 $usepdffont = $custom_pdf_font[0];
11016 }
11017
11018 // set array variable to the template file
11019 $booking_info = self::getBookingInfoFromID($booking['id']);
11020
11021 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
11022
11023 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
11024
11025 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11026
11027 if (is_file($pathpdf)) {
11028 @unlink($pathpdf);
11029 }
11030
11031 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11032
11033 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11034 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
11035
11036 // header for each page of the pdf
11037 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
11038 $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]);
11039 }
11040
11041 // change some currencies to their unicode (decimal) value
11042 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11043 if (array_key_exists($booking['currencyname'], $unichr_map)) {
11044 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
11045 }
11046
11047 // header and footer fonts
11048 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11049 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11050
11051 // margins
11052 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11053 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11054 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11055
11056 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11057 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11058 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11059
11060 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11061 $pdf->SetPrintHeader(false);
11062 }
11063 if ($pdfparams['show_footer'] == 0) {
11064 $pdf->SetPrintFooter(false);
11065 }
11066
11067 $pdf->AddPage();
11068 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11069 $pdf->lastPage();
11070 $pdf->Output($pathpdf, 'F');
11071
11072 if (!is_file($pathpdf)) {
11073 return false;
11074 }
11075
11076 if (VBOPlatformDetection::isWordPress()) {
11077 /**
11078 * @wponly - trigger files mirroring
11079 */
11080 VikBookingLoader::import('update.manager');
11081 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11082 }
11083
11084 // Booking History
11085 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
11086
11087 if ($retval) {
11088 // a new invoice ID was inserted above
11089 return $retval;
11090 }
11091
11092 // prepare record object to be updated (if insert needed, it's done at first)
11093 $invoice_record = new stdClass;
11094 $invoice_record->id = (int)$invoice_id;
11095 $invoice_record->number = $invoice_num . $invoice_suff;
11096 $invoice_record->file_name = $pdffname;
11097 $invoice_record->idorder = (int)$booking['id'];
11098 $invoice_record->idcustomer = (int)$booking['idcustomer'];
11099 $invoice_record->created_on = time();
11100 $invoice_record->for_date = (int)$used_date;
11101
11102 // update record
11103 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
11104
11105 // return the PDF invoice ID
11106 return $invoice_id;
11107 }
11108
11109 /**
11110 * Generates an analogic invoice in PDF format for a custom list of services.
11111 * No bookings are assigned to this custom invoice. The method parses the same
11112 * invoice template as for the regular process with real booking IDs.
11113 * The invoice number must be stored before calling this method.
11114 *
11115 * @param int $invoice_id the ID of the custom invoice record
11116 *
11117 * @return bool
11118 *
11119 * @since 1.11.1 (J) - 1.1.1 (WP)
11120 */
11121 public static function generateCustomInvoice($invoice_id)
11122 {
11123 $dbo = JFactory::getDbo();
11124
11125 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
11126 $dbo->setQuery($q);
11127 $invoice = $dbo->loadAssoc();
11128 if (!$invoice) {
11129 return false;
11130 }
11131
11132 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
11133 $rawcont = is_array($rawcont) ? $rawcont : array();
11134 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
11135 $invoice['rawcont'] = $rawcont;
11136 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
11137 if (!$rawcont || !$rows || !$customer) {
11138 // at least one invoice raw is mandatory as well as the customer
11139 return false;
11140 }
11141
11142 /**
11143 * Start the generation of the electronic invoice, if any driver was enabled.
11144 *
11145 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11146 * though additional information would be available only after the transmission.
11147 */
11148 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11149
11150 if ($drivers) {
11151 // make sure the invoice number is just a number
11152 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
11153
11154 // invoke all drivers that should run automatically
11155 foreach ($drivers as $driver_obj) {
11156 // set the flag of the external call
11157 $driver_obj->externalCall = __METHOD__;
11158
11159 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
11160 $driver_obj->externalData['einvnum'] = $invoice['number'];
11161 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
11162 $driver_obj->externalData['einvcustom'] = $invoice;
11163
11164 // prepare data array for the generation of the e-Invoice
11165 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
11166
11167 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
11168 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
11169 if ($preveinv !== false) {
11170 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
11171 }
11172
11173 // generate the e-Invoice
11174 $driver_err = '';
11175 if (!$driver_obj->generateEInvoice($einvdata)) {
11176 $driver_err = $driver_obj->getError();
11177 VikError::raiseWarning('', $driver_err);
11178 }
11179 }
11180 }
11181
11182 /**
11183 * Start the generation of the analogic (courtesy) PDF invoice.
11184 */
11185
11186 // load dependencies
11187 if (!class_exists('TCPDF')) {
11188 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11189 }
11190 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11191
11192 /**
11193 * Trigger event to allow third party plugins to return a specific font name.
11194 *
11195 * @since 1.16.0 (J) - 1.6.0 (WP)
11196 */
11197 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11198 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11199 $usepdffont = $custom_pdf_font[0];
11200 }
11201
11202 // load invoice template file
11203 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
11204
11205 // parse invoice template file
11206 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
11207
11208 $pdffname = $invoice['file_name'];
11209 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11210
11211 if (is_file($pathpdf)) {
11212 @unlink($pathpdf);
11213 }
11214
11215 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11216
11217 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11218 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
11219
11220 // header for each page of the pdf
11221 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
11222 $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]);
11223 }
11224
11225 // change some currencies to their unicode (decimal) value
11226 $currencyname = self::getCurrencyName();
11227 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11228 if (array_key_exists($currencyname, $unichr_map)) {
11229 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
11230 }
11231
11232 // header and footer fonts
11233 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11234 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11235
11236 // margins
11237 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11238 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11239 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11240
11241 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11242 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11243 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11244
11245 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11246 $pdf->SetPrintHeader(false);
11247 }
11248 if ($pdfparams['show_footer'] == 0) {
11249 $pdf->SetPrintFooter(false);
11250 }
11251
11252 $pdf->AddPage();
11253 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11254 $pdf->lastPage();
11255 $pdf->Output($pathpdf, 'F');
11256
11257 if (!is_file($pathpdf)) {
11258 return false;
11259 }
11260
11261 if (VBOPlatformDetection::isWordPress()) {
11262 /**
11263 * @wponly - trigger files mirroring
11264 */
11265 VikBookingLoader::import('update.manager');
11266 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11267 }
11268
11269 // return the result of the generation of the PDF invoice
11270 return true;
11271 }
11272
11273 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
11274 {
11275 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
11276 return false;
11277 }
11278
11279 $dbo = JFactory::getDbo();
11280
11281 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
11282 $dbo->setQuery($q);
11283 $invoice_data = $dbo->loadAssoc();
11284
11285 if (!$invoice_data) {
11286 return false;
11287 }
11288
11289 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
11290 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
11291 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
11292 if (!file_exists($invoice_file_path)) {
11293 return false;
11294 }
11295
11296 $vbo_app = self::getVboApplication();
11297 $admin_sendermail = self::getSenderMail();
11298
11299 $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);
11300
11301 // update record
11302 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
11303 $dbo->setQuery($q);
11304 $dbo->execute();
11305
11306 return true;
11307 }
11308
11309 public static function loadCheckinDocTmpl(array $booking_info = [], array $booking_rooms = [], array $customer = [])
11310 {
11311 if (!defined('_VIKBOOKINGEXEC')) {
11312 define('_VIKBOOKINGEXEC', '1');
11313 }
11314
11315 ob_start();
11316 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php';
11317 $content = ob_get_contents();
11318 ob_end_clean();
11319
11320 $default_params = [
11321 'show_header' => 0,
11322 'header_data' => [],
11323 'show_footer' => 0,
11324 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
11325 'pdf_unit' => 'PDF_UNIT',
11326 'pdf_page_format' => 'PDF_PAGE_FORMAT',
11327 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
11328 'pdf_margin_top' => 'PDF_MARGIN_TOP',
11329 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
11330 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
11331 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
11332 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
11333 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
11334 'header_font_size' => '10',
11335 'body_font_size' => '10',
11336 'footer_font_size' => '8'
11337 ];
11338
11339 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
11340 $default_params = array_merge($default_params, $checkin_params);
11341 }
11342
11343 return [$content, $default_params];
11344 }
11345
11346 public static function parseCheckinDocTemplate(string $checkintpl, array $booking, array $booking_rooms, array $customer = [])
11347 {
11348 $dbo = JFactory::getDbo();
11349 $app = JFactory::getApplication();
11350
11351 // clone string
11352 $parsed = $checkintpl;
11353
11354 $datesep = self::getDateSeparator();
11355 $nowdf = self::getDateFormat();
11356 if ($nowdf == "%d/%m/%Y") {
11357 $df = 'd/m/Y';
11358 } elseif ($nowdf == "%m/%d/%Y") {
11359 $df = 'm/d/Y';
11360 } else {
11361 $df = 'Y/m/d';
11362 }
11363
11364 $company_name = self::getFrontTitle();
11365 $company_info = self::getInvoiceCompanyInfo();
11366 $companylogo = self::getSiteLogo();
11367 $uselogo = '';
11368 if (!empty($companylogo)) {
11369 /**
11370 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
11371 *
11372 * @since March 16th 2021
11373 */
11374 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11375 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
11376 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
11377 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11378 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
11379 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
11380 }
11381 }
11382
11383 $parsed = str_replace("{company_name}", $company_name, $parsed);
11384 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
11385 $parsed = str_replace("{company_info}", $company_info, $parsed);
11386 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
11387 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
11388 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
11389 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
11390
11391 $tot_guests = 0;
11392 $tot_adults = 0;
11393 $tot_children = 0;
11394 foreach ($booking_rooms as $kor => $or) {
11395 $tot_guests += ($or['adults'] + $or['children']);
11396 $tot_adults += $or['adults'];
11397 $tot_children += $or['children'];
11398 }
11399 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
11400 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
11401 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
11402 if ($customer && isset($customer['comments'])) {
11403 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
11404 }
11405
11406 $termsconds = self::getTermsConditions();
11407 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
11408
11409 // custom fields replacemenet
11410 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
11411 if (is_array($cmatches[1]) && $cmatches[1]) {
11412 $cfids = array();
11413 foreach ($cmatches[1] as $cfid) {
11414 $cfids[] = $cfid;
11415 }
11416 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
11417 $dbo->setQuery($q);
11418 $cfields = $dbo->loadAssocList();
11419 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
11420 $cfmap = array();
11421 foreach ($cfields as $cf) {
11422 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
11423 }
11424 $cfmapreplace = array();
11425 $partsreceived = explode("\n", $booking['custdata']);
11426 if (count($partsreceived) > 0) {
11427 foreach ($partsreceived as $pst) {
11428 if (!empty($pst)) {
11429 $tmpdata = explode(":", $pst);
11430 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
11431 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
11432 }
11433 }
11434 }
11435 }
11436 foreach ($cmatches[1] as $cfid) {
11437 if (array_key_exists($cfid, $cfmapreplace)) {
11438 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
11439 } else {
11440 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
11441 }
11442 }
11443 }
11444
11445 /**
11446 * Parse all conditional text rules.
11447 */
11448 self::getConditionalRulesInstance()
11449 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
11450 ->parseTokens($parsed);
11451
11452 /**
11453 * Parse all Door Access Control tags.
11454 *
11455 * @since 1.18.4 (J) - 1.8.4 (WP)
11456 */
11457 VBOFactory::getDoorAccessControl()
11458 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $parsed);
11459
11460 return $parsed;
11461 }
11462
11463 /**
11464 * Returns an array of key-value pairs to be used for building the Guest Details
11465 * in the Check-in process. The keys will be compared to the fields of the table
11466 * #__customers to see if some values already exist. The values can use lang defs
11467 * of both front-end or back-end. To be called as list(fields, attributes).
11468 *
11469 * @param boolean $precheckin true if requested for front-end pre check-in.
11470 * @param string $type optional type of custom pax fields to force.
11471 *
11472 * @return array key-value pairs for showing and collecting guest details.
11473 *
11474 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
11475 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
11476 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
11477 */
11478 public static function getPaxFields($precheckin = false, $type = null)
11479 {
11480 // check the type of pax fields collection data
11481 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
11482
11483 if (!$precheckin) {
11484 // back-end check-in ("registration") key-value pairs
11485
11486 // return the requested pax fields collection list
11487 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
11488
11489 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11490 // requested driver returned a list of fields
11491 return $custom_pax_fields;
11492 }
11493
11494 // in case the driver is unsupported, fallback to "basic" (default) driver
11495 return VBOCheckinPax::getFields('basic');
11496 }
11497
11498 // default front-end key-value pairs for pre check-in
11499 $precheckin_pax_fields = [
11500 [
11501 'first_name' => JText::translate('VBCCFIRSTNAME'),
11502 'last_name' => JText::translate('VBCCLASTNAME'),
11503 'date_birth' => JText::translate('ORDER_DBIRTH'),
11504 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
11505 'country' => JText::translate('ORDER_STATE'),
11506 'city' => JText::translate('ORDER_CITY'),
11507 'zip' => JText::translate('ORDER_ZIP'),
11508 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
11509 'gender' => JText::translate('VBOCUSTGENDER'),
11510 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
11511 'docnum' => JText::translate('VBOCUSTDOCNUM'),
11512 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
11513 ],
11514 [
11515 'first_name' => 'text',
11516 'last_name' => 'text',
11517 'date_birth' => 'calendar',
11518 'place_birth' => 'text',
11519 'country' => 'country',
11520 'city' => 'text',
11521 'zip' => 'text',
11522 'nationality' => 'country',
11523 'gender' => [
11524 'M' => JText::translate('VBOCUSTGENDERM'),
11525 'F' => JText::translate('VBOCUSTGENDERF'),
11526 ],
11527 'doctype' => 'text',
11528 'docnum' => 'text',
11529 'documents' => 'file',
11530 ],
11531 ];
11532
11533 // access the pre-checkin pax fields collection list from the current collector
11534 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
11535
11536 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11537 // set the pax fields for pre-checkin from the given data collection driver
11538 $precheckin_pax_fields = $custom_pax_fields;
11539 }
11540
11541 // make a safe copy of the default precheckin fields
11542 $use_precheckin_pax_fields = $precheckin_pax_fields;
11543
11544 /**
11545 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
11546 *
11547 * @since 1.16.3 (J) - 1.6.3 (WP)
11548 */
11549 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
11550
11551 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
11552 // use the filtered pax fields
11553 $use_precheckin_pax_fields = $precheckin_pax_fields;
11554 }
11555
11556 // return the front-end key-value pairs for pre check-in
11557 return $use_precheckin_pax_fields;
11558 }
11559
11560 /**
11561 * Returns the associative list of countries from the DB.
11562 *
11563 * @param bool $tn whether to translate the country name.
11564 * @param bool $no_id whether to unset the ID of the country.
11565 *
11566 * @return array associative or empty array.
11567 *
11568 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
11569 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
11570 */
11571 public static function getCountriesArray($tn = true, $no_id = true)
11572 {
11573 $dbo = JFactory::getDbo();
11574 $all_countries = [];
11575
11576 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
11577 $dbo->setQuery($q);
11578 $countries = $dbo->loadAssocList();
11579 if (!$countries) {
11580 return [];
11581 }
11582
11583 if ($tn === true) {
11584 $vbo_tn = self::getTranslator();
11585 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
11586 // re-apply sorting by country name
11587 $sorting = [];
11588 foreach ($countries as $country) {
11589 $sorting[$country['country_name']] = $country;
11590 }
11591 ksort($sorting);
11592 $sorted = [];
11593 foreach ($sorting as $country) {
11594 $sorted[] = $country;
11595 }
11596 $countries = $sorted;
11597 unset($sorting, $sorted);
11598 }
11599
11600 foreach ($countries as $v) {
11601 if ($no_id) {
11602 // keep the original structure by unsetting the ID only needed for translation
11603 unset($v['id']);
11604 }
11605 $all_countries[$v['country_3_code']] = $v;
11606 }
11607
11608 return $all_countries;
11609 }
11610
11611 public static function getCountriesSelect(string $name, array $all_countries = [], ?string $current_value = '', ?string $empty_value = ' ', ?string $idAttr = null)
11612 {
11613 if (!$all_countries) {
11614 $all_countries = self::getCountriesArray();
11615 }
11616
11617 $countries = '<select ' . ($idAttr ? 'id="' . $idAttr . '" ' : '') . 'name="' . $name . '">' . "\n";
11618 if (strlen($empty_value)) {
11619 $countries .= '<option value="">' . $empty_value . '</option>' . "\n";
11620 }
11621 foreach ($all_countries as $v) {
11622 $countries .= '<option value="' . $v['country_3_code'] . '"' . ($v['country_3_code'] == $current_value ? ' selected="selected"' : '') . '>' . $v['country_name'] . '</option>' . "\n";
11623 }
11624 $countries .= '</select>';
11625
11626 return $countries;
11627 }
11628
11629 public static function getThumbSize($skipsession = false)
11630 {
11631 static $thumb_size = null;
11632
11633 if ($thumb_size) {
11634 return $thumb_size;
11635 }
11636
11637 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11638
11639 return $thumb_size;
11640 }
11641
11642 /**
11643 * Checks whether an iCal file for the reservation should be
11644 * attached to the confirmation email for customer and/or admin.
11645 *
11646 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11647 *
11648 * @since 1.12.0 (J) - 1.2.0 (WP)
11649 */
11650 public static function attachIcal()
11651 {
11652 return VBOFactory::getConfig()->getInt('attachical', 1);
11653 }
11654
11655 /**
11656 * How the calculation of the orphan dates should take place.
11657 *
11658 * @return string "next" for only checking the bookings ahead, "prevnext" if
11659 * also the previous bookings should be checked.
11660 *
11661 * @since 1.13 (J) - 1.3.0 (WP)
11662 */
11663 public static function orphansCalculation()
11664 {
11665 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11666 }
11667
11668 /**
11669 * Returns the name of the template file to use for the front-end View "search".
11670 *
11671 * @return string the name of the template file. New and upgraded user will both use the classic file.
11672 *
11673 * @since 1.13 (J) - 1.3.0 (WP)
11674 */
11675 public static function searchResultsTmpl()
11676 {
11677 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11678 }
11679
11680 /**
11681 * Returns a string without any new-line characters
11682 * to be used for JavaScript values without facing
11683 * errors like 'unterminated string literal'.
11684 * By passing nl2br($str) as argument, we can keep
11685 * the wanted new-line HTML tags for PRE tags.
11686 * We use implode() with just one argument to
11687 * not use an empty string as glue for the string.
11688 *
11689 * @param $str string
11690 *
11691 * @return string
11692 */
11693 public static function strTrimLiteral($str) {
11694 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11695 $lines = explode("\n", $str);
11696 $new_lines = array();
11697 foreach ($lines as $i => $line) {
11698 if (strlen($line)) {
11699 $new_lines[] = trim($line);
11700 }
11701 }
11702 return implode($new_lines);
11703 }
11704
11705 /**
11706 * Returns an instance of the VboApplication class.
11707 *
11708 * @return VboApplication
11709 */
11710 public static function getVboApplication()
11711 {
11712 // require library
11713 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11714
11715 // return a new instance
11716 return new VboApplication();
11717 }
11718
11719 /**
11720 * Returns the translation of the given 0-indexed week day.
11721 *
11722 * @param int $wd the 0-indexed day of the week.
11723 * @param bool $short whether to get the short version of the weekday.
11724 *
11725 * @return string the name of the week day.
11726 *
11727 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11728 */
11729 public static function sayWeekDay($wd, $short = false)
11730 {
11731 switch ($wd) {
11732 case '6' :
11733 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11734 break;
11735 case '5' :
11736 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11737 break;
11738 case '4' :
11739 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11740 break;
11741 case '3' :
11742 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11743 break;
11744 case '2' :
11745 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11746 break;
11747 case '1' :
11748 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11749 break;
11750 default :
11751 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11752 break;
11753 }
11754 return $ret;
11755 }
11756
11757 /**
11758 * Returns the translation of the given 1-indexed month of the year.
11759 *
11760 * @param int $idm the 1-indexed month of the year.
11761 * @param bool $short whether to get the short version of the month.
11762 *
11763 * @return string the name of the month.
11764 *
11765 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11766 */
11767 public static function sayMonth($idm, $short = false)
11768 {
11769 switch ($idm) {
11770 case '12' :
11771 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11772 break;
11773 case '11' :
11774 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11775 break;
11776 case '10' :
11777 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11778 break;
11779 case '9' :
11780 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11781 break;
11782 case '8' :
11783 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11784 break;
11785 case '7' :
11786 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11787 break;
11788 case '6' :
11789 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11790 break;
11791 case '5' :
11792 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11793 break;
11794 case '4' :
11795 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11796 break;
11797 case '3' :
11798 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11799 break;
11800 case '2' :
11801 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11802 break;
11803 default :
11804 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11805 break;
11806 }
11807 return $ret;
11808 }
11809
11810 public static function sayDayMonth($d) {
11811 switch ($d) {
11812 case '31' :
11813 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11814 break;
11815 case '30' :
11816 $ret = JText::translate('VBDAYMONTHTHIRTY');
11817 break;
11818 case '29' :
11819 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11820 break;
11821 case '28' :
11822 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11823 break;
11824 case '27' :
11825 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11826 break;
11827 case '26' :
11828 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11829 break;
11830 case '25' :
11831 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11832 break;
11833 case '24' :
11834 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11835 break;
11836 case '23' :
11837 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11838 break;
11839 case '22' :
11840 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11841 break;
11842 case '21' :
11843 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11844 break;
11845 case '20' :
11846 $ret = JText::translate('VBDAYMONTHTWENTY');
11847 break;
11848 case '19' :
11849 $ret = JText::translate('VBDAYMONTHNINETEEN');
11850 break;
11851 case '18' :
11852 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11853 break;
11854 case '17' :
11855 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11856 break;
11857 case '16' :
11858 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11859 break;
11860 case '15' :
11861 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11862 break;
11863 case '14' :
11864 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11865 break;
11866 case '13' :
11867 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11868 break;
11869 case '12' :
11870 $ret = JText::translate('VBDAYMONTHTWELVE');
11871 break;
11872 case '11' :
11873 $ret = JText::translate('VBDAYMONTHELEVEN');
11874 break;
11875 case '10' :
11876 $ret = JText::translate('VBDAYMONTHTEN');
11877 break;
11878 case '9' :
11879 $ret = JText::translate('VBDAYMONTHNINE');
11880 break;
11881 case '8' :
11882 $ret = JText::translate('VBDAYMONTHEIGHT');
11883 break;
11884 case '7' :
11885 $ret = JText::translate('VBDAYMONTHSEVEN');
11886 break;
11887 case '6' :
11888 $ret = JText::translate('VBDAYMONTHSIX');
11889 break;
11890 case '5' :
11891 $ret = JText::translate('VBDAYMONTHFIVE');
11892 break;
11893 case '4' :
11894 $ret = JText::translate('VBDAYMONTHFOUR');
11895 break;
11896 case '3' :
11897 $ret = JText::translate('VBDAYMONTHTHREE');
11898 break;
11899 case '2' :
11900 $ret = JText::translate('VBDAYMONTHTWO');
11901 break;
11902 default :
11903 $ret = JText::translate('VBDAYMONTHONE');
11904 break;
11905 }
11906 return $ret;
11907 }
11908
11909 public static function totElements($arr) {
11910 $n = 0;
11911 if (is_array($arr)) {
11912 foreach ($arr as $a) {
11913 if (!empty($a)) {
11914 $n++;
11915 }
11916 }
11917 return $n;
11918 }
11919 return false;
11920 }
11921
11922 /**
11923 * Returns a list of documents that were uploaded
11924 * for the specified customer.
11925 *
11926 * @param integer $id The customer ID.
11927 *
11928 * @return array A list of documents.
11929 *
11930 * @since 1.3.0
11931 */
11932 public static function getCustomerDocuments($id)
11933 {
11934 $dbo = JFactory::getDbo();
11935
11936 $q = $dbo->getQuery(true)
11937 ->select($dbo->qn('docsfolder'))
11938 ->from($dbo->qn('#__vikbooking_customers'))
11939 ->where($dbo->qn('id') . ' = ' . (int) $id);
11940
11941 $dbo->setQuery($q, 0, 1);
11942 $dbo->execute();
11943
11944 if (!$dbo->getNumRows())
11945 {
11946 // customer not found
11947 return array();
11948 }
11949
11950 // retrieve customer documents directory name
11951 $dirname = $dbo->loadResult();
11952
11953 if (empty($dirname))
11954 {
11955 // no available directory
11956 return array();
11957 }
11958
11959 // build documents folder path
11960 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11961
11962 if (!is_dir($dirname))
11963 {
11964 // the customer directory doesn't exist
11965 return array();
11966 }
11967
11968 // read all files from customer directory
11969 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11970
11971 $files = array();
11972
11973 foreach ($glob as $path)
11974 {
11975 // skip "index.html"
11976 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11977 {
11978 // extract name and extension from file path
11979 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11980 {
11981 $name = $match[1];
11982 $ext = $match[2];
11983 }
11984 else
11985 {
11986 $name = basename($path);
11987 $ext = '';
11988 }
11989
11990 $file = new stdClass;
11991 $file->path = $path;
11992 $file->name = $name;
11993 $file->ext = $ext;
11994 $file->basename = $file->name . '.' . $file->ext;
11995 $file->size = filesize($path);
11996 $file->date = filemtime($path);
11997 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11998
11999 $files[] = $file;
12000 }
12001 }
12002
12003 // sort files by creation date
12004 usort($files, function($a, $b)
12005 {
12006 return $b->date - $a->date;
12007 });
12008
12009 return $files;
12010 }
12011
12012 public static function displayPaymentParameters($pfile, $pparams = '') {
12013 $html = '<p>---------</p>';
12014
12015 /**
12016 * @wponly The payment gateway is now loaded
12017 * using the apposite dispatcher.
12018 *
12019 * @since 1.0.5
12020 */
12021 JLoader::import('adapter.payment.dispatcher');
12022
12023 try
12024 {
12025 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
12026 }
12027 catch (Exception $e)
12028 {
12029 // payment not found
12030 $html = $e->getMessage();
12031
12032 if ($code = $e->getCode())
12033 {
12034 $html = '<b>' . $code . '</b> : ' . $html;
12035 }
12036
12037 return $html;
12038 }
12039 //
12040
12041 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
12042 $arrparams = !is_array($arrparams) ? array() : $arrparams;
12043
12044 // get admin parameters
12045 $pconfig = $payment->getAdminParameters();
12046
12047 if (count($pconfig) > 0) {
12048 $html = '';
12049 foreach ($pconfig as $value => $cont) {
12050 if (empty($value)) {
12051 continue;
12052 }
12053 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12054 $label = $labelparts[0];
12055 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12056 if (!empty($cont['help'])) {
12057 $labelhelp = $cont['help'];
12058 }
12059 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12060 $html .= '<div class="vbo-param-container">';
12061 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12062 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12063 }
12064 $html .= '<div class="vbo-param-setting">';
12065 switch ($cont['type']) {
12066 case 'custom':
12067 $html .= $cont['html'];
12068 break;
12069 case 'select':
12070 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12071 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12072 if (isset($cont['multiple']) && $cont['multiple']) {
12073 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
12074 } else {
12075 $html .= '<select name="vikpaymentparams['.$value.']">';
12076 }
12077 foreach ($options as $optkey => $poption) {
12078 $checkval = $is_assoc ? $optkey : $poption;
12079 $selected = false;
12080 if (isset($arrparams[$value])) {
12081 if (is_array($arrparams[$value])) {
12082 $selected = in_array($checkval, $arrparams[$value]);
12083 } else {
12084 $selected = ($checkval == $arrparams[$value]);
12085 }
12086 } elseif (isset($default_paramv)) {
12087 if (is_array($default_paramv)) {
12088 $selected = in_array($checkval, $default_paramv);
12089 } else {
12090 $selected = ($checkval == $default_paramv);
12091 }
12092 }
12093 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12094 }
12095 $html .= '</select>';
12096 break;
12097 case 'password':
12098 $html .= '<div class="btn-wrapper input-append">';
12099 $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"/>';
12100 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12101 $html .= '</div>';
12102 break;
12103 case 'number':
12104 $number_attr = array();
12105 if (isset($cont['min'])) {
12106 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12107 }
12108 if (isset($cont['max'])) {
12109 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12110 }
12111 if (isset($cont['step'])) {
12112 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12113 }
12114 $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) . '/>';
12115 break;
12116 case 'textarea':
12117 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12118 break;
12119 case 'hidden':
12120 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
12121 break;
12122 case 'checkbox':
12123 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12124 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
12125 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
12126 break;
12127 default:
12128 $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"/>';
12129 break;
12130 }
12131 if (strlen($labelhelp) > 0) {
12132 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12133 }
12134 $html .= '</div>';
12135 $html .= '</div>';
12136 }
12137 // JS helper function to toggle the password fields
12138 $html .= "\n" . '<script>' . "\n";
12139 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
12140 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12141 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12142 $html .= ' var inp_type = inp.attr("type");' . "\n";
12143 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12144 $html .= '}' . "\n";
12145 $html .= "\n" . '</script>' . "\n";
12146 }
12147
12148 return $html;
12149 }
12150
12151 public static function displaySMSParameters($pfile, $params = null)
12152 {
12153 $html = '---------';
12154
12155 $params = is_string($params) ? json_decode($params, true) : $params;
12156 $params = is_array($params) ? $params : [];
12157
12158 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
12159 return $html;
12160 }
12161
12162 // attempt to load the class file
12163 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
12164
12165 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
12166 return $html;
12167 }
12168
12169 // load the gateway parameters
12170 $config = VikSmsApi::getAdminParameters();
12171
12172 if (!is_array($config) || !count($config)) {
12173 return $html;
12174 }
12175
12176 // flags for JS helpers
12177 $js_helpers = array();
12178
12179 $html = '';
12180 foreach ($config as $value => $cont) {
12181 if (empty($value)) {
12182 continue;
12183 }
12184 $inp_attr = '';
12185 if (isset($cont['attributes'])) {
12186 foreach ($cont['attributes'] as $inpk => $inpv) {
12187 $inp_attr .= $inpk.'="'.$inpv.'" ';
12188 }
12189 $inp_attr = ' ' . rtrim($inp_attr);
12190 }
12191 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12192 $label = $labelparts[0];
12193 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12194 if (!empty($cont['help'])) {
12195 $labelhelp = $cont['help'];
12196 }
12197 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12198 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
12199 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12200 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12201 }
12202 $html .= '<div class="vbo-param-setting">';
12203 switch ($cont['type']) {
12204 case 'custom':
12205 $html .= $cont['html'];
12206 break;
12207 case 'select':
12208 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12209 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12210 if (isset($cont['multiple']) && $cont['multiple']) {
12211 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
12212 } else {
12213 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
12214 }
12215 foreach ($options as $optkey => $poption) {
12216 $checkval = $is_assoc ? $optkey : $poption;
12217 $selected = false;
12218 if (isset($params[$value])) {
12219 if (is_array($params[$value])) {
12220 $selected = in_array($checkval, $params[$value]);
12221 } else {
12222 $selected = ($checkval == $params[$value]);
12223 }
12224 } elseif (isset($default_paramv)) {
12225 if (is_array($default_paramv)) {
12226 $selected = in_array($checkval, $default_paramv);
12227 } else {
12228 $selected = ($checkval == $default_paramv);
12229 }
12230 }
12231 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12232 }
12233 $html .= '</select>';
12234 break;
12235 case 'password':
12236 $html .= '<div class="btn-wrapper input-append">';
12237 $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 . '/>';
12238 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12239 $html .= '</div>';
12240 // set flag for JS helper
12241 $js_helpers[] = $cont['type'];
12242 break;
12243 case 'number':
12244 $number_attr = array();
12245 if (isset($cont['min'])) {
12246 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12247 }
12248 if (isset($cont['max'])) {
12249 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12250 }
12251 if (isset($cont['step'])) {
12252 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12253 }
12254 $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 . '/>';
12255 break;
12256 case 'textarea':
12257 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12258 break;
12259 case 'visual_html':
12260 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
12261 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
12262 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
12263 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
12264 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
12265 break;
12266 case 'hidden':
12267 $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 . '/>';
12268 break;
12269 case 'checkbox':
12270 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12271 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
12272 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
12273 break;
12274 default:
12275 $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 . '/>';
12276 break;
12277 }
12278 if (strlen($labelhelp) > 0) {
12279 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12280 }
12281 $html .= '</div>';
12282 $html .= '</div>';
12283 }
12284
12285 // JS helper functions
12286 if (in_array('password', $js_helpers)) {
12287 // toggle the password fields
12288 $html .= "\n" . '<script>' . "\n";
12289 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
12290 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12291 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12292 $html .= ' var inp_type = inp.attr("type");' . "\n";
12293 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12294 $html .= '}' . "\n";
12295 $html .= "\n" . '</script>' . "\n";
12296 }
12297
12298 return $html;
12299 }
12300
12301 /**
12302 * Renders the params of the requested cron job file-class.
12303 *
12304 * @param string $file the name of the cron job driver file.
12305 * @param array $params the parameters array.
12306 *
12307 * @return string the necessary HTML content to render.
12308 *
12309 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
12310 */
12311 public static function displayCronParameters($file, $params = [])
12312 {
12313 try
12314 {
12315 // attempt to create a new instance
12316 $job = VBOFactory::getCronFactory()->createInstance($file);
12317 }
12318 catch (Exception $e)
12319 {
12320 // something went wrong, display error message
12321 return '<p>' . $e->getMessage() . '<p>';
12322 }
12323
12324 // get admin parameters
12325 $config = $job->getForm();
12326
12327 if (!is_array($config) || !$config) {
12328 return '<p>---------</p>';
12329 }
12330
12331 /**
12332 * Render the cron parameters through the global params rendering class.
12333 *
12334 * @since 1.16.9 (J) - 1.6.9 (WP)
12335 */
12336 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
12337 }
12338
12339 public static function invokeChannelManager($skiporder = true, $order = array()) {
12340 $task = VikRequest::getString('task', '', 'request');
12341 $view = VikRequest::getString('view', '', 'request');
12342 $tmpl = VikRequest::getString('tmpl', '', 'request');
12343 $noimpression = array('vieworder', 'booking');
12344 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')) {
12345 //VCM Channel Impression
12346 if (!class_exists('VikChannelManagerConfig')) {
12347 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12348 }
12349 if (!class_exists('VikChannelManager')) {
12350 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12351 }
12352 VikChannelManager::invokeChannelImpression();
12353 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
12354 //VCM Channel Conversion-Impression
12355 if (!class_exists('VikChannelManagerConfig')) {
12356 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12357 }
12358 if (!class_exists('VikChannelManager')) {
12359 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12360 }
12361 VikChannelManager::invokeChannelConversionImpression($order);
12362 }
12363 }
12364
12365 public static function validEmail($email) {
12366 $isValid = true;
12367 $atIndex = strrpos($email, "@");
12368 if (is_bool($atIndex) && !$atIndex) {
12369 $isValid = false;
12370 } else {
12371 $domain = substr($email, $atIndex +1);
12372 $local = substr($email, 0, $atIndex);
12373 $localLen = strlen($local);
12374 $domainLen = strlen($domain);
12375 if ($localLen < 1 || $localLen > 64) {
12376 // local part length exceeded
12377 $isValid = false;
12378 } else
12379 if ($domainLen < 1 || $domainLen > 255) {
12380 // domain part length exceeded
12381 $isValid = false;
12382 } else
12383 if ($local[0] == '.' || $local[$localLen -1] == '.') {
12384 // local part starts or ends with '.'
12385 $isValid = false;
12386 } else
12387 if (preg_match('/\\.\\./', $local)) {
12388 // local part has two consecutive dots
12389 $isValid = false;
12390 } else
12391 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
12392 // character not valid in domain part
12393 $isValid = false;
12394 } else
12395 if (preg_match('/\\.\\./', $domain)) {
12396 // domain part has two consecutive dots
12397 $isValid = false;
12398 } else
12399 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
12400 // character not valid in local part unless
12401 // local part is quoted
12402 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
12403 $isValid = false;
12404 }
12405 }
12406 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
12407 // domain not found in DNS
12408 $isValid = false;
12409 }
12410 }
12411 return $isValid;
12412 }
12413
12414 public static function caniWrite($path) {
12415 if ($path[strlen($path) - 1] == '/') {
12416 // ricorsivo return a temporary file path
12417 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
12418 }
12419 if (is_dir($path)) {
12420 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
12421 }
12422 // check tmp file for read/write capabilities
12423 $rm = file_exists($path);
12424 $f = @fopen($path, 'a');
12425 if ($f === false) {
12426 return false;
12427 }
12428 fclose($f);
12429 if (!$rm) {
12430 unlink($path);
12431 }
12432 return true;
12433 }
12434
12435 /**
12436 * Alias method of JFile::upload to unify any
12437 * upload function into one.
12438 *
12439 * @param string $src The name of the php (temporary) uploaded file.
12440 * @param string $dest The path (including filename) to move the uploaded file to.
12441 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
12442 *
12443 * @return boolean True on success.
12444 *
12445 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
12446 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
12447 */
12448 public static function uploadFile($src, $dest, $copy_only = false) {
12449 // always attempt to include the File class
12450 jimport('joomla.filesystem.file');
12451
12452 // upload the file
12453 if (!$copy_only) {
12454 $result = JFile::upload($src, $dest);
12455 } else {
12456 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
12457 $result = JFile::copy($src, $dest);
12458 }
12459
12460 /**
12461 * @wponly in order to not lose uploaded files after installing an update,
12462 * we need to move any uploaded file onto a recovery folder.
12463 */
12464 if ($result) {
12465 VikBookingLoader::import('update.manager');
12466 VikBookingUpdateManager::triggerUploadBackup($dest);
12467 }
12468 //
12469
12470 // return upload result
12471 return $result;
12472 }
12473
12474 /**
12475 * Helper method used to upload the given file (retrieved from $_FILES)
12476 * into the specified destination.
12477 *
12478 * @param array $file An associative array with the file details.
12479 * @param string $dest The destination path.
12480 * @param string $filters A string (or a regex) containing the allowed extensions.
12481 *
12482 * @return array The uploading result.
12483 *
12484 * @throws RuntimeException
12485 *
12486 * @since 1.3.0
12487 */
12488 public static function uploadFileFromRequest($file, $dest, $filters = '*')
12489 {
12490 jimport('joomla.filesystem.file');
12491
12492 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
12493
12494 if (empty($file['name']))
12495 {
12496 throw new RuntimeException('Missing file', 400);
12497 }
12498
12499 /**
12500 * Make sure the upload of the file didn't raise any errors.
12501 *
12502 * @link https://www.php.net/manual/en/features.file-upload.errors.php
12503 */
12504 if ((int) $file['error'])
12505 {
12506 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
12507 {
12508 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
12509 }
12510 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
12511 {
12512 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
12513 }
12514 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
12515 {
12516 $error = 'The uploaded file was only partially uploaded.';
12517 }
12518 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
12519 {
12520 $error = 'No file was uploaded.';
12521 }
12522 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
12523 {
12524 $error = 'Missing a temporary folder.';
12525 }
12526 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
12527 {
12528 $error = 'Failed to write file to disk.';
12529 }
12530 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
12531 {
12532 $error = 'A PHP extension stopped the file upload.';
12533 }
12534 else
12535 {
12536 $error = 'Unknown error.';
12537 }
12538
12539 throw new RuntimeException($error, 500);
12540 }
12541
12542 $src = $file['tmp_name'];
12543
12544 // extract file name and extension
12545 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
12546 {
12547 $filename = $match[1];
12548 $fileext = $match[2];
12549 }
12550 else
12551 {
12552 // probably no extension provided
12553 $filename = basename($file['name']);
12554 $fileext = '';
12555 }
12556
12557 $j = '';
12558
12559 if (file_exists($dest . $filename . $fileext))
12560 {
12561 $j = 2;
12562
12563 while (file_exists($dest . $filename . '-' . $j . $fileext))
12564 {
12565 $j++;
12566 }
12567
12568 $j = '-' . $j;
12569 }
12570
12571 $finaldest = $dest . $filename . $j . $fileext;
12572
12573 // make sure the file extension is supported
12574 if (!self::isFileTypeCompatible(basename($finaldest), $filters))
12575 {
12576 // extension not supported
12577 throw new RuntimeException(sprintf('Extension [%s] is not supported', $fileext), 400);
12578 }
12579
12580 // try to upload the file
12581 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
12582 {
12583 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
12584 }
12585
12586 $file = new stdClass;
12587 $file->name = $filename . $j;
12588 $file->ext = ltrim($fileext, '.');
12589 $file->filename = basename($finaldest);
12590 $file->path = $finaldest;
12591
12592 return $file;
12593 }
12594
12595 /**
12596 * Helper method used to check whether the given file name
12597 * supports one of the given filters.
12598 *
12599 * @param mixed $file Either the file name or the uploaded file.
12600 * @param string $filters Either a regex or a comma-separated list of supported extensions.
12601 * The regex must be inclusive of delimiters.
12602 *
12603 * @return bool True if supported, false otherwise.
12604 *
12605 * @since 1.7.3 (WP) - 1.17.3 (J)
12606 */
12607 public static function isFileTypeCompatible($file, $filters)
12608 {
12609 // make sure the filters query is not empty
12610 if (strlen($filters) == 0)
12611 {
12612 // cannot assert whether the file could be accepted or not
12613 return false;
12614 }
12615
12616 // check whether all the files are accepted
12617 if ($filters == '*')
12618 {
12619 return true;
12620 }
12621
12622 // use the file MIME TYPE in case of array
12623 if (is_array($file))
12624 {
12625 $file = $file['type'];
12626 }
12627
12628 // check if we are dealing with a regex
12629 if (static::isRegex($filters))
12630 {
12631 return (bool) preg_match($filters, $file);
12632 }
12633
12634 // fallback to comma-separated list
12635 $types = array_filter(preg_split("/\s*,\s*/", $filters));
12636
12637 foreach ($types as $t)
12638 {
12639 // remove initial dot if specified
12640 $t = ltrim($t, '.');
12641 // escape slashes to avoid breaking the regex
12642 $t = preg_replace("/\//", '\/', $t);
12643
12644 // check if the file ends with the given extension
12645 if (preg_match("/{$t}$/i", $file))
12646 {
12647 return true;
12648 }
12649 }
12650
12651 return false;
12652 }
12653
12654 /**
12655 * Checks whether the given string is a structured PCRE regex.
12656 * It simply makes sure that the string owns valid delimiters.
12657 * A delimiter can be any non-alphanumeric, non-backslash,
12658 * non-whitespace character.
12659 *
12660 * @param string $str The string to check.
12661 *
12662 * @return boolean True if a regex, false otherwise.
12663 *
12664 * @since 1.7.3 (WP) - 1.17.3 (J)
12665 */
12666 public static function isRegex($str)
12667 {
12668 // first of all make sure the first character is a supported delimiter
12669 if (!preg_match("/^([!#$%&'*+,.\/:;=?@^_`|~\-(\[{<\"])/", $str, $match))
12670 {
12671 // no valid delimiter
12672 return false;
12673 }
12674
12675 // get delimiter
12676 $d = $match[1];
12677
12678 // lookup used to check if we should take a different ending delimiter
12679 $lookup = array(
12680 '{' => '}',
12681 '[' => ']',
12682 '(' => ')',
12683 '<' => '>',
12684 );
12685
12686 if (isset($lookup[$d]))
12687 {
12688 $d = $lookup[$d];
12689 }
12690
12691 // make sure the regex ends with the delimiter found
12692 return (bool) preg_match("/\\{$d}[gimsxU]*$/", $str);
12693 }
12694
12695 /**
12696 * Gets the instance of a specific report class.
12697 *
12698 * @param string $report the name of the report to load.
12699 *
12700 * @return mixed false or report object instance.
12701 *
12702 * @since 1.3.0
12703 */
12704 public static function getReportInstance($report)
12705 {
12706 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12707
12708 return VikBookingReport::getInstanceOf($report);
12709 }
12710
12711 /**
12712 * Checks whether the guest reviews are enabled and VCM is installed.
12713 *
12714 * @return boolean true if enabled, false otherwise.
12715 *
12716 * @since 1.3.0
12717 */
12718 public static function allowGuestReviews()
12719 {
12720 $dbo = JFactory::getDbo();
12721 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12722 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12723 $dbo->setQuery($q);
12724 $dbo->execute();
12725 if ($dbo->getNumRows()) {
12726 $s = $dbo->loadResult();
12727 return ((int)$s === 1 && $vcm_installed);
12728 }
12729 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12730 $dbo->setQuery($q);
12731 $dbo->execute();
12732 return $vcm_installed;
12733 }
12734
12735 /**
12736 * Gets the minimum chars for the guest review message.
12737 *
12738 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12739 *
12740 * @since 1.3.0
12741 */
12742 public static function guestReviewMinChars()
12743 {
12744 $dbo = JFactory::getDbo();
12745 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12746 $dbo->setQuery($q);
12747 $dbo->execute();
12748 if ($dbo->getNumRows() > 0) {
12749 return (int)$dbo->loadResult();
12750 }
12751 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12752 $dbo->setQuery($q);
12753 $dbo->execute();
12754 return 15;
12755 }
12756
12757 /**
12758 * The approval type for new guest reviews.
12759 *
12760 * @return string auto or manual.
12761 *
12762 * @since 1.3.0
12763 */
12764 public static function guestReviewsApproval()
12765 {
12766 $dbo = JFactory::getDbo();
12767 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12768 $dbo->setQuery($q);
12769 $dbo->execute();
12770 if ($dbo->getNumRows() > 0) {
12771 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12772 }
12773 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12774 $dbo->setQuery($q);
12775 $dbo->execute();
12776 return 'auto';
12777 }
12778
12779 /**
12780 * The type of reviews guests should leave.
12781 *
12782 * @return string global or service.
12783 *
12784 * @since 1.3.0
12785 */
12786 public static function guestReviewsType()
12787 {
12788 $dbo = JFactory::getDbo();
12789 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12790 $dbo->setQuery($q);
12791 $dbo->execute();
12792 if ($dbo->getNumRows() > 0) {
12793 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12794 }
12795 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12796 $dbo->setQuery($q);
12797 $dbo->execute();
12798 return 'service';
12799 }
12800
12801 /**
12802 * The services to be reviewed by the guests.
12803 *
12804 * @return array list, empty or not, of the services to be reviewed.
12805 *
12806 * @since 1.3.0
12807 */
12808 public static function guestReviewsServices()
12809 {
12810 $dbo = JFactory::getDbo();
12811 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12812 $dbo->setQuery($q);
12813 $dbo->execute();
12814 if ($dbo->getNumRows()) {
12815 return $dbo->loadAssocList();
12816 }
12817 // insert the default services
12818 $default_services = array(
12819 JText::translate('VBOGREVVALUE'),
12820 JText::translate('VBOGREVLOCATION'),
12821 JText::translate('VBOGREVSTAFF'),
12822 JText::translate('VBOGREVCLEAN'),
12823 JText::translate('VBOGREVCOMFORT'),
12824 JText::translate('VBOGREVFACILITIES'),
12825 );
12826 foreach ($default_services as $def_service) {
12827 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12828 $dbo->setQuery($q);
12829 $dbo->execute();
12830 }
12831 return array();
12832 }
12833
12834 /**
12835 * Checks whether the guest reviews should be downloaded (max 1 per day).
12836 * VCM must be installed in order to download the new reviews.
12837 *
12838 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12839 *
12840 * @since 1.3.0
12841 */
12842 public static function shouldDownloadReviews()
12843 {
12844 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12845 return -1;
12846 }
12847
12848 $today = date('Y-m-d');
12849
12850 $dbo = JFactory::getDbo();
12851 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12852 $dbo->setQuery($q);
12853 $dbo->execute();
12854 if ($dbo->getNumRows()) {
12855 $last_download = $dbo->loadResult();
12856 // update last download day
12857 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12858 $dbo->setQuery($q);
12859 $dbo->execute();
12860 //
12861 return ($last_download != $today ? 1 : 0);
12862 }
12863 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12864 $dbo->setQuery($q);
12865 $dbo->execute();
12866 return 1;
12867 }
12868
12869 /**
12870 * Tells whether a booking can be reviewed.
12871 *
12872 * @param array $booking booking array details.
12873 *
12874 * @return boolean true if a review can be left, false otherwise.
12875 *
12876 * @since 1.3.0
12877 */
12878 public static function canBookingBeReviewed($booking)
12879 {
12880 // reviews must be enabled and supported
12881 if (!self::allowGuestReviews()) {
12882 return false;
12883 }
12884
12885 // booking status must be confirmed
12886 if ($booking['status'] != 'confirmed') {
12887 return false;
12888 }
12889
12890 // review can be left starting from the check-out day
12891 $checkout_info = getdate($booking['checkout']);
12892 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12893 if (time() < $checkout_midn) {
12894 return false;
12895 }
12896
12897 // make sure a review for this booking does not exist
12898 $noreview = true;
12899 $dbo = JFactory::getDbo();
12900 try {
12901 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12902 $dbo->setQuery($q);
12903 $dbo->execute();
12904 $noreview = ($dbo->getNumRows() < 1);
12905 } catch (Exception $e) {
12906 // if the query fails, we do not allow the review to be left
12907 $noreview = false;
12908 }
12909
12910 return $noreview;
12911 }
12912
12913 /**
12914 * Gets the review for a booking, no matter if it's published or not.
12915 * Review will be returned only if reviews are enabled and supported.
12916 * Only reviews left through the website, no OTA reviews as this method
12917 * should be used in the front-end to display the review to the guest.
12918 *
12919 * @param array $booking booking array details.
12920 *
12921 * @return mixed associative array or false.
12922 *
12923 * @since 1.3.0
12924 */
12925 public static function getBookingReview($booking)
12926 {
12927 // reviews must be enabled and supported
12928 if (!self::allowGuestReviews()) {
12929 return false;
12930 }
12931
12932 if (empty($booking['id'])) {
12933 return false;
12934 }
12935
12936 // make sure a review for this booking exists
12937 $dbo = JFactory::getDbo();
12938 $review = array();
12939 try {
12940 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12941 $dbo->setQuery($q);
12942 $dbo->execute();
12943 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12944 } catch (Exception $e) {
12945 // query has failed
12946 $review = array();
12947 }
12948
12949 if (count($review)) {
12950 // make sure the review was left from the website
12951 $review['uniquekey'] = (int)$review['uniquekey'];
12952 }
12953
12954 if (count($review) && !empty($review['uniquekey'])) {
12955 // this is an OTA review
12956 return false;
12957 }
12958
12959 if (count($review) && !empty($review['content'])) {
12960 // decode content
12961 $review['content'] = json_decode($review['content'], true);
12962 }
12963
12964 return count($review) ? $review : false;
12965 }
12966
12967 /**
12968 * Attempts to find a translation for a raw customer data label.
12969 * Bookings downloaded from the OTAs will save a raw string of information
12970 * composed of pairs of label-value separated by new line feeds.
12971 * We try to translate the given label into the current language.
12972 *
12973 * @param string $label the raw string label to translate.
12974 *
12975 * @return string either the original or the translated label.
12976 *
12977 * @since 1.3.5
12978 * @since 1.4.0 back-end support added.
12979 */
12980 public static function tnCustomerRawDataLabel($label)
12981 {
12982 // this is a map of the known labels
12983 $known_lbls = array(
12984 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12985 'LASTNAME' => 'ORDER_LNAME',
12986 'COUNTRY' => 'ORDER_STATE',
12987 'EMAIL' => 'VBMAIL',
12988 'TELEPHONE' => 'VBPHONE',
12989 'PHONE' => 'VBPHONE',
12990 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12991 'MEAL_PLAN' => 'VBOMEALPLAN',
12992 'CITY' => 'VBCITY',
12993 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12994 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12995 );
12996
12997 if (self::isAdmin()) {
12998 // override the map of known translation strings
12999 $known_lbls = array(
13000 'NAME' => 'ORDER_NAME',
13001 'LASTNAME' => 'ORDER_LNAME',
13002 'COUNTRY' => 'ORDER_STATE',
13003 'ADDRESS' => 'ORDER_ADDRESS',
13004 'CITY' => 'ORDER_CITY',
13005 'LOCATION' => 'ORDER_CITY',
13006 'EMAIL' => 'ORDER_EMAIL',
13007 'TELEPHONE' => 'ORDER_PHONE',
13008 'PHONE' => 'ORDER_PHONE',
13009 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13010 );
13011 }
13012
13013 // we get rid of any empty space by keeping the underscores
13014 $converted = str_replace(' ', '', strtoupper($label));
13015
13016 if (isset($known_lbls[$converted])) {
13017 // this language definition has been mapped
13018 return JText::translate($known_lbls[$converted]);
13019 }
13020
13021 // we try to guess the translation string by prepending VBO
13022 $guessed = JText::translate('VBO' . $converted);
13023 if ($guessed != 'VBO' . $converted) {
13024 // the label was translated correctly
13025 return $guessed;
13026 }
13027
13028 // this label could not be translated, so we return the plain string
13029 return $label;
13030 }
13031
13032 /**
13033 * We check whether some bookings are available for import from third party plugins.
13034 *
13035 * @return mixed array with list of plugins supported, false otherwise.
13036 *
13037 * @wponly this method is only useful for WordPress.
13038 *
13039 * @since 1.3.5
13040 */
13041 public static function canImportBookingsFromThirdPartyPlugins()
13042 {
13043 $dbo = JFactory::getDbo();
13044
13045 $plugins = array();
13046
13047 /**
13048 * As requested from hundreds of our clients, for the moment we check only the custom
13049 * post types of type "mphb_booking" to see if some bookings are available for import.
13050 */
13051 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
13052 $dbo->setQuery($q);
13053 $dbo->execute();
13054
13055 if (!$dbo->getNumRows()) {
13056 return false;
13057 }
13058
13059 // push this third party plugin
13060 $plugins['mphb'] = 'MotoPress Hotel Booking';
13061
13062 return count($plugins) ? $plugins : false;
13063 }
13064
13065 /**
13066 * This method returns a list of the known languages sorted by the
13067 * administrator custom preferences. Useful for the phone input fields.
13068 *
13069 * @param boolean $code_assoc whether to get an associative array with the lang name.
13070 *
13071 * @return array the sorted list of preferred countries.
13072 *
13073 * @since 1.3.11
13074 */
13075 public static function preferredCountriesOrdering($code_assoc = false)
13076 {
13077 $preferred_countries = array();
13078
13079 // try to get the preferred countries from db
13080 $dbo = JFactory::getDbo();
13081 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
13082 $dbo->setQuery($q);
13083 $setting = $dbo->loadResult();
13084 if (!$setting) {
13085 // create empty configuration record
13086 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
13087 $dbo->setQuery($q);
13088 $dbo->execute();
13089 } else {
13090 $preferred_countries = json_decode($setting);
13091 }
13092
13093 // get the default known languages
13094 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
13095
13096 if (!is_array($preferred_countries) || !count($preferred_countries)) {
13097 // sort the default known languages by country code alphabetically
13098 ksort($sorted_known_langs);
13099 foreach ($sorted_known_langs as $k => $v) {
13100 $langsep = strpos($k, '_') !== false ? '_' : '-';
13101 $langparts = explode($langsep, $k);
13102 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
13103 }
13104 // update the database record
13105 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
13106 $dbo->setQuery($q);
13107 $dbo->execute();
13108 }
13109
13110 if ($code_assoc) {
13111 // this is useful for displaying the preferred countries codes together with the language name
13112 $map = array();
13113 foreach ($preferred_countries as $ccode) {
13114 // look for the current country code in the keys of the known language tags
13115 $match_found = false;
13116 foreach ($sorted_known_langs as $langtag => $langinfo) {
13117 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
13118 $langparts = explode($langsep, $langtag);
13119 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
13120 // match found
13121 $match_found = true;
13122 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13123 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
13124 // match found
13125 $match_found = true;
13126 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13127 }
13128 }
13129 if (!$match_found) {
13130 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
13131 $map[$ccode] = strtoupper($ccode);
13132 }
13133 }
13134 if (count($map)) {
13135 // set the associatve array to be returned
13136 $preferred_countries = $map;
13137 }
13138 }
13139
13140 return $preferred_countries;
13141 }
13142
13143 /**
13144 * Gets the instance of the admin widgets helper class.
13145 *
13146 * @return VikBookingHelperAdminWidgets
13147 *
13148 * @since 1.4.0
13149 */
13150 public static function getAdminWidgetsInstance()
13151 {
13152 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
13153
13154 return VikBookingHelperAdminWidgets::getInstance();
13155 }
13156
13157 /**
13158 * Gets the instance of the conditional rules helper class.
13159 *
13160 * @param bool $require_only whether to return the object.
13161 *
13162 * @return mixed VikBookingHelperConditionalRules or true.
13163 *
13164 * @since 1.4.0
13165 */
13166 public static function getConditionalRulesInstance($require_only = false)
13167 {
13168 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
13169
13170 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
13171 }
13172
13173 /**
13174 * Gets the instance of the geocoding helper class.
13175 *
13176 * @param bool $require_only whether to return the object.
13177 *
13178 * @return mixed VikBookingHelperGeocoding or true.
13179 *
13180 * @since 1.4.0
13181 */
13182 public static function getGeocodingInstance($require_only = false)
13183 {
13184 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
13185
13186 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
13187 }
13188
13189 /**
13190 * Helper method to cope with the removal of the same method
13191 * in the JApplication class introduced with Joomla 4. Using
13192 * isClient() would break the compatibility with J < 3.7 so
13193 * we can rely on this helper method to avoid Fatal Errors.
13194 *
13195 * @return boolean
13196 *
13197 * @since October 2020
13198 */
13199 public static function isAdmin()
13200 {
13201 $app = JFactory::getApplication();
13202 if (method_exists($app, 'isClient')) {
13203 return $app->isClient('administrator');
13204 }
13205
13206 return $app->isAdmin();
13207 }
13208
13209 /**
13210 * Helper method to cope with the removal of the same method
13211 * in the JApplication class introduced with Joomla 4. Using
13212 * isClient() would break the compatibility with J < 3.7 so
13213 * we can rely on this helper method to avoid Fatal Errors.
13214 *
13215 * @return boolean
13216 *
13217 * @since October 2020
13218 */
13219 public static function isSite()
13220 {
13221 $app = JFactory::getApplication();
13222 if (method_exists($app, 'isClient')) {
13223 return $app->isClient('site');
13224 }
13225
13226 return $app->isSite();
13227 }
13228
13229 /**
13230 * Gets the Google Maps API Key.
13231 *
13232 * @return string
13233 *
13234 * @since 1.14 (J) - 1.4.0 (WP)
13235 */
13236 public static function getGoogleMapsKey()
13237 {
13238 return VBOFactory::getConfig()->getString('gmapskey', '');
13239 }
13240
13241 /**
13242 * Checks whether the interactive map booking is enabled.
13243 *
13244 * @return bool
13245 *
13246 * @since 1.14 (J) - 1.4.0 (WP)
13247 */
13248 public static function interactiveMapEnabled()
13249 {
13250 return VBOFactory::getConfig()->getBool('interactive_map', false);
13251 }
13252
13253 /**
13254 * Gets the preferred colors saved in the configuration, if any.
13255 *
13256 * @return array
13257 *
13258 * @since 1.14 (J) - 1.4.0 (WP)
13259 */
13260 public static function getPreferredColors()
13261 {
13262 $config = VBOFactory::getConfig();
13263
13264 // default colors
13265 $pref_colors = [
13266 'textcolor' => '#2282bd',
13267 'bgcolor' => '#2282bd',
13268 'fontcolor' => '#fff',
13269 'bgcolorhov' => '#1890ce',
13270 'fontcolorhov' => '#fff',
13271 ];
13272
13273 // saved colors
13274 $colors = $config->getArray('pref_colors', []);
13275
13276 if (!array_filter($colors)) {
13277 // unset the list to fallback onto the default ones
13278 $colors = [];
13279 }
13280
13281 return $colors ?: $pref_colors;
13282 }
13283
13284 /**
13285 * Adds to the document inline styles for the preferred colors, if any.
13286 *
13287 * @return void
13288 *
13289 * @since 1.14 (J) - 1.4.0 (WP)
13290 */
13291 public static function loadPreferredColorStyles()
13292 {
13293 $pref_colors = self::getPreferredColors();
13294 $css_classes = [];
13295
13296 /**
13297 * Register all preferred colors as CSS variables.
13298 *
13299 * @since 1.18.8 (J) - 1.8.8 (WP)
13300 */
13301 $css_vars = array_filter(array_combine(array_keys($pref_colors), array_values($pref_colors)));
13302 if ($css_vars) {
13303 $css_vars_decl = array_map(function($k, $v) {
13304 return sprintf('--vbo-pref-%s: %s;', preg_replace('/[^a-z0-9]/', '', strtolower($k)), $v);
13305 }, array_keys($css_vars), array_values($css_vars));
13306 $css_classes[] = ':root {' . "\n" . implode("\n", $css_vars_decl) . "\n" . '}';
13307 }
13308
13309 // add in-line style declaration
13310 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
13311 }
13312
13313 /**
13314 * Given the full endpoint URL for the AJAX request, it
13315 * returns an appropriate URI for the current platform.
13316 *
13317 * @param mixed $query The query string or a routed URL.
13318 * @param boolean $xhtml Replace & by &amp; for XML compliance.
13319 *
13320 * @return string The AJAX end-point URI.
13321 *
13322 * @since 1.14 (J) - 1.4.0 (WP)
13323 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
13324 */
13325 public static function ajaxUrl($query = '', $xhtml = false)
13326 {
13327 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
13328 }
13329
13330 /**
13331 * Tells whether no tax rates have been defined so far.
13332 *
13333 * @return bool True if no tax rates defined, false otherwise.
13334 *
13335 * @since 1.14 (J) - 1.4.0 (WP)
13336 */
13337 public static function noTaxRates()
13338 {
13339 $dbo = JFactory::getDbo();
13340
13341 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
13342 $dbo->setQuery($q);
13343 $dbo->execute();
13344
13345 return ($dbo->getNumRows() < 1);
13346 }
13347
13348 /**
13349 * Booking Type feature is strictly connected to VCM and OTAs. Updated
13350 * versions of VBO will always support it as long as VCM is installed.
13351 * Needed by VCM to understand whether certain SQL queries can be performed.
13352 *
13353 * @return bool true if VCM exists, false otherwise.
13354 *
13355 * @since 1.14 (J) - 1.4.0 (WP)
13356 */
13357 public static function isBookingTypeSupported()
13358 {
13359 static $isBookingTypeSupported = null;
13360
13361 if (is_bool($isBookingTypeSupported)) {
13362 return $isBookingTypeSupported;
13363 }
13364
13365 $isBookingTypeSupported = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
13366
13367 return $isBookingTypeSupported;
13368 }
13369
13370 /**
13371 * Returns an instance of the Rates Flow helper class of VCM, if available.
13372 *
13373 * @param bool $anew True to request a new object instance.
13374 *
13375 * @return VCMRatesFlow|null
13376 *
13377 * @since 1.15.0 (J) - 1.5.0 (WP)
13378 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
13379 */
13380 public static function getRatesFlowInstance($anew = false)
13381 {
13382 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
13383 if (!is_file($vcm_fpath)) {
13384 return null;
13385 }
13386
13387 // load dependencies
13388 require_once $vcm_fpath;
13389
13390 // return the instance of the class
13391 return VCMRatesFlow::getInstance($anew);
13392 }
13393
13394 /**
13395 * Returns the current appearance preference.
13396 *
13397 * @return string light, auto or dark.
13398 *
13399 * @since 1.15.0 (J) - 1.5.0 (WP)
13400 */
13401 public static function getAppearancePref()
13402 {
13403 static $preference = null;
13404
13405 if ($preference !== null) {
13406 return $preference;
13407 }
13408
13409 // auto is the default appearance preference
13410 $default_pref = 'auto';
13411
13412 // accepted preferences
13413 $valid_pref = [
13414 'auto',
13415 'light',
13416 'dark',
13417 ];
13418
13419 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
13420 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
13421
13422 return $preference;
13423 }
13424
13425 /**
13426 * According to the appearance preferences, the apposite
13427 * CSS assets are loaded within the document.
13428 *
13429 * @param bool $get_info true to not load any assets.
13430 *
13431 * @return mixed string light, auto, dark, info array or false.
13432 *
13433 * @since 1.15.0 (J) - 1.5.0 (WP)
13434 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
13435 */
13436 public static function loadAppearancePreferenceAssets($get_info = false)
13437 {
13438 // get document object
13439 $document = JFactory::getDocument();
13440
13441 // load current color scheme preference
13442 $current_pref = self::getAppearancePref();
13443
13444 // define caching values
13445 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
13446
13447 // define file attributes
13448 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
13449
13450 // apposite file path and URI for back-end
13451 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
13452 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
13453
13454 // check if this is for the front-end ("auto" file only)
13455 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
13456 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
13457 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
13458 // check if front-end appearance is disabled
13459 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
13460 // do nothing for the front-end by default
13461 return false;
13462 }
13463 }
13464
13465 if (VBOPlatformDetection::isJoomla()) {
13466 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
13467 $css_uri = str_replace('resources/', '', $css_uri);
13468 }
13469
13470 if (!is_file($css_path)) {
13471 // this preference does not require a specific stylesheet
13472 return false;
13473 }
13474
13475 if ($get_info) {
13476 // do not actually load any assets
13477 return [
13478 'id' => $file_attr['id'],
13479 'href' => $css_uri,
13480 ];
13481 }
13482
13483 // load the apposite CSS file
13484 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
13485
13486 return $current_pref;
13487 }
13488
13489 /**
13490 * VBO trigger for VCM subscription status and updates. If called within the admin section
13491 * returns the expiration reminder array to display a modal window, and sets a warning
13492 * message. If called within the front-end, may trigger an email alert.
13493 *
13494 * @return mixed null, array or boolean depending on client position.
13495 *
13496 * @since 1.15.0 (J) - 1.5.0 (WP)
13497 */
13498 public static function getVCMSubscriptionStatus()
13499 {
13500 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
13501 if (!is_file($vcm_lib_path)) {
13502 // do nothing
13503 return null;
13504 }
13505
13506 // determine client position
13507 $is_admin = self::isAdmin();
13508
13509 try {
13510 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
13511 // VCM is outdated
13512 return null;
13513 }
13514
13515 // always attempt to load the admin lang handler of VCM
13516 $lang = JFactory::getLanguage();
13517
13518 // load the VCM admin language file
13519 $vcm_admin_lang_path = '';
13520 if (VBOPlatformDetection::isJoomla()) {
13521 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
13522 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
13523 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
13524 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
13525 /**
13526 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
13527 */
13528 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
13529 }
13530 // load translation file
13531 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
13532 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
13533 /**
13534 * @wponly load language admin handler as well for WP.
13535 * We do this only because of WordPress, but in a way also compatible with Joomla as
13536 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
13537 */
13538 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
13539 }
13540
13541 if ($is_admin) {
13542 // back-end section of the site: check status
13543 if (VikChannelManager::loadExpirationDetails() === false) {
13544 // download and update the expiration details
13545 VikChannelManager::downloadExpirationDetails();
13546 }
13547 $expiration_reminder = null;
13548 if (VikChannelManager::isSubscriptionExpired()) {
13549 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
13550 } else {
13551 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
13552 // we also check if an update is available
13553 if (VikChannelManager::checkUpdates()) {
13554 // an update is more important
13555 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
13556 exit;
13557 }
13558 }
13559 // return reminder status
13560 return $expiration_reminder;
13561 }
13562
13563 // front-end section of the site: trigger alert, if any (bool)
13564 return VikChannelManager::checkSubscriptionReminder();
13565
13566 } catch (Exception $e) {
13567 // do nothing
13568 }
13569
13570 return null;
13571 }
13572
13573 /**
13574 * Given a list of website rate plans, returns the sorted list.
13575 * Naming score is the highest sorting method, pricing scoring is also supported.
13576 *
13577 * @param array $rate_plans the list of rate plan arrays to sort.
13578 * @param bool $assoc whether to keep the original array keys.
13579 *
13580 * @return array the sorted list of rate plan arrays.
13581 *
13582 * @since 1.15.0 (J) - 1.5.0 (WP)
13583 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
13584 */
13585 public static function sortRatePlans($rate_plans, $assoc = false)
13586 {
13587 if (!is_array($rate_plans) || count($rate_plans) < 2) {
13588 return $rate_plans;
13589 }
13590
13591 foreach ($rate_plans as $rp_ind => $rate_plan) {
13592 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
13593 // do not proceed in case of a weird array structure
13594 return $rate_plans;
13595 }
13596
13597 // count the sorting-score of this rate plan
13598 $sort_score = 0;
13599 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
13600 // "standard rate" rate plans should go first
13601 $sort_score += 3;
13602 } else {
13603 $sort_score--;
13604 }
13605
13606 // check for derived rate plan
13607 if (!empty($rate_plan['derived_id'])) {
13608 // derived rates should go last
13609 $sort_score -= 2;
13610 } else {
13611 // parent rates should go first
13612 $sort_score += 2;
13613 }
13614
13615 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
13616 // this doesn't seem to be a "non refundable rate" so it should go first
13617 $sort_score += 2;
13618 }
13619
13620 // set sorting score for this rate plan ID
13621 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13622 }
13623
13624 // apply custom sorting and keep index association
13625 uasort($rate_plans, function($a, $b) {
13626 // sort by name-scoring DESC
13627 $sort_diff = $b['sort_score'] - $a['sort_score'];
13628 if (isset($b['cost'])) {
13629 // apply also sorting by price ASC
13630 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13631 }
13632 return $sort_diff;
13633 });
13634
13635 return !$assoc ? array_values($rate_plans) : $rate_plans;
13636 }
13637
13638 /**
13639 * Returns an instance of the Availability helper class.
13640 *
13641 * @param bool $anew True for forcing a new instance.
13642 *
13643 * @return VikBookingAvailability
13644 *
13645 * @since 1.15.0 (J) - 1.5.0 (WP)
13646 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13647 */
13648 public static function getAvailabilityInstance($anew = false)
13649 {
13650 // require library
13651 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13652
13653 // return the instance of the class
13654 return VikBookingAvailability::getInstance($anew);
13655 }
13656
13657 /**
13658 * Helper method to quickly guess the internal dependency to import.
13659 *
13660 * @param string $path_id either a full path to a file or its identifier.
13661 *
13662 * @return bool true if the requested library was imported or false.
13663 *
13664 * @since 1.15.0 (J) - 1.5.0 (WP)
13665 */
13666 public static function import($path_id)
13667 {
13668 if (empty($path_id)) {
13669 return false;
13670 }
13671
13672 // always append .php extension
13673 $path_id = basename($path_id, '.php') . '.php';
13674
13675 if (is_file($path_id)) {
13676 // full path given
13677 require_once $path_id;
13678
13679 return true;
13680 }
13681
13682 // try guessing where the file id is located
13683 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13684 // library was found
13685 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13686
13687 return true;
13688 }
13689
13690 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13691 // library was found
13692 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13693
13694 return true;
13695 }
13696
13697 return false;
13698 }
13699 }
13700
13701 if (!class_exists('VikResizer'))
13702 {
13703 class VikResizer
13704 {
13705 public function __construct()
13706 {
13707 // objects of this class can also be instantiated without calling the methods statically.
13708 }
13709
13710 /**
13711 * Resizes an image proportionally. For PNG files it can optionally
13712 * trim the image to exclude the transparency, and add some padding to it.
13713 * All PNG files keep the alpha background in the resized version.
13714 *
13715 * @param string $fileimg path to original image file
13716 * @param string $dest path to destination image file
13717 * @param int $towidth
13718 * @param int $toheight
13719 * @param bool $trim_png remove empty background from image
13720 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13721 *
13722 * @return boolean
13723 */
13724 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13725 {
13726 if (!is_file($fileimg)) {
13727 return false;
13728 }
13729 if (empty($towidth) && empty($toheight)) {
13730 return copy($fileimg, $dest);
13731 }
13732
13733 $result = true;
13734
13735 list ($owid, $ohei, $type) = getimagesize($fileimg);
13736
13737 if ($owid > $towidth || $ohei > $toheight) {
13738 $xscale = $owid / $towidth;
13739 $yscale = $ohei / $toheight;
13740 if ($yscale > $xscale) {
13741 $new_width = round($owid * (1 / $yscale));
13742 $new_height = round($ohei * (1 / $yscale));
13743 } else {
13744 $new_width = round($owid * (1 / $xscale));
13745 $new_height = round($ohei * (1 / $xscale));
13746 }
13747
13748 $imageresized = imagecreatetruecolor($new_width, $new_height);
13749
13750 switch ($type) {
13751 case '1' :
13752 $imagetmp = imagecreatefromgif ($fileimg);
13753 break;
13754 case '2' :
13755 $imagetmp = imagecreatefromjpeg($fileimg);
13756 break;
13757 case '18' :
13758 $imagetmp = imagecreatefromwebp($fileimg);
13759 break;
13760 default :
13761 //keep alpha for PNG files
13762 $background = imagecolorallocate($imageresized, 0, 0, 0);
13763 imagecolortransparent($imageresized, $background);
13764 imagealphablending($imageresized, false);
13765 imagesavealpha($imageresized, true);
13766 //
13767 $imagetmp = imagecreatefrompng($fileimg);
13768 break;
13769 }
13770
13771 if (!$imagetmp) {
13772 return false;
13773 }
13774
13775 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13776
13777 switch ($type) {
13778 case '1' :
13779 imagegif($imageresized, $dest);
13780 break;
13781 case '2' :
13782 imagejpeg($imageresized, $dest);
13783 break;
13784 case '18' :
13785 imagewebp($imageresized, $dest);
13786 break;
13787 default :
13788 if ($trim_png) {
13789 self::imageTrim($imageresized, $background, $trim_pad);
13790 }
13791 imagepng($imageresized, $dest);
13792 break;
13793 }
13794
13795 imagedestroy($imageresized);
13796 } else {
13797 $result = copy($fileimg, $dest);
13798 }
13799
13800 if (VBOPlatformDetection::isWordPress()) {
13801 /**
13802 * @wponly in order to not lose resized files after installing an update,
13803 * we need to move any uploaded file onto a recovery folder.
13804 */
13805 VikBookingLoader::import('update.manager');
13806 VikBookingUpdateManager::triggerUploadBackup($dest);
13807 }
13808
13809 return $result;
13810 }
13811
13812 /**
13813 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13814 * trim the image to exclude the transparency, and add some padding to it.
13815 * All PNG files keep the alpha background in the resized version.
13816 *
13817 * @param resource $im Image link resource (reference)
13818 * @param int $bg imagecolorallocate color identifier
13819 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13820 *
13821 * @return void
13822 */
13823 public static function imagetrim(&$im, $bg, $pad = null)
13824 {
13825 // Calculate padding for each side.
13826 if (isset($pad)) {
13827 $pp = explode(' ', $pad);
13828 if (isset($pp[3])) {
13829 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13830 } elseif (isset($pp[2])) {
13831 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13832 } elseif (isset($pp[1])) {
13833 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13834 } else {
13835 $p = array_fill(0, 4, (int) $pp[0]);
13836 }
13837 } else {
13838 $p = array_fill(0, 4, 0);
13839 }
13840
13841 // Get the image width and height.
13842 $imw = imagesx($im);
13843 $imh = imagesy($im);
13844
13845 // Set the X variables.
13846 $xmin = $imw;
13847 $xmax = 0;
13848
13849 // Start scanning for the edges.
13850 for ($iy=0; $iy<$imh; $iy++) {
13851 $first = true;
13852 for ($ix=0; $ix<$imw; $ix++) {
13853 $ndx = imagecolorat($im, $ix, $iy);
13854 if ($ndx != $bg) {
13855 if ($xmin > $ix) {
13856 $xmin = $ix;
13857 }
13858 if ($xmax < $ix) {
13859 $xmax = $ix;
13860 }
13861 if (!isset($ymin)) {
13862 $ymin = $iy;
13863 }
13864 $ymax = $iy;
13865 if ($first) {
13866 $ix = $xmax;
13867 $first = false;
13868 }
13869 }
13870 }
13871 }
13872
13873 // The new width and height of the image. (not including padding)
13874 $imw = 1+$xmax-$xmin; // Image width in pixels
13875 $imh = 1+$ymax-$ymin; // Image height in pixels
13876
13877 // Make another image to place the trimmed version in.
13878 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13879
13880 // Make the background of the new image the same as the background of the old one.
13881 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13882 imagefill($im2, 0, 0, $bg2);
13883
13884 // Copy it over to the new image.
13885 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13886
13887 // To finish up, we replace the old image which is referenced.
13888 $im = $im2;
13889 }
13890
13891 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13892 {
13893 if (!file_exists($fileimg)) {
13894 return false;
13895 }
13896 if (empty($towidth) && empty($toheight)) {
13897 copy($fileimg, $dest);
13898 return true;
13899 }
13900
13901 $exp = explode(",", $rgb);
13902 if (count($exp) == 3) {
13903 $r = trim($exp[0]);
13904 $g = trim($exp[1]);
13905 $b = trim($exp[2]);
13906 } else {
13907 $r = 0;
13908 $g = 0;
13909 $b = 0;
13910 }
13911
13912 list ($owid, $ohei, $type) = getimagesize($fileimg);
13913
13914 if ($owid > $towidth || $ohei > $toheight) {
13915 $xscale = $owid / $towidth;
13916 $yscale = $ohei / $toheight;
13917 if ($yscale > $xscale) {
13918 $new_width = round($owid * (1 / $yscale));
13919 $new_height = round($ohei * (1 / $yscale));
13920 $ydest = 0;
13921 $diff = $towidth - $new_width;
13922 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13923 } else {
13924 $new_width = round($owid * (1 / $xscale));
13925 $new_height = round($ohei * (1 / $xscale));
13926 $xdest = 0;
13927 $diff = $toheight - $new_height;
13928 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13929 }
13930
13931 $imageresized = imagecreatetruecolor($towidth, $toheight);
13932
13933 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13934 imagefill($imageresized, 0, 0, $bgColor);
13935
13936 switch ($type) {
13937 case '1' :
13938 $imagetmp = imagecreatefromgif ($fileimg);
13939 break;
13940 case '2' :
13941 $imagetmp = imagecreatefromjpeg($fileimg);
13942 break;
13943 default :
13944 $imagetmp = imagecreatefrompng($fileimg);
13945 break;
13946 }
13947
13948 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13949
13950 switch ($type) {
13951 case '1' :
13952 imagegif ($imageresized, $dest);
13953 break;
13954 case '2' :
13955 imagejpeg($imageresized, $dest);
13956 break;
13957 default :
13958 imagepng($imageresized, $dest);
13959 break;
13960 }
13961
13962 imagedestroy($imageresized);
13963
13964 return true;
13965 } else {
13966 copy($fileimg, $dest);
13967 }
13968 return true;
13969 }
13970
13971 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13972 {
13973 if (!file_exists($fileimg)) {
13974 return false;
13975 }
13976 if (empty($towidth) && empty($toheight)) {
13977 copy($fileimg, $dest);
13978 return true;
13979 }
13980
13981 list ($owid, $ohei, $type) = getimagesize($fileimg);
13982
13983 if ($owid <= $ohei) {
13984 $new_width = $towidth;
13985 $new_height = ($towidth / $owid) * $ohei;
13986 } else {
13987 $new_height = $toheight;
13988 $new_width = ($new_height / $ohei) * $owid;
13989 }
13990
13991 switch ($type) {
13992 case '1' :
13993 $img_src = imagecreatefromgif ($fileimg);
13994 $img_dest = imagecreate($new_width, $new_height);
13995 break;
13996 case '2' :
13997 $img_src = imagecreatefromjpeg($fileimg);
13998 $img_dest = imagecreatetruecolor($new_width, $new_height);
13999 break;
14000 default :
14001 $img_src = imagecreatefrompng($fileimg);
14002 $img_dest = imagecreatetruecolor($new_width, $new_height);
14003 break;
14004 }
14005
14006 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
14007
14008 switch ($type) {
14009 case '1' :
14010 $cropped = imagecreate($towidth, $toheight);
14011 break;
14012 case '2' :
14013 $cropped = imagecreatetruecolor($towidth, $toheight);
14014 break;
14015 default :
14016 $cropped = imagecreatetruecolor($towidth, $toheight);
14017 break;
14018 }
14019
14020 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
14021
14022 switch ($type) {
14023 case '1' :
14024 imagegif ($cropped, $dest);
14025 break;
14026 case '2' :
14027 imagejpeg($cropped, $dest);
14028 break;
14029 default :
14030 imagepng($cropped, $dest);
14031 break;
14032 }
14033
14034 imagedestroy($img_dest);
14035 imagedestroy($cropped);
14036
14037 return true;
14038 }
14039 }
14040 }
14041