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

13,423 lines 451.8 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 public static function getRoomUnitNumsUnavailable($order, $idroom) {
184 $dbo = JFactory::getDbo();
185 $unavailable_indexes = array();
186 $first = $order['checkin'];
187 $second = $order['checkout'];
188 $secdiff = $second - $first;
189 $daysdiff = $secdiff / 86400;
190 if (is_int($daysdiff)) {
191 if ($daysdiff < 1) {
192 $daysdiff = 1;
193 }
194 } else {
195 if ($daysdiff < 1) {
196 $daysdiff = 1;
197 } else {
198 $sum = floor($daysdiff) * 86400;
199 $newdiff = $secdiff - $sum;
200 $maxhmore = self::getHoursMoreRb() * 3600;
201 if ($maxhmore >= $newdiff) {
202 $daysdiff = floor($daysdiff);
203 } else {
204 $daysdiff = ceil($daysdiff);
205 }
206 }
207 }
208 $groupdays = self::getGroupDays($first, $second, $daysdiff);
209 $q = "SELECT `b`.`id`,`b`.`checkin`,`b`.`checkout`,`b`.`realback`,`ob`.`idorder`,`ob`.`idbusy`,`or`.`id` AS `or_id`,`or`.`idroom`,`or`.`roomindex`,`o`.`status` ".
210 "FROM `#__vikbooking_busy` AS `b` ".
211 "LEFT JOIN `#__vikbooking_ordersbusy` `ob` ON `ob`.`idbusy`=`b`.`id` ".
212 "LEFT JOIN `#__vikbooking_ordersrooms` `or` ON `or`.`idorder`=`ob`.`idorder` AND `or`.`idorder`!=".(int)$order['id']." ".
213 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` AND `o`.`id`=`ob`.`idorder` AND `o`.`id`!=".(int)$order['id']." ".
214 "WHERE `or`.`idroom`=".(int)$idroom." AND `b`.`checkout` > ".time()." AND `o`.`status`='confirmed' AND `ob`.`idorder`!=".(int)$order['id']." AND `ob`.`idorder` > 0;";
215 $dbo->setQuery($q);
216 $busy = $dbo->loadAssocList();
217 if ($busy) {
218 foreach ($groupdays as $gday) {
219 foreach ($busy as $bu) {
220 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
221 continue;
222 }
223 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
224 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
225 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
226 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
227 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
228 }
229 }
230 }
231 }
232 }
233
234 return $unavailable_indexes;
235 }
236
237 public static function getRoomUnitNumsAvailable($order, $idroom) {
238 $dbo = JFactory::getDbo();
239 $unavailable_indexes = array();
240 $available_indexes = array();
241 $first = $order['checkin'];
242 $second = $order['checkout'];
243 $secdiff = $second - $first;
244 $daysdiff = $secdiff / 86400;
245 if (is_int($daysdiff)) {
246 if ($daysdiff < 1) {
247 $daysdiff = 1;
248 }
249 } else {
250 if ($daysdiff < 1) {
251 $daysdiff = 1;
252 } else {
253 $sum = floor($daysdiff) * 86400;
254 $newdiff = $secdiff - $sum;
255 $maxhmore = self::getHoursMoreRb() * 3600;
256 if ($maxhmore >= $newdiff) {
257 $daysdiff = floor($daysdiff);
258 } else {
259 $daysdiff = ceil($daysdiff);
260 }
261 }
262 }
263 $groupdays = self::getGroupDays($first, $second, $daysdiff);
264 $q = "SELECT `b`.`id`,`b`.`checkin`,`b`.`checkout`,`b`.`realback`,`ob`.`idorder`,`ob`.`idbusy`,`or`.`id` AS `or_id`,`or`.`idroom`,`or`.`roomindex`,`o`.`status` ".
265 "FROM `#__vikbooking_busy` AS `b` ".
266 "LEFT JOIN `#__vikbooking_ordersbusy` `ob` ON `ob`.`idbusy`=`b`.`id` ".
267 "LEFT JOIN `#__vikbooking_ordersrooms` `or` ON `or`.`idorder`=`ob`.`idorder` AND `or`.`idorder`!=".(int)$order['id']." ".
268 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` AND `o`.`id`=`ob`.`idorder` AND `o`.`id`!=".(int)$order['id']." ".
269 "WHERE `or`.`idroom`=".(int)$idroom." AND `b`.`checkout` > ".time()." AND `o`.`status`='confirmed' AND `ob`.`idorder`!=".(int)$order['id']." AND `ob`.`idorder` > 0;";
270 $dbo->setQuery($q);
271 $busy = $dbo->loadAssocList();
272 if ($busy) {
273 foreach ($groupdays as $gday) {
274 foreach ($busy as $bu) {
275 if (empty($bu['roomindex']) || empty($bu['idorder'])) {
276 continue;
277 }
278 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
279 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
280 } elseif (count($groupdays) == 2 && $gday == $groupdays[0]) {
281 if ($groupdays[0] < $bu['checkin'] && $groupdays[0] < $bu['realback'] && $groupdays[1] > $bu['checkin'] && $groupdays[1] > $bu['realback']) {
282 $unavailable_indexes[$bu['or_id']] = $bu['roomindex'];
283 }
284 }
285 }
286 }
287 }
288 $q = "SELECT `params` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
289 $dbo->setQuery($q);
290 $room_params = $dbo->loadResult();
291 if ($room_params) {
292 $room_params_arr = json_decode($room_params, true);
293 if (array_key_exists('features', $room_params_arr) && is_array($room_params_arr['features']) && count($room_params_arr['features'])) {
294 foreach ($room_params_arr['features'] as $rind => $rfeatures) {
295 if (in_array($rind, $unavailable_indexes)) {
296 continue;
297 }
298 $available_indexes[] = $rind;
299 }
300 }
301 }
302
303 return $available_indexes;
304 }
305
306 /**
307 * Load the restrictions applying the given filters to the passed rooms.
308 * The ordering of the query SHOULD remain unchanged, because it's required
309 * to have it by ascending order of the ID. So the older records first.
310 * Even when no filters, we always exclude expired restrictions.
311 *
312 * @param boolean $filters whether to apply filters
313 * @param array $rooms the list of rooms to filter
314 *
315 * @return array the list of restrictions found or an empty array.
316 */
317 public static function loadRestrictions($filters = true, $rooms = [])
318 {
319 $dbo = JFactory::getDbo();
320
321 $restrictions = [];
322 $limts = strtotime(date('Y-m-d'));
323
324 if (!$filters) {
325 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts." ORDER BY `id` ASC;";
326 } else {
327 if (!$rooms) {
328 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.") ORDER BY `id` ASC;";
329 } else {
330 $clause = [];
331 foreach ($rooms as $idr) {
332 if (empty($idr)) {
333 continue;
334 }
335 $clause[] = "`idrooms` LIKE '%-" . (int) $idr . "-%'";
336 }
337 if ($clause) {
338 $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;";
339 } else {
340 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= " . $limts . ") ORDER BY `id` ASC;";
341 }
342 }
343 }
344
345 $dbo->setQuery($q);
346 $allrestrictions = $dbo->loadAssocList();
347
348 foreach ($allrestrictions as $k => $res) {
349 if (!empty($res['month'])) {
350 $restrictions[$res['month']] = $res;
351 } else {
352 if (!isset($restrictions['range'])) {
353 $restrictions['range'] = array();
354 }
355 $restrictions['range'][$k] = $res;
356 }
357 }
358
359 return $restrictions;
360 }
361
362 public static function globalRestrictions($restrictions = [])
363 {
364 $ret = [];
365 foreach ($restrictions as $kr => $rr) {
366 if ($kr == 'range') {
367 foreach ($rr as $kd => $dr) {
368 if ($dr['allrooms'] == 1) {
369 $ret['range'][$kd] = $restrictions[$kr][$kd];
370 }
371 }
372 continue;
373 }
374 if ($rr['allrooms'] == 1) {
375 $ret[$kr] = $restrictions[$kr];
376 }
377 }
378 return $ret;
379 }
380
381 /**
382 * From the given restrictions, check-in, check-out and nights, looks for
383 * a restriction to be returned and applied over these stays.
384 * In order to give priority to newer Restriction IDs, the order of the
385 * $restrictions array should be ascending, so that the newer restrictions
386 * will overwrite the array with the last ID (more recent). Only for date ranges.
387 * Returns an array with the record of the restriction found. The loop is not
388 * broken when the first valid restriction is found to give priority to
389 * newer restriction IDs to overwrite older records.
390 *
391 * @param int $first the unix timestamp for the check-in date
392 * @param int $second the unix timestamp for the check-out date
393 * @param int $daysdiff the number of nights of stay
394 * @param array $restrictions the list of restrictions loaded
395 *
396 * @return array the restriction found, or an empty array.
397 */
398 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
399 $season_restrictions = array();
400 $restrcheckin = getdate($first);
401 $restrcheckout = getdate($second);
402 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
403 //restriction found for this month, checking:
404 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
405 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
406 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
407 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
408 //Week Day Arrival Restriction
409 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
410 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
411 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
412 }
413 $season_restrictions['wdays'] = $rvalidwdays;
414 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
415 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
416 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
417 }
418 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
419 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
420 }
421 }
422 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
423 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
424 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
425 $season_restrictions['allowed'] = false;
426 }
427 }
428 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
429 $season_restrictions['allowed'] = false;
430 }
431 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
432 } elseif (array_key_exists('range', $restrictions)) {
433 foreach ($restrictions['range'] as $restr) {
434 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
435 //restriction found for this date range, checking:
436 $season_restrictions['id'] = $restr['id'];
437 $season_restrictions['name'] = $restr['name'];
438 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
439 if (strlen((string)$restr['wday']) > 0) {
440 //Week Day Arrival Restriction
441 $rvalidwdays = array($restr['wday']);
442 if (strlen((string)$restr['wdaytwo']) > 0) {
443 $rvalidwdays[] = $restr['wdaytwo'];
444 }
445 $season_restrictions['wdays'] = $rvalidwdays;
446 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
447 if (!empty($restr['ctad'])) {
448 $season_restrictions['cta'] = explode(',', $restr['ctad']);
449 }
450 if (!empty($restr['ctdd'])) {
451 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
452 }
453 }
454 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] > $restr['minlos']) {
455 $season_restrictions['maxlos'] = $restr['maxlos'];
456 if ($daysdiff > $restr['maxlos']) {
457 $season_restrictions['allowed'] = false;
458 }
459 }
460 if ($daysdiff < $restr['minlos']) {
461 $season_restrictions['allowed'] = false;
462 }
463 $season_restrictions['minlos'] = $restr['minlos'];
464 }
465 }
466 }
467
468 return $season_restrictions;
469 }
470
471 public static function compareSeasonRestrictionsNights($restrictions)
472 {
473 $base_compare = array();
474 $base_nights = 0;
475 foreach ($restrictions as $nights => $restr) {
476 $base_compare = $restr;
477 $base_nights = $nights;
478 break;
479 }
480
481 /**
482 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
483 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
484 *
485 * @since 1.15.0 (J) - 1.5.0 (WP)
486 */
487 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
488
489 foreach ($restrictions as $nights => $restr) {
490 if ($nights == $base_nights) {
491 continue;
492 }
493
494 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
495 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
496
497 // get associative array of differences
498 $diff = array_diff($use_base_compare, $use_restr);
499
500 if (count($diff) > 0 && array_key_exists('id', $diff)) {
501 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
502 // only one Restriction per time should be applied to certain Season Dates but check just in case.
503 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
504 }
505 }
506
507 return array();
508 }
509
510 /**
511 * Methods using array_diff() may need one-dimension arrays, because this
512 * native functions applies a string comparison, and so casting an array
513 * value to a string would generate a Notice message. This method simply
514 * converts any sub-array into a restorable string into a non-scalar var.
515 * This only works when comparing 2 array variables.
516 *
517 * @param array $arr the hypothetical multi dimension array.
518 *
519 * @return array the one dimension array to be passed to array_diff()
520 * and the list of keys converted to strings for comparison.
521 *
522 * @since 1.15.0 (J) - 1.5.0 (WP)
523 */
524 public static function prepareMultiDimArrayDiff($arr)
525 {
526 $casted_keys = array();
527
528 if (!is_array($arr) || !count($arr)) {
529 return array($casted_keys, $arr);
530 }
531
532 foreach ($arr as $key => $val) {
533 if (!is_scalar($val)) {
534 // memorize original value
535 $casted_keys[$key] = $val;
536 // cast to string must be applied
537 $arr[$key] = json_encode($val);
538 }
539 }
540
541 return array($casted_keys, $arr);
542 }
543
544 /**
545 * Methods using array_diff() may need one-dimension arrays, because this
546 * native functions applies a string comparison, and so casting an array
547 * value to a string would generate a Notice message. This method restores
548 * the original values of the "prepared" arrays by getting the list of the
549 * modified keys from non-scalar to scalar values.
550 * This only works when comparing 2 array variables.
551 *
552 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
553 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
554 * @param array $arr the array result of array_diff().
555 *
556 * @return array the array result of array_diff() with its original values.
557 *
558 * @since 1.15.0 (J) - 1.5.0 (WP)
559 */
560 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
561 {
562 if (!is_array($arr) || !count($arr)) {
563 return $arr;
564 }
565
566 foreach ($arr as $key => $val) {
567 // keys returned by array_diff can be present in both $keys_one and $keys_two
568 if (isset($keys_one[$key])) {
569 // always give higher priority to the 1st argument keys for array_diff()
570 $arr[$key] = json_decode($keys_one[$key]);
571 } elseif (isset($keys_two[$key])) {
572 // fallback to 2nd argument keys
573 $arr[$key] = json_decode($keys_two[$key]);
574 }
575 }
576
577 return $arr;
578 }
579
580 public static function roomRestrictions($roomid, $restrictions) {
581 $ret = array();
582 if (!empty($roomid) && count($restrictions) > 0) {
583 foreach ($restrictions as $kr => $rr) {
584 if ($kr == 'range') {
585 foreach ($rr as $kd => $dr) {
586 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
587 $allrooms = explode(';', $dr['idrooms']);
588 if (in_array('-'.$roomid.'-', $allrooms)) {
589 $ret['range'][$kd] = $restrictions[$kr][$kd];
590 }
591 }
592 }
593 } else {
594 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
595 $allrooms = explode(';', $rr['idrooms']);
596 if (in_array('-'.$roomid.'-', $allrooms)) {
597 $ret[$kr] = $restrictions[$kr];
598 }
599 }
600 }
601 }
602 }
603 return $ret;
604 }
605
606 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff)
607 {
608 // default states
609 $restrictionerrmsg = '';
610 $restrictions_affcount = 0;
611
612 // check for month-level or range-level restrictions
613 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
614 //restriction found for this month, checking:
615 $restrictions_affcount++;
616 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
617 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
618 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
619 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
620 }
621 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
622 $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']) : ''));
623 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
624 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
625 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
626 }
627 }
628 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
629 if (count($comborestr) > 0) {
630 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
631 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
632 $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']));
633 }
634 }
635 }
636 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
637 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
638 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
639 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
640 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
641 }
642 }
643 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
644 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
645 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
646 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
647 }
648 }
649 }
650 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
651 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
652 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
653 }
654 }
655 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
656 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
657 }
658 } elseif (array_key_exists('range', $roomrestr)) {
659 // start valid flag
660 $restrictionsvalid = true;
661
662 /**
663 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
664 * The most recent restrictions will have a higher priority over the oldest ones.
665 *
666 * @since 1.12.1
667 */
668 $minlos_priority = array(
669 'ok' => array(),
670 'nok' => array()
671 );
672
673 /**
674 * Build a map of CTA/CTD priorities to be compared, to ensure they are regularly applied.
675 *
676 * @since 1.16.10 (J) - 1.6.10 (WP)
677 */
678 $ctad_priority = [
679 'ok' => [],
680 'nok' => [],
681 ];
682
683 foreach ($roomrestr['range'] as $restr) {
684 /**
685 * We should not always add 82799 seconds to the end date of the restriction
686 * because if they only last for one day (like a Saturday), then $restr['dto']
687 * will be already set to the time 23:59:59.
688 *
689 * @since 1.13 (J) - 1.2.18 (WP)
690 */
691 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) ? 82799 : 0;
692
693 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
694 // restriction found for this date range based on arrival date, check if compliant
695 $restrictions_affcount++;
696
697 // set flag for CTA/CTD compliance
698 $cta_ctd_passed = true;
699
700 if (strlen((string)$restr['wday']) > 0) {
701 $rvalidwdays = array($restr['wday']);
702 if (strlen((string)$restr['wdaytwo']) > 0) {
703 $rvalidwdays[] = $restr['wdaytwo'];
704 }
705 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
706 $restrictionsvalid = false;
707 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
708 } elseif ($restr['multiplyminlos'] == 1) {
709 if (($daysdiff % $restr['minlos']) != 0) {
710 $restrictionsvalid = false;
711 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
712 }
713 }
714 $comborestr = self::parseJsDrangeWdayCombo($restr);
715 if ($comborestr) {
716 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
717 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
718 $restrictionsvalid = false;
719 $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']));
720 }
721 }
722 }
723 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
724 if (!empty($restr['ctad'])) {
725 $ctarestrictions = explode(',', $restr['ctad']);
726 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
727 $restrictionsvalid = false;
728 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
729 $cta_ctd_passed = false;
730 }
731 }
732 if (!empty($restr['ctdd'])) {
733 $ctdrestrictions = explode(',', $restr['ctdd']);
734 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
735 $restrictionsvalid = false;
736 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
737 $cta_ctd_passed = false;
738 }
739 }
740 }
741
742 // check CTA/CTD compliance
743 if (!$cta_ctd_passed) {
744 array_push($ctad_priority['nok'], (int)$restr['id']);
745 } else {
746 array_push($ctad_priority['ok'], (int)$restr['id']);
747 }
748
749 // max LOS validation
750 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] > $restr['minlos']) {
751 if ($daysdiff > $restr['maxlos']) {
752 $restrictionsvalid = false;
753 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
754 }
755 }
756
757 // min LOS validation
758 if ($daysdiff < $restr['minlos']) {
759 $restrictionsvalid = false;
760 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
761 array_push($minlos_priority['nok'], (int)$restr['id']);
762 } else {
763 array_push($minlos_priority['ok'], (int)$restr['id']);
764 }
765 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
766 /**
767 * We validate the CTD restrictions depending on the check-out date.
768 *
769 * @since 1.16.3 (J) - 1.6.3 (WP)
770 */
771 $ctdrestrictions = explode(',', $restr['ctdd']);
772 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
773 $restrictions_affcount++;
774 $restrictionsvalid = false;
775 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
776 }
777 }
778 }
779
780 if (!$restrictionsvalid && $minlos_priority['ok'] && $minlos_priority['nok'] && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
781 // a more recent restriction is allowing this MinLOS
782 // ensure there are no recent CTA/CTD rules not compliant
783 $cta_ctd_priority_ok = true;
784
785 if ($ctad_priority['ok'] && $ctad_priority['nok'] && max($ctad_priority['nok']) > max($ctad_priority['ok'])) {
786 // a more recent restriction is not compliant with the CTA/CTD rules
787 $cta_ctd_priority_ok = false;
788 }
789
790 if ($cta_ctd_priority_ok) {
791 // we unset the error message because more recent restriction(s) are allowing this stay dates
792 $restrictionerrmsg = '';
793 }
794 }
795 }
796
797 // check global restriction of Min LOS for TAC functions in VBO and VCM
798 if (empty($restrictionerrmsg) && count($roomrestr) && $restrictions_affcount <= 0) {
799 // check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
800 $globminlos = self::getDefaultNightsCalendar();
801 if ($globminlos > 1 && $daysdiff < $globminlos) {
802 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
803 }
804 }
805
806 // return the restriction error message string, if anything wrong was found
807 return $restrictionerrmsg;
808 }
809
810 public static function parseJsDrangeWdayCombo($drestr) {
811 $combo = array();
812 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
813 $cparts = explode(':', $drestr['wdaycombo']);
814 foreach ($cparts as $kc => $cw) {
815 if (!empty($cw)) {
816 $nowcombo = explode('-', $cw);
817 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
818 }
819 }
820 }
821 return $combo;
822 }
823
824 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
825 $dbo = JFactory::getDbo();
826 $pkg = array();
827 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
828 $dbo->setQuery($q);
829 $dbo->execute();
830 if ($dbo->getNumRows() == 1) {
831 $pkg = $dbo->loadAssoc();
832 $vbo_tn = self::getTranslator();
833 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
834 } else {
835 return JText::translate('VBOPKGERRNOTFOUND');
836 }
837 $rooms_req = array();
838 foreach ($rooms as $num => $room) {
839 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
840 $rooms_req[] = $room['id'];
841 }
842 }
843 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
844 $dbo->setQuery($q);
845 $dbo->execute();
846 if ($dbo->getNumRows() != count($rooms_req)) {
847 //error, not all the rooms requested are available for this package
848 return JText::translate('VBOPKGERRNOTROOM');
849 }
850 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
851 return JText::translate('VBOPKGERRNUMNIGHTS');
852 }
853 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
854 return JText::translate('VBOPKGERRCHECKIND');
855 }
856 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
857 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
858 return JText::translate('VBOPKGERRCHECKOUTD');
859 }
860 if (!empty($pkg['excldates'])) {
861 //this would check if any stay date is excluded
862 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
863 //check just the arrival and departure dates
864 $bookdates_ts = array($checkints, $checkoutts);
865 $bookdates = array();
866 foreach ($bookdates_ts as $bookdate_ts) {
867 $info_d = getdate($bookdate_ts);
868 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
869 }
870 $edates = explode(';', $pkg['excldates']);
871 foreach ($edates as $edate) {
872 if (!empty($edate) && in_array($edate, $bookdates)) {
873 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
874 }
875 }
876 }
877 return $pkg;
878 }
879
880 public static function getPackage($pkg_id) {
881 $dbo = JFactory::getDbo();
882 $pkg = array();
883 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
884 $dbo->setQuery($q);
885 $dbo->execute();
886 if ($dbo->getNumRows() == 1) {
887 $pkg = $dbo->loadAssoc();
888 }
889 return $pkg;
890 }
891
892 public static function getRoomParam($paramname, $paramstr)
893 {
894 if (empty($paramstr)) {
895 return '';
896 }
897
898 $paramarr = json_decode($paramstr, true);
899
900 if (is_array($paramarr) && isset($paramarr[$paramname])) {
901 return $paramarr[$paramname];
902 }
903
904 return '';
905 }
906
907 public static function filterNightsSeasonsCal($arr_nights) {
908 $nights = array();
909 foreach ($arr_nights as $night) {
910 if (intval(trim($night)) > 0) {
911 $nights[] = intval(trim($night));
912 }
913 }
914 sort($nights);
915 return array_unique($nights);
916 }
917
918 public static function getSeasonRangeTs($from, $to, $year) {
919 $sfrom = 0;
920 $sto = 0;
921 $tsbase = mktime(0, 0, 0, 1, 1, $year);
922 $curyear = $year;
923 $tsbasetwo = $tsbase;
924 $curyeartwo = $year;
925 if ($from > $to) {
926 //between two years
927 $curyeartwo += 1;
928 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
929 }
930 $sfrom = ($tsbase + $from);
931 $sto = ($tsbasetwo + $to);
932 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
933 //leap years
934 $infoseason = getdate($sfrom);
935 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
936 if ($infoseason[0] > $leapts) {
937 /**
938 * Timestamp must be greater than the leap-day of Feb 29th.
939 * It used to be checked for >= $leapts.
940 *
941 * @since July 3rd 2019
942 */
943 $sfrom += 86400;
944 if ($curyear == $curyeartwo) {
945 $sto += 86400;
946 }
947 }
948 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
949 //leap years
950 $infoseason = getdate($sto);
951 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
952 if ($infoseason[0] > $leapts) {
953 /**
954 * Timestamp must be greater than the leap-day of Feb 29th.
955 * It used to be checked for >= $leapts.
956 *
957 * @since July 3rd 2019
958 */
959 $sto += 86400;
960 }
961 }
962 return array($sfrom, $sto);
963 }
964
965 public static function sortSeasonsRangeTs ($all_seasons) {
966 $sorted = array();
967 $map = array();
968 foreach ($all_seasons as $key => $season) {
969 $map[$key] = $season['from_ts'];
970 }
971 asort($map);
972 foreach ($map as $key => $s) {
973 $sorted[] = $all_seasons[$key];
974 }
975 return $sorted;
976 }
977
978 public static function formatSeasonDates ($from_ts, $to_ts) {
979 $one = getdate($from_ts);
980 $two = getdate($to_ts);
981 $months_map = array(
982 1 => JText::translate('VBSHORTMONTHONE'),
983 2 => JText::translate('VBSHORTMONTHTWO'),
984 3 => JText::translate('VBSHORTMONTHTHREE'),
985 4 => JText::translate('VBSHORTMONTHFOUR'),
986 5 => JText::translate('VBSHORTMONTHFIVE'),
987 6 => JText::translate('VBSHORTMONTHSIX'),
988 7 => JText::translate('VBSHORTMONTHSEVEN'),
989 8 => JText::translate('VBSHORTMONTHEIGHT'),
990 9 => JText::translate('VBSHORTMONTHNINE'),
991 10 => JText::translate('VBSHORTMONTHTEN'),
992 11 => JText::translate('VBSHORTMONTHELEVEN'),
993 12 => JText::translate('VBSHORTMONTHTWELVE')
994 );
995 $mday_map = array(
996 1 => JText::translate('VBMDAYFRIST'),
997 2 => JText::translate('VBMDAYSECOND'),
998 3 => JText::translate('VBMDAYTHIRD'),
999 'generic' => JText::translate('VBMDAYNUMGEN')
1000 );
1001 if ($one['year'] == $two['year']) {
1002 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>';
1003 }
1004 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'];
1005 }
1006
1007 public static function getFirstCustDataField($custdata) {
1008 $first_field = '';
1009 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
1010 //Room is closed with this booking
1011 return '----';
1012 }
1013 $parts = explode("\n", $custdata);
1014 foreach ($parts as $part) {
1015 if (!empty($part)) {
1016 $field = explode(':', trim($part));
1017 if (!empty($field[1])) {
1018 return trim($field[1]);
1019 }
1020 }
1021 }
1022 return $first_field;
1023 }
1024
1025 /**
1026 * This method composes a string to be logged for the admin
1027 * to keep track of what was inside the booking before the
1028 * modification. Returns a string and it uses language definitions
1029 * that should be available on the front-end and back-end INI files.
1030 *
1031 * @param array $old_booking the array of the booking prior to the modification
1032 * @param array $room_stay_dates optional list of room stay information in case of split stays.
1033 *
1034 * @return string text describing the situation with the booking before the changes.
1035 *
1036 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
1037 */
1038 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
1039 {
1040 $vbo_df = self::getDateFormat();
1041 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1042 $wdays_map = array(
1043 JText::translate('VBWEEKDAYZERO'),
1044 JText::translate('VBWEEKDAYONE'),
1045 JText::translate('VBWEEKDAYTWO'),
1046 JText::translate('VBWEEKDAYTHREE'),
1047 JText::translate('VBWEEKDAYFOUR'),
1048 JText::translate('VBWEEKDAYFIVE'),
1049 JText::translate('VBWEEKDAYSIX'),
1050 );
1051 $now_info = getdate();
1052 $checkin_info = getdate($old_booking['checkin']);
1053 $checkout_info = getdate($old_booking['checkout']);
1054
1055 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1056 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1057 $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]);
1058 $prev_rooms = '';
1059
1060 $orooms_map = [];
1061 $orooms_arr = [];
1062
1063 if (isset($old_booking['rooms_info'])) {
1064 foreach ($old_booking['rooms_info'] as $oroom) {
1065 $orooms_arr[] = $oroom['name'].', '.JText::translate('VBMAILADULTS').': '.$oroom['adults'].', '.JText::translate('VBMAILCHILDREN').': '.$oroom['children'];
1066 if (!empty($oroom['idroom'])) {
1067 $orooms_map[$oroom['idroom']] = $oroom['name'];
1068 }
1069 }
1070 $prev_rooms = implode("\n", $orooms_arr);
1071 }
1072
1073 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && count($orooms_map)) {
1074 $split_stay_prev_infos = [];
1075 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1076 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1077 continue;
1078 }
1079 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1080 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1081 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1082
1083 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1084 if (!empty($room_nights)) {
1085 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1086 }
1087 $split_stay_descr .= ', ';
1088 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1089
1090 // push split stay description string
1091 $split_stay_prev_infos[] = $split_stay_descr;
1092 }
1093 if (count($split_stay_prev_infos)) {
1094 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1095 }
1096 }
1097
1098 $currencyname = self::getCurrencyName();
1099 $prev_total = $currencyname.' '.self::numberFormat($old_booking['total']);
1100
1101 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1102 }
1103
1104 /**
1105 * This method invokes the class
1106 * VikChannelManagerLogos (new in VCM 1.6.4) to
1107 * map the name of a channel to its corresponding logo.
1108 * The method can also be used to get an istance of the class.
1109 *
1110 * @param mixed $provenience either a string or an array with main and sub channels.
1111 * @param boolean $get_istance
1112 *
1113 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1114 */
1115 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1116 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1117 return false;
1118 }
1119 if (!class_exists('VikChannelManagerLogos')) {
1120 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1121 }
1122
1123 /**
1124 * Due to the new iCal channel we now support main and sub channels.
1125 *
1126 * @since 1.13 - VCM 1.7.0
1127 */
1128 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1129 $provenience = explode('_', $provenience);
1130 }
1131 $main_channel = '';
1132 $full_channel = '';
1133 if (is_array($provenience)) {
1134 if (count($provenience) > 1) {
1135 $main_channel = $provenience[1];
1136 } else {
1137 $main_channel = $provenience[0];
1138 }
1139 if (stripos($provenience[0], 'ical') !== false) {
1140 $full_channel = implode('_', $provenience);
1141 }
1142 } else {
1143 $main_channel = $provenience;
1144 }
1145
1146 // get object instance by passing the main provenience
1147 $obj = new VikChannelManagerLogos($main_channel);
1148
1149 // update provenience with main and full channel
1150 if (!empty($full_channel)) {
1151 $obj->setProvenience($main_channel, $full_channel);
1152 }
1153
1154 // return either the instance or the logo URL for this channel source
1155 return $get_istance ? $obj : $obj->getLogoURL();
1156 }
1157
1158 public static function vcmAutoUpdate()
1159 {
1160 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1161 return -1;
1162 }
1163
1164 $vcm_auto_upd = (int)VBOFactory::getConfig()->get('vcmautoupd', 0);
1165
1166 return $vcm_auto_upd ? 1 : 0;
1167 }
1168
1169 public static function getVcmInvoker()
1170 {
1171 if (!class_exists('VboVcmInvoker')) {
1172 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1173 }
1174
1175 return new VboVcmInvoker();
1176 }
1177
1178 /**
1179 * Returns an instance of VikBooking History object.
1180 *
1181 * @param int $bid optional booking ID to bind.
1182 *
1183 * @return VboBookingHistory
1184 *
1185 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1186 */
1187 public static function getBookingHistoryInstance($bid = 0)
1188 {
1189 if (!class_exists('VboBookingHistory')) {
1190 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1191 }
1192
1193 return new VboBookingHistory($bid);
1194 }
1195
1196 /**
1197 * Returns an instance of the VCMChatHandler class to handle
1198 * the messaging/chat for the given reservation ID.
1199 *
1200 * @param int $oid the ID of the booking in VBO
1201 * @param string $channel the name of the source channel
1202 *
1203 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1204 *
1205 * @since 1.11.2 (J) - 1.1.2 (WP)
1206 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1207 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1208 */
1209 public static function getVcmChatInstance($oid, $channel = null)
1210 {
1211 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1212 if (!is_file($vcm_messaging_helper)) {
1213 // VCM is not available
1214 return null;
1215 }
1216
1217 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1218 // VCM must be inactive, prevent the loading of the instance
1219 return null;
1220 }
1221
1222 // make sure the channel name is correct as it may contain the sub-network
1223 if (!empty($channel)) {
1224 $segments = explode('_', $channel);
1225 if (count($segments) > 1) {
1226 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1227 $channel = $segments[0];
1228 }
1229 }
1230
1231 // always require main file of the abstract class even if arguments are empty/invalid
1232 require_once $vcm_messaging_helper;
1233
1234 // return the instance of the class for this channel handler
1235 return VCMChatHandler::getInstance($oid, $channel);
1236 }
1237
1238 /**
1239 * Returns an instance of the VCMOpportunityHandler class to handle
1240 * the various opportunities through VCM.
1241 *
1242 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1243 *
1244 * @since 1.2.0
1245 */
1246 public static function getVcmOpportunityInstance() {
1247 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1248 if (!is_file($vcm_opp_helper)) {
1249 // VCM is not available
1250 return null;
1251 }
1252 // require main file of the class
1253 require_once $vcm_opp_helper;
1254 // return the instance of the class
1255 return VCMOpportunityHandler::getInstance();
1256 }
1257
1258 /**
1259 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1260 *
1261 * @param array $data optional data to bind to the object.
1262 *
1263 * @return bool
1264 *
1265 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1266 *
1267 * @requires VCM >= 1.8.24
1268 */
1269 public static function vcmBcomReportingSupported(array $data = [])
1270 {
1271 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1272 }
1273
1274 /**
1275 * Gets a list of all channels supporting the promotions.
1276 *
1277 * @param string $key the key of the handler.
1278 *
1279 * @return mixed false if VCM is not installed, empty array otherwise.
1280 *
1281 * @requires Vik Channel Manager 1.7.1
1282 *
1283 * @since 1.3.0
1284 */
1285 public static function getPromotionHandlers($key = null)
1286 {
1287 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1288 // VCM not installed
1289 return false;
1290 }
1291
1292 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1293 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1294 return [];
1295 }
1296
1297 return VikChannelManager::getPromotionHandlers($key);
1298 }
1299
1300 /**
1301 * Gets the factors for suggesting the application of the promotions.
1302 *
1303 * @param mixed $data some optional instructions to be passed as argument.
1304 *
1305 * @return mixed false if VCM is not installed, associative array otherwise.
1306 *
1307 * @requires Vik Channel Manager 1.7.1
1308 *
1309 * @since 1.3.0
1310 */
1311 public static function getPromotionFactors($data = null)
1312 {
1313 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1314 // VCM not installed
1315 return false;
1316 }
1317
1318 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1319 // VCM is outdated
1320 return false;
1321 }
1322
1323 return VikChannelManager::getPromotionFactors($data);
1324 }
1325
1326 public static function getTheme()
1327 {
1328 return VBOFactory::getConfig()->get('theme', '');
1329 }
1330
1331 public static function getFooterOrdMail($vbo_tn = null)
1332 {
1333 $dbo = JFactory::getDbo();
1334
1335 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1336 $dbo->setQuery($q);
1337 $ft = $dbo->loadAssocList();
1338
1339 if (!is_object($vbo_tn)) {
1340 $vbo_tn = self::getTranslator();
1341 }
1342
1343 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1344
1345 return $ft ? $ft[0]['setting'] : '';
1346 }
1347
1348 public static function requireLogin()
1349 {
1350 return VBOFactory::getConfig()->getBool('requirelogin', false);
1351 }
1352
1353 public static function autoRoomUnit()
1354 {
1355 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1356 }
1357
1358 public static function todayBookings()
1359 {
1360 static $todayBookings = null;
1361
1362 if ($todayBookings === null) {
1363 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1364 }
1365
1366 return $todayBookings;
1367 }
1368
1369 public static function couponsEnabled()
1370 {
1371 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1372 }
1373
1374 public static function customersPinEnabled()
1375 {
1376 return VBOFactory::getConfig()->getBool('enablepin', false);
1377 }
1378
1379 /**
1380 * Detects the type of visitor from the user agent.
1381 * Known types are: computer, smartphone, tablet.
1382 *
1383 * @param boolean $returnua whether the type of visitor should be returned. If false
1384 * boolean is returned in case of mobile device detected.
1385 * @param boolean $loadassets whether the system should load an apposite CSS file.
1386 *
1387 * @return mixed string for the type of visitor or boolean if mobile detected.
1388 *
1389 * @since 1.0.13 - Revision September 2018
1390 */
1391 public static function detectUserAgent($returnua = false, $loadassets = true) {
1392 $session = JFactory::getSession();
1393 $sval = $session->get('vbuseragent', '');
1394 $mobiles = array('tablet', 'smartphone');
1395 if (!empty($sval)) {
1396 if ($loadassets) {
1397 self::userAgentStyleSheet($sval);
1398 }
1399 return $returnua ? $sval : in_array($sval, $mobiles);
1400 }
1401
1402 // detect visitor (device) type
1403 $visitoris = 'computer';
1404 try {
1405 if (!class_exists('MobileDetector')) {
1406 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1407 }
1408 $detector = new MobileDetector;
1409 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1410 } catch (Exception $e) {
1411 // do nothing
1412 }
1413
1414 $session->set('vbuseragent', $visitoris);
1415 if ($loadassets) {
1416 self::userAgentStyleSheet($visitoris);
1417 }
1418
1419 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1420 }
1421
1422 public static function userAgentStyleSheet($ua)
1423 {
1424 /**
1425 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1426 */
1427 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1428 return;
1429 }
1430 //
1431
1432 $document = JFactory::getDocument();
1433 /**
1434 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1435 */
1436 if ($ua == 'smartphone') {
1437 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1438 } elseif ($ua == 'tablet') {
1439 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1440 }
1441 return true;
1442 }
1443
1444 public static function loadJquery()
1445 {
1446 return VBOFactory::getConfig()->getBool('loadjquery', true);
1447 }
1448
1449 public static function loadBootstrap()
1450 {
1451 return VBOFactory::getConfig()->getBool('bootstrap', true);
1452 }
1453
1454 public static function allowMultiLanguage()
1455 {
1456 return VBOFactory::getConfig()->getBool('multilang', true);
1457 }
1458
1459 public static function getTranslator()
1460 {
1461 if (!class_exists('VikBookingTranslator')) {
1462 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1463 }
1464
1465 return new VikBookingTranslator();
1466 }
1467
1468 /**
1469 * New method's name spelled correctly. The object will be cached by default.
1470 *
1471 * @return VikBookingCustomersPin
1472 *
1473 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1474 */
1475 public static function getCPinInstance()
1476 {
1477 static $cpin_instance = null;
1478
1479 if (!$cpin_instance) {
1480 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1481
1482 $cpin_instance = new VikBookingCustomersPin();
1483 }
1484
1485 return $cpin_instance;
1486 }
1487
1488 /**
1489 * Old method's name spelled wrongly for BC.
1490 *
1491 * @return VikBookingCustomersPin
1492 */
1493 public static function getCPinIstance()
1494 {
1495 return self::getCPinInstance();
1496 }
1497
1498 /**
1499 * Returns an instance of the VikBookingTracker Class.
1500 * It is also possible to call this method to just require the library.
1501 * This is useful for the back-end to access some static methods
1502 * without tracking any data.
1503 *
1504 * @param boolean $require_only whether to return the object.
1505 *
1506 * @return VikBookingTracker
1507 *
1508 * @since 1.11
1509 */
1510 public static function getTracker($require_only = false) {
1511 if (!class_exists('VikBookingTracker')) {
1512 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1513 }
1514 return $require_only ? true : VikBookingTracker::getInstance();
1515 }
1516
1517 /**
1518 * Returns an instance of the VikBookingOperator Class.
1519 *
1520 * @return VikBookingOperator
1521 *
1522 * @since 1.11
1523 */
1524 public static function getOperatorInstance() {
1525 if (!class_exists('VikBookingOperator')) {
1526 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1527 }
1528 return VikBookingOperator::getInstance();
1529 }
1530
1531 /**
1532 * Returns an instance of the VikBookingFestivities Class.
1533 *
1534 * @return VikBookingFestivities
1535 *
1536 * @since 1.12
1537 */
1538 public static function getFestivitiesInstance() {
1539 if (!class_exists('VikBookingFestivities')) {
1540 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1541 }
1542 return VikBookingFestivities::getInstance();
1543 }
1544
1545 /**
1546 * Returns an instance of the VikBookingCriticalDates Class.
1547 *
1548 * @return VikBookingCriticalDates
1549 *
1550 * @since 1.13.5
1551 */
1552 public static function getCriticalDatesInstance() {
1553 if (!class_exists('VikBookingCriticalDates')) {
1554 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1555 }
1556 return VikBookingCriticalDates::getInstance();
1557 }
1558
1559 /**
1560 * Checks whether the chat is enabled.
1561 *
1562 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1563 *
1564 * @since 1.12
1565 */
1566 public static function chatEnabled() {
1567 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1568 return -1;
1569 }
1570 $dbo = JFactory::getDbo();
1571 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1572 $dbo->setQuery($q);
1573 $dbo->execute();
1574 if (!$dbo->getNumRows()) {
1575 // enable the chat by default if VCM is installed
1576 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1577 $dbo->setQuery($q);
1578 $dbo->execute();
1579 return 1;
1580 }
1581 $s = $dbo->loadResult();
1582 return intval($s);
1583 }
1584
1585 /**
1586 * Loads the chat parameters from the configuration.
1587 *
1588 * @return object stdClass object from decoded JSON string
1589 *
1590 * @since 1.12
1591 */
1592 public static function getChatParams() {
1593 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1594 return new stdClass;
1595 }
1596 $dbo = JFactory::getDbo();
1597 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1598 $dbo->setQuery($q);
1599 $dbo->execute();
1600 if (!$dbo->getNumRows()) {
1601 // compose default and basic chat params for the first time
1602 $basic_params = new stdClass;
1603 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1604 $basic_params->av_type = 'checkin';
1605 $basic_params->av_days = 0;
1606
1607 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1608 $dbo->setQuery($q);
1609 $dbo->execute();
1610
1611 return $basic_params;
1612 }
1613 return json_decode($dbo->loadResult());
1614 }
1615
1616 /**
1617 * Checks whether the pre-checkin is enabled.
1618 *
1619 * @return int 0 if disabled, 1 otherwise
1620 *
1621 * @since 1.12
1622 */
1623 public static function precheckinEnabled() {
1624 $dbo = JFactory::getDbo();
1625 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinenabled';";
1626 $dbo->setQuery($q);
1627 $dbo->execute();
1628 if (!$dbo->getNumRows()) {
1629 // enable the pre-checkin by default
1630 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinenabled', '1');";
1631 $dbo->setQuery($q);
1632 $dbo->execute();
1633 return 1;
1634 }
1635 $s = $dbo->loadResult();
1636 return intval($s);
1637 }
1638
1639 /**
1640 * Returns the minimum number of days in advance to
1641 * enable the pre-checkin via front-end.
1642 *
1643 * @return int the min number of days in advance for pre-checkin.
1644 *
1645 * @since 1.12
1646 */
1647 public static function precheckinMinOffset() {
1648 $dbo = JFactory::getDbo();
1649 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinminoffset';";
1650 $dbo->setQuery($q);
1651 $dbo->execute();
1652 if (!$dbo->getNumRows()) {
1653 // set the limit to 1 day before arrival by default
1654 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinminoffset', '1');";
1655 $dbo->setQuery($q);
1656 $dbo->execute();
1657 return 1;
1658 }
1659 $s = $dbo->loadResult();
1660 return intval($s);
1661 }
1662
1663 /**
1664 * Whether upselling extra services is enabled.
1665 *
1666 * @return boolean true if enabled, false otherwise.
1667 *
1668 * @since 1.13
1669 */
1670 public static function upsellingEnabled() {
1671 $dbo = JFactory::getDbo();
1672 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='upselling';";
1673 $dbo->setQuery($q);
1674 $dbo->execute();
1675 if (!$dbo->getNumRows()) {
1676 // enable the upselling feature by default
1677 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('upselling', '1');";
1678 $dbo->setQuery($q);
1679 $dbo->execute();
1680 return true;
1681 }
1682 return (intval($dbo->loadResult()) > 0);
1683 }
1684
1685 /**
1686 * Collects a list of options/extra that can be upsold for the
1687 * the rooms booked.
1688 *
1689 * @param array $upsell_data list of stdClass objects for each room booked.
1690 * @param array $info some booking details (id, checkin, checkout).
1691 * @param object $vbo_tn the translation object.
1692 *
1693 * @return array list of upsellable options for each room booked.
1694 *
1695 * @since 1.3.0
1696 */
1697 public static function loadUpsellingData($upsell_data, $info, $vbo_tn) {
1698 $dbo = JFactory::getDbo();
1699 // get all options for all rooms booked
1700 $all_room_ids = array();
1701 foreach ($upsell_data as $v) {
1702 if (!in_array($v->id, $all_room_ids)) {
1703 array_push($all_room_ids, $v->id);
1704 }
1705 }
1706 $q = "SELECT `id`, `idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(', ', $all_room_ids) . ");";
1707 $dbo->setQuery($q);
1708 $dbo->execute();
1709 if (!$dbo->getNumRows()) {
1710 // no rooms found
1711 return array();
1712 }
1713 $records = $dbo->loadAssocList();
1714 $all_options = array();
1715 $rooms_options = array();
1716 foreach ($records as $v) {
1717 $allopts = explode(';', $v['idopt']);
1718 $room_opt = array();
1719 foreach ($allopts as $o) {
1720 if (empty($o)) {
1721 continue;
1722 }
1723 if (!in_array((int)$o, $all_options)) {
1724 array_push($all_options, (int)$o);
1725 }
1726 array_push($room_opt, (int)$o);
1727 }
1728 $rooms_options[$v['id']] = $room_opt;
1729 }
1730 if (!count($all_options)) {
1731 // no options found
1732 return array();
1733 }
1734 // load all options that could be used by the booked rooms no matter what was already booked
1735 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (" . implode(', ', $all_options) . ") AND `forcesel`=0 AND `ifchildren`=0 AND `is_citytax`=0 AND `is_fee`=0;";
1736 $dbo->setQuery($q);
1737 $dbo->execute();
1738 if (!$dbo->getNumRows()) {
1739 // no upsell-able options found
1740 return array();
1741 }
1742 $records = $dbo->loadAssocList();
1743 // filter options by available date and translate records
1744 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1745 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1746 $records = !is_array($records) ? array() : $records;
1747 //
1748 $tot_upsellable = 0;
1749 foreach ($upsell_data as $k => $rdata) {
1750 if (!isset($upsell_data[$k]->upsellable)) {
1751 $upsell_data[$k]->upsellable = array();
1752 }
1753 foreach ($records as $opt) {
1754 if (!empty($opt['ageintervals'])) {
1755 // upsellable options should not contain age intervals for children
1756 continue;
1757 }
1758 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
1759 // this option is not assigned to this room
1760 continue;
1761 }
1762 // check if the option is suited for this room party
1763 $clone_opt = array($opt);
1764 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
1765 if (!is_array($clone_opt) || !count($clone_opt)) {
1766 // this option is not suited for this room party
1767 continue;
1768 }
1769 //
1770 if (in_array($opt['id'], $rdata->options)) {
1771 // this option has already been booked
1772 continue;
1773 }
1774 // push this option and increase counter
1775 array_push($upsell_data[$k]->upsellable, $opt);
1776 $tot_upsellable++;
1777 }
1778 }
1779
1780 // if no upsellable options were found, we return an empty array
1781 return $tot_upsellable > 0 ? $upsell_data : array();
1782 }
1783
1784 /**
1785 * Returns the minimum days in advance for booking, by considering by default
1786 * also the property closing dates. If the property is currently closed, then
1787 * the minimum number of days in advance will be increased.
1788 *
1789 * @param bool $no_closing_dates whether to skip checking closing dates.
1790 *
1791 * @return int the number of days in advance for booking.
1792 *
1793 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
1794 * getMinDaysAdvance($skipsession = false) to:
1795 * getMinDaysAdvance($no_closing_dates = false)
1796 * This was made to return the proper number of days in advance
1797 * in case the property is currently closed.
1798 */
1799 public static function getMinDaysAdvance($no_closing_dates = false) {
1800 // cache value in static var
1801 static $getMinDaysAdvance = null;
1802
1803 if ($getMinDaysAdvance) {
1804 return $getMinDaysAdvance;
1805 }
1806 //
1807
1808 $dbo = JFactory::getDbo();
1809
1810 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='mindaysadvance';";
1811 $dbo->setQuery($q);
1812 $dbo->execute();
1813 $mind = $dbo->getNumRows() ? (int)$dbo->loadResult() : 0;
1814
1815 // update cached var
1816 $getMinDaysAdvance = $mind;
1817
1818 if ($no_closing_dates) {
1819 // do not check the closing dates
1820 return $mind;
1821 }
1822
1823 // check if the property is currently closed
1824 $cur_closed_dates = self::getClosingDates();
1825 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
1826 $today_midnight = mktime(0, 0, 0);
1827 $closed_until = null;
1828 foreach ($cur_closed_dates as $kcd => $vcd) {
1829 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
1830 // closing period found
1831 $closed_until = $vcd['to'];
1832 break;
1833 }
1834 }
1835 if ($closed_until !== null) {
1836 // count the number of days until property is closed
1837 $mind = 0;
1838 $today_info = getdate($today_midnight);
1839 while ($today_info[0] <= $closed_until) {
1840 $mind++;
1841 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
1842 }
1843 }
1844 }
1845
1846 // update cached var
1847 $getMinDaysAdvance = $mind;
1848
1849 return $mind;
1850 }
1851
1852 public static function getDefaultNightsCalendar($skipsession = false) {
1853 if ($skipsession) {
1854 $dbo = JFactory::getDbo();
1855 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1856 $dbo->setQuery($q);
1857 $dbo->execute();
1858 $s = $dbo->loadAssocList();
1859 return (int)$s[0]['setting'];
1860 } else {
1861 $session = JFactory::getSession();
1862 $sval = $session->get('vbdefaultNightsCalendar', '');
1863 if (!empty($sval)) {
1864 return (int)$sval;
1865 } else {
1866 $dbo = JFactory::getDbo();
1867 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1868 $dbo->setQuery($q);
1869 $dbo->execute();
1870 $s = $dbo->loadAssocList();
1871 $session->set('vbdefaultNightsCalendar', $s[0]['setting']);
1872 return (int)$s[0]['setting'];
1873 }
1874 }
1875 }
1876
1877 public static function getSearchNumRooms($skipsession = true)
1878 {
1879 $dbo = JFactory::getDbo();
1880 if ($skipsession) {
1881 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1882 $dbo->setQuery($q);
1883 $dbo->execute();
1884 $s = $dbo->loadAssocList();
1885 return (int)$s[0]['setting'];
1886 }
1887 $session = JFactory::getSession();
1888 $sval = $session->get('vbsearchNumRooms', '');
1889 if (!empty($sval)) {
1890 return (int)$sval;
1891 }
1892 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1893 $dbo->setQuery($q);
1894 $dbo->execute();
1895 $s = $dbo->loadAssocList();
1896 $session->set('vbsearchNumRooms', $s[0]['setting']);
1897 return (int)$s[0]['setting'];
1898 }
1899
1900 public static function getSearchNumAdults($skipsession = true)
1901 {
1902 $dbo = JFactory::getDbo();
1903 if ($skipsession) {
1904 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1905 $dbo->setQuery($q);
1906 $dbo->execute();
1907 $s = $dbo->loadAssocList();
1908 return $s[0]['setting'];
1909 }
1910 $session = JFactory::getSession();
1911 $sval = $session->get('vbsearchNumAdults', '');
1912 if (!empty($sval)) {
1913 return $sval;
1914 }
1915 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1916 $dbo->setQuery($q);
1917 $dbo->execute();
1918 $s = $dbo->loadAssocList();
1919 $session->set('vbsearchNumAdults', $s[0]['setting']);
1920 return $s[0]['setting'];
1921 }
1922
1923 public static function getSearchNumChildren($skipsession = true)
1924 {
1925 $dbo = JFactory::getDbo();
1926 if ($skipsession) {
1927 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1928 $dbo->setQuery($q);
1929 $dbo->execute();
1930 $s = $dbo->loadAssocList();
1931 return $s[0]['setting'];
1932 }
1933 $session = JFactory::getSession();
1934 $sval = $session->get('vbsearchNumChildren', '');
1935 if (!empty($sval)) {
1936 return $sval;
1937 }
1938 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1939 $dbo->setQuery($q);
1940 $dbo->execute();
1941 $s = $dbo->loadAssocList();
1942 $session->set('vbsearchNumChildren', $s[0]['setting']);
1943 return $s[0]['setting'];
1944 }
1945
1946 public static function getSmartSearchType($skipsession = true)
1947 {
1948 $dbo = JFactory::getDbo();
1949 if ($skipsession) {
1950 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1951 $dbo->setQuery($q);
1952 $dbo->execute();
1953 $s = $dbo->loadAssocList();
1954 return $s[0]['setting'];
1955 }
1956 $session = JFactory::getSession();
1957 $sval = $session->get('vbsmartSearchType', '');
1958 if (!empty($sval)) {
1959 return $sval;
1960 }
1961 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1962 $dbo->setQuery($q);
1963 $dbo->execute();
1964 $s = $dbo->loadAssocList();
1965 $session->set('vbsmartSearchType', $s[0]['setting']);
1966 return $s[0]['setting'];
1967 }
1968
1969 /**
1970 * Returns the maximum advance booking notice (offset) as a period.
1971 * It either returns the global configuration setting, or the value
1972 * defined at room-level, if any and if requested.
1973 *
1974 * @param int $idroom optional room ID for room-level setting.
1975 *
1976 * @return string the maximum booking notice period (i.e. +1Y).
1977 *
1978 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
1979 * $idroom to support room-level settings.
1980 */
1981 public static function getMaxDateFuture($idroom = 0)
1982 {
1983 static $max_adv_book_pool = [];
1984
1985 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
1986
1987 if (isset($max_adv_book_pool[$pool_id])) {
1988 return $max_adv_book_pool[$pool_id];
1989 }
1990
1991 $config = VBOFactory::getConfig();
1992
1993 if ($pool_id === 'global') {
1994 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
1995 } else {
1996 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
1997 if (!empty($room_level_max_notice)) {
1998 // setting is defined at room-level
1999 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
2000 } else {
2001 // recurse for global setting
2002 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
2003 }
2004 }
2005
2006 return $max_adv_book_pool[$pool_id];
2007 }
2008
2009 /**
2010 * Validates the maximum advance booking notice against the requested check-in
2011 * timestamp. The method supports the validation at both room and property levels.
2012 *
2013 * @param int $checkints the check-in date timestamp.
2014 * @param int $idroom the optional room ID for room-level validation.
2015 *
2016 * @return string error date string in case of validation failure.
2017 *
2018 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
2019 */
2020 public static function validateMaxDateBookings($checkints, $idroom = 0)
2021 {
2022 $datelim = self::getMaxDateFuture($idroom);
2023 $datelim = empty($datelim) ? '+2y' : $datelim;
2024 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
2025 $quantlim = substr($datelim, -1, 1);
2026
2027 $now = getdate();
2028 if ($quantlim == 'w') {
2029 $until_ts = strtotime("+$numlim weeks") + 86399;
2030 } else {
2031 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
2032 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
2033 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
2034 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
2035 }
2036
2037 if ($until_ts > $now[0] && $checkints > $until_ts) {
2038 $vbo_df = self::getDateFormat();
2039 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
2040
2041 // error, return the maximum date in the future allowed
2042 return date($df, $until_ts);
2043 }
2044
2045 // validation passed
2046 return '';
2047 }
2048
2049 public static function validateMinDaysAdvance($checkints) {
2050 $mindadv = self::getMinDaysAdvance(true);
2051 if ($mindadv > 0) {
2052 $tsinfo = getdate($checkints);
2053 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2054 if ($checkints < $limit_ts) {
2055 return $mindadv;
2056 }
2057 }
2058
2059 return '';
2060 }
2061
2062 /**
2063 * The only supported calendar type has been changed to jQuery UI.
2064 *
2065 * @since 1.13
2066 */
2067 public static function calendarType($skipsession = false)
2068 {
2069 return 'jqueryui';
2070 }
2071
2072 public static function getSiteLogo()
2073 {
2074 static $siteLogo = null;
2075
2076 if ($siteLogo !== null) {
2077 return $siteLogo;
2078 }
2079
2080 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2081
2082 return $siteLogo;
2083 }
2084
2085 public static function getBackendLogo()
2086 {
2087 static $backLogo = null;
2088
2089 if ($backLogo !== null) {
2090 return $backLogo;
2091 }
2092
2093 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2094
2095 return $backLogo;
2096 }
2097
2098 public static function numCalendars()
2099 {
2100 static $numCalendars = null;
2101
2102 if ($numCalendars !== null) {
2103 return $numCalendars;
2104 }
2105
2106 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2107
2108 return $numCalendars;
2109 }
2110
2111 public static function getFirstWeekDay()
2112 {
2113 static $firstWeekDay = null;
2114
2115 if ($firstWeekDay !== null) {
2116 return $firstWeekDay;
2117 }
2118
2119 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2120
2121 return $firstWeekDay;
2122 }
2123
2124 public static function showPartlyReserved()
2125 {
2126 return VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2127 }
2128
2129 public static function showStatusCheckinoutOnly()
2130 {
2131 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2132 }
2133
2134 public static function getDisclaimer($vbo_tn = null)
2135 {
2136 $dbo = JFactory::getDbo();
2137
2138 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2139 $dbo->setQuery($q);
2140 $ft = $dbo->loadAssocList();
2141 if (!$ft) {
2142 return '';
2143 }
2144
2145 if (!is_object($vbo_tn)) {
2146 $vbo_tn = self::getTranslator();
2147 }
2148 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2149
2150 return $ft[0]['setting'];
2151 }
2152
2153 public static function showFooter()
2154 {
2155 return VBOFactory::getConfig()->getBool('showfooter', false);
2156 }
2157
2158 public static function getPriceName($idp, $vbo_tn = null)
2159 {
2160 $dbo = JFactory::getDbo();
2161
2162 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2163 $dbo->setQuery($q, 0, 1);
2164 $n = $dbo->loadAssocList();
2165 if ($n) {
2166 if (is_object($vbo_tn)) {
2167 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2168 }
2169 return $n[0]['name'];
2170 }
2171
2172 return "";
2173 }
2174
2175 public static function getPriceAttr($idp, $vbo_tn = null)
2176 {
2177 $dbo = JFactory::getDbo();
2178
2179 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2180 $dbo->setQuery($q, 0, 1);
2181 $n = $dbo->loadAssocList();
2182 if ($n) {
2183 if (is_object($vbo_tn)) {
2184 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2185 }
2186 return $n[0]['attr'];
2187 }
2188
2189 return "";
2190 }
2191
2192 /**
2193 * Fetches the requested rate plan information.
2194 *
2195 * @param int $idp the rate plan ID.
2196 * @param object $vbo_tn the Vik Booking translator object.
2197 *
2198 * @return array empty array or associative record.
2199 */
2200 public static function getPriceInfo($idp, $vbo_tn = null)
2201 {
2202 // cache values
2203 static $priceInfos = [];
2204
2205 $idp = (int)$idp;
2206
2207 if ($priceInfos && isset($priceInfos[$idp])) {
2208 return $priceInfos[$idp];
2209 }
2210
2211 $dbo = JFactory::getDbo();
2212
2213 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2214 $dbo->setQuery($q, 0, 1);
2215 $rplan = $dbo->loadAssocList();
2216 if (!$rplan) {
2217 return [];
2218 }
2219
2220 if (is_object($vbo_tn)) {
2221 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2222 }
2223
2224 // set cached value
2225 $priceInfos[$idp] = $rplan[0];
2226
2227 return $rplan[0];
2228 }
2229
2230 public static function getAliq($idal)
2231 {
2232 $dbo = JFactory::getDbo();
2233
2234 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2235 $dbo->setQuery($q);
2236 $aliq = $dbo->loadResult();
2237 if ($aliq) {
2238 return $aliq;
2239 }
2240
2241 return 0;
2242 }
2243
2244 public static function getTimeOpenStore($skipsession = false)
2245 {
2246 // cache value in static var
2247 static $getTimeOpenStore = null;
2248
2249 if ($getTimeOpenStore) {
2250 return $getTimeOpenStore;
2251 }
2252 //
2253
2254 $dbo = JFactory::getDbo();
2255 $session = JFactory::getSession();
2256
2257 if ($skipsession) {
2258 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2259 if (empty($setting) && $setting != "0") {
2260 return false;
2261 } else {
2262 $x = explode("-", $setting);
2263 if (!empty($x[1]) && $x[1] != "0") {
2264 $getTimeOpenStore = $x;
2265 return $x;
2266 }
2267 }
2268 } else {
2269 $sval = $session->get('vbgetTimeOpenStore', '');
2270 if (!empty($sval)) {
2271 return $sval;
2272 } else {
2273 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2274 if (empty($setting) && $setting != "0") {
2275 return false;
2276 } else {
2277 $x = explode("-", $setting);
2278 if (!empty($x[1]) && $x[1] != "0") {
2279 $session->set('vbgetTimeOpenStore', $x);
2280 $getTimeOpenStore = $x;
2281 return $x;
2282 }
2283 }
2284 }
2285 }
2286 return false;
2287 }
2288
2289 public static function getHoursMinutes($secs) {
2290 if ($secs >= 3600) {
2291 $op = $secs / 3600;
2292 $hours = floor($op);
2293 $less = $hours * 3600;
2294 $newsec = $secs - $less;
2295 $optwo = $newsec / 60;
2296 $minutes = floor($optwo);
2297 } else {
2298 $hours = "0";
2299 $optwo = $secs / 60;
2300 $minutes = floor($optwo);
2301 }
2302 $x[] = $hours;
2303 $x[] = $minutes;
2304 return $x;
2305 }
2306
2307 public static function getClosingDates()
2308 {
2309 // cache value in static var
2310 static $getClosingDates = null;
2311
2312 if (is_array($getClosingDates)) {
2313 return $getClosingDates;
2314 }
2315
2316 $dbo = JFactory::getDbo();
2317
2318 $allcd = [];
2319
2320 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2321 $dbo->setQuery($q);
2322 $s = $dbo->loadAssocList();
2323 if ($s && !empty($s[0]['setting'])) {
2324 $allcd = json_decode($s[0]['setting'], true);
2325 $allcd = is_array($allcd) ? $allcd : array();
2326 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2327 foreach ($allcd as $k => $v) {
2328 if ($v['to'] < $base_ts) {
2329 unset($allcd[$k]);
2330 }
2331 }
2332 $allcd = array_values($allcd);
2333 }
2334
2335 $getClosingDates = $allcd;
2336
2337 return $allcd;
2338 }
2339
2340 public static function parseJsClosingDates()
2341 {
2342 $cd = self::getClosingDates();
2343 if (!$cd) {
2344 return [];
2345 }
2346
2347 $cdjs = [];
2348 foreach ($cd as $k => $v) {
2349 $cdjs[] = [date('Y-m-d', $v['from']), date('Y-m-d', $v['to'])];
2350 }
2351
2352 return $cdjs;
2353 }
2354
2355 public static function validateClosingDates($checkints, $checkoutts, $df = null)
2356 {
2357 $cd = self::getClosingDates();
2358
2359 if (!$cd) {
2360 return '';
2361 }
2362
2363 $df = empty($df) ? 'Y-m-d' : $df;
2364 $margin_seconds = 22 * 60 * 60;
2365
2366 foreach ($cd as $k => $v) {
2367 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2368 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2369 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2370 if ($inner_closed || $outer_closed || $middle_closed) {
2371 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2372 }
2373 }
2374
2375 return '';
2376 }
2377
2378 /**
2379 * Whether the categories dropdown filter menu should be displayed.
2380 *
2381 * @param boolean $skipsession [optional] re-read the configuration setting.
2382 *
2383 * @return boolean
2384 */
2385 public static function showCategoriesFront($skipsession = true)
2386 {
2387 $dbo = JFactory::getDbo();
2388 if ($skipsession) {
2389 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2390 $dbo->setQuery($q);
2391 $dbo->execute();
2392 if ($dbo->getNumRows() > 0) {
2393 $s = $dbo->loadAssocList();
2394 return (intval($s[0]['setting']) == 1);
2395 }
2396 return false;
2397 }
2398 $session = JFactory::getSession();
2399 $sval = $session->get('vbshowCategoriesFront', '');
2400 if (strlen($sval)) {
2401 return (intval($sval) == 1 ? true : false);
2402 }
2403 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2404 $dbo->setQuery($q);
2405 $dbo->execute();
2406 if ($dbo->getNumRows() > 0) {
2407 $s = $dbo->loadAssocList();
2408 $session->set('vbshowCategoriesFront', $s[0]['setting']);
2409 return (intval($s[0]['setting']) == 1);
2410 }
2411 return false;
2412 }
2413
2414 /**
2415 * Whether the number of children dropdown menu should be displayed.
2416 * Defaults to skip the session values and to re-read the configuration setting.
2417 *
2418 * @param boolean $skipsession [optional] re-read the configuration setting.
2419 *
2420 * @return boolean
2421 */
2422 public static function showChildrenFront($skipsession = true)
2423 {
2424 $dbo = JFactory::getDbo();
2425 if ($skipsession) {
2426 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2427 $dbo->setQuery($q);
2428 $dbo->execute();
2429 if ($dbo->getNumRows() > 0) {
2430 $s = $dbo->loadAssocList();
2431 return (intval($s[0]['setting']) == 1);
2432 }
2433 return false;
2434 }
2435 $session = JFactory::getSession();
2436 $sval = $session->get('vbshowChildrenFront', '');
2437 if (strlen($sval)) {
2438 return (intval($sval) == 1 ? true : false);
2439 }
2440 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2441 $dbo->setQuery($q);
2442 $dbo->execute();
2443 if ($dbo->getNumRows() > 0) {
2444 $s = $dbo->loadAssocList();
2445 $session->set('vbshowChildrenFront', $s[0]['setting']);
2446 return (intval($s[0]['setting']) == 1);
2447 }
2448 return false;
2449 }
2450
2451 public static function allowBooking() {
2452 $dbo = JFactory::getDbo();
2453 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='allowbooking';";
2454 $dbo->setQuery($q);
2455 $dbo->execute();
2456 if ($dbo->getNumRows() > 0) {
2457 $s = $dbo->loadAssocList();
2458 return (intval($s[0]['setting']) == 1 ? true : false);
2459 } else {
2460 return false;
2461 }
2462 }
2463
2464 public static function getDisabledBookingMsg($vbo_tn = null) {
2465 $dbo = JFactory::getDbo();
2466 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2467 $dbo->setQuery($q);
2468 $dbo->execute();
2469 $s = $dbo->loadAssocList();
2470 if (!is_object($vbo_tn)) {
2471 $vbo_tn = self::getTranslator();
2472 }
2473 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2474 return $s[0]['setting'];
2475 }
2476
2477 /**
2478 * Returns the text for the guests allowed policy.
2479 *
2480 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2481 *
2482 * @return string either the raw or the translated policy text.
2483 *
2484 * @since 1.16.5 (WP) - 1.6.5 (WP)
2485 */
2486 public static function getGuestsAllowedPolicy($vbo_tn = null)
2487 {
2488 $dbo = JFactory::getDbo();
2489
2490 $q = $dbo->getQuery(true)
2491 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2492 ->from($dbo->qn('#__vikbooking_texts'))
2493 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2494
2495 $dbo->setQuery($q);
2496 $text_record = $dbo->loadAssocList();
2497
2498 if (!$text_record) {
2499 // missing record, insert it with empty values
2500 if (self::isAdmin()) {
2501 $record = new stdClass;
2502 $record->param = 'guests_allowed_policy';
2503 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2504 $record->setting = '';
2505 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2506 }
2507
2508 return '';
2509 }
2510
2511 if (is_object($vbo_tn)) {
2512 // translate the record only if requested
2513 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2514 }
2515
2516 return $text_record[0]['setting'];
2517 }
2518
2519 public static function getDateFormat($skipsession = true)
2520 {
2521 // cache value in static var
2522 static $getDateFormat = null;
2523
2524 if ($getDateFormat) {
2525 return $getDateFormat;
2526 }
2527
2528 $dbo = JFactory::getDbo();
2529
2530 if ($skipsession) {
2531 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2532 $dbo->setQuery($q, 0, 1);
2533 $getDateFormat = $dbo->loadResult();
2534
2535 return $getDateFormat;
2536 }
2537
2538 $session = JFactory::getSession();
2539 $sval = $session->get('vbgetDateFormat', '');
2540 if (!empty($sval)) {
2541 return $sval;
2542 }
2543
2544 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2545 $dbo->setQuery($q, 0, 1);
2546 $getDateFormat = $dbo->loadResult();
2547
2548 $session->set('vbgetDateFormat', $getDateFormat);
2549
2550 return $getDateFormat;
2551 }
2552
2553 public static function getDateSeparator($skipsession = true)
2554 {
2555 // cache value in static var
2556 static $getDateSeparator = null;
2557
2558 if ($getDateSeparator) {
2559 return $getDateSeparator;
2560 }
2561
2562 $dbo = JFactory::getDbo();
2563
2564 if ($skipsession) {
2565 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2566 $dbo->setQuery($q, 0, 1);
2567 $val = $dbo->loadResult();
2568
2569 $getDateSeparator = empty($val) ? "/" : $val;
2570
2571 return $getDateSeparator;
2572 }
2573
2574 $session = JFactory::getSession();
2575 $sval = $session->get('vbgetDateSep', '');
2576 if (!empty($sval)) {
2577 return $sval;
2578 }
2579
2580 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2581 $dbo->setQuery($q, 0, 1);
2582 $val = $dbo->loadResult();
2583
2584 $getDateSeparator = empty($val) ? "/" : $val;
2585
2586 $session->set('vbgetDateSep', $getDateSeparator);
2587
2588 return $getDateSeparator;
2589 }
2590
2591 public static function getHoursMoreRb($skipsession = false)
2592 {
2593 // cache value in static var
2594 static $getHoursMoreRb = null;
2595
2596 if ($getHoursMoreRb !== null) {
2597 return $getHoursMoreRb;
2598 }
2599
2600 $dbo = JFactory::getDbo();
2601
2602 if ($skipsession) {
2603 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2604 $dbo->setQuery($q, 0, 1);
2605 $getHoursMoreRb = (int)$dbo->loadResult();
2606
2607 return $getHoursMoreRb;
2608 }
2609
2610 $session = JFactory::getSession();
2611 $sval = $session->get('getHoursMoreRb', '');
2612 if (strlen($sval)) {
2613 return $sval;
2614 }
2615
2616 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2617 $dbo->setQuery($q, 0, 1);
2618 $getHoursMoreRb = (int)$dbo->loadResult();
2619
2620 $session->set('getHoursMoreRb', $getHoursMoreRb);
2621
2622 return $getHoursMoreRb;
2623 }
2624
2625 public static function getHoursRoomAvail()
2626 {
2627 $dbo = JFactory::getDbo();
2628
2629 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2630 $dbo->setQuery($q, 0, 1);
2631
2632 return (int)$dbo->loadResult();
2633 }
2634
2635 public static function getFrontTitle($vbo_tn = null) {
2636 // cache value in static var
2637 static $getFrontTitle = null;
2638
2639 if ($getFrontTitle) {
2640 return $getFrontTitle;
2641 }
2642 //
2643
2644 $dbo = JFactory::getDbo();
2645 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2646 $dbo->setQuery($q);
2647 $dbo->execute();
2648 $ft = $dbo->loadAssocList();
2649 if (!is_object($vbo_tn)) {
2650 $vbo_tn = self::getTranslator();
2651 }
2652 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2653
2654 $getFrontTitle = $ft[0]['setting'];
2655
2656 return $ft[0]['setting'];
2657 }
2658
2659 public static function getFrontTitleTag()
2660 {
2661 return VBOFactory::getConfig()->get('fronttitletag', '');
2662 }
2663
2664 public static function getFrontTitleTagClass()
2665 {
2666 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2667 }
2668
2669 public static function getCurrencyName()
2670 {
2671 static $currencyName = null;
2672
2673 if ($currencyName !== null) {
2674 return $currencyName;
2675 }
2676
2677 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2678
2679 return $currencyName;
2680 }
2681
2682 public static function getCurrencySymb($skipsession = false) {
2683 // cache value in static var
2684 static $getCurrencySymb = null;
2685
2686 if ($getCurrencySymb) {
2687 return $getCurrencySymb;
2688 }
2689
2690 $dbo = JFactory::getDbo();
2691
2692 if ($skipsession) {
2693 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2694 $dbo->setQuery($q, 0, 1);
2695 $csymb = $dbo->loadResult();
2696
2697 $getCurrencySymb = $csymb;
2698
2699 return $csymb;
2700 }
2701
2702 $session = JFactory::getSession();
2703 $sval = $session->get('vbgetCurrencySymb', '');
2704 if (!empty($sval)) {
2705 return $sval;
2706 }
2707
2708 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2709 $dbo->setQuery($q, 0, 1);
2710 $csymb = $dbo->loadResult();
2711
2712 // update session value, cache it and return it
2713 $session->set('vbgetCurrencySymb', $csymb);
2714 $getCurrencySymb = $csymb;
2715
2716 return $csymb;
2717 }
2718
2719 public static function getNumberFormatData($skipsession = false) {
2720 // cache value in static var
2721 static $getNumberFormatData = null;
2722
2723 if ($getNumberFormatData) {
2724 return $getNumberFormatData;
2725 }
2726 //
2727
2728 $dbo = JFactory::getDbo();
2729
2730 if ($skipsession) {
2731 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2732 $dbo->setQuery($q, 0, 1);
2733 $dbo->execute();
2734 $numfdata = $dbo->loadResult();
2735
2736 // cache value and return it
2737 $getNumberFormatData = $numfdata;
2738
2739 return $numfdata;
2740 }
2741
2742 $session = JFactory::getSession();
2743 $sval = $session->get('getNumberFormatData', '');
2744 if (!empty($sval)) {
2745 return $sval;
2746 }
2747
2748 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2749 $dbo->setQuery($q, 0, 1);
2750 $dbo->execute();
2751 $numfdata = $dbo->loadResult();
2752
2753 // update session value, cache it and return it
2754 $session->set('getNumberFormatData', $numfdata);
2755 $getNumberFormatData = $numfdata;
2756
2757 return $numfdata;
2758 }
2759
2760 /**
2761 * It is possible to hide the decimals if they are like "N.00".
2762 *
2763 * @return int 0 if disabled, 1 if enabled (default).
2764 *
2765 * @since 1.14 (J) - 1.4.0 (WP)
2766 */
2767 public static function hideEmptyDecimals()
2768 {
2769 // cache value in static var
2770 static $hideEmptyDecimals = null;
2771
2772 if ($hideEmptyDecimals !== null) {
2773 return $hideEmptyDecimals;
2774 }
2775 //
2776
2777 $dbo = JFactory::getDbo();
2778
2779 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='noemptydecimals';";
2780 $dbo->setQuery($q);
2781 $hideEmptyDecimals = $dbo->loadResult();
2782 if ($hideEmptyDecimals !== null) {
2783 $hideEmptyDecimals = (int)$hideEmptyDecimals;
2784 return $hideEmptyDecimals;
2785 }
2786
2787 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('noemptydecimals', '1');";
2788 $dbo->setQuery($q);
2789 $dbo->execute();
2790
2791 $hideEmptyDecimals = 1;
2792
2793 return $hideEmptyDecimals;
2794 }
2795
2796 public static function numberFormat($num, $skipsession = false)
2797 {
2798 $formatvals = self::getNumberFormatData($skipsession);
2799 $formatparts = explode(':', $formatvals);
2800
2801 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
2802 // number has got no decimals
2803 $formatparts[0] = 0;
2804 }
2805
2806 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
2807 }
2808
2809 /**
2810 * Given a date timestamp, returns the formatted date.
2811 *
2812 * @param int|string $ts the date representation in Unix timestamp.
2813 * @param bool $wtime if true, a formatted date-time will be returned.
2814 *
2815 * @return string the formatted date according to settings.
2816 *
2817 * @since 1.16.0 (J) - 1.6.0 (WP)
2818 */
2819 public static function formatDateTs($ts, $wtime = false)
2820 {
2821 $nowdf = self::getDateFormat();
2822 if ($nowdf == "%d/%m/%Y") {
2823 $df = 'd/m/Y';
2824 } elseif ($nowdf == "%m/%d/%Y") {
2825 $df = 'm/d/Y';
2826 } else {
2827 $df = 'Y/m/d';
2828 }
2829 $datesep = self::getDateSeparator();
2830
2831 $time_f = $wtime ? ' H:i' : '';
2832
2833 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
2834 }
2835
2836 /**
2837 * Given a country name, 3-char or 2-char code, this method attempts
2838 * to guess the best language to assign to the booking according to
2839 * what languages are installed on the website. This way, cron jobs
2840 * and any other email notification will be correctly and automatically
2841 * sent to the guest in the proper language without any manual action.
2842 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
2843 *
2844 * @param string $country the country name, 3-char or 2-char code.
2845 * @param string $locale optional locale lang tag of the guest.
2846 *
2847 * @return mixed the best lang-tag string to use or null.
2848 *
2849 * @since 1.16.0 (J) - 1.6.0 (WP)
2850 */
2851 public static function guessBookingLangFromCountry($country, $locale = null)
2852 {
2853 if (empty($country)) {
2854 return null;
2855 }
2856
2857 // get all the available languages
2858 $known_langs = self::getVboApplication()->getKnownLanguages();
2859 if (!is_array($known_langs) || !count($known_langs)) {
2860 return null;
2861 }
2862
2863 // check if the booking included a supported "locale" for the guest
2864 if (!empty($locale)) {
2865 // make the locale compatible with the language tags format
2866 $locale = str_replace('_', '-', $locale);
2867 foreach ($known_langs as $ltag => $ldet) {
2868 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
2869 // we support this language tag, so we just use it
2870 return $ltag;
2871 }
2872 }
2873 }
2874
2875 // build similarities with country-languages
2876 $similarities = array(
2877 'AU' => 'en',
2878 'GB' => 'en',
2879 'IE' => 'en',
2880 'NZ' => 'en',
2881 'US' => 'en',
2882 'CA' => array(
2883 'en',
2884 'fr',
2885 ),
2886 'CL' => 'es',
2887 'AR' => 'es',
2888 'PE' => 'es',
2889 'MX' => 'es',
2890 'CR' => 'es',
2891 'CO' => 'es',
2892 'EC' => 'es',
2893 'BO' => 'es',
2894 'CU' => 'es',
2895 'VE' => 'es',
2896 'BE' => 'fr',
2897 'LU' => 'fr',
2898 'CH' => array(
2899 'de',
2900 'it',
2901 'fr',
2902 ),
2903 'AT' => 'de',
2904 'GR' => 'el',
2905 'GL' => 'dk',
2906 );
2907
2908 // fetch values from db
2909 $dbo = JFactory::getDbo();
2910 $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);
2911 $dbo->setQuery($q, 0, 1);
2912 $dbo->execute();
2913 if (!$dbo->getNumRows()) {
2914 return null;
2915 }
2916 $country_record = $dbo->loadAssoc();
2917
2918 // assign country name/code versions
2919 $country_name = $country_record['country_name'];
2920 $country_3char = strtoupper($country_record['country_3_code']);
2921 $country_2char = strtoupper($country_record['country_2_code']);
2922
2923 // build an associative array of language tags and related match-score
2924 $langtags_score = array();
2925 foreach ($known_langs as $ltag => $ldet) {
2926 // default language tag score is 0 for no matches
2927 $langtags_score[$ltag] = 0;
2928 // get language and country codes
2929 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
2930
2931 // check matches with the installed language details
2932 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
2933 // increase language tag score
2934 $langtags_score[$ltag]++;
2935 }
2936 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
2937 // increase language tag score
2938 $langtags_score[$ltag]++;
2939 }
2940 if (!empty($ldet['locale'])) {
2941 // sanitize locale for matching the 2-char code safely
2942 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
2943 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
2944 // increase language tag score
2945 $langtags_score[$ltag]++;
2946 }
2947 }
2948 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
2949 // increase language tag score
2950 $langtags_score[$ltag]++;
2951 }
2952 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
2953 // increase language tag score
2954 $langtags_score[$ltag]++;
2955 }
2956
2957 // check language similarities between countries
2958 if (isset($similarities[$country_2char])) {
2959 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
2960 // check if language tag(s) is available for this spoken language
2961 foreach ($spoken_tags as $spoken_tag) {
2962 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
2963 // increase language tag score
2964 $langtags_score[$ltag]++;
2965 }
2966 }
2967 }
2968 }
2969
2970 // make sure at least one language tag has got some points
2971 if (max($langtags_score) === 0) {
2972 // no languages installed to honor this country
2973 return null;
2974 }
2975
2976 // sort language tag scores
2977 arsort($langtags_score);
2978
2979 // reset array pointer to the first (highest) element
2980 reset($langtags_score);
2981
2982 // return the language tag with the highest score
2983 return key($langtags_score);
2984 }
2985
2986 public static function getCurrencyCodePp() {
2987 $dbo = JFactory::getDbo();
2988 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencycodepp';";
2989 $dbo->setQuery($q);
2990 $dbo->execute();
2991 $ft = $dbo->loadAssocList();
2992 return $ft[0]['setting'];
2993 }
2994
2995 public static function getIntroMain($vbo_tn = null) {
2996 $dbo = JFactory::getDbo();
2997 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
2998 $dbo->setQuery($q);
2999 $dbo->execute();
3000 $ft = $dbo->loadAssocList();
3001 if (!is_object($vbo_tn)) {
3002 $vbo_tn = self::getTranslator();
3003 }
3004 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3005 return $ft[0]['setting'];
3006 }
3007
3008 public static function getClosingMain($vbo_tn = null) {
3009 $dbo = JFactory::getDbo();
3010 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
3011 $dbo->setQuery($q);
3012 $dbo->execute();
3013 $ft = $dbo->loadAssocList();
3014 if (!is_object($vbo_tn)) {
3015 $vbo_tn = self::getTranslator();
3016 }
3017 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3018 return $ft[0]['setting'];
3019 }
3020
3021 public static function getFullFrontTitle($vbo_tn = null) {
3022 $dbo = JFactory::getDbo();
3023 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
3024 $dbo->setQuery($q);
3025 $dbo->execute();
3026 $ft = $dbo->loadAssocList();
3027 if (!is_object($vbo_tn)) {
3028 $vbo_tn = self::getTranslator();
3029 }
3030 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
3031 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
3032 $dbo->setQuery($q);
3033 $dbo->execute();
3034 $fttag = $dbo->loadAssocList();
3035 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
3036 $dbo->setQuery($q);
3037 $dbo->execute();
3038 $fttagclass = $dbo->loadAssocList();
3039 if (empty($ft[0]['setting'])) {
3040 return "";
3041 } else {
3042 if (empty($fttag[0]['setting'])) {
3043 return $ft[0]['setting'] . "<br/>\n";
3044 } else {
3045 $tag = str_replace("<", "", $fttag[0]['setting']);
3046 $tag = str_replace(">", "", $tag);
3047 $tag = str_replace("/", "", $tag);
3048 $tag = trim($tag);
3049 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
3050 }
3051 }
3052 }
3053
3054 public static function dateIsValid($date) {
3055 $df = self::getDateFormat();
3056 $datesep = self::getDateSeparator();
3057 if (strlen($date) != 10) {
3058 return false;
3059 }
3060 $cur_dsep = "/";
3061 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3062 $cur_dsep = $datesep;
3063 }
3064 $x = explode($cur_dsep, $date);
3065 if ($df == "%d/%m/%Y") {
3066 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3067 return false;
3068 }
3069 } elseif ($df == "%m/%d/%Y") {
3070 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3071 return false;
3072 }
3073 } else {
3074 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3075 return false;
3076 }
3077 }
3078 return true;
3079 }
3080
3081 public static function sayDateFormat() {
3082 $dbo = JFactory::getDbo();
3083 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3084 $dbo->setQuery($q);
3085 $dbo->execute();
3086 $s = $dbo->loadAssocList();
3087 if ($s[0]['setting'] == "%d/%m/%Y") {
3088 return JText::translate('VBCONFIGONETWELVE');
3089 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3090 return JText::translate('VBCONFIGONEMDY');
3091 } else {
3092 return JText::translate('VBCONFIGONETENTHREE');
3093 }
3094 }
3095
3096 /**
3097 * Calculates the Unix timestamp from the given date and
3098 * time. Avoids DST issues thanks to mktime. With prior releases,
3099 * DST issues may occur due to the sum of seconds.
3100 *
3101 * @param string $date the date string formatted with the current settings
3102 * @param int $h hours from 0 to 23 for check-in/check-out
3103 * @param int $m minutes from 0 to 59 for check-in/check-out
3104 * @param int $s seconds from 0 to 59 for check-in/check-out
3105 *
3106 * @return int the Unix timestamp of the date
3107 *
3108 * @since 1.0.14
3109 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3110 * signature modified for $h and $m that now take 0 as default value.
3111 */
3112 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3113 $df = self::getDateFormat();
3114 $datesep = self::getDateSeparator();
3115 $cur_dsep = "/";
3116 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3117 $cur_dsep = $datesep;
3118 }
3119 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3120 // date is in Y-m-d format (with no time)
3121 $cur_dsep = '-';
3122 $df = "%Y/%m/%d";
3123 }
3124 $x = explode($cur_dsep, $date);
3125 if (!(count($x) > 2)) {
3126 return 0;
3127 }
3128 if ($df == "%d/%m/%Y") {
3129 $month = (int)$x[1];
3130 $mday = (int)$x[0];
3131 $year = (int)$x[2];
3132 } elseif ($df == "%m/%d/%Y") {
3133 $month = (int)$x[0];
3134 $mday = (int)$x[1];
3135 $year = (int)$x[2];
3136 } else {
3137 $month = (int)$x[1];
3138 $mday = (int)$x[2];
3139 $year = (int)$x[0];
3140 }
3141 $h = empty($h) ? 0 : (int)$h;
3142 $m = empty($m) ? 0 : (int)$m;
3143 $s = $s > 0 && $s <= 59 ? $s : 0;
3144
3145 return mktime($h, $m, $s, $month, $mday, $year);
3146 }
3147
3148 public static function ivaInclusa($skipsession = false) {
3149 // cache value in static var
3150 static $getTaxIncluded = null;
3151
3152 if ($getTaxIncluded !== null) {
3153 return (bool)$getTaxIncluded;
3154 }
3155
3156 $dbo = JFactory::getDbo();
3157
3158 if ($skipsession) {
3159 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3160 $dbo->setQuery($q, 0, 1);
3161 $dbo->execute();
3162 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3163
3164 // cache value and return it
3165 $getTaxIncluded = $vat_incl_data;
3166
3167 return (bool)$vat_incl_data;
3168 }
3169
3170 $session = JFactory::getSession();
3171 $sval = $session->get('getTaxIncluded', '');
3172 if (strlen($sval)) {
3173 return (bool)$sval;
3174 }
3175
3176 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3177 $dbo->setQuery($q, 0, 1);
3178 $dbo->execute();
3179 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3180
3181 // update session value, cache it and return it
3182 $session->set('getTaxIncluded', $vat_incl_data);
3183 $getTaxIncluded = $vat_incl_data;
3184
3185 return (bool)$vat_incl_data;
3186 }
3187
3188 public static function showTaxOnSummaryOnly($skipsession = false) {
3189 if ($skipsession) {
3190 $dbo = JFactory::getDbo();
3191 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3192 $dbo->setQuery($q);
3193 $dbo->execute();
3194 $s = $dbo->loadAssocList();
3195 return (intval($s[0]['setting']) == 1 ? true : false);
3196 } else {
3197 $session = JFactory::getSession();
3198 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3199 if (strlen($sval) > 0) {
3200 return (intval($sval) == 1 ? true : false);
3201 } else {
3202 $dbo = JFactory::getDbo();
3203 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3204 $dbo->setQuery($q);
3205 $dbo->execute();
3206 $s = $dbo->loadAssocList();
3207 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3208 return (intval($s[0]['setting']) == 1 ? true : false);
3209 }
3210 }
3211 }
3212
3213 public static function tokenForm() {
3214 $dbo = JFactory::getDbo();
3215 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3216 $dbo->setQuery($q);
3217 $dbo->execute();
3218 $s = $dbo->loadAssocList();
3219 return (intval($s[0]['setting']) == 1 ? true : false);
3220 }
3221
3222 public static function getPaypalAcc() {
3223 $dbo = JFactory::getDbo();
3224 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3225 $dbo->setQuery($q);
3226 $dbo->execute();
3227 $s = $dbo->loadAssocList();
3228 return $s[0]['setting'];
3229 }
3230
3231 public static function getAccPerCent() {
3232 $dbo = JFactory::getDbo();
3233 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3234 $dbo->setQuery($q);
3235 $dbo->execute();
3236 $s = $dbo->loadAssocList();
3237 return $s[0]['setting'];
3238 }
3239
3240 public static function getTypeDeposit($skipsession = false) {
3241 if ($skipsession) {
3242 $dbo = JFactory::getDbo();
3243 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3244 $dbo->setQuery($q);
3245 $dbo->execute();
3246 $s = $dbo->loadAssocList();
3247 return $s[0]['setting'];
3248 } else {
3249 $session = JFactory::getSession();
3250 $sval = $session->get('getTypeDeposit', '');
3251 if (strlen($sval) > 0) {
3252 return $sval;
3253 } else {
3254 $dbo = JFactory::getDbo();
3255 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3256 $dbo->setQuery($q);
3257 $dbo->execute();
3258 $s = $dbo->loadAssocList();
3259 $session->set('getTypeDeposit', $s[0]['setting']);
3260 return $s[0]['setting'];
3261 }
3262 }
3263 }
3264
3265 public static function multiplePayments()
3266 {
3267 return VBOFactory::getConfig()->getBool('multipay', false);
3268 }
3269
3270 public static function getAdminMail()
3271 {
3272 return VBOFactory::getConfig()->getString('adminemail', '');
3273 }
3274
3275 public static function getSenderMail()
3276 {
3277 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3278
3279 return empty($sender) ? self::getAdminMail() : $sender;
3280 }
3281
3282 public static function getPaymentName($vbo_tn = null) {
3283 $dbo = JFactory::getDbo();
3284 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3285 $dbo->setQuery($q);
3286 $dbo->execute();
3287 $s = $dbo->loadAssocList();
3288 if (!is_object($vbo_tn)) {
3289 $vbo_tn = self::getTranslator();
3290 }
3291 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3292 return $s[0]['setting'];
3293 }
3294
3295 public static function getTermsConditions($vbo_tn = null) {
3296 //VBO 1.10
3297 $dbo = JFactory::getDbo();
3298 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3299 $dbo->setQuery($q);
3300 $dbo->execute();
3301 if ($dbo->getNumRows() > 0) {
3302 $s = $dbo->loadAssocList();
3303 if (!is_object($vbo_tn)) {
3304 $vbo_tn = self::getTranslator();
3305 }
3306 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3307 } else {
3308 //the record has never been saved. Compose it with the default lang definition
3309 $timeopst = self::getTimeOpenStore(true);
3310 if (is_array($timeopst)) {
3311 $openat = self::getHoursMinutes($timeopst[0]);
3312 $closeat = self::getHoursMinutes($timeopst[1]);
3313 } else {
3314 $openat = array(12, 0);
3315 $closeat = array(10, 0);
3316 }
3317 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3318 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3319 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3320 }
3321
3322 return $s[0]['setting'];
3323 }
3324
3325 public static function getMinutesLock($conv = false)
3326 {
3327 static $minutesLock = null;
3328
3329 if ($minutesLock === null) {
3330 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3331 }
3332
3333 if (!$minutesLock) {
3334 return 0;
3335 }
3336
3337 if ($conv) {
3338 return (time() + ($minutesLock * 60));
3339 }
3340
3341 return $minutesLock;
3342 }
3343
3344 /**
3345 * Checks if the room units are temporarily locked on the given dates.
3346 *
3347 * @param int $idroom the room ID.
3348 * @param int $units the total number of room units.
3349 * @param int $first the check-in timestamp.
3350 * @param int $second the check-out timestamp.
3351 * @param bool $occupied if true, the system will also consider the booked units.
3352 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3353 *
3354 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3355 *
3356 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3357 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3358 */
3359 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3360 {
3361 $dbo = JFactory::getDbo();
3362
3363 if ($units < 1) {
3364 return false;
3365 }
3366
3367 $actnow = time();
3368
3369 // clean up expired records
3370 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3371 $dbo->setQuery($q);
3372 $dbo->execute();
3373
3374 $secdiff = $second - $first;
3375 $daysdiff = $secdiff / 86400;
3376 if (is_int($daysdiff)) {
3377 if ($daysdiff < 1) {
3378 $daysdiff = 1;
3379 }
3380 } else {
3381 if ($daysdiff < 1) {
3382 $daysdiff = 1;
3383 } else {
3384 $sum = floor($daysdiff) * 86400;
3385 $newdiff = $secdiff - $sum;
3386 $maxhmore = self::getHoursMoreRb() * 3600;
3387 if ($maxhmore >= $newdiff) {
3388 $daysdiff = floor($daysdiff);
3389 } else {
3390 $daysdiff = ceil($daysdiff);
3391 }
3392 }
3393 }
3394 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3395 $tmplock_map = [];
3396
3397 // check temporarily locked records
3398 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3399 $dbo->setQuery($check);
3400 $busy = $dbo->loadAssocList();
3401 if ($busy) {
3402 foreach ($groupdays as $kg => $gday) {
3403 $bfound = 0;
3404 foreach ($busy as $bu) {
3405 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3406 $bfound++;
3407 }
3408 }
3409 $tmplock_map[$kg] = $bfound;
3410 if ($bfound >= $units) {
3411 return false;
3412 }
3413 }
3414 }
3415
3416 if ($occupied) {
3417 // check also the busy records and some the occupied units to the temporarily locked ones
3418 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3419 $dbo->setQuery($check);
3420 $busy = $dbo->loadAssocList();
3421 if (!$busy) {
3422 return true;
3423 }
3424
3425 foreach ($groupdays as $kg => $gday) {
3426 $bfound = 0;
3427 foreach ($busy as $bu) {
3428 if (in_array($bu['id'], $skip_busy_ids)) {
3429 // Booking modification
3430 continue;
3431 }
3432 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3433 $bfound++;
3434 }
3435 }
3436 // sum units occupied to the units temporarily locked
3437 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3438 if ($bfound >= $units) {
3439 return false;
3440 }
3441 }
3442 }
3443
3444 return true;
3445 }
3446
3447 public static function getGroupDays($first, $second, $daysdiff) {
3448 $ret = array();
3449 $ret[] = $first;
3450 if ($daysdiff > 1) {
3451 $start = getdate($first);
3452 $end = getdate($second);
3453 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3454 for($i = 1; $i < $daysdiff; $i++) {
3455 $checkday = $start['mday'] + $i;
3456 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3457 if ($dayts != $endcheck) {
3458 $ret[] = $dayts;
3459 }
3460 }
3461 }
3462 $ret[] = $second;
3463 return $ret;
3464 }
3465
3466 /**
3467 * Counts the hours of difference between the current
3468 * server time and the selected check-in date and time.
3469 *
3470 * @param int $checkin_ts
3471 * @param [int] $now_ts
3472 *
3473 * @return int
3474 */
3475 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3476 $hoursdiff = 0;
3477
3478 if (empty($now_ts)) {
3479 $now_ts = time();
3480 }
3481
3482 if ($now_ts >= $checkin_ts) {
3483 return $hoursdiff;
3484 }
3485
3486 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3487
3488 return $hoursdiff;
3489 }
3490
3491 /**
3492 * Loads all the occupied records for the given rooms and offset.
3493 *
3494 * @param array $roomids the optional list of room IDs to filter.
3495 * @param int $from_ts the optional base date to use.
3496 * @param int $max_ts the optional max date to use.
3497 * @param bool $closures whether to ignore, include or exclude closures.
3498 *
3499 * @return array
3500 *
3501 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3502 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3503 */
3504 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3505 {
3506 $base_ts = empty($from_ts) ? time() : $from_ts;
3507 $busy = [];
3508 $clauses = [];
3509
3510 if (is_array($roomids) && count($roomids)) {
3511 // filter busy records only by the requested room ids
3512 $roomids = array_map(function($rid) {
3513 return (int)$rid;
3514 }, $roomids);
3515 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3516 }
3517
3518 // exclude past reservations
3519 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3520
3521 if (!empty($max_ts)) {
3522 // exclude future reservations (recommended for large datasets)
3523 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3524 }
3525
3526 if (is_bool($closures)) {
3527 // include or exclude closures
3528 if ($closures) {
3529 $clauses[] = "`o`.`closure` = 1";
3530 } else {
3531 $clauses[] = "`o`.`closure` != 1";
3532 }
3533 }
3534
3535 $dbo = JFactory::getDbo();
3536
3537 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3538 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3539 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3540 WHERE " . implode(' AND ', $clauses) . ";";
3541 $dbo->setQuery($q);
3542 $allbusy = $dbo->loadAssocList();
3543 if ($allbusy) {
3544 foreach ($allbusy as $br) {
3545 if (!isset($busy[$br['idroom']])) {
3546 $busy[$br['idroom']] = [];
3547 }
3548 $busy[$br['idroom']][] = $br;
3549 }
3550 }
3551
3552 return $busy;
3553 }
3554
3555 /**
3556 * Loads all the busy records by excluding the closures.
3557 * Built for the page dashboard to calculate the actual
3558 * rooms occupancy by excluding the rooms closed.
3559 *
3560 * @param array $roomids the optional list of room IDs to filter.
3561 * @param int $from_ts the optional base date to use.
3562 * @param int $max_ts the optional max date to use.
3563 *
3564 * @return array the list of busy records.
3565 *
3566 * @since 1.12
3567 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3568 */
3569 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3570 {
3571 if (!is_array($roomids) || !count($roomids)) {
3572 return [];
3573 }
3574
3575 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3576 }
3577
3578 public static function loadBookingBusyIds($idorder) {
3579 $busy = array();
3580 if (empty($idorder)) {
3581 return $busy;
3582 }
3583 $dbo = JFactory::getDbo();
3584 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$idorder.";";
3585 $dbo->setQuery($q);
3586 $allbusy = $dbo->loadAssocList();
3587 if ($allbusy) {
3588 foreach ($allbusy as $b) {
3589 array_push($busy, $b['idbusy']);
3590 }
3591 }
3592 return $busy;
3593 }
3594
3595 public static function loadLockedRecords($roomids, $ts = 0) {
3596 $dbo = JFactory::getDbo();
3597 $actnow = empty($ts) ? time() : $ts;
3598 $locked = array();
3599 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3600 $dbo->setQuery($q);
3601 $dbo->execute();
3602 if (!is_array($roomids) || !(count($roomids) > 0)) {
3603 return $locked;
3604 }
3605 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3606 $dbo->setQuery($check);
3607 $all_locked = $dbo->loadAssocList();
3608 if ($all_locked) {
3609 foreach ($all_locked as $kb => $br) {
3610 $locked[$br['idroom']][$kb] = $br;
3611 }
3612 }
3613 return $locked;
3614 }
3615
3616 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3617 $bookings = [];
3618 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3619 return $bookings;
3620 }
3621 $dbo = JFactory::getDbo();
3622 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3623 $dbo->setQuery($q);
3624 $all_booking_ids = $dbo->loadAssocList();
3625 if ($all_booking_ids) {
3626 $oids = array();
3627 foreach ($all_booking_ids as $bid) {
3628 $oids[] = $bid['idorder'];
3629 }
3630 $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` ".
3631 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3632 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3633 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3634 "ORDER BY `o`.`checkout` ASC;";
3635 $dbo->setQuery($q);
3636 $bookings = $dbo->loadAssocList();
3637 if (!$bookings) {
3638 $bookings = [];
3639 }
3640 }
3641 return $bookings;
3642 }
3643
3644 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3645 {
3646 $dbo = JFactory::getDbo();
3647
3648 if ($units < 1) {
3649 return false;
3650 }
3651
3652 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img'], $no_cache = true);
3653 if (!$room_info) {
3654 return false;
3655 }
3656
3657 $secdiff = $second - $first;
3658 $daysdiff = $secdiff / 86400;
3659 if (is_int($daysdiff)) {
3660 if ($daysdiff < 1) {
3661 $daysdiff = 1;
3662 }
3663 } else {
3664 if ($daysdiff < 1) {
3665 $daysdiff = 1;
3666 } else {
3667 $sum = floor($daysdiff) * 86400;
3668 $newdiff = $secdiff - $sum;
3669 $maxhmore = self::getHoursMoreRb() * 3600;
3670 if ($maxhmore >= $newdiff) {
3671 $daysdiff = floor($daysdiff);
3672 } else {
3673 $daysdiff = ceil($daysdiff);
3674 }
3675 }
3676 }
3677
3678 $q = $dbo->getQuery(true)
3679 ->select($dbo->qn([
3680 'id',
3681 'checkin',
3682 'realback',
3683 ]))
3684 ->from($dbo->qn('#__vikbooking_busy'))
3685 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
3686 ->where($dbo->qn('realback') . ' >= ' . $first);
3687
3688 $dbo->setQuery($q);
3689 $busy = $dbo->loadAssocList();
3690 if (!$busy) {
3691 return ($units <= ($room_info['units'] ?? 1));
3692 }
3693
3694 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3695 foreach ($groupdays as $gday) {
3696 $bfound = 0;
3697 foreach ($busy as $bu) {
3698 if (in_array($bu['id'], $skip_busy_ids)) {
3699 // booking modification
3700 continue;
3701 }
3702 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3703 $bfound++;
3704 }
3705 }
3706 if ($bfound >= $units) {
3707 return false;
3708 }
3709 }
3710
3711 return ($units <= ($room_info['units'] ?? 1));
3712 }
3713
3714 public static function payTotal()
3715 {
3716 return VBOFactory::getConfig()->getBool('paytotal', true);
3717 }
3718
3719 public static function getDepositIfDays()
3720 {
3721 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
3722 }
3723
3724 public static function depositAllowedDaysAdv($checkints) {
3725 $days_adv = self::getDepositIfDays();
3726 if (!($days_adv > 0) || !($checkints > 0)) {
3727 return true;
3728 }
3729 $now_info = getdate();
3730 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
3731 return $maxts > $checkints ? false : true;
3732 }
3733
3734 public static function depositCustomerChoice() {
3735 $dbo = JFactory::getDbo();
3736 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
3737 $dbo->setQuery($q);
3738 $dbo->execute();
3739 $s = $dbo->loadAssocList();
3740 return (intval($s[0]['setting']) == 1 ? true : false);
3741 }
3742
3743 public static function getDepositOverrides($getjson = false) {
3744 $dbo = JFactory::getDbo();
3745 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
3746 $dbo->setQuery($q);
3747 $dbo->execute();
3748 if ($dbo->getNumRows() > 0) {
3749 $s = $dbo->loadResult();
3750 return $getjson ? $s : json_decode($s, true);
3751 }
3752 //count of this array will be at least 1 to store the "more" property
3753 $def_arr = array('more' => '');
3754 $def_val = json_encode($def_arr);
3755 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
3756 $dbo->setQuery($q);
3757 $dbo->execute();
3758 return $getjson ? $def_val : $def_arr;
3759 }
3760
3761 public static function calcDepositOverride($amount_deposit, $nights) {
3762 $overrides = self::getDepositOverrides();
3763 $nights = intval($nights);
3764 $andmore = intval($overrides['more']);
3765 if (!(count($overrides) > 1)) {
3766 //no overrides
3767 return $amount_deposit;
3768 }
3769 foreach ($overrides as $k => $v) {
3770 if ((int)$k == $nights && strlen($v) > 0) {
3771 //exact override found
3772 return (float)$v;
3773 }
3774 }
3775 if ($andmore > 0 && $andmore <= $nights) {
3776 foreach ($overrides as $k => $v) {
3777 if ((int)$k == $andmore && strlen($v) > 0) {
3778 //"and more" nights found
3779 return (float)$v;
3780 }
3781 }
3782 }
3783 //nothing was found
3784 return $amount_deposit;
3785 }
3786
3787 public static function noDepositForNonRefund() {
3788 $dbo = JFactory::getDbo();
3789 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
3790 $dbo->setQuery($q);
3791 $dbo->execute();
3792 if ($dbo->getNumRows() > 0) {
3793 $s = $dbo->loadResult();
3794 return ((int)$s == 1);
3795 }
3796 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
3797 $dbo->setQuery($q);
3798 $dbo->execute();
3799 //default to false
3800 return false;
3801 }
3802
3803 /**
3804 * This method returns the room-rate array with the lowest price
3805 * that matches the preferred rate plan parameter (if available).
3806 * The array $website_rates could not be an array, or it could be
3807 * an array with the error string (response from the TACVBO Class).
3808 * The method has been introduced in VBO 1.10 and it's mainly used
3809 * by the module mod_vikbooking_channelrates and its ajax requests.
3810 *
3811 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
3812 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
3813 *
3814 * @return array
3815 */
3816 public static function getBestRoomRate($website_rates, $def_rplan) {
3817 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
3818 return array();
3819 }
3820 $best_room_rate = array();
3821 foreach ($website_rates as $rid => $tars) {
3822 foreach ($tars as $tar) {
3823 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
3824 //the array $website_rates is already sorted by price ASC, so we take the first useful array
3825 $best_room_rate = $tar;
3826 break 2;
3827 }
3828 }
3829 }
3830 if (!(count($best_room_rate) > 0)) {
3831 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
3832 foreach ($website_rates as $rid => $tars) {
3833 foreach ($tars as $tar) {
3834 $best_room_rate = $tar;
3835 break 2;
3836 }
3837 }
3838 }
3839
3840 return $best_room_rate;
3841 }
3842
3843 /**
3844 * This method returns an array with the details
3845 * of all channels in VCM that supports AV requests,
3846 * and that have at least one room type mapped.
3847 * The method invokes the Logos Class to return details
3848 * about the name and logo URL of the channel.
3849 * The method has been introduced in VBO 1.10 and it's mainly used
3850 * by the module mod_vikbooking_channelrates and its ajax requests.
3851 *
3852 * @param array $channels an array of channel IDs to be mapped on the VCM relations
3853 *
3854 * @return array
3855 */
3856 public static function getChannelsMap($channels) {
3857 if (!is_array($channels) || !(count($channels))) {
3858 return array();
3859 }
3860 $vcm_logos = self::getVcmChannelsLogo('', true);
3861 if (!is_object($vcm_logos)) {
3862 return array();
3863 }
3864 $channels_ids = array();
3865 foreach ($channels as $chid) {
3866 $ichid = (int)$chid;
3867 if ($ichid < 1) {
3868 continue;
3869 }
3870 array_push($channels_ids, $ichid);
3871 }
3872 if (!(count($channels_ids))) {
3873 return array();
3874 }
3875 $dbo = JFactory::getDbo();
3876 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
3877 $dbo->setQuery($q);
3878 $dbo->execute();
3879 if ($dbo->getNumRows() < 1) {
3880 return array();
3881 }
3882 $channels_names = $dbo->loadAssocList();
3883 $channels_map = array();
3884 foreach ($channels_names as $ch) {
3885 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
3886 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
3887 $chdata = array(
3888 'id' => $ch['idchannel'],
3889 'name' => ucwords($ch['channel']),
3890 'logo' => $ota_logo_url
3891 );
3892 array_push($channels_map, $chdata);
3893 }
3894 return $channels_map;
3895 }
3896
3897 /**
3898 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
3899 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
3900 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
3901 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
3902 * module/widget and its ajax requests.
3903 *
3904 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
3905 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
3906 *
3907 * @return mixed string (empty or indicating the alteration), or associative array per channel.
3908 *
3909 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
3910 */
3911 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
3912 {
3913 $otas_rates_val = '';
3914 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
3915 return $otas_rates_val;
3916 }
3917 if (!class_exists('VikChannelManager')) {
3918 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
3919 }
3920 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
3921 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
3922 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
3923 // the Bulk Rates Cache contains data for this room type and rate plan
3924 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
3925 // build rates modification string
3926 if ((int)$data_cont['rmod'] > 0) {
3927 // rates were modified for the OTAs, check how
3928 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
3929 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
3930 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
3931 // check alterations per channel, if requested
3932 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
3933 // we compose an associative array with the alteration for each channel (0 = generic rule)
3934 $channel_alterations = array($otas_rates_val);
3935 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
3936 if ((int)$ch_mods['rmod'] > 0) {
3937 // custom rate alteration for this channel
3938 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
3939 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
3940 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
3941 } else {
3942 // no rates alteration for this channel identifier
3943 $ch_alter_string = '+0%';
3944 }
3945 $channel_alterations[$ch_id] = $ch_alter_string;
3946 }
3947 // overwrite return value with associative array
3948 $otas_rates_val = $channel_alterations;
3949 }
3950 }
3951 }
3952 }
3953
3954 return $otas_rates_val;
3955 }
3956
3957 /**
3958 * This method checks if some non-refundable rates were selected
3959 * (`free_cancellation`=0), the only argument is an array of tariffs.
3960 * The property 'idprice' must be defined on each sub-array.
3961 *
3962 * @param $tars array
3963 *
3964 * @return boolean
3965 **/
3966 public static function findNonRefundableRates($tars = [])
3967 {
3968 $id_prices = [];
3969
3970 foreach ($tars as $tar) {
3971 if (isset($tar['idprice'])) {
3972 if (!in_array($tar['idprice'], $id_prices)) {
3973 array_push($id_prices, (int)$tar['idprice']);
3974 }
3975 continue;
3976 }
3977 foreach ($tar as $t) {
3978 if (isset($t['idprice'])) {
3979 if (!in_array($t['idprice'], $id_prices)) {
3980 array_push($id_prices, (int)$t['idprice']);
3981 }
3982 }
3983 }
3984 }
3985
3986 if (!count($id_prices)) {
3987 // no price IDs found (probably a package)
3988 return false;
3989 }
3990
3991 $dbo = JFactory::getDbo();
3992
3993 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
3994 $dbo->setQuery($q);
3995 $dbo->execute();
3996
3997 return (bool)($dbo->getNumRows() > 0);
3998 }
3999
4000 /**
4001 * This method checks if the deposit is allowed depending on
4002 * the selected rate plans (idprice) for the rooms reserved.
4003 * If the configuration setting is enabled, and if some
4004 * non-refundable rates were selected (`free_cancellation`=0),
4005 * the method will return false, because the deposit is not allowed.
4006 * The only argument is an array of tariffs. The property 'idprice'
4007 * must be defined on each sub-array (multi-dimension supported)
4008 * throgh the method findNonRefundableRates();
4009 *
4010 * @param $tars array
4011 *
4012 * @return boolean
4013 **/
4014 public static function allowDepositFromRates($tars) {
4015 if (!self::noDepositForNonRefund()) {
4016 //deposit can be paid also if non-refundable rates
4017 return true;
4018 }
4019 return !self::findNonRefundableRates($tars);
4020 }
4021
4022 public static function showSearchSuggestions() {
4023 $dbo = JFactory::getDbo();
4024 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
4025 $dbo->setQuery($q);
4026 $dbo->execute();
4027 if ($dbo->getNumRows() > 0) {
4028 return (int)$dbo->loadResult();
4029 }
4030 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
4031 $dbo->setQuery($q);
4032 $dbo->execute();
4033 return 1;
4034 }
4035
4036 /**
4037 * Given a code, returns the coupon information array.
4038 *
4039 * @param string $code the coupon code.
4040 *
4041 * @return array empty array or coupon record.
4042 *
4043 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
4044 */
4045 public static function getCouponInfo($code)
4046 {
4047 $dbo = JFactory::getDbo();
4048
4049 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
4050 $dbo->setQuery($q, 0, 1);
4051 $coupon = $dbo->loadAssoc();
4052 if (!$coupon) {
4053 return [];
4054 }
4055
4056 // check for customers assigned
4057 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4058 $dbo->setQuery($q);
4059 $coupon_customers = $dbo->loadAssocList();
4060 if ($coupon_customers) {
4061 // set customers and automatic properties
4062 $coupon['customers'] = [];
4063 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4064 foreach ($coupon_customers as $coupon_customer) {
4065 // push customer ID
4066 $coupon['customers'][] = $coupon_customer['idcustomer'];
4067 }
4068 }
4069
4070 return $coupon;
4071 }
4072
4073 /**
4074 * Reads the details of a given room record ID.
4075 *
4076 * @param int $idroom The room ID to fetch.
4077 * @param array $columns The optional room columns to fetch.
4078 * @param bool $no_cache Whether to ignore a previously cached record.
4079 *
4080 * @return array
4081 *
4082 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $no_cache.
4083 */
4084 public static function getRoomInfo($idroom, $columns = [], $no_cache = false)
4085 {
4086 static $room_infos = [];
4087
4088 if (isset($room_infos[$idroom]) && !$no_cache) {
4089 return $room_infos[$idroom];
4090 }
4091
4092 $dbo = JFactory::getDbo();
4093
4094 if ($columns) {
4095 $columns = array_map([$dbo, 'qn'], $columns);
4096 }
4097
4098 $q = $dbo->getQuery(true)
4099 ->select($columns ? $columns : '*')
4100 ->from($dbo->qn('#__vikbooking_rooms'))
4101 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4102
4103 $dbo->setQuery($q);
4104 $room = $dbo->loadAssoc();
4105
4106 if ($no_cache) {
4107 return $room ?: [];
4108 }
4109
4110 $room_infos[$idroom] = $room ?: [];
4111
4112 return $room_infos[$idroom];
4113 }
4114
4115 public static function loadOrdersRoomsData($idorder)
4116 {
4117 $dbo = JFactory::getDbo();
4118
4119 $q = $dbo->getQuery(true)
4120 ->select($dbo->qn('or') . '.*')
4121 ->select($dbo->qn('r.name', 'room_name'))
4122 ->select($dbo->qn('r.params'))
4123 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4124 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4125 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4126 ->order($dbo->qn('or.id') . ' ASC');
4127
4128 $dbo->setQuery($q);
4129
4130 return $dbo->loadAssocList();
4131 }
4132
4133 public static function sayCategory($ids, $vbo_tn = null)
4134 {
4135 $dbo = JFactory::getDbo();
4136 $split = explode(";", $ids);
4137 $say = "";
4138 foreach ($split as $k => $s) {
4139 if (!strlen($s)) {
4140 continue;
4141 }
4142 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4143 $dbo->setQuery($q);
4144 $nam = $dbo->loadAssocList();
4145 if (!$nam) {
4146 continue;
4147 }
4148 if (is_object($vbo_tn)) {
4149 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4150 }
4151 $say .= $nam[0]['name'];
4152 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4153 }
4154 return $say;
4155 }
4156
4157 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4158 {
4159 $dbo = JFactory::getDbo();
4160 $split = explode(";", $idc);
4161 $carat = "";
4162 $arr = array();
4163 $where = array();
4164 foreach ($split as $s) {
4165 if (!empty($s)) {
4166 $where[] = $s;
4167 }
4168 }
4169 if ($where) {
4170 $q = "SELECT `c`.* FROM `#__vikbooking_characteristics` AS `c` WHERE `c`.`id` IN (" . implode(",", $where) . ") ORDER BY `c`.`ordering` ASC;";
4171 $dbo->setQuery($q);
4172 $arr = $dbo->loadAssocList();
4173 if ($arr && is_object($vbo_tn)) {
4174 $vbo_tn->translateContents($arr, '#__vikbooking_characteristics');
4175 }
4176 }
4177 if ($arr) {
4178 $carat .= "<div class=\"vbo-room-carats\">\n";
4179 foreach ($arr as $a) {
4180 if (!empty($a['textimg'])) {
4181 //tooltip icon text is not empty
4182 if (!empty($a['icon'])) {
4183 //an icon has been uploaded: display the image
4184 $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";
4185 } else {
4186 if (strpos($a['textimg'], '</i>') !== false || strpos($a['textimg'], '<svg') !== false) {
4187 //the tooltip icon text is a font-icon or an SVG field, we can use the name as tooltip
4188 $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";
4189 } else {
4190 //display just the text
4191 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4192 }
4193 }
4194 } else {
4195 $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");
4196 }
4197 }
4198 $carat .= "</div>\n";
4199 }
4200 return $carat;
4201 }
4202
4203 public static function getRoomOptionals($idopts, $vbo_tn = null)
4204 {
4205 $dbo = JFactory::getDbo();
4206 $split = explode(";", $idopts);
4207 $fetch = array();
4208 foreach ($split as $s) {
4209 if (!empty($s)) {
4210 $fetch[] = $s;
4211 }
4212 }
4213 if ($fetch) {
4214 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $fetch).") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4215 $dbo->setQuery($q);
4216 $arr = $dbo->loadAssocList();
4217 if ($arr) {
4218 if (is_object($vbo_tn)) {
4219 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4220 }
4221 return $arr;
4222 }
4223 }
4224 return [];
4225 }
4226
4227 public static function getSingleOption($idopt, $vbo_tn = null)
4228 {
4229 $dbo = JFactory::getDbo();
4230 $opt = array();
4231 if (!empty($idopt)) {
4232 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4233 $dbo->setQuery($q);
4234 $opt = $dbo->loadAssoc();
4235 if ($opt && is_object($vbo_tn)) {
4236 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4237 }
4238 }
4239 return $opt;
4240 }
4241
4242 /**
4243 * Unsets the options that are not available in the requested dates.
4244 *
4245 * @param array $optionals the array of options passed by reference
4246 * @param int $checkin the timestamp of the check-in date and time
4247 * @param int $checkout the timestamp of the check-out date and time
4248 *
4249 * @return void
4250 *
4251 * @since 1.11
4252 */
4253 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4254 {
4255 if (!$optionals || empty($checkin) || empty($checkout)) {
4256 return;
4257 }
4258
4259 foreach ($optionals as $k => $v) {
4260 if (!empty($v['alwaysav'])) {
4261 $dates = explode(';', $v['alwaysav']);
4262 if (empty($dates[0]) || empty($dates[1])) {
4263 continue;
4264 }
4265 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4266 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4267 unset($optionals[$k]);
4268 }
4269 }
4270 }
4271 }
4272
4273 /**
4274 * Unsets the options that are not suited for the requested room party.
4275 *
4276 * @param array $optionals the array of options passed by reference.
4277 * @param int $adults the number of adults in the room party.
4278 * @param int $children the number of children in the room party.
4279 *
4280 * @return void
4281 *
4282 * @since 1.13.5
4283 */
4284 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4285 {
4286 if (!$optionals || (empty($adults) && empty($children))) {
4287 return;
4288 }
4289
4290 foreach ($optionals as $k => $v) {
4291 if (empty($v['oparams'])) {
4292 continue;
4293 }
4294 $v['oparams'] = json_decode($v['oparams'], true);
4295 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4296 continue;
4297 }
4298 if (!empty($v['oparams']['minguestsnum'])) {
4299 // filter by minimum adults/guests
4300 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4301 // minimum number of adults not sufficient, unset option
4302 unset($optionals[$k]);
4303 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4304 // minimum number of total guests not sufficient, unset option
4305 unset($optionals[$k]);
4306 }
4307 }
4308 if (!empty($v['oparams']['maxguestsnum'])) {
4309 // filter by maximum adults/guests
4310 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4311 // maximum number of adults exceeded, unset option
4312 unset($optionals[$k]);
4313 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4314 // maximum number of total guests exceeded, unset option
4315 unset($optionals[$k]);
4316 }
4317 }
4318 }
4319 }
4320
4321 /**
4322 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4323 *
4324 * @param array $id_rooms list of room IDs.
4325 * @param int $num_adults number of adults.
4326 * @param int $num_nights number of nights of stay.
4327 * @param int $checkin optional check-in timestamp.
4328 * @param int $checkout optional check-out timestamp.
4329 *
4330 * @return array associative information of city taxes, fees and options.
4331 *
4332 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4333 */
4334 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4335 {
4336 $dbo = JFactory::getDbo();
4337
4338 $taxes = 0;
4339 $fees = 0;
4340
4341 $options_data = [];
4342 $id_options = [];
4343
4344 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4345 $dbo->setQuery($q);
4346 $assocs = $dbo->loadAssocList();
4347 if ($assocs) {
4348 foreach ($assocs as $opts) {
4349 if (!empty($opts['idopt'])) {
4350 $r_ido = explode(';', rtrim($opts['idopt']));
4351 foreach ($r_ido as $ido) {
4352 if (!empty($ido) && !in_array($ido, $id_options)) {
4353 $id_options[] = (int)$ido;
4354 }
4355 }
4356 }
4357 }
4358 }
4359
4360 if ($id_options) {
4361 $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);";
4362 $dbo->setQuery($q);
4363 $alltaxesfees = $dbo->loadAssocList();
4364
4365 if ($checkin && $checkout) {
4366 // filter the records by stay dates to support, for example, seasonal city taxes
4367 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4368 }
4369
4370 foreach ($alltaxesfees as $tf) {
4371 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4372 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4373 $realcost = $tf['maxprice'];
4374 }
4375 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4376 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4377 if ($tf['is_citytax'] == 1) {
4378 $taxes += $realcost;
4379 } elseif ($tf['is_fee'] == 1) {
4380 $fees += $realcost;
4381 }
4382 $optsett = explode('-', $tf['forceval']);
4383 $options_data[] = $tf['id'].':'.$optsett[0];
4384 }
4385 }
4386
4387 return [
4388 'city_taxes' => $taxes,
4389 'fees' => $fees,
4390 'options' => $options_data
4391 ];
4392 }
4393
4394 /**
4395 * From a list of option/extras records, it returns an array with the options/extras
4396 * that do not support age intervals, and options for the children age intervals.
4397 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4398 *
4399 * @param array $optionals the full list of options/extras records.
4400 * @param int $adults the number of adults in the room requested.
4401 * @param int $children the number of children in the room requested.
4402 *
4403 * @return array the filtered regular and children age intervals options.
4404 */
4405 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4406 {
4407 // container for age intervals
4408 $ageintervals = [];
4409
4410 // regular options should start as an array
4411 if (!is_array($optionals)) {
4412 $optionals = [];
4413 }
4414
4415 // check for the first valid age intervals option
4416 foreach ($optionals as $kopt => $opt) {
4417 if (!empty($opt['ageintervals'])) {
4418 $intervals = explode(';;', $opt['ageintervals']);
4419 foreach ($intervals as $intv) {
4420 if (empty($intv)) {
4421 continue;
4422 }
4423 $parts = explode('_', $intv);
4424 if (count($parts) >= 3) {
4425 // age intervals option found, get the first and only one
4426 $ageintervals = $optionals[$kopt];
4427 break 2;
4428 }
4429 }
4430 }
4431 }
4432
4433 if ($ageintervals) {
4434 /**
4435 * We allow price adjustments for a minimum total guests by overriding the costs
4436 * for each age interval for specific child numbers when the party covers some children.
4437 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4438 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4439 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4440 *
4441 * @since 1.13.5
4442 */
4443 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4444 $oparams = !is_array($oparams) ? array() : $oparams;
4445 $adults = (int)$adults;
4446 $children = (int)$children;
4447 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4448 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4449 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4450 if ($children_to_pay < $children) {
4451 // compose string for free age intervals
4452 $intervals = explode(';;', $ageintervals['ageintervals']);
4453 foreach ($intervals as $k => $v) {
4454 $parts = explode('_', $v);
4455 // set cost to 0 (key = 2)
4456 $parts[2] = '0';
4457 $intervals[$k] = implode('_', $parts);
4458 }
4459 $free_ageintervals = implode(';;', $intervals);
4460 for ($i = 1; $i <= $children; $i++) {
4461 if (($children - $i + 1) <= $children_to_pay) {
4462 // costs for this Nth last child should be regular as this child should pay
4463 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4464 } else {
4465 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4466 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4467 }
4468 }
4469 }
4470 }
4471
4472 // remove age intervals from regular options
4473 foreach ($optionals as $kopt => $opt) {
4474 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4475 // unset the option of type age intervals from the regular options
4476 unset($optionals[$kopt]);
4477 }
4478 }
4479 }
4480
4481 // return the filtered list of regular options and age intervals options
4482 return [$optionals, $ageintervals];
4483 }
4484
4485 /**
4486 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4487 * This is because some children in the party may not need to pay anything due to the min guests.
4488 *
4489 * @param array $optional the option full record to parse.
4490 * @param int $adults the number of adults in the room requested.
4491 * @param int $children the number of children in the room requested.
4492 *
4493 * @return array associative array with default age intervals and override strings.
4494 *
4495 * @since 1.13.5
4496 */
4497 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4498 {
4499 if (!is_array($optional)) {
4500 $optional = array();
4501 }
4502
4503 $overrides = array(
4504 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4505 );
4506
4507 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4508 $oparams = !is_array($oparams) ? array() : $oparams;
4509 $adults = (int)$adults;
4510 $children = (int)$children;
4511
4512 if (!count($oparams)) {
4513 return $overrides;
4514 }
4515
4516 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4517 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4518 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4519 if ($children_to_pay < $children) {
4520 // compose string for free age intervals
4521 $intervals = explode(';;', $optional['ageintervals']);
4522 foreach ($intervals as $k => $v) {
4523 $parts = explode('_', $v);
4524 // set cost to 0 (key = 2)
4525 $parts[2] = '0';
4526 $intervals[$k] = implode('_', $parts);
4527 }
4528 $free_ageintervals = implode(';;', $intervals);
4529 for ($i = 1; $i <= $children; $i++) {
4530 if (($children - $i + 1) <= $children_to_pay) {
4531 // costs for this Nth last child should be regular as this child should pay
4532 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4533 } else {
4534 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4535 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4536 }
4537 }
4538 }
4539 }
4540
4541 return $overrides;
4542 }
4543
4544 /**
4545 * Returns the number/index of the children being parsed given the full list of room options string,
4546 * the current key in the array of the split room options string, the ID of the ageintervals option,
4547 * and the total number of children. This is useful for later applying the cost overrides for the
4548 * children ages through the method getOptionIntervalChildOverrides().
4549 *
4550 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4551 * @param int $optid the ID of the option of type age intervals to check.
4552 * @param int $roptkey the current position in the loop of the room options string.
4553 * @param int $children the number of children in the room requested.
4554 *
4555 * @return int the position/number of the children being parsed, -1 if not found.
4556 *
4557 * @since 1.13.5
4558 */
4559 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4560 {
4561 // default to -1 for not found
4562 $child_num = -1;
4563
4564 $valid_opt_counter = 0;
4565
4566 $roptions = explode(";", $roptstr);
4567 foreach ($roptions as $k => $opt) {
4568 if (empty($opt)) {
4569 continue;
4570 }
4571 $optvals = explode(":", $opt);
4572 /**
4573 * In some cases, like the "saveorder" task, the room option string may contain
4574 * the room number beside the option ID, separated with an underscore.
4575 * If an underscore is present, we need to split it to find the option ID.
4576 */
4577 if (strpos($optvals[0], '_') !== false) {
4578 // underscore found in room number portion, extract the option ID
4579 $rn_parts = explode('_', $optvals[0]);
4580 // 0th element is the room number in the party, 1st elem is the option ID
4581 $optvals[0] = $rn_parts[1];
4582 }
4583 //
4584 if ((int)$optvals[0] != (int)$optid) {
4585 // we are not interested into this option ID
4586 continue;
4587 }
4588 // increase counter for this option ID
4589 $valid_opt_counter++;
4590
4591 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4592 // children position found, we need to return it as a 0th base
4593 $child_num = ($valid_opt_counter - 1);
4594 break;
4595 }
4596 }
4597
4598 return $child_num;
4599 }
4600
4601 public static function getOptionIntervalsCosts($intvstr) {
4602 $optcosts = array();
4603 $intervals = explode(';;', $intvstr);
4604 foreach ($intervals as $kintv => $intv) {
4605 if (empty($intv)) continue;
4606 $parts = explode('_', $intv);
4607 if (count($parts) >= 3) {
4608 $optcosts[$kintv] = (float)$parts[2];
4609 }
4610 }
4611 return $optcosts;
4612 }
4613
4614 public static function getOptionIntervalsAges($intvstr) {
4615 $optages = array();
4616 $intervals = explode(';;', $intvstr);
4617 foreach ($intervals as $kintv => $intv) {
4618 if (empty($intv)) continue;
4619 $parts = explode('_', $intv);
4620 if (count($parts) >= 3) {
4621 $optages[$kintv] = $parts[0].' - '.$parts[1];
4622 }
4623 }
4624 return $optages;
4625 }
4626
4627 public static function getOptionIntervalsPercentage($intvstr) {
4628 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4629 $optcostspcent = array();
4630 $intervals = explode(';;', $intvstr);
4631 foreach ($intervals as $kintv => $intv) {
4632 if (empty($intv)) continue;
4633 $parts = explode('_', $intv);
4634 if (count($parts) >= 3) {
4635 //fixed amount
4636 $setval = 0;
4637 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
4638 //percentage value of the room base cost (VBO 1.10)
4639 $setval = 2;
4640 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
4641 //percentage value of the adults tariff
4642 $setval = 1;
4643 }
4644 $optcostspcent[$kintv] = $setval;
4645 }
4646 }
4647 return $optcostspcent;
4648 }
4649
4650 public static function dayValidTs($days, $first, $second)
4651 {
4652 $secdiff = $second - $first;
4653 $daysdiff = $secdiff / 86400;
4654 if (is_int($daysdiff)) {
4655 if ($daysdiff < 1) {
4656 $daysdiff = 1;
4657 }
4658 } else {
4659 if ($daysdiff < 1) {
4660 $daysdiff = 1;
4661 } else {
4662 $sum = floor($daysdiff) * 86400;
4663 $newdiff = $secdiff - $sum;
4664 $maxhmore = self::getHoursMoreRb() * 3600;
4665 if ($maxhmore >= $newdiff) {
4666 $daysdiff = floor($daysdiff);
4667 } else {
4668 $daysdiff = ceil($daysdiff);
4669 }
4670 }
4671 }
4672
4673 return ($daysdiff == $days);
4674 }
4675
4676 /**
4677 * Calculates the stay fee inclusive of taxes.
4678 *
4679 * @param float $cost the cost to evaluate.
4680 * @param int $idprice the rate plan ID associated.
4681 *
4682 * @return float the given cost after tax, if taxes had to be applied.
4683 */
4684 public static function sayCostPlusIva($cost, $idprice)
4685 {
4686 $ivainclusa = self::ivaInclusa();
4687 if ($ivainclusa) {
4688 return $cost;
4689 }
4690
4691 static $tax_rplans_map = [];
4692
4693 if (array_key_exists($idprice, $tax_rplans_map)) {
4694 $tax_rate = $tax_rplans_map[$idprice];
4695 } else {
4696 $dbo = JFactory::getDbo();
4697 $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 . ";";
4698 $dbo->setQuery($q);
4699 $tax_rate = $dbo->loadAssoc();
4700
4701 // cache value
4702 $tax_rplans_map[$idprice] = $tax_rate;
4703 }
4704
4705 if (!$tax_rate || empty($tax_rate['aliq'])) {
4706 return $cost;
4707 }
4708
4709 $subt = 100 + $tax_rate['aliq'];
4710 $op = ($cost * $subt / 100);
4711
4712 /**
4713 * Tax Cap implementation for prices tax excluded (most common).
4714 *
4715 * @since 1.12
4716 */
4717 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4718 $op = ($cost + $tax_rate['taxcap']);
4719 }
4720
4721 // apply rounding to avoid issues with multiple tax rates when tax excluded
4722 $formatvals = self::getNumberFormatData();
4723 $formatparts = explode(':', $formatvals);
4724 $rounded_op = round($op, (int)$formatparts[0]);
4725 if ($rounded_op > $op) {
4726 return $rounded_op;
4727 }
4728
4729 /**
4730 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4731 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4732 *
4733 * @since 1.11.1
4734 */
4735 return round($op, (int)$formatparts[0]);
4736 }
4737
4738 /**
4739 * Calculates the stay fee exclusive of taxes.
4740 *
4741 * @param float $cost the cost to evaluate.
4742 * @param int $idprice the rate plan ID associated.
4743 *
4744 * @return float the given cost before tax.
4745 */
4746 public static function sayCostMinusIva($cost, $idprice)
4747 {
4748 $ivainclusa = self::ivaInclusa();
4749 if (!$ivainclusa) {
4750 return $cost;
4751 }
4752
4753 static $tax_rplans_map = [];
4754
4755 if (array_key_exists($idprice, $tax_rplans_map)) {
4756 $tax_rate = $tax_rplans_map[$idprice];
4757 } else {
4758 $dbo = JFactory::getDbo();
4759 $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 . ";";
4760 $dbo->setQuery($q);
4761 $tax_rate = $dbo->loadAssoc();
4762
4763 // cache value
4764 $tax_rplans_map[$idprice] = $tax_rate;
4765 }
4766
4767 if (!$tax_rate || empty($tax_rate['aliq'])) {
4768 return $cost;
4769 }
4770
4771 $subt = 100 + $tax_rate['aliq'];
4772 $op = ($cost * 100 / $subt);
4773
4774 /**
4775 * Tax Cap implementation also when prices tax included.
4776 *
4777 * @since 1.12
4778 */
4779 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4780 $op = ($cost - $tax_rate['taxcap']);
4781 }
4782
4783 // apply rounding to avoid issues with multiple tax rates when tax included
4784 $formatvals = self::getNumberFormatData();
4785 $formatparts = explode(':', $formatvals);
4786 $rounded_op = round($op, (int)$formatparts[0]);
4787 if ($rounded_op < $op) {
4788 return $rounded_op;
4789 }
4790
4791 /**
4792 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4793 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4794 *
4795 * @since 1.11.1
4796 */
4797 return round($op, (int)$formatparts[0]);
4798 }
4799
4800 /**
4801 * Given an option/extra cost, determines whether taxes should be applied over it.
4802 * Can also be used to calculate taxes on the extra costs per room in the bookings
4803 *
4804 * @param float $cost the cost to evaluate, option or extra service fee.
4805 * @param int $idiva the ID of the tax rate to use to apply taxes.
4806 * @param bool $force if true, taxes will always be applied.
4807 *
4808 * @return float the given option/extra cost plus tax, if taxes had to be applied.
4809 *
4810 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4811 */
4812 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
4813 {
4814 $ivainclusa = self::ivaInclusa();
4815 if ($ivainclusa && $force !== true) {
4816 return $cost;
4817 }
4818
4819 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4820 if (!$tax_rate) {
4821 return $cost;
4822 }
4823
4824 $subt = 100 + $tax_rate['aliq'];
4825 $op = ($cost * $subt / 100);
4826
4827 /**
4828 * Tax Cap implementation for prices tax excluded (most common).
4829 *
4830 * @since 1.12
4831 */
4832 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4833 $op = ($cost + $tax_rate['taxcap']);
4834 }
4835
4836 // apply rounding to avoid issues with multiple tax rates when tax excluded
4837 $formatvals = self::getNumberFormatData();
4838 $formatparts = explode(':', $formatvals);
4839 $rounded_op = round($op, (int)$formatparts[0]);
4840 if ($rounded_op > $op) {
4841 return $rounded_op;
4842 }
4843
4844 return $op;
4845 }
4846
4847 /**
4848 * Calculates the net amount for an option/extra service.
4849 *
4850 * @param float $cost the cost to evaluate, option or extra service fee.
4851 * @param int $idiva the ID of the tax rate to use to apply taxes.
4852 *
4853 * @return float the given option/extra cost before tax, if taxes were inclusive.
4854 */
4855 public static function sayOptionalsMinusIva($cost, $idiva)
4856 {
4857 if (empty($idiva)) {
4858 return $cost;
4859 }
4860
4861 $ivainclusa = self::ivaInclusa();
4862 if (!$ivainclusa) {
4863 return $cost;
4864 }
4865
4866 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4867 if (!$tax_rate) {
4868 return $cost;
4869 }
4870
4871 $subt = 100 + $tax_rate['aliq'];
4872 $op = ($cost * 100 / $subt);
4873
4874 /**
4875 * Tax Cap implementation also when prices tax included.
4876 *
4877 * @since 1.12
4878 */
4879 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4880 $op = ($cost - $tax_rate['taxcap']);
4881 }
4882
4883 // apply rounding to avoid issues with multiple tax rates when tax included
4884 $formatvals = self::getNumberFormatData();
4885 $formatparts = explode(':', $formatvals);
4886 $rounded_op = round($op, (int)$formatparts[0]);
4887 if ($rounded_op < $op) {
4888 return $rounded_op;
4889 }
4890
4891 return $op;
4892 }
4893
4894 /**
4895 * Given a cost, determines whether taxes should be applied over it.
4896 *
4897 * @param float $cost the cost to evaluate, could be a room custom price.
4898 * @param int $idiva the ID of the tax rate to use to apply taxes.
4899 * @param bool $force if true, taxes will always be applied.
4900 *
4901 * @return float the given cost plus tax, if taxes had to be applied.
4902 *
4903 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4904 */
4905 public static function sayPackagePlusIva($cost, $idiva, $force = false)
4906 {
4907 $ivainclusa = self::ivaInclusa();
4908 if ($ivainclusa && $force !== true) {
4909 return $cost;
4910 }
4911
4912 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4913 if (!$tax_rate) {
4914 return $cost;
4915 }
4916
4917 $subt = 100 + $tax_rate['aliq'];
4918 $op = ($cost * $subt / 100);
4919
4920 /**
4921 * Tax Cap implementation for prices tax excluded (most common).
4922 *
4923 * @since 1.12
4924 */
4925 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4926 $op = ($cost + $tax_rate['taxcap']);
4927 }
4928
4929 // apply rounding to avoid issues with multiple tax rates when tax excluded
4930 $formatvals = self::getNumberFormatData();
4931 $formatparts = explode(':', $formatvals);
4932 $rounded_op = round($op, (int)$formatparts[0]);
4933 if ($rounded_op > $op) {
4934 return $rounded_op;
4935 }
4936
4937 return $op;
4938 }
4939
4940 /**
4941 * Calculates the net amount for a package.
4942 *
4943 * @param float $cost the cost to evaluate, option or extra service fee.
4944 * @param int $idiva the ID of the tax rate to use to apply taxes.
4945 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
4946 *
4947 * @return float the given cost before tax.
4948 */
4949 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
4950 {
4951 $ivainclusa = self::ivaInclusa();
4952 if (!$ivainclusa && $force_invoice_excltax !== true) {
4953 return $cost;
4954 }
4955
4956 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4957 if (!$tax_rate) {
4958 return $cost;
4959 }
4960
4961 $subt = 100 + $tax_rate['aliq'];
4962 $op = ($cost * 100 / $subt);
4963
4964 /**
4965 * Tax Cap implementation also when prices tax included.
4966 *
4967 * @since 1.12
4968 */
4969 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4970 $op = ($cost - $tax_rate['taxcap']);
4971 }
4972
4973 // apply rounding to avoid issues with multiple tax rates when tax included
4974 $formatvals = self::getNumberFormatData();
4975 $formatparts = explode(':', $formatvals);
4976 $rounded_op = round($op, (int)$formatparts[0]);
4977 if ($rounded_op < $op) {
4978 return $rounded_op;
4979 }
4980
4981 return $op;
4982 }
4983
4984 public static function getSecretLink()
4985 {
4986 $dbo = JFactory::getDbo();
4987
4988 $sid = mt_rand();
4989
4990 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
4991 $dbo->setQuery($q);
4992 $all = $dbo->loadAssocList();
4993 if ($all) {
4994 $list = [];
4995 foreach ($all as $s) {
4996 $list[] = $s['sid'];
4997 }
4998 if (in_array($sid, $list)) {
4999 while(in_array($sid, $list)) {
5000 $sid++;
5001 }
5002 }
5003 }
5004
5005 return $sid;
5006 }
5007
5008 /**
5009 * Generates a confirmation number for the given booking.
5010 * Modified the way random string-numbers are obtained to
5011 * avoid any possible brute-force attempt.
5012 *
5013 * @since 1.15.1 (J) - 1.5.4 (WP)
5014 */
5015 public static function generateConfirmNumber($oid, $update = true)
5016 {
5017 $dbo = JFactory::getDbo();
5018
5019 $confirmnumb = date('y') . $oid;
5020 for ($i = 0; $i < 8; $i++) {
5021 $confirmnumb .= rand(0, 9);
5022 }
5023
5024 if ($update) {
5025 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
5026 $dbo->setQuery($q);
5027 $dbo->execute();
5028 }
5029
5030 return $confirmnumb;
5031 }
5032
5033 public static function buildCustData($arr, $sep) {
5034 $cdata = "";
5035 foreach ($arr as $k => $e) {
5036 if (strlen($e)) {
5037 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
5038 }
5039 }
5040 return $cdata;
5041 }
5042
5043 /**
5044 * This method parses the Joomla menu object to see if a menu item of a
5045 * specific type is available, to get its ID. Useful when links should be
5046 * displayed in pages where there is no Itemid set (booking details pages).
5047 *
5048 * @param array $viewtypes list of accepted menu items
5049 * @param string $lang the optional language to use.
5050 *
5051 * @return int
5052 *
5053 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
5054 */
5055 public static function findProperItemIdType($viewtypes, $lang = null)
5056 {
5057 if (VBOPlatformDetection::isWordPress()) {
5058 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5059
5060 $itemid = $model->best($viewtypes, $lang);
5061
5062 if ($itemid) {
5063 return $itemid;
5064 }
5065
5066 return 0;
5067 }
5068
5069 $bestitemid = 0;
5070
5071 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5072
5073 $app = JFactory::getApplication();
5074
5075 $menu = $app->getMenu('site');
5076
5077 if (!$menu) {
5078 return 0;
5079 }
5080
5081 $menu_items = $menu->getMenu();
5082
5083 if (!$menu_items) {
5084 return 0;
5085 }
5086
5087 foreach ($menu_items as $itemid => $item) {
5088 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5089 // proper menu item type found
5090 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5091
5092 if (isset($item->language) && $item->language == $current_lang) {
5093 // we found the exact menu item type for the given language
5094 return $itemid;
5095 }
5096 }
5097 }
5098
5099 return $bestitemid;
5100 }
5101
5102 /**
5103 * Rewrites an internal URI that needs to be used outside of the website.
5104 * This means that the routed URI MUST start with the base path of the site.
5105 *
5106 * @param mixed $query The query string or an associative array of data.
5107 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5108 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5109 *
5110 * @return string The complete routed URI.
5111 *
5112 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5113 * routing from back-end (if available on the CMS version).
5114 */
5115 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5116 {
5117 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5118 }
5119
5120 /**
5121 * Generates an iCal file to be attached to the email message for the
5122 * customer or the administrator with some basic booking details.
5123 *
5124 * @param string $recip either admin or customer.
5125 * @param array $booking the booking array or some keys.
5126 *
5127 * @return mixed string in case of success, false otherwise.
5128 *
5129 * @since 1.12.0
5130 */
5131 public static function getEmailIcal($recip, $booking) {
5132 // load configuration setting
5133 $attachical = self::attachIcal();
5134
5135 if ($attachical === 0) {
5136 // do not attach any iCal file
5137 return false;
5138 }
5139
5140 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5141 // skip the iCal for the admin
5142 return false;
5143 }
5144
5145 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5146 // skip the iCal for the customer
5147 return false;
5148 }
5149
5150 if (strpos($recip, 'admin') !== false) {
5151 // prepare event description and summary for the admin
5152 $description = $booking['custdata'];
5153 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5154 $fname = $booking['ts'] . '.ics';
5155 } else {
5156 // event description and summary for the customer
5157 $description = '';
5158 $summary = self::getFrontTitle();
5159 $fname = 'reservation_reminder.ics';
5160 }
5161
5162 // prepare iCal head
5163 $company_name = self::getFrontTitle();
5164 $ics_str = "BEGIN:VCALENDAR\r\n" .
5165 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5166 "CALSCALE:GREGORIAN\r\n" .
5167 "VERSION:2.0\r\n";
5168 // compose iCal body
5169 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5170 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5171 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5172 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5173 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5174 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5175 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5176 $ics_str .= 'END:VEVENT'."\r\n";
5177 // close iCal file content
5178 $ics_str .= "END:VCALENDAR";
5179
5180 /**
5181 * Trigger event to allow third party plugins to overwrite the iCal file.
5182 *
5183 * @since 1.16.0 (J) - 1.6.0 (WP)
5184 */
5185 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5186
5187 if (empty($ics_str)) {
5188 return false;
5189 }
5190
5191 // store the event onto a .ics file. We use the resources folder in back-end.
5192 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5193 $fp = fopen($fpath, 'w+');
5194 $bytes = fwrite($fp, $ics_str);
5195 fclose($fp);
5196
5197 return $bytes ? $fpath : false;
5198 }
5199
5200 public static function loadEmailTemplate($booking_info = array()) {
5201 define('_VIKBOOKINGEXEC', '1');
5202 ob_start();
5203 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5204 $content = ob_get_contents();
5205 ob_end_clean();
5206 return $content;
5207 }
5208
5209 /**
5210 * Parses the raw HTML content of the booking email template.
5211 *
5212 * @param string $tmpl the raw content of the template.
5213 * @param mixed $bid int for the booking ID or booking array.
5214 * @param array $rooms list of rooms booked and translated.
5215 * @param array $rates list of translated rates for the booked rooms.
5216 * @param array $options list of translated options booked.
5217 * @param float [$total] the booking total amount (in case it has changed).
5218 * @param string [$link] the booking link can be passed for the no-deposit.
5219 *
5220 * @return string the HTML content of the parsed email template.
5221 *
5222 * @since 1.13 with different arguments.
5223 */
5224 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5225 {
5226 $app = JFactory::getApplication();
5227 $dbo = JFactory::getDbo();
5228 $vbo_tn = self::getTranslator();
5229
5230 // availability helper
5231 $av_helper = self::getAvailabilityInstance();
5232
5233 // get necessary values
5234 if (is_array($bid)) {
5235 // we got the full booking record
5236 $order_info = $bid;
5237 $bid = $order_info['id'];
5238 } else {
5239 $order_info = array();
5240 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5241 $dbo->setQuery($q);
5242 $order_info = $dbo->loadAssoc();
5243 if (!$order_info) {
5244 throw new Exception('Booking not found', 404);
5245 }
5246 }
5247
5248 $tars_info = array();
5249 $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'] . ";";
5250 $dbo->setQuery($q);
5251 $tars_info = $dbo->loadAssocList();
5252 if (!$tars_info) {
5253 throw new Exception('No rooms found', 404);
5254 }
5255
5256 /**
5257 * Split stay reservation.
5258 *
5259 * @since 1.16.0 (J) - 1.6.0 (WP)
5260 */
5261 $room_stay_dates = [];
5262 if ($order_info['split_stay']) {
5263 if ($order_info['status'] == 'confirmed') {
5264 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5265 } else {
5266 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5267 }
5268 // immediately count the number of nights of stay for each split room
5269 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5270 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5271 // overwrite values for compatibility with non-confirmed bookings
5272 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5273 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5274 }
5275 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5276 // overwrite the whole array
5277 $room_stay_dates[$sps_r_k] = $sps_r_v;
5278 }
5279 }
5280
5281 // check if the language in use is the same as the one used during the checkout
5282 $lang = JFactory::getLanguage();
5283 if (!empty($order_info['lang'])) {
5284 if ($lang->getTag() != $order_info['lang']) {
5285 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5286 if (VBOPlatformDetection::isJoomla()) {
5287 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5288 }
5289 }
5290 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5291 // force the translation to start because contents should be translated
5292 $vbo_tn::$force_tolang = $order_info['lang'];
5293 }
5294 }
5295
5296 // values for replacements
5297 $company_name = self::getFrontTitle();
5298 $currencyname = self::getCurrencyName();
5299 $sitelogo = self::getSiteLogo();
5300 $footermess = self::getFooterOrdMail($vbo_tn);
5301 $dateformat = self::getDateFormat();
5302 $datesep = self::getDateSeparator();
5303 if ($dateformat == "%d/%m/%Y") {
5304 $df = 'd/m/Y';
5305 } elseif ($dateformat == "%m/%d/%Y") {
5306 $df = 'm/d/Y';
5307 } else {
5308 $df = 'Y/m/d';
5309 }
5310 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5311 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5312 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5313 $customer_info = nl2br($order_info['custdata']);
5314 $company_logo = '';
5315 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5316 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5317 }
5318 if ($order_info['status'] == 'cancelled') {
5319 $confirmnumber = '';
5320 $status_str = JText::translate('VBCANCELLED');
5321 } elseif ($order_info['status'] == 'standby') {
5322 $confirmnumber = '';
5323 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5324 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5325 // this is an inquiry reservation
5326 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5327 }
5328 } else {
5329 $confirmnumber = $order_info['confirmnumber'];
5330 $status_str = JText::translate('VBCOMPLETED');
5331 }
5332 // booking total amount
5333 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5334 // booking link
5335 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5336 if (is_null($link)) {
5337 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5338 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5339 }
5340
5341 // raw HTML content
5342 $parsed = $tmpl;
5343
5344 /**
5345 * Trigger event to allow third-party plugins to manipulate the template string.
5346 *
5347 * @since 1.16.10 (J) - 1.6.10 (WP)
5348 */
5349 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseEmailTemplate', [$parsed, $order_info, $rooms]);
5350
5351 /**
5352 * Parse all conditional text rules.
5353 *
5354 * @since 1.14 (J) - 1.4.0 (WP)
5355 */
5356 self::getConditionalRulesInstance()
5357 ->set(array('booking', 'rooms'), array($order_info, $rooms))
5358 ->parseTokens($parsed);
5359
5360 // special tokens (tags) replacement
5361 $parsed = str_replace("{logo}", $company_logo, $parsed);
5362 $parsed = str_replace("{company_name}", $company_name, $parsed);
5363 $parsed = str_replace(['{order_id}', '{booking_id}'], $order_info['id'], $parsed);
5364 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5365 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5366 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5367 $parsed = str_replace("{order_status}", $status_str, $parsed);
5368 $parsed = str_replace("{order_date}", $create_date, $parsed);
5369
5370 // customer record
5371 $cpin = self::getCPinInstance();
5372 $customer_record = $cpin->getCustomerFromBooking($order_info['id']);
5373 $customer_name = '';
5374 $customer_pin = '';
5375 if ($customer_record) {
5376 $customer_name = $customer_record['first_name'] . ' ' . $customer_record['last_name'];
5377 $customer_pin = $customer_record['pin'];
5378 }
5379 $parsed = str_replace("{customer_name}", $customer_name, $parsed);
5380 $parsed = str_replace("{customer_pin}", $customer_pin, $parsed);
5381
5382 // PIN Code
5383 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5384 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5385 if (!empty($customer_pin)) {
5386 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5387 }
5388 }
5389
5390 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5391
5392 // Confirmation Number
5393 if ($confirmnumber) {
5394 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5395 } else {
5396 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5397 }
5398 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5399 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5400
5401 $roomsnum = count($rooms);
5402 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5403 $roomstr = "";
5404 $tot_adults = 0;
5405 $tot_children = 0;
5406 $tot_guests = 0;
5407
5408 // Rooms Distinctive Features
5409 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5410
5411 foreach ($rooms as $num => $r) {
5412 // guests
5413 $tot_adults += (int) $r['adults'];
5414 $tot_children += (int) $r['children'];
5415 $tot_guests += ((int) $r['adults'] + (int) $r['children']);
5416
5417 // room info
5418 $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/>";
5419 // Rooms Distinctive Features
5420 if (is_array($matches[1] ?? []) && $matches[1]) {
5421 $distinctive_features = array();
5422 $rparams = (array)json_decode($r['params'], true);
5423 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'])) {
5424 $distinctive_features = $rparams['features'][$r['roomindex']];
5425 }
5426 $docheck = (count($distinctive_features) > 0);
5427 foreach ($matches[1] as $reqf) {
5428 $feature_found = false;
5429 if ($docheck) {
5430 foreach ($distinctive_features as $dfk => $dfv) {
5431 if (stripos($dfk, $reqf) !== false) {
5432 $feature_found = $dfk;
5433 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5434 break;
5435 }
5436 }
5437 }
5438 }
5439 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5440 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5441 }
5442 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5443 }
5444 }
5445 }
5446
5447 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
5448 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
5449 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
5450
5451 // custom fields replace
5452 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5453 if (is_array($cmatches[1]) && count($cmatches[1])) {
5454 $cfids = array();
5455 foreach ($cmatches[1] as $cfid ) {
5456 $cfids[] = $cfid;
5457 }
5458 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5459 $dbo->setQuery($q);
5460 $cfields = $dbo->loadAssocList();
5461 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5462 $cfmap = array();
5463 foreach ($cfields as $cf) {
5464 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5465 }
5466 $cfmapreplace = array();
5467 $partsreceived = explode("\n", $order_info['custdata']);
5468 if (count($partsreceived) > 0) {
5469 foreach ($partsreceived as $pst) {
5470 if (!empty($pst)) {
5471 $tmpdata = explode(":", $pst);
5472 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5473 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5474 }
5475 }
5476 }
5477 }
5478 foreach ($cmatches[1] as $cfid ) {
5479 if (array_key_exists($cfid, $cfmapreplace)) {
5480 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5481 } else {
5482 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5483 }
5484 }
5485 }
5486
5487 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5488 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5489 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5490
5491 // order details
5492 $orderdetails = "";
5493 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5494 foreach ($rooms as $num => $r) {
5495 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5496 $use_room_ind = $num - $room_loop_ind;
5497 $split_stay_str = '';
5498 $split_stay_info = [];
5499 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) {
5500 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5501 $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']);
5502 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5503 }
5504
5505 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5506 $faredets = explode(":", $expdet[0]);
5507 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5508 if (!empty($expdet[1])) {
5509 $attrfaredets = explode(":", $expdet[1]);
5510 if (strlen($attrfaredets[1]) > 0) {
5511 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5512 }
5513 }
5514 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5515 $orderdetails .= '<div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($fareprice).'</span></div></div>';
5516 // options
5517 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5518 foreach ($options[$num] as $oo) {
5519 $expopts = explode("\n", $oo);
5520 foreach ($expopts as $optinfo) {
5521 if (!empty($optinfo)) {
5522 $splitopt = explode(":", $optinfo);
5523 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5524 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($optprice).'</span></div></div>';
5525 }
5526 }
5527 }
5528 }
5529 //
5530 if ($roomsnum > 1 && $num < $roomsnum) {
5531 $orderdetails .= '<br/>';
5532 }
5533 }
5534
5535 // coupon
5536 if (!empty($order_info['coupon'])) {
5537 $expcoupon = explode(";", $order_info['coupon']);
5538 $orderdetails .= '<br/><div class="discount"><span>'.JText::translate('VBCOUPON').' '.$expcoupon[2].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($expcoupon[1]).'</span></div></div>';
5539 }
5540
5541 // discount and payment method
5542 $payment_info = [];
5543 $payment_name = '';
5544 if (!empty($order_info['idpayment'])) {
5545 $exppay = explode('=', $order_info['idpayment']);
5546 $payment = self::getPayment($exppay[0], $vbo_tn);
5547 if ((array) $payment) {
5548 $payment_name = $payment['name'];
5549 }
5550 }
5551
5552 $parsed = str_replace("{payment_method}", $payment_name, $parsed);
5553
5554 if ($order_info['status'] != 'cancelled') {
5555 if ($payment_info) {
5556 if ($payment_info['charge'] > 0.00 && $payment_info['ch_disc'] != 1) {
5557 // Discount (not charge)
5558 if ($payment_info['val_pcent'] == 1) {
5559 // fixed value
5560 $total -= $payment_info['charge'];
5561 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($payment_info['charge']).'</span></div></div>';
5562 } else {
5563 // percent value
5564 $percent_disc = $total * $payment_info['charge'] / 100;
5565 $total -= $percent_disc;
5566 $orderdetails .= '<br/><div class="discount"><span>'.$payment_info['name'].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($percent_disc).'</span></div></div>';
5567 }
5568 }
5569 }
5570 }
5571
5572 // booking details string
5573 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5574
5575 // additional information
5576 $parsed = str_replace("{order_total}", $currencyname.' '.self::numberFormat($total), $parsed);
5577 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5578 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5579 $parsed = str_replace("{booking_link}", $link, $parsed);
5580
5581 // deposit
5582 $deposit_str = '';
5583 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5584 $percentdeposit = self::getAccPerCent();
5585 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5586 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5587 if (self::getTypeDeposit() == "fixed") {
5588 $deposit_amount = $percentdeposit;
5589 } else {
5590 $deposit_amount = $total * $percentdeposit / 100;
5591 }
5592 if ($deposit_amount > 0) {
5593 $deposit_str = '<div class="deposit"><span>'.JText::translate('VBLEAVEDEPOSIT').'</span><div class="service-amount" style="float: right;"><strong>'.$currencyname.' '.self::numberFormat($deposit_amount).'</strong></div></div>';
5594 }
5595 }
5596 }
5597 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5598 //
5599 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5600 $totpaid_str = '';
5601 if ($order_info['refund'] > 0) {
5602 $totpaid_str .= '<div class="amountpaid amountrefunded"><span>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</span><div class="service-amount" style="float: right;"><strong>' . $currencyname . ' ' . self::numberFormat($order_info['refund']) . '</strong></div></div>';
5603 }
5604 if ($order_info['status'] != 'cancelled') {
5605 $tot_paid = $order_info['totpaid'];
5606 $diff_topay = (float)$total - (float)$tot_paid;
5607 if ((float)$tot_paid > 0) {
5608 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBAMOUNTPAID').'</span><div class="service-amount" style="float: right;"><strong>'.$currencyname.' '.self::numberFormat($tot_paid).'</strong></div></div>';
5609 // only in case the remaining balance is greater than 1 to avoid commissions issues
5610 if ($diff_topay > 1) {
5611 $totpaid_str .= '<div class="amountpaid"><span>'.JText::translate('VBTOTALREMAINING').'</span><div class="service-amount" style="float: right;"><strong>'.$currencyname.' '.self::numberFormat($diff_topay).'</strong></div></div>';
5612 }
5613 }
5614 }
5615 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5616 $totpaid_str .= '<div class="amountpaid amountcancfee"><span>' . JText::translate('VBO_CANC_FEE') . '</span><div class="service-amount" style="float: right;"><strong>' . $currencyname . ' ' . self::numberFormat($order_info['canc_fee']) . '</strong></div></div>';
5617 }
5618 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
5619
5620 /**
5621 * Language direction (LTR or RTL).
5622 *
5623 * @since 1.16.4 (J) - 1.6.4 (WP)
5624 */
5625 $lang_direction = 'ltr';
5626 if (!empty($order_info['lang'])) {
5627 // attempt to define the language direction and replace the special tag
5628 try {
5629 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
5630 } catch (Throwable $e) {
5631 // do nothing, default to ltr
5632 $lang_direction = 'ltr';
5633 }
5634 // static map for Arabic and Hebrew
5635 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
5636 }
5637 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
5638 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
5639 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
5640
5641 /**
5642 * Replace static floating styles depending on language direction (LTR or RTL).
5643 * For BC with older email template sources, we need to keep the inline style
5644 * for the floating of certain elements, which looks good only on LTR.
5645 *
5646 * @since 1.16.4 (J) - 1.6.4 (WP)
5647 */
5648 if ($lang_direction === 'rtl') {
5649 // get rid of inline static style declaration because the template uses CSS classes
5650 $parsed = str_replace("float: right;", '', $parsed);
5651 }
5652
5653 return $parsed;
5654 }
5655
5656 /**
5657 * New method for sending booking email messages
5658 * to the guest or to the administrator(s).
5659 *
5660 * @param int $bid the booking ID.
5661 * @param array $for guest, admin or a custom email address.
5662 * @param bool $send whether to send or return the HTML message.
5663 * @param bool $no_config if true, no configuration settings will be used for the status.
5664 * @param string $type optional type to indicate a booking modification.
5665 *
5666 * @return mixed True or False depending on the result or HTML string for the preview.
5667 *
5668 * @since 1.13 (J) - 1.3 (WP)
5669 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
5670 * the reservation status is pending and against the configuration.
5671 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
5672 */
5673 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
5674 {
5675 $dbo = JFactory::getDbo();
5676 $app = JFactory::getApplication();
5677
5678 $vbo_tn = self::getTranslator();
5679 $av_helper = self::getAvailabilityInstance();
5680
5681 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5682 $dbo->setQuery($q);
5683 $booking = $dbo->loadAssoc();
5684 if (!$booking) {
5685 return false;
5686 }
5687
5688 $result = false;
5689
5690 // check if the language in use is the same as the one used during the checkout
5691 $lang = JFactory::getLanguage();
5692 if (!empty($booking['lang'])) {
5693 if ($lang->getTag() != $booking['lang']) {
5694 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
5695 if (VBOPlatformDetection::isJoomla()) {
5696 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
5697 }
5698 }
5699 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
5700 // force the translation to start because contents should be translated
5701 $vbo_tn::$force_tolang = $booking['lang'];
5702 }
5703 }
5704
5705 // load rooms booked
5706 $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;";
5707 $dbo->setQuery($q);
5708 $ordersrooms = $dbo->loadAssocList();
5709 if (!$ordersrooms) {
5710 return false;
5711 }
5712 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
5713
5714 /**
5715 * Split stay reservation.
5716 *
5717 * @since 1.16.0 (J) - 1.6.0 (WP)
5718 */
5719 $room_stay_dates = [];
5720 if ($booking['split_stay']) {
5721 if ($booking['status'] == 'confirmed') {
5722 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
5723 } else {
5724 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
5725 }
5726 // immediately count the number of nights of stay for each split room
5727 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5728 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5729 // overwrite values for compatibility with non-confirmed bookings
5730 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5731 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5732 }
5733 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5734 // overwrite the whole array
5735 $room_stay_dates[$sps_r_k] = $sps_r_v;
5736 }
5737 }
5738
5739 $rooms = array();
5740 $tars = array();
5741 $is_package = !empty($booking['pkg']) ? true : false;
5742 $ftitle = self::getFrontTitle();
5743 $currencyname = self::getCurrencyName();
5744 foreach ($ordersrooms as $kor => $or) {
5745 $num = $kor + 1;
5746 $rooms[$num] = $or;
5747 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5748 // package or custom cost set from the back-end
5749 continue;
5750 }
5751
5752 // determine the days to consider for the count of the availability
5753 $room_nights = $booking['days'];
5754 $room_checkin = $booking['checkin'];
5755 $room_checkout = $booking['checkout'];
5756 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5757 $room_nights = $room_stay_dates[$kor]['nights'];
5758 $room_checkin = $room_stay_dates[$kor]['checkin'];
5759 $room_checkout = $room_stay_dates[$kor]['checkout'];
5760 }
5761
5762 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
5763 $dbo->setQuery($q);
5764 $tar = $dbo->loadAssocList();
5765 if (!$tar) {
5766 // tariff not found
5767 if (self::isAdmin()) {
5768 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
5769 }
5770 continue;
5771 }
5772
5773 // apply seasonal rates
5774 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
5775
5776 // different usage
5777 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
5778 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
5779 // Occupancy Override
5780 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
5781 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
5782 //
5783 if (is_array($diffusageprice)) {
5784 // set a charge or discount to the price(s) for the different usage of the room
5785 foreach ($tar as $kpr => $vpr) {
5786 $tar[$kpr]['diffusage'] = $or['adults'];
5787 if ($diffusageprice['chdisc'] == 1) {
5788 // charge
5789 if ($diffusageprice['valpcent'] == 1) {
5790 // fixed value
5791 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5792 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5793 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
5794 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5795 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
5796 } else {
5797 // percentage value
5798 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5799 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
5800 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
5801 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5802 $tar[$kpr]['cost'] = $aduseval;
5803 }
5804 } else {
5805 // discount
5806 if ($diffusageprice['valpcent'] == 1) {
5807 // fixed value
5808 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5809 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5810 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
5811 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5812 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
5813 } else {
5814 // percentage value
5815 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5816 $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);
5817 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
5818 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5819 $tar[$kpr]['cost'] = $aduseval;
5820 }
5821 }
5822 }
5823 }
5824 }
5825 //
5826 $tars[$num] = $tar[0];
5827 }
5828
5829 $secdiff = $booking['checkout'] - $booking['checkin'];
5830 $daysdiff = $secdiff / 86400;
5831 if (is_int($daysdiff)) {
5832 if ($daysdiff < 1) {
5833 $daysdiff = 1;
5834 }
5835 } else {
5836 if ($daysdiff < 1) {
5837 $daysdiff = 1;
5838 } else {
5839 $sum = floor($daysdiff) * 86400;
5840 $newdiff = $secdiff - $sum;
5841 $maxhmore = self::getHoursMoreRb() * 3600;
5842 if ($maxhmore >= $newdiff) {
5843 $daysdiff = floor($daysdiff);
5844 } else {
5845 $daysdiff = ceil($daysdiff);
5846 }
5847 }
5848 }
5849
5850 $isdue = 0;
5851 $pricestr = array();
5852 $optstr = array();
5853 foreach ($ordersrooms as $kor => $or) {
5854 $num = $kor + 1;
5855
5856 // determine the days to consider for the count of the availability
5857 $room_nights = $booking['days'];
5858 $room_checkin = $booking['checkin'];
5859 $room_checkout = $booking['checkout'];
5860 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5861 $room_nights = $room_stay_dates[$kor]['nights'];
5862 $room_checkin = $room_stay_dates[$kor]['checkin'];
5863 $room_checkout = $room_stay_dates[$kor]['checkout'];
5864 }
5865
5866 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5867 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
5868 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
5869 $isdue += $calctar;
5870 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
5871 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
5872 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5873 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
5874 $tars[$num]['calctar'] = $calctar;
5875 $isdue += $calctar;
5876 $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'] : "");
5877 }
5878
5879 if (!empty($or['optionals'])) {
5880 $stepo = explode(";", $or['optionals']);
5881 foreach ($stepo as $roptkey => $oo) {
5882 if (empty($oo)) {
5883 continue;
5884 }
5885 $stept = explode(":", $oo);
5886 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
5887 $dbo->setQuery($q);
5888 $actopt = $dbo->loadAssocList();
5889 if ($actopt) {
5890 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
5891 $chvar = '';
5892 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
5893 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
5894 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
5895 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
5896 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
5897 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
5898 $agestept = explode('-', $stept[1]);
5899 $stept[1] = $agestept[0];
5900 $chvar = $agestept[1];
5901 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
5902 // percentage value of the adults tariff
5903 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5904 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5905 } else {
5906 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5907 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5908 }
5909 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
5910 // VBO 1.10 - percentage value of room base cost
5911 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5912 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5913 } else {
5914 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
5915 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5916 }
5917 }
5918 $actopt[0]['chageintv'] = $chvar;
5919 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
5920 $actopt[0]['quan'] = $stept[1];
5921 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
5922 } else {
5923 $actopt[0]['quan'] = $stept[1];
5924 // VBO 1.11 - options percentage cost of the room total fee
5925 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5926 $deftar_basecosts = $or['cust_cost'];
5927 } else {
5928 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5929 }
5930 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
5931 //
5932 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
5933 }
5934 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
5935 $realcost = $actopt[0]['maxprice'];
5936 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
5937 $realcost = $actopt[0]['maxprice'] * $stept[1];
5938 }
5939 }
5940 if ($actopt[0]['perperson'] == 1) {
5941 $realcost = $realcost * $or['adults'];
5942 }
5943 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
5944 $isdue += $tmpopr;
5945 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
5946 }
5947 }
5948 }
5949
5950 // custom extra costs
5951 if (!empty($or['extracosts'])) {
5952 $cur_extra_costs = json_decode($or['extracosts'], true);
5953 foreach ($cur_extra_costs as $eck => $ecv) {
5954 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
5955 $isdue += $ecplustax;
5956 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
5957 }
5958 }
5959 }
5960
5961 // force the original total amount if rates have changed
5962 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
5963 $isdue = $booking['total'];
5964 }
5965
5966 // mail subject
5967 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
5968 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
5969
5970 // inject the recipient of the message for the template
5971 $booking['for'] = $for;
5972
5973 // load template file that will get $booking as variable
5974 $tmpl = self::loadEmailTemplate($booking);
5975
5976 // parse email template
5977 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
5978 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
5979
5980 if ($send !== true) {
5981 // return the content of the email message parsed
5982 return $hmess;
5983 }
5984
5985 // when the message can be sent
5986 $sendwhen = self::getSendEmailWhen();
5987
5988 if ($no_config) {
5989 // force the configuration setting to send the message no matter what's the booking status
5990 $sendwhen = 1;
5991 }
5992
5993 // send the message
5994 foreach ($for as $who) {
5995 $use_subject = $subject;
5996 $recipients = array();
5997 $attachments = self::addEmailAttachment(null);
5998 $attach_ical = false;
5999 $force_replyto = null;
6000 if (strpos($who, '@') !== false) {
6001 // send email to custom email address
6002 array_push($recipients, trim($who));
6003 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
6004 // send email to the customer
6005 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6006 continue;
6007 }
6008 array_push($recipients, $booking['custmail']);
6009 /**
6010 * Check whether an iCal should be attached for the customer.
6011 *
6012 * @since 1.12.0
6013 */
6014 $attach_ical = self::getEmailIcal('customer', $booking);
6015 } elseif (stripos($who, 'admin') !== false) {
6016 // send email to the administrator(s)
6017 if ($sendwhen > 1 && $booking['status'] == 'standby') {
6018 continue;
6019 }
6020 $use_subject = $subject . ' #' . $booking['id'];
6021 if (!strcasecmp($type, 'modified')) {
6022 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
6023 }
6024 $adminemail = self::getAdminMail();
6025 $extra_admin_recipients = self::addAdminEmailRecipient(null);
6026 if (empty($adminemail) && empty($extra_admin_recipients)) {
6027 // Prevent Joomla Exceptions that would stop the script execution
6028 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
6029 continue;
6030 }
6031 if (strpos($adminemail, ',') !== false) {
6032 // multiple addresses
6033 $adminemails = explode(',', $adminemail);
6034 foreach ($adminemails as $am) {
6035 if (strpos($am, '@') !== false) {
6036 array_push($recipients, trim($am));
6037 }
6038 }
6039 } else {
6040 // single address
6041 array_push($recipients, trim($adminemail));
6042 }
6043
6044 // merge extra recipients
6045 $recipients = array_merge($recipients, $extra_admin_recipients);
6046
6047 // admin should reply to the customer
6048 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
6049
6050 /**
6051 * Check whether an iCal should be attached for the admin.
6052 *
6053 * @since 1.2.0
6054 */
6055 $attach_ical = self::getEmailIcal('admin', array(
6056 'ts' => $booking['ts'],
6057 'custdata' => $booking['custdata'],
6058 'checkin' => $booking['checkin'],
6059 'checkout' => $booking['checkout'],
6060 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
6061 ));
6062 }
6063
6064 // send the message, recipients should always be an array to support multiple admin addresses
6065
6066 // get sender e-mail
6067 $adsendermail = VBOFactory::getConfig()->get('senderemail');
6068
6069 // init mail data
6070 $mail = new VBOMailWrapper([
6071 'sender' => [$adsendermail, $ftitle],
6072 'recipient' => $recipients,
6073 'bcc' => self::addAdminEmailRecipient(null, true),
6074 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
6075 'subject' => $use_subject,
6076 'content' => $hmess,
6077 'attachments' => $attachments,
6078 ]);
6079
6080 if ($attach_ical !== false && $booking['status'] != 'standby') {
6081 // attach iCal file
6082 $mail->addAttachment($attach_ical);
6083 }
6084
6085 /**
6086 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
6087 *
6088 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
6089 *
6090 * @since 1.15.4 (J) - 1.5.10 (WP)
6091 */
6092 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
6093
6094 // send e-mail
6095 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
6096
6097 // unlink iCal file
6098 if ($attach_ical !== false) {
6099 @unlink($attach_ical);
6100 }
6101 }
6102
6103 return $result;
6104 }
6105
6106 /**
6107 * This method allows to add one or more recipient email
6108 * addresses for the next queue of email sending for the admin.
6109 * This method can be used in the template file for the customer
6110 * email to register an additional email address, maybe when a
6111 * specific room-type is booked.
6112 * The methods sending the email messages are supposed to call this
6113 * method by passing no arguments to obtain the extra addresses set.
6114 *
6115 * @param mixed $email null, string or array of email address(es).
6116 * @param bool $bcc if true, addresses will be used as bcc.
6117 * @param bool $reset if true, the queues will be emptied.
6118 *
6119 * @return array the current extra recipients or bcc addresses set.
6120 *
6121 * @since 1.13 (J) - 1.3.0 (WP)
6122 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6123 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6124 */
6125 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6126 {
6127 static $extra_recipients = [];
6128 static $extra_bcc = [];
6129
6130 if ($reset) {
6131 $extra_recipients = [];
6132 $extra_bcc = [];
6133 }
6134
6135 if (!empty($email)) {
6136 if (is_scalar($email)) {
6137 if ($bcc) {
6138 array_push($extra_bcc, $email);
6139 } else {
6140 array_push($extra_recipients, $email);
6141 }
6142 } else {
6143 if ($bcc) {
6144 $extra_bcc = array_merge($extra_bcc, $email);
6145 } else {
6146 $extra_recipients = array_merge($extra_recipients, $email);
6147 }
6148 }
6149 }
6150
6151 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6152 }
6153
6154 /**
6155 * This method serves to add one or more attachments to the
6156 * next queue of email sending for the admin.
6157 * The methods sending the email messages are supposed to call this
6158 * method by passing a null argument to obtain the attachments set.
6159 *
6160 * @param mixed $file null or string with path to file to attach.
6161 * @param bool $reset if true, the queue will be emptied.
6162 *
6163 * @return array the current attachments set.
6164 *
6165 * @since 1.14
6166 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6167 */
6168 public static function addEmailAttachment($file, $reset = false)
6169 {
6170 static $extra_attachments = [];
6171
6172 if ($reset) {
6173 $extra_attachments = [];
6174 }
6175
6176 if (!empty($file)) {
6177 if (is_scalar($file)) {
6178 array_push($extra_attachments, $file);
6179 } else {
6180 $extra_attachments = array_merge($extra_attachments, $file);
6181 }
6182 }
6183
6184 return array_unique($extra_attachments);
6185 }
6186
6187 /**
6188 * This method is called whenever some rooms get booked.
6189 * It checks whether the rooms involved have shared calendars.
6190 * If some are found, then also such rooms will be occupied.
6191 *
6192 * @param int $bid the booking ID.
6193 * @param array $roomids the list of rooms booked.
6194 * @param int $checkin checkin timestamp.
6195 * @param int $checkout checkout timestamp.
6196 *
6197 * @return boolean true if some other cals were occupied, false otherwise.
6198 *
6199 * @since 1.13 (J) - 1.3.0 (WP)
6200 *
6201 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6202 */
6203 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6204 {
6205 $dbo = JFactory::getDbo();
6206 $bid = (int)$bid;
6207
6208 // availability helper
6209 $av_helper = self::getAvailabilityInstance();
6210
6211 // check for split stay rooms reservation
6212 $known_split_stay = false;
6213 $is_split_stay = 0;
6214
6215 if (empty($checkin) || empty($checkout)) {
6216 // get checkin and checkout timestamps from booking
6217 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6218 $dbo->setQuery($q);
6219 $dbo->execute();
6220 if (!$dbo->getNumRows()) {
6221 // booking not found
6222 return false;
6223 }
6224 $bdata = $dbo->loadAssoc();
6225 $checkin = $bdata['checkin'];
6226 $checkout = $bdata['checkout'];
6227 $is_split_stay = $bdata['split_stay'];
6228 // turn flag on
6229 $known_split_stay = true;
6230 }
6231
6232 if (!$known_split_stay) {
6233 // we need to query the db to access this information
6234 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6235 $dbo->setQuery($q, 0, 1);
6236 $dbo->execute();
6237 if ($dbo->getNumRows()) {
6238 $is_split_stay = (int)$dbo->loadResult();
6239 }
6240 // turn flag on
6241 $known_split_stay = true;
6242 }
6243
6244 // check split stay room records
6245 $room_stay_dates = [];
6246 if ($is_split_stay) {
6247 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6248 // immediately count the number of nights of stay for each split room
6249 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6250 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6251 // overwrite values for compatibility with non-confirmed bookings
6252 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6253 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6254 }
6255 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6256 // overwrite the whole array
6257 $room_stay_dates[$sps_r_k] = $sps_r_v;
6258 }
6259 }
6260
6261 // get the rooms booked
6262 if (!count($roomids)) {
6263 // get the IDs of all rooms booked
6264 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6265 $dbo->setQuery($q);
6266 $orr = $dbo->loadAssocList();
6267 if ($orr) {
6268 foreach ($orr as $or) {
6269 array_push($roomids, $or['idroom']);
6270 }
6271 }
6272 }
6273 if (!count($roomids) || empty($bid)) {
6274 // unable to proceed
6275 return false;
6276 }
6277 $roomids = array_unique($roomids);
6278
6279 // build room stay dates map in case of split stay
6280 $room_split_stay_map = [];
6281 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6282 // we have unique room IDs in a split stay reservation for more than one room
6283 foreach ($room_stay_dates as $room_stay_date) {
6284 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6285 }
6286 }
6287
6288 // get rooms involved
6289 $involved = [];
6290 $involved_stay_map = [];
6291 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6292 $dbo->setQuery($q);
6293 $rooms_found = $dbo->loadAssocList();
6294 if (!$rooms_found) {
6295 // no rooms involved that need their calendars updated
6296 return false;
6297 }
6298 foreach ($rooms_found as $rf) {
6299 if (!in_array($rf['mainroom'], $roomids)) {
6300 // push room ID
6301 array_push($involved, $rf['mainroom']);
6302 // check for specific stay dates in connected room
6303 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6304 // split stay information available
6305 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6306 }
6307 }
6308 if (!in_array($rf['childroom'], $roomids)) {
6309 // push room ID
6310 array_push($involved, $rf['childroom']);
6311 // check for specific stay dates in connected room
6312 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6313 // split stay information available
6314 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6315 }
6316 }
6317 }
6318 $involved = array_unique($involved);
6319 if (!count($involved)) {
6320 // no rooms involved
6321 return false;
6322 }
6323
6324 // turnover seconds
6325 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6326
6327 // occupy the calendars for the involved rooms found
6328 $bids_generated = [];
6329 foreach ($involved as $rid) {
6330 // determine the values to use
6331 $room_checkin = (int)$checkin;
6332 $room_checkout = (int)$checkout;
6333 $room_realback = ($checkout + $turnover_secs);
6334 if (isset($involved_stay_map[$rid])) {
6335 // split stay dates available
6336 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6337 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6338 $room_realback = ($room_checkout + $turnover_secs);
6339 }
6340
6341 // occupy record in room with shared calendar
6342 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6343 $dbo->setQuery($q);
6344 $dbo->execute();
6345 array_push($bids_generated, $dbo->insertid());
6346 }
6347
6348 // store busy relations created
6349 foreach ($bids_generated as $busyid) {
6350 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6351 $dbo->setQuery($q);
6352 $dbo->execute();
6353 }
6354
6355 return true;
6356 }
6357
6358 /**
6359 * This method is needed whenever a booking gets modified by
6360 * adding or removing rooms. This way we reset (remove) all
6361 * busy records that were previously stored due to a shared
6362 * calendar. The correct relations should then be re-created
6363 * by calling updateSharedCalendars().
6364 *
6365 * @param int $bid the booking ID.
6366 *
6367 * @return boolean true if some records were cleaned.
6368 *
6369 * @see updateSharedCalendars() should be called after.
6370 *
6371 * @since 1.3.0
6372 */
6373 public static function cleanSharedCalendarsBusy($bid) {
6374 $dbo = JFactory::getDbo();
6375 $bid = (int)$bid;
6376 // get all the occupied records due to shared calendars for this booking
6377 $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;";
6378 $dbo->setQuery($q);
6379 $allbusy = $dbo->loadAssocList();
6380 if (!$allbusy) {
6381 return false;
6382 }
6383
6384 $busy_ids = array();
6385 foreach ($allbusy as $b) {
6386 // push busy ID to be removed later
6387 array_push($busy_ids, $b['id']);
6388 // delete the current busy ID-booking ID relation for this shared calendar
6389 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6390 $dbo->setQuery($q);
6391 $dbo->execute();
6392 }
6393 if (count($busy_ids)) {
6394 // delete all busy records due to shared calendars
6395 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6396 $dbo->setQuery($q);
6397 $dbo->execute();
6398 }
6399
6400 return true;
6401 }
6402
6403 public static function sendJutility() {
6404 //deprecated in VBO 1.10
6405 $dbo = JFactory::getDbo();
6406 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6407 $dbo->setQuery($q);
6408 $dbo->execute();
6409 $s = $dbo->loadAssocList();
6410 return (intval($s[0]['setting']) == 1 ? true : false);
6411 }
6412
6413 public static function getCategoryName($idcat, $vbo_tn = null) {
6414 $dbo = JFactory::getDbo();
6415 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6416 $dbo->setQuery($q);
6417 $dbo->execute();
6418 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6419 if (is_object($vbo_tn) && count($p) > 0) {
6420 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6421 }
6422 return count($p) > 0 ? $p[0]['name'] : '';
6423 }
6424
6425 public static function loadAdultsDiff($idroom, $adults)
6426 {
6427 static $obp_map = [];
6428
6429 $map_sign = "{$idroom}:{$adults}";
6430
6431 if (array_key_exists($map_sign, $obp_map)) {
6432 return $obp_map[$map_sign];
6433 }
6434
6435 $dbo = JFactory::getDbo();
6436
6437 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6438 $dbo->setQuery($q, 0, 1);
6439 $obp_diff = $dbo->loadAssoc();
6440 if ($obp_diff) {
6441 // cache value and return it
6442 $obp_map[$map_sign] = $obp_diff;
6443
6444 return $obp_diff;
6445 }
6446
6447 // cache value and return it
6448 $obp_map[$map_sign] = null;
6449
6450 return null;
6451 }
6452
6453 public static function loadRoomAdultsDiff($idroom)
6454 {
6455 $dbo = JFactory::getDbo();
6456
6457 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6458 $dbo->setQuery($q);
6459 $diff = $dbo->loadAssocList();
6460 if ($diff) {
6461 $roomdiff = [];
6462 foreach ($diff as $v) {
6463 $roomdiff[$v['adults']] = $v;
6464 }
6465
6466 return $roomdiff;
6467 }
6468
6469 return [];
6470 }
6471
6472 public static function occupancyOverrideExists($tar, $adults) {
6473 foreach ($tar as $k => $v) {
6474 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6475 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6476 return $v['occupancy_ovr'][$adults];
6477 }
6478 }
6479 }
6480 return false;
6481 }
6482
6483 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6484 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6485 $charges = array();
6486 if (!($children > 0) || !(count($ages) > 0)) {
6487 return $charges;
6488 }
6489 $dbo = JFactory::getDbo();
6490 $id_options = array();
6491 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6492 $dbo->setQuery($q);
6493 $dbo->execute();
6494 if ($dbo->getNumRows() > 0) {
6495 $assocs = $dbo->loadAssocList();
6496 foreach ($assocs as $opts) {
6497 if (!empty($opts['idopt'])) {
6498 $r_ido = explode(';', rtrim($opts['idopt']));
6499 foreach ($r_ido as $ido) {
6500 if (!empty($ido) && !in_array($ido, $id_options)) {
6501 $id_options[] = $ido;
6502 }
6503 }
6504 }
6505 }
6506 }
6507 if (count($id_options) > 0) {
6508 $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;";
6509 $dbo->setQuery($q);
6510 $dbo->execute();
6511 if ($dbo->getNumRows() > 0) {
6512 $ageintervals = $dbo->loadAssocList();
6513 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6514 $age_range = array();
6515 foreach ($split_ages as $kg => $spage) {
6516 if (empty($spage)) {
6517 continue;
6518 }
6519 $parts = explode('_', $spage);
6520 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6521 $ind = count($age_range);
6522 $age_range[$ind]['from'] = intval($parts[0]);
6523 $age_range[$ind]['to'] = intval($parts[1]);
6524 //taxes are calculated later in VCM
6525 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6526 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6527 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6528 }
6529 }
6530 if (count($age_range) > 0) {
6531 $tot_charge = 0;
6532 $affected = array();
6533 $option_str = '';
6534 foreach ($ages as $age) {
6535 if (strlen($age) == 0) {
6536 continue;
6537 }
6538 foreach ($age_range as $range) {
6539 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6540 $tot_charge += $range['cost'];
6541 $affected[] = $age;
6542 $option_str .= $range['option_str'].';';
6543 break;
6544 }
6545 }
6546 }
6547 if ($tot_charge > 0) {
6548 $charges['total'] = $tot_charge;
6549 $charges['affected'] = $affected;
6550 $charges['options'] = $option_str;
6551 }
6552 }
6553 }
6554 }
6555
6556 return $charges;
6557 }
6558
6559 public static function sortRoomPrices($arr) {
6560 $newarr = array();
6561 foreach ($arr as $k => $v) {
6562 $newarr[$k] = $v['cost'];
6563 }
6564 asort($newarr);
6565 $sorted = array();
6566 foreach ($newarr as $k => $v) {
6567 $sorted[$k] = $arr[$k];
6568 }
6569 return $sorted;
6570 }
6571
6572 public static function sortResults($arr) {
6573 $newarr = array();
6574 foreach ($arr as $k => $v) {
6575 $newarr[$k] = $v[0]['cost'];
6576 }
6577 asort($newarr);
6578 $sorted = array();
6579 foreach ($newarr as $k => $v) {
6580 $sorted[$k] = $arr[$k];
6581 }
6582 return $sorted;
6583 }
6584
6585 public static function sortMultipleResults($arr) {
6586 foreach ($arr as $k => $v) {
6587 $newarr = array();
6588 foreach ($v as $subk => $subv) {
6589 $newarr[$subk] = $subv[0]['cost'];
6590 }
6591 asort($newarr);
6592 $sorted = array();
6593 foreach ($newarr as $nk => $v) {
6594 $sorted[$nk] = $arr[$k][$nk];
6595 }
6596 $arr[$k] = $sorted;
6597 }
6598 return $arr;
6599 }
6600
6601 /**
6602 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6603 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
6604 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
6605 *
6606 * @param array $promos optional list of promotion IDs to register in the execution flow.
6607 *
6608 * @return array list of cached promotion IDs (if any).
6609 *
6610 * @since 1.16.4 (J) - 1.6.4 (WP)
6611 */
6612 public static function registerPromotionIds(array $promos = null)
6613 {
6614 static $flagged_promos = [];
6615
6616 if ($promos) {
6617 // set cached value
6618 $flagged_promos = $promos;
6619 }
6620
6621 return $flagged_promos;
6622 }
6623
6624 /**
6625 * Getter and setter for seasons cache allowed.
6626 *
6627 * @param bool $enabled true or false for setter, null for getter.
6628 *
6629 * @return bool|null
6630 *
6631 * @since 1.16.5 (J) - 1.6.5 (WP)
6632 */
6633 public static function setSeasonsCache($enabled = null)
6634 {
6635 static $seasons_cache = null;
6636
6637 if ($enabled !== null) {
6638 // set value
6639 $seasons_cache = (bool)$enabled;
6640 }
6641
6642 return $seasons_cache;
6643 }
6644
6645 /**
6646 * Allows to preload and cache seasonal records for a list of rooms and dates.
6647 * Any subsequent preloading of season records for the same room(s) will return
6648 * the cached list. Should be used to preload a large window of data so that
6649 * subsequent calls will always return cached value.
6650 *
6651 * @param array $rooms List of involved room IDs.
6652 * @param int|bool $from Unix timestamp for start date or false to unset cache.
6653 * @param int $to Unix timestamp for end date.
6654 *
6655 * @return ?array
6656 *
6657 * @since 1.17.2 (J) - 1.7.2 (WP)
6658 */
6659 public static function preloadSeasonRecords(array $rooms, $from = null, $to = null)
6660 {
6661 static $preloaded_records = [];
6662
6663 if (!$rooms) {
6664 // no cached signature
6665 return [];
6666 }
6667
6668 $signature = md5(implode(',', $rooms));
6669
6670 if ($from === false) {
6671 // unset cached records
6672 unset($preloaded_records[$signature]);
6673
6674 return;
6675 }
6676
6677 if ($from !== null && $to !== null) {
6678 // setter args signature for preloading records
6679 $preloaded_records[$signature] = self::getDateSeasonRecords($from, $to, $rooms);
6680
6681 return;
6682 }
6683
6684 // getter args signature for getting the preloaded and cached records
6685 return $preloaded_records[$signature] ?? [];
6686 }
6687
6688 /**
6689 * Fetches all season records affecting a range of date timestamps.
6690 * Useful to pre-cache season records in case of hundreds of thousands
6691 * of records, but it can use up several MBs of server's memory.
6692 *
6693 * @param int $from unix timestamp for start date.
6694 * @param int $to unix timestamp for end date.
6695 * @param array $rooms optional list of involved room IDs.
6696 *
6697 * @return array
6698 *
6699 * @since 1.16.10 (J) - 1.6.10 (WP)
6700 */
6701 public static function getDateSeasonRecords($from, $to, array $rooms = [])
6702 {
6703 /**
6704 * We allow external systems to preload season records and cache them.
6705 *
6706 * @since 1.17.2 (J) - 1.7.2 (WP)
6707 */
6708 if ($preloaded_records = self::preloadSeasonRecords($rooms)) {
6709 return $preloaded_records;
6710 }
6711
6712 /**
6713 * For a more accurate records caching, ensure we have a range of two dates at least.
6714 *
6715 * @since 1.17.2 (J) - 1.7.2 (WP)
6716 */
6717 if (date('Y-m-d', $from) === date('Y-m-d', $to)) {
6718 // add one day to the end timestamp
6719 $to = strtotime('+1 day', $to);
6720 }
6721
6722 $dbo = JFactory::getDbo();
6723
6724 $one = getdate($from);
6725 $two = getdate($to);
6726
6727 // leap years
6728 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6729 $isleap = true;
6730 } else {
6731 $isleap = false;
6732 }
6733
6734 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6735 $tomidnightone = intval($one['hours']) * 3600;
6736 $tomidnightone += intval($one['minutes']) * 60;
6737 $sfrom = $from - $baseone - $tomidnightone;
6738 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6739 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6740 $tomidnighttwo = intval($two['hours']) * 3600;
6741 $tomidnighttwo += intval($two['minutes']) * 60;
6742 $sto = $to - $basetwo - $tomidnighttwo;
6743
6744 // leap years, check what dates to manipulate
6745 if ($isleap) {
6746 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
6747 if ($one[0] > $leapts) {
6748 /**
6749 * Timestamp must be greater than the leap-day of Feb 29th.
6750 * It used to be checked for >= $leapts.
6751 *
6752 * @since July 3rd 2019
6753 */
6754 $sfrom -= 86400;
6755 }
6756 if ($two[0] > $leapts && $one['year'] == $two['year']) {
6757 // lower checkin date when in leap year but not for checkout
6758 $sto -= 86400;
6759 }
6760 }
6761
6762 // check for DST changes to adjust the query values to fetch
6763 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6764 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6765 // we are parsing the day when the DST changed (probably a Sunday)
6766 if (!date('I', $from)) {
6767 // DST was just turned off
6768 $sfrom -= 3600;
6769 }
6770 }
6771 }
6772
6773 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6774 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6775 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6776 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6777 ($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`)") .
6778 ($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`)") .
6779 ($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`)") .
6780 ($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`)") .
6781 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6782 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6783
6784 /**
6785 * Avoid issues when querying data for a whole year by fetching all records.
6786 * For example, from 2024-12-30 till 2025-12-30 it is more efficient to get all records.
6787 *
6788 * @since 1.17.3 (J) - 1.7.3 (WP)
6789 */
6790 if ($one['mon'] == $two['mon'] && $one['year'] < $two['year']) {
6791 // fetch all records when targeting a whole year of data
6792 $q = $dbo->getQuery(true)
6793 ->select('*')
6794 ->from($dbo->qn('#__vikbooking_seasons'))
6795 ->order($dbo->qn('promo') . ' ASC');
6796 }
6797
6798 $dbo->setQuery($q);
6799 $seasons = $dbo->loadAssocList();
6800
6801 if ($rooms) {
6802 // filter records by affected room IDs
6803 $seasons = array_filter($seasons, function($s) use ($rooms) {
6804 $allrooms = !empty($s['idrooms']) ? explode(',', $s['idrooms']) : [];
6805 foreach ($rooms as $idroom) {
6806 if (in_array("-" . $idroom . "-", $allrooms)) {
6807 return true;
6808 }
6809 }
6810 return false;
6811 });
6812
6813 // reset array keys
6814 $seasons = array_values($seasons);
6815 }
6816
6817 return $seasons;
6818 }
6819
6820 /**
6821 * Applies the seasonal rates over a list of room rate records.
6822 *
6823 * @param array $arr list of room rate records.
6824 * @param int $from unix timestamp for start date.
6825 * @param int $to unix timestamp for end date.
6826 *
6827 * @return array list of manipulated room rate records.
6828 */
6829 public static function applySeasonalPrices(array $arr, $from, $to)
6830 {
6831 $dbo = JFactory::getDbo();
6832 $vbo_tn = self::getTranslator();
6833
6834 $roomschange = [];
6835 $one = getdate($from);
6836
6837 // leap years
6838 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6839 $isleap = true;
6840 } else {
6841 $isleap = false;
6842 }
6843
6844 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6845 $tomidnightone = intval($one['hours']) * 3600;
6846 $tomidnightone += intval($one['minutes']) * 60;
6847 $sfrom = $from - $baseone - $tomidnightone;
6848 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6849 $two = getdate($to);
6850 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6851 $tomidnighttwo = intval($two['hours']) * 3600;
6852 $tomidnighttwo += intval($two['minutes']) * 60;
6853 $sto = $to - $basetwo - $tomidnighttwo;
6854
6855 // leap years, check what dates to manipulate
6856 if ($isleap) {
6857 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
6858 if ($one[0] > $leapts) {
6859 /**
6860 * Timestamp must be greater than the leap-day of Feb 29th.
6861 * It used to be checked for >= $leapts.
6862 *
6863 * @since July 3rd 2019
6864 */
6865 $sfrom -= 86400;
6866 }
6867 if ($two[0] > $leapts && $one['year'] == $two['year']) {
6868 // lower checkin date when in leap year but not for checkout
6869 $sto -= 86400;
6870 }
6871 }
6872
6873 // check for DST changes to adjust the query values to fetch
6874 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6875 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6876 // we are parsing the day when the DST changed (probably a Sunday)
6877 if (!date('I', $from)) {
6878 // DST was just turned off
6879 $sfrom -= 3600;
6880 }
6881 }
6882 }
6883
6884 // count nights requested
6885 $booking_nights = 1;
6886 foreach ($arr as $k => $a) {
6887 if (!isset($a[0])) {
6888 continue;
6889 }
6890 if (isset($a[0]['booking_nights'])) {
6891 // this value may be set when displaying pricing calendars
6892 $booking_nights = $a[0]['booking_nights'];
6893 break;
6894 }
6895 if (isset($a[0]['days'])) {
6896 $booking_nights = $a[0]['days'];
6897 break;
6898 }
6899 }
6900
6901 /**
6902 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
6903 *
6904 * @since 1.16.4 (J) - 1.6.4 (WP)
6905 */
6906 $skip_promo_ids = self::registerPromotionIds();
6907
6908 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6909 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6910 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6911 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6912 ($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`)") .
6913 ($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`)") .
6914 ($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`)") .
6915 ($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`)") .
6916 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6917 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6918
6919 $dbo->setQuery($q);
6920 $seasons = $dbo->loadAssocList();
6921
6922 if ($seasons) {
6923 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
6924 $applyseasons = false;
6925 $mem = [];
6926 foreach ($arr as $k => $a) {
6927 $mem[$k]['daysused'] = 0;
6928 $mem[$k]['sum'] = [];
6929 /**
6930 * The keys below are all needed to apply the promotions on the room's final cost.
6931 *
6932 * @since 1.13.5
6933 */
6934 $mem[$k]['diffs'] = [];
6935 $mem[$k]['trans_keys'] = [];
6936 $mem[$k]['trans_factors'] = [];
6937 $mem[$k]['trans_affdays'] = [];
6938 }
6939 foreach ($seasons as $s) {
6940 // check if this is a promotion registered for skipping
6941 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
6942 continue;
6943 }
6944
6945 // Special Price tied to the year
6946 if (!empty($s['year']) && $s['year'] > 0) {
6947 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
6948 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
6949 //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
6950 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
6951 continue;
6952 }
6953 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
6954 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
6955 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
6956 if ($calc_ends < ($from - $tomidnightone)) {
6957 continue;
6958 }
6959 }
6960 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
6961 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
6962 if ($s['from'] > $s['to']) {
6963 continue;
6964 }
6965 if (($basetwo + $s['from'] + 86399) > $to) {
6966 /**
6967 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
6968 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
6969 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
6970 *
6971 * @since 1.14.3 (J) - 1.4.3 (WP)
6972 */
6973 continue;
6974 }
6975 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
6976 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
6977 /**
6978 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
6979 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
6980 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
6981 *
6982 * @since 1.14.3 (J) - 1.4.3 (WP)
6983 */
6984 continue;
6985 }
6986 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
6987 // 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)
6988 if (($baseone + $s['from']) > $to) {
6989 continue;
6990 }
6991 }
6992 }
6993 //
6994 $allrooms = explode(",", $s['idrooms']);
6995 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
6996 $inits = $baseone + $s['from'];
6997 if ($s['from'] < $s['to']) {
6998 $ends = $basetwo + $s['to'];
6999 // check if the inits must be set to the year after
7000 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7001 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7002 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7003 $inits = $tmpbase + $s['from'];
7004 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7005 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7006 $ends = $baseone + $s['to'];
7007 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7008 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7009 $ends = $baseone + $s['to'];
7010 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7011 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7012 $inits = $basetwo + $s['from'];
7013 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7014 // booking dates across two years for a season tied to a specific year
7015 if ($one['year'] == $s['year']) {
7016 $inits = $baseone + $s['from'];
7017 } else {
7018 $inits = $basetwo + $s['from'];
7019 }
7020 if ($two['year'] == $s['year']) {
7021 $ends = $basetwo + $s['to'];
7022 } else {
7023 $ends = $baseone + $s['to'];
7024 }
7025 }
7026 } else {
7027 // between 2 years
7028 if ($baseone < $basetwo) {
7029 // ex. 29/12/2012 - 14/01/2013
7030 $ends = $basetwo + $s['to'];
7031 } else {
7032 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7033 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7034 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7035 $ends = $tmpbase + $s['to'];
7036 } else {
7037 // ex. 03/01 - 09/01
7038 $ends = $basetwo + $s['to'];
7039 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7040 $inits = $tmpbase + $s['from'];
7041 }
7042 }
7043 }
7044
7045 // leap years
7046 if ($isleap == true) {
7047 $infoseason = getdate($inits);
7048 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7049 // leap years, check what dates to manipulate
7050 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7051 /**
7052 * Timestamp must be greater than the leap-day of Feb 29th.
7053 * It used to be checked for >= $leapts.
7054 *
7055 * @since July 3rd 2019
7056 */
7057 $inits += 86400;
7058 if ($s['from'] < $s['to']) {
7059 // increase season end date only if still on the same leap year
7060 $ends += 86400;
7061 }
7062 }
7063 }
7064
7065 // promotions
7066 $promotion = [];
7067 if (($s['promo'] ?? 0) == 1) {
7068 $daysadv = (($inits - time()) / 86400);
7069 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7070 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7071 continue;
7072 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7073 $secstocheckin = ($from - time());
7074 if ($s['promolastmin'] < $secstocheckin) {
7075 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7076 continue;
7077 }
7078 }
7079 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7080 /**
7081 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7082 *
7083 * @since 1.13.5
7084 */
7085 continue;
7086 }
7087 $promotion['todaydaysadv'] = $daysadv;
7088 $promotion['promodaysadv'] = $s['promodaysadv'];
7089 $promotion['promotxt'] = $s['promotxt'];
7090 }
7091
7092 // occupancy override
7093 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7094
7095 // week days
7096 $filterwdays = !empty($s['wdays']) ? true : false;
7097 $wdays = $filterwdays ? explode(';', $s['wdays']) : '';
7098 if (is_array($wdays) && $wdays) {
7099 foreach ($wdays as $kw => $wd) {
7100 if (strlen($wd) == 0) {
7101 unset($wdays[$kw]);
7102 }
7103 }
7104 }
7105
7106 // checkin must be after the beginning of the season
7107 $checkininclok = true;
7108 if ($s['checkinincl'] == 1) {
7109 $checkininclok = false;
7110 if ($s['from'] < $s['to']) {
7111 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7112 $checkininclok = true;
7113 }
7114 } else {
7115 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7116 $checkininclok = true;
7117 }
7118 }
7119 }
7120 if ($checkininclok !== true) {
7121 continue;
7122 }
7123
7124 foreach ($arr as $k => $a) {
7125 // applied only to some types of price
7126 if ($allprices && !empty($allprices[0])) {
7127 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7128 continue;
7129 }
7130 }
7131 // applied only to some room types
7132 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7133 continue;
7134 }
7135
7136 // count affected nights of stay
7137 $affdays = 0;
7138 $season_fromdayts = $fromdayts;
7139 $is_dst = date('I', $season_fromdayts);
7140 for ($i = 0; $i < $a[0]['days']; $i++) {
7141 $todayts = $season_fromdayts + ($i * 86400);
7142 $is_now_dst = date('I', $todayts);
7143 if ($is_dst != $is_now_dst) {
7144 // Daylight Saving Time has changed, check how
7145 if ((bool)$is_dst === true) {
7146 $todayts += 3600;
7147 $season_fromdayts += 3600;
7148 } else {
7149 $todayts -= 3600;
7150 $season_fromdayts -= 3600;
7151 }
7152 $is_dst = $is_now_dst;
7153 }
7154 if ($todayts >= $inits && $todayts <= $ends) {
7155 // week days
7156 if ($filterwdays == true) {
7157 $checkwday = getdate($todayts);
7158 if (in_array($checkwday['wday'], $wdays)) {
7159 $affdays++;
7160 }
7161 } else {
7162 $affdays++;
7163 }
7164 //
7165 }
7166 }
7167 if (!($affdays > 0)) {
7168 // no nights affected
7169 continue;
7170 }
7171
7172 // apply the rule
7173 $applyseasons = true;
7174 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7175
7176 // modification factor object
7177 $factor = new stdClass;
7178
7179 // calculate new price progressively
7180 if (intval($s['val_pcent']) == 2) {
7181 // percentage value
7182 $factor->pcent = 1;
7183 $pctval = $s['diffcost'];
7184 if (strlen((string)$s['losoverride'])) {
7185 // values overrides
7186 $arrvaloverrides = [];
7187 $valovrparts = explode('_', $s['losoverride']);
7188 foreach ($valovrparts as $valovr) {
7189 if (!empty($valovr)) {
7190 $ovrinfo = explode(':', $valovr);
7191 if (strstr($ovrinfo[0], '-i') != false) {
7192 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7193 if ((int)$ovrinfo[0] < $a[0]['days']) {
7194 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7195 }
7196 }
7197 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7198 }
7199 }
7200 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7201 $pctval = $arrvaloverrides[$a[0]['days']];
7202 }
7203 }
7204 if (intval($s['type']) == 1) {
7205 // charge
7206 $factor->type = '+';
7207 $cpercent = 100 + $pctval;
7208 } else {
7209 // discount
7210 $factor->type = '-';
7211 $cpercent = 100 - $pctval;
7212 }
7213 $factor->amount = $pctval;
7214 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7215 } else {
7216 // absolute value
7217 $factor->pcent = 0;
7218 $absval = $s['diffcost'];
7219 if (strlen((string)$s['losoverride'])) {
7220 // values overrides
7221 $arrvaloverrides = [];
7222 $valovrparts = explode('_', $s['losoverride']);
7223 foreach ($valovrparts as $valovr) {
7224 if (!empty($valovr)) {
7225 $ovrinfo = explode(':', $valovr);
7226 if (strstr($ovrinfo[0], '-i') != false) {
7227 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7228 if ((int)$ovrinfo[0] < $a[0]['days']) {
7229 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7230 }
7231 }
7232 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7233 }
7234 }
7235 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7236 $absval = $arrvaloverrides[$a[0]['days']];
7237 }
7238 }
7239 if (intval($s['type']) == 1) {
7240 // charge
7241 $factor->type = '+';
7242 $newprice = ($dailyprice + $absval) * $affdays;
7243 } else {
7244 // discount
7245 $factor->type = '-';
7246 $newprice = ($dailyprice - $absval) * $affdays;
7247 }
7248 $factor->amount = $absval;
7249 }
7250
7251 // apply rounding
7252 $factor->roundmode = $s['roundmode'] ?? null;
7253 if (!empty($s['roundmode'])) {
7254 $newprice = round($newprice, 0, constant($s['roundmode']));
7255 } else {
7256 $newprice = round($newprice, 2);
7257 }
7258
7259 // define the promotion (only if no value overrides set the amount to 0)
7260 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
7261 /**
7262 * Include the discount information (if any). The cost re-calculated may not be
7263 * precise if multiple special prices were applied over the same dates.
7264 *
7265 * @since 1.13
7266 */
7267 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7268 $promotion['discount'] = [
7269 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
7270 'pcent' => (int)($s['val_pcent'] == 2),
7271 ];
7272 }
7273 //
7274 $mem[$k]['promotion'] = $promotion;
7275 }
7276
7277 // define the occupancy override
7278 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
7279 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
7280 }
7281
7282 // push difference generated only if to be applied progressively
7283 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
7284 /**
7285 * Push the difference generated by this special price for later transliteration of final price,
7286 * only if the special price is calculated progressively and not on the final price.
7287 *
7288 * @since 1.13.5
7289 */
7290 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7291 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7292 /**
7293 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7294 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7295 * all differences applied by the previous special pricing rules.
7296 *
7297 * @since 1.13.5
7298 */
7299 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7300 array_push($mem[$k]['trans_factors'], $factor);
7301 array_push($mem[$k]['trans_affdays'], $affdays);
7302 }
7303
7304 // push values in memory array
7305 array_push($mem[$k]['sum'], $newprice);
7306 $mem[$k]['daysused'] += $affdays;
7307 array_push($roomschange, $a[0]['idroom']);
7308 }
7309 }
7310 if ($applyseasons) {
7311 foreach ($mem as $k => $v) {
7312 if ($v['daysused'] > 0 && $v['sum']) {
7313 $newprice = 0;
7314 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7315 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7316 $addrest = $restdays * $dailyprice;
7317 $newprice += $addrest;
7318
7319 // calculate new final cost
7320 $redo_rounding = null;
7321 foreach ($v['sum'] as $sum_index => $add) {
7322 /**
7323 * The application of the various special pricing rules is made in a progressive and cumulative way
7324 * by always starting from the room base cost or its average daily cost. However, promotions may need
7325 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7326 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7327 * onto the differences generated by all the regular and progressively applied special pricing rules.
7328 *
7329 * @since 1.13.5
7330 */
7331 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7332 /**
7333 * This progressive price difference must be applied on the room final cost, so we need to
7334 * apply the transliteration over the other differences applied by other special prices.
7335 */
7336 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7337 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7338 // this is the % promotion we are looking for applying it on the final cost
7339 $factor = $v['trans_factors'][$transliterate_key];
7340 if (is_object($factor) && $factor->pcent) {
7341 $final_factor = 0;
7342 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7343 $final_factor += $prog_diff * $factor->amount / 100;
7344 }
7345 // update rounding
7346 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7347
7348 /**
7349 * Leverage the final factor depending on how many nights this affected.
7350 *
7351 * @since 1.15.0 (J) - 1.5.0 (WP)
7352 */
7353 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7354 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7355 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7356 }
7357
7358 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7359 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7360 }
7361 }
7362 }
7363
7364 // apply new price progressively
7365 $newprice += $add;
7366 }
7367
7368 // apply rounding from factor
7369 if (!empty($redo_rounding)) {
7370 $newprice = round($newprice, 0, constant($redo_rounding));
7371 }
7372
7373 // set promotion (if any)
7374 if (isset($v['promotion'])) {
7375 $arr[$k][0]['promotion'] = $v['promotion'];
7376 }
7377
7378 // set occupancy overrides (if any)
7379 if (isset($v['occupancy_ovr'])) {
7380 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7381 }
7382
7383 // set new final cost and update nights affected
7384 $arr[$k][0]['cost'] = $newprice;
7385 $arr[$k][0]['affdays'] = $v['daysused'];
7386 }
7387 }
7388 }
7389 }
7390
7391 // week days with no season
7392 $roomschange = array_unique($roomschange);
7393 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7394 $dbo->setQuery($q);
7395 $specials = $dbo->loadAssocList();
7396 if ($specials) {
7397 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7398 $applyseasons = false;
7399 unset($mem);
7400 $mem = [];
7401 foreach ($arr as $k => $a) {
7402 $mem[$k]['daysused'] = 0;
7403 $mem[$k]['sum'] = [];
7404 }
7405 foreach ($specials as $s) {
7406 // check if this is a promotion registered for skipping
7407 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7408 continue;
7409 }
7410
7411 // Special Price tied to the year
7412 if (!empty($s['year']) && $s['year'] > 0) {
7413 if ($one['year'] != $s['year']) {
7414 continue;
7415 }
7416 }
7417
7418 $allrooms = explode(",", $s['idrooms']);
7419 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7420 // week days
7421 $filterwdays = !empty($s['wdays']) ? true : false;
7422 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7423 if (is_array($wdays) && $wdays) {
7424 foreach ($wdays as $kw => $wd) {
7425 if (strlen($wd) == 0) {
7426 unset($wdays[$kw]);
7427 }
7428 }
7429 }
7430
7431 foreach ($arr as $k => $a) {
7432 // only rooms with no price modifications from seasons
7433
7434 // applied only to some types of price
7435 if ($allprices && !empty($allprices[0])) {
7436 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7437 continue;
7438 }
7439 }
7440
7441 /**
7442 * We should not exclude the rooms that already had a modification of the price through a season
7443 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7444 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7445 *
7446 * @since 1.11
7447 */
7448 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7449 continue;
7450 }
7451
7452 $affdays = 0;
7453 $season_fromdayts = $fromdayts;
7454 $is_dst = date('I', $season_fromdayts);
7455 for ($i = 0; $i < $a[0]['days']; $i++) {
7456 $todayts = $season_fromdayts + ($i * 86400);
7457 $is_now_dst = date('I', $todayts);
7458 if ($is_dst != $is_now_dst) {
7459 // Daylight Saving Time has changed, check how
7460 if ((bool)$is_dst === true) {
7461 $todayts += 3600;
7462 $season_fromdayts += 3600;
7463 } else {
7464 $todayts -= 3600;
7465 $season_fromdayts -= 3600;
7466 }
7467 $is_dst = $is_now_dst;
7468 }
7469 // week days
7470 if ($filterwdays == true) {
7471 $checkwday = getdate($todayts);
7472 if (in_array($checkwday['wday'], $wdays)) {
7473 $affdays++;
7474 }
7475 }
7476 }
7477 if (!($affdays > 0)) {
7478 // no nights affected
7479 continue;
7480 }
7481
7482 // apply the rule
7483 $applyseasons = true;
7484 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7485
7486 if (intval($s['val_pcent']) == 2) {
7487 // percentage value
7488 $pctval = $s['diffcost'];
7489 if (strlen((string)$s['losoverride'])) {
7490 // values overrides
7491 $arrvaloverrides = [];
7492 $valovrparts = explode('_', $s['losoverride']);
7493 foreach ($valovrparts as $valovr) {
7494 if (!empty($valovr)) {
7495 $ovrinfo = explode(':', $valovr);
7496 if (strstr($ovrinfo[0], '-i') != false) {
7497 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7498 if ((int)$ovrinfo[0] < $a[0]['days']) {
7499 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7500 }
7501 }
7502 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7503 }
7504 }
7505 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7506 $pctval = $arrvaloverrides[$a[0]['days']];
7507 }
7508 }
7509 if (intval($s['type']) == 1) {
7510 // charge
7511 $cpercent = 100 + $pctval;
7512 } else {
7513 // discount
7514 $cpercent = 100 - $pctval;
7515 }
7516 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7517 } else {
7518 // absolute value
7519 $absval = $s['diffcost'];
7520 if (strlen((string)$s['losoverride'])) {
7521 // values overrides
7522 $arrvaloverrides = [];
7523 $valovrparts = explode('_', $s['losoverride']);
7524 foreach ($valovrparts as $valovr) {
7525 if (!empty($valovr)) {
7526 $ovrinfo = explode(':', $valovr);
7527 if (strstr($ovrinfo[0], '-i') != false) {
7528 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7529 if ((int)$ovrinfo[0] < $a[0]['days']) {
7530 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7531 }
7532 }
7533 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7534 }
7535 }
7536 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7537 $absval = $arrvaloverrides[$a[0]['days']];
7538 }
7539 }
7540 if (intval($s['type']) == 1) {
7541 // charge
7542 $newprice = ($dailyprice + $absval) * $affdays;
7543 } else {
7544 // discount
7545 $newprice = ($dailyprice - $absval) * $affdays;
7546 }
7547 }
7548
7549 // apply rounding
7550 if (!empty($s['roundmode'])) {
7551 $newprice = round($newprice, 0, constant($s['roundmode']));
7552 } else {
7553 $newprice = round($newprice, 2);
7554 }
7555
7556 // push values in memory array
7557 array_push($mem[$k]['sum'], $newprice);
7558 $mem[$k]['daysused'] += $affdays;
7559 }
7560 }
7561 if ($applyseasons) {
7562 foreach ($mem as $k => $v) {
7563 if ($v['daysused'] > 0 && $v['sum']) {
7564 $newprice = 0;
7565 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7566 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7567 $addrest = $restdays * $dailyprice;
7568 $newprice += $addrest;
7569 foreach ($v['sum'] as $add) {
7570 $newprice += $add;
7571 }
7572 $arr[$k][0]['cost'] = $newprice;
7573 $arr[$k][0]['affdays'] = $v['daysused'];
7574 }
7575 }
7576 }
7577 }
7578 // end week days with no season
7579
7580 return $arr;
7581 }
7582
7583 /**
7584 * Applies the special prices over an array of tariffs.
7585 * The function is also used by VCM (>= 1.6.5) with specific arguments.
7586 *
7587 * @param array $arr array of tariffs taken from the DB
7588 * @param int $from start timestamp
7589 * @param int $to end timestamp
7590 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
7591 * @param array $seasons_dates (VBO 1.10) array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7592 * @param array $seasons_wdays (VBO 1.10) array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7593 *
7594 * @return array
7595 */
7596 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], array $seasons_wdays = [])
7597 {
7598 static $cached_seasons = [];
7599
7600 $cache_signature = '';
7601 $cache_signature_wdays = '';
7602 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates) {
7603 // enable season records caching
7604 $cache_signature = "{$from}_{$to}";
7605 }
7606 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_wdays) {
7607 // enable week-day season records caching
7608 $cache_signature_wdays = "wdays";
7609 }
7610
7611 $dbo = JFactory::getDbo();
7612 $vbo_tn = self::getTranslator();
7613
7614 $roomschange = [];
7615 $one = getdate($from);
7616
7617 // leap years
7618 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7619 $isleap = true;
7620 } else {
7621 $isleap = false;
7622 }
7623
7624 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7625 $tomidnightone = intval($one['hours']) * 3600;
7626 $tomidnightone += intval($one['minutes']) * 60;
7627 $sfrom = $from - $baseone - $tomidnightone;
7628 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7629 $two = getdate($to);
7630 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7631 $tomidnighttwo = intval($two['hours']) * 3600;
7632 $tomidnighttwo += intval($two['minutes']) * 60;
7633 $sto = $to - $basetwo - $tomidnighttwo;
7634
7635 // leap years, check what dates to manipulate
7636 if ($isleap) {
7637 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7638 if ($one[0] > $leapts) {
7639 /**
7640 * Timestamp must be greater than the leap-day of Feb 29th.
7641 * It used to be checked for >= $leapts.
7642 *
7643 * @since July 3rd 2019
7644 */
7645 $sfrom -= 86400;
7646 }
7647 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7648 // lower checkin date when in leap year but not for checkout
7649 $sto -= 86400;
7650 }
7651 }
7652
7653 // check for DST changes to adjust the query values to fetch
7654 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7655 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7656 // we are parsing the day when the DST changed (probably a Sunday)
7657 if (!date('I', $from)) {
7658 // DST was just turned off
7659 $sfrom -= 3600;
7660 }
7661 }
7662 }
7663
7664 // count nights requested
7665 $booking_nights = 1;
7666 foreach ($arr as $k => $a) {
7667 if (isset($a['booking_nights'])) {
7668 // this value may be set when displaying pricing calendars
7669 $booking_nights = $a['booking_nights'];
7670 break;
7671 }
7672 if (isset($a['days'])) {
7673 $booking_nights = $a['days'];
7674 break;
7675 }
7676 }
7677
7678 /**
7679 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7680 *
7681 * @since 1.16.4 (J) - 1.6.4 (WP)
7682 */
7683 $skip_promo_ids = self::registerPromotionIds();
7684
7685 $totseasons = 0;
7686 if (!$parsed_season && !$seasons_dates) {
7687 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7688 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7689 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7690 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7691 ($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`)") .
7692 ($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`)") .
7693 ($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`)") .
7694 ($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`)") .
7695 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7696 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7697
7698 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
7699 // avoid making a query
7700 $seasons = $cached_seasons[$cache_signature];
7701 } else {
7702 // get the season records by running the query
7703 $dbo->setQuery($q);
7704 $seasons = $dbo->loadAssocList();
7705 }
7706
7707 // count total seasons
7708 $totseasons = $seasons ? count($seasons) : 0;
7709
7710 if ($cache_signature) {
7711 // cache season records
7712 $cached_seasons[$cache_signature] = $seasons;
7713 }
7714 }
7715
7716 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
7717 if ($totseasons > 0) {
7718 $seasons = $seasons;
7719 } elseif ($parsed_season) {
7720 $seasons = [$parsed_season];
7721 } else {
7722 $seasons = $seasons_dates;
7723 }
7724 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7725 $applyseasons = false;
7726 $mem = [];
7727 foreach ($arr as $k => $a) {
7728 $mem[$k]['daysused'] = 0;
7729 $mem[$k]['sum'] = [];
7730 $mem[$k]['spids'] = [];
7731 /**
7732 * The keys below are all needed to apply the promotions on the room's final cost.
7733 *
7734 * @since 1.13.5
7735 */
7736 $mem[$k]['diffs'] = [];
7737 $mem[$k]['trans_keys'] = [];
7738 $mem[$k]['trans_factors'] = [];
7739 $mem[$k]['trans_affdays'] = [];
7740 }
7741 $affdayslistless = [];
7742 foreach ($seasons as $s) {
7743 // 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
7744 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
7745 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
7746 continue;
7747 }
7748
7749 /**
7750 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
7751 * We need to skip such fake seasons as they do not need any parsing.
7752 *
7753 * @since 1.13
7754 */
7755 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
7756 continue;
7757 }
7758
7759 // check if this is a promotion registered for skipping
7760 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7761 continue;
7762 }
7763
7764 // Special Price tied to the year
7765 if (!empty($s['year']) && $s['year'] > 0) {
7766 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7767 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7768 //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
7769 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7770 continue;
7771 }
7772 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7773 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7774 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7775 if ($calc_ends < ($from - $tomidnightone)) {
7776 continue;
7777 }
7778 }
7779 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7780 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7781 if ($s['from'] > $s['to']) {
7782 continue;
7783 }
7784 if (($basetwo + $s['from'] + 86399) > $to) {
7785 /**
7786 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7787 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7788 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7789 *
7790 * @since 1.14.3 (J) - 1.4.3 (WP)
7791 */
7792 continue;
7793 }
7794 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7795 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7796 /**
7797 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7798 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7799 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7800 *
7801 * @since 1.14.3 (J) - 1.4.3 (WP)
7802 */
7803 continue;
7804 }
7805 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7806 // 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)
7807 if (($baseone + $s['from']) > $to) {
7808 continue;
7809 }
7810 }
7811 }
7812 //
7813 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7814 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7815 $inits = $baseone + $s['from'];
7816 if ($s['from'] < $s['to']) {
7817 $ends = $basetwo + $s['to'];
7818 // check if the inits must be set to the year after
7819 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7820 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7821 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7822 $inits = $tmpbase + $s['from'];
7823 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7824 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7825 $ends = $baseone + $s['to'];
7826 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7827 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7828 $ends = $baseone + $s['to'];
7829 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7830 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7831 $inits = $basetwo + $s['from'];
7832 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7833 // booking dates across two years for a season tied to a specific year
7834 if ($one['year'] == $s['year']) {
7835 $inits = $baseone + $s['from'];
7836 } else {
7837 $inits = $basetwo + $s['from'];
7838 }
7839 if ($two['year'] == $s['year']) {
7840 $ends = $basetwo + $s['to'];
7841 } else {
7842 $ends = $baseone + $s['to'];
7843 }
7844 }
7845 } else {
7846 // between 2 years
7847 if ($baseone < $basetwo) {
7848 // ex. 29/12/2012 - 14/01/2013
7849 $ends = $basetwo + $s['to'];
7850 } else {
7851 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7852 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7853 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7854 $ends = $tmpbase + $s['to'];
7855 } else {
7856 // ex. 03/01 - 09/01
7857 $ends = $basetwo + $s['to'];
7858 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7859 $inits = $tmpbase + $s['from'];
7860 }
7861 }
7862 }
7863 // leap years
7864 if ($isleap == true) {
7865 $infoseason = getdate($inits);
7866 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7867 // leap years, check what dates to manipulate
7868 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7869 /**
7870 * Timestamp must be greater than the leap-day of Feb 29th.
7871 * It used to be checked for >= $leapts.
7872 *
7873 * @since July 3rd 2019
7874 */
7875 $inits += 86400;
7876 if ($s['from'] < $s['to']) {
7877 // increase season end date only if still on the same leap year
7878 $ends += 86400;
7879 }
7880 }
7881 }
7882
7883 // promotions
7884 $promotion = [];
7885 if (($s['promo'] ?? 0) == 1) {
7886 $daysadv = (($inits - time()) / 86400);
7887 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7888 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7889 continue;
7890 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7891 $secstocheckin = ($from - time());
7892 if ($s['promolastmin'] < $secstocheckin) {
7893 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7894 continue;
7895 }
7896 }
7897 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7898 /**
7899 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7900 *
7901 * @since 1.13.5
7902 */
7903 continue;
7904 }
7905 $promotion['todaydaysadv'] = $daysadv;
7906 $promotion['promodaysadv'] = $s['promodaysadv'];
7907 $promotion['promotxt'] = $s['promotxt'];
7908 }
7909
7910 // occupancy override
7911 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7912
7913 //week days
7914 $filterwdays = !empty($s['wdays']) ? true : false;
7915 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7916 if (is_array($wdays) && $wdays) {
7917 foreach ($wdays as $kw => $wd) {
7918 if (strlen($wd) == 0) {
7919 unset($wdays[$kw]);
7920 }
7921 }
7922 }
7923
7924 // checkin must be after the begin of the season
7925 $checkininclok = true;
7926 if ($s['checkinincl'] == 1) {
7927 $checkininclok = false;
7928 if ($s['from'] < $s['to']) {
7929 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7930 $checkininclok = true;
7931 }
7932 } else {
7933 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7934 $checkininclok = true;
7935 }
7936 }
7937 }
7938 if ($checkininclok !== true) {
7939 continue;
7940 }
7941
7942 foreach ($arr as $k => $a) {
7943 // applied only to some types of price
7944 if ($allprices && !empty($allprices[0])) {
7945 // VikBooking 1.6: Price Calendar sets the idprice to -1
7946 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
7947 continue;
7948 }
7949 }
7950 // applied only to some room types
7951 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
7952 continue;
7953 }
7954
7955 if (isset($a['days']) && is_float($a['days'])) {
7956 // prevent any possible warning with array_key_exists()
7957 $a['days'] = (int)$a['days'];
7958 }
7959
7960 $affdays = 0;
7961 $season_fromdayts = $fromdayts;
7962 $is_dst = date('I', $season_fromdayts);
7963 for ($i = 0; $i < $a['days']; $i++) {
7964 $todayts = $season_fromdayts + ($i * 86400);
7965 $is_now_dst = date('I', $todayts);
7966 if ($is_dst != $is_now_dst) {
7967 // Daylight Saving Time has changed, check how
7968 if ((bool)$is_dst === true) {
7969 $todayts += 3600;
7970 $season_fromdayts += 3600;
7971 } else {
7972 $todayts -= 3600;
7973 $season_fromdayts -= 3600;
7974 }
7975 $is_dst = $is_now_dst;
7976 }
7977 if ($todayts >= $inits && $todayts <= $ends) {
7978 $checkwday = getdate($todayts);
7979 // week days
7980 if ($filterwdays == true) {
7981 if (in_array($checkwday['wday'], $wdays)) {
7982 if (!isset($arr[$k]['affdayslist'])) {
7983 $arr[$k]['affdayslist'] = [];
7984 }
7985 $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;
7986 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7987 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7988 $affdays++;
7989 }
7990 } else {
7991 if (!isset($arr[$k]['affdayslist'])) {
7992 $arr[$k]['affdayslist'] = [];
7993 }
7994 $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;
7995 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7996 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7997 $affdays++;
7998 }
7999 //
8000 }
8001 }
8002
8003 if (!($affdays > 0)) {
8004 // no nights affected
8005 continue;
8006 }
8007
8008 // apply the rule
8009 $applyseasons = true;
8010 $dailyprice = $a['cost'] / $a['days'];
8011
8012 // modification factor object
8013 $factor = new stdClass;
8014
8015 // calculate new price progressively
8016 if (intval($s['val_pcent']) == 2) {
8017 // percentage value
8018 $factor->pcent = 1;
8019 $pctval = $s['diffcost'];
8020 if (strlen((string)$s['losoverride'])) {
8021 // values overrides
8022 $arrvaloverrides = [];
8023 $valovrparts = explode('_', $s['losoverride']);
8024 foreach ($valovrparts as $valovr) {
8025 if (!empty($valovr)) {
8026 $ovrinfo = explode(':', $valovr);
8027 if (strstr($ovrinfo[0], '-i') != false) {
8028 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8029 if ((int)$ovrinfo[0] < $a['days']) {
8030 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8031 }
8032 }
8033 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8034 }
8035 }
8036 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8037 $pctval = $arrvaloverrides[$a['days']];
8038 }
8039 }
8040 if (intval($s['type']) == 1) {
8041 // charge
8042 $factor->type = '+';
8043 $cpercent = 100 + $pctval;
8044 } else {
8045 // discount
8046 $factor->type = '-';
8047 $cpercent = 100 - $pctval;
8048 }
8049 $factor->amount = $pctval;
8050 $dailysum = ($dailyprice * $cpercent / 100);
8051 $newprice = $dailysum * $affdays;
8052 } else {
8053 // absolute value
8054 $factor->pcent = 0;
8055 $absval = $s['diffcost'];
8056 if (strlen((string)$s['losoverride'])) {
8057 // values overrides
8058 $arrvaloverrides = [];
8059 $valovrparts = explode('_', $s['losoverride']);
8060 foreach ($valovrparts as $valovr) {
8061 if (!empty($valovr)) {
8062 $ovrinfo = explode(':', $valovr);
8063 if (strstr($ovrinfo[0], '-i') != false) {
8064 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8065 if ((int)$ovrinfo[0] < $a['days']) {
8066 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8067 }
8068 }
8069 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8070 }
8071 }
8072 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8073 $absval = $arrvaloverrides[$a['days']];
8074 }
8075 }
8076 if (intval($s['type']) == 1) {
8077 // charge
8078 $factor->type = '+';
8079 $dailysum = ($dailyprice + $absval);
8080 $newprice = $dailysum * $affdays;
8081 } else {
8082 // discount
8083 $factor->type = '-';
8084 $dailysum = ($dailyprice - $absval);
8085 $newprice = $dailysum * $affdays;
8086 }
8087 $factor->amount = $absval;
8088 }
8089
8090 // apply rounding
8091 $factor->roundmode = $s['roundmode'] ?? null;
8092 if (!empty($s['roundmode'])) {
8093 $newprice = round($newprice, 0, constant($s['roundmode']));
8094 } else {
8095 $newprice = round($newprice, 2);
8096 }
8097
8098 // define the promotion (only if no value overrides set the amount to 0)
8099 if ($promotion && (($absval ?? 0) > 0 || ($pctval ?? 0) > 0)) {
8100 /**
8101 * Include the discount information (if any). The cost re-calculated may not be
8102 * precise if multiple special prices were applied over the same dates.
8103 *
8104 * @since 1.13
8105 */
8106 if ($s['type'] == 2 && $s['diffcost'] > 0) {
8107 $promotion['discount'] = [
8108 'amount' => (($pctval ?? 0) > 0 ? $pctval : $s['diffcost']),
8109 'pcent' => (int)($s['val_pcent'] == 2),
8110 ];
8111 }
8112 //
8113 $mem[$k]['promotion'] = $promotion;
8114 }
8115
8116 // define the occupancy override
8117 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
8118 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
8119 }
8120
8121 // affected days list
8122 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
8123 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8124 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
8125 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
8126 }
8127 }
8128 }
8129
8130 // push special price ID
8131 if (!in_array($s['id'], $mem[$k]['spids'])) {
8132 array_push($mem[$k]['spids'], $s['id']);
8133 }
8134
8135 // push difference generated only if to be applied progressively
8136 if (!($s['promo'] ?? 0) || ($s['promo'] && !$s['promofinalprice'])) {
8137 /**
8138 * Push the difference generated by this special price for later transliteration of final price,
8139 * only if the special price is calculated progressively and not on the final price.
8140 *
8141 * @since 1.13.5
8142 */
8143 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
8144 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
8145 /**
8146 * This is a % promotion to be applied on the final price, so we need to save that this memory key
8147 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
8148 * all differences applied by the previous special pricing rules.
8149 *
8150 * @since 1.13.5
8151 */
8152 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
8153 array_push($mem[$k]['trans_factors'], $factor);
8154 array_push($mem[$k]['trans_affdays'], $affdays);
8155 }
8156
8157 // push values in memory array
8158 array_push($mem[$k]['sum'], $newprice);
8159 $mem[$k]['daysused'] += $affdays;
8160 array_push($roomschange, $a['idroom']);
8161 }
8162 }
8163 if ($applyseasons) {
8164 foreach ($mem as $k => $v) {
8165 if ($v['daysused'] > 0 && $v['sum']) {
8166 $newprice = 0;
8167 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8168 $restdays = $arr[$k]['days'] - $v['daysused'];
8169 $addrest = $restdays * $dailyprice;
8170 $newprice += $addrest;
8171
8172 // calculate new final cost
8173 $redo_rounding = null;
8174 foreach ($v['sum'] as $sum_index => $add) {
8175 /**
8176 * The application of the various special pricing rules is made in a progressive and cumulative way
8177 * by always starting from the room base cost or its average daily cost. However, promotions may need
8178 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
8179 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
8180 * onto the differences generated by all the regular and progressively applied special pricing rules.
8181 *
8182 * @since 1.13.5
8183 */
8184 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
8185 /**
8186 * This progressive price difference must be applied on the room final cost, so we need to
8187 * apply the transliteration over the other differences applied by other special prices.
8188 */
8189 $transliterate_key = array_search($sum_index, $v['trans_keys']);
8190 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
8191 // this is the % promotion we are looking for applying it on the final cost
8192 $factor = $v['trans_factors'][$transliterate_key];
8193 if (is_object($factor) && $factor->pcent) {
8194 $final_factor = 0;
8195 foreach ($v['diffs'] as $diff_index => $prog_diff) {
8196 $final_factor += $prog_diff * $factor->amount / 100;
8197 }
8198 // update rounding
8199 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
8200
8201 /**
8202 * Leverage the final factor depending on how many nights this affected.
8203 *
8204 * @since 1.15.0 (J) - 1.5.0 (WP)
8205 */
8206 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
8207 $avg_final_factor = $final_factor / $arr[$k]['days'];
8208 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
8209 }
8210
8211 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
8212 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
8213 }
8214 }
8215 }
8216
8217 // apply new price progressively
8218 $newprice += $add;
8219 }
8220
8221 // apply rounding from factor
8222 if (!empty($redo_rounding)) {
8223 $newprice = round($newprice, 0, constant($redo_rounding));
8224 }
8225
8226 // set promotion (if any)
8227 if (isset($v['promotion'])) {
8228 $arr[$k]['promotion'] = $v['promotion'];
8229 }
8230
8231 // set occupancy overrides (if any)
8232 if (isset($v['occupancy_ovr'])) {
8233 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
8234 }
8235
8236 // set new final cost and update nights affected
8237 $arr[$k]['cost'] = $newprice;
8238 $arr[$k]['affdays'] = $v['daysused'];
8239 if (array_key_exists('spids', $v) && $v['spids']) {
8240 $arr[$k]['spids'] = $v['spids'];
8241 }
8242 }
8243 }
8244 }
8245 }
8246
8247 // week days with no season
8248 $roomschange = array_unique($roomschange);
8249 $totspecials = 0;
8250 if (!$seasons_wdays) {
8251 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
8252
8253 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
8254 // avoid making a query
8255 $specials = $cached_seasons[$cache_signature_wdays];
8256 } else {
8257 // get the week-days-with-no-season records by running the query
8258 $dbo->setQuery($q);
8259 $specials = $dbo->loadAssocList();
8260 }
8261
8262 // count records
8263 $totspecials = $specials ? count($specials) : 0;
8264
8265 if ($cache_signature_wdays) {
8266 // cache week-days-with-no-season records
8267 $cached_seasons[$cache_signature_wdays] = $specials;
8268 }
8269 }
8270 if ($totspecials > 0 || $seasons_wdays) {
8271 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
8272 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
8273 $applyseasons = false;
8274 /**
8275 * We no longer unset the previous memory of the seasons with dates filters
8276 * because we need the responses to be merged. We do it only if not set.
8277 * We only keep the property 'spids' but the others should be unset.
8278 *
8279 * @since 1.11
8280 */
8281 if (!isset($mem)) {
8282 $mem = [];
8283 foreach ($arr as $k => $a) {
8284 $mem[$k]['daysused'] = 0;
8285 $mem[$k]['sum'] = [];
8286 $mem[$k]['spids'] = [];
8287 }
8288 } else {
8289 foreach ($arr as $k => $a) {
8290 $mem[$k]['daysused'] = 0;
8291 $mem[$k]['sum'] = [];
8292 }
8293 }
8294 //
8295 foreach ($specials as $s) {
8296 // 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
8297 if (!empty($s['from']) || !empty($s['to'])) {
8298 continue;
8299 }
8300
8301 // check if this is a promotion registered for skipping
8302 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8303 continue;
8304 }
8305
8306 // Special Price tied to the year
8307 if (!empty($s['year']) && $s['year'] > 0) {
8308 if ($one['year'] != $s['year']) {
8309 continue;
8310 }
8311 }
8312
8313 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8314 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8315 // week days
8316 $filterwdays = !empty($s['wdays']) ? true : false;
8317 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8318 if (is_array($wdays) && $wdays) {
8319 foreach ($wdays as $kw => $wd) {
8320 if (strlen($wd) == 0) {
8321 unset($wdays[$kw]);
8322 }
8323 }
8324 }
8325
8326 foreach ($arr as $k => $a) {
8327 // only rooms with no price modifications from seasons
8328
8329 // applied only to some types of price
8330 if ($allprices && !empty($allprices[0])) {
8331 // VikBooking 1.6: Price Calendar sets the idprice to -1
8332 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8333 continue;
8334 }
8335 }
8336
8337 /**
8338 * We should not exclude the rooms that already had a modification of the price through a season
8339 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8340 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8341 *
8342 * @since 1.11
8343 */
8344 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8345 continue;
8346 }
8347
8348 if (isset($a['days']) && is_float($a['days'])) {
8349 // prevent any possible warning with array_key_exists()
8350 $a['days'] = (int)$a['days'];
8351 }
8352
8353 $affdays = 0;
8354 $season_fromdayts = $fromdayts;
8355 $is_dst = date('I', $season_fromdayts);
8356 for ($i = 0; $i < $a['days']; $i++) {
8357 $todayts = $season_fromdayts + ($i * 86400);
8358 $is_now_dst = date('I', $todayts);
8359 if ($is_dst != $is_now_dst) {
8360 // Daylight Saving Time has changed, check how
8361 if ((bool)$is_dst === true) {
8362 $todayts += 3600;
8363 $season_fromdayts += 3600;
8364 } else {
8365 $todayts -= 3600;
8366 $season_fromdayts -= 3600;
8367 }
8368 $is_dst = $is_now_dst;
8369 }
8370 // week days
8371 if ($filterwdays == true) {
8372 $checkwday = getdate($todayts);
8373 if (in_array($checkwday['wday'], $wdays)) {
8374 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8375 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8376 $affdays++;
8377 }
8378 }
8379 //
8380 }
8381
8382 if (!($affdays > 0)) {
8383 // no nights affected
8384 continue;
8385 }
8386
8387 // apply the rule
8388 $applyseasons = true;
8389 $dailyprice = $a['cost'] / $a['days'];
8390
8391 if (intval($s['val_pcent']) == 2) {
8392 // percentage value
8393 $pctval = $s['diffcost'];
8394 if (strlen((string)$s['losoverride'])) {
8395 // values overrides
8396 $arrvaloverrides = [];
8397 $valovrparts = explode('_', $s['losoverride']);
8398 foreach ($valovrparts as $valovr) {
8399 if (!empty($valovr)) {
8400 $ovrinfo = explode(':', $valovr);
8401 if (strstr($ovrinfo[0], '-i') != false) {
8402 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8403 if ((int)$ovrinfo[0] < $a['days']) {
8404 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8405 }
8406 }
8407 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8408 }
8409 }
8410 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8411 $pctval = $arrvaloverrides[$a['days']];
8412 }
8413 }
8414 if (intval($s['type']) == 1) {
8415 // charge
8416 $cpercent = 100 + $pctval;
8417 } else {
8418 // discount
8419 $cpercent = 100 - $pctval;
8420 }
8421 $dailysum = ($dailyprice * $cpercent / 100);
8422 $newprice = $dailysum * $affdays;
8423 } else {
8424 // absolute value
8425 $absval = $s['diffcost'];
8426 if (strlen((string)$s['losoverride'])) {
8427 // values overrides
8428 $arrvaloverrides = [];
8429 $valovrparts = explode('_', $s['losoverride']);
8430 foreach ($valovrparts as $valovr) {
8431 if (!empty($valovr)) {
8432 $ovrinfo = explode(':', $valovr);
8433 if (strstr($ovrinfo[0], '-i') != false) {
8434 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8435 if ((int)$ovrinfo[0] < $a['days']) {
8436 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8437 }
8438 }
8439 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8440 }
8441 }
8442 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8443 $absval = $arrvaloverrides[$a['days']];
8444 }
8445 }
8446 if (intval($s['type']) == 1) {
8447 // charge
8448 $dailysum = ($dailyprice + $absval);
8449 $newprice = $dailysum * $affdays;
8450 } else {
8451 // discount
8452 $dailysum = ($dailyprice - $absval);
8453 $newprice = $dailysum * $affdays;
8454 }
8455 }
8456
8457 // apply rounding
8458 if (!empty($s['roundmode'])) {
8459 $newprice = round($newprice, 0, constant($s['roundmode']));
8460 } else {
8461 $newprice = round($newprice, 2);
8462 }
8463
8464 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8465 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8466 }
8467 if (!in_array($s['id'], $mem[$k]['spids'])) {
8468 $mem[$k]['spids'][] = $s['id'];
8469 }
8470 $mem[$k]['sum'][] = $newprice;
8471 $mem[$k]['daysused'] += $affdays;
8472 }
8473 }
8474 if ($applyseasons) {
8475 foreach ($mem as $k => $v) {
8476 if ($v['daysused'] > 0 && $v['sum']) {
8477 $newprice = 0;
8478 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8479 $restdays = $arr[$k]['days'] - $v['daysused'];
8480 $addrest = $restdays * $dailyprice;
8481 $newprice += $addrest;
8482 foreach ($v['sum'] as $add) {
8483 $newprice += $add;
8484 }
8485 $arr[$k]['cost'] = $newprice;
8486 $arr[$k]['affdays'] = $v['daysused'];
8487 if (array_key_exists('spids', $v) && $v['spids']) {
8488 $arr[$k]['spids'] = $v['spids'];
8489 }
8490 }
8491 }
8492 }
8493 }
8494 // end week days with no season
8495
8496 return $arr;
8497 }
8498
8499 public static function getRoomRplansClosingDates($idroom)
8500 {
8501 $dbo = JFactory::getDbo();
8502
8503 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8504 $dbo->setQuery($q);
8505 $price_records = $dbo->loadAssocList();
8506 if (!$price_records) {
8507 return [];
8508 }
8509
8510 $closingd = [];
8511 foreach ($price_records as $prec) {
8512 if (empty($prec['closingd'])) {
8513 continue;
8514 }
8515 $price_closing = json_decode($prec['closingd'], true);
8516 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8517 continue;
8518 }
8519 // check expired dates and clean up
8520 $today_midnight = mktime(0, 0, 0);
8521 $cleaned = false;
8522 foreach ($price_closing[$idroom] as $k => $v) {
8523 if (strtotime($v) < $today_midnight) {
8524 $cleaned = true;
8525 unset($price_closing[$idroom][$k]);
8526 }
8527 }
8528 if (!$price_closing[$idroom]) {
8529 unset($price_closing[$idroom]);
8530 } elseif ($cleaned === true) {
8531 // reset array keys for smaller JSON size
8532 $price_closing[$idroom] = array_values($price_closing[$idroom]);
8533 }
8534 if ($cleaned === true) {
8535 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
8536 $dbo->setQuery($q);
8537 $dbo->execute();
8538 }
8539 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8540 continue;
8541 }
8542 $closingd[$prec['id']] = $price_closing[$idroom];
8543 }
8544
8545 return $closingd;
8546 }
8547
8548 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
8549 {
8550 $dbo = JFactory::getDbo();
8551
8552 $closingd = [];
8553
8554 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8555 $dbo->setQuery($q);
8556 $price_records = $dbo->loadAssocList();
8557 if ($price_records && $roomids) {
8558 $info_start = getdate($checkints);
8559 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
8560 $all_nights = [];
8561 for ($i = 0; $i < (int)$numnights; $i++) {
8562 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
8563 $all_nights[] = date('Y-m-d', $next_midnight);
8564 }
8565 foreach ($price_records as $prec) {
8566 if (empty($prec['closingd'])) {
8567 continue;
8568 }
8569 $price_closing = json_decode($prec['closingd'], true);
8570 if (!$price_closing) {
8571 continue;
8572 }
8573 foreach ($price_closing as $idroom => $rclosedd) {
8574 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
8575 continue;
8576 }
8577 if (!array_key_exists($idroom, $closingd)) {
8578 $closingd[$idroom] = [];
8579 }
8580 foreach ($all_nights as $night) {
8581 if (in_array($night, $rclosedd)) {
8582 if (array_key_exists($prec['id'], $closingd[$idroom])) {
8583 $closingd[$idroom][$prec['id']][] = $night;
8584 } else {
8585 $closingd[$idroom][$prec['id']] = [$night];
8586 }
8587 }
8588 }
8589 }
8590 }
8591 }
8592
8593 return $closingd;
8594 }
8595
8596 /**
8597 * Tells whether at least one payment method must be selected to check-out.
8598 *
8599 * @param array $rooms_involved optional list of room IDs being booked.
8600 *
8601 * @return bool true if at least one payment method is applicable.
8602 *
8603 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
8604 */
8605 public static function areTherePayments($rooms_involved = [])
8606 {
8607 $dbo = JFactory::getDbo();
8608
8609 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
8610 $dbo->setQuery($q);
8611 $dbo->execute();
8612
8613 if (!$dbo->getNumRows()) {
8614 // no published payment options found
8615 return false;
8616 }
8617
8618 $pay_options = $dbo->loadAssocList();
8619
8620 if (!count($rooms_involved)) {
8621 // if no filters given, return true
8622 return true;
8623 }
8624
8625 // validate room associations
8626 foreach ($pay_options as $pay_k => $pay_v) {
8627 if (empty($pay_v['idrooms'])) {
8628 continue;
8629 }
8630 $rooms_supported = json_decode($pay_v['idrooms']);
8631 if (!is_array($rooms_supported) || !count($rooms_supported)) {
8632 continue;
8633 }
8634 // make sure all rooms being booked are supported
8635 foreach ($rooms_involved as $room_id_booked) {
8636 if (!in_array($room_id_booked, $rooms_supported)) {
8637 // we need to unset this payment option as soon as we find one non-matching room ID
8638 unset($pay_options[$pay_k]);
8639 break;
8640 }
8641 }
8642 }
8643
8644 return (bool)count($pay_options);
8645 }
8646
8647 public static function getPayment($idp, $vbo_tn = null)
8648 {
8649 if (empty($idp)) {
8650 return false;
8651 }
8652
8653 if (strstr((string)$idp, '=') !== false) {
8654 $parts = explode('=', $idp);
8655 $idp = $parts[0];
8656 }
8657
8658 $dbo = JFactory::getDbo();
8659
8660 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
8661 $dbo->setQuery($q);
8662 $payment = $dbo->loadAssocList();
8663 if (!$payment) {
8664 return false;
8665 }
8666
8667 if (is_object($vbo_tn)) {
8668 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
8669 }
8670
8671 return $payment[0];
8672 }
8673
8674 public static function getCronKey()
8675 {
8676 return VBOFactory::getConfig()->get('cronkey', '');
8677 }
8678
8679 /**
8680 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
8681 *
8682 * @return int
8683 *
8684 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
8685 */
8686 public static function getNextInvoiceNumber()
8687 {
8688 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
8689 }
8690
8691 public static function getInvoiceNumberSuffix()
8692 {
8693 return VBOFactory::getConfig()->get('invoicesuffix', '');
8694 }
8695
8696 public static function getInvoiceCompanyInfo()
8697 {
8698 return VBOFactory::getConfig()->get('invcompanyinfo', '');
8699 }
8700
8701 /**
8702 * Gets the number for the next booking receipt generation,
8703 * updates the last receipt number used for the later iterations,
8704 * stores a new receipt record to keep track of the receipts issued.
8705 *
8706 * @param int $bid the Booking ID for which we are/want to generating the receipt.
8707 * @param [int] $newnum the last number used for generating the receipt.
8708 *
8709 * @return int
8710 */
8711 public static function getNextReceiptNumber ($bid, $newnum = false) {
8712 $dbo = JFactory::getDbo();
8713 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
8714 $dbo->setQuery($q);
8715 $dbo->execute();
8716 if ($dbo->getNumRows() > 0) {
8717 $s = $dbo->loadResult();
8718 //check if this booking has already a receipt, and return that number
8719 $prev_receipt = array();
8720 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
8721 $dbo->setQuery($q);
8722 $dbo->execute();
8723 if ($dbo->getNumRows() > 0) {
8724 $prev_receipt = $dbo->loadAssoc();
8725 }
8726 //update value (receipt generated)
8727 if ($newnum !== false && $newnum > 0) {
8728 $s = (int)$newnum;
8729 if (!(count($prev_receipt) > 0)) {
8730 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
8731 $dbo->setQuery($q);
8732 $dbo->execute();
8733 //insert the new receipt record
8734 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
8735 $dbo->setQuery($q);
8736 $dbo->execute();
8737 } else {
8738 //update receipt record
8739 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
8740 $dbo->setQuery($q);
8741 $dbo->execute();
8742 }
8743 }
8744 //
8745 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
8746 }
8747 //first execution of the method should create the configuration record
8748 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
8749 $dbo->setQuery($q);
8750 $dbo->execute();
8751 return 1;
8752 }
8753
8754 public static function getReceiptNotes ($newnotes = false) {
8755 $dbo = JFactory::getDbo();
8756 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
8757 $dbo->setQuery($q);
8758 $dbo->execute();
8759 if ($dbo->getNumRows() > 0) {
8760 $s = $dbo->loadResult();
8761 //update value
8762 if ($newnotes !== false) {
8763 $s = $newnotes;
8764 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
8765 $dbo->setQuery($q);
8766 $dbo->execute();
8767 }
8768 //
8769 return $s;
8770 }
8771 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
8772 $dbo->setQuery($q);
8773 $dbo->execute();
8774 return "";
8775 }
8776
8777 public static function loadColorTagsRules()
8778 {
8779 $color_tag_rules = [
8780 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
8781 'pend1' => 'VBWAITINGFORPAYMENT',
8782 'conf1' => 'VBDBTEXTROOMCLOSED',
8783 'conf2' => 'VBOCOLORTAGRULECONFTWO',
8784 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
8785 'inv1' => 'VBOCOLORTAGRULEINVONE',
8786 'rcp1' => 'VBOCOLORTAGRULERCPONE',
8787 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
8788 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
8789 'inv2' => 'VBOCOLORTAGRULEINVTWO',
8790 ];
8791
8792 /**
8793 * Trigger event to let third-party plugins override the default booking color tag rules.
8794 *
8795 * @since 1.16.4 (J) - 1.6.4 (WP)
8796 */
8797 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
8798
8799 return $color_tag_rules;
8800 }
8801
8802 public static function loadDefaultColorTags()
8803 {
8804 $default_color_tags = [
8805 [
8806 'color' => '#9b9b9b',
8807 'name' => 'VBWAITINGFORPAYMENT',
8808 'rule' => 'pend1'
8809 ],
8810 [
8811 'color' => '#333333',
8812 'name' => 'VBDBTEXTROOMCLOSED',
8813 'rule' => 'conf1'
8814 ],
8815 [
8816 'color' => '#ff8606',
8817 'name' => 'VBOCOLORTAGRULECONFTWO',
8818 'rule' => 'conf2'
8819 ],
8820 [
8821 'color' => '#0418c9',
8822 'name' => 'VBOCOLORTAGRULECONFTHREE',
8823 'rule' => 'conf3'
8824 ],
8825 [
8826 'color' => '#bed953',
8827 'name' => 'VBOCOLORTAGRULEINVONE',
8828 'rule' => 'inv1'
8829 ],
8830 [
8831 'color' => '#67f5b5',
8832 'name' => 'VBOCOLORTAGRULERCPONE',
8833 'rule' => 'rcp1'
8834 ],
8835 [
8836 'color' => '#04d2c2',
8837 'name' => 'VBOCOLORTAGRULECONFFOUR',
8838 'rule' => 'conf4'
8839 ],
8840 [
8841 'color' => '#00b316',
8842 'name' => 'VBOCOLORTAGRULECONFFIVE',
8843 'rule' => 'conf5'
8844 ],
8845 [
8846 'color' => '#00f323',
8847 'name' => 'VBOCOLORTAGRULEINVTWO',
8848 'rule' => 'inv2'
8849 ],
8850 ];
8851
8852 /**
8853 * Trigger event to let third-party plugins override the default color tags.
8854 *
8855 * @since 1.16.4 (J) - 1.6.4 (WP)
8856 */
8857 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
8858
8859 return $default_color_tags;
8860 }
8861
8862 public static function loadBookingsColorTags()
8863 {
8864 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
8865
8866 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
8867 }
8868
8869 public static function getBestColorContrast($hexcolor)
8870 {
8871 $hexcolor = str_replace('#', '', $hexcolor);
8872
8873 if (empty($hexcolor) || strlen($hexcolor) != 6) {
8874 return '#000000';
8875 }
8876
8877 $r = hexdec(substr($hexcolor, 0, 2));
8878 $g = hexdec(substr($hexcolor, 2, 2));
8879 $b = hexdec(substr($hexcolor, 4, 2));
8880
8881 // Counting the perceptive luminance - human eye favors green color
8882 // < 0.5 bright colors
8883 // > 0.5 dark colors
8884
8885 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
8886 }
8887
8888 public static function applyBookingColorTag($booking, $tags = [])
8889 {
8890 if (!is_array($tags) || !$tags) {
8891 $tags = self::loadBookingsColorTags();
8892 }
8893
8894 if (!empty($booking['colortag'])) {
8895 $color_tag_arr = json_decode($booking['colortag'], true);
8896 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
8897 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
8898 return $color_tag_arr;
8899 }
8900 }
8901
8902 $dbo = JFactory::getDbo();
8903
8904 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
8905 $docs_data = [];
8906 $invoice_numb = false;
8907 $receipt_numb = false;
8908
8909 if ($booking['status'] == 'confirmed') {
8910 $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`
8911 FROM `#__vikbooking_orders` AS `b`
8912 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
8913 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
8914 WHERE `b`.`id`=" . (int)$bid . ";";
8915 $dbo->setQuery($q);
8916 $docs_data = $dbo->loadAssoc();
8917 if ($docs_data) {
8918 $invoice_numb = (!empty($docs_data['inv_id']));
8919 $receipt_numb = (!empty($docs_data['rcp_id']));
8920
8921 /**
8922 * We inject the invoice number or receipt number into the color tag rules.
8923 * This is useful for the Tableaux page to show the invoice/receipt number.
8924 *
8925 * @since 1.12 (J) - 1.1.7 (WP)
8926 */
8927 if ($invoice_numb || $receipt_numb) {
8928 foreach ($tags as &$tval) {
8929 if ($invoice_numb) {
8930 $tval['invoice_number'] = $docs_data['inv_number'];
8931 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
8932 }
8933 if ($receipt_numb) {
8934 $tval['receipt_number'] = $docs_data['rcp_number'];
8935 }
8936 }
8937 // unset last reference
8938 unset($tval);
8939 }
8940 }
8941 }
8942
8943 /**
8944 * Some OTA bookings using an OTA-Collect business model may look as not entirely
8945 * paid when they actually are, due to commissions and pass through taxes. We also
8946 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
8947 *
8948 * @since 1.16.3 (J) - 1.6.3 (WP)
8949 */
8950 $ota_collected_booking = false;
8951 $hotel_collect_vcc_unpaid = false;
8952 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
8953 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
8954 $ota_collected_booking = true;
8955 }
8956 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
8957 // VCC detected, check the history for a payment event
8958 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
8959 // list of history types to fetch
8960 $h_payment_types = [];
8961 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
8962 if ($h_payment_group && $h_payment_group['types']) {
8963 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
8964 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
8965 }
8966 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
8967 if (!$payment_ev_datas) {
8968 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
8969 $hotel_collect_vcc_unpaid = true;
8970 }
8971 }
8972 }
8973
8974 foreach ($tags as $tkey => $tval) {
8975 if (empty($tval['rule'])) {
8976 continue;
8977 }
8978
8979 /**
8980 * Trigger event to let third-party plugins validate the custom rule.
8981 *
8982 * @since 1.16.4 (J) - 1.6.4 (WP)
8983 */
8984 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
8985 if (!empty($custom_rule)) {
8986 return (array) $custom_rule[0];
8987 }
8988
8989 switch ($tval['rule']) {
8990 case 'pend1':
8991 // Room is waiting for the payment (locked record)
8992 if ($booking['status'] == 'standby') {
8993 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
8994 $dbo->setQuery($q);
8995 $tmp_lock = $dbo->loadAssoc();
8996 if ($tmp_lock) {
8997 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8998 return $tval;
8999 }
9000 }
9001 break;
9002 case 'conf1':
9003 // Confirmed (Room Closed)
9004 if ($booking['status'] == 'confirmed' && $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED')) {
9005 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9006 return $tval;
9007 }
9008 break;
9009 case 'conf2':
9010 // Confirmed (No Rate 0.00/NULL Total)
9011 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
9012 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9013 return $tval;
9014 }
9015 break;
9016 case 'conf3':
9017 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
9018 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) {
9019 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9020 return $tval;
9021 }
9022 break;
9023 case 'inv1':
9024 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
9025 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
9026 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9027 return $tval;
9028 }
9029 break;
9030 case 'rcp1':
9031 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
9032 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
9033 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9034 return $tval;
9035 }
9036 break;
9037 case 'conf4':
9038 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
9039 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
9040 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9041 return $tval;
9042 }
9043 break;
9044 case 'conf5':
9045 // Confirmed (Total > 0 && Total Paid >= Total)
9046 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
9047 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9048 return $tval;
9049 }
9050 break;
9051 case 'inv2':
9052 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
9053 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
9054 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
9055 return $tval;
9056 }
9057 break;
9058 default:
9059 // unsupported rule
9060 break;
9061 }
9062 }
9063
9064 return [];
9065 }
9066
9067 public static function getBookingInfoFromID($bid)
9068 {
9069 $dbo = JFactory::getDbo();
9070
9071 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
9072 $dbo->setQuery($q);
9073 $booking_info = $dbo->loadAssoc();
9074 if ($booking_info) {
9075 return $booking_info;
9076 }
9077
9078 return [];
9079 }
9080
9081 /**
9082 * Load booking details for a given list of IDs.
9083 *
9084 * @param int $roomid the room integer number.
9085 * @param array $room_bids_pool list of booking IDs involved.
9086 *
9087 * @return array
9088 */
9089 public static function loadRoomIndexesBookings($roomid, array $room_bids_pool)
9090 {
9091 $dbo = JFactory::getDbo();
9092
9093 if (empty($roomid) || !$room_bids_pool) {
9094 return [];
9095 }
9096
9097 $room_features_bookings = [];
9098 $roomid = (int)$roomid;
9099
9100 $all_bids = [];
9101 foreach ($room_bids_pool as $day => $bids) {
9102 $all_bids = array_merge($all_bids, $bids);
9103 }
9104 $all_bids = array_map('intval', array_unique($all_bids));
9105 $all_bids_str = implode(', ', $all_bids);
9106
9107 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
9108 $dbo->setQuery($q);
9109 $rbookings = $dbo->loadAssocList();
9110 if (!$rbookings) {
9111 return [];
9112 }
9113
9114 foreach ($rbookings as $k => $v) {
9115 if (empty($v['roomindex'])) {
9116 continue;
9117 }
9118 if (!isset($room_features_bookings[$v['roomindex']])) {
9119 $room_features_bookings[$v['roomindex']] = [];
9120 }
9121 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
9122 }
9123
9124 return $room_features_bookings;
9125 }
9126
9127 public static function getSendEmailWhen()
9128 {
9129 return VBOFactory::getConfig()->getInt('emailsendwhen', 1);
9130 }
9131
9132 public static function getMinutesAutoRemove()
9133 {
9134 return VBOFactory::getConfig()->getInt('minautoremove', 0);
9135 }
9136
9137 public static function getSMSAPIClass()
9138 {
9139 return VBOFactory::getConfig()->getString('smsapi', '');
9140 }
9141
9142 public static function autoSendSMSEnabled()
9143 {
9144 return VBOFactory::getConfig()->getBool('smsautosend', false);
9145 }
9146
9147 public static function getSendSMSTo()
9148 {
9149 return (array) VBOFactory::getConfig()->getArray('smssendto', []);
9150 }
9151
9152 public static function getSendSMSWhen()
9153 {
9154 return VBOFactory::getConfig()->getInt('smssendwhen', 1);
9155 }
9156
9157 public static function getSMSAdminPhone()
9158 {
9159 return VBOFactory::getConfig()->getString('smsadminphone', '');
9160 }
9161
9162 public static function getSMSParams($as_array = true)
9163 {
9164 if (!$as_array) {
9165 return VBOFactory::getConfig()->getString('smsparams', '');
9166 }
9167
9168 return (array) VBOFactory::getConfig()->getArray('smsparams', []);
9169 }
9170
9171 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
9172 $dbo = JFactory::getDbo();
9173 switch (strtolower($booking_status)) {
9174 case 'standby':
9175 $status = 'pend';
9176 break;
9177 case 'cancelled':
9178 $status = 'canc';
9179 break;
9180 default:
9181 $status = '';
9182 break;
9183 }
9184 $paramtype = 'sms'.$type.'tpl'.$status;
9185 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
9186 $dbo->setQuery($q);
9187 $dbo->execute();
9188 if ($dbo->getNumRows() < 1) {
9189 if ($status == 'canc') {
9190 //Type cancelled is used by VCM since v1.6.6
9191 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
9192 $dbo->setQuery($q);
9193 $dbo->execute();
9194 }
9195 return '';
9196 }
9197 $ft = $dbo->loadAssocList();
9198 if (is_object($vbo_tn)) {
9199 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
9200 }
9201 return $ft[0]['setting'];
9202 }
9203
9204 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9205 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
9206 }
9207
9208 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
9209 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
9210 }
9211
9212 public static function checkPhonePrefixCountry($phone, $country_threecode) {
9213 $dbo = JFactory::getDbo();
9214 $phone = str_replace(" ", '', trim($phone));
9215 $cprefix = '';
9216 if (!empty($country_threecode)) {
9217 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
9218 $dbo->setQuery($q);
9219 $dbo->execute();
9220 if ($dbo->getNumRows() > 0) {
9221 $cprefix = $dbo->loadResult();
9222 $cprefix = str_replace(" ", '', trim($cprefix));
9223 }
9224 }
9225 if (!empty($cprefix)) {
9226 if (substr($phone, 0, 1) != '+') {
9227 if (substr($phone, 0, 2) == '00') {
9228 $phone = '+'.substr($phone, 2);
9229 } else {
9230 $phone = $cprefix.$phone;
9231 }
9232 }
9233 }
9234 return $phone;
9235 }
9236
9237 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9238 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9239
9240 /**
9241 * Trigger event to allow third-party plugins to manipulate the template string.
9242 *
9243 * @since 1.16.10 (J) - 1.6.10 (WP)
9244 */
9245 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseAdminSMSTemplate', [$tpl, $booking, $booking_rooms]);
9246
9247 /**
9248 * Parse all conditional text rules.
9249 *
9250 * @since 1.14 (J) - 1.4.0 (WP)
9251 */
9252 self::getConditionalRulesInstance()
9253 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9254 ->parseTokens($tpl);
9255
9256 $vbo_df = self::getDateFormat();
9257 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9258 $datesep = self::getDateSeparator();
9259
9260 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9261 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9262 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9263 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9264 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9265 if (!empty($booking['confirmnumber'])) {
9266 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9267 } else {
9268 $tpl = str_replace("{confirmnumb}", '', $tpl);
9269 }
9270 if (!empty($booking['idorderota'])) {
9271 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9272 } else {
9273 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9274 }
9275
9276 $rooms_booked = array();
9277 $rooms_names = array();
9278 $tot_adults = 0;
9279 $tot_children = 0;
9280 $tot_guests = 0;
9281 foreach ($booking_rooms as $broom) {
9282 $rooms_names[] = $broom['room_name'];
9283 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9284 $rooms_booked[$broom['room_name']] += 1;
9285 } else {
9286 $rooms_booked[$broom['room_name']] = 1;
9287 }
9288 $tot_adults += (int)$broom['adults'];
9289 $tot_children += (int)$broom['children'];
9290 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9291 }
9292 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9293 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9294 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9295 $rooms_booked_quant = array();
9296 foreach ($rooms_booked as $rname => $quant) {
9297 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9298 }
9299 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9300 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9301 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9302 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9303 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9304 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9305 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9306 $remaining_bal = $booking['total'] - $booking['totpaid'];
9307 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9308
9309 return $tpl;
9310 }
9311
9312 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9313 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9314
9315 /**
9316 * Trigger event to allow third-party plugins to manipulate the template string.
9317 *
9318 * @since 1.16.10 (J) - 1.6.10 (WP)
9319 */
9320 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseCustomerSMSTemplate', [$tpl, $booking, $booking_rooms]);
9321
9322 /**
9323 * Parse all conditional text rules.
9324 *
9325 * @since 1.14 (J) - 1.4.0 (WP)
9326 */
9327 self::getConditionalRulesInstance()
9328 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9329 ->parseTokens($tpl);
9330
9331 $vbo_df = self::getDateFormat();
9332 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9333 $datesep = self::getDateSeparator();
9334
9335 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9336 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9337 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9338 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9339 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9340 if (!empty($booking['confirmnumber'])) {
9341 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9342 } else {
9343 $tpl = str_replace("{confirmnumb}", '', $tpl);
9344 }
9345 if (!empty($booking['idorderota'])) {
9346 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9347 } else {
9348 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9349 }
9350
9351 $rooms_booked = array();
9352 $rooms_names = array();
9353 $tot_adults = 0;
9354 $tot_children = 0;
9355 $tot_guests = 0;
9356 foreach ($booking_rooms as $broom) {
9357 $rooms_names[] = $broom['room_name'];
9358 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9359 $rooms_booked[$broom['room_name']] += 1;
9360 } else {
9361 $rooms_booked[$broom['room_name']] = 1;
9362 }
9363 $tot_adults += (int)$broom['adults'];
9364 $tot_children += (int)$broom['children'];
9365 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9366 }
9367 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9368 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9369 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9370 $rooms_booked_quant = array();
9371 foreach ($rooms_booked as $rname => $quant) {
9372 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9373 }
9374 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9375 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9376 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9377 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9378 $remaining_bal = $booking['total'] - $booking['totpaid'];
9379 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9380 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9381
9382 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9383 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9384
9385 if (VBOPlatformDetection::isWordPress()) {
9386 /**
9387 * @wponly Rewrite URI for front-end
9388 */
9389 $book_link = str_replace(JUri::root(), '', $book_link);
9390 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9391 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9392 if ($itemid) {
9393 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9394 }
9395 } else {
9396 /**
9397 * @joomlaonly Rewrite URI for front-end
9398 */
9399 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9400 $uri_parts = [
9401 'option' => 'com_vikbooking',
9402 'view' => 'booking',
9403 'sid' => $use_sid,
9404 'ts' => $booking['ts'],
9405 ];
9406 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9407 // inject lang in query string for proper link routing
9408 $uri_parts['lang'] = $booking['lang'];
9409 }
9410 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9411 }
9412
9413 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9414 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9415 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9416
9417 return $tpl;
9418 }
9419
9420 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9421 $dbo = JFactory::getDbo();
9422 if (!class_exists('VboApplication')) {
9423 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9424 }
9425 $vbo_app = new VboApplication;
9426 if (empty($oid)) {
9427 return false;
9428 }
9429 $sms_api = self::getSMSAPIClass();
9430 if (empty($sms_api)) {
9431 return false;
9432 }
9433 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
9434 return false;
9435 }
9436 $sms_api_params = self::getSMSParams();
9437 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
9438 return false;
9439 }
9440 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
9441 return false;
9442 }
9443 $send_sms_to = self::getSendSMSTo();
9444 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
9445 return false;
9446 }
9447 $booking = array();
9448 $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.";";
9449 $dbo->setQuery($q);
9450 $dbo->execute();
9451 if ($dbo->getNumRows() > 0) {
9452 $booking = $dbo->loadAssoc();
9453 }
9454 if (!(count($booking) > 0)) {
9455 return false;
9456 }
9457 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
9458 return false;
9459 }
9460 $booking_rooms = array();
9461 $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'].";";
9462 $dbo->setQuery($q);
9463 $dbo->execute();
9464 if ($dbo->getNumRows() > 0) {
9465 $booking_rooms = $dbo->loadAssocList();
9466 }
9467 $admin_phone = self::getSMSAdminPhone();
9468 $admin_sendermail = self::getSenderMail();
9469 $admin_email = self::getAdminMail();
9470 $f_result = false;
9471 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
9472 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
9473 //SMS for the administrator
9474 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
9475 if (!empty($sms_text)) {
9476 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9477 //administrator phone can contain multiple numbers separated by comma or semicolon
9478 $admin_phones = array();
9479 if (strpos($admin_phone, ',') !== false) {
9480 $all_phones = explode(',', $admin_phone);
9481 foreach ($all_phones as $aph) {
9482 if (!empty($aph)) {
9483 $admin_phones[] = trim($aph);
9484 }
9485 }
9486 } elseif (strpos($admin_phone, ';') !== false) {
9487 $all_phones = explode(';', $admin_phone);
9488 foreach ($all_phones as $aph) {
9489 if (!empty($aph)) {
9490 $admin_phones[] = trim($aph);
9491 }
9492 }
9493 } else {
9494 $admin_phones[] = $admin_phone;
9495 }
9496 foreach ($admin_phones as $admphone) {
9497 $response_obj = $sms_obj->sendMessage($admphone, strip_tags($sms_text));
9498 if ( !$sms_obj->validateResponse($response_obj) ) {
9499 //notify the administrator via email with the error of the SMS sending
9500 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9501 } else {
9502 $f_result = true;
9503 }
9504 }
9505 }
9506 }
9507 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
9508 //SMS for the Customer
9509 $vbo_tn = self::getTranslator();
9510 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
9511 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
9512 if (!empty($sms_text)) {
9513 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9514 $response_obj = $sms_obj->sendMessage($booking['phone'], strip_tags($sms_text));
9515 if ( !$sms_obj->validateResponse($response_obj) ) {
9516 //notify the administrator via email with the error of the SMS sending
9517 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9518 } else {
9519 $f_result = true;
9520 }
9521 }
9522 }
9523 return $f_result;
9524 }
9525
9526 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
9527 {
9528 if (!defined('_VIKBOOKINGEXEC')) {
9529 define('_VIKBOOKINGEXEC', '1');
9530 }
9531
9532 ob_start();
9533 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
9534 $content = ob_get_contents();
9535 ob_end_clean();
9536
9537 $default_params = array(
9538 'show_header' => 0,
9539 'header_data' => array(),
9540 'show_footer' => 0,
9541 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9542 'pdf_unit' => 'PDF_UNIT',
9543 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9544 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9545 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9546 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9547 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9548 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9549 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9550 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9551 'header_font_size' => '10',
9552 'body_font_size' => '10',
9553 'footer_font_size' => '8',
9554 'show_lines_taxrate_col' => 0,
9555 );
9556
9557 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
9558 $default_params = array_merge($default_params, $invoice_params);
9559 }
9560
9561 return array($content, $default_params);
9562 }
9563
9564 /**
9565 * Includes within a buffer the template file for the custom (manual) invoice.
9566 *
9567 * @param array $invoice the record of the custom invoice
9568 * @param array $customer the customer record
9569 *
9570 * @return string the HTML content of the template file
9571 *
9572 * @since 1.11.1
9573 */
9574 public static function loadCustomInvoiceTmpl($invoice, $customer) {
9575 if (!defined('_VIKBOOKINGEXEC')) {
9576 define('_VIKBOOKINGEXEC', '1');
9577 }
9578 ob_start();
9579 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
9580 $content = ob_get_contents();
9581 ob_end_clean();
9582 $default_params = array(
9583 'show_header' => 0,
9584 'header_data' => array(),
9585 'show_footer' => 0,
9586 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9587 'pdf_unit' => 'PDF_UNIT',
9588 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9589 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9590 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9591 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9592 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9593 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9594 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9595 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9596 'header_font_size' => '10',
9597 'body_font_size' => '10',
9598 'footer_font_size' => '8'
9599 );
9600 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
9601 $default_params = array_merge($default_params, $invoice_params);
9602 }
9603 return array($content, $default_params);
9604 }
9605
9606 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
9607 {
9608 // clone the original template file source
9609 $parsed = $invoicetpl;
9610
9611 /**
9612 * Parse all conditional text rules.
9613 *
9614 * @since 1.14 (J) - 1.4.0 (WP)
9615 */
9616 self::getConditionalRulesInstance()
9617 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9618 ->parseTokens($parsed);
9619 //
9620
9621 $dbo = JFactory::getDbo();
9622 if (is_null($vbo_tn)) {
9623 $vbo_tn = self::getTranslator();
9624 }
9625 // availability helper
9626 $av_helper = self::getAvailabilityInstance();
9627
9628 $nowdf = self::getDateFormat();
9629 if ($nowdf == "%d/%m/%Y") {
9630 $df = 'd/m/Y';
9631 } elseif ($nowdf == "%m/%d/%Y") {
9632 $df = 'm/d/Y';
9633 } else {
9634 $df = 'Y/m/d';
9635 }
9636 $datesep = self::getDateSeparator();
9637
9638 /**
9639 * Split stay reservation.
9640 *
9641 * @since 1.16.0 (J) - 1.6.0 (WP)
9642 */
9643 $room_stay_dates = [];
9644 if ($booking['split_stay']) {
9645 if ($booking['status'] == 'confirmed') {
9646 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
9647 } else {
9648 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
9649 }
9650 // immediately count the number of nights of stay for each split room
9651 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
9652 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
9653 // overwrite values for compatibility with non-confirmed bookings
9654 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
9655 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
9656 }
9657 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
9658 // overwrite the whole array
9659 $room_stay_dates[$sps_r_k] = $sps_r_v;
9660 }
9661 }
9662
9663 $companylogo = self::getSiteLogo();
9664 $uselogo = '';
9665 if (!empty($companylogo)) {
9666 /**
9667 * Let's try to prevent TCPDF errors.
9668 *
9669 * @since August 2nd 2019
9670 */
9671 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9672 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9673 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9674 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9675 }
9676 }
9677 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
9678 $parsed = str_replace("{company_info}", $company_info, $parsed);
9679 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
9680 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
9681 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
9682 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
9683
9684 // custom fields replacement
9685 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
9686 if (is_array($cmatches[1]) && $cmatches[1]) {
9687 $cfids = array();
9688 foreach ($cmatches[1] as $cfid) {
9689 $cfids[] = $cfid;
9690 }
9691 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
9692 $dbo->setQuery($q);
9693 $cfields = $dbo->loadAssocList();
9694 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
9695 $cfmap = array();
9696 foreach ($cfields as $cf) {
9697 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
9698 }
9699 $cfmapreplace = array();
9700 $partsreceived = explode("\n", $booking['custdata']);
9701 if (count($partsreceived) > 0) {
9702 foreach ($partsreceived as $pst) {
9703 if (!empty($pst)) {
9704 $tmpdata = explode(":", $pst);
9705 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
9706 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
9707 }
9708 }
9709 }
9710 }
9711 foreach ($cmatches[1] as $cfid) {
9712 if (array_key_exists($cfid, $cfmapreplace)) {
9713 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
9714 } else {
9715 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
9716 }
9717 }
9718 }
9719
9720 // invoice price description - Start
9721 $rooms = array();
9722 $tars = array();
9723 $arrpeople = array();
9724 $is_package = !empty($booking['pkg']) ? true : false;
9725 $tot_adults = 0;
9726 $tot_children = 0;
9727 $tot_guests = 0;
9728 foreach ($booking_rooms as $kor => $or) {
9729 $num = $kor + 1;
9730 $rooms[$num] = $or;
9731
9732 // determine the days to consider for the count of the availability
9733 $room_nights = $booking['days'];
9734 $room_checkin = $booking['checkin'];
9735 $room_checkout = $booking['checkout'];
9736 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9737 $room_nights = $room_stay_dates[$kor]['nights'];
9738 $room_checkin = $room_stay_dates[$kor]['checkin'];
9739 $room_checkout = $room_stay_dates[$kor]['checkout'];
9740 }
9741
9742 $arrpeople[$num]['adults'] = $or['adults'];
9743 $arrpeople[$num]['children'] = $or['children'];
9744 $tot_adults += $or['adults'];
9745 $tot_children += $or['children'];
9746 $tot_guests += ($or['adults'] + $or['children']);
9747 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9748 //package or custom cost set from the back-end
9749 continue;
9750 }
9751 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
9752 $dbo->setQuery($q);
9753 $tar = $dbo->loadAssocList();
9754 if ($tar) {
9755 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
9756 //different usage
9757 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
9758 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
9759 //Occupancy Override
9760 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
9761 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
9762 //
9763 if (is_array($diffusageprice)) {
9764 //set a charge or discount to the price(s) for the different usage of the room
9765 foreach ($tar as $kpr => $vpr) {
9766 $tar[$kpr]['diffusage'] = $or['adults'];
9767 if ($diffusageprice['chdisc'] == 1) {
9768 //charge
9769 if ($diffusageprice['valpcent'] == 1) {
9770 //fixed value
9771 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9772 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9773 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
9774 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9775 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
9776 } else {
9777 //percentage value
9778 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9779 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
9780 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
9781 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9782 $tar[$kpr]['cost'] = $aduseval;
9783 }
9784 } else {
9785 //discount
9786 if ($diffusageprice['valpcent'] == 1) {
9787 //fixed value
9788 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9789 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9790 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
9791 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9792 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
9793 } else {
9794 //percentage value
9795 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9796 $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);
9797 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
9798 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9799 $tar[$kpr]['cost'] = $aduseval;
9800 }
9801 }
9802 }
9803 }
9804 }
9805 //
9806 $tars[$num] = $tar[0];
9807 }
9808 }
9809 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
9810 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
9811 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
9812 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
9813 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
9814 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
9815
9816 $isdue = 0;
9817 $tot_taxes = 0;
9818 $tot_city_taxes = 0;
9819 $tot_fees = 0;
9820 $pricestr = array();
9821 $optstr = array();
9822 // start building the tax summary
9823 VBOTaxonomySummary::start();
9824 foreach ($booking_rooms as $kor => $or) {
9825 $num = $kor + 1;
9826
9827 // determine the days to consider for the count of the availability
9828 $room_nights = $booking['days'];
9829 $room_checkin = $booking['checkin'];
9830 $room_checkout = $booking['checkout'];
9831 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9832 $room_nights = $room_stay_dates[$kor]['nights'];
9833 $room_checkin = $room_stay_dates[$kor]['checkin'];
9834 $room_checkout = $room_stay_dates[$kor]['checkout'];
9835 }
9836
9837 $pricestr[$num] = array();
9838 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9839 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
9840 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
9841 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
9842 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
9843 $pricestr[$num]['tot'] = $calctar;
9844 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
9845 $tot_taxes += ($calctar - $cost_minus_tax);
9846 $isdue += $calctar;
9847 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
9848 $pricestr[$num]['tax_rate'] = $tax_rate;
9849 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
9850 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9851 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
9852 $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'] : "");
9853 $pricestr[$num]['tot'] = $calctar;
9854 $tars[$num]['calctar'] = $calctar;
9855 $isdue += $calctar;
9856 if ($calctar == $display_rate) {
9857 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
9858 $tot_taxes += ($display_rate - $cost_minus_tax);
9859 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
9860 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
9861 $pricestr[$num]['tax_rate'] = $tax_rate;
9862 } else {
9863 $tot_taxes += ($calctar - $display_rate);
9864 $pricestr[$num]['tax'] = ($calctar - $display_rate);
9865 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
9866 $pricestr[$num]['tax_rate'] = $tax_rate;
9867 }
9868 }
9869 $optstr[$num] = array();
9870 $opt_ind = 0;
9871 if (!empty($or['optionals'])) {
9872 $stepo = explode(";", $or['optionals']);
9873 foreach ($stepo as $roptkey => $oo) {
9874 if (!empty($oo)) {
9875 $stept = explode(":", $oo);
9876 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
9877 $dbo->setQuery($q);
9878 $actopt = $dbo->loadAssocList();
9879 if ($actopt) {
9880 if (is_object($vbo_tn)) {
9881 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
9882 }
9883 $optstr[$num][$opt_ind] = array();
9884 $chvar = '';
9885 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
9886 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
9887 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
9888 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
9889 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
9890 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
9891 $agestept = explode('-', $stept[1]);
9892 $stept[1] = $agestept[0];
9893 $chvar = $agestept[1];
9894 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
9895 //percentage value of the adults tariff
9896 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9897 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9898 } else {
9899 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9900 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9901 }
9902 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
9903 //VBO 1.10 - percentage value of room base cost
9904 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9905 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9906 } else {
9907 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
9908 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9909 }
9910 }
9911 $actopt[0]['chageintv'] = $chvar;
9912 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
9913 $actopt[0]['quan'] = $stept[1];
9914 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
9915 } else {
9916 $actopt[0]['quan'] = $stept[1];
9917 // VBO 1.11 - options percentage cost of the room total fee
9918 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9919 $deftar_basecosts = $or['cust_cost'];
9920 } else {
9921 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9922 }
9923 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
9924 //
9925 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
9926 }
9927 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
9928 $realcost = $actopt[0]['maxprice'];
9929 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
9930 $realcost = $actopt[0]['maxprice'] * $stept[1];
9931 }
9932 }
9933 if ($actopt[0]['perperson'] == 1) {
9934 $realcost = $realcost * $or['adults'];
9935 }
9936 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
9937 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
9938 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
9939 $optstr[$num][$opt_ind]['tax'] = 0;
9940 if ($actopt[0]['is_citytax'] == 1) {
9941 $tot_city_taxes += $tmpopr;
9942 } elseif ($actopt[0]['is_fee'] == 1) {
9943 $tot_fees += $tmpopr;
9944 }
9945 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
9946 if ($tmpopr == $realcost) {
9947 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
9948 $tot_taxes += ($realcost - $opt_minus_tax);
9949 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
9950 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
9951 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9952 } else {
9953 $tot_taxes += ($tmpopr - $realcost);
9954 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
9955 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
9956 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9957 }
9958 //
9959 $opt_ind++;
9960 $isdue += $tmpopr;
9961 }
9962 }
9963 }
9964 }
9965
9966 // custom extra costs
9967 if (!empty($or['extracosts'])) {
9968 $cur_extra_costs = json_decode($or['extracosts'], true);
9969 foreach ($cur_extra_costs as $eck => $ecv) {
9970 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9971 $isdue += $ecplustax;
9972 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
9973 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
9974 $optstr[$num][$opt_ind]['tax'] = 0;
9975 if ($ecplustax == $ecv['cost']) {
9976 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9977 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
9978 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
9979 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
9980 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9981 } else {
9982 $tot_taxes += ($ecplustax - $ecv['cost']);
9983 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
9984 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
9985 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9986 }
9987 $opt_ind++;
9988 }
9989 }
9990 }
9991
9992 $usedcoupon = false;
9993 if (strlen($booking['coupon']) > 0) {
9994 $orig_isdue = $isdue;
9995 $expcoupon = explode(";", $booking['coupon']);
9996 $usedcoupon = $expcoupon;
9997 $isdue = $isdue - (float)$expcoupon[1];
9998 if ($isdue != $orig_isdue) {
9999 //lower taxes proportionally
10000 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10001 }
10002 }
10003 if ($booking['refund'] > 0) {
10004 $orig_isdue = $isdue;
10005 $isdue -= $booking['refund'];
10006 if ($isdue != $orig_isdue) {
10007 //lower taxes proportionally
10008 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
10009 }
10010 }
10011 $rows_written = 0;
10012 $inv_rows = '';
10013 foreach ($pricestr as $num => $price_descr) {
10014 // add room split stay information, if any
10015 $split_stay_str = '';
10016 $kor = $num - 1;
10017 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
10018 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
10019 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
10020 }
10021
10022 $inv_rows .= '<tr>'."\n";
10023 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
10024 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($price_descr['tot'] - $price_descr['tax'])).'</td>'."\n";
10025 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tax']).'</td>'."\n";
10026 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tot']).'</td>'."\n";
10027 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10028 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
10029 }
10030 $inv_rows .= '</tr>'."\n";
10031 $rows_written++;
10032 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
10033 foreach ($optstr[$num] as $optk => $optv) {
10034 $inv_rows .= '<tr>'."\n";
10035 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
10036 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($optv['tot'] - $optv['tax'])).'</td>'."\n";
10037 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tax']).'</td>'."\n";
10038 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tot']).'</td>'."\n";
10039 if (!empty($pdfparams['show_lines_taxrate_col'])) {
10040 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
10041 }
10042 $inv_rows .= '</tr>'."\n";
10043 $rows_written++;
10044 }
10045 }
10046 }
10047
10048 // if discount print row
10049 if ($usedcoupon !== false) {
10050 $inv_rows .= '<tr>'."\n";
10051 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10052 $inv_rows .= '</tr>'."\n";
10053 $inv_rows .= '<tr>'."\n";
10054 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
10055 $inv_rows .= '<td></td>'."\n";
10056 $inv_rows .= '<td></td>'."\n";
10057 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($usedcoupon[1]).'</td>'."\n";
10058 $inv_rows .= '</tr>'."\n";
10059 $rows_written += 2;
10060 }
10061 // if refunded amount, print row
10062 if ($booking['refund'] > 0) {
10063 $inv_rows .= '<tr>'."\n";
10064 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
10065 $inv_rows .= '</tr>'."\n";
10066 $inv_rows .= '<tr>'."\n";
10067 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
10068 $inv_rows .= '<td></td>'."\n";
10069 $inv_rows .= '<td></td>'."\n";
10070 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($booking['refund']).'</td>'."\n";
10071 $inv_rows .= '</tr>'."\n";
10072 $rows_written += 2;
10073 }
10074 //
10075 $min_records = 8;
10076 if ($rows_written < $min_records) {
10077 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
10078 $inv_rows .= '<tr>'."\n";
10079 $inv_rows .= '<td></td>'."\n";
10080 $inv_rows .= '<td></td>'."\n";
10081 $inv_rows .= '<td></td>'."\n";
10082 $inv_rows .= '</tr>'."\n";
10083 }
10084 }
10085 //invoice price description - End
10086 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
10087 $parsed = str_replace("{invoice_totalnet}", $booking['currencyname'].' '.self::numberFormat(($isdue - $tot_taxes)), $parsed);
10088 $parsed = str_replace("{invoice_totaltax}", $booking['currencyname'].' '.self::numberFormat($tot_taxes), $parsed);
10089 $parsed = str_replace("{invoice_grandtotal}", $booking['currencyname'].' '.self::numberFormat($isdue), $parsed);
10090 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
10091
10092 // invoice tax summary
10093 $tax_summary = VBOTaxonomySummary::get();
10094 $tax_names = VBOTaxonomySummary::getNames();
10095 $taxsum_rows = "";
10096 foreach ($tax_summary as $tax_rate => $tax_amount) {
10097 if (!$tax_rate && !$tax_amount) {
10098 continue;
10099 }
10100 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
10101 $taxsum_rows .= '<tr>' . "\n";
10102 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
10103 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
10104 $taxsum_rows .= '<td>' . $booking['currencyname'] . ' ' . self::numberFormat($tax_amount) . '</td>' . "\n";
10105 $taxsum_rows .= '</tr>' . "\n";
10106 }
10107 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
10108
10109 return $parsed;
10110 }
10111
10112 /**
10113 * Specific method for parsing the template file for the custom (manual) invoices.
10114 *
10115 * @param string $invoicetpl the plain custom invoice template file before parsing
10116 * @param array $invoice the invoice record
10117 * @param array $customer the customer record
10118 *
10119 * @return string the HTML content of the parsed custom invoice template
10120 *
10121 * @since 1.11.1
10122 */
10123 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
10124 $nowdf = self::getDateFormat(true);
10125 if ($nowdf == "%d/%m/%Y") {
10126 $df = 'd/m/Y';
10127 } elseif ($nowdf == "%m/%d/%Y") {
10128 $df = 'm/d/Y';
10129 } else {
10130 $df = 'Y/m/d';
10131 }
10132 $datesep = self::getDateSeparator(true);
10133 $companylogo = self::getSiteLogo();
10134 $uselogo = '';
10135 if (!empty($companylogo)) {
10136 /**
10137 * Let's try to prevent TCPDF errors by checking if the file exists.
10138 *
10139 * @since August 2nd 2019
10140 */
10141 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10142 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10143 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10144 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10145 }
10146 }
10147 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
10148 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
10149 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
10150 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
10151 // customer information
10152 $custinfo = '';
10153 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
10154 $custinfo .= $customer['email'] . "\n";
10155 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
10156 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
10157 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
10158 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
10159 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
10160 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
10161 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
10162 // invoice notes
10163 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
10164
10165 return $invoicetpl;
10166 }
10167
10168 /**
10169 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
10170 *
10171 * @param array $booking the booking information.
10172 * @param int $invoice_num the invoice number.
10173 * @param string $invoice_suff the invoice suffix.
10174 * @param string $invoice_date the invoice date.
10175 * @param string $company_info optional company information.
10176 * @param bool $translate whether the invoice contents should be translated in the booking language.
10177 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
10178 *
10179 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
10180 *
10181 * @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.
10182 */
10183 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
10184 {
10185 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
10186 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
10187 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
10188
10189 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
10190 return false;
10191 }
10192
10193 /**
10194 * Trigger event to allow third party plugins to override some values for the invoice generation.
10195 *
10196 * @since 1.16.3 (J) - 1.6.3 (WP)
10197 */
10198 $event_args = [
10199 'invoice_num' => $invoice_num,
10200 'invoice_suff' => $invoice_suff,
10201 'invoice_date' => $invoice_date,
10202 'company_info' => $company_info,
10203 'translate' => $translate,
10204 ];
10205 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
10206 if (is_array($event_args) && $event_args) {
10207 // extract the new values to be used
10208 extract($event_args);
10209 }
10210
10211 $dbo = JFactory::getDbo();
10212 $vbo_tn = self::getTranslator();
10213
10214 // inject the currency name value
10215 $currencyname = self::getCurrencyName();
10216 $booking['currencyname'] = $currencyname;
10217
10218 // make sure we've got a customer ID assigned to the reservation record
10219 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
10220 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
10221 if ($booking_customer) {
10222 $booking['idcustomer'] = $booking_customer['id'];
10223 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
10224 $booking['customer_pin'] = $booking_customer['pin'];
10225 $booking['country_name'] = $booking_customer['country_name'] ?? '';
10226 }
10227 }
10228
10229 $nowdf = self::getDateFormat(true);
10230 if ($nowdf == "%d/%m/%Y") {
10231 $df = 'd/m/Y';
10232 } elseif ($nowdf == "%m/%d/%Y") {
10233 $df = 'm/d/Y';
10234 } else {
10235 $df = 'Y/m/d';
10236 }
10237 $datesep = self::getDateSeparator(true);
10238
10239 if (empty($invoice_date)) {
10240 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10241 $used_date = $booking['ts'];
10242 } else {
10243 /**
10244 * We could be re-generating an invoice for a booking that already had an invoice.
10245 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10246 * It is now possible to keep the same invoice date as the previous one, so check
10247 * what value contains $invoice_date to see if it's different from today's date.
10248 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10249 * to also check the length of the string $invoice_date before using that date.
10250 *
10251 * @since 1.10 - August 2018
10252 */
10253 $base_ts = time();
10254 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10255 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10256 }
10257 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10258 $used_date = $base_ts;
10259 }
10260
10261 // load room reservation records
10262 $q = $dbo->getQuery(true)
10263 ->select($dbo->qn('or') . '.*')
10264 ->select($dbo->qn('r.name', 'room_name'))
10265 ->select($dbo->qn('r.fromadult'))
10266 ->select($dbo->qn('r.toadult'))
10267 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10268 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10269 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10270 ->order($dbo->qn('or.id') . ' ASC');
10271
10272 $dbo->setQuery($q);
10273 $booking_rooms = $dbo->loadAssocList();
10274
10275 if (!$booking_rooms) {
10276 return false;
10277 }
10278
10279 // check if an invoice for this reservation exists
10280 $invoice_id = 0;
10281 $dbo->setQuery(
10282 $dbo->getQuery(true)
10283 ->select('*')
10284 ->from($dbo->qn('#__vikbooking_invoices'))
10285 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10286 ->order($dbo->qn('id') . ' DESC')
10287 );
10288 $invoice_data = $dbo->loadAssoc();
10289 if ($invoice_data) {
10290 $invoice_id = $invoice_data['id'];
10291 }
10292
10293 // if requested, prepare data to refresh the PDF invoice
10294 if ($refresh_pdf === true) {
10295 if (!$invoice_id || !$invoice_data) {
10296 // cannot refresh the PDF if an invoice was never created before
10297 return false;
10298 }
10299
10300 // load the same exact previous information to refresh the invoice
10301 $invoice_num = $invoice_data['number'];
10302 $invoice_suff = $invoice_suff;
10303 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10304 $used_date = $invoice_data['for_date'];
10305
10306 // parse the previous (current) invoice number
10307 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10308 if ($matches) {
10309 // overwrite values
10310 $invoice_num = $matches[1] ?: $invoice_num;
10311 $invoice_suff = $matches[2] ?: $invoice_suff;
10312 }
10313 }
10314
10315 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10316 if ($translate === true) {
10317 if (!empty($booking['lang'])) {
10318 $lang = JFactory::getLanguage();
10319 if ($lang->getTag() != $booking['lang']) {
10320 if (VBOPlatformDetection::isWordPress()) {
10321 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10322 } else {
10323 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10324 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10325 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10326 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10327 }
10328 }
10329 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10330 // force the translation to start because contents should be translated
10331 $vbo_tn::$force_tolang = $booking['lang'];
10332 }
10333 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10334 }
10335 }
10336
10337 // the PDF file name
10338 $pdffname = $booking['id'] . '_' . ($booking['sid'] ?: ($booking['idorderota'] ?? '') ?: '') . '.pdf';
10339
10340 /**
10341 * If an analogic invoice is not available, make sure to create the record
10342 * before letting the e-invoicing drivers run to avoid a loop.
10343 *
10344 * @since 1.16.7 (J) - 1.6.7 (WP)
10345 */
10346 $retval = null;
10347 if (!$refresh_pdf && !$invoice_id) {
10348 // prepare record object
10349 $invoice_record = new stdClass;
10350 $invoice_record->number = $invoice_num . $invoice_suff;
10351 $invoice_record->file_name = $pdffname;
10352 $invoice_record->idorder = (int)$booking['id'];
10353 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10354 $invoice_record->created_on = time();
10355 $invoice_record->for_date = (int)$used_date;
10356
10357 // insert record
10358 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10359 return false;
10360 }
10361
10362 $retval = $invoice_record->id ?? false;
10363
10364 if (!$retval) {
10365 return false;
10366 }
10367 }
10368
10369 /**
10370 * The generation of the analogic invoices can trigger the drivers for the
10371 * generation of the e-Invoices if they are set to automatically run.
10372 * However, the e-Invoicing classes may be calling this method, so we need
10373 * to make sure the eInvoicing class is not running before proceeding.
10374 * This method could be called within a loop, and so the second iterations
10375 * may already have loaded the eInvocing class. For this we use a static variable.
10376 *
10377 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10378 * though additional information may be available only after the transmission. On top
10379 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10380 */
10381 static $einvocing_can_run = false;
10382
10383 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10384 // allow second iterations calling this method to run
10385 $einvocing_can_run = true;
10386 }
10387
10388 // always load all drivers, db and plugin based ones
10389 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10390
10391 foreach ($drivers as $driver_obj) {
10392 // set the flag of the external call
10393 $driver_obj->externalCall = __METHOD__;
10394
10395 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10396 $driver_obj->externalData['einvnum'] = $invoice_num;
10397
10398 // let the driver eventually elaborate the booking information before invoicing
10399 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10400
10401 if ($einvocing_can_run === true && !$refresh_pdf) {
10402 // generate the e-Invoice
10403 $driver_err = '';
10404 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10405 $driver_err = $driver_obj->getError();
10406 VikError::raiseWarning('', $driver_err);
10407 }
10408
10409 // Booking History - store log for the e-Invoice result
10410 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10411 }
10412 }
10413
10414 /**
10415 * Start the generation of the analogic (courtesy) PDF invoice.
10416 */
10417
10418 // load dependencies
10419 if (!class_exists('TCPDF')) {
10420 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10421 }
10422 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10423
10424 /**
10425 * Trigger event to allow third party plugins to return a specific font name.
10426 *
10427 * @since 1.16.0 (J) - 1.6.0 (WP)
10428 */
10429 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10430 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10431 $usepdffont = $custom_pdf_font[0];
10432 }
10433
10434 // set array variable to the template file
10435 $booking_info = self::getBookingInfoFromID($booking['id']);
10436
10437 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
10438
10439 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
10440
10441 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10442
10443 if (is_file($pathpdf)) {
10444 @unlink($pathpdf);
10445 }
10446
10447 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10448
10449 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10450 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
10451
10452 // header for each page of the pdf
10453 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
10454 $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]);
10455 }
10456
10457 // change some currencies to their unicode (decimal) value
10458 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10459 if (array_key_exists($booking['currencyname'], $unichr_map)) {
10460 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
10461 }
10462
10463 // header and footer fonts
10464 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10465 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10466
10467 // margins
10468 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10469 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10470 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10471
10472 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10473 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10474 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10475
10476 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10477 $pdf->SetPrintHeader(false);
10478 }
10479 if ($pdfparams['show_footer'] == 0) {
10480 $pdf->SetPrintFooter(false);
10481 }
10482
10483 $pdf->AddPage();
10484 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10485 $pdf->lastPage();
10486 $pdf->Output($pathpdf, 'F');
10487
10488 if (!is_file($pathpdf)) {
10489 return false;
10490 }
10491
10492 if (VBOPlatformDetection::isWordPress()) {
10493 /**
10494 * @wponly - trigger files mirroring
10495 */
10496 VikBookingLoader::import('update.manager');
10497 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10498 }
10499
10500 // Booking History
10501 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
10502
10503 if ($retval) {
10504 // a new invoice ID was inserted above
10505 return $retval;
10506 }
10507
10508 // prepare record object to be updated (if insert needed, it's done at first)
10509 $invoice_record = new stdClass;
10510 $invoice_record->id = (int)$invoice_id;
10511 $invoice_record->number = $invoice_num . $invoice_suff;
10512 $invoice_record->file_name = $pdffname;
10513 $invoice_record->idorder = (int)$booking['id'];
10514 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10515 $invoice_record->created_on = time();
10516 $invoice_record->for_date = (int)$used_date;
10517
10518 // update record
10519 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
10520
10521 // return the PDF invoice ID
10522 return $invoice_id;
10523 }
10524
10525 /**
10526 * Generates an analogic invoice in PDF format for a custom list of services.
10527 * No bookings are assigned to this custom invoice. The method parses the same
10528 * invoice template as for the regular process with real booking IDs.
10529 * The invoice number must be stored before calling this method.
10530 *
10531 * @param int $invoice_id the ID of the custom invoice record
10532 *
10533 * @return bool
10534 *
10535 * @since 1.11.1 (J) - 1.1.1 (WP)
10536 */
10537 public static function generateCustomInvoice($invoice_id)
10538 {
10539 $dbo = JFactory::getDbo();
10540
10541 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
10542 $dbo->setQuery($q);
10543 $invoice = $dbo->loadAssoc();
10544 if (!$invoice) {
10545 return false;
10546 }
10547
10548 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
10549 $rawcont = is_array($rawcont) ? $rawcont : array();
10550 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
10551 $invoice['rawcont'] = $rawcont;
10552 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
10553 if (!$rawcont || !$rows || !$customer) {
10554 // at least one invoice raw is mandatory as well as the customer
10555 return false;
10556 }
10557
10558 /**
10559 * Start the generation of the electronic invoice, if any driver was enabled.
10560 *
10561 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10562 * though additional information would be available only after the transmission.
10563 */
10564 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10565
10566 if ($drivers) {
10567 // make sure the invoice number is just a number
10568 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
10569
10570 // invoke all drivers that should run automatically
10571 foreach ($drivers as $driver_obj) {
10572 // set the flag of the external call
10573 $driver_obj->externalCall = __METHOD__;
10574
10575 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
10576 $driver_obj->externalData['einvnum'] = $invoice['number'];
10577 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
10578 $driver_obj->externalData['einvcustom'] = $invoice;
10579
10580 // prepare data array for the generation of the e-Invoice
10581 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
10582
10583 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
10584 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
10585 if ($preveinv !== false) {
10586 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
10587 }
10588
10589 // generate the e-Invoice
10590 $driver_err = '';
10591 if (!$driver_obj->generateEInvoice($einvdata)) {
10592 $driver_err = $driver_obj->getError();
10593 VikError::raiseWarning('', $driver_err);
10594 }
10595 }
10596 }
10597
10598 /**
10599 * Start the generation of the analogic (courtesy) PDF invoice.
10600 */
10601
10602 // load dependencies
10603 if (!class_exists('TCPDF')) {
10604 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10605 }
10606 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10607
10608 /**
10609 * Trigger event to allow third party plugins to return a specific font name.
10610 *
10611 * @since 1.16.0 (J) - 1.6.0 (WP)
10612 */
10613 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10614 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10615 $usepdffont = $custom_pdf_font[0];
10616 }
10617
10618 // load invoice template file
10619 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
10620
10621 // parse invoice template file
10622 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
10623
10624 $pdffname = $invoice['file_name'];
10625 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10626
10627 if (is_file($pathpdf)) {
10628 @unlink($pathpdf);
10629 }
10630
10631 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10632
10633 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10634 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
10635
10636 // header for each page of the pdf
10637 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
10638 $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]);
10639 }
10640
10641 // change some currencies to their unicode (decimal) value
10642 $currencyname = self::getCurrencyName();
10643 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10644 if (array_key_exists($currencyname, $unichr_map)) {
10645 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
10646 }
10647
10648 // header and footer fonts
10649 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10650 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10651
10652 // margins
10653 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10654 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10655 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10656
10657 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10658 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10659 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10660
10661 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10662 $pdf->SetPrintHeader(false);
10663 }
10664 if ($pdfparams['show_footer'] == 0) {
10665 $pdf->SetPrintFooter(false);
10666 }
10667
10668 $pdf->AddPage();
10669 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10670 $pdf->lastPage();
10671 $pdf->Output($pathpdf, 'F');
10672
10673 if (!is_file($pathpdf)) {
10674 return false;
10675 }
10676
10677 if (VBOPlatformDetection::isWordPress()) {
10678 /**
10679 * @wponly - trigger files mirroring
10680 */
10681 VikBookingLoader::import('update.manager');
10682 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10683 }
10684
10685 // return the result of the generation of the PDF invoice
10686 return true;
10687 }
10688
10689 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
10690 {
10691 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
10692 return false;
10693 }
10694
10695 $dbo = JFactory::getDbo();
10696
10697 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
10698 $dbo->setQuery($q);
10699 $invoice_data = $dbo->loadAssoc();
10700
10701 if (!$invoice_data) {
10702 return false;
10703 }
10704
10705 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
10706 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
10707 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
10708 if (!file_exists($invoice_file_path)) {
10709 return false;
10710 }
10711
10712 $vbo_app = self::getVboApplication();
10713 $admin_sendermail = self::getSenderMail();
10714
10715 $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);
10716
10717 // update record
10718 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
10719 $dbo->setQuery($q);
10720 $dbo->execute();
10721
10722 return true;
10723 }
10724
10725 public static function loadCheckinDocTmpl($booking_info = array(), $booking_rooms = array(), $customer = array()) {
10726 if (!defined('_VIKBOOKINGEXEC')) {
10727 define('_VIKBOOKINGEXEC', '1');
10728 }
10729 ob_start();
10730 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "checkins" . DIRECTORY_SEPARATOR . "checkin_tmpl.php";
10731 $content = ob_get_contents();
10732 ob_end_clean();
10733 $default_params = array(
10734 'show_header' => 0,
10735 'header_data' => array(),
10736 'show_footer' => 0,
10737 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10738 'pdf_unit' => 'PDF_UNIT',
10739 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10740 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10741 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10742 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10743 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10744 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10745 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10746 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10747 'header_font_size' => '10',
10748 'body_font_size' => '10',
10749 'footer_font_size' => '8'
10750 );
10751 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
10752 $default_params = array_merge($default_params, $checkin_params);
10753 }
10754 return array($content, $default_params);
10755 }
10756
10757 public static function parseCheckinDocTemplate($checkintpl, $booking, $booking_rooms, $customer) {
10758 $parsed = $checkintpl;
10759 $dbo = JFactory::getDbo();
10760 $app = JFactory::getApplication();
10761 $nowdf = self::getDateFormat();
10762 if ($nowdf == "%d/%m/%Y") {
10763 $df='d/m/Y';
10764 } elseif ($nowdf == "%m/%d/%Y") {
10765 $df='m/d/Y';
10766 } else {
10767 $df='Y/m/d';
10768 }
10769 $datesep = self::getDateSeparator();
10770 $companylogo = self::getSiteLogo();
10771 $uselogo = '';
10772 if (!empty($companylogo)) {
10773 /**
10774 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
10775 *
10776 * @since March 16th 2021
10777 */
10778 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10779 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10780 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
10781 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10782 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10783 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
10784 }
10785 }
10786 $company_name = self::getFrontTitle();
10787 $company_info = self::getInvoiceCompanyInfo();
10788 $parsed = str_replace("{company_name}", $company_name, $parsed);
10789 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10790 $parsed = str_replace("{company_info}", $company_info, $parsed);
10791 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10792 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10793 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10794 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10795 $tot_guests = 0;
10796 $tot_adults = 0;
10797 $tot_children = 0;
10798 foreach ($booking_rooms as $kor => $or) {
10799 $tot_guests += ($or['adults'] + $or['children']);
10800 $tot_adults += $or['adults'];
10801 $tot_children += $or['children'];
10802 }
10803 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10804 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10805 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10806 if (count($customer) && isset($customer['comments'])) {
10807 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
10808 }
10809 $termsconds = self::getTermsConditions();
10810 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
10811 //custom fields replace
10812 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10813 if (is_array($cmatches[1]) && $cmatches[1]) {
10814 $cfids = array();
10815 foreach ($cmatches[1] as $cfid) {
10816 $cfids[] = $cfid;
10817 }
10818 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10819 $dbo->setQuery($q);
10820 $cfields = $dbo->loadAssocList();
10821 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10822 $cfmap = array();
10823 foreach ($cfields as $cf) {
10824 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10825 }
10826 $cfmapreplace = array();
10827 $partsreceived = explode("\n", $booking['custdata']);
10828 if (count($partsreceived) > 0) {
10829 foreach ($partsreceived as $pst) {
10830 if (!empty($pst)) {
10831 $tmpdata = explode(":", $pst);
10832 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10833 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10834 }
10835 }
10836 }
10837 }
10838 foreach ($cmatches[1] as $cfid) {
10839 if (array_key_exists($cfid, $cfmapreplace)) {
10840 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10841 } else {
10842 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10843 }
10844 }
10845 }
10846 //end custom fields replace
10847
10848 return $parsed;
10849 }
10850
10851 /**
10852 * Returns an array of key-value pairs to be used for building the Guest Details
10853 * in the Check-in process. The keys will be compared to the fields of the table
10854 * #__customers to see if some values already exist. The values can use lang defs
10855 * of both front-end or back-end. To be called as list(fields, attributes).
10856 *
10857 * @param boolean $precheckin true if requested for front-end pre check-in.
10858 * @param string $type optional type of custom pax fields to force.
10859 *
10860 * @return array key-value pairs for showing and collecting guest details.
10861 *
10862 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
10863 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
10864 * @since 1.17.2 (J) - 1.7.2 (WP) added support to pre-checkin pax fields in data collection drivers.
10865 */
10866 public static function getPaxFields($precheckin = false, $type = null)
10867 {
10868 // check the type of pax fields collection data
10869 $collection_type = $type ?: VBOFactory::getConfig()->getString('checkindata', 'basic');
10870
10871 if (!$precheckin) {
10872 // back-end check-in ("registration") key-value pairs
10873
10874 // return the requested pax fields collection list
10875 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
10876
10877 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
10878 // requested driver returned a list of fields
10879 return $custom_pax_fields;
10880 }
10881
10882 // in case the driver is unsupported, fallback to "basic" (default) driver
10883 return VBOCheckinPax::getFields('basic');
10884 }
10885
10886 // default front-end key-value pairs for pre check-in
10887 $precheckin_pax_fields = [
10888 [
10889 'first_name' => JText::translate('VBCCFIRSTNAME'),
10890 'last_name' => JText::translate('VBCCLASTNAME'),
10891 'date_birth' => JText::translate('ORDER_DBIRTH'),
10892 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
10893 'country' => JText::translate('ORDER_STATE'),
10894 'city' => JText::translate('ORDER_CITY'),
10895 'zip' => JText::translate('ORDER_ZIP'),
10896 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
10897 'gender' => JText::translate('VBOCUSTGENDER'),
10898 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
10899 'docnum' => JText::translate('VBOCUSTDOCNUM'),
10900 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
10901 ],
10902 [
10903 'first_name' => 'text',
10904 'last_name' => 'text',
10905 'date_birth' => 'calendar',
10906 'place_birth' => 'text',
10907 'country' => 'country',
10908 'city' => 'text',
10909 'zip' => 'text',
10910 'nationality' => 'country',
10911 'gender' => [
10912 'M' => JText::translate('VBOCUSTGENDERM'),
10913 'F' => JText::translate('VBOCUSTGENDERF'),
10914 ],
10915 'doctype' => 'text',
10916 'docnum' => 'text',
10917 'documents' => 'file',
10918 ],
10919 ];
10920
10921 // access the pre-checkin pax fields collection list from the current collector
10922 $custom_pax_fields = VBOCheckinPax::getPrecheckinFields($collection_type, $precheckin_pax_fields);
10923
10924 if ($custom_pax_fields && ($custom_pax_fields[0] ?? []) && ($custom_pax_fields[1] ?? [])) {
10925 // set the pax fields for pre-checkin from the given data collection driver
10926 $precheckin_pax_fields = $custom_pax_fields;
10927 }
10928
10929 // make a safe copy of the default precheckin fields
10930 $use_precheckin_pax_fields = $precheckin_pax_fields;
10931
10932 /**
10933 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
10934 *
10935 * @since 1.16.3 (J) - 1.6.3 (WP)
10936 */
10937 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
10938
10939 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
10940 // use the filtered pax fields
10941 $use_precheckin_pax_fields = $precheckin_pax_fields;
10942 }
10943
10944 // return the front-end key-value pairs for pre check-in
10945 return $use_precheckin_pax_fields;
10946 }
10947
10948 /**
10949 * Returns the associative list of countries from the DB.
10950 *
10951 * @param bool $tn whether to translate the country name.
10952 * @param bool $no_id whether to unset the ID of the country.
10953 *
10954 * @return array associative or empty array.
10955 *
10956 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
10957 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
10958 */
10959 public static function getCountriesArray($tn = true, $no_id = true)
10960 {
10961 $dbo = JFactory::getDbo();
10962 $all_countries = [];
10963
10964 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
10965 $dbo->setQuery($q);
10966 $countries = $dbo->loadAssocList();
10967 if (!$countries) {
10968 return [];
10969 }
10970
10971 if ($tn === true) {
10972 $vbo_tn = self::getTranslator();
10973 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
10974 // re-apply sorting by country name
10975 $sorting = [];
10976 foreach ($countries as $country) {
10977 $sorting[$country['country_name']] = $country;
10978 }
10979 ksort($sorting);
10980 $sorted = [];
10981 foreach ($sorting as $country) {
10982 $sorted[] = $country;
10983 }
10984 $countries = $sorted;
10985 unset($sorting, $sorted);
10986 }
10987
10988 foreach ($countries as $v) {
10989 if ($no_id) {
10990 // keep the original structure by unsetting the ID only needed for translation
10991 unset($v['id']);
10992 }
10993 $all_countries[$v['country_3_code']] = $v;
10994 }
10995
10996 return $all_countries;
10997 }
10998
10999 public static function getCountriesSelect($name, $all_countries = array(), $current_value = '', $empty_value = ' ')
11000 {
11001 if (!count($all_countries)) {
11002 $all_countries = self::getCountriesArray();
11003 }
11004
11005 $countries = '<select name="'.$name.'">'."\n";
11006 if (strlen($empty_value)) {
11007 $countries .= '<option value="">'.$empty_value.'</option>'."\n";
11008 }
11009 foreach ($all_countries as $v) {
11010 $countries .= '<option value="'.$v['country_3_code'].'"'.($v['country_3_code'] == $current_value ? ' selected="selected"' : '').'>'.$v['country_name'].'</option>'."\n";
11011 }
11012 $countries .= '</select>';
11013
11014 return $countries;
11015 }
11016
11017 public static function getThumbSize($skipsession = false)
11018 {
11019 static $thumb_size = null;
11020
11021 if ($thumb_size) {
11022 return $thumb_size;
11023 }
11024
11025 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
11026
11027 return $thumb_size;
11028 }
11029
11030 /**
11031 * Checks whether an iCal file for the reservation should be
11032 * attached to the confirmation email for customer and/or admin.
11033 *
11034 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
11035 *
11036 * @since 1.12.0 (J) - 1.2.0 (WP)
11037 */
11038 public static function attachIcal()
11039 {
11040 return VBOFactory::getConfig()->getInt('attachical', 1);
11041 }
11042
11043 /**
11044 * How the calculation of the orphan dates should take place.
11045 *
11046 * @return string "next" for only checking the bookings ahead, "prevnext" if
11047 * also the previous bookings should be checked.
11048 *
11049 * @since 1.13 (J) - 1.3.0 (WP)
11050 */
11051 public static function orphansCalculation()
11052 {
11053 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
11054 }
11055
11056 /**
11057 * Returns the name of the template file to use for the front-end View "search".
11058 *
11059 * @return string the name of the template file. New and upgraded user will both use the classic file.
11060 *
11061 * @since 1.13 (J) - 1.3.0 (WP)
11062 */
11063 public static function searchResultsTmpl()
11064 {
11065 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
11066 }
11067
11068 /**
11069 * Returns a string without any new-line characters
11070 * to be used for JavaScript values without facing
11071 * errors like 'unterminated string literal'.
11072 * By passing nl2br($str) as argument, we can keep
11073 * the wanted new-line HTML tags for PRE tags.
11074 * We use implode() with just one argument to
11075 * not use an empty string as glue for the string.
11076 *
11077 * @param $str string
11078 *
11079 * @return string
11080 */
11081 public static function strTrimLiteral($str) {
11082 $str = str_replace(array("\r\n", "\r"), "\n", $str);
11083 $lines = explode("\n", $str);
11084 $new_lines = array();
11085 foreach ($lines as $i => $line) {
11086 if (strlen($line)) {
11087 $new_lines[] = trim($line);
11088 }
11089 }
11090 return implode($new_lines);
11091 }
11092
11093 /**
11094 * Returns an instance of the VboApplication class.
11095 *
11096 * @return VboApplication
11097 */
11098 public static function getVboApplication()
11099 {
11100 // require library
11101 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
11102
11103 // return a new instance
11104 return new VboApplication();
11105 }
11106
11107 /**
11108 * Returns the translation of the given 0-indexed week day.
11109 *
11110 * @param int $wd the 0-indexed day of the week.
11111 * @param bool $short whether to get the short version of the weekday.
11112 *
11113 * @return string the name of the week day.
11114 *
11115 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11116 */
11117 public static function sayWeekDay($wd, $short = false)
11118 {
11119 switch ($wd) {
11120 case '6' :
11121 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
11122 break;
11123 case '5' :
11124 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
11125 break;
11126 case '4' :
11127 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
11128 break;
11129 case '3' :
11130 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
11131 break;
11132 case '2' :
11133 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
11134 break;
11135 case '1' :
11136 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
11137 break;
11138 default :
11139 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
11140 break;
11141 }
11142 return $ret;
11143 }
11144
11145 /**
11146 * Returns the translation of the given 1-indexed month of the year.
11147 *
11148 * @param int $idm the 1-indexed month of the year.
11149 * @param bool $short whether to get the short version of the month.
11150 *
11151 * @return string the name of the month.
11152 *
11153 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
11154 */
11155 public static function sayMonth($idm, $short = false)
11156 {
11157 switch ($idm) {
11158 case '12' :
11159 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
11160 break;
11161 case '11' :
11162 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
11163 break;
11164 case '10' :
11165 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
11166 break;
11167 case '9' :
11168 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
11169 break;
11170 case '8' :
11171 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
11172 break;
11173 case '7' :
11174 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
11175 break;
11176 case '6' :
11177 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
11178 break;
11179 case '5' :
11180 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
11181 break;
11182 case '4' :
11183 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
11184 break;
11185 case '3' :
11186 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
11187 break;
11188 case '2' :
11189 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
11190 break;
11191 default :
11192 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
11193 break;
11194 }
11195 return $ret;
11196 }
11197
11198 public static function sayDayMonth($d) {
11199 switch ($d) {
11200 case '31' :
11201 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
11202 break;
11203 case '30' :
11204 $ret = JText::translate('VBDAYMONTHTHIRTY');
11205 break;
11206 case '29' :
11207 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
11208 break;
11209 case '28' :
11210 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
11211 break;
11212 case '27' :
11213 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
11214 break;
11215 case '26' :
11216 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
11217 break;
11218 case '25' :
11219 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
11220 break;
11221 case '24' :
11222 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
11223 break;
11224 case '23' :
11225 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
11226 break;
11227 case '22' :
11228 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
11229 break;
11230 case '21' :
11231 $ret = JText::translate('VBDAYMONTHTWENTYONE');
11232 break;
11233 case '20' :
11234 $ret = JText::translate('VBDAYMONTHTWENTY');
11235 break;
11236 case '19' :
11237 $ret = JText::translate('VBDAYMONTHNINETEEN');
11238 break;
11239 case '18' :
11240 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11241 break;
11242 case '17' :
11243 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11244 break;
11245 case '16' :
11246 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11247 break;
11248 case '15' :
11249 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11250 break;
11251 case '14' :
11252 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11253 break;
11254 case '13' :
11255 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11256 break;
11257 case '12' :
11258 $ret = JText::translate('VBDAYMONTHTWELVE');
11259 break;
11260 case '11' :
11261 $ret = JText::translate('VBDAYMONTHELEVEN');
11262 break;
11263 case '10' :
11264 $ret = JText::translate('VBDAYMONTHTEN');
11265 break;
11266 case '9' :
11267 $ret = JText::translate('VBDAYMONTHNINE');
11268 break;
11269 case '8' :
11270 $ret = JText::translate('VBDAYMONTHEIGHT');
11271 break;
11272 case '7' :
11273 $ret = JText::translate('VBDAYMONTHSEVEN');
11274 break;
11275 case '6' :
11276 $ret = JText::translate('VBDAYMONTHSIX');
11277 break;
11278 case '5' :
11279 $ret = JText::translate('VBDAYMONTHFIVE');
11280 break;
11281 case '4' :
11282 $ret = JText::translate('VBDAYMONTHFOUR');
11283 break;
11284 case '3' :
11285 $ret = JText::translate('VBDAYMONTHTHREE');
11286 break;
11287 case '2' :
11288 $ret = JText::translate('VBDAYMONTHTWO');
11289 break;
11290 default :
11291 $ret = JText::translate('VBDAYMONTHONE');
11292 break;
11293 }
11294 return $ret;
11295 }
11296
11297 public static function totElements($arr) {
11298 $n = 0;
11299 if (is_array($arr)) {
11300 foreach ($arr as $a) {
11301 if (!empty($a)) {
11302 $n++;
11303 }
11304 }
11305 return $n;
11306 }
11307 return false;
11308 }
11309
11310 /**
11311 * Returns a list of documents that were uploaded
11312 * for the specified customer.
11313 *
11314 * @param integer $id The customer ID.
11315 *
11316 * @return array A list of documents.
11317 *
11318 * @since 1.3.0
11319 */
11320 public static function getCustomerDocuments($id)
11321 {
11322 $dbo = JFactory::getDbo();
11323
11324 $q = $dbo->getQuery(true)
11325 ->select($dbo->qn('docsfolder'))
11326 ->from($dbo->qn('#__vikbooking_customers'))
11327 ->where($dbo->qn('id') . ' = ' . (int) $id);
11328
11329 $dbo->setQuery($q, 0, 1);
11330 $dbo->execute();
11331
11332 if (!$dbo->getNumRows())
11333 {
11334 // customer not found
11335 return array();
11336 }
11337
11338 // retrieve customer documents directory name
11339 $dirname = $dbo->loadResult();
11340
11341 if (empty($dirname))
11342 {
11343 // no available directory
11344 return array();
11345 }
11346
11347 // build documents folder path
11348 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11349
11350 if (!is_dir($dirname))
11351 {
11352 // the customer directory doesn't exist
11353 return array();
11354 }
11355
11356 // read all files from customer directory
11357 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11358
11359 $files = array();
11360
11361 foreach ($glob as $path)
11362 {
11363 // skip "index.html"
11364 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11365 {
11366 // extract name and extension from file path
11367 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11368 {
11369 $name = $match[1];
11370 $ext = $match[2];
11371 }
11372 else
11373 {
11374 $name = basename($path);
11375 $ext = '';
11376 }
11377
11378 $file = new stdClass;
11379 $file->path = $path;
11380 $file->name = $name;
11381 $file->ext = $ext;
11382 $file->basename = $file->name . '.' . $file->ext;
11383 $file->size = filesize($path);
11384 $file->date = filemtime($path);
11385 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11386
11387 $files[] = $file;
11388 }
11389 }
11390
11391 // sort files by creation date
11392 usort($files, function($a, $b)
11393 {
11394 return $b->date - $a->date;
11395 });
11396
11397 return $files;
11398 }
11399
11400 public static function displayPaymentParameters($pfile, $pparams = '') {
11401 $html = '<p>---------</p>';
11402
11403 /**
11404 * @wponly The payment gateway is now loaded
11405 * using the apposite dispatcher.
11406 *
11407 * @since 1.0.5
11408 */
11409 JLoader::import('adapter.payment.dispatcher');
11410
11411 try
11412 {
11413 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
11414 }
11415 catch (Exception $e)
11416 {
11417 // payment not found
11418 $html = $e->getMessage();
11419
11420 if ($code = $e->getCode())
11421 {
11422 $html = '<b>' . $code . '</b> : ' . $html;
11423 }
11424
11425 return $html;
11426 }
11427 //
11428
11429 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
11430 $arrparams = !is_array($arrparams) ? array() : $arrparams;
11431
11432 // get admin parameters
11433 $pconfig = $payment->getAdminParameters();
11434
11435 if (count($pconfig) > 0) {
11436 $html = '';
11437 foreach ($pconfig as $value => $cont) {
11438 if (empty($value)) {
11439 continue;
11440 }
11441 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11442 $label = $labelparts[0];
11443 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11444 if (!empty($cont['help'])) {
11445 $labelhelp = $cont['help'];
11446 }
11447 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11448 $html .= '<div class="vbo-param-container">';
11449 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11450 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11451 }
11452 $html .= '<div class="vbo-param-setting">';
11453 switch ($cont['type']) {
11454 case 'custom':
11455 $html .= $cont['html'];
11456 break;
11457 case 'select':
11458 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11459 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11460 if (isset($cont['multiple']) && $cont['multiple']) {
11461 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
11462 } else {
11463 $html .= '<select name="vikpaymentparams['.$value.']">';
11464 }
11465 foreach ($options as $optkey => $poption) {
11466 $checkval = $is_assoc ? $optkey : $poption;
11467 $selected = false;
11468 if (isset($arrparams[$value])) {
11469 if (is_array($arrparams[$value])) {
11470 $selected = in_array($checkval, $arrparams[$value]);
11471 } else {
11472 $selected = ($checkval == $arrparams[$value]);
11473 }
11474 } elseif (isset($default_paramv)) {
11475 if (is_array($default_paramv)) {
11476 $selected = in_array($checkval, $default_paramv);
11477 } else {
11478 $selected = ($checkval == $default_paramv);
11479 }
11480 }
11481 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11482 }
11483 $html .= '</select>';
11484 break;
11485 case 'password':
11486 $html .= '<div class="btn-wrapper input-append">';
11487 $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"/>';
11488 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11489 $html .= '</div>';
11490 break;
11491 case 'number':
11492 $number_attr = array();
11493 if (isset($cont['min'])) {
11494 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11495 }
11496 if (isset($cont['max'])) {
11497 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11498 }
11499 if (isset($cont['step'])) {
11500 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11501 }
11502 $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) . '/>';
11503 break;
11504 case 'textarea':
11505 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11506 break;
11507 case 'hidden':
11508 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
11509 break;
11510 case 'checkbox':
11511 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11512 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
11513 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
11514 break;
11515 default:
11516 $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"/>';
11517 break;
11518 }
11519 if (strlen($labelhelp) > 0) {
11520 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11521 }
11522 $html .= '</div>';
11523 $html .= '</div>';
11524 }
11525 // JS helper function to toggle the password fields
11526 $html .= "\n" . '<script>' . "\n";
11527 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
11528 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11529 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11530 $html .= ' var inp_type = inp.attr("type");' . "\n";
11531 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11532 $html .= '}' . "\n";
11533 $html .= "\n" . '</script>' . "\n";
11534 }
11535
11536 return $html;
11537 }
11538
11539 public static function displaySMSParameters($pfile, $params = null)
11540 {
11541 $html = '---------';
11542
11543 $params = is_string($params) ? json_decode($params, true) : $params;
11544 $params = is_array($params) ? $params : [];
11545
11546 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
11547 return $html;
11548 }
11549
11550 // attempt to load the class file
11551 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
11552
11553 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
11554 return $html;
11555 }
11556
11557 // load the gateway parameters
11558 $config = VikSmsApi::getAdminParameters();
11559
11560 if (!is_array($config) || !count($config)) {
11561 return $html;
11562 }
11563
11564 // flags for JS helpers
11565 $js_helpers = array();
11566
11567 $html = '';
11568 foreach ($config as $value => $cont) {
11569 if (empty($value)) {
11570 continue;
11571 }
11572 $inp_attr = '';
11573 if (isset($cont['attributes'])) {
11574 foreach ($cont['attributes'] as $inpk => $inpv) {
11575 $inp_attr .= $inpk.'="'.$inpv.'" ';
11576 }
11577 $inp_attr = ' ' . rtrim($inp_attr);
11578 }
11579 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11580 $label = $labelparts[0];
11581 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11582 if (!empty($cont['help'])) {
11583 $labelhelp = $cont['help'];
11584 }
11585 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11586 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
11587 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11588 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11589 }
11590 $html .= '<div class="vbo-param-setting">';
11591 switch ($cont['type']) {
11592 case 'custom':
11593 $html .= $cont['html'];
11594 break;
11595 case 'select':
11596 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11597 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11598 if (isset($cont['multiple']) && $cont['multiple']) {
11599 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11600 } else {
11601 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
11602 }
11603 foreach ($options as $optkey => $poption) {
11604 $checkval = $is_assoc ? $optkey : $poption;
11605 $selected = false;
11606 if (isset($params[$value])) {
11607 if (is_array($params[$value])) {
11608 $selected = in_array($checkval, $params[$value]);
11609 } else {
11610 $selected = ($checkval == $params[$value]);
11611 }
11612 } elseif (isset($default_paramv)) {
11613 if (is_array($default_paramv)) {
11614 $selected = in_array($checkval, $default_paramv);
11615 } else {
11616 $selected = ($checkval == $default_paramv);
11617 }
11618 }
11619 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11620 }
11621 $html .= '</select>';
11622 break;
11623 case 'password':
11624 $html .= '<div class="btn-wrapper input-append">';
11625 $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 . '/>';
11626 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11627 $html .= '</div>';
11628 // set flag for JS helper
11629 $js_helpers[] = $cont['type'];
11630 break;
11631 case 'number':
11632 $number_attr = array();
11633 if (isset($cont['min'])) {
11634 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11635 }
11636 if (isset($cont['max'])) {
11637 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11638 }
11639 if (isset($cont['step'])) {
11640 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11641 }
11642 $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 . '/>';
11643 break;
11644 case 'textarea':
11645 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11646 break;
11647 case 'visual_html':
11648 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11649 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
11650 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
11651 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
11652 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11653 break;
11654 case 'hidden':
11655 $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 . '/>';
11656 break;
11657 case 'checkbox':
11658 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11659 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
11660 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11661 break;
11662 default:
11663 $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 . '/>';
11664 break;
11665 }
11666 if (strlen($labelhelp) > 0) {
11667 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11668 }
11669 $html .= '</div>';
11670 $html .= '</div>';
11671 }
11672
11673 // JS helper functions
11674 if (in_array('password', $js_helpers)) {
11675 // toggle the password fields
11676 $html .= "\n" . '<script>' . "\n";
11677 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
11678 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11679 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11680 $html .= ' var inp_type = inp.attr("type");' . "\n";
11681 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11682 $html .= '}' . "\n";
11683 $html .= "\n" . '</script>' . "\n";
11684 }
11685
11686 return $html;
11687 }
11688
11689 /**
11690 * Renders the params of the requested cron job file-class.
11691 *
11692 * @param string $file the name of the cron job driver file.
11693 * @param array $params the parameters array.
11694 *
11695 * @return string the necessary HTML content to render.
11696 *
11697 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
11698 */
11699 public static function displayCronParameters($file, $params = [])
11700 {
11701 try
11702 {
11703 // attempt to create a new instance
11704 $job = VBOFactory::getCronFactory()->createInstance($file);
11705 }
11706 catch (Exception $e)
11707 {
11708 // something went wrong, display error message
11709 return '<p>' . $e->getMessage() . '<p>';
11710 }
11711
11712 // get admin parameters
11713 $config = $job->getForm();
11714
11715 if (!is_array($config) || !$config) {
11716 return '<p>---------</p>';
11717 }
11718
11719 /**
11720 * Render the cron parameters through the global params rendering class.
11721 *
11722 * @since 1.16.9 (J) - 1.6.9 (WP)
11723 */
11724 return VBOParamsRendering::getInstance($config, $params)->setInputName('vikcronparams')->getHtml();
11725 }
11726
11727 public static function invokeChannelManager($skiporder = true, $order = array()) {
11728 $task = VikRequest::getString('task', '', 'request');
11729 $view = VikRequest::getString('view', '', 'request');
11730 $tmpl = VikRequest::getString('tmpl', '', 'request');
11731 $noimpression = array('vieworder', 'booking');
11732 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')) {
11733 //VCM Channel Impression
11734 if (!class_exists('VikChannelManagerConfig')) {
11735 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11736 }
11737 if (!class_exists('VikChannelManager')) {
11738 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11739 }
11740 VikChannelManager::invokeChannelImpression();
11741 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
11742 //VCM Channel Conversion-Impression
11743 if (!class_exists('VikChannelManagerConfig')) {
11744 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11745 }
11746 if (!class_exists('VikChannelManager')) {
11747 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11748 }
11749 VikChannelManager::invokeChannelConversionImpression($order);
11750 }
11751 }
11752
11753 public static function validEmail($email) {
11754 $isValid = true;
11755 $atIndex = strrpos($email, "@");
11756 if (is_bool($atIndex) && !$atIndex) {
11757 $isValid = false;
11758 } else {
11759 $domain = substr($email, $atIndex +1);
11760 $local = substr($email, 0, $atIndex);
11761 $localLen = strlen($local);
11762 $domainLen = strlen($domain);
11763 if ($localLen < 1 || $localLen > 64) {
11764 // local part length exceeded
11765 $isValid = false;
11766 } else
11767 if ($domainLen < 1 || $domainLen > 255) {
11768 // domain part length exceeded
11769 $isValid = false;
11770 } else
11771 if ($local[0] == '.' || $local[$localLen -1] == '.') {
11772 // local part starts or ends with '.'
11773 $isValid = false;
11774 } else
11775 if (preg_match('/\\.\\./', $local)) {
11776 // local part has two consecutive dots
11777 $isValid = false;
11778 } else
11779 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
11780 // character not valid in domain part
11781 $isValid = false;
11782 } else
11783 if (preg_match('/\\.\\./', $domain)) {
11784 // domain part has two consecutive dots
11785 $isValid = false;
11786 } else
11787 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
11788 // character not valid in local part unless
11789 // local part is quoted
11790 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
11791 $isValid = false;
11792 }
11793 }
11794 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
11795 // domain not found in DNS
11796 $isValid = false;
11797 }
11798 }
11799 return $isValid;
11800 }
11801
11802 public static function caniWrite($path) {
11803 if ($path[strlen($path) - 1] == '/') {
11804 // ricorsivo return a temporary file path
11805 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
11806 }
11807 if (is_dir($path)) {
11808 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
11809 }
11810 // check tmp file for read/write capabilities
11811 $rm = file_exists($path);
11812 $f = @fopen($path, 'a');
11813 if ($f === false) {
11814 return false;
11815 }
11816 fclose($f);
11817 if (!$rm) {
11818 unlink($path);
11819 }
11820 return true;
11821 }
11822
11823 /**
11824 * Alias method of JFile::upload to unify any
11825 * upload function into one.
11826 *
11827 * @param string $src The name of the php (temporary) uploaded file.
11828 * @param string $dest The path (including filename) to move the uploaded file to.
11829 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
11830 *
11831 * @return boolean True on success.
11832 *
11833 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
11834 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
11835 */
11836 public static function uploadFile($src, $dest, $copy_only = false) {
11837 // always attempt to include the File class
11838 jimport('joomla.filesystem.file');
11839
11840 // upload the file
11841 if (!$copy_only) {
11842 $result = JFile::upload($src, $dest);
11843 } else {
11844 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
11845 $result = JFile::copy($src, $dest);
11846 }
11847
11848 /**
11849 * @wponly in order to not lose uploaded files after installing an update,
11850 * we need to move any uploaded file onto a recovery folder.
11851 */
11852 if ($result) {
11853 VikBookingLoader::import('update.manager');
11854 VikBookingUpdateManager::triggerUploadBackup($dest);
11855 }
11856 //
11857
11858 // return upload result
11859 return $result;
11860 }
11861
11862 /**
11863 * Helper method used to upload the given file (retrieved from $_FILES)
11864 * into the specified destination.
11865 *
11866 * @param array $file An associative array with the file details.
11867 * @param string $dest The destination path.
11868 * @param string $filters A string (or a regex) containing the allowed extensions.
11869 *
11870 * @return array The uploading result.
11871 *
11872 * @throws RuntimeException
11873 *
11874 * @since 1.3.0
11875 */
11876 public static function uploadFileFromRequest($file, $dest, $filters = '*')
11877 {
11878 jimport('joomla.filesystem.file');
11879
11880 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
11881
11882 if (empty($file['name']))
11883 {
11884 throw new RuntimeException('Missing file', 400);
11885 }
11886
11887 /**
11888 * Make sure the upload of the file didn't raise any errors.
11889 *
11890 * @link https://www.php.net/manual/en/features.file-upload.errors.php
11891 */
11892 if ((int) $file['error'])
11893 {
11894 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
11895 {
11896 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
11897 }
11898 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
11899 {
11900 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
11901 }
11902 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
11903 {
11904 $error = 'The uploaded file was only partially uploaded.';
11905 }
11906 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
11907 {
11908 $error = 'No file was uploaded.';
11909 }
11910 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
11911 {
11912 $error = 'Missing a temporary folder.';
11913 }
11914 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
11915 {
11916 $error = 'Failed to write file to disk.';
11917 }
11918 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
11919 {
11920 $error = 'A PHP extension stopped the file upload.';
11921 }
11922 else
11923 {
11924 $error = 'Unknown error.';
11925 }
11926
11927 throw new RuntimeException($error, 500);
11928 }
11929
11930 $src = $file['tmp_name'];
11931
11932 // extract file name and extension
11933 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
11934 {
11935 $filename = $match[1];
11936 $fileext = $match[2];
11937 }
11938 else
11939 {
11940 // probably no extension provided
11941 $filename = basename($file['name']);
11942 $fileext = '';
11943 }
11944
11945 $j = '';
11946
11947 if (file_exists($dest . $filename . $fileext))
11948 {
11949 $j = 2;
11950
11951 while (file_exists($dest . $filename . '-' . $j . $fileext))
11952 {
11953 $j++;
11954 }
11955
11956 $j = '-' . $j;
11957 }
11958
11959 $finaldest = $dest . $filename . $j . $fileext;
11960
11961 if ($filters !== '*')
11962 {
11963 $ext = $file['type'];
11964
11965 // check if we have a regex
11966 if (preg_match("/^[#\/]/", $filters) && preg_match("/[#\/][a-z]*$/", $filters))
11967 {
11968 if (!preg_match($filters, $ext))
11969 {
11970 // extension not supported
11971 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11972 }
11973 }
11974 else
11975 {
11976 // get all supported types
11977 $types = array_map('strtolower', array_filter(explode(',', $filters)));
11978
11979 if (!in_array($ext, $types))
11980 {
11981 // extension not supported
11982 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11983 }
11984 }
11985 }
11986
11987 // try to upload the file
11988 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
11989 {
11990 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
11991 }
11992
11993 $file = new stdClass;
11994 $file->name = $filename . $j;
11995 $file->ext = ltrim($fileext, '.');
11996 $file->filename = basename($finaldest);
11997 $file->path = $finaldest;
11998
11999 return $file;
12000 }
12001
12002 /**
12003 * Gets the instance of a specific report class.
12004 *
12005 * @param string $report the name of the report to load.
12006 *
12007 * @return mixed false or report object instance.
12008 *
12009 * @since 1.3.0
12010 */
12011 public static function getReportInstance($report)
12012 {
12013 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
12014
12015 return VikBookingReport::getInstanceOf($report);
12016 }
12017
12018 /**
12019 * Checks whether the guest reviews are enabled and VCM is installed.
12020 *
12021 * @return boolean true if enabled, false otherwise.
12022 *
12023 * @since 1.3.0
12024 */
12025 public static function allowGuestReviews()
12026 {
12027 $dbo = JFactory::getDbo();
12028 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12029 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
12030 $dbo->setQuery($q);
12031 $dbo->execute();
12032 if ($dbo->getNumRows()) {
12033 $s = $dbo->loadResult();
12034 return ((int)$s === 1 && $vcm_installed);
12035 }
12036 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
12037 $dbo->setQuery($q);
12038 $dbo->execute();
12039 return $vcm_installed;
12040 }
12041
12042 /**
12043 * Gets the minimum chars for the guest review message.
12044 *
12045 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
12046 *
12047 * @since 1.3.0
12048 */
12049 public static function guestReviewMinChars()
12050 {
12051 $dbo = JFactory::getDbo();
12052 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
12053 $dbo->setQuery($q);
12054 $dbo->execute();
12055 if ($dbo->getNumRows() > 0) {
12056 return (int)$dbo->loadResult();
12057 }
12058 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
12059 $dbo->setQuery($q);
12060 $dbo->execute();
12061 return 15;
12062 }
12063
12064 /**
12065 * The approval type for new guest reviews.
12066 *
12067 * @return string auto or manual.
12068 *
12069 * @since 1.3.0
12070 */
12071 public static function guestReviewsApproval()
12072 {
12073 $dbo = JFactory::getDbo();
12074 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12075 $dbo->setQuery($q);
12076 $dbo->execute();
12077 if ($dbo->getNumRows() > 0) {
12078 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12079 }
12080 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12081 $dbo->setQuery($q);
12082 $dbo->execute();
12083 return 'auto';
12084 }
12085
12086 /**
12087 * The type of reviews guests should leave.
12088 *
12089 * @return string global or service.
12090 *
12091 * @since 1.3.0
12092 */
12093 public static function guestReviewsType()
12094 {
12095 $dbo = JFactory::getDbo();
12096 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12097 $dbo->setQuery($q);
12098 $dbo->execute();
12099 if ($dbo->getNumRows() > 0) {
12100 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12101 }
12102 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12103 $dbo->setQuery($q);
12104 $dbo->execute();
12105 return 'service';
12106 }
12107
12108 /**
12109 * The services to be reviewed by the guests.
12110 *
12111 * @return array list, empty or not, of the services to be reviewed.
12112 *
12113 * @since 1.3.0
12114 */
12115 public static function guestReviewsServices()
12116 {
12117 $dbo = JFactory::getDbo();
12118 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12119 $dbo->setQuery($q);
12120 $dbo->execute();
12121 if ($dbo->getNumRows()) {
12122 return $dbo->loadAssocList();
12123 }
12124 // insert the default services
12125 $default_services = array(
12126 JText::translate('VBOGREVVALUE'),
12127 JText::translate('VBOGREVLOCATION'),
12128 JText::translate('VBOGREVSTAFF'),
12129 JText::translate('VBOGREVCLEAN'),
12130 JText::translate('VBOGREVCOMFORT'),
12131 JText::translate('VBOGREVFACILITIES'),
12132 );
12133 foreach ($default_services as $def_service) {
12134 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12135 $dbo->setQuery($q);
12136 $dbo->execute();
12137 }
12138 return array();
12139 }
12140
12141 /**
12142 * Checks whether the guest reviews should be downloaded (max 1 per day).
12143 * VCM must be installed in order to download the new reviews.
12144 *
12145 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12146 *
12147 * @since 1.3.0
12148 */
12149 public static function shouldDownloadReviews()
12150 {
12151 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12152 return -1;
12153 }
12154
12155 $today = date('Y-m-d');
12156
12157 $dbo = JFactory::getDbo();
12158 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12159 $dbo->setQuery($q);
12160 $dbo->execute();
12161 if ($dbo->getNumRows()) {
12162 $last_download = $dbo->loadResult();
12163 // update last download day
12164 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12165 $dbo->setQuery($q);
12166 $dbo->execute();
12167 //
12168 return ($last_download != $today ? 1 : 0);
12169 }
12170 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12171 $dbo->setQuery($q);
12172 $dbo->execute();
12173 return 1;
12174 }
12175
12176 /**
12177 * Tells whether a booking can be reviewed.
12178 *
12179 * @param array $booking booking array details.
12180 *
12181 * @return boolean true if a review can be left, false otherwise.
12182 *
12183 * @since 1.3.0
12184 */
12185 public static function canBookingBeReviewed($booking)
12186 {
12187 // reviews must be enabled and supported
12188 if (!self::allowGuestReviews()) {
12189 return false;
12190 }
12191
12192 // booking status must be confirmed
12193 if ($booking['status'] != 'confirmed') {
12194 return false;
12195 }
12196
12197 // review can be left starting from the check-out day
12198 $checkout_info = getdate($booking['checkout']);
12199 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12200 if (time() < $checkout_midn) {
12201 return false;
12202 }
12203
12204 // make sure a review for this booking does not exist
12205 $noreview = true;
12206 $dbo = JFactory::getDbo();
12207 try {
12208 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12209 $dbo->setQuery($q);
12210 $dbo->execute();
12211 $noreview = ($dbo->getNumRows() < 1);
12212 } catch (Exception $e) {
12213 // if the query fails, we do not allow the review to be left
12214 $noreview = false;
12215 }
12216
12217 return $noreview;
12218 }
12219
12220 /**
12221 * Gets the review for a booking, no matter if it's published or not.
12222 * Review will be returned only if reviews are enabled and supported.
12223 * Only reviews left through the website, no OTA reviews as this method
12224 * should be used in the front-end to display the review to the guest.
12225 *
12226 * @param array $booking booking array details.
12227 *
12228 * @return mixed associative array or false.
12229 *
12230 * @since 1.3.0
12231 */
12232 public static function getBookingReview($booking)
12233 {
12234 // reviews must be enabled and supported
12235 if (!self::allowGuestReviews()) {
12236 return false;
12237 }
12238
12239 if (empty($booking['id'])) {
12240 return false;
12241 }
12242
12243 // make sure a review for this booking exists
12244 $dbo = JFactory::getDbo();
12245 $review = array();
12246 try {
12247 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12248 $dbo->setQuery($q);
12249 $dbo->execute();
12250 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12251 } catch (Exception $e) {
12252 // query has failed
12253 $review = array();
12254 }
12255
12256 if (count($review)) {
12257 // make sure the review was left from the website
12258 $review['uniquekey'] = (int)$review['uniquekey'];
12259 }
12260
12261 if (count($review) && !empty($review['uniquekey'])) {
12262 // this is an OTA review
12263 return false;
12264 }
12265
12266 if (count($review) && !empty($review['content'])) {
12267 // decode content
12268 $review['content'] = json_decode($review['content'], true);
12269 }
12270
12271 return count($review) ? $review : false;
12272 }
12273
12274 /**
12275 * Attempts to find a translation for a raw customer data label.
12276 * Bookings downloaded from the OTAs will save a raw string of information
12277 * composed of pairs of label-value separated by new line feeds.
12278 * We try to translate the given label into the current language.
12279 *
12280 * @param string $label the raw string label to translate.
12281 *
12282 * @return string either the original or the translated label.
12283 *
12284 * @since 1.3.5
12285 * @since 1.4.0 back-end support added.
12286 */
12287 public static function tnCustomerRawDataLabel($label)
12288 {
12289 // this is a map of the known labels
12290 $known_lbls = array(
12291 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12292 'LASTNAME' => 'ORDER_LNAME',
12293 'COUNTRY' => 'ORDER_STATE',
12294 'EMAIL' => 'VBMAIL',
12295 'TELEPHONE' => 'VBPHONE',
12296 'PHONE' => 'VBPHONE',
12297 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12298 'MEAL_PLAN' => 'VBOMEALPLAN',
12299 'CITY' => 'VBCITY',
12300 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12301 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12302 );
12303
12304 if (self::isAdmin()) {
12305 // override the map of known translation strings
12306 $known_lbls = array(
12307 'NAME' => 'ORDER_NAME',
12308 'LASTNAME' => 'ORDER_LNAME',
12309 'COUNTRY' => 'ORDER_STATE',
12310 'ADDRESS' => 'ORDER_ADDRESS',
12311 'CITY' => 'ORDER_CITY',
12312 'LOCATION' => 'ORDER_CITY',
12313 'EMAIL' => 'ORDER_EMAIL',
12314 'TELEPHONE' => 'ORDER_PHONE',
12315 'PHONE' => 'ORDER_PHONE',
12316 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12317 );
12318 }
12319
12320 // we get rid of any empty space by keeping the underscores
12321 $converted = str_replace(' ', '', strtoupper($label));
12322
12323 if (isset($known_lbls[$converted])) {
12324 // this language definition has been mapped
12325 return JText::translate($known_lbls[$converted]);
12326 }
12327
12328 // we try to guess the translation string by prepending VBO
12329 $guessed = JText::translate('VBO' . $converted);
12330 if ($guessed != 'VBO' . $converted) {
12331 // the label was translated correctly
12332 return $guessed;
12333 }
12334
12335 // this label could not be translated, so we return the plain string
12336 return $label;
12337 }
12338
12339 /**
12340 * We check whether some bookings are available for import from third party plugins.
12341 *
12342 * @return mixed array with list of plugins supported, false otherwise.
12343 *
12344 * @wponly this method is only useful for WordPress.
12345 *
12346 * @since 1.3.5
12347 */
12348 public static function canImportBookingsFromThirdPartyPlugins()
12349 {
12350 $dbo = JFactory::getDbo();
12351
12352 $plugins = array();
12353
12354 /**
12355 * As requested from hundreds of our clients, for the moment we check only the custom
12356 * post types of type "mphb_booking" to see if some bookings are available for import.
12357 */
12358 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
12359 $dbo->setQuery($q);
12360 $dbo->execute();
12361
12362 if (!$dbo->getNumRows()) {
12363 return false;
12364 }
12365
12366 // push this third party plugin
12367 $plugins['mphb'] = 'MotoPress Hotel Booking';
12368
12369 return count($plugins) ? $plugins : false;
12370 }
12371
12372 /**
12373 * This method returns a list of the known languages sorted by the
12374 * administrator custom preferences. Useful for the phone input fields.
12375 *
12376 * @param boolean $code_assoc whether to get an associative array with the lang name.
12377 *
12378 * @return array the sorted list of preferred countries.
12379 *
12380 * @since 1.3.11
12381 */
12382 public static function preferredCountriesOrdering($code_assoc = false)
12383 {
12384 $preferred_countries = array();
12385
12386 // try to get the preferred countries from db
12387 $dbo = JFactory::getDbo();
12388 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
12389 $dbo->setQuery($q);
12390 $setting = $dbo->loadResult();
12391 if (!$setting) {
12392 // create empty configuration record
12393 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
12394 $dbo->setQuery($q);
12395 $dbo->execute();
12396 } else {
12397 $preferred_countries = json_decode($setting);
12398 }
12399
12400 // get the default known languages
12401 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
12402
12403 if (!is_array($preferred_countries) || !count($preferred_countries)) {
12404 // sort the default known languages by country code alphabetically
12405 ksort($sorted_known_langs);
12406 foreach ($sorted_known_langs as $k => $v) {
12407 $langsep = strpos($k, '_') !== false ? '_' : '-';
12408 $langparts = explode($langsep, $k);
12409 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
12410 }
12411 // update the database record
12412 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
12413 $dbo->setQuery($q);
12414 $dbo->execute();
12415 }
12416
12417 if ($code_assoc) {
12418 // this is useful for displaying the preferred countries codes together with the language name
12419 $map = array();
12420 foreach ($preferred_countries as $ccode) {
12421 // look for the current country code in the keys of the known language tags
12422 $match_found = false;
12423 foreach ($sorted_known_langs as $langtag => $langinfo) {
12424 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
12425 $langparts = explode($langsep, $langtag);
12426 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
12427 // match found
12428 $match_found = true;
12429 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12430 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
12431 // match found
12432 $match_found = true;
12433 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12434 }
12435 }
12436 if (!$match_found) {
12437 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
12438 $map[$ccode] = strtoupper($ccode);
12439 }
12440 }
12441 if (count($map)) {
12442 // set the associatve array to be returned
12443 $preferred_countries = $map;
12444 }
12445 }
12446
12447 return $preferred_countries;
12448 }
12449
12450 /**
12451 * Gets the instance of the admin widgets helper class.
12452 *
12453 * @return VikBookingHelperAdminWidgets
12454 *
12455 * @since 1.4.0
12456 */
12457 public static function getAdminWidgetsInstance()
12458 {
12459 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
12460
12461 return VikBookingHelperAdminWidgets::getInstance();
12462 }
12463
12464 /**
12465 * Gets the instance of the conditional rules helper class.
12466 *
12467 * @param bool $require_only whether to return the object.
12468 *
12469 * @return mixed VikBookingHelperConditionalRules or true.
12470 *
12471 * @since 1.4.0
12472 */
12473 public static function getConditionalRulesInstance($require_only = false)
12474 {
12475 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
12476
12477 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
12478 }
12479
12480 /**
12481 * Gets the instance of the geocoding helper class.
12482 *
12483 * @param bool $require_only whether to return the object.
12484 *
12485 * @return mixed VikBookingHelperGeocoding or true.
12486 *
12487 * @since 1.4.0
12488 */
12489 public static function getGeocodingInstance($require_only = false)
12490 {
12491 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
12492
12493 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
12494 }
12495
12496 /**
12497 * Helper method to cope with the removal of the same method
12498 * in the JApplication class introduced with Joomla 4. Using
12499 * isClient() would break the compatibility with J < 3.7 so
12500 * we can rely on this helper method to avoid Fatal Errors.
12501 *
12502 * @return boolean
12503 *
12504 * @since October 2020
12505 */
12506 public static function isAdmin()
12507 {
12508 $app = JFactory::getApplication();
12509 if (method_exists($app, 'isClient')) {
12510 return $app->isClient('administrator');
12511 }
12512
12513 return $app->isAdmin();
12514 }
12515
12516 /**
12517 * Helper method to cope with the removal of the same method
12518 * in the JApplication class introduced with Joomla 4. Using
12519 * isClient() would break the compatibility with J < 3.7 so
12520 * we can rely on this helper method to avoid Fatal Errors.
12521 *
12522 * @return boolean
12523 *
12524 * @since October 2020
12525 */
12526 public static function isSite()
12527 {
12528 $app = JFactory::getApplication();
12529 if (method_exists($app, 'isClient')) {
12530 return $app->isClient('site');
12531 }
12532
12533 return $app->isSite();
12534 }
12535
12536 /**
12537 * Gets the Google Maps API Key.
12538 *
12539 * @return string
12540 *
12541 * @since 1.14 (J) - 1.4.0 (WP)
12542 */
12543 public static function getGoogleMapsKey()
12544 {
12545 $dbo = JFactory::getDbo();
12546 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gmapskey';";
12547 $dbo->setQuery($q);
12548 $dbo->execute();
12549 if ($dbo->getNumRows()) {
12550 return $dbo->loadResult();
12551 }
12552 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gmapskey', '');";
12553 $dbo->setQuery($q);
12554 $dbo->execute();
12555 return '';
12556 }
12557
12558 /**
12559 * Checks whether the interactive map booking is enabled.
12560 *
12561 * @return bool
12562 *
12563 * @since 1.14 (J) - 1.4.0 (WP)
12564 */
12565 public static function interactiveMapEnabled()
12566 {
12567 $dbo = JFactory::getDbo();
12568 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='interactive_map';";
12569 $dbo->setQuery($q);
12570 $dbo->execute();
12571 if ($dbo->getNumRows()) {
12572 return ((int)$dbo->loadResult() > 0);
12573 }
12574
12575 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('interactive_map', '0');";
12576 $dbo->setQuery($q);
12577 $dbo->execute();
12578
12579 return false;
12580 }
12581
12582 /**
12583 * Gets the preferred colors saved in the configuration, if any.
12584 *
12585 * @return array
12586 *
12587 * @since 1.14 (J) - 1.4.0 (WP)
12588 */
12589 public static function getPreferredColors()
12590 {
12591 $dbo = JFactory::getDbo();
12592 $pref_colors = array(
12593 'textcolor' => '',
12594 'bgcolor' => '',
12595 'fontcolor' => '',
12596 'bgcolorhov' => '',
12597 'fontcolorhov' => '',
12598 );
12599
12600 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='pref_colors';";
12601 $dbo->setQuery($q);
12602 $dbo->execute();
12603 if ($dbo->getNumRows()) {
12604 $colors = json_decode($dbo->loadResult(), true);
12605 if (!is_array($colors) || !isset($colors['textcolor'])) {
12606 return $pref_colors;
12607 }
12608 return $colors;
12609 }
12610 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('pref_colors', '{}');";
12611 $dbo->setQuery($q);
12612 $dbo->execute();
12613 return $pref_colors;
12614 }
12615
12616 /**
12617 * Adds to the document inline styles for the preferred colors, if any.
12618 *
12619 * @return void
12620 *
12621 * @since 1.14 (J) - 1.4.0 (WP)
12622 */
12623 public static function loadPreferredColorStyles()
12624 {
12625 $view = VikRequest::getString('view', '', 'request');
12626 $pref_colors = self::getPreferredColors();
12627
12628 $css_classes = array();
12629
12630 if (!empty($pref_colors['textcolor'])) {
12631 // titles and headings
12632 array_push($css_classes, '.vbo-pref-color-text { color: ' . $pref_colors['textcolor'] . ' !important; }');
12633 // stepbar, oconfirm
12634 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete, ol.vbo-stepbar li.vbo-step-current, ol.vbo-stepbar li.vbo-step-current:before, .vbo-coupon-outer, .vbo-enterpin-block { border-color: ' . $pref_colors['textcolor'] . ' !important; }');
12635 // buttons secondary color
12636 array_push($css_classes, '.vbo-pref-color-btn-secondary { border: 2px solid ' . $pref_colors['textcolor'] . ' !important; color: ' . $pref_colors['textcolor'] . ' !important; background: transparent !important; }');
12637 if (!empty($pref_colors['fontcolor'])) {
12638 array_push($css_classes, '.vbo-pref-color-btn-secondary:hover { color: ' . $pref_colors['fontcolor'] . ' !important; background: ' . $pref_colors['textcolor'] . ' !important; }');
12639 }
12640 // datepicker
12641 array_push($css_classes, '.ui-datepicker .ui-datepicker-today {
12642 color: ' . $pref_colors['textcolor'] . ' !important;
12643 border-color: ' . $pref_colors['textcolor'] . ' !important;
12644 }');
12645 array_push($css_classes, '.ui-datepicker .ui-datepicker-today a {
12646 color: ' . $pref_colors['textcolor'] . ' !important;
12647 }');
12648 // operators tableaux
12649 if ($view == 'tableaux') {
12650 array_push($css_classes, '.vbo-roomdaynote-empty .vbo-roomdaynote-trigger i { color: ' . $pref_colors['textcolor'] . ' !important; }');
12651 }
12652 }
12653
12654 if (!empty($pref_colors['bgcolor']) && !empty($pref_colors['fontcolor'])) {
12655 // elements with backgrounds
12656 array_push($css_classes, '.vbo-pref-color-element { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12657 array_push($css_classes, '.vbo-pref-bordercolor { border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12658 array_push($css_classes, '.vbo-pref-bordertext { color: ' . $pref_colors['bgcolor'] . ' !important; border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12659 // buttons with backgrounds
12660 array_push($css_classes, '.vbo-pref-color-btn { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12661 // stepbar
12662 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete:before { background-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12663 // datepicker
12664 array_push($css_classes, '.ui-datepicker table td:hover {
12665 border-color: ' . $pref_colors['bgcolor'] . ' !important;
12666 }');
12667 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day {
12668 background: ' . $pref_colors['bgcolor'] . ' !important;
12669 color: ' . $pref_colors['fontcolor'] . ' !important;
12670 }');
12671 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day a {
12672 color: ' . $pref_colors['fontcolor'] . ' !important;
12673 }');
12674 // operators tableaux
12675 if ($view == 'tableaux') {
12676 array_push($css_classes, '.vbo-tableaux-roombooks > div:not(.vbo-tableaux-booking-empty), .vbo-tableaux-togglefullscreen { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12677 }
12678 }
12679
12680 if (!empty($pref_colors['bgcolorhov']) && !empty($pref_colors['fontcolorhov'])) {
12681 // buttons with backgrounds during hover state
12682 array_push($css_classes, '.vbo-pref-color-btn:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12683 // operators tableaux
12684 if ($view == 'tableaux') {
12685 array_push($css_classes, '.vbo-tableaux-togglefullscreen:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12686 }
12687 }
12688
12689 if (!count($css_classes)) {
12690 return;
12691 }
12692
12693 // add in-line style declaration
12694 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
12695 }
12696
12697 /**
12698 * Given the full endpoint URL for the AJAX request, it
12699 * returns an appropriate URI for the current platform.
12700 *
12701 * @param mixed $query The query string or a routed URL.
12702 * @param boolean $xhtml Replace & by &amp; for XML compliance.
12703 *
12704 * @return string The AJAX end-point URI.
12705 *
12706 * @since 1.14 (J) - 1.4.0 (WP)
12707 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
12708 */
12709 public static function ajaxUrl($query = '', $xhtml = false)
12710 {
12711 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
12712 }
12713
12714 /**
12715 * Tells whether no tax rates have been defined so far.
12716 *
12717 * @return bool True if no tax rates defined, false otherwise.
12718 *
12719 * @since 1.14 (J) - 1.4.0 (WP)
12720 */
12721 public static function noTaxRates()
12722 {
12723 $dbo = JFactory::getDbo();
12724
12725 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
12726 $dbo->setQuery($q);
12727 $dbo->execute();
12728
12729 return ($dbo->getNumRows() < 1);
12730 }
12731
12732 /**
12733 * Booking Type feature is strictly connected to VCM and OTAs. Updated
12734 * versions of VBO will always support it as long as VCM is installed.
12735 * Needed by VCM to understand whether certain SQL queries can be performed.
12736 *
12737 * @return bool true if VCM exists, false otherwise.
12738 *
12739 * @since 1.14 (J) - 1.4.0 (WP)
12740 */
12741 public static function isBookingTypeSupported()
12742 {
12743 return is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12744 }
12745
12746 /**
12747 * Returns an instance of the Rates Flow helper class of VCM, if available.
12748 *
12749 * @param bool $anew True to request a new object instance.
12750 *
12751 * @return VCMRatesFlow|null
12752 *
12753 * @since 1.15.0 (J) - 1.5.0 (WP)
12754 * @since 1.16.10 (J) - 1.6.10 (WP) added argument $anew.
12755 */
12756 public static function getRatesFlowInstance($anew = false)
12757 {
12758 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
12759 if (!is_file($vcm_fpath)) {
12760 return null;
12761 }
12762
12763 // load dependencies
12764 require_once $vcm_fpath;
12765
12766 // return the instance of the class
12767 return VCMRatesFlow::getInstance($anew);
12768 }
12769
12770 /**
12771 * Returns the current appearance preference.
12772 *
12773 * @return string light, auto or dark.
12774 *
12775 * @since 1.15.0 (J) - 1.5.0 (WP)
12776 */
12777 public static function getAppearancePref()
12778 {
12779 static $preference = null;
12780
12781 if ($preference !== null) {
12782 return $preference;
12783 }
12784
12785 // auto is the default appearance preference
12786 $default_pref = 'auto';
12787
12788 // accepted preferences
12789 $valid_pref = [
12790 'auto',
12791 'light',
12792 'dark',
12793 ];
12794
12795 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
12796 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
12797
12798 return $preference;
12799 }
12800
12801 /**
12802 * According to the appearance preferences, the apposite
12803 * CSS assets are loaded within the document.
12804 *
12805 * @param bool $get_info true to not load any assets.
12806 *
12807 * @return mixed string light, auto, dark, info array or false.
12808 *
12809 * @since 1.15.0 (J) - 1.5.0 (WP)
12810 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
12811 */
12812 public static function loadAppearancePreferenceAssets($get_info = false)
12813 {
12814 // get document object
12815 $document = JFactory::getDocument();
12816
12817 // load current color scheme preference
12818 $current_pref = self::getAppearancePref();
12819
12820 // define caching values
12821 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
12822
12823 // define file attributes
12824 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
12825
12826 // apposite file path and URI for back-end
12827 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
12828 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
12829
12830 // check if this is for the front-end ("auto" file only)
12831 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
12832 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
12833 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
12834 // check if front-end appearance is disabled
12835 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
12836 // do nothing for the front-end by default
12837 return false;
12838 }
12839 }
12840
12841 if (VBOPlatformDetection::isJoomla()) {
12842 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
12843 $css_uri = str_replace('resources/', '', $css_uri);
12844 }
12845
12846 if (!is_file($css_path)) {
12847 // this preference does not require a specific stylesheet
12848 return false;
12849 }
12850
12851 if ($get_info) {
12852 // do not actually load any assets
12853 return [
12854 'id' => $file_attr['id'],
12855 'href' => $css_uri,
12856 ];
12857 }
12858
12859 // load the apposite CSS file
12860 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
12861
12862 return $current_pref;
12863 }
12864
12865 /**
12866 * VBO trigger for VCM subscription status and updates. If called within the admin section
12867 * returns the expiration reminder array to display a modal window, and sets a warning
12868 * message. If called within the front-end, may trigger an email alert.
12869 *
12870 * @return mixed null, array or boolean depending on client position.
12871 *
12872 * @since 1.15.0 (J) - 1.5.0 (WP)
12873 */
12874 public static function getVCMSubscriptionStatus()
12875 {
12876 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
12877 if (!is_file($vcm_lib_path)) {
12878 // do nothing
12879 return null;
12880 }
12881
12882 // determine client position
12883 $is_admin = self::isAdmin();
12884
12885 try {
12886 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
12887 // VCM is outdated
12888 return null;
12889 }
12890
12891 // always attempt to load the admin lang handler of VCM
12892 $lang = JFactory::getLanguage();
12893
12894 // load the VCM admin language file
12895 $vcm_admin_lang_path = '';
12896 if (VBOPlatformDetection::isJoomla()) {
12897 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
12898 /**
12899 * @joomlaonly if not defined VCM Software Version constant, include the defines files.
12900 */
12901 if (!defined('VIKCHANNELMANAGER_SOFTWARE_VERSION')) {
12902 include_once VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'adapter' . DIRECTORY_SEPARATOR . 'defines.php';
12903 }
12904 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
12905 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
12906 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
12907 /**
12908 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
12909 */
12910 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
12911 }
12912 // load translation file
12913 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
12914 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
12915 /**
12916 * @wponly load language admin handler as well for WP.
12917 * We do this only because of WordPress, but in a way also compatible with Joomla as
12918 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
12919 */
12920 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
12921 }
12922
12923 if ($is_admin) {
12924 // back-end section of the site: check status
12925 if (VikChannelManager::loadExpirationDetails() === false) {
12926 // download and update the expiration details
12927 VikChannelManager::downloadExpirationDetails();
12928 }
12929 $expiration_reminder = null;
12930 if (VikChannelManager::isSubscriptionExpired()) {
12931 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
12932 } else {
12933 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
12934 // we also check if an update is available
12935 if (VikChannelManager::checkUpdates()) {
12936 // an update is more important
12937 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
12938 exit;
12939 }
12940 }
12941 // return reminder status
12942 return $expiration_reminder;
12943 }
12944
12945 // front-end section of the site: trigger alert, if any (bool)
12946 return VikChannelManager::checkSubscriptionReminder();
12947
12948 } catch (Exception $e) {
12949 // do nothing
12950 }
12951
12952 return null;
12953 }
12954
12955 /**
12956 * Given a list of website rate plans, returns the sorted list.
12957 * Naming score is the highest sorting method, pricing scoring is also supported.
12958 *
12959 * @param array $rate_plans the list of rate plan arrays to sort.
12960 * @param bool $assoc whether to keep the original array keys.
12961 *
12962 * @return array the sorted list of rate plan arrays.
12963 *
12964 * @since 1.15.0 (J) - 1.5.0 (WP)
12965 * @since 1.16.10 (J) - 1.6.10 (WP) added support for derived rate plans.
12966 */
12967 public static function sortRatePlans($rate_plans, $assoc = false)
12968 {
12969 if (!is_array($rate_plans) || count($rate_plans) < 2) {
12970 return $rate_plans;
12971 }
12972
12973 foreach ($rate_plans as $rp_ind => $rate_plan) {
12974 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
12975 // do not proceed in case of a weird array structure
12976 return $rate_plans;
12977 }
12978
12979 // count the sorting-score of this rate plan
12980 $sort_score = 0;
12981 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
12982 // "standard rate" rate plans should go first
12983 $sort_score += 3;
12984 } else {
12985 $sort_score--;
12986 }
12987
12988 // check for derived rate plan
12989 if (!empty($rate_plan['derived_id']) && !empty($rate_plan['derived_data'])) {
12990 // derived rates should go last
12991 $sort_score -= 2;
12992 } else {
12993 // parent rates should go first
12994 $sort_score += 2;
12995 }
12996
12997 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
12998 // this doesn't seem to be a "non refundable rate" so it should go first
12999 $sort_score += 2;
13000 }
13001
13002 // set sorting score for this rate plan ID
13003 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
13004 }
13005
13006 // apply custom sorting and keep index association
13007 uasort($rate_plans, function($a, $b) {
13008 // sort by name-scoring DESC
13009 $sort_diff = $b['sort_score'] - $a['sort_score'];
13010 if (isset($b['cost'])) {
13011 // apply also sorting by price ASC
13012 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
13013 }
13014 return $sort_diff;
13015 });
13016
13017 return !$assoc ? array_values($rate_plans) : $rate_plans;
13018 }
13019
13020 /**
13021 * Returns an instance of the Availability helper class.
13022 *
13023 * @param bool $anew True for forcing a new instance.
13024 *
13025 * @return VikBookingAvailability
13026 *
13027 * @since 1.15.0 (J) - 1.5.0 (WP)
13028 * @since 1.16.10 (J) - 1.6.10 (WP) added $anew argument.
13029 */
13030 public static function getAvailabilityInstance($anew = false)
13031 {
13032 // require library
13033 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
13034
13035 // return the instance of the class
13036 return VikBookingAvailability::getInstance($anew);
13037 }
13038
13039 /**
13040 * Helper method to quickly guess the internal dependency to import.
13041 *
13042 * @param string $path_id either a full path to a file or its identifier.
13043 *
13044 * @return bool true if the requested library was imported or false.
13045 *
13046 * @since 1.15.0 (J) - 1.5.0 (WP)
13047 */
13048 public static function import($path_id)
13049 {
13050 if (empty($path_id)) {
13051 return false;
13052 }
13053
13054 // always append .php extension
13055 $path_id = basename($path_id, '.php') . '.php';
13056
13057 if (is_file($path_id)) {
13058 // full path given
13059 require_once $path_id;
13060
13061 return true;
13062 }
13063
13064 // try guessing where the file id is located
13065 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13066 // library was found
13067 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13068
13069 return true;
13070 }
13071
13072 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
13073 // library was found
13074 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
13075
13076 return true;
13077 }
13078
13079 return false;
13080 }
13081 }
13082
13083 if (!class_exists('VikResizer'))
13084 {
13085 class VikResizer
13086 {
13087 public function __construct()
13088 {
13089 // objects of this class can also be instantiated without calling the methods statically.
13090 }
13091
13092 /**
13093 * Resizes an image proportionally. For PNG files it can optionally
13094 * trim the image to exclude the transparency, and add some padding to it.
13095 * All PNG files keep the alpha background in the resized version.
13096 *
13097 * @param string $fileimg path to original image file
13098 * @param string $dest path to destination image file
13099 * @param int $towidth
13100 * @param int $toheight
13101 * @param bool $trim_png remove empty background from image
13102 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13103 *
13104 * @return boolean
13105 */
13106 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13107 {
13108 if (!is_file($fileimg)) {
13109 return false;
13110 }
13111 if (empty($towidth) && empty($toheight)) {
13112 return copy($fileimg, $dest);
13113 }
13114
13115 $result = true;
13116
13117 list ($owid, $ohei, $type) = getimagesize($fileimg);
13118
13119 if ($owid > $towidth || $ohei > $toheight) {
13120 $xscale = $owid / $towidth;
13121 $yscale = $ohei / $toheight;
13122 if ($yscale > $xscale) {
13123 $new_width = round($owid * (1 / $yscale));
13124 $new_height = round($ohei * (1 / $yscale));
13125 } else {
13126 $new_width = round($owid * (1 / $xscale));
13127 $new_height = round($ohei * (1 / $xscale));
13128 }
13129
13130 $imageresized = imagecreatetruecolor($new_width, $new_height);
13131
13132 switch ($type) {
13133 case '1' :
13134 $imagetmp = imagecreatefromgif ($fileimg);
13135 break;
13136 case '2' :
13137 $imagetmp = imagecreatefromjpeg($fileimg);
13138 break;
13139 case '18' :
13140 $imagetmp = imagecreatefromwebp($fileimg);
13141 break;
13142 default :
13143 //keep alpha for PNG files
13144 $background = imagecolorallocate($imageresized, 0, 0, 0);
13145 imagecolortransparent($imageresized, $background);
13146 imagealphablending($imageresized, false);
13147 imagesavealpha($imageresized, true);
13148 //
13149 $imagetmp = imagecreatefrompng($fileimg);
13150 break;
13151 }
13152
13153 if (!$imagetmp) {
13154 return false;
13155 }
13156
13157 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13158
13159 switch ($type) {
13160 case '1' :
13161 imagegif($imageresized, $dest);
13162 break;
13163 case '2' :
13164 imagejpeg($imageresized, $dest);
13165 break;
13166 case '18' :
13167 imagewebp($imageresized, $dest);
13168 break;
13169 default :
13170 if ($trim_png) {
13171 self::imageTrim($imageresized, $background, $trim_pad);
13172 }
13173 imagepng($imageresized, $dest);
13174 break;
13175 }
13176
13177 imagedestroy($imageresized);
13178 } else {
13179 $result = copy($fileimg, $dest);
13180 }
13181
13182 if (VBOPlatformDetection::isWordPress()) {
13183 /**
13184 * @wponly in order to not lose resized files after installing an update,
13185 * we need to move any uploaded file onto a recovery folder.
13186 */
13187 VikBookingLoader::import('update.manager');
13188 VikBookingUpdateManager::triggerUploadBackup($dest);
13189 }
13190
13191 return $result;
13192 }
13193
13194 /**
13195 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13196 * trim the image to exclude the transparency, and add some padding to it.
13197 * All PNG files keep the alpha background in the resized version.
13198 *
13199 * @param resource $im Image link resource (reference)
13200 * @param int $bg imagecolorallocate color identifier
13201 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13202 *
13203 * @return void
13204 */
13205 public static function imagetrim(&$im, $bg, $pad = null)
13206 {
13207 // Calculate padding for each side.
13208 if (isset($pad)) {
13209 $pp = explode(' ', $pad);
13210 if (isset($pp[3])) {
13211 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13212 } elseif (isset($pp[2])) {
13213 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13214 } elseif (isset($pp[1])) {
13215 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13216 } else {
13217 $p = array_fill(0, 4, (int) $pp[0]);
13218 }
13219 } else {
13220 $p = array_fill(0, 4, 0);
13221 }
13222
13223 // Get the image width and height.
13224 $imw = imagesx($im);
13225 $imh = imagesy($im);
13226
13227 // Set the X variables.
13228 $xmin = $imw;
13229 $xmax = 0;
13230
13231 // Start scanning for the edges.
13232 for ($iy=0; $iy<$imh; $iy++) {
13233 $first = true;
13234 for ($ix=0; $ix<$imw; $ix++) {
13235 $ndx = imagecolorat($im, $ix, $iy);
13236 if ($ndx != $bg) {
13237 if ($xmin > $ix) {
13238 $xmin = $ix;
13239 }
13240 if ($xmax < $ix) {
13241 $xmax = $ix;
13242 }
13243 if (!isset($ymin)) {
13244 $ymin = $iy;
13245 }
13246 $ymax = $iy;
13247 if ($first) {
13248 $ix = $xmax;
13249 $first = false;
13250 }
13251 }
13252 }
13253 }
13254
13255 // The new width and height of the image. (not including padding)
13256 $imw = 1+$xmax-$xmin; // Image width in pixels
13257 $imh = 1+$ymax-$ymin; // Image height in pixels
13258
13259 // Make another image to place the trimmed version in.
13260 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13261
13262 // Make the background of the new image the same as the background of the old one.
13263 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13264 imagefill($im2, 0, 0, $bg2);
13265
13266 // Copy it over to the new image.
13267 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13268
13269 // To finish up, we replace the old image which is referenced.
13270 $im = $im2;
13271 }
13272
13273 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13274 {
13275 if (!file_exists($fileimg)) {
13276 return false;
13277 }
13278 if (empty($towidth) && empty($toheight)) {
13279 copy($fileimg, $dest);
13280 return true;
13281 }
13282
13283 $exp = explode(",", $rgb);
13284 if (count($exp) == 3) {
13285 $r = trim($exp[0]);
13286 $g = trim($exp[1]);
13287 $b = trim($exp[2]);
13288 } else {
13289 $r = 0;
13290 $g = 0;
13291 $b = 0;
13292 }
13293
13294 list ($owid, $ohei, $type) = getimagesize($fileimg);
13295
13296 if ($owid > $towidth || $ohei > $toheight) {
13297 $xscale = $owid / $towidth;
13298 $yscale = $ohei / $toheight;
13299 if ($yscale > $xscale) {
13300 $new_width = round($owid * (1 / $yscale));
13301 $new_height = round($ohei * (1 / $yscale));
13302 $ydest = 0;
13303 $diff = $towidth - $new_width;
13304 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13305 } else {
13306 $new_width = round($owid * (1 / $xscale));
13307 $new_height = round($ohei * (1 / $xscale));
13308 $xdest = 0;
13309 $diff = $toheight - $new_height;
13310 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13311 }
13312
13313 $imageresized = imagecreatetruecolor($towidth, $toheight);
13314
13315 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13316 imagefill($imageresized, 0, 0, $bgColor);
13317
13318 switch ($type) {
13319 case '1' :
13320 $imagetmp = imagecreatefromgif ($fileimg);
13321 break;
13322 case '2' :
13323 $imagetmp = imagecreatefromjpeg($fileimg);
13324 break;
13325 default :
13326 $imagetmp = imagecreatefrompng($fileimg);
13327 break;
13328 }
13329
13330 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13331
13332 switch ($type) {
13333 case '1' :
13334 imagegif ($imageresized, $dest);
13335 break;
13336 case '2' :
13337 imagejpeg($imageresized, $dest);
13338 break;
13339 default :
13340 imagepng($imageresized, $dest);
13341 break;
13342 }
13343
13344 imagedestroy($imageresized);
13345
13346 return true;
13347 } else {
13348 copy($fileimg, $dest);
13349 }
13350 return true;
13351 }
13352
13353 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13354 {
13355 if (!file_exists($fileimg)) {
13356 return false;
13357 }
13358 if (empty($towidth) && empty($toheight)) {
13359 copy($fileimg, $dest);
13360 return true;
13361 }
13362
13363 list ($owid, $ohei, $type) = getimagesize($fileimg);
13364
13365 if ($owid <= $ohei) {
13366 $new_width = $towidth;
13367 $new_height = ($towidth / $owid) * $ohei;
13368 } else {
13369 $new_height = $toheight;
13370 $new_width = ($new_height / $ohei) * $owid;
13371 }
13372
13373 switch ($type) {
13374 case '1' :
13375 $img_src = imagecreatefromgif ($fileimg);
13376 $img_dest = imagecreate($new_width, $new_height);
13377 break;
13378 case '2' :
13379 $img_src = imagecreatefromjpeg($fileimg);
13380 $img_dest = imagecreatetruecolor($new_width, $new_height);
13381 break;
13382 default :
13383 $img_src = imagecreatefrompng($fileimg);
13384 $img_dest = imagecreatetruecolor($new_width, $new_height);
13385 break;
13386 }
13387
13388 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13389
13390 switch ($type) {
13391 case '1' :
13392 $cropped = imagecreate($towidth, $toheight);
13393 break;
13394 case '2' :
13395 $cropped = imagecreatetruecolor($towidth, $toheight);
13396 break;
13397 default :
13398 $cropped = imagecreatetruecolor($towidth, $toheight);
13399 break;
13400 }
13401
13402 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
13403
13404 switch ($type) {
13405 case '1' :
13406 imagegif ($cropped, $dest);
13407 break;
13408 case '2' :
13409 imagejpeg($cropped, $dest);
13410 break;
13411 default :
13412 imagepng($cropped, $dest);
13413 break;
13414 }
13415
13416 imagedestroy($img_dest);
13417 imagedestroy($cropped);
13418
13419 return true;
13420 }
13421 }
13422 }
13423