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

14,085 lines 469.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 if (!class_exists('VikBookingIcons')) {
14 // require the Icons class
15 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "icons.php");
16 }
17
18 if (!function_exists('showSelectVb')) {
19 function showSelectVb($err, $err_code_info = array()) {
20 include(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'error_form.php');
21 }
22 }
23
24 class VikBooking
25 {
26 public static function addJoomlaUser($name, $username, $email, $password) {
27 //new method
28 jimport('joomla.application.component.helper');
29 $params = JComponentHelper::getParams('com_users');
30 $user = new JUser;
31 $data = array();
32 //Get the default new user group, Registered if not specified.
33 $system = $params->get('new_usertype', 2);
34 $data['groups'] = array();
35 $data['groups'][] = $system;
36 $data['name'] = $name;
37 $data['username'] = $username;
38 $data['email'] = self::getVboApplication()->emailToPunycode($email);
39 $data['password'] = $password;
40 $data['password2'] = $password;
41 $data['sendEmail'] = 0; //should the user receive system mails?
42 //$data['block'] = 0;
43 if (!$user->bind($data)) {
44 VikError::raiseWarning('', JText::translate($user->getError()));
45 return false;
46 }
47 if (!$user->save()) {
48 VikError::raiseWarning('', JText::translate($user->getError()));
49 return false;
50 }
51 return $user->id;
52 }
53
54 public static function userIsLogged() {
55 $user = JFactory::getUser();
56
57 return !$user->guest;
58 }
59
60 public static function prepareViewContent()
61 {
62 /**
63 * @wponly JApplication::getMenu() cannot be adapted to WP
64 */
65 }
66
67 public static function isFontAwesomeEnabled()
68 {
69 return VBOFactory::getConfig()->getBool('usefa', true);
70 }
71
72 public static function loadFontAwesome($force_load = false)
73 {
74 if (!self::isFontAwesomeEnabled() && !$force_load) {
75 return false;
76 }
77
78 /**
79 * We let the class VikBookingIcons load the proper FontAwesome libraries.
80 *
81 * @since 1.11
82 */
83 VikBookingIcons::loadAssets();
84
85 return true;
86 }
87
88 /**
89 * Checks if modifications or cancellations via front-end are allowed.
90 * 0 = everything is Disabled.
91 * 1 = Disabled, with request message (default).
92 * 2 = Modification Enabled, Cancellation Disabled.
93 * 3 = Cancellation Enabled, Modification Disabled.
94 * 4 = everything is Enabled.
95 *
96 * @return int
97 */
98 public static function getReservationModCanc()
99 {
100 return VBOFactory::getConfig()->getInt('resmodcanc', 1);
101 }
102
103 public static function getReservationModCancMin()
104 {
105 return VBOFactory::getConfig()->getInt('resmodcancmin', 1);
106 }
107
108 public static function getDefaultDistinctiveFeatures()
109 {
110 return [
111 'VBODEFAULTDISTFEATUREONE' => '',
112 // Below is the default feature for 'Room Code'. One default feature is sufficient
113 // 'VBODEFAULTDISTFEATURETWO' => '',
114 ];
115 }
116
117 /**
118 * Given the room's parameters and index, tries to take the first distinctive feature.
119 *
120 * @param mixed $rparams string to be decoded, or decoded array/object params
121 * @param int $rindex room index to look for, starting from 1.
122 *
123 * @return mixed false on failure, array otherwise [feature name, feature value]
124 *
125 * @since 1.14 (J) - 1.4.0 (WP)
126 */
127 public static function getRoomUnitDistinctiveFeature($rparams, $rindex) {
128 $rindex = (int)$rindex;
129 if ($rindex < 1 || empty($rparams)) {
130 return false;
131 }
132
133 if (is_string($rparams)) {
134 // decode params
135 $rparams = self::getRoomParam('features', $rparams);
136 }
137
138 if (is_object($rparams)) {
139 // typecast to array
140 $rparams = (array)$rparams;
141 }
142
143 if (!is_array($rparams) || !count($rparams)) {
144 return false;
145 }
146
147 if (isset($rparams['features'])) {
148 $rparams = $rparams['features'];
149 }
150
151 $feature = array();
152 foreach ($rparams as $param_index => $rfeatures) {
153 if ((int)$param_index != $rindex || !is_array($rfeatures) || !count($rfeatures)) {
154 continue;
155 }
156 foreach ($rfeatures as $featname => $featval) {
157 if (empty($featval)) {
158 continue;
159 }
160 // use the first distinctive feature
161 $tn_featname = JText::translate($featname);
162 if ($tn_featname == $featname) {
163 // no translation was applied
164 if (VBOPlatformDetection::isWordPress()) {
165 // try to apply a translation through Gettext even if we have to pass a variable
166 $tn_featname = __($featname);
167 } else {
168 // convert the string to a hypothetical INI constant
169 $ini_constant = str_replace(' ', '_', strtoupper($featname));
170 $tn_featname = JText::translate($ini_constant);
171 $tn_featname = $tn_featname == $ini_constant ? $featname : $tn_featname;
172 }
173 }
174 // store values and break loop
175 $feature = array($tn_featname, $featval);
176 break;
177 }
178 }
179
180 return count($feature) ? $feature : false;
181 }
182
183 /**
184 * Returns an associative list of booking room records and occupied sub-unit indexes.
185 *
186 * @param array $order The current booking record for which the list is needed.
187 * @param int $idroom The booked room ID involved for the associative list.
188 *
189 * @return array
190 *
191 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
192 */
193 public static function getRoomUnitNumsUnavailable(array $order, $idroom)
194 {
195 $dbo = JFactory::getDbo();
196
197 // list of data to build
198 $unavailable_indexes = [];
199
200 $first = $order['checkin'];
201 $second = $order['checkout'];
202 $secdiff = $second - $first;
203 $daysdiff = $secdiff / 86400;
204 if (is_int($daysdiff)) {
205 if ($daysdiff < 1) {
206 $daysdiff = 1;
207 }
208 } else {
209 if ($daysdiff < 1) {
210 $daysdiff = 1;
211 } else {
212 $sum = floor($daysdiff) * 86400;
213 $newdiff = $secdiff - $sum;
214 $maxhmore = self::getHoursMoreRb() * 3600;
215 if ($maxhmore >= $newdiff) {
216 $daysdiff = floor($daysdiff);
217 } else {
218 $daysdiff = ceil($daysdiff);
219 }
220 }
221 }
222
223 // the busy records information pool
224 $busyMap = [];
225
226 // query just the busy records
227 $dbo->setQuery(
228 $dbo->getQuery(true)
229 ->select($dbo->qn([
230 'b.idroom',
231 'b.checkin',
232 'b.checkout',
233 'b.realback',
234 'ob.idorder',
235 ]))
236 ->from($dbo->qn('#__vikbooking_busy', 'b'))
237 ->innerJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('ob.idbusy') . ' = ' . $dbo->qn('b.id'))
238 ->where($dbo->qn('b.idroom') . ' = ' . (int) $idroom)
239 ->where($dbo->qn('b.checkin') . ' < ' . $second)
240 ->where($dbo->qn('b.checkout') . ' > ' . $first)
241 ->where($dbo->qn('ob.idorder') . ' != ' . (int) $order['id'])
242 ->order($dbo->qn('b.id') . ' ASC')
243 );
244 $busyRecords = $dbo->loadAssocList();
245
246 if (!$busyRecords) {
247 // useless to proceed
248 return [];
249 }
250
251 // query bookings and rooms based on the busy records obtained
252 $dbo->setQuery(
253 $dbo->getQuery(true)
254 ->select([
255 $dbo->qn('or.id', 'or_id'),
256 $dbo->qn('or.idorder'),
257 $dbo->qn('or.idroom'),
258 $dbo->qn('or.roomindex'),
259 ])
260 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
261 ->where($dbo->qn('or.idroom') . ' = ' . (int) $idroom)
262 ->where($dbo->qn('or.idorder') . ' IN (' . implode(', ', array_map('intval', array_column($busyRecords, 'idorder'))) . ')')
263 ->order($dbo->qn('or.id') . ' ASC')
264 );
265 $busyRooms = $dbo->loadAssocList();
266
267 // build the final map by merging values
268 foreach ($busyRecords as $busyRecord) {
269 // busy signature
270 $signature = sprintf('%d-%d', $busyRecord['idorder'], $busyRecord['idroom']);
271 // push initial record values
272 $busyMap[$signature][] = $busyRecord;
273 }
274
275 // list of booking-id/booking-room-id signatures
276 $roomSignaturesCounter = [];
277
278 foreach ($busyRooms as $busyRoom) {
279 // busy signature
280 $signature = sprintf('%d-%d', $busyRoom['idorder'], $busyRoom['idroom']);
281
282 // determine booking room signature index
283 $roomSignaturesCounter[$signature] = ($roomSignaturesCounter[$signature] ?? -1) + 1;
284
285 if (!isset($busyMap[$signature][$roomSignaturesCounter[$signature]])) {
286 // unmatched signature
287 continue;
288 }
289
290 // merge booking room values with existing busy record values
291 $busyMap[$signature][$roomSignaturesCounter[$signature]] = array_merge($busyMap[$signature][$roomSignaturesCounter[$signature]], $busyRoom);
292 }
293
294 // simulate a list of associative busy and booking room records
295 $busy = [];
296 foreach ($busyMap as $mapList) {
297 foreach ($mapList as $record) {
298 // push record
299 $busy[] = $record;
300 }
301 }
302
303 if (!$busy) {
304 // useless to proceed
305 return [];
306 }
307
308 // get a list of affected timestamps
309 $groupdays = self::getGroupDays($first, $second, $daysdiff);
310
311 // scan the affected stay timestamps to build the sub-unit matches
312 foreach ($groupdays as $gday) {
313 foreach ($busy as $bu) {
314 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
315 continue;
316 }
317 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
318 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
319 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
320 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
321 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
322 }
323 }
324 }
325 }
326
327 return $unavailable_indexes;
328 }
329
330 /**
331 * Returns an associative list of booking room records and available sub-unit indexes.
332 *
333 * @param array $order The current booking record for which the list is needed.
334 * @param int $idroom The booked room ID involved for the associative list.
335 *
336 * @return array
337 *
338 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
339 */
340 public static function getRoomUnitNumsAvailable(array $order, $idroom)
341 {
342 // build available indexes
343 $available_indexes = [];
344
345 // get the unavailable room indexes
346 $unavailable_indexes = self::getRoomUnitNumsUnavailable($order, $idroom);
347
348 // load room params
349 $room_data = self::getRoomInfo($idroom, ['params'], $no_cache = false);
350 $room_params_arr = (array) json_decode($room_data['params'] ?? '[]', true);
351
352 if (is_array($room_params_arr['features'] ?? null) && $room_params_arr['features']) {
353 foreach ($room_params_arr['features'] as $rind => $rfeatures) {
354 if (in_array($rind, $unavailable_indexes)) {
355 continue;
356 }
357 // push available room index
358 $available_indexes[] = $rind;
359 }
360 }
361
362 // return the available indexes
363 return $available_indexes;
364 }
365
366 /**
367 * Generates a random UUID v4.
368 *
369 * A UUID is a 16-octet (128-bit) number. In its canonical form, a UUID is represented by 32
370 * hexadecimal digits, displayed in five groups separated by hyphens, in the form 8-4-4-4-12
371 * for a total of 36 characters (32 alphanumeric characters and four hyphens).
372 *
373 * @return string
374 *
375 * @since 1.18.8 (J) - 1.8.8 (WP)
376 */
377 public static function uuid()
378 {
379 /*
380 return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
381 // 32 bits for "time_low"
382 mt_rand(0, 0xffff), mt_rand(0, 0xffff),
383
384 // 16 bits for "time_mid"
385 mt_rand(0, 0xffff),
386
387 // 16 bits for "time_hi_and_version",
388 // four most significant bits holds version number 4
389 mt_rand(0, 0x0fff) | 0x4000,
390
391 // 16 bits, 8 bits for "clk_seq_hi_res",
392 // 8 bits for "clk_seq_low",
393 // two most significant bits holds zero and one for variant DCE1.1
394 mt_rand(0, 0x3fff) | 0x8000,
395
396 // 48 bits for "node"
397 mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
398 );
399 */
400
401 // generate 16 random bytes through CSPRNG
402 $data = random_bytes(16);
403
404 // four most significant bits holds version number 4
405 $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
406 // two most significant bits holds zero and one for variant DCE1.1
407 $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
408
409 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
410 }
411
412 /**
413 * Load the restrictions applying the given filters to the passed rooms.
414 * The ordering of the query SHOULD remain unchanged, because it's required
415 * to have it by ascending order of the ID. So the older records first.
416 * Even when no filters, we always exclude expired restrictions.
417 *
418 * @param boolean $filters whether to apply filters
419 * @param array $rooms the list of rooms to filter
420 *
421 * @return array the list of restrictions found or an empty array.
422 */
423 public static function loadRestrictions($filters = true, $rooms = [])
424 {
425 $dbo = JFactory::getDbo();
426
427 $restrictions = [];
428 $limts = strtotime(date('Y-m-d'));
429
430 if (!$filters) {
431 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
432 } else {
433 if (!$rooms) {
434 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
435 } else {
436 $clause = [];
437 foreach ($rooms as $idr) {
438 if (empty($idr)) {
439 continue;
440 }
441 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
442 }
443 if ($clause) {
444 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE (`dto` = 0 OR `dto` >= " . $limts . ") AND (`allrooms` = 1 OR (`allrooms` = 0 AND (" . implode(" OR ", $clause) . "))) ORDER BY `id` ASC;";
445 } else {
446 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
447 }
448 }
449 }
450
451 $dbo->setQuery($q);
452 $allrestrictions = $dbo->loadAssocList();
453
454 foreach ($allrestrictions as $k => $res) {
455 if (!empty($res['month'])) {
456 $restrictions[$res['month']] = $res;
457 } else {
458 if (!isset($restrictions['range'])) {
459 $restrictions['range'] = array();
460 }
461 $restrictions['range'][$k] = $res;
462 }
463 }
464
465 return $restrictions;
466 }
467
468 /**
469 * Given a list of restriction records split into range or months, filters
470 * the list by including the "global" restrictions applied to all rooms.
471 *
472 * @param array $restrictions List of restriction records loaded.
473 *
474 * @return array Associative list of filtered restriction records.
475 */
476 public static function globalRestrictions(array $restrictions)
477 {
478 $list = [];
479
480 // iterate all records
481 foreach ($restrictions as $kr => $rr) {
482 if ($kr == 'range') {
483 // scan all date-range restriction records
484 foreach ($rr as $kd => $dr) {
485 if ($dr['allrooms'] == 1) {
486 $list['range'][$kd] = $restrictions[$kr][$kd];
487 }
488 }
489 // go next
490 continue;
491 }
492
493 // month restriction record
494 if ($rr['allrooms'] == 1) {
495 $list[$kr] = $restrictions[$kr];
496 }
497 }
498
499 return $list;
500 }
501
502 /**
503 * Given a list of restriction records split into range or months, filters the list
504 * by including the "listing-level" or "global" restrictions affecting to given rooms.
505 *
506 * @param array $restrictions List of restriction records loaded.
507 * @param array $room_ids List of involved and eligible room IDs.
508 *
509 * @return array Associative list of filtered restriction records.
510 *
511 * @since 1.18.2 (J) - 1.8.2 (WP)
512 */
513 public static function listingRestrictions(array $restrictions, array $room_ids)
514 {
515 $list = [];
516
517 // iterate all records
518 foreach ($restrictions as $kr => $rr) {
519 if ($kr == 'range') {
520 // scan all date-range restriction records
521 foreach ($rr as $kd => $dr) {
522 if ($dr['allrooms'] == 1) {
523 // push global restriction
524 $list['range'][$kd] = $restrictions[$kr][$kd];
525 } elseif (!empty($dr['idrooms'])) {
526 // scan the list of room IDs to match against
527 foreach ($room_ids as $room_id) {
528 if (strpos($dr['idrooms'], '-' . $room_id . '-') !== false) {
529 // push listing-level restriction
530 $list['range'][$kd] = $restrictions[$kr][$kd];
531 break;
532 }
533 }
534 }
535 }
536 // go next
537 continue;
538 }
539
540 // month restriction record
541 if ($rr['allrooms'] == 1) {
542 // push global restriction
543 $list[$kr] = $restrictions[$kr];
544 } elseif (!empty($rr['idrooms'])) {
545 // scan the list of room IDs to match against
546 foreach ($room_ids as $room_id) {
547 if (strpos($rr['idrooms'], '-' . $room_id . '-') !== false) {
548 // push listing-level restriction
549 $list[$kr] = $restrictions[$kr];
550 break;
551 }
552 }
553 }
554 }
555
556 return $list;
557 }
558
559 /**
560 * From the given restrictions, check-in, check-out and nights, looks for
561 * a restriction to be returned and applied over these stays.
562 * In order to give priority to newer Restriction IDs, the order of the
563 * $restrictions array should be ascending, so that the newer restrictions
564 * will overwrite the array with the last ID (more recent). Only for date ranges.
565 * Returns an array with the record of the restriction found. The loop is not
566 * broken when the first valid restriction is found to give priority to
567 * newer restriction IDs to overwrite older records.
568 *
569 * @param int $first the unix timestamp for the check-in date
570 * @param int $second the unix timestamp for the check-out date
571 * @param int $daysdiff the number of nights of stay
572 * @param array $restrictions the list of restrictions loaded
573 *
574 * @return array the restriction found, or an empty array.
575 */
576 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
577 $season_restrictions = array();
578 $restrcheckin = getdate($first);
579 $restrcheckout = getdate($second);
580 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
581 //restriction found for this month, checking:
582 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
583 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
584 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
585 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
586 //Week Day Arrival Restriction
587 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
588 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
589 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
590 }
591 $season_restrictions['wdays'] = $rvalidwdays;
592 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
593 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
594 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
595 }
596 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
597 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
598 }
599 }
600 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
601 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
602 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
603 $season_restrictions['allowed'] = false;
604 }
605 }
606 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
607 $season_restrictions['allowed'] = false;
608 }
609 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
610 } elseif (array_key_exists('range', $restrictions)) {
611 foreach ($restrictions['range'] as $restr) {
612 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
613 //restriction found for this date range, checking:
614 $season_restrictions['id'] = $restr['id'];
615 $season_restrictions['name'] = $restr['name'];
616 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
617 if (strlen((string)$restr['wday']) > 0) {
618 //Week Day Arrival Restriction
619 $rvalidwdays = array($restr['wday']);
620 if (strlen((string)$restr['wdaytwo']) > 0) {
621 $rvalidwdays[] = $restr['wdaytwo'];
622 }
623 $season_restrictions['wdays'] = $rvalidwdays;
624 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
625 if (!empty($restr['ctad'])) {
626 $season_restrictions['cta'] = explode(',', $restr['ctad']);
627 }
628 if (!empty($restr['ctdd'])) {
629 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
630 }
631 }
632 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
633 $season_restrictions['maxlos'] = $restr['maxlos'];
634 if ($daysdiff > $restr['maxlos']) {
635 $season_restrictions['allowed'] = false;
636 }
637 }
638 if ($daysdiff < $restr['minlos']) {
639 $season_restrictions['allowed'] = false;
640 }
641 $season_restrictions['minlos'] = $restr['minlos'];
642 }
643 }
644 }
645
646 return $season_restrictions;
647 }
648
649 public static function compareSeasonRestrictionsNights($restrictions)
650 {
651 $base_compare = array();
652 $base_nights = 0;
653 foreach ($restrictions as $nights => $restr) {
654 $base_compare = $restr;
655 $base_nights = $nights;
656 break;
657 }
658
659 /**
660 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
661 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
662 *
663 * @since 1.15.0 (J) - 1.5.0 (WP)
664 */
665 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
666
667 foreach ($restrictions as $nights => $restr) {
668 if ($nights == $base_nights) {
669 continue;
670 }
671
672 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
673 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
674
675 // get associative array of differences
676 $diff = array_diff($use_base_compare, $use_restr);
677
678 if (count($diff) > 0 && array_key_exists('id', $diff)) {
679 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
680 // only one Restriction per time should be applied to certain Season Dates but check just in case.
681 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
682 }
683 }
684
685 return array();
686 }
687
688 /**
689 * Methods using array_diff() may need one-dimension arrays, because this
690 * native functions applies a string comparison, and so casting an array
691 * value to a string would generate a Notice message. This method simply
692 * converts any sub-array into a restorable string into a non-scalar var.
693 * This only works when comparing 2 array variables.
694 *
695 * @param array $arr the hypothetical multi dimension array.
696 *
697 * @return array the one dimension array to be passed to array_diff()
698 * and the list of keys converted to strings for comparison.
699 *
700 * @since 1.15.0 (J) - 1.5.0 (WP)
701 */
702 public static function prepareMultiDimArrayDiff($arr)
703 {
704 $casted_keys = array();
705
706 if (!is_array($arr) || !count($arr)) {
707 return array($casted_keys, $arr);
708 }
709
710 foreach ($arr as $key => $val) {
711 if (!is_scalar($val)) {
712 // memorize original value
713 $casted_keys[$key] = $val;
714 // cast to string must be applied
715 $arr[$key] = json_encode($val);
716 }
717 }
718
719 return array($casted_keys, $arr);
720 }
721
722 /**
723 * Methods using array_diff() may need one-dimension arrays, because this
724 * native functions applies a string comparison, and so casting an array
725 * value to a string would generate a Notice message. This method restores
726 * the original values of the "prepared" arrays by getting the list of the
727 * modified keys from non-scalar to scalar values.
728 * This only works when comparing 2 array variables.
729 *
730 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
731 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
732 * @param array $arr the array result of array_diff().
733 *
734 * @return array the array result of array_diff() with its original values.
735 *
736 * @since 1.15.0 (J) - 1.5.0 (WP)
737 */
738 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
739 {
740 if (!is_array($arr) || !count($arr)) {
741 return $arr;
742 }
743
744 foreach ($arr as $key => $val) {
745 // keys returned by array_diff can be present in both $keys_one and $keys_two
746 if (isset($keys_one[$key])) {
747 // always give higher priority to the 1st argument keys for array_diff()
748 $arr[$key] = json_decode($keys_one[$key]);
749 } elseif (isset($keys_two[$key])) {
750 // fallback to 2nd argument keys
751 $arr[$key] = json_decode($keys_two[$key]);
752 }
753 }
754
755 return $arr;
756 }
757
758 public static function roomRestrictions($roomid, $restrictions) {
759 $ret = array();
760 if (!empty($roomid) && count($restrictions) > 0) {
761 foreach ($restrictions as $kr => $rr) {
762 if ($kr == 'range') {
763 foreach ($rr as $kd => $dr) {
764 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
765 $allrooms = explode(';', $dr['idrooms']);
766 if (in_array('-'.$roomid.'-', $allrooms)) {
767 $ret['range'][$kd] = $restrictions[$kr][$kd];
768 }
769 }
770 }
771 } else {
772 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
773 $allrooms = explode(';', $rr['idrooms']);
774 if (in_array('-'.$roomid.'-', $allrooms)) {
775 $ret[$kr] = $restrictions[$kr];
776 }
777 }
778 }
779 }
780 }
781 return $ret;
782 }
783
784 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
785 {
786 // default states
787 $restrictionerrmsg = '';
788 $restrictions_affcount = 0;
789 $minlos_errors_pool = [];
790
791 // check for month-level or range-level restrictions
792 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
793 //restriction found for this month, checking:
794 $restrictions_affcount++;
795 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
796 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
797 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
798 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
799 }
800 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
801 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVAL', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wday']).(strlen($roomrestr[$restrcheckin['mon']]['wdaytwo']) > 0 ? '/'.self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wdaytwo']) : ''));
802 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
803 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
804 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
805 }
806 }
807 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
808 if (count($comborestr) > 0) {
809 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
810 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
811 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBO', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
812 }
813 }
814 }
815 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
816 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
817 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
818 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
819 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
820 }
821 }
822 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
823 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
824 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
825 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
826 }
827 }
828 }
829 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
830 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
831 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
832 }
833 }
834 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
835 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
836 }
837 } elseif (array_key_exists('range', $roomrestr)) {
838 // start valid flag
839 $restrictionsvalid = true;
840
841 /**
842 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
843 * The most recent restrictions will have a higher priority over the oldest ones.
844 *
845 * @since 1.12.1
846 */
847 $minlos_priority = [
848 'ok' => [],
849 'nok' => [],
850 ];
851
852 /**
853 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
854 *
855 * @since 1.16.10 (J) - 1.6.10 (WP)
856 */
857 $ctad_priority = [
858 'ok' => [],
859 'nok' => [],
860 ];
861
862 foreach ($roomrestr['range'] as $restr) {
863 /**
864 * We should not always add 82799 seconds to the end date of the restriction
865 * because if they only last for one day (like a Saturday), then $restr['dto']
866 * will be already set to the time 23:59:59.
867 *
868 * @since 1.13 (J) - 1.2.18 (WP)
869 * @since 1.18.3 (J) - 1.8.3 (WP) timestamps ending with 9 for a range greater than 1 day indicate that the restriction end time
870 * is already set to 23:59:59 (VCM Connector may be responsible), so no seconds should be added or the restriction end date
871 * will be the day after, and if after the check-in time, it may be included.
872 */
873 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) && substr((string) $restr['dto'], -1, 1) != 9 ? 82799 : 0;
874
875 /**
876 * Adjust at runtime the restriction end date in case we have records written by third-party
877 * dynamic pricing systems that last for one day, where the date-from and date-to are identical.
878 *
879 * @since 1.18.16 (J) - 1.8.16 (WP)
880 */
881 if ($restr['dfrom'] == $restr['dto'] && !$end_operator) {
882 $end_operator = 82799;
883 }
884
885 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
886 // restriction found for this date range based on arrival date, check if compliant
887 $restrictions_affcount++;
888
889 // set flag for CTA/CTD compliance
890 $cta_ctd_passed = true;
891
892 if (strlen((string)$restr['wday']) > 0) {
893 $rvalidwdays = array($restr['wday']);
894 if (strlen((string)$restr['wdaytwo']) > 0) {
895 $rvalidwdays[] = $restr['wdaytwo'];
896 }
897 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
898 $restrictionsvalid = false;
899 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
900 } elseif ($restr['multiplyminlos'] == 1) {
901 if (($daysdiff % $restr['minlos']) != 0) {
902 $restrictionsvalid = false;
903 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
904 }
905 }
906 $comborestr = self::parseJsDrangeWdayCombo($restr);
907 if ($comborestr) {
908 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
909 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
910 $restrictionsvalid = false;
911 $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']));
912 }
913 }
914 }
915 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
916 if (!empty($restr['ctad'])) {
917 $ctarestrictions = explode(',', $restr['ctad']);
918 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
919 $restrictionsvalid = false;
920 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
921 $cta_ctd_passed = false;
922 }
923 }
924 if (!empty($restr['ctdd'])) {
925 $ctdrestrictions = explode(',', $restr['ctdd']);
926 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
927 $restrictionsvalid = false;
928 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
929 $cta_ctd_passed = false;
930 }
931 }
932 }
933
934 // check CTA/CTD compliance
935 if (!$cta_ctd_passed) {
936 array_push($ctad_priority['nok'], (int)$restr['id']);
937 } else {
938 array_push($ctad_priority['ok'], (int)$restr['id']);
939 }
940
941 // max LOS validation
942 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
943 if ($daysdiff > $restr['maxlos']) {
944 $restrictionsvalid = false;
945 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
946 }
947 }
948
949 // min LOS validation
950 if ($daysdiff < $restr['minlos']) {
951 $restrictionsvalid = false;
952 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
953 // push error value
954 array_push($minlos_priority['nok'], (int) $restr['id']);
955 // set error message with related minimum stay
956 $minlos_errors_pool[$restr['minlos']] = $restrictionerrmsg;
957 } else {
958 array_push($minlos_priority['ok'], (int) $restr['id']);
959 }
960 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
961 /**
962 * We validate the CTD restrictions depending on the check-out date.
963 *
964 * @since 1.16.3 (J) - 1.6.3 (WP)
965 */
966 $ctdrestrictions = explode(',', $restr['ctdd']);
967 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
968 $restrictions_affcount++;
969 $restrictionsvalid = false;
970 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
971 }
972 }
973 }
974
975 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
976 // a more recent restriction is allowing this MinLOS
977 // ensure there are no recent CTA/CTD rules not compliant
978 $cta_ctd_priority_ok = true;
979
980 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
981 // a more recent restriction is not compliant with the CTA/CTD rules
982 $cta_ctd_priority_ok = false;
983 }
984
985 if ($cta_ctd_priority_ok) {
986 // we unset the error message because more recent restriction(s) are allowing this stay dates
987 $restrictionerrmsg = '';
988 }
989 }
990 }
991
992 // check global restriction of Min LOS for TAC functions in VBO and VCM
993 if (empty($restrictionerrmsg) && $roomrestr && $restrictions_affcount <= 0) {
994 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
995 $globminlos = self::getDefaultNightsCalendar();
996 if ($globminlos > 1 && $daysdiff < $globminlos) {
997 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
998 }
999 }
1000
1001 /**
1002 * When working with room-level restrictions and different minLOS across multiple apartments,
1003 * we need the display the actually lowest minimum stay among all listings from the errors found.
1004 *
1005 * @since 1.17.3 (J) - 1.7.3 (WP)
1006 */
1007 if ($restrictionerrmsg && in_array($restrictionerrmsg, $minlos_errors_pool) && count($minlos_errors_pool) > 1) {
1008 // make sure to return the error message for the lowest minimum stay
1009 $lowest_minlos = min(array_map('intval', array_keys($minlos_errors_pool)));
1010 $restrictionerrmsg = $minlos_errors_pool[$lowest_minlos] ?? $restrictionerrmsg;
1011 }
1012
1013 // return the restriction error message string, if anything wrong was found
1014 return $restrictionerrmsg;
1015 }
1016
1017 public static function parseJsDrangeWdayCombo($drestr) {
1018 $combo = array();
1019 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
1020 $cparts = explode(':', $drestr['wdaycombo']);
1021 foreach ($cparts as $kc => $cw) {
1022 if (!empty($cw)) {
1023 $nowcombo = explode('-', $cw);
1024 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
1025 }
1026 }
1027 }
1028 return $combo;
1029 }
1030
1031 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
1032 $dbo = JFactory::getDbo();
1033 $pkg = array();
1034 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1035 $dbo->setQuery($q);
1036 $dbo->execute();
1037 if ($dbo->getNumRows() == 1) {
1038 $pkg = $dbo->loadAssoc();
1039 $vbo_tn = self::getTranslator();
1040 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
1041 } else {
1042 return JText::translate('VBOPKGERRNOTFOUND');
1043 }
1044 $rooms_req = array();
1045 foreach ($rooms as $num => $room) {
1046 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
1047 $rooms_req[] = $room['id'];
1048 }
1049 }
1050 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
1051 $dbo->setQuery($q);
1052 $dbo->execute();
1053 if ($dbo->getNumRows() != count($rooms_req)) {
1054 //error, not all the rooms requested are available for this package
1055 return JText::translate('VBOPKGERRNOTROOM');
1056 }
1057 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
1058 return JText::translate('VBOPKGERRNUMNIGHTS');
1059 }
1060 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
1061 return JText::translate('VBOPKGERRCHECKIND');
1062 }
1063 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
1064 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
1065 return JText::translate('VBOPKGERRCHECKOUTD');
1066 }
1067 if (!empty($pkg['excldates'])) {
1068 //this would check if any stay date is excluded
1069 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
1070 //check just the arrival and departure dates
1071 $bookdates_ts = array($checkints, $checkoutts);
1072 $bookdates = array();
1073 foreach ($bookdates_ts as $bookdate_ts) {
1074 $info_d = getdate($bookdate_ts);
1075 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
1076 }
1077 $edates = explode(';', $pkg['excldates']);
1078 foreach ($edates as $edate) {
1079 if (!empty($edate) && in_array($edate, $bookdates)) {
1080 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
1081 }
1082 }
1083 }
1084 return $pkg;
1085 }
1086
1087 public static function getPackage($pkg_id) {
1088 $dbo = JFactory::getDbo();
1089 $pkg = array();
1090 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1091 $dbo->setQuery($q);
1092 $dbo->execute();
1093 if ($dbo->getNumRows() == 1) {
1094 $pkg = $dbo->loadAssoc();
1095 }
1096 return $pkg;
1097 }
1098
1099 /**
1100 * Returns the requested room parameter or default value.
1101 *
1102 * @param string $paramname The parameter name.
1103 * @param mixed $params The room params string, or json-decoded array/object.
1104 * @param mixed $def The default value to return as fallback.
1105 *
1106 * @return mixed
1107 *
1108 * @since 1.17.3 (J) - 1.7.3 (WP) added 3rd argument $def.
1109 */
1110 public static function getRoomParam($paramname, $params, $def = '')
1111 {
1112 if (empty($params)) {
1113 return $def;
1114 }
1115
1116 if (is_string($params)) {
1117 $params = (array) json_decode($params, true);
1118 } elseif (is_object($params)) {
1119 $params = (array) $params;
1120 }
1121
1122 if (!is_array($params)) {
1123 return $def;
1124 }
1125
1126 return $params[$paramname] ?? $def;
1127 }
1128
1129 public static function filterNightsSeasonsCal($arr_nights) {
1130 $nights = array();
1131 foreach ($arr_nights as $night) {
1132 if (intval(trim($night)) > 0) {
1133 $nights[] = intval(trim($night));
1134 }
1135 }
1136 sort($nights);
1137 return array_unique($nights);
1138 }
1139
1140 public static function getSeasonRangeTs($from, $to, $year) {
1141 $sfrom = 0;
1142 $sto = 0;
1143 $tsbase = mktime(0, 0, 0, 1, 1, $year);
1144 $curyear = $year;
1145 $tsbasetwo = $tsbase;
1146 $curyeartwo = $year;
1147 if ($from > $to) {
1148 //between two years
1149 $curyeartwo += 1;
1150 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
1151 }
1152 $sfrom = ($tsbase + $from);
1153 $sto = ($tsbasetwo + $to);
1154 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
1155 //leap years
1156 $infoseason = getdate($sfrom);
1157 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1158 if ($infoseason[0] > $leapts) {
1159 /**
1160 * Timestamp must be greater than the leap-day of Feb 29th.
1161 * It used to be checked for >= $leapts.
1162 *
1163 * @since July 3rd 2019
1164 */
1165 $sfrom += 86400;
1166 if ($curyear == $curyeartwo) {
1167 $sto += 86400;
1168 }
1169 }
1170 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
1171 //leap years
1172 $infoseason = getdate($sto);
1173 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1174 if ($infoseason[0] > $leapts) {
1175 /**
1176 * Timestamp must be greater than the leap-day of Feb 29th.
1177 * It used to be checked for >= $leapts.
1178 *
1179 * @since July 3rd 2019
1180 */
1181 $sto += 86400;
1182 }
1183 }
1184 return array($sfrom, $sto);
1185 }
1186
1187 public static function sortSeasonsRangeTs ($all_seasons) {
1188 $sorted = array();
1189 $map = array();
1190 foreach ($all_seasons as $key => $season) {
1191 $map[$key] = $season['from_ts'];
1192 }
1193 asort($map);
1194 foreach ($map as $key => $s) {
1195 $sorted[] = $all_seasons[$key];
1196 }
1197 return $sorted;
1198 }
1199
1200 public static function formatSeasonDates ($from_ts, $to_ts) {
1201 $one = getdate($from_ts);
1202 $two = getdate($to_ts);
1203 $months_map = array(
1204 1 => JText::translate('VBSHORTMONTHONE'),
1205 2 => JText::translate('VBSHORTMONTHTWO'),
1206 3 => JText::translate('VBSHORTMONTHTHREE'),
1207 4 => JText::translate('VBSHORTMONTHFOUR'),
1208 5 => JText::translate('VBSHORTMONTHFIVE'),
1209 6 => JText::translate('VBSHORTMONTHSIX'),
1210 7 => JText::translate('VBSHORTMONTHSEVEN'),
1211 8 => JText::translate('VBSHORTMONTHEIGHT'),
1212 9 => JText::translate('VBSHORTMONTHNINE'),
1213 10 => JText::translate('VBSHORTMONTHTEN'),
1214 11 => JText::translate('VBSHORTMONTHELEVEN'),
1215 12 => JText::translate('VBSHORTMONTHTWELVE')
1216 );
1217 $mday_map = array(
1218 1 => JText::translate('VBMDAYFRIST'),
1219 2 => JText::translate('VBMDAYSECOND'),
1220 3 => JText::translate('VBMDAYTHIRD'),
1221 'generic' => JText::translate('VBMDAYNUMGEN')
1222 );
1223 if ($one['year'] == $two['year']) {
1224 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>';
1225 }
1226 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'];
1227 }
1228
1229 public static function getFirstCustDataField($custdata) {
1230 $first_field = '';
1231 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1232 //Room is closed with this booking
1233 return '----';
1234 }
1235 $parts = explode("\n", $custdata);
1236 foreach ($parts as $part) {
1237 if (!empty($part)) {
1238 $field = explode(':', trim($part));
1239 if (!empty($field[1])) {
1240 return trim($field[1]);
1241 }
1242 }
1243 }
1244 return $first_field;
1245 }
1246
1247 /**
1248 * This method composes a string to be logged for the admin
1249 * to keep track of what was inside the booking before the
1250 * modification. Returns a string and it uses language definitions
1251 * that should be available on the front-end and back-end INI files.
1252 *
1253 * @param array $old_booking the array of the booking prior to the modification
1254 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1255 *
1256 * @return string text describing the situation with the booking before the changes.
1257 *
1258 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1259 */
1260 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1261 {
1262 $vbo_df = self::getDateFormat();
1263 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1264 $wdays_map = array(
1265 JText::translate('VBWEEKDAYZERO'),
1266 JText::translate('VBWEEKDAYONE'),
1267 JText::translate('VBWEEKDAYTWO'),
1268 JText::translate('VBWEEKDAYTHREE'),
1269 JText::translate('VBWEEKDAYFOUR'),
1270 JText::translate('VBWEEKDAYFIVE'),
1271 JText::translate('VBWEEKDAYSIX'),
1272 );
1273 $now_info = getdate();
1274 $checkin_info = getdate($old_booking['checkin']);
1275 $checkout_info = getdate($old_booking['checkout']);
1276
1277 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1278 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1279 $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]);
1280
1281 $orooms_map = [];
1282 $orooms_arr = [];
1283
1284 // scan the previously booked room records
1285 foreach ((array) ($old_booking['rooms_info'] ?? []) as $oroom) {
1286 // determine room name
1287 $roomName = $oroom['name'] ?? $oroom['roomnamevb'] ?? $oroom['idroom'] ?? '---';
1288 $orooms_arr[] = sprintf(
1289 '%s, %s: %d, %s: %d',
1290 $roomName,
1291 JText::translate('VBMAILADULTS'),
1292 (int) ($oroom['adults'] ?? 0),
1293 JText::translate('VBMAILCHILDREN'),
1294 (int) ($oroom['children'] ?? 0),
1295 );
1296 if (!empty($oroom['idroom'])) {
1297 $orooms_map[$oroom['idroom']] = $roomName;
1298 }
1299 }
1300
1301 // build previous rooms booked string
1302 $prev_rooms = $orooms_arr ? implode("\n", $orooms_arr) : '';
1303
1304 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && $orooms_map) {
1305 $split_stay_prev_infos = [];
1306 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1307 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1308 continue;
1309 }
1310 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1311 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1312 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1313
1314 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1315 if (!empty($room_nights)) {
1316 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1317 }
1318 $split_stay_descr .= ', ';
1319 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1320
1321 // push split stay description string
1322 $split_stay_prev_infos[] = $split_stay_descr;
1323 }
1324 if (count($split_stay_prev_infos)) {
1325 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1326 }
1327 }
1328
1329 $currencyname = self::getCurrencyName();
1330 $prev_total = self::formatCurrencyNumber(self::numberFormat($old_booking['total']), $currencyname);
1331
1332 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1333 }
1334
1335 /**
1336 * This method invokes the class
1337 * VikChannelManagerLogos (new in VCM 1.6.4) to
1338 * map the name of a channel to its corresponding logo.
1339 * The method can also be used to get an istance of the class.
1340 *
1341 * @param mixed $provenience either a string or an array with main and sub channels.
1342 * @param boolean $get_istance
1343 *
1344 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1345 */
1346 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1347 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1348 return false;
1349 }
1350 if (!class_exists('VikChannelManagerLogos')) {
1351 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1352 }
1353
1354 /**
1355 * Due to the new iCal channel we now support main and sub channels.
1356 *
1357 * @since 1.13 - VCM 1.7.0
1358 */
1359 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1360 $provenience = explode('_', $provenience);
1361 }
1362 $main_channel = '';
1363 $full_channel = '';
1364 if (is_array($provenience)) {
1365 if (count($provenience) > 1) {
1366 $main_channel = $provenience[1];
1367 } else {
1368 $main_channel = $provenience[0];
1369 }
1370 if (stripos($provenience[0], 'ical') !== false) {
1371 $full_channel = implode('_', $provenience);
1372 }
1373 } else {
1374 $main_channel = $provenience;
1375 }
1376
1377 // get object instance by passing the main provenience
1378 $obj = new VikChannelManagerLogos($main_channel);
1379
1380 // update provenience with main and full channel
1381 if (!empty($full_channel)) {
1382 $obj->setProvenience($main_channel, $full_channel);
1383 }
1384
1385 // return either the instance or the logo URL for this channel source
1386 return $get_istance ? $obj : $obj->getLogoURL();
1387 }
1388
1389 public static function vcmAutoUpdate()
1390 {
1391 static $autoUpdate = null;
1392
1393 if ($autoUpdate !== null) {
1394 return $autoUpdate;
1395 }
1396
1397 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1398 $autoUpdate = -1;
1399 return $autoUpdate;
1400 }
1401
1402 $autoUpdate = (int) VBOFactory::getConfig()->getBool('vcmautoupd', false);
1403
1404 return $autoUpdate;
1405 }
1406
1407 public static function getVcmInvoker()
1408 {
1409 if (!class_exists('VboVcmInvoker')) {
1410 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1411 }
1412
1413 return new VboVcmInvoker();
1414 }
1415
1416 /**
1417 * Returns an instance of VikBooking History object.
1418 *
1419 * @param int $bid optional booking ID to bind.
1420 *
1421 * @return VboBookingHistory
1422 *
1423 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1424 */
1425 public static function getBookingHistoryInstance($bid = 0)
1426 {
1427 if (!class_exists('VboBookingHistory')) {
1428 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1429 }
1430
1431 return new VboBookingHistory($bid);
1432 }
1433
1434 /**
1435 * Returns an instance of the VCMChatHandler class to handle
1436 * the messaging/chat for the given reservation ID.
1437 *
1438 * @param mixed $oid VBO booking ID or a linear array containing ID and secret key (timestamp).
1439 * @param string $channel the name of the source channel
1440 *
1441 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1442 *
1443 * @since 1.11.2 (J) - 1.1.2 (WP)
1444 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1445 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1446 * @since 1.17.6 (J) - 1.7.6 (WP) empty signature arguments will only require the dependencies.
1447 * @since 1.18.13 (J) - 1.8.13 (WP) added support to $oid argument as either int or array.
1448 */
1449 public static function getVcmChatInstance($oid, $channel = null)
1450 {
1451 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1452 if (!is_file($vcm_messaging_helper)) {
1453 // VCM is not available
1454 return null;
1455 }
1456
1457 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1458 // VCM must be inactive, prevent the loading of the instance
1459 return null;
1460 }
1461
1462 // make sure the channel name is correct as it may contain the sub-network
1463 if (!empty($channel)) {
1464 $segments = explode('_', $channel);
1465 if (count($segments) > 1) {
1466 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1467 $channel = $segments[0];
1468 }
1469 }
1470
1471 // always require main file of the abstract class even if arguments are empty/invalid
1472 require_once $vcm_messaging_helper;
1473
1474 if (empty($oid) && empty($channel)) {
1475 // do not proceed in order to save resources
1476 return null;
1477 }
1478
1479 // return the instance of the class for this channel handler
1480 return VCMChatHandler::getInstance($oid, $channel);
1481 }
1482
1483 /**
1484 * Returns an instance of the VCMOpportunityHandler class to handle
1485 * the various opportunities through VCM.
1486 *
1487 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1488 *
1489 * @since 1.2.0
1490 */
1491 public static function getVcmOpportunityInstance() {
1492 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1493 if (!is_file($vcm_opp_helper)) {
1494 // VCM is not available
1495 return null;
1496 }
1497 // require main file of the class
1498 require_once $vcm_opp_helper;
1499 // return the instance of the class
1500 return VCMOpportunityHandler::getInstance();
1501 }
1502
1503 /**
1504 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1505 *
1506 * @param array $data optional data to bind to the object.
1507 *
1508 * @return bool
1509 *
1510 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1511 *
1512 * @requires VCM >= 1.8.24
1513 */
1514 public static function vcmBcomReportingSupported(array $data = [])
1515 {
1516 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1517 }
1518
1519 /**
1520 * Gets a list of all channels supporting the promotions.
1521 *
1522 * @param string $key the key of the handler.
1523 *
1524 * @return mixed false if VCM is not installed, empty array otherwise.
1525 *
1526 * @requires Vik Channel Manager 1.7.1
1527 *
1528 * @since 1.3.0
1529 */
1530 public static function getPromotionHandlers($key = null)
1531 {
1532 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1533 // VCM not installed
1534 return false;
1535 }
1536
1537 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1538 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1539 return [];
1540 }
1541
1542 return VikChannelManager::getPromotionHandlers($key);
1543 }
1544
1545 /**
1546 * Gets the factors for suggesting the application of the promotions.
1547 *
1548 * @param mixed $data some optional instructions to be passed as argument.
1549 *
1550 * @return mixed false if VCM is not installed, associative array otherwise.
1551 *
1552 * @requires Vik Channel Manager 1.7.1
1553 *
1554 * @since 1.3.0
1555 */
1556 public static function getPromotionFactors($data = null)
1557 {
1558 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1559 // VCM not installed
1560 return false;
1561 }
1562
1563 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1564 // VCM is outdated
1565 return false;
1566 }
1567
1568 return VikChannelManager::getPromotionFactors($data);
1569 }
1570
1571 public static function getTheme()
1572 {
1573 return VBOFactory::getConfig()->get('theme', '');
1574 }
1575
1576 public static function getFooterOrdMail($vbo_tn = null)
1577 {
1578 $dbo = JFactory::getDbo();
1579
1580 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1581 $dbo->setQuery($q);
1582 $ft = $dbo->loadAssocList();
1583
1584 if (!is_object($vbo_tn)) {
1585 $vbo_tn = self::getTranslator();
1586 }
1587
1588 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1589
1590 return $ft ? $ft[0]['setting'] : '';
1591 }
1592
1593 public static function requireLogin()
1594 {
1595 return VBOFactory::getConfig()->getBool('requirelogin', false);
1596 }
1597
1598 public static function autoRoomUnit()
1599 {
1600 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1601 }
1602
1603 public static function todayBookings()
1604 {
1605 static $todayBookings = null;
1606
1607 if ($todayBookings === null) {
1608 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1609 }
1610
1611 return $todayBookings;
1612 }
1613
1614 public static function couponsEnabled()
1615 {
1616 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1617 }
1618
1619 public static function customersPinEnabled()
1620 {
1621 return VBOFactory::getConfig()->getBool('enablepin', false);
1622 }
1623
1624 /**
1625 * Detects the type of visitor from the user agent.
1626 * Known types are: computer, smartphone, tablet.
1627 *
1628 * @param boolean $returnua whether the type of visitor should be returned. If false
1629 * boolean is returned in case of mobile device detected.
1630 * @param boolean $loadassets whether the system should load an apposite CSS file.
1631 *
1632 * @return mixed string for the type of visitor or boolean if mobile detected.
1633 *
1634 * @since 1.0.13 - Revision September 2018
1635 */
1636 public static function detectUserAgent($returnua = false, $loadassets = true) {
1637 $session = JFactory::getSession();
1638 $sval = $session->get('vbuseragent', '');
1639 $mobiles = array('tablet', 'smartphone');
1640 if (!empty($sval)) {
1641 if ($loadassets) {
1642 self::userAgentStyleSheet($sval);
1643 }
1644 return $returnua ? $sval : in_array($sval, $mobiles);
1645 }
1646
1647 // detect visitor (device) type
1648 $visitoris = 'computer';
1649 try {
1650 if (!class_exists('MobileDetector')) {
1651 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1652 }
1653 $detector = new MobileDetector;
1654 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1655 } catch (Exception $e) {
1656 // do nothing
1657 }
1658
1659 $session->set('vbuseragent', $visitoris);
1660 if ($loadassets) {
1661 self::userAgentStyleSheet($visitoris);
1662 }
1663
1664 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1665 }
1666
1667 public static function userAgentStyleSheet($ua)
1668 {
1669 /**
1670 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1671 */
1672 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1673 return;
1674 }
1675 //
1676
1677 $document = JFactory::getDocument();
1678 /**
1679 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1680 */
1681 if ($ua == 'smartphone') {
1682 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1683 } elseif ($ua == 'tablet') {
1684 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1685 }
1686 return true;
1687 }
1688
1689 public static function loadJquery()
1690 {
1691 return VBOFactory::getConfig()->getBool('loadjquery', true);
1692 }
1693
1694 public static function loadBootstrap()
1695 {
1696 return VBOFactory::getConfig()->getBool('bootstrap', true);
1697 }
1698
1699 public static function allowMultiLanguage()
1700 {
1701 return VBOFactory::getConfig()->getBool('multilang', true);
1702 }
1703
1704 public static function getTranslator()
1705 {
1706 if (!class_exists('VikBookingTranslator')) {
1707 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1708 }
1709
1710 return new VikBookingTranslator();
1711 }
1712
1713 /**
1714 * New method's name spelled correctly. The object will be cached by default.
1715 *
1716 * @return VikBookingCustomersPin
1717 *
1718 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1719 */
1720 public static function getCPinInstance()
1721 {
1722 static $cpin_instance = null;
1723
1724 if (!$cpin_instance) {
1725 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1726
1727 $cpin_instance = new VikBookingCustomersPin();
1728 }
1729
1730 return $cpin_instance;
1731 }
1732
1733 /**
1734 * Old method's name spelled wrongly for BC.
1735 *
1736 * @return VikBookingCustomersPin
1737 */
1738 public static function getCPinIstance()
1739 {
1740 return self::getCPinInstance();
1741 }
1742
1743 /**
1744 * Returns an instance of the VikBookingTracker Class.
1745 * It is also possible to call this method to just require the library.
1746 * This is useful for the back-end to access some static methods
1747 * without tracking any data.
1748 *
1749 * @param boolean $require_only whether to return the object.
1750 *
1751 * @return VikBookingTracker
1752 *
1753 * @since 1.11
1754 */
1755 public static function getTracker($require_only = false) {
1756 if (!class_exists('VikBookingTracker')) {
1757 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1758 }
1759 return $require_only ? true : VikBookingTracker::getInstance();
1760 }
1761
1762 /**
1763 * Returns an instance of the VikBookingOperator Class.
1764 *
1765 * @return VikBookingOperator
1766 *
1767 * @since 1.11
1768 */
1769 public static function getOperatorInstance() {
1770 if (!class_exists('VikBookingOperator')) {
1771 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1772 }
1773 return VikBookingOperator::getInstance();
1774 }
1775
1776 /**
1777 * Returns an instance of the VikBookingFestivities Class.
1778 *
1779 * @return VikBookingFestivities
1780 *
1781 * @since 1.12
1782 */
1783 public static function getFestivitiesInstance() {
1784 if (!class_exists('VikBookingFestivities')) {
1785 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1786 }
1787 return VikBookingFestivities::getInstance();
1788 }
1789
1790 /**
1791 * Returns an instance of the VikBookingCriticalDates Class.
1792 *
1793 * @return VikBookingCriticalDates
1794 *
1795 * @since 1.13.5
1796 */
1797 public static function getCriticalDatesInstance() {
1798 if (!class_exists('VikBookingCriticalDates')) {
1799 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1800 }
1801 return VikBookingCriticalDates::getInstance();
1802 }
1803
1804 /**
1805 * Checks whether the chat is enabled.
1806 *
1807 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1808 *
1809 * @since 1.12
1810 */
1811 public static function chatEnabled() {
1812 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1813 return -1;
1814 }
1815 $dbo = JFactory::getDbo();
1816 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1817 $dbo->setQuery($q);
1818 $dbo->execute();
1819 if (!$dbo->getNumRows()) {
1820 // enable the chat by default if VCM is installed
1821 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1822 $dbo->setQuery($q);
1823 $dbo->execute();
1824 return 1;
1825 }
1826 $s = $dbo->loadResult();
1827 return intval($s);
1828 }
1829
1830 /**
1831 * Loads the chat parameters from the configuration.
1832 *
1833 * @return object stdClass object from decoded JSON string
1834 *
1835 * @since 1.12
1836 */
1837 public static function getChatParams() {
1838 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1839 return new stdClass;
1840 }
1841 $dbo = JFactory::getDbo();
1842 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1843 $dbo->setQuery($q);
1844 $dbo->execute();
1845 if (!$dbo->getNumRows()) {
1846 // compose default and basic chat params for the first time
1847 $basic_params = new stdClass;
1848 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1849 $basic_params->av_type = 'checkin';
1850 $basic_params->av_days = 0;
1851
1852 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1853 $dbo->setQuery($q);
1854 $dbo->execute();
1855
1856 return $basic_params;
1857 }
1858 return json_decode($dbo->loadResult());
1859 }
1860
1861 /**
1862 * Checks whether the pre-checkin is enabled.
1863 *
1864 * @return int 0 if disabled, 1 otherwise
1865 *
1866 * @since 1.12
1867 */
1868 public static function precheckinEnabled()
1869 {
1870 return (int) VBOFactory::getConfig()->getBool('precheckinenabled', true);
1871 }
1872
1873 /**
1874 * Returns the minimum number of days in advance to
1875 * enable the pre-checkin via front-end.
1876 *
1877 * @return int the min number of days in advance for pre-checkin.
1878 *
1879 * @since 1.12
1880 */
1881 public static function precheckinMinOffset()
1882 {
1883 return VBOFactory::getConfig()->getInt('precheckinminoffset', 0);
1884 }
1885
1886 /**
1887 * Whether upselling extra services is enabled.
1888 *
1889 * @return boolean true if enabled, false otherwise.
1890 *
1891 * @since 1.13
1892 */
1893 public static function upsellingEnabled()
1894 {
1895 return VBOFactory::getConfig()->getBool('upselling', true);
1896 }
1897
1898 /**
1899 * Collects a list of options/extra that can be upsold for the
1900 * the rooms booked.
1901 *
1902 * @param array $upsell_data list of stdClass objects for each room booked.
1903 * @param array $info some booking details (id, checkin, checkout).
1904 * @param object $vbo_tn the translation object.
1905 *
1906 * @return array list of upsellable options for each room booked.
1907 *
1908 * @since 1.13 (J) - 1.3.0 (WP)
1909 * @since 1.17.7 (J) - 1.7.7 (WP) added support for damage deposit to confirmed bookings.
1910 */
1911 public static function loadUpsellingData($upsell_data, $info, $vbo_tn)
1912 {
1913 $dbo = JFactory::getDbo();
1914
1915 // get all rooms booked
1916 $all_room_ids = [];
1917 foreach ($upsell_data as $v) {
1918 if (!in_array($v->id, $all_room_ids)) {
1919 array_push($all_room_ids, (int) $v->id);
1920 }
1921 }
1922
1923 $dbo->setQuery(
1924 $dbo->getQuery(true)
1925 ->select([
1926 $dbo->qn('id'),
1927 $dbo->qn('idopt'),
1928 ])
1929 ->from($dbo->qn('#__vikbooking_rooms'))
1930 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_room_ids) . ')')
1931 );
1932 $records = $dbo->loadAssocList();
1933 if (!$records) {
1934 // no rooms found
1935 return [];
1936 }
1937
1938 // get all suitable options
1939 $all_options = [];
1940 $rooms_options = [];
1941 foreach ($records as $v) {
1942 $allopts = explode(';', $v['idopt']);
1943 $room_opt = [];
1944 foreach ($allopts as $o) {
1945 if (empty($o)) {
1946 continue;
1947 }
1948 if (!in_array($o, $all_options)) {
1949 array_push($all_options, (int) $o);
1950 }
1951 array_push($room_opt, (int) $o);
1952 }
1953 $rooms_options[$v['id']] = $room_opt;
1954 }
1955 if (!$all_options) {
1956 // no options found
1957 return [];
1958 }
1959
1960 // load all options that could be used by the booked rooms no matter what was already booked
1961 $dbo->setQuery(
1962 $dbo->getQuery(true)
1963 ->select('*')
1964 ->from($dbo->qn('#__vikbooking_optionals'))
1965 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1966 ->where($dbo->qn('forcesel') . ' = 0')
1967 ->where($dbo->qn('ifchildren') . ' = 0')
1968 ->where($dbo->qn('is_citytax') . ' = 0')
1969 ->where($dbo->qn('is_fee') . ' = 0')
1970 ->order($dbo->qn('ordering') . ' ASC')
1971 );
1972 $records = $dbo->loadAssocList();
1973
1974 if (!strcasecmp(($info['status'] ?? ''), 'confirmed')) {
1975 // merge mandatory damage deposit
1976 $dbo->setQuery(
1977 $dbo->getQuery(true)
1978 ->select('*')
1979 ->from($dbo->qn('#__vikbooking_optionals'))
1980 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1981 ->where($dbo->qn('forcesel') . ' = 1')
1982 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
1983 );
1984 $dd_records = $dbo->loadAssocList();
1985
1986 // merge with regular options
1987 $records = array_merge($records, $dd_records);
1988 }
1989
1990 if (!$records) {
1991 // no upsell-able options found
1992 return [];
1993 }
1994
1995 // filter options by available date and translate records
1996 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1997 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1998 $records = !is_array($records) ? [] : $records;
1999
2000 $tot_upsellable = 0;
2001 foreach ($upsell_data as $k => $rdata) {
2002 if (!isset($upsell_data[$k]->upsellable)) {
2003 $upsell_data[$k]->upsellable = [];
2004 }
2005 foreach ($records as $opt) {
2006 if (!empty($opt['ageintervals'])) {
2007 // upsellable options should not contain age intervals for children
2008 continue;
2009 }
2010 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
2011 // this option is not assigned to this room
2012 continue;
2013 }
2014 // check if the option is suited for this room party
2015 $clone_opt = [$opt];
2016 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
2017 if (!is_array($clone_opt) || !$clone_opt) {
2018 // this option is not suited for this room party
2019 continue;
2020 }
2021
2022 if (in_array($opt['id'], $rdata->options)) {
2023 // this option has already been booked
2024 continue;
2025 }
2026
2027 // push this option and increase counter
2028 array_push($upsell_data[$k]->upsellable, $opt);
2029 $tot_upsellable++;
2030 }
2031 }
2032
2033 // if no upsellable options were found, we return an empty array
2034 return $tot_upsellable > 0 ? $upsell_data : [];
2035 }
2036
2037 /**
2038 * Returns the minimum days in advance for booking, by considering by default
2039 * also the property closing dates. If the property is currently closed, then
2040 * the minimum number of days in advance will be increased.
2041 *
2042 * @param bool $no_closing_dates whether to skip checking closing dates.
2043 *
2044 * @return int the number of days in advance for booking.
2045 *
2046 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
2047 * getMinDaysAdvance($skipsession = false) to:
2048 * getMinDaysAdvance($no_closing_dates = false)
2049 * This was made to return the proper number of days in advance
2050 * in case the property is currently closed.
2051 */
2052 public static function getMinDaysAdvance($no_closing_dates = false)
2053 {
2054 // cache value in static var
2055 static $getMinDaysAdvance = null;
2056
2057 if ($getMinDaysAdvance) {
2058 return $getMinDaysAdvance;
2059 }
2060
2061 $mind = VBOFactory::getConfig()->getInt('mindaysadvance', 0);
2062
2063 // update cached var
2064 $getMinDaysAdvance = $mind;
2065
2066 if ($no_closing_dates) {
2067 // do not check the closing dates
2068 return $mind;
2069 }
2070
2071 // check if the property is currently closed
2072 $cur_closed_dates = self::getClosingDates();
2073 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
2074 $today_midnight = mktime(0, 0, 0);
2075 $closed_until = null;
2076 foreach ($cur_closed_dates as $kcd => $vcd) {
2077 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
2078 // closing period found
2079 $closed_until = $vcd['to'];
2080 break;
2081 }
2082 }
2083 if ($closed_until !== null) {
2084 // count the number of days until property is closed
2085 $mind = 0;
2086 $today_info = getdate($today_midnight);
2087 while ($today_info[0] <= $closed_until) {
2088 $mind++;
2089 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
2090 }
2091 }
2092 }
2093
2094 // update cached var
2095 $getMinDaysAdvance = $mind;
2096
2097 return $mind;
2098 }
2099
2100 public static function getDefaultNightsCalendar()
2101 {
2102 return VBOFactory::getConfig()->getInt('autodefcalnights', 1);
2103 }
2104
2105 public static function getSearchNumRooms($skipsession = true)
2106 {
2107 $dbo = JFactory::getDbo();
2108 if ($skipsession) {
2109 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2110 $dbo->setQuery($q);
2111 $dbo->execute();
2112 $s = $dbo->loadAssocList();
2113 return (int)$s[0]['setting'];
2114 }
2115 $session = JFactory::getSession();
2116 $sval = $session->get('vbsearchNumRooms', '');
2117 if (!empty($sval)) {
2118 return (int)$sval;
2119 }
2120 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2121 $dbo->setQuery($q);
2122 $dbo->execute();
2123 $s = $dbo->loadAssocList();
2124 $session->set('vbsearchNumRooms', $s[0]['setting']);
2125 return (int)$s[0]['setting'];
2126 }
2127
2128 public static function getSearchNumAdults($skipsession = true)
2129 {
2130 $dbo = JFactory::getDbo();
2131 if ($skipsession) {
2132 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2133 $dbo->setQuery($q);
2134 $dbo->execute();
2135 $s = $dbo->loadAssocList();
2136 return $s[0]['setting'];
2137 }
2138 $session = JFactory::getSession();
2139 $sval = $session->get('vbsearchNumAdults', '');
2140 if (!empty($sval)) {
2141 return $sval;
2142 }
2143 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2144 $dbo->setQuery($q);
2145 $dbo->execute();
2146 $s = $dbo->loadAssocList();
2147 $session->set('vbsearchNumAdults', $s[0]['setting']);
2148 return $s[0]['setting'];
2149 }
2150
2151 public static function getSearchNumChildren($skipsession = true)
2152 {
2153 $dbo = JFactory::getDbo();
2154 if ($skipsession) {
2155 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2156 $dbo->setQuery($q);
2157 $dbo->execute();
2158 $s = $dbo->loadAssocList();
2159 return $s[0]['setting'];
2160 }
2161 $session = JFactory::getSession();
2162 $sval = $session->get('vbsearchNumChildren', '');
2163 if (!empty($sval)) {
2164 return $sval;
2165 }
2166 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2167 $dbo->setQuery($q);
2168 $dbo->execute();
2169 $s = $dbo->loadAssocList();
2170 $session->set('vbsearchNumChildren', $s[0]['setting']);
2171 return $s[0]['setting'];
2172 }
2173
2174 public static function getSmartSearchType($skipsession = true)
2175 {
2176 $dbo = JFactory::getDbo();
2177 if ($skipsession) {
2178 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2179 $dbo->setQuery($q);
2180 $dbo->execute();
2181 $s = $dbo->loadAssocList();
2182 return $s[0]['setting'];
2183 }
2184 $session = JFactory::getSession();
2185 $sval = $session->get('vbsmartSearchType', '');
2186 if (!empty($sval)) {
2187 return $sval;
2188 }
2189 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2190 $dbo->setQuery($q);
2191 $dbo->execute();
2192 $s = $dbo->loadAssocList();
2193 $session->set('vbsmartSearchType', $s[0]['setting']);
2194 return $s[0]['setting'];
2195 }
2196
2197 /**
2198 * Retrieves the listing-level minimum advance booking offset.
2199 *
2200 * @param int $idroom The listing ID to evaluate.
2201 * @param bool $period Whether to format the value as a period string.
2202 *
2203 * @return int|string|null Number of hours or period string ("2H"), if set.
2204 *
2205 * @since 1.18.3 (J) - 1.8.3 (WP)
2206 */
2207 public static function getMinDateFutureListing($idroom, $period = false)
2208 {
2209 $config = VBOFactory::getConfig();
2210
2211 $room_level_min_notice = $config->get("room_{$idroom}_min_adv_notice", null);
2212
2213 if (is_null($room_level_min_notice) || $room_level_min_notice === '') {
2214 return null;
2215 }
2216
2217 if ($period) {
2218 return sprintf('%dH', (int) $room_level_min_notice);
2219 }
2220
2221 return (int) $room_level_min_notice;
2222 }
2223
2224 /**
2225 * Returns the maximum advance booking notice (offset) as a period.
2226 * It either returns the global configuration setting, or the value
2227 * defined at room-level, if any and if requested.
2228 *
2229 * @param int $idroom optional room ID for room-level setting.
2230 *
2231 * @return string the maximum booking notice period (i.e. +1Y).
2232 *
2233 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
2234 * $idroom to support room-level settings.
2235 */
2236 public static function getMaxDateFuture($idroom = 0)
2237 {
2238 static $max_adv_book_pool = [];
2239
2240 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
2241
2242 if (isset($max_adv_book_pool[$pool_id])) {
2243 return $max_adv_book_pool[$pool_id];
2244 }
2245
2246 $config = VBOFactory::getConfig();
2247
2248 if ($pool_id === 'global') {
2249 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
2250 } else {
2251 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
2252 if (!empty($room_level_max_notice)) {
2253 // setting is defined at room-level
2254 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2255 } else {
2256 // recurse for global setting
2257 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2258 }
2259 }
2260
2261 return $max_adv_book_pool[$pool_id];
2262 }
2263
2264 /**
2265 * Validates the maximum advance booking notice against the requested check-in
2266 * timestamp. The method supports the validation at both room and property levels.
2267 *
2268 * @param int $checkints the check-in date timestamp.
2269 * @param int $idroom the optional room ID for room-level validation.
2270 *
2271 * @return string error date string in case of validation failure.
2272 *
2273 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2274 */
2275 public static function validateMaxDateBookings($checkints, $idroom = 0)
2276 {
2277 $datelim = self::getMaxDateFuture($idroom);
2278 $datelim = empty($datelim) ? '+2y' : $datelim;
2279 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2280 $quantlim = substr($datelim, -1, 1);
2281
2282 $now = getdate();
2283 if ($quantlim == 'w') {
2284 $until_ts = strtotime("+$numlim weeks") + 86399;
2285 } else {
2286 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2287 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2288 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2289 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2290 }
2291
2292 if ($until_ts > $now[0] && $checkints > $until_ts) {
2293 $vbo_df = self::getDateFormat();
2294 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2295
2296 // error, return the maximum date in the future allowed
2297 return date($df, $until_ts);
2298 }
2299
2300 // validation passed
2301 return '';
2302 }
2303
2304 /**
2305 * Validates the minimum advance booking notice against the requested check-in
2306 * timestamp. The method supports the validation at both room and property levels.
2307 *
2308 * @param int $checkints The check-in date timestamp.
2309 * @param int $idroom Optional room ID for room-level validation.
2310 *
2311 * @return int|float Minimum days (int) or hours (float) in advance required.
2312 *
2313 * @since 1.18.3 (J) - 1.8.3 (WP) introduced $idroom argument for room-level.
2314 */
2315 public static function validateMinDaysAdvance($checkints, $idroom = 0)
2316 {
2317 if ($idroom) {
2318 // listing-level minimum advance booking notice (hours, could be null or 0)
2319 $minhadv = self::getMinDateFutureListing($idroom);
2320
2321 // perform listing-level validation only when value is set
2322 if (!$minhadv) {
2323 // validation passed
2324 return '';
2325 }
2326
2327 // access listing params
2328 $listing_data = self::getRoomInfo($idroom, ['params'], $no_cache = true);
2329 if ($listing_data['params'] ?? null) {
2330 // access listing-level check-in time
2331 $listing_checkin = self::getRoomParam('checkin', $listing_data['params']);
2332 if ($listing_checkin) {
2333 // overwrite check-in time
2334 $listing_checkin_parts = explode(':', $listing_checkin);
2335 $checkin_hours = (int) $listing_checkin_parts[0];
2336 $checkin_minutes = (int) $listing_checkin_parts[1];
2337 // read current check-in date-time
2338 $tsinfo = getdate($checkints);
2339 // update check-in time with proper hours, minutes and seconds
2340 $checkints = mktime($checkin_hours, $checkin_minutes, 0, $tsinfo['mon'], $tsinfo['mday'], $tsinfo['year']);
2341 }
2342 }
2343
2344 if ($checkints < strtotime('+' . $minhadv . ($minhadv > 1 ? 'hours' : 'hour'))) {
2345 return $minhadv >= 24 ? floor($minhadv / 24) : ($minhadv / 24);
2346 }
2347
2348 // validation passed without checking the global settings when listing ID given
2349 return '';
2350 }
2351
2352 // global validation
2353 $mindadv = self::getMinDaysAdvance(true);
2354 if ($mindadv > 0) {
2355 $tsinfo = getdate($checkints);
2356 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2357 if ($checkints < $limit_ts) {
2358 return $mindadv;
2359 }
2360 }
2361
2362 // validation passed
2363 return '';
2364 }
2365
2366 /**
2367 * The only supported calendar type has been changed to jQuery UI.
2368 *
2369 * @since 1.13
2370 */
2371 public static function calendarType($skipsession = false)
2372 {
2373 return 'jqueryui';
2374 }
2375
2376 public static function getSiteLogo()
2377 {
2378 static $siteLogo = null;
2379
2380 if ($siteLogo !== null) {
2381 return $siteLogo;
2382 }
2383
2384 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2385
2386 return $siteLogo;
2387 }
2388
2389 public static function getBackendLogo()
2390 {
2391 static $backLogo = null;
2392
2393 if ($backLogo !== null) {
2394 return $backLogo;
2395 }
2396
2397 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2398
2399 return $backLogo;
2400 }
2401
2402 public static function numCalendars()
2403 {
2404 static $numCalendars = null;
2405
2406 if ($numCalendars !== null) {
2407 return $numCalendars;
2408 }
2409
2410 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2411
2412 return $numCalendars;
2413 }
2414
2415 public static function getFirstWeekDay()
2416 {
2417 static $firstWeekDay = null;
2418
2419 if ($firstWeekDay !== null) {
2420 return $firstWeekDay;
2421 }
2422
2423 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2424
2425 return $firstWeekDay;
2426 }
2427
2428 public static function showPartlyReserved()
2429 {
2430 static $partlyReserved = null;
2431
2432 if ($partlyReserved !== null) {
2433 return $partlyReserved;
2434 }
2435
2436 $partlyReserved = VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2437
2438 return $partlyReserved;
2439 }
2440
2441 public static function showStatusCheckinoutOnly()
2442 {
2443 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2444 }
2445
2446 public static function getDisclaimer($vbo_tn = null)
2447 {
2448 $dbo = JFactory::getDbo();
2449
2450 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2451 $dbo->setQuery($q);
2452 $ft = $dbo->loadAssocList();
2453 if (!$ft) {
2454 return '';
2455 }
2456
2457 if (!is_object($vbo_tn)) {
2458 $vbo_tn = self::getTranslator();
2459 }
2460 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2461
2462 return $ft[0]['setting'];
2463 }
2464
2465 public static function showFooter()
2466 {
2467 return VBOFactory::getConfig()->getBool('showfooter', false);
2468 }
2469
2470 public static function getPriceName($idp, $vbo_tn = null)
2471 {
2472 $dbo = JFactory::getDbo();
2473
2474 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2475 $dbo->setQuery($q, 0, 1);
2476 $n = $dbo->loadAssocList();
2477 if ($n) {
2478 if (is_object($vbo_tn)) {
2479 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2480 }
2481 return $n[0]['name'];
2482 }
2483
2484 return "";
2485 }
2486
2487 public static function getPriceAttr($idp, $vbo_tn = null)
2488 {
2489 $dbo = JFactory::getDbo();
2490
2491 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2492 $dbo->setQuery($q, 0, 1);
2493 $n = $dbo->loadAssocList();
2494 if ($n) {
2495 if (is_object($vbo_tn)) {
2496 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2497 }
2498 return $n[0]['attr'];
2499 }
2500
2501 return "";
2502 }
2503
2504 /**
2505 * Fetches the requested rate plan information.
2506 *
2507 * @param int $idp the rate plan ID.
2508 * @param object $vbo_tn the Vik Booking translator object.
2509 *
2510 * @return array empty array or associative record.
2511 */
2512 public static function getPriceInfo($idp, $vbo_tn = null)
2513 {
2514 // cache values
2515 static $priceInfos = [];
2516
2517 $idp = (int)$idp;
2518
2519 if ($priceInfos && isset($priceInfos[$idp])) {
2520 return $priceInfos[$idp];
2521 }
2522
2523 $dbo = JFactory::getDbo();
2524
2525 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2526 $dbo->setQuery($q, 0, 1);
2527 $rplan = $dbo->loadAssocList();
2528 if (!$rplan) {
2529 return [];
2530 }
2531
2532 if (is_object($vbo_tn)) {
2533 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2534 }
2535
2536 // set cached value
2537 $priceInfos[$idp] = $rplan[0];
2538
2539 return $rplan[0];
2540 }
2541
2542 public static function getAliq($idal)
2543 {
2544 $dbo = JFactory::getDbo();
2545
2546 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2547 $dbo->setQuery($q);
2548 $aliq = $dbo->loadResult();
2549 if ($aliq) {
2550 return $aliq;
2551 }
2552
2553 return 0;
2554 }
2555
2556 public static function getTimeOpenStore($skipsession = false)
2557 {
2558 // cache value in static var
2559 static $getTimeOpenStore = null;
2560
2561 if ($getTimeOpenStore) {
2562 return $getTimeOpenStore;
2563 }
2564 //
2565
2566 $dbo = JFactory::getDbo();
2567 $session = JFactory::getSession();
2568
2569 if ($skipsession) {
2570 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2571 if (empty($setting) && $setting != "0") {
2572 return false;
2573 } else {
2574 $x = explode("-", $setting);
2575 if (!empty($x[1]) && $x[1] != "0") {
2576 $getTimeOpenStore = $x;
2577 return $x;
2578 }
2579 }
2580 } else {
2581 $sval = $session->get('vbgetTimeOpenStore', '');
2582 if (!empty($sval)) {
2583 return $sval;
2584 } else {
2585 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2586 if (empty($setting) && $setting != "0") {
2587 return false;
2588 } else {
2589 $x = explode("-", $setting);
2590 if (!empty($x[1]) && $x[1] != "0") {
2591 $session->set('vbgetTimeOpenStore', $x);
2592 $getTimeOpenStore = $x;
2593 return $x;
2594 }
2595 }
2596 }
2597 }
2598 return false;
2599 }
2600
2601 public static function getHoursMinutes($secs)
2602 {
2603 if ($secs >= 3600) {
2604 $op = $secs / 3600;
2605 $hours = floor($op);
2606 $less = $hours * 3600;
2607 $newsec = $secs - $less;
2608 $optwo = $newsec / 60;
2609 $minutes = floor($optwo);
2610 } else {
2611 $hours = "0";
2612 $optwo = $secs / 60;
2613 $minutes = floor($optwo);
2614 }
2615
2616 return [$hours, $minutes];
2617 }
2618
2619 public static function getClosingDates()
2620 {
2621 // cache value in static var
2622 static $getClosingDates = null;
2623
2624 if (is_array($getClosingDates)) {
2625 return $getClosingDates;
2626 }
2627
2628 $dbo = JFactory::getDbo();
2629
2630 $allcd = [];
2631
2632 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2633 $dbo->setQuery($q);
2634 $s = $dbo->loadAssocList();
2635 if ($s && !empty($s[0]['setting'])) {
2636 $allcd = json_decode($s[0]['setting'], true);
2637 $allcd = is_array($allcd) ? $allcd : array();
2638 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2639 foreach ($allcd as $k => $v) {
2640 if ($v['to'] < $base_ts) {
2641 unset($allcd[$k]);
2642 }
2643 }
2644 $allcd = array_values($allcd);
2645 }
2646
2647 $getClosingDates = $allcd;
2648
2649 return $allcd;
2650 }
2651
2652 public static function parseJsClosingDates()
2653 {
2654 $cd = self::getClosingDates();
2655 if (!$cd) {
2656 return [];
2657 }
2658
2659 $cdjs = [];
2660 foreach ($cd as $k => $v) {
2661 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2662 }
2663
2664 return $cdjs;
2665 }
2666
2667 /**
2668 * Given stay date timestamps, tells if the booking is disallowed due to global closing dates.
2669 *
2670 * @param int $checkints The check-in date timestamp.
2671 * @param int $checkoutts The check-in date timestamp.
2672 * @param ?string $df Optional date format to return the closed stay dates.
2673 * @param bool $closingcmp True to obtain the stay components affected by a global closure.
2674 *
2675 * @return string|array Empty string if no closures, closure dates string or affected components list.
2676 *
2677 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $closingcmp to improve the search suggestion details.
2678 */
2679 public static function validateClosingDates($checkints, $checkoutts, $df = null, $closingcmp = false)
2680 {
2681 $cd = self::getClosingDates();
2682
2683 if (!$cd) {
2684 return '';
2685 }
2686
2687 $df = empty($df) ? 'Y-m-d' : $df;
2688 $margin_seconds = 22 * 60 * 60;
2689
2690 foreach ($cd as $k => $v) {
2691 // determine if the stay dates are closed
2692 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2693 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2694 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2695
2696 if ($inner_closed || $outer_closed || $middle_closed) {
2697 // stay dates are closed
2698 if ($closingcmp) {
2699 // return the components affected by the globally closed dates to allow analysis
2700 return [
2701 'checkin' => $inner_closed,
2702 'checkout' => $outer_closed,
2703 'stay' => $middle_closed,
2704 ];
2705 }
2706
2707 // return the string with the range of dates closed globally
2708 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2709 }
2710 }
2711
2712 return '';
2713 }
2714
2715 /**
2716 * Whether the categories dropdown filter menu should be displayed.
2717 *
2718 * @param bool $skipsession [optional] re-read the configuration setting.
2719 *
2720 * @return bool
2721 *
2722 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2723 */
2724 public static function showCategoriesFront($skipsession = true)
2725 {
2726 return VBOFactory::getConfig()->getBool('showcategories', false);
2727 }
2728
2729 /**
2730 * Whether the number of children dropdown menu should be displayed.
2731 * Defaults to skip the session values and to re-read the configuration setting.
2732 *
2733 * @param bool $skipsession [optional] re-read the configuration setting.
2734 *
2735 * @return bool
2736 *
2737 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2738 */
2739 public static function showChildrenFront($skipsession = true)
2740 {
2741 return VBOFactory::getConfig()->getBool('showchildren', true);
2742 }
2743
2744 public static function allowBooking()
2745 {
2746 // cache value in static var
2747 static $allowBooking = null;
2748
2749 if (is_bool($allowBooking)) {
2750 return $allowBooking;
2751 }
2752
2753 $allowBooking = (bool) VBOFactory::getConfig()->get('allowbooking');
2754
2755 return $allowBooking;
2756 }
2757
2758 public static function getDisabledBookingMsg($vbo_tn = null) {
2759 $dbo = JFactory::getDbo();
2760 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2761 $dbo->setQuery($q);
2762 $dbo->execute();
2763 $s = $dbo->loadAssocList();
2764 if (!is_object($vbo_tn)) {
2765 $vbo_tn = self::getTranslator();
2766 }
2767 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2768 return $s[0]['setting'];
2769 }
2770
2771 /**
2772 * Returns the text for the guests allowed policy.
2773 *
2774 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2775 *
2776 * @return string either the raw or the translated policy text.
2777 *
2778 * @since 1.16.5 (WP) - 1.6.5 (WP)
2779 */
2780 public static function getGuestsAllowedPolicy($vbo_tn = null)
2781 {
2782 $dbo = JFactory::getDbo();
2783
2784 $q = $dbo->getQuery(true)
2785 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2786 ->from($dbo->qn('#__vikbooking_texts'))
2787 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2788
2789 $dbo->setQuery($q);
2790 $text_record = $dbo->loadAssocList();
2791
2792 if (!$text_record) {
2793 // missing record, insert it with empty values
2794 if (self::isAdmin()) {
2795 $record = new stdClass;
2796 $record->param = 'guests_allowed_policy';
2797 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2798 $record->setting = '';
2799 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2800 }
2801
2802 return '';
2803 }
2804
2805 if (is_object($vbo_tn)) {
2806 // translate the record only if requested
2807 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2808 }
2809
2810 return $text_record[0]['setting'];
2811 }
2812
2813 public static function getDateFormat($skipsession = true)
2814 {
2815 // cache value in static var
2816 static $getDateFormat = null;
2817
2818 if ($getDateFormat) {
2819 return $getDateFormat;
2820 }
2821
2822 $dbo = JFactory::getDbo();
2823
2824 if ($skipsession) {
2825 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2826 $dbo->setQuery($q, 0, 1);
2827 $getDateFormat = $dbo->loadResult();
2828
2829 return $getDateFormat;
2830 }
2831
2832 $session = JFactory::getSession();
2833 $sval = $session->get('vbgetDateFormat', '');
2834 if (!empty($sval)) {
2835 return $sval;
2836 }
2837
2838 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2839 $dbo->setQuery($q, 0, 1);
2840 $getDateFormat = $dbo->loadResult();
2841
2842 $session->set('vbgetDateFormat', $getDateFormat);
2843
2844 return $getDateFormat;
2845 }
2846
2847 public static function getDateSeparator($skipsession = true)
2848 {
2849 // cache value in static var
2850 static $getDateSeparator = null;
2851
2852 if ($getDateSeparator) {
2853 return $getDateSeparator;
2854 }
2855
2856 $dbo = JFactory::getDbo();
2857
2858 if ($skipsession) {
2859 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2860 $dbo->setQuery($q, 0, 1);
2861 $val = $dbo->loadResult();
2862
2863 $getDateSeparator = empty($val) ? "/" : $val;
2864
2865 return $getDateSeparator;
2866 }
2867
2868 $session = JFactory::getSession();
2869 $sval = $session->get('vbgetDateSep', '');
2870 if (!empty($sval)) {
2871 return $sval;
2872 }
2873
2874 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2875 $dbo->setQuery($q, 0, 1);
2876 $val = $dbo->loadResult();
2877
2878 $getDateSeparator = empty($val) ? "/" : $val;
2879
2880 $session->set('vbgetDateSep', $getDateSeparator);
2881
2882 return $getDateSeparator;
2883 }
2884
2885 public static function getHoursMoreRb($skipsession = false)
2886 {
2887 // cache value in static var
2888 static $getHoursMoreRb = null;
2889
2890 if ($getHoursMoreRb !== null) {
2891 return $getHoursMoreRb;
2892 }
2893
2894 $dbo = JFactory::getDbo();
2895
2896 if ($skipsession) {
2897 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2898 $dbo->setQuery($q, 0, 1);
2899 $getHoursMoreRb = (int)$dbo->loadResult();
2900
2901 return $getHoursMoreRb;
2902 }
2903
2904 $session = JFactory::getSession();
2905 $sval = $session->get('getHoursMoreRb', '');
2906 if (strlen($sval)) {
2907 return $sval;
2908 }
2909
2910 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2911 $dbo->setQuery($q, 0, 1);
2912 $getHoursMoreRb = (int)$dbo->loadResult();
2913
2914 $session->set('getHoursMoreRb', $getHoursMoreRb);
2915
2916 return $getHoursMoreRb;
2917 }
2918
2919 public static function getHoursRoomAvail()
2920 {
2921 $dbo = JFactory::getDbo();
2922
2923 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2924 $dbo->setQuery($q, 0, 1);
2925
2926 return (int)$dbo->loadResult();
2927 }
2928
2929 public static function getFrontTitle($vbo_tn = null) {
2930 // cache value in static var
2931 static $getFrontTitle = null;
2932
2933 if ($getFrontTitle) {
2934 return $getFrontTitle;
2935 }
2936 //
2937
2938 $dbo = JFactory::getDbo();
2939 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2940 $dbo->setQuery($q);
2941 $dbo->execute();
2942 $ft = $dbo->loadAssocList();
2943 if (!is_object($vbo_tn)) {
2944 $vbo_tn = self::getTranslator();
2945 }
2946 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2947
2948 $getFrontTitle = $ft[0]['setting'];
2949
2950 return $ft[0]['setting'];
2951 }
2952
2953 public static function getFrontTitleTag()
2954 {
2955 return VBOFactory::getConfig()->get('fronttitletag', '');
2956 }
2957
2958 public static function getFrontTitleTagClass()
2959 {
2960 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2961 }
2962
2963 /**
2964 * Returns the configured currency name. Often used to apply currency
2965 * conversions, and so it should match the currency ISO 4217 (Alpha-3) code.
2966 *
2967 * @param bool $iso True to ensure we fetch a 3-char currency code.
2968 *
2969 * @return string
2970 *
2971 * @since 1.18.0 (J) - 1.8.0 (WP) added argument $iso.
2972 */
2973 public static function getCurrencyName($iso = false)
2974 {
2975 static $currencyName = null;
2976
2977 if ($currencyName !== null) {
2978 return $currencyName;
2979 }
2980
2981 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2982
2983 if ($iso && strlen((string) $currencyName) != 3) {
2984 // attempt to fetch the currency code configuration setting
2985 $currencyName = self::getCurrencyCodePp();
2986 }
2987
2988 return $currencyName;
2989 }
2990
2991 public static function getCurrencySymb()
2992 {
2993 // cache value in static var
2994 static $getCurrencySymb = null;
2995
2996 if ($getCurrencySymb) {
2997 return $getCurrencySymb;
2998 }
2999
3000 $getCurrencySymb = VBOFactory::getConfig()->get('currencysymb', '');
3001
3002 return $getCurrencySymb;
3003 }
3004
3005 /**
3006 * Returns the configured currency position: either "before" or "after".
3007 *
3008 * @return string
3009 *
3010 * @since 1.18.3 (J) - 1.8.3 (WP)
3011 */
3012 public static function getCurrencyPosition()
3013 {
3014 // cache value in static var
3015 static $getCurrencyPos = null;
3016
3017 if ($getCurrencyPos) {
3018 return $getCurrencyPos;
3019 }
3020
3021 $getCurrencyPos = VBOFactory::getConfig()->get('currencypos', 'before');
3022
3023 return $getCurrencyPos;
3024 }
3025
3026 /**
3027 * Given a number, formats it according to currency preferences.
3028 *
3029 * @param string $number The number to format.
3030 * @param ?string $currency Optional currency to use.
3031 * @param ?array $config Optional format configuration (i.e. currency/number "HTML template").
3032 *
3033 * @return string
3034 *
3035 * @since 1.18.3 (J) - 1.8.3 (WP)
3036 */
3037 public static function formatCurrencyNumber(string $number, ?string $currency = null, ?array $config = null)
3038 {
3039 // get the currency symbol/value to use
3040 $currency = $currency ?: self::getCurrencySymb();
3041
3042 // obtain currency template (wildcard expected)
3043 if (is_string($config[0] ?? null) || is_string($config['currency'] ?? null)) {
3044 // apply templating to string
3045 $currency = sprintf($config[0] ?? $config['currency'], $currency);
3046 }
3047
3048 // obtain number template
3049 if (is_string($config[1] ?? null) || is_string($config['number'] ?? null)) {
3050 // apply templating to string
3051 $number = sprintf($config[1] ?? $config['number'], $number);
3052 }
3053
3054 // default value spacing (empty white space)
3055 $spacing = (string) ($config['spacing'] ?? ' ');
3056
3057 if (self::getCurrencyPosition() === 'after') {
3058 return sprintf('%s' . $spacing . '%s', $number, $currency);
3059 }
3060
3061 return sprintf('%s' . $spacing . '%s', $currency, $number);
3062 }
3063
3064 public static function getCurrencyCodePp()
3065 {
3066 static $currencyCode = null;
3067
3068 if ($currencyCode !== null) {
3069 return $currencyCode;
3070 }
3071
3072 $currencyCode = VBOFactory::getConfig()->get('currencycodepp', '');
3073
3074 return $currencyCode;
3075 }
3076
3077 public static function getNumberFormatData()
3078 {
3079 // cache value in static var
3080 static $getNumberFormatData = null;
3081
3082 if ($getNumberFormatData) {
3083 return $getNumberFormatData;
3084 }
3085
3086 $getNumberFormatData = VBOFactory::getConfig()->get('numberformat', '');
3087
3088 return $getNumberFormatData;
3089 }
3090
3091 /**
3092 * It is possible to hide the decimals if they are like "N.00".
3093 *
3094 * @return int 0 if disabled, 1 if enabled (default).
3095 *
3096 * @since 1.14 (J) - 1.4.0 (WP)
3097 */
3098 public static function hideEmptyDecimals()
3099 {
3100 // cache value in static var
3101 static $hideEmptyDecimals = null;
3102
3103 if ($hideEmptyDecimals !== null) {
3104 return $hideEmptyDecimals;
3105 }
3106
3107 $hideEmptyDecimals = (int) VBOFactory::getConfig()->get('noemptydecimals', '1');
3108
3109 return $hideEmptyDecimals;
3110 }
3111
3112 public static function numberFormat($num)
3113 {
3114 $formatvals = self::getNumberFormatData();
3115 $formatparts = explode(':', $formatvals);
3116
3117 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
3118 // number has got no decimals
3119 $formatparts[0] = 0;
3120 }
3121
3122 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
3123 }
3124
3125 /**
3126 * Given a date timestamp, returns the formatted date.
3127 *
3128 * @param int|string $ts the date representation in Unix timestamp.
3129 * @param bool $wtime if true, a formatted date-time will be returned.
3130 *
3131 * @return string the formatted date according to settings.
3132 *
3133 * @since 1.16.0 (J) - 1.6.0 (WP)
3134 */
3135 public static function formatDateTs($ts, $wtime = false)
3136 {
3137 $nowdf = self::getDateFormat();
3138 if ($nowdf == "%d/%m/%Y") {
3139 $df = 'd/m/Y';
3140 } elseif ($nowdf == "%m/%d/%Y") {
3141 $df = 'm/d/Y';
3142 } else {
3143 $df = 'Y/m/d';
3144 }
3145 $datesep = self::getDateSeparator();
3146
3147 $time_f = $wtime ? ' H:i' : '';
3148
3149 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
3150 }
3151
3152 /**
3153 * Given a country name, 3-char or 2-char code, this method attempts
3154 * to guess the best language to assign to the booking according to
3155 * what languages are installed on the website. This way, cron jobs
3156 * and any other email notification will be correctly and automatically
3157 * sent to the guest in the proper language without any manual action.
3158 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
3159 *
3160 * @param string $country the country name, 3-char or 2-char code.
3161 * @param string $locale optional locale lang tag of the guest.
3162 *
3163 * @return mixed the best lang-tag string to use or null.
3164 *
3165 * @since 1.16.0 (J) - 1.6.0 (WP)
3166 */
3167 public static function guessBookingLangFromCountry($country, $locale = null)
3168 {
3169 if (empty($country)) {
3170 return null;
3171 }
3172
3173 // get all the available languages
3174 $known_langs = self::getVboApplication()->getKnownLanguages();
3175 if (!is_array($known_langs) || !count($known_langs)) {
3176 return null;
3177 }
3178
3179 // check if the booking included a supported "locale" for the guest
3180 if (!empty($locale)) {
3181 // make the locale compatible with the language tags format
3182 $locale = str_replace('_', '-', $locale);
3183 foreach ($known_langs as $ltag => $ldet) {
3184 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
3185 // we support this language tag, so we just use it
3186 return $ltag;
3187 }
3188 }
3189 }
3190
3191 // build similarities with country-languages
3192 $similarities = array(
3193 'AU' => 'en',
3194 'GB' => 'en',
3195 'IE' => 'en',
3196 'NZ' => 'en',
3197 'US' => 'en',
3198 'CA' => array(
3199 'en',
3200 'fr',
3201 ),
3202 'CL' => 'es',
3203 'AR' => 'es',
3204 'PE' => 'es',
3205 'MX' => 'es',
3206 'CR' => 'es',
3207 'CO' => 'es',
3208 'EC' => 'es',
3209 'BO' => 'es',
3210 'CU' => 'es',
3211 'VE' => 'es',
3212 'BE' => 'fr',
3213 'LU' => 'fr',
3214 'CH' => array(
3215 'de',
3216 'it',
3217 'fr',
3218 ),
3219 'AT' => 'de',
3220 'GR' => 'el',
3221 'GL' => 'dk',
3222 );
3223
3224 // fetch values from db
3225 $dbo = JFactory::getDbo();
3226 $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);
3227 $dbo->setQuery($q, 0, 1);
3228 $dbo->execute();
3229 if (!$dbo->getNumRows()) {
3230 return null;
3231 }
3232 $country_record = $dbo->loadAssoc();
3233
3234 // assign country name/code versions
3235 $country_name = $country_record['country_name'];
3236 $country_3char = strtoupper($country_record['country_3_code']);
3237 $country_2char = strtoupper($country_record['country_2_code']);
3238
3239 // build an associative array of language tags and related match-score
3240 $langtags_score = array();
3241 foreach ($known_langs as $ltag => $ldet) {
3242 // default language tag score is 0 for no matches
3243 $langtags_score[$ltag] = 0;
3244 // get language and country codes
3245 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
3246
3247 // check matches with the installed language details
3248 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
3249 // increase language tag score
3250 $langtags_score[$ltag]++;
3251 }
3252 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
3253 // increase language tag score
3254 $langtags_score[$ltag]++;
3255 }
3256 if (!empty($ldet['locale'])) {
3257 // sanitize locale for matching the 2-char code safely
3258 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
3259 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
3260 // increase language tag score
3261 $langtags_score[$ltag]++;
3262 }
3263 }
3264 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
3265 // increase language tag score
3266 $langtags_score[$ltag]++;
3267 }
3268 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
3269 // increase language tag score
3270 $langtags_score[$ltag]++;
3271 }
3272
3273 // check language similarities between countries
3274 if (isset($similarities[$country_2char])) {
3275 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
3276 // check if language tag(s) is available for this spoken language
3277 foreach ($spoken_tags as $spoken_tag) {
3278 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
3279 // increase language tag score
3280 $langtags_score[$ltag]++;
3281 }
3282 }
3283 }
3284 }
3285
3286 // make sure at least one language tag has got some points
3287 if (max($langtags_score) === 0) {
3288 // no languages installed to honor this country
3289 return null;
3290 }
3291
3292 // sort language tag scores
3293 arsort($langtags_score);
3294
3295 // reset array pointer to the first (highest) element
3296 reset($langtags_score);
3297
3298 // return the language tag with the highest score
3299 return key($langtags_score);
3300 }
3301
3302 public static function getIntroMain($vbo_tn = null) {
3303 $dbo = JFactory::getDbo();
3304 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
3305 $dbo->setQuery($q);
3306 $dbo->execute();
3307 $ft = $dbo->loadAssocList();
3308 if (!is_object($vbo_tn)) {
3309 $vbo_tn = self::getTranslator();
3310 }
3311 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3312 return $ft[0]['setting'];
3313 }
3314
3315 public static function getClosingMain($vbo_tn = null) {
3316 $dbo = JFactory::getDbo();
3317 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3318 $dbo->setQuery($q);
3319 $dbo->execute();
3320 $ft = $dbo->loadAssocList();
3321 if (!is_object($vbo_tn)) {
3322 $vbo_tn = self::getTranslator();
3323 }
3324 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3325 return $ft[0]['setting'];
3326 }
3327
3328 public static function getFullFrontTitle($vbo_tn = null) {
3329 $dbo = JFactory::getDbo();
3330 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3331 $dbo->setQuery($q);
3332 $dbo->execute();
3333 $ft = $dbo->loadAssocList();
3334 if (!is_object($vbo_tn)) {
3335 $vbo_tn = self::getTranslator();
3336 }
3337 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3338 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3339 $dbo->setQuery($q);
3340 $dbo->execute();
3341 $fttag = $dbo->loadAssocList();
3342 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3343 $dbo->setQuery($q);
3344 $dbo->execute();
3345 $fttagclass = $dbo->loadAssocList();
3346 if (empty($ft[0]['setting'])) {
3347 return "";
3348 } else {
3349 if (empty($fttag[0]['setting'])) {
3350 return $ft[0]['setting'] . "<br/>\n";
3351 } else {
3352 $tag = str_replace("<", "", $fttag[0]['setting']);
3353 $tag = str_replace(">", "", $tag);
3354 $tag = str_replace("/", "", $tag);
3355 $tag = trim($tag);
3356 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3357 }
3358 }
3359 }
3360
3361 /**
3362 * Tells whether a date string is valid.
3363 *
3364 * @param ?string $date The date to evaluate.
3365 *
3366 * @return bool
3367 *
3368 * @since 1.18.8 (J) - 1.8.8 (WP) Military format Y-m-d is always accepted.
3369 */
3370 public static function dateIsValid(?string $date)
3371 {
3372 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", (string) $date)) {
3373 // date is in Y-m-d format (with no time)
3374 return true;
3375 }
3376
3377 $df = self::getDateFormat();
3378 $datesep = self::getDateSeparator();
3379 if (strlen((string) $date) != 10) {
3380 return false;
3381 }
3382 $cur_dsep = "/";
3383 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3384 $cur_dsep = $datesep;
3385 }
3386 $x = explode($cur_dsep, $date);
3387 if ($df == "%d/%m/%Y") {
3388 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3389 return false;
3390 }
3391 } elseif ($df == "%m/%d/%Y") {
3392 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3393 return false;
3394 }
3395 } else {
3396 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3397 return false;
3398 }
3399 }
3400 return true;
3401 }
3402
3403 public static function sayDateFormat() {
3404 $dbo = JFactory::getDbo();
3405 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3406 $dbo->setQuery($q);
3407 $dbo->execute();
3408 $s = $dbo->loadAssocList();
3409 if ($s[0]['setting'] == "%d/%m/%Y") {
3410 return JText::translate('VBCONFIGONETWELVE');
3411 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3412 return JText::translate('VBCONFIGONEMDY');
3413 } else {
3414 return JText::translate('VBCONFIGONETENTHREE');
3415 }
3416 }
3417
3418 /**
3419 * Calculates the Unix timestamp from the given date and
3420 * time. Avoids DST issues thanks to mktime. With prior releases,
3421 * DST issues may occur due to the sum of seconds.
3422 *
3423 * @param string $date the date string formatted with the current settings
3424 * @param int $h hours from 0 to 23 for check-in/check-out
3425 * @param int $m minutes from 0 to 59 for check-in/check-out
3426 * @param int $s seconds from 0 to 59 for check-in/check-out
3427 *
3428 * @return int the Unix timestamp of the date
3429 *
3430 * @since 1.0.14
3431 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3432 * signature modified for $h and $m that now take 0 as default value.
3433 */
3434 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3435 $df = self::getDateFormat();
3436 $datesep = self::getDateSeparator();
3437 $cur_dsep = "/";
3438 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3439 $cur_dsep = $datesep;
3440 }
3441 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3442 // date is in Y-m-d format (with no time)
3443 $cur_dsep = '-';
3444 $df = "%Y/%m/%d";
3445 }
3446 $x = explode($cur_dsep, $date);
3447 if (!(count($x) > 2)) {
3448 return 0;
3449 }
3450 if ($df == "%d/%m/%Y") {
3451 $month = (int)$x[1];
3452 $mday = (int)$x[0];
3453 $year = (int)$x[2];
3454 } elseif ($df == "%m/%d/%Y") {
3455 $month = (int)$x[0];
3456 $mday = (int)$x[1];
3457 $year = (int)$x[2];
3458 } else {
3459 $month = (int)$x[1];
3460 $mday = (int)$x[2];
3461 $year = (int)$x[0];
3462 }
3463 $h = empty($h) ? 0 : (int)$h;
3464 $m = empty($m) ? 0 : (int)$m;
3465 $s = $s > 0 && $s <= 59 ? $s : 0;
3466
3467 return mktime($h, $m, $s, $month, $mday, $year);
3468 }
3469
3470 public static function ivaInclusa($skipsession = false) {
3471 // cache value in static var
3472 static $getTaxIncluded = null;
3473
3474 if ($getTaxIncluded !== null) {
3475 return (bool)$getTaxIncluded;
3476 }
3477
3478 $dbo = JFactory::getDbo();
3479
3480 if ($skipsession) {
3481 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3482 $dbo->setQuery($q, 0, 1);
3483 $dbo->execute();
3484 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3485
3486 // cache value and return it
3487 $getTaxIncluded = $vat_incl_data;
3488
3489 return (bool)$vat_incl_data;
3490 }
3491
3492 $session = JFactory::getSession();
3493 $sval = $session->get('getTaxIncluded', '');
3494 if (strlen($sval)) {
3495 return (bool)$sval;
3496 }
3497
3498 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3499 $dbo->setQuery($q, 0, 1);
3500 $dbo->execute();
3501 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3502
3503 // update session value, cache it and return it
3504 $session->set('getTaxIncluded', $vat_incl_data);
3505 $getTaxIncluded = $vat_incl_data;
3506
3507 return (bool)$vat_incl_data;
3508 }
3509
3510 public static function showTaxOnSummaryOnly($skipsession = false) {
3511 if ($skipsession) {
3512 $dbo = JFactory::getDbo();
3513 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3514 $dbo->setQuery($q);
3515 $dbo->execute();
3516 $s = $dbo->loadAssocList();
3517 return (intval($s[0]['setting']) == 1 ? true : false);
3518 } else {
3519 $session = JFactory::getSession();
3520 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3521 if (strlen($sval) > 0) {
3522 return (intval($sval) == 1 ? true : false);
3523 } else {
3524 $dbo = JFactory::getDbo();
3525 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3526 $dbo->setQuery($q);
3527 $dbo->execute();
3528 $s = $dbo->loadAssocList();
3529 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3530 return (intval($s[0]['setting']) == 1 ? true : false);
3531 }
3532 }
3533 }
3534
3535 public static function tokenForm() {
3536 $dbo = JFactory::getDbo();
3537 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3538 $dbo->setQuery($q);
3539 $dbo->execute();
3540 $s = $dbo->loadAssocList();
3541 return (intval($s[0]['setting']) == 1 ? true : false);
3542 }
3543
3544 public static function getPaypalAcc() {
3545 $dbo = JFactory::getDbo();
3546 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3547 $dbo->setQuery($q);
3548 $dbo->execute();
3549 $s = $dbo->loadAssocList();
3550 return $s[0]['setting'];
3551 }
3552
3553 public static function getAccPerCent() {
3554 $dbo = JFactory::getDbo();
3555 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3556 $dbo->setQuery($q);
3557 $dbo->execute();
3558 $s = $dbo->loadAssocList();
3559 return $s[0]['setting'];
3560 }
3561
3562 public static function getTypeDeposit($skipsession = false) {
3563 if ($skipsession) {
3564 $dbo = JFactory::getDbo();
3565 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3566 $dbo->setQuery($q);
3567 $dbo->execute();
3568 $s = $dbo->loadAssocList();
3569 return $s[0]['setting'];
3570 } else {
3571 $session = JFactory::getSession();
3572 $sval = $session->get('getTypeDeposit', '');
3573 if (strlen($sval) > 0) {
3574 return $sval;
3575 } else {
3576 $dbo = JFactory::getDbo();
3577 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3578 $dbo->setQuery($q);
3579 $dbo->execute();
3580 $s = $dbo->loadAssocList();
3581 $session->set('getTypeDeposit', $s[0]['setting']);
3582 return $s[0]['setting'];
3583 }
3584 }
3585 }
3586
3587 public static function multiplePayments()
3588 {
3589 return VBOFactory::getConfig()->getBool('multipay', false);
3590 }
3591
3592 public static function getAdminMail()
3593 {
3594 return VBOFactory::getConfig()->getString('adminemail', '');
3595 }
3596
3597 public static function getSenderMail()
3598 {
3599 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3600
3601 return empty($sender) ? self::getAdminMail() : $sender;
3602 }
3603
3604 public static function getPaymentName($vbo_tn = null) {
3605 $dbo = JFactory::getDbo();
3606 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3607 $dbo->setQuery($q);
3608 $dbo->execute();
3609 $s = $dbo->loadAssocList();
3610 if (!is_object($vbo_tn)) {
3611 $vbo_tn = self::getTranslator();
3612 }
3613 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3614 return $s[0]['setting'];
3615 }
3616
3617 public static function getTermsConditions($vbo_tn = null) {
3618 //VBO 1.10
3619 $dbo = JFactory::getDbo();
3620 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3621 $dbo->setQuery($q);
3622 $dbo->execute();
3623 if ($dbo->getNumRows() > 0) {
3624 $s = $dbo->loadAssocList();
3625 if (!is_object($vbo_tn)) {
3626 $vbo_tn = self::getTranslator();
3627 }
3628 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3629 } else {
3630 //the record has never been saved. Compose it with the default lang definition
3631 $timeopst = self::getTimeOpenStore(true);
3632 if (is_array($timeopst)) {
3633 $openat = self::getHoursMinutes($timeopst[0]);
3634 $closeat = self::getHoursMinutes($timeopst[1]);
3635 } else {
3636 $openat = array(12, 0);
3637 $closeat = array(10, 0);
3638 }
3639 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3640 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3641 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3642 }
3643
3644 return $s[0]['setting'];
3645 }
3646
3647 public static function getMinutesLock($conv = false)
3648 {
3649 static $minutesLock = null;
3650
3651 if ($minutesLock === null) {
3652 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3653 }
3654
3655 if (!$minutesLock) {
3656 return 0;
3657 }
3658
3659 if ($conv) {
3660 return (time() + ($minutesLock * 60));
3661 }
3662
3663 return $minutesLock;
3664 }
3665
3666 /**
3667 * Checks if the room units are temporarily locked on the given dates.
3668 *
3669 * @param int $idroom the room ID.
3670 * @param int $units the total number of room units.
3671 * @param int $first the check-in timestamp.
3672 * @param int $second the check-out timestamp.
3673 * @param bool $occupied if true, the system will also consider the booked units.
3674 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3675 *
3676 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3677 *
3678 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3679 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3680 */
3681 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3682 {
3683 $dbo = JFactory::getDbo();
3684
3685 if ($units < 1) {
3686 return false;
3687 }
3688
3689 $actnow = time();
3690
3691 // clean up expired records
3692 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3693 $dbo->setQuery($q);
3694 $dbo->execute();
3695
3696 $secdiff = $second - $first;
3697 $daysdiff = $secdiff / 86400;
3698 if (is_int($daysdiff)) {
3699 if ($daysdiff < 1) {
3700 $daysdiff = 1;
3701 }
3702 } else {
3703 if ($daysdiff < 1) {
3704 $daysdiff = 1;
3705 } else {
3706 $sum = floor($daysdiff) * 86400;
3707 $newdiff = $secdiff - $sum;
3708 $maxhmore = self::getHoursMoreRb() * 3600;
3709 if ($maxhmore >= $newdiff) {
3710 $daysdiff = floor($daysdiff);
3711 } else {
3712 $daysdiff = ceil($daysdiff);
3713 }
3714 }
3715 }
3716 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3717 $tmplock_map = [];
3718
3719 // check temporarily locked records
3720 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3721 $dbo->setQuery($check);
3722 $busy = $dbo->loadAssocList();
3723 if ($busy) {
3724 foreach ($groupdays as $kg => $gday) {
3725 $bfound = 0;
3726 foreach ($busy as $bu) {
3727 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3728 $bfound++;
3729 }
3730 }
3731 $tmplock_map[$kg] = $bfound;
3732 if ($bfound >= $units) {
3733 return false;
3734 }
3735 }
3736 }
3737
3738 if ($occupied) {
3739 // check also the busy records and some the occupied units to the temporarily locked ones
3740 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3741 $dbo->setQuery($check);
3742 $busy = $dbo->loadAssocList();
3743 if (!$busy) {
3744 return true;
3745 }
3746
3747 foreach ($groupdays as $kg => $gday) {
3748 $bfound = 0;
3749 foreach ($busy as $bu) {
3750 if (in_array($bu['id'], $skip_busy_ids)) {
3751 // Booking modification
3752 continue;
3753 }
3754 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3755 $bfound++;
3756 }
3757 }
3758 // sum units occupied to the units temporarily locked
3759 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3760 if ($bfound >= $units) {
3761 return false;
3762 }
3763 }
3764 }
3765
3766 return true;
3767 }
3768
3769 public static function getGroupDays($first, $second, $daysdiff) {
3770 $ret = array();
3771 $ret[] = $first;
3772 if ($daysdiff > 1) {
3773 $start = getdate($first);
3774 $end = getdate($second);
3775 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3776 for($i = 1; $i < $daysdiff; $i++) {
3777 $checkday = $start['mday'] + $i;
3778 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3779 if ($dayts != $endcheck) {
3780 $ret[] = $dayts;
3781 }
3782 }
3783 }
3784 $ret[] = $second;
3785 return $ret;
3786 }
3787
3788 /**
3789 * Counts the hours of difference between the current
3790 * server time and the selected check-in date and time.
3791 *
3792 * @param int $checkin_ts
3793 * @param [int] $now_ts
3794 *
3795 * @return int
3796 */
3797 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3798 $hoursdiff = 0;
3799
3800 if (empty($now_ts)) {
3801 $now_ts = time();
3802 }
3803
3804 if ($now_ts >= $checkin_ts) {
3805 return $hoursdiff;
3806 }
3807
3808 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3809
3810 return $hoursdiff;
3811 }
3812
3813 /**
3814 * Loads all the occupied records for the given rooms and offset.
3815 *
3816 * @param array $roomids the optional list of room IDs to filter.
3817 * @param int $from_ts the optional base date to use.
3818 * @param int $max_ts the optional max date to use.
3819 * @param bool $closures whether to ignore, include or exclude closures.
3820 *
3821 * @return array
3822 *
3823 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3824 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3825 */
3826 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3827 {
3828 $base_ts = empty($from_ts) ? time() : $from_ts;
3829 $busy = [];
3830 $clauses = [];
3831
3832 if (is_array($roomids) && count($roomids)) {
3833 // filter busy records only by the requested room ids
3834 $roomids = array_map(function($rid) {
3835 return (int)$rid;
3836 }, $roomids);
3837 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3838 }
3839
3840 // exclude past reservations
3841 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3842
3843 if (!empty($max_ts)) {
3844 // exclude future reservations (recommended for large datasets)
3845 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3846 }
3847
3848 if (is_bool($closures)) {
3849 // include or exclude closures
3850 if ($closures) {
3851 $clauses[] = "`o`.`closure` = 1";
3852 } else {
3853 $clauses[] = "`o`.`closure` != 1";
3854 }
3855 }
3856
3857 $dbo = JFactory::getDbo();
3858
3859 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3860 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3861 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3862 WHERE " . implode(' AND ', $clauses) . ";";
3863 $dbo->setQuery($q);
3864 $allbusy = $dbo->loadAssocList();
3865 if ($allbusy) {
3866 foreach ($allbusy as $br) {
3867 if (!isset($busy[$br['idroom']])) {
3868 $busy[$br['idroom']] = [];
3869 }
3870 $busy[$br['idroom']][] = $br;
3871 }
3872 }
3873
3874 return $busy;
3875 }
3876
3877 /**
3878 * Loads all the busy records by excluding the closures.
3879 * Built for the page dashboard to calculate the actual
3880 * rooms occupancy by excluding the rooms closed.
3881 *
3882 * @param array $roomids the optional list of room IDs to filter.
3883 * @param int $from_ts the optional base date to use.
3884 * @param int $max_ts the optional max date to use.
3885 *
3886 * @return array the list of busy records.
3887 *
3888 * @since 1.12
3889 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3890 */
3891 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3892 {
3893 if (!is_array($roomids) || !count($roomids)) {
3894 return [];
3895 }
3896
3897 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3898 }
3899
3900 /**
3901 * Loads the occupied record IDs for a given booking ID.
3902 *
3903 * @param int $idorder The booking record ID.
3904 *
3905 * @return array List of busy IDs, if any.
3906 */
3907 public static function loadBookingBusyIds($idorder)
3908 {
3909 if (empty($idorder)) {
3910 return [];
3911 }
3912
3913 $dbo = JFactory::getDbo();
3914
3915 $dbo->setQuery(
3916 $dbo->getQuery(true)
3917 ->select($dbo->qn('idbusy'))
3918 ->from($dbo->qn('#__vikbooking_ordersbusy'))
3919 ->where($dbo->qn('idorder') . ' = ' . (int) $idorder)
3920 );
3921
3922 return array_map('intval', array_column($dbo->loadAssocList(), 'idbusy'));
3923 }
3924
3925 public static function loadLockedRecords($roomids, $ts = 0) {
3926 $dbo = JFactory::getDbo();
3927 $actnow = empty($ts) ? time() : $ts;
3928 $locked = array();
3929 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3930 $dbo->setQuery($q);
3931 $dbo->execute();
3932 if (!is_array($roomids) || !(count($roomids) > 0)) {
3933 return $locked;
3934 }
3935 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3936 $dbo->setQuery($check);
3937 $all_locked = $dbo->loadAssocList();
3938 if ($all_locked) {
3939 foreach ($all_locked as $kb => $br) {
3940 $locked[$br['idroom']][$kb] = $br;
3941 }
3942 }
3943 return $locked;
3944 }
3945
3946 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3947 $bookings = [];
3948 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3949 return $bookings;
3950 }
3951 $dbo = JFactory::getDbo();
3952 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3953 $dbo->setQuery($q);
3954 $all_booking_ids = $dbo->loadAssocList();
3955 if ($all_booking_ids) {
3956 $oids = array();
3957 foreach ($all_booking_ids as $bid) {
3958 $oids[] = $bid['idorder'];
3959 }
3960 $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` ".
3961 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3962 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3963 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3964 "ORDER BY `o`.`checkout` ASC;";
3965 $dbo->setQuery($q);
3966 $bookings = $dbo->loadAssocList();
3967 if (!$bookings) {
3968 $bookings = [];
3969 }
3970 }
3971 return $bookings;
3972 }
3973
3974 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3975 {
3976 $dbo = JFactory::getDbo();
3977
3978 if ($units < 1) {
3979 return false;
3980 }
3981
3982 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3983 if (!$room_info) {
3984 return false;
3985 }
3986
3987 $secdiff = $second - $first;
3988 $daysdiff = $secdiff / 86400;
3989 if (is_int($daysdiff)) {
3990 if ($daysdiff < 1) {
3991 $daysdiff = 1;
3992 }
3993 } else {
3994 if ($daysdiff < 1) {
3995 $daysdiff = 1;
3996 } else {
3997 $sum = floor($daysdiff) * 86400;
3998 $newdiff = $secdiff - $sum;
3999 $maxhmore = self::getHoursMoreRb() * 3600;
4000 if ($maxhmore >= $newdiff) {
4001 $daysdiff = floor($daysdiff);
4002 } else {
4003 $daysdiff = ceil($daysdiff);
4004 }
4005 }
4006 }
4007
4008 $q = $dbo->getQuery(true)
4009 ->select($dbo->qn([
4010 'id',
4011 'checkin',
4012 'realback',
4013 ]))
4014 ->from($dbo->qn('#__vikbooking_busy'))
4015 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
4016 ->where($dbo->qn('realback') . ' >= ' . $first);
4017
4018 $dbo->setQuery($q);
4019 $busy = $dbo->loadAssocList();
4020 if (!$busy) {
4021 return ($units <= ($room_info['units'] ?? 1));
4022 }
4023
4024 $groupdays = self::getGroupDays($first, $second, $daysdiff);
4025 foreach ($groupdays as $gday) {
4026 $bfound = 0;
4027 foreach ($busy as $bu) {
4028 if (in_array($bu['id'], $skip_busy_ids)) {
4029 // booking modification
4030 continue;
4031 }
4032 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
4033 $bfound++;
4034 }
4035 }
4036 if ($bfound >= $units) {
4037 return false;
4038 }
4039 }
4040
4041 return ($units <= ($room_info['units'] ?? 1));
4042 }
4043
4044 public static function payTotal()
4045 {
4046 return VBOFactory::getConfig()->getBool('paytotal', true);
4047 }
4048
4049 public static function getDepositIfDays()
4050 {
4051 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
4052 }
4053
4054 public static function depositAllowedDaysAdv($checkints) {
4055 $days_adv = self::getDepositIfDays();
4056 if (!($days_adv > 0) || !($checkints > 0)) {
4057 return true;
4058 }
4059 $now_info = getdate();
4060 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
4061 return $maxts > $checkints ? false : true;
4062 }
4063
4064 public static function depositCustomerChoice() {
4065 $dbo = JFactory::getDbo();
4066 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
4067 $dbo->setQuery($q);
4068 $dbo->execute();
4069 $s = $dbo->loadAssocList();
4070 return (intval($s[0]['setting']) == 1 ? true : false);
4071 }
4072
4073 public static function getDepositOverrides($getjson = false) {
4074 $dbo = JFactory::getDbo();
4075 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
4076 $dbo->setQuery($q);
4077 $dbo->execute();
4078 if ($dbo->getNumRows() > 0) {
4079 $s = $dbo->loadResult();
4080 return $getjson ? $s : json_decode($s, true);
4081 }
4082 //count of this array will be at least 1 to store the "more" property
4083 $def_arr = array('more' => '');
4084 $def_val = json_encode($def_arr);
4085 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
4086 $dbo->setQuery($q);
4087 $dbo->execute();
4088 return $getjson ? $def_val : $def_arr;
4089 }
4090
4091 public static function calcDepositOverride($amount_deposit, $nights) {
4092 $overrides = self::getDepositOverrides();
4093 $nights = intval($nights);
4094 $andmore = intval($overrides['more']);
4095 if (!(count($overrides) > 1)) {
4096 //no overrides
4097 return $amount_deposit;
4098 }
4099 foreach ($overrides as $k => $v) {
4100 if ((int)$k == $nights && strlen($v) > 0) {
4101 //exact override found
4102 return (float)$v;
4103 }
4104 }
4105 if ($andmore > 0 && $andmore <= $nights) {
4106 foreach ($overrides as $k => $v) {
4107 if ((int)$k == $andmore && strlen($v) > 0) {
4108 //"and more" nights found
4109 return (float)$v;
4110 }
4111 }
4112 }
4113 //nothing was found
4114 return $amount_deposit;
4115 }
4116
4117 public static function noDepositForNonRefund() {
4118 $dbo = JFactory::getDbo();
4119 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
4120 $dbo->setQuery($q);
4121 $dbo->execute();
4122 if ($dbo->getNumRows() > 0) {
4123 $s = $dbo->loadResult();
4124 return ((int)$s == 1);
4125 }
4126 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
4127 $dbo->setQuery($q);
4128 $dbo->execute();
4129 //default to false
4130 return false;
4131 }
4132
4133 /**
4134 * This method returns the room-rate array with the lowest price
4135 * that matches the preferred rate plan parameter (if available).
4136 * The array $website_rates could not be an array, or it could be
4137 * an array with the error string (response from the TACVBO Class).
4138 * The method has been introduced in VBO 1.10 and it's mainly used
4139 * by the module mod_vikbooking_channelrates and its ajax requests.
4140 *
4141 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
4142 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
4143 *
4144 * @return array
4145 */
4146 public static function getBestRoomRate($website_rates, $def_rplan) {
4147 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
4148 return array();
4149 }
4150 $best_room_rate = array();
4151 foreach ($website_rates as $rid => $tars) {
4152 foreach ($tars as $tar) {
4153 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
4154 //the array $website_rates is already sorted by price ASC, so we take the first useful array
4155 $best_room_rate = $tar;
4156 break 2;
4157 }
4158 }
4159 }
4160 if (!(count($best_room_rate) > 0)) {
4161 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
4162 foreach ($website_rates as $rid => $tars) {
4163 foreach ($tars as $tar) {
4164 $best_room_rate = $tar;
4165 break 2;
4166 }
4167 }
4168 }
4169
4170 return $best_room_rate;
4171 }
4172
4173 /**
4174 * This method returns an array with the details
4175 * of all channels in VCM that supports AV requests,
4176 * and that have at least one room type mapped.
4177 * The method invokes the Logos Class to return details
4178 * about the name and logo URL of the channel.
4179 * The method has been introduced in VBO 1.10 and it's mainly used
4180 * by the module mod_vikbooking_channelrates and its ajax requests.
4181 *
4182 * @param array $channels an array of channel IDs to be mapped on the VCM relations
4183 *
4184 * @return array
4185 */
4186 public static function getChannelsMap($channels) {
4187 if (!is_array($channels) || !(count($channels))) {
4188 return array();
4189 }
4190 $vcm_logos = self::getVcmChannelsLogo('', true);
4191 if (!is_object($vcm_logos)) {
4192 return array();
4193 }
4194 $channels_ids = array();
4195 foreach ($channels as $chid) {
4196 $ichid = (int)$chid;
4197 if ($ichid < 1) {
4198 continue;
4199 }
4200 array_push($channels_ids, $ichid);
4201 }
4202 if (!(count($channels_ids))) {
4203 return array();
4204 }
4205 $dbo = JFactory::getDbo();
4206 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
4207 $dbo->setQuery($q);
4208 $dbo->execute();
4209 if ($dbo->getNumRows() < 1) {
4210 return array();
4211 }
4212 $channels_names = $dbo->loadAssocList();
4213 $channels_map = array();
4214 foreach ($channels_names as $ch) {
4215 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
4216 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
4217 $chdata = array(
4218 'id' => $ch['idchannel'],
4219 'name' => ucwords($ch['channel']),
4220 'logo' => $ota_logo_url
4221 );
4222 array_push($channels_map, $chdata);
4223 }
4224 return $channels_map;
4225 }
4226
4227 /**
4228 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
4229 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
4230 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
4231 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
4232 * module/widget and its ajax requests.
4233 *
4234 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
4235 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
4236 *
4237 * @return mixed string (empty or indicating the alteration), or associative array per channel.
4238 *
4239 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
4240 */
4241 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
4242 {
4243 $otas_rates_val = '';
4244 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
4245 return $otas_rates_val;
4246 }
4247 if (!class_exists('VikChannelManager')) {
4248 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
4249 }
4250 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
4251 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
4252 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
4253 // the Bulk Rates Cache contains data for this room type and rate plan
4254 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
4255 // build rates modification string
4256 if ((int)$data_cont['rmod'] > 0) {
4257 // rates were modified for the OTAs, check how
4258 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
4259 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
4260 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
4261 // check alterations per channel, if requested
4262 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
4263 // we compose an associative array with the alteration for each channel (0 = generic rule)
4264 $channel_alterations = array($otas_rates_val);
4265 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
4266 if ((int)$ch_mods['rmod'] > 0) {
4267 // custom rate alteration for this channel
4268 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
4269 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
4270 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
4271 } else {
4272 // no rates alteration for this channel identifier
4273 $ch_alter_string = '+0%';
4274 }
4275 $channel_alterations[$ch_id] = $ch_alter_string;
4276 }
4277 // overwrite return value with associative array
4278 $otas_rates_val = $channel_alterations;
4279 }
4280 }
4281 }
4282 }
4283
4284 return $otas_rates_val;
4285 }
4286
4287 /**
4288 * This method checks if some non-refundable rates were selected
4289 * (`free_cancellation`=0), the only argument is an array of tariffs.
4290 * The property 'idprice' must be defined on each sub-array.
4291 *
4292 * @param $tars array
4293 *
4294 * @return boolean
4295 **/
4296 public static function findNonRefundableRates($tars = [])
4297 {
4298 $id_prices = [];
4299
4300 foreach ($tars as $tar) {
4301 if (isset($tar['idprice'])) {
4302 if (!in_array($tar['idprice'], $id_prices)) {
4303 array_push($id_prices, (int)$tar['idprice']);
4304 }
4305 continue;
4306 }
4307 foreach ($tar as $t) {
4308 if (isset($t['idprice'])) {
4309 if (!in_array($t['idprice'], $id_prices)) {
4310 array_push($id_prices, (int)$t['idprice']);
4311 }
4312 }
4313 }
4314 }
4315
4316 if (!count($id_prices)) {
4317 // no price IDs found (probably a package)
4318 return false;
4319 }
4320
4321 $dbo = JFactory::getDbo();
4322
4323 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
4324 $dbo->setQuery($q);
4325 $dbo->execute();
4326
4327 return (bool)($dbo->getNumRows() > 0);
4328 }
4329
4330 /**
4331 * This method checks if the deposit is allowed depending on
4332 * the selected rate plans (idprice) for the rooms reserved.
4333 * If the configuration setting is enabled, and if some
4334 * non-refundable rates were selected (`free_cancellation`=0),
4335 * the method will return false, because the deposit is not allowed.
4336 * The only argument is an array of tariffs. The property 'idprice'
4337 * must be defined on each sub-array (multi-dimension supported)
4338 * throgh the method findNonRefundableRates();
4339 *
4340 * @param $tars array
4341 *
4342 * @return boolean
4343 **/
4344 public static function allowDepositFromRates($tars) {
4345 if (!self::noDepositForNonRefund()) {
4346 //deposit can be paid also if non-refundable rates
4347 return true;
4348 }
4349 return !self::findNonRefundableRates($tars);
4350 }
4351
4352 public static function showSearchSuggestions() {
4353 $dbo = JFactory::getDbo();
4354 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4355 $dbo->setQuery($q);
4356 $dbo->execute();
4357 if ($dbo->getNumRows() > 0) {
4358 return (int)$dbo->loadResult();
4359 }
4360 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4361 $dbo->setQuery($q);
4362 $dbo->execute();
4363 return 1;
4364 }
4365
4366 /**
4367 * Given a code, returns the coupon information array.
4368 *
4369 * @param string $code the coupon code.
4370 *
4371 * @return array empty array or coupon record.
4372 *
4373 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4374 */
4375 public static function getCouponInfo($code)
4376 {
4377 $dbo = JFactory::getDbo();
4378
4379 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4380 $dbo->setQuery($q, 0, 1);
4381 $coupon = $dbo->loadAssoc();
4382 if (!$coupon) {
4383 return [];
4384 }
4385
4386 // check for customers assigned
4387 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4388 $dbo->setQuery($q);
4389 $coupon_customers = $dbo->loadAssocList();
4390 if ($coupon_customers) {
4391 // set customers and automatic properties
4392 $coupon['customers'] = [];
4393 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4394 foreach ($coupon_customers as $coupon_customer) {
4395 // push customer ID
4396 $coupon['customers'][] = $coupon_customer['idcustomer'];
4397 }
4398 }
4399
4400 return $coupon;
4401 }
4402
4403 /**
4404 * Reads the details of a given room record ID.
4405 *
4406 * @param int $idroom The room ID to fetch.
4407 * @param array $columns The optional room columns to fetch.
4408 * @param bool $no_cache Whether to ignore a previously cached record.
4409 * @param bool $translate Whether to translate the record.
4410 *
4411 * @return array
4412 *
4413 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4414 * @since 1.18.12 (J) - 1.8.12 (WP) added argument $translate.
4415 */
4416 public static function getRoomInfo($idroom, $columns = [], $no_cache = false, $translate = false)
4417 {
4418 static $room_infos = [];
4419
4420 if (isset($room_infos[$idroom]) && !$no_cache) {
4421 return $room_infos[$idroom];
4422 }
4423
4424 $dbo = JFactory::getDbo();
4425
4426 if ($columns) {
4427 $columns = array_map([$dbo, 'qn'], $columns);
4428 }
4429
4430 $q = $dbo->getQuery(true)
4431 ->select($columns ? $columns : '*')
4432 ->from($dbo->qn('#__vikbooking_rooms'))
4433 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4434
4435 $dbo->setQuery($q);
4436 $room = $dbo->loadAssoc();
4437
4438 if ($translate && $room) {
4439 // apply translation
4440 if ($columns && !in_array('id', $columns)) {
4441 // force the record identifier
4442 $room['id'] = $idroom;
4443 }
4444 $records = [$room];
4445 VikBooking::getTranslator()->translateContents($records, '#__vikbooking_rooms');
4446 $room = $records[0];
4447 }
4448
4449 if ($no_cache) {
4450 return $room ?: [];
4451 }
4452
4453 $room_infos[$idroom] = $room ?: [];
4454
4455 return $room_infos[$idroom];
4456 }
4457
4458 public static function loadOrdersRoomsData($idorder)
4459 {
4460 $dbo = JFactory::getDbo();
4461
4462 $q = $dbo->getQuery(true)
4463 ->select($dbo->qn('or') . '.*')
4464 ->select($dbo->qn('r.name', 'room_name'))
4465 ->select($dbo->qn('r.units', 'tot_units'))
4466 ->select($dbo->qn('r.params'))
4467 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4468 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4469 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4470 ->order($dbo->qn('or.id') . ' ASC');
4471
4472 $dbo->setQuery($q);
4473
4474 return $dbo->loadAssocList();
4475 }
4476
4477 public static function sayCategory($ids, $vbo_tn = null)
4478 {
4479 $dbo = JFactory::getDbo();
4480 $split = explode(";", $ids);
4481 $say = "";
4482 foreach ($split as $k => $s) {
4483 if (!strlen($s)) {
4484 continue;
4485 }
4486 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4487 $dbo->setQuery($q);
4488 $nam = $dbo->loadAssocList();
4489 if (!$nam) {
4490 continue;
4491 }
4492 if (is_object($vbo_tn)) {
4493 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4494 }
4495 $say .= $nam[0]['name'];
4496 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4497 }
4498 return $say;
4499 }
4500
4501 /**
4502 * Returns a list of the given room amenities string.
4503 *
4504 * @param string $amenities Room amenities string.
4505 * @param ?object $vbo_tn Optional VBOTranslator.
4506 * @param bool $all True for loading all records when no amenities given.
4507 *
4508 * @return array
4509 *
4510 * @since 1.17.3 (J) - 1.7.3 (WP)
4511 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $all.
4512 */
4513 public static function loadRoomAmenities($amenities, $vbo_tn = null, $all = false)
4514 {
4515 $dbo = JFactory::getDbo();
4516
4517 $split = explode(";", (string) $amenities);
4518 $where = [];
4519 foreach ($split as $s) {
4520 if (!empty($s)) {
4521 $where[] = (int) $s;
4522 }
4523 }
4524
4525 $list = [];
4526 $where = array_filter($where);
4527
4528 $query = $dbo->getQuery(true)
4529 ->select('*')
4530 ->from($dbo->qn('#__vikbooking_characteristics'))
4531 ->order($dbo->qn('ordering') . ' ASC');
4532
4533 if ($where) {
4534 $query->where($dbo->qn('id') . ' IN (' . implode(', ', $where) . ')');
4535 }
4536
4537 if ($where || $all) {
4538 $dbo->setQuery($query);
4539 $list = $dbo->loadAssocList();
4540 if ($list && is_object($vbo_tn)) {
4541 $vbo_tn->translateContents($list, '#__vikbooking_characteristics');
4542 }
4543 }
4544
4545 return $list;
4546 }
4547
4548 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4549 {
4550 $carat = '';
4551 $arr = self::loadRoomAmenities($idc, $vbo_tn);
4552
4553 if ($arr) {
4554 $carat .= "<div class=\"vbo-room-carats\">\n";
4555 foreach ($arr as $a) {
4556 if (!empty($a['textimg'])) {
4557 //tooltip icon text is not empty
4558 if (!empty($a['icon'])) {
4559 //an icon has been uploaded: display the image
4560 $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";
4561 } else {
4562 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4563 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4564 $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";
4565 } else {
4566 //display just the text
4567 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4568 }
4569 }
4570 } else {
4571 $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");
4572 }
4573 }
4574 $carat .= "</div>\n";
4575 }
4576
4577 return $carat;
4578 }
4579
4580 public static function getRoomOptionals($idopts, $vbo_tn = null)
4581 {
4582 $dbo = JFactory::getDbo();
4583 $split = explode(";", $idopts);
4584 $fetch = array();
4585 foreach ($split as $s) {
4586 if (!empty($s)) {
4587 $fetch[] = $s;
4588 }
4589 }
4590 if ($fetch) {
4591 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', array_map('intval', $fetch)) . ") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4592 $dbo->setQuery($q);
4593 $arr = $dbo->loadAssocList();
4594 if ($arr) {
4595 if (is_object($vbo_tn)) {
4596 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4597 }
4598 return $arr;
4599 }
4600 }
4601 return [];
4602 }
4603
4604 public static function getSingleOption($idopt, $vbo_tn = null)
4605 {
4606 $dbo = JFactory::getDbo();
4607 $opt = array();
4608 if (!empty($idopt)) {
4609 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4610 $dbo->setQuery($q);
4611 $opt = $dbo->loadAssoc();
4612 if ($opt && is_object($vbo_tn)) {
4613 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4614 }
4615 }
4616 return $opt;
4617 }
4618
4619 /**
4620 * Unsets the options that are not available in the requested dates.
4621 *
4622 * @param array $optionals the array of options passed by reference
4623 * @param int $checkin the timestamp of the check-in date and time
4624 * @param int $checkout the timestamp of the check-out date and time
4625 *
4626 * @return void
4627 *
4628 * @since 1.11
4629 */
4630 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4631 {
4632 if (!$optionals || empty($checkin) || empty($checkout)) {
4633 return;
4634 }
4635
4636 foreach ($optionals as $k => $v) {
4637 if (!empty($v['alwaysav'])) {
4638 $dates = explode(';', $v['alwaysav']);
4639 if (empty($dates[0]) || empty($dates[1])) {
4640 continue;
4641 }
4642 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4643 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4644 unset($optionals[$k]);
4645 }
4646 }
4647 }
4648 }
4649
4650 /**
4651 * Unsets the options that are not suited for the requested room party.
4652 *
4653 * @param array $optionals the array of options passed by reference.
4654 * @param int $adults the number of adults in the room party.
4655 * @param int $children the number of children in the room party.
4656 *
4657 * @return void
4658 *
4659 * @since 1.13.5
4660 */
4661 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4662 {
4663 if (!$optionals || (empty($adults) && empty($children))) {
4664 return;
4665 }
4666
4667 foreach ($optionals as $k => $v) {
4668 if (empty($v['oparams'])) {
4669 continue;
4670 }
4671 $v['oparams'] = json_decode($v['oparams'], true);
4672 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4673 continue;
4674 }
4675 if (!empty($v['oparams']['minguestsnum'])) {
4676 // filter by minimum adults/guests
4677 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4678 // minimum number of adults not sufficient, unset option
4679 unset($optionals[$k]);
4680 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4681 // minimum number of total guests not sufficient, unset option
4682 unset($optionals[$k]);
4683 }
4684 }
4685 if (!empty($v['oparams']['maxguestsnum'])) {
4686 // filter by maximum adults/guests
4687 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4688 // maximum number of adults exceeded, unset option
4689 unset($optionals[$k]);
4690 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4691 // maximum number of total guests exceeded, unset option
4692 unset($optionals[$k]);
4693 }
4694 }
4695 }
4696 }
4697
4698 /**
4699 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4700 *
4701 * @param array $id_rooms list of room IDs.
4702 * @param int $num_adults number of adults.
4703 * @param int $num_nights number of nights of stay.
4704 * @param int $checkin optional check-in timestamp.
4705 * @param int $checkout optional check-out timestamp.
4706 *
4707 * @return array associative information of city taxes, fees and options.
4708 *
4709 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4710 */
4711 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4712 {
4713 $dbo = JFactory::getDbo();
4714
4715 $taxes = 0;
4716 $fees = 0;
4717
4718 $options_data = [];
4719 $id_options = [];
4720
4721 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4722 $dbo->setQuery($q);
4723 $assocs = $dbo->loadAssocList();
4724 if ($assocs) {
4725 foreach ($assocs as $opts) {
4726 if (!empty($opts['idopt'])) {
4727 $r_ido = explode(';', rtrim($opts['idopt']));
4728 foreach ($r_ido as $ido) {
4729 if (!empty($ido) && !in_array($ido, $id_options)) {
4730 $id_options[] = (int)$ido;
4731 }
4732 }
4733 }
4734 }
4735 }
4736
4737 if ($id_options) {
4738 $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);";
4739 $dbo->setQuery($q);
4740 $alltaxesfees = $dbo->loadAssocList();
4741
4742 if ($checkin && $checkout) {
4743 // filter the records by stay dates to support, for example, seasonal city taxes
4744 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4745 }
4746
4747 foreach ($alltaxesfees as $tf) {
4748 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4749 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4750 $realcost = $tf['maxprice'];
4751 }
4752 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4753
4754 /**
4755 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
4756 *
4757 * @since 1.17.7 (J) - 1.7.7 (WP)
4758 */
4759 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$tf, ['days' => $num_nights], ['adults' => $num_adults]]);
4760 if ($custom_calculation) {
4761 $realcost = (float) $custom_calculation[0];
4762 }
4763
4764 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4765 if ($tf['is_citytax'] == 1) {
4766 $taxes += $realcost;
4767 } elseif ($tf['is_fee'] == 1) {
4768 $fees += $realcost;
4769 }
4770 $optsett = explode('-', $tf['forceval']);
4771 $options_data[] = $tf['id'].':'.$optsett[0];
4772 }
4773 }
4774
4775 return [
4776 'city_taxes' => $taxes,
4777 'fees' => $fees,
4778 'options' => $options_data
4779 ];
4780 }
4781
4782 /**
4783 * From a list of option/extras records, it returns an array with the options/extras
4784 * that do not support age intervals, and options for the children age intervals.
4785 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4786 *
4787 * @param array $optionals the full list of options/extras records.
4788 * @param int $adults the number of adults in the room requested.
4789 * @param int $children the number of children in the room requested.
4790 *
4791 * @return array the filtered regular and children age intervals options.
4792 */
4793 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4794 {
4795 // container for age intervals
4796 $ageintervals = [];
4797
4798 // regular options should start as an array
4799 if (!is_array($optionals)) {
4800 $optionals = [];
4801 }
4802
4803 // check for the first valid age intervals option
4804 foreach ($optionals as $kopt => $opt) {
4805 if (!empty($opt['ageintervals'])) {
4806 $intervals = explode(';;', $opt['ageintervals']);
4807 foreach ($intervals as $intv) {
4808 if (empty($intv)) {
4809 continue;
4810 }
4811 $parts = explode('_', $intv);
4812 if (count($parts) >= 3) {
4813 // age intervals option found, get the first and only one
4814 $ageintervals = $optionals[$kopt];
4815 break 2;
4816 }
4817 }
4818 }
4819 }
4820
4821 if ($ageintervals) {
4822 /**
4823 * We allow price adjustments for a minimum total guests by overriding the costs
4824 * for each age interval for specific child numbers when the party covers some children.
4825 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4826 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4827 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4828 *
4829 * @since 1.13.5
4830 */
4831 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4832 $oparams = !is_array($oparams) ? array() : $oparams;
4833 $adults = (int)$adults;
4834 $children = (int)$children;
4835 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4836 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4837 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4838 if ($children_to_pay < $children) {
4839 // compose string for free age intervals
4840 $intervals = explode(';;', $ageintervals['ageintervals']);
4841 foreach ($intervals as $k => $v) {
4842 $parts = explode('_', $v);
4843 // set cost to 0 (key = 2)
4844 $parts[2] = '0';
4845 $intervals[$k] = implode('_', $parts);
4846 }
4847 $free_ageintervals = implode(';;', $intervals);
4848 for ($i = 1; $i <= $children; $i++) {
4849 if (($children - $i + 1) <= $children_to_pay) {
4850 // costs for this Nth last child should be regular as this child should pay
4851 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4852 } else {
4853 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4854 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4855 }
4856 }
4857 }
4858 }
4859
4860 // remove age intervals from regular options
4861 foreach ($optionals as $kopt => $opt) {
4862 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4863 // unset the option of type age intervals from the regular options
4864 unset($optionals[$kopt]);
4865 }
4866 }
4867 }
4868
4869 // return the filtered list of regular options and age intervals options
4870 return [$optionals, $ageintervals];
4871 }
4872
4873 /**
4874 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4875 * This is because some children in the party may not need to pay anything due to the min guests.
4876 *
4877 * @param array $optional the option full record to parse.
4878 * @param int $adults the number of adults in the room requested.
4879 * @param int $children the number of children in the room requested.
4880 *
4881 * @return array associative array with default age intervals and override strings.
4882 *
4883 * @since 1.13.5
4884 */
4885 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4886 {
4887 if (!is_array($optional)) {
4888 $optional = array();
4889 }
4890
4891 $overrides = array(
4892 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4893 );
4894
4895 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4896 $oparams = !is_array($oparams) ? array() : $oparams;
4897 $adults = (int)$adults;
4898 $children = (int)$children;
4899
4900 if (!count($oparams)) {
4901 return $overrides;
4902 }
4903
4904 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4905 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4906 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4907 if ($children_to_pay < $children) {
4908 // compose string for free age intervals
4909 $intervals = explode(';;', $optional['ageintervals']);
4910 foreach ($intervals as $k => $v) {
4911 $parts = explode('_', $v);
4912 // set cost to 0 (key = 2)
4913 $parts[2] = '0';
4914 $intervals[$k] = implode('_', $parts);
4915 }
4916 $free_ageintervals = implode(';;', $intervals);
4917 for ($i = 1; $i <= $children; $i++) {
4918 if (($children - $i + 1) <= $children_to_pay) {
4919 // costs for this Nth last child should be regular as this child should pay
4920 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4921 } else {
4922 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4923 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4924 }
4925 }
4926 }
4927 }
4928
4929 return $overrides;
4930 }
4931
4932 /**
4933 * Returns the number/index of the children being parsed given the full list of room options string,
4934 * the current key in the array of the split room options string, the ID of the ageintervals option,
4935 * and the total number of children. This is useful for later applying the cost overrides for the
4936 * children ages through the method getOptionIntervalChildOverrides().
4937 *
4938 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4939 * @param int $optid the ID of the option of type age intervals to check.
4940 * @param int $roptkey the current position in the loop of the room options string.
4941 * @param int $children the number of children in the room requested.
4942 *
4943 * @return int the position/number of the children being parsed, -1 if not found.
4944 *
4945 * @since 1.13.5
4946 */
4947 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4948 {
4949 // default to -1 for not found
4950 $child_num = -1;
4951
4952 $valid_opt_counter = 0;
4953
4954 $roptions = explode(";", $roptstr);
4955 foreach ($roptions as $k => $opt) {
4956 if (empty($opt)) {
4957 continue;
4958 }
4959 $optvals = explode(":", $opt);
4960 /**
4961 * In some cases, like the "saveorder" task, the room option string may contain
4962 * the room number beside the option ID, separated with an underscore.
4963 * If an underscore is present, we need to split it to find the option ID.
4964 */
4965 if (strpos($optvals[0], '_') !== false) {
4966 // underscore found in room number portion, extract the option ID
4967 $rn_parts = explode('_', $optvals[0]);
4968 // 0th element is the room number in the party, 1st elem is the option ID
4969 $optvals[0] = $rn_parts[1];
4970 }
4971 //
4972 if ((int)$optvals[0] != (int)$optid) {
4973 // we are not interested into this option ID
4974 continue;
4975 }
4976 // increase counter for this option ID
4977 $valid_opt_counter++;
4978
4979 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4980 // children position found, we need to return it as a 0th base
4981 $child_num = ($valid_opt_counter - 1);
4982 break;
4983 }
4984 }
4985
4986 return $child_num;
4987 }
4988
4989 public static function getOptionIntervalsCosts($intvstr) {
4990 $optcosts = array();
4991 $intervals = explode(';;', $intvstr);
4992 foreach ($intervals as $kintv => $intv) {
4993 if (empty($intv)) continue;
4994 $parts = explode('_', $intv);
4995 if (count($parts) >= 3) {
4996 $optcosts[$kintv] = (float)$parts[2];
4997 }
4998 }
4999 return $optcosts;
5000 }
5001
5002 public static function getOptionIntervalsAges($intvstr) {
5003 $optages = array();
5004 $intervals = explode(';;', $intvstr);
5005 foreach ($intervals as $kintv => $intv) {
5006 if (empty($intv)) continue;
5007 $parts = explode('_', $intv);
5008 if (count($parts) >= 3) {
5009 $optages[$kintv] = $parts[0].' - '.$parts[1];
5010 }
5011 }
5012 return $optages;
5013 }
5014
5015 public static function getOptionIntervalsPercentage($intvstr) {
5016 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
5017 $optcostspcent = array();
5018 $intervals = explode(';;', $intvstr);
5019 foreach ($intervals as $kintv => $intv) {
5020 if (empty($intv)) continue;
5021 $parts = explode('_', $intv);
5022 if (count($parts) >= 3) {
5023 //fixed amount
5024 $setval = 0;
5025 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
5026 //percentage value of the room base cost (VBO 1.10)
5027 $setval = 2;
5028 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
5029 //percentage value of the adults tariff
5030 $setval = 1;
5031 }
5032 $optcostspcent[$kintv] = $setval;
5033 }
5034 }
5035 return $optcostspcent;
5036 }
5037
5038 public static function dayValidTs($days, $first, $second)
5039 {
5040 $secdiff = $second - $first;
5041 $daysdiff = $secdiff / 86400;
5042 if (is_int($daysdiff)) {
5043 if ($daysdiff < 1) {
5044 $daysdiff = 1;
5045 }
5046 } else {
5047 if ($daysdiff < 1) {
5048 $daysdiff = 1;
5049 } else {
5050 $sum = floor($daysdiff) * 86400;
5051 $newdiff = $secdiff - $sum;
5052 $maxhmore = self::getHoursMoreRb() * 3600;
5053 if ($maxhmore >= $newdiff) {
5054 $daysdiff = floor($daysdiff);
5055 } else {
5056 $daysdiff = ceil($daysdiff);
5057 }
5058 }
5059 }
5060
5061 return ($daysdiff == $days);
5062 }
5063
5064 /**
5065 * Calculates the stay fee inclusive of taxes.
5066 *
5067 * @param float $cost the cost to evaluate.
5068 * @param int $idprice the rate plan ID associated.
5069 *
5070 * @return float the given cost after tax, if taxes had to be applied.
5071 */
5072 public static function sayCostPlusIva($cost, $idprice)
5073 {
5074 $ivainclusa = self::ivaInclusa();
5075 if ($ivainclusa) {
5076 return $cost;
5077 }
5078
5079 static $tax_rplans_map = [];
5080
5081 if (array_key_exists($idprice, $tax_rplans_map)) {
5082 $tax_rate = $tax_rplans_map[$idprice];
5083 } else {
5084 $dbo = JFactory::getDbo();
5085 $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 . ";";
5086 $dbo->setQuery($q);
5087 $tax_rate = $dbo->loadAssoc();
5088
5089 // cache value
5090 $tax_rplans_map[$idprice] = $tax_rate;
5091 }
5092
5093 if (!$tax_rate || empty($tax_rate['aliq'])) {
5094 return $cost;
5095 }
5096
5097 $subt = 100 + $tax_rate['aliq'];
5098 $op = ($cost * $subt / 100);
5099
5100 /**
5101 * Tax Cap implementation for prices tax excluded (most common).
5102 *
5103 * @since 1.12
5104 */
5105 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5106 $op = ($cost + $tax_rate['taxcap']);
5107 }
5108
5109 // apply rounding to avoid issues with multiple tax rates when tax excluded
5110 $formatvals = self::getNumberFormatData();
5111 $formatparts = explode(':', $formatvals);
5112 $rounded_op = round($op, (int)$formatparts[0]);
5113 if ($rounded_op > $op) {
5114 return $rounded_op;
5115 }
5116
5117 /**
5118 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5119 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5120 *
5121 * @since 1.11.1
5122 */
5123 return round($op, (int)$formatparts[0]);
5124 }
5125
5126 /**
5127 * Calculates the stay fee exclusive of taxes.
5128 *
5129 * @param float $cost the cost to evaluate.
5130 * @param int $idprice the rate plan ID associated.
5131 *
5132 * @return float the given cost before tax.
5133 */
5134 public static function sayCostMinusIva($cost, $idprice)
5135 {
5136 $ivainclusa = self::ivaInclusa();
5137 if (!$ivainclusa) {
5138 return $cost;
5139 }
5140
5141 static $tax_rplans_map = [];
5142
5143 if (array_key_exists($idprice, $tax_rplans_map)) {
5144 $tax_rate = $tax_rplans_map[$idprice];
5145 } else {
5146 $dbo = JFactory::getDbo();
5147 $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 . ";";
5148 $dbo->setQuery($q);
5149 $tax_rate = $dbo->loadAssoc();
5150
5151 // cache value
5152 $tax_rplans_map[$idprice] = $tax_rate;
5153 }
5154
5155 if (!$tax_rate || empty($tax_rate['aliq'])) {
5156 return $cost;
5157 }
5158
5159 $subt = 100 + $tax_rate['aliq'];
5160 $op = ($cost * 100 / $subt);
5161
5162 /**
5163 * Tax Cap implementation also when prices tax included.
5164 *
5165 * @since 1.12
5166 */
5167 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5168 $op = ($cost - $tax_rate['taxcap']);
5169 }
5170
5171 // apply rounding to avoid issues with multiple tax rates when tax included
5172 $formatvals = self::getNumberFormatData();
5173 $formatparts = explode(':', $formatvals);
5174 $rounded_op = round($op, (int)$formatparts[0]);
5175 if ($rounded_op < $op) {
5176 return $rounded_op;
5177 }
5178
5179 /**
5180 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5181 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5182 *
5183 * @since 1.11.1
5184 */
5185 return round($op, (int)$formatparts[0]);
5186 }
5187
5188 /**
5189 * Given an option/extra cost, determines whether taxes should be applied over it.
5190 * Can also be used to calculate taxes on the extra costs per room in the bookings
5191 *
5192 * @param float $cost the cost to evaluate, option or extra service fee.
5193 * @param int $idiva the ID of the tax rate to use to apply taxes.
5194 * @param bool $force if true, taxes will always be applied.
5195 *
5196 * @return float the given option/extra cost plus tax, if taxes had to be applied.
5197 *
5198 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5199 */
5200 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
5201 {
5202 $ivainclusa = self::ivaInclusa();
5203 if ($ivainclusa && $force !== true) {
5204 return $cost;
5205 }
5206
5207 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5208 if (!$tax_rate) {
5209 return $cost;
5210 }
5211
5212 $subt = 100 + $tax_rate['aliq'];
5213 $op = ($cost * $subt / 100);
5214
5215 /**
5216 * Tax Cap implementation for prices tax excluded (most common).
5217 *
5218 * @since 1.12
5219 */
5220 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5221 $op = ($cost + $tax_rate['taxcap']);
5222 }
5223
5224 // apply rounding to avoid issues with multiple tax rates when tax excluded
5225 $formatvals = self::getNumberFormatData();
5226 $formatparts = explode(':', $formatvals);
5227 $rounded_op = round($op, (int)$formatparts[0]);
5228 if ($rounded_op > $op) {
5229 return $rounded_op;
5230 }
5231
5232 return $op;
5233 }
5234
5235 /**
5236 * Calculates the net amount for an option/extra service.
5237 *
5238 * @param float $cost the cost to evaluate, option or extra service fee.
5239 * @param int $idiva the ID of the tax rate to use to apply taxes.
5240 *
5241 * @return float the given option/extra cost before tax, if taxes were inclusive.
5242 */
5243 public static function sayOptionalsMinusIva($cost, $idiva)
5244 {
5245 if (empty($idiva)) {
5246 return $cost;
5247 }
5248
5249 $ivainclusa = self::ivaInclusa();
5250 if (!$ivainclusa) {
5251 return $cost;
5252 }
5253
5254 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5255 if (!$tax_rate) {
5256 return $cost;
5257 }
5258
5259 $subt = 100 + $tax_rate['aliq'];
5260 $op = ($cost * 100 / $subt);
5261
5262 /**
5263 * Tax Cap implementation also when prices tax included.
5264 *
5265 * @since 1.12
5266 */
5267 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5268 $op = ($cost - $tax_rate['taxcap']);
5269 }
5270
5271 // apply rounding to avoid issues with multiple tax rates when tax included
5272 $formatvals = self::getNumberFormatData();
5273 $formatparts = explode(':', $formatvals);
5274 $rounded_op = round($op, (int)$formatparts[0]);
5275 if ($rounded_op < $op) {
5276 return $rounded_op;
5277 }
5278
5279 return $op;
5280 }
5281
5282 /**
5283 * Given a cost, determines whether taxes should be applied over it.
5284 *
5285 * @param float $cost the cost to evaluate, could be a room custom price.
5286 * @param int $idiva the ID of the tax rate to use to apply taxes.
5287 * @param bool $force if true, taxes will always be applied.
5288 *
5289 * @return float the given cost plus tax, if taxes had to be applied.
5290 *
5291 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5292 */
5293 public static function sayPackagePlusIva($cost, $idiva, $force = false)
5294 {
5295 $ivainclusa = self::ivaInclusa();
5296 if ($ivainclusa && $force !== true) {
5297 return $cost;
5298 }
5299
5300 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5301 if (!$tax_rate) {
5302 return $cost;
5303 }
5304
5305 $subt = 100 + $tax_rate['aliq'];
5306 $op = ($cost * $subt / 100);
5307
5308 /**
5309 * Tax Cap implementation for prices tax excluded (most common).
5310 *
5311 * @since 1.12
5312 */
5313 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5314 $op = ($cost + $tax_rate['taxcap']);
5315 }
5316
5317 // apply rounding to avoid issues with multiple tax rates when tax excluded
5318 $formatvals = self::getNumberFormatData();
5319 $formatparts = explode(':', $formatvals);
5320 $rounded_op = round($op, (int)$formatparts[0]);
5321 if ($rounded_op > $op) {
5322 return $rounded_op;
5323 }
5324
5325 return $op;
5326 }
5327
5328 /**
5329 * Calculates the net amount for a package.
5330 *
5331 * @param float $cost the cost to evaluate, option or extra service fee.
5332 * @param int $idiva the ID of the tax rate to use to apply taxes.
5333 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
5334 *
5335 * @return float the given cost before tax.
5336 */
5337 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
5338 {
5339 $ivainclusa = self::ivaInclusa();
5340 if (!$ivainclusa && $force_invoice_excltax !== true) {
5341 return $cost;
5342 }
5343
5344 $tax_rate = VBOTaxonomySummary::getTaxRateRecord((int) $idiva);
5345 if (!$tax_rate) {
5346 return $cost;
5347 }
5348
5349 $subt = 100 + $tax_rate['aliq'];
5350 $op = ($cost * 100 / $subt);
5351
5352 /**
5353 * Tax Cap implementation also when prices tax included.
5354 *
5355 * @since 1.12
5356 */
5357 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5358 $op = ($cost - $tax_rate['taxcap']);
5359 }
5360
5361 // apply rounding to avoid issues with multiple tax rates when tax included
5362 $formatvals = self::getNumberFormatData();
5363 $formatparts = explode(':', $formatvals);
5364 $rounded_op = round($op, (int)$formatparts[0]);
5365 if ($rounded_op < $op) {
5366 return $rounded_op;
5367 }
5368
5369 return $op;
5370 }
5371
5372 public static function getSecretLink()
5373 {
5374 $dbo = JFactory::getDbo();
5375
5376 $sid = mt_rand();
5377
5378 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
5379 $dbo->setQuery($q);
5380 $all = $dbo->loadAssocList();
5381 if ($all) {
5382 $list = [];
5383 foreach ($all as $s) {
5384 $list[] = $s['sid'];
5385 }
5386 if (in_array($sid, $list)) {
5387 while(in_array($sid, $list)) {
5388 $sid++;
5389 }
5390 }
5391 }
5392
5393 return $sid;
5394 }
5395
5396 /**
5397 * Generates a confirmation number for the given booking.
5398 * Modified the way random string-numbers are obtained to
5399 * avoid any possible brute-force attempt.
5400 *
5401 * @since 1.15.1 (J) - 1.5.4 (WP)
5402 */
5403 public static function generateConfirmNumber($oid, $update = true)
5404 {
5405 $dbo = JFactory::getDbo();
5406
5407 $confirmnumb = date('y') . $oid;
5408 for ($i = 0; $i < 8; $i++) {
5409 $confirmnumb .= rand(0, 9);
5410 }
5411
5412 if ($update) {
5413 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5414 $dbo->setQuery($q);
5415 $dbo->execute();
5416 }
5417
5418 return $confirmnumb;
5419 }
5420
5421 public static function buildCustData($arr, $sep) {
5422 $cdata = "";
5423 foreach ($arr as $k => $e) {
5424 if (strlen($e)) {
5425 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5426 }
5427 }
5428 return $cdata;
5429 }
5430
5431 /**
5432 * This method parses the Joomla menu object to see if a menu item of a
5433 * specific type is available, to get its ID. Useful when links should be
5434 * displayed in pages where there is no Itemid set (booking details pages).
5435 *
5436 * @param array $viewtypes list of accepted menu items
5437 * @param string $lang the optional language to use.
5438 *
5439 * @return int
5440 *
5441 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5442 */
5443 public static function findProperItemIdType($viewtypes, $lang = null)
5444 {
5445 if (VBOPlatformDetection::isWordPress()) {
5446 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5447
5448 $itemid = $model->best($viewtypes, $lang);
5449
5450 if ($itemid) {
5451 return $itemid;
5452 }
5453
5454 return 0;
5455 }
5456
5457 $bestitemid = 0;
5458
5459 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5460
5461 $app = JFactory::getApplication();
5462
5463 $menu = $app->getMenu('site');
5464
5465 if (!$menu) {
5466 return 0;
5467 }
5468
5469 $menu_items = $menu->getMenu();
5470
5471 if (!$menu_items) {
5472 return 0;
5473 }
5474
5475 foreach ($menu_items as $itemid => $item) {
5476 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5477 // proper menu item type found
5478 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5479
5480 if (isset($item->language) && $item->language == $current_lang) {
5481 // we found the exact menu item type for the given language
5482 return $itemid;
5483 }
5484 }
5485 }
5486
5487 return $bestitemid;
5488 }
5489
5490 /**
5491 * Rewrites an internal URI that needs to be used outside of the website.
5492 * This means that the routed URI MUST start with the base path of the site.
5493 *
5494 * @param mixed $query The query string or an associative array of data.
5495 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5496 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5497 *
5498 * @return string The complete routed URI.
5499 *
5500 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5501 * routing from back-end (if available on the CMS version).
5502 */
5503 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5504 {
5505 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5506 }
5507
5508 /**
5509 * Generates an iCal file to be attached to the email message for the
5510 * customer or the administrator with some basic booking details.
5511 *
5512 * @param string $recip either admin or customer.
5513 * @param array $booking the booking array or some keys.
5514 *
5515 * @return mixed string in case of success, false otherwise.
5516 *
5517 * @since 1.12.0
5518 */
5519 public static function getEmailIcal($recip, $booking) {
5520 // load configuration setting
5521 $attachical = self::attachIcal();
5522
5523 if ($attachical === 0) {
5524 // do not attach any iCal file
5525 return false;
5526 }
5527
5528 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5529 // skip the iCal for the admin
5530 return false;
5531 }
5532
5533 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5534 // skip the iCal for the customer
5535 return false;
5536 }
5537
5538 if (strpos($recip, 'admin') !== false) {
5539 // prepare event description and summary for the admin
5540 $description = $booking['custdata'];
5541 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5542 $fname = $booking['ts'] . '.ics';
5543 } else {
5544 // event description and summary for the customer
5545 $description = '';
5546 $summary = self::getFrontTitle();
5547 $fname = 'reservation_reminder.ics';
5548 }
5549
5550 // prepare iCal head
5551 $company_name = self::getFrontTitle();
5552 $ics_str = "BEGIN:VCALENDAR\r\n" .
5553 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5554 "CALSCALE:GREGORIAN\r\n" .
5555 "VERSION:2.0\r\n";
5556 // compose iCal body
5557 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5558 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5559 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5560 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5561 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5562 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5563 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5564 $ics_str .= 'END:VEVENT'."\r\n";
5565 // close iCal file content
5566 $ics_str .= "END:VCALENDAR";
5567
5568 /**
5569 * Trigger event to allow third party plugins to overwrite the iCal file.
5570 *
5571 * @since 1.16.0 (J) - 1.6.0 (WP)
5572 */
5573 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5574
5575 if (empty($ics_str)) {
5576 return false;
5577 }
5578
5579 // store the event onto a .ics file. We use the resources folder in back-end.
5580 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5581 $fp = fopen($fpath, 'w+');
5582 $bytes = fwrite($fp, $ics_str);
5583 fclose($fp);
5584
5585 return $bytes ? $fpath : false;
5586 }
5587
5588 public static function loadEmailTemplate($booking_info = array()) {
5589 define('_VIKBOOKINGEXEC', '1');
5590 ob_start();
5591 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5592 $content = ob_get_contents();
5593 ob_end_clean();
5594 return $content;
5595 }
5596
5597 /**
5598 * Parses the raw HTML content of the booking email template.
5599 *
5600 * @param string $tmpl the raw content of the template.
5601 * @param mixed $bid int for the booking ID or booking array.
5602 * @param array $rooms list of rooms booked and translated.
5603 * @param array $rates list of translated rates for the booked rooms.
5604 * @param array $options list of translated options booked.
5605 * @param float [$total] the booking total amount (in case it has changed).
5606 * @param string [$link] the booking link can be passed for the no-deposit.
5607 *
5608 * @return string the HTML content of the parsed email template.
5609 *
5610 * @since 1.13 with different arguments.
5611 */
5612 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5613 {
5614 $app = JFactory::getApplication();
5615 $dbo = JFactory::getDbo();
5616 $vbo_tn = self::getTranslator();
5617
5618 // availability helper
5619 $av_helper = self::getAvailabilityInstance();
5620
5621 // get necessary values
5622 if (is_array($bid)) {
5623 // we got the full booking record
5624 $order_info = $bid;
5625 $bid = $order_info['id'];
5626 } else {
5627 $order_info = array();
5628 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5629 $dbo->setQuery($q);
5630 $order_info = $dbo->loadAssoc();
5631 if (!$order_info) {
5632 throw new Exception('Booking not found', 404);
5633 }
5634 }
5635
5636 $tars_info = array();
5637 $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'] . ";";
5638 $dbo->setQuery($q);
5639 $tars_info = $dbo->loadAssocList();
5640 if (!$tars_info) {
5641 throw new Exception('No rooms found', 404);
5642 }
5643
5644 /**
5645 * Split stay reservation.
5646 *
5647 * @since 1.16.0 (J) - 1.6.0 (WP)
5648 */
5649 $room_stay_dates = [];
5650 if ($order_info['split_stay']) {
5651 if ($order_info['status'] == 'confirmed') {
5652 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5653 } else {
5654 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5655 }
5656 // immediately count the number of nights of stay for each split room
5657 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5658 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5659 // overwrite values for compatibility with non-confirmed bookings
5660 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5661 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5662 }
5663 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5664 // overwrite the whole array
5665 $room_stay_dates[$sps_r_k] = $sps_r_v;
5666 }
5667 }
5668
5669 // check if the language in use is the same as the one used during the checkout
5670 $lang = JFactory::getLanguage();
5671 if (!empty($order_info['lang'])) {
5672 if ($lang->getTag() != $order_info['lang']) {
5673 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5674 if (VBOPlatformDetection::isJoomla()) {
5675 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5676 }
5677 }
5678 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5679 // force the translation to start because contents should be translated
5680 $vbo_tn::$force_tolang = $order_info['lang'];
5681 }
5682 }
5683
5684 // values for replacements
5685 $company_name = self::getFrontTitle();
5686 $currencyname = self::getCurrencyName();
5687 $sitelogo = self::getSiteLogo();
5688 $footermess = self::getFooterOrdMail($vbo_tn);
5689 $dateformat = self::getDateFormat();
5690 $datesep = self::getDateSeparator();
5691 if ($dateformat == "%d/%m/%Y") {
5692 $df = 'd/m/Y';
5693 } elseif ($dateformat == "%m/%d/%Y") {
5694 $df = 'm/d/Y';
5695 } else {
5696 $df = 'Y/m/d';
5697 }
5698 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5699 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5700 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5701 $customer_info = nl2br($order_info['custdata']);
5702 $company_logo = '';
5703 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5704 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5705 }
5706 if ($order_info['status'] == 'cancelled') {
5707 $confirmnumber = '';
5708 $status_str = JText::translate('VBCANCELLED');
5709 } elseif ($order_info['status'] == 'standby') {
5710 $confirmnumber = '';
5711 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5712 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5713 // this is an inquiry reservation
5714 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5715 }
5716 } else {
5717 $confirmnumber = $order_info['confirmnumber'];
5718 $status_str = JText::translate('VBCOMPLETED');
5719 }
5720 // booking total amount
5721 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5722 // booking link
5723 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5724 if (is_null($link)) {
5725 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5726 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5727 }
5728
5729 // raw HTML content
5730 $parsed = $tmpl;
5731
5732 /**
5733 * Trigger event to allow third-party plugins to manipulate the template string.
5734 *
5735 * @since 1.16.10 (J) - 1.6.10 (WP)
5736 */
5737 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5738
5739 /**
5740 * Parse all conditional text rules.
5741 *
5742 * @since 1.14 (J) - 1.4.0 (WP)
5743 */
5744 self::getConditionalRulesInstance()
5745 ->set(['booking', 'rooms'], [$order_info, $rooms])
5746 ->parseTokens($parsed);
5747
5748 /**
5749 * Parse all Door Access Control tags.
5750 *
5751 * @since 1.18.4 (J) - 1.8.4 (WP)
5752 */
5753 VBOFactory::getDoorAccessControl()
5754 ->parseTokens((new VBOBookingRegistry($order_info, $rooms)), $parsed);
5755
5756 // special tokens (tags) replacement
5757 $parsed = str_replace("{logo}", $company_logo, $parsed);
5758 $parsed = str_replace("{company_name}", $company_name, $parsed);
5759 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5760 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5761 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5762 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5763 $parsed = str_replace("{order_status}", $status_str, $parsed);
5764 $parsed = str_replace("{order_date}", $create_date, $parsed);
5765
5766 // customer record
5767 $cpin = self::getCPinInstance();
5768 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5769 $customer_name = '';
5770 $customer_pin = '';
5771 if ($customer_record) {
5772 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5773 $customer_pin = $customer_record['pin'];
5774 }
5775 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5776 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5777
5778 // PIN Code
5779 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5780 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5781 if (!empty($customer_pin)) {
5782 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5783 }
5784 }
5785
5786 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5787
5788 // Confirmation Number
5789 if ($confirmnumber) {
5790 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5791 } else {
5792 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5793 }
5794 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5795 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5796
5797 $roomsnum = count($rooms);
5798 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5799 $roomstr = "";
5800 $tot_adults = 0;
5801 $tot_children = 0;
5802 $tot_guests = 0;
5803
5804 // Rooms Distinctive Features
5805 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5806
5807 foreach ($rooms as $num => $r) {
5808 // guests
5809 $tot_adults += (int) $r['adults'];
5810 $tot_children += (int) $r['children'];
5811 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5812
5813 // room info
5814 $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/>";
5815 // Rooms Distinctive Features
5816 if (is_array($matches[1] ?? []) && $matches[1]) {
5817 $distinctive_features = array();
5818 $rparams = (array)json_decode($r['params'], true);
5819 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'])) {
5820 $distinctive_features = $rparams['features'][$r['roomindex']];
5821 }
5822 $docheck = (count($distinctive_features) > 0);
5823 foreach ($matches[1] as $reqf) {
5824 $feature_found = false;
5825 if ($docheck) {
5826 foreach ($distinctive_features as $dfk => $dfv) {
5827 if (stripos($dfk, $reqf) !== false) {
5828 $feature_found = $dfk;
5829 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5830 break;
5831 }
5832 }
5833 }
5834 }
5835 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5836 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5837 }
5838 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5839 }
5840 }
5841 }
5842
5843 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5844 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5845 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5846
5847 // custom fields replace
5848 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5849 if (is_array($cmatches[1]) && count($cmatches[1])) {
5850 $cfids = array();
5851 foreach ($cmatches[1] as $cfid ) {
5852 $cfids[] = $cfid;
5853 }
5854 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5855 $dbo->setQuery($q);
5856 $cfields = $dbo->loadAssocList();
5857 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5858 $cfmap = array();
5859 foreach ($cfields as $cf) {
5860 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5861 }
5862 $cfmapreplace = array();
5863 $partsreceived = explode("\n", $order_info['custdata']);
5864 if (count($partsreceived) > 0) {
5865 foreach ($partsreceived as $pst) {
5866 if (!empty($pst)) {
5867 $tmpdata = explode(":", $pst);
5868 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5869 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5870 }
5871 }
5872 }
5873 }
5874 foreach ($cmatches[1] as $cfid ) {
5875 if (array_key_exists($cfid, $cfmapreplace)) {
5876 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5877 } else {
5878 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5879 }
5880 }
5881 }
5882
5883 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5884 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5885 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5886 $parsed = str_replace("{num_nights}", ($order_info['days'] ?? 1), $parsed);
5887
5888 // order details
5889 $orderdetails = "";
5890 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5891 foreach ($rooms as $num => $r) {
5892 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5893 $use_room_ind = $num - $room_loop_ind;
5894 $split_stay_str = '';
5895 $split_stay_info = [];
5896 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) {
5897 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5898 $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']);
5899 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5900 }
5901
5902 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5903 $faredets = explode(":", $expdet[0]);
5904 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5905 if (!empty($expdet[1])) {
5906 $attrfaredets = explode(":", $expdet[1]);
5907 if (strlen($attrfaredets[1]) > 0) {
5908 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5909 }
5910 }
5911 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5912 $orderdetails .= '<div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($fareprice), $currencyname) . '</span></div></div>';
5913 // options
5914 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5915 foreach ($options[$num] as $oo) {
5916 $expopts = explode("\n", $oo);
5917 foreach ($expopts as $optinfo) {
5918 if (!empty($optinfo)) {
5919 $splitopt = explode(":", $optinfo);
5920 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5921 $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>';
5922 }
5923 }
5924 }
5925 }
5926 //
5927 if ($roomsnum > 1 && $num < $roomsnum) {
5928 $orderdetails .= '<br/>';
5929 }
5930 }
5931
5932 // coupon
5933 if (!empty($order_info['coupon'])) {
5934 $expcoupon = explode(";", $order_info['coupon']);
5935 $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>';
5936 }
5937
5938 // discount and payment method
5939 $payment_info = [];
5940 $payment_name = '';
5941 if (!empty($order_info['idpayment'])) {
5942 $exppay = explode('=', $order_info['idpayment']);
5943 $payment = self::getPayment($exppay[0], $vbo_tn);
5944 if ((array) $payment) {
5945 $payment_name = $payment['name'];
5946 }
5947 }
5948
5949 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5950
5951 if ($order_info['status'] != 'cancelled') {
5952 if ($payment_info) {
5953 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5954 // Discount (not charge)
5955 if ($payment_info['val_pcent'] == 1) {
5956 // fixed value
5957 $total -= $payment_info['charge'];
5958 $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>';
5959 } else {
5960 // percent value
5961 $percent_disc = $total * $payment_info['charge'] / 100;
5962 $total -= $percent_disc;
5963 $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>';
5964 }
5965 }
5966 }
5967 }
5968
5969 // booking details string
5970 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5971
5972 // additional information
5973 $parsed = str_replace("{order_total}", self::formatCurrencyNumber(self::numberFormat($total), $currencyname), $parsed);
5974 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5975 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5976 $parsed = str_replace("{booking_link}", $link, $parsed);
5977
5978 // deposit
5979 $deposit_str = '';
5980 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5981 $percentdeposit = self::getAccPerCent();
5982 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5983 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5984 if (self::getTypeDeposit() == "fixed") {
5985 $deposit_amount = $percentdeposit;
5986 } else {
5987 $baseTotal = $total;
5988 if ($order_info['tot_damage_dep'] ?? 0) {
5989 // check if the damage deposit will have to be paid later/separately
5990 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($order_info, $rooms);
5991 if (($damage_deposit_payment['damagedep_gross'] ?? 0) && ($damage_deposit_payment['payment_window'] ?? []) && ($damage_deposit_payment['payment_window']['payable'] ?? null) !== true) {
5992 // damage deposit is not payable right now
5993 $baseTotal -= (float) $order_info['tot_damage_dep'];
5994 }
5995 }
5996 $deposit_amount = $baseTotal * $percentdeposit / 100;
5997 }
5998 if ($deposit_amount > 0) {
5999 $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>';
6000 }
6001 }
6002 }
6003 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
6004 //
6005 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
6006 $totpaid_str = '';
6007 if ($order_info['refund'] > 0) {
6008 $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>';
6009 }
6010 if ($order_info['status'] != 'cancelled') {
6011 $tot_paid = $order_info['totpaid'];
6012 $diff_topay = (float)$total - (float)$tot_paid;
6013 if ((float)$tot_paid > 0) {
6014 $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>';
6015 // only in case the remaining balance is greater than 1 to avoid commissions issues
6016 if ($diff_topay > 1) {
6017 $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>';
6018 }
6019 }
6020 }
6021 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
6022 $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>';
6023 }
6024 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
6025
6026 /**
6027 * Language direction (LTR or RTL).
6028 *
6029 * @since 1.16.4 (J) - 1.6.4 (WP)
6030 */
6031 $lang_direction = 'ltr';
6032 if (!empty($order_info['lang'])) {
6033 // attempt to define the language direction and replace the special tag
6034 try {
6035 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
6036 } catch (Throwable $e) {
6037 // do nothing, default to ltr
6038 $lang_direction = 'ltr';
6039 }
6040 // static map for Arabic and Hebrew
6041 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
6042 }
6043 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
6044 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
6045 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
6046
6047 /**
6048 * Replace static floating styles depending on language direction (LTR or RTL).
6049 * For BC with older email template sources, we need to keep the inline style
6050 * for the floating of certain elements, which looks good only on LTR.
6051 *
6052 * @since 1.16.4 (J) - 1.6.4 (WP)
6053 */
6054 if ($lang_direction === 'rtl') {
6055 // get rid of inline static style declaration because the template uses CSS classes
6056 $parsed = str_replace("float: right;", '', $parsed);
6057 }
6058
6059 return $parsed;
6060 }
6061
6062 /**
6063 * New method for sending booking email messages
6064 * to the guest or to the administrator(s).
6065 *
6066 * @param int $bid the booking ID.
6067 * @param array $for guest, admin or a custom email address.
6068 * @param bool $send whether to send or return the HTML message.
6069 * @param bool $no_config if true, no configuration settings will be used for the status.
6070 * @param string $type optional type to indicate a booking modification.
6071 *
6072 * @return mixed True or False depending on the result or HTML string for the preview.
6073 *
6074 * @since 1.13 (J) - 1.3 (WP)
6075 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
6076 * the reservation status is pending and against the configuration.
6077 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
6078 */
6079 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
6080 {
6081 $dbo = JFactory::getDbo();
6082 $app = JFactory::getApplication();
6083
6084 $vbo_tn = self::getTranslator();
6085 $av_helper = self::getAvailabilityInstance();
6086
6087 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
6088 $dbo->setQuery($q);
6089 $booking = $dbo->loadAssoc();
6090 if (!$booking) {
6091 return false;
6092 }
6093
6094 $result = false;
6095
6096 // check if the language in use is the same as the one used during the checkout
6097 $lang = JFactory::getLanguage();
6098 if (!empty($booking['lang'])) {
6099 if ($lang->getTag() != $booking['lang']) {
6100 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
6101 if (VBOPlatformDetection::isJoomla()) {
6102 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
6103 }
6104 }
6105 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
6106 // force the translation to start because contents should be translated
6107 $vbo_tn::$force_tolang = $booking['lang'];
6108 }
6109 }
6110
6111 // load rooms booked
6112 $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;";
6113 $dbo->setQuery($q);
6114 $ordersrooms = $dbo->loadAssocList();
6115 if (!$ordersrooms) {
6116 return false;
6117 }
6118 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
6119
6120 /**
6121 * Split stay reservation.
6122 *
6123 * @since 1.16.0 (J) - 1.6.0 (WP)
6124 */
6125 $room_stay_dates = [];
6126 if ($booking['split_stay']) {
6127 if ($booking['status'] == 'confirmed') {
6128 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
6129 } else {
6130 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
6131 }
6132 // immediately count the number of nights of stay for each split room
6133 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6134 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6135 // overwrite values for compatibility with non-confirmed bookings
6136 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6137 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6138 }
6139 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6140 // overwrite the whole array
6141 $room_stay_dates[$sps_r_k] = $sps_r_v;
6142 }
6143 }
6144
6145 $rooms = array();
6146 $tars = array();
6147 $is_package = !empty($booking['pkg']) ? true : false;
6148 $ftitle = self::getFrontTitle();
6149 $currencyname = self::getCurrencyName();
6150 foreach ($ordersrooms as $kor => $or) {
6151 $num = $kor + 1;
6152 $rooms[$num] = $or;
6153 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6154 // package or custom cost set from the back-end
6155 continue;
6156 }
6157
6158 // determine the days to consider for the count of the availability
6159 $room_nights = $booking['days'];
6160 $room_checkin = $booking['checkin'];
6161 $room_checkout = $booking['checkout'];
6162 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6163 $room_nights = $room_stay_dates[$kor]['nights'];
6164 $room_checkin = $room_stay_dates[$kor]['checkin'];
6165 $room_checkout = $room_stay_dates[$kor]['checkout'];
6166 }
6167
6168 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
6169 $dbo->setQuery($q);
6170 $tar = $dbo->loadAssocList();
6171 if (!$tar) {
6172 // tariff not found
6173 if (self::isAdmin()) {
6174 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
6175 }
6176 continue;
6177 }
6178
6179 // apply seasonal rates
6180 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
6181
6182 // different usage
6183 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
6184 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
6185 // Occupancy Override
6186 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
6187 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
6188 //
6189 if (is_array($diffusageprice)) {
6190 // set a charge or discount to the price(s) for the different usage of the room
6191 foreach ($tar as $kpr => $vpr) {
6192 $tar[$kpr]['diffusage'] = $or['adults'];
6193 if ($diffusageprice['chdisc'] == 1) {
6194 // charge
6195 if ($diffusageprice['valpcent'] == 1) {
6196 // fixed value
6197 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6198 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6199 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
6200 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6201 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
6202 } else {
6203 // percentage value
6204 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6205 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
6206 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
6207 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6208 $tar[$kpr]['cost'] = $aduseval;
6209 }
6210 } else {
6211 // discount
6212 if ($diffusageprice['valpcent'] == 1) {
6213 // fixed value
6214 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6215 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6216 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
6217 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6218 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
6219 } else {
6220 // percentage value
6221 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6222 $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);
6223 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
6224 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6225 $tar[$kpr]['cost'] = $aduseval;
6226 }
6227 }
6228 }
6229 }
6230 }
6231 //
6232 $tars[$num] = $tar[0];
6233 }
6234
6235 $secdiff = $booking['checkout'] - $booking['checkin'];
6236 $daysdiff = $secdiff / 86400;
6237 if (is_int($daysdiff)) {
6238 if ($daysdiff < 1) {
6239 $daysdiff = 1;
6240 }
6241 } else {
6242 if ($daysdiff < 1) {
6243 $daysdiff = 1;
6244 } else {
6245 $sum = floor($daysdiff) * 86400;
6246 $newdiff = $secdiff - $sum;
6247 $maxhmore = self::getHoursMoreRb() * 3600;
6248 if ($maxhmore >= $newdiff) {
6249 $daysdiff = floor($daysdiff);
6250 } else {
6251 $daysdiff = ceil($daysdiff);
6252 }
6253 }
6254 }
6255
6256 $isdue = 0;
6257 $pricestr = array();
6258 $optstr = array();
6259 foreach ($ordersrooms as $kor => $or) {
6260 $num = $kor + 1;
6261
6262 // determine the days to consider for the count of the availability
6263 $room_nights = $booking['days'];
6264 $room_checkin = $booking['checkin'];
6265 $room_checkout = $booking['checkout'];
6266 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6267 $room_nights = $room_stay_dates[$kor]['nights'];
6268 $room_checkin = $room_stay_dates[$kor]['checkin'];
6269 $room_checkout = $room_stay_dates[$kor]['checkout'];
6270 }
6271
6272 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6273 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
6274 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
6275 $isdue += $calctar;
6276 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
6277 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
6278 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6279 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
6280 $tars[$num]['calctar'] = $calctar;
6281 $isdue += $calctar;
6282 $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'] : "");
6283 }
6284
6285 if (!empty($or['optionals'])) {
6286 $stepo = explode(";", $or['optionals']);
6287 foreach ($stepo as $roptkey => $oo) {
6288 if (empty($oo)) {
6289 continue;
6290 }
6291 $stept = explode(":", $oo);
6292 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
6293 $dbo->setQuery($q);
6294 $actopt = $dbo->loadAssocList();
6295 if ($actopt) {
6296 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
6297 $chvar = '';
6298 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
6299 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
6300 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
6301 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
6302 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
6303 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
6304 $agestept = explode('-', $stept[1]);
6305 $stept[1] = $agestept[0];
6306 $chvar = $agestept[1];
6307 $realcost = 0;
6308 if (!empty($chvar)) {
6309 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
6310 // percentage value of the adults tariff
6311 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6312 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6313 } else {
6314 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6315 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6316 }
6317 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
6318 // VBO 1.10 - percentage value of room base cost
6319 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6320 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6321 } else {
6322 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
6323 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6324 }
6325 }
6326 $actopt[0]['chageintv'] = $chvar;
6327 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
6328 $actopt[0]['quan'] = $stept[1];
6329 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
6330 }
6331 } else {
6332 $actopt[0]['quan'] = $stept[1];
6333 // VBO 1.11 - options percentage cost of the room total fee
6334 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6335 $deftar_basecosts = $or['cust_cost'];
6336 } else {
6337 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6338 }
6339 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
6340 //
6341 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
6342 }
6343 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
6344 $realcost = $actopt[0]['maxprice'];
6345 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
6346 $realcost = $actopt[0]['maxprice'] * $stept[1];
6347 }
6348 }
6349 if ($actopt[0]['perperson'] == 1) {
6350 $realcost = $realcost * $or['adults'];
6351 }
6352
6353 /**
6354 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
6355 *
6356 * @since 1.17.7 (J) - 1.7.7 (WP)
6357 */
6358 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
6359 if ($custom_calculation) {
6360 $realcost = (float) $custom_calculation[0];
6361 }
6362
6363 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
6364 $isdue += $tmpopr;
6365 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
6366 }
6367 }
6368 }
6369
6370 // custom extra costs
6371 if (!empty($or['extracosts'])) {
6372 $cur_extra_costs = json_decode($or['extracosts'], true);
6373 foreach ($cur_extra_costs as $eck => $ecv) {
6374 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
6375 $isdue += $ecplustax;
6376 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
6377 }
6378 }
6379 }
6380
6381 // force the original total amount if rates have changed
6382 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
6383 $isdue = $booking['total'];
6384 }
6385
6386 // mail subject
6387 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
6388 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
6389
6390 // inject the recipient of the message for the template
6391 $booking['for'] = $for;
6392
6393 // load template file that will get $booking as variable
6394 $tmpl = self::loadEmailTemplate($booking);
6395
6396 // parse email template
6397 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
6398 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
6399
6400 if ($send !== true) {
6401 // return the content of the email message parsed
6402 return $hmess;
6403 }
6404
6405 // when the message can be sent
6406 $sendwhen = self::getSendEmailWhen();
6407
6408 if ($no_config) {
6409 // force the configuration setting to send the message no matter what's the booking status
6410 $sendwhen = 1;
6411 }
6412
6413 // send the message
6414 foreach ($for as $who) {
6415 $use_subject = $subject;
6416 $recipients = array();
6417 $attachments = self::addEmailAttachment(null);
6418 $attach_ical = false;
6419 $force_replyto = null;
6420 if (strpos($who, '@') !== false) {
6421 // send email to custom email address
6422 array_push($recipients, trim($who));
6423 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6424 // send email to the customer
6425 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6426 continue;
6427 }
6428 array_push($recipients, $booking['custmail']);
6429 /**
6430 * Check whether an iCal should be attached for the customer.
6431 *
6432 * @since 1.12.0
6433 */
6434 $attach_ical = self::getEmailIcal('customer', $booking);
6435 } elseif (stripos($who, 'admin') !== false) {
6436 // send email to the administrator(s)
6437 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6438 continue;
6439 }
6440 $use_subject = $subject . ' #' . $booking['id'];
6441 if (!strcasecmp($type, 'modified')) {
6442 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6443 }
6444 $adminemail = self::getAdminMail();
6445 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6446 if (empty($adminemail) && empty($extra_admin_recipients)) {
6447 // Prevent Joomla Exceptions that would stop the script execution
6448 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6449 continue;
6450 }
6451 if (strpos($adminemail, ',') !== false) {
6452 // multiple addresses
6453 $adminemails = explode(',', $adminemail);
6454 foreach ($adminemails as $am) {
6455 if (strpos($am, '@') !== false) {
6456 array_push($recipients, trim($am));
6457 }
6458 }
6459 } else {
6460 // single address
6461 array_push($recipients, trim($adminemail));
6462 }
6463
6464 // merge extra recipients
6465 $recipients = array_merge($recipients, $extra_admin_recipients);
6466
6467 // admin should reply to the customer
6468 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6469
6470 /**
6471 * Check whether an iCal should be attached for the admin.
6472 *
6473 * @since 1.2.0
6474 */
6475 $attach_ical = self::getEmailIcal('admin', array(
6476 'ts' => $booking['ts'],
6477 'custdata' => $booking['custdata'],
6478 'checkin' => $booking['checkin'],
6479 'checkout' => $booking['checkout'],
6480 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6481 ));
6482 }
6483
6484 // send the message, recipients should always be an array to support multiple admin addresses
6485
6486 // get sender e-mail
6487 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6488
6489 // init mail data
6490 $mail = new VBOMailWrapper([
6491 'sender' => [$adsendermail, $ftitle],
6492 'recipient' => $recipients,
6493 'bcc' => self::addAdminEmailRecipient(null, true),
6494 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6495 'subject' => $use_subject,
6496 'content' => $hmess,
6497 'attachments' => $attachments,
6498 ]);
6499
6500 if ($attach_ical !== false && $booking['status'] != 'standby') {
6501 // attach iCal file
6502 $mail->addAttachment($attach_ical);
6503 }
6504
6505 /**
6506 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6507 *
6508 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6509 *
6510 * @since 1.15.4 (J) - 1.5.10 (WP)
6511 */
6512 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6513
6514 // send e-mail
6515 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6516
6517 // unlink iCal file
6518 if ($attach_ical !== false) {
6519 @unlink($attach_ical);
6520 }
6521 }
6522
6523 return $result;
6524 }
6525
6526 /**
6527 * This method allows to add one or more recipient email
6528 * addresses for the next queue of email sending for the admin.
6529 * This method can be used in the template file for the customer
6530 * email to register an additional email address, maybe when a
6531 * specific room-type is booked.
6532 * The methods sending the email messages are supposed to call this
6533 * method by passing no arguments to obtain the extra addresses set.
6534 *
6535 * @param mixed $email null, string or array of email address(es).
6536 * @param bool $bcc if true, addresses will be used as bcc.
6537 * @param bool $reset if true, the queues will be emptied.
6538 *
6539 * @return array the current extra recipients or bcc addresses set.
6540 *
6541 * @since 1.13 (J) - 1.3.0 (WP)
6542 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6543 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6544 */
6545 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6546 {
6547 static $extra_recipients = [];
6548 static $extra_bcc = [];
6549
6550 if ($reset) {
6551 $extra_recipients = [];
6552 $extra_bcc = [];
6553 }
6554
6555 if (!empty($email)) {
6556 if (is_scalar($email)) {
6557 if ($bcc) {
6558 array_push($extra_bcc, $email);
6559 } else {
6560 array_push($extra_recipients, $email);
6561 }
6562 } else {
6563 if ($bcc) {
6564 $extra_bcc = array_merge($extra_bcc, $email);
6565 } else {
6566 $extra_recipients = array_merge($extra_recipients, $email);
6567 }
6568 }
6569 }
6570
6571 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6572 }
6573
6574 /**
6575 * This method serves to add one or more attachments to the
6576 * next queue of email sending for the admin.
6577 * The methods sending the email messages are supposed to call this
6578 * method by passing a null argument to obtain the attachments set.
6579 *
6580 * @param mixed $file null or string with path to file to attach.
6581 * @param bool $reset if true, the queue will be emptied.
6582 *
6583 * @return array the current attachments set.
6584 *
6585 * @since 1.14
6586 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6587 */
6588 public static function addEmailAttachment($file, $reset = false)
6589 {
6590 static $extra_attachments = [];
6591
6592 if ($reset) {
6593 $extra_attachments = [];
6594 }
6595
6596 if (!empty($file)) {
6597 if (is_scalar($file)) {
6598 array_push($extra_attachments, $file);
6599 } else {
6600 $extra_attachments = array_merge($extra_attachments, $file);
6601 }
6602 }
6603
6604 return array_unique($extra_attachments);
6605 }
6606
6607 /**
6608 * This method is called whenever some rooms get booked.
6609 * It checks whether the rooms involved have shared calendars.
6610 * If some are found, then also such rooms will be occupied.
6611 *
6612 * @param int $bid the booking ID.
6613 * @param array $roomids the list of rooms booked.
6614 * @param int $checkin checkin timestamp.
6615 * @param int $checkout checkout timestamp.
6616 *
6617 * @return boolean true if some other cals were occupied, false otherwise.
6618 *
6619 * @since 1.13 (J) - 1.3.0 (WP)
6620 *
6621 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6622 */
6623 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6624 {
6625 $dbo = JFactory::getDbo();
6626 $bid = (int)$bid;
6627
6628 // availability helper
6629 $av_helper = self::getAvailabilityInstance();
6630
6631 // check for split stay rooms reservation
6632 $known_split_stay = false;
6633 $is_split_stay = 0;
6634
6635 if (empty($checkin) || empty($checkout)) {
6636 // get checkin and checkout timestamps from booking
6637 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6638 $dbo->setQuery($q);
6639 $dbo->execute();
6640 if (!$dbo->getNumRows()) {
6641 // booking not found
6642 return false;
6643 }
6644 $bdata = $dbo->loadAssoc();
6645 $checkin = $bdata['checkin'];
6646 $checkout = $bdata['checkout'];
6647 $is_split_stay = $bdata['split_stay'];
6648 // turn flag on
6649 $known_split_stay = true;
6650 }
6651
6652 if (!$known_split_stay) {
6653 // we need to query the db to access this information
6654 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6655 $dbo->setQuery($q, 0, 1);
6656 $dbo->execute();
6657 if ($dbo->getNumRows()) {
6658 $is_split_stay = (int)$dbo->loadResult();
6659 }
6660 // turn flag on
6661 $known_split_stay = true;
6662 }
6663
6664 // check split stay room records
6665 $room_stay_dates = [];
6666 if ($is_split_stay) {
6667 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6668 // immediately count the number of nights of stay for each split room
6669 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6670 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6671 // overwrite values for compatibility with non-confirmed bookings
6672 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6673 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6674 }
6675 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6676 // overwrite the whole array
6677 $room_stay_dates[$sps_r_k] = $sps_r_v;
6678 }
6679 }
6680
6681 // get the rooms booked
6682 if (!count($roomids)) {
6683 // get the IDs of all rooms booked
6684 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6685 $dbo->setQuery($q);
6686 $orr = $dbo->loadAssocList();
6687 if ($orr) {
6688 foreach ($orr as $or) {
6689 array_push($roomids, $or['idroom']);
6690 }
6691 }
6692 }
6693 if (!count($roomids) || empty($bid)) {
6694 // unable to proceed
6695 return false;
6696 }
6697 $roomids = array_unique($roomids);
6698
6699 // build room stay dates map in case of split stay
6700 $room_split_stay_map = [];
6701 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6702 // we have unique room IDs in a split stay reservation for more than one room
6703 foreach ($room_stay_dates as $room_stay_date) {
6704 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6705 }
6706 }
6707
6708 // get rooms involved
6709 $involved = [];
6710 $involved_stay_map = [];
6711 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6712 $dbo->setQuery($q);
6713 $rooms_found = $dbo->loadAssocList();
6714 if (!$rooms_found) {
6715 // no rooms involved that need their calendars updated
6716 return false;
6717 }
6718 foreach ($rooms_found as $rf) {
6719 if (!in_array($rf['mainroom'], $roomids)) {
6720 // push room ID
6721 array_push($involved, $rf['mainroom']);
6722 // check for specific stay dates in connected room
6723 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6724 // split stay information available
6725 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6726 }
6727 }
6728 if (!in_array($rf['childroom'], $roomids)) {
6729 // push room ID
6730 array_push($involved, $rf['childroom']);
6731 // check for specific stay dates in connected room
6732 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6733 // split stay information available
6734 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6735 }
6736 }
6737 }
6738 $involved = array_unique($involved);
6739 if (!count($involved)) {
6740 // no rooms involved
6741 return false;
6742 }
6743
6744 // turnover seconds
6745 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6746
6747 // occupy the calendars for the involved rooms found
6748 $bids_generated = [];
6749 foreach ($involved as $rid) {
6750 // determine the values to use
6751 $room_checkin = (int)$checkin;
6752 $room_checkout = (int)$checkout;
6753 $room_realback = ($checkout + $turnover_secs);
6754 if (isset($involved_stay_map[$rid])) {
6755 // split stay dates available
6756 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6757 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6758 $room_realback = ($room_checkout + $turnover_secs);
6759 }
6760
6761 // occupy record in room with shared calendar
6762 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6763 $dbo->setQuery($q);
6764 $dbo->execute();
6765 array_push($bids_generated, $dbo->insertid());
6766 }
6767
6768 // store busy relations created
6769 foreach ($bids_generated as $busyid) {
6770 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6771 $dbo->setQuery($q);
6772 $dbo->execute();
6773 }
6774
6775 return true;
6776 }
6777
6778 /**
6779 * This method is needed whenever a booking gets modified by
6780 * adding or removing rooms. This way we reset (remove) all
6781 * busy records that were previously stored due to a shared
6782 * calendar. The correct relations should then be re-created
6783 * by calling updateSharedCalendars().
6784 *
6785 * @param int $bid the booking ID.
6786 *
6787 * @return boolean true if some records were cleaned.
6788 *
6789 * @see updateSharedCalendars() should be called after.
6790 *
6791 * @since 1.3.0
6792 */
6793 public static function cleanSharedCalendarsBusy($bid) {
6794 $dbo = JFactory::getDbo();
6795 $bid = (int)$bid;
6796 // get all the occupied records due to shared calendars for this booking
6797 $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;";
6798 $dbo->setQuery($q);
6799 $allbusy = $dbo->loadAssocList();
6800 if (!$allbusy) {
6801 return false;
6802 }
6803
6804 $busy_ids = array();
6805 foreach ($allbusy as $b) {
6806 // push busy ID to be removed later
6807 array_push($busy_ids, $b['id']);
6808 // delete the current busy ID-booking ID relation for this shared calendar
6809 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6810 $dbo->setQuery($q);
6811 $dbo->execute();
6812 }
6813 if (count($busy_ids)) {
6814 // delete all busy records due to shared calendars
6815 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6816 $dbo->setQuery($q);
6817 $dbo->execute();
6818 }
6819
6820 return true;
6821 }
6822
6823 public static function sendJutility() {
6824 //deprecated in VBO 1.10
6825 $dbo = JFactory::getDbo();
6826 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6827 $dbo->setQuery($q);
6828 $dbo->execute();
6829 $s = $dbo->loadAssocList();
6830 return (intval($s[0]['setting']) == 1 ? true : false);
6831 }
6832
6833 public static function getCategoryName($idcat, $vbo_tn = null) {
6834 $dbo = JFactory::getDbo();
6835 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6836 $dbo->setQuery($q);
6837 $dbo->execute();
6838 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6839 if (is_object($vbo_tn) && count($p) > 0) {
6840 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6841 }
6842 return count($p) > 0 ? $p[0]['name'] : '';
6843 }
6844
6845 public static function loadAdultsDiff($idroom, $adults)
6846 {
6847 static $obp_map = [];
6848
6849 $map_sign = "{$idroom}:{$adults}";
6850
6851 if (array_key_exists($map_sign, $obp_map)) {
6852 return $obp_map[$map_sign];
6853 }
6854
6855 $dbo = JFactory::getDbo();
6856
6857 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6858 $dbo->setQuery($q, 0, 1);
6859 $obp_diff = $dbo->loadAssoc();
6860 if ($obp_diff) {
6861 // cache value and return it
6862 $obp_map[$map_sign] = $obp_diff;
6863
6864 return $obp_diff;
6865 }
6866
6867 // cache value and return it
6868 $obp_map[$map_sign] = null;
6869
6870 return null;
6871 }
6872
6873 public static function loadRoomAdultsDiff($idroom)
6874 {
6875 $dbo = JFactory::getDbo();
6876
6877 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6878 $dbo->setQuery($q);
6879 $diff = $dbo->loadAssocList();
6880 if ($diff) {
6881 $roomdiff = [];
6882 foreach ($diff as $v) {
6883 $roomdiff[$v['adults']] = $v;
6884 }
6885
6886 return $roomdiff;
6887 }
6888
6889 return [];
6890 }
6891
6892 public static function occupancyOverrideExists($tar, $adults) {
6893 foreach ($tar as $k => $v) {
6894 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6895 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6896 return $v['occupancy_ovr'][$adults];
6897 }
6898 }
6899 }
6900 return false;
6901 }
6902
6903 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6904 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6905 $charges = array();
6906 if (!($children > 0) || !(count($ages) > 0)) {
6907 return $charges;
6908 }
6909 $dbo = JFactory::getDbo();
6910 $id_options = array();
6911 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6912 $dbo->setQuery($q);
6913 $dbo->execute();
6914 if ($dbo->getNumRows() > 0) {
6915 $assocs = $dbo->loadAssocList();
6916 foreach ($assocs as $opts) {
6917 if (!empty($opts['idopt'])) {
6918 $r_ido = explode(';', rtrim($opts['idopt']));
6919 foreach ($r_ido as $ido) {
6920 if (!empty($ido) && !in_array($ido, $id_options)) {
6921 $id_options[] = $ido;
6922 }
6923 }
6924 }
6925 }
6926 }
6927 if (count($id_options) > 0) {
6928 $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;";
6929 $dbo->setQuery($q);
6930 $dbo->execute();
6931 if ($dbo->getNumRows() > 0) {
6932 $ageintervals = $dbo->loadAssocList();
6933 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6934 $age_range = array();
6935 foreach ($split_ages as $kg => $spage) {
6936 if (empty($spage)) {
6937 continue;
6938 }
6939 $parts = explode('_', $spage);
6940 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6941 $ind = count($age_range);
6942 $age_range[$ind]['from'] = intval($parts[0]);
6943 $age_range[$ind]['to'] = intval($parts[1]);
6944 //taxes are calculated later in VCM
6945 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6946 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6947 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6948 }
6949 }
6950 if (count($age_range) > 0) {
6951 $tot_charge = 0;
6952 $affected = array();
6953 $option_str = '';
6954 foreach ($ages as $age) {
6955 if (strlen($age) == 0) {
6956 continue;
6957 }
6958 foreach ($age_range as $range) {
6959 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6960 $tot_charge += $range['cost'];
6961 $affected[] = $age;
6962 $option_str .= $range['option_str'].';';
6963 break;
6964 }
6965 }
6966 }
6967 if ($tot_charge > 0) {
6968 $charges['total'] = $tot_charge;
6969 $charges['affected'] = $affected;
6970 $charges['options'] = $option_str;
6971 }
6972 }
6973 }
6974 }
6975
6976 return $charges;
6977 }
6978
6979 public static function sortRoomPrices($arr) {
6980 $newarr = array();
6981 foreach ($arr as $k => $v) {
6982 $newarr[$k] = $v['cost'];
6983 }
6984 asort($newarr);
6985 $sorted = array();
6986 foreach ($newarr as $k => $v) {
6987 $sorted[$k] = $arr[$k];
6988 }
6989 return $sorted;
6990 }
6991
6992 public static function sortResults($arr) {
6993 $newarr = array();
6994 foreach ($arr as $k => $v) {
6995 $newarr[$k] = $v[0]['cost'];
6996 }
6997 asort($newarr);
6998 $sorted = array();
6999 foreach ($newarr as $k => $v) {
7000 $sorted[$k] = $arr[$k];
7001 }
7002 return $sorted;
7003 }
7004
7005 public static function sortMultipleResults($arr) {
7006 foreach ($arr as $k => $v) {
7007 $newarr = array();
7008 foreach ($v as $subk => $subv) {
7009 $newarr[$subk] = $subv[0]['cost'];
7010 }
7011 asort($newarr);
7012 $sorted = array();
7013 foreach ($newarr as $nk => $v) {
7014 $sorted[$nk] = $arr[$k][$nk];
7015 }
7016 $arr[$k] = $sorted;
7017 }
7018 return $arr;
7019 }
7020
7021 /**
7022 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
7023 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
7024 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
7025 *
7026 * @param array $promos optional list of promotion IDs to register in the execution flow.
7027 *
7028 * @return array list of cached promotion IDs (if any).
7029 *
7030 * @since 1.16.4 (J) - 1.6.4 (WP)
7031 */
7032 public static function registerPromotionIds(array $promos = [])
7033 {
7034 static $flagged_promos = [];
7035
7036 if ($promos) {
7037 // set cached value
7038 $flagged_promos = $promos;
7039 }
7040
7041 return $flagged_promos;
7042 }
7043
7044 /**
7045 * Getter and setter for seasons cache allowed.
7046 *
7047 * @param bool $enabled true or false for setter, null for getter.
7048 *
7049 * @return bool|null
7050 *
7051 * @since 1.16.5 (J) - 1.6.5 (WP)
7052 */
7053 public static function setSeasonsCache($enabled = null)
7054 {
7055 static $seasons_cache = null;
7056
7057 if ($enabled !== null) {
7058 // set value
7059 $seasons_cache = (bool)$enabled;
7060 }
7061
7062 return $seasons_cache;
7063 }
7064
7065 /**
7066 * Allows to preload and cache seasonal records for a list of rooms and dates.
7067 * Any subsequent preloading of season records for the same room(s) will return
7068 * the cached list. Should be used to preload a large window of data so that
7069 * subsequent calls will always return cached value.
7070 *
7071 * @param array $rooms List of involved room IDs.
7072 * @param int|bool $from Unix timestamp for start date or false to unset cache.
7073 * @param int $to Unix timestamp for end date.
7074 *
7075 * @return ?array
7076 *
7077 * @since 1.17.2 (J) - 1.7.2 (WP)
7078 */
7079 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
7080 {
7081 static $preloaded_records = [];
7082
7083 if (!$rooms) {
7084 // no cached signature
7085 return [];
7086 }
7087
7088 $signature = md5(implode(',', $rooms));
7089
7090 if ($from === false) {
7091 // unset cached records
7092 unset($preloaded_records[$signature]);
7093
7094 return;
7095 }
7096
7097 if ($from !== null && $to !== null) {
7098 // setter args signature for preloading records
7099 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
7100
7101 return;
7102 }
7103
7104 // getter args signature for getting the preloaded and cached records
7105 return $preloaded_records[$signature] ?? [];
7106 }
7107
7108 /**
7109 * Fetches all season records affecting a range of date timestamps.
7110 * Useful to pre-cache season records in case of hundreds of thousands
7111 * of records, but it can use up several MBs of server's memory.
7112 *
7113 * @param int $from unix timestamp for start date.
7114 * @param int $to unix timestamp for end date.
7115 * @param array $rooms optional list of involved room IDs.
7116 *
7117 * @return array
7118 *
7119 * @since 1.16.10 (J) - 1.6.10 (WP)
7120 */
7121 public static function getDateSeasonRecords($from, $to, array $rooms = [])
7122 {
7123 /**
7124 * We allow external systems to preload season records and cache them.
7125 *
7126 * @since 1.17.2 (J) - 1.7.2 (WP)
7127 */
7128 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
7129 return $preloaded_records;
7130 }
7131
7132 /**
7133 * For a more accurate records caching, ensure we have a range of two dates at least.
7134 *
7135 * @since 1.17.2 (J) - 1.7.2 (WP)
7136 */
7137 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
7138 // add one day to the end timestamp
7139 $to = strtotime('+1 day', $to);
7140 }
7141
7142 $dbo = JFactory::getDbo();
7143
7144 $one = getdate($from);
7145 $two = getdate($to);
7146
7147 // leap years
7148 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7149 $isleap = true;
7150 } else {
7151 $isleap = false;
7152 }
7153
7154 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7155 $tomidnightone = intval($one['hours']) * 3600;
7156 $tomidnightone += intval($one['minutes']) * 60;
7157 $sfrom = $from - $baseone - $tomidnightone;
7158 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7159 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7160 $tomidnighttwo = intval($two['hours']) * 3600;
7161 $tomidnighttwo += intval($two['minutes']) * 60;
7162 $sto = $to - $basetwo - $tomidnighttwo;
7163
7164 // leap years, check what dates to manipulate
7165 if ($isleap) {
7166 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7167 if ($one[0] > $leapts) {
7168 /**
7169 * Timestamp must be greater than the leap-day of Feb 29th.
7170 * It used to be checked for >= $leapts.
7171 *
7172 * @since July 3rd 2019
7173 */
7174 $sfrom -= 86400;
7175 }
7176 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7177 // lower checkin date when in leap year but not for checkout
7178 $sto -= 86400;
7179 }
7180 }
7181
7182 // check for DST changes to adjust the query values to fetch
7183 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7184 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7185 // we are parsing the day when the DST changed (probably a Sunday)
7186 if (!date('I', $from)) {
7187 // DST was just turned off
7188 $sfrom -= 3600;
7189 }
7190 }
7191 }
7192
7193 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7194 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7195 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7196 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7197 ($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`)") .
7198 ($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`)") .
7199 ($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`)") .
7200 ($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`)") .
7201 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7202 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7203
7204 /**
7205 * Avoid issues when querying data for a whole year by fetching all records.
7206 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
7207 *
7208 * @since 1.17.3 (J) - 1.7.3 (WP)
7209 */
7210 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
7211 // fetch all records when targeting a whole year of data
7212 $q = $dbo->getQuery(true)
7213 ->select('*')
7214 ->from($dbo->qn('#__vikbooking_seasons'))
7215 ->order($dbo->qn('promo') . ' ASC');
7216 }
7217
7218 $dbo->setQuery($q);
7219 $seasons = $dbo->loadAssocList();
7220
7221 if ($rooms) {
7222 // filter records by affected room IDs
7223 $seasons = array_filter($seasons, function($s) use ($rooms) {
7224 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
7225 foreach ($rooms as $idroom) {
7226 if (in_array("-" . $idroom . "-", $allrooms)) {
7227 return true;
7228 }
7229 }
7230 return false;
7231 });
7232
7233 // reset array keys
7234 $seasons = array_values($seasons);
7235 }
7236
7237 return $seasons;
7238 }
7239
7240 /**
7241 * Fetches all season records affecting week days with no dates.
7242 * Useful to avoid duplicate queries and cache/preload the records.
7243 *
7244 * @return array
7245 *
7246 * @since 1.18.5 (J) - 1.8.5 (WP)
7247 */
7248 public static function getWdaySeasonRecords()
7249 {
7250 static $cached_wdayseasons = null;
7251
7252 if (!is_null($cached_wdayseasons)) {
7253 return $cached_wdayseasons;
7254 }
7255
7256 $dbo = JFactory::getDbo();
7257
7258 $dbo->setQuery("SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL)) ORDER BY `promo` ASC;");
7259 $cached_wdayseasons = $dbo->loadAssocList();
7260
7261 return $cached_wdayseasons;
7262 }
7263
7264 /**
7265 * Applies the seasonal rates over a list of room rate records.
7266 *
7267 * @param array $arr list of room rate records.
7268 * @param int $from unix timestamp for start date.
7269 * @param int $to unix timestamp for end date.
7270 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7271 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7272 *
7273 * @return array list of manipulated room rate records.
7274 *
7275 * @since 1.18.0 (J) - 1.8.0 (WP) added support to records caching and preloading with args $seasons_dates and $seasons_wdays.
7276 * @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.
7277 */
7278 public static function applySeasonalPrices(array $arr, $from, $to, array $seasons_dates = [], ?array $seasons_wdays = null)
7279 {
7280 static $cached_seasons = [];
7281
7282 $cache_signature_wdays = '';
7283 if (self::setSeasonsCache() !== false && !$seasons_wdays) {
7284 // enable week-day season records caching
7285 $cache_signature_wdays = "wdays";
7286 }
7287
7288 $dbo = JFactory::getDbo();
7289 $vbo_tn = self::getTranslator();
7290
7291 $roomschange = [];
7292 $one = getdate($from);
7293
7294 // leap years
7295 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7296 $isleap = true;
7297 } else {
7298 $isleap = false;
7299 }
7300
7301 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7302 $tomidnightone = intval($one['hours']) * 3600;
7303 $tomidnightone += intval($one['minutes']) * 60;
7304 $sfrom = $from - $baseone - $tomidnightone;
7305 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7306 $two = getdate($to);
7307 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7308 $tomidnighttwo = intval($two['hours']) * 3600;
7309 $tomidnighttwo += intval($two['minutes']) * 60;
7310 $sto = $to - $basetwo - $tomidnighttwo;
7311
7312 // leap years, check what dates to manipulate
7313 if ($isleap) {
7314 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7315 if ($one[0] > $leapts) {
7316 /**
7317 * Timestamp must be greater than the leap-day of Feb 29th.
7318 * It used to be checked for >= $leapts.
7319 *
7320 * @since July 3rd 2019
7321 */
7322 $sfrom -= 86400;
7323 }
7324 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7325 // lower checkin date when in leap year but not for checkout
7326 $sto -= 86400;
7327 }
7328 }
7329
7330 // check for DST changes to adjust the query values to fetch
7331 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7332 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7333 // we are parsing the day when the DST changed (probably a Sunday)
7334 if (!date('I', $from)) {
7335 // DST was just turned off
7336 $sfrom -= 3600;
7337 }
7338 }
7339 }
7340
7341 // count nights requested
7342 $booking_nights = 1;
7343 foreach ($arr as $k => $a) {
7344 if (!isset($a[0])) {
7345 continue;
7346 }
7347 if (isset($a[0]['booking_nights'])) {
7348 // this value may be set when displaying pricing calendars
7349 $booking_nights = $a[0]['booking_nights'];
7350 break;
7351 }
7352 if (isset($a[0]['days'])) {
7353 $booking_nights = $a[0]['days'];
7354 break;
7355 }
7356 }
7357
7358 /**
7359 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7360 *
7361 * @since 1.16.4 (J) - 1.6.4 (WP)
7362 */
7363 $skip_promo_ids = self::registerPromotionIds();
7364
7365 $totseasons = 0;
7366 if (!$seasons_dates) {
7367 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7368 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7369 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7370 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7371 ($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`)") .
7372 ($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`)") .
7373 ($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`)") .
7374 ($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`)") .
7375 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7376 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7377
7378 // get the season records by running the query
7379 $dbo->setQuery($q);
7380 $seasons = $dbo->loadAssocList();
7381
7382 // count total seasons
7383 $totseasons = $seasons ? count($seasons) : 0;
7384 }
7385
7386 if ($totseasons > 0 || $seasons_dates) {
7387 if ($totseasons > 0) {
7388 $seasons = $seasons;
7389 } else {
7390 $seasons = $seasons_dates;
7391 }
7392 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7393 $applyseasons = false;
7394 $mem = [];
7395 foreach ($arr as $k => $a) {
7396 $mem[$k]['daysused'] = 0;
7397 $mem[$k]['sum'] = [];
7398 $mem[$k]['spids'] = [];
7399 /**
7400 * The keys below are all needed to apply the promotions on the room's final cost.
7401 *
7402 * @since 1.13.5
7403 */
7404 $mem[$k]['diffs'] = [];
7405 $mem[$k]['trans_keys'] = [];
7406 $mem[$k]['trans_factors'] = [];
7407 $mem[$k]['trans_affdays'] = [];
7408 }
7409 foreach ($seasons as $s) {
7410 // check if this is a promotion registered for skipping
7411 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7412 continue;
7413 }
7414
7415 // Special Price tied to the year
7416 if (!empty($s['year']) && $s['year'] > 0) {
7417 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7418 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7419 //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
7420 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7421 continue;
7422 }
7423 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7424 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7425 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7426 if ($calc_ends < ($from - $tomidnightone)) {
7427 continue;
7428 }
7429 }
7430 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7431 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7432 if ($s['from'] > $s['to']) {
7433 continue;
7434 }
7435 if (($basetwo + $s['from'] + 86399) > $to) {
7436 /**
7437 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7438 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7439 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7440 *
7441 * @since 1.14.3 (J) - 1.4.3 (WP)
7442 */
7443 continue;
7444 }
7445 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7446 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7447 /**
7448 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7449 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7450 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7451 *
7452 * @since 1.14.3 (J) - 1.4.3 (WP)
7453 */
7454 continue;
7455 }
7456 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7457 // 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)
7458 if (($baseone + $s['from']) > $to) {
7459 continue;
7460 }
7461 }
7462 }
7463 //
7464 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7465 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7466 $inits = $baseone + $s['from'];
7467 if ($s['from'] < $s['to']) {
7468 $ends = $basetwo + $s['to'];
7469 // check if the inits must be set to the year after
7470 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7471 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7472 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7473 $inits = $tmpbase + $s['from'];
7474 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7475 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7476 $ends = $baseone + $s['to'];
7477 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7478 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7479 $ends = $baseone + $s['to'];
7480 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7481 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7482 $inits = $basetwo + $s['from'];
7483 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7484 // booking dates across two years for a season tied to a specific year
7485 if ($one['year'] == $s['year']) {
7486 $inits = $baseone + $s['from'];
7487 } else {
7488 $inits = $basetwo + $s['from'];
7489 }
7490 if ($two['year'] == $s['year']) {
7491 $ends = $basetwo + $s['to'];
7492 } else {
7493 $ends = $baseone + $s['to'];
7494 }
7495 }
7496 } else {
7497 // between 2 years
7498 if ($baseone < $basetwo) {
7499 // ex. 29/12/2012 - 14/01/2013
7500 $ends = $basetwo + $s['to'];
7501 } else {
7502 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7503 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7504 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7505 $ends = $tmpbase + $s['to'];
7506 } else {
7507 // ex. 03/01 - 09/01
7508 $ends = $basetwo + $s['to'];
7509 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7510 $inits = $tmpbase + $s['from'];
7511 }
7512 }
7513 }
7514 // leap years
7515 if ($isleap == true) {
7516 $infoseason = getdate($inits);
7517 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7518 // leap years, check what dates to manipulate
7519 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7520 /**
7521 * Timestamp must be greater than the leap-day of Feb 29th.
7522 * It used to be checked for >= $leapts.
7523 *
7524 * @since July 3rd 2019
7525 */
7526 $inits += 86400;
7527 if ($s['from'] < $s['to']) {
7528 // increase season end date only if still on the same leap year
7529 $ends += 86400;
7530 }
7531 }
7532 }
7533
7534 // promotions
7535 $promotion = [];
7536 if (($s['promo'] ?? 0) == 1) {
7537 $daysadv = (($inits - time()) / 86400);
7538 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7539 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7540 continue;
7541 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7542 $secstocheckin = ($from - time());
7543 if ($s['promolastmin'] < $secstocheckin) {
7544 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7545 continue;
7546 }
7547 }
7548 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7549 /**
7550 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7551 *
7552 * @since 1.13.5
7553 */
7554 continue;
7555 }
7556 $promotion['todaydaysadv'] = $daysadv;
7557 $promotion['promodaysadv'] = $s['promodaysadv'];
7558 $promotion['promotxt'] = $s['promotxt'];
7559 }
7560
7561 // occupancy override
7562 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7563
7564 // week days
7565 $filterwdays = !empty($s['wdays']) ? true : false;
7566 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7567 if (is_array($wdays) && $wdays) {
7568 foreach ($wdays as $kw => $wd) {
7569 if (strlen($wd) == 0) {
7570 unset($wdays[$kw]);
7571 }
7572 }
7573 }
7574
7575 // checkin must be after the beginning of the season
7576 $checkininclok = true;
7577 if ($s['checkinincl'] == 1) {
7578 $checkininclok = false;
7579 if ($s['from'] < $s['to']) {
7580 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7581 $checkininclok = true;
7582 }
7583 } else {
7584 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7585 $checkininclok = true;
7586 }
7587 }
7588 }
7589 if ($checkininclok !== true) {
7590 continue;
7591 }
7592
7593 foreach ($arr as $k => $a) {
7594 // applied only to some types of price
7595 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7596 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7597 continue;
7598 }
7599 }
7600 // applied only to some room types
7601 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7602 continue;
7603 }
7604
7605 // count affected nights of stay
7606 $affdays = 0;
7607 $season_fromdayts = $fromdayts;
7608 $is_dst = date('I', $season_fromdayts);
7609 for ($i = 0; $i < $a[0]['days']; $i++) {
7610 $todayts = $season_fromdayts + ($i * 86400);
7611 $is_now_dst = date('I', $todayts);
7612 if ($is_dst != $is_now_dst) {
7613 // Daylight Saving Time has changed, check how
7614 if ((bool)$is_dst === true) {
7615 $todayts += 3600;
7616 $season_fromdayts += 3600;
7617 } else {
7618 $todayts -= 3600;
7619 $season_fromdayts -= 3600;
7620 }
7621 $is_dst = $is_now_dst;
7622 }
7623 if ($todayts >= $inits && $todayts <= $ends) {
7624 // week days
7625 if ($filterwdays == true) {
7626 $checkwday = getdate($todayts);
7627 if (in_array($checkwday['wday'], $wdays)) {
7628 $affdays++;
7629 }
7630 } else {
7631 $affdays++;
7632 }
7633 //
7634 }
7635 }
7636
7637 if (!($affdays > 0)) {
7638 // no nights affected
7639 continue;
7640 }
7641
7642 // apply the rule
7643 $applyseasons = true;
7644 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7645 $arr[$k][0]['calcdailycost'] = $dailyprice;
7646
7647 // modification factor object
7648 $factor = new stdClass;
7649
7650 // calculate new price progressively
7651 if (intval($s['val_pcent']) == 2) {
7652 // percentage value
7653 $factor->pcent = 1;
7654 $pctval = $s['diffcost'];
7655 if (strlen((string)$s['losoverride'])) {
7656 // values overrides
7657 $arrvaloverrides = [];
7658 $valovrparts = explode('_', $s['losoverride']);
7659 foreach ($valovrparts as $valovr) {
7660 if (!empty($valovr)) {
7661 $ovrinfo = explode(':', $valovr);
7662 if (strstr($ovrinfo[0], '-i') != false) {
7663 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7664 if ((int)$ovrinfo[0] < $a[0]['days']) {
7665 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7666 }
7667 }
7668 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7669 }
7670 }
7671 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7672 $pctval = $arrvaloverrides[$a[0]['days']];
7673 }
7674 }
7675 if (intval($s['type']) == 1) {
7676 // charge
7677 $factor->type = '+';
7678 $cpercent = 100 + $pctval;
7679 } else {
7680 // discount
7681 $factor->type = '-';
7682 $cpercent = 100 - $pctval;
7683 }
7684 $factor->amount = $pctval;
7685 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7686 } else {
7687 // absolute value
7688 $factor->pcent = 0;
7689 $absval = $s['diffcost'];
7690 if (strlen((string)$s['losoverride'])) {
7691 // values overrides
7692 $arrvaloverrides = [];
7693 $valovrparts = explode('_', $s['losoverride']);
7694 foreach ($valovrparts as $valovr) {
7695 if (!empty($valovr)) {
7696 $ovrinfo = explode(':', $valovr);
7697 if (strstr($ovrinfo[0], '-i') != false) {
7698 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7699 if ((int)$ovrinfo[0] < $a[0]['days']) {
7700 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7701 }
7702 }
7703 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7704 }
7705 }
7706 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7707 $absval = $arrvaloverrides[$a[0]['days']];
7708 }
7709 }
7710 if (intval($s['type']) == 1) {
7711 // charge
7712 $factor->type = '+';
7713 $newprice = ($dailyprice + $absval) * $affdays;
7714 } else {
7715 // discount
7716 $factor->type = '-';
7717 $newprice = ($dailyprice - $absval) * $affdays;
7718 }
7719 $factor->amount = $absval;
7720 }
7721
7722 // apply rounding
7723 $factor->roundmode = $s['roundmode'] ?? null;
7724 if (!empty($s['roundmode'])) {
7725 $newprice = round($newprice, 0, constant($s['roundmode']));
7726 } else {
7727 $newprice = round($newprice, 2);
7728 }
7729
7730 // define the promotion (only if no value overrides set the amount to 0)
7731 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7732 /**
7733 * Include the discount information (if any). The cost re-calculated may not be
7734 * precise if multiple special prices were applied over the same dates.
7735 *
7736 * @since 1.13
7737 */
7738 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7739 $promotion['discount'] = [
7740 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7741 'pcent' => (int)($s['val_pcent'] == 2),
7742 ];
7743 }
7744 //
7745 $mem[$k]['promotion'] = $promotion;
7746 }
7747
7748 // define the occupancy override
7749 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7750 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7751 }
7752
7753 // push special price ID
7754 if (!in_array($s['id'], $mem[$k]['spids'])) {
7755 array_push($mem[$k]['spids'], $s['id']);
7756 }
7757
7758 // push difference generated only if to be applied progressively
7759 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7760 /**
7761 * Push the difference generated by this special price for later transliteration of final price,
7762 * only if the special price is calculated progressively and not on the final price.
7763 *
7764 * @since 1.13.5
7765 */
7766 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7767 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7768 /**
7769 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7770 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7771 * all differences applied by the previous special pricing rules.
7772 *
7773 * @since 1.13.5
7774 */
7775 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7776 array_push($mem[$k]['trans_factors'], $factor);
7777 array_push($mem[$k]['trans_affdays'], $affdays);
7778 }
7779
7780 // push values in memory array
7781 array_push($mem[$k]['sum'], $newprice);
7782 $mem[$k]['daysused'] += $affdays;
7783 array_push($roomschange, $a[0]['idroom']);
7784 }
7785 }
7786 if ($applyseasons) {
7787 foreach ($mem as $k => $v) {
7788 if ($v['daysused'] > 0 && $v['sum']) {
7789 $newprice = 0;
7790 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7791 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7792 $addrest = $restdays * $dailyprice;
7793 $newprice += $addrest;
7794
7795 // calculate new final cost
7796 $redo_rounding = null;
7797 foreach ($v['sum'] as $sum_index => $add) {
7798 /**
7799 * The application of the various special pricing rules is made in a progressive and cumulative way
7800 * by always starting from the room base cost or its average daily cost. However, promotions may need
7801 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7802 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7803 * onto the differences generated by all the regular and progressively applied special pricing rules.
7804 *
7805 * @since 1.13.5
7806 */
7807 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7808 /**
7809 * Attempt to calculate the effective room rate before the first promo was applied.
7810 *
7811 * @since 1.18.15 (J) - 1.8.15 (WP)
7812 */
7813 if (isset($v['promotion']) && !isset($arr[$k][0]['before_promo'])) {
7814 $arr[$k][0]['before_promo'] = $newprice + ($arr[$k][0]['calcdailycost'] ?? 0);
7815 }
7816
7817 /**
7818 * This progressive price difference must be applied on the room final cost, so we need to
7819 * apply the transliteration over the other differences applied by other special prices.
7820 */
7821 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7822 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7823 // this is the % promotion we are looking for applying it on the final cost
7824 $factor = $v['trans_factors'][$transliterate_key];
7825 if (is_object($factor) && $factor->pcent) {
7826 $final_factor = 0;
7827 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7828 $final_factor += $prog_diff * $factor->amount / 100;
7829 }
7830 // update rounding
7831 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7832
7833 /**
7834 * Leverage the final factor depending on how many nights this affected.
7835 *
7836 * @since 1.15.0 (J) - 1.5.0 (WP)
7837 */
7838 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7839 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7840 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7841 }
7842
7843 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7844 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7845 }
7846 }
7847 }
7848
7849 // apply new price progressively
7850 $newprice += $add;
7851 }
7852
7853 // apply rounding from factor
7854 if (!empty($redo_rounding)) {
7855 $newprice = round($newprice, 0, constant($redo_rounding));
7856 }
7857
7858 // set promotion (if any)
7859 if (isset($v['promotion'])) {
7860 $arr[$k][0]['promotion'] = $v['promotion'];
7861 }
7862
7863 // set occupancy overrides (if any)
7864 if (isset($v['occupancy_ovr'])) {
7865 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7866 }
7867
7868 // set new final cost and update nights affected
7869 $arr[$k][0]['cost'] = $newprice;
7870 $arr[$k][0]['affdays'] = $v['daysused'];
7871 if (array_key_exists('spids', $v) && $v['spids']) {
7872 $arr[$k][0]['spids'] = $v['spids'];
7873 }
7874 }
7875 }
7876 }
7877 }
7878
7879 // week days with no season
7880 $roomschange = array_unique($roomschange);
7881 $totspecials = 0;
7882 if (!is_array($seasons_wdays)) {
7883 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7884 // avoid making a query
7885 $specials = $cached_seasons[$cache_signature_wdays];
7886 } else {
7887 // get the week-days-with-no-season records by running the query
7888 $specials = self::getWdaySeasonRecords();
7889 }
7890
7891 // count records
7892 $totspecials = $specials ? count($specials) : 0;
7893
7894 if ($cache_signature_wdays) {
7895 // cache week-days-with-no-season records
7896 $cached_seasons[$cache_signature_wdays] = $specials;
7897 }
7898 }
7899 if ($totspecials > 0 || $seasons_wdays) {
7900 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7901 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7902 $applyseasons = false;
7903 /**
7904 * We no longer unset the previous memory of the seasons with dates filters
7905 * because we need the responses to be merged. We do it only if not set.
7906 * We only keep the property 'spids' but the others should be unset.
7907 *
7908 * @since 1.11
7909 */
7910 if (!isset($mem)) {
7911 $mem = [];
7912 foreach ($arr as $k => $a) {
7913 $mem[$k]['daysused'] = 0;
7914 $mem[$k]['sum'] = [];
7915 $mem[$k]['spids'] = [];
7916 }
7917 } else {
7918 foreach ($arr as $k => $a) {
7919 $mem[$k]['daysused'] = 0;
7920 $mem[$k]['sum'] = [];
7921 }
7922 }
7923 //
7924 foreach ($specials as $s) {
7925 // check if this is a promotion registered for skipping
7926 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7927 continue;
7928 }
7929
7930 // Special Price tied to the year
7931 if (!empty($s['year']) && $s['year'] > 0) {
7932 if ($one['year'] != $s['year']) {
7933 continue;
7934 }
7935 }
7936
7937 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7938 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7939 // week days
7940 $filterwdays = !empty($s['wdays']) ? true : false;
7941 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7942 if (is_array($wdays) && $wdays) {
7943 foreach ($wdays as $kw => $wd) {
7944 if (strlen($wd) == 0) {
7945 unset($wdays[$kw]);
7946 }
7947 }
7948 }
7949
7950 foreach ($arr as $k => $a) {
7951 // only rooms with no price modifications from seasons
7952
7953 // applied only to some types of price
7954 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7955 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7956 continue;
7957 }
7958 }
7959
7960 /**
7961 * We should not exclude the rooms that already had a modification of the price through a season
7962 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7963 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7964 *
7965 * @since 1.11
7966 */
7967 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7968 continue;
7969 }
7970
7971 $affdays = 0;
7972 $season_fromdayts = $fromdayts;
7973 $is_dst = date('I', $season_fromdayts);
7974 for ($i = 0; $i < $a[0]['days']; $i++) {
7975 $todayts = $season_fromdayts + ($i * 86400);
7976 $is_now_dst = date('I', $todayts);
7977 if ($is_dst != $is_now_dst) {
7978 // Daylight Saving Time has changed, check how
7979 if ((bool)$is_dst === true) {
7980 $todayts += 3600;
7981 $season_fromdayts += 3600;
7982 } else {
7983 $todayts -= 3600;
7984 $season_fromdayts -= 3600;
7985 }
7986 $is_dst = $is_now_dst;
7987 }
7988 // week days
7989 if ($filterwdays == true) {
7990 $checkwday = getdate($todayts);
7991 if (in_array($checkwday['wday'], $wdays)) {
7992 $affdays++;
7993 }
7994 }
7995 }
7996 if (!($affdays > 0)) {
7997 // no nights affected
7998 continue;
7999 }
8000
8001 // apply the rule
8002 $applyseasons = true;
8003 $dailyprice = $a[0]['cost'] / $a[0]['days'];
8004 $arr[$k][0]['calcdailycost'] = $dailyprice;
8005
8006 if (intval($s['val_pcent']) == 2) {
8007 // percentage value
8008 $pctval = $s['diffcost'];
8009 if (strlen((string)$s['losoverride'])) {
8010 // values overrides
8011 $arrvaloverrides = [];
8012 $valovrparts = explode('_', $s['losoverride']);
8013 foreach ($valovrparts as $valovr) {
8014 if (!empty($valovr)) {
8015 $ovrinfo = explode(':', $valovr);
8016 if (strstr($ovrinfo[0], '-i') != false) {
8017 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8018 if ((int)$ovrinfo[0] < $a[0]['days']) {
8019 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
8020 }
8021 }
8022 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8023 }
8024 }
8025 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8026 $pctval = $arrvaloverrides[$a[0]['days']];
8027 }
8028 }
8029 if (intval($s['type']) == 1) {
8030 // charge
8031 $cpercent = 100 + $pctval;
8032 } else {
8033 // discount
8034 $cpercent = 100 - $pctval;
8035 }
8036 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
8037 } else {
8038 // absolute value
8039 $absval = $s['diffcost'];
8040 if (strlen((string)$s['losoverride'])) {
8041 // values overrides
8042 $arrvaloverrides = [];
8043 $valovrparts = explode('_', $s['losoverride']);
8044 foreach ($valovrparts as $valovr) {
8045 if (!empty($valovr)) {
8046 $ovrinfo = explode(':', $valovr);
8047 if (strstr($ovrinfo[0], '-i') != false) {
8048 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8049 if ((int)$ovrinfo[0] < $a[0]['days']) {
8050 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
8051 }
8052 }
8053 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8054 }
8055 }
8056 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8057 $absval = $arrvaloverrides[$a[0]['days']];
8058 }
8059 }
8060 if (intval($s['type']) == 1) {
8061 // charge
8062 $newprice = ($dailyprice + $absval) * $affdays;
8063 } else {
8064 // discount
8065 $newprice = ($dailyprice - $absval) * $affdays;
8066 }
8067 }
8068
8069 // apply rounding
8070 if (!empty($s['roundmode'])) {
8071 $newprice = round($newprice, 0, constant($s['roundmode']));
8072 } else {
8073 $newprice = round($newprice, 2);
8074 }
8075
8076 // push values in memory array
8077 if (!in_array($s['id'], $mem[$k]['spids'])) {
8078 $mem[$k]['spids'][] = $s['id'];
8079 }
8080 $mem[$k]['sum'][] = $newprice;
8081 $mem[$k]['daysused'] += $affdays;
8082 }
8083 }
8084 if ($applyseasons) {
8085 foreach ($mem as $k => $v) {
8086 if ($v['daysused'] > 0 && $v['sum']) {
8087 $newprice = 0;
8088 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
8089 $restdays = $arr[$k][0]['days'] - $v['daysused'];
8090 $addrest = $restdays * $dailyprice;
8091 $newprice += $addrest;
8092 foreach ($v['sum'] as $add) {
8093 $newprice += $add;
8094 }
8095 $arr[$k][0]['cost'] = $newprice;
8096 $arr[$k][0]['affdays'] = $v['daysused'];
8097 if (array_key_exists('spids', $v) && $v['spids']) {
8098 $arr[$k][0]['spids'] = $v['spids'];
8099 }
8100 }
8101 }
8102 }
8103 }
8104 // end week days with no season
8105
8106 return $arr;
8107 }
8108
8109 /**
8110 * Applies the seasonal rates over room rate records.
8111 *
8112 * @param array $arr array of tariffs taken from the DB
8113 * @param int $from start timestamp
8114 * @param int $to end timestamp
8115 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
8116 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
8117 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
8118 *
8119 * @return array
8120 *
8121 * @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.
8122 */
8123 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], ?array $seasons_wdays = null)
8124 {
8125 static $cached_seasons = [];
8126
8127 $cache_signature = '';
8128 $cache_signature_wdays = '';
8129 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
8130 // enable season records caching
8131 $cache_signature = "{$from}_{$to}";
8132 }
8133 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
8134 // enable week-day season records caching
8135 $cache_signature_wdays = "wdays";
8136 }
8137
8138 $dbo = JFactory::getDbo();
8139 $vbo_tn = self::getTranslator();
8140
8141 $roomschange = [];
8142 $one = getdate($from);
8143
8144 // leap years
8145 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
8146 $isleap = true;
8147 } else {
8148 $isleap = false;
8149 }
8150
8151 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
8152 $tomidnightone = intval($one['hours']) * 3600;
8153 $tomidnightone += intval($one['minutes']) * 60;
8154 $sfrom = $from - $baseone - $tomidnightone;
8155 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
8156 $two = getdate($to);
8157 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
8158 $tomidnighttwo = intval($two['hours']) * 3600;
8159 $tomidnighttwo += intval($two['minutes']) * 60;
8160 $sto = $to - $basetwo - $tomidnighttwo;
8161
8162 // leap years, check what dates to manipulate
8163 if ($isleap) {
8164 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
8165 if ($one[0] > $leapts) {
8166 /**
8167 * Timestamp must be greater than the leap-day of Feb 29th.
8168 * It used to be checked for >= $leapts.
8169 *
8170 * @since July 3rd 2019
8171 */
8172 $sfrom -= 86400;
8173 }
8174 if ($two[0] > $leapts && $one['year'] == $two['year']) {
8175 // lower checkin date when in leap year but not for checkout
8176 $sto -= 86400;
8177 }
8178 }
8179
8180 // check for DST changes to adjust the query values to fetch
8181 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
8182 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
8183 // we are parsing the day when the DST changed (probably a Sunday)
8184 if (!date('I', $from)) {
8185 // DST was just turned off
8186 $sfrom -= 3600;
8187 }
8188 }
8189 }
8190
8191 // count nights requested
8192 $booking_nights = 1;
8193 foreach ($arr as $k => $a) {
8194 if (isset($a['booking_nights'])) {
8195 // this value may be set when displaying pricing calendars
8196 $booking_nights = $a['booking_nights'];
8197 break;
8198 }
8199 if (isset($a['days'])) {
8200 $booking_nights = $a['days'];
8201 break;
8202 }
8203 }
8204
8205 /**
8206 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
8207 *
8208 * @since 1.16.4 (J) - 1.6.4 (WP)
8209 */
8210 $skip_promo_ids = self::registerPromotionIds();
8211
8212 $totseasons = 0;
8213 if (!$parsed_season && !$seasons_dates) {
8214 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
8215 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
8216 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
8217 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
8218 ($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`)") .
8219 ($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`)") .
8220 ($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`)") .
8221 ($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`)") .
8222 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
8223 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
8224
8225 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
8226 // avoid making a query
8227 $seasons = $cached_seasons[$cache_signature];
8228 } else {
8229 // get the season records by running the query
8230 $dbo->setQuery($q);
8231 $seasons = $dbo->loadAssocList();
8232 }
8233
8234 // count total seasons
8235 $totseasons = $seasons ? count($seasons) : 0;
8236
8237 if ($cache_signature) {
8238 // cache season records
8239 $cached_seasons[$cache_signature] = $seasons;
8240 }
8241 }
8242
8243 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
8244 if ($totseasons > 0) {
8245 $seasons = $seasons;
8246 } elseif ($parsed_season) {
8247 $seasons = [$parsed_season];
8248 } else {
8249 $seasons = $seasons_dates;
8250 }
8251 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
8252 $applyseasons = false;
8253 $mem = [];
8254 foreach ($arr as $k => $a) {
8255 $mem[$k]['daysused'] = 0;
8256 $mem[$k]['sum'] = [];
8257 $mem[$k]['spids'] = [];
8258 /**
8259 * The keys below are all needed to apply the promotions on the room's final cost.
8260 *
8261 * @since 1.13.5
8262 */
8263 $mem[$k]['diffs'] = [];
8264 $mem[$k]['trans_keys'] = [];
8265 $mem[$k]['trans_factors'] = [];
8266 $mem[$k]['trans_affdays'] = [];
8267 }
8268 $affdayslistless = [];
8269 foreach ($seasons as $s) {
8270 // 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
8271 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
8272 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
8273 continue;
8274 }
8275
8276 /**
8277 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
8278 * We need to skip such fake seasons as they do not need any parsing.
8279 *
8280 * @since 1.13
8281 */
8282 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
8283 continue;
8284 }
8285
8286 // check if this is a promotion registered for skipping
8287 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8288 continue;
8289 }
8290
8291 // Special Price tied to the year
8292 if (!empty($s['year']) && $s['year'] > 0) {
8293 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
8294 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
8295 //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
8296 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
8297 continue;
8298 }
8299 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
8300 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
8301 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
8302 if ($calc_ends < ($from - $tomidnightone)) {
8303 continue;
8304 }
8305 }
8306 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
8307 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
8308 if ($s['from'] > $s['to']) {
8309 continue;
8310 }
8311 if (($basetwo + $s['from'] + 86399) > $to) {
8312 /**
8313 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
8314 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
8315 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
8316 *
8317 * @since 1.14.3 (J) - 1.4.3 (WP)
8318 */
8319 continue;
8320 }
8321 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
8322 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
8323 /**
8324 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
8325 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
8326 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
8327 *
8328 * @since 1.14.3 (J) - 1.4.3 (WP)
8329 */
8330 continue;
8331 }
8332 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
8333 // 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)
8334 if (($baseone + $s['from']) > $to) {
8335 continue;
8336 }
8337 }
8338 }
8339 //
8340 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8341 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8342 $inits = $baseone + $s['from'];
8343 if ($s['from'] < $s['to']) {
8344 $ends = $basetwo + $s['to'];
8345 // check if the inits must be set to the year after
8346 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
8347 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
8348 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8349 $inits = $tmpbase + $s['from'];
8350 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8351 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
8352 $ends = $baseone + $s['to'];
8353 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8354 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
8355 $ends = $baseone + $s['to'];
8356 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
8357 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
8358 $inits = $basetwo + $s['from'];
8359 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
8360 // booking dates across two years for a season tied to a specific year
8361 if ($one['year'] == $s['year']) {
8362 $inits = $baseone + $s['from'];
8363 } else {
8364 $inits = $basetwo + $s['from'];
8365 }
8366 if ($two['year'] == $s['year']) {
8367 $ends = $basetwo + $s['to'];
8368 } else {
8369 $ends = $baseone + $s['to'];
8370 }
8371 }
8372 } else {
8373 // between 2 years
8374 if ($baseone < $basetwo) {
8375 // ex. 29/12/2012 - 14/01/2013
8376 $ends = $basetwo + $s['to'];
8377 } else {
8378 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
8379 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
8380 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8381 $ends = $tmpbase + $s['to'];
8382 } else {
8383 // ex. 03/01 - 09/01
8384 $ends = $basetwo + $s['to'];
8385 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
8386 $inits = $tmpbase + $s['from'];
8387 }
8388 }
8389 }
8390 // leap years
8391 if ($isleap == true) {
8392 $infoseason = getdate($inits);
8393 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
8394 // leap years, check what dates to manipulate
8395 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
8396 /**
8397 * Timestamp must be greater than the leap-day of Feb 29th.
8398 * It used to be checked for >= $leapts.
8399 *
8400 * @since July 3rd 2019
8401 */
8402 $inits += 86400;
8403 if ($s['from'] < $s['to']) {
8404 // increase season end date only if still on the same leap year
8405 $ends += 86400;
8406 }
8407 }
8408 }
8409
8410 // promotions
8411 $promotion = [];
8412 if (($s['promo'] ?? 0) == 1) {
8413 $daysadv = (($inits - time()) / 86400);
8414 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
8415 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
8416 continue;
8417 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
8418 $secstocheckin = ($from - time());
8419 if ($s['promolastmin'] < $secstocheckin) {
8420 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
8421 continue;
8422 }
8423 }
8424 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
8425 /**
8426 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
8427 *
8428 * @since 1.13.5
8429 */
8430 continue;
8431 }
8432 $promotion['todaydaysadv'] = $daysadv;
8433 $promotion['promodaysadv'] = $s['promodaysadv'];
8434 $promotion['promotxt'] = $s['promotxt'];
8435 }
8436
8437 // occupancy override
8438 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
8439
8440 // week days
8441 $filterwdays = !empty($s['wdays']) ? true : false;
8442 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8443 if (is_array($wdays) && $wdays) {
8444 foreach ($wdays as $kw => $wd) {
8445 if (strlen($wd) == 0) {
8446 unset($wdays[$kw]);
8447 }
8448 }
8449 }
8450
8451 // checkin must be after the beginning of the season
8452 $checkininclok = true;
8453 if ($s['checkinincl'] == 1) {
8454 $checkininclok = false;
8455 if ($s['from'] < $s['to']) {
8456 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
8457 $checkininclok = true;
8458 }
8459 } else {
8460 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
8461 $checkininclok = true;
8462 }
8463 }
8464 }
8465 if ($checkininclok !== true) {
8466 continue;
8467 }
8468
8469 foreach ($arr as $k => $a) {
8470 // applied only to some types of price
8471 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8472 // VikBooking 1.6: Price Calendar sets the idprice to -1
8473 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8474 continue;
8475 }
8476 }
8477 // applied only to some room types
8478 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8479 continue;
8480 }
8481
8482 if (isset($a['days']) && is_float($a['days'])) {
8483 // prevent any possible warning with array_key_exists()
8484 $a['days'] = (int)$a['days'];
8485 }
8486
8487 $affdays = 0;
8488 $season_fromdayts = $fromdayts;
8489 $is_dst = date('I', $season_fromdayts);
8490 for ($i = 0; $i < $a['days']; $i++) {
8491 $todayts = $season_fromdayts + ($i * 86400);
8492 $is_now_dst = date('I', $todayts);
8493 if ($is_dst != $is_now_dst) {
8494 // Daylight Saving Time has changed, check how
8495 if ((bool)$is_dst === true) {
8496 $todayts += 3600;
8497 $season_fromdayts += 3600;
8498 } else {
8499 $todayts -= 3600;
8500 $season_fromdayts -= 3600;
8501 }
8502 $is_dst = $is_now_dst;
8503 }
8504 if ($todayts >= $inits && $todayts <= $ends) {
8505 $checkwday = getdate($todayts);
8506 // week days
8507 if ($filterwdays == true) {
8508 if (in_array($checkwday['wday'], $wdays)) {
8509 if (!isset($arr[$k]['affdayslist'])) {
8510 $arr[$k]['affdayslist'] = [];
8511 }
8512 $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;
8513 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8514 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8515 $affdays++;
8516 }
8517 } else {
8518 if (!isset($arr[$k]['affdayslist'])) {
8519 $arr[$k]['affdayslist'] = [];
8520 }
8521 $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;
8522 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8523 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8524 $affdays++;
8525 }
8526 //
8527 }
8528 }
8529
8530 if (!($affdays > 0)) {
8531 // no nights affected
8532 continue;
8533 }
8534
8535 // apply the rule
8536 $applyseasons = true;
8537 $dailyprice = $a['cost'] / $a['days'];
8538
8539 // modification factor object
8540 $factor = new stdClass;
8541
8542 // calculate new price progressively
8543 if (intval($s['val_pcent']) == 2) {
8544 // percentage value
8545 $factor->pcent = 1;
8546 $pctval = $s['diffcost'];
8547 if (strlen((string)$s['losoverride'])) {
8548 // values overrides
8549 $arrvaloverrides = [];
8550 $valovrparts = explode('_', $s['losoverride']);
8551 foreach ($valovrparts as $valovr) {
8552 if (!empty($valovr)) {
8553 $ovrinfo = explode(':', $valovr);
8554 if (strstr($ovrinfo[0], '-i') != false) {
8555 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8556 if ((int)$ovrinfo[0] < $a['days']) {
8557 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8558 }
8559 }
8560 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8561 }
8562 }
8563 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8564 $pctval = $arrvaloverrides[$a['days']];
8565 }
8566 }
8567 if (intval($s['type']) == 1) {
8568 // charge
8569 $factor->type = '+';
8570 $cpercent = 100 + $pctval;
8571 } else {
8572 // discount
8573 $factor->type = '-';
8574 $cpercent = 100 - $pctval;
8575 }
8576 $factor->amount = $pctval;
8577 $dailysum = ($dailyprice * $cpercent / 100);
8578 $newprice = $dailysum * $affdays;
8579 } else {
8580 // absolute value
8581 $factor->pcent = 0;
8582 $absval = $s['diffcost'];
8583 if (strlen((string)$s['losoverride'])) {
8584 // values overrides
8585 $arrvaloverrides = [];
8586 $valovrparts = explode('_', $s['losoverride']);
8587 foreach ($valovrparts as $valovr) {
8588 if (!empty($valovr)) {
8589 $ovrinfo = explode(':', $valovr);
8590 if (strstr($ovrinfo[0], '-i') != false) {
8591 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8592 if ((int)$ovrinfo[0] < $a['days']) {
8593 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8594 }
8595 }
8596 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8597 }
8598 }
8599 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8600 $absval = $arrvaloverrides[$a['days']];
8601 }
8602 }
8603 if (intval($s['type']) == 1) {
8604 // charge
8605 $factor->type = '+';
8606 $dailysum = ($dailyprice + $absval);
8607 $newprice = $dailysum * $affdays;
8608 } else {
8609 // discount
8610 $factor->type = '-';
8611 $dailysum = ($dailyprice - $absval);
8612 $newprice = $dailysum * $affdays;
8613 }
8614 $factor->amount = $absval;
8615 }
8616
8617 // apply rounding
8618 $factor->roundmode = $s['roundmode'] ?? null;
8619 if (!empty($s['roundmode'])) {
8620 $newprice = round($newprice, 0, constant($s['roundmode']));
8621 } else {
8622 $newprice = round($newprice, 2);
8623 }
8624
8625 // define the promotion (only if no value overrides set the amount to 0)
8626 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8627 /**
8628 * Include the discount information (if any). The cost re-calculated may not be
8629 * precise if multiple special prices were applied over the same dates.
8630 *
8631 * @since 1.13
8632 */
8633 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8634 $promotion['discount'] = [
8635 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8636 'pcent' => (int)($s['val_pcent'] == 2),
8637 ];
8638 }
8639 //
8640 $mem[$k]['promotion'] = $promotion;
8641 }
8642
8643 // define the occupancy override
8644 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8645 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8646 }
8647
8648 // affected days list
8649 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8650 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8651 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8652 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8653 }
8654 }
8655 }
8656
8657 // push special price ID
8658 if (!in_array($s['id'], $mem[$k]['spids'])) {
8659 array_push($mem[$k]['spids'], $s['id']);
8660 }
8661
8662 // push difference generated only if to be applied progressively
8663 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8664 /**
8665 * Push the difference generated by this special price for later transliteration of final price,
8666 * only if the special price is calculated progressively and not on the final price.
8667 *
8668 * @since 1.13.5
8669 */
8670 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8671 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8672 /**
8673 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8674 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8675 * all differences applied by the previous special pricing rules.
8676 *
8677 * @since 1.13.5
8678 */
8679 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8680 array_push($mem[$k]['trans_factors'], $factor);
8681 array_push($mem[$k]['trans_affdays'], $affdays);
8682 }
8683
8684 // push values in memory array
8685 array_push($mem[$k]['sum'], $newprice);
8686 $mem[$k]['daysused'] += $affdays;
8687 array_push($roomschange, $a['idroom']);
8688 }
8689 }
8690 if ($applyseasons) {
8691 foreach ($mem as $k => $v) {
8692 if ($v['daysused'] > 0 && $v['sum']) {
8693 $newprice = 0;
8694 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8695 $restdays = $arr[$k]['days'] - $v['daysused'];
8696 $addrest = $restdays * $dailyprice;
8697 $newprice += $addrest;
8698
8699 // calculate new final cost
8700 $redo_rounding = null;
8701 foreach ($v['sum'] as $sum_index => $add) {
8702 /**
8703 * The application of the various special pricing rules is made in a progressive and cumulative way
8704 * by always starting from the room base cost or its average daily cost. However, promotions may need
8705 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8706 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8707 * onto the differences generated by all the regular and progressively applied special pricing rules.
8708 *
8709 * @since 1.13.5
8710 */
8711 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8712 /**
8713 * Attempt to calculate the effective room rate before the first promo was applied.
8714 *
8715 * @since 1.18.15 (J) - 1.8.15 (WP)
8716 */
8717 if (isset($v['promotion']) && !isset($arr[$k]['before_promo'])) {
8718 $arr[$k]['before_promo'] = $newprice + ($arr[$k]['origdailycost'] ?? 0);
8719 }
8720
8721 /**
8722 * This progressive price difference must be applied on the room final cost, so we need to
8723 * apply the transliteration over the other differences applied by other special prices.
8724 */
8725 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8726 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8727 // this is the % promotion we are looking for applying it on the final cost
8728 $factor = $v['trans_factors'][$transliterate_key];
8729 if (is_object($factor) && $factor->pcent) {
8730 $final_factor = 0;
8731 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8732 $final_factor += $prog_diff * $factor->amount / 100;
8733 }
8734 // update rounding
8735 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8736
8737 /**
8738 * Leverage the final factor depending on how many nights this affected.
8739 *
8740 * @since 1.15.0 (J) - 1.5.0 (WP)
8741 */
8742 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8743 $avg_final_factor = $final_factor / $arr[$k]['days'];
8744 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8745 }
8746
8747 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8748 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8749 }
8750 }
8751 }
8752
8753 // apply new price progressively
8754 $newprice += $add;
8755 }
8756
8757 // apply rounding from factor
8758 if (!empty($redo_rounding)) {
8759 $newprice = round($newprice, 0, constant($redo_rounding));
8760 }
8761
8762 // set promotion (if any)
8763 if (isset($v['promotion'])) {
8764 $arr[$k]['promotion'] = $v['promotion'];
8765 }
8766
8767 // set occupancy overrides (if any)
8768 if (isset($v['occupancy_ovr'])) {
8769 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8770 }
8771
8772 // set new final cost and update nights affected
8773 $arr[$k]['cost'] = $newprice;
8774 $arr[$k]['affdays'] = $v['daysused'];
8775 if (array_key_exists('spids', $v) && $v['spids']) {
8776 $arr[$k]['spids'] = $v['spids'];
8777 }
8778 }
8779 }
8780 }
8781 }
8782
8783 // week days with no season
8784 $roomschange = array_unique($roomschange);
8785 $totspecials = 0;
8786 if (!is_array($seasons_wdays)) {
8787 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8788 // avoid making a query
8789 $specials = $cached_seasons[$cache_signature_wdays];
8790 } else {
8791 // get the week-days-with-no-season records by running the query
8792 $specials = self::getWdaySeasonRecords();
8793 }
8794
8795 // count records
8796 $totspecials = $specials ? count($specials) : 0;
8797
8798 if ($cache_signature_wdays) {
8799 // cache week-days-with-no-season records
8800 $cached_seasons[$cache_signature_wdays] = $specials;
8801 }
8802 }
8803 if ($totspecials > 0 || $seasons_wdays) {
8804 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8805 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8806 $applyseasons = false;
8807 /**
8808 * We no longer unset the previous memory of the seasons with dates filters
8809 * because we need the responses to be merged. We do it only if not set.
8810 * We only keep the property 'spids' but the others should be unset.
8811 *
8812 * @since 1.11
8813 */
8814 if (!isset($mem)) {
8815 $mem = [];
8816 foreach ($arr as $k => $a) {
8817 $mem[$k]['daysused'] = 0;
8818 $mem[$k]['sum'] = [];
8819 $mem[$k]['spids'] = [];
8820 }
8821 } else {
8822 foreach ($arr as $k => $a) {
8823 $mem[$k]['daysused'] = 0;
8824 $mem[$k]['sum'] = [];
8825 }
8826 }
8827 //
8828 foreach ($specials as $s) {
8829 // 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
8830 if (!empty($s['from']) || !empty($s['to'])) {
8831 continue;
8832 }
8833
8834 // check if this is a promotion registered for skipping
8835 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8836 continue;
8837 }
8838
8839 // Special Price tied to the year
8840 if (!empty($s['year']) && $s['year'] > 0) {
8841 if ($one['year'] != $s['year']) {
8842 continue;
8843 }
8844 }
8845
8846 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8847 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8848 // week days
8849 $filterwdays = !empty($s['wdays']) ? true : false;
8850 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8851 if (is_array($wdays) && $wdays) {
8852 foreach ($wdays as $kw => $wd) {
8853 if (strlen($wd) == 0) {
8854 unset($wdays[$kw]);
8855 }
8856 }
8857 }
8858
8859 foreach ($arr as $k => $a) {
8860 // only rooms with no price modifications from seasons
8861
8862 // applied only to some types of price
8863 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8864 // VikBooking 1.6: Price Calendar sets the idprice to -1
8865 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8866 continue;
8867 }
8868 }
8869
8870 /**
8871 * We should not exclude the rooms that already had a modification of the price through a season
8872 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8873 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8874 *
8875 * @since 1.11
8876 */
8877 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8878 continue;
8879 }
8880
8881 if (isset($a['days']) && is_float($a['days'])) {
8882 // prevent any possible warning with array_key_exists()
8883 $a['days'] = (int)$a['days'];
8884 }
8885
8886 $affdays = 0;
8887 $season_fromdayts = $fromdayts;
8888 $is_dst = date('I', $season_fromdayts);
8889 for ($i = 0; $i < $a['days']; $i++) {
8890 $todayts = $season_fromdayts + ($i * 86400);
8891 $is_now_dst = date('I', $todayts);
8892 if ($is_dst != $is_now_dst) {
8893 // Daylight Saving Time has changed, check how
8894 if ((bool)$is_dst === true) {
8895 $todayts += 3600;
8896 $season_fromdayts += 3600;
8897 } else {
8898 $todayts -= 3600;
8899 $season_fromdayts -= 3600;
8900 }
8901 $is_dst = $is_now_dst;
8902 }
8903 // week days
8904 if ($filterwdays == true) {
8905 $checkwday = getdate($todayts);
8906 if (in_array($checkwday['wday'], $wdays)) {
8907 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8908 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8909 $affdays++;
8910 }
8911 }
8912 //
8913 }
8914
8915 if (!($affdays > 0)) {
8916 // no nights affected
8917 continue;
8918 }
8919
8920 // apply the rule
8921 $applyseasons = true;
8922 $dailyprice = $a['cost'] / $a['days'];
8923
8924 if (intval($s['val_pcent']) == 2) {
8925 // percentage value
8926 $pctval = $s['diffcost'];
8927 if (strlen((string)$s['losoverride'])) {
8928 // values overrides
8929 $arrvaloverrides = [];
8930 $valovrparts = explode('_', $s['losoverride']);
8931 foreach ($valovrparts as $valovr) {
8932 if (!empty($valovr)) {
8933 $ovrinfo = explode(':', $valovr);
8934 if (strstr($ovrinfo[0], '-i') != false) {
8935 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8936 if ((int)$ovrinfo[0] < $a['days']) {
8937 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8938 }
8939 }
8940 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8941 }
8942 }
8943 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8944 $pctval = $arrvaloverrides[$a['days']];
8945 }
8946 }
8947 if (intval($s['type']) == 1) {
8948 // charge
8949 $cpercent = 100 + $pctval;
8950 } else {
8951 // discount
8952 $cpercent = 100 - $pctval;
8953 }
8954 $dailysum = ($dailyprice * $cpercent / 100);
8955 $newprice = $dailysum * $affdays;
8956 } else {
8957 // absolute value
8958 $absval = $s['diffcost'];
8959 if (strlen((string)$s['losoverride'])) {
8960 // values overrides
8961 $arrvaloverrides = [];
8962 $valovrparts = explode('_', $s['losoverride']);
8963 foreach ($valovrparts as $valovr) {
8964 if (!empty($valovr)) {
8965 $ovrinfo = explode(':', $valovr);
8966 if (strstr($ovrinfo[0], '-i') != false) {
8967 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8968 if ((int)$ovrinfo[0] < $a['days']) {
8969 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8970 }
8971 }
8972 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8973 }
8974 }
8975 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8976 $absval = $arrvaloverrides[$a['days']];
8977 }
8978 }
8979 if (intval($s['type']) == 1) {
8980 // charge
8981 $dailysum = ($dailyprice + $absval);
8982 $newprice = $dailysum * $affdays;
8983 } else {
8984 // discount
8985 $dailysum = ($dailyprice - $absval);
8986 $newprice = $dailysum * $affdays;
8987 }
8988 }
8989
8990 // apply rounding
8991 if (!empty($s['roundmode'])) {
8992 $newprice = round($newprice, 0, constant($s['roundmode']));
8993 } else {
8994 $newprice = round($newprice, 2);
8995 }
8996
8997 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8998 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8999 }
9000 if (!in_array($s['id'], $mem[$k]['spids'])) {
9001 $mem[$k]['spids'][] = $s['id'];
9002 }
9003 $mem[$k]['sum'][] = $newprice;
9004 $mem[$k]['daysused'] += $affdays;
9005 }
9006 }
9007 if ($applyseasons) {
9008 foreach ($mem as $k => $v) {
9009 if ($v['daysused'] > 0 && $v['sum']) {
9010 $newprice = 0;
9011 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
9012 $restdays = $arr[$k]['days'] - $v['daysused'];
9013 $addrest = $restdays * $dailyprice;
9014 $newprice += $addrest;
9015 foreach ($v['sum'] as $add) {
9016 $newprice += $add;
9017 }
9018 $arr[$k]['cost'] = $newprice;
9019 $arr[$k]['affdays'] = $v['daysused'];
9020 if (array_key_exists('spids', $v) && $v['spids']) {
9021 $arr[$k]['spids'] = $v['spids'];
9022 }
9023 }
9024 }
9025 }
9026 }
9027 // end week days with no season
9028
9029 return $arr;
9030 }
9031
9032 public static function getRoomRplansClosingDates($idroom)
9033 {
9034 $dbo = JFactory::getDbo();
9035
9036 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9037 $dbo->setQuery($q);
9038 $price_records = $dbo->loadAssocList();
9039 if (!$price_records) {
9040 return [];
9041 }
9042
9043 $closingd = [];
9044 foreach ($price_records as $prec) {
9045 if (empty($prec['closingd'])) {
9046 continue;
9047 }
9048 $price_closing = json_decode($prec['closingd'], true);
9049 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9050 continue;
9051 }
9052 // check expired dates and clean up
9053 $today_midnight = mktime(0, 0, 0);
9054 $cleaned = false;
9055 foreach ($price_closing[$idroom] as $k => $v) {
9056 if (strtotime($v) < $today_midnight) {
9057 $cleaned = true;
9058 unset($price_closing[$idroom][$k]);
9059 }
9060 }
9061 if (!$price_closing[$idroom]) {
9062 unset($price_closing[$idroom]);
9063 } elseif ($cleaned === true) {
9064 // reset array keys for smaller JSON size
9065 $price_closing[$idroom] = array_values($price_closing[$idroom]);
9066 }
9067 if ($cleaned === true) {
9068 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
9069 $dbo->setQuery($q);
9070 $dbo->execute();
9071 }
9072 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9073 continue;
9074 }
9075 $closingd[$prec['id']] = $price_closing[$idroom];
9076 }
9077
9078 return $closingd;
9079 }
9080
9081 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
9082 {
9083 $dbo = JFactory::getDbo();
9084
9085 $closingd = [];
9086
9087 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9088 $dbo->setQuery($q);
9089 $price_records = $dbo->loadAssocList();
9090 if ($price_records && $roomids) {
9091 $info_start = getdate($checkints);
9092 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
9093 $all_nights = [];
9094 for ($i = 0; $i < (int)$numnights; $i++) {
9095 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
9096 $all_nights[] = date('Y-m-d', $next_midnight);
9097 }
9098 foreach ($price_records as $prec) {
9099 if (empty($prec['closingd'])) {
9100 continue;
9101 }
9102 $price_closing = json_decode($prec['closingd'], true);
9103 if (!$price_closing) {
9104 continue;
9105 }
9106 foreach ($price_closing as $idroom => $rclosedd) {
9107 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
9108 continue;
9109 }
9110 if (!array_key_exists($idroom, $closingd)) {
9111 $closingd[$idroom] = [];
9112 }
9113 foreach ($all_nights as $night) {
9114 if (in_array($night, $rclosedd)) {
9115 if (array_key_exists($prec['id'], $closingd[$idroom])) {
9116 $closingd[$idroom][$prec['id']][] = $night;
9117 } else {
9118 $closingd[$idroom][$prec['id']] = [$night];
9119 }
9120 }
9121 }
9122 }
9123 }
9124 }
9125
9126 return $closingd;
9127 }
9128
9129 /**
9130 * Tells whether at least one payment method must be selected to check-out.
9131 *
9132 * @param array $rooms_involved optional list of room IDs being booked.
9133 *
9134 * @return bool true if at least one payment method is applicable.
9135 *
9136 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
9137 */
9138 public static function areTherePayments($rooms_involved = [])
9139 {
9140 $dbo = JFactory::getDbo();
9141
9142 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
9143 $dbo->setQuery($q);
9144 $dbo->execute();
9145
9146 if (!$dbo->getNumRows()) {
9147 // no published payment options found
9148 return false;
9149 }
9150
9151 $pay_options = $dbo->loadAssocList();
9152
9153 if (!count($rooms_involved)) {
9154 // if no filters given, return true
9155 return true;
9156 }
9157
9158 // validate room associations
9159 foreach ($pay_options as $pay_k => $pay_v) {
9160 if (empty($pay_v['idrooms'])) {
9161 continue;
9162 }
9163 $rooms_supported = json_decode($pay_v['idrooms']);
9164 if (!is_array($rooms_supported) || !count($rooms_supported)) {
9165 continue;
9166 }
9167 // make sure all rooms being booked are supported
9168 foreach ($rooms_involved as $room_id_booked) {
9169 if (!in_array($room_id_booked, $rooms_supported)) {
9170 // we need to unset this payment option as soon as we find one non-matching room ID
9171 unset($pay_options[$pay_k]);
9172 break;
9173 }
9174 }
9175 }
9176
9177 return (bool)count($pay_options);
9178 }
9179
9180 public static function getPayment($idp, $vbo_tn = null)
9181 {
9182 if (empty($idp)) {
9183 return false;
9184 }
9185
9186 if (strstr((string)$idp, '=') !== false) {
9187 $parts = explode('=', $idp);
9188 $idp = $parts[0];
9189 }
9190
9191 $dbo = JFactory::getDbo();
9192
9193 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
9194 $dbo->setQuery($q);
9195 $payment = $dbo->loadAssocList();
9196 if (!$payment) {
9197 return false;
9198 }
9199
9200 if (is_object($vbo_tn)) {
9201 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
9202 }
9203
9204 return $payment[0];
9205 }
9206
9207 public static function getCronKey()
9208 {
9209 return VBOFactory::getConfig()->get('cronkey', '');
9210 }
9211
9212 /**
9213 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
9214 *
9215 * @return int
9216 *
9217 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
9218 */
9219 public static function getNextInvoiceNumber()
9220 {
9221 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
9222 }
9223
9224 public static function getInvoiceNumberSuffix()
9225 {
9226 return VBOFactory::getConfig()->get('invoicesuffix', '');
9227 }
9228
9229 public static function getInvoiceCompanyInfo()
9230 {
9231 return VBOFactory::getConfig()->get('invcompanyinfo', '');
9232 }
9233
9234 /**
9235 * Gets the number for the next booking receipt generation,
9236 * updates the last receipt number used for the later iterations,
9237 * stores a new receipt record to keep track of the receipts issued.
9238 *
9239 * @param int $bid the Booking ID for which we are/want to generating the receipt.
9240 * @param [int] $newnum the last number used for generating the receipt.
9241 *
9242 * @return int
9243 */
9244 public static function getNextReceiptNumber ($bid, $newnum = false) {
9245 $dbo = JFactory::getDbo();
9246 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
9247 $dbo->setQuery($q);
9248 $dbo->execute();
9249 if ($dbo->getNumRows() > 0) {
9250 $s = $dbo->loadResult();
9251 //check if this booking has already a receipt, and return that number
9252 $prev_receipt = array();
9253 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
9254 $dbo->setQuery($q);
9255 $dbo->execute();
9256 if ($dbo->getNumRows() > 0) {
9257 $prev_receipt = $dbo->loadAssoc();
9258 }
9259 //update value (receipt generated)
9260 if ($newnum !== false && $newnum > 0) {
9261 $s = (int)$newnum;
9262 if (!(count($prev_receipt) > 0)) {
9263 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
9264 $dbo->setQuery($q);
9265 $dbo->execute();
9266 //insert the new receipt record
9267 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
9268 $dbo->setQuery($q);
9269 $dbo->execute();
9270 } else {
9271 //update receipt record
9272 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
9273 $dbo->setQuery($q);
9274 $dbo->execute();
9275 }
9276 }
9277 //
9278 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
9279 }
9280 //first execution of the method should create the configuration record
9281 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
9282 $dbo->setQuery($q);
9283 $dbo->execute();
9284 return 1;
9285 }
9286
9287 public static function getReceiptNotes ($newnotes = false) {
9288 $dbo = JFactory::getDbo();
9289 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
9290 $dbo->setQuery($q);
9291 $dbo->execute();
9292 if ($dbo->getNumRows() > 0) {
9293 $s = $dbo->loadResult();
9294 //update value
9295 if ($newnotes !== false) {
9296 $s = $newnotes;
9297 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
9298 $dbo->setQuery($q);
9299 $dbo->execute();
9300 }
9301 //
9302 return $s;
9303 }
9304 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
9305 $dbo->setQuery($q);
9306 $dbo->execute();
9307 return "";
9308 }
9309
9310 public static function loadColorTagsRules()
9311 {
9312 $color_tag_rules = [
9313 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
9314 'pend1' => 'VBWAITINGFORPAYMENT',
9315 'conf1' => 'VBDBTEXTROOMCLOSED',
9316 'ovbook' => 'VBO_BTYPE_OVERBOOKING',
9317 'conf2' => 'VBOCOLORTAGRULECONFTWO',
9318 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
9319 'inv1' => 'VBOCOLORTAGRULEINVONE',
9320 'rcp1' => 'VBOCOLORTAGRULERCPONE',
9321 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
9322 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
9323 'inv2' => 'VBOCOLORTAGRULEINVTWO',
9324 ];
9325
9326 /**
9327 * Trigger event to let third-party plugins override the default booking color tag rules.
9328 *
9329 * @since 1.16.4 (J) - 1.6.4 (WP)
9330 */
9331 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
9332
9333 return $color_tag_rules;
9334 }
9335
9336 public static function loadDefaultColorTags()
9337 {
9338 $default_color_tags = [
9339 [
9340 'color' => '#9b9b9b',
9341 'name' => 'VBWAITINGFORPAYMENT',
9342 'rule' => 'pend1',
9343 ],
9344 [
9345 'color' => '#333333',
9346 'name' => 'VBDBTEXTROOMCLOSED',
9347 'rule' => 'conf1',
9348 ],
9349 [
9350 'color' => '#f04848',
9351 'name' => 'VBO_BTYPE_OVERBOOKING',
9352 'rule' => 'ovbook',
9353 ],
9354 [
9355 'color' => '#ff8606',
9356 'name' => 'VBOCOLORTAGRULECONFTWO',
9357 'rule' => 'conf2',
9358 ],
9359 [
9360 'color' => '#0418c9',
9361 'name' => 'VBOCOLORTAGRULECONFTHREE',
9362 'rule' => 'conf3',
9363 ],
9364 [
9365 'color' => '#bed953',
9366 'name' => 'VBOCOLORTAGRULEINVONE',
9367 'rule' => 'inv1',
9368 ],
9369 [
9370 'color' => '#67f5b5',
9371 'name' => 'VBOCOLORTAGRULERCPONE',
9372 'rule' => 'rcp1',
9373 ],
9374 [
9375 'color' => '#04d2c2',
9376 'name' => 'VBOCOLORTAGRULECONFFOUR',
9377 'rule' => 'conf4',
9378 ],
9379 [
9380 'color' => '#00b316',
9381 'name' => 'VBOCOLORTAGRULECONFFIVE',
9382 'rule' => 'conf5',
9383 ],
9384 [
9385 'color' => '#00f323',
9386 'name' => 'VBOCOLORTAGRULEINVTWO',
9387 'rule' => 'inv2',
9388 ],
9389 ];
9390
9391 /**
9392 * Trigger event to let third-party plugins override the default color tags.
9393 *
9394 * @since 1.16.4 (J) - 1.6.4 (WP)
9395 */
9396 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
9397
9398 return $default_color_tags;
9399 }
9400
9401 public static function loadBookingsColorTags()
9402 {
9403 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
9404
9405 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
9406 }
9407
9408 public static function getBestColorContrast($hexcolor)
9409 {
9410 $hexcolor = str_replace('#', '', $hexcolor);
9411
9412 if (empty($hexcolor) || strlen($hexcolor) != 6) {
9413 return '#000000';
9414 }
9415
9416 $r = hexdec(substr($hexcolor, 0, 2));
9417 $g = hexdec(substr($hexcolor, 2, 2));
9418 $b = hexdec(substr($hexcolor, 4, 2));
9419
9420 // Counting the perceptive luminance - human eye favors green color
9421 // < 0.5 bright colors
9422 // > 0.5 dark colors
9423
9424 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
9425 }
9426
9427 public static function applyBookingColorTag($booking, $tags = [])
9428 {
9429 if (!is_array($tags) || !$tags) {
9430 $tags = self::loadBookingsColorTags();
9431 }
9432
9433 if (!empty($booking['colortag'])) {
9434 $color_tag_arr = json_decode($booking['colortag'], true);
9435 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
9436 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
9437 return $color_tag_arr;
9438 }
9439 }
9440
9441 $dbo = JFactory::getDbo();
9442
9443 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
9444 $docs_data = [];
9445 $invoice_numb = false;
9446 $receipt_numb = false;
9447
9448 if ($booking['status'] == 'confirmed' && ($booking['checkin'] ?? 0) > time()) {
9449 $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`
9450 FROM `#__vikbooking_orders` AS `b`
9451 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
9452 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
9453 WHERE `b`.`id`=" . (int)$bid . ";";
9454 $dbo->setQuery($q);
9455 $docs_data = $dbo->loadAssoc();
9456 if ($docs_data) {
9457 $invoice_numb = (!empty($docs_data['inv_id']));
9458 $receipt_numb = (!empty($docs_data['rcp_id']));
9459
9460 /**
9461 * We inject the invoice number or receipt number into the color tag rules.
9462 * This is useful for the Tableaux page to show the invoice/receipt number.
9463 *
9464 * @since 1.12 (J) - 1.1.7 (WP)
9465 */
9466 if ($invoice_numb || $receipt_numb) {
9467 foreach ($tags as &$tval) {
9468 if ($invoice_numb) {
9469 $tval['invoice_number'] = $docs_data['inv_number'];
9470 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
9471 }
9472 if ($receipt_numb) {
9473 $tval['receipt_number'] = $docs_data['rcp_number'];
9474 }
9475 }
9476 // unset last reference
9477 unset($tval);
9478 }
9479 }
9480 }
9481
9482 /**
9483 * Some OTA bookings using an OTA-Collect business model may look as not entirely
9484 * paid when they actually are, due to commissions and pass through taxes. We also
9485 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
9486 *
9487 * @since 1.16.3 (J) - 1.6.3 (WP)
9488 */
9489 $ota_collected_booking = false;
9490 $hotel_collect_vcc_unpaid = false;
9491 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
9492 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
9493 $ota_collected_booking = true;
9494 }
9495 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
9496 // VCC detected, check the history for a payment event
9497 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
9498 // list of history types to fetch
9499 $h_payment_types = [];
9500 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
9501 if ($h_payment_group && $h_payment_group['types']) {
9502 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
9503 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
9504 }
9505 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
9506 if (!$payment_ev_datas) {
9507 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
9508 $hotel_collect_vcc_unpaid = true;
9509 }
9510 }
9511 }
9512
9513 foreach ($tags as $tkey => $tval) {
9514 if (empty($tval['rule'])) {
9515 continue;
9516 }
9517
9518 /**
9519 * Trigger event to let third-party plugins validate the custom rule.
9520 *
9521 * @since 1.16.4 (J) - 1.6.4 (WP)
9522 */
9523 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
9524 if (!empty($custom_rule)) {
9525 return (array) $custom_rule[0];
9526 }
9527
9528 switch ($tval['rule']) {
9529 case 'pend1':
9530 // Room is waiting for the payment (locked record)
9531 if ($booking['status'] == 'standby') {
9532 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
9533 $dbo->setQuery($q);
9534 $tmp_lock = $dbo->loadAssoc();
9535 if ($tmp_lock) {
9536 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9537 return $tval;
9538 }
9539 }
9540 break;
9541 case 'conf1':
9542 // Confirmed (Room Closed)
9543 if ($booking['status'] == 'confirmed' && (!empty($booking['closure']) || $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED'))) {
9544 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9545 return $tval;
9546 }
9547 break;
9548 case 'ovbook':
9549 // Confirmed (Overbooking)
9550 if ($booking['status'] == 'confirmed' && !empty($booking['type']) && $booking['type'] == 'overbooking') {
9551 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9552 return $tval;
9553 }
9554 break;
9555 case 'conf2':
9556 // Confirmed (No Rate 0.00/NULL Total)
9557 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9558 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9559 return $tval;
9560 }
9561 break;
9562 case 'conf3':
9563 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9564 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) {
9565 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9566 return $tval;
9567 }
9568 break;
9569 case 'inv1':
9570 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9571 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9572 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9573 return $tval;
9574 }
9575 break;
9576 case 'rcp1':
9577 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9578 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9579 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9580 return $tval;
9581 }
9582 break;
9583 case 'conf4':
9584 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9585 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9586 // make sure the commissions are not making the difference
9587 if ($booking['totpaid'] + ($booking['cmms'] ?? 0) < $booking['total']) {
9588 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9589 return $tval;
9590 }
9591 }
9592 break;
9593 case 'conf5':
9594 // Confirmed (Total > 0 && Total Paid >= Total)
9595 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) {
9596 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9597 return $tval;
9598 }
9599 break;
9600 case 'inv2':
9601 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9602 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9603 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9604 return $tval;
9605 }
9606 break;
9607 default:
9608 // unsupported rule
9609 break;
9610 }
9611 }
9612
9613 return [];
9614 }
9615
9616 public static function getBookingInfoFromID($bid)
9617 {
9618 $dbo = JFactory::getDbo();
9619
9620 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9621 $dbo->setQuery($q);
9622 $booking_info = $dbo->loadAssoc();
9623 if ($booking_info) {
9624 return $booking_info;
9625 }
9626
9627 return [];
9628 }
9629
9630 /**
9631 * Load booking details for a given list of IDs.
9632 *
9633 * @param int $roomid The room integer number.
9634 * @param array $room_bids_pool List of booking IDs involved.
9635 * @param bool $unassigned Whether to return also the unassigned bookings.
9636 *
9637 * @return array Associative list of sub-unit bookings, or list containing also the unassigned bookings.
9638 *
9639 * @since 1.18.2 (J) - 1.8.2 (WP) added argument $unassigned.
9640 */
9641 public static function loadRoomIndexesBookings(int $roomid, array $room_bids_pool, $unassigned = false)
9642 {
9643 $dbo = JFactory::getDbo();
9644
9645 $pool_values = [];
9646 if ($unassigned) {
9647 // prepare the default return value
9648 $pool_values = [
9649 [],
9650 [],
9651 ];
9652 }
9653
9654 if (empty($roomid) || !$room_bids_pool) {
9655 return $pool_values;
9656 }
9657
9658 $room_features_bookings = [];
9659 $unassigned_bookings = [];
9660
9661 $all_bids = [];
9662 foreach ($room_bids_pool as $day => $bids) {
9663 $all_bids = array_merge($all_bids, $bids);
9664 }
9665 $all_bids = array_map('intval', array_unique($all_bids));
9666 $all_bids_str = implode(', ', $all_bids);
9667
9668 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9669 $dbo->setQuery($q);
9670 $rbookings = $dbo->loadAssocList();
9671 if (!$rbookings) {
9672 return $pool_values;
9673 }
9674
9675 foreach ($rbookings as $k => $v) {
9676 if (empty($v['roomindex'])) {
9677 if ($unassigned) {
9678 // push this booking to the list of unassigned reservations
9679 $unassigned_bookings[] = $v['idorder'];
9680 }
9681 // do not proceed further
9682 continue;
9683 }
9684 if (!isset($room_features_bookings[$v['roomindex']])) {
9685 $room_features_bookings[$v['roomindex']] = [];
9686 }
9687 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9688 }
9689
9690 if ($unassigned) {
9691 // return a list of information
9692 return [
9693 $room_features_bookings,
9694 $unassigned_bookings,
9695 ];
9696 }
9697
9698 // return just the list of room sub-unit bookings
9699 return $room_features_bookings;
9700 }
9701
9702 public static function getSendEmailWhen()
9703 {
9704 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9705 }
9706
9707 public static function getMinutesAutoRemove()
9708 {
9709 static $minAutoRemove = null;
9710
9711 if ($minAutoRemove !== null) {
9712 return $minAutoRemove;
9713 }
9714
9715 $minAutoRemove = VBOFactory::getConfig()->getInt('minautoremove', 0);
9716
9717 return $minAutoRemove;
9718 }
9719
9720 /**
9721 * Returns the currently active SMS driver handle.
9722 *
9723 * @return string
9724 *
9725 * @since 1.18.8 (J) - 1.8.8 (WP) removed support for WhatsApp free-text messages due to Meta
9726 * business template requirements - use the apposite E4jConnect
9727 * WhatsApp Business Messaging service with Vik Channel Manager.
9728 */
9729 public static function getSMSAPIClass()
9730 {
9731 $smsApiValue = VBOFactory::getConfig()->getString('smsapi', '');
9732
9733 if (preg_match('/^whatsapp/i', $smsApiValue)) {
9734 // no longer supported
9735 $smsApiValue = '';
9736 }
9737
9738 return $smsApiValue;
9739 }
9740
9741 public static function autoSendSMSEnabled()
9742 {
9743 return VBOFactory::getConfig()->getBool('smsautosend', false);
9744 }
9745
9746 public static function getSendSMSTo()
9747 {
9748 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9749 }
9750
9751 public static function getSendSMSWhen()
9752 {
9753 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9754 }
9755
9756 public static function getSMSAdminPhone()
9757 {
9758 return VBOFactory::getConfig()->getString('smsadminphone', '');
9759 }
9760
9761 public static function getSMSParams($as_array = true)
9762 {
9763 if (!$as_array) {
9764 return VBOFactory::getConfig()->getString('smsparams', '');
9765 }
9766
9767 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9768 }
9769
9770 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9771 $dbo = JFactory::getDbo();
9772 switch (strtolower($booking_status)) {
9773 case 'standby':
9774 $status = 'pend';
9775 break;
9776 case 'cancelled':
9777 $status = 'canc';
9778 break;
9779 default:
9780 $status = '';
9781 break;
9782 }
9783 $paramtype = 'sms'.$type.'tpl'.$status;
9784 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9785 $dbo->setQuery($q);
9786 $dbo->execute();
9787 if ($dbo->getNumRows() < 1) {
9788 if ($status == 'canc') {
9789 //Type cancelled is used by VCM since v1.6.6
9790 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9791 $dbo->setQuery($q);
9792 $dbo->execute();
9793 }
9794 return '';
9795 }
9796 $ft = $dbo->loadAssocList();
9797 if (is_object($vbo_tn)) {
9798 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9799 }
9800 return $ft[0]['setting'];
9801 }
9802
9803 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9804 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9805 }
9806
9807 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9808 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9809 }
9810
9811 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9812 $dbo = JFactory::getDbo();
9813 $phone = str_replace(" ", '', trim($phone));
9814 $cprefix = '';
9815 if (!empty($country_threecode)) {
9816 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9817 $dbo->setQuery($q);
9818 $dbo->execute();
9819 if ($dbo->getNumRows() > 0) {
9820 $cprefix = $dbo->loadResult();
9821 $cprefix = str_replace(" ", '', trim($cprefix));
9822 }
9823 }
9824 if (!empty($cprefix)) {
9825 if (substr($phone, 0, 1) != '+') {
9826 if (substr($phone, 0, 2) == '00') {
9827 $phone = '+'.substr($phone, 2);
9828 } else {
9829 $phone = $cprefix.$phone;
9830 }
9831 }
9832 }
9833 return $phone;
9834 }
9835
9836 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9837 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9838
9839 /**
9840 * Trigger event to allow third-party plugins to manipulate the template string.
9841 *
9842 * @since 1.16.10 (J) - 1.6.10 (WP)
9843 */
9844 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9845
9846 /**
9847 * Parse all conditional text rules.
9848 *
9849 * @since 1.14 (J) - 1.4.0 (WP)
9850 */
9851 self::getConditionalRulesInstance()
9852 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9853 ->parseTokens($tpl);
9854
9855 /**
9856 * Parse all Door Access Control tags.
9857 *
9858 * @since 1.18.4 (J) - 1.8.4 (WP)
9859 */
9860 VBOFactory::getDoorAccessControl()
9861 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9862
9863 $vbo_df = self::getDateFormat();
9864 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9865 $datesep = self::getDateSeparator();
9866
9867 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9868 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9869 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9870 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9871 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9872 if (!empty($booking['confirmnumber'])) {
9873 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9874 } else {
9875 $tpl = str_replace("{confirmnumb}", '', $tpl);
9876 }
9877 if (!empty($booking['idorderota'])) {
9878 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9879 } else {
9880 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9881 }
9882
9883 $rooms_booked = array();
9884 $rooms_names = array();
9885 $tot_adults = 0;
9886 $tot_children = 0;
9887 $tot_guests = 0;
9888 foreach ($booking_rooms as $broom) {
9889 $rooms_names[] = $broom['room_name'];
9890 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9891 $rooms_booked[$broom['room_name']] += 1;
9892 } else {
9893 $rooms_booked[$broom['room_name']] = 1;
9894 }
9895 $tot_adults += (int)$broom['adults'];
9896 $tot_children += (int)$broom['children'];
9897 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9898 }
9899 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9900 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9901 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9902 $rooms_booked_quant = array();
9903 foreach ($rooms_booked as $rname => $quant) {
9904 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9905 }
9906 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9907 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9908 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9909 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9910 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9911 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9912 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9913 $remaining_bal = $booking['total'] - $booking['totpaid'];
9914 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9915
9916 return $tpl;
9917 }
9918
9919 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9920 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9921
9922 /**
9923 * Trigger event to allow third-party plugins to manipulate the template string.
9924 *
9925 * @since 1.16.10 (J) - 1.6.10 (WP)
9926 */
9927 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9928
9929 /**
9930 * Parse all conditional text rules.
9931 *
9932 * @since 1.14 (J) - 1.4.0 (WP)
9933 */
9934 self::getConditionalRulesInstance()
9935 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9936 ->parseTokens($tpl);
9937
9938 /**
9939 * Parse all Door Access Control tags.
9940 *
9941 * @since 1.18.4 (J) - 1.8.4 (WP)
9942 */
9943 VBOFactory::getDoorAccessControl()
9944 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9945
9946 $vbo_df = self::getDateFormat();
9947 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9948 $datesep = self::getDateSeparator();
9949
9950 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9951 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9952 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9953 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9954 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9955 if (!empty($booking['confirmnumber'])) {
9956 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9957 } else {
9958 $tpl = str_replace("{confirmnumb}", '', $tpl);
9959 }
9960 if (!empty($booking['idorderota'])) {
9961 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9962 } else {
9963 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9964 }
9965
9966 $rooms_booked = array();
9967 $rooms_names = array();
9968 $tot_adults = 0;
9969 $tot_children = 0;
9970 $tot_guests = 0;
9971 foreach ($booking_rooms as $broom) {
9972 $rooms_names[] = $broom['room_name'];
9973 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9974 $rooms_booked[$broom['room_name']] += 1;
9975 } else {
9976 $rooms_booked[$broom['room_name']] = 1;
9977 }
9978 $tot_adults += (int)$broom['adults'];
9979 $tot_children += (int)$broom['children'];
9980 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9981 }
9982 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9983 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9984 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9985 $rooms_booked_quant = array();
9986 foreach ($rooms_booked as $rname => $quant) {
9987 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9988 }
9989 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9990 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9991 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9992 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9993 $remaining_bal = $booking['total'] - $booking['totpaid'];
9994 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9995 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9996
9997 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9998 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9999
10000 if (VBOPlatformDetection::isWordPress()) {
10001 /**
10002 * @wponly Rewrite URI for front-end
10003 */
10004 $book_link = str_replace(JUri::root(), '', $book_link);
10005 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
10006 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
10007 if ($itemid) {
10008 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
10009 }
10010 } else {
10011 /**
10012 * @joomlaonly Rewrite URI for front-end
10013 */
10014 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
10015 $uri_parts = [
10016 'option' => 'com_vikbooking',
10017 'view' => 'booking',
10018 'sid' => $use_sid,
10019 'ts' => $booking['ts'],
10020 ];
10021 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
10022 // inject lang in query string for proper link routing
10023 $uri_parts['lang'] = $booking['lang'];
10024 }
10025 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
10026 }
10027
10028 $tpl = str_replace('{booking_link}', $book_link, $tpl);
10029 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
10030 $tpl = str_replace('{booking_url}', $book_link, $tpl);
10031
10032 return $tpl;
10033 }
10034
10035 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
10036 $dbo = JFactory::getDbo();
10037 if (!class_exists('VboApplication')) {
10038 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
10039 }
10040 $vbo_app = new VboApplication;
10041 if (empty($oid)) {
10042 return false;
10043 }
10044 $sms_api = self::getSMSAPIClass();
10045 if (empty($sms_api)) {
10046 return false;
10047 }
10048 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
10049 return false;
10050 }
10051 $sms_api_params = self::getSMSParams();
10052 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
10053 return false;
10054 }
10055 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
10056 return false;
10057 }
10058 $send_sms_to = self::getSendSMSTo();
10059 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
10060 return false;
10061 }
10062 $booking = array();
10063 $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.";";
10064 $dbo->setQuery($q);
10065 $dbo->execute();
10066 if ($dbo->getNumRows() > 0) {
10067 $booking = $dbo->loadAssoc();
10068 }
10069 if (!(count($booking) > 0)) {
10070 return false;
10071 }
10072 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
10073 return false;
10074 }
10075 $booking_rooms = array();
10076 $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'].";";
10077 $dbo->setQuery($q);
10078 $dbo->execute();
10079 if ($dbo->getNumRows() > 0) {
10080 $booking_rooms = $dbo->loadAssocList();
10081 }
10082 $admin_phone = self::getSMSAdminPhone();
10083 $admin_sendermail = self::getSenderMail();
10084 $admin_email = self::getAdminMail();
10085 $f_result = false;
10086 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
10087 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
10088 //SMS for the administrator
10089 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
10090 if (!empty($sms_text)) {
10091 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10092 //administrator phone can contain multiple numbers separated by comma or semicolon
10093 $admin_phones = array();
10094 if (strpos($admin_phone, ',') !== false) {
10095 $all_phones = explode(',', $admin_phone);
10096 foreach ($all_phones as $aph) {
10097 if (!empty($aph)) {
10098 $admin_phones[] = trim($aph);
10099 }
10100 }
10101 } elseif (strpos($admin_phone, ';') !== false) {
10102 $all_phones = explode(';', $admin_phone);
10103 foreach ($all_phones as $aph) {
10104 if (!empty($aph)) {
10105 $admin_phones[] = trim($aph);
10106 }
10107 }
10108 } else {
10109 $admin_phones[] = $admin_phone;
10110 }
10111 foreach ($admin_phones as $admphone) {
10112 $response_obj = $sms_obj->sendMessage($admphone, strip_tags($sms_text));
10113 if ( !$sms_obj->validateResponse($response_obj) ) {
10114 //notify the administrator via email with the error of the SMS sending
10115 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10116 } else {
10117 $f_result = true;
10118 }
10119 }
10120 }
10121 }
10122 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
10123 //SMS for the Customer
10124 $vbo_tn = self::getTranslator();
10125 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
10126 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
10127 if (!empty($sms_text)) {
10128 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10129 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
10130 if ( !$sms_obj->validateResponse($response_obj) ) {
10131 //notify the administrator via email with the error of the SMS sending
10132 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10133 } else {
10134 $f_result = true;
10135 }
10136 }
10137 }
10138 return $f_result;
10139 }
10140
10141 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
10142 {
10143 if (!defined('_VIKBOOKINGEXEC')) {
10144 define('_VIKBOOKINGEXEC', '1');
10145 }
10146
10147 ob_start();
10148 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
10149 $content = ob_get_contents();
10150 ob_end_clean();
10151
10152 $default_params = array(
10153 'show_header' => 0,
10154 'header_data' => array(),
10155 'show_footer' => 0,
10156 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10157 'pdf_unit' => 'PDF_UNIT',
10158 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10159 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10160 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10161 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10162 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10163 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10164 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10165 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10166 'header_font_size' => '10',
10167 'body_font_size' => '10',
10168 'footer_font_size' => '8',
10169 'show_lines_taxrate_col' => 0,
10170 );
10171
10172 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
10173 $default_params = array_merge($default_params, $invoice_params);
10174 }
10175
10176 return array($content, $default_params);
10177 }
10178
10179 /**
10180 * Includes within a buffer the template file for the custom (manual) invoice.
10181 *
10182 * @param array $invoice the record of the custom invoice
10183 * @param array $customer the customer record
10184 *
10185 * @return string the HTML content of the template file
10186 *
10187 * @since 1.11.1
10188 */
10189 public static function loadCustomInvoiceTmpl($invoice, $customer) {
10190 if (!defined('_VIKBOOKINGEXEC')) {
10191 define('_VIKBOOKINGEXEC', '1');
10192 }
10193 ob_start();
10194 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
10195 $content = ob_get_contents();
10196 ob_end_clean();
10197 $default_params = array(
10198 'show_header' => 0,
10199 'header_data' => array(),
10200 'show_footer' => 0,
10201 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10202 'pdf_unit' => 'PDF_UNIT',
10203 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10204 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10205 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10206 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10207 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10208 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10209 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10210 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10211 'header_font_size' => '10',
10212 'body_font_size' => '10',
10213 'footer_font_size' => '8'
10214 );
10215 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
10216 $default_params = array_merge($default_params, $invoice_params);
10217 }
10218 return array($content, $default_params);
10219 }
10220
10221 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
10222 {
10223 // clone the original template file source
10224 $parsed = $invoicetpl;
10225
10226 /**
10227 * Parse all conditional text rules.
10228 *
10229 * @since 1.14 (J) - 1.4.0 (WP)
10230 */
10231 self::getConditionalRulesInstance()
10232 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
10233 ->parseTokens($parsed);
10234
10235 $dbo = JFactory::getDbo();
10236 if (is_null($vbo_tn)) {
10237 $vbo_tn = self::getTranslator();
10238 }
10239 // availability helper
10240 $av_helper = self::getAvailabilityInstance();
10241
10242 $nowdf = self::getDateFormat();
10243 if ($nowdf == "%d/%m/%Y") {
10244 $df = 'd/m/Y';
10245 } elseif ($nowdf == "%m/%d/%Y") {
10246 $df = 'm/d/Y';
10247 } else {
10248 $df = 'Y/m/d';
10249 }
10250 $datesep = self::getDateSeparator();
10251
10252 /**
10253 * Split stay reservation.
10254 *
10255 * @since 1.16.0 (J) - 1.6.0 (WP)
10256 */
10257 $room_stay_dates = [];
10258 if ($booking['split_stay']) {
10259 if ($booking['status'] == 'confirmed') {
10260 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
10261 } else {
10262 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
10263 }
10264 // immediately count the number of nights of stay for each split room
10265 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
10266 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
10267 // overwrite values for compatibility with non-confirmed bookings
10268 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
10269 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
10270 }
10271 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
10272 // overwrite the whole array
10273 $room_stay_dates[$sps_r_k] = $sps_r_v;
10274 }
10275 }
10276
10277 $companylogo = self::getSiteLogo();
10278 $uselogo = '';
10279 if (!empty($companylogo)) {
10280 /**
10281 * Let's try to prevent TCPDF errors.
10282 *
10283 * @since August 2nd 2019
10284 */
10285 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10286 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10287 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10288 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10289 }
10290 }
10291 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10292 $parsed = str_replace("{company_info}", $company_info, $parsed);
10293 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
10294 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
10295 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
10296 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10297
10298 // custom fields replacement
10299 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10300 if (is_array($cmatches[1]) && $cmatches[1]) {
10301 $cfids = array();
10302 foreach ($cmatches[1] as $cfid) {
10303 $cfids[] = $cfid;
10304 }
10305 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10306 $dbo->setQuery($q);
10307 $cfields = $dbo->loadAssocList();
10308 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10309 $cfmap = array();
10310 foreach ($cfields as $cf) {
10311 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10312 }
10313 $cfmapreplace = array();
10314 $partsreceived = explode("\n", $booking['custdata']);
10315 if (count($partsreceived) > 0) {
10316 foreach ($partsreceived as $pst) {
10317 if (!empty($pst)) {
10318 $tmpdata = explode(":", $pst);
10319 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10320 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10321 }
10322 }
10323 }
10324 }
10325 foreach ($cmatches[1] as $cfid) {
10326 if (array_key_exists($cfid, $cfmapreplace)) {
10327 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10328 } else {
10329 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10330 }
10331 }
10332 }
10333
10334 // invoice price description - Start
10335 $rooms = array();
10336 $tars = array();
10337 $arrpeople = array();
10338 $is_package = !empty($booking['pkg']) ? true : false;
10339 $tot_adults = 0;
10340 $tot_children = 0;
10341 $tot_guests = 0;
10342 foreach ($booking_rooms as $kor => $or) {
10343 $num = $kor + 1;
10344 $rooms[$num] = $or;
10345
10346 // determine the days to consider for the count of the availability
10347 $room_nights = $booking['days'];
10348 $room_checkin = $booking['checkin'];
10349 $room_checkout = $booking['checkout'];
10350 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10351 $room_nights = $room_stay_dates[$kor]['nights'];
10352 $room_checkin = $room_stay_dates[$kor]['checkin'];
10353 $room_checkout = $room_stay_dates[$kor]['checkout'];
10354 }
10355
10356 $arrpeople[$num]['adults'] = $or['adults'];
10357 $arrpeople[$num]['children'] = $or['children'];
10358 $tot_adults += $or['adults'];
10359 $tot_children += $or['children'];
10360 $tot_guests += ($or['adults'] + $or['children']);
10361 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10362 //package or custom cost set from the back-end
10363 continue;
10364 }
10365 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
10366 $dbo->setQuery($q);
10367 $tar = $dbo->loadAssocList();
10368 if ($tar) {
10369 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
10370 //different usage
10371 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
10372 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
10373 //Occupancy Override
10374 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
10375 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
10376 //
10377 if (is_array($diffusageprice)) {
10378 //set a charge or discount to the price(s) for the different usage of the room
10379 foreach ($tar as $kpr => $vpr) {
10380 $tar[$kpr]['diffusage'] = $or['adults'];
10381 if ($diffusageprice['chdisc'] == 1) {
10382 //charge
10383 if ($diffusageprice['valpcent'] == 1) {
10384 //fixed value
10385 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10386 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10387 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
10388 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10389 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
10390 } else {
10391 //percentage value
10392 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10393 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
10394 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
10395 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10396 $tar[$kpr]['cost'] = $aduseval;
10397 }
10398 } else {
10399 //discount
10400 if ($diffusageprice['valpcent'] == 1) {
10401 //fixed value
10402 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10403 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10404 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
10405 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10406 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
10407 } else {
10408 //percentage value
10409 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10410 $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);
10411 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
10412 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10413 $tar[$kpr]['cost'] = $aduseval;
10414 }
10415 }
10416 }
10417 }
10418 }
10419 //
10420 $tars[$num] = $tar[0];
10421 }
10422 }
10423 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10424 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10425 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10426 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10427 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10428 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10429
10430 $isdue = 0;
10431 $tot_taxes = 0;
10432 $tot_city_taxes = 0;
10433 $tot_fees = 0;
10434 $pricestr = array();
10435 $optstr = array();
10436 // start building the tax summary
10437 VBOTaxonomySummary::start();
10438 foreach ($booking_rooms as $kor => $or) {
10439 $num = $kor + 1;
10440
10441 // determine the days to consider for the count of the availability
10442 $room_nights = $booking['days'];
10443 $room_checkin = $booking['checkin'];
10444 $room_checkout = $booking['checkout'];
10445 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10446 $room_nights = $room_stay_dates[$kor]['nights'];
10447 $room_checkin = $room_stay_dates[$kor]['checkin'];
10448 $room_checkout = $room_stay_dates[$kor]['checkout'];
10449 }
10450
10451 $pricestr[$num] = array();
10452 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10453 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
10454 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
10455 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
10456 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
10457 $pricestr[$num]['tot'] = $calctar;
10458 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
10459 $tot_taxes += ($calctar - $cost_minus_tax);
10460 $isdue += $calctar;
10461 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
10462 $pricestr[$num]['tax_rate'] = $tax_rate;
10463 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
10464 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10465 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
10466 $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'] : "");
10467 $pricestr[$num]['tot'] = $calctar;
10468 $tars[$num]['calctar'] = $calctar;
10469 $isdue += $calctar;
10470 if ($calctar == $display_rate) {
10471 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
10472 $tot_taxes += ($display_rate - $cost_minus_tax);
10473 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
10474 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
10475 $pricestr[$num]['tax_rate'] = $tax_rate;
10476 } else {
10477 $tot_taxes += ($calctar - $display_rate);
10478 $pricestr[$num]['tax'] = ($calctar - $display_rate);
10479 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
10480 $pricestr[$num]['tax_rate'] = $tax_rate;
10481 }
10482 }
10483 $optstr[$num] = array();
10484 $opt_ind = 0;
10485 if (!empty($or['optionals'])) {
10486 $stepo = explode(";", $or['optionals']);
10487 foreach ($stepo as $roptkey => $oo) {
10488 if (!empty($oo)) {
10489 $stept = explode(":", $oo);
10490 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
10491 $dbo->setQuery($q);
10492 $actopt = $dbo->loadAssocList();
10493 if ($actopt) {
10494 if (is_object($vbo_tn)) {
10495 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
10496 }
10497 $optstr[$num][$opt_ind] = array();
10498 $chvar = '';
10499 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
10500 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
10501 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
10502 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
10503 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
10504 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
10505 $agestept = explode('-', $stept[1]);
10506 $stept[1] = $agestept[0];
10507 $chvar = $agestept[1];
10508 $realcost = 0;
10509 if (!empty($chvar)) {
10510 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
10511 //percentage value of the adults tariff
10512 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10513 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10514 } else {
10515 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10516 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10517 }
10518 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
10519 //VBO 1.10 - percentage value of room base cost
10520 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10521 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10522 } else {
10523 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
10524 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10525 }
10526 }
10527 $actopt[0]['chageintv'] = $chvar;
10528 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
10529 $actopt[0]['quan'] = $stept[1];
10530 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
10531 }
10532 } else {
10533 $actopt[0]['quan'] = $stept[1];
10534 // VBO 1.11 - options percentage cost of the room total fee
10535 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10536 $deftar_basecosts = $or['cust_cost'];
10537 } else {
10538 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10539 }
10540 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
10541 //
10542 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
10543 }
10544 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
10545 $realcost = $actopt[0]['maxprice'];
10546 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
10547 $realcost = $actopt[0]['maxprice'] * $stept[1];
10548 }
10549 }
10550 if ($actopt[0]['perperson'] == 1) {
10551 $realcost = $realcost * $or['adults'];
10552 }
10553
10554 /**
10555 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
10556 *
10557 * @since 1.17.7 (J) - 1.7.7 (WP)
10558 */
10559 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
10560 if ($custom_calculation) {
10561 $realcost = (float) $custom_calculation[0];
10562 }
10563
10564 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
10565 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
10566 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
10567 $optstr[$num][$opt_ind]['tax'] = 0;
10568 if ($actopt[0]['is_citytax'] == 1) {
10569 $tot_city_taxes += $tmpopr;
10570 } elseif ($actopt[0]['is_fee'] == 1) {
10571 $tot_fees += $tmpopr;
10572 }
10573 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
10574 if ($tmpopr == $realcost) {
10575 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
10576 $tot_taxes += ($realcost - $opt_minus_tax);
10577 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
10578 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
10579 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10580 } else {
10581 $tot_taxes += ($tmpopr - $realcost);
10582 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
10583 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
10584 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10585 }
10586 //
10587 $opt_ind++;
10588 $isdue += $tmpopr;
10589 }
10590 }
10591 }
10592 }
10593
10594 // custom extra costs
10595 if (!empty($or['extracosts'])) {
10596 $cur_extra_costs = json_decode($or['extracosts'], true);
10597 foreach ($cur_extra_costs as $eck => $ecv) {
10598 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10599 $isdue += $ecplustax;
10600 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
10601 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
10602 $optstr[$num][$opt_ind]['tax'] = 0;
10603 if ($ecplustax == $ecv['cost']) {
10604 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10605 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
10606 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
10607 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
10608 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10609 } else {
10610 $tot_taxes += ($ecplustax - $ecv['cost']);
10611 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
10612 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
10613 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10614 }
10615 $opt_ind++;
10616 }
10617 }
10618 }
10619
10620 $usedcoupon = false;
10621 if (strlen($booking['coupon']) > 0) {
10622 $orig_isdue = $isdue;
10623 $expcoupon = explode(";", $booking['coupon']);
10624 $usedcoupon = $expcoupon;
10625 $isdue = $isdue - (float)$expcoupon[1];
10626 if ($isdue != $orig_isdue) {
10627 //lower taxes proportionally
10628 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10629 }
10630 }
10631 if ($booking['refund'] > 0) {
10632 $orig_isdue = $isdue;
10633 $isdue -= $booking['refund'];
10634 if ($isdue != $orig_isdue) {
10635 //lower taxes proportionally
10636 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10637 }
10638 }
10639 $rows_written = 0;
10640 $inv_rows = '';
10641 foreach ($pricestr as $num => $price_descr) {
10642 // add room split stay information, if any
10643 $split_stay_str = '';
10644 $kor = $num - 1;
10645 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10646 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10647 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10648 }
10649
10650 $inv_rows .= '<tr>'."\n";
10651 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10652 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($price_descr['tot'] - $price_descr['tax'])), $booking['currencyname']) . '</td>'."\n";
10653 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tax']), $booking['currencyname']) . '</td>'."\n";
10654 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tot']), $booking['currencyname']) . '</td>'."\n";
10655 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10656 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10657 }
10658 $inv_rows .= '</tr>'."\n";
10659 $rows_written++;
10660 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10661 foreach ($optstr[$num] as $optk => $optv) {
10662 $inv_rows .= '<tr>'."\n";
10663 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10664 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($optv['tot'] - $optv['tax'])), $booking['currencyname']) . '</td>'."\n";
10665 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tax']), $booking['currencyname']) . '</td>'."\n";
10666 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tot']), $booking['currencyname']) . '</td>'."\n";
10667 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10668 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10669 }
10670 $inv_rows .= '</tr>'."\n";
10671 $rows_written++;
10672 }
10673 }
10674 }
10675
10676 // if discount print row
10677 if ($usedcoupon !== false) {
10678 $inv_rows .= '<tr>'."\n";
10679 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10680 $inv_rows .= '</tr>'."\n";
10681 $inv_rows .= '<tr>'."\n";
10682 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10683 $inv_rows .= '<td></td>'."\n";
10684 $inv_rows .= '<td></td>'."\n";
10685 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($usedcoupon[1]), $booking['currencyname']) . '</td>'."\n";
10686 $inv_rows .= '</tr>'."\n";
10687 $rows_written += 2;
10688 }
10689 // if refunded amount, print row
10690 if ($booking['refund'] > 0) {
10691 $inv_rows .= '<tr>'."\n";
10692 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10693 $inv_rows .= '</tr>'."\n";
10694 $inv_rows .= '<tr>'."\n";
10695 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10696 $inv_rows .= '<td></td>'."\n";
10697 $inv_rows .= '<td></td>'."\n";
10698 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($booking['refund']), $booking['currencyname']) . '</td>'."\n";
10699 $inv_rows .= '</tr>'."\n";
10700 $rows_written += 2;
10701 }
10702 //
10703 $min_records = 8;
10704 if ($rows_written < $min_records) {
10705 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10706 $inv_rows .= '<tr>'."\n";
10707 $inv_rows .= '<td></td>'."\n";
10708 $inv_rows .= '<td></td>'."\n";
10709 $inv_rows .= '<td></td>'."\n";
10710 $inv_rows .= '</tr>'."\n";
10711 }
10712 }
10713 //invoice price description - End
10714 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10715 $parsed = str_replace("{invoice_totalnet}", self::formatCurrencyNumber(self::numberFormat(($isdue - $tot_taxes)), $booking['currencyname']), $parsed);
10716 $parsed = str_replace("{invoice_totaltax}", self::formatCurrencyNumber(self::numberFormat($tot_taxes), $booking['currencyname']), $parsed);
10717 $parsed = str_replace("{invoice_grandtotal}", self::formatCurrencyNumber(self::numberFormat($isdue), $booking['currencyname']), $parsed);
10718 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10719
10720 // invoice tax summary
10721 $tax_summary = VBOTaxonomySummary::get();
10722 $tax_names = VBOTaxonomySummary::getNames();
10723 $taxsum_rows = "";
10724 foreach ($tax_summary as $tax_rate => $tax_amount) {
10725 if (!$tax_rate && !$tax_amount) {
10726 continue;
10727 }
10728 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10729 $taxsum_rows .= '<tr>' . "\n";
10730 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10731 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10732 $taxsum_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($tax_amount), $booking['currencyname']) . '</td>' . "\n";
10733 $taxsum_rows .= '</tr>' . "\n";
10734 }
10735 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10736
10737 return $parsed;
10738 }
10739
10740 /**
10741 * Specific method for parsing the template file for the custom (manual) invoices.
10742 *
10743 * @param string $invoicetpl the plain custom invoice template file before parsing
10744 * @param array $invoice the invoice record
10745 * @param array $customer the customer record
10746 *
10747 * @return string the HTML content of the parsed custom invoice template
10748 *
10749 * @since 1.11.1
10750 */
10751 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10752 $nowdf = self::getDateFormat(true);
10753 if ($nowdf == "%d/%m/%Y") {
10754 $df = 'd/m/Y';
10755 } elseif ($nowdf == "%m/%d/%Y") {
10756 $df = 'm/d/Y';
10757 } else {
10758 $df = 'Y/m/d';
10759 }
10760 $datesep = self::getDateSeparator(true);
10761 $companylogo = self::getSiteLogo();
10762 $uselogo = '';
10763 if (!empty($companylogo)) {
10764 /**
10765 * Let's try to prevent TCPDF errors by checking if the file exists.
10766 *
10767 * @since August 2nd 2019
10768 */
10769 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10770 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10771 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10772 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10773 }
10774 }
10775 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10776 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10777 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10778 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10779 // customer information
10780 $custinfo = '';
10781 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10782 $custinfo .= $customer['email'] . "\n";
10783 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10784 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10785 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10786 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10787 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10788 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10789 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10790 // invoice notes
10791 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10792
10793 return $invoicetpl;
10794 }
10795
10796 /**
10797 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10798 *
10799 * @param array $booking the booking information.
10800 * @param int $invoice_num the invoice number.
10801 * @param string $invoice_suff the invoice suffix.
10802 * @param string $invoice_date the invoice date.
10803 * @param string $company_info optional company information.
10804 * @param bool $translate whether the invoice contents should be translated in the booking language.
10805 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10806 *
10807 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10808 *
10809 * @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.
10810 */
10811 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10812 {
10813 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10814 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10815 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10816
10817 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10818 return false;
10819 }
10820
10821 /**
10822 * Trigger event to allow third party plugins to override some values for the invoice generation.
10823 *
10824 * @since 1.16.3 (J) - 1.6.3 (WP)
10825 */
10826 $event_args = [
10827 'invoice_num' => $invoice_num,
10828 'invoice_suff' => $invoice_suff,
10829 'invoice_date' => $invoice_date,
10830 'company_info' => $company_info,
10831 'translate' => $translate,
10832 ];
10833 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10834 if (is_array($event_args) && $event_args) {
10835 // extract the new values to be used
10836 extract($event_args);
10837 }
10838
10839 $dbo = JFactory::getDbo();
10840 $vbo_tn = self::getTranslator();
10841
10842 // inject the currency name value
10843 $currencyname = self::getCurrencyName();
10844 $booking['currencyname'] = $currencyname;
10845
10846 // make sure we've got a customer ID assigned to the reservation record
10847 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10848 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10849 if ($booking_customer) {
10850 $booking['idcustomer'] = $booking_customer['id'];
10851 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10852 $booking['customer_pin'] = $booking_customer['pin'];
10853 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10854 }
10855 }
10856
10857 $nowdf = self::getDateFormat(true);
10858 if ($nowdf == "%d/%m/%Y") {
10859 $df = 'd/m/Y';
10860 } elseif ($nowdf == "%m/%d/%Y") {
10861 $df = 'm/d/Y';
10862 } else {
10863 $df = 'Y/m/d';
10864 }
10865 $datesep = self::getDateSeparator(true);
10866
10867 if (empty($invoice_date)) {
10868 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10869 $used_date = $booking['ts'];
10870 } else {
10871 /**
10872 * We could be re-generating an invoice for a booking that already had an invoice.
10873 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10874 * It is now possible to keep the same invoice date as the previous one, so check
10875 * what value contains $invoice_date to see if it's different from today's date.
10876 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10877 * to also check the length of the string $invoice_date before using that date.
10878 *
10879 * @since 1.10 - August 2018
10880 */
10881 $base_ts = time();
10882 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10883 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10884 }
10885 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10886 $used_date = $base_ts;
10887 }
10888
10889 // load room reservation records
10890 $q = $dbo->getQuery(true)
10891 ->select($dbo->qn('or') . '.*')
10892 ->select($dbo->qn('r.name', 'room_name'))
10893 ->select($dbo->qn('r.fromadult'))
10894 ->select($dbo->qn('r.toadult'))
10895 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10896 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10897 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10898 ->order($dbo->qn('or.id') . ' ASC');
10899
10900 $dbo->setQuery($q);
10901 $booking_rooms = $dbo->loadAssocList();
10902
10903 if (!$booking_rooms) {
10904 return false;
10905 }
10906
10907 // check if an invoice for this reservation exists
10908 $invoice_id = 0;
10909 $dbo->setQuery(
10910 $dbo->getQuery(true)
10911 ->select('*')
10912 ->from($dbo->qn('#__vikbooking_invoices'))
10913 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10914 ->order($dbo->qn('id') . ' DESC')
10915 );
10916 $invoice_data = $dbo->loadAssoc();
10917 if ($invoice_data) {
10918 $invoice_id = $invoice_data['id'];
10919 }
10920
10921 // if requested, prepare data to refresh the PDF invoice
10922 if ($refresh_pdf === true) {
10923 if (!$invoice_id || !$invoice_data) {
10924 // cannot refresh the PDF if an invoice was never created before
10925 return false;
10926 }
10927
10928 // load the same exact previous information to refresh the invoice
10929 $invoice_num = $invoice_data['number'];
10930 $invoice_suff = $invoice_suff;
10931 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10932 $used_date = $invoice_data['for_date'];
10933
10934 // parse the previous (current) invoice number
10935 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10936 if ($matches) {
10937 // overwrite values
10938 $invoice_num = $matches[1] ?: $invoice_num;
10939 $invoice_suff = $matches[2] ?: $invoice_suff;
10940 }
10941 }
10942
10943 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10944 if ($translate === true) {
10945 if (!empty($booking['lang'])) {
10946 $lang = JFactory::getLanguage();
10947 if ($lang->getTag() != $booking['lang']) {
10948 if (VBOPlatformDetection::isWordPress()) {
10949 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10950 } else {
10951 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10952 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10953 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10954 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10955 }
10956 }
10957 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10958 // force the translation to start because contents should be translated
10959 $vbo_tn::$force_tolang = $booking['lang'];
10960 }
10961 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10962 }
10963 }
10964
10965 // the PDF file name
10966 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10967
10968 /**
10969 * If an analogic invoice is not available, make sure to create the record
10970 * before letting the e-invoicing drivers run to avoid a loop.
10971 *
10972 * @since 1.16.7 (J) - 1.6.7 (WP)
10973 */
10974 $retval = null;
10975 if (!$refresh_pdf && !$invoice_id) {
10976 // prepare record object
10977 $invoice_record = new stdClass;
10978 $invoice_record->number = $invoice_num . $invoice_suff;
10979 $invoice_record->file_name = $pdffname;
10980 $invoice_record->idorder = (int)$booking['id'];
10981 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10982 $invoice_record->created_on = time();
10983 $invoice_record->for_date = (int)$used_date;
10984
10985 // insert record
10986 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10987 return false;
10988 }
10989
10990 $retval = $invoice_record->id ?? false;
10991
10992 if (!$retval) {
10993 return false;
10994 }
10995 }
10996
10997 /**
10998 * The generation of the analogic invoices can trigger the drivers for the
10999 * generation of the e-Invoices if they are set to automatically run.
11000 * However, the e-Invoicing classes may be calling this method, so we need
11001 * to make sure the eInvoicing class is not running before proceeding.
11002 * This method could be called within a loop, and so the second iterations
11003 * may already have loaded the eInvocing class. For this we use a static variable.
11004 *
11005 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11006 * though additional information may be available only after the transmission. On top
11007 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
11008 */
11009 static $einvocing_can_run = false;
11010
11011 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
11012 // allow second iterations calling this method to run
11013 $einvocing_can_run = true;
11014 }
11015
11016 // always load all drivers, db and plugin based ones
11017 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11018
11019 foreach ($drivers as $driver_obj) {
11020 // set the flag of the external call
11021 $driver_obj->externalCall = __METHOD__;
11022
11023 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
11024 $driver_obj->externalData['einvnum'] = $invoice_num;
11025
11026 // let the driver eventually elaborate the booking information before invoicing
11027 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
11028
11029 if ($einvocing_can_run === true && !$refresh_pdf) {
11030 // generate the e-Invoice
11031 $driver_err = '';
11032 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
11033 $driver_err = $driver_obj->getError();
11034 VikError::raiseWarning('', $driver_err);
11035 }
11036
11037 // Booking History - store log for the e-Invoice result
11038 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
11039 }
11040 }
11041
11042 /**
11043 * Start the generation of the analogic (courtesy) PDF invoice.
11044 */
11045
11046 // load dependencies
11047 if (!class_exists('TCPDF')) {
11048 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11049 }
11050 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11051
11052 /**
11053 * Trigger event to allow third party plugins to return a specific font name.
11054 *
11055 * @since 1.16.0 (J) - 1.6.0 (WP)
11056 */
11057 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11058 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11059 $usepdffont = $custom_pdf_font[0];
11060 }
11061
11062 // set array variable to the template file
11063 $booking_info = self::getBookingInfoFromID($booking['id']);
11064
11065 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
11066
11067 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
11068
11069 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11070
11071 if (is_file($pathpdf)) {
11072 @unlink($pathpdf);
11073 }
11074
11075 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11076
11077 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11078 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
11079
11080 // header for each page of the pdf
11081 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
11082 $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]);
11083 }
11084
11085 // change some currencies to their unicode (decimal) value
11086 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11087 if (array_key_exists($booking['currencyname'], $unichr_map)) {
11088 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
11089 }
11090
11091 // header and footer fonts
11092 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11093 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11094
11095 // margins
11096 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11097 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11098 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11099
11100 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11101 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11102 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11103
11104 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11105 $pdf->SetPrintHeader(false);
11106 }
11107 if ($pdfparams['show_footer'] == 0) {
11108 $pdf->SetPrintFooter(false);
11109 }
11110
11111 $pdf->AddPage();
11112 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11113 $pdf->lastPage();
11114 $pdf->Output($pathpdf, 'F');
11115
11116 if (!is_file($pathpdf)) {
11117 return false;
11118 }
11119
11120 if (VBOPlatformDetection::isWordPress()) {
11121 /**
11122 * @wponly - trigger files mirroring
11123 */
11124 VikBookingLoader::import('update.manager');
11125 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11126 }
11127
11128 // Booking History
11129 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
11130
11131 if ($retval) {
11132 // a new invoice ID was inserted above
11133 return $retval;
11134 }
11135
11136 // prepare record object to be updated (if insert needed, it's done at first)
11137 $invoice_record = new stdClass;
11138 $invoice_record->id = (int)$invoice_id;
11139 $invoice_record->number = $invoice_num . $invoice_suff;
11140 $invoice_record->file_name = $pdffname;
11141 $invoice_record->idorder = (int)$booking['id'];
11142 $invoice_record->idcustomer = (int)$booking['idcustomer'];
11143 $invoice_record->created_on = time();
11144 $invoice_record->for_date = (int)$used_date;
11145
11146 // update record
11147 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
11148
11149 // return the PDF invoice ID
11150 return $invoice_id;
11151 }
11152
11153 /**
11154 * Generates an analogic invoice in PDF format for a custom list of services.
11155 * No bookings are assigned to this custom invoice. The method parses the same
11156 * invoice template as for the regular process with real booking IDs.
11157 * The invoice number must be stored before calling this method.
11158 *
11159 * @param int $invoice_id the ID of the custom invoice record
11160 *
11161 * @return bool
11162 *
11163 * @since 1.11.1 (J) - 1.1.1 (WP)
11164 */
11165 public static function generateCustomInvoice($invoice_id)
11166 {
11167 $dbo = JFactory::getDbo();
11168
11169 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
11170 $dbo->setQuery($q);
11171 $invoice = $dbo->loadAssoc();
11172 if (!$invoice) {
11173 return false;
11174 }
11175
11176 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
11177 $rawcont = is_array($rawcont) ? $rawcont : array();
11178 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
11179 $invoice['rawcont'] = $rawcont;
11180 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
11181 if (!$rawcont || !$rows || !$customer) {
11182 // at least one invoice raw is mandatory as well as the customer
11183 return false;
11184 }
11185
11186 /**
11187 * Start the generation of the electronic invoice, if any driver was enabled.
11188 *
11189 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11190 * though additional information would be available only after the transmission.
11191 */
11192 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11193
11194 if ($drivers) {
11195 // make sure the invoice number is just a number
11196 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
11197
11198 // invoke all drivers that should run automatically
11199 foreach ($drivers as $driver_obj) {
11200 // set the flag of the external call
11201 $driver_obj->externalCall = __METHOD__;
11202
11203 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
11204 $driver_obj->externalData['einvnum'] = $invoice['number'];
11205 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
11206 $driver_obj->externalData['einvcustom'] = $invoice;
11207
11208 // prepare data array for the generation of the e-Invoice
11209 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
11210
11211 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
11212 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
11213 if ($preveinv !== false) {
11214 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
11215 }
11216
11217 // generate the e-Invoice
11218 $driver_err = '';
11219 if (!$driver_obj->generateEInvoice($einvdata)) {
11220 $driver_err = $driver_obj->getError();
11221 VikError::raiseWarning('', $driver_err);
11222 }
11223 }
11224 }
11225
11226 /**
11227 * Start the generation of the analogic (courtesy) PDF invoice.
11228 */
11229
11230 // load dependencies
11231 if (!class_exists('TCPDF')) {
11232 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11233 }
11234 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11235
11236 /**
11237 * Trigger event to allow third party plugins to return a specific font name.
11238 *
11239 * @since 1.16.0 (J) - 1.6.0 (WP)
11240 */
11241 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11242 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11243 $usepdffont = $custom_pdf_font[0];
11244 }
11245
11246 // load invoice template file
11247 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
11248
11249 // parse invoice template file
11250 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
11251
11252 $pdffname = $invoice['file_name'];
11253 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11254
11255 if (is_file($pathpdf)) {
11256 @unlink($pathpdf);
11257 }
11258
11259 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11260
11261 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11262 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
11263
11264 // header for each page of the pdf
11265 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
11266 $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]);
11267 }
11268
11269 // change some currencies to their unicode (decimal) value
11270 $currencyname = self::getCurrencyName();
11271 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11272 if (array_key_exists($currencyname, $unichr_map)) {
11273 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
11274 }
11275
11276 // header and footer fonts
11277 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11278 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11279
11280 // margins
11281 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11282 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11283 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11284
11285 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11286 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11287 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11288
11289 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11290 $pdf->SetPrintHeader(false);
11291 }
11292 if ($pdfparams['show_footer'] == 0) {
11293 $pdf->SetPrintFooter(false);
11294 }
11295
11296 $pdf->AddPage();
11297 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11298 $pdf->lastPage();
11299 $pdf->Output($pathpdf, 'F');
11300
11301 if (!is_file($pathpdf)) {
11302 return false;
11303 }
11304
11305 if (VBOPlatformDetection::isWordPress()) {
11306 /**
11307 * @wponly - trigger files mirroring
11308 */
11309 VikBookingLoader::import('update.manager');
11310 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11311 }
11312
11313 // return the result of the generation of the PDF invoice
11314 return true;
11315 }
11316
11317 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
11318 {
11319 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
11320 return false;
11321 }
11322
11323 $dbo = JFactory::getDbo();
11324
11325 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
11326 $dbo->setQuery($q);
11327 $invoice_data = $dbo->loadAssoc();
11328
11329 if (!$invoice_data) {
11330 return false;
11331 }
11332
11333 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
11334 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
11335 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
11336 if (!file_exists($invoice_file_path)) {
11337 return false;
11338 }
11339
11340 $vbo_app = self::getVboApplication();
11341 $admin_sendermail = self::getSenderMail();
11342
11343 $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);
11344
11345 // update record
11346 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
11347 $dbo->setQuery($q);
11348 $dbo->execute();
11349
11350 return true;
11351 }
11352
11353 public static function loadCheckinDocTmpl(array $booking_info = [], array $booking_rooms = [], array $customer = [])
11354 {
11355 if (!defined('_VIKBOOKINGEXEC')) {
11356 define('_VIKBOOKINGEXEC', '1');
11357 }
11358
11359 ob_start();
11360 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php';
11361 $content = ob_get_contents();
11362 ob_end_clean();
11363
11364 $default_params = [
11365 'show_header' => 0,
11366 'header_data' => [],
11367 'show_footer' => 0,
11368 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
11369 'pdf_unit' => 'PDF_UNIT',
11370 'pdf_page_format' => 'PDF_PAGE_FORMAT',
11371 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
11372 'pdf_margin_top' => 'PDF_MARGIN_TOP',
11373 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
11374 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
11375 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
11376 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
11377 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
11378 'header_font_size' => '10',
11379 'body_font_size' => '10',
11380 'footer_font_size' => '8'
11381 ];
11382
11383 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
11384 $default_params = array_merge($default_params, $checkin_params);
11385 }
11386
11387 return [$content, $default_params];
11388 }
11389
11390 public static function parseCheckinDocTemplate(string $checkintpl, array $booking, array $booking_rooms, array $customer = [])
11391 {
11392 $dbo = JFactory::getDbo();
11393 $app = JFactory::getApplication();
11394
11395 // clone string
11396 $parsed = $checkintpl;
11397
11398 $datesep = self::getDateSeparator();
11399 $nowdf = self::getDateFormat();
11400 if ($nowdf == "%d/%m/%Y") {
11401 $df = 'd/m/Y';
11402 } elseif ($nowdf == "%m/%d/%Y") {
11403 $df = 'm/d/Y';
11404 } else {
11405 $df = 'Y/m/d';
11406 }
11407
11408 $company_name = self::getFrontTitle();
11409 $company_info = self::getInvoiceCompanyInfo();
11410 $companylogo = self::getSiteLogo();
11411 $uselogo = '';
11412 if (!empty($companylogo)) {
11413 /**
11414 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
11415 *
11416 * @since March 16th 2021
11417 */
11418 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11419 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
11420 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
11421 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11422 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
11423 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
11424 }
11425 }
11426
11427 $parsed = str_replace("{company_name}", $company_name, $parsed);
11428 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
11429 $parsed = str_replace("{company_info}", $company_info, $parsed);
11430 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
11431 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
11432 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
11433 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
11434
11435 $tot_guests = 0;
11436 $tot_adults = 0;
11437 $tot_children = 0;
11438 foreach ($booking_rooms as $kor => $or) {
11439 $tot_guests += ($or['adults'] + $or['children']);
11440 $tot_adults += $or['adults'];
11441 $tot_children += $or['children'];
11442 }
11443 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
11444 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
11445 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
11446 if ($customer && isset($customer['comments'])) {
11447 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
11448 }
11449
11450 $termsconds = self::getTermsConditions();
11451 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
11452
11453 // custom fields replacemenet
11454 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
11455 if (is_array($cmatches[1]) && $cmatches[1]) {
11456 $cfids = array();
11457 foreach ($cmatches[1] as $cfid) {
11458 $cfids[] = $cfid;
11459 }
11460 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
11461 $dbo->setQuery($q);
11462 $cfields = $dbo->loadAssocList();
11463 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
11464 $cfmap = array();
11465 foreach ($cfields as $cf) {
11466 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
11467 }
11468 $cfmapreplace = array();
11469 $partsreceived = explode("\n", $booking['custdata']);
11470 if (count($partsreceived) > 0) {
11471 foreach ($partsreceived as $pst) {
11472 if (!empty($pst)) {
11473 $tmpdata = explode(":", $pst);
11474 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
11475 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
11476 }
11477 }
11478 }
11479 }
11480 foreach ($cmatches[1] as $cfid) {
11481 if (array_key_exists($cfid, $cfmapreplace)) {
11482 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
11483 } else {
11484 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
11485 }
11486 }
11487 }
11488
11489 /**
11490 * Parse all conditional text rules.
11491 */
11492 self::getConditionalRulesInstance()
11493 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
11494 ->parseTokens($parsed);
11495
11496 /**
11497 * Parse all Door Access Control tags.
11498 *
11499 * @since 1.18.4 (J) - 1.8.4 (WP)
11500 */
11501 VBOFactory::getDoorAccessControl()
11502 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $parsed);
11503
11504 return $parsed;
11505 }
11506
11507 /**
11508 * Returns an array of key-value pairs to be used for building the Guest Details
11509 * in the Check-in process. The keys will be compared to the fields of the table
11510 * #__customers to see if some values already exist. The values can use lang defs
11511 * of both front-end or back-end. To be called as list(fields, attributes).
11512 *
11513 * @param boolean $precheckin true if requested for front-end pre check-in.
11514 * @param string $type optional type of custom pax fields to force.
11515 *
11516 * @return array key-value pairs for showing and collecting guest details.
11517 *
11518 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
11519 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
11520 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
11521 */
11522 public static function getPaxFields($precheckin = false, $type = null)
11523 {
11524 // check the type of pax fields collection data
11525 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
11526
11527 if (!$precheckin) {
11528 // back-end check-in ("registration") key-value pairs
11529
11530 // return the requested pax fields collection list
11531 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
11532
11533 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11534 // requested driver returned a list of fields
11535 return $custom_pax_fields;
11536 }
11537
11538 // in case the driver is unsupported, fallback to "basic" (default) driver
11539 return VBOCheckinPax::getFields('basic');
11540 }
11541
11542 // default front-end key-value pairs for pre check-in
11543 $precheckin_pax_fields = [
11544 [
11545 'first_name' => JText::translate('VBCCFIRSTNAME'),
11546 'last_name' => JText::translate('VBCCLASTNAME'),
11547 'date_birth' => JText::translate('ORDER_DBIRTH'),
11548 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
11549 'country' => JText::translate('ORDER_STATE'),
11550 'city' => JText::translate('ORDER_CITY'),
11551 'zip' => JText::translate('ORDER_ZIP'),
11552 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
11553 'gender' => JText::translate('VBOCUSTGENDER'),
11554 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
11555 'docnum' => JText::translate('VBOCUSTDOCNUM'),
11556 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
11557 ],
11558 [
11559 'first_name' => 'text',
11560 'last_name' => 'text',
11561 'date_birth' => 'calendar',
11562 'place_birth' => 'text',
11563 'country' => 'country',
11564 'city' => 'text',
11565 'zip' => 'text',
11566 'nationality' => 'country',
11567 'gender' => [
11568 'M' => JText::translate('VBOCUSTGENDERM'),
11569 'F' => JText::translate('VBOCUSTGENDERF'),
11570 ],
11571 'doctype' => 'text',
11572 'docnum' => 'text',
11573 'documents' => 'file',
11574 ],
11575 ];
11576
11577 // access the pre-checkin pax fields collection list from the current collector
11578 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
11579
11580 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11581 // set the pax fields for pre-checkin from the given data collection driver
11582 $precheckin_pax_fields = $custom_pax_fields;
11583 }
11584
11585 // make a safe copy of the default precheckin fields
11586 $use_precheckin_pax_fields = $precheckin_pax_fields;
11587
11588 /**
11589 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
11590 *
11591 * @since 1.16.3 (J) - 1.6.3 (WP)
11592 */
11593 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
11594
11595 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
11596 // use the filtered pax fields
11597 $use_precheckin_pax_fields = $precheckin_pax_fields;
11598 }
11599
11600 // return the front-end key-value pairs for pre check-in
11601 return $use_precheckin_pax_fields;
11602 }
11603
11604 /**
11605 * Returns the associative list of countries from the DB.
11606 *
11607 * @param bool $tn whether to translate the country name.
11608 * @param bool $no_id whether to unset the ID of the country.
11609 *
11610 * @return array associative or empty array.
11611 *
11612 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
11613 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
11614 */
11615 public static function getCountriesArray($tn = true, $no_id = true)
11616 {
11617 $dbo = JFactory::getDbo();
11618 $all_countries = [];
11619
11620 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
11621 $dbo->setQuery($q);
11622 $countries = $dbo->loadAssocList();
11623 if (!$countries) {
11624 return [];
11625 }
11626
11627 if ($tn === true) {
11628 $vbo_tn = self::getTranslator();
11629 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
11630 // re-apply sorting by country name
11631 $sorting = [];
11632 foreach ($countries as $country) {
11633 $sorting[$country['country_name']] = $country;
11634 }
11635 ksort($sorting);
11636 $sorted = [];
11637 foreach ($sorting as $country) {
11638 $sorted[] = $country;
11639 }
11640 $countries = $sorted;
11641 unset($sorting, $sorted);
11642 }
11643
11644 foreach ($countries as $v) {
11645 if ($no_id) {
11646 // keep the original structure by unsetting the ID only needed for translation
11647 unset($v['id']);
11648 }
11649 $all_countries[$v['country_3_code']] = $v;
11650 }
11651
11652 return $all_countries;
11653 }
11654
11655 public static function getCountriesSelect(string $name, array $all_countries = [], ?string $current_value = '', ?string $empty_value = ' ', ?string $idAttr = null)
11656 {
11657 if (!$all_countries) {
11658 $all_countries = self::getCountriesArray();
11659 }
11660
11661 $countries = '<select ' . ($idAttr ? 'id="' . $idAttr . '" ' : '') . 'name="' . $name . '">' . "\n";
11662 if (strlen($empty_value)) {
11663 $countries .= '<option value="">' . $empty_value . '</option>' . "\n";
11664 }
11665 foreach ($all_countries as $v) {
11666 $countries .= '<option value="' . $v['country_3_code'] . '"' . ($v['country_3_code'] == $current_value ? ' selected="selected"' : '') . '>' . $v['country_name'] . '</option>' . "\n";
11667 }
11668 $countries .= '</select>';
11669
11670 return $countries;
11671 }
11672
11673 public static function getThumbSize($skipsession = false)
11674 {
11675 static $thumb_size = null;
11676
11677 if ($thumb_size) {
11678 return $thumb_size;
11679 }
11680
11681 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11682
11683 return $thumb_size;
11684 }
11685
11686 /**
11687 * Checks whether an iCal file for the reservation should be
11688 * attached to the confirmation email for customer and/or admin.
11689 *
11690 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11691 *
11692 * @since 1.12.0 (J) - 1.2.0 (WP)
11693 */
11694 public static function attachIcal()
11695 {
11696 return VBOFactory::getConfig()->getInt('attachical', 1);
11697 }
11698
11699 /**
11700 * How the calculation of the orphan dates should take place.
11701 *
11702 * @return string "next" for only checking the bookings ahead, "prevnext" if
11703 * also the previous bookings should be checked.
11704 *
11705 * @since 1.13 (J) - 1.3.0 (WP)
11706 */
11707 public static function orphansCalculation()
11708 {
11709 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11710 }
11711
11712 /**
11713 * Returns the name of the template file to use for the front-end View "search".
11714 *
11715 * @return string the name of the template file. New and upgraded user will both use the classic file.
11716 *
11717 * @since 1.13 (J) - 1.3.0 (WP)
11718 */
11719 public static function searchResultsTmpl()
11720 {
11721 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11722 }
11723
11724 /**
11725 * Returns a string without any new-line characters
11726 * to be used for JavaScript values without facing
11727 * errors like 'unterminated string literal'.
11728 * By passing nl2br($str) as argument, we can keep
11729 * the wanted new-line HTML tags for PRE tags.
11730 * We use implode() with just one argument to
11731 * not use an empty string as glue for the string.
11732 *
11733 * @param $str string
11734 *
11735 * @return string
11736 */
11737 public static function strTrimLiteral($str) {
11738 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11739 $lines = explode("\n", $str);
11740 $new_lines = array();
11741 foreach ($lines as $i => $line) {
11742 if (strlen($line)) {
11743 $new_lines[] = trim($line);
11744 }
11745 }
11746 return implode($new_lines);
11747 }
11748
11749 /**
11750 * Returns an instance of the VboApplication class.
11751 *
11752 * @return VboApplication
11753 */
11754 public static function getVboApplication()
11755 {
11756 // require library
11757 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11758
11759 // return a new instance
11760 return new VboApplication();
11761 }
11762
11763 /**
11764 * Returns the translation of the given 0-indexed week day.
11765 *
11766 * @param int $wd the 0-indexed day of the week.
11767 * @param bool $short whether to get the short version of the weekday.
11768 *
11769 * @return string the name of the week day.
11770 *
11771 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11772 */
11773 public static function sayWeekDay($wd, $short = false)
11774 {
11775 switch ($wd) {
11776 case '6' :
11777 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11778 break;
11779 case '5' :
11780 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11781 break;
11782 case '4' :
11783 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11784 break;
11785 case '3' :
11786 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11787 break;
11788 case '2' :
11789 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11790 break;
11791 case '1' :
11792 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11793 break;
11794 default :
11795 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11796 break;
11797 }
11798 return $ret;
11799 }
11800
11801 /**
11802 * Returns the translation of the given 1-indexed month of the year.
11803 *
11804 * @param int $idm the 1-indexed month of the year.
11805 * @param bool $short whether to get the short version of the month.
11806 *
11807 * @return string the name of the month.
11808 *
11809 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11810 */
11811 public static function sayMonth($idm, $short = false)
11812 {
11813 switch ($idm) {
11814 case '12' :
11815 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11816 break;
11817 case '11' :
11818 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11819 break;
11820 case '10' :
11821 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11822 break;
11823 case '9' :
11824 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11825 break;
11826 case '8' :
11827 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11828 break;
11829 case '7' :
11830 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11831 break;
11832 case '6' :
11833 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11834 break;
11835 case '5' :
11836 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11837 break;
11838 case '4' :
11839 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11840 break;
11841 case '3' :
11842 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11843 break;
11844 case '2' :
11845 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11846 break;
11847 default :
11848 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11849 break;
11850 }
11851 return $ret;
11852 }
11853
11854 public static function sayDayMonth($d) {
11855 switch ($d) {
11856 case '31' :
11857 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11858 break;
11859 case '30' :
11860 $ret = JText::translate('VBDAYMONTHTHIRTY');
11861 break;
11862 case '29' :
11863 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11864 break;
11865 case '28' :
11866 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11867 break;
11868 case '27' :
11869 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11870 break;
11871 case '26' :
11872 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11873 break;
11874 case '25' :
11875 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11876 break;
11877 case '24' :
11878 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11879 break;
11880 case '23' :
11881 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11882 break;
11883 case '22' :
11884 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11885 break;
11886 case '21' :
11887 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11888 break;
11889 case '20' :
11890 $ret = JText::translate('VBDAYMONTHTWENTY');
11891 break;
11892 case '19' :
11893 $ret = JText::translate('VBDAYMONTHNINETEEN');
11894 break;
11895 case '18' :
11896 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11897 break;
11898 case '17' :
11899 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11900 break;
11901 case '16' :
11902 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11903 break;
11904 case '15' :
11905 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11906 break;
11907 case '14' :
11908 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11909 break;
11910 case '13' :
11911 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11912 break;
11913 case '12' :
11914 $ret = JText::translate('VBDAYMONTHTWELVE');
11915 break;
11916 case '11' :
11917 $ret = JText::translate('VBDAYMONTHELEVEN');
11918 break;
11919 case '10' :
11920 $ret = JText::translate('VBDAYMONTHTEN');
11921 break;
11922 case '9' :
11923 $ret = JText::translate('VBDAYMONTHNINE');
11924 break;
11925 case '8' :
11926 $ret = JText::translate('VBDAYMONTHEIGHT');
11927 break;
11928 case '7' :
11929 $ret = JText::translate('VBDAYMONTHSEVEN');
11930 break;
11931 case '6' :
11932 $ret = JText::translate('VBDAYMONTHSIX');
11933 break;
11934 case '5' :
11935 $ret = JText::translate('VBDAYMONTHFIVE');
11936 break;
11937 case '4' :
11938 $ret = JText::translate('VBDAYMONTHFOUR');
11939 break;
11940 case '3' :
11941 $ret = JText::translate('VBDAYMONTHTHREE');
11942 break;
11943 case '2' :
11944 $ret = JText::translate('VBDAYMONTHTWO');
11945 break;
11946 default :
11947 $ret = JText::translate('VBDAYMONTHONE');
11948 break;
11949 }
11950 return $ret;
11951 }
11952
11953 public static function totElements($arr) {
11954 $n = 0;
11955 if (is_array($arr)) {
11956 foreach ($arr as $a) {
11957 if (!empty($a)) {
11958 $n++;
11959 }
11960 }
11961 return $n;
11962 }
11963 return false;
11964 }
11965
11966 /**
11967 * Returns a list of documents that were uploaded
11968 * for the specified customer.
11969 *
11970 * @param integer $id The customer ID.
11971 *
11972 * @return array A list of documents.
11973 *
11974 * @since 1.3.0
11975 */
11976 public static function getCustomerDocuments($id)
11977 {
11978 $dbo = JFactory::getDbo();
11979
11980 $q = $dbo->getQuery(true)
11981 ->select($dbo->qn('docsfolder'))
11982 ->from($dbo->qn('#__vikbooking_customers'))
11983 ->where($dbo->qn('id') . ' = ' . (int) $id);
11984
11985 $dbo->setQuery($q, 0, 1);
11986 $dbo->execute();
11987
11988 if (!$dbo->getNumRows())
11989 {
11990 // customer not found
11991 return array();
11992 }
11993
11994 // retrieve customer documents directory name
11995 $dirname = $dbo->loadResult();
11996
11997 if (empty($dirname))
11998 {
11999 // no available directory
12000 return array();
12001 }
12002
12003 // build documents folder path
12004 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
12005
12006 if (!is_dir($dirname))
12007 {
12008 // the customer directory doesn't exist
12009 return array();
12010 }
12011
12012 // read all files from customer directory
12013 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
12014
12015 $files = array();
12016
12017 foreach ($glob as $path)
12018 {
12019 // skip "index.html"
12020 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
12021 {
12022 // extract name and extension from file path
12023 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
12024 {
12025 $name = $match[1];
12026 $ext = $match[2];
12027 }
12028 else
12029 {
12030 $name = basename($path);
12031 $ext = '';
12032 }
12033
12034 $file = new stdClass;
12035 $file->path = $path;
12036 $file->name = $name;
12037 $file->ext = $ext;
12038 $file->basename = $file->name . '.' . $file->ext;
12039 $file->size = filesize($path);
12040 $file->date = filemtime($path);
12041 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
12042
12043 $files[] = $file;
12044 }
12045 }
12046
12047 // sort files by creation date
12048 usort($files, function($a, $b)
12049 {
12050 return $b->date - $a->date;
12051 });
12052
12053 return $files;
12054 }
12055
12056 public static function displayPaymentParameters($pfile, $pparams = '') {
12057 $html = '<p>---------</p>';
12058
12059 /**
12060 * @wponly The payment gateway is now loaded
12061 * using the apposite dispatcher.
12062 *
12063 * @since 1.0.5
12064 */
12065 JLoader::import('adapter.payment.dispatcher');
12066
12067 try
12068 {
12069 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
12070 }
12071 catch (Exception $e)
12072 {
12073 // payment not found
12074 $html = $e->getMessage();
12075
12076 if ($code = $e->getCode())
12077 {
12078 $html = '<b>' . $code . '</b> : ' . $html;
12079 }
12080
12081 return $html;
12082 }
12083 //
12084
12085 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
12086 $arrparams = !is_array($arrparams) ? array() : $arrparams;
12087
12088 // get admin parameters
12089 $pconfig = $payment->getAdminParameters();
12090
12091 if (count($pconfig) > 0) {
12092 $html = '';
12093 foreach ($pconfig as $value => $cont) {
12094 if (empty($value)) {
12095 continue;
12096 }
12097 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12098 $label = $labelparts[0];
12099 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12100 if (!empty($cont['help'])) {
12101 $labelhelp = $cont['help'];
12102 }
12103 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12104 $html .= '<div class="vbo-param-container">';
12105 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12106 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12107 }
12108 $html .= '<div class="vbo-param-setting">';
12109 switch ($cont['type']) {
12110 case 'custom':
12111 $html .= $cont['html'];
12112 break;
12113 case 'select':
12114 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12115 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12116 if (isset($cont['multiple']) && $cont['multiple']) {
12117 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
12118 } else {
12119 $html .= '<select name="vikpaymentparams['.$value.']">';
12120 }
12121 foreach ($options as $optkey => $poption) {
12122 $checkval = $is_assoc ? $optkey : $poption;
12123 $selected = false;
12124 if (isset($arrparams[$value])) {
12125 if (is_array($arrparams[$value])) {
12126 $selected = in_array($checkval, $arrparams[$value]);
12127 } else {
12128 $selected = ($checkval == $arrparams[$value]);
12129 }
12130 } elseif (isset($default_paramv)) {
12131 if (is_array($default_paramv)) {
12132 $selected = in_array($checkval, $default_paramv);
12133 } else {
12134 $selected = ($checkval == $default_paramv);
12135 }
12136 }
12137 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12138 }
12139 $html .= '</select>';
12140 break;
12141 case 'password':
12142 $html .= '<div class="btn-wrapper input-append">';
12143 $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"/>';
12144 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12145 $html .= '</div>';
12146 break;
12147 case 'number':
12148 $number_attr = array();
12149 if (isset($cont['min'])) {
12150 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12151 }
12152 if (isset($cont['max'])) {
12153 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12154 }
12155 if (isset($cont['step'])) {
12156 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12157 }
12158 $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) . '/>';
12159 break;
12160 case 'textarea':
12161 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12162 break;
12163 case 'hidden':
12164 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
12165 break;
12166 case 'checkbox':
12167 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12168 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
12169 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
12170 break;
12171 default:
12172 $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"/>';
12173 break;
12174 }
12175 if (strlen($labelhelp) > 0) {
12176 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12177 }
12178 $html .= '</div>';
12179 $html .= '</div>';
12180 }
12181 // JS helper function to toggle the password fields
12182 $html .= "\n" . '<script>' . "\n";
12183 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
12184 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12185 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12186 $html .= ' var inp_type = inp.attr("type");' . "\n";
12187 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12188 $html .= '}' . "\n";
12189 $html .= "\n" . '</script>' . "\n";
12190 }
12191
12192 return $html;
12193 }
12194
12195 public static function displaySMSParameters($pfile, $params = null)
12196 {
12197 $html = '---------';
12198
12199 $params = is_string($params) ? json_decode($params, true) : $params;
12200 $params = is_array($params) ? $params : [];
12201
12202 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
12203 return $html;
12204 }
12205
12206 // attempt to load the class file
12207 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
12208
12209 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
12210 return $html;
12211 }
12212
12213 // load the gateway parameters
12214 $config = VikSmsApi::getAdminParameters();
12215
12216 if (!is_array($config) || !count($config)) {
12217 return $html;
12218 }
12219
12220 // flags for JS helpers
12221 $js_helpers = array();
12222
12223 $html = '';
12224 foreach ($config as $value => $cont) {
12225 if (empty($value)) {
12226 continue;
12227 }
12228 $inp_attr = '';
12229 if (isset($cont['attributes'])) {
12230 foreach ($cont['attributes'] as $inpk => $inpv) {
12231 $inp_attr .= $inpk.'="'.$inpv.'" ';
12232 }
12233 $inp_attr = ' ' . rtrim($inp_attr);
12234 }
12235 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12236 $label = $labelparts[0];
12237 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12238 if (!empty($cont['help'])) {
12239 $labelhelp = $cont['help'];
12240 }
12241 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12242 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
12243 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12244 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12245 }
12246 $html .= '<div class="vbo-param-setting">';
12247 switch ($cont['type']) {
12248 case 'custom':
12249 $html .= $cont['html'];
12250 break;
12251 case 'select':
12252 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12253 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12254 if (isset($cont['multiple']) && $cont['multiple']) {
12255 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
12256 } else {
12257 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
12258 }
12259 foreach ($options as $optkey => $poption) {
12260 $checkval = $is_assoc ? $optkey : $poption;
12261 $selected = false;
12262 if (isset($params[$value])) {
12263 if (is_array($params[$value])) {
12264 $selected = in_array($checkval, $params[$value]);
12265 } else {
12266 $selected = ($checkval == $params[$value]);
12267 }
12268 } elseif (isset($default_paramv)) {
12269 if (is_array($default_paramv)) {
12270 $selected = in_array($checkval, $default_paramv);
12271 } else {
12272 $selected = ($checkval == $default_paramv);
12273 }
12274 }
12275 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12276 }
12277 $html .= '</select>';
12278 break;
12279 case 'password':
12280 $html .= '<div class="btn-wrapper input-append">';
12281 $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 . '/>';
12282 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12283 $html .= '</div>';
12284 // set flag for JS helper
12285 $js_helpers[] = $cont['type'];
12286 break;
12287 case 'number':
12288 $number_attr = array();
12289 if (isset($cont['min'])) {
12290 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12291 }
12292 if (isset($cont['max'])) {
12293 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12294 }
12295 if (isset($cont['step'])) {
12296 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12297 }
12298 $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 . '/>';
12299 break;
12300 case 'textarea':
12301 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12302 break;
12303 case 'visual_html':
12304 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
12305 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
12306 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
12307 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
12308 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
12309 break;
12310 case 'hidden':
12311 $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 . '/>';
12312 break;
12313 case 'checkbox':
12314 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12315 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
12316 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
12317 break;
12318 default:
12319 $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 . '/>';
12320 break;
12321 }
12322 if (strlen($labelhelp) > 0) {
12323 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12324 }
12325 $html .= '</div>';
12326 $html .= '</div>';
12327 }
12328
12329 // JS helper functions
12330 if (in_array('password', $js_helpers)) {
12331 // toggle the password fields
12332 $html .= "\n" . '<script>' . "\n";
12333 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
12334 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12335 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12336 $html .= ' var inp_type = inp.attr("type");' . "\n";
12337 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12338 $html .= '}' . "\n";
12339 $html .= "\n" . '</script>' . "\n";
12340 }
12341
12342 return $html;
12343 }
12344
12345 /**
12346 * Renders the params of the requested cron job file-class.
12347 *
12348 * @param string $file the name of the cron job driver file.
12349 * @param array $params the parameters array.
12350 *
12351 * @return string the necessary HTML content to render.
12352 *
12353 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
12354 */
12355 public static function displayCronParameters($file, $params = [])
12356 {
12357 try
12358 {
12359 // attempt to create a new instance
12360 $job = VBOFactory::getCronFactory()->createInstance($file);
12361 }
12362 catch (Exception $e)
12363 {
12364 // something went wrong, display error message
12365 return '<p>' . $e->getMessage() . '<p>';
12366 }
12367
12368 // get admin parameters
12369 $config = $job->getForm();
12370
12371 if (!is_array($config) || !$config) {
12372 return '<p>---------</p>';
12373 }
12374
12375 /**
12376 * Render the cron parameters through the global params rendering class.
12377 *
12378 * @since 1.16.9 (J) - 1.6.9 (WP)
12379 */
12380 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
12381 }
12382
12383 public static function invokeChannelManager($skiporder = true, $order = array()) {
12384 $task = VikRequest::getString('task', '', 'request');
12385 $view = VikRequest::getString('view', '', 'request');
12386 $tmpl = VikRequest::getString('tmpl', '', 'request');
12387 $noimpression = array('vieworder', 'booking');
12388 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')) {
12389 //VCM Channel Impression
12390 if (!class_exists('VikChannelManagerConfig')) {
12391 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12392 }
12393 if (!class_exists('VikChannelManager')) {
12394 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12395 }
12396 VikChannelManager::invokeChannelImpression();
12397 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
12398 //VCM Channel Conversion-Impression
12399 if (!class_exists('VikChannelManagerConfig')) {
12400 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12401 }
12402 if (!class_exists('VikChannelManager')) {
12403 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12404 }
12405 VikChannelManager::invokeChannelConversionImpression($order);
12406 }
12407 }
12408
12409 public static function validEmail($email) {
12410 $isValid = true;
12411 $atIndex = strrpos($email, "@");
12412 if (is_bool($atIndex) && !$atIndex) {
12413 $isValid = false;
12414 } else {
12415 $domain = substr($email, $atIndex +1);
12416 $local = substr($email, 0, $atIndex);
12417 $localLen = strlen($local);
12418 $domainLen = strlen($domain);
12419 if ($localLen < 1 || $localLen > 64) {
12420 // local part length exceeded
12421 $isValid = false;
12422 } else
12423 if ($domainLen < 1 || $domainLen > 255) {
12424 // domain part length exceeded
12425 $isValid = false;
12426 } else
12427 if ($local[0] == '.' || $local[$localLen -1] == '.') {
12428 // local part starts or ends with '.'
12429 $isValid = false;
12430 } else
12431 if (preg_match('/\\.\\./', $local)) {
12432 // local part has two consecutive dots
12433 $isValid = false;
12434 } else
12435 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
12436 // character not valid in domain part
12437 $isValid = false;
12438 } else
12439 if (preg_match('/\\.\\./', $domain)) {
12440 // domain part has two consecutive dots
12441 $isValid = false;
12442 } else
12443 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
12444 // character not valid in local part unless
12445 // local part is quoted
12446 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
12447 $isValid = false;
12448 }
12449 }
12450 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
12451 // domain not found in DNS
12452 $isValid = false;
12453 }
12454 }
12455 return $isValid;
12456 }
12457
12458 public static function caniWrite($path) {
12459 if ($path[strlen($path) - 1] == '/') {
12460 // ricorsivo return a temporary file path
12461 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
12462 }
12463 if (is_dir($path)) {
12464 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
12465 }
12466 // check tmp file for read/write capabilities
12467 $rm = file_exists($path);
12468 $f = @fopen($path, 'a');
12469 if ($f === false) {
12470 return false;
12471 }
12472 fclose($f);
12473 if (!$rm) {
12474 unlink($path);
12475 }
12476 return true;
12477 }
12478
12479 /**
12480 * Alias method of JFile::upload to unify any
12481 * upload function into one.
12482 *
12483 * @param string $src The name of the php (temporary) uploaded file.
12484 * @param string $dest The path (including filename) to move the uploaded file to.
12485 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
12486 *
12487 * @return boolean True on success.
12488 *
12489 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
12490 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
12491 */
12492 public static function uploadFile($src, $dest, $copy_only = false) {
12493 // always attempt to include the File class
12494 jimport('joomla.filesystem.file');
12495
12496 // upload the file
12497 if (!$copy_only) {
12498 $result = JFile::upload($src, $dest);
12499 } else {
12500 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
12501 $result = JFile::copy($src, $dest);
12502 }
12503
12504 /**
12505 * @wponly in order to not lose uploaded files after installing an update,
12506 * we need to move any uploaded file onto a recovery folder.
12507 */
12508 if ($result) {
12509 VikBookingLoader::import('update.manager');
12510 VikBookingUpdateManager::triggerUploadBackup($dest);
12511 }
12512 //
12513
12514 // return upload result
12515 return $result;
12516 }
12517
12518 /**
12519 * Helper method used to upload the given file (retrieved from $_FILES)
12520 * into the specified destination.
12521 *
12522 * @param array $file An associative array with the file details.
12523 * @param string $dest The destination path.
12524 * @param string $filters A string (or a regex) containing the allowed extensions.
12525 *
12526 * @return array The uploading result.
12527 *
12528 * @throws RuntimeException
12529 *
12530 * @since 1.3.0
12531 */
12532 public static function uploadFileFromRequest($file, $dest, $filters = '*')
12533 {
12534 jimport('joomla.filesystem.file');
12535
12536 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
12537
12538 if (empty($file['name']))
12539 {
12540 throw new RuntimeException('Missing file', 400);
12541 }
12542
12543 /**
12544 * Make sure the upload of the file didn't raise any errors.
12545 *
12546 * @link https://www.php.net/manual/en/features.file-upload.errors.php
12547 */
12548 if ((int) $file['error'])
12549 {
12550 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
12551 {
12552 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
12553 }
12554 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
12555 {
12556 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
12557 }
12558 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
12559 {
12560 $error = 'The uploaded file was only partially uploaded.';
12561 }
12562 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
12563 {
12564 $error = 'No file was uploaded.';
12565 }
12566 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
12567 {
12568 $error = 'Missing a temporary folder.';
12569 }
12570 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
12571 {
12572 $error = 'Failed to write file to disk.';
12573 }
12574 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
12575 {
12576 $error = 'A PHP extension stopped the file upload.';
12577 }
12578 else
12579 {
12580 $error = 'Unknown error.';
12581 }
12582
12583 throw new RuntimeException($error, 500);
12584 }
12585
12586 $src = $file['tmp_name'];
12587
12588 // extract file name and extension
12589 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
12590 {
12591 $filename = $match[1];
12592 $fileext = $match[2];
12593 }
12594 else
12595 {
12596 // probably no extension provided
12597 $filename = basename($file['name']);
12598 $fileext = '';
12599 }
12600
12601 $j = '';
12602
12603 if (file_exists($dest . $filename . $fileext))
12604 {
12605 $j = 2;
12606
12607 while (file_exists($dest . $filename . '-' . $j . $fileext))
12608 {
12609 $j++;
12610 }
12611
12612 $j = '-' . $j;
12613 }
12614
12615 $finaldest = $dest . $filename . $j . $fileext;
12616
12617 // make sure the file extension is supported
12618 if (!self::isFileTypeCompatible(basename($finaldest), $filters))
12619 {
12620 // extension not supported
12621 throw new RuntimeException(sprintf('Extension [%s] is not supported', $fileext), 400);
12622 }
12623
12624 // try to upload the file
12625 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
12626 {
12627 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
12628 }
12629
12630 $file = new stdClass;
12631 $file->name = $filename . $j;
12632 $file->ext = ltrim($fileext, '.');
12633 $file->filename = basename($finaldest);
12634 $file->path = $finaldest;
12635
12636 return $file;
12637 }
12638
12639 /**
12640 * Helper method used to check whether the given file name
12641 * supports one of the given filters.
12642 *
12643 * @param mixed $file Either the file name or the uploaded file.
12644 * @param string $filters Either a regex or a comma-separated list of supported extensions.
12645 * The regex must be inclusive of delimiters.
12646 *
12647 * @return bool True if supported, false otherwise.
12648 *
12649 * @since 1.7.3 (WP) - 1.17.3 (J)
12650 */
12651 public static function isFileTypeCompatible($file, $filters)
12652 {
12653 // make sure the filters query is not empty
12654 if (strlen($filters) == 0)
12655 {
12656 // cannot assert whether the file could be accepted or not
12657 return false;
12658 }
12659
12660 // check whether all the files are accepted
12661 if ($filters == '*')
12662 {
12663 return true;
12664 }
12665
12666 // use the file MIME TYPE in case of array
12667 if (is_array($file))
12668 {
12669 $file = $file['type'];
12670 }
12671
12672 // check if we are dealing with a regex
12673 if (static::isRegex($filters))
12674 {
12675 return (bool) preg_match($filters, $file);
12676 }
12677
12678 // fallback to comma-separated list
12679 $types = array_filter(preg_split("/\s*,\s*/", $filters));
12680
12681 foreach ($types as $t)
12682 {
12683 // remove initial dot if specified
12684 $t = ltrim($t, '.');
12685 // escape slashes to avoid breaking the regex
12686 $t = preg_replace("/\//", '\/', $t);
12687
12688 // check if the file ends with the given extension
12689 if (preg_match("/{$t}$/i", $file))
12690 {
12691 return true;
12692 }
12693 }
12694
12695 return false;
12696 }
12697
12698 /**
12699 * Checks whether the given string is a structured PCRE regex.
12700 * It simply makes sure that the string owns valid delimiters.
12701 * A delimiter can be any non-alphanumeric, non-backslash,
12702 * non-whitespace character.
12703 *
12704 * @param string $str The string to check.
12705 *
12706 * @return boolean True if a regex, false otherwise.
12707 *
12708 * @since 1.7.3 (WP) - 1.17.3 (J)
12709 */
12710 public static function isRegex($str)
12711 {
12712 // first of all make sure the first character is a supported delimiter
12713 if (!preg_match("/^([!#$%&'*+,.\/:;=?@^_`|~\-(\[{<\"])/", $str, $match))
12714 {
12715 // no valid delimiter
12716 return false;
12717 }
12718
12719 // get delimiter
12720 $d = $match[1];
12721
12722 // lookup used to check if we should take a different ending delimiter
12723 $lookup = array(
12724 '{' => '}',
12725 '[' => ']',
12726 '(' => ')',
12727 '<' => '>',
12728 );
12729
12730 if (isset($lookup[$d]))
12731 {
12732 $d = $lookup[$d];
12733 }
12734
12735 // make sure the regex ends with the delimiter found
12736 return (bool) preg_match("/\\{$d}[gimsxU]*$/", $str);
12737 }
12738
12739 /**
12740 * Gets the instance of a specific report class.
12741 *
12742 * @param string $report the name of the report to load.
12743 *
12744 * @return mixed false or report object instance.
12745 *
12746 * @since 1.3.0
12747 */
12748 public static function getReportInstance($report)
12749 {
12750 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12751
12752 return VikBookingReport::getInstanceOf($report);
12753 }
12754
12755 /**
12756 * Checks whether the guest reviews are enabled and VCM is installed.
12757 *
12758 * @return boolean true if enabled, false otherwise.
12759 *
12760 * @since 1.3.0
12761 */
12762 public static function allowGuestReviews()
12763 {
12764 $dbo = JFactory::getDbo();
12765 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12766 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12767 $dbo->setQuery($q);
12768 $dbo->execute();
12769 if ($dbo->getNumRows()) {
12770 $s = $dbo->loadResult();
12771 return ((int)$s === 1 && $vcm_installed);
12772 }
12773 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12774 $dbo->setQuery($q);
12775 $dbo->execute();
12776 return $vcm_installed;
12777 }
12778
12779 /**
12780 * Gets the minimum chars for the guest review message.
12781 *
12782 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12783 *
12784 * @since 1.3.0
12785 */
12786 public static function guestReviewMinChars()
12787 {
12788 $dbo = JFactory::getDbo();
12789 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12790 $dbo->setQuery($q);
12791 $dbo->execute();
12792 if ($dbo->getNumRows() > 0) {
12793 return (int)$dbo->loadResult();
12794 }
12795 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12796 $dbo->setQuery($q);
12797 $dbo->execute();
12798 return 15;
12799 }
12800
12801 /**
12802 * The approval type for new guest reviews.
12803 *
12804 * @return string auto or manual.
12805 *
12806 * @since 1.3.0
12807 */
12808 public static function guestReviewsApproval()
12809 {
12810 $dbo = JFactory::getDbo();
12811 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12812 $dbo->setQuery($q);
12813 $dbo->execute();
12814 if ($dbo->getNumRows() > 0) {
12815 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12816 }
12817 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12818 $dbo->setQuery($q);
12819 $dbo->execute();
12820 return 'auto';
12821 }
12822
12823 /**
12824 * The type of reviews guests should leave.
12825 *
12826 * @return string global or service.
12827 *
12828 * @since 1.3.0
12829 */
12830 public static function guestReviewsType()
12831 {
12832 $dbo = JFactory::getDbo();
12833 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12834 $dbo->setQuery($q);
12835 $dbo->execute();
12836 if ($dbo->getNumRows() > 0) {
12837 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12838 }
12839 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12840 $dbo->setQuery($q);
12841 $dbo->execute();
12842 return 'service';
12843 }
12844
12845 /**
12846 * The services to be reviewed by the guests.
12847 *
12848 * @return array list, empty or not, of the services to be reviewed.
12849 *
12850 * @since 1.3.0
12851 */
12852 public static function guestReviewsServices()
12853 {
12854 $dbo = JFactory::getDbo();
12855 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12856 $dbo->setQuery($q);
12857 $dbo->execute();
12858 if ($dbo->getNumRows()) {
12859 return $dbo->loadAssocList();
12860 }
12861 // insert the default services
12862 $default_services = array(
12863 JText::translate('VBOGREVVALUE'),
12864 JText::translate('VBOGREVLOCATION'),
12865 JText::translate('VBOGREVSTAFF'),
12866 JText::translate('VBOGREVCLEAN'),
12867 JText::translate('VBOGREVCOMFORT'),
12868 JText::translate('VBOGREVFACILITIES'),
12869 );
12870 foreach ($default_services as $def_service) {
12871 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12872 $dbo->setQuery($q);
12873 $dbo->execute();
12874 }
12875 return array();
12876 }
12877
12878 /**
12879 * Checks whether the guest reviews should be downloaded (max 1 per day).
12880 * VCM must be installed in order to download the new reviews.
12881 *
12882 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12883 *
12884 * @since 1.3.0
12885 */
12886 public static function shouldDownloadReviews()
12887 {
12888 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12889 return -1;
12890 }
12891
12892 $today = date('Y-m-d');
12893
12894 $dbo = JFactory::getDbo();
12895 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12896 $dbo->setQuery($q);
12897 $dbo->execute();
12898 if ($dbo->getNumRows()) {
12899 $last_download = $dbo->loadResult();
12900 // update last download day
12901 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12902 $dbo->setQuery($q);
12903 $dbo->execute();
12904 //
12905 return ($last_download != $today ? 1 : 0);
12906 }
12907 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12908 $dbo->setQuery($q);
12909 $dbo->execute();
12910 return 1;
12911 }
12912
12913 /**
12914 * Tells whether a booking can be reviewed.
12915 *
12916 * @param array $booking booking array details.
12917 *
12918 * @return boolean true if a review can be left, false otherwise.
12919 *
12920 * @since 1.3.0
12921 */
12922 public static function canBookingBeReviewed($booking)
12923 {
12924 // reviews must be enabled and supported
12925 if (!self::allowGuestReviews()) {
12926 return false;
12927 }
12928
12929 // booking status must be confirmed
12930 if ($booking['status'] != 'confirmed') {
12931 return false;
12932 }
12933
12934 // review can be left starting from the check-out day
12935 $checkout_info = getdate($booking['checkout']);
12936 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12937 if (time() < $checkout_midn) {
12938 return false;
12939 }
12940
12941 // make sure a review for this booking does not exist
12942 $noreview = true;
12943 $dbo = JFactory::getDbo();
12944 try {
12945 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12946 $dbo->setQuery($q);
12947 $dbo->execute();
12948 $noreview = ($dbo->getNumRows() < 1);
12949 } catch (Exception $e) {
12950 // if the query fails, we do not allow the review to be left
12951 $noreview = false;
12952 }
12953
12954 return $noreview;
12955 }
12956
12957 /**
12958 * Gets the review for a booking, no matter if it's published or not.
12959 * Review will be returned only if reviews are enabled and supported.
12960 * Only reviews left through the website, no OTA reviews as this method
12961 * should be used in the front-end to display the review to the guest.
12962 *
12963 * @param array $booking booking array details.
12964 *
12965 * @return mixed associative array or false.
12966 *
12967 * @since 1.3.0
12968 */
12969 public static function getBookingReview($booking)
12970 {
12971 // reviews must be enabled and supported
12972 if (!self::allowGuestReviews()) {
12973 return false;
12974 }
12975
12976 if (empty($booking['id'])) {
12977 return false;
12978 }
12979
12980 // make sure a review for this booking exists
12981 $dbo = JFactory::getDbo();
12982 $review = array();
12983 try {
12984 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12985 $dbo->setQuery($q);
12986 $dbo->execute();
12987 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12988 } catch (Exception $e) {
12989 // query has failed
12990 $review = array();
12991 }
12992
12993 if (count($review)) {
12994 // make sure the review was left from the website
12995 $review['uniquekey'] = (int)$review['uniquekey'];
12996 }
12997
12998 if (count($review) && !empty($review['uniquekey'])) {
12999 // this is an OTA review
13000 return false;
13001 }
13002
13003 if (count($review) && !empty($review['content'])) {
13004 // decode content
13005 $review['content'] = json_decode($review['content'], true);
13006 }
13007
13008 return count($review) ? $review : false;
13009 }
13010
13011 /**
13012 * Attempts to find a translation for a raw customer data label.
13013 * Bookings downloaded from the OTAs will save a raw string of information
13014 * composed of pairs of label-value separated by new line feeds.
13015 * We try to translate the given label into the current language.
13016 *
13017 * @param string $label the raw string label to translate.
13018 *
13019 * @return string either the original or the translated label.
13020 *
13021 * @since 1.3.5
13022 * @since 1.4.0 back-end support added.
13023 */
13024 public static function tnCustomerRawDataLabel($label)
13025 {
13026 // this is a map of the known labels
13027 $known_lbls = array(
13028 'NAME' => 'VBOCUSTOMERNOMINATIVE',
13029 'LASTNAME' => 'ORDER_LNAME',
13030 'COUNTRY' => 'ORDER_STATE',
13031 'EMAIL' => 'VBMAIL',
13032 'TELEPHONE' => 'VBPHONE',
13033 'PHONE' => 'VBPHONE',
13034 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13035 'MEAL_PLAN' => 'VBOMEALPLAN',
13036 'CITY' => 'VBCITY',
13037 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
13038 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
13039 );
13040
13041 if (self::isAdmin()) {
13042 // override the map of known translation strings
13043 $known_lbls = array(
13044 'NAME' => 'ORDER_NAME',
13045 'LASTNAME' => 'ORDER_LNAME',
13046 'COUNTRY' => 'ORDER_STATE',
13047 'ADDRESS' => 'ORDER_ADDRESS',
13048 'CITY' => 'ORDER_CITY',
13049 'LOCATION' => 'ORDER_CITY',
13050 'EMAIL' => 'ORDER_EMAIL',
13051 'TELEPHONE' => 'ORDER_PHONE',
13052 'PHONE' => 'ORDER_PHONE',
13053 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13054 );
13055 }
13056
13057 // we get rid of any empty space by keeping the underscores
13058 $converted = str_replace(' ', '', strtoupper($label));
13059
13060 if (isset($known_lbls[$converted])) {
13061 // this language definition has been mapped
13062 return JText::translate($known_lbls[$converted]);
13063 }
13064
13065 // we try to guess the translation string by prepending VBO
13066 $guessed = JText::translate('VBO' . $converted);
13067 if ($guessed != 'VBO' . $converted) {
13068 // the label was translated correctly
13069 return $guessed;
13070 }
13071
13072 // this label could not be translated, so we return the plain string
13073 return $label;
13074 }
13075
13076 /**
13077 * We check whether some bookings are available for import from third party plugins.
13078 *
13079 * @return mixed array with list of plugins supported, false otherwise.
13080 *
13081 * @wponly this method is only useful for WordPress.
13082 *
13083 * @since 1.3.5
13084 */
13085 public static function canImportBookingsFromThirdPartyPlugins()
13086 {
13087 $dbo = JFactory::getDbo();
13088
13089 $plugins = array();
13090
13091 /**
13092 * As requested from hundreds of our clients, for the moment we check only the custom
13093 * post types of type "mphb_booking" to see if some bookings are available for import.
13094 */
13095 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
13096 $dbo->setQuery($q);
13097 $dbo->execute();
13098
13099 if (!$dbo->getNumRows()) {
13100 return false;
13101 }
13102
13103 // push this third party plugin
13104 $plugins['mphb'] = 'MotoPress Hotel Booking';
13105
13106 return count($plugins) ? $plugins : false;
13107 }
13108
13109 /**
13110 * This method returns a list of the known languages sorted by the
13111 * administrator custom preferences. Useful for the phone input fields.
13112 *
13113 * @param boolean $code_assoc whether to get an associative array with the lang name.
13114 *
13115 * @return array the sorted list of preferred countries.
13116 *
13117 * @since 1.3.11
13118 */
13119 public static function preferredCountriesOrdering($code_assoc = false)
13120 {
13121 $preferred_countries = array();
13122
13123 // try to get the preferred countries from db
13124 $dbo = JFactory::getDbo();
13125 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
13126 $dbo->setQuery($q);
13127 $setting = $dbo->loadResult();
13128 if (!$setting) {
13129 // create empty configuration record
13130 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
13131 $dbo->setQuery($q);
13132 $dbo->execute();
13133 } else {
13134 $preferred_countries = json_decode($setting);
13135 }
13136
13137 // get the default known languages
13138 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
13139
13140 if (!is_array($preferred_countries) || !count($preferred_countries)) {
13141 // sort the default known languages by country code alphabetically
13142 ksort($sorted_known_langs);
13143 foreach ($sorted_known_langs as $k => $v) {
13144 $langsep = strpos($k, '_') !== false ? '_' : '-';
13145 $langparts = explode($langsep, $k);
13146 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
13147 }
13148 // update the database record
13149 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
13150 $dbo->setQuery($q);
13151 $dbo->execute();
13152 }
13153
13154 if ($code_assoc) {
13155 // this is useful for displaying the preferred countries codes together with the language name
13156 $map = array();
13157 foreach ($preferred_countries as $ccode) {
13158 // look for the current country code in the keys of the known language tags
13159 $match_found = false;
13160 foreach ($sorted_known_langs as $langtag => $langinfo) {
13161 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
13162 $langparts = explode($langsep, $langtag);
13163 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
13164 // match found
13165 $match_found = true;
13166 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13167 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
13168 // match found
13169 $match_found = true;
13170 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13171 }
13172 }
13173 if (!$match_found) {
13174 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
13175 $map[$ccode] = strtoupper($ccode);
13176 }
13177 }
13178 if (count($map)) {
13179 // set the associatve array to be returned
13180 $preferred_countries = $map;
13181 }
13182 }
13183
13184 return $preferred_countries;
13185 }
13186
13187 /**
13188 * Gets the instance of the admin widgets helper class.
13189 *
13190 * @return VikBookingHelperAdminWidgets
13191 *
13192 * @since 1.4.0
13193 */
13194 public static function getAdminWidgetsInstance()
13195 {
13196 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
13197
13198 return VikBookingHelperAdminWidgets::getInstance();
13199 }
13200
13201 /**
13202 * Gets the instance of the conditional rules helper class.
13203 *
13204 * @param bool $require_only whether to return the object.
13205 *
13206 * @return mixed VikBookingHelperConditionalRules or true.
13207 *
13208 * @since 1.4.0
13209 */
13210 public static function getConditionalRulesInstance($require_only = false)
13211 {
13212 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
13213
13214 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
13215 }
13216
13217 /**
13218 * Gets the instance of the geocoding helper class.
13219 *
13220 * @param bool $require_only whether to return the object.
13221 *
13222 * @return mixed VikBookingHelperGeocoding or true.
13223 *
13224 * @since 1.4.0
13225 */
13226 public static function getGeocodingInstance($require_only = false)
13227 {
13228 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
13229
13230 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
13231 }
13232
13233 /**
13234 * Helper method to cope with the removal of the same method
13235 * in the JApplication class introduced with Joomla 4. Using
13236 * isClient() would break the compatibility with J < 3.7 so
13237 * we can rely on this helper method to avoid Fatal Errors.
13238 *
13239 * @return boolean
13240 *
13241 * @since October 2020
13242 */
13243 public static function isAdmin()
13244 {
13245 $app = JFactory::getApplication();
13246 if (method_exists($app, 'isClient')) {
13247 return $app->isClient('administrator');
13248 }
13249
13250 return $app->isAdmin();
13251 }
13252
13253 /**
13254 * Helper method to cope with the removal of the same method
13255 * in the JApplication class introduced with Joomla 4. Using
13256 * isClient() would break the compatibility with J < 3.7 so
13257 * we can rely on this helper method to avoid Fatal Errors.
13258 *
13259 * @return boolean
13260 *
13261 * @since October 2020
13262 */
13263 public static function isSite()
13264 {
13265 $app = JFactory::getApplication();
13266 if (method_exists($app, 'isClient')) {
13267 return $app->isClient('site');
13268 }
13269
13270 return $app->isSite();
13271 }
13272
13273 /**
13274 * Gets the Google Maps API Key.
13275 *
13276 * @return string
13277 *
13278 * @since 1.14 (J) - 1.4.0 (WP)
13279 */
13280 public static function getGoogleMapsKey()
13281 {
13282 return VBOFactory::getConfig()->getString('gmapskey', '');
13283 }
13284
13285 /**
13286 * Checks whether the interactive map booking is enabled.
13287 *
13288 * @return bool
13289 *
13290 * @since 1.14 (J) - 1.4.0 (WP)
13291 */
13292 public static function interactiveMapEnabled()
13293 {
13294 return VBOFactory::getConfig()->getBool('interactive_map', false);
13295 }
13296
13297 /**
13298 * Gets the preferred colors saved in the configuration, if any.
13299 *
13300 * @return array
13301 *
13302 * @since 1.14 (J) - 1.4.0 (WP)
13303 */
13304 public static function getPreferredColors()
13305 {
13306 $config = VBOFactory::getConfig();
13307
13308 // default colors
13309 $pref_colors = [
13310 'textcolor' => '#2282bd',
13311 'bgcolor' => '#2282bd',
13312 'fontcolor' => '#fff',
13313 'bgcolorhov' => '#1890ce',
13314 'fontcolorhov' => '#fff',
13315 ];
13316
13317 // saved colors
13318 $colors = $config->getArray('pref_colors', []);
13319
13320 if (!array_filter($colors)) {
13321 // unset the list to fallback onto the default ones
13322 $colors = [];
13323 }
13324
13325 return $colors ?: $pref_colors;
13326 }
13327
13328 /**
13329 * Adds to the document inline styles for the preferred colors, if any.
13330 *
13331 * @return void
13332 *
13333 * @since 1.14 (J) - 1.4.0 (WP)
13334 */
13335 public static function loadPreferredColorStyles()
13336 {
13337 $pref_colors = self::getPreferredColors();
13338 $css_classes = [];
13339
13340 /**
13341 * Register all preferred colors as CSS variables.
13342 *
13343 * @since 1.18.8 (J) - 1.8.8 (WP)
13344 */
13345 $css_vars = array_filter(array_combine(array_keys($pref_colors), array_values($pref_colors)));
13346 if ($css_vars) {
13347 $css_vars_decl = array_map(function($k, $v) {
13348 return sprintf('--vbo-pref-%s: %s;', preg_replace('/[^a-z0-9]/', '', strtolower($k)), $v);
13349 }, array_keys($css_vars), array_values($css_vars));
13350 $css_classes[] = ':root {' . "\n" . implode("\n", $css_vars_decl) . "\n" . '}';
13351 }
13352
13353 // add in-line style declaration
13354 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
13355 }
13356
13357 /**
13358 * Given the full endpoint URL for the AJAX request, it
13359 * returns an appropriate URI for the current platform.
13360 *
13361 * @param mixed $query The query string or a routed URL.
13362 * @param boolean $xhtml Replace & by &amp; for XML compliance.
13363 *
13364 * @return string The AJAX end-point URI.
13365 *
13366 * @since 1.14 (J) - 1.4.0 (WP)
13367 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
13368 */
13369 public static function ajaxUrl($query = '', $xhtml = false)
13370 {
13371 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
13372 }
13373
13374 /**
13375 * Tells whether no tax rates have been defined so far.
13376 *
13377 * @return bool True if no tax rates defined, false otherwise.
13378 *
13379 * @since 1.14 (J) - 1.4.0 (WP)
13380 */
13381 public static function noTaxRates()
13382 {
13383 $dbo = JFactory::getDbo();
13384
13385 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
13386 $dbo->setQuery($q);
13387 $dbo->execute();
13388
13389 return ($dbo->getNumRows() < 1);
13390 }
13391
13392 /**
13393 * Booking Type feature is strictly connected to VCM and OTAs. Updated
13394 * versions of VBO will always support it as long as VCM is installed.
13395 * Needed by VCM to understand whether certain SQL queries can be performed.
13396 *
13397 * @return bool true if VCM exists, false otherwise.
13398 *
13399 * @since 1.14 (J) - 1.4.0 (WP)
13400 */
13401 public static function isBookingTypeSupported()
13402 {
13403 static $isBookingTypeSupported = null;
13404
13405 if (is_bool($isBookingTypeSupported)) {
13406 return $isBookingTypeSupported;
13407 }
13408
13409 $isBookingTypeSupported = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
13410
13411 return $isBookingTypeSupported;
13412 }
13413
13414 /**
13415 * Returns an instance of the Rates Flow helper class of VCM, if available.
13416 *
13417 * @param bool $anew True to request a new object instance.
13418 *
13419 * @return VCMRatesFlow|null
13420 *
13421 * @since 1.15.0 (J) - 1.5.0 (WP)
13422 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
13423 */
13424 public static function getRatesFlowInstance($anew = false)
13425 {
13426 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
13427 if (!is_file($vcm_fpath)) {
13428 return null;
13429 }
13430
13431 // load dependencies
13432 require_once $vcm_fpath;
13433
13434 // return the instance of the class
13435 return VCMRatesFlow::getInstance($anew);
13436 }
13437
13438 /**
13439 * Returns the current appearance preference.
13440 *
13441 * @return string light, auto or dark.
13442 *
13443 * @since 1.15.0 (J) - 1.5.0 (WP)
13444 */
13445 public static function getAppearancePref()
13446 {
13447 static $preference = null;
13448
13449 if ($preference !== null) {
13450 return $preference;
13451 }
13452
13453 // auto is the default appearance preference
13454 $default_pref = 'auto';
13455
13456 // accepted preferences
13457 $valid_pref = [
13458 'auto',
13459 'light',
13460 'dark',
13461 ];
13462
13463 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
13464 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
13465
13466 return $preference;
13467 }
13468
13469 /**
13470 * According to the appearance preferences, the apposite
13471 * CSS assets are loaded within the document.
13472 *
13473 * @param bool $get_info true to not load any assets.
13474 *
13475 * @return mixed string light, auto, dark, info array or false.
13476 *
13477 * @since 1.15.0 (J) - 1.5.0 (WP)
13478 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
13479 */
13480 public static function loadAppearancePreferenceAssets($get_info = false)
13481 {
13482 // get document object
13483 $document = JFactory::getDocument();
13484
13485 // load current color scheme preference
13486 $current_pref = self::getAppearancePref();
13487
13488 // define caching values
13489 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
13490
13491 // define file attributes
13492 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
13493
13494 // apposite file path and URI for back-end
13495 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
13496 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
13497
13498 // check if this is for the front-end ("auto" file only)
13499 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
13500 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
13501 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
13502 // check if front-end appearance is disabled
13503 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
13504 // do nothing for the front-end by default
13505 return false;
13506 }
13507 }
13508
13509 if (VBOPlatformDetection::isJoomla()) {
13510 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
13511 $css_uri = str_replace('resources/', '', $css_uri);
13512 }
13513
13514 if (!is_file($css_path)) {
13515 // this preference does not require a specific stylesheet
13516 return false;
13517 }
13518
13519 if ($get_info) {
13520 // do not actually load any assets
13521 return [
13522 'id' => $file_attr['id'],
13523 'href' => $css_uri,
13524 ];
13525 }
13526
13527 // load the apposite CSS file
13528 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
13529
13530 return $current_pref;
13531 }
13532
13533 /**
13534 * VBO trigger for VCM subscription status and updates. If called within the admin section
13535 * returns the expiration reminder array to display a modal window, and sets a warning
13536 * message. If called within the front-end, may trigger an email alert.
13537 *
13538 * @return mixed null, array or boolean depending on client position.
13539 *
13540 * @since 1.15.0 (J) - 1.5.0 (WP)
13541 */
13542 public static function getVCMSubscriptionStatus()
13543 {
13544 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
13545 if (!is_file($vcm_lib_path)) {
13546 // do nothing
13547 return null;
13548 }
13549
13550 // determine client position
13551 $is_admin = self::isAdmin();
13552
13553 try {
13554 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
13555 // VCM is outdated
13556 return null;
13557 }
13558
13559 // always attempt to load the admin lang handler of VCM
13560 $lang = JFactory::getLanguage();
13561
13562 // load the VCM admin language file
13563 $vcm_admin_lang_path = '';
13564 if (VBOPlatformDetection::isJoomla()) {
13565 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
13566 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
13567 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
13568 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
13569 /**
13570 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
13571 */
13572 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
13573 }
13574 // load translation file
13575 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
13576 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
13577 /**
13578 * @wponly load language admin handler as well for WP.
13579 * We do this only because of WordPress, but in a way also compatible with Joomla as
13580 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
13581 */
13582 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
13583 }
13584
13585 if ($is_admin) {
13586 // back-end section of the site: check status
13587 if (VikChannelManager::loadExpirationDetails() === false) {
13588 // download and update the expiration details
13589 VikChannelManager::downloadExpirationDetails();
13590 }
13591 $expiration_reminder = null;
13592 if (VikChannelManager::isSubscriptionExpired()) {
13593 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
13594 } else {
13595 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
13596 // we also check if an update is available
13597 if (VikChannelManager::checkUpdates()) {
13598 // an update is more important
13599 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
13600 exit;
13601 }
13602 }
13603 // return reminder status
13604 return $expiration_reminder;
13605 }
13606
13607 // front-end section of the site: trigger alert, if any (bool)
13608 return VikChannelManager::checkSubscriptionReminder();
13609
13610 } catch (Exception $e) {
13611 // do nothing
13612 }
13613
13614 return null;
13615 }
13616
13617 /**
13618 * Given a list of website rate plans, returns the sorted list.
13619 * Naming score is the highest sorting method, pricing scoring is also supported.
13620 *
13621 * @param array $rate_plans the list of rate plan arrays to sort.
13622 * @param bool $assoc whether to keep the original array keys.
13623 *
13624 * @return array the sorted list of rate plan arrays.
13625 *
13626 * @since 1.15.0 (J) - 1.5.0 (WP)
13627 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
13628 */
13629 public static function sortRatePlans($rate_plans, $assoc = false)
13630 {
13631 if (!is_array($rate_plans) || count($rate_plans) < 2) {
13632 return $rate_plans;
13633 }
13634
13635 foreach ($rate_plans as $rp_ind => $rate_plan) {
13636 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
13637 // do not proceed in case of a weird array structure
13638 return $rate_plans;
13639 }
13640
13641 // count the sorting-score of this rate plan
13642 $sort_score = 0;
13643 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
13644 // "standard rate" rate plans should go first
13645 $sort_score += 3;
13646 } else {
13647 $sort_score--;
13648 }
13649
13650 // check for derived rate plan
13651 if (!empty($rate_plan['derived_id'])) {
13652 // derived rates should go last
13653 $sort_score -= 2;
13654 } else {
13655 // parent rates should go first
13656 $sort_score += 2;
13657 }
13658
13659 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
13660 // this doesn't seem to be a "non refundable rate" so it should go first
13661 $sort_score += 2;
13662 }
13663
13664 // set sorting score for this rate plan ID
13665 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13666 }
13667
13668 // apply custom sorting and keep index association
13669 uasort($rate_plans, function($a, $b) {
13670 // sort by name-scoring DESC
13671 $sort_diff = $b['sort_score'] - $a['sort_score'];
13672 if (isset($b['cost'])) {
13673 // apply also sorting by price ASC
13674 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13675 }
13676 return $sort_diff;
13677 });
13678
13679 return !$assoc ? array_values($rate_plans) : $rate_plans;
13680 }
13681
13682 /**
13683 * Returns an instance of the Availability helper class.
13684 *
13685 * @param bool $anew True for forcing a new instance.
13686 *
13687 * @return VikBookingAvailability
13688 *
13689 * @since 1.15.0 (J) - 1.5.0 (WP)
13690 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13691 */
13692 public static function getAvailabilityInstance($anew = false)
13693 {
13694 // require library
13695 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13696
13697 // return the instance of the class
13698 return VikBookingAvailability::getInstance($anew);
13699 }
13700
13701 /**
13702 * Helper method to quickly guess the internal dependency to import.
13703 *
13704 * @param string $path_id either a full path to a file or its identifier.
13705 *
13706 * @return bool true if the requested library was imported or false.
13707 *
13708 * @since 1.15.0 (J) - 1.5.0 (WP)
13709 */
13710 public static function import($path_id)
13711 {
13712 if (empty($path_id)) {
13713 return false;
13714 }
13715
13716 // always append .php extension
13717 $path_id = basename($path_id, '.php') . '.php';
13718
13719 if (is_file($path_id)) {
13720 // full path given
13721 require_once $path_id;
13722
13723 return true;
13724 }
13725
13726 // try guessing where the file id is located
13727 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13728 // library was found
13729 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13730
13731 return true;
13732 }
13733
13734 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13735 // library was found
13736 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13737
13738 return true;
13739 }
13740
13741 return false;
13742 }
13743 }
13744
13745 if (!class_exists('VikResizer'))
13746 {
13747 class VikResizer
13748 {
13749 public function __construct()
13750 {
13751 // objects of this class can also be instantiated without calling the methods statically.
13752 }
13753
13754 /**
13755 * Resizes an image proportionally. For PNG files it can optionally
13756 * trim the image to exclude the transparency, and add some padding to it.
13757 * All PNG files keep the alpha background in the resized version.
13758 *
13759 * @param string $fileimg path to original image file
13760 * @param string $dest path to destination image file
13761 * @param int $towidth
13762 * @param int $toheight
13763 * @param bool $trim_png remove empty background from image
13764 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13765 *
13766 * @return boolean
13767 */
13768 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13769 {
13770 if (!is_file($fileimg)) {
13771 return false;
13772 }
13773 if (empty($towidth) && empty($toheight)) {
13774 return copy($fileimg, $dest);
13775 }
13776
13777 $result = true;
13778
13779 list ($owid, $ohei, $type) = getimagesize($fileimg);
13780
13781 if ($owid > $towidth || $ohei > $toheight) {
13782 $xscale = $owid / $towidth;
13783 $yscale = $ohei / $toheight;
13784 if ($yscale > $xscale) {
13785 $new_width = round($owid * (1 / $yscale));
13786 $new_height = round($ohei * (1 / $yscale));
13787 } else {
13788 $new_width = round($owid * (1 / $xscale));
13789 $new_height = round($ohei * (1 / $xscale));
13790 }
13791
13792 $imageresized = imagecreatetruecolor($new_width, $new_height);
13793
13794 switch ($type) {
13795 case '1' :
13796 $imagetmp = imagecreatefromgif ($fileimg);
13797 break;
13798 case '2' :
13799 $imagetmp = imagecreatefromjpeg($fileimg);
13800 break;
13801 case '18' :
13802 $imagetmp = imagecreatefromwebp($fileimg);
13803 break;
13804 default :
13805 //keep alpha for PNG files
13806 $background = imagecolorallocate($imageresized, 0, 0, 0);
13807 imagecolortransparent($imageresized, $background);
13808 imagealphablending($imageresized, false);
13809 imagesavealpha($imageresized, true);
13810 //
13811 $imagetmp = imagecreatefrompng($fileimg);
13812 break;
13813 }
13814
13815 if (!$imagetmp) {
13816 return false;
13817 }
13818
13819 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13820
13821 switch ($type) {
13822 case '1' :
13823 imagegif($imageresized, $dest);
13824 break;
13825 case '2' :
13826 imagejpeg($imageresized, $dest);
13827 break;
13828 case '18' :
13829 imagewebp($imageresized, $dest);
13830 break;
13831 default :
13832 if ($trim_png) {
13833 self::imageTrim($imageresized, $background, $trim_pad);
13834 }
13835 imagepng($imageresized, $dest);
13836 break;
13837 }
13838
13839 imagedestroy($imageresized);
13840 } else {
13841 $result = copy($fileimg, $dest);
13842 }
13843
13844 if (VBOPlatformDetection::isWordPress()) {
13845 /**
13846 * @wponly in order to not lose resized files after installing an update,
13847 * we need to move any uploaded file onto a recovery folder.
13848 */
13849 VikBookingLoader::import('update.manager');
13850 VikBookingUpdateManager::triggerUploadBackup($dest);
13851 }
13852
13853 return $result;
13854 }
13855
13856 /**
13857 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13858 * trim the image to exclude the transparency, and add some padding to it.
13859 * All PNG files keep the alpha background in the resized version.
13860 *
13861 * @param resource $im Image link resource (reference)
13862 * @param int $bg imagecolorallocate color identifier
13863 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13864 *
13865 * @return void
13866 */
13867 public static function imagetrim(&$im, $bg, $pad = null)
13868 {
13869 // Calculate padding for each side.
13870 if (isset($pad)) {
13871 $pp = explode(' ', $pad);
13872 if (isset($pp[3])) {
13873 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13874 } elseif (isset($pp[2])) {
13875 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13876 } elseif (isset($pp[1])) {
13877 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13878 } else {
13879 $p = array_fill(0, 4, (int) $pp[0]);
13880 }
13881 } else {
13882 $p = array_fill(0, 4, 0);
13883 }
13884
13885 // Get the image width and height.
13886 $imw = imagesx($im);
13887 $imh = imagesy($im);
13888
13889 // Set the X variables.
13890 $xmin = $imw;
13891 $xmax = 0;
13892
13893 // Start scanning for the edges.
13894 for ($iy=0; $iy<$imh; $iy++) {
13895 $first = true;
13896 for ($ix=0; $ix<$imw; $ix++) {
13897 $ndx = imagecolorat($im, $ix, $iy);
13898 if ($ndx != $bg) {
13899 if ($xmin > $ix) {
13900 $xmin = $ix;
13901 }
13902 if ($xmax < $ix) {
13903 $xmax = $ix;
13904 }
13905 if (!isset($ymin)) {
13906 $ymin = $iy;
13907 }
13908 $ymax = $iy;
13909 if ($first) {
13910 $ix = $xmax;
13911 $first = false;
13912 }
13913 }
13914 }
13915 }
13916
13917 // The new width and height of the image. (not including padding)
13918 $imw = 1+$xmax-$xmin; // Image width in pixels
13919 $imh = 1+$ymax-$ymin; // Image height in pixels
13920
13921 // Make another image to place the trimmed version in.
13922 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13923
13924 // Make the background of the new image the same as the background of the old one.
13925 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13926 imagefill($im2, 0, 0, $bg2);
13927
13928 // Copy it over to the new image.
13929 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13930
13931 // To finish up, we replace the old image which is referenced.
13932 $im = $im2;
13933 }
13934
13935 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13936 {
13937 if (!file_exists($fileimg)) {
13938 return false;
13939 }
13940 if (empty($towidth) && empty($toheight)) {
13941 copy($fileimg, $dest);
13942 return true;
13943 }
13944
13945 $exp = explode(",", $rgb);
13946 if (count($exp) == 3) {
13947 $r = trim($exp[0]);
13948 $g = trim($exp[1]);
13949 $b = trim($exp[2]);
13950 } else {
13951 $r = 0;
13952 $g = 0;
13953 $b = 0;
13954 }
13955
13956 list ($owid, $ohei, $type) = getimagesize($fileimg);
13957
13958 if ($owid > $towidth || $ohei > $toheight) {
13959 $xscale = $owid / $towidth;
13960 $yscale = $ohei / $toheight;
13961 if ($yscale > $xscale) {
13962 $new_width = round($owid * (1 / $yscale));
13963 $new_height = round($ohei * (1 / $yscale));
13964 $ydest = 0;
13965 $diff = $towidth - $new_width;
13966 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13967 } else {
13968 $new_width = round($owid * (1 / $xscale));
13969 $new_height = round($ohei * (1 / $xscale));
13970 $xdest = 0;
13971 $diff = $toheight - $new_height;
13972 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13973 }
13974
13975 $imageresized = imagecreatetruecolor($towidth, $toheight);
13976
13977 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13978 imagefill($imageresized, 0, 0, $bgColor);
13979
13980 switch ($type) {
13981 case '1' :
13982 $imagetmp = imagecreatefromgif ($fileimg);
13983 break;
13984 case '2' :
13985 $imagetmp = imagecreatefromjpeg($fileimg);
13986 break;
13987 default :
13988 $imagetmp = imagecreatefrompng($fileimg);
13989 break;
13990 }
13991
13992 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13993
13994 switch ($type) {
13995 case '1' :
13996 imagegif ($imageresized, $dest);
13997 break;
13998 case '2' :
13999 imagejpeg($imageresized, $dest);
14000 break;
14001 default :
14002 imagepng($imageresized, $dest);
14003 break;
14004 }
14005
14006 imagedestroy($imageresized);
14007
14008 return true;
14009 } else {
14010 copy($fileimg, $dest);
14011 }
14012 return true;
14013 }
14014
14015 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
14016 {
14017 if (!file_exists($fileimg)) {
14018 return false;
14019 }
14020 if (empty($towidth) && empty($toheight)) {
14021 copy($fileimg, $dest);
14022 return true;
14023 }
14024
14025 list ($owid, $ohei, $type) = getimagesize($fileimg);
14026
14027 if ($owid <= $ohei) {
14028 $new_width = $towidth;
14029 $new_height = ($towidth / $owid) * $ohei;
14030 } else {
14031 $new_height = $toheight;
14032 $new_width = ($new_height / $ohei) * $owid;
14033 }
14034
14035 switch ($type) {
14036 case '1' :
14037 $img_src = imagecreatefromgif ($fileimg);
14038 $img_dest = imagecreate($new_width, $new_height);
14039 break;
14040 case '2' :
14041 $img_src = imagecreatefromjpeg($fileimg);
14042 $img_dest = imagecreatetruecolor($new_width, $new_height);
14043 break;
14044 default :
14045 $img_src = imagecreatefrompng($fileimg);
14046 $img_dest = imagecreatetruecolor($new_width, $new_height);
14047 break;
14048 }
14049
14050 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
14051
14052 switch ($type) {
14053 case '1' :
14054 $cropped = imagecreate($towidth, $toheight);
14055 break;
14056 case '2' :
14057 $cropped = imagecreatetruecolor($towidth, $toheight);
14058 break;
14059 default :
14060 $cropped = imagecreatetruecolor($towidth, $toheight);
14061 break;
14062 }
14063
14064 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
14065
14066 switch ($type) {
14067 case '1' :
14068 imagegif ($cropped, $dest);
14069 break;
14070 case '2' :
14071 imagejpeg($cropped, $dest);
14072 break;
14073 default :
14074 imagepng($cropped, $dest);
14075 break;
14076 }
14077
14078 imagedestroy($img_dest);
14079 imagedestroy($cropped);
14080
14081 return true;
14082 }
14083 }
14084 }
14085