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

14,038 lines 467.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 if (!class_exists('VikBookingIcons')) {
14 // require the Icons class
15 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "icons.php");
16 }
17
18 if (!function_exists('showSelectVb')) {
19 function showSelectVb($err, $err_code_info = array()) {
20 include(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'error_form.php');
21 }
22 }
23
24 class VikBooking
25 {
26 public static function addJoomlaUser($name, $username, $email, $password) {
27 //new method
28 jimport('joomla.application.component.helper');
29 $params = JComponentHelper::getParams('com_users');
30 $user = new JUser;
31 $data = array();
32 //Get the default new user group, Registered if not specified.
33 $system = $params->get('new_usertype', 2);
34 $data['groups'] = array();
35 $data['groups'][] = $system;
36 $data['name'] = $name;
37 $data['username'] = $username;
38 $data['email'] = self::getVboApplication()->emailToPunycode($email);
39 $data['password'] = $password;
40 $data['password2'] = $password;
41 $data['sendEmail'] = 0; //should the user receive system mails?
42 //$data['block'] = 0;
43 if (!$user->bind($data)) {
44 VikError::raiseWarning('', JText::translate($user->getError()));
45 return false;
46 }
47 if (!$user->save()) {
48 VikError::raiseWarning('', JText::translate($user->getError()));
49 return false;
50 }
51 return $user->id;
52 }
53
54 public static function userIsLogged() {
55 $user = JFactory::getUser();
56
57 return !$user->guest;
58 }
59
60 public static function prepareViewContent()
61 {
62 /**
63 * @wponly JApplication::getMenu() cannot be adapted to WP
64 */
65 }
66
67 public static function isFontAwesomeEnabled()
68 {
69 return VBOFactory::getConfig()->getBool('usefa', true);
70 }
71
72 public static function loadFontAwesome($force_load = false)
73 {
74 if (!self::isFontAwesomeEnabled() && !$force_load) {
75 return false;
76 }
77
78 /**
79 * We let the class VikBookingIcons load the proper FontAwesome libraries.
80 *
81 * @since 1.11
82 */
83 VikBookingIcons::loadAssets();
84
85 return true;
86 }
87
88 /**
89 * Checks if modifications or cancellations via front-end are allowed.
90 * 0 = everything is Disabled.
91 * 1 = Disabled, with request message (default).
92 * 2 = Modification Enabled, Cancellation Disabled.
93 * 3 = Cancellation Enabled, Modification Disabled.
94 * 4 = everything is Enabled.
95 *
96 * @return int
97 */
98 public static function getReservationModCanc()
99 {
100 return VBOFactory::getConfig()->getInt('resmodcanc', 1);
101 }
102
103 public static function getReservationModCancMin()
104 {
105 return VBOFactory::getConfig()->getInt('resmodcancmin', 1);
106 }
107
108 public static function getDefaultDistinctiveFeatures()
109 {
110 return [
111 'VBODEFAULTDISTFEATUREONE' => '',
112 // Below is the default feature for 'Room Code'. One default feature is sufficient
113 // 'VBODEFAULTDISTFEATURETWO' => '',
114 ];
115 }
116
117 /**
118 * Given the room's parameters and index, tries to take the first distinctive feature.
119 *
120 * @param mixed $rparams string to be decoded, or decoded array/object params
121 * @param int $rindex room index to look for, starting from 1.
122 *
123 * @return mixed false on failure, array otherwise [feature name, feature value]
124 *
125 * @since 1.14 (J) - 1.4.0 (WP)
126 */
127 public static function getRoomUnitDistinctiveFeature($rparams, $rindex) {
128 $rindex = (int)$rindex;
129 if ($rindex < 1 || empty($rparams)) {
130 return false;
131 }
132
133 if (is_string($rparams)) {
134 // decode params
135 $rparams = self::getRoomParam('features', $rparams);
136 }
137
138 if (is_object($rparams)) {
139 // typecast to array
140 $rparams = (array)$rparams;
141 }
142
143 if (!is_array($rparams) || !count($rparams)) {
144 return false;
145 }
146
147 if (isset($rparams['features'])) {
148 $rparams = $rparams['features'];
149 }
150
151 $feature = array();
152 foreach ($rparams as $param_index => $rfeatures) {
153 if ((int)$param_index != $rindex || !is_array($rfeatures) || !count($rfeatures)) {
154 continue;
155 }
156 foreach ($rfeatures as $featname => $featval) {
157 if (empty($featval)) {
158 continue;
159 }
160 // use the first distinctive feature
161 $tn_featname = JText::translate($featname);
162 if ($tn_featname == $featname) {
163 // no translation was applied
164 if (VBOPlatformDetection::isWordPress()) {
165 // try to apply a translation through Gettext even if we have to pass a variable
166 $tn_featname = __($featname);
167 } else {
168 // convert the string to a hypothetical INI constant
169 $ini_constant = str_replace(' ', '_', strtoupper($featname));
170 $tn_featname = JText::translate($ini_constant);
171 $tn_featname = $tn_featname == $ini_constant ? $featname : $tn_featname;
172 }
173 }
174 // store values and break loop
175 $feature = array($tn_featname, $featval);
176 break;
177 }
178 }
179
180 return count($feature) ? $feature : false;
181 }
182
183 /**
184 * Returns an associative list of booking room records and occupied sub-unit indexes.
185 *
186 * @param array $order The current booking record for which the list is needed.
187 * @param int $idroom The booked room ID involved for the associative list.
188 *
189 * @return array
190 *
191 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
192 */
193 public static function getRoomUnitNumsUnavailable(array $order, $idroom)
194 {
195 $dbo = JFactory::getDbo();
196
197 // list of data to build
198 $unavailable_indexes = [];
199
200 $first = $order['checkin'];
201 $second = $order['checkout'];
202 $secdiff = $second - $first;
203 $daysdiff = $secdiff / 86400;
204 if (is_int($daysdiff)) {
205 if ($daysdiff < 1) {
206 $daysdiff = 1;
207 }
208 } else {
209 if ($daysdiff < 1) {
210 $daysdiff = 1;
211 } else {
212 $sum = floor($daysdiff) * 86400;
213 $newdiff = $secdiff - $sum;
214 $maxhmore = self::getHoursMoreRb() * 3600;
215 if ($maxhmore >= $newdiff) {
216 $daysdiff = floor($daysdiff);
217 } else {
218 $daysdiff = ceil($daysdiff);
219 }
220 }
221 }
222
223 // the busy records information pool
224 $busyMap = [];
225
226 // query just the busy records
227 $dbo->setQuery(
228 $dbo->getQuery(true)
229 ->select($dbo->qn([
230 'b.idroom',
231 'b.checkin',
232 'b.checkout',
233 'b.realback',
234 'ob.idorder',
235 ]))
236 ->from($dbo->qn('#__vikbooking_busy', 'b'))
237 ->innerJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('ob.idbusy') . ' = ' . $dbo->qn('b.id'))
238 ->where($dbo->qn('b.idroom') . ' = ' . (int) $idroom)
239 ->where($dbo->qn('b.checkin') . ' < ' . $second)
240 ->where($dbo->qn('b.checkout') . ' > ' . $first)
241 ->where($dbo->qn('ob.idorder') . ' != ' . (int) $order['id'])
242 ->order($dbo->qn('b.id') . ' ASC')
243 );
244 $busyRecords = $dbo->loadAssocList();
245
246 if (!$busyRecords) {
247 // useless to proceed
248 return [];
249 }
250
251 // query bookings and rooms based on the busy records obtained
252 $dbo->setQuery(
253 $dbo->getQuery(true)
254 ->select([
255 $dbo->qn('or.id', 'or_id'),
256 $dbo->qn('or.idorder'),
257 $dbo->qn('or.idroom'),
258 $dbo->qn('or.roomindex'),
259 ])
260 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
261 ->where($dbo->qn('or.idroom') . ' = ' . (int) $idroom)
262 ->where($dbo->qn('or.idorder') . ' IN (' . implode(', ', array_map('intval', array_column($busyRecords, 'idorder'))) . ')')
263 ->order($dbo->qn('or.id') . ' ASC')
264 );
265 $busyRooms = $dbo->loadAssocList();
266
267 // build the final map by merging values
268 foreach ($busyRecords as $busyRecord) {
269 // busy signature
270 $signature = sprintf('%d-%d', $busyRecord['idorder'], $busyRecord['idroom']);
271 // push initial record values
272 $busyMap[$signature][] = $busyRecord;
273 }
274
275 // list of booking-id/booking-room-id signatures
276 $roomSignaturesCounter = [];
277
278 foreach ($busyRooms as $busyRoom) {
279 // busy signature
280 $signature = sprintf('%d-%d', $busyRoom['idorder'], $busyRoom['idroom']);
281
282 // determine booking room signature index
283 $roomSignaturesCounter[$signature] = ($roomSignaturesCounter[$signature] ?? -1) + 1;
284
285 if (!isset($busyMap[$signature][$roomSignaturesCounter[$signature]])) {
286 // unmatched signature
287 continue;
288 }
289
290 // merge booking room values with existing busy record values
291 $busyMap[$signature][$roomSignaturesCounter[$signature]] = array_merge($busyMap[$signature][$roomSignaturesCounter[$signature]], $busyRoom);
292 }
293
294 // simulate a list of associative busy and booking room records
295 $busy = [];
296 foreach ($busyMap as $mapList) {
297 foreach ($mapList as $record) {
298 // push record
299 $busy[] = $record;
300 }
301 }
302
303 if (!$busy) {
304 // useless to proceed
305 return [];
306 }
307
308 // get a list of affected timestamps
309 $groupdays = self::getGroupDays($first, $second, $daysdiff);
310
311 // scan the affected stay timestamps to build the sub-unit matches
312 foreach ($groupdays as $gday) {
313 foreach ($busy as $bu) {
314 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
315 continue;
316 }
317 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
318 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
319 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
320 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
321 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
322 }
323 }
324 }
325 }
326
327 return $unavailable_indexes;
328 }
329
330 /**
331 * Returns an associative list of booking room records and available sub-unit indexes.
332 *
333 * @param array $order The current booking record for which the list is needed.
334 * @param int $idroom The booked room ID involved for the associative list.
335 *
336 * @return array
337 *
338 * @since 1.18.2 (J) - 1.8.2 (WP) refactoring to improve speed and efficiency for large datasets.
339 */
340 public static function getRoomUnitNumsAvailable(array $order, $idroom)
341 {
342 // build available indexes
343 $available_indexes = [];
344
345 // get the unavailable room indexes
346 $unavailable_indexes = self::getRoomUnitNumsUnavailable($order, $idroom);
347
348 // load room params
349 $room_data = self::getRoomInfo($idroom, ['params'], $no_cache = false);
350 $room_params_arr = (array) json_decode($room_data['params'] ?? '[]', true);
351
352 if (is_array($room_params_arr['features'] ?? null) && $room_params_arr['features']) {
353 foreach ($room_params_arr['features'] as $rind => $rfeatures) {
354 if (in_array($rind, $unavailable_indexes)) {
355 continue;
356 }
357 // push available room index
358 $available_indexes[] = $rind;
359 }
360 }
361
362 // return the available indexes
363 return $available_indexes;
364 }
365
366 /**
367 * Generates a random UUID v4.
368 *
369 * A UUID is a 16-octet (128-bit) number. In its canonical form, a UUID is represented by 32
370 * hexadecimal digits, displayed in five groups separated by hyphens, in the form 8-4-4-4-12
371 * for a total of 36 characters (32 alphanumeric characters and four hyphens).
372 *
373 * @return string
374 *
375 * @since 1.18.8 (J) - 1.8.8 (WP)
376 */
377 public static function uuid()
378 {
379 /*
380 return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
381 // 32 bits for "time_low"
382 mt_rand(0, 0xffff), mt_rand(0, 0xffff),
383
384 // 16 bits for "time_mid"
385 mt_rand(0, 0xffff),
386
387 // 16 bits for "time_hi_and_version",
388 // four most significant bits holds version number 4
389 mt_rand(0, 0x0fff) | 0x4000,
390
391 // 16 bits, 8 bits for "clk_seq_hi_res",
392 // 8 bits for "clk_seq_low",
393 // two most significant bits holds zero and one for variant DCE1.1
394 mt_rand(0, 0x3fff) | 0x8000,
395
396 // 48 bits for "node"
397 mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
398 );
399 */
400
401 // generate 16 random bytes through CSPRNG
402 $data = random_bytes(16);
403
404 // four most significant bits holds version number 4
405 $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
406 // two most significant bits holds zero and one for variant DCE1.1
407 $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
408
409 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
410 }
411
412 /**
413 * Load the restrictions applying the given filters to the passed rooms.
414 * The ordering of the query SHOULD remain unchanged, because it's required
415 * to have it by ascending order of the ID. So the older records first.
416 * Even when no filters, we always exclude expired restrictions.
417 *
418 * @param boolean $filters whether to apply filters
419 * @param array $rooms the list of rooms to filter
420 *
421 * @return array the list of restrictions found or an empty array.
422 */
423 public static function loadRestrictions($filters = true, $rooms = [])
424 {
425 $dbo = JFactory::getDbo();
426
427 $restrictions = [];
428 $limts = strtotime(date('Y-m-d'));
429
430 if (!$filters) {
431 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
432 } else {
433 if (!$rooms) {
434 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
435 } else {
436 $clause = [];
437 foreach ($rooms as $idr) {
438 if (empty($idr)) {
439 continue;
440 }
441 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
442 }
443 if ($clause) {
444 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE (`dto` = 0 OR `dto` >= " . $limts . ") AND (`allrooms` = 1 OR (`allrooms` = 0 AND (" . implode(" OR ", $clause) . "))) ORDER BY `id` ASC;";
445 } else {
446 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
447 }
448 }
449 }
450
451 $dbo->setQuery($q);
452 $allrestrictions = $dbo->loadAssocList();
453
454 foreach ($allrestrictions as $k => $res) {
455 if (!empty($res['month'])) {
456 $restrictions[$res['month']] = $res;
457 } else {
458 if (!isset($restrictions['range'])) {
459 $restrictions['range'] = array();
460 }
461 $restrictions['range'][$k] = $res;
462 }
463 }
464
465 return $restrictions;
466 }
467
468 /**
469 * Given a list of restriction records split into range or months, filters
470 * the list by including the "global" restrictions applied to all rooms.
471 *
472 * @param array $restrictions List of restriction records loaded.
473 *
474 * @return array Associative list of filtered restriction records.
475 */
476 public static function globalRestrictions(array $restrictions)
477 {
478 $list = [];
479
480 // iterate all records
481 foreach ($restrictions as $kr => $rr) {
482 if ($kr == 'range') {
483 // scan all date-range restriction records
484 foreach ($rr as $kd => $dr) {
485 if ($dr['allrooms'] == 1) {
486 $list['range'][$kd] = $restrictions[$kr][$kd];
487 }
488 }
489 // go next
490 continue;
491 }
492
493 // month restriction record
494 if ($rr['allrooms'] == 1) {
495 $list[$kr] = $restrictions[$kr];
496 }
497 }
498
499 return $list;
500 }
501
502 /**
503 * Given a list of restriction records split into range or months, filters the list
504 * by including the "listing-level" or "global" restrictions affecting to given rooms.
505 *
506 * @param array $restrictions List of restriction records loaded.
507 * @param array $room_ids List of involved and eligible room IDs.
508 *
509 * @return array Associative list of filtered restriction records.
510 *
511 * @since 1.18.2 (J) - 1.8.2 (WP)
512 */
513 public static function listingRestrictions(array $restrictions, array $room_ids)
514 {
515 $list = [];
516
517 // iterate all records
518 foreach ($restrictions as $kr => $rr) {
519 if ($kr == 'range') {
520 // scan all date-range restriction records
521 foreach ($rr as $kd => $dr) {
522 if ($dr['allrooms'] == 1) {
523 // push global restriction
524 $list['range'][$kd] = $restrictions[$kr][$kd];
525 } elseif (!empty($dr['idrooms'])) {
526 // scan the list of room IDs to match against
527 foreach ($room_ids as $room_id) {
528 if (strpos($dr['idrooms'], '-' . $room_id . '-') !== false) {
529 // push listing-level restriction
530 $list['range'][$kd] = $restrictions[$kr][$kd];
531 break;
532 }
533 }
534 }
535 }
536 // go next
537 continue;
538 }
539
540 // month restriction record
541 if ($rr['allrooms'] == 1) {
542 // push global restriction
543 $list[$kr] = $restrictions[$kr];
544 } elseif (!empty($rr['idrooms'])) {
545 // scan the list of room IDs to match against
546 foreach ($room_ids as $room_id) {
547 if (strpos($rr['idrooms'], '-' . $room_id . '-') !== false) {
548 // push listing-level restriction
549 $list[$kr] = $restrictions[$kr];
550 break;
551 }
552 }
553 }
554 }
555
556 return $list;
557 }
558
559 /**
560 * From the given restrictions, check-in, check-out and nights, looks for
561 * a restriction to be returned and applied over these stays.
562 * In order to give priority to newer Restriction IDs, the order of the
563 * $restrictions array should be ascending, so that the newer restrictions
564 * will overwrite the array with the last ID (more recent). Only for date ranges.
565 * Returns an array with the record of the restriction found. The loop is not
566 * broken when the first valid restriction is found to give priority to
567 * newer restriction IDs to overwrite older records.
568 *
569 * @param int $first the unix timestamp for the check-in date
570 * @param int $second the unix timestamp for the check-out date
571 * @param int $daysdiff the number of nights of stay
572 * @param array $restrictions the list of restrictions loaded
573 *
574 * @return array the restriction found, or an empty array.
575 */
576 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
577 $season_restrictions = array();
578 $restrcheckin = getdate($first);
579 $restrcheckout = getdate($second);
580 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
581 //restriction found for this month, checking:
582 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
583 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
584 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
585 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
586 //Week Day Arrival Restriction
587 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
588 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
589 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
590 }
591 $season_restrictions['wdays'] = $rvalidwdays;
592 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
593 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
594 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
595 }
596 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
597 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
598 }
599 }
600 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
601 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
602 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
603 $season_restrictions['allowed'] = false;
604 }
605 }
606 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
607 $season_restrictions['allowed'] = false;
608 }
609 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
610 } elseif (array_key_exists('range', $restrictions)) {
611 foreach ($restrictions['range'] as $restr) {
612 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
613 //restriction found for this date range, checking:
614 $season_restrictions['id'] = $restr['id'];
615 $season_restrictions['name'] = $restr['name'];
616 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
617 if (strlen((string)$restr['wday']) > 0) {
618 //Week Day Arrival Restriction
619 $rvalidwdays = array($restr['wday']);
620 if (strlen((string)$restr['wdaytwo']) > 0) {
621 $rvalidwdays[] = $restr['wdaytwo'];
622 }
623 $season_restrictions['wdays'] = $rvalidwdays;
624 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
625 if (!empty($restr['ctad'])) {
626 $season_restrictions['cta'] = explode(',', $restr['ctad']);
627 }
628 if (!empty($restr['ctdd'])) {
629 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
630 }
631 }
632 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
633 $season_restrictions['maxlos'] = $restr['maxlos'];
634 if ($daysdiff > $restr['maxlos']) {
635 $season_restrictions['allowed'] = false;
636 }
637 }
638 if ($daysdiff < $restr['minlos']) {
639 $season_restrictions['allowed'] = false;
640 }
641 $season_restrictions['minlos'] = $restr['minlos'];
642 }
643 }
644 }
645
646 return $season_restrictions;
647 }
648
649 public static function compareSeasonRestrictionsNights($restrictions)
650 {
651 $base_compare = array();
652 $base_nights = 0;
653 foreach ($restrictions as $nights => $restr) {
654 $base_compare = $restr;
655 $base_nights = $nights;
656 break;
657 }
658
659 /**
660 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
661 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
662 *
663 * @since 1.15.0 (J) - 1.5.0 (WP)
664 */
665 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
666
667 foreach ($restrictions as $nights => $restr) {
668 if ($nights == $base_nights) {
669 continue;
670 }
671
672 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
673 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
674
675 // get associative array of differences
676 $diff = array_diff($use_base_compare, $use_restr);
677
678 if (count($diff) > 0 && array_key_exists('id', $diff)) {
679 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
680 // only one Restriction per time should be applied to certain Season Dates but check just in case.
681 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
682 }
683 }
684
685 return array();
686 }
687
688 /**
689 * Methods using array_diff() may need one-dimension arrays, because this
690 * native functions applies a string comparison, and so casting an array
691 * value to a string would generate a Notice message. This method simply
692 * converts any sub-array into a restorable string into a non-scalar var.
693 * This only works when comparing 2 array variables.
694 *
695 * @param array $arr the hypothetical multi dimension array.
696 *
697 * @return array the one dimension array to be passed to array_diff()
698 * and the list of keys converted to strings for comparison.
699 *
700 * @since 1.15.0 (J) - 1.5.0 (WP)
701 */
702 public static function prepareMultiDimArrayDiff($arr)
703 {
704 $casted_keys = array();
705
706 if (!is_array($arr) || !count($arr)) {
707 return array($casted_keys, $arr);
708 }
709
710 foreach ($arr as $key => $val) {
711 if (!is_scalar($val)) {
712 // memorize original value
713 $casted_keys[$key] = $val;
714 // cast to string must be applied
715 $arr[$key] = json_encode($val);
716 }
717 }
718
719 return array($casted_keys, $arr);
720 }
721
722 /**
723 * Methods using array_diff() may need one-dimension arrays, because this
724 * native functions applies a string comparison, and so casting an array
725 * value to a string would generate a Notice message. This method restores
726 * the original values of the "prepared" arrays by getting the list of the
727 * modified keys from non-scalar to scalar values.
728 * This only works when comparing 2 array variables.
729 *
730 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
731 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
732 * @param array $arr the array result of array_diff().
733 *
734 * @return array the array result of array_diff() with its original values.
735 *
736 * @since 1.15.0 (J) - 1.5.0 (WP)
737 */
738 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
739 {
740 if (!is_array($arr) || !count($arr)) {
741 return $arr;
742 }
743
744 foreach ($arr as $key => $val) {
745 // keys returned by array_diff can be present in both $keys_one and $keys_two
746 if (isset($keys_one[$key])) {
747 // always give higher priority to the 1st argument keys for array_diff()
748 $arr[$key] = json_decode($keys_one[$key]);
749 } elseif (isset($keys_two[$key])) {
750 // fallback to 2nd argument keys
751 $arr[$key] = json_decode($keys_two[$key]);
752 }
753 }
754
755 return $arr;
756 }
757
758 public static function roomRestrictions($roomid, $restrictions) {
759 $ret = array();
760 if (!empty($roomid) && count($restrictions) > 0) {
761 foreach ($restrictions as $kr => $rr) {
762 if ($kr == 'range') {
763 foreach ($rr as $kd => $dr) {
764 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
765 $allrooms = explode(';', $dr['idrooms']);
766 if (in_array('-'.$roomid.'-', $allrooms)) {
767 $ret['range'][$kd] = $restrictions[$kr][$kd];
768 }
769 }
770 }
771 } else {
772 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
773 $allrooms = explode(';', $rr['idrooms']);
774 if (in_array('-'.$roomid.'-', $allrooms)) {
775 $ret[$kr] = $restrictions[$kr];
776 }
777 }
778 }
779 }
780 }
781 return $ret;
782 }
783
784 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
785 {
786 // default states
787 $restrictionerrmsg = '';
788 $restrictions_affcount = 0;
789 $minlos_errors_pool = [];
790
791 // check for month-level or range-level restrictions
792 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
793 //restriction found for this month, checking:
794 $restrictions_affcount++;
795 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
796 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
797 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
798 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
799 }
800 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
801 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVAL', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wday']).(strlen($roomrestr[$restrcheckin['mon']]['wdaytwo']) > 0 ? '/'.self::sayWeekDay($roomrestr[$restrcheckin['mon']]['wdaytwo']) : ''));
802 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
803 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
804 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
805 }
806 }
807 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
808 if (count($comborestr) > 0) {
809 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
810 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
811 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBO', self::sayMonth($restrcheckin['mon']), self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
812 }
813 }
814 }
815 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
816 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
817 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
818 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
819 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
820 }
821 }
822 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
823 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
824 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
825 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
826 }
827 }
828 }
829 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
830 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
831 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
832 }
833 }
834 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
835 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
836 }
837 } elseif (array_key_exists('range', $roomrestr)) {
838 // start valid flag
839 $restrictionsvalid = true;
840
841 /**
842 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
843 * The most recent restrictions will have a higher priority over the oldest ones.
844 *
845 * @since 1.12.1
846 */
847 $minlos_priority = [
848 'ok' => [],
849 'nok' => [],
850 ];
851
852 /**
853 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
854 *
855 * @since 1.16.10 (J) - 1.6.10 (WP)
856 */
857 $ctad_priority = [
858 'ok' => [],
859 'nok' => [],
860 ];
861
862 foreach ($roomrestr['range'] as $restr) {
863 /**
864 * We should not always add 82799 seconds to the end date of the restriction
865 * because if they only last for one day (like a Saturday), then $restr['dto']
866 * will be already set to the time 23:59:59.
867 *
868 * @since 1.13 (J) - 1.2.18 (WP)
869 * @since 1.18.3 (J) - 1.8.3 (WP) timestamps ending with 9 for a range greater than 1 day indicate that the restriction end time
870 * is already set to 23:59:59 (VCM Connector may be responsible), so no seconds should be added or the restriction end date
871 * will be the day after, and if after the check-in time, it may be included.
872 */
873 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) && substr((string) $restr['dto'], -1, 1) != 9 ? 82799 : 0;
874
875 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
876 // restriction found for this date range based on arrival date, check if compliant
877 $restrictions_affcount++;
878
879 // set flag for CTA/CTD compliance
880 $cta_ctd_passed = true;
881
882 if (strlen((string)$restr['wday']) > 0) {
883 $rvalidwdays = array($restr['wday']);
884 if (strlen((string)$restr['wdaytwo']) > 0) {
885 $rvalidwdays[] = $restr['wdaytwo'];
886 }
887 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
888 $restrictionsvalid = false;
889 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
890 } elseif ($restr['multiplyminlos'] == 1) {
891 if (($daysdiff % $restr['minlos']) != 0) {
892 $restrictionsvalid = false;
893 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
894 }
895 }
896 $comborestr = self::parseJsDrangeWdayCombo($restr);
897 if ($comborestr) {
898 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
899 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
900 $restrictionsvalid = false;
901 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYCOMBORANGE', self::sayWeekDay($comborestr[$restrcheckin['wday']][0]).(count($comborestr[$restrcheckin['wday']]) == 2 ? '/'.self::sayWeekDay($comborestr[$restrcheckin['wday']][1]) : ''), self::sayWeekDay($restrcheckin['wday']));
902 }
903 }
904 }
905 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
906 if (!empty($restr['ctad'])) {
907 $ctarestrictions = explode(',', $restr['ctad']);
908 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
909 $restrictionsvalid = false;
910 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
911 $cta_ctd_passed = false;
912 }
913 }
914 if (!empty($restr['ctdd'])) {
915 $ctdrestrictions = explode(',', $restr['ctdd']);
916 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
917 $restrictionsvalid = false;
918 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
919 $cta_ctd_passed = false;
920 }
921 }
922 }
923
924 // check CTA/CTD compliance
925 if (!$cta_ctd_passed) {
926 array_push($ctad_priority['nok'], (int)$restr['id']);
927 } else {
928 array_push($ctad_priority['ok'], (int)$restr['id']);
929 }
930
931 // max LOS validation
932 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] >= $restr['minlos']) {
933 if ($daysdiff > $restr['maxlos']) {
934 $restrictionsvalid = false;
935 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
936 }
937 }
938
939 // min LOS validation
940 if ($daysdiff < $restr['minlos']) {
941 $restrictionsvalid = false;
942 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
943 // push error value
944 array_push($minlos_priority['nok'], (int) $restr['id']);
945 // set error message with related minimum stay
946 $minlos_errors_pool[$restr['minlos']] = $restrictionerrmsg;
947 } else {
948 array_push($minlos_priority['ok'], (int) $restr['id']);
949 }
950 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
951 /**
952 * We validate the CTD restrictions depending on the check-out date.
953 *
954 * @since 1.16.3 (J) - 1.6.3 (WP)
955 */
956 $ctdrestrictions = explode(',', $restr['ctdd']);
957 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
958 $restrictions_affcount++;
959 $restrictionsvalid = false;
960 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
961 }
962 }
963 }
964
965 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
966 // a more recent restriction is allowing this MinLOS
967 // ensure there are no recent CTA/CTD rules not compliant
968 $cta_ctd_priority_ok = true;
969
970 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
971 // a more recent restriction is not compliant with the CTA/CTD rules
972 $cta_ctd_priority_ok = false;
973 }
974
975 if ($cta_ctd_priority_ok) {
976 // we unset the error message because more recent restriction(s) are allowing this stay dates
977 $restrictionerrmsg = '';
978 }
979 }
980 }
981
982 // check global restriction of Min LOS for TAC functions in VBO and VCM
983 if (empty($restrictionerrmsg) && $roomrestr && $restrictions_affcount <= 0) {
984 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
985 $globminlos = self::getDefaultNightsCalendar();
986 if ($globminlos > 1 && $daysdiff < $globminlos) {
987 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
988 }
989 }
990
991 /**
992 * When working with room-level restrictions and different minLOS across multiple apartments,
993 * we need the display the actually lowest minimum stay among all listings from the errors found.
994 *
995 * @since 1.17.3 (J) - 1.7.3 (WP)
996 */
997 if ($restrictionerrmsg && in_array($restrictionerrmsg, $minlos_errors_pool) && count($minlos_errors_pool) > 1) {
998 // make sure to return the error message for the lowest minimum stay
999 $lowest_minlos = min(array_map('intval', array_keys($minlos_errors_pool)));
1000 $restrictionerrmsg = $minlos_errors_pool[$lowest_minlos] ?? $restrictionerrmsg;
1001 }
1002
1003 // return the restriction error message string, if anything wrong was found
1004 return $restrictionerrmsg;
1005 }
1006
1007 public static function parseJsDrangeWdayCombo($drestr) {
1008 $combo = array();
1009 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
1010 $cparts = explode(':', $drestr['wdaycombo']);
1011 foreach ($cparts as $kc => $cw) {
1012 if (!empty($cw)) {
1013 $nowcombo = explode('-', $cw);
1014 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
1015 }
1016 }
1017 }
1018 return $combo;
1019 }
1020
1021 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
1022 $dbo = JFactory::getDbo();
1023 $pkg = array();
1024 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1025 $dbo->setQuery($q);
1026 $dbo->execute();
1027 if ($dbo->getNumRows() == 1) {
1028 $pkg = $dbo->loadAssoc();
1029 $vbo_tn = self::getTranslator();
1030 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
1031 } else {
1032 return JText::translate('VBOPKGERRNOTFOUND');
1033 }
1034 $rooms_req = array();
1035 foreach ($rooms as $num => $room) {
1036 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
1037 $rooms_req[] = $room['id'];
1038 }
1039 }
1040 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
1041 $dbo->setQuery($q);
1042 $dbo->execute();
1043 if ($dbo->getNumRows() != count($rooms_req)) {
1044 //error, not all the rooms requested are available for this package
1045 return JText::translate('VBOPKGERRNOTROOM');
1046 }
1047 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
1048 return JText::translate('VBOPKGERRNUMNIGHTS');
1049 }
1050 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
1051 return JText::translate('VBOPKGERRCHECKIND');
1052 }
1053 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
1054 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
1055 return JText::translate('VBOPKGERRCHECKOUTD');
1056 }
1057 if (!empty($pkg['excldates'])) {
1058 //this would check if any stay date is excluded
1059 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
1060 //check just the arrival and departure dates
1061 $bookdates_ts = array($checkints, $checkoutts);
1062 $bookdates = array();
1063 foreach ($bookdates_ts as $bookdate_ts) {
1064 $info_d = getdate($bookdate_ts);
1065 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
1066 }
1067 $edates = explode(';', $pkg['excldates']);
1068 foreach ($edates as $edate) {
1069 if (!empty($edate) && in_array($edate, $bookdates)) {
1070 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
1071 }
1072 }
1073 }
1074 return $pkg;
1075 }
1076
1077 public static function getPackage($pkg_id) {
1078 $dbo = JFactory::getDbo();
1079 $pkg = array();
1080 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
1081 $dbo->setQuery($q);
1082 $dbo->execute();
1083 if ($dbo->getNumRows() == 1) {
1084 $pkg = $dbo->loadAssoc();
1085 }
1086 return $pkg;
1087 }
1088
1089 /**
1090 * Returns the requested room parameter or default value.
1091 *
1092 * @param string $paramname The parameter name.
1093 * @param mixed $params The room params string, or json-decoded array/object.
1094 * @param mixed $def The default value to return as fallback.
1095 *
1096 * @return mixed
1097 *
1098 * @since 1.17.3 (J) - 1.7.3 (WP) added 3rd argument $def.
1099 */
1100 public static function getRoomParam($paramname, $params, $def = '')
1101 {
1102 if (empty($params)) {
1103 return $def;
1104 }
1105
1106 if (is_string($params)) {
1107 $params = (array) json_decode($params, true);
1108 } elseif (is_object($params)) {
1109 $params = (array) $params;
1110 }
1111
1112 if (!is_array($params)) {
1113 return $def;
1114 }
1115
1116 return $params[$paramname] ?? $def;
1117 }
1118
1119 public static function filterNightsSeasonsCal($arr_nights) {
1120 $nights = array();
1121 foreach ($arr_nights as $night) {
1122 if (intval(trim($night)) > 0) {
1123 $nights[] = intval(trim($night));
1124 }
1125 }
1126 sort($nights);
1127 return array_unique($nights);
1128 }
1129
1130 public static function getSeasonRangeTs($from, $to, $year) {
1131 $sfrom = 0;
1132 $sto = 0;
1133 $tsbase = mktime(0, 0, 0, 1, 1, $year);
1134 $curyear = $year;
1135 $tsbasetwo = $tsbase;
1136 $curyeartwo = $year;
1137 if ($from > $to) {
1138 //between two years
1139 $curyeartwo += 1;
1140 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
1141 }
1142 $sfrom = ($tsbase + $from);
1143 $sto = ($tsbasetwo + $to);
1144 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
1145 //leap years
1146 $infoseason = getdate($sfrom);
1147 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1148 if ($infoseason[0] > $leapts) {
1149 /**
1150 * Timestamp must be greater than the leap-day of Feb 29th.
1151 * It used to be checked for >= $leapts.
1152 *
1153 * @since July 3rd 2019
1154 */
1155 $sfrom += 86400;
1156 if ($curyear == $curyeartwo) {
1157 $sto += 86400;
1158 }
1159 }
1160 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
1161 //leap years
1162 $infoseason = getdate($sto);
1163 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
1164 if ($infoseason[0] > $leapts) {
1165 /**
1166 * Timestamp must be greater than the leap-day of Feb 29th.
1167 * It used to be checked for >= $leapts.
1168 *
1169 * @since July 3rd 2019
1170 */
1171 $sto += 86400;
1172 }
1173 }
1174 return array($sfrom, $sto);
1175 }
1176
1177 public static function sortSeasonsRangeTs ($all_seasons) {
1178 $sorted = array();
1179 $map = array();
1180 foreach ($all_seasons as $key => $season) {
1181 $map[$key] = $season['from_ts'];
1182 }
1183 asort($map);
1184 foreach ($map as $key => $s) {
1185 $sorted[] = $all_seasons[$key];
1186 }
1187 return $sorted;
1188 }
1189
1190 public static function formatSeasonDates ($from_ts, $to_ts) {
1191 $one = getdate($from_ts);
1192 $two = getdate($to_ts);
1193 $months_map = array(
1194 1 => JText::translate('VBSHORTMONTHONE'),
1195 2 => JText::translate('VBSHORTMONTHTWO'),
1196 3 => JText::translate('VBSHORTMONTHTHREE'),
1197 4 => JText::translate('VBSHORTMONTHFOUR'),
1198 5 => JText::translate('VBSHORTMONTHFIVE'),
1199 6 => JText::translate('VBSHORTMONTHSIX'),
1200 7 => JText::translate('VBSHORTMONTHSEVEN'),
1201 8 => JText::translate('VBSHORTMONTHEIGHT'),
1202 9 => JText::translate('VBSHORTMONTHNINE'),
1203 10 => JText::translate('VBSHORTMONTHTEN'),
1204 11 => JText::translate('VBSHORTMONTHELEVEN'),
1205 12 => JText::translate('VBSHORTMONTHTWELVE')
1206 );
1207 $mday_map = array(
1208 1 => JText::translate('VBMDAYFRIST'),
1209 2 => JText::translate('VBMDAYSECOND'),
1210 3 => JText::translate('VBMDAYTHIRD'),
1211 'generic' => JText::translate('VBMDAYNUMGEN')
1212 );
1213 if ($one['year'] == $two['year']) {
1214 return $one['year'].' '.$months_map[(int)$one['mon']].' '.$one['mday'].'<sup>'.(array_key_exists((int)substr($one['mday'], -1), $mday_map) && ($one['mday'] < 10 || $one['mday'] > 20) ? $mday_map[(int)substr($one['mday'], -1)] : $mday_map['generic']).'</sup> - '.$months_map[(int)$two['mon']].' '.$two['mday'].'<sup>'.(array_key_exists((int)substr($two['mday'], -1), $mday_map) && ($two['mday'] < 10 || $two['mday'] > 20) ? $mday_map[(int)substr($two['mday'], -1)] : $mday_map['generic']).'</sup>';
1215 }
1216 return $months_map[(int)$one['mon']].' '.$one['mday'].'<sup>'.(array_key_exists((int)substr($one['mday'], -1), $mday_map) && ($one['mday'] < 10 || $one['mday'] > 20) ? $mday_map[(int)substr($one['mday'], -1)] : $mday_map['generic']).'</sup> '.$one['year'].' - '.$months_map[(int)$two['mon']].' '.$two['mday'].'<sup>'.(array_key_exists((int)substr($two['mday'], -1), $mday_map) && ($two['mday'] < 10 || $two['mday'] > 20) ? $mday_map[(int)substr($two['mday'], -1)] : $mday_map['generic']).'</sup> '.$two['year'];
1217 }
1218
1219 public static function getFirstCustDataField($custdata) {
1220 $first_field = '';
1221 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1222 //Room is closed with this booking
1223 return '----';
1224 }
1225 $parts = explode("\n", $custdata);
1226 foreach ($parts as $part) {
1227 if (!empty($part)) {
1228 $field = explode(':', trim($part));
1229 if (!empty($field[1])) {
1230 return trim($field[1]);
1231 }
1232 }
1233 }
1234 return $first_field;
1235 }
1236
1237 /**
1238 * This method composes a string to be logged for the admin
1239 * to keep track of what was inside the booking before the
1240 * modification. Returns a string and it uses language definitions
1241 * that should be available on the front-end and back-end INI files.
1242 *
1243 * @param array $old_booking the array of the booking prior to the modification
1244 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1245 *
1246 * @return string text describing the situation with the booking before the changes.
1247 *
1248 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1249 */
1250 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1251 {
1252 $vbo_df = self::getDateFormat();
1253 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1254 $wdays_map = array(
1255 JText::translate('VBWEEKDAYZERO'),
1256 JText::translate('VBWEEKDAYONE'),
1257 JText::translate('VBWEEKDAYTWO'),
1258 JText::translate('VBWEEKDAYTHREE'),
1259 JText::translate('VBWEEKDAYFOUR'),
1260 JText::translate('VBWEEKDAYFIVE'),
1261 JText::translate('VBWEEKDAYSIX'),
1262 );
1263 $now_info = getdate();
1264 $checkin_info = getdate($old_booking['checkin']);
1265 $checkout_info = getdate($old_booking['checkout']);
1266
1267 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1268 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1269 $prev_dates = $prev_nights.' - '.$wdays_map[$checkin_info['wday']].', '.date($df.' H:i', $checkin_info[0]).' - '.$wdays_map[$checkout_info['wday']].', '.date($df.' H:i', $checkout_info[0]);
1270
1271 $orooms_map = [];
1272 $orooms_arr = [];
1273
1274 // scan the previously booked room records
1275 foreach ((array) ($old_booking['rooms_info'] ?? []) as $oroom) {
1276 // determine room name
1277 $roomName = $oroom['name'] ?? $oroom['roomnamevb'] ?? $oroom['idroom'] ?? '---';
1278 $orooms_arr[] = sprintf(
1279 '%s, %s: %d, %s: %d',
1280 $roomName,
1281 JText::translate('VBMAILADULTS'),
1282 (int) ($oroom['adults'] ?? 0),
1283 JText::translate('VBMAILCHILDREN'),
1284 (int) ($oroom['children'] ?? 0),
1285 );
1286 if (!empty($oroom['idroom'])) {
1287 $orooms_map[$oroom['idroom']] = $roomName;
1288 }
1289 }
1290
1291 // build previous rooms booked string
1292 $prev_rooms = $orooms_arr ? implode("\n", $orooms_arr) : '';
1293
1294 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && $orooms_map) {
1295 $split_stay_prev_infos = [];
1296 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1297 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1298 continue;
1299 }
1300 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1301 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1302 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1303
1304 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1305 if (!empty($room_nights)) {
1306 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1307 }
1308 $split_stay_descr .= ', ';
1309 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1310
1311 // push split stay description string
1312 $split_stay_prev_infos[] = $split_stay_descr;
1313 }
1314 if (count($split_stay_prev_infos)) {
1315 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1316 }
1317 }
1318
1319 $currencyname = self::getCurrencyName();
1320 $prev_total = self::formatCurrencyNumber(self::numberFormat($old_booking['total']), $currencyname);
1321
1322 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1323 }
1324
1325 /**
1326 * This method invokes the class
1327 * VikChannelManagerLogos (new in VCM 1.6.4) to
1328 * map the name of a channel to its corresponding logo.
1329 * The method can also be used to get an istance of the class.
1330 *
1331 * @param mixed $provenience either a string or an array with main and sub channels.
1332 * @param boolean $get_istance
1333 *
1334 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1335 */
1336 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1337 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1338 return false;
1339 }
1340 if (!class_exists('VikChannelManagerLogos')) {
1341 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1342 }
1343
1344 /**
1345 * Due to the new iCal channel we now support main and sub channels.
1346 *
1347 * @since 1.13 - VCM 1.7.0
1348 */
1349 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1350 $provenience = explode('_', $provenience);
1351 }
1352 $main_channel = '';
1353 $full_channel = '';
1354 if (is_array($provenience)) {
1355 if (count($provenience) > 1) {
1356 $main_channel = $provenience[1];
1357 } else {
1358 $main_channel = $provenience[0];
1359 }
1360 if (stripos($provenience[0], 'ical') !== false) {
1361 $full_channel = implode('_', $provenience);
1362 }
1363 } else {
1364 $main_channel = $provenience;
1365 }
1366
1367 // get object instance by passing the main provenience
1368 $obj = new VikChannelManagerLogos($main_channel);
1369
1370 // update provenience with main and full channel
1371 if (!empty($full_channel)) {
1372 $obj->setProvenience($main_channel, $full_channel);
1373 }
1374
1375 // return either the instance or the logo URL for this channel source
1376 return $get_istance ? $obj : $obj->getLogoURL();
1377 }
1378
1379 public static function vcmAutoUpdate()
1380 {
1381 static $autoUpdate = null;
1382
1383 if ($autoUpdate !== null) {
1384 return $autoUpdate;
1385 }
1386
1387 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1388 $autoUpdate = -1;
1389 return $autoUpdate;
1390 }
1391
1392 $autoUpdate = (int) VBOFactory::getConfig()->getBool('vcmautoupd', false);
1393
1394 return $autoUpdate;
1395 }
1396
1397 public static function getVcmInvoker()
1398 {
1399 if (!class_exists('VboVcmInvoker')) {
1400 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1401 }
1402
1403 return new VboVcmInvoker();
1404 }
1405
1406 /**
1407 * Returns an instance of VikBooking History object.
1408 *
1409 * @param int $bid optional booking ID to bind.
1410 *
1411 * @return VboBookingHistory
1412 *
1413 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1414 */
1415 public static function getBookingHistoryInstance($bid = 0)
1416 {
1417 if (!class_exists('VboBookingHistory')) {
1418 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1419 }
1420
1421 return new VboBookingHistory($bid);
1422 }
1423
1424 /**
1425 * Returns an instance of the VCMChatHandler class to handle
1426 * the messaging/chat for the given reservation ID.
1427 *
1428 * @param int $oid the ID of the booking in VBO
1429 * @param string $channel the name of the source channel
1430 *
1431 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1432 *
1433 * @since 1.11.2 (J) - 1.1.2 (WP)
1434 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1435 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1436 * @since 1.17.6 (J) - 1.7.6 (WP) empty signature arguments will only require the dependencies.
1437 */
1438 public static function getVcmChatInstance($oid, $channel = null)
1439 {
1440 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1441 if (!is_file($vcm_messaging_helper)) {
1442 // VCM is not available
1443 return null;
1444 }
1445
1446 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1447 // VCM must be inactive, prevent the loading of the instance
1448 return null;
1449 }
1450
1451 // make sure the channel name is correct as it may contain the sub-network
1452 if (!empty($channel)) {
1453 $segments = explode('_', $channel);
1454 if (count($segments) > 1) {
1455 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1456 $channel = $segments[0];
1457 }
1458 }
1459
1460 // always require main file of the abstract class even if arguments are empty/invalid
1461 require_once $vcm_messaging_helper;
1462
1463 if (empty($oid) && empty($channel)) {
1464 // do not proceed in order to save resources
1465 return null;
1466 }
1467
1468 // return the instance of the class for this channel handler
1469 return VCMChatHandler::getInstance($oid, $channel);
1470 }
1471
1472 /**
1473 * Returns an instance of the VCMOpportunityHandler class to handle
1474 * the various opportunities through VCM.
1475 *
1476 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1477 *
1478 * @since 1.2.0
1479 */
1480 public static function getVcmOpportunityInstance() {
1481 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1482 if (!is_file($vcm_opp_helper)) {
1483 // VCM is not available
1484 return null;
1485 }
1486 // require main file of the class
1487 require_once $vcm_opp_helper;
1488 // return the instance of the class
1489 return VCMOpportunityHandler::getInstance();
1490 }
1491
1492 /**
1493 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1494 *
1495 * @param array $data optional data to bind to the object.
1496 *
1497 * @return bool
1498 *
1499 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1500 *
1501 * @requires VCM >= 1.8.24
1502 */
1503 public static function vcmBcomReportingSupported(array $data = [])
1504 {
1505 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1506 }
1507
1508 /**
1509 * Gets a list of all channels supporting the promotions.
1510 *
1511 * @param string $key the key of the handler.
1512 *
1513 * @return mixed false if VCM is not installed, empty array otherwise.
1514 *
1515 * @requires Vik Channel Manager 1.7.1
1516 *
1517 * @since 1.3.0
1518 */
1519 public static function getPromotionHandlers($key = null)
1520 {
1521 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1522 // VCM not installed
1523 return false;
1524 }
1525
1526 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1527 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1528 return [];
1529 }
1530
1531 return VikChannelManager::getPromotionHandlers($key);
1532 }
1533
1534 /**
1535 * Gets the factors for suggesting the application of the promotions.
1536 *
1537 * @param mixed $data some optional instructions to be passed as argument.
1538 *
1539 * @return mixed false if VCM is not installed, associative array otherwise.
1540 *
1541 * @requires Vik Channel Manager 1.7.1
1542 *
1543 * @since 1.3.0
1544 */
1545 public static function getPromotionFactors($data = null)
1546 {
1547 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1548 // VCM not installed
1549 return false;
1550 }
1551
1552 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1553 // VCM is outdated
1554 return false;
1555 }
1556
1557 return VikChannelManager::getPromotionFactors($data);
1558 }
1559
1560 public static function getTheme()
1561 {
1562 return VBOFactory::getConfig()->get('theme', '');
1563 }
1564
1565 public static function getFooterOrdMail($vbo_tn = null)
1566 {
1567 $dbo = JFactory::getDbo();
1568
1569 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1570 $dbo->setQuery($q);
1571 $ft = $dbo->loadAssocList();
1572
1573 if (!is_object($vbo_tn)) {
1574 $vbo_tn = self::getTranslator();
1575 }
1576
1577 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1578
1579 return $ft ? $ft[0]['setting'] : '';
1580 }
1581
1582 public static function requireLogin()
1583 {
1584 return VBOFactory::getConfig()->getBool('requirelogin', false);
1585 }
1586
1587 public static function autoRoomUnit()
1588 {
1589 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1590 }
1591
1592 public static function todayBookings()
1593 {
1594 static $todayBookings = null;
1595
1596 if ($todayBookings === null) {
1597 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1598 }
1599
1600 return $todayBookings;
1601 }
1602
1603 public static function couponsEnabled()
1604 {
1605 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1606 }
1607
1608 public static function customersPinEnabled()
1609 {
1610 return VBOFactory::getConfig()->getBool('enablepin', false);
1611 }
1612
1613 /**
1614 * Detects the type of visitor from the user agent.
1615 * Known types are: computer, smartphone, tablet.
1616 *
1617 * @param boolean $returnua whether the type of visitor should be returned. If false
1618 * boolean is returned in case of mobile device detected.
1619 * @param boolean $loadassets whether the system should load an apposite CSS file.
1620 *
1621 * @return mixed string for the type of visitor or boolean if mobile detected.
1622 *
1623 * @since 1.0.13 - Revision September 2018
1624 */
1625 public static function detectUserAgent($returnua = false, $loadassets = true) {
1626 $session = JFactory::getSession();
1627 $sval = $session->get('vbuseragent', '');
1628 $mobiles = array('tablet', 'smartphone');
1629 if (!empty($sval)) {
1630 if ($loadassets) {
1631 self::userAgentStyleSheet($sval);
1632 }
1633 return $returnua ? $sval : in_array($sval, $mobiles);
1634 }
1635
1636 // detect visitor (device) type
1637 $visitoris = 'computer';
1638 try {
1639 if (!class_exists('MobileDetector')) {
1640 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1641 }
1642 $detector = new MobileDetector;
1643 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1644 } catch (Exception $e) {
1645 // do nothing
1646 }
1647
1648 $session->set('vbuseragent', $visitoris);
1649 if ($loadassets) {
1650 self::userAgentStyleSheet($visitoris);
1651 }
1652
1653 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1654 }
1655
1656 public static function userAgentStyleSheet($ua)
1657 {
1658 /**
1659 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1660 */
1661 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1662 return;
1663 }
1664 //
1665
1666 $document = JFactory::getDocument();
1667 /**
1668 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1669 */
1670 if ($ua == 'smartphone') {
1671 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1672 } elseif ($ua == 'tablet') {
1673 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1674 }
1675 return true;
1676 }
1677
1678 public static function loadJquery()
1679 {
1680 return VBOFactory::getConfig()->getBool('loadjquery', true);
1681 }
1682
1683 public static function loadBootstrap()
1684 {
1685 return VBOFactory::getConfig()->getBool('bootstrap', true);
1686 }
1687
1688 public static function allowMultiLanguage()
1689 {
1690 return VBOFactory::getConfig()->getBool('multilang', true);
1691 }
1692
1693 public static function getTranslator()
1694 {
1695 if (!class_exists('VikBookingTranslator')) {
1696 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1697 }
1698
1699 return new VikBookingTranslator();
1700 }
1701
1702 /**
1703 * New method's name spelled correctly. The object will be cached by default.
1704 *
1705 * @return VikBookingCustomersPin
1706 *
1707 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1708 */
1709 public static function getCPinInstance()
1710 {
1711 static $cpin_instance = null;
1712
1713 if (!$cpin_instance) {
1714 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1715
1716 $cpin_instance = new VikBookingCustomersPin();
1717 }
1718
1719 return $cpin_instance;
1720 }
1721
1722 /**
1723 * Old method's name spelled wrongly for BC.
1724 *
1725 * @return VikBookingCustomersPin
1726 */
1727 public static function getCPinIstance()
1728 {
1729 return self::getCPinInstance();
1730 }
1731
1732 /**
1733 * Returns an instance of the VikBookingTracker Class.
1734 * It is also possible to call this method to just require the library.
1735 * This is useful for the back-end to access some static methods
1736 * without tracking any data.
1737 *
1738 * @param boolean $require_only whether to return the object.
1739 *
1740 * @return VikBookingTracker
1741 *
1742 * @since 1.11
1743 */
1744 public static function getTracker($require_only = false) {
1745 if (!class_exists('VikBookingTracker')) {
1746 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1747 }
1748 return $require_only ? true : VikBookingTracker::getInstance();
1749 }
1750
1751 /**
1752 * Returns an instance of the VikBookingOperator Class.
1753 *
1754 * @return VikBookingOperator
1755 *
1756 * @since 1.11
1757 */
1758 public static function getOperatorInstance() {
1759 if (!class_exists('VikBookingOperator')) {
1760 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1761 }
1762 return VikBookingOperator::getInstance();
1763 }
1764
1765 /**
1766 * Returns an instance of the VikBookingFestivities Class.
1767 *
1768 * @return VikBookingFestivities
1769 *
1770 * @since 1.12
1771 */
1772 public static function getFestivitiesInstance() {
1773 if (!class_exists('VikBookingFestivities')) {
1774 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1775 }
1776 return VikBookingFestivities::getInstance();
1777 }
1778
1779 /**
1780 * Returns an instance of the VikBookingCriticalDates Class.
1781 *
1782 * @return VikBookingCriticalDates
1783 *
1784 * @since 1.13.5
1785 */
1786 public static function getCriticalDatesInstance() {
1787 if (!class_exists('VikBookingCriticalDates')) {
1788 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1789 }
1790 return VikBookingCriticalDates::getInstance();
1791 }
1792
1793 /**
1794 * Checks whether the chat is enabled.
1795 *
1796 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1797 *
1798 * @since 1.12
1799 */
1800 public static function chatEnabled() {
1801 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1802 return -1;
1803 }
1804 $dbo = JFactory::getDbo();
1805 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1806 $dbo->setQuery($q);
1807 $dbo->execute();
1808 if (!$dbo->getNumRows()) {
1809 // enable the chat by default if VCM is installed
1810 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1811 $dbo->setQuery($q);
1812 $dbo->execute();
1813 return 1;
1814 }
1815 $s = $dbo->loadResult();
1816 return intval($s);
1817 }
1818
1819 /**
1820 * Loads the chat parameters from the configuration.
1821 *
1822 * @return object stdClass object from decoded JSON string
1823 *
1824 * @since 1.12
1825 */
1826 public static function getChatParams() {
1827 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1828 return new stdClass;
1829 }
1830 $dbo = JFactory::getDbo();
1831 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1832 $dbo->setQuery($q);
1833 $dbo->execute();
1834 if (!$dbo->getNumRows()) {
1835 // compose default and basic chat params for the first time
1836 $basic_params = new stdClass;
1837 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1838 $basic_params->av_type = 'checkin';
1839 $basic_params->av_days = 0;
1840
1841 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1842 $dbo->setQuery($q);
1843 $dbo->execute();
1844
1845 return $basic_params;
1846 }
1847 return json_decode($dbo->loadResult());
1848 }
1849
1850 /**
1851 * Checks whether the pre-checkin is enabled.
1852 *
1853 * @return int 0 if disabled, 1 otherwise
1854 *
1855 * @since 1.12
1856 */
1857 public static function precheckinEnabled()
1858 {
1859 return (int) VBOFactory::getConfig()->getBool('precheckinenabled', true);
1860 }
1861
1862 /**
1863 * Returns the minimum number of days in advance to
1864 * enable the pre-checkin via front-end.
1865 *
1866 * @return int the min number of days in advance for pre-checkin.
1867 *
1868 * @since 1.12
1869 */
1870 public static function precheckinMinOffset()
1871 {
1872 return VBOFactory::getConfig()->getInt('precheckinminoffset', 0);
1873 }
1874
1875 /**
1876 * Whether upselling extra services is enabled.
1877 *
1878 * @return boolean true if enabled, false otherwise.
1879 *
1880 * @since 1.13
1881 */
1882 public static function upsellingEnabled()
1883 {
1884 return VBOFactory::getConfig()->getBool('upselling', true);
1885 }
1886
1887 /**
1888 * Collects a list of options/extra that can be upsold for the
1889 * the rooms booked.
1890 *
1891 * @param array $upsell_data list of stdClass objects for each room booked.
1892 * @param array $info some booking details (id, checkin, checkout).
1893 * @param object $vbo_tn the translation object.
1894 *
1895 * @return array list of upsellable options for each room booked.
1896 *
1897 * @since 1.13 (J) - 1.3.0 (WP)
1898 * @since 1.17.7 (J) - 1.7.7 (WP) added support for damage deposit to confirmed bookings.
1899 */
1900 public static function loadUpsellingData($upsell_data, $info, $vbo_tn)
1901 {
1902 $dbo = JFactory::getDbo();
1903
1904 // get all rooms booked
1905 $all_room_ids = [];
1906 foreach ($upsell_data as $v) {
1907 if (!in_array($v->id, $all_room_ids)) {
1908 array_push($all_room_ids, (int) $v->id);
1909 }
1910 }
1911
1912 $dbo->setQuery(
1913 $dbo->getQuery(true)
1914 ->select([
1915 $dbo->qn('id'),
1916 $dbo->qn('idopt'),
1917 ])
1918 ->from($dbo->qn('#__vikbooking_rooms'))
1919 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_room_ids) . ')')
1920 );
1921 $records = $dbo->loadAssocList();
1922 if (!$records) {
1923 // no rooms found
1924 return [];
1925 }
1926
1927 // get all suitable options
1928 $all_options = [];
1929 $rooms_options = [];
1930 foreach ($records as $v) {
1931 $allopts = explode(';', $v['idopt']);
1932 $room_opt = [];
1933 foreach ($allopts as $o) {
1934 if (empty($o)) {
1935 continue;
1936 }
1937 if (!in_array($o, $all_options)) {
1938 array_push($all_options, (int) $o);
1939 }
1940 array_push($room_opt, (int) $o);
1941 }
1942 $rooms_options[$v['id']] = $room_opt;
1943 }
1944 if (!$all_options) {
1945 // no options found
1946 return [];
1947 }
1948
1949 // load all options that could be used by the booked rooms no matter what was already booked
1950 $dbo->setQuery(
1951 $dbo->getQuery(true)
1952 ->select('*')
1953 ->from($dbo->qn('#__vikbooking_optionals'))
1954 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1955 ->where($dbo->qn('forcesel') . ' = 0')
1956 ->where($dbo->qn('ifchildren') . ' = 0')
1957 ->where($dbo->qn('is_citytax') . ' = 0')
1958 ->where($dbo->qn('is_fee') . ' = 0')
1959 ->order($dbo->qn('ordering') . ' ASC')
1960 );
1961 $records = $dbo->loadAssocList();
1962
1963 if (!strcasecmp(($info['status'] ?? ''), 'confirmed')) {
1964 // merge mandatory damage deposit
1965 $dbo->setQuery(
1966 $dbo->getQuery(true)
1967 ->select('*')
1968 ->from($dbo->qn('#__vikbooking_optionals'))
1969 ->where($dbo->qn('id') . ' IN (' . implode(', ', $all_options) . ')')
1970 ->where($dbo->qn('forcesel') . ' = 1')
1971 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
1972 );
1973 $dd_records = $dbo->loadAssocList();
1974
1975 // merge with regular options
1976 $records = array_merge($records, $dd_records);
1977 }
1978
1979 if (!$records) {
1980 // no upsell-able options found
1981 return [];
1982 }
1983
1984 // filter options by available date and translate records
1985 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1986 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1987 $records = !is_array($records) ? [] : $records;
1988
1989 $tot_upsellable = 0;
1990 foreach ($upsell_data as $k => $rdata) {
1991 if (!isset($upsell_data[$k]->upsellable)) {
1992 $upsell_data[$k]->upsellable = [];
1993 }
1994 foreach ($records as $opt) {
1995 if (!empty($opt['ageintervals'])) {
1996 // upsellable options should not contain age intervals for children
1997 continue;
1998 }
1999 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
2000 // this option is not assigned to this room
2001 continue;
2002 }
2003 // check if the option is suited for this room party
2004 $clone_opt = [$opt];
2005 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
2006 if (!is_array($clone_opt) || !$clone_opt) {
2007 // this option is not suited for this room party
2008 continue;
2009 }
2010
2011 if (in_array($opt['id'], $rdata->options)) {
2012 // this option has already been booked
2013 continue;
2014 }
2015
2016 // push this option and increase counter
2017 array_push($upsell_data[$k]->upsellable, $opt);
2018 $tot_upsellable++;
2019 }
2020 }
2021
2022 // if no upsellable options were found, we return an empty array
2023 return $tot_upsellable > 0 ? $upsell_data : [];
2024 }
2025
2026 /**
2027 * Returns the minimum days in advance for booking, by considering by default
2028 * also the property closing dates. If the property is currently closed, then
2029 * the minimum number of days in advance will be increased.
2030 *
2031 * @param bool $no_closing_dates whether to skip checking closing dates.
2032 *
2033 * @return int the number of days in advance for booking.
2034 *
2035 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
2036 * getMinDaysAdvance($skipsession = false) to:
2037 * getMinDaysAdvance($no_closing_dates = false)
2038 * This was made to return the proper number of days in advance
2039 * in case the property is currently closed.
2040 */
2041 public static function getMinDaysAdvance($no_closing_dates = false) {
2042 // cache value in static var
2043 static $getMinDaysAdvance = null;
2044
2045 if ($getMinDaysAdvance) {
2046 return $getMinDaysAdvance;
2047 }
2048 //
2049
2050 $dbo = JFactory::getDbo();
2051
2052 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='mindaysadvance';";
2053 $dbo->setQuery($q);
2054 $dbo->execute();
2055 $mind = $dbo->getNumRows() ? (int)$dbo->loadResult() : 0;
2056
2057 // update cached var
2058 $getMinDaysAdvance = $mind;
2059
2060 if ($no_closing_dates) {
2061 // do not check the closing dates
2062 return $mind;
2063 }
2064
2065 // check if the property is currently closed
2066 $cur_closed_dates = self::getClosingDates();
2067 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
2068 $today_midnight = mktime(0, 0, 0);
2069 $closed_until = null;
2070 foreach ($cur_closed_dates as $kcd => $vcd) {
2071 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
2072 // closing period found
2073 $closed_until = $vcd['to'];
2074 break;
2075 }
2076 }
2077 if ($closed_until !== null) {
2078 // count the number of days until property is closed
2079 $mind = 0;
2080 $today_info = getdate($today_midnight);
2081 while ($today_info[0] <= $closed_until) {
2082 $mind++;
2083 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
2084 }
2085 }
2086 }
2087
2088 // update cached var
2089 $getMinDaysAdvance = $mind;
2090
2091 return $mind;
2092 }
2093
2094 public static function getDefaultNightsCalendar()
2095 {
2096 return VBOFactory::getConfig()->getInt('autodefcalnights', 1);
2097 }
2098
2099 public static function getSearchNumRooms($skipsession = true)
2100 {
2101 $dbo = JFactory::getDbo();
2102 if ($skipsession) {
2103 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2104 $dbo->setQuery($q);
2105 $dbo->execute();
2106 $s = $dbo->loadAssocList();
2107 return (int)$s[0]['setting'];
2108 }
2109 $session = JFactory::getSession();
2110 $sval = $session->get('vbsearchNumRooms', '');
2111 if (!empty($sval)) {
2112 return (int)$sval;
2113 }
2114 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
2115 $dbo->setQuery($q);
2116 $dbo->execute();
2117 $s = $dbo->loadAssocList();
2118 $session->set('vbsearchNumRooms', $s[0]['setting']);
2119 return (int)$s[0]['setting'];
2120 }
2121
2122 public static function getSearchNumAdults($skipsession = true)
2123 {
2124 $dbo = JFactory::getDbo();
2125 if ($skipsession) {
2126 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2127 $dbo->setQuery($q);
2128 $dbo->execute();
2129 $s = $dbo->loadAssocList();
2130 return $s[0]['setting'];
2131 }
2132 $session = JFactory::getSession();
2133 $sval = $session->get('vbsearchNumAdults', '');
2134 if (!empty($sval)) {
2135 return $sval;
2136 }
2137 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
2138 $dbo->setQuery($q);
2139 $dbo->execute();
2140 $s = $dbo->loadAssocList();
2141 $session->set('vbsearchNumAdults', $s[0]['setting']);
2142 return $s[0]['setting'];
2143 }
2144
2145 public static function getSearchNumChildren($skipsession = true)
2146 {
2147 $dbo = JFactory::getDbo();
2148 if ($skipsession) {
2149 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2150 $dbo->setQuery($q);
2151 $dbo->execute();
2152 $s = $dbo->loadAssocList();
2153 return $s[0]['setting'];
2154 }
2155 $session = JFactory::getSession();
2156 $sval = $session->get('vbsearchNumChildren', '');
2157 if (!empty($sval)) {
2158 return $sval;
2159 }
2160 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
2161 $dbo->setQuery($q);
2162 $dbo->execute();
2163 $s = $dbo->loadAssocList();
2164 $session->set('vbsearchNumChildren', $s[0]['setting']);
2165 return $s[0]['setting'];
2166 }
2167
2168 public static function getSmartSearchType($skipsession = true)
2169 {
2170 $dbo = JFactory::getDbo();
2171 if ($skipsession) {
2172 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2173 $dbo->setQuery($q);
2174 $dbo->execute();
2175 $s = $dbo->loadAssocList();
2176 return $s[0]['setting'];
2177 }
2178 $session = JFactory::getSession();
2179 $sval = $session->get('vbsmartSearchType', '');
2180 if (!empty($sval)) {
2181 return $sval;
2182 }
2183 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
2184 $dbo->setQuery($q);
2185 $dbo->execute();
2186 $s = $dbo->loadAssocList();
2187 $session->set('vbsmartSearchType', $s[0]['setting']);
2188 return $s[0]['setting'];
2189 }
2190
2191 /**
2192 * Retrieves the listing-level minimum advance booking offset.
2193 *
2194 * @param int $idroom The listing ID to evaluate.
2195 * @param bool $period Whether to format the value as a period string.
2196 *
2197 * @return int|string|null Number of hours or period string ("2H"), if set.
2198 *
2199 * @since 1.18.3 (J) - 1.8.3 (WP)
2200 */
2201 public static function getMinDateFutureListing($idroom, $period = false)
2202 {
2203 $config = VBOFactory::getConfig();
2204
2205 $room_level_min_notice = $config->get("room_{$idroom}_min_adv_notice", null);
2206
2207 if (is_null($room_level_min_notice) || $room_level_min_notice === '') {
2208 return null;
2209 }
2210
2211 if ($period) {
2212 return sprintf('%dH', (int) $room_level_min_notice);
2213 }
2214
2215 return (int) $room_level_min_notice;
2216 }
2217
2218 /**
2219 * Returns the maximum advance booking notice (offset) as a period.
2220 * It either returns the global configuration setting, or the value
2221 * defined at room-level, if any and if requested.
2222 *
2223 * @param int $idroom optional room ID for room-level setting.
2224 *
2225 * @return string the maximum booking notice period (i.e. +1Y).
2226 *
2227 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
2228 * $idroom to support room-level settings.
2229 */
2230 public static function getMaxDateFuture($idroom = 0)
2231 {
2232 static $max_adv_book_pool = [];
2233
2234 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
2235
2236 if (isset($max_adv_book_pool[$pool_id])) {
2237 return $max_adv_book_pool[$pool_id];
2238 }
2239
2240 $config = VBOFactory::getConfig();
2241
2242 if ($pool_id === 'global') {
2243 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
2244 } else {
2245 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
2246 if (!empty($room_level_max_notice)) {
2247 // setting is defined at room-level
2248 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2249 } else {
2250 // recurse for global setting
2251 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2252 }
2253 }
2254
2255 return $max_adv_book_pool[$pool_id];
2256 }
2257
2258 /**
2259 * Validates the maximum advance booking notice against the requested check-in
2260 * timestamp. The method supports the validation at both room and property levels.
2261 *
2262 * @param int $checkints the check-in date timestamp.
2263 * @param int $idroom the optional room ID for room-level validation.
2264 *
2265 * @return string error date string in case of validation failure.
2266 *
2267 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2268 */
2269 public static function validateMaxDateBookings($checkints, $idroom = 0)
2270 {
2271 $datelim = self::getMaxDateFuture($idroom);
2272 $datelim = empty($datelim) ? '+2y' : $datelim;
2273 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2274 $quantlim = substr($datelim, -1, 1);
2275
2276 $now = getdate();
2277 if ($quantlim == 'w') {
2278 $until_ts = strtotime("+$numlim weeks") + 86399;
2279 } else {
2280 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2281 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2282 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2283 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2284 }
2285
2286 if ($until_ts > $now[0] && $checkints > $until_ts) {
2287 $vbo_df = self::getDateFormat();
2288 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2289
2290 // error, return the maximum date in the future allowed
2291 return date($df, $until_ts);
2292 }
2293
2294 // validation passed
2295 return '';
2296 }
2297
2298 /**
2299 * Validates the minimum advance booking notice against the requested check-in
2300 * timestamp. The method supports the validation at both room and property levels.
2301 *
2302 * @param int $checkints The check-in date timestamp.
2303 * @param int $idroom Optional room ID for room-level validation.
2304 *
2305 * @return int|float Minimum days (int) or hours (float) in advance required.
2306 *
2307 * @since 1.18.3 (J) - 1.8.3 (WP) introduced $idroom argument for room-level.
2308 */
2309 public static function validateMinDaysAdvance($checkints, $idroom = 0)
2310 {
2311 if ($idroom) {
2312 // listing-level minimum advance booking notice (hours, could be null or 0)
2313 $minhadv = self::getMinDateFutureListing($idroom);
2314
2315 // perform listing-level validation only when value is set
2316 if (!$minhadv) {
2317 // validation passed
2318 return '';
2319 }
2320
2321 // access listing params
2322 $listing_data = self::getRoomInfo($idroom, ['params'], $no_cache = true);
2323 if ($listing_data['params'] ?? null) {
2324 // access listing-level check-in time
2325 $listing_checkin = self::getRoomParam('checkin', $listing_data['params']);
2326 if ($listing_checkin) {
2327 // overwrite check-in time
2328 $listing_checkin_parts = explode(':', $listing_checkin);
2329 $checkin_hours = (int) $listing_checkin_parts[0];
2330 $checkin_minutes = (int) $listing_checkin_parts[1];
2331 // read current check-in date-time
2332 $tsinfo = getdate($checkints);
2333 // update check-in time with proper hours, minutes and seconds
2334 $checkints = mktime($checkin_hours, $checkin_minutes, 0, $tsinfo['mon'], $tsinfo['mday'], $tsinfo['year']);
2335 }
2336 }
2337
2338 if ($checkints < strtotime('+' . $minhadv . ($minhadv > 1 ? 'hours' : 'hour'))) {
2339 return $minhadv >= 24 ? floor($minhadv / 24) : ($minhadv / 24);
2340 }
2341
2342 // validation passed without checking the global settings when listing ID given
2343 return '';
2344 }
2345
2346 // global validation
2347 $mindadv = self::getMinDaysAdvance(true);
2348 if ($mindadv > 0) {
2349 $tsinfo = getdate($checkints);
2350 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2351 if ($checkints < $limit_ts) {
2352 return $mindadv;
2353 }
2354 }
2355
2356 // validation passed
2357 return '';
2358 }
2359
2360 /**
2361 * The only supported calendar type has been changed to jQuery UI.
2362 *
2363 * @since 1.13
2364 */
2365 public static function calendarType($skipsession = false)
2366 {
2367 return 'jqueryui';
2368 }
2369
2370 public static function getSiteLogo()
2371 {
2372 static $siteLogo = null;
2373
2374 if ($siteLogo !== null) {
2375 return $siteLogo;
2376 }
2377
2378 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2379
2380 return $siteLogo;
2381 }
2382
2383 public static function getBackendLogo()
2384 {
2385 static $backLogo = null;
2386
2387 if ($backLogo !== null) {
2388 return $backLogo;
2389 }
2390
2391 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2392
2393 return $backLogo;
2394 }
2395
2396 public static function numCalendars()
2397 {
2398 static $numCalendars = null;
2399
2400 if ($numCalendars !== null) {
2401 return $numCalendars;
2402 }
2403
2404 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2405
2406 return $numCalendars;
2407 }
2408
2409 public static function getFirstWeekDay()
2410 {
2411 static $firstWeekDay = null;
2412
2413 if ($firstWeekDay !== null) {
2414 return $firstWeekDay;
2415 }
2416
2417 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2418
2419 return $firstWeekDay;
2420 }
2421
2422 public static function showPartlyReserved()
2423 {
2424 static $partlyReserved = null;
2425
2426 if ($partlyReserved !== null) {
2427 return $partlyReserved;
2428 }
2429
2430 $partlyReserved = VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2431
2432 return $partlyReserved;
2433 }
2434
2435 public static function showStatusCheckinoutOnly()
2436 {
2437 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2438 }
2439
2440 public static function getDisclaimer($vbo_tn = null)
2441 {
2442 $dbo = JFactory::getDbo();
2443
2444 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2445 $dbo->setQuery($q);
2446 $ft = $dbo->loadAssocList();
2447 if (!$ft) {
2448 return '';
2449 }
2450
2451 if (!is_object($vbo_tn)) {
2452 $vbo_tn = self::getTranslator();
2453 }
2454 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2455
2456 return $ft[0]['setting'];
2457 }
2458
2459 public static function showFooter()
2460 {
2461 return VBOFactory::getConfig()->getBool('showfooter', false);
2462 }
2463
2464 public static function getPriceName($idp, $vbo_tn = null)
2465 {
2466 $dbo = JFactory::getDbo();
2467
2468 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2469 $dbo->setQuery($q, 0, 1);
2470 $n = $dbo->loadAssocList();
2471 if ($n) {
2472 if (is_object($vbo_tn)) {
2473 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2474 }
2475 return $n[0]['name'];
2476 }
2477
2478 return "";
2479 }
2480
2481 public static function getPriceAttr($idp, $vbo_tn = null)
2482 {
2483 $dbo = JFactory::getDbo();
2484
2485 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2486 $dbo->setQuery($q, 0, 1);
2487 $n = $dbo->loadAssocList();
2488 if ($n) {
2489 if (is_object($vbo_tn)) {
2490 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2491 }
2492 return $n[0]['attr'];
2493 }
2494
2495 return "";
2496 }
2497
2498 /**
2499 * Fetches the requested rate plan information.
2500 *
2501 * @param int $idp the rate plan ID.
2502 * @param object $vbo_tn the Vik Booking translator object.
2503 *
2504 * @return array empty array or associative record.
2505 */
2506 public static function getPriceInfo($idp, $vbo_tn = null)
2507 {
2508 // cache values
2509 static $priceInfos = [];
2510
2511 $idp = (int)$idp;
2512
2513 if ($priceInfos && isset($priceInfos[$idp])) {
2514 return $priceInfos[$idp];
2515 }
2516
2517 $dbo = JFactory::getDbo();
2518
2519 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2520 $dbo->setQuery($q, 0, 1);
2521 $rplan = $dbo->loadAssocList();
2522 if (!$rplan) {
2523 return [];
2524 }
2525
2526 if (is_object($vbo_tn)) {
2527 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2528 }
2529
2530 // set cached value
2531 $priceInfos[$idp] = $rplan[0];
2532
2533 return $rplan[0];
2534 }
2535
2536 public static function getAliq($idal)
2537 {
2538 $dbo = JFactory::getDbo();
2539
2540 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2541 $dbo->setQuery($q);
2542 $aliq = $dbo->loadResult();
2543 if ($aliq) {
2544 return $aliq;
2545 }
2546
2547 return 0;
2548 }
2549
2550 public static function getTimeOpenStore($skipsession = false)
2551 {
2552 // cache value in static var
2553 static $getTimeOpenStore = null;
2554
2555 if ($getTimeOpenStore) {
2556 return $getTimeOpenStore;
2557 }
2558 //
2559
2560 $dbo = JFactory::getDbo();
2561 $session = JFactory::getSession();
2562
2563 if ($skipsession) {
2564 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2565 if (empty($setting) && $setting != "0") {
2566 return false;
2567 } else {
2568 $x = explode("-", $setting);
2569 if (!empty($x[1]) && $x[1] != "0") {
2570 $getTimeOpenStore = $x;
2571 return $x;
2572 }
2573 }
2574 } else {
2575 $sval = $session->get('vbgetTimeOpenStore', '');
2576 if (!empty($sval)) {
2577 return $sval;
2578 } else {
2579 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2580 if (empty($setting) && $setting != "0") {
2581 return false;
2582 } else {
2583 $x = explode("-", $setting);
2584 if (!empty($x[1]) && $x[1] != "0") {
2585 $session->set('vbgetTimeOpenStore', $x);
2586 $getTimeOpenStore = $x;
2587 return $x;
2588 }
2589 }
2590 }
2591 }
2592 return false;
2593 }
2594
2595 public static function getHoursMinutes($secs)
2596 {
2597 if ($secs >= 3600) {
2598 $op = $secs / 3600;
2599 $hours = floor($op);
2600 $less = $hours * 3600;
2601 $newsec = $secs - $less;
2602 $optwo = $newsec / 60;
2603 $minutes = floor($optwo);
2604 } else {
2605 $hours = "0";
2606 $optwo = $secs / 60;
2607 $minutes = floor($optwo);
2608 }
2609
2610 return [$hours, $minutes];
2611 }
2612
2613 public static function getClosingDates()
2614 {
2615 // cache value in static var
2616 static $getClosingDates = null;
2617
2618 if (is_array($getClosingDates)) {
2619 return $getClosingDates;
2620 }
2621
2622 $dbo = JFactory::getDbo();
2623
2624 $allcd = [];
2625
2626 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2627 $dbo->setQuery($q);
2628 $s = $dbo->loadAssocList();
2629 if ($s && !empty($s[0]['setting'])) {
2630 $allcd = json_decode($s[0]['setting'], true);
2631 $allcd = is_array($allcd) ? $allcd : array();
2632 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2633 foreach ($allcd as $k => $v) {
2634 if ($v['to'] < $base_ts) {
2635 unset($allcd[$k]);
2636 }
2637 }
2638 $allcd = array_values($allcd);
2639 }
2640
2641 $getClosingDates = $allcd;
2642
2643 return $allcd;
2644 }
2645
2646 public static function parseJsClosingDates()
2647 {
2648 $cd = self::getClosingDates();
2649 if (!$cd) {
2650 return [];
2651 }
2652
2653 $cdjs = [];
2654 foreach ($cd as $k => $v) {
2655 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2656 }
2657
2658 return $cdjs;
2659 }
2660
2661 /**
2662 * Given stay date timestamps, tells if the booking is disallowed due to global closing dates.
2663 *
2664 * @param int $checkints The check-in date timestamp.
2665 * @param int $checkoutts The check-in date timestamp.
2666 * @param ?string $df Optional date format to return the closed stay dates.
2667 * @param bool $closingcmp True to obtain the stay components affected by a global closure.
2668 *
2669 * @return string|array Empty string if no closures, closure dates string or affected components list.
2670 *
2671 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $closingcmp to improve the search suggestion details.
2672 */
2673 public static function validateClosingDates($checkints, $checkoutts, $df = null, $closingcmp = false)
2674 {
2675 $cd = self::getClosingDates();
2676
2677 if (!$cd) {
2678 return '';
2679 }
2680
2681 $df = empty($df) ? 'Y-m-d' : $df;
2682 $margin_seconds = 22 * 60 * 60;
2683
2684 foreach ($cd as $k => $v) {
2685 // determine if the stay dates are closed
2686 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2687 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2688 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2689
2690 if ($inner_closed || $outer_closed || $middle_closed) {
2691 // stay dates are closed
2692 if ($closingcmp) {
2693 // return the components affected by the globally closed dates to allow analysis
2694 return [
2695 'checkin' => $inner_closed,
2696 'checkout' => $outer_closed,
2697 'stay' => $middle_closed,
2698 ];
2699 }
2700
2701 // return the string with the range of dates closed globally
2702 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2703 }
2704 }
2705
2706 return '';
2707 }
2708
2709 /**
2710 * Whether the categories dropdown filter menu should be displayed.
2711 *
2712 * @param bool $skipsession [optional] re-read the configuration setting.
2713 *
2714 * @return bool
2715 *
2716 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2717 */
2718 public static function showCategoriesFront($skipsession = true)
2719 {
2720 return VBOFactory::getConfig()->getBool('showcategories', false);
2721 }
2722
2723 /**
2724 * Whether the number of children dropdown menu should be displayed.
2725 * Defaults to skip the session values and to re-read the configuration setting.
2726 *
2727 * @param bool $skipsession [optional] re-read the configuration setting.
2728 *
2729 * @return bool
2730 *
2731 * @since 1.18.3 (J) - 1.8.3 (WP) argument $skipsession is deprecated.
2732 */
2733 public static function showChildrenFront($skipsession = true)
2734 {
2735 return VBOFactory::getConfig()->getBool('showchildren', true);
2736 }
2737
2738 public static function allowBooking()
2739 {
2740 // cache value in static var
2741 static $allowBooking = null;
2742
2743 if (is_bool($allowBooking)) {
2744 return $allowBooking;
2745 }
2746
2747 $allowBooking = (bool) VBOFactory::getConfig()->get('allowbooking');
2748
2749 return $allowBooking;
2750 }
2751
2752 public static function getDisabledBookingMsg($vbo_tn = null) {
2753 $dbo = JFactory::getDbo();
2754 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2755 $dbo->setQuery($q);
2756 $dbo->execute();
2757 $s = $dbo->loadAssocList();
2758 if (!is_object($vbo_tn)) {
2759 $vbo_tn = self::getTranslator();
2760 }
2761 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2762 return $s[0]['setting'];
2763 }
2764
2765 /**
2766 * Returns the text for the guests allowed policy.
2767 *
2768 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2769 *
2770 * @return string either the raw or the translated policy text.
2771 *
2772 * @since 1.16.5 (WP) - 1.6.5 (WP)
2773 */
2774 public static function getGuestsAllowedPolicy($vbo_tn = null)
2775 {
2776 $dbo = JFactory::getDbo();
2777
2778 $q = $dbo->getQuery(true)
2779 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2780 ->from($dbo->qn('#__vikbooking_texts'))
2781 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2782
2783 $dbo->setQuery($q);
2784 $text_record = $dbo->loadAssocList();
2785
2786 if (!$text_record) {
2787 // missing record, insert it with empty values
2788 if (self::isAdmin()) {
2789 $record = new stdClass;
2790 $record->param = 'guests_allowed_policy';
2791 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2792 $record->setting = '';
2793 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2794 }
2795
2796 return '';
2797 }
2798
2799 if (is_object($vbo_tn)) {
2800 // translate the record only if requested
2801 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2802 }
2803
2804 return $text_record[0]['setting'];
2805 }
2806
2807 public static function getDateFormat($skipsession = true)
2808 {
2809 // cache value in static var
2810 static $getDateFormat = null;
2811
2812 if ($getDateFormat) {
2813 return $getDateFormat;
2814 }
2815
2816 $dbo = JFactory::getDbo();
2817
2818 if ($skipsession) {
2819 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2820 $dbo->setQuery($q, 0, 1);
2821 $getDateFormat = $dbo->loadResult();
2822
2823 return $getDateFormat;
2824 }
2825
2826 $session = JFactory::getSession();
2827 $sval = $session->get('vbgetDateFormat', '');
2828 if (!empty($sval)) {
2829 return $sval;
2830 }
2831
2832 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2833 $dbo->setQuery($q, 0, 1);
2834 $getDateFormat = $dbo->loadResult();
2835
2836 $session->set('vbgetDateFormat', $getDateFormat);
2837
2838 return $getDateFormat;
2839 }
2840
2841 public static function getDateSeparator($skipsession = true)
2842 {
2843 // cache value in static var
2844 static $getDateSeparator = null;
2845
2846 if ($getDateSeparator) {
2847 return $getDateSeparator;
2848 }
2849
2850 $dbo = JFactory::getDbo();
2851
2852 if ($skipsession) {
2853 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2854 $dbo->setQuery($q, 0, 1);
2855 $val = $dbo->loadResult();
2856
2857 $getDateSeparator = empty($val) ? "/" : $val;
2858
2859 return $getDateSeparator;
2860 }
2861
2862 $session = JFactory::getSession();
2863 $sval = $session->get('vbgetDateSep', '');
2864 if (!empty($sval)) {
2865 return $sval;
2866 }
2867
2868 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2869 $dbo->setQuery($q, 0, 1);
2870 $val = $dbo->loadResult();
2871
2872 $getDateSeparator = empty($val) ? "/" : $val;
2873
2874 $session->set('vbgetDateSep', $getDateSeparator);
2875
2876 return $getDateSeparator;
2877 }
2878
2879 public static function getHoursMoreRb($skipsession = false)
2880 {
2881 // cache value in static var
2882 static $getHoursMoreRb = null;
2883
2884 if ($getHoursMoreRb !== null) {
2885 return $getHoursMoreRb;
2886 }
2887
2888 $dbo = JFactory::getDbo();
2889
2890 if ($skipsession) {
2891 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2892 $dbo->setQuery($q, 0, 1);
2893 $getHoursMoreRb = (int)$dbo->loadResult();
2894
2895 return $getHoursMoreRb;
2896 }
2897
2898 $session = JFactory::getSession();
2899 $sval = $session->get('getHoursMoreRb', '');
2900 if (strlen($sval)) {
2901 return $sval;
2902 }
2903
2904 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2905 $dbo->setQuery($q, 0, 1);
2906 $getHoursMoreRb = (int)$dbo->loadResult();
2907
2908 $session->set('getHoursMoreRb', $getHoursMoreRb);
2909
2910 return $getHoursMoreRb;
2911 }
2912
2913 public static function getHoursRoomAvail()
2914 {
2915 $dbo = JFactory::getDbo();
2916
2917 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2918 $dbo->setQuery($q, 0, 1);
2919
2920 return (int)$dbo->loadResult();
2921 }
2922
2923 public static function getFrontTitle($vbo_tn = null) {
2924 // cache value in static var
2925 static $getFrontTitle = null;
2926
2927 if ($getFrontTitle) {
2928 return $getFrontTitle;
2929 }
2930 //
2931
2932 $dbo = JFactory::getDbo();
2933 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2934 $dbo->setQuery($q);
2935 $dbo->execute();
2936 $ft = $dbo->loadAssocList();
2937 if (!is_object($vbo_tn)) {
2938 $vbo_tn = self::getTranslator();
2939 }
2940 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2941
2942 $getFrontTitle = $ft[0]['setting'];
2943
2944 return $ft[0]['setting'];
2945 }
2946
2947 public static function getFrontTitleTag()
2948 {
2949 return VBOFactory::getConfig()->get('fronttitletag', '');
2950 }
2951
2952 public static function getFrontTitleTagClass()
2953 {
2954 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2955 }
2956
2957 /**
2958 * Returns the configured currency name. Often used to apply currency
2959 * conversions, and so it should match the currency ISO 4217 (Alpha-3) code.
2960 *
2961 * @param bool $iso True to ensure we fetch a 3-char currency code.
2962 *
2963 * @return string
2964 *
2965 * @since 1.18.0 (J) - 1.8.0 (WP) added argument $iso.
2966 */
2967 public static function getCurrencyName($iso = false)
2968 {
2969 static $currencyName = null;
2970
2971 if ($currencyName !== null) {
2972 return $currencyName;
2973 }
2974
2975 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2976
2977 if ($iso && strlen((string) $currencyName) != 3) {
2978 // attempt to fetch the currency code configuration setting
2979 $currencyName = self::getCurrencyCodePp();
2980 }
2981
2982 return $currencyName;
2983 }
2984
2985 public static function getCurrencySymb()
2986 {
2987 // cache value in static var
2988 static $getCurrencySymb = null;
2989
2990 if ($getCurrencySymb) {
2991 return $getCurrencySymb;
2992 }
2993
2994 $getCurrencySymb = VBOFactory::getConfig()->get('currencysymb', '');
2995
2996 return $getCurrencySymb;
2997 }
2998
2999 /**
3000 * Returns the configured currency position: either "before" or "after".
3001 *
3002 * @return string
3003 *
3004 * @since 1.18.3 (J) - 1.8.3 (WP)
3005 */
3006 public static function getCurrencyPosition()
3007 {
3008 // cache value in static var
3009 static $getCurrencyPos = null;
3010
3011 if ($getCurrencyPos) {
3012 return $getCurrencyPos;
3013 }
3014
3015 $getCurrencyPos = VBOFactory::getConfig()->get('currencypos', 'before');
3016
3017 return $getCurrencyPos;
3018 }
3019
3020 /**
3021 * Given a number, formats it according to currency preferences.
3022 *
3023 * @param string $number The number to format.
3024 * @param ?string $currency Optional currency to use.
3025 * @param ?array $config Optional format configuration (i.e. currency/number "HTML template").
3026 *
3027 * @return string
3028 *
3029 * @since 1.18.3 (J) - 1.8.3 (WP)
3030 */
3031 public static function formatCurrencyNumber(string $number, ?string $currency = null, ?array $config = null)
3032 {
3033 // get the currency symbol/value to use
3034 $currency = $currency ?: self::getCurrencySymb();
3035
3036 // obtain currency template (wildcard expected)
3037 if (is_string($config[0] ?? null) || is_string($config['currency'] ?? null)) {
3038 // apply templating to string
3039 $currency = sprintf($config[0] ?? $config['currency'], $currency);
3040 }
3041
3042 // obtain number template
3043 if (is_string($config[1] ?? null) || is_string($config['number'] ?? null)) {
3044 // apply templating to string
3045 $number = sprintf($config[1] ?? $config['number'], $number);
3046 }
3047
3048 // default value spacing (empty white space)
3049 $spacing = (string) ($config['spacing'] ?? ' ');
3050
3051 if (self::getCurrencyPosition() === 'after') {
3052 return sprintf('%s' . $spacing . '%s', $number, $currency);
3053 }
3054
3055 return sprintf('%s' . $spacing . '%s', $currency, $number);
3056 }
3057
3058 public static function getCurrencyCodePp()
3059 {
3060 static $currencyCode = null;
3061
3062 if ($currencyCode !== null) {
3063 return $currencyCode;
3064 }
3065
3066 $currencyCode = VBOFactory::getConfig()->get('currencycodepp', '');
3067
3068 return $currencyCode;
3069 }
3070
3071 public static function getNumberFormatData()
3072 {
3073 // cache value in static var
3074 static $getNumberFormatData = null;
3075
3076 if ($getNumberFormatData) {
3077 return $getNumberFormatData;
3078 }
3079
3080 $getNumberFormatData = VBOFactory::getConfig()->get('numberformat', '');
3081
3082 return $getNumberFormatData;
3083 }
3084
3085 /**
3086 * It is possible to hide the decimals if they are like "N.00".
3087 *
3088 * @return int 0 if disabled, 1 if enabled (default).
3089 *
3090 * @since 1.14 (J) - 1.4.0 (WP)
3091 */
3092 public static function hideEmptyDecimals()
3093 {
3094 // cache value in static var
3095 static $hideEmptyDecimals = null;
3096
3097 if ($hideEmptyDecimals !== null) {
3098 return $hideEmptyDecimals;
3099 }
3100
3101 $hideEmptyDecimals = (int) VBOFactory::getConfig()->get('noemptydecimals', '1');
3102
3103 return $hideEmptyDecimals;
3104 }
3105
3106 public static function numberFormat($num)
3107 {
3108 $formatvals = self::getNumberFormatData();
3109 $formatparts = explode(':', $formatvals);
3110
3111 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
3112 // number has got no decimals
3113 $formatparts[0] = 0;
3114 }
3115
3116 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
3117 }
3118
3119 /**
3120 * Given a date timestamp, returns the formatted date.
3121 *
3122 * @param int|string $ts the date representation in Unix timestamp.
3123 * @param bool $wtime if true, a formatted date-time will be returned.
3124 *
3125 * @return string the formatted date according to settings.
3126 *
3127 * @since 1.16.0 (J) - 1.6.0 (WP)
3128 */
3129 public static function formatDateTs($ts, $wtime = false)
3130 {
3131 $nowdf = self::getDateFormat();
3132 if ($nowdf == "%d/%m/%Y") {
3133 $df = 'd/m/Y';
3134 } elseif ($nowdf == "%m/%d/%Y") {
3135 $df = 'm/d/Y';
3136 } else {
3137 $df = 'Y/m/d';
3138 }
3139 $datesep = self::getDateSeparator();
3140
3141 $time_f = $wtime ? ' H:i' : '';
3142
3143 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
3144 }
3145
3146 /**
3147 * Given a country name, 3-char or 2-char code, this method attempts
3148 * to guess the best language to assign to the booking according to
3149 * what languages are installed on the website. This way, cron jobs
3150 * and any other email notification will be correctly and automatically
3151 * sent to the guest in the proper language without any manual action.
3152 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
3153 *
3154 * @param string $country the country name, 3-char or 2-char code.
3155 * @param string $locale optional locale lang tag of the guest.
3156 *
3157 * @return mixed the best lang-tag string to use or null.
3158 *
3159 * @since 1.16.0 (J) - 1.6.0 (WP)
3160 */
3161 public static function guessBookingLangFromCountry($country, $locale = null)
3162 {
3163 if (empty($country)) {
3164 return null;
3165 }
3166
3167 // get all the available languages
3168 $known_langs = self::getVboApplication()->getKnownLanguages();
3169 if (!is_array($known_langs) || !count($known_langs)) {
3170 return null;
3171 }
3172
3173 // check if the booking included a supported "locale" for the guest
3174 if (!empty($locale)) {
3175 // make the locale compatible with the language tags format
3176 $locale = str_replace('_', '-', $locale);
3177 foreach ($known_langs as $ltag => $ldet) {
3178 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
3179 // we support this language tag, so we just use it
3180 return $ltag;
3181 }
3182 }
3183 }
3184
3185 // build similarities with country-languages
3186 $similarities = array(
3187 'AU' => 'en',
3188 'GB' => 'en',
3189 'IE' => 'en',
3190 'NZ' => 'en',
3191 'US' => 'en',
3192 'CA' => array(
3193 'en',
3194 'fr',
3195 ),
3196 'CL' => 'es',
3197 'AR' => 'es',
3198 'PE' => 'es',
3199 'MX' => 'es',
3200 'CR' => 'es',
3201 'CO' => 'es',
3202 'EC' => 'es',
3203 'BO' => 'es',
3204 'CU' => 'es',
3205 'VE' => 'es',
3206 'BE' => 'fr',
3207 'LU' => 'fr',
3208 'CH' => array(
3209 'de',
3210 'it',
3211 'fr',
3212 ),
3213 'AT' => 'de',
3214 'GR' => 'el',
3215 'GL' => 'dk',
3216 );
3217
3218 // fetch values from db
3219 $dbo = JFactory::getDbo();
3220 $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);
3221 $dbo->setQuery($q, 0, 1);
3222 $dbo->execute();
3223 if (!$dbo->getNumRows()) {
3224 return null;
3225 }
3226 $country_record = $dbo->loadAssoc();
3227
3228 // assign country name/code versions
3229 $country_name = $country_record['country_name'];
3230 $country_3char = strtoupper($country_record['country_3_code']);
3231 $country_2char = strtoupper($country_record['country_2_code']);
3232
3233 // build an associative array of language tags and related match-score
3234 $langtags_score = array();
3235 foreach ($known_langs as $ltag => $ldet) {
3236 // default language tag score is 0 for no matches
3237 $langtags_score[$ltag] = 0;
3238 // get language and country codes
3239 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
3240
3241 // check matches with the installed language details
3242 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
3243 // increase language tag score
3244 $langtags_score[$ltag]++;
3245 }
3246 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
3247 // increase language tag score
3248 $langtags_score[$ltag]++;
3249 }
3250 if (!empty($ldet['locale'])) {
3251 // sanitize locale for matching the 2-char code safely
3252 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
3253 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
3254 // increase language tag score
3255 $langtags_score[$ltag]++;
3256 }
3257 }
3258 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
3259 // increase language tag score
3260 $langtags_score[$ltag]++;
3261 }
3262 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
3263 // increase language tag score
3264 $langtags_score[$ltag]++;
3265 }
3266
3267 // check language similarities between countries
3268 if (isset($similarities[$country_2char])) {
3269 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
3270 // check if language tag(s) is available for this spoken language
3271 foreach ($spoken_tags as $spoken_tag) {
3272 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
3273 // increase language tag score
3274 $langtags_score[$ltag]++;
3275 }
3276 }
3277 }
3278 }
3279
3280 // make sure at least one language tag has got some points
3281 if (max($langtags_score) === 0) {
3282 // no languages installed to honor this country
3283 return null;
3284 }
3285
3286 // sort language tag scores
3287 arsort($langtags_score);
3288
3289 // reset array pointer to the first (highest) element
3290 reset($langtags_score);
3291
3292 // return the language tag with the highest score
3293 return key($langtags_score);
3294 }
3295
3296 public static function getIntroMain($vbo_tn = null) {
3297 $dbo = JFactory::getDbo();
3298 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
3299 $dbo->setQuery($q);
3300 $dbo->execute();
3301 $ft = $dbo->loadAssocList();
3302 if (!is_object($vbo_tn)) {
3303 $vbo_tn = self::getTranslator();
3304 }
3305 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3306 return $ft[0]['setting'];
3307 }
3308
3309 public static function getClosingMain($vbo_tn = null) {
3310 $dbo = JFactory::getDbo();
3311 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3312 $dbo->setQuery($q);
3313 $dbo->execute();
3314 $ft = $dbo->loadAssocList();
3315 if (!is_object($vbo_tn)) {
3316 $vbo_tn = self::getTranslator();
3317 }
3318 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3319 return $ft[0]['setting'];
3320 }
3321
3322 public static function getFullFrontTitle($vbo_tn = null) {
3323 $dbo = JFactory::getDbo();
3324 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3325 $dbo->setQuery($q);
3326 $dbo->execute();
3327 $ft = $dbo->loadAssocList();
3328 if (!is_object($vbo_tn)) {
3329 $vbo_tn = self::getTranslator();
3330 }
3331 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3332 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3333 $dbo->setQuery($q);
3334 $dbo->execute();
3335 $fttag = $dbo->loadAssocList();
3336 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3337 $dbo->setQuery($q);
3338 $dbo->execute();
3339 $fttagclass = $dbo->loadAssocList();
3340 if (empty($ft[0]['setting'])) {
3341 return "";
3342 } else {
3343 if (empty($fttag[0]['setting'])) {
3344 return $ft[0]['setting'] . "<br/>\n";
3345 } else {
3346 $tag = str_replace("<", "", $fttag[0]['setting']);
3347 $tag = str_replace(">", "", $tag);
3348 $tag = str_replace("/", "", $tag);
3349 $tag = trim($tag);
3350 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3351 }
3352 }
3353 }
3354
3355 /**
3356 * Tells whether a date string is valid.
3357 *
3358 * @param ?string $date The date to evaluate.
3359 *
3360 * @return bool
3361 *
3362 * @since 1.18.8 (J) - 1.8.8 (WP) Military format Y-m-d is always accepted.
3363 */
3364 public static function dateIsValid(?string $date)
3365 {
3366 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", (string) $date)) {
3367 // date is in Y-m-d format (with no time)
3368 return true;
3369 }
3370
3371 $df = self::getDateFormat();
3372 $datesep = self::getDateSeparator();
3373 if (strlen((string) $date) != 10) {
3374 return false;
3375 }
3376 $cur_dsep = "/";
3377 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3378 $cur_dsep = $datesep;
3379 }
3380 $x = explode($cur_dsep, $date);
3381 if ($df == "%d/%m/%Y") {
3382 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3383 return false;
3384 }
3385 } elseif ($df == "%m/%d/%Y") {
3386 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3387 return false;
3388 }
3389 } else {
3390 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3391 return false;
3392 }
3393 }
3394 return true;
3395 }
3396
3397 public static function sayDateFormat() {
3398 $dbo = JFactory::getDbo();
3399 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3400 $dbo->setQuery($q);
3401 $dbo->execute();
3402 $s = $dbo->loadAssocList();
3403 if ($s[0]['setting'] == "%d/%m/%Y") {
3404 return JText::translate('VBCONFIGONETWELVE');
3405 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3406 return JText::translate('VBCONFIGONEMDY');
3407 } else {
3408 return JText::translate('VBCONFIGONETENTHREE');
3409 }
3410 }
3411
3412 /**
3413 * Calculates the Unix timestamp from the given date and
3414 * time. Avoids DST issues thanks to mktime. With prior releases,
3415 * DST issues may occur due to the sum of seconds.
3416 *
3417 * @param string $date the date string formatted with the current settings
3418 * @param int $h hours from 0 to 23 for check-in/check-out
3419 * @param int $m minutes from 0 to 59 for check-in/check-out
3420 * @param int $s seconds from 0 to 59 for check-in/check-out
3421 *
3422 * @return int the Unix timestamp of the date
3423 *
3424 * @since 1.0.14
3425 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3426 * signature modified for $h and $m that now take 0 as default value.
3427 */
3428 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3429 $df = self::getDateFormat();
3430 $datesep = self::getDateSeparator();
3431 $cur_dsep = "/";
3432 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3433 $cur_dsep = $datesep;
3434 }
3435 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3436 // date is in Y-m-d format (with no time)
3437 $cur_dsep = '-';
3438 $df = "%Y/%m/%d";
3439 }
3440 $x = explode($cur_dsep, $date);
3441 if (!(count($x) > 2)) {
3442 return 0;
3443 }
3444 if ($df == "%d/%m/%Y") {
3445 $month = (int)$x[1];
3446 $mday = (int)$x[0];
3447 $year = (int)$x[2];
3448 } elseif ($df == "%m/%d/%Y") {
3449 $month = (int)$x[0];
3450 $mday = (int)$x[1];
3451 $year = (int)$x[2];
3452 } else {
3453 $month = (int)$x[1];
3454 $mday = (int)$x[2];
3455 $year = (int)$x[0];
3456 }
3457 $h = empty($h) ? 0 : (int)$h;
3458 $m = empty($m) ? 0 : (int)$m;
3459 $s = $s > 0 && $s <= 59 ? $s : 0;
3460
3461 return mktime($h, $m, $s, $month, $mday, $year);
3462 }
3463
3464 public static function ivaInclusa($skipsession = false) {
3465 // cache value in static var
3466 static $getTaxIncluded = null;
3467
3468 if ($getTaxIncluded !== null) {
3469 return (bool)$getTaxIncluded;
3470 }
3471
3472 $dbo = JFactory::getDbo();
3473
3474 if ($skipsession) {
3475 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3476 $dbo->setQuery($q, 0, 1);
3477 $dbo->execute();
3478 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3479
3480 // cache value and return it
3481 $getTaxIncluded = $vat_incl_data;
3482
3483 return (bool)$vat_incl_data;
3484 }
3485
3486 $session = JFactory::getSession();
3487 $sval = $session->get('getTaxIncluded', '');
3488 if (strlen($sval)) {
3489 return (bool)$sval;
3490 }
3491
3492 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3493 $dbo->setQuery($q, 0, 1);
3494 $dbo->execute();
3495 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3496
3497 // update session value, cache it and return it
3498 $session->set('getTaxIncluded', $vat_incl_data);
3499 $getTaxIncluded = $vat_incl_data;
3500
3501 return (bool)$vat_incl_data;
3502 }
3503
3504 public static function showTaxOnSummaryOnly($skipsession = false) {
3505 if ($skipsession) {
3506 $dbo = JFactory::getDbo();
3507 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3508 $dbo->setQuery($q);
3509 $dbo->execute();
3510 $s = $dbo->loadAssocList();
3511 return (intval($s[0]['setting']) == 1 ? true : false);
3512 } else {
3513 $session = JFactory::getSession();
3514 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3515 if (strlen($sval) > 0) {
3516 return (intval($sval) == 1 ? true : false);
3517 } else {
3518 $dbo = JFactory::getDbo();
3519 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3520 $dbo->setQuery($q);
3521 $dbo->execute();
3522 $s = $dbo->loadAssocList();
3523 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3524 return (intval($s[0]['setting']) == 1 ? true : false);
3525 }
3526 }
3527 }
3528
3529 public static function tokenForm() {
3530 $dbo = JFactory::getDbo();
3531 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3532 $dbo->setQuery($q);
3533 $dbo->execute();
3534 $s = $dbo->loadAssocList();
3535 return (intval($s[0]['setting']) == 1 ? true : false);
3536 }
3537
3538 public static function getPaypalAcc() {
3539 $dbo = JFactory::getDbo();
3540 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3541 $dbo->setQuery($q);
3542 $dbo->execute();
3543 $s = $dbo->loadAssocList();
3544 return $s[0]['setting'];
3545 }
3546
3547 public static function getAccPerCent() {
3548 $dbo = JFactory::getDbo();
3549 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3550 $dbo->setQuery($q);
3551 $dbo->execute();
3552 $s = $dbo->loadAssocList();
3553 return $s[0]['setting'];
3554 }
3555
3556 public static function getTypeDeposit($skipsession = false) {
3557 if ($skipsession) {
3558 $dbo = JFactory::getDbo();
3559 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3560 $dbo->setQuery($q);
3561 $dbo->execute();
3562 $s = $dbo->loadAssocList();
3563 return $s[0]['setting'];
3564 } else {
3565 $session = JFactory::getSession();
3566 $sval = $session->get('getTypeDeposit', '');
3567 if (strlen($sval) > 0) {
3568 return $sval;
3569 } else {
3570 $dbo = JFactory::getDbo();
3571 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3572 $dbo->setQuery($q);
3573 $dbo->execute();
3574 $s = $dbo->loadAssocList();
3575 $session->set('getTypeDeposit', $s[0]['setting']);
3576 return $s[0]['setting'];
3577 }
3578 }
3579 }
3580
3581 public static function multiplePayments()
3582 {
3583 return VBOFactory::getConfig()->getBool('multipay', false);
3584 }
3585
3586 public static function getAdminMail()
3587 {
3588 return VBOFactory::getConfig()->getString('adminemail', '');
3589 }
3590
3591 public static function getSenderMail()
3592 {
3593 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3594
3595 return empty($sender) ? self::getAdminMail() : $sender;
3596 }
3597
3598 public static function getPaymentName($vbo_tn = null) {
3599 $dbo = JFactory::getDbo();
3600 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3601 $dbo->setQuery($q);
3602 $dbo->execute();
3603 $s = $dbo->loadAssocList();
3604 if (!is_object($vbo_tn)) {
3605 $vbo_tn = self::getTranslator();
3606 }
3607 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3608 return $s[0]['setting'];
3609 }
3610
3611 public static function getTermsConditions($vbo_tn = null) {
3612 //VBO 1.10
3613 $dbo = JFactory::getDbo();
3614 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3615 $dbo->setQuery($q);
3616 $dbo->execute();
3617 if ($dbo->getNumRows() > 0) {
3618 $s = $dbo->loadAssocList();
3619 if (!is_object($vbo_tn)) {
3620 $vbo_tn = self::getTranslator();
3621 }
3622 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3623 } else {
3624 //the record has never been saved. Compose it with the default lang definition
3625 $timeopst = self::getTimeOpenStore(true);
3626 if (is_array($timeopst)) {
3627 $openat = self::getHoursMinutes($timeopst[0]);
3628 $closeat = self::getHoursMinutes($timeopst[1]);
3629 } else {
3630 $openat = array(12, 0);
3631 $closeat = array(10, 0);
3632 }
3633 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3634 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3635 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3636 }
3637
3638 return $s[0]['setting'];
3639 }
3640
3641 public static function getMinutesLock($conv = false)
3642 {
3643 static $minutesLock = null;
3644
3645 if ($minutesLock === null) {
3646 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3647 }
3648
3649 if (!$minutesLock) {
3650 return 0;
3651 }
3652
3653 if ($conv) {
3654 return (time() + ($minutesLock * 60));
3655 }
3656
3657 return $minutesLock;
3658 }
3659
3660 /**
3661 * Checks if the room units are temporarily locked on the given dates.
3662 *
3663 * @param int $idroom the room ID.
3664 * @param int $units the total number of room units.
3665 * @param int $first the check-in timestamp.
3666 * @param int $second the check-out timestamp.
3667 * @param bool $occupied if true, the system will also consider the booked units.
3668 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3669 *
3670 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3671 *
3672 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3673 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3674 */
3675 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3676 {
3677 $dbo = JFactory::getDbo();
3678
3679 if ($units < 1) {
3680 return false;
3681 }
3682
3683 $actnow = time();
3684
3685 // clean up expired records
3686 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3687 $dbo->setQuery($q);
3688 $dbo->execute();
3689
3690 $secdiff = $second - $first;
3691 $daysdiff = $secdiff / 86400;
3692 if (is_int($daysdiff)) {
3693 if ($daysdiff < 1) {
3694 $daysdiff = 1;
3695 }
3696 } else {
3697 if ($daysdiff < 1) {
3698 $daysdiff = 1;
3699 } else {
3700 $sum = floor($daysdiff) * 86400;
3701 $newdiff = $secdiff - $sum;
3702 $maxhmore = self::getHoursMoreRb() * 3600;
3703 if ($maxhmore >= $newdiff) {
3704 $daysdiff = floor($daysdiff);
3705 } else {
3706 $daysdiff = ceil($daysdiff);
3707 }
3708 }
3709 }
3710 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3711 $tmplock_map = [];
3712
3713 // check temporarily locked records
3714 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3715 $dbo->setQuery($check);
3716 $busy = $dbo->loadAssocList();
3717 if ($busy) {
3718 foreach ($groupdays as $kg => $gday) {
3719 $bfound = 0;
3720 foreach ($busy as $bu) {
3721 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3722 $bfound++;
3723 }
3724 }
3725 $tmplock_map[$kg] = $bfound;
3726 if ($bfound >= $units) {
3727 return false;
3728 }
3729 }
3730 }
3731
3732 if ($occupied) {
3733 // check also the busy records and some the occupied units to the temporarily locked ones
3734 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3735 $dbo->setQuery($check);
3736 $busy = $dbo->loadAssocList();
3737 if (!$busy) {
3738 return true;
3739 }
3740
3741 foreach ($groupdays as $kg => $gday) {
3742 $bfound = 0;
3743 foreach ($busy as $bu) {
3744 if (in_array($bu['id'], $skip_busy_ids)) {
3745 // Booking modification
3746 continue;
3747 }
3748 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3749 $bfound++;
3750 }
3751 }
3752 // sum units occupied to the units temporarily locked
3753 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3754 if ($bfound >= $units) {
3755 return false;
3756 }
3757 }
3758 }
3759
3760 return true;
3761 }
3762
3763 public static function getGroupDays($first, $second, $daysdiff) {
3764 $ret = array();
3765 $ret[] = $first;
3766 if ($daysdiff > 1) {
3767 $start = getdate($first);
3768 $end = getdate($second);
3769 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3770 for($i = 1; $i < $daysdiff; $i++) {
3771 $checkday = $start['mday'] + $i;
3772 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3773 if ($dayts != $endcheck) {
3774 $ret[] = $dayts;
3775 }
3776 }
3777 }
3778 $ret[] = $second;
3779 return $ret;
3780 }
3781
3782 /**
3783 * Counts the hours of difference between the current
3784 * server time and the selected check-in date and time.
3785 *
3786 * @param int $checkin_ts
3787 * @param [int] $now_ts
3788 *
3789 * @return int
3790 */
3791 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3792 $hoursdiff = 0;
3793
3794 if (empty($now_ts)) {
3795 $now_ts = time();
3796 }
3797
3798 if ($now_ts >= $checkin_ts) {
3799 return $hoursdiff;
3800 }
3801
3802 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3803
3804 return $hoursdiff;
3805 }
3806
3807 /**
3808 * Loads all the occupied records for the given rooms and offset.
3809 *
3810 * @param array $roomids the optional list of room IDs to filter.
3811 * @param int $from_ts the optional base date to use.
3812 * @param int $max_ts the optional max date to use.
3813 * @param bool $closures whether to ignore, include or exclude closures.
3814 *
3815 * @return array
3816 *
3817 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3818 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3819 */
3820 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3821 {
3822 $base_ts = empty($from_ts) ? time() : $from_ts;
3823 $busy = [];
3824 $clauses = [];
3825
3826 if (is_array($roomids) && count($roomids)) {
3827 // filter busy records only by the requested room ids
3828 $roomids = array_map(function($rid) {
3829 return (int)$rid;
3830 }, $roomids);
3831 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3832 }
3833
3834 // exclude past reservations
3835 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3836
3837 if (!empty($max_ts)) {
3838 // exclude future reservations (recommended for large datasets)
3839 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3840 }
3841
3842 if (is_bool($closures)) {
3843 // include or exclude closures
3844 if ($closures) {
3845 $clauses[] = "`o`.`closure` = 1";
3846 } else {
3847 $clauses[] = "`o`.`closure` != 1";
3848 }
3849 }
3850
3851 $dbo = JFactory::getDbo();
3852
3853 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3854 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3855 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3856 WHERE " . implode(' AND ', $clauses) . ";";
3857 $dbo->setQuery($q);
3858 $allbusy = $dbo->loadAssocList();
3859 if ($allbusy) {
3860 foreach ($allbusy as $br) {
3861 if (!isset($busy[$br['idroom']])) {
3862 $busy[$br['idroom']] = [];
3863 }
3864 $busy[$br['idroom']][] = $br;
3865 }
3866 }
3867
3868 return $busy;
3869 }
3870
3871 /**
3872 * Loads all the busy records by excluding the closures.
3873 * Built for the page dashboard to calculate the actual
3874 * rooms occupancy by excluding the rooms closed.
3875 *
3876 * @param array $roomids the optional list of room IDs to filter.
3877 * @param int $from_ts the optional base date to use.
3878 * @param int $max_ts the optional max date to use.
3879 *
3880 * @return array the list of busy records.
3881 *
3882 * @since 1.12
3883 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3884 */
3885 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3886 {
3887 if (!is_array($roomids) || !count($roomids)) {
3888 return [];
3889 }
3890
3891 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3892 }
3893
3894 /**
3895 * Loads the occupied record IDs for a given booking ID.
3896 *
3897 * @param int $idorder The booking record ID.
3898 *
3899 * @return array List of busy IDs, if any.
3900 */
3901 public static function loadBookingBusyIds($idorder)
3902 {
3903 if (empty($idorder)) {
3904 return [];
3905 }
3906
3907 $dbo = JFactory::getDbo();
3908
3909 $dbo->setQuery(
3910 $dbo->getQuery(true)
3911 ->select($dbo->qn('idbusy'))
3912 ->from($dbo->qn('#__vikbooking_ordersbusy'))
3913 ->where($dbo->qn('idorder') . ' = ' . (int) $idorder)
3914 );
3915
3916 return array_map('intval', array_column($dbo->loadAssocList(), 'idbusy'));
3917 }
3918
3919 public static function loadLockedRecords($roomids, $ts = 0) {
3920 $dbo = JFactory::getDbo();
3921 $actnow = empty($ts) ? time() : $ts;
3922 $locked = array();
3923 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3924 $dbo->setQuery($q);
3925 $dbo->execute();
3926 if (!is_array($roomids) || !(count($roomids) > 0)) {
3927 return $locked;
3928 }
3929 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3930 $dbo->setQuery($check);
3931 $all_locked = $dbo->loadAssocList();
3932 if ($all_locked) {
3933 foreach ($all_locked as $kb => $br) {
3934 $locked[$br['idroom']][$kb] = $br;
3935 }
3936 }
3937 return $locked;
3938 }
3939
3940 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3941 $bookings = [];
3942 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3943 return $bookings;
3944 }
3945 $dbo = JFactory::getDbo();
3946 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3947 $dbo->setQuery($q);
3948 $all_booking_ids = $dbo->loadAssocList();
3949 if ($all_booking_ids) {
3950 $oids = array();
3951 foreach ($all_booking_ids as $bid) {
3952 $oids[] = $bid['idorder'];
3953 }
3954 $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` ".
3955 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3956 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3957 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3958 "ORDER BY `o`.`checkout` ASC;";
3959 $dbo->setQuery($q);
3960 $bookings = $dbo->loadAssocList();
3961 if (!$bookings) {
3962 $bookings = [];
3963 }
3964 }
3965 return $bookings;
3966 }
3967
3968 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3969 {
3970 $dbo = JFactory::getDbo();
3971
3972 if ($units < 1) {
3973 return false;
3974 }
3975
3976 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3977 if (!$room_info) {
3978 return false;
3979 }
3980
3981 $secdiff = $second - $first;
3982 $daysdiff = $secdiff / 86400;
3983 if (is_int($daysdiff)) {
3984 if ($daysdiff < 1) {
3985 $daysdiff = 1;
3986 }
3987 } else {
3988 if ($daysdiff < 1) {
3989 $daysdiff = 1;
3990 } else {
3991 $sum = floor($daysdiff) * 86400;
3992 $newdiff = $secdiff - $sum;
3993 $maxhmore = self::getHoursMoreRb() * 3600;
3994 if ($maxhmore >= $newdiff) {
3995 $daysdiff = floor($daysdiff);
3996 } else {
3997 $daysdiff = ceil($daysdiff);
3998 }
3999 }
4000 }
4001
4002 $q = $dbo->getQuery(true)
4003 ->select($dbo->qn([
4004 'id',
4005 'checkin',
4006 'realback',
4007 ]))
4008 ->from($dbo->qn('#__vikbooking_busy'))
4009 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
4010 ->where($dbo->qn('realback') . ' >= ' . $first);
4011
4012 $dbo->setQuery($q);
4013 $busy = $dbo->loadAssocList();
4014 if (!$busy) {
4015 return ($units <= ($room_info['units'] ?? 1));
4016 }
4017
4018 $groupdays = self::getGroupDays($first, $second, $daysdiff);
4019 foreach ($groupdays as $gday) {
4020 $bfound = 0;
4021 foreach ($busy as $bu) {
4022 if (in_array($bu['id'], $skip_busy_ids)) {
4023 // booking modification
4024 continue;
4025 }
4026 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
4027 $bfound++;
4028 }
4029 }
4030 if ($bfound >= $units) {
4031 return false;
4032 }
4033 }
4034
4035 return ($units <= ($room_info['units'] ?? 1));
4036 }
4037
4038 public static function payTotal()
4039 {
4040 return VBOFactory::getConfig()->getBool('paytotal', true);
4041 }
4042
4043 public static function getDepositIfDays()
4044 {
4045 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
4046 }
4047
4048 public static function depositAllowedDaysAdv($checkints) {
4049 $days_adv = self::getDepositIfDays();
4050 if (!($days_adv > 0) || !($checkints > 0)) {
4051 return true;
4052 }
4053 $now_info = getdate();
4054 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
4055 return $maxts > $checkints ? false : true;
4056 }
4057
4058 public static function depositCustomerChoice() {
4059 $dbo = JFactory::getDbo();
4060 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
4061 $dbo->setQuery($q);
4062 $dbo->execute();
4063 $s = $dbo->loadAssocList();
4064 return (intval($s[0]['setting']) == 1 ? true : false);
4065 }
4066
4067 public static function getDepositOverrides($getjson = false) {
4068 $dbo = JFactory::getDbo();
4069 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
4070 $dbo->setQuery($q);
4071 $dbo->execute();
4072 if ($dbo->getNumRows() > 0) {
4073 $s = $dbo->loadResult();
4074 return $getjson ? $s : json_decode($s, true);
4075 }
4076 //count of this array will be at least 1 to store the "more" property
4077 $def_arr = array('more' => '');
4078 $def_val = json_encode($def_arr);
4079 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
4080 $dbo->setQuery($q);
4081 $dbo->execute();
4082 return $getjson ? $def_val : $def_arr;
4083 }
4084
4085 public static function calcDepositOverride($amount_deposit, $nights) {
4086 $overrides = self::getDepositOverrides();
4087 $nights = intval($nights);
4088 $andmore = intval($overrides['more']);
4089 if (!(count($overrides) > 1)) {
4090 //no overrides
4091 return $amount_deposit;
4092 }
4093 foreach ($overrides as $k => $v) {
4094 if ((int)$k == $nights && strlen($v) > 0) {
4095 //exact override found
4096 return (float)$v;
4097 }
4098 }
4099 if ($andmore > 0 && $andmore <= $nights) {
4100 foreach ($overrides as $k => $v) {
4101 if ((int)$k == $andmore && strlen($v) > 0) {
4102 //"and more" nights found
4103 return (float)$v;
4104 }
4105 }
4106 }
4107 //nothing was found
4108 return $amount_deposit;
4109 }
4110
4111 public static function noDepositForNonRefund() {
4112 $dbo = JFactory::getDbo();
4113 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
4114 $dbo->setQuery($q);
4115 $dbo->execute();
4116 if ($dbo->getNumRows() > 0) {
4117 $s = $dbo->loadResult();
4118 return ((int)$s == 1);
4119 }
4120 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
4121 $dbo->setQuery($q);
4122 $dbo->execute();
4123 //default to false
4124 return false;
4125 }
4126
4127 /**
4128 * This method returns the room-rate array with the lowest price
4129 * that matches the preferred rate plan parameter (if available).
4130 * The array $website_rates could not be an array, or it could be
4131 * an array with the error string (response from the TACVBO Class).
4132 * The method has been introduced in VBO 1.10 and it's mainly used
4133 * by the module mod_vikbooking_channelrates and its ajax requests.
4134 *
4135 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
4136 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
4137 *
4138 * @return array
4139 */
4140 public static function getBestRoomRate($website_rates, $def_rplan) {
4141 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
4142 return array();
4143 }
4144 $best_room_rate = array();
4145 foreach ($website_rates as $rid => $tars) {
4146 foreach ($tars as $tar) {
4147 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
4148 //the array $website_rates is already sorted by price ASC, so we take the first useful array
4149 $best_room_rate = $tar;
4150 break 2;
4151 }
4152 }
4153 }
4154 if (!(count($best_room_rate) > 0)) {
4155 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
4156 foreach ($website_rates as $rid => $tars) {
4157 foreach ($tars as $tar) {
4158 $best_room_rate = $tar;
4159 break 2;
4160 }
4161 }
4162 }
4163
4164 return $best_room_rate;
4165 }
4166
4167 /**
4168 * This method returns an array with the details
4169 * of all channels in VCM that supports AV requests,
4170 * and that have at least one room type mapped.
4171 * The method invokes the Logos Class to return details
4172 * about the name and logo URL of the channel.
4173 * The method has been introduced in VBO 1.10 and it's mainly used
4174 * by the module mod_vikbooking_channelrates and its ajax requests.
4175 *
4176 * @param array $channels an array of channel IDs to be mapped on the VCM relations
4177 *
4178 * @return array
4179 */
4180 public static function getChannelsMap($channels) {
4181 if (!is_array($channels) || !(count($channels))) {
4182 return array();
4183 }
4184 $vcm_logos = self::getVcmChannelsLogo('', true);
4185 if (!is_object($vcm_logos)) {
4186 return array();
4187 }
4188 $channels_ids = array();
4189 foreach ($channels as $chid) {
4190 $ichid = (int)$chid;
4191 if ($ichid < 1) {
4192 continue;
4193 }
4194 array_push($channels_ids, $ichid);
4195 }
4196 if (!(count($channels_ids))) {
4197 return array();
4198 }
4199 $dbo = JFactory::getDbo();
4200 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
4201 $dbo->setQuery($q);
4202 $dbo->execute();
4203 if ($dbo->getNumRows() < 1) {
4204 return array();
4205 }
4206 $channels_names = $dbo->loadAssocList();
4207 $channels_map = array();
4208 foreach ($channels_names as $ch) {
4209 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
4210 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
4211 $chdata = array(
4212 'id' => $ch['idchannel'],
4213 'name' => ucwords($ch['channel']),
4214 'logo' => $ota_logo_url
4215 );
4216 array_push($channels_map, $chdata);
4217 }
4218 return $channels_map;
4219 }
4220
4221 /**
4222 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
4223 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
4224 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
4225 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
4226 * module/widget and its ajax requests.
4227 *
4228 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
4229 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
4230 *
4231 * @return mixed string (empty or indicating the alteration), or associative array per channel.
4232 *
4233 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
4234 */
4235 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
4236 {
4237 $otas_rates_val = '';
4238 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
4239 return $otas_rates_val;
4240 }
4241 if (!class_exists('VikChannelManager')) {
4242 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
4243 }
4244 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
4245 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
4246 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
4247 // the Bulk Rates Cache contains data for this room type and rate plan
4248 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
4249 // build rates modification string
4250 if ((int)$data_cont['rmod'] > 0) {
4251 // rates were modified for the OTAs, check how
4252 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
4253 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
4254 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
4255 // check alterations per channel, if requested
4256 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
4257 // we compose an associative array with the alteration for each channel (0 = generic rule)
4258 $channel_alterations = array($otas_rates_val);
4259 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
4260 if ((int)$ch_mods['rmod'] > 0) {
4261 // custom rate alteration for this channel
4262 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
4263 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
4264 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
4265 } else {
4266 // no rates alteration for this channel identifier
4267 $ch_alter_string = '+0%';
4268 }
4269 $channel_alterations[$ch_id] = $ch_alter_string;
4270 }
4271 // overwrite return value with associative array
4272 $otas_rates_val = $channel_alterations;
4273 }
4274 }
4275 }
4276 }
4277
4278 return $otas_rates_val;
4279 }
4280
4281 /**
4282 * This method checks if some non-refundable rates were selected
4283 * (`free_cancellation`=0), the only argument is an array of tariffs.
4284 * The property 'idprice' must be defined on each sub-array.
4285 *
4286 * @param $tars array
4287 *
4288 * @return boolean
4289 **/
4290 public static function findNonRefundableRates($tars = [])
4291 {
4292 $id_prices = [];
4293
4294 foreach ($tars as $tar) {
4295 if (isset($tar['idprice'])) {
4296 if (!in_array($tar['idprice'], $id_prices)) {
4297 array_push($id_prices, (int)$tar['idprice']);
4298 }
4299 continue;
4300 }
4301 foreach ($tar as $t) {
4302 if (isset($t['idprice'])) {
4303 if (!in_array($t['idprice'], $id_prices)) {
4304 array_push($id_prices, (int)$t['idprice']);
4305 }
4306 }
4307 }
4308 }
4309
4310 if (!count($id_prices)) {
4311 // no price IDs found (probably a package)
4312 return false;
4313 }
4314
4315 $dbo = JFactory::getDbo();
4316
4317 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
4318 $dbo->setQuery($q);
4319 $dbo->execute();
4320
4321 return (bool)($dbo->getNumRows() > 0);
4322 }
4323
4324 /**
4325 * This method checks if the deposit is allowed depending on
4326 * the selected rate plans (idprice) for the rooms reserved.
4327 * If the configuration setting is enabled, and if some
4328 * non-refundable rates were selected (`free_cancellation`=0),
4329 * the method will return false, because the deposit is not allowed.
4330 * The only argument is an array of tariffs. The property 'idprice'
4331 * must be defined on each sub-array (multi-dimension supported)
4332 * throgh the method findNonRefundableRates();
4333 *
4334 * @param $tars array
4335 *
4336 * @return boolean
4337 **/
4338 public static function allowDepositFromRates($tars) {
4339 if (!self::noDepositForNonRefund()) {
4340 //deposit can be paid also if non-refundable rates
4341 return true;
4342 }
4343 return !self::findNonRefundableRates($tars);
4344 }
4345
4346 public static function showSearchSuggestions() {
4347 $dbo = JFactory::getDbo();
4348 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4349 $dbo->setQuery($q);
4350 $dbo->execute();
4351 if ($dbo->getNumRows() > 0) {
4352 return (int)$dbo->loadResult();
4353 }
4354 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4355 $dbo->setQuery($q);
4356 $dbo->execute();
4357 return 1;
4358 }
4359
4360 /**
4361 * Given a code, returns the coupon information array.
4362 *
4363 * @param string $code the coupon code.
4364 *
4365 * @return array empty array or coupon record.
4366 *
4367 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4368 */
4369 public static function getCouponInfo($code)
4370 {
4371 $dbo = JFactory::getDbo();
4372
4373 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4374 $dbo->setQuery($q, 0, 1);
4375 $coupon = $dbo->loadAssoc();
4376 if (!$coupon) {
4377 return [];
4378 }
4379
4380 // check for customers assigned
4381 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4382 $dbo->setQuery($q);
4383 $coupon_customers = $dbo->loadAssocList();
4384 if ($coupon_customers) {
4385 // set customers and automatic properties
4386 $coupon['customers'] = [];
4387 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4388 foreach ($coupon_customers as $coupon_customer) {
4389 // push customer ID
4390 $coupon['customers'][] = $coupon_customer['idcustomer'];
4391 }
4392 }
4393
4394 return $coupon;
4395 }
4396
4397 /**
4398 * Reads the details of a given room record ID.
4399 *
4400 * @param int $idroom The room ID to fetch.
4401 * @param array $columns The optional room columns to fetch.
4402 * @param bool $no_cache Whether to ignore a previously cached record.
4403 *
4404 * @return array
4405 *
4406 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4407 */
4408 public static function getRoomInfo($idroom, $columns = [], $no_cache = false)
4409 {
4410 static $room_infos = [];
4411
4412 if (isset($room_infos[$idroom]) && !$no_cache) {
4413 return $room_infos[$idroom];
4414 }
4415
4416 $dbo = JFactory::getDbo();
4417
4418 if ($columns) {
4419 $columns = array_map([$dbo, 'qn'], $columns);
4420 }
4421
4422 $q = $dbo->getQuery(true)
4423 ->select($columns ? $columns : '*')
4424 ->from($dbo->qn('#__vikbooking_rooms'))
4425 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4426
4427 $dbo->setQuery($q);
4428 $room = $dbo->loadAssoc();
4429
4430 if ($no_cache) {
4431 return $room ?: [];
4432 }
4433
4434 $room_infos[$idroom] = $room ?: [];
4435
4436 return $room_infos[$idroom];
4437 }
4438
4439 public static function loadOrdersRoomsData($idorder)
4440 {
4441 $dbo = JFactory::getDbo();
4442
4443 $q = $dbo->getQuery(true)
4444 ->select($dbo->qn('or') . '.*')
4445 ->select($dbo->qn('r.name', 'room_name'))
4446 ->select($dbo->qn('r.units', 'tot_units'))
4447 ->select($dbo->qn('r.params'))
4448 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4449 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4450 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4451 ->order($dbo->qn('or.id') . ' ASC');
4452
4453 $dbo->setQuery($q);
4454
4455 return $dbo->loadAssocList();
4456 }
4457
4458 public static function sayCategory($ids, $vbo_tn = null)
4459 {
4460 $dbo = JFactory::getDbo();
4461 $split = explode(";", $ids);
4462 $say = "";
4463 foreach ($split as $k => $s) {
4464 if (!strlen($s)) {
4465 continue;
4466 }
4467 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4468 $dbo->setQuery($q);
4469 $nam = $dbo->loadAssocList();
4470 if (!$nam) {
4471 continue;
4472 }
4473 if (is_object($vbo_tn)) {
4474 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4475 }
4476 $say .= $nam[0]['name'];
4477 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4478 }
4479 return $say;
4480 }
4481
4482 /**
4483 * Returns a list of the given room amenities string.
4484 *
4485 * @param string $amenities Room amenities string.
4486 * @param ?object $vbo_tn Optional VBOTranslator.
4487 * @param bool $all True for loading all records when no amenities given.
4488 *
4489 * @return array
4490 *
4491 * @since 1.17.3 (J) - 1.7.3 (WP)
4492 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $all.
4493 */
4494 public static function loadRoomAmenities($amenities, $vbo_tn = null, $all = false)
4495 {
4496 $dbo = JFactory::getDbo();
4497
4498 $split = explode(";", (string) $amenities);
4499 $where = [];
4500 foreach ($split as $s) {
4501 if (!empty($s)) {
4502 $where[] = (int) $s;
4503 }
4504 }
4505
4506 $list = [];
4507 $where = array_filter($where);
4508
4509 $query = $dbo->getQuery(true)
4510 ->select('*')
4511 ->from($dbo->qn('#__vikbooking_characteristics'))
4512 ->order($dbo->qn('ordering') . ' ASC');
4513
4514 if ($where) {
4515 $query->where($dbo->qn('id') . ' IN (' . implode(', ', $where) . ')');
4516 }
4517
4518 if ($where || $all) {
4519 $dbo->setQuery($query);
4520 $list = $dbo->loadAssocList();
4521 if ($list && is_object($vbo_tn)) {
4522 $vbo_tn->translateContents($list, '#__vikbooking_characteristics');
4523 }
4524 }
4525
4526 return $list;
4527 }
4528
4529 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4530 {
4531 $carat = '';
4532 $arr = self::loadRoomAmenities($idc, $vbo_tn);
4533
4534 if ($arr) {
4535 $carat .= "<div class=\"vbo-room-carats\">\n";
4536 foreach ($arr as $a) {
4537 if (!empty($a['textimg'])) {
4538 //tooltip icon text is not empty
4539 if (!empty($a['icon'])) {
4540 //an icon has been uploaded: display the image
4541 $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";
4542 } else {
4543 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4544 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4545 $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";
4546 } else {
4547 //display just the text
4548 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4549 }
4550 }
4551 } else {
4552 $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");
4553 }
4554 }
4555 $carat .= "</div>\n";
4556 }
4557
4558 return $carat;
4559 }
4560
4561 public static function getRoomOptionals($idopts, $vbo_tn = null)
4562 {
4563 $dbo = JFactory::getDbo();
4564 $split = explode(";", $idopts);
4565 $fetch = array();
4566 foreach ($split as $s) {
4567 if (!empty($s)) {
4568 $fetch[] = $s;
4569 }
4570 }
4571 if ($fetch) {
4572 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', array_map('intval', $fetch)) . ") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4573 $dbo->setQuery($q);
4574 $arr = $dbo->loadAssocList();
4575 if ($arr) {
4576 if (is_object($vbo_tn)) {
4577 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4578 }
4579 return $arr;
4580 }
4581 }
4582 return [];
4583 }
4584
4585 public static function getSingleOption($idopt, $vbo_tn = null)
4586 {
4587 $dbo = JFactory::getDbo();
4588 $opt = array();
4589 if (!empty($idopt)) {
4590 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4591 $dbo->setQuery($q);
4592 $opt = $dbo->loadAssoc();
4593 if ($opt && is_object($vbo_tn)) {
4594 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4595 }
4596 }
4597 return $opt;
4598 }
4599
4600 /**
4601 * Unsets the options that are not available in the requested dates.
4602 *
4603 * @param array $optionals the array of options passed by reference
4604 * @param int $checkin the timestamp of the check-in date and time
4605 * @param int $checkout the timestamp of the check-out date and time
4606 *
4607 * @return void
4608 *
4609 * @since 1.11
4610 */
4611 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4612 {
4613 if (!$optionals || empty($checkin) || empty($checkout)) {
4614 return;
4615 }
4616
4617 foreach ($optionals as $k => $v) {
4618 if (!empty($v['alwaysav'])) {
4619 $dates = explode(';', $v['alwaysav']);
4620 if (empty($dates[0]) || empty($dates[1])) {
4621 continue;
4622 }
4623 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4624 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4625 unset($optionals[$k]);
4626 }
4627 }
4628 }
4629 }
4630
4631 /**
4632 * Unsets the options that are not suited for the requested room party.
4633 *
4634 * @param array $optionals the array of options passed by reference.
4635 * @param int $adults the number of adults in the room party.
4636 * @param int $children the number of children in the room party.
4637 *
4638 * @return void
4639 *
4640 * @since 1.13.5
4641 */
4642 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4643 {
4644 if (!$optionals || (empty($adults) && empty($children))) {
4645 return;
4646 }
4647
4648 foreach ($optionals as $k => $v) {
4649 if (empty($v['oparams'])) {
4650 continue;
4651 }
4652 $v['oparams'] = json_decode($v['oparams'], true);
4653 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4654 continue;
4655 }
4656 if (!empty($v['oparams']['minguestsnum'])) {
4657 // filter by minimum adults/guests
4658 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4659 // minimum number of adults not sufficient, unset option
4660 unset($optionals[$k]);
4661 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4662 // minimum number of total guests not sufficient, unset option
4663 unset($optionals[$k]);
4664 }
4665 }
4666 if (!empty($v['oparams']['maxguestsnum'])) {
4667 // filter by maximum adults/guests
4668 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4669 // maximum number of adults exceeded, unset option
4670 unset($optionals[$k]);
4671 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4672 // maximum number of total guests exceeded, unset option
4673 unset($optionals[$k]);
4674 }
4675 }
4676 }
4677 }
4678
4679 /**
4680 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4681 *
4682 * @param array $id_rooms list of room IDs.
4683 * @param int $num_adults number of adults.
4684 * @param int $num_nights number of nights of stay.
4685 * @param int $checkin optional check-in timestamp.
4686 * @param int $checkout optional check-out timestamp.
4687 *
4688 * @return array associative information of city taxes, fees and options.
4689 *
4690 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4691 */
4692 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4693 {
4694 $dbo = JFactory::getDbo();
4695
4696 $taxes = 0;
4697 $fees = 0;
4698
4699 $options_data = [];
4700 $id_options = [];
4701
4702 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4703 $dbo->setQuery($q);
4704 $assocs = $dbo->loadAssocList();
4705 if ($assocs) {
4706 foreach ($assocs as $opts) {
4707 if (!empty($opts['idopt'])) {
4708 $r_ido = explode(';', rtrim($opts['idopt']));
4709 foreach ($r_ido as $ido) {
4710 if (!empty($ido) && !in_array($ido, $id_options)) {
4711 $id_options[] = (int)$ido;
4712 }
4713 }
4714 }
4715 }
4716 }
4717
4718 if ($id_options) {
4719 $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);";
4720 $dbo->setQuery($q);
4721 $alltaxesfees = $dbo->loadAssocList();
4722
4723 if ($checkin && $checkout) {
4724 // filter the records by stay dates to support, for example, seasonal city taxes
4725 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4726 }
4727
4728 foreach ($alltaxesfees as $tf) {
4729 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4730 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4731 $realcost = $tf['maxprice'];
4732 }
4733 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4734
4735 /**
4736 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
4737 *
4738 * @since 1.17.7 (J) - 1.7.7 (WP)
4739 */
4740 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$tf, ['days' => $num_nights], ['adults' => $num_adults]]);
4741 if ($custom_calculation) {
4742 $realcost = (float) $custom_calculation[0];
4743 }
4744
4745 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4746 if ($tf['is_citytax'] == 1) {
4747 $taxes += $realcost;
4748 } elseif ($tf['is_fee'] == 1) {
4749 $fees += $realcost;
4750 }
4751 $optsett = explode('-', $tf['forceval']);
4752 $options_data[] = $tf['id'].':'.$optsett[0];
4753 }
4754 }
4755
4756 return [
4757 'city_taxes' => $taxes,
4758 'fees' => $fees,
4759 'options' => $options_data
4760 ];
4761 }
4762
4763 /**
4764 * From a list of option/extras records, it returns an array with the options/extras
4765 * that do not support age intervals, and options for the children age intervals.
4766 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4767 *
4768 * @param array $optionals the full list of options/extras records.
4769 * @param int $adults the number of adults in the room requested.
4770 * @param int $children the number of children in the room requested.
4771 *
4772 * @return array the filtered regular and children age intervals options.
4773 */
4774 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4775 {
4776 // container for age intervals
4777 $ageintervals = [];
4778
4779 // regular options should start as an array
4780 if (!is_array($optionals)) {
4781 $optionals = [];
4782 }
4783
4784 // check for the first valid age intervals option
4785 foreach ($optionals as $kopt => $opt) {
4786 if (!empty($opt['ageintervals'])) {
4787 $intervals = explode(';;', $opt['ageintervals']);
4788 foreach ($intervals as $intv) {
4789 if (empty($intv)) {
4790 continue;
4791 }
4792 $parts = explode('_', $intv);
4793 if (count($parts) >= 3) {
4794 // age intervals option found, get the first and only one
4795 $ageintervals = $optionals[$kopt];
4796 break 2;
4797 }
4798 }
4799 }
4800 }
4801
4802 if ($ageintervals) {
4803 /**
4804 * We allow price adjustments for a minimum total guests by overriding the costs
4805 * for each age interval for specific child numbers when the party covers some children.
4806 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4807 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4808 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4809 *
4810 * @since 1.13.5
4811 */
4812 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4813 $oparams = !is_array($oparams) ? array() : $oparams;
4814 $adults = (int)$adults;
4815 $children = (int)$children;
4816 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4817 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4818 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4819 if ($children_to_pay < $children) {
4820 // compose string for free age intervals
4821 $intervals = explode(';;', $ageintervals['ageintervals']);
4822 foreach ($intervals as $k => $v) {
4823 $parts = explode('_', $v);
4824 // set cost to 0 (key = 2)
4825 $parts[2] = '0';
4826 $intervals[$k] = implode('_', $parts);
4827 }
4828 $free_ageintervals = implode(';;', $intervals);
4829 for ($i = 1; $i <= $children; $i++) {
4830 if (($children - $i + 1) <= $children_to_pay) {
4831 // costs for this Nth last child should be regular as this child should pay
4832 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4833 } else {
4834 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4835 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4836 }
4837 }
4838 }
4839 }
4840
4841 // remove age intervals from regular options
4842 foreach ($optionals as $kopt => $opt) {
4843 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4844 // unset the option of type age intervals from the regular options
4845 unset($optionals[$kopt]);
4846 }
4847 }
4848 }
4849
4850 // return the filtered list of regular options and age intervals options
4851 return [$optionals, $ageintervals];
4852 }
4853
4854 /**
4855 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4856 * This is because some children in the party may not need to pay anything due to the min guests.
4857 *
4858 * @param array $optional the option full record to parse.
4859 * @param int $adults the number of adults in the room requested.
4860 * @param int $children the number of children in the room requested.
4861 *
4862 * @return array associative array with default age intervals and override strings.
4863 *
4864 * @since 1.13.5
4865 */
4866 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4867 {
4868 if (!is_array($optional)) {
4869 $optional = array();
4870 }
4871
4872 $overrides = array(
4873 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4874 );
4875
4876 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4877 $oparams = !is_array($oparams) ? array() : $oparams;
4878 $adults = (int)$adults;
4879 $children = (int)$children;
4880
4881 if (!count($oparams)) {
4882 return $overrides;
4883 }
4884
4885 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4886 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4887 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4888 if ($children_to_pay < $children) {
4889 // compose string for free age intervals
4890 $intervals = explode(';;', $optional['ageintervals']);
4891 foreach ($intervals as $k => $v) {
4892 $parts = explode('_', $v);
4893 // set cost to 0 (key = 2)
4894 $parts[2] = '0';
4895 $intervals[$k] = implode('_', $parts);
4896 }
4897 $free_ageintervals = implode(';;', $intervals);
4898 for ($i = 1; $i <= $children; $i++) {
4899 if (($children - $i + 1) <= $children_to_pay) {
4900 // costs for this Nth last child should be regular as this child should pay
4901 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4902 } else {
4903 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4904 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4905 }
4906 }
4907 }
4908 }
4909
4910 return $overrides;
4911 }
4912
4913 /**
4914 * Returns the number/index of the children being parsed given the full list of room options string,
4915 * the current key in the array of the split room options string, the ID of the ageintervals option,
4916 * and the total number of children. This is useful for later applying the cost overrides for the
4917 * children ages through the method getOptionIntervalChildOverrides().
4918 *
4919 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4920 * @param int $optid the ID of the option of type age intervals to check.
4921 * @param int $roptkey the current position in the loop of the room options string.
4922 * @param int $children the number of children in the room requested.
4923 *
4924 * @return int the position/number of the children being parsed, -1 if not found.
4925 *
4926 * @since 1.13.5
4927 */
4928 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4929 {
4930 // default to -1 for not found
4931 $child_num = -1;
4932
4933 $valid_opt_counter = 0;
4934
4935 $roptions = explode(";", $roptstr);
4936 foreach ($roptions as $k => $opt) {
4937 if (empty($opt)) {
4938 continue;
4939 }
4940 $optvals = explode(":", $opt);
4941 /**
4942 * In some cases, like the "saveorder" task, the room option string may contain
4943 * the room number beside the option ID, separated with an underscore.
4944 * If an underscore is present, we need to split it to find the option ID.
4945 */
4946 if (strpos($optvals[0], '_') !== false) {
4947 // underscore found in room number portion, extract the option ID
4948 $rn_parts = explode('_', $optvals[0]);
4949 // 0th element is the room number in the party, 1st elem is the option ID
4950 $optvals[0] = $rn_parts[1];
4951 }
4952 //
4953 if ((int)$optvals[0] != (int)$optid) {
4954 // we are not interested into this option ID
4955 continue;
4956 }
4957 // increase counter for this option ID
4958 $valid_opt_counter++;
4959
4960 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4961 // children position found, we need to return it as a 0th base
4962 $child_num = ($valid_opt_counter - 1);
4963 break;
4964 }
4965 }
4966
4967 return $child_num;
4968 }
4969
4970 public static function getOptionIntervalsCosts($intvstr) {
4971 $optcosts = array();
4972 $intervals = explode(';;', $intvstr);
4973 foreach ($intervals as $kintv => $intv) {
4974 if (empty($intv)) continue;
4975 $parts = explode('_', $intv);
4976 if (count($parts) >= 3) {
4977 $optcosts[$kintv] = (float)$parts[2];
4978 }
4979 }
4980 return $optcosts;
4981 }
4982
4983 public static function getOptionIntervalsAges($intvstr) {
4984 $optages = array();
4985 $intervals = explode(';;', $intvstr);
4986 foreach ($intervals as $kintv => $intv) {
4987 if (empty($intv)) continue;
4988 $parts = explode('_', $intv);
4989 if (count($parts) >= 3) {
4990 $optages[$kintv] = $parts[0].' - '.$parts[1];
4991 }
4992 }
4993 return $optages;
4994 }
4995
4996 public static function getOptionIntervalsPercentage($intvstr) {
4997 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4998 $optcostspcent = array();
4999 $intervals = explode(';;', $intvstr);
5000 foreach ($intervals as $kintv => $intv) {
5001 if (empty($intv)) continue;
5002 $parts = explode('_', $intv);
5003 if (count($parts) >= 3) {
5004 //fixed amount
5005 $setval = 0;
5006 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
5007 //percentage value of the room base cost (VBO 1.10)
5008 $setval = 2;
5009 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
5010 //percentage value of the adults tariff
5011 $setval = 1;
5012 }
5013 $optcostspcent[$kintv] = $setval;
5014 }
5015 }
5016 return $optcostspcent;
5017 }
5018
5019 public static function dayValidTs($days, $first, $second)
5020 {
5021 $secdiff = $second - $first;
5022 $daysdiff = $secdiff / 86400;
5023 if (is_int($daysdiff)) {
5024 if ($daysdiff < 1) {
5025 $daysdiff = 1;
5026 }
5027 } else {
5028 if ($daysdiff < 1) {
5029 $daysdiff = 1;
5030 } else {
5031 $sum = floor($daysdiff) * 86400;
5032 $newdiff = $secdiff - $sum;
5033 $maxhmore = self::getHoursMoreRb() * 3600;
5034 if ($maxhmore >= $newdiff) {
5035 $daysdiff = floor($daysdiff);
5036 } else {
5037 $daysdiff = ceil($daysdiff);
5038 }
5039 }
5040 }
5041
5042 return ($daysdiff == $days);
5043 }
5044
5045 /**
5046 * Calculates the stay fee inclusive of taxes.
5047 *
5048 * @param float $cost the cost to evaluate.
5049 * @param int $idprice the rate plan ID associated.
5050 *
5051 * @return float the given cost after tax, if taxes had to be applied.
5052 */
5053 public static function sayCostPlusIva($cost, $idprice)
5054 {
5055 $ivainclusa = self::ivaInclusa();
5056 if ($ivainclusa) {
5057 return $cost;
5058 }
5059
5060 static $tax_rplans_map = [];
5061
5062 if (array_key_exists($idprice, $tax_rplans_map)) {
5063 $tax_rate = $tax_rplans_map[$idprice];
5064 } else {
5065 $dbo = JFactory::getDbo();
5066 $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 . ";";
5067 $dbo->setQuery($q);
5068 $tax_rate = $dbo->loadAssoc();
5069
5070 // cache value
5071 $tax_rplans_map[$idprice] = $tax_rate;
5072 }
5073
5074 if (!$tax_rate || empty($tax_rate['aliq'])) {
5075 return $cost;
5076 }
5077
5078 $subt = 100 + $tax_rate['aliq'];
5079 $op = ($cost * $subt / 100);
5080
5081 /**
5082 * Tax Cap implementation for prices tax excluded (most common).
5083 *
5084 * @since 1.12
5085 */
5086 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5087 $op = ($cost + $tax_rate['taxcap']);
5088 }
5089
5090 // apply rounding to avoid issues with multiple tax rates when tax excluded
5091 $formatvals = self::getNumberFormatData();
5092 $formatparts = explode(':', $formatvals);
5093 $rounded_op = round($op, (int)$formatparts[0]);
5094 if ($rounded_op > $op) {
5095 return $rounded_op;
5096 }
5097
5098 /**
5099 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5100 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5101 *
5102 * @since 1.11.1
5103 */
5104 return round($op, (int)$formatparts[0]);
5105 }
5106
5107 /**
5108 * Calculates the stay fee exclusive of taxes.
5109 *
5110 * @param float $cost the cost to evaluate.
5111 * @param int $idprice the rate plan ID associated.
5112 *
5113 * @return float the given cost before tax.
5114 */
5115 public static function sayCostMinusIva($cost, $idprice)
5116 {
5117 $ivainclusa = self::ivaInclusa();
5118 if (!$ivainclusa) {
5119 return $cost;
5120 }
5121
5122 static $tax_rplans_map = [];
5123
5124 if (array_key_exists($idprice, $tax_rplans_map)) {
5125 $tax_rate = $tax_rplans_map[$idprice];
5126 } else {
5127 $dbo = JFactory::getDbo();
5128 $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 . ";";
5129 $dbo->setQuery($q);
5130 $tax_rate = $dbo->loadAssoc();
5131
5132 // cache value
5133 $tax_rplans_map[$idprice] = $tax_rate;
5134 }
5135
5136 if (!$tax_rate || empty($tax_rate['aliq'])) {
5137 return $cost;
5138 }
5139
5140 $subt = 100 + $tax_rate['aliq'];
5141 $op = ($cost * 100 / $subt);
5142
5143 /**
5144 * Tax Cap implementation also when prices tax included.
5145 *
5146 * @since 1.12
5147 */
5148 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5149 $op = ($cost - $tax_rate['taxcap']);
5150 }
5151
5152 // apply rounding to avoid issues with multiple tax rates when tax included
5153 $formatvals = self::getNumberFormatData();
5154 $formatparts = explode(':', $formatvals);
5155 $rounded_op = round($op, (int)$formatparts[0]);
5156 if ($rounded_op < $op) {
5157 return $rounded_op;
5158 }
5159
5160 /**
5161 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
5162 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
5163 *
5164 * @since 1.11.1
5165 */
5166 return round($op, (int)$formatparts[0]);
5167 }
5168
5169 /**
5170 * Given an option/extra cost, determines whether taxes should be applied over it.
5171 * Can also be used to calculate taxes on the extra costs per room in the bookings
5172 *
5173 * @param float $cost the cost to evaluate, option or extra service fee.
5174 * @param int $idiva the ID of the tax rate to use to apply taxes.
5175 * @param bool $force if true, taxes will always be applied.
5176 *
5177 * @return float the given option/extra cost plus tax, if taxes had to be applied.
5178 *
5179 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5180 */
5181 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
5182 {
5183 $ivainclusa = self::ivaInclusa();
5184 if ($ivainclusa && $force !== true) {
5185 return $cost;
5186 }
5187
5188 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5189 if (!$tax_rate) {
5190 return $cost;
5191 }
5192
5193 $subt = 100 + $tax_rate['aliq'];
5194 $op = ($cost * $subt / 100);
5195
5196 /**
5197 * Tax Cap implementation for prices tax excluded (most common).
5198 *
5199 * @since 1.12
5200 */
5201 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5202 $op = ($cost + $tax_rate['taxcap']);
5203 }
5204
5205 // apply rounding to avoid issues with multiple tax rates when tax excluded
5206 $formatvals = self::getNumberFormatData();
5207 $formatparts = explode(':', $formatvals);
5208 $rounded_op = round($op, (int)$formatparts[0]);
5209 if ($rounded_op > $op) {
5210 return $rounded_op;
5211 }
5212
5213 return $op;
5214 }
5215
5216 /**
5217 * Calculates the net amount for an option/extra service.
5218 *
5219 * @param float $cost the cost to evaluate, option or extra service fee.
5220 * @param int $idiva the ID of the tax rate to use to apply taxes.
5221 *
5222 * @return float the given option/extra cost before tax, if taxes were inclusive.
5223 */
5224 public static function sayOptionalsMinusIva($cost, $idiva)
5225 {
5226 if (empty($idiva)) {
5227 return $cost;
5228 }
5229
5230 $ivainclusa = self::ivaInclusa();
5231 if (!$ivainclusa) {
5232 return $cost;
5233 }
5234
5235 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5236 if (!$tax_rate) {
5237 return $cost;
5238 }
5239
5240 $subt = 100 + $tax_rate['aliq'];
5241 $op = ($cost * 100 / $subt);
5242
5243 /**
5244 * Tax Cap implementation also when prices tax included.
5245 *
5246 * @since 1.12
5247 */
5248 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5249 $op = ($cost - $tax_rate['taxcap']);
5250 }
5251
5252 // apply rounding to avoid issues with multiple tax rates when tax included
5253 $formatvals = self::getNumberFormatData();
5254 $formatparts = explode(':', $formatvals);
5255 $rounded_op = round($op, (int)$formatparts[0]);
5256 if ($rounded_op < $op) {
5257 return $rounded_op;
5258 }
5259
5260 return $op;
5261 }
5262
5263 /**
5264 * Given a cost, determines whether taxes should be applied over it.
5265 *
5266 * @param float $cost the cost to evaluate, could be a room custom price.
5267 * @param int $idiva the ID of the tax rate to use to apply taxes.
5268 * @param bool $force if true, taxes will always be applied.
5269 *
5270 * @return float the given cost plus tax, if taxes had to be applied.
5271 *
5272 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
5273 */
5274 public static function sayPackagePlusIva($cost, $idiva, $force = false)
5275 {
5276 $ivainclusa = self::ivaInclusa();
5277 if ($ivainclusa && $force !== true) {
5278 return $cost;
5279 }
5280
5281 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5282 if (!$tax_rate) {
5283 return $cost;
5284 }
5285
5286 $subt = 100 + $tax_rate['aliq'];
5287 $op = ($cost * $subt / 100);
5288
5289 /**
5290 * Tax Cap implementation for prices tax excluded (most common).
5291 *
5292 * @since 1.12
5293 */
5294 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
5295 $op = ($cost + $tax_rate['taxcap']);
5296 }
5297
5298 // apply rounding to avoid issues with multiple tax rates when tax excluded
5299 $formatvals = self::getNumberFormatData();
5300 $formatparts = explode(':', $formatvals);
5301 $rounded_op = round($op, (int)$formatparts[0]);
5302 if ($rounded_op > $op) {
5303 return $rounded_op;
5304 }
5305
5306 return $op;
5307 }
5308
5309 /**
5310 * Calculates the net amount for a package.
5311 *
5312 * @param float $cost the cost to evaluate, option or extra service fee.
5313 * @param int $idiva the ID of the tax rate to use to apply taxes.
5314 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
5315 *
5316 * @return float the given cost before tax.
5317 */
5318 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
5319 {
5320 $ivainclusa = self::ivaInclusa();
5321 if (!$ivainclusa && $force_invoice_excltax !== true) {
5322 return $cost;
5323 }
5324
5325 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
5326 if (!$tax_rate) {
5327 return $cost;
5328 }
5329
5330 $subt = 100 + $tax_rate['aliq'];
5331 $op = ($cost * 100 / $subt);
5332
5333 /**
5334 * Tax Cap implementation also when prices tax included.
5335 *
5336 * @since 1.12
5337 */
5338 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
5339 $op = ($cost - $tax_rate['taxcap']);
5340 }
5341
5342 // apply rounding to avoid issues with multiple tax rates when tax included
5343 $formatvals = self::getNumberFormatData();
5344 $formatparts = explode(':', $formatvals);
5345 $rounded_op = round($op, (int)$formatparts[0]);
5346 if ($rounded_op < $op) {
5347 return $rounded_op;
5348 }
5349
5350 return $op;
5351 }
5352
5353 public static function getSecretLink()
5354 {
5355 $dbo = JFactory::getDbo();
5356
5357 $sid = mt_rand();
5358
5359 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
5360 $dbo->setQuery($q);
5361 $all = $dbo->loadAssocList();
5362 if ($all) {
5363 $list = [];
5364 foreach ($all as $s) {
5365 $list[] = $s['sid'];
5366 }
5367 if (in_array($sid, $list)) {
5368 while(in_array($sid, $list)) {
5369 $sid++;
5370 }
5371 }
5372 }
5373
5374 return $sid;
5375 }
5376
5377 /**
5378 * Generates a confirmation number for the given booking.
5379 * Modified the way random string-numbers are obtained to
5380 * avoid any possible brute-force attempt.
5381 *
5382 * @since 1.15.1 (J) - 1.5.4 (WP)
5383 */
5384 public static function generateConfirmNumber($oid, $update = true)
5385 {
5386 $dbo = JFactory::getDbo();
5387
5388 $confirmnumb = date('y') . $oid;
5389 for ($i = 0; $i < 8; $i++) {
5390 $confirmnumb .= rand(0, 9);
5391 }
5392
5393 if ($update) {
5394 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5395 $dbo->setQuery($q);
5396 $dbo->execute();
5397 }
5398
5399 return $confirmnumb;
5400 }
5401
5402 public static function buildCustData($arr, $sep) {
5403 $cdata = "";
5404 foreach ($arr as $k => $e) {
5405 if (strlen($e)) {
5406 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5407 }
5408 }
5409 return $cdata;
5410 }
5411
5412 /**
5413 * This method parses the Joomla menu object to see if a menu item of a
5414 * specific type is available, to get its ID. Useful when links should be
5415 * displayed in pages where there is no Itemid set (booking details pages).
5416 *
5417 * @param array $viewtypes list of accepted menu items
5418 * @param string $lang the optional language to use.
5419 *
5420 * @return int
5421 *
5422 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5423 */
5424 public static function findProperItemIdType($viewtypes, $lang = null)
5425 {
5426 if (VBOPlatformDetection::isWordPress()) {
5427 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5428
5429 $itemid = $model->best($viewtypes, $lang);
5430
5431 if ($itemid) {
5432 return $itemid;
5433 }
5434
5435 return 0;
5436 }
5437
5438 $bestitemid = 0;
5439
5440 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5441
5442 $app = JFactory::getApplication();
5443
5444 $menu = $app->getMenu('site');
5445
5446 if (!$menu) {
5447 return 0;
5448 }
5449
5450 $menu_items = $menu->getMenu();
5451
5452 if (!$menu_items) {
5453 return 0;
5454 }
5455
5456 foreach ($menu_items as $itemid => $item) {
5457 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5458 // proper menu item type found
5459 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5460
5461 if (isset($item->language) && $item->language == $current_lang) {
5462 // we found the exact menu item type for the given language
5463 return $itemid;
5464 }
5465 }
5466 }
5467
5468 return $bestitemid;
5469 }
5470
5471 /**
5472 * Rewrites an internal URI that needs to be used outside of the website.
5473 * This means that the routed URI MUST start with the base path of the site.
5474 *
5475 * @param mixed $query The query string or an associative array of data.
5476 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5477 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5478 *
5479 * @return string The complete routed URI.
5480 *
5481 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5482 * routing from back-end (if available on the CMS version).
5483 */
5484 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5485 {
5486 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5487 }
5488
5489 /**
5490 * Generates an iCal file to be attached to the email message for the
5491 * customer or the administrator with some basic booking details.
5492 *
5493 * @param string $recip either admin or customer.
5494 * @param array $booking the booking array or some keys.
5495 *
5496 * @return mixed string in case of success, false otherwise.
5497 *
5498 * @since 1.12.0
5499 */
5500 public static function getEmailIcal($recip, $booking) {
5501 // load configuration setting
5502 $attachical = self::attachIcal();
5503
5504 if ($attachical === 0) {
5505 // do not attach any iCal file
5506 return false;
5507 }
5508
5509 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5510 // skip the iCal for the admin
5511 return false;
5512 }
5513
5514 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5515 // skip the iCal for the customer
5516 return false;
5517 }
5518
5519 if (strpos($recip, 'admin') !== false) {
5520 // prepare event description and summary for the admin
5521 $description = $booking['custdata'];
5522 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5523 $fname = $booking['ts'] . '.ics';
5524 } else {
5525 // event description and summary for the customer
5526 $description = '';
5527 $summary = self::getFrontTitle();
5528 $fname = 'reservation_reminder.ics';
5529 }
5530
5531 // prepare iCal head
5532 $company_name = self::getFrontTitle();
5533 $ics_str = "BEGIN:VCALENDAR\r\n" .
5534 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5535 "CALSCALE:GREGORIAN\r\n" .
5536 "VERSION:2.0\r\n";
5537 // compose iCal body
5538 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5539 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5540 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5541 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5542 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5543 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5544 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5545 $ics_str .= 'END:VEVENT'."\r\n";
5546 // close iCal file content
5547 $ics_str .= "END:VCALENDAR";
5548
5549 /**
5550 * Trigger event to allow third party plugins to overwrite the iCal file.
5551 *
5552 * @since 1.16.0 (J) - 1.6.0 (WP)
5553 */
5554 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5555
5556 if (empty($ics_str)) {
5557 return false;
5558 }
5559
5560 // store the event onto a .ics file. We use the resources folder in back-end.
5561 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5562 $fp = fopen($fpath, 'w+');
5563 $bytes = fwrite($fp, $ics_str);
5564 fclose($fp);
5565
5566 return $bytes ? $fpath : false;
5567 }
5568
5569 public static function loadEmailTemplate($booking_info = array()) {
5570 define('_VIKBOOKINGEXEC', '1');
5571 ob_start();
5572 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5573 $content = ob_get_contents();
5574 ob_end_clean();
5575 return $content;
5576 }
5577
5578 /**
5579 * Parses the raw HTML content of the booking email template.
5580 *
5581 * @param string $tmpl the raw content of the template.
5582 * @param mixed $bid int for the booking ID or booking array.
5583 * @param array $rooms list of rooms booked and translated.
5584 * @param array $rates list of translated rates for the booked rooms.
5585 * @param array $options list of translated options booked.
5586 * @param float [$total] the booking total amount (in case it has changed).
5587 * @param string [$link] the booking link can be passed for the no-deposit.
5588 *
5589 * @return string the HTML content of the parsed email template.
5590 *
5591 * @since 1.13 with different arguments.
5592 */
5593 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5594 {
5595 $app = JFactory::getApplication();
5596 $dbo = JFactory::getDbo();
5597 $vbo_tn = self::getTranslator();
5598
5599 // availability helper
5600 $av_helper = self::getAvailabilityInstance();
5601
5602 // get necessary values
5603 if (is_array($bid)) {
5604 // we got the full booking record
5605 $order_info = $bid;
5606 $bid = $order_info['id'];
5607 } else {
5608 $order_info = array();
5609 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5610 $dbo->setQuery($q);
5611 $order_info = $dbo->loadAssoc();
5612 if (!$order_info) {
5613 throw new Exception('Booking not found', 404);
5614 }
5615 }
5616
5617 $tars_info = array();
5618 $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'] . ";";
5619 $dbo->setQuery($q);
5620 $tars_info = $dbo->loadAssocList();
5621 if (!$tars_info) {
5622 throw new Exception('No rooms found', 404);
5623 }
5624
5625 /**
5626 * Split stay reservation.
5627 *
5628 * @since 1.16.0 (J) - 1.6.0 (WP)
5629 */
5630 $room_stay_dates = [];
5631 if ($order_info['split_stay']) {
5632 if ($order_info['status'] == 'confirmed') {
5633 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5634 } else {
5635 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5636 }
5637 // immediately count the number of nights of stay for each split room
5638 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5639 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5640 // overwrite values for compatibility with non-confirmed bookings
5641 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5642 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5643 }
5644 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5645 // overwrite the whole array
5646 $room_stay_dates[$sps_r_k] = $sps_r_v;
5647 }
5648 }
5649
5650 // check if the language in use is the same as the one used during the checkout
5651 $lang = JFactory::getLanguage();
5652 if (!empty($order_info['lang'])) {
5653 if ($lang->getTag() != $order_info['lang']) {
5654 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5655 if (VBOPlatformDetection::isJoomla()) {
5656 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5657 }
5658 }
5659 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5660 // force the translation to start because contents should be translated
5661 $vbo_tn::$force_tolang = $order_info['lang'];
5662 }
5663 }
5664
5665 // values for replacements
5666 $company_name = self::getFrontTitle();
5667 $currencyname = self::getCurrencyName();
5668 $sitelogo = self::getSiteLogo();
5669 $footermess = self::getFooterOrdMail($vbo_tn);
5670 $dateformat = self::getDateFormat();
5671 $datesep = self::getDateSeparator();
5672 if ($dateformat == "%d/%m/%Y") {
5673 $df = 'd/m/Y';
5674 } elseif ($dateformat == "%m/%d/%Y") {
5675 $df = 'm/d/Y';
5676 } else {
5677 $df = 'Y/m/d';
5678 }
5679 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5680 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5681 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5682 $customer_info = nl2br($order_info['custdata']);
5683 $company_logo = '';
5684 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5685 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5686 }
5687 if ($order_info['status'] == 'cancelled') {
5688 $confirmnumber = '';
5689 $status_str = JText::translate('VBCANCELLED');
5690 } elseif ($order_info['status'] == 'standby') {
5691 $confirmnumber = '';
5692 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5693 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5694 // this is an inquiry reservation
5695 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5696 }
5697 } else {
5698 $confirmnumber = $order_info['confirmnumber'];
5699 $status_str = JText::translate('VBCOMPLETED');
5700 }
5701 // booking total amount
5702 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5703 // booking link
5704 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5705 if (is_null($link)) {
5706 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5707 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5708 }
5709
5710 // raw HTML content
5711 $parsed = $tmpl;
5712
5713 /**
5714 * Trigger event to allow third-party plugins to manipulate the template string.
5715 *
5716 * @since 1.16.10 (J) - 1.6.10 (WP)
5717 */
5718 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5719
5720 /**
5721 * Parse all conditional text rules.
5722 *
5723 * @since 1.14 (J) - 1.4.0 (WP)
5724 */
5725 self::getConditionalRulesInstance()
5726 ->set(['booking', 'rooms'], [$order_info, $rooms])
5727 ->parseTokens($parsed);
5728
5729 /**
5730 * Parse all Door Access Control tags.
5731 *
5732 * @since 1.18.4 (J) - 1.8.4 (WP)
5733 */
5734 VBOFactory::getDoorAccessControl()
5735 ->parseTokens((new VBOBookingRegistry($order_info, $rooms)), $parsed);
5736
5737 // special tokens (tags) replacement
5738 $parsed = str_replace("{logo}", $company_logo, $parsed);
5739 $parsed = str_replace("{company_name}", $company_name, $parsed);
5740 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5741 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5742 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5743 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5744 $parsed = str_replace("{order_status}", $status_str, $parsed);
5745 $parsed = str_replace("{order_date}", $create_date, $parsed);
5746
5747 // customer record
5748 $cpin = self::getCPinInstance();
5749 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5750 $customer_name = '';
5751 $customer_pin = '';
5752 if ($customer_record) {
5753 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5754 $customer_pin = $customer_record['pin'];
5755 }
5756 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5757 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5758
5759 // PIN Code
5760 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5761 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5762 if (!empty($customer_pin)) {
5763 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5764 }
5765 }
5766
5767 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5768
5769 // Confirmation Number
5770 if ($confirmnumber) {
5771 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5772 } else {
5773 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5774 }
5775 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5776 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5777
5778 $roomsnum = count($rooms);
5779 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5780 $roomstr = "";
5781 $tot_adults = 0;
5782 $tot_children = 0;
5783 $tot_guests = 0;
5784
5785 // Rooms Distinctive Features
5786 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5787
5788 foreach ($rooms as $num => $r) {
5789 // guests
5790 $tot_adults += (int) $r['adults'];
5791 $tot_children += (int) $r['children'];
5792 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5793
5794 // room info
5795 $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/>";
5796 // Rooms Distinctive Features
5797 if (is_array($matches[1] ?? []) && $matches[1]) {
5798 $distinctive_features = array();
5799 $rparams = (array)json_decode($r['params'], true);
5800 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'])) {
5801 $distinctive_features = $rparams['features'][$r['roomindex']];
5802 }
5803 $docheck = (count($distinctive_features) > 0);
5804 foreach ($matches[1] as $reqf) {
5805 $feature_found = false;
5806 if ($docheck) {
5807 foreach ($distinctive_features as $dfk => $dfv) {
5808 if (stripos($dfk, $reqf) !== false) {
5809 $feature_found = $dfk;
5810 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5811 break;
5812 }
5813 }
5814 }
5815 }
5816 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5817 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5818 }
5819 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5820 }
5821 }
5822 }
5823
5824 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5825 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5826 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5827
5828 // custom fields replace
5829 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5830 if (is_array($cmatches[1]) && count($cmatches[1])) {
5831 $cfids = array();
5832 foreach ($cmatches[1] as $cfid ) {
5833 $cfids[] = $cfid;
5834 }
5835 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5836 $dbo->setQuery($q);
5837 $cfields = $dbo->loadAssocList();
5838 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5839 $cfmap = array();
5840 foreach ($cfields as $cf) {
5841 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5842 }
5843 $cfmapreplace = array();
5844 $partsreceived = explode("\n", $order_info['custdata']);
5845 if (count($partsreceived) > 0) {
5846 foreach ($partsreceived as $pst) {
5847 if (!empty($pst)) {
5848 $tmpdata = explode(":", $pst);
5849 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5850 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5851 }
5852 }
5853 }
5854 }
5855 foreach ($cmatches[1] as $cfid ) {
5856 if (array_key_exists($cfid, $cfmapreplace)) {
5857 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5858 } else {
5859 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5860 }
5861 }
5862 }
5863
5864 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5865 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5866 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5867 $parsed = str_replace("{num_nights}", ($order_info['days'] ?? 1), $parsed);
5868
5869 // order details
5870 $orderdetails = "";
5871 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5872 foreach ($rooms as $num => $r) {
5873 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5874 $use_room_ind = $num - $room_loop_ind;
5875 $split_stay_str = '';
5876 $split_stay_info = [];
5877 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) {
5878 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5879 $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']);
5880 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5881 }
5882
5883 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5884 $faredets = explode(":", $expdet[0]);
5885 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5886 if (!empty($expdet[1])) {
5887 $attrfaredets = explode(":", $expdet[1]);
5888 if (strlen($attrfaredets[1]) > 0) {
5889 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5890 }
5891 }
5892 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5893 $orderdetails .= '<div class="service-amount" style="float: right;"><span>' . self::formatCurrencyNumber(self::numberFormat($fareprice), $currencyname) . '</span></div></div>';
5894 // options
5895 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5896 foreach ($options[$num] as $oo) {
5897 $expopts = explode("\n", $oo);
5898 foreach ($expopts as $optinfo) {
5899 if (!empty($optinfo)) {
5900 $splitopt = explode(":", $optinfo);
5901 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5902 $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>';
5903 }
5904 }
5905 }
5906 }
5907 //
5908 if ($roomsnum > 1 && $num < $roomsnum) {
5909 $orderdetails .= '<br/>';
5910 }
5911 }
5912
5913 // coupon
5914 if (!empty($order_info['coupon'])) {
5915 $expcoupon = explode(";", $order_info['coupon']);
5916 $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>';
5917 }
5918
5919 // discount and payment method
5920 $payment_info = [];
5921 $payment_name = '';
5922 if (!empty($order_info['idpayment'])) {
5923 $exppay = explode('=', $order_info['idpayment']);
5924 $payment = self::getPayment($exppay[0], $vbo_tn);
5925 if ((array) $payment) {
5926 $payment_name = $payment['name'];
5927 }
5928 }
5929
5930 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5931
5932 if ($order_info['status'] != 'cancelled') {
5933 if ($payment_info) {
5934 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5935 // Discount (not charge)
5936 if ($payment_info['val_pcent'] == 1) {
5937 // fixed value
5938 $total -= $payment_info['charge'];
5939 $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>';
5940 } else {
5941 // percent value
5942 $percent_disc = $total * $payment_info['charge'] / 100;
5943 $total -= $percent_disc;
5944 $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>';
5945 }
5946 }
5947 }
5948 }
5949
5950 // booking details string
5951 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5952
5953 // additional information
5954 $parsed = str_replace("{order_total}", self::formatCurrencyNumber(self::numberFormat($total), $currencyname), $parsed);
5955 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5956 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5957 $parsed = str_replace("{booking_link}", $link, $parsed);
5958
5959 // deposit
5960 $deposit_str = '';
5961 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5962 $percentdeposit = self::getAccPerCent();
5963 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5964 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5965 if (self::getTypeDeposit() == "fixed") {
5966 $deposit_amount = $percentdeposit;
5967 } else {
5968 $baseTotal = $total;
5969 if ($order_info['tot_damage_dep'] ?? 0) {
5970 // check if the damage deposit will have to be paid later/separately
5971 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($order_info, $rooms);
5972 if (($damage_deposit_payment['damagedep_gross'] ?? 0) && ($damage_deposit_payment['payment_window'] ?? []) && ($damage_deposit_payment['payment_window']['payable'] ?? null) !== true) {
5973 // damage deposit is not payable right now
5974 $baseTotal -= (float) $order_info['tot_damage_dep'];
5975 }
5976 }
5977 $deposit_amount = $baseTotal * $percentdeposit / 100;
5978 }
5979 if ($deposit_amount > 0) {
5980 $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>';
5981 }
5982 }
5983 }
5984 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5985 //
5986 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5987 $totpaid_str = '';
5988 if ($order_info['refund'] > 0) {
5989 $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>';
5990 }
5991 if ($order_info['status'] != 'cancelled') {
5992 $tot_paid = $order_info['totpaid'];
5993 $diff_topay = (float)$total - (float)$tot_paid;
5994 if ((float)$tot_paid > 0) {
5995 $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>';
5996 // only in case the remaining balance is greater than 1 to avoid commissions issues
5997 if ($diff_topay > 1) {
5998 $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>';
5999 }
6000 }
6001 }
6002 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
6003 $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>';
6004 }
6005 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
6006
6007 /**
6008 * Language direction (LTR or RTL).
6009 *
6010 * @since 1.16.4 (J) - 1.6.4 (WP)
6011 */
6012 $lang_direction = 'ltr';
6013 if (!empty($order_info['lang'])) {
6014 // attempt to define the language direction and replace the special tag
6015 try {
6016 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
6017 } catch (Throwable $e) {
6018 // do nothing, default to ltr
6019 $lang_direction = 'ltr';
6020 }
6021 // static map for Arabic and Hebrew
6022 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
6023 }
6024 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
6025 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
6026 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
6027
6028 /**
6029 * Replace static floating styles depending on language direction (LTR or RTL).
6030 * For BC with older email template sources, we need to keep the inline style
6031 * for the floating of certain elements, which looks good only on LTR.
6032 *
6033 * @since 1.16.4 (J) - 1.6.4 (WP)
6034 */
6035 if ($lang_direction === 'rtl') {
6036 // get rid of inline static style declaration because the template uses CSS classes
6037 $parsed = str_replace("float: right;", '', $parsed);
6038 }
6039
6040 return $parsed;
6041 }
6042
6043 /**
6044 * New method for sending booking email messages
6045 * to the guest or to the administrator(s).
6046 *
6047 * @param int $bid the booking ID.
6048 * @param array $for guest, admin or a custom email address.
6049 * @param bool $send whether to send or return the HTML message.
6050 * @param bool $no_config if true, no configuration settings will be used for the status.
6051 * @param string $type optional type to indicate a booking modification.
6052 *
6053 * @return mixed True or False depending on the result or HTML string for the preview.
6054 *
6055 * @since 1.13 (J) - 1.3 (WP)
6056 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
6057 * the reservation status is pending and against the configuration.
6058 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
6059 */
6060 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
6061 {
6062 $dbo = JFactory::getDbo();
6063 $app = JFactory::getApplication();
6064
6065 $vbo_tn = self::getTranslator();
6066 $av_helper = self::getAvailabilityInstance();
6067
6068 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
6069 $dbo->setQuery($q);
6070 $booking = $dbo->loadAssoc();
6071 if (!$booking) {
6072 return false;
6073 }
6074
6075 $result = false;
6076
6077 // check if the language in use is the same as the one used during the checkout
6078 $lang = JFactory::getLanguage();
6079 if (!empty($booking['lang'])) {
6080 if ($lang->getTag() != $booking['lang']) {
6081 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
6082 if (VBOPlatformDetection::isJoomla()) {
6083 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
6084 }
6085 }
6086 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
6087 // force the translation to start because contents should be translated
6088 $vbo_tn::$force_tolang = $booking['lang'];
6089 }
6090 }
6091
6092 // load rooms booked
6093 $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;";
6094 $dbo->setQuery($q);
6095 $ordersrooms = $dbo->loadAssocList();
6096 if (!$ordersrooms) {
6097 return false;
6098 }
6099 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
6100
6101 /**
6102 * Split stay reservation.
6103 *
6104 * @since 1.16.0 (J) - 1.6.0 (WP)
6105 */
6106 $room_stay_dates = [];
6107 if ($booking['split_stay']) {
6108 if ($booking['status'] == 'confirmed') {
6109 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
6110 } else {
6111 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
6112 }
6113 // immediately count the number of nights of stay for each split room
6114 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6115 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6116 // overwrite values for compatibility with non-confirmed bookings
6117 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6118 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6119 }
6120 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6121 // overwrite the whole array
6122 $room_stay_dates[$sps_r_k] = $sps_r_v;
6123 }
6124 }
6125
6126 $rooms = array();
6127 $tars = array();
6128 $is_package = !empty($booking['pkg']) ? true : false;
6129 $ftitle = self::getFrontTitle();
6130 $currencyname = self::getCurrencyName();
6131 foreach ($ordersrooms as $kor => $or) {
6132 $num = $kor + 1;
6133 $rooms[$num] = $or;
6134 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6135 // package or custom cost set from the back-end
6136 continue;
6137 }
6138
6139 // determine the days to consider for the count of the availability
6140 $room_nights = $booking['days'];
6141 $room_checkin = $booking['checkin'];
6142 $room_checkout = $booking['checkout'];
6143 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6144 $room_nights = $room_stay_dates[$kor]['nights'];
6145 $room_checkin = $room_stay_dates[$kor]['checkin'];
6146 $room_checkout = $room_stay_dates[$kor]['checkout'];
6147 }
6148
6149 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
6150 $dbo->setQuery($q);
6151 $tar = $dbo->loadAssocList();
6152 if (!$tar) {
6153 // tariff not found
6154 if (self::isAdmin()) {
6155 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
6156 }
6157 continue;
6158 }
6159
6160 // apply seasonal rates
6161 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
6162
6163 // different usage
6164 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
6165 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
6166 // Occupancy Override
6167 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
6168 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
6169 //
6170 if (is_array($diffusageprice)) {
6171 // set a charge or discount to the price(s) for the different usage of the room
6172 foreach ($tar as $kpr => $vpr) {
6173 $tar[$kpr]['diffusage'] = $or['adults'];
6174 if ($diffusageprice['chdisc'] == 1) {
6175 // charge
6176 if ($diffusageprice['valpcent'] == 1) {
6177 // fixed value
6178 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6179 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6180 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
6181 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6182 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
6183 } else {
6184 // percentage value
6185 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6186 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
6187 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
6188 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6189 $tar[$kpr]['cost'] = $aduseval;
6190 }
6191 } else {
6192 // discount
6193 if ($diffusageprice['valpcent'] == 1) {
6194 // fixed value
6195 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
6196 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
6197 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
6198 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6199 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
6200 } else {
6201 // percentage value
6202 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
6203 $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);
6204 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
6205 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
6206 $tar[$kpr]['cost'] = $aduseval;
6207 }
6208 }
6209 }
6210 }
6211 }
6212 //
6213 $tars[$num] = $tar[0];
6214 }
6215
6216 $secdiff = $booking['checkout'] - $booking['checkin'];
6217 $daysdiff = $secdiff / 86400;
6218 if (is_int($daysdiff)) {
6219 if ($daysdiff < 1) {
6220 $daysdiff = 1;
6221 }
6222 } else {
6223 if ($daysdiff < 1) {
6224 $daysdiff = 1;
6225 } else {
6226 $sum = floor($daysdiff) * 86400;
6227 $newdiff = $secdiff - $sum;
6228 $maxhmore = self::getHoursMoreRb() * 3600;
6229 if ($maxhmore >= $newdiff) {
6230 $daysdiff = floor($daysdiff);
6231 } else {
6232 $daysdiff = ceil($daysdiff);
6233 }
6234 }
6235 }
6236
6237 $isdue = 0;
6238 $pricestr = array();
6239 $optstr = array();
6240 foreach ($ordersrooms as $kor => $or) {
6241 $num = $kor + 1;
6242
6243 // determine the days to consider for the count of the availability
6244 $room_nights = $booking['days'];
6245 $room_checkin = $booking['checkin'];
6246 $room_checkout = $booking['checkout'];
6247 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
6248 $room_nights = $room_stay_dates[$kor]['nights'];
6249 $room_checkin = $room_stay_dates[$kor]['checkin'];
6250 $room_checkout = $room_stay_dates[$kor]['checkout'];
6251 }
6252
6253 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6254 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
6255 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
6256 $isdue += $calctar;
6257 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
6258 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
6259 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6260 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
6261 $tars[$num]['calctar'] = $calctar;
6262 $isdue += $calctar;
6263 $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'] : "");
6264 }
6265
6266 if (!empty($or['optionals'])) {
6267 $stepo = explode(";", $or['optionals']);
6268 foreach ($stepo as $roptkey => $oo) {
6269 if (empty($oo)) {
6270 continue;
6271 }
6272 $stept = explode(":", $oo);
6273 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
6274 $dbo->setQuery($q);
6275 $actopt = $dbo->loadAssocList();
6276 if ($actopt) {
6277 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
6278 $chvar = '';
6279 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
6280 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
6281 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
6282 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
6283 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
6284 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
6285 $agestept = explode('-', $stept[1]);
6286 $stept[1] = $agestept[0];
6287 $chvar = $agestept[1];
6288 $realcost = 0;
6289 if (!empty($chvar)) {
6290 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
6291 // percentage value of the adults tariff
6292 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6293 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6294 } else {
6295 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6296 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6297 }
6298 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
6299 // VBO 1.10 - percentage value of room base cost
6300 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6301 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
6302 } else {
6303 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
6304 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
6305 }
6306 }
6307 $actopt[0]['chageintv'] = $chvar;
6308 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
6309 $actopt[0]['quan'] = $stept[1];
6310 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
6311 }
6312 } else {
6313 $actopt[0]['quan'] = $stept[1];
6314 // VBO 1.11 - options percentage cost of the room total fee
6315 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
6316 $deftar_basecosts = $or['cust_cost'];
6317 } else {
6318 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
6319 }
6320 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
6321 //
6322 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
6323 }
6324 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
6325 $realcost = $actopt[0]['maxprice'];
6326 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
6327 $realcost = $actopt[0]['maxprice'] * $stept[1];
6328 }
6329 }
6330 if ($actopt[0]['perperson'] == 1) {
6331 $realcost = $realcost * $or['adults'];
6332 }
6333
6334 /**
6335 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
6336 *
6337 * @since 1.17.7 (J) - 1.7.7 (WP)
6338 */
6339 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
6340 if ($custom_calculation) {
6341 $realcost = (float) $custom_calculation[0];
6342 }
6343
6344 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
6345 $isdue += $tmpopr;
6346 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
6347 }
6348 }
6349 }
6350
6351 // custom extra costs
6352 if (!empty($or['extracosts'])) {
6353 $cur_extra_costs = json_decode($or['extracosts'], true);
6354 foreach ($cur_extra_costs as $eck => $ecv) {
6355 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
6356 $isdue += $ecplustax;
6357 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
6358 }
6359 }
6360 }
6361
6362 // force the original total amount if rates have changed
6363 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
6364 $isdue = $booking['total'];
6365 }
6366
6367 // mail subject
6368 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
6369 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
6370
6371 // inject the recipient of the message for the template
6372 $booking['for'] = $for;
6373
6374 // load template file that will get $booking as variable
6375 $tmpl = self::loadEmailTemplate($booking);
6376
6377 // parse email template
6378 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
6379 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
6380
6381 if ($send !== true) {
6382 // return the content of the email message parsed
6383 return $hmess;
6384 }
6385
6386 // when the message can be sent
6387 $sendwhen = self::getSendEmailWhen();
6388
6389 if ($no_config) {
6390 // force the configuration setting to send the message no matter what's the booking status
6391 $sendwhen = 1;
6392 }
6393
6394 // send the message
6395 foreach ($for as $who) {
6396 $use_subject = $subject;
6397 $recipients = array();
6398 $attachments = self::addEmailAttachment(null);
6399 $attach_ical = false;
6400 $force_replyto = null;
6401 if (strpos($who, '@') !== false) {
6402 // send email to custom email address
6403 array_push($recipients, trim($who));
6404 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6405 // send email to the customer
6406 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6407 continue;
6408 }
6409 array_push($recipients, $booking['custmail']);
6410 /**
6411 * Check whether an iCal should be attached for the customer.
6412 *
6413 * @since 1.12.0
6414 */
6415 $attach_ical = self::getEmailIcal('customer', $booking);
6416 } elseif (stripos($who, 'admin') !== false) {
6417 // send email to the administrator(s)
6418 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6419 continue;
6420 }
6421 $use_subject = $subject . ' #' . $booking['id'];
6422 if (!strcasecmp($type, 'modified')) {
6423 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6424 }
6425 $adminemail = self::getAdminMail();
6426 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6427 if (empty($adminemail) && empty($extra_admin_recipients)) {
6428 // Prevent Joomla Exceptions that would stop the script execution
6429 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6430 continue;
6431 }
6432 if (strpos($adminemail, ',') !== false) {
6433 // multiple addresses
6434 $adminemails = explode(',', $adminemail);
6435 foreach ($adminemails as $am) {
6436 if (strpos($am, '@') !== false) {
6437 array_push($recipients, trim($am));
6438 }
6439 }
6440 } else {
6441 // single address
6442 array_push($recipients, trim($adminemail));
6443 }
6444
6445 // merge extra recipients
6446 $recipients = array_merge($recipients, $extra_admin_recipients);
6447
6448 // admin should reply to the customer
6449 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6450
6451 /**
6452 * Check whether an iCal should be attached for the admin.
6453 *
6454 * @since 1.2.0
6455 */
6456 $attach_ical = self::getEmailIcal('admin', array(
6457 'ts' => $booking['ts'],
6458 'custdata' => $booking['custdata'],
6459 'checkin' => $booking['checkin'],
6460 'checkout' => $booking['checkout'],
6461 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6462 ));
6463 }
6464
6465 // send the message, recipients should always be an array to support multiple admin addresses
6466
6467 // get sender e-mail
6468 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6469
6470 // init mail data
6471 $mail = new VBOMailWrapper([
6472 'sender' => [$adsendermail, $ftitle],
6473 'recipient' => $recipients,
6474 'bcc' => self::addAdminEmailRecipient(null, true),
6475 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6476 'subject' => $use_subject,
6477 'content' => $hmess,
6478 'attachments' => $attachments,
6479 ]);
6480
6481 if ($attach_ical !== false && $booking['status'] != 'standby') {
6482 // attach iCal file
6483 $mail->addAttachment($attach_ical);
6484 }
6485
6486 /**
6487 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6488 *
6489 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6490 *
6491 * @since 1.15.4 (J) - 1.5.10 (WP)
6492 */
6493 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6494
6495 // send e-mail
6496 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6497
6498 // unlink iCal file
6499 if ($attach_ical !== false) {
6500 @unlink($attach_ical);
6501 }
6502 }
6503
6504 return $result;
6505 }
6506
6507 /**
6508 * This method allows to add one or more recipient email
6509 * addresses for the next queue of email sending for the admin.
6510 * This method can be used in the template file for the customer
6511 * email to register an additional email address, maybe when a
6512 * specific room-type is booked.
6513 * The methods sending the email messages are supposed to call this
6514 * method by passing no arguments to obtain the extra addresses set.
6515 *
6516 * @param mixed $email null, string or array of email address(es).
6517 * @param bool $bcc if true, addresses will be used as bcc.
6518 * @param bool $reset if true, the queues will be emptied.
6519 *
6520 * @return array the current extra recipients or bcc addresses set.
6521 *
6522 * @since 1.13 (J) - 1.3.0 (WP)
6523 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6524 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6525 */
6526 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6527 {
6528 static $extra_recipients = [];
6529 static $extra_bcc = [];
6530
6531 if ($reset) {
6532 $extra_recipients = [];
6533 $extra_bcc = [];
6534 }
6535
6536 if (!empty($email)) {
6537 if (is_scalar($email)) {
6538 if ($bcc) {
6539 array_push($extra_bcc, $email);
6540 } else {
6541 array_push($extra_recipients, $email);
6542 }
6543 } else {
6544 if ($bcc) {
6545 $extra_bcc = array_merge($extra_bcc, $email);
6546 } else {
6547 $extra_recipients = array_merge($extra_recipients, $email);
6548 }
6549 }
6550 }
6551
6552 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6553 }
6554
6555 /**
6556 * This method serves to add one or more attachments to the
6557 * next queue of email sending for the admin.
6558 * The methods sending the email messages are supposed to call this
6559 * method by passing a null argument to obtain the attachments set.
6560 *
6561 * @param mixed $file null or string with path to file to attach.
6562 * @param bool $reset if true, the queue will be emptied.
6563 *
6564 * @return array the current attachments set.
6565 *
6566 * @since 1.14
6567 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6568 */
6569 public static function addEmailAttachment($file, $reset = false)
6570 {
6571 static $extra_attachments = [];
6572
6573 if ($reset) {
6574 $extra_attachments = [];
6575 }
6576
6577 if (!empty($file)) {
6578 if (is_scalar($file)) {
6579 array_push($extra_attachments, $file);
6580 } else {
6581 $extra_attachments = array_merge($extra_attachments, $file);
6582 }
6583 }
6584
6585 return array_unique($extra_attachments);
6586 }
6587
6588 /**
6589 * This method is called whenever some rooms get booked.
6590 * It checks whether the rooms involved have shared calendars.
6591 * If some are found, then also such rooms will be occupied.
6592 *
6593 * @param int $bid the booking ID.
6594 * @param array $roomids the list of rooms booked.
6595 * @param int $checkin checkin timestamp.
6596 * @param int $checkout checkout timestamp.
6597 *
6598 * @return boolean true if some other cals were occupied, false otherwise.
6599 *
6600 * @since 1.13 (J) - 1.3.0 (WP)
6601 *
6602 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6603 */
6604 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6605 {
6606 $dbo = JFactory::getDbo();
6607 $bid = (int)$bid;
6608
6609 // availability helper
6610 $av_helper = self::getAvailabilityInstance();
6611
6612 // check for split stay rooms reservation
6613 $known_split_stay = false;
6614 $is_split_stay = 0;
6615
6616 if (empty($checkin) || empty($checkout)) {
6617 // get checkin and checkout timestamps from booking
6618 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6619 $dbo->setQuery($q);
6620 $dbo->execute();
6621 if (!$dbo->getNumRows()) {
6622 // booking not found
6623 return false;
6624 }
6625 $bdata = $dbo->loadAssoc();
6626 $checkin = $bdata['checkin'];
6627 $checkout = $bdata['checkout'];
6628 $is_split_stay = $bdata['split_stay'];
6629 // turn flag on
6630 $known_split_stay = true;
6631 }
6632
6633 if (!$known_split_stay) {
6634 // we need to query the db to access this information
6635 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6636 $dbo->setQuery($q, 0, 1);
6637 $dbo->execute();
6638 if ($dbo->getNumRows()) {
6639 $is_split_stay = (int)$dbo->loadResult();
6640 }
6641 // turn flag on
6642 $known_split_stay = true;
6643 }
6644
6645 // check split stay room records
6646 $room_stay_dates = [];
6647 if ($is_split_stay) {
6648 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6649 // immediately count the number of nights of stay for each split room
6650 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6651 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6652 // overwrite values for compatibility with non-confirmed bookings
6653 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6654 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6655 }
6656 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6657 // overwrite the whole array
6658 $room_stay_dates[$sps_r_k] = $sps_r_v;
6659 }
6660 }
6661
6662 // get the rooms booked
6663 if (!count($roomids)) {
6664 // get the IDs of all rooms booked
6665 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6666 $dbo->setQuery($q);
6667 $orr = $dbo->loadAssocList();
6668 if ($orr) {
6669 foreach ($orr as $or) {
6670 array_push($roomids, $or['idroom']);
6671 }
6672 }
6673 }
6674 if (!count($roomids) || empty($bid)) {
6675 // unable to proceed
6676 return false;
6677 }
6678 $roomids = array_unique($roomids);
6679
6680 // build room stay dates map in case of split stay
6681 $room_split_stay_map = [];
6682 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6683 // we have unique room IDs in a split stay reservation for more than one room
6684 foreach ($room_stay_dates as $room_stay_date) {
6685 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6686 }
6687 }
6688
6689 // get rooms involved
6690 $involved = [];
6691 $involved_stay_map = [];
6692 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6693 $dbo->setQuery($q);
6694 $rooms_found = $dbo->loadAssocList();
6695 if (!$rooms_found) {
6696 // no rooms involved that need their calendars updated
6697 return false;
6698 }
6699 foreach ($rooms_found as $rf) {
6700 if (!in_array($rf['mainroom'], $roomids)) {
6701 // push room ID
6702 array_push($involved, $rf['mainroom']);
6703 // check for specific stay dates in connected room
6704 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6705 // split stay information available
6706 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6707 }
6708 }
6709 if (!in_array($rf['childroom'], $roomids)) {
6710 // push room ID
6711 array_push($involved, $rf['childroom']);
6712 // check for specific stay dates in connected room
6713 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6714 // split stay information available
6715 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6716 }
6717 }
6718 }
6719 $involved = array_unique($involved);
6720 if (!count($involved)) {
6721 // no rooms involved
6722 return false;
6723 }
6724
6725 // turnover seconds
6726 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6727
6728 // occupy the calendars for the involved rooms found
6729 $bids_generated = [];
6730 foreach ($involved as $rid) {
6731 // determine the values to use
6732 $room_checkin = (int)$checkin;
6733 $room_checkout = (int)$checkout;
6734 $room_realback = ($checkout + $turnover_secs);
6735 if (isset($involved_stay_map[$rid])) {
6736 // split stay dates available
6737 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6738 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6739 $room_realback = ($room_checkout + $turnover_secs);
6740 }
6741
6742 // occupy record in room with shared calendar
6743 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6744 $dbo->setQuery($q);
6745 $dbo->execute();
6746 array_push($bids_generated, $dbo->insertid());
6747 }
6748
6749 // store busy relations created
6750 foreach ($bids_generated as $busyid) {
6751 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6752 $dbo->setQuery($q);
6753 $dbo->execute();
6754 }
6755
6756 return true;
6757 }
6758
6759 /**
6760 * This method is needed whenever a booking gets modified by
6761 * adding or removing rooms. This way we reset (remove) all
6762 * busy records that were previously stored due to a shared
6763 * calendar. The correct relations should then be re-created
6764 * by calling updateSharedCalendars().
6765 *
6766 * @param int $bid the booking ID.
6767 *
6768 * @return boolean true if some records were cleaned.
6769 *
6770 * @see updateSharedCalendars() should be called after.
6771 *
6772 * @since 1.3.0
6773 */
6774 public static function cleanSharedCalendarsBusy($bid) {
6775 $dbo = JFactory::getDbo();
6776 $bid = (int)$bid;
6777 // get all the occupied records due to shared calendars for this booking
6778 $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;";
6779 $dbo->setQuery($q);
6780 $allbusy = $dbo->loadAssocList();
6781 if (!$allbusy) {
6782 return false;
6783 }
6784
6785 $busy_ids = array();
6786 foreach ($allbusy as $b) {
6787 // push busy ID to be removed later
6788 array_push($busy_ids, $b['id']);
6789 // delete the current busy ID-booking ID relation for this shared calendar
6790 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6791 $dbo->setQuery($q);
6792 $dbo->execute();
6793 }
6794 if (count($busy_ids)) {
6795 // delete all busy records due to shared calendars
6796 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6797 $dbo->setQuery($q);
6798 $dbo->execute();
6799 }
6800
6801 return true;
6802 }
6803
6804 public static function sendJutility() {
6805 //deprecated in VBO 1.10
6806 $dbo = JFactory::getDbo();
6807 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6808 $dbo->setQuery($q);
6809 $dbo->execute();
6810 $s = $dbo->loadAssocList();
6811 return (intval($s[0]['setting']) == 1 ? true : false);
6812 }
6813
6814 public static function getCategoryName($idcat, $vbo_tn = null) {
6815 $dbo = JFactory::getDbo();
6816 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6817 $dbo->setQuery($q);
6818 $dbo->execute();
6819 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6820 if (is_object($vbo_tn) && count($p) > 0) {
6821 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6822 }
6823 return count($p) > 0 ? $p[0]['name'] : '';
6824 }
6825
6826 public static function loadAdultsDiff($idroom, $adults)
6827 {
6828 static $obp_map = [];
6829
6830 $map_sign = "{$idroom}:{$adults}";
6831
6832 if (array_key_exists($map_sign, $obp_map)) {
6833 return $obp_map[$map_sign];
6834 }
6835
6836 $dbo = JFactory::getDbo();
6837
6838 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6839 $dbo->setQuery($q, 0, 1);
6840 $obp_diff = $dbo->loadAssoc();
6841 if ($obp_diff) {
6842 // cache value and return it
6843 $obp_map[$map_sign] = $obp_diff;
6844
6845 return $obp_diff;
6846 }
6847
6848 // cache value and return it
6849 $obp_map[$map_sign] = null;
6850
6851 return null;
6852 }
6853
6854 public static function loadRoomAdultsDiff($idroom)
6855 {
6856 $dbo = JFactory::getDbo();
6857
6858 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6859 $dbo->setQuery($q);
6860 $diff = $dbo->loadAssocList();
6861 if ($diff) {
6862 $roomdiff = [];
6863 foreach ($diff as $v) {
6864 $roomdiff[$v['adults']] = $v;
6865 }
6866
6867 return $roomdiff;
6868 }
6869
6870 return [];
6871 }
6872
6873 public static function occupancyOverrideExists($tar, $adults) {
6874 foreach ($tar as $k => $v) {
6875 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6876 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6877 return $v['occupancy_ovr'][$adults];
6878 }
6879 }
6880 }
6881 return false;
6882 }
6883
6884 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6885 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6886 $charges = array();
6887 if (!($children > 0) || !(count($ages) > 0)) {
6888 return $charges;
6889 }
6890 $dbo = JFactory::getDbo();
6891 $id_options = array();
6892 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6893 $dbo->setQuery($q);
6894 $dbo->execute();
6895 if ($dbo->getNumRows() > 0) {
6896 $assocs = $dbo->loadAssocList();
6897 foreach ($assocs as $opts) {
6898 if (!empty($opts['idopt'])) {
6899 $r_ido = explode(';', rtrim($opts['idopt']));
6900 foreach ($r_ido as $ido) {
6901 if (!empty($ido) && !in_array($ido, $id_options)) {
6902 $id_options[] = $ido;
6903 }
6904 }
6905 }
6906 }
6907 }
6908 if (count($id_options) > 0) {
6909 $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;";
6910 $dbo->setQuery($q);
6911 $dbo->execute();
6912 if ($dbo->getNumRows() > 0) {
6913 $ageintervals = $dbo->loadAssocList();
6914 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6915 $age_range = array();
6916 foreach ($split_ages as $kg => $spage) {
6917 if (empty($spage)) {
6918 continue;
6919 }
6920 $parts = explode('_', $spage);
6921 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6922 $ind = count($age_range);
6923 $age_range[$ind]['from'] = intval($parts[0]);
6924 $age_range[$ind]['to'] = intval($parts[1]);
6925 //taxes are calculated later in VCM
6926 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6927 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6928 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6929 }
6930 }
6931 if (count($age_range) > 0) {
6932 $tot_charge = 0;
6933 $affected = array();
6934 $option_str = '';
6935 foreach ($ages as $age) {
6936 if (strlen($age) == 0) {
6937 continue;
6938 }
6939 foreach ($age_range as $range) {
6940 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6941 $tot_charge += $range['cost'];
6942 $affected[] = $age;
6943 $option_str .= $range['option_str'].';';
6944 break;
6945 }
6946 }
6947 }
6948 if ($tot_charge > 0) {
6949 $charges['total'] = $tot_charge;
6950 $charges['affected'] = $affected;
6951 $charges['options'] = $option_str;
6952 }
6953 }
6954 }
6955 }
6956
6957 return $charges;
6958 }
6959
6960 public static function sortRoomPrices($arr) {
6961 $newarr = array();
6962 foreach ($arr as $k => $v) {
6963 $newarr[$k] = $v['cost'];
6964 }
6965 asort($newarr);
6966 $sorted = array();
6967 foreach ($newarr as $k => $v) {
6968 $sorted[$k] = $arr[$k];
6969 }
6970 return $sorted;
6971 }
6972
6973 public static function sortResults($arr) {
6974 $newarr = array();
6975 foreach ($arr as $k => $v) {
6976 $newarr[$k] = $v[0]['cost'];
6977 }
6978 asort($newarr);
6979 $sorted = array();
6980 foreach ($newarr as $k => $v) {
6981 $sorted[$k] = $arr[$k];
6982 }
6983 return $sorted;
6984 }
6985
6986 public static function sortMultipleResults($arr) {
6987 foreach ($arr as $k => $v) {
6988 $newarr = array();
6989 foreach ($v as $subk => $subv) {
6990 $newarr[$subk] = $subv[0]['cost'];
6991 }
6992 asort($newarr);
6993 $sorted = array();
6994 foreach ($newarr as $nk => $v) {
6995 $sorted[$nk] = $arr[$k][$nk];
6996 }
6997 $arr[$k] = $sorted;
6998 }
6999 return $arr;
7000 }
7001
7002 /**
7003 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
7004 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
7005 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
7006 *
7007 * @param array $promos optional list of promotion IDs to register in the execution flow.
7008 *
7009 * @return array list of cached promotion IDs (if any).
7010 *
7011 * @since 1.16.4 (J) - 1.6.4 (WP)
7012 */
7013 public static function registerPromotionIds(array $promos = [])
7014 {
7015 static $flagged_promos = [];
7016
7017 if ($promos) {
7018 // set cached value
7019 $flagged_promos = $promos;
7020 }
7021
7022 return $flagged_promos;
7023 }
7024
7025 /**
7026 * Getter and setter for seasons cache allowed.
7027 *
7028 * @param bool $enabled true or false for setter, null for getter.
7029 *
7030 * @return bool|null
7031 *
7032 * @since 1.16.5 (J) - 1.6.5 (WP)
7033 */
7034 public static function setSeasonsCache($enabled = null)
7035 {
7036 static $seasons_cache = null;
7037
7038 if ($enabled !== null) {
7039 // set value
7040 $seasons_cache = (bool)$enabled;
7041 }
7042
7043 return $seasons_cache;
7044 }
7045
7046 /**
7047 * Allows to preload and cache seasonal records for a list of rooms and dates.
7048 * Any subsequent preloading of season records for the same room(s) will return
7049 * the cached list. Should be used to preload a large window of data so that
7050 * subsequent calls will always return cached value.
7051 *
7052 * @param array $rooms List of involved room IDs.
7053 * @param int|bool $from Unix timestamp for start date or false to unset cache.
7054 * @param int $to Unix timestamp for end date.
7055 *
7056 * @return ?array
7057 *
7058 * @since 1.17.2 (J) - 1.7.2 (WP)
7059 */
7060 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
7061 {
7062 static $preloaded_records = [];
7063
7064 if (!$rooms) {
7065 // no cached signature
7066 return [];
7067 }
7068
7069 $signature = md5(implode(',', $rooms));
7070
7071 if ($from === false) {
7072 // unset cached records
7073 unset($preloaded_records[$signature]);
7074
7075 return;
7076 }
7077
7078 if ($from !== null && $to !== null) {
7079 // setter args signature for preloading records
7080 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
7081
7082 return;
7083 }
7084
7085 // getter args signature for getting the preloaded and cached records
7086 return $preloaded_records[$signature] ?? [];
7087 }
7088
7089 /**
7090 * Fetches all season records affecting a range of date timestamps.
7091 * Useful to pre-cache season records in case of hundreds of thousands
7092 * of records, but it can use up several MBs of server's memory.
7093 *
7094 * @param int $from unix timestamp for start date.
7095 * @param int $to unix timestamp for end date.
7096 * @param array $rooms optional list of involved room IDs.
7097 *
7098 * @return array
7099 *
7100 * @since 1.16.10 (J) - 1.6.10 (WP)
7101 */
7102 public static function getDateSeasonRecords($from, $to, array $rooms = [])
7103 {
7104 /**
7105 * We allow external systems to preload season records and cache them.
7106 *
7107 * @since 1.17.2 (J) - 1.7.2 (WP)
7108 */
7109 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
7110 return $preloaded_records;
7111 }
7112
7113 /**
7114 * For a more accurate records caching, ensure we have a range of two dates at least.
7115 *
7116 * @since 1.17.2 (J) - 1.7.2 (WP)
7117 */
7118 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
7119 // add one day to the end timestamp
7120 $to = strtotime('+1 day', $to);
7121 }
7122
7123 $dbo = JFactory::getDbo();
7124
7125 $one = getdate($from);
7126 $two = getdate($to);
7127
7128 // leap years
7129 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7130 $isleap = true;
7131 } else {
7132 $isleap = false;
7133 }
7134
7135 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7136 $tomidnightone = intval($one['hours']) * 3600;
7137 $tomidnightone += intval($one['minutes']) * 60;
7138 $sfrom = $from - $baseone - $tomidnightone;
7139 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7140 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7141 $tomidnighttwo = intval($two['hours']) * 3600;
7142 $tomidnighttwo += intval($two['minutes']) * 60;
7143 $sto = $to - $basetwo - $tomidnighttwo;
7144
7145 // leap years, check what dates to manipulate
7146 if ($isleap) {
7147 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7148 if ($one[0] > $leapts) {
7149 /**
7150 * Timestamp must be greater than the leap-day of Feb 29th.
7151 * It used to be checked for >= $leapts.
7152 *
7153 * @since July 3rd 2019
7154 */
7155 $sfrom -= 86400;
7156 }
7157 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7158 // lower checkin date when in leap year but not for checkout
7159 $sto -= 86400;
7160 }
7161 }
7162
7163 // check for DST changes to adjust the query values to fetch
7164 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7165 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7166 // we are parsing the day when the DST changed (probably a Sunday)
7167 if (!date('I', $from)) {
7168 // DST was just turned off
7169 $sfrom -= 3600;
7170 }
7171 }
7172 }
7173
7174 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7175 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7176 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7177 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7178 ($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`)") .
7179 ($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`)") .
7180 ($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`)") .
7181 ($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`)") .
7182 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7183 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7184
7185 /**
7186 * Avoid issues when querying data for a whole year by fetching all records.
7187 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
7188 *
7189 * @since 1.17.3 (J) - 1.7.3 (WP)
7190 */
7191 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
7192 // fetch all records when targeting a whole year of data
7193 $q = $dbo->getQuery(true)
7194 ->select('*')
7195 ->from($dbo->qn('#__vikbooking_seasons'))
7196 ->order($dbo->qn('promo') . ' ASC');
7197 }
7198
7199 $dbo->setQuery($q);
7200 $seasons = $dbo->loadAssocList();
7201
7202 if ($rooms) {
7203 // filter records by affected room IDs
7204 $seasons = array_filter($seasons, function($s) use ($rooms) {
7205 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
7206 foreach ($rooms as $idroom) {
7207 if (in_array("-" . $idroom . "-", $allrooms)) {
7208 return true;
7209 }
7210 }
7211 return false;
7212 });
7213
7214 // reset array keys
7215 $seasons = array_values($seasons);
7216 }
7217
7218 return $seasons;
7219 }
7220
7221 /**
7222 * Fetches all season records affecting week days with no dates.
7223 * Useful to avoid duplicate queries and cache/preload the records.
7224 *
7225 * @return array
7226 *
7227 * @since 1.18.5 (J) - 1.8.5 (WP)
7228 */
7229 public static function getWdaySeasonRecords()
7230 {
7231 static $cached_wdayseasons = null;
7232
7233 if (!is_null($cached_wdayseasons)) {
7234 return $cached_wdayseasons;
7235 }
7236
7237 $dbo = JFactory::getDbo();
7238
7239 $dbo->setQuery("SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));");
7240 $cached_wdayseasons = $dbo->loadAssocList();
7241
7242 return $cached_wdayseasons;
7243 }
7244
7245 /**
7246 * Applies the seasonal rates over a list of room rate records.
7247 *
7248 * @param array $arr list of room rate records.
7249 * @param int $from unix timestamp for start date.
7250 * @param int $to unix timestamp for end date.
7251 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7252 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7253 *
7254 * @return array list of manipulated room rate records.
7255 *
7256 * @since 1.18.0 (J) - 1.8.0 (WP) added support to records caching and preloading with args $seasons_dates and $seasons_wdays.
7257 * @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.
7258 */
7259 public static function applySeasonalPrices(array $arr, $from, $to, array $seasons_dates = [], ?array $seasons_wdays = null)
7260 {
7261 static $cached_seasons = [];
7262
7263 $cache_signature_wdays = '';
7264 if (self::setSeasonsCache() !== false && !$seasons_wdays) {
7265 // enable week-day season records caching
7266 $cache_signature_wdays = "wdays";
7267 }
7268
7269 $dbo = JFactory::getDbo();
7270 $vbo_tn = self::getTranslator();
7271
7272 $roomschange = [];
7273 $one = getdate($from);
7274
7275 // leap years
7276 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7277 $isleap = true;
7278 } else {
7279 $isleap = false;
7280 }
7281
7282 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7283 $tomidnightone = intval($one['hours']) * 3600;
7284 $tomidnightone += intval($one['minutes']) * 60;
7285 $sfrom = $from - $baseone - $tomidnightone;
7286 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7287 $two = getdate($to);
7288 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7289 $tomidnighttwo = intval($two['hours']) * 3600;
7290 $tomidnighttwo += intval($two['minutes']) * 60;
7291 $sto = $to - $basetwo - $tomidnighttwo;
7292
7293 // leap years, check what dates to manipulate
7294 if ($isleap) {
7295 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7296 if ($one[0] > $leapts) {
7297 /**
7298 * Timestamp must be greater than the leap-day of Feb 29th.
7299 * It used to be checked for >= $leapts.
7300 *
7301 * @since July 3rd 2019
7302 */
7303 $sfrom -= 86400;
7304 }
7305 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7306 // lower checkin date when in leap year but not for checkout
7307 $sto -= 86400;
7308 }
7309 }
7310
7311 // check for DST changes to adjust the query values to fetch
7312 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7313 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7314 // we are parsing the day when the DST changed (probably a Sunday)
7315 if (!date('I', $from)) {
7316 // DST was just turned off
7317 $sfrom -= 3600;
7318 }
7319 }
7320 }
7321
7322 // count nights requested
7323 $booking_nights = 1;
7324 foreach ($arr as $k => $a) {
7325 if (!isset($a[0])) {
7326 continue;
7327 }
7328 if (isset($a[0]['booking_nights'])) {
7329 // this value may be set when displaying pricing calendars
7330 $booking_nights = $a[0]['booking_nights'];
7331 break;
7332 }
7333 if (isset($a[0]['days'])) {
7334 $booking_nights = $a[0]['days'];
7335 break;
7336 }
7337 }
7338
7339 /**
7340 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7341 *
7342 * @since 1.16.4 (J) - 1.6.4 (WP)
7343 */
7344 $skip_promo_ids = self::registerPromotionIds();
7345
7346 $totseasons = 0;
7347 if (!$seasons_dates) {
7348 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7349 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7350 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7351 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7352 ($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`)") .
7353 ($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`)") .
7354 ($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`)") .
7355 ($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`)") .
7356 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7357 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7358
7359 // get the season records by running the query
7360 $dbo->setQuery($q);
7361 $seasons = $dbo->loadAssocList();
7362
7363 // count total seasons
7364 $totseasons = $seasons ? count($seasons) : 0;
7365 }
7366
7367 if ($totseasons > 0 || $seasons_dates) {
7368 if ($totseasons > 0) {
7369 $seasons = $seasons;
7370 } else {
7371 $seasons = $seasons_dates;
7372 }
7373 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7374 $applyseasons = false;
7375 $mem = [];
7376 foreach ($arr as $k => $a) {
7377 $mem[$k]['daysused'] = 0;
7378 $mem[$k]['sum'] = [];
7379 $mem[$k]['spids'] = [];
7380 /**
7381 * The keys below are all needed to apply the promotions on the room's final cost.
7382 *
7383 * @since 1.13.5
7384 */
7385 $mem[$k]['diffs'] = [];
7386 $mem[$k]['trans_keys'] = [];
7387 $mem[$k]['trans_factors'] = [];
7388 $mem[$k]['trans_affdays'] = [];
7389 }
7390 foreach ($seasons as $s) {
7391 // check if this is a promotion registered for skipping
7392 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7393 continue;
7394 }
7395
7396 // Special Price tied to the year
7397 if (!empty($s['year']) && $s['year'] > 0) {
7398 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7399 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7400 //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
7401 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7402 continue;
7403 }
7404 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7405 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7406 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7407 if ($calc_ends < ($from - $tomidnightone)) {
7408 continue;
7409 }
7410 }
7411 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7412 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7413 if ($s['from'] > $s['to']) {
7414 continue;
7415 }
7416 if (($basetwo + $s['from'] + 86399) > $to) {
7417 /**
7418 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7419 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7420 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7421 *
7422 * @since 1.14.3 (J) - 1.4.3 (WP)
7423 */
7424 continue;
7425 }
7426 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7427 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7428 /**
7429 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7430 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7431 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7432 *
7433 * @since 1.14.3 (J) - 1.4.3 (WP)
7434 */
7435 continue;
7436 }
7437 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7438 // 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)
7439 if (($baseone + $s['from']) > $to) {
7440 continue;
7441 }
7442 }
7443 }
7444 //
7445 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7446 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7447 $inits = $baseone + $s['from'];
7448 if ($s['from'] < $s['to']) {
7449 $ends = $basetwo + $s['to'];
7450 // check if the inits must be set to the year after
7451 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7452 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7453 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7454 $inits = $tmpbase + $s['from'];
7455 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7456 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7457 $ends = $baseone + $s['to'];
7458 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7459 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7460 $ends = $baseone + $s['to'];
7461 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7462 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7463 $inits = $basetwo + $s['from'];
7464 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7465 // booking dates across two years for a season tied to a specific year
7466 if ($one['year'] == $s['year']) {
7467 $inits = $baseone + $s['from'];
7468 } else {
7469 $inits = $basetwo + $s['from'];
7470 }
7471 if ($two['year'] == $s['year']) {
7472 $ends = $basetwo + $s['to'];
7473 } else {
7474 $ends = $baseone + $s['to'];
7475 }
7476 }
7477 } else {
7478 // between 2 years
7479 if ($baseone < $basetwo) {
7480 // ex. 29/12/2012 - 14/01/2013
7481 $ends = $basetwo + $s['to'];
7482 } else {
7483 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7484 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7485 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7486 $ends = $tmpbase + $s['to'];
7487 } else {
7488 // ex. 03/01 - 09/01
7489 $ends = $basetwo + $s['to'];
7490 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7491 $inits = $tmpbase + $s['from'];
7492 }
7493 }
7494 }
7495 // leap years
7496 if ($isleap == true) {
7497 $infoseason = getdate($inits);
7498 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7499 // leap years, check what dates to manipulate
7500 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7501 /**
7502 * Timestamp must be greater than the leap-day of Feb 29th.
7503 * It used to be checked for >= $leapts.
7504 *
7505 * @since July 3rd 2019
7506 */
7507 $inits += 86400;
7508 if ($s['from'] < $s['to']) {
7509 // increase season end date only if still on the same leap year
7510 $ends += 86400;
7511 }
7512 }
7513 }
7514
7515 // promotions
7516 $promotion = [];
7517 if (($s['promo'] ?? 0) == 1) {
7518 $daysadv = (($inits - time()) / 86400);
7519 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7520 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7521 continue;
7522 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7523 $secstocheckin = ($from - time());
7524 if ($s['promolastmin'] < $secstocheckin) {
7525 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7526 continue;
7527 }
7528 }
7529 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7530 /**
7531 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7532 *
7533 * @since 1.13.5
7534 */
7535 continue;
7536 }
7537 $promotion['todaydaysadv'] = $daysadv;
7538 $promotion['promodaysadv'] = $s['promodaysadv'];
7539 $promotion['promotxt'] = $s['promotxt'];
7540 }
7541
7542 // occupancy override
7543 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7544
7545 // week days
7546 $filterwdays = !empty($s['wdays']) ? true : false;
7547 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7548 if (is_array($wdays) && $wdays) {
7549 foreach ($wdays as $kw => $wd) {
7550 if (strlen($wd) == 0) {
7551 unset($wdays[$kw]);
7552 }
7553 }
7554 }
7555
7556 // checkin must be after the beginning of the season
7557 $checkininclok = true;
7558 if ($s['checkinincl'] == 1) {
7559 $checkininclok = false;
7560 if ($s['from'] < $s['to']) {
7561 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7562 $checkininclok = true;
7563 }
7564 } else {
7565 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7566 $checkininclok = true;
7567 }
7568 }
7569 }
7570 if ($checkininclok !== true) {
7571 continue;
7572 }
7573
7574 foreach ($arr as $k => $a) {
7575 // applied only to some types of price
7576 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7577 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7578 continue;
7579 }
7580 }
7581 // applied only to some room types
7582 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7583 continue;
7584 }
7585
7586 // count affected nights of stay
7587 $affdays = 0;
7588 $season_fromdayts = $fromdayts;
7589 $is_dst = date('I', $season_fromdayts);
7590 for ($i = 0; $i < $a[0]['days']; $i++) {
7591 $todayts = $season_fromdayts + ($i * 86400);
7592 $is_now_dst = date('I', $todayts);
7593 if ($is_dst != $is_now_dst) {
7594 // Daylight Saving Time has changed, check how
7595 if ((bool)$is_dst === true) {
7596 $todayts += 3600;
7597 $season_fromdayts += 3600;
7598 } else {
7599 $todayts -= 3600;
7600 $season_fromdayts -= 3600;
7601 }
7602 $is_dst = $is_now_dst;
7603 }
7604 if ($todayts >= $inits && $todayts <= $ends) {
7605 // week days
7606 if ($filterwdays == true) {
7607 $checkwday = getdate($todayts);
7608 if (in_array($checkwday['wday'], $wdays)) {
7609 $affdays++;
7610 }
7611 } else {
7612 $affdays++;
7613 }
7614 //
7615 }
7616 }
7617
7618 if (!($affdays > 0)) {
7619 // no nights affected
7620 continue;
7621 }
7622
7623 // apply the rule
7624 $applyseasons = true;
7625 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7626
7627 // modification factor object
7628 $factor = new stdClass;
7629
7630 // calculate new price progressively
7631 if (intval($s['val_pcent']) == 2) {
7632 // percentage value
7633 $factor->pcent = 1;
7634 $pctval = $s['diffcost'];
7635 if (strlen((string)$s['losoverride'])) {
7636 // values overrides
7637 $arrvaloverrides = [];
7638 $valovrparts = explode('_', $s['losoverride']);
7639 foreach ($valovrparts as $valovr) {
7640 if (!empty($valovr)) {
7641 $ovrinfo = explode(':', $valovr);
7642 if (strstr($ovrinfo[0], '-i') != false) {
7643 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7644 if ((int)$ovrinfo[0] < $a[0]['days']) {
7645 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7646 }
7647 }
7648 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7649 }
7650 }
7651 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7652 $pctval = $arrvaloverrides[$a[0]['days']];
7653 }
7654 }
7655 if (intval($s['type']) == 1) {
7656 // charge
7657 $factor->type = '+';
7658 $cpercent = 100 + $pctval;
7659 } else {
7660 // discount
7661 $factor->type = '-';
7662 $cpercent = 100 - $pctval;
7663 }
7664 $factor->amount = $pctval;
7665 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7666 } else {
7667 // absolute value
7668 $factor->pcent = 0;
7669 $absval = $s['diffcost'];
7670 if (strlen((string)$s['losoverride'])) {
7671 // values overrides
7672 $arrvaloverrides = [];
7673 $valovrparts = explode('_', $s['losoverride']);
7674 foreach ($valovrparts as $valovr) {
7675 if (!empty($valovr)) {
7676 $ovrinfo = explode(':', $valovr);
7677 if (strstr($ovrinfo[0], '-i') != false) {
7678 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7679 if ((int)$ovrinfo[0] < $a[0]['days']) {
7680 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7681 }
7682 }
7683 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7684 }
7685 }
7686 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7687 $absval = $arrvaloverrides[$a[0]['days']];
7688 }
7689 }
7690 if (intval($s['type']) == 1) {
7691 // charge
7692 $factor->type = '+';
7693 $newprice = ($dailyprice + $absval) * $affdays;
7694 } else {
7695 // discount
7696 $factor->type = '-';
7697 $newprice = ($dailyprice - $absval) * $affdays;
7698 }
7699 $factor->amount = $absval;
7700 }
7701
7702 // apply rounding
7703 $factor->roundmode = $s['roundmode'] ?? null;
7704 if (!empty($s['roundmode'])) {
7705 $newprice = round($newprice, 0, constant($s['roundmode']));
7706 } else {
7707 $newprice = round($newprice, 2);
7708 }
7709
7710 // define the promotion (only if no value overrides set the amount to 0)
7711 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7712 /**
7713 * Include the discount information (if any). The cost re-calculated may not be
7714 * precise if multiple special prices were applied over the same dates.
7715 *
7716 * @since 1.13
7717 */
7718 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7719 $promotion['discount'] = [
7720 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7721 'pcent' => (int)($s['val_pcent'] == 2),
7722 ];
7723 }
7724 //
7725 $mem[$k]['promotion'] = $promotion;
7726 }
7727
7728 // define the occupancy override
7729 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7730 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7731 }
7732
7733 // push special price ID
7734 if (!in_array($s['id'], $mem[$k]['spids'])) {
7735 array_push($mem[$k]['spids'], $s['id']);
7736 }
7737
7738 // push difference generated only if to be applied progressively
7739 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7740 /**
7741 * Push the difference generated by this special price for later transliteration of final price,
7742 * only if the special price is calculated progressively and not on the final price.
7743 *
7744 * @since 1.13.5
7745 */
7746 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7747 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7748 /**
7749 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7750 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7751 * all differences applied by the previous special pricing rules.
7752 *
7753 * @since 1.13.5
7754 */
7755 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7756 array_push($mem[$k]['trans_factors'], $factor);
7757 array_push($mem[$k]['trans_affdays'], $affdays);
7758 }
7759
7760 // push values in memory array
7761 array_push($mem[$k]['sum'], $newprice);
7762 $mem[$k]['daysused'] += $affdays;
7763 array_push($roomschange, $a[0]['idroom']);
7764 }
7765 }
7766 if ($applyseasons) {
7767 foreach ($mem as $k => $v) {
7768 if ($v['daysused'] > 0 && $v['sum']) {
7769 $newprice = 0;
7770 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7771 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7772 $addrest = $restdays * $dailyprice;
7773 $newprice += $addrest;
7774
7775 // calculate new final cost
7776 $redo_rounding = null;
7777 foreach ($v['sum'] as $sum_index => $add) {
7778 /**
7779 * The application of the various special pricing rules is made in a progressive and cumulative way
7780 * by always starting from the room base cost or its average daily cost. However, promotions may need
7781 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7782 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7783 * onto the differences generated by all the regular and progressively applied special pricing rules.
7784 *
7785 * @since 1.13.5
7786 */
7787 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7788 /**
7789 * This progressive price difference must be applied on the room final cost, so we need to
7790 * apply the transliteration over the other differences applied by other special prices.
7791 */
7792 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7793 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7794 // this is the % promotion we are looking for applying it on the final cost
7795 $factor = $v['trans_factors'][$transliterate_key];
7796 if (is_object($factor) && $factor->pcent) {
7797 $final_factor = 0;
7798 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7799 $final_factor += $prog_diff * $factor->amount / 100;
7800 }
7801 // update rounding
7802 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7803
7804 /**
7805 * Leverage the final factor depending on how many nights this affected.
7806 *
7807 * @since 1.15.0 (J) - 1.5.0 (WP)
7808 */
7809 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7810 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7811 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7812 }
7813
7814 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7815 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7816 }
7817 }
7818 }
7819
7820 // apply new price progressively
7821 $newprice += $add;
7822 }
7823
7824 // apply rounding from factor
7825 if (!empty($redo_rounding)) {
7826 $newprice = round($newprice, 0, constant($redo_rounding));
7827 }
7828
7829 // set promotion (if any)
7830 if (isset($v['promotion'])) {
7831 $arr[$k][0]['promotion'] = $v['promotion'];
7832 }
7833
7834 // set occupancy overrides (if any)
7835 if (isset($v['occupancy_ovr'])) {
7836 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7837 }
7838
7839 // set new final cost and update nights affected
7840 $arr[$k][0]['cost'] = $newprice;
7841 $arr[$k][0]['affdays'] = $v['daysused'];
7842 if (array_key_exists('spids', $v) && $v['spids']) {
7843 $arr[$k][0]['spids'] = $v['spids'];
7844 }
7845 }
7846 }
7847 }
7848 }
7849
7850 // week days with no season
7851 $roomschange = array_unique($roomschange);
7852 $totspecials = 0;
7853 if (!is_array($seasons_wdays)) {
7854 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7855 // avoid making a query
7856 $specials = $cached_seasons[$cache_signature_wdays];
7857 } else {
7858 // get the week-days-with-no-season records by running the query
7859 $specials = self::getWdaySeasonRecords();
7860 }
7861
7862 // count records
7863 $totspecials = $specials ? count($specials) : 0;
7864
7865 if ($cache_signature_wdays) {
7866 // cache week-days-with-no-season records
7867 $cached_seasons[$cache_signature_wdays] = $specials;
7868 }
7869 }
7870 if ($totspecials > 0 || $seasons_wdays) {
7871 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7872 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7873 $applyseasons = false;
7874 /**
7875 * We no longer unset the previous memory of the seasons with dates filters
7876 * because we need the responses to be merged. We do it only if not set.
7877 * We only keep the property 'spids' but the others should be unset.
7878 *
7879 * @since 1.11
7880 */
7881 if (!isset($mem)) {
7882 $mem = [];
7883 foreach ($arr as $k => $a) {
7884 $mem[$k]['daysused'] = 0;
7885 $mem[$k]['sum'] = [];
7886 $mem[$k]['spids'] = [];
7887 }
7888 } else {
7889 foreach ($arr as $k => $a) {
7890 $mem[$k]['daysused'] = 0;
7891 $mem[$k]['sum'] = [];
7892 }
7893 }
7894 //
7895 foreach ($specials as $s) {
7896 // check if this is a promotion registered for skipping
7897 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7898 continue;
7899 }
7900
7901 // Special Price tied to the year
7902 if (!empty($s['year']) && $s['year'] > 0) {
7903 if ($one['year'] != $s['year']) {
7904 continue;
7905 }
7906 }
7907
7908 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7909 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7910 // week days
7911 $filterwdays = !empty($s['wdays']) ? true : false;
7912 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7913 if (is_array($wdays) && $wdays) {
7914 foreach ($wdays as $kw => $wd) {
7915 if (strlen($wd) == 0) {
7916 unset($wdays[$kw]);
7917 }
7918 }
7919 }
7920
7921 foreach ($arr as $k => $a) {
7922 // only rooms with no price modifications from seasons
7923
7924 // applied only to some types of price
7925 if ($allprices && !empty($allprices[0]) && !empty($a[0]['idprice'])) {
7926 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7927 continue;
7928 }
7929 }
7930
7931 /**
7932 * We should not exclude the rooms that already had a modification of the price through a season
7933 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7934 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7935 *
7936 * @since 1.11
7937 */
7938 if (!in_array("-" . ($a[0]['idroom'] ?? 0) . "-", $allrooms)) {
7939 continue;
7940 }
7941
7942 $affdays = 0;
7943 $season_fromdayts = $fromdayts;
7944 $is_dst = date('I', $season_fromdayts);
7945 for ($i = 0; $i < $a[0]['days']; $i++) {
7946 $todayts = $season_fromdayts + ($i * 86400);
7947 $is_now_dst = date('I', $todayts);
7948 if ($is_dst != $is_now_dst) {
7949 // Daylight Saving Time has changed, check how
7950 if ((bool)$is_dst === true) {
7951 $todayts += 3600;
7952 $season_fromdayts += 3600;
7953 } else {
7954 $todayts -= 3600;
7955 $season_fromdayts -= 3600;
7956 }
7957 $is_dst = $is_now_dst;
7958 }
7959 // week days
7960 if ($filterwdays == true) {
7961 $checkwday = getdate($todayts);
7962 if (in_array($checkwday['wday'], $wdays)) {
7963 $affdays++;
7964 }
7965 }
7966 }
7967 if (!($affdays > 0)) {
7968 // no nights affected
7969 continue;
7970 }
7971
7972 // apply the rule
7973 $applyseasons = true;
7974 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7975
7976 if (intval($s['val_pcent']) == 2) {
7977 // percentage value
7978 $pctval = $s['diffcost'];
7979 if (strlen((string)$s['losoverride'])) {
7980 // values overrides
7981 $arrvaloverrides = [];
7982 $valovrparts = explode('_', $s['losoverride']);
7983 foreach ($valovrparts as $valovr) {
7984 if (!empty($valovr)) {
7985 $ovrinfo = explode(':', $valovr);
7986 if (strstr($ovrinfo[0], '-i') != false) {
7987 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7988 if ((int)$ovrinfo[0] < $a[0]['days']) {
7989 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7990 }
7991 }
7992 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7993 }
7994 }
7995 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7996 $pctval = $arrvaloverrides[$a[0]['days']];
7997 }
7998 }
7999 if (intval($s['type']) == 1) {
8000 // charge
8001 $cpercent = 100 + $pctval;
8002 } else {
8003 // discount
8004 $cpercent = 100 - $pctval;
8005 }
8006 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
8007 } else {
8008 // absolute value
8009 $absval = $s['diffcost'];
8010 if (strlen((string)$s['losoverride'])) {
8011 // values overrides
8012 $arrvaloverrides = [];
8013 $valovrparts = explode('_', $s['losoverride']);
8014 foreach ($valovrparts as $valovr) {
8015 if (!empty($valovr)) {
8016 $ovrinfo = explode(':', $valovr);
8017 if (strstr($ovrinfo[0], '-i') != false) {
8018 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8019 if ((int)$ovrinfo[0] < $a[0]['days']) {
8020 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
8021 }
8022 }
8023 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8024 }
8025 }
8026 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
8027 $absval = $arrvaloverrides[$a[0]['days']];
8028 }
8029 }
8030 if (intval($s['type']) == 1) {
8031 // charge
8032 $newprice = ($dailyprice + $absval) * $affdays;
8033 } else {
8034 // discount
8035 $newprice = ($dailyprice - $absval) * $affdays;
8036 }
8037 }
8038
8039 // apply rounding
8040 if (!empty($s['roundmode'])) {
8041 $newprice = round($newprice, 0, constant($s['roundmode']));
8042 } else {
8043 $newprice = round($newprice, 2);
8044 }
8045
8046 // push values in memory array
8047 if (!in_array($s['id'], $mem[$k]['spids'])) {
8048 $mem[$k]['spids'][] = $s['id'];
8049 }
8050 $mem[$k]['sum'][] = $newprice;
8051 $mem[$k]['daysused'] += $affdays;
8052 }
8053 }
8054 if ($applyseasons) {
8055 foreach ($mem as $k => $v) {
8056 if ($v['daysused'] > 0 && $v['sum']) {
8057 $newprice = 0;
8058 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
8059 $restdays = $arr[$k][0]['days'] - $v['daysused'];
8060 $addrest = $restdays * $dailyprice;
8061 $newprice += $addrest;
8062 foreach ($v['sum'] as $add) {
8063 $newprice += $add;
8064 }
8065 $arr[$k][0]['cost'] = $newprice;
8066 $arr[$k][0]['affdays'] = $v['daysused'];
8067 if (array_key_exists('spids', $v) && $v['spids']) {
8068 $arr[$k][0]['spids'] = $v['spids'];
8069 }
8070 }
8071 }
8072 }
8073 }
8074 // end week days with no season
8075
8076 return $arr;
8077 }
8078
8079 /**
8080 * Applies the seasonal rates over room rate records.
8081 *
8082 * @param array $arr array of tariffs taken from the DB
8083 * @param int $from start timestamp
8084 * @param int $to end timestamp
8085 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
8086 * @param array $seasons_dates array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
8087 * @param ?array $seasons_wdays array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
8088 *
8089 * @return array
8090 *
8091 * @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.
8092 */
8093 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], ?array $seasons_wdays = null)
8094 {
8095 static $cached_seasons = [];
8096
8097 $cache_signature = '';
8098 $cache_signature_wdays = '';
8099 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
8100 // enable season records caching
8101 $cache_signature = "{$from}_{$to}";
8102 }
8103 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
8104 // enable week-day season records caching
8105 $cache_signature_wdays = "wdays";
8106 }
8107
8108 $dbo = JFactory::getDbo();
8109 $vbo_tn = self::getTranslator();
8110
8111 $roomschange = [];
8112 $one = getdate($from);
8113
8114 // leap years
8115 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
8116 $isleap = true;
8117 } else {
8118 $isleap = false;
8119 }
8120
8121 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
8122 $tomidnightone = intval($one['hours']) * 3600;
8123 $tomidnightone += intval($one['minutes']) * 60;
8124 $sfrom = $from - $baseone - $tomidnightone;
8125 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
8126 $two = getdate($to);
8127 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
8128 $tomidnighttwo = intval($two['hours']) * 3600;
8129 $tomidnighttwo += intval($two['minutes']) * 60;
8130 $sto = $to - $basetwo - $tomidnighttwo;
8131
8132 // leap years, check what dates to manipulate
8133 if ($isleap) {
8134 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
8135 if ($one[0] > $leapts) {
8136 /**
8137 * Timestamp must be greater than the leap-day of Feb 29th.
8138 * It used to be checked for >= $leapts.
8139 *
8140 * @since July 3rd 2019
8141 */
8142 $sfrom -= 86400;
8143 }
8144 if ($two[0] > $leapts && $one['year'] == $two['year']) {
8145 // lower checkin date when in leap year but not for checkout
8146 $sto -= 86400;
8147 }
8148 }
8149
8150 // check for DST changes to adjust the query values to fetch
8151 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
8152 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
8153 // we are parsing the day when the DST changed (probably a Sunday)
8154 if (!date('I', $from)) {
8155 // DST was just turned off
8156 $sfrom -= 3600;
8157 }
8158 }
8159 }
8160
8161 // count nights requested
8162 $booking_nights = 1;
8163 foreach ($arr as $k => $a) {
8164 if (isset($a['booking_nights'])) {
8165 // this value may be set when displaying pricing calendars
8166 $booking_nights = $a['booking_nights'];
8167 break;
8168 }
8169 if (isset($a['days'])) {
8170 $booking_nights = $a['days'];
8171 break;
8172 }
8173 }
8174
8175 /**
8176 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
8177 *
8178 * @since 1.16.4 (J) - 1.6.4 (WP)
8179 */
8180 $skip_promo_ids = self::registerPromotionIds();
8181
8182 $totseasons = 0;
8183 if (!$parsed_season && !$seasons_dates) {
8184 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
8185 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
8186 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
8187 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
8188 ($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`)") .
8189 ($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`)") .
8190 ($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`)") .
8191 ($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`)") .
8192 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
8193 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
8194
8195 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
8196 // avoid making a query
8197 $seasons = $cached_seasons[$cache_signature];
8198 } else {
8199 // get the season records by running the query
8200 $dbo->setQuery($q);
8201 $seasons = $dbo->loadAssocList();
8202 }
8203
8204 // count total seasons
8205 $totseasons = $seasons ? count($seasons) : 0;
8206
8207 if ($cache_signature) {
8208 // cache season records
8209 $cached_seasons[$cache_signature] = $seasons;
8210 }
8211 }
8212
8213 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
8214 if ($totseasons > 0) {
8215 $seasons = $seasons;
8216 } elseif ($parsed_season) {
8217 $seasons = [$parsed_season];
8218 } else {
8219 $seasons = $seasons_dates;
8220 }
8221 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
8222 $applyseasons = false;
8223 $mem = [];
8224 foreach ($arr as $k => $a) {
8225 $mem[$k]['daysused'] = 0;
8226 $mem[$k]['sum'] = [];
8227 $mem[$k]['spids'] = [];
8228 /**
8229 * The keys below are all needed to apply the promotions on the room's final cost.
8230 *
8231 * @since 1.13.5
8232 */
8233 $mem[$k]['diffs'] = [];
8234 $mem[$k]['trans_keys'] = [];
8235 $mem[$k]['trans_factors'] = [];
8236 $mem[$k]['trans_affdays'] = [];
8237 }
8238 $affdayslistless = [];
8239 foreach ($seasons as $s) {
8240 // 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
8241 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
8242 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
8243 continue;
8244 }
8245
8246 /**
8247 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
8248 * We need to skip such fake seasons as they do not need any parsing.
8249 *
8250 * @since 1.13
8251 */
8252 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
8253 continue;
8254 }
8255
8256 // check if this is a promotion registered for skipping
8257 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8258 continue;
8259 }
8260
8261 // Special Price tied to the year
8262 if (!empty($s['year']) && $s['year'] > 0) {
8263 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
8264 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
8265 //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
8266 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
8267 continue;
8268 }
8269 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
8270 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
8271 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
8272 if ($calc_ends < ($from - $tomidnightone)) {
8273 continue;
8274 }
8275 }
8276 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
8277 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
8278 if ($s['from'] > $s['to']) {
8279 continue;
8280 }
8281 if (($basetwo + $s['from'] + 86399) > $to) {
8282 /**
8283 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
8284 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
8285 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
8286 *
8287 * @since 1.14.3 (J) - 1.4.3 (WP)
8288 */
8289 continue;
8290 }
8291 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
8292 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
8293 /**
8294 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
8295 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
8296 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
8297 *
8298 * @since 1.14.3 (J) - 1.4.3 (WP)
8299 */
8300 continue;
8301 }
8302 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
8303 // 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)
8304 if (($baseone + $s['from']) > $to) {
8305 continue;
8306 }
8307 }
8308 }
8309 //
8310 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8311 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8312 $inits = $baseone + $s['from'];
8313 if ($s['from'] < $s['to']) {
8314 $ends = $basetwo + $s['to'];
8315 // check if the inits must be set to the year after
8316 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
8317 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
8318 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8319 $inits = $tmpbase + $s['from'];
8320 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8321 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
8322 $ends = $baseone + $s['to'];
8323 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
8324 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
8325 $ends = $baseone + $s['to'];
8326 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
8327 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
8328 $inits = $basetwo + $s['from'];
8329 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
8330 // booking dates across two years for a season tied to a specific year
8331 if ($one['year'] == $s['year']) {
8332 $inits = $baseone + $s['from'];
8333 } else {
8334 $inits = $basetwo + $s['from'];
8335 }
8336 if ($two['year'] == $s['year']) {
8337 $ends = $basetwo + $s['to'];
8338 } else {
8339 $ends = $baseone + $s['to'];
8340 }
8341 }
8342 } else {
8343 // between 2 years
8344 if ($baseone < $basetwo) {
8345 // ex. 29/12/2012 - 14/01/2013
8346 $ends = $basetwo + $s['to'];
8347 } else {
8348 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
8349 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
8350 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
8351 $ends = $tmpbase + $s['to'];
8352 } else {
8353 // ex. 03/01 - 09/01
8354 $ends = $basetwo + $s['to'];
8355 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
8356 $inits = $tmpbase + $s['from'];
8357 }
8358 }
8359 }
8360 // leap years
8361 if ($isleap == true) {
8362 $infoseason = getdate($inits);
8363 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
8364 // leap years, check what dates to manipulate
8365 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
8366 /**
8367 * Timestamp must be greater than the leap-day of Feb 29th.
8368 * It used to be checked for >= $leapts.
8369 *
8370 * @since July 3rd 2019
8371 */
8372 $inits += 86400;
8373 if ($s['from'] < $s['to']) {
8374 // increase season end date only if still on the same leap year
8375 $ends += 86400;
8376 }
8377 }
8378 }
8379
8380 // promotions
8381 $promotion = [];
8382 if (($s['promo'] ?? 0) == 1) {
8383 $daysadv = (($inits - time()) / 86400);
8384 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
8385 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
8386 continue;
8387 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
8388 $secstocheckin = ($from - time());
8389 if ($s['promolastmin'] < $secstocheckin) {
8390 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
8391 continue;
8392 }
8393 }
8394 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
8395 /**
8396 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
8397 *
8398 * @since 1.13.5
8399 */
8400 continue;
8401 }
8402 $promotion['todaydaysadv'] = $daysadv;
8403 $promotion['promodaysadv'] = $s['promodaysadv'];
8404 $promotion['promotxt'] = $s['promotxt'];
8405 }
8406
8407 // occupancy override
8408 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
8409
8410 // week days
8411 $filterwdays = !empty($s['wdays']) ? true : false;
8412 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8413 if (is_array($wdays) && $wdays) {
8414 foreach ($wdays as $kw => $wd) {
8415 if (strlen($wd) == 0) {
8416 unset($wdays[$kw]);
8417 }
8418 }
8419 }
8420
8421 // checkin must be after the beginning of the season
8422 $checkininclok = true;
8423 if ($s['checkinincl'] == 1) {
8424 $checkininclok = false;
8425 if ($s['from'] < $s['to']) {
8426 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
8427 $checkininclok = true;
8428 }
8429 } else {
8430 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
8431 $checkininclok = true;
8432 }
8433 }
8434 }
8435 if ($checkininclok !== true) {
8436 continue;
8437 }
8438
8439 foreach ($arr as $k => $a) {
8440 // applied only to some types of price
8441 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8442 // VikBooking 1.6: Price Calendar sets the idprice to -1
8443 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8444 continue;
8445 }
8446 }
8447 // applied only to some room types
8448 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8449 continue;
8450 }
8451
8452 if (isset($a['days']) && is_float($a['days'])) {
8453 // prevent any possible warning with array_key_exists()
8454 $a['days'] = (int)$a['days'];
8455 }
8456
8457 $affdays = 0;
8458 $season_fromdayts = $fromdayts;
8459 $is_dst = date('I', $season_fromdayts);
8460 for ($i = 0; $i < $a['days']; $i++) {
8461 $todayts = $season_fromdayts + ($i * 86400);
8462 $is_now_dst = date('I', $todayts);
8463 if ($is_dst != $is_now_dst) {
8464 // Daylight Saving Time has changed, check how
8465 if ((bool)$is_dst === true) {
8466 $todayts += 3600;
8467 $season_fromdayts += 3600;
8468 } else {
8469 $todayts -= 3600;
8470 $season_fromdayts -= 3600;
8471 }
8472 $is_dst = $is_now_dst;
8473 }
8474 if ($todayts >= $inits && $todayts <= $ends) {
8475 $checkwday = getdate($todayts);
8476 // week days
8477 if ($filterwdays == true) {
8478 if (in_array($checkwday['wday'], $wdays)) {
8479 if (!isset($arr[$k]['affdayslist'])) {
8480 $arr[$k]['affdayslist'] = [];
8481 }
8482 $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;
8483 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8484 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8485 $affdays++;
8486 }
8487 } else {
8488 if (!isset($arr[$k]['affdayslist'])) {
8489 $arr[$k]['affdayslist'] = [];
8490 }
8491 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = isset($arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']]) && $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] > 0 ? $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] : 0;
8492 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8493 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
8494 $affdays++;
8495 }
8496 //
8497 }
8498 }
8499
8500 if (!($affdays > 0)) {
8501 // no nights affected
8502 continue;
8503 }
8504
8505 // apply the rule
8506 $applyseasons = true;
8507 $dailyprice = $a['cost'] / $a['days'];
8508
8509 // modification factor object
8510 $factor = new stdClass;
8511
8512 // calculate new price progressively
8513 if (intval($s['val_pcent']) == 2) {
8514 // percentage value
8515 $factor->pcent = 1;
8516 $pctval = $s['diffcost'];
8517 if (strlen((string)$s['losoverride'])) {
8518 // values overrides
8519 $arrvaloverrides = [];
8520 $valovrparts = explode('_', $s['losoverride']);
8521 foreach ($valovrparts as $valovr) {
8522 if (!empty($valovr)) {
8523 $ovrinfo = explode(':', $valovr);
8524 if (strstr($ovrinfo[0], '-i') != false) {
8525 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8526 if ((int)$ovrinfo[0] < $a['days']) {
8527 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8528 }
8529 }
8530 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8531 }
8532 }
8533 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8534 $pctval = $arrvaloverrides[$a['days']];
8535 }
8536 }
8537 if (intval($s['type']) == 1) {
8538 // charge
8539 $factor->type = '+';
8540 $cpercent = 100 + $pctval;
8541 } else {
8542 // discount
8543 $factor->type = '-';
8544 $cpercent = 100 - $pctval;
8545 }
8546 $factor->amount = $pctval;
8547 $dailysum = ($dailyprice * $cpercent / 100);
8548 $newprice = $dailysum * $affdays;
8549 } else {
8550 // absolute value
8551 $factor->pcent = 0;
8552 $absval = $s['diffcost'];
8553 if (strlen((string)$s['losoverride'])) {
8554 // values overrides
8555 $arrvaloverrides = [];
8556 $valovrparts = explode('_', $s['losoverride']);
8557 foreach ($valovrparts as $valovr) {
8558 if (!empty($valovr)) {
8559 $ovrinfo = explode(':', $valovr);
8560 if (strstr($ovrinfo[0], '-i') != false) {
8561 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8562 if ((int)$ovrinfo[0] < $a['days']) {
8563 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8564 }
8565 }
8566 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8567 }
8568 }
8569 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8570 $absval = $arrvaloverrides[$a['days']];
8571 }
8572 }
8573 if (intval($s['type']) == 1) {
8574 // charge
8575 $factor->type = '+';
8576 $dailysum = ($dailyprice + $absval);
8577 $newprice = $dailysum * $affdays;
8578 } else {
8579 // discount
8580 $factor->type = '-';
8581 $dailysum = ($dailyprice - $absval);
8582 $newprice = $dailysum * $affdays;
8583 }
8584 $factor->amount = $absval;
8585 }
8586
8587 // apply rounding
8588 $factor->roundmode = $s['roundmode'] ?? null;
8589 if (!empty($s['roundmode'])) {
8590 $newprice = round($newprice, 0, constant($s['roundmode']));
8591 } else {
8592 $newprice = round($newprice, 2);
8593 }
8594
8595 // define the promotion (only if no value overrides set the amount to 0)
8596 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8597 /**
8598 * Include the discount information (if any). The cost re-calculated may not be
8599 * precise if multiple special prices were applied over the same dates.
8600 *
8601 * @since 1.13
8602 */
8603 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8604 $promotion['discount'] = [
8605 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8606 'pcent' => (int)($s['val_pcent'] == 2),
8607 ];
8608 }
8609 //
8610 $mem[$k]['promotion'] = $promotion;
8611 }
8612
8613 // define the occupancy override
8614 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8615 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8616 }
8617
8618 // affected days list
8619 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8620 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8621 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8622 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8623 }
8624 }
8625 }
8626
8627 // push special price ID
8628 if (!in_array($s['id'], $mem[$k]['spids'])) {
8629 array_push($mem[$k]['spids'], $s['id']);
8630 }
8631
8632 // push difference generated only if to be applied progressively
8633 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8634 /**
8635 * Push the difference generated by this special price for later transliteration of final price,
8636 * only if the special price is calculated progressively and not on the final price.
8637 *
8638 * @since 1.13.5
8639 */
8640 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8641 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8642 /**
8643 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8644 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8645 * all differences applied by the previous special pricing rules.
8646 *
8647 * @since 1.13.5
8648 */
8649 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8650 array_push($mem[$k]['trans_factors'], $factor);
8651 array_push($mem[$k]['trans_affdays'], $affdays);
8652 }
8653
8654 // push values in memory array
8655 array_push($mem[$k]['sum'], $newprice);
8656 $mem[$k]['daysused'] += $affdays;
8657 array_push($roomschange, $a['idroom']);
8658 }
8659 }
8660 if ($applyseasons) {
8661 foreach ($mem as $k => $v) {
8662 if ($v['daysused'] > 0 && $v['sum']) {
8663 $newprice = 0;
8664 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8665 $restdays = $arr[$k]['days'] - $v['daysused'];
8666 $addrest = $restdays * $dailyprice;
8667 $newprice += $addrest;
8668
8669 // calculate new final cost
8670 $redo_rounding = null;
8671 foreach ($v['sum'] as $sum_index => $add) {
8672 /**
8673 * The application of the various special pricing rules is made in a progressive and cumulative way
8674 * by always starting from the room base cost or its average daily cost. However, promotions may need
8675 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8676 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8677 * onto the differences generated by all the regular and progressively applied special pricing rules.
8678 *
8679 * @since 1.13.5
8680 */
8681 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8682 /**
8683 * This progressive price difference must be applied on the room final cost, so we need to
8684 * apply the transliteration over the other differences applied by other special prices.
8685 */
8686 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8687 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8688 // this is the % promotion we are looking for applying it on the final cost
8689 $factor = $v['trans_factors'][$transliterate_key];
8690 if (is_object($factor) && $factor->pcent) {
8691 $final_factor = 0;
8692 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8693 $final_factor += $prog_diff * $factor->amount / 100;
8694 }
8695 // update rounding
8696 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8697
8698 /**
8699 * Leverage the final factor depending on how many nights this affected.
8700 *
8701 * @since 1.15.0 (J) - 1.5.0 (WP)
8702 */
8703 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8704 $avg_final_factor = $final_factor / $arr[$k]['days'];
8705 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8706 }
8707
8708 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8709 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8710 }
8711 }
8712 }
8713
8714 // apply new price progressively
8715 $newprice += $add;
8716 }
8717
8718 // apply rounding from factor
8719 if (!empty($redo_rounding)) {
8720 $newprice = round($newprice, 0, constant($redo_rounding));
8721 }
8722
8723 // set promotion (if any)
8724 if (isset($v['promotion'])) {
8725 $arr[$k]['promotion'] = $v['promotion'];
8726 }
8727
8728 // set occupancy overrides (if any)
8729 if (isset($v['occupancy_ovr'])) {
8730 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8731 }
8732
8733 // set new final cost and update nights affected
8734 $arr[$k]['cost'] = $newprice;
8735 $arr[$k]['affdays'] = $v['daysused'];
8736 if (array_key_exists('spids', $v) && $v['spids']) {
8737 $arr[$k]['spids'] = $v['spids'];
8738 }
8739 }
8740 }
8741 }
8742 }
8743
8744 // week days with no season
8745 $roomschange = array_unique($roomschange);
8746 $totspecials = 0;
8747 if (!is_array($seasons_wdays)) {
8748 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8749 // avoid making a query
8750 $specials = $cached_seasons[$cache_signature_wdays];
8751 } else {
8752 // get the week-days-with-no-season records by running the query
8753 $specials = self::getWdaySeasonRecords();
8754 }
8755
8756 // count records
8757 $totspecials = $specials ? count($specials) : 0;
8758
8759 if ($cache_signature_wdays) {
8760 // cache week-days-with-no-season records
8761 $cached_seasons[$cache_signature_wdays] = $specials;
8762 }
8763 }
8764 if ($totspecials > 0 || $seasons_wdays) {
8765 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8766 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8767 $applyseasons = false;
8768 /**
8769 * We no longer unset the previous memory of the seasons with dates filters
8770 * because we need the responses to be merged. We do it only if not set.
8771 * We only keep the property 'spids' but the others should be unset.
8772 *
8773 * @since 1.11
8774 */
8775 if (!isset($mem)) {
8776 $mem = [];
8777 foreach ($arr as $k => $a) {
8778 $mem[$k]['daysused'] = 0;
8779 $mem[$k]['sum'] = [];
8780 $mem[$k]['spids'] = [];
8781 }
8782 } else {
8783 foreach ($arr as $k => $a) {
8784 $mem[$k]['daysused'] = 0;
8785 $mem[$k]['sum'] = [];
8786 }
8787 }
8788 //
8789 foreach ($specials as $s) {
8790 // 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
8791 if (!empty($s['from']) || !empty($s['to'])) {
8792 continue;
8793 }
8794
8795 // check if this is a promotion registered for skipping
8796 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8797 continue;
8798 }
8799
8800 // Special Price tied to the year
8801 if (!empty($s['year']) && $s['year'] > 0) {
8802 if ($one['year'] != $s['year']) {
8803 continue;
8804 }
8805 }
8806
8807 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8808 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8809 // week days
8810 $filterwdays = !empty($s['wdays']) ? true : false;
8811 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8812 if (is_array($wdays) && $wdays) {
8813 foreach ($wdays as $kw => $wd) {
8814 if (strlen($wd) == 0) {
8815 unset($wdays[$kw]);
8816 }
8817 }
8818 }
8819
8820 foreach ($arr as $k => $a) {
8821 // only rooms with no price modifications from seasons
8822
8823 // applied only to some types of price
8824 if ($allprices && !empty($allprices[0]) && !empty($a['idprice'])) {
8825 // VikBooking 1.6: Price Calendar sets the idprice to -1
8826 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8827 continue;
8828 }
8829 }
8830
8831 /**
8832 * We should not exclude the rooms that already had a modification of the price through a season
8833 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8834 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8835 *
8836 * @since 1.11
8837 */
8838 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8839 continue;
8840 }
8841
8842 if (isset($a['days']) && is_float($a['days'])) {
8843 // prevent any possible warning with array_key_exists()
8844 $a['days'] = (int)$a['days'];
8845 }
8846
8847 $affdays = 0;
8848 $season_fromdayts = $fromdayts;
8849 $is_dst = date('I', $season_fromdayts);
8850 for ($i = 0; $i < $a['days']; $i++) {
8851 $todayts = $season_fromdayts + ($i * 86400);
8852 $is_now_dst = date('I', $todayts);
8853 if ($is_dst != $is_now_dst) {
8854 // Daylight Saving Time has changed, check how
8855 if ((bool)$is_dst === true) {
8856 $todayts += 3600;
8857 $season_fromdayts += 3600;
8858 } else {
8859 $todayts -= 3600;
8860 $season_fromdayts -= 3600;
8861 }
8862 $is_dst = $is_now_dst;
8863 }
8864 // week days
8865 if ($filterwdays == true) {
8866 $checkwday = getdate($todayts);
8867 if (in_array($checkwday['wday'], $wdays)) {
8868 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8869 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8870 $affdays++;
8871 }
8872 }
8873 //
8874 }
8875
8876 if (!($affdays > 0)) {
8877 // no nights affected
8878 continue;
8879 }
8880
8881 // apply the rule
8882 $applyseasons = true;
8883 $dailyprice = $a['cost'] / $a['days'];
8884
8885 if (intval($s['val_pcent']) == 2) {
8886 // percentage value
8887 $pctval = $s['diffcost'];
8888 if (strlen((string)$s['losoverride'])) {
8889 // values overrides
8890 $arrvaloverrides = [];
8891 $valovrparts = explode('_', $s['losoverride']);
8892 foreach ($valovrparts as $valovr) {
8893 if (!empty($valovr)) {
8894 $ovrinfo = explode(':', $valovr);
8895 if (strstr($ovrinfo[0], '-i') != false) {
8896 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8897 if ((int)$ovrinfo[0] < $a['days']) {
8898 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8899 }
8900 }
8901 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8902 }
8903 }
8904 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8905 $pctval = $arrvaloverrides[$a['days']];
8906 }
8907 }
8908 if (intval($s['type']) == 1) {
8909 // charge
8910 $cpercent = 100 + $pctval;
8911 } else {
8912 // discount
8913 $cpercent = 100 - $pctval;
8914 }
8915 $dailysum = ($dailyprice * $cpercent / 100);
8916 $newprice = $dailysum * $affdays;
8917 } else {
8918 // absolute value
8919 $absval = $s['diffcost'];
8920 if (strlen((string)$s['losoverride'])) {
8921 // values overrides
8922 $arrvaloverrides = [];
8923 $valovrparts = explode('_', $s['losoverride']);
8924 foreach ($valovrparts as $valovr) {
8925 if (!empty($valovr)) {
8926 $ovrinfo = explode(':', $valovr);
8927 if (strstr($ovrinfo[0], '-i') != false) {
8928 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8929 if ((int)$ovrinfo[0] < $a['days']) {
8930 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8931 }
8932 }
8933 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8934 }
8935 }
8936 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8937 $absval = $arrvaloverrides[$a['days']];
8938 }
8939 }
8940 if (intval($s['type']) == 1) {
8941 // charge
8942 $dailysum = ($dailyprice + $absval);
8943 $newprice = $dailysum * $affdays;
8944 } else {
8945 // discount
8946 $dailysum = ($dailyprice - $absval);
8947 $newprice = $dailysum * $affdays;
8948 }
8949 }
8950
8951 // apply rounding
8952 if (!empty($s['roundmode'])) {
8953 $newprice = round($newprice, 0, constant($s['roundmode']));
8954 } else {
8955 $newprice = round($newprice, 2);
8956 }
8957
8958 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8959 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8960 }
8961 if (!in_array($s['id'], $mem[$k]['spids'])) {
8962 $mem[$k]['spids'][] = $s['id'];
8963 }
8964 $mem[$k]['sum'][] = $newprice;
8965 $mem[$k]['daysused'] += $affdays;
8966 }
8967 }
8968 if ($applyseasons) {
8969 foreach ($mem as $k => $v) {
8970 if ($v['daysused'] > 0 && $v['sum']) {
8971 $newprice = 0;
8972 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8973 $restdays = $arr[$k]['days'] - $v['daysused'];
8974 $addrest = $restdays * $dailyprice;
8975 $newprice += $addrest;
8976 foreach ($v['sum'] as $add) {
8977 $newprice += $add;
8978 }
8979 $arr[$k]['cost'] = $newprice;
8980 $arr[$k]['affdays'] = $v['daysused'];
8981 if (array_key_exists('spids', $v) && $v['spids']) {
8982 $arr[$k]['spids'] = $v['spids'];
8983 }
8984 }
8985 }
8986 }
8987 }
8988 // end week days with no season
8989
8990 return $arr;
8991 }
8992
8993 public static function getRoomRplansClosingDates($idroom)
8994 {
8995 $dbo = JFactory::getDbo();
8996
8997 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8998 $dbo->setQuery($q);
8999 $price_records = $dbo->loadAssocList();
9000 if (!$price_records) {
9001 return [];
9002 }
9003
9004 $closingd = [];
9005 foreach ($price_records as $prec) {
9006 if (empty($prec['closingd'])) {
9007 continue;
9008 }
9009 $price_closing = json_decode($prec['closingd'], true);
9010 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9011 continue;
9012 }
9013 // check expired dates and clean up
9014 $today_midnight = mktime(0, 0, 0);
9015 $cleaned = false;
9016 foreach ($price_closing[$idroom] as $k => $v) {
9017 if (strtotime($v) < $today_midnight) {
9018 $cleaned = true;
9019 unset($price_closing[$idroom][$k]);
9020 }
9021 }
9022 if (!$price_closing[$idroom]) {
9023 unset($price_closing[$idroom]);
9024 } elseif ($cleaned === true) {
9025 // reset array keys for smaller JSON size
9026 $price_closing[$idroom] = array_values($price_closing[$idroom]);
9027 }
9028 if ($cleaned === true) {
9029 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
9030 $dbo->setQuery($q);
9031 $dbo->execute();
9032 }
9033 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
9034 continue;
9035 }
9036 $closingd[$prec['id']] = $price_closing[$idroom];
9037 }
9038
9039 return $closingd;
9040 }
9041
9042 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
9043 {
9044 $dbo = JFactory::getDbo();
9045
9046 $closingd = [];
9047
9048 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
9049 $dbo->setQuery($q);
9050 $price_records = $dbo->loadAssocList();
9051 if ($price_records && $roomids) {
9052 $info_start = getdate($checkints);
9053 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
9054 $all_nights = [];
9055 for ($i = 0; $i < (int)$numnights; $i++) {
9056 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
9057 $all_nights[] = date('Y-m-d', $next_midnight);
9058 }
9059 foreach ($price_records as $prec) {
9060 if (empty($prec['closingd'])) {
9061 continue;
9062 }
9063 $price_closing = json_decode($prec['closingd'], true);
9064 if (!$price_closing) {
9065 continue;
9066 }
9067 foreach ($price_closing as $idroom => $rclosedd) {
9068 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
9069 continue;
9070 }
9071 if (!array_key_exists($idroom, $closingd)) {
9072 $closingd[$idroom] = [];
9073 }
9074 foreach ($all_nights as $night) {
9075 if (in_array($night, $rclosedd)) {
9076 if (array_key_exists($prec['id'], $closingd[$idroom])) {
9077 $closingd[$idroom][$prec['id']][] = $night;
9078 } else {
9079 $closingd[$idroom][$prec['id']] = [$night];
9080 }
9081 }
9082 }
9083 }
9084 }
9085 }
9086
9087 return $closingd;
9088 }
9089
9090 /**
9091 * Tells whether at least one payment method must be selected to check-out.
9092 *
9093 * @param array $rooms_involved optional list of room IDs being booked.
9094 *
9095 * @return bool true if at least one payment method is applicable.
9096 *
9097 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
9098 */
9099 public static function areTherePayments($rooms_involved = [])
9100 {
9101 $dbo = JFactory::getDbo();
9102
9103 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
9104 $dbo->setQuery($q);
9105 $dbo->execute();
9106
9107 if (!$dbo->getNumRows()) {
9108 // no published payment options found
9109 return false;
9110 }
9111
9112 $pay_options = $dbo->loadAssocList();
9113
9114 if (!count($rooms_involved)) {
9115 // if no filters given, return true
9116 return true;
9117 }
9118
9119 // validate room associations
9120 foreach ($pay_options as $pay_k => $pay_v) {
9121 if (empty($pay_v['idrooms'])) {
9122 continue;
9123 }
9124 $rooms_supported = json_decode($pay_v['idrooms']);
9125 if (!is_array($rooms_supported) || !count($rooms_supported)) {
9126 continue;
9127 }
9128 // make sure all rooms being booked are supported
9129 foreach ($rooms_involved as $room_id_booked) {
9130 if (!in_array($room_id_booked, $rooms_supported)) {
9131 // we need to unset this payment option as soon as we find one non-matching room ID
9132 unset($pay_options[$pay_k]);
9133 break;
9134 }
9135 }
9136 }
9137
9138 return (bool)count($pay_options);
9139 }
9140
9141 public static function getPayment($idp, $vbo_tn = null)
9142 {
9143 if (empty($idp)) {
9144 return false;
9145 }
9146
9147 if (strstr((string)$idp, '=') !== false) {
9148 $parts = explode('=', $idp);
9149 $idp = $parts[0];
9150 }
9151
9152 $dbo = JFactory::getDbo();
9153
9154 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
9155 $dbo->setQuery($q);
9156 $payment = $dbo->loadAssocList();
9157 if (!$payment) {
9158 return false;
9159 }
9160
9161 if (is_object($vbo_tn)) {
9162 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
9163 }
9164
9165 return $payment[0];
9166 }
9167
9168 public static function getCronKey()
9169 {
9170 return VBOFactory::getConfig()->get('cronkey', '');
9171 }
9172
9173 /**
9174 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
9175 *
9176 * @return int
9177 *
9178 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
9179 */
9180 public static function getNextInvoiceNumber()
9181 {
9182 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
9183 }
9184
9185 public static function getInvoiceNumberSuffix()
9186 {
9187 return VBOFactory::getConfig()->get('invoicesuffix', '');
9188 }
9189
9190 public static function getInvoiceCompanyInfo()
9191 {
9192 return VBOFactory::getConfig()->get('invcompanyinfo', '');
9193 }
9194
9195 /**
9196 * Gets the number for the next booking receipt generation,
9197 * updates the last receipt number used for the later iterations,
9198 * stores a new receipt record to keep track of the receipts issued.
9199 *
9200 * @param int $bid the Booking ID for which we are/want to generating the receipt.
9201 * @param [int] $newnum the last number used for generating the receipt.
9202 *
9203 * @return int
9204 */
9205 public static function getNextReceiptNumber ($bid, $newnum = false) {
9206 $dbo = JFactory::getDbo();
9207 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
9208 $dbo->setQuery($q);
9209 $dbo->execute();
9210 if ($dbo->getNumRows() > 0) {
9211 $s = $dbo->loadResult();
9212 //check if this booking has already a receipt, and return that number
9213 $prev_receipt = array();
9214 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
9215 $dbo->setQuery($q);
9216 $dbo->execute();
9217 if ($dbo->getNumRows() > 0) {
9218 $prev_receipt = $dbo->loadAssoc();
9219 }
9220 //update value (receipt generated)
9221 if ($newnum !== false && $newnum > 0) {
9222 $s = (int)$newnum;
9223 if (!(count($prev_receipt) > 0)) {
9224 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
9225 $dbo->setQuery($q);
9226 $dbo->execute();
9227 //insert the new receipt record
9228 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
9229 $dbo->setQuery($q);
9230 $dbo->execute();
9231 } else {
9232 //update receipt record
9233 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
9234 $dbo->setQuery($q);
9235 $dbo->execute();
9236 }
9237 }
9238 //
9239 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
9240 }
9241 //first execution of the method should create the configuration record
9242 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
9243 $dbo->setQuery($q);
9244 $dbo->execute();
9245 return 1;
9246 }
9247
9248 public static function getReceiptNotes ($newnotes = false) {
9249 $dbo = JFactory::getDbo();
9250 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
9251 $dbo->setQuery($q);
9252 $dbo->execute();
9253 if ($dbo->getNumRows() > 0) {
9254 $s = $dbo->loadResult();
9255 //update value
9256 if ($newnotes !== false) {
9257 $s = $newnotes;
9258 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
9259 $dbo->setQuery($q);
9260 $dbo->execute();
9261 }
9262 //
9263 return $s;
9264 }
9265 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
9266 $dbo->setQuery($q);
9267 $dbo->execute();
9268 return "";
9269 }
9270
9271 public static function loadColorTagsRules()
9272 {
9273 $color_tag_rules = [
9274 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
9275 'pend1' => 'VBWAITINGFORPAYMENT',
9276 'conf1' => 'VBDBTEXTROOMCLOSED',
9277 'ovbook' => 'VBO_BTYPE_OVERBOOKING',
9278 'conf2' => 'VBOCOLORTAGRULECONFTWO',
9279 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
9280 'inv1' => 'VBOCOLORTAGRULEINVONE',
9281 'rcp1' => 'VBOCOLORTAGRULERCPONE',
9282 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
9283 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
9284 'inv2' => 'VBOCOLORTAGRULEINVTWO',
9285 ];
9286
9287 /**
9288 * Trigger event to let third-party plugins override the default booking color tag rules.
9289 *
9290 * @since 1.16.4 (J) - 1.6.4 (WP)
9291 */
9292 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
9293
9294 return $color_tag_rules;
9295 }
9296
9297 public static function loadDefaultColorTags()
9298 {
9299 $default_color_tags = [
9300 [
9301 'color' => '#9b9b9b',
9302 'name' => 'VBWAITINGFORPAYMENT',
9303 'rule' => 'pend1',
9304 ],
9305 [
9306 'color' => '#333333',
9307 'name' => 'VBDBTEXTROOMCLOSED',
9308 'rule' => 'conf1',
9309 ],
9310 [
9311 'color' => '#f04848',
9312 'name' => 'VBO_BTYPE_OVERBOOKING',
9313 'rule' => 'ovbook',
9314 ],
9315 [
9316 'color' => '#ff8606',
9317 'name' => 'VBOCOLORTAGRULECONFTWO',
9318 'rule' => 'conf2',
9319 ],
9320 [
9321 'color' => '#0418c9',
9322 'name' => 'VBOCOLORTAGRULECONFTHREE',
9323 'rule' => 'conf3',
9324 ],
9325 [
9326 'color' => '#bed953',
9327 'name' => 'VBOCOLORTAGRULEINVONE',
9328 'rule' => 'inv1',
9329 ],
9330 [
9331 'color' => '#67f5b5',
9332 'name' => 'VBOCOLORTAGRULERCPONE',
9333 'rule' => 'rcp1',
9334 ],
9335 [
9336 'color' => '#04d2c2',
9337 'name' => 'VBOCOLORTAGRULECONFFOUR',
9338 'rule' => 'conf4',
9339 ],
9340 [
9341 'color' => '#00b316',
9342 'name' => 'VBOCOLORTAGRULECONFFIVE',
9343 'rule' => 'conf5',
9344 ],
9345 [
9346 'color' => '#00f323',
9347 'name' => 'VBOCOLORTAGRULEINVTWO',
9348 'rule' => 'inv2',
9349 ],
9350 ];
9351
9352 /**
9353 * Trigger event to let third-party plugins override the default color tags.
9354 *
9355 * @since 1.16.4 (J) - 1.6.4 (WP)
9356 */
9357 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
9358
9359 return $default_color_tags;
9360 }
9361
9362 public static function loadBookingsColorTags()
9363 {
9364 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
9365
9366 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
9367 }
9368
9369 public static function getBestColorContrast($hexcolor)
9370 {
9371 $hexcolor = str_replace('#', '', $hexcolor);
9372
9373 if (empty($hexcolor) || strlen($hexcolor) != 6) {
9374 return '#000000';
9375 }
9376
9377 $r = hexdec(substr($hexcolor, 0, 2));
9378 $g = hexdec(substr($hexcolor, 2, 2));
9379 $b = hexdec(substr($hexcolor, 4, 2));
9380
9381 // Counting the perceptive luminance - human eye favors green color
9382 // < 0.5 bright colors
9383 // > 0.5 dark colors
9384
9385 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
9386 }
9387
9388 public static function applyBookingColorTag($booking, $tags = [])
9389 {
9390 if (!is_array($tags) || !$tags) {
9391 $tags = self::loadBookingsColorTags();
9392 }
9393
9394 if (!empty($booking['colortag'])) {
9395 $color_tag_arr = json_decode($booking['colortag'], true);
9396 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
9397 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
9398 return $color_tag_arr;
9399 }
9400 }
9401
9402 $dbo = JFactory::getDbo();
9403
9404 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
9405 $docs_data = [];
9406 $invoice_numb = false;
9407 $receipt_numb = false;
9408
9409 if ($booking['status'] == 'confirmed' && ($booking['checkin'] ?? 0) > time()) {
9410 $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`
9411 FROM `#__vikbooking_orders` AS `b`
9412 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
9413 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
9414 WHERE `b`.`id`=" . (int)$bid . ";";
9415 $dbo->setQuery($q);
9416 $docs_data = $dbo->loadAssoc();
9417 if ($docs_data) {
9418 $invoice_numb = (!empty($docs_data['inv_id']));
9419 $receipt_numb = (!empty($docs_data['rcp_id']));
9420
9421 /**
9422 * We inject the invoice number or receipt number into the color tag rules.
9423 * This is useful for the Tableaux page to show the invoice/receipt number.
9424 *
9425 * @since 1.12 (J) - 1.1.7 (WP)
9426 */
9427 if ($invoice_numb || $receipt_numb) {
9428 foreach ($tags as &$tval) {
9429 if ($invoice_numb) {
9430 $tval['invoice_number'] = $docs_data['inv_number'];
9431 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
9432 }
9433 if ($receipt_numb) {
9434 $tval['receipt_number'] = $docs_data['rcp_number'];
9435 }
9436 }
9437 // unset last reference
9438 unset($tval);
9439 }
9440 }
9441 }
9442
9443 /**
9444 * Some OTA bookings using an OTA-Collect business model may look as not entirely
9445 * paid when they actually are, due to commissions and pass through taxes. We also
9446 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
9447 *
9448 * @since 1.16.3 (J) - 1.6.3 (WP)
9449 */
9450 $ota_collected_booking = false;
9451 $hotel_collect_vcc_unpaid = false;
9452 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
9453 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
9454 $ota_collected_booking = true;
9455 }
9456 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
9457 // VCC detected, check the history for a payment event
9458 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
9459 // list of history types to fetch
9460 $h_payment_types = [];
9461 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
9462 if ($h_payment_group && $h_payment_group['types']) {
9463 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
9464 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
9465 }
9466 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
9467 if (!$payment_ev_datas) {
9468 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
9469 $hotel_collect_vcc_unpaid = true;
9470 }
9471 }
9472 }
9473
9474 foreach ($tags as $tkey => $tval) {
9475 if (empty($tval['rule'])) {
9476 continue;
9477 }
9478
9479 /**
9480 * Trigger event to let third-party plugins validate the custom rule.
9481 *
9482 * @since 1.16.4 (J) - 1.6.4 (WP)
9483 */
9484 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
9485 if (!empty($custom_rule)) {
9486 return (array) $custom_rule[0];
9487 }
9488
9489 switch ($tval['rule']) {
9490 case 'pend1':
9491 // Room is waiting for the payment (locked record)
9492 if ($booking['status'] == 'standby') {
9493 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
9494 $dbo->setQuery($q);
9495 $tmp_lock = $dbo->loadAssoc();
9496 if ($tmp_lock) {
9497 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9498 return $tval;
9499 }
9500 }
9501 break;
9502 case 'conf1':
9503 // Confirmed (Room Closed)
9504 if ($booking['status'] == 'confirmed' && (!empty($booking['closure']) || $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED'))) {
9505 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9506 return $tval;
9507 }
9508 break;
9509 case 'ovbook':
9510 // Confirmed (Overbooking)
9511 if ($booking['status'] == 'confirmed' && !empty($booking['type']) && $booking['type'] == 'overbooking') {
9512 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9513 return $tval;
9514 }
9515 break;
9516 case 'conf2':
9517 // Confirmed (No Rate 0.00/NULL Total)
9518 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9519 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9520 return $tval;
9521 }
9522 break;
9523 case 'conf3':
9524 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9525 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) {
9526 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9527 return $tval;
9528 }
9529 break;
9530 case 'inv1':
9531 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9532 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9533 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9534 return $tval;
9535 }
9536 break;
9537 case 'rcp1':
9538 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9539 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9540 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9541 return $tval;
9542 }
9543 break;
9544 case 'conf4':
9545 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9546 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9547 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9548 return $tval;
9549 }
9550 break;
9551 case 'conf5':
9552 // Confirmed (Total > 0 && Total Paid >= Total)
9553 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
9554 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9555 return $tval;
9556 }
9557 break;
9558 case 'inv2':
9559 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9560 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9561 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9562 return $tval;
9563 }
9564 break;
9565 default:
9566 // unsupported rule
9567 break;
9568 }
9569 }
9570
9571 return [];
9572 }
9573
9574 public static function getBookingInfoFromID($bid)
9575 {
9576 $dbo = JFactory::getDbo();
9577
9578 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9579 $dbo->setQuery($q);
9580 $booking_info = $dbo->loadAssoc();
9581 if ($booking_info) {
9582 return $booking_info;
9583 }
9584
9585 return [];
9586 }
9587
9588 /**
9589 * Load booking details for a given list of IDs.
9590 *
9591 * @param int $roomid The room integer number.
9592 * @param array $room_bids_pool List of booking IDs involved.
9593 * @param bool $unassigned Whether to return also the unassigned bookings.
9594 *
9595 * @return array Associative list of sub-unit bookings, or list containing also the unassigned bookings.
9596 *
9597 * @since 1.18.2 (J) - 1.8.2 (WP) added argument $unassigned.
9598 */
9599 public static function loadRoomIndexesBookings(int $roomid, array $room_bids_pool, $unassigned = false)
9600 {
9601 $dbo = JFactory::getDbo();
9602
9603 $pool_values = [];
9604 if ($unassigned) {
9605 // prepare the default return value
9606 $pool_values = [
9607 [],
9608 [],
9609 ];
9610 }
9611
9612 if (empty($roomid) || !$room_bids_pool) {
9613 return $pool_values;
9614 }
9615
9616 $room_features_bookings = [];
9617 $unassigned_bookings = [];
9618
9619 $all_bids = [];
9620 foreach ($room_bids_pool as $day => $bids) {
9621 $all_bids = array_merge($all_bids, $bids);
9622 }
9623 $all_bids = array_map('intval', array_unique($all_bids));
9624 $all_bids_str = implode(', ', $all_bids);
9625
9626 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9627 $dbo->setQuery($q);
9628 $rbookings = $dbo->loadAssocList();
9629 if (!$rbookings) {
9630 return $pool_values;
9631 }
9632
9633 foreach ($rbookings as $k => $v) {
9634 if (empty($v['roomindex'])) {
9635 if ($unassigned) {
9636 // push this booking to the list of unassigned reservations
9637 $unassigned_bookings[] = $v['idorder'];
9638 }
9639 // do not proceed further
9640 continue;
9641 }
9642 if (!isset($room_features_bookings[$v['roomindex']])) {
9643 $room_features_bookings[$v['roomindex']] = [];
9644 }
9645 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9646 }
9647
9648 if ($unassigned) {
9649 // return a list of information
9650 return [
9651 $room_features_bookings,
9652 $unassigned_bookings,
9653 ];
9654 }
9655
9656 // return just the list of room sub-unit bookings
9657 return $room_features_bookings;
9658 }
9659
9660 public static function getSendEmailWhen()
9661 {
9662 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9663 }
9664
9665 public static function getMinutesAutoRemove()
9666 {
9667 static $minAutoRemove = null;
9668
9669 if ($minAutoRemove !== null) {
9670 return $minAutoRemove;
9671 }
9672
9673 $minAutoRemove = VBOFactory::getConfig()->getInt('minautoremove', 0);
9674
9675 return $minAutoRemove;
9676 }
9677
9678 /**
9679 * Returns the currently active SMS driver handle.
9680 *
9681 * @return string
9682 *
9683 * @since 1.18.8 (J) - 1.8.8 (WP) removed support for WhatsApp free-text messages due to Meta
9684 * business template requirements - use the apposite E4jConnect
9685 * WhatsApp Business Messaging service with Vik Channel Manager.
9686 */
9687 public static function getSMSAPIClass()
9688 {
9689 $smsApiValue = VBOFactory::getConfig()->getString('smsapi', '');
9690
9691 if (preg_match('/^whatsapp/i', $smsApiValue)) {
9692 // no longer supported
9693 $smsApiValue = '';
9694 }
9695
9696 return $smsApiValue;
9697 }
9698
9699 public static function autoSendSMSEnabled()
9700 {
9701 return VBOFactory::getConfig()->getBool('smsautosend', false);
9702 }
9703
9704 public static function getSendSMSTo()
9705 {
9706 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9707 }
9708
9709 public static function getSendSMSWhen()
9710 {
9711 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9712 }
9713
9714 public static function getSMSAdminPhone()
9715 {
9716 return VBOFactory::getConfig()->getString('smsadminphone', '');
9717 }
9718
9719 public static function getSMSParams($as_array = true)
9720 {
9721 if (!$as_array) {
9722 return VBOFactory::getConfig()->getString('smsparams', '');
9723 }
9724
9725 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9726 }
9727
9728 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9729 $dbo = JFactory::getDbo();
9730 switch (strtolower($booking_status)) {
9731 case 'standby':
9732 $status = 'pend';
9733 break;
9734 case 'cancelled':
9735 $status = 'canc';
9736 break;
9737 default:
9738 $status = '';
9739 break;
9740 }
9741 $paramtype = 'sms'.$type.'tpl'.$status;
9742 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9743 $dbo->setQuery($q);
9744 $dbo->execute();
9745 if ($dbo->getNumRows() < 1) {
9746 if ($status == 'canc') {
9747 //Type cancelled is used by VCM since v1.6.6
9748 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9749 $dbo->setQuery($q);
9750 $dbo->execute();
9751 }
9752 return '';
9753 }
9754 $ft = $dbo->loadAssocList();
9755 if (is_object($vbo_tn)) {
9756 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9757 }
9758 return $ft[0]['setting'];
9759 }
9760
9761 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9762 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9763 }
9764
9765 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9766 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9767 }
9768
9769 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9770 $dbo = JFactory::getDbo();
9771 $phone = str_replace(" ", '', trim($phone));
9772 $cprefix = '';
9773 if (!empty($country_threecode)) {
9774 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9775 $dbo->setQuery($q);
9776 $dbo->execute();
9777 if ($dbo->getNumRows() > 0) {
9778 $cprefix = $dbo->loadResult();
9779 $cprefix = str_replace(" ", '', trim($cprefix));
9780 }
9781 }
9782 if (!empty($cprefix)) {
9783 if (substr($phone, 0, 1) != '+') {
9784 if (substr($phone, 0, 2) == '00') {
9785 $phone = '+'.substr($phone, 2);
9786 } else {
9787 $phone = $cprefix.$phone;
9788 }
9789 }
9790 }
9791 return $phone;
9792 }
9793
9794 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9795 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9796
9797 /**
9798 * Trigger event to allow third-party plugins to manipulate the template string.
9799 *
9800 * @since 1.16.10 (J) - 1.6.10 (WP)
9801 */
9802 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9803
9804 /**
9805 * Parse all conditional text rules.
9806 *
9807 * @since 1.14 (J) - 1.4.0 (WP)
9808 */
9809 self::getConditionalRulesInstance()
9810 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9811 ->parseTokens($tpl);
9812
9813 /**
9814 * Parse all Door Access Control tags.
9815 *
9816 * @since 1.18.4 (J) - 1.8.4 (WP)
9817 */
9818 VBOFactory::getDoorAccessControl()
9819 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9820
9821 $vbo_df = self::getDateFormat();
9822 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9823 $datesep = self::getDateSeparator();
9824
9825 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9826 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9827 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9828 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9829 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9830 if (!empty($booking['confirmnumber'])) {
9831 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9832 } else {
9833 $tpl = str_replace("{confirmnumb}", '', $tpl);
9834 }
9835 if (!empty($booking['idorderota'])) {
9836 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9837 } else {
9838 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9839 }
9840
9841 $rooms_booked = array();
9842 $rooms_names = array();
9843 $tot_adults = 0;
9844 $tot_children = 0;
9845 $tot_guests = 0;
9846 foreach ($booking_rooms as $broom) {
9847 $rooms_names[] = $broom['room_name'];
9848 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9849 $rooms_booked[$broom['room_name']] += 1;
9850 } else {
9851 $rooms_booked[$broom['room_name']] = 1;
9852 }
9853 $tot_adults += (int)$broom['adults'];
9854 $tot_children += (int)$broom['children'];
9855 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9856 }
9857 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9858 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9859 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9860 $rooms_booked_quant = array();
9861 foreach ($rooms_booked as $rname => $quant) {
9862 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9863 }
9864 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9865 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9866 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9867 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9868 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9869 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9870 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9871 $remaining_bal = $booking['total'] - $booking['totpaid'];
9872 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9873
9874 return $tpl;
9875 }
9876
9877 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9878 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9879
9880 /**
9881 * Trigger event to allow third-party plugins to manipulate the template string.
9882 *
9883 * @since 1.16.10 (J) - 1.6.10 (WP)
9884 */
9885 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9886
9887 /**
9888 * Parse all conditional text rules.
9889 *
9890 * @since 1.14 (J) - 1.4.0 (WP)
9891 */
9892 self::getConditionalRulesInstance()
9893 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
9894 ->parseTokens($tpl);
9895
9896 /**
9897 * Parse all Door Access Control tags.
9898 *
9899 * @since 1.18.4 (J) - 1.8.4 (WP)
9900 */
9901 VBOFactory::getDoorAccessControl()
9902 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
9903
9904 $vbo_df = self::getDateFormat();
9905 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9906 $datesep = self::getDateSeparator();
9907
9908 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9909 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9910 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9911 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9912 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9913 if (!empty($booking['confirmnumber'])) {
9914 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9915 } else {
9916 $tpl = str_replace("{confirmnumb}", '', $tpl);
9917 }
9918 if (!empty($booking['idorderota'])) {
9919 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9920 } else {
9921 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9922 }
9923
9924 $rooms_booked = array();
9925 $rooms_names = array();
9926 $tot_adults = 0;
9927 $tot_children = 0;
9928 $tot_guests = 0;
9929 foreach ($booking_rooms as $broom) {
9930 $rooms_names[] = $broom['room_name'];
9931 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9932 $rooms_booked[$broom['room_name']] += 1;
9933 } else {
9934 $rooms_booked[$broom['room_name']] = 1;
9935 }
9936 $tot_adults += (int)$broom['adults'];
9937 $tot_children += (int)$broom['children'];
9938 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9939 }
9940 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9941 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9942 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9943 $rooms_booked_quant = array();
9944 foreach ($rooms_booked as $rname => $quant) {
9945 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9946 }
9947 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9948 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9949 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9950 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9951 $remaining_bal = $booking['total'] - $booking['totpaid'];
9952 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9953 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9954
9955 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9956 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9957
9958 if (VBOPlatformDetection::isWordPress()) {
9959 /**
9960 * @wponly Rewrite URI for front-end
9961 */
9962 $book_link = str_replace(JUri::root(), '', $book_link);
9963 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9964 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9965 if ($itemid) {
9966 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9967 }
9968 } else {
9969 /**
9970 * @joomlaonly Rewrite URI for front-end
9971 */
9972 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9973 $uri_parts = [
9974 'option' => 'com_vikbooking',
9975 'view' => 'booking',
9976 'sid' => $use_sid,
9977 'ts' => $booking['ts'],
9978 ];
9979 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9980 // inject lang in query string for proper link routing
9981 $uri_parts['lang'] = $booking['lang'];
9982 }
9983 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9984 }
9985
9986 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9987 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9988 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9989
9990 return $tpl;
9991 }
9992
9993 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9994 $dbo = JFactory::getDbo();
9995 if (!class_exists('VboApplication')) {
9996 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9997 }
9998 $vbo_app = new VboApplication;
9999 if (empty($oid)) {
10000 return false;
10001 }
10002 $sms_api = self::getSMSAPIClass();
10003 if (empty($sms_api)) {
10004 return false;
10005 }
10006 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
10007 return false;
10008 }
10009 $sms_api_params = self::getSMSParams();
10010 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
10011 return false;
10012 }
10013 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
10014 return false;
10015 }
10016 $send_sms_to = self::getSendSMSTo();
10017 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
10018 return false;
10019 }
10020 $booking = array();
10021 $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.";";
10022 $dbo->setQuery($q);
10023 $dbo->execute();
10024 if ($dbo->getNumRows() > 0) {
10025 $booking = $dbo->loadAssoc();
10026 }
10027 if (!(count($booking) > 0)) {
10028 return false;
10029 }
10030 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
10031 return false;
10032 }
10033 $booking_rooms = array();
10034 $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'].";";
10035 $dbo->setQuery($q);
10036 $dbo->execute();
10037 if ($dbo->getNumRows() > 0) {
10038 $booking_rooms = $dbo->loadAssocList();
10039 }
10040 $admin_phone = self::getSMSAdminPhone();
10041 $admin_sendermail = self::getSenderMail();
10042 $admin_email = self::getAdminMail();
10043 $f_result = false;
10044 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
10045 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
10046 //SMS for the administrator
10047 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
10048 if (!empty($sms_text)) {
10049 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10050 //administrator phone can contain multiple numbers separated by comma or semicolon
10051 $admin_phones = array();
10052 if (strpos($admin_phone, ',') !== false) {
10053 $all_phones = explode(',', $admin_phone);
10054 foreach ($all_phones as $aph) {
10055 if (!empty($aph)) {
10056 $admin_phones[] = trim($aph);
10057 }
10058 }
10059 } elseif (strpos($admin_phone, ';') !== false) {
10060 $all_phones = explode(';', $admin_phone);
10061 foreach ($all_phones as $aph) {
10062 if (!empty($aph)) {
10063 $admin_phones[] = trim($aph);
10064 }
10065 }
10066 } else {
10067 $admin_phones[] = $admin_phone;
10068 }
10069 foreach ($admin_phones as $admphone) {
10070 $response_obj = $sms_obj->sendMessage($admphone, strip_tags($sms_text));
10071 if ( !$sms_obj->validateResponse($response_obj) ) {
10072 //notify the administrator via email with the error of the SMS sending
10073 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10074 } else {
10075 $f_result = true;
10076 }
10077 }
10078 }
10079 }
10080 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
10081 //SMS for the Customer
10082 $vbo_tn = self::getTranslator();
10083 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
10084 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
10085 if (!empty($sms_text)) {
10086 $sms_obj = new VikSmsApi($booking, $sms_api_params);
10087 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
10088 if ( !$sms_obj->validateResponse($response_obj) ) {
10089 //notify the administrator via email with the error of the SMS sending
10090 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
10091 } else {
10092 $f_result = true;
10093 }
10094 }
10095 }
10096 return $f_result;
10097 }
10098
10099 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
10100 {
10101 if (!defined('_VIKBOOKINGEXEC')) {
10102 define('_VIKBOOKINGEXEC', '1');
10103 }
10104
10105 ob_start();
10106 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
10107 $content = ob_get_contents();
10108 ob_end_clean();
10109
10110 $default_params = array(
10111 'show_header' => 0,
10112 'header_data' => array(),
10113 'show_footer' => 0,
10114 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10115 'pdf_unit' => 'PDF_UNIT',
10116 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10117 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10118 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10119 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10120 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10121 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10122 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10123 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10124 'header_font_size' => '10',
10125 'body_font_size' => '10',
10126 'footer_font_size' => '8',
10127 'show_lines_taxrate_col' => 0,
10128 );
10129
10130 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
10131 $default_params = array_merge($default_params, $invoice_params);
10132 }
10133
10134 return array($content, $default_params);
10135 }
10136
10137 /**
10138 * Includes within a buffer the template file for the custom (manual) invoice.
10139 *
10140 * @param array $invoice the record of the custom invoice
10141 * @param array $customer the customer record
10142 *
10143 * @return string the HTML content of the template file
10144 *
10145 * @since 1.11.1
10146 */
10147 public static function loadCustomInvoiceTmpl($invoice, $customer) {
10148 if (!defined('_VIKBOOKINGEXEC')) {
10149 define('_VIKBOOKINGEXEC', '1');
10150 }
10151 ob_start();
10152 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
10153 $content = ob_get_contents();
10154 ob_end_clean();
10155 $default_params = array(
10156 'show_header' => 0,
10157 'header_data' => array(),
10158 'show_footer' => 0,
10159 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10160 'pdf_unit' => 'PDF_UNIT',
10161 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10162 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10163 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10164 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10165 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10166 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10167 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10168 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10169 'header_font_size' => '10',
10170 'body_font_size' => '10',
10171 'footer_font_size' => '8'
10172 );
10173 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
10174 $default_params = array_merge($default_params, $invoice_params);
10175 }
10176 return array($content, $default_params);
10177 }
10178
10179 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
10180 {
10181 // clone the original template file source
10182 $parsed = $invoicetpl;
10183
10184 /**
10185 * Parse all conditional text rules.
10186 *
10187 * @since 1.14 (J) - 1.4.0 (WP)
10188 */
10189 self::getConditionalRulesInstance()
10190 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
10191 ->parseTokens($parsed);
10192
10193 $dbo = JFactory::getDbo();
10194 if (is_null($vbo_tn)) {
10195 $vbo_tn = self::getTranslator();
10196 }
10197 // availability helper
10198 $av_helper = self::getAvailabilityInstance();
10199
10200 $nowdf = self::getDateFormat();
10201 if ($nowdf == "%d/%m/%Y") {
10202 $df = 'd/m/Y';
10203 } elseif ($nowdf == "%m/%d/%Y") {
10204 $df = 'm/d/Y';
10205 } else {
10206 $df = 'Y/m/d';
10207 }
10208 $datesep = self::getDateSeparator();
10209
10210 /**
10211 * Split stay reservation.
10212 *
10213 * @since 1.16.0 (J) - 1.6.0 (WP)
10214 */
10215 $room_stay_dates = [];
10216 if ($booking['split_stay']) {
10217 if ($booking['status'] == 'confirmed') {
10218 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
10219 } else {
10220 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
10221 }
10222 // immediately count the number of nights of stay for each split room
10223 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
10224 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
10225 // overwrite values for compatibility with non-confirmed bookings
10226 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
10227 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
10228 }
10229 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
10230 // overwrite the whole array
10231 $room_stay_dates[$sps_r_k] = $sps_r_v;
10232 }
10233 }
10234
10235 $companylogo = self::getSiteLogo();
10236 $uselogo = '';
10237 if (!empty($companylogo)) {
10238 /**
10239 * Let's try to prevent TCPDF errors.
10240 *
10241 * @since August 2nd 2019
10242 */
10243 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10244 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10245 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10246 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10247 }
10248 }
10249 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10250 $parsed = str_replace("{company_info}", $company_info, $parsed);
10251 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
10252 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
10253 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
10254 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10255
10256 // custom fields replacement
10257 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10258 if (is_array($cmatches[1]) && $cmatches[1]) {
10259 $cfids = array();
10260 foreach ($cmatches[1] as $cfid) {
10261 $cfids[] = $cfid;
10262 }
10263 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10264 $dbo->setQuery($q);
10265 $cfields = $dbo->loadAssocList();
10266 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10267 $cfmap = array();
10268 foreach ($cfields as $cf) {
10269 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10270 }
10271 $cfmapreplace = array();
10272 $partsreceived = explode("\n", $booking['custdata']);
10273 if (count($partsreceived) > 0) {
10274 foreach ($partsreceived as $pst) {
10275 if (!empty($pst)) {
10276 $tmpdata = explode(":", $pst);
10277 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10278 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10279 }
10280 }
10281 }
10282 }
10283 foreach ($cmatches[1] as $cfid) {
10284 if (array_key_exists($cfid, $cfmapreplace)) {
10285 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10286 } else {
10287 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10288 }
10289 }
10290 }
10291
10292 // invoice price description - Start
10293 $rooms = array();
10294 $tars = array();
10295 $arrpeople = array();
10296 $is_package = !empty($booking['pkg']) ? true : false;
10297 $tot_adults = 0;
10298 $tot_children = 0;
10299 $tot_guests = 0;
10300 foreach ($booking_rooms as $kor => $or) {
10301 $num = $kor + 1;
10302 $rooms[$num] = $or;
10303
10304 // determine the days to consider for the count of the availability
10305 $room_nights = $booking['days'];
10306 $room_checkin = $booking['checkin'];
10307 $room_checkout = $booking['checkout'];
10308 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10309 $room_nights = $room_stay_dates[$kor]['nights'];
10310 $room_checkin = $room_stay_dates[$kor]['checkin'];
10311 $room_checkout = $room_stay_dates[$kor]['checkout'];
10312 }
10313
10314 $arrpeople[$num]['adults'] = $or['adults'];
10315 $arrpeople[$num]['children'] = $or['children'];
10316 $tot_adults += $or['adults'];
10317 $tot_children += $or['children'];
10318 $tot_guests += ($or['adults'] + $or['children']);
10319 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10320 //package or custom cost set from the back-end
10321 continue;
10322 }
10323 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
10324 $dbo->setQuery($q);
10325 $tar = $dbo->loadAssocList();
10326 if ($tar) {
10327 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
10328 //different usage
10329 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
10330 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
10331 //Occupancy Override
10332 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
10333 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
10334 //
10335 if (is_array($diffusageprice)) {
10336 //set a charge or discount to the price(s) for the different usage of the room
10337 foreach ($tar as $kpr => $vpr) {
10338 $tar[$kpr]['diffusage'] = $or['adults'];
10339 if ($diffusageprice['chdisc'] == 1) {
10340 //charge
10341 if ($diffusageprice['valpcent'] == 1) {
10342 //fixed value
10343 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10344 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10345 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
10346 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10347 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
10348 } else {
10349 //percentage value
10350 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10351 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
10352 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
10353 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10354 $tar[$kpr]['cost'] = $aduseval;
10355 }
10356 } else {
10357 //discount
10358 if ($diffusageprice['valpcent'] == 1) {
10359 //fixed value
10360 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
10361 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
10362 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
10363 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10364 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
10365 } else {
10366 //percentage value
10367 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
10368 $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);
10369 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
10370 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
10371 $tar[$kpr]['cost'] = $aduseval;
10372 }
10373 }
10374 }
10375 }
10376 }
10377 //
10378 $tars[$num] = $tar[0];
10379 }
10380 }
10381 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10382 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10383 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10384 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10385 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10386 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10387
10388 $isdue = 0;
10389 $tot_taxes = 0;
10390 $tot_city_taxes = 0;
10391 $tot_fees = 0;
10392 $pricestr = array();
10393 $optstr = array();
10394 // start building the tax summary
10395 VBOTaxonomySummary::start();
10396 foreach ($booking_rooms as $kor => $or) {
10397 $num = $kor + 1;
10398
10399 // determine the days to consider for the count of the availability
10400 $room_nights = $booking['days'];
10401 $room_checkin = $booking['checkin'];
10402 $room_checkout = $booking['checkout'];
10403 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
10404 $room_nights = $room_stay_dates[$kor]['nights'];
10405 $room_checkin = $room_stay_dates[$kor]['checkin'];
10406 $room_checkout = $room_stay_dates[$kor]['checkout'];
10407 }
10408
10409 $pricestr[$num] = array();
10410 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10411 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
10412 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
10413 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
10414 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
10415 $pricestr[$num]['tot'] = $calctar;
10416 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
10417 $tot_taxes += ($calctar - $cost_minus_tax);
10418 $isdue += $calctar;
10419 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
10420 $pricestr[$num]['tax_rate'] = $tax_rate;
10421 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
10422 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10423 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
10424 $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'] : "");
10425 $pricestr[$num]['tot'] = $calctar;
10426 $tars[$num]['calctar'] = $calctar;
10427 $isdue += $calctar;
10428 if ($calctar == $display_rate) {
10429 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
10430 $tot_taxes += ($display_rate - $cost_minus_tax);
10431 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
10432 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
10433 $pricestr[$num]['tax_rate'] = $tax_rate;
10434 } else {
10435 $tot_taxes += ($calctar - $display_rate);
10436 $pricestr[$num]['tax'] = ($calctar - $display_rate);
10437 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
10438 $pricestr[$num]['tax_rate'] = $tax_rate;
10439 }
10440 }
10441 $optstr[$num] = array();
10442 $opt_ind = 0;
10443 if (!empty($or['optionals'])) {
10444 $stepo = explode(";", $or['optionals']);
10445 foreach ($stepo as $roptkey => $oo) {
10446 if (!empty($oo)) {
10447 $stept = explode(":", $oo);
10448 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
10449 $dbo->setQuery($q);
10450 $actopt = $dbo->loadAssocList();
10451 if ($actopt) {
10452 if (is_object($vbo_tn)) {
10453 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
10454 }
10455 $optstr[$num][$opt_ind] = array();
10456 $chvar = '';
10457 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
10458 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
10459 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
10460 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
10461 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
10462 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
10463 $agestept = explode('-', $stept[1]);
10464 $stept[1] = $agestept[0];
10465 $chvar = $agestept[1];
10466 $realcost = 0;
10467 if (!empty($chvar)) {
10468 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
10469 //percentage value of the adults tariff
10470 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10471 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10472 } else {
10473 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10474 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10475 }
10476 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
10477 //VBO 1.10 - percentage value of room base cost
10478 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10479 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
10480 } else {
10481 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
10482 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
10483 }
10484 }
10485 $actopt[0]['chageintv'] = $chvar;
10486 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
10487 $actopt[0]['quan'] = $stept[1];
10488 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
10489 }
10490 } else {
10491 $actopt[0]['quan'] = $stept[1];
10492 // VBO 1.11 - options percentage cost of the room total fee
10493 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
10494 $deftar_basecosts = $or['cust_cost'];
10495 } else {
10496 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
10497 }
10498 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
10499 //
10500 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
10501 }
10502 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
10503 $realcost = $actopt[0]['maxprice'];
10504 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
10505 $realcost = $actopt[0]['maxprice'] * $stept[1];
10506 }
10507 }
10508 if ($actopt[0]['perperson'] == 1) {
10509 $realcost = $realcost * $or['adults'];
10510 }
10511
10512 /**
10513 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
10514 *
10515 * @since 1.17.7 (J) - 1.7.7 (WP)
10516 */
10517 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $booking, $or]);
10518 if ($custom_calculation) {
10519 $realcost = (float) $custom_calculation[0];
10520 }
10521
10522 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
10523 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
10524 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
10525 $optstr[$num][$opt_ind]['tax'] = 0;
10526 if ($actopt[0]['is_citytax'] == 1) {
10527 $tot_city_taxes += $tmpopr;
10528 } elseif ($actopt[0]['is_fee'] == 1) {
10529 $tot_fees += $tmpopr;
10530 }
10531 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
10532 if ($tmpopr == $realcost) {
10533 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
10534 $tot_taxes += ($realcost - $opt_minus_tax);
10535 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
10536 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
10537 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10538 } else {
10539 $tot_taxes += ($tmpopr - $realcost);
10540 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
10541 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
10542 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10543 }
10544 //
10545 $opt_ind++;
10546 $isdue += $tmpopr;
10547 }
10548 }
10549 }
10550 }
10551
10552 // custom extra costs
10553 if (!empty($or['extracosts'])) {
10554 $cur_extra_costs = json_decode($or['extracosts'], true);
10555 foreach ($cur_extra_costs as $eck => $ecv) {
10556 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10557 $isdue += $ecplustax;
10558 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
10559 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
10560 $optstr[$num][$opt_ind]['tax'] = 0;
10561 if ($ecplustax == $ecv['cost']) {
10562 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
10563 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
10564 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
10565 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
10566 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10567 } else {
10568 $tot_taxes += ($ecplustax - $ecv['cost']);
10569 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
10570 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
10571 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
10572 }
10573 $opt_ind++;
10574 }
10575 }
10576 }
10577
10578 $usedcoupon = false;
10579 if (strlen($booking['coupon']) > 0) {
10580 $orig_isdue = $isdue;
10581 $expcoupon = explode(";", $booking['coupon']);
10582 $usedcoupon = $expcoupon;
10583 $isdue = $isdue - (float)$expcoupon[1];
10584 if ($isdue != $orig_isdue) {
10585 //lower taxes proportionally
10586 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10587 }
10588 }
10589 if ($booking['refund'] > 0) {
10590 $orig_isdue = $isdue;
10591 $isdue -= $booking['refund'];
10592 if ($isdue != $orig_isdue) {
10593 //lower taxes proportionally
10594 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10595 }
10596 }
10597 $rows_written = 0;
10598 $inv_rows = '';
10599 foreach ($pricestr as $num => $price_descr) {
10600 // add room split stay information, if any
10601 $split_stay_str = '';
10602 $kor = $num - 1;
10603 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10604 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10605 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10606 }
10607
10608 $inv_rows .= '<tr>'."\n";
10609 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10610 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($price_descr['tot'] - $price_descr['tax'])), $booking['currencyname']) . '</td>'."\n";
10611 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tax']), $booking['currencyname']) . '</td>'."\n";
10612 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($price_descr['tot']), $booking['currencyname']) . '</td>'."\n";
10613 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10614 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10615 }
10616 $inv_rows .= '</tr>'."\n";
10617 $rows_written++;
10618 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10619 foreach ($optstr[$num] as $optk => $optv) {
10620 $inv_rows .= '<tr>'."\n";
10621 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10622 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat(($optv['tot'] - $optv['tax'])), $booking['currencyname']) . '</td>'."\n";
10623 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tax']), $booking['currencyname']) . '</td>'."\n";
10624 $inv_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($optv['tot']), $booking['currencyname']) . '</td>'."\n";
10625 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10626 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10627 }
10628 $inv_rows .= '</tr>'."\n";
10629 $rows_written++;
10630 }
10631 }
10632 }
10633
10634 // if discount print row
10635 if ($usedcoupon !== false) {
10636 $inv_rows .= '<tr>'."\n";
10637 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10638 $inv_rows .= '</tr>'."\n";
10639 $inv_rows .= '<tr>'."\n";
10640 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10641 $inv_rows .= '<td></td>'."\n";
10642 $inv_rows .= '<td></td>'."\n";
10643 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($usedcoupon[1]), $booking['currencyname']) . '</td>'."\n";
10644 $inv_rows .= '</tr>'."\n";
10645 $rows_written += 2;
10646 }
10647 // if refunded amount, print row
10648 if ($booking['refund'] > 0) {
10649 $inv_rows .= '<tr>'."\n";
10650 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10651 $inv_rows .= '</tr>'."\n";
10652 $inv_rows .= '<tr>'."\n";
10653 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10654 $inv_rows .= '<td></td>'."\n";
10655 $inv_rows .= '<td></td>'."\n";
10656 $inv_rows .= '<td>- ' . self::formatCurrencyNumber(self::numberFormat($booking['refund']), $booking['currencyname']) . '</td>'."\n";
10657 $inv_rows .= '</tr>'."\n";
10658 $rows_written += 2;
10659 }
10660 //
10661 $min_records = 8;
10662 if ($rows_written < $min_records) {
10663 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10664 $inv_rows .= '<tr>'."\n";
10665 $inv_rows .= '<td></td>'."\n";
10666 $inv_rows .= '<td></td>'."\n";
10667 $inv_rows .= '<td></td>'."\n";
10668 $inv_rows .= '</tr>'."\n";
10669 }
10670 }
10671 //invoice price description - End
10672 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10673 $parsed = str_replace("{invoice_totalnet}", self::formatCurrencyNumber(self::numberFormat(($isdue - $tot_taxes)), $booking['currencyname']), $parsed);
10674 $parsed = str_replace("{invoice_totaltax}", self::formatCurrencyNumber(self::numberFormat($tot_taxes), $booking['currencyname']), $parsed);
10675 $parsed = str_replace("{invoice_grandtotal}", self::formatCurrencyNumber(self::numberFormat($isdue), $booking['currencyname']), $parsed);
10676 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10677
10678 // invoice tax summary
10679 $tax_summary = VBOTaxonomySummary::get();
10680 $tax_names = VBOTaxonomySummary::getNames();
10681 $taxsum_rows = "";
10682 foreach ($tax_summary as $tax_rate => $tax_amount) {
10683 if (!$tax_rate && !$tax_amount) {
10684 continue;
10685 }
10686 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10687 $taxsum_rows .= '<tr>' . "\n";
10688 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10689 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10690 $taxsum_rows .= '<td>' . self::formatCurrencyNumber(self::numberFormat($tax_amount), $booking['currencyname']) . '</td>' . "\n";
10691 $taxsum_rows .= '</tr>' . "\n";
10692 }
10693 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10694
10695 return $parsed;
10696 }
10697
10698 /**
10699 * Specific method for parsing the template file for the custom (manual) invoices.
10700 *
10701 * @param string $invoicetpl the plain custom invoice template file before parsing
10702 * @param array $invoice the invoice record
10703 * @param array $customer the customer record
10704 *
10705 * @return string the HTML content of the parsed custom invoice template
10706 *
10707 * @since 1.11.1
10708 */
10709 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10710 $nowdf = self::getDateFormat(true);
10711 if ($nowdf == "%d/%m/%Y") {
10712 $df = 'd/m/Y';
10713 } elseif ($nowdf == "%m/%d/%Y") {
10714 $df = 'm/d/Y';
10715 } else {
10716 $df = 'Y/m/d';
10717 }
10718 $datesep = self::getDateSeparator(true);
10719 $companylogo = self::getSiteLogo();
10720 $uselogo = '';
10721 if (!empty($companylogo)) {
10722 /**
10723 * Let's try to prevent TCPDF errors by checking if the file exists.
10724 *
10725 * @since August 2nd 2019
10726 */
10727 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10728 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10729 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10730 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10731 }
10732 }
10733 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10734 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10735 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10736 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10737 // customer information
10738 $custinfo = '';
10739 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10740 $custinfo .= $customer['email'] . "\n";
10741 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10742 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10743 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10744 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10745 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10746 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10747 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10748 // invoice notes
10749 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10750
10751 return $invoicetpl;
10752 }
10753
10754 /**
10755 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10756 *
10757 * @param array $booking the booking information.
10758 * @param int $invoice_num the invoice number.
10759 * @param string $invoice_suff the invoice suffix.
10760 * @param string $invoice_date the invoice date.
10761 * @param string $company_info optional company information.
10762 * @param bool $translate whether the invoice contents should be translated in the booking language.
10763 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10764 *
10765 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10766 *
10767 * @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.
10768 */
10769 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10770 {
10771 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10772 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10773 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10774
10775 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10776 return false;
10777 }
10778
10779 /**
10780 * Trigger event to allow third party plugins to override some values for the invoice generation.
10781 *
10782 * @since 1.16.3 (J) - 1.6.3 (WP)
10783 */
10784 $event_args = [
10785 'invoice_num' => $invoice_num,
10786 'invoice_suff' => $invoice_suff,
10787 'invoice_date' => $invoice_date,
10788 'company_info' => $company_info,
10789 'translate' => $translate,
10790 ];
10791 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10792 if (is_array($event_args) && $event_args) {
10793 // extract the new values to be used
10794 extract($event_args);
10795 }
10796
10797 $dbo = JFactory::getDbo();
10798 $vbo_tn = self::getTranslator();
10799
10800 // inject the currency name value
10801 $currencyname = self::getCurrencyName();
10802 $booking['currencyname'] = $currencyname;
10803
10804 // make sure we've got a customer ID assigned to the reservation record
10805 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10806 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10807 if ($booking_customer) {
10808 $booking['idcustomer'] = $booking_customer['id'];
10809 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10810 $booking['customer_pin'] = $booking_customer['pin'];
10811 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10812 }
10813 }
10814
10815 $nowdf = self::getDateFormat(true);
10816 if ($nowdf == "%d/%m/%Y") {
10817 $df = 'd/m/Y';
10818 } elseif ($nowdf == "%m/%d/%Y") {
10819 $df = 'm/d/Y';
10820 } else {
10821 $df = 'Y/m/d';
10822 }
10823 $datesep = self::getDateSeparator(true);
10824
10825 if (empty($invoice_date)) {
10826 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10827 $used_date = $booking['ts'];
10828 } else {
10829 /**
10830 * We could be re-generating an invoice for a booking that already had an invoice.
10831 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10832 * It is now possible to keep the same invoice date as the previous one, so check
10833 * what value contains $invoice_date to see if it's different from today's date.
10834 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10835 * to also check the length of the string $invoice_date before using that date.
10836 *
10837 * @since 1.10 - August 2018
10838 */
10839 $base_ts = time();
10840 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10841 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10842 }
10843 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10844 $used_date = $base_ts;
10845 }
10846
10847 // load room reservation records
10848 $q = $dbo->getQuery(true)
10849 ->select($dbo->qn('or') . '.*')
10850 ->select($dbo->qn('r.name', 'room_name'))
10851 ->select($dbo->qn('r.fromadult'))
10852 ->select($dbo->qn('r.toadult'))
10853 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10854 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10855 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10856 ->order($dbo->qn('or.id') . ' ASC');
10857
10858 $dbo->setQuery($q);
10859 $booking_rooms = $dbo->loadAssocList();
10860
10861 if (!$booking_rooms) {
10862 return false;
10863 }
10864
10865 // check if an invoice for this reservation exists
10866 $invoice_id = 0;
10867 $dbo->setQuery(
10868 $dbo->getQuery(true)
10869 ->select('*')
10870 ->from($dbo->qn('#__vikbooking_invoices'))
10871 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10872 ->order($dbo->qn('id') . ' DESC')
10873 );
10874 $invoice_data = $dbo->loadAssoc();
10875 if ($invoice_data) {
10876 $invoice_id = $invoice_data['id'];
10877 }
10878
10879 // if requested, prepare data to refresh the PDF invoice
10880 if ($refresh_pdf === true) {
10881 if (!$invoice_id || !$invoice_data) {
10882 // cannot refresh the PDF if an invoice was never created before
10883 return false;
10884 }
10885
10886 // load the same exact previous information to refresh the invoice
10887 $invoice_num = $invoice_data['number'];
10888 $invoice_suff = $invoice_suff;
10889 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10890 $used_date = $invoice_data['for_date'];
10891
10892 // parse the previous (current) invoice number
10893 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10894 if ($matches) {
10895 // overwrite values
10896 $invoice_num = $matches[1] ?: $invoice_num;
10897 $invoice_suff = $matches[2] ?: $invoice_suff;
10898 }
10899 }
10900
10901 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10902 if ($translate === true) {
10903 if (!empty($booking['lang'])) {
10904 $lang = JFactory::getLanguage();
10905 if ($lang->getTag() != $booking['lang']) {
10906 if (VBOPlatformDetection::isWordPress()) {
10907 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10908 } else {
10909 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10910 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10911 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10912 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10913 }
10914 }
10915 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10916 // force the translation to start because contents should be translated
10917 $vbo_tn::$force_tolang = $booking['lang'];
10918 }
10919 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10920 }
10921 }
10922
10923 // the PDF file name
10924 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10925
10926 /**
10927 * If an analogic invoice is not available, make sure to create the record
10928 * before letting the e-invoicing drivers run to avoid a loop.
10929 *
10930 * @since 1.16.7 (J) - 1.6.7 (WP)
10931 */
10932 $retval = null;
10933 if (!$refresh_pdf && !$invoice_id) {
10934 // prepare record object
10935 $invoice_record = new stdClass;
10936 $invoice_record->number = $invoice_num . $invoice_suff;
10937 $invoice_record->file_name = $pdffname;
10938 $invoice_record->idorder = (int)$booking['id'];
10939 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10940 $invoice_record->created_on = time();
10941 $invoice_record->for_date = (int)$used_date;
10942
10943 // insert record
10944 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10945 return false;
10946 }
10947
10948 $retval = $invoice_record->id ?? false;
10949
10950 if (!$retval) {
10951 return false;
10952 }
10953 }
10954
10955 /**
10956 * The generation of the analogic invoices can trigger the drivers for the
10957 * generation of the e-Invoices if they are set to automatically run.
10958 * However, the e-Invoicing classes may be calling this method, so we need
10959 * to make sure the eInvoicing class is not running before proceeding.
10960 * This method could be called within a loop, and so the second iterations
10961 * may already have loaded the eInvocing class. For this we use a static variable.
10962 *
10963 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10964 * though additional information may be available only after the transmission. On top
10965 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10966 */
10967 static $einvocing_can_run = false;
10968
10969 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10970 // allow second iterations calling this method to run
10971 $einvocing_can_run = true;
10972 }
10973
10974 // always load all drivers, db and plugin based ones
10975 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10976
10977 foreach ($drivers as $driver_obj) {
10978 // set the flag of the external call
10979 $driver_obj->externalCall = __METHOD__;
10980
10981 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10982 $driver_obj->externalData['einvnum'] = $invoice_num;
10983
10984 // let the driver eventually elaborate the booking information before invoicing
10985 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10986
10987 if ($einvocing_can_run === true && !$refresh_pdf) {
10988 // generate the e-Invoice
10989 $driver_err = '';
10990 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10991 $driver_err = $driver_obj->getError();
10992 VikError::raiseWarning('', $driver_err);
10993 }
10994
10995 // Booking History - store log for the e-Invoice result
10996 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10997 }
10998 }
10999
11000 /**
11001 * Start the generation of the analogic (courtesy) PDF invoice.
11002 */
11003
11004 // load dependencies
11005 if (!class_exists('TCPDF')) {
11006 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11007 }
11008 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11009
11010 /**
11011 * Trigger event to allow third party plugins to return a specific font name.
11012 *
11013 * @since 1.16.0 (J) - 1.6.0 (WP)
11014 */
11015 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11016 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11017 $usepdffont = $custom_pdf_font[0];
11018 }
11019
11020 // set array variable to the template file
11021 $booking_info = self::getBookingInfoFromID($booking['id']);
11022
11023 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
11024
11025 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
11026
11027 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11028
11029 if (is_file($pathpdf)) {
11030 @unlink($pathpdf);
11031 }
11032
11033 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11034
11035 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11036 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
11037
11038 // header for each page of the pdf
11039 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
11040 $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]);
11041 }
11042
11043 // change some currencies to their unicode (decimal) value
11044 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11045 if (array_key_exists($booking['currencyname'], $unichr_map)) {
11046 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
11047 }
11048
11049 // header and footer fonts
11050 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11051 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11052
11053 // margins
11054 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11055 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11056 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11057
11058 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11059 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11060 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11061
11062 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11063 $pdf->SetPrintHeader(false);
11064 }
11065 if ($pdfparams['show_footer'] == 0) {
11066 $pdf->SetPrintFooter(false);
11067 }
11068
11069 $pdf->AddPage();
11070 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11071 $pdf->lastPage();
11072 $pdf->Output($pathpdf, 'F');
11073
11074 if (!is_file($pathpdf)) {
11075 return false;
11076 }
11077
11078 if (VBOPlatformDetection::isWordPress()) {
11079 /**
11080 * @wponly - trigger files mirroring
11081 */
11082 VikBookingLoader::import('update.manager');
11083 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11084 }
11085
11086 // Booking History
11087 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
11088
11089 if ($retval) {
11090 // a new invoice ID was inserted above
11091 return $retval;
11092 }
11093
11094 // prepare record object to be updated (if insert needed, it's done at first)
11095 $invoice_record = new stdClass;
11096 $invoice_record->id = (int)$invoice_id;
11097 $invoice_record->number = $invoice_num . $invoice_suff;
11098 $invoice_record->file_name = $pdffname;
11099 $invoice_record->idorder = (int)$booking['id'];
11100 $invoice_record->idcustomer = (int)$booking['idcustomer'];
11101 $invoice_record->created_on = time();
11102 $invoice_record->for_date = (int)$used_date;
11103
11104 // update record
11105 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
11106
11107 // return the PDF invoice ID
11108 return $invoice_id;
11109 }
11110
11111 /**
11112 * Generates an analogic invoice in PDF format for a custom list of services.
11113 * No bookings are assigned to this custom invoice. The method parses the same
11114 * invoice template as for the regular process with real booking IDs.
11115 * The invoice number must be stored before calling this method.
11116 *
11117 * @param int $invoice_id the ID of the custom invoice record
11118 *
11119 * @return bool
11120 *
11121 * @since 1.11.1 (J) - 1.1.1 (WP)
11122 */
11123 public static function generateCustomInvoice($invoice_id)
11124 {
11125 $dbo = JFactory::getDbo();
11126
11127 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
11128 $dbo->setQuery($q);
11129 $invoice = $dbo->loadAssoc();
11130 if (!$invoice) {
11131 return false;
11132 }
11133
11134 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
11135 $rawcont = is_array($rawcont) ? $rawcont : array();
11136 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
11137 $invoice['rawcont'] = $rawcont;
11138 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
11139 if (!$rawcont || !$rows || !$customer) {
11140 // at least one invoice raw is mandatory as well as the customer
11141 return false;
11142 }
11143
11144 /**
11145 * Start the generation of the electronic invoice, if any driver was enabled.
11146 *
11147 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
11148 * though additional information would be available only after the transmission.
11149 */
11150 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
11151
11152 if ($drivers) {
11153 // make sure the invoice number is just a number
11154 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
11155
11156 // invoke all drivers that should run automatically
11157 foreach ($drivers as $driver_obj) {
11158 // set the flag of the external call
11159 $driver_obj->externalCall = __METHOD__;
11160
11161 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
11162 $driver_obj->externalData['einvnum'] = $invoice['number'];
11163 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
11164 $driver_obj->externalData['einvcustom'] = $invoice;
11165
11166 // prepare data array for the generation of the e-Invoice
11167 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
11168
11169 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
11170 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
11171 if ($preveinv !== false) {
11172 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
11173 }
11174
11175 // generate the e-Invoice
11176 $driver_err = '';
11177 if (!$driver_obj->generateEInvoice($einvdata)) {
11178 $driver_err = $driver_obj->getError();
11179 VikError::raiseWarning('', $driver_err);
11180 }
11181 }
11182 }
11183
11184 /**
11185 * Start the generation of the analogic (courtesy) PDF invoice.
11186 */
11187
11188 // load dependencies
11189 if (!class_exists('TCPDF')) {
11190 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
11191 }
11192 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
11193
11194 /**
11195 * Trigger event to allow third party plugins to return a specific font name.
11196 *
11197 * @since 1.16.0 (J) - 1.6.0 (WP)
11198 */
11199 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
11200 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
11201 $usepdffont = $custom_pdf_font[0];
11202 }
11203
11204 // load invoice template file
11205 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
11206
11207 // parse invoice template file
11208 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
11209
11210 $pdffname = $invoice['file_name'];
11211 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
11212
11213 if (is_file($pathpdf)) {
11214 @unlink($pathpdf);
11215 }
11216
11217 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
11218
11219 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
11220 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
11221
11222 // header for each page of the pdf
11223 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
11224 $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]);
11225 }
11226
11227 // change some currencies to their unicode (decimal) value
11228 $currencyname = self::getCurrencyName();
11229 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
11230 if (array_key_exists($currencyname, $unichr_map)) {
11231 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
11232 }
11233
11234 // header and footer fonts
11235 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
11236 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
11237
11238 // margins
11239 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
11240 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
11241 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
11242
11243 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
11244 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
11245 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
11246
11247 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
11248 $pdf->SetPrintHeader(false);
11249 }
11250 if ($pdfparams['show_footer'] == 0) {
11251 $pdf->SetPrintFooter(false);
11252 }
11253
11254 $pdf->AddPage();
11255 $pdf->writeHTML($invoice_body, true, false, true, false, '');
11256 $pdf->lastPage();
11257 $pdf->Output($pathpdf, 'F');
11258
11259 if (!is_file($pathpdf)) {
11260 return false;
11261 }
11262
11263 if (VBOPlatformDetection::isWordPress()) {
11264 /**
11265 * @wponly - trigger files mirroring
11266 */
11267 VikBookingLoader::import('update.manager');
11268 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
11269 }
11270
11271 // return the result of the generation of the PDF invoice
11272 return true;
11273 }
11274
11275 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
11276 {
11277 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
11278 return false;
11279 }
11280
11281 $dbo = JFactory::getDbo();
11282
11283 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
11284 $dbo->setQuery($q);
11285 $invoice_data = $dbo->loadAssoc();
11286
11287 if (!$invoice_data) {
11288 return false;
11289 }
11290
11291 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
11292 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
11293 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
11294 if (!file_exists($invoice_file_path)) {
11295 return false;
11296 }
11297
11298 $vbo_app = self::getVboApplication();
11299 $admin_sendermail = self::getSenderMail();
11300
11301 $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);
11302
11303 // update record
11304 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
11305 $dbo->setQuery($q);
11306 $dbo->execute();
11307
11308 return true;
11309 }
11310
11311 public static function loadCheckinDocTmpl(array $booking_info = [], array $booking_rooms = [], array $customer = [])
11312 {
11313 if (!defined('_VIKBOOKINGEXEC')) {
11314 define('_VIKBOOKINGEXEC', '1');
11315 }
11316
11317 ob_start();
11318 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php';
11319 $content = ob_get_contents();
11320 ob_end_clean();
11321
11322 $default_params = [
11323 'show_header' => 0,
11324 'header_data' => [],
11325 'show_footer' => 0,
11326 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
11327 'pdf_unit' => 'PDF_UNIT',
11328 'pdf_page_format' => 'PDF_PAGE_FORMAT',
11329 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
11330 'pdf_margin_top' => 'PDF_MARGIN_TOP',
11331 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
11332 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
11333 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
11334 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
11335 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
11336 'header_font_size' => '10',
11337 'body_font_size' => '10',
11338 'footer_font_size' => '8'
11339 ];
11340
11341 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
11342 $default_params = array_merge($default_params, $checkin_params);
11343 }
11344
11345 return [$content, $default_params];
11346 }
11347
11348 public static function parseCheckinDocTemplate(string $checkintpl, array $booking, array $booking_rooms, array $customer = [])
11349 {
11350 $dbo = JFactory::getDbo();
11351 $app = JFactory::getApplication();
11352
11353 // clone string
11354 $parsed = $checkintpl;
11355
11356 $datesep = self::getDateSeparator();
11357 $nowdf = self::getDateFormat();
11358 if ($nowdf == "%d/%m/%Y") {
11359 $df = 'd/m/Y';
11360 } elseif ($nowdf == "%m/%d/%Y") {
11361 $df = 'm/d/Y';
11362 } else {
11363 $df = 'Y/m/d';
11364 }
11365
11366 $company_name = self::getFrontTitle();
11367 $company_info = self::getInvoiceCompanyInfo();
11368 $companylogo = self::getSiteLogo();
11369 $uselogo = '';
11370 if (!empty($companylogo)) {
11371 /**
11372 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
11373 *
11374 * @since March 16th 2021
11375 */
11376 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11377 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
11378 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
11379 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
11380 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
11381 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
11382 }
11383 }
11384
11385 $parsed = str_replace("{company_name}", $company_name, $parsed);
11386 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
11387 $parsed = str_replace("{company_info}", $company_info, $parsed);
11388 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
11389 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
11390 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
11391 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
11392
11393 $tot_guests = 0;
11394 $tot_adults = 0;
11395 $tot_children = 0;
11396 foreach ($booking_rooms as $kor => $or) {
11397 $tot_guests += ($or['adults'] + $or['children']);
11398 $tot_adults += $or['adults'];
11399 $tot_children += $or['children'];
11400 }
11401 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
11402 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
11403 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
11404 if ($customer && isset($customer['comments'])) {
11405 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
11406 }
11407
11408 $termsconds = self::getTermsConditions();
11409 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
11410
11411 // custom fields replacemenet
11412 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
11413 if (is_array($cmatches[1]) && $cmatches[1]) {
11414 $cfids = array();
11415 foreach ($cmatches[1] as $cfid) {
11416 $cfids[] = $cfid;
11417 }
11418 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
11419 $dbo->setQuery($q);
11420 $cfields = $dbo->loadAssocList();
11421 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
11422 $cfmap = array();
11423 foreach ($cfields as $cf) {
11424 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
11425 }
11426 $cfmapreplace = array();
11427 $partsreceived = explode("\n", $booking['custdata']);
11428 if (count($partsreceived) > 0) {
11429 foreach ($partsreceived as $pst) {
11430 if (!empty($pst)) {
11431 $tmpdata = explode(":", $pst);
11432 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
11433 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
11434 }
11435 }
11436 }
11437 }
11438 foreach ($cmatches[1] as $cfid) {
11439 if (array_key_exists($cfid, $cfmapreplace)) {
11440 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
11441 } else {
11442 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
11443 }
11444 }
11445 }
11446
11447 /**
11448 * Parse all conditional text rules.
11449 */
11450 self::getConditionalRulesInstance()
11451 ->set(['booking', 'rooms'], [$booking, $booking_rooms])
11452 ->parseTokens($parsed);
11453
11454 /**
11455 * Parse all Door Access Control tags.
11456 *
11457 * @since 1.18.4 (J) - 1.8.4 (WP)
11458 */
11459 VBOFactory::getDoorAccessControl()
11460 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $parsed);
11461
11462 return $parsed;
11463 }
11464
11465 /**
11466 * Returns an array of key-value pairs to be used for building the Guest Details
11467 * in the Check-in process. The keys will be compared to the fields of the table
11468 * #__customers to see if some values already exist. The values can use lang defs
11469 * of both front-end or back-end. To be called as list(fields, attributes).
11470 *
11471 * @param boolean $precheckin true if requested for front-end pre check-in.
11472 * @param string $type optional type of custom pax fields to force.
11473 *
11474 * @return array key-value pairs for showing and collecting guest details.
11475 *
11476 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
11477 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
11478 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
11479 */
11480 public static function getPaxFields($precheckin = false, $type = null)
11481 {
11482 // check the type of pax fields collection data
11483 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
11484
11485 if (!$precheckin) {
11486 // back-end check-in ("registration") key-value pairs
11487
11488 // return the requested pax fields collection list
11489 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
11490
11491 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11492 // requested driver returned a list of fields
11493 return $custom_pax_fields;
11494 }
11495
11496 // in case the driver is unsupported, fallback to "basic" (default) driver
11497 return VBOCheckinPax::getFields('basic');
11498 }
11499
11500 // default front-end key-value pairs for pre check-in
11501 $precheckin_pax_fields = [
11502 [
11503 'first_name' => JText::translate('VBCCFIRSTNAME'),
11504 'last_name' => JText::translate('VBCCLASTNAME'),
11505 'date_birth' => JText::translate('ORDER_DBIRTH'),
11506 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
11507 'country' => JText::translate('ORDER_STATE'),
11508 'city' => JText::translate('ORDER_CITY'),
11509 'zip' => JText::translate('ORDER_ZIP'),
11510 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
11511 'gender' => JText::translate('VBOCUSTGENDER'),
11512 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
11513 'docnum' => JText::translate('VBOCUSTDOCNUM'),
11514 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
11515 ],
11516 [
11517 'first_name' => 'text',
11518 'last_name' => 'text',
11519 'date_birth' => 'calendar',
11520 'place_birth' => 'text',
11521 'country' => 'country',
11522 'city' => 'text',
11523 'zip' => 'text',
11524 'nationality' => 'country',
11525 'gender' => [
11526 'M' => JText::translate('VBOCUSTGENDERM'),
11527 'F' => JText::translate('VBOCUSTGENDERF'),
11528 ],
11529 'doctype' => 'text',
11530 'docnum' => 'text',
11531 'documents' => 'file',
11532 ],
11533 ];
11534
11535 // access the pre-checkin pax fields collection list from the current collector
11536 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
11537
11538 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
11539 // set the pax fields for pre-checkin from the given data collection driver
11540 $precheckin_pax_fields = $custom_pax_fields;
11541 }
11542
11543 // make a safe copy of the default precheckin fields
11544 $use_precheckin_pax_fields = $precheckin_pax_fields;
11545
11546 /**
11547 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
11548 *
11549 * @since 1.16.3 (J) - 1.6.3 (WP)
11550 */
11551 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
11552
11553 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
11554 // use the filtered pax fields
11555 $use_precheckin_pax_fields = $precheckin_pax_fields;
11556 }
11557
11558 // return the front-end key-value pairs for pre check-in
11559 return $use_precheckin_pax_fields;
11560 }
11561
11562 /**
11563 * Returns the associative list of countries from the DB.
11564 *
11565 * @param bool $tn whether to translate the country name.
11566 * @param bool $no_id whether to unset the ID of the country.
11567 *
11568 * @return array associative or empty array.
11569 *
11570 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
11571 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
11572 */
11573 public static function getCountriesArray($tn = true, $no_id = true)
11574 {
11575 $dbo = JFactory::getDbo();
11576 $all_countries = [];
11577
11578 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
11579 $dbo->setQuery($q);
11580 $countries = $dbo->loadAssocList();
11581 if (!$countries) {
11582 return [];
11583 }
11584
11585 if ($tn === true) {
11586 $vbo_tn = self::getTranslator();
11587 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
11588 // re-apply sorting by country name
11589 $sorting = [];
11590 foreach ($countries as $country) {
11591 $sorting[$country['country_name']] = $country;
11592 }
11593 ksort($sorting);
11594 $sorted = [];
11595 foreach ($sorting as $country) {
11596 $sorted[] = $country;
11597 }
11598 $countries = $sorted;
11599 unset($sorting, $sorted);
11600 }
11601
11602 foreach ($countries as $v) {
11603 if ($no_id) {
11604 // keep the original structure by unsetting the ID only needed for translation
11605 unset($v['id']);
11606 }
11607 $all_countries[$v['country_3_code']] = $v;
11608 }
11609
11610 return $all_countries;
11611 }
11612
11613 public static function getCountriesSelect(string $name, array $all_countries = [], ?string $current_value = '', ?string $empty_value = ' ', ?string $idAttr = null)
11614 {
11615 if (!$all_countries) {
11616 $all_countries = self::getCountriesArray();
11617 }
11618
11619 $countries = '<select ' . ($idAttr ? 'id="' . $idAttr . '" ' : '') . 'name="' . $name . '">' . "\n";
11620 if (strlen($empty_value)) {
11621 $countries .= '<option value="">' . $empty_value . '</option>' . "\n";
11622 }
11623 foreach ($all_countries as $v) {
11624 $countries .= '<option value="' . $v['country_3_code'] . '"' . ($v['country_3_code'] == $current_value ? ' selected="selected"' : '') . '>' . $v['country_name'] . '</option>' . "\n";
11625 }
11626 $countries .= '</select>';
11627
11628 return $countries;
11629 }
11630
11631 public static function getThumbSize($skipsession = false)
11632 {
11633 static $thumb_size = null;
11634
11635 if ($thumb_size) {
11636 return $thumb_size;
11637 }
11638
11639 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11640
11641 return $thumb_size;
11642 }
11643
11644 /**
11645 * Checks whether an iCal file for the reservation should be
11646 * attached to the confirmation email for customer and/or admin.
11647 *
11648 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11649 *
11650 * @since 1.12.0 (J) - 1.2.0 (WP)
11651 */
11652 public static function attachIcal()
11653 {
11654 return VBOFactory::getConfig()->getInt('attachical', 1);
11655 }
11656
11657 /**
11658 * How the calculation of the orphan dates should take place.
11659 *
11660 * @return string "next" for only checking the bookings ahead, "prevnext" if
11661 * also the previous bookings should be checked.
11662 *
11663 * @since 1.13 (J) - 1.3.0 (WP)
11664 */
11665 public static function orphansCalculation()
11666 {
11667 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11668 }
11669
11670 /**
11671 * Returns the name of the template file to use for the front-end View "search".
11672 *
11673 * @return string the name of the template file. New and upgraded user will both use the classic file.
11674 *
11675 * @since 1.13 (J) - 1.3.0 (WP)
11676 */
11677 public static function searchResultsTmpl()
11678 {
11679 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11680 }
11681
11682 /**
11683 * Returns a string without any new-line characters
11684 * to be used for JavaScript values without facing
11685 * errors like 'unterminated string literal'.
11686 * By passing nl2br($str) as argument, we can keep
11687 * the wanted new-line HTML tags for PRE tags.
11688 * We use implode() with just one argument to
11689 * not use an empty string as glue for the string.
11690 *
11691 * @param $str string
11692 *
11693 * @return string
11694 */
11695 public static function strTrimLiteral($str) {
11696 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11697 $lines = explode("\n", $str);
11698 $new_lines = array();
11699 foreach ($lines as $i => $line) {
11700 if (strlen($line)) {
11701 $new_lines[] = trim($line);
11702 }
11703 }
11704 return implode($new_lines);
11705 }
11706
11707 /**
11708 * Returns an instance of the VboApplication class.
11709 *
11710 * @return VboApplication
11711 */
11712 public static function getVboApplication()
11713 {
11714 // require library
11715 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11716
11717 // return a new instance
11718 return new VboApplication();
11719 }
11720
11721 /**
11722 * Returns the translation of the given 0-indexed week day.
11723 *
11724 * @param int $wd the 0-indexed day of the week.
11725 * @param bool $short whether to get the short version of the weekday.
11726 *
11727 * @return string the name of the week day.
11728 *
11729 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11730 */
11731 public static function sayWeekDay($wd, $short = false)
11732 {
11733 switch ($wd) {
11734 case '6' :
11735 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11736 break;
11737 case '5' :
11738 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11739 break;
11740 case '4' :
11741 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11742 break;
11743 case '3' :
11744 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11745 break;
11746 case '2' :
11747 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11748 break;
11749 case '1' :
11750 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11751 break;
11752 default :
11753 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11754 break;
11755 }
11756 return $ret;
11757 }
11758
11759 /**
11760 * Returns the translation of the given 1-indexed month of the year.
11761 *
11762 * @param int $idm the 1-indexed month of the year.
11763 * @param bool $short whether to get the short version of the month.
11764 *
11765 * @return string the name of the month.
11766 *
11767 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11768 */
11769 public static function sayMonth($idm, $short = false)
11770 {
11771 switch ($idm) {
11772 case '12' :
11773 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11774 break;
11775 case '11' :
11776 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11777 break;
11778 case '10' :
11779 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11780 break;
11781 case '9' :
11782 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11783 break;
11784 case '8' :
11785 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11786 break;
11787 case '7' :
11788 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11789 break;
11790 case '6' :
11791 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11792 break;
11793 case '5' :
11794 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11795 break;
11796 case '4' :
11797 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11798 break;
11799 case '3' :
11800 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11801 break;
11802 case '2' :
11803 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11804 break;
11805 default :
11806 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11807 break;
11808 }
11809 return $ret;
11810 }
11811
11812 public static function sayDayMonth($d) {
11813 switch ($d) {
11814 case '31' :
11815 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11816 break;
11817 case '30' :
11818 $ret = JText::translate('VBDAYMONTHTHIRTY');
11819 break;
11820 case '29' :
11821 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11822 break;
11823 case '28' :
11824 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11825 break;
11826 case '27' :
11827 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11828 break;
11829 case '26' :
11830 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11831 break;
11832 case '25' :
11833 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11834 break;
11835 case '24' :
11836 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11837 break;
11838 case '23' :
11839 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11840 break;
11841 case '22' :
11842 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11843 break;
11844 case '21' :
11845 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11846 break;
11847 case '20' :
11848 $ret = JText::translate('VBDAYMONTHTWENTY');
11849 break;
11850 case '19' :
11851 $ret = JText::translate('VBDAYMONTHNINETEEN');
11852 break;
11853 case '18' :
11854 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11855 break;
11856 case '17' :
11857 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11858 break;
11859 case '16' :
11860 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11861 break;
11862 case '15' :
11863 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11864 break;
11865 case '14' :
11866 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11867 break;
11868 case '13' :
11869 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11870 break;
11871 case '12' :
11872 $ret = JText::translate('VBDAYMONTHTWELVE');
11873 break;
11874 case '11' :
11875 $ret = JText::translate('VBDAYMONTHELEVEN');
11876 break;
11877 case '10' :
11878 $ret = JText::translate('VBDAYMONTHTEN');
11879 break;
11880 case '9' :
11881 $ret = JText::translate('VBDAYMONTHNINE');
11882 break;
11883 case '8' :
11884 $ret = JText::translate('VBDAYMONTHEIGHT');
11885 break;
11886 case '7' :
11887 $ret = JText::translate('VBDAYMONTHSEVEN');
11888 break;
11889 case '6' :
11890 $ret = JText::translate('VBDAYMONTHSIX');
11891 break;
11892 case '5' :
11893 $ret = JText::translate('VBDAYMONTHFIVE');
11894 break;
11895 case '4' :
11896 $ret = JText::translate('VBDAYMONTHFOUR');
11897 break;
11898 case '3' :
11899 $ret = JText::translate('VBDAYMONTHTHREE');
11900 break;
11901 case '2' :
11902 $ret = JText::translate('VBDAYMONTHTWO');
11903 break;
11904 default :
11905 $ret = JText::translate('VBDAYMONTHONE');
11906 break;
11907 }
11908 return $ret;
11909 }
11910
11911 public static function totElements($arr) {
11912 $n = 0;
11913 if (is_array($arr)) {
11914 foreach ($arr as $a) {
11915 if (!empty($a)) {
11916 $n++;
11917 }
11918 }
11919 return $n;
11920 }
11921 return false;
11922 }
11923
11924 /**
11925 * Returns a list of documents that were uploaded
11926 * for the specified customer.
11927 *
11928 * @param integer $id The customer ID.
11929 *
11930 * @return array A list of documents.
11931 *
11932 * @since 1.3.0
11933 */
11934 public static function getCustomerDocuments($id)
11935 {
11936 $dbo = JFactory::getDbo();
11937
11938 $q = $dbo->getQuery(true)
11939 ->select($dbo->qn('docsfolder'))
11940 ->from($dbo->qn('#__vikbooking_customers'))
11941 ->where($dbo->qn('id') . ' = ' . (int) $id);
11942
11943 $dbo->setQuery($q, 0, 1);
11944 $dbo->execute();
11945
11946 if (!$dbo->getNumRows())
11947 {
11948 // customer not found
11949 return array();
11950 }
11951
11952 // retrieve customer documents directory name
11953 $dirname = $dbo->loadResult();
11954
11955 if (empty($dirname))
11956 {
11957 // no available directory
11958 return array();
11959 }
11960
11961 // build documents folder path
11962 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11963
11964 if (!is_dir($dirname))
11965 {
11966 // the customer directory doesn't exist
11967 return array();
11968 }
11969
11970 // read all files from customer directory
11971 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11972
11973 $files = array();
11974
11975 foreach ($glob as $path)
11976 {
11977 // skip "index.html"
11978 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11979 {
11980 // extract name and extension from file path
11981 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11982 {
11983 $name = $match[1];
11984 $ext = $match[2];
11985 }
11986 else
11987 {
11988 $name = basename($path);
11989 $ext = '';
11990 }
11991
11992 $file = new stdClass;
11993 $file->path = $path;
11994 $file->name = $name;
11995 $file->ext = $ext;
11996 $file->basename = $file->name . '.' . $file->ext;
11997 $file->size = filesize($path);
11998 $file->date = filemtime($path);
11999 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
12000
12001 $files[] = $file;
12002 }
12003 }
12004
12005 // sort files by creation date
12006 usort($files, function($a, $b)
12007 {
12008 return $b->date - $a->date;
12009 });
12010
12011 return $files;
12012 }
12013
12014 public static function displayPaymentParameters($pfile, $pparams = '') {
12015 $html = '<p>---------</p>';
12016
12017 /**
12018 * @wponly The payment gateway is now loaded
12019 * using the apposite dispatcher.
12020 *
12021 * @since 1.0.5
12022 */
12023 JLoader::import('adapter.payment.dispatcher');
12024
12025 try
12026 {
12027 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
12028 }
12029 catch (Exception $e)
12030 {
12031 // payment not found
12032 $html = $e->getMessage();
12033
12034 if ($code = $e->getCode())
12035 {
12036 $html = '<b>' . $code . '</b> : ' . $html;
12037 }
12038
12039 return $html;
12040 }
12041 //
12042
12043 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
12044 $arrparams = !is_array($arrparams) ? array() : $arrparams;
12045
12046 // get admin parameters
12047 $pconfig = $payment->getAdminParameters();
12048
12049 if (count($pconfig) > 0) {
12050 $html = '';
12051 foreach ($pconfig as $value => $cont) {
12052 if (empty($value)) {
12053 continue;
12054 }
12055 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12056 $label = $labelparts[0];
12057 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12058 if (!empty($cont['help'])) {
12059 $labelhelp = $cont['help'];
12060 }
12061 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12062 $html .= '<div class="vbo-param-container">';
12063 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12064 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12065 }
12066 $html .= '<div class="vbo-param-setting">';
12067 switch ($cont['type']) {
12068 case 'custom':
12069 $html .= $cont['html'];
12070 break;
12071 case 'select':
12072 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12073 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12074 if (isset($cont['multiple']) && $cont['multiple']) {
12075 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
12076 } else {
12077 $html .= '<select name="vikpaymentparams['.$value.']">';
12078 }
12079 foreach ($options as $optkey => $poption) {
12080 $checkval = $is_assoc ? $optkey : $poption;
12081 $selected = false;
12082 if (isset($arrparams[$value])) {
12083 if (is_array($arrparams[$value])) {
12084 $selected = in_array($checkval, $arrparams[$value]);
12085 } else {
12086 $selected = ($checkval == $arrparams[$value]);
12087 }
12088 } elseif (isset($default_paramv)) {
12089 if (is_array($default_paramv)) {
12090 $selected = in_array($checkval, $default_paramv);
12091 } else {
12092 $selected = ($checkval == $default_paramv);
12093 }
12094 }
12095 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12096 }
12097 $html .= '</select>';
12098 break;
12099 case 'password':
12100 $html .= '<div class="btn-wrapper input-append">';
12101 $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"/>';
12102 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12103 $html .= '</div>';
12104 break;
12105 case 'number':
12106 $number_attr = array();
12107 if (isset($cont['min'])) {
12108 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12109 }
12110 if (isset($cont['max'])) {
12111 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12112 }
12113 if (isset($cont['step'])) {
12114 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12115 }
12116 $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) . '/>';
12117 break;
12118 case 'textarea':
12119 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12120 break;
12121 case 'hidden':
12122 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
12123 break;
12124 case 'checkbox':
12125 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12126 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
12127 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
12128 break;
12129 default:
12130 $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"/>';
12131 break;
12132 }
12133 if (strlen($labelhelp) > 0) {
12134 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12135 }
12136 $html .= '</div>';
12137 $html .= '</div>';
12138 }
12139 // JS helper function to toggle the password fields
12140 $html .= "\n" . '<script>' . "\n";
12141 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
12142 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12143 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12144 $html .= ' var inp_type = inp.attr("type");' . "\n";
12145 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12146 $html .= '}' . "\n";
12147 $html .= "\n" . '</script>' . "\n";
12148 }
12149
12150 return $html;
12151 }
12152
12153 public static function displaySMSParameters($pfile, $params = null)
12154 {
12155 $html = '---------';
12156
12157 $params = is_string($params) ? json_decode($params, true) : $params;
12158 $params = is_array($params) ? $params : [];
12159
12160 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
12161 return $html;
12162 }
12163
12164 // attempt to load the class file
12165 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
12166
12167 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
12168 return $html;
12169 }
12170
12171 // load the gateway parameters
12172 $config = VikSmsApi::getAdminParameters();
12173
12174 if (!is_array($config) || !count($config)) {
12175 return $html;
12176 }
12177
12178 // flags for JS helpers
12179 $js_helpers = array();
12180
12181 $html = '';
12182 foreach ($config as $value => $cont) {
12183 if (empty($value)) {
12184 continue;
12185 }
12186 $inp_attr = '';
12187 if (isset($cont['attributes'])) {
12188 foreach ($cont['attributes'] as $inpk => $inpv) {
12189 $inp_attr .= $inpk.'="'.$inpv.'" ';
12190 }
12191 $inp_attr = ' ' . rtrim($inp_attr);
12192 }
12193 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
12194 $label = $labelparts[0];
12195 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
12196 if (!empty($cont['help'])) {
12197 $labelhelp = $cont['help'];
12198 }
12199 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
12200 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
12201 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
12202 $html .= '<div class="vbo-param-label">'.$label.'</div>';
12203 }
12204 $html .= '<div class="vbo-param-setting">';
12205 switch ($cont['type']) {
12206 case 'custom':
12207 $html .= $cont['html'];
12208 break;
12209 case 'select':
12210 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
12211 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
12212 if (isset($cont['multiple']) && $cont['multiple']) {
12213 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
12214 } else {
12215 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
12216 }
12217 foreach ($options as $optkey => $poption) {
12218 $checkval = $is_assoc ? $optkey : $poption;
12219 $selected = false;
12220 if (isset($params[$value])) {
12221 if (is_array($params[$value])) {
12222 $selected = in_array($checkval, $params[$value]);
12223 } else {
12224 $selected = ($checkval == $params[$value]);
12225 }
12226 } elseif (isset($default_paramv)) {
12227 if (is_array($default_paramv)) {
12228 $selected = in_array($checkval, $default_paramv);
12229 } else {
12230 $selected = ($checkval == $default_paramv);
12231 }
12232 }
12233 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
12234 }
12235 $html .= '</select>';
12236 break;
12237 case 'password':
12238 $html .= '<div class="btn-wrapper input-append">';
12239 $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 . '/>';
12240 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
12241 $html .= '</div>';
12242 // set flag for JS helper
12243 $js_helpers[] = $cont['type'];
12244 break;
12245 case 'number':
12246 $number_attr = array();
12247 if (isset($cont['min'])) {
12248 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
12249 }
12250 if (isset($cont['max'])) {
12251 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
12252 }
12253 if (isset($cont['step'])) {
12254 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
12255 }
12256 $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 . '/>';
12257 break;
12258 case 'textarea':
12259 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
12260 break;
12261 case 'visual_html':
12262 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
12263 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
12264 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
12265 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
12266 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
12267 break;
12268 case 'hidden':
12269 $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 . '/>';
12270 break;
12271 case 'checkbox':
12272 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
12273 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
12274 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
12275 break;
12276 default:
12277 $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 . '/>';
12278 break;
12279 }
12280 if (strlen($labelhelp) > 0) {
12281 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
12282 }
12283 $html .= '</div>';
12284 $html .= '</div>';
12285 }
12286
12287 // JS helper functions
12288 if (in_array('password', $js_helpers)) {
12289 // toggle the password fields
12290 $html .= "\n" . '<script>' . "\n";
12291 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
12292 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
12293 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
12294 $html .= ' var inp_type = inp.attr("type");' . "\n";
12295 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
12296 $html .= '}' . "\n";
12297 $html .= "\n" . '</script>' . "\n";
12298 }
12299
12300 return $html;
12301 }
12302
12303 /**
12304 * Renders the params of the requested cron job file-class.
12305 *
12306 * @param string $file the name of the cron job driver file.
12307 * @param array $params the parameters array.
12308 *
12309 * @return string the necessary HTML content to render.
12310 *
12311 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
12312 */
12313 public static function displayCronParameters($file, $params = [])
12314 {
12315 try
12316 {
12317 // attempt to create a new instance
12318 $job = VBOFactory::getCronFactory()->createInstance($file);
12319 }
12320 catch (Exception $e)
12321 {
12322 // something went wrong, display error message
12323 return '<p>' . $e->getMessage() . '<p>';
12324 }
12325
12326 // get admin parameters
12327 $config = $job->getForm();
12328
12329 if (!is_array($config) || !$config) {
12330 return '<p>---------</p>';
12331 }
12332
12333 /**
12334 * Render the cron parameters through the global params rendering class.
12335 *
12336 * @since 1.16.9 (J) - 1.6.9 (WP)
12337 */
12338 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
12339 }
12340
12341 public static function invokeChannelManager($skiporder = true, $order = array()) {
12342 $task = VikRequest::getString('task', '', 'request');
12343 $view = VikRequest::getString('view', '', 'request');
12344 $tmpl = VikRequest::getString('tmpl', '', 'request');
12345 $noimpression = array('vieworder', 'booking');
12346 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')) {
12347 //VCM Channel Impression
12348 if (!class_exists('VikChannelManagerConfig')) {
12349 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12350 }
12351 if (!class_exists('VikChannelManager')) {
12352 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12353 }
12354 VikChannelManager::invokeChannelImpression();
12355 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
12356 //VCM Channel Conversion-Impression
12357 if (!class_exists('VikChannelManagerConfig')) {
12358 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
12359 }
12360 if (!class_exists('VikChannelManager')) {
12361 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
12362 }
12363 VikChannelManager::invokeChannelConversionImpression($order);
12364 }
12365 }
12366
12367 public static function validEmail($email) {
12368 $isValid = true;
12369 $atIndex = strrpos($email, "@");
12370 if (is_bool($atIndex) && !$atIndex) {
12371 $isValid = false;
12372 } else {
12373 $domain = substr($email, $atIndex +1);
12374 $local = substr($email, 0, $atIndex);
12375 $localLen = strlen($local);
12376 $domainLen = strlen($domain);
12377 if ($localLen < 1 || $localLen > 64) {
12378 // local part length exceeded
12379 $isValid = false;
12380 } else
12381 if ($domainLen < 1 || $domainLen > 255) {
12382 // domain part length exceeded
12383 $isValid = false;
12384 } else
12385 if ($local[0] == '.' || $local[$localLen -1] == '.') {
12386 // local part starts or ends with '.'
12387 $isValid = false;
12388 } else
12389 if (preg_match('/\\.\\./', $local)) {
12390 // local part has two consecutive dots
12391 $isValid = false;
12392 } else
12393 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
12394 // character not valid in domain part
12395 $isValid = false;
12396 } else
12397 if (preg_match('/\\.\\./', $domain)) {
12398 // domain part has two consecutive dots
12399 $isValid = false;
12400 } else
12401 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
12402 // character not valid in local part unless
12403 // local part is quoted
12404 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
12405 $isValid = false;
12406 }
12407 }
12408 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
12409 // domain not found in DNS
12410 $isValid = false;
12411 }
12412 }
12413 return $isValid;
12414 }
12415
12416 public static function caniWrite($path) {
12417 if ($path[strlen($path) - 1] == '/') {
12418 // ricorsivo return a temporary file path
12419 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
12420 }
12421 if (is_dir($path)) {
12422 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
12423 }
12424 // check tmp file for read/write capabilities
12425 $rm = file_exists($path);
12426 $f = @fopen($path, 'a');
12427 if ($f === false) {
12428 return false;
12429 }
12430 fclose($f);
12431 if (!$rm) {
12432 unlink($path);
12433 }
12434 return true;
12435 }
12436
12437 /**
12438 * Alias method of JFile::upload to unify any
12439 * upload function into one.
12440 *
12441 * @param string $src The name of the php (temporary) uploaded file.
12442 * @param string $dest The path (including filename) to move the uploaded file to.
12443 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
12444 *
12445 * @return boolean True on success.
12446 *
12447 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
12448 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
12449 */
12450 public static function uploadFile($src, $dest, $copy_only = false) {
12451 // always attempt to include the File class
12452 jimport('joomla.filesystem.file');
12453
12454 // upload the file
12455 if (!$copy_only) {
12456 $result = JFile::upload($src, $dest);
12457 } else {
12458 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
12459 $result = JFile::copy($src, $dest);
12460 }
12461
12462 /**
12463 * @wponly in order to not lose uploaded files after installing an update,
12464 * we need to move any uploaded file onto a recovery folder.
12465 */
12466 if ($result) {
12467 VikBookingLoader::import('update.manager');
12468 VikBookingUpdateManager::triggerUploadBackup($dest);
12469 }
12470 //
12471
12472 // return upload result
12473 return $result;
12474 }
12475
12476 /**
12477 * Helper method used to upload the given file (retrieved from $_FILES)
12478 * into the specified destination.
12479 *
12480 * @param array $file An associative array with the file details.
12481 * @param string $dest The destination path.
12482 * @param string $filters A string (or a regex) containing the allowed extensions.
12483 *
12484 * @return array The uploading result.
12485 *
12486 * @throws RuntimeException
12487 *
12488 * @since 1.3.0
12489 */
12490 public static function uploadFileFromRequest($file, $dest, $filters = '*')
12491 {
12492 jimport('joomla.filesystem.file');
12493
12494 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
12495
12496 if (empty($file['name']))
12497 {
12498 throw new RuntimeException('Missing file', 400);
12499 }
12500
12501 /**
12502 * Make sure the upload of the file didn't raise any errors.
12503 *
12504 * @link https://www.php.net/manual/en/features.file-upload.errors.php
12505 */
12506 if ((int) $file['error'])
12507 {
12508 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
12509 {
12510 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
12511 }
12512 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
12513 {
12514 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
12515 }
12516 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
12517 {
12518 $error = 'The uploaded file was only partially uploaded.';
12519 }
12520 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
12521 {
12522 $error = 'No file was uploaded.';
12523 }
12524 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
12525 {
12526 $error = 'Missing a temporary folder.';
12527 }
12528 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
12529 {
12530 $error = 'Failed to write file to disk.';
12531 }
12532 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
12533 {
12534 $error = 'A PHP extension stopped the file upload.';
12535 }
12536 else
12537 {
12538 $error = 'Unknown error.';
12539 }
12540
12541 throw new RuntimeException($error, 500);
12542 }
12543
12544 $src = $file['tmp_name'];
12545
12546 // extract file name and extension
12547 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
12548 {
12549 $filename = $match[1];
12550 $fileext = $match[2];
12551 }
12552 else
12553 {
12554 // probably no extension provided
12555 $filename = basename($file['name']);
12556 $fileext = '';
12557 }
12558
12559 $j = '';
12560
12561 if (file_exists($dest . $filename . $fileext))
12562 {
12563 $j = 2;
12564
12565 while (file_exists($dest . $filename . '-' . $j . $fileext))
12566 {
12567 $j++;
12568 }
12569
12570 $j = '-' . $j;
12571 }
12572
12573 $finaldest = $dest . $filename . $j . $fileext;
12574
12575 // make sure the file extension is supported
12576 if (!self::isFileTypeCompatible(basename($finaldest), $filters))
12577 {
12578 // extension not supported
12579 throw new RuntimeException(sprintf('Extension [%s] is not supported', $fileext), 400);
12580 }
12581
12582 // try to upload the file
12583 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
12584 {
12585 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
12586 }
12587
12588 $file = new stdClass;
12589 $file->name = $filename . $j;
12590 $file->ext = ltrim($fileext, '.');
12591 $file->filename = basename($finaldest);
12592 $file->path = $finaldest;
12593
12594 return $file;
12595 }
12596
12597 /**
12598 * Helper method used to check whether the given file name
12599 * supports one of the given filters.
12600 *
12601 * @param mixed $file Either the file name or the uploaded file.
12602 * @param string $filters Either a regex or a comma-separated list of supported extensions.
12603 * The regex must be inclusive of delimiters.
12604 *
12605 * @return bool True if supported, false otherwise.
12606 *
12607 * @since 1.7.3 (WP) - 1.17.3 (J)
12608 */
12609 public static function isFileTypeCompatible($file, $filters)
12610 {
12611 // make sure the filters query is not empty
12612 if (strlen($filters) == 0)
12613 {
12614 // cannot assert whether the file could be accepted or not
12615 return false;
12616 }
12617
12618 // check whether all the files are accepted
12619 if ($filters == '*')
12620 {
12621 return true;
12622 }
12623
12624 // use the file MIME TYPE in case of array
12625 if (is_array($file))
12626 {
12627 $file = $file['type'];
12628 }
12629
12630 // check if we are dealing with a regex
12631 if (static::isRegex($filters))
12632 {
12633 return (bool) preg_match($filters, $file);
12634 }
12635
12636 // fallback to comma-separated list
12637 $types = array_filter(preg_split("/\s*,\s*/", $filters));
12638
12639 foreach ($types as $t)
12640 {
12641 // remove initial dot if specified
12642 $t = ltrim($t, '.');
12643 // escape slashes to avoid breaking the regex
12644 $t = preg_replace("/\//", '\/', $t);
12645
12646 // check if the file ends with the given extension
12647 if (preg_match("/{$t}$/i", $file))
12648 {
12649 return true;
12650 }
12651 }
12652
12653 return false;
12654 }
12655
12656 /**
12657 * Checks whether the given string is a structured PCRE regex.
12658 * It simply makes sure that the string owns valid delimiters.
12659 * A delimiter can be any non-alphanumeric, non-backslash,
12660 * non-whitespace character.
12661 *
12662 * @param string $str The string to check.
12663 *
12664 * @return boolean True if a regex, false otherwise.
12665 *
12666 * @since 1.7.3 (WP) - 1.17.3 (J)
12667 */
12668 public static function isRegex($str)
12669 {
12670 // first of all make sure the first character is a supported delimiter
12671 if (!preg_match("/^([!#$%&'*+,.\/:;=?@^_`|~\-(\[{<\"])/", $str, $match))
12672 {
12673 // no valid delimiter
12674 return false;
12675 }
12676
12677 // get delimiter
12678 $d = $match[1];
12679
12680 // lookup used to check if we should take a different ending delimiter
12681 $lookup = array(
12682 '{' => '}',
12683 '[' => ']',
12684 '(' => ')',
12685 '<' => '>',
12686 );
12687
12688 if (isset($lookup[$d]))
12689 {
12690 $d = $lookup[$d];
12691 }
12692
12693 // make sure the regex ends with the delimiter found
12694 return (bool) preg_match("/\\{$d}[gimsxU]*$/", $str);
12695 }
12696
12697 /**
12698 * Gets the instance of a specific report class.
12699 *
12700 * @param string $report the name of the report to load.
12701 *
12702 * @return mixed false or report object instance.
12703 *
12704 * @since 1.3.0
12705 */
12706 public static function getReportInstance($report)
12707 {
12708 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12709
12710 return VikBookingReport::getInstanceOf($report);
12711 }
12712
12713 /**
12714 * Checks whether the guest reviews are enabled and VCM is installed.
12715 *
12716 * @return boolean true if enabled, false otherwise.
12717 *
12718 * @since 1.3.0
12719 */
12720 public static function allowGuestReviews()
12721 {
12722 $dbo = JFactory::getDbo();
12723 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12724 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12725 $dbo->setQuery($q);
12726 $dbo->execute();
12727 if ($dbo->getNumRows()) {
12728 $s = $dbo->loadResult();
12729 return ((int)$s === 1 && $vcm_installed);
12730 }
12731 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12732 $dbo->setQuery($q);
12733 $dbo->execute();
12734 return $vcm_installed;
12735 }
12736
12737 /**
12738 * Gets the minimum chars for the guest review message.
12739 *
12740 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12741 *
12742 * @since 1.3.0
12743 */
12744 public static function guestReviewMinChars()
12745 {
12746 $dbo = JFactory::getDbo();
12747 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12748 $dbo->setQuery($q);
12749 $dbo->execute();
12750 if ($dbo->getNumRows() > 0) {
12751 return (int)$dbo->loadResult();
12752 }
12753 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12754 $dbo->setQuery($q);
12755 $dbo->execute();
12756 return 15;
12757 }
12758
12759 /**
12760 * The approval type for new guest reviews.
12761 *
12762 * @return string auto or manual.
12763 *
12764 * @since 1.3.0
12765 */
12766 public static function guestReviewsApproval()
12767 {
12768 $dbo = JFactory::getDbo();
12769 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12770 $dbo->setQuery($q);
12771 $dbo->execute();
12772 if ($dbo->getNumRows() > 0) {
12773 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12774 }
12775 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12776 $dbo->setQuery($q);
12777 $dbo->execute();
12778 return 'auto';
12779 }
12780
12781 /**
12782 * The type of reviews guests should leave.
12783 *
12784 * @return string global or service.
12785 *
12786 * @since 1.3.0
12787 */
12788 public static function guestReviewsType()
12789 {
12790 $dbo = JFactory::getDbo();
12791 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12792 $dbo->setQuery($q);
12793 $dbo->execute();
12794 if ($dbo->getNumRows() > 0) {
12795 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12796 }
12797 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12798 $dbo->setQuery($q);
12799 $dbo->execute();
12800 return 'service';
12801 }
12802
12803 /**
12804 * The services to be reviewed by the guests.
12805 *
12806 * @return array list, empty or not, of the services to be reviewed.
12807 *
12808 * @since 1.3.0
12809 */
12810 public static function guestReviewsServices()
12811 {
12812 $dbo = JFactory::getDbo();
12813 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12814 $dbo->setQuery($q);
12815 $dbo->execute();
12816 if ($dbo->getNumRows()) {
12817 return $dbo->loadAssocList();
12818 }
12819 // insert the default services
12820 $default_services = array(
12821 JText::translate('VBOGREVVALUE'),
12822 JText::translate('VBOGREVLOCATION'),
12823 JText::translate('VBOGREVSTAFF'),
12824 JText::translate('VBOGREVCLEAN'),
12825 JText::translate('VBOGREVCOMFORT'),
12826 JText::translate('VBOGREVFACILITIES'),
12827 );
12828 foreach ($default_services as $def_service) {
12829 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12830 $dbo->setQuery($q);
12831 $dbo->execute();
12832 }
12833 return array();
12834 }
12835
12836 /**
12837 * Checks whether the guest reviews should be downloaded (max 1 per day).
12838 * VCM must be installed in order to download the new reviews.
12839 *
12840 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12841 *
12842 * @since 1.3.0
12843 */
12844 public static function shouldDownloadReviews()
12845 {
12846 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12847 return -1;
12848 }
12849
12850 $today = date('Y-m-d');
12851
12852 $dbo = JFactory::getDbo();
12853 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12854 $dbo->setQuery($q);
12855 $dbo->execute();
12856 if ($dbo->getNumRows()) {
12857 $last_download = $dbo->loadResult();
12858 // update last download day
12859 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12860 $dbo->setQuery($q);
12861 $dbo->execute();
12862 //
12863 return ($last_download != $today ? 1 : 0);
12864 }
12865 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12866 $dbo->setQuery($q);
12867 $dbo->execute();
12868 return 1;
12869 }
12870
12871 /**
12872 * Tells whether a booking can be reviewed.
12873 *
12874 * @param array $booking booking array details.
12875 *
12876 * @return boolean true if a review can be left, false otherwise.
12877 *
12878 * @since 1.3.0
12879 */
12880 public static function canBookingBeReviewed($booking)
12881 {
12882 // reviews must be enabled and supported
12883 if (!self::allowGuestReviews()) {
12884 return false;
12885 }
12886
12887 // booking status must be confirmed
12888 if ($booking['status'] != 'confirmed') {
12889 return false;
12890 }
12891
12892 // review can be left starting from the check-out day
12893 $checkout_info = getdate($booking['checkout']);
12894 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12895 if (time() < $checkout_midn) {
12896 return false;
12897 }
12898
12899 // make sure a review for this booking does not exist
12900 $noreview = true;
12901 $dbo = JFactory::getDbo();
12902 try {
12903 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12904 $dbo->setQuery($q);
12905 $dbo->execute();
12906 $noreview = ($dbo->getNumRows() < 1);
12907 } catch (Exception $e) {
12908 // if the query fails, we do not allow the review to be left
12909 $noreview = false;
12910 }
12911
12912 return $noreview;
12913 }
12914
12915 /**
12916 * Gets the review for a booking, no matter if it's published or not.
12917 * Review will be returned only if reviews are enabled and supported.
12918 * Only reviews left through the website, no OTA reviews as this method
12919 * should be used in the front-end to display the review to the guest.
12920 *
12921 * @param array $booking booking array details.
12922 *
12923 * @return mixed associative array or false.
12924 *
12925 * @since 1.3.0
12926 */
12927 public static function getBookingReview($booking)
12928 {
12929 // reviews must be enabled and supported
12930 if (!self::allowGuestReviews()) {
12931 return false;
12932 }
12933
12934 if (empty($booking['id'])) {
12935 return false;
12936 }
12937
12938 // make sure a review for this booking exists
12939 $dbo = JFactory::getDbo();
12940 $review = array();
12941 try {
12942 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12943 $dbo->setQuery($q);
12944 $dbo->execute();
12945 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12946 } catch (Exception $e) {
12947 // query has failed
12948 $review = array();
12949 }
12950
12951 if (count($review)) {
12952 // make sure the review was left from the website
12953 $review['uniquekey'] = (int)$review['uniquekey'];
12954 }
12955
12956 if (count($review) && !empty($review['uniquekey'])) {
12957 // this is an OTA review
12958 return false;
12959 }
12960
12961 if (count($review) && !empty($review['content'])) {
12962 // decode content
12963 $review['content'] = json_decode($review['content'], true);
12964 }
12965
12966 return count($review) ? $review : false;
12967 }
12968
12969 /**
12970 * Attempts to find a translation for a raw customer data label.
12971 * Bookings downloaded from the OTAs will save a raw string of information
12972 * composed of pairs of label-value separated by new line feeds.
12973 * We try to translate the given label into the current language.
12974 *
12975 * @param string $label the raw string label to translate.
12976 *
12977 * @return string either the original or the translated label.
12978 *
12979 * @since 1.3.5
12980 * @since 1.4.0 back-end support added.
12981 */
12982 public static function tnCustomerRawDataLabel($label)
12983 {
12984 // this is a map of the known labels
12985 $known_lbls = array(
12986 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12987 'LASTNAME' => 'ORDER_LNAME',
12988 'COUNTRY' => 'ORDER_STATE',
12989 'EMAIL' => 'VBMAIL',
12990 'TELEPHONE' => 'VBPHONE',
12991 'PHONE' => 'VBPHONE',
12992 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12993 'MEAL_PLAN' => 'VBOMEALPLAN',
12994 'CITY' => 'VBCITY',
12995 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12996 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12997 );
12998
12999 if (self::isAdmin()) {
13000 // override the map of known translation strings
13001 $known_lbls = array(
13002 'NAME' => 'ORDER_NAME',
13003 'LASTNAME' => 'ORDER_LNAME',
13004 'COUNTRY' => 'ORDER_STATE',
13005 'ADDRESS' => 'ORDER_ADDRESS',
13006 'CITY' => 'ORDER_CITY',
13007 'LOCATION' => 'ORDER_CITY',
13008 'EMAIL' => 'ORDER_EMAIL',
13009 'TELEPHONE' => 'ORDER_PHONE',
13010 'PHONE' => 'ORDER_PHONE',
13011 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
13012 );
13013 }
13014
13015 // we get rid of any empty space by keeping the underscores
13016 $converted = str_replace(' ', '', strtoupper($label));
13017
13018 if (isset($known_lbls[$converted])) {
13019 // this language definition has been mapped
13020 return JText::translate($known_lbls[$converted]);
13021 }
13022
13023 // we try to guess the translation string by prepending VBO
13024 $guessed = JText::translate('VBO' . $converted);
13025 if ($guessed != 'VBO' . $converted) {
13026 // the label was translated correctly
13027 return $guessed;
13028 }
13029
13030 // this label could not be translated, so we return the plain string
13031 return $label;
13032 }
13033
13034 /**
13035 * We check whether some bookings are available for import from third party plugins.
13036 *
13037 * @return mixed array with list of plugins supported, false otherwise.
13038 *
13039 * @wponly this method is only useful for WordPress.
13040 *
13041 * @since 1.3.5
13042 */
13043 public static function canImportBookingsFromThirdPartyPlugins()
13044 {
13045 $dbo = JFactory::getDbo();
13046
13047 $plugins = array();
13048
13049 /**
13050 * As requested from hundreds of our clients, for the moment we check only the custom
13051 * post types of type "mphb_booking" to see if some bookings are available for import.
13052 */
13053 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
13054 $dbo->setQuery($q);
13055 $dbo->execute();
13056
13057 if (!$dbo->getNumRows()) {
13058 return false;
13059 }
13060
13061 // push this third party plugin
13062 $plugins['mphb'] = 'MotoPress Hotel Booking';
13063
13064 return count($plugins) ? $plugins : false;
13065 }
13066
13067 /**
13068 * This method returns a list of the known languages sorted by the
13069 * administrator custom preferences. Useful for the phone input fields.
13070 *
13071 * @param boolean $code_assoc whether to get an associative array with the lang name.
13072 *
13073 * @return array the sorted list of preferred countries.
13074 *
13075 * @since 1.3.11
13076 */
13077 public static function preferredCountriesOrdering($code_assoc = false)
13078 {
13079 $preferred_countries = array();
13080
13081 // try to get the preferred countries from db
13082 $dbo = JFactory::getDbo();
13083 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
13084 $dbo->setQuery($q);
13085 $setting = $dbo->loadResult();
13086 if (!$setting) {
13087 // create empty configuration record
13088 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
13089 $dbo->setQuery($q);
13090 $dbo->execute();
13091 } else {
13092 $preferred_countries = json_decode($setting);
13093 }
13094
13095 // get the default known languages
13096 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
13097
13098 if (!is_array($preferred_countries) || !count($preferred_countries)) {
13099 // sort the default known languages by country code alphabetically
13100 ksort($sorted_known_langs);
13101 foreach ($sorted_known_langs as $k => $v) {
13102 $langsep = strpos($k, '_') !== false ? '_' : '-';
13103 $langparts = explode($langsep, $k);
13104 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
13105 }
13106 // update the database record
13107 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
13108 $dbo->setQuery($q);
13109 $dbo->execute();
13110 }
13111
13112 if ($code_assoc) {
13113 // this is useful for displaying the preferred countries codes together with the language name
13114 $map = array();
13115 foreach ($preferred_countries as $ccode) {
13116 // look for the current country code in the keys of the known language tags
13117 $match_found = false;
13118 foreach ($sorted_known_langs as $langtag => $langinfo) {
13119 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
13120 $langparts = explode($langsep, $langtag);
13121 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
13122 // match found
13123 $match_found = true;
13124 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13125 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
13126 // match found
13127 $match_found = true;
13128 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
13129 }
13130 }
13131 if (!$match_found) {
13132 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
13133 $map[$ccode] = strtoupper($ccode);
13134 }
13135 }
13136 if (count($map)) {
13137 // set the associatve array to be returned
13138 $preferred_countries = $map;
13139 }
13140 }
13141
13142 return $preferred_countries;
13143 }
13144
13145 /**
13146 * Gets the instance of the admin widgets helper class.
13147 *
13148 * @return VikBookingHelperAdminWidgets
13149 *
13150 * @since 1.4.0
13151 */
13152 public static function getAdminWidgetsInstance()
13153 {
13154 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
13155
13156 return VikBookingHelperAdminWidgets::getInstance();
13157 }
13158
13159 /**
13160 * Gets the instance of the conditional rules helper class.
13161 *
13162 * @param bool $require_only whether to return the object.
13163 *
13164 * @return mixed VikBookingHelperConditionalRules or true.
13165 *
13166 * @since 1.4.0
13167 */
13168 public static function getConditionalRulesInstance($require_only = false)
13169 {
13170 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
13171
13172 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
13173 }
13174
13175 /**
13176 * Gets the instance of the geocoding helper class.
13177 *
13178 * @param bool $require_only whether to return the object.
13179 *
13180 * @return mixed VikBookingHelperGeocoding or true.
13181 *
13182 * @since 1.4.0
13183 */
13184 public static function getGeocodingInstance($require_only = false)
13185 {
13186 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
13187
13188 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
13189 }
13190
13191 /**
13192 * Helper method to cope with the removal of the same method
13193 * in the JApplication class introduced with Joomla 4. Using
13194 * isClient() would break the compatibility with J < 3.7 so
13195 * we can rely on this helper method to avoid Fatal Errors.
13196 *
13197 * @return boolean
13198 *
13199 * @since October 2020
13200 */
13201 public static function isAdmin()
13202 {
13203 $app = JFactory::getApplication();
13204 if (method_exists($app, 'isClient')) {
13205 return $app->isClient('administrator');
13206 }
13207
13208 return $app->isAdmin();
13209 }
13210
13211 /**
13212 * Helper method to cope with the removal of the same method
13213 * in the JApplication class introduced with Joomla 4. Using
13214 * isClient() would break the compatibility with J < 3.7 so
13215 * we can rely on this helper method to avoid Fatal Errors.
13216 *
13217 * @return boolean
13218 *
13219 * @since October 2020
13220 */
13221 public static function isSite()
13222 {
13223 $app = JFactory::getApplication();
13224 if (method_exists($app, 'isClient')) {
13225 return $app->isClient('site');
13226 }
13227
13228 return $app->isSite();
13229 }
13230
13231 /**
13232 * Gets the Google Maps API Key.
13233 *
13234 * @return string
13235 *
13236 * @since 1.14 (J) - 1.4.0 (WP)
13237 */
13238 public static function getGoogleMapsKey()
13239 {
13240 return VBOFactory::getConfig()->getString('gmapskey', '');
13241 }
13242
13243 /**
13244 * Checks whether the interactive map booking is enabled.
13245 *
13246 * @return bool
13247 *
13248 * @since 1.14 (J) - 1.4.0 (WP)
13249 */
13250 public static function interactiveMapEnabled()
13251 {
13252 return VBOFactory::getConfig()->getBool('interactive_map', false);
13253 }
13254
13255 /**
13256 * Gets the preferred colors saved in the configuration, if any.
13257 *
13258 * @return array
13259 *
13260 * @since 1.14 (J) - 1.4.0 (WP)
13261 */
13262 public static function getPreferredColors()
13263 {
13264 $config = VBOFactory::getConfig();
13265
13266 // default colors
13267 $pref_colors = [
13268 'textcolor' => '#2282bd',
13269 'bgcolor' => '#2282bd',
13270 'fontcolor' => '#fff',
13271 'bgcolorhov' => '#1890ce',
13272 'fontcolorhov' => '#fff',
13273 ];
13274
13275 // saved colors
13276 $colors = $config->getArray('pref_colors', []);
13277
13278 return $colors ?: $pref_colors;
13279 }
13280
13281 /**
13282 * Adds to the document inline styles for the preferred colors, if any.
13283 *
13284 * @return void
13285 *
13286 * @since 1.14 (J) - 1.4.0 (WP)
13287 */
13288 public static function loadPreferredColorStyles()
13289 {
13290 $pref_colors = self::getPreferredColors();
13291 $css_classes = [];
13292
13293 /**
13294 * Register all preferred colors as CSS variables.
13295 *
13296 * @since 1.18.8 (J) - 1.8.8 (WP)
13297 */
13298 $css_vars = array_filter(array_combine(array_keys($pref_colors), array_values($pref_colors)));
13299 if ($css_vars) {
13300 $css_vars_decl = array_map(function($k, $v) {
13301 return sprintf('--vbo-pref-%s: %s;', preg_replace('/[^a-z0-9]/', '', strtolower($k)), $v);
13302 }, array_keys($css_vars), array_values($css_vars));
13303 $css_classes[] = ':root {' . "\n" . implode("\n", $css_vars_decl) . "\n" . '}';
13304 }
13305
13306 // add in-line style declaration
13307 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
13308 }
13309
13310 /**
13311 * Given the full endpoint URL for the AJAX request, it
13312 * returns an appropriate URI for the current platform.
13313 *
13314 * @param mixed $query The query string or a routed URL.
13315 * @param boolean $xhtml Replace & by &amp; for XML compliance.
13316 *
13317 * @return string The AJAX end-point URI.
13318 *
13319 * @since 1.14 (J) - 1.4.0 (WP)
13320 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
13321 */
13322 public static function ajaxUrl($query = '', $xhtml = false)
13323 {
13324 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
13325 }
13326
13327 /**
13328 * Tells whether no tax rates have been defined so far.
13329 *
13330 * @return bool True if no tax rates defined, false otherwise.
13331 *
13332 * @since 1.14 (J) - 1.4.0 (WP)
13333 */
13334 public static function noTaxRates()
13335 {
13336 $dbo = JFactory::getDbo();
13337
13338 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
13339 $dbo->setQuery($q);
13340 $dbo->execute();
13341
13342 return ($dbo->getNumRows() < 1);
13343 }
13344
13345 /**
13346 * Booking Type feature is strictly connected to VCM and OTAs. Updated
13347 * versions of VBO will always support it as long as VCM is installed.
13348 * Needed by VCM to understand whether certain SQL queries can be performed.
13349 *
13350 * @return bool true if VCM exists, false otherwise.
13351 *
13352 * @since 1.14 (J) - 1.4.0 (WP)
13353 */
13354 public static function isBookingTypeSupported()
13355 {
13356 static $isBookingTypeSupported = null;
13357
13358 if (is_bool($isBookingTypeSupported)) {
13359 return $isBookingTypeSupported;
13360 }
13361
13362 $isBookingTypeSupported = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
13363
13364 return $isBookingTypeSupported;
13365 }
13366
13367 /**
13368 * Returns an instance of the Rates Flow helper class of VCM, if available.
13369 *
13370 * @param bool $anew True to request a new object instance.
13371 *
13372 * @return VCMRatesFlow|null
13373 *
13374 * @since 1.15.0 (J) - 1.5.0 (WP)
13375 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
13376 */
13377 public static function getRatesFlowInstance($anew = false)
13378 {
13379 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
13380 if (!is_file($vcm_fpath)) {
13381 return null;
13382 }
13383
13384 // load dependencies
13385 require_once $vcm_fpath;
13386
13387 // return the instance of the class
13388 return VCMRatesFlow::getInstance($anew);
13389 }
13390
13391 /**
13392 * Returns the current appearance preference.
13393 *
13394 * @return string light, auto or dark.
13395 *
13396 * @since 1.15.0 (J) - 1.5.0 (WP)
13397 */
13398 public static function getAppearancePref()
13399 {
13400 static $preference = null;
13401
13402 if ($preference !== null) {
13403 return $preference;
13404 }
13405
13406 // auto is the default appearance preference
13407 $default_pref = 'auto';
13408
13409 // accepted preferences
13410 $valid_pref = [
13411 'auto',
13412 'light',
13413 'dark',
13414 ];
13415
13416 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
13417 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
13418
13419 return $preference;
13420 }
13421
13422 /**
13423 * According to the appearance preferences, the apposite
13424 * CSS assets are loaded within the document.
13425 *
13426 * @param bool $get_info true to not load any assets.
13427 *
13428 * @return mixed string light, auto, dark, info array or false.
13429 *
13430 * @since 1.15.0 (J) - 1.5.0 (WP)
13431 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
13432 */
13433 public static function loadAppearancePreferenceAssets($get_info = false)
13434 {
13435 // get document object
13436 $document = JFactory::getDocument();
13437
13438 // load current color scheme preference
13439 $current_pref = self::getAppearancePref();
13440
13441 // define caching values
13442 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
13443
13444 // define file attributes
13445 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
13446
13447 // apposite file path and URI for back-end
13448 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
13449 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
13450
13451 // check if this is for the front-end ("auto" file only)
13452 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
13453 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
13454 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
13455 // check if front-end appearance is disabled
13456 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
13457 // do nothing for the front-end by default
13458 return false;
13459 }
13460 }
13461
13462 if (VBOPlatformDetection::isJoomla()) {
13463 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
13464 $css_uri = str_replace('resources/', '', $css_uri);
13465 }
13466
13467 if (!is_file($css_path)) {
13468 // this preference does not require a specific stylesheet
13469 return false;
13470 }
13471
13472 if ($get_info) {
13473 // do not actually load any assets
13474 return [
13475 'id' => $file_attr['id'],
13476 'href' => $css_uri,
13477 ];
13478 }
13479
13480 // load the apposite CSS file
13481 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
13482
13483 return $current_pref;
13484 }
13485
13486 /**
13487 * VBO trigger for VCM subscription status and updates. If called within the admin section
13488 * returns the expiration reminder array to display a modal window, and sets a warning
13489 * message. If called within the front-end, may trigger an email alert.
13490 *
13491 * @return mixed null, array or boolean depending on client position.
13492 *
13493 * @since 1.15.0 (J) - 1.5.0 (WP)
13494 */
13495 public static function getVCMSubscriptionStatus()
13496 {
13497 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
13498 if (!is_file($vcm_lib_path)) {
13499 // do nothing
13500 return null;
13501 }
13502
13503 // determine client position
13504 $is_admin = self::isAdmin();
13505
13506 try {
13507 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
13508 // VCM is outdated
13509 return null;
13510 }
13511
13512 // always attempt to load the admin lang handler of VCM
13513 $lang = JFactory::getLanguage();
13514
13515 // load the VCM admin language file
13516 $vcm_admin_lang_path = '';
13517 if (VBOPlatformDetection::isJoomla()) {
13518 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
13519 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
13520 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
13521 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
13522 /**
13523 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
13524 */
13525 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
13526 }
13527 // load translation file
13528 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
13529 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
13530 /**
13531 * @wponly load language admin handler as well for WP.
13532 * We do this only because of WordPress, but in a way also compatible with Joomla as
13533 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
13534 */
13535 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
13536 }
13537
13538 if ($is_admin) {
13539 // back-end section of the site: check status
13540 if (VikChannelManager::loadExpirationDetails() === false) {
13541 // download and update the expiration details
13542 VikChannelManager::downloadExpirationDetails();
13543 }
13544 $expiration_reminder = null;
13545 if (VikChannelManager::isSubscriptionExpired()) {
13546 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
13547 } else {
13548 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
13549 // we also check if an update is available
13550 if (VikChannelManager::checkUpdates()) {
13551 // an update is more important
13552 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
13553 exit;
13554 }
13555 }
13556 // return reminder status
13557 return $expiration_reminder;
13558 }
13559
13560 // front-end section of the site: trigger alert, if any (bool)
13561 return VikChannelManager::checkSubscriptionReminder();
13562
13563 } catch (Exception $e) {
13564 // do nothing
13565 }
13566
13567 return null;
13568 }
13569
13570 /**
13571 * Given a list of website rate plans, returns the sorted list.
13572 * Naming score is the highest sorting method, pricing scoring is also supported.
13573 *
13574 * @param array $rate_plans the list of rate plan arrays to sort.
13575 * @param bool $assoc whether to keep the original array keys.
13576 *
13577 * @return array the sorted list of rate plan arrays.
13578 *
13579 * @since 1.15.0 (J) - 1.5.0 (WP)
13580 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
13581 */
13582 public static function sortRatePlans($rate_plans, $assoc = false)
13583 {
13584 if (!is_array($rate_plans) || count($rate_plans) < 2) {
13585 return $rate_plans;
13586 }
13587
13588 foreach ($rate_plans as $rp_ind => $rate_plan) {
13589 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
13590 // do not proceed in case of a weird array structure
13591 return $rate_plans;
13592 }
13593
13594 // count the sorting-score of this rate plan
13595 $sort_score = 0;
13596 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
13597 // "standard rate" rate plans should go first
13598 $sort_score += 3;
13599 } else {
13600 $sort_score--;
13601 }
13602
13603 // check for derived rate plan
13604 if (!empty($rate_plan['derived_id'])) {
13605 // derived rates should go last
13606 $sort_score -= 2;
13607 } else {
13608 // parent rates should go first
13609 $sort_score += 2;
13610 }
13611
13612 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
13613 // this doesn't seem to be a "non refundable rate" so it should go first
13614 $sort_score += 2;
13615 }
13616
13617 // set sorting score for this rate plan ID
13618 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13619 }
13620
13621 // apply custom sorting and keep index association
13622 uasort($rate_plans, function($a, $b) {
13623 // sort by name-scoring DESC
13624 $sort_diff = $b['sort_score'] - $a['sort_score'];
13625 if (isset($b['cost'])) {
13626 // apply also sorting by price ASC
13627 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13628 }
13629 return $sort_diff;
13630 });
13631
13632 return !$assoc ? array_values($rate_plans) : $rate_plans;
13633 }
13634
13635 /**
13636 * Returns an instance of the Availability helper class.
13637 *
13638 * @param bool $anew True for forcing a new instance.
13639 *
13640 * @return VikBookingAvailability
13641 *
13642 * @since 1.15.0 (J) - 1.5.0 (WP)
13643 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13644 */
13645 public static function getAvailabilityInstance($anew = false)
13646 {
13647 // require library
13648 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13649
13650 // return the instance of the class
13651 return VikBookingAvailability::getInstance($anew);
13652 }
13653
13654 /**
13655 * Helper method to quickly guess the internal dependency to import.
13656 *
13657 * @param string $path_id either a full path to a file or its identifier.
13658 *
13659 * @return bool true if the requested library was imported or false.
13660 *
13661 * @since 1.15.0 (J) - 1.5.0 (WP)
13662 */
13663 public static function import($path_id)
13664 {
13665 if (empty($path_id)) {
13666 return false;
13667 }
13668
13669 // always append .php extension
13670 $path_id = basename($path_id, '.php') . '.php';
13671
13672 if (is_file($path_id)) {
13673 // full path given
13674 require_once $path_id;
13675
13676 return true;
13677 }
13678
13679 // try guessing where the file id is located
13680 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13681 // library was found
13682 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13683
13684 return true;
13685 }
13686
13687 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13688 // library was found
13689 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13690
13691 return true;
13692 }
13693
13694 return false;
13695 }
13696 }
13697
13698 if (!class_exists('VikResizer'))
13699 {
13700 class VikResizer
13701 {
13702 public function __construct()
13703 {
13704 // objects of this class can also be instantiated without calling the methods statically.
13705 }
13706
13707 /**
13708 * Resizes an image proportionally. For PNG files it can optionally
13709 * trim the image to exclude the transparency, and add some padding to it.
13710 * All PNG files keep the alpha background in the resized version.
13711 *
13712 * @param string $fileimg path to original image file
13713 * @param string $dest path to destination image file
13714 * @param int $towidth
13715 * @param int $toheight
13716 * @param bool $trim_png remove empty background from image
13717 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13718 *
13719 * @return boolean
13720 */
13721 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13722 {
13723 if (!is_file($fileimg)) {
13724 return false;
13725 }
13726 if (empty($towidth) && empty($toheight)) {
13727 return copy($fileimg, $dest);
13728 }
13729
13730 $result = true;
13731
13732 list ($owid, $ohei, $type) = getimagesize($fileimg);
13733
13734 if ($owid > $towidth || $ohei > $toheight) {
13735 $xscale = $owid / $towidth;
13736 $yscale = $ohei / $toheight;
13737 if ($yscale > $xscale) {
13738 $new_width = round($owid * (1 / $yscale));
13739 $new_height = round($ohei * (1 / $yscale));
13740 } else {
13741 $new_width = round($owid * (1 / $xscale));
13742 $new_height = round($ohei * (1 / $xscale));
13743 }
13744
13745 $imageresized = imagecreatetruecolor($new_width, $new_height);
13746
13747 switch ($type) {
13748 case '1' :
13749 $imagetmp = imagecreatefromgif ($fileimg);
13750 break;
13751 case '2' :
13752 $imagetmp = imagecreatefromjpeg($fileimg);
13753 break;
13754 case '18' :
13755 $imagetmp = imagecreatefromwebp($fileimg);
13756 break;
13757 default :
13758 //keep alpha for PNG files
13759 $background = imagecolorallocate($imageresized, 0, 0, 0);
13760 imagecolortransparent($imageresized, $background);
13761 imagealphablending($imageresized, false);
13762 imagesavealpha($imageresized, true);
13763 //
13764 $imagetmp = imagecreatefrompng($fileimg);
13765 break;
13766 }
13767
13768 if (!$imagetmp) {
13769 return false;
13770 }
13771
13772 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13773
13774 switch ($type) {
13775 case '1' :
13776 imagegif($imageresized, $dest);
13777 break;
13778 case '2' :
13779 imagejpeg($imageresized, $dest);
13780 break;
13781 case '18' :
13782 imagewebp($imageresized, $dest);
13783 break;
13784 default :
13785 if ($trim_png) {
13786 self::imageTrim($imageresized, $background, $trim_pad);
13787 }
13788 imagepng($imageresized, $dest);
13789 break;
13790 }
13791
13792 imagedestroy($imageresized);
13793 } else {
13794 $result = copy($fileimg, $dest);
13795 }
13796
13797 if (VBOPlatformDetection::isWordPress()) {
13798 /**
13799 * @wponly in order to not lose resized files after installing an update,
13800 * we need to move any uploaded file onto a recovery folder.
13801 */
13802 VikBookingLoader::import('update.manager');
13803 VikBookingUpdateManager::triggerUploadBackup($dest);
13804 }
13805
13806 return $result;
13807 }
13808
13809 /**
13810 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13811 * trim the image to exclude the transparency, and add some padding to it.
13812 * All PNG files keep the alpha background in the resized version.
13813 *
13814 * @param resource $im Image link resource (reference)
13815 * @param int $bg imagecolorallocate color identifier
13816 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13817 *
13818 * @return void
13819 */
13820 public static function imagetrim(&$im, $bg, $pad = null)
13821 {
13822 // Calculate padding for each side.
13823 if (isset($pad)) {
13824 $pp = explode(' ', $pad);
13825 if (isset($pp[3])) {
13826 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13827 } elseif (isset($pp[2])) {
13828 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13829 } elseif (isset($pp[1])) {
13830 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13831 } else {
13832 $p = array_fill(0, 4, (int) $pp[0]);
13833 }
13834 } else {
13835 $p = array_fill(0, 4, 0);
13836 }
13837
13838 // Get the image width and height.
13839 $imw = imagesx($im);
13840 $imh = imagesy($im);
13841
13842 // Set the X variables.
13843 $xmin = $imw;
13844 $xmax = 0;
13845
13846 // Start scanning for the edges.
13847 for ($iy=0; $iy<$imh; $iy++) {
13848 $first = true;
13849 for ($ix=0; $ix<$imw; $ix++) {
13850 $ndx = imagecolorat($im, $ix, $iy);
13851 if ($ndx != $bg) {
13852 if ($xmin > $ix) {
13853 $xmin = $ix;
13854 }
13855 if ($xmax < $ix) {
13856 $xmax = $ix;
13857 }
13858 if (!isset($ymin)) {
13859 $ymin = $iy;
13860 }
13861 $ymax = $iy;
13862 if ($first) {
13863 $ix = $xmax;
13864 $first = false;
13865 }
13866 }
13867 }
13868 }
13869
13870 // The new width and height of the image. (not including padding)
13871 $imw = 1+$xmax-$xmin; // Image width in pixels
13872 $imh = 1+$ymax-$ymin; // Image height in pixels
13873
13874 // Make another image to place the trimmed version in.
13875 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13876
13877 // Make the background of the new image the same as the background of the old one.
13878 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13879 imagefill($im2, 0, 0, $bg2);
13880
13881 // Copy it over to the new image.
13882 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13883
13884 // To finish up, we replace the old image which is referenced.
13885 $im = $im2;
13886 }
13887
13888 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13889 {
13890 if (!file_exists($fileimg)) {
13891 return false;
13892 }
13893 if (empty($towidth) && empty($toheight)) {
13894 copy($fileimg, $dest);
13895 return true;
13896 }
13897
13898 $exp = explode(",", $rgb);
13899 if (count($exp) == 3) {
13900 $r = trim($exp[0]);
13901 $g = trim($exp[1]);
13902 $b = trim($exp[2]);
13903 } else {
13904 $r = 0;
13905 $g = 0;
13906 $b = 0;
13907 }
13908
13909 list ($owid, $ohei, $type) = getimagesize($fileimg);
13910
13911 if ($owid > $towidth || $ohei > $toheight) {
13912 $xscale = $owid / $towidth;
13913 $yscale = $ohei / $toheight;
13914 if ($yscale > $xscale) {
13915 $new_width = round($owid * (1 / $yscale));
13916 $new_height = round($ohei * (1 / $yscale));
13917 $ydest = 0;
13918 $diff = $towidth - $new_width;
13919 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13920 } else {
13921 $new_width = round($owid * (1 / $xscale));
13922 $new_height = round($ohei * (1 / $xscale));
13923 $xdest = 0;
13924 $diff = $toheight - $new_height;
13925 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13926 }
13927
13928 $imageresized = imagecreatetruecolor($towidth, $toheight);
13929
13930 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13931 imagefill($imageresized, 0, 0, $bgColor);
13932
13933 switch ($type) {
13934 case '1' :
13935 $imagetmp = imagecreatefromgif ($fileimg);
13936 break;
13937 case '2' :
13938 $imagetmp = imagecreatefromjpeg($fileimg);
13939 break;
13940 default :
13941 $imagetmp = imagecreatefrompng($fileimg);
13942 break;
13943 }
13944
13945 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13946
13947 switch ($type) {
13948 case '1' :
13949 imagegif ($imageresized, $dest);
13950 break;
13951 case '2' :
13952 imagejpeg($imageresized, $dest);
13953 break;
13954 default :
13955 imagepng($imageresized, $dest);
13956 break;
13957 }
13958
13959 imagedestroy($imageresized);
13960
13961 return true;
13962 } else {
13963 copy($fileimg, $dest);
13964 }
13965 return true;
13966 }
13967
13968 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13969 {
13970 if (!file_exists($fileimg)) {
13971 return false;
13972 }
13973 if (empty($towidth) && empty($toheight)) {
13974 copy($fileimg, $dest);
13975 return true;
13976 }
13977
13978 list ($owid, $ohei, $type) = getimagesize($fileimg);
13979
13980 if ($owid <= $ohei) {
13981 $new_width = $towidth;
13982 $new_height = ($towidth / $owid) * $ohei;
13983 } else {
13984 $new_height = $toheight;
13985 $new_width = ($new_height / $ohei) * $owid;
13986 }
13987
13988 switch ($type) {
13989 case '1' :
13990 $img_src = imagecreatefromgif ($fileimg);
13991 $img_dest = imagecreate($new_width, $new_height);
13992 break;
13993 case '2' :
13994 $img_src = imagecreatefromjpeg($fileimg);
13995 $img_dest = imagecreatetruecolor($new_width, $new_height);
13996 break;
13997 default :
13998 $img_src = imagecreatefrompng($fileimg);
13999 $img_dest = imagecreatetruecolor($new_width, $new_height);
14000 break;
14001 }
14002
14003 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
14004
14005 switch ($type) {
14006 case '1' :
14007 $cropped = imagecreate($towidth, $toheight);
14008 break;
14009 case '2' :
14010 $cropped = imagecreatetruecolor($towidth, $toheight);
14011 break;
14012 default :
14013 $cropped = imagecreatetruecolor($towidth, $toheight);
14014 break;
14015 }
14016
14017 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
14018
14019 switch ($type) {
14020 case '1' :
14021 imagegif ($cropped, $dest);
14022 break;
14023 case '2' :
14024 imagejpeg($cropped, $dest);
14025 break;
14026 default :
14027 imagepng($cropped, $dest);
14028 break;
14029 }
14030
14031 imagedestroy($img_dest);
14032 imagedestroy($cropped);
14033
14034 return true;
14035 }
14036 }
14037 }
14038